feat(dds): agent-first Party Master, account actions menu, FD-style statuses
- Party Master opens on agents (paged, from the server summary), then one agent's dashboard: accounts, maturity, money and collection cards, each with a View that filters the accounts table. The page title follows the view, and Back from an account returns to its agent. - DDS account page: an actions menu with Edit, Premature Closure, and Maturity Payout & Close. The manual Close Account buttons are gone, since payout and premature closure close the account themselves. - Statuses Active / Matured / Closed / Premature Closed are shown and filtered on every DDS screen through a shared dds-status helper. - Premature closure dialog redesigned: term progress, numbered steps, and a payout calculation for each choice using the server's formulas. - Agent & Slab Approval tab: slab approvals moved in beside agents, with toggle buttons and labels named for the action; richer slab review. - Batch processing: calculation runs are reported as applied, and an empty run is reported as nothing to do. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+189
-2
@@ -19,7 +19,7 @@
|
||||
<span>Loading entry…</span>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="dri-body" [class.ddr-body]="isProductReview || isAgentReview || isPartyReview">
|
||||
<div class="dri-body" [class.ddr-body]="isProductReview || isAgentReview || isPartyReview || isSlabReview">
|
||||
@if (isProductReview) {
|
||||
<!--
|
||||
A DDS product request reads as the product it creates or changes: who and what at the top,
|
||||
@@ -205,6 +205,190 @@
|
||||
}}</span>
|
||||
}
|
||||
</ng-template>
|
||||
} @else if (isSlabReview) {
|
||||
<!--
|
||||
A commission slab request reads as the slab: the volume band and the rate it pays, what an
|
||||
agent earns at the band's start, the change for an edit as old → new, and the whole chart of
|
||||
that type with this slab in place - so the checker sees it still gives one rate per volume.
|
||||
-->
|
||||
<section class="ddr-hero">
|
||||
<div class="ddr-hero__icon">
|
||||
<mat-icon>{{ isEdit ? 'edit_note' : 'stacked_bar_chart' }}</mat-icon>
|
||||
</div>
|
||||
<div class="ddr-hero__main">
|
||||
<div class="ddr-hero__eyebrow">
|
||||
<span class="ddr-chip" [class.ddr-chip--edit]="isEdit">{{ isEdit ? 'Edit request' : 'New slab' }}</span>
|
||||
<span class="dri-status" [ngClass]="statusClass(entry.status)">{{ entry.status }}</span>
|
||||
</div>
|
||||
<h2 class="ddr-hero__title">{{ valueOf('slabFrom') || '—' }} – {{ valueOf('slabTo') || '—' }}</h2>
|
||||
<div class="ddr-hero__code">
|
||||
<span>{{ valueOf('slabType') || 'Commission' }} slab</span>
|
||||
@if (entry.resourceId) {
|
||||
<span class="ddr-dot"></span>
|
||||
<span class="ddr-mono">#{{ entry.resourceId }}</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="ddr-hero__meta">
|
||||
<span class="ddr-meta">
|
||||
<mat-icon>person</mat-icon>
|
||||
<span>
|
||||
<b>{{ entry.makerName || entry.maker }}</b>
|
||||
<small>{{ entry.maker }}</small>
|
||||
</span>
|
||||
</span>
|
||||
<span class="ddr-meta">
|
||||
<mat-icon>schedule</mat-icon>
|
||||
<span>
|
||||
<b>{{ formatDateTime(entry.madeOn) }}</b>
|
||||
<small>Submitted</small>
|
||||
</span>
|
||||
</span>
|
||||
@if (entry.status !== 'PENDING' && entry.checker) {
|
||||
<span class="ddr-meta">
|
||||
<mat-icon>verified_user</mat-icon>
|
||||
<span>
|
||||
<b>{{ entry.checkerName || entry.checker }}</b>
|
||||
<small>{{ entry.status === 'APPROVED' ? 'Approved by' : 'Rejected by' }}</small>
|
||||
</span>
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="ddr-stats ddr-stats--three">
|
||||
<div class="ddr-stat ddr-stat--accent">
|
||||
<span class="ddr-stat__label"><mat-icon>percent</mat-icon>Commission</span>
|
||||
<span class="ddr-stat__value">{{ valueOf('commissionPercent') || '—' }}</span>
|
||||
<span class="ddr-stat__hint">of the month's collection</span>
|
||||
</div>
|
||||
<div class="ddr-stat">
|
||||
<span class="ddr-stat__label"><mat-icon>straighten</mat-icon>Collection band</span>
|
||||
<span class="ddr-stat__value ddr-stat__value--small"
|
||||
>{{ valueOf('slabFrom') || '—' }} – {{ valueOf('slabTo') || '—' }}</span
|
||||
>
|
||||
<span class="ddr-stat__hint">agent's collection volume</span>
|
||||
</div>
|
||||
<div class="ddr-stat">
|
||||
<span class="ddr-stat__label"><mat-icon>calculate</mat-icon>Example</span>
|
||||
<span class="ddr-stat__value ddr-stat__value--small">{{ slabExample || '—' }}</span>
|
||||
<span class="ddr-stat__hint">commission at the band's start</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@if (compareFields) {
|
||||
<div class="ddr-banner" [class.ddr-banner--none]="!changedCount">
|
||||
<mat-icon>{{ changedCount ? 'compare_arrows' : 'info' }}</mat-icon>
|
||||
@if (changedCount) {
|
||||
<span>
|
||||
<b>{{ changedCount }} term{{ changedCount !== 1 ? 's' : '' }}</b> will change on approval - shown below
|
||||
as current → proposed.
|
||||
</span>
|
||||
} @else {
|
||||
<span>This edit changes nothing - every term matches the slab as it stands.</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@for (group of fieldGroups; track group.name) {
|
||||
<section class="ddr-section">
|
||||
<header class="ddr-section__head">
|
||||
<span class="ddr-section__icon"
|
||||
><mat-icon>{{ group.icon }}</mat-icon></span
|
||||
>
|
||||
<span class="ddr-section__title">{{ group.name }}</span>
|
||||
@if (group.changed) {
|
||||
<span class="ddr-chip ddr-chip--edit">{{ group.changed }} changed</span>
|
||||
}
|
||||
</header>
|
||||
<div class="ddr-grid">
|
||||
@for (field of group.fields; track field.key) {
|
||||
<div class="ddr-tile" [class.ddr-tile--changed]="field.changed">
|
||||
<span class="ddr-tile__label">
|
||||
{{ field.label }}
|
||||
@if (field.changed) {
|
||||
<span class="ddr-tile__flag">Changed</span>
|
||||
}
|
||||
</span>
|
||||
@if (field.changed) {
|
||||
<span class="ddr-tile__diff">
|
||||
<span class="ddr-tile__old">{{ field.current || '—' }}</span>
|
||||
<mat-icon>arrow_forward</mat-icon>
|
||||
<span class="ddr-tile__value">{{ field.value || '—' }}</span>
|
||||
</span>
|
||||
} @else {
|
||||
<span class="ddr-tile__value">{{ field.value || '—' }}</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
|
||||
@if (slabChart.length) {
|
||||
<section class="ddr-section">
|
||||
<header class="ddr-section__head">
|
||||
<span class="ddr-section__icon"><mat-icon>table_rows</mat-icon></span>
|
||||
<span class="ddr-section__title">{{ valueOf('slabType') || 'Commission' }} slab chart</span>
|
||||
<span class="ddr-slabs__note">{{
|
||||
entry.status === 'PENDING' ? 'as it will read once approved' : 'as it reads now'
|
||||
}}</span>
|
||||
</header>
|
||||
@if (slabOverlaps) {
|
||||
<div class="ddr-banner ddr-banner--danger">
|
||||
<mat-icon>warning</mat-icon>
|
||||
<span>
|
||||
This slab's band overlaps
|
||||
<b>{{ slabOverlaps }} other slab{{ slabOverlaps !== 1 ? 's' : '' }}</b> - a collection in the overlap
|
||||
would match more than one rate.
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
<div class="ddr-slabs">
|
||||
<div class="ddr-slabs__row ddr-slabs__row--head">
|
||||
<span>#</span>
|
||||
<span>Collection from</span>
|
||||
<span>Collection to</span>
|
||||
<span class="ddr-slabs__num">Commission</span>
|
||||
</div>
|
||||
@for (slab of slabChart; track $index) {
|
||||
<div
|
||||
class="ddr-slabs__row"
|
||||
[class.ddr-slabs__row--requested]="slab.requested"
|
||||
[class.ddr-slabs__row--overlap]="slab.overlaps"
|
||||
>
|
||||
<span class="ddr-mono">
|
||||
{{ slab.id ? slab.id : 'New' }}
|
||||
@if (slab.requested) {
|
||||
<span class="ddr-tile__flag">{{ isEdit ? 'Edited' : 'New' }}</span>
|
||||
}
|
||||
@if (slab.overlaps) {
|
||||
<span class="ddr-slabs__flag">Overlaps</span>
|
||||
}
|
||||
</span>
|
||||
<span>
|
||||
@if (slab.was && !same(slab.was.from, slab.from)) {
|
||||
<span class="ddr-tile__old">{{ rupees(slab.was.from) }}</span>
|
||||
}
|
||||
{{ rupees(slab.from) }}
|
||||
</span>
|
||||
<span>
|
||||
@if (slab.was && !same(slab.was.to, slab.to)) {
|
||||
<span class="ddr-tile__old">{{ rupees(slab.was.to) }}</span>
|
||||
}
|
||||
{{ rupees(slab.to) }}
|
||||
</span>
|
||||
<span class="ddr-slabs__num">
|
||||
@if (slab.was && !same(slab.was.rate, slab.rate)) {
|
||||
<span class="ddr-tile__old">{{ percent(slab.was.rate) }}</span>
|
||||
}
|
||||
<b>{{ percent(slab.rate) }}</b>
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
} @else if (isAgentReview) {
|
||||
<!--
|
||||
An agent request reads as the agent it opens or changes: who and when at the top, then the
|
||||
@@ -700,7 +884,10 @@
|
||||
}
|
||||
|
||||
@if (entry.status === 'PENDING' && !done) {
|
||||
<div class="dri-actions" [class.ddr-actions]="isProductReview || isAgentReview || isPartyReview">
|
||||
<div
|
||||
class="dri-actions"
|
||||
[class.ddr-actions]="isProductReview || isAgentReview || isPartyReview || isSlabReview"
|
||||
>
|
||||
@if (!confirmReject) {
|
||||
<button type="button" class="dri-btn dri-btn--approve" [disabled]="!canAct" (click)="act('approve')">
|
||||
<mat-icon>check_circle</mat-icon>
|
||||
|
||||
@@ -239,6 +239,89 @@ $shadow-sm:
|
||||
}
|
||||
}
|
||||
|
||||
.ddr-stats--three {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
@media (width <= 640px) {
|
||||
.ddr-stats,
|
||||
.ddr-stats--three {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Commission slab chart ────────────────────────────────────────────────────
|
||||
.ddr-slabs {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 1px solid $border;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
|
||||
&__note {
|
||||
margin-left: auto;
|
||||
font-size: 12px;
|
||||
color: $text-soft;
|
||||
}
|
||||
|
||||
&__row {
|
||||
display: grid;
|
||||
grid-template-columns: 1.1fr 1.4fr 1.4fr 1fr;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
padding: 10px 14px;
|
||||
font-size: 13px;
|
||||
color: $text-dark;
|
||||
border-top: 1px solid $border;
|
||||
|
||||
&:first-child {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
> span {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
&--head {
|
||||
background: $bg-soft;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: uppercase;
|
||||
color: $text-soft;
|
||||
}
|
||||
|
||||
&--requested {
|
||||
background: $brand-light;
|
||||
box-shadow: inset 3px 0 0 $brand-mid;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&--overlap {
|
||||
background: #fef2f2;
|
||||
box-shadow: inset 3px 0 0 #dc2626;
|
||||
}
|
||||
}
|
||||
|
||||
&__num {
|
||||
justify-content: flex-end;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
&__flag {
|
||||
padding: 1px 7px;
|
||||
border-radius: 999px;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
background: #fee2e2;
|
||||
color: #b91c1c;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Edit banner ──────────────────────────────────────────────────────────────
|
||||
.ddr-banner {
|
||||
display: flex;
|
||||
@@ -262,6 +345,13 @@ $shadow-sm:
|
||||
border-color: $border;
|
||||
color: $text-mid;
|
||||
}
|
||||
|
||||
&--danger {
|
||||
margin-bottom: 12px;
|
||||
background: #fef2f2;
|
||||
border-color: #fecaca;
|
||||
color: #b91c1c;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Sections ─────────────────────────────────────────────────────────────────
|
||||
|
||||
+47
-1
@@ -104,6 +104,51 @@ export class DdsApprovalReviewComponent implements OnInit {
|
||||
return this.entry?.entity === 'PARTY' && this.fields.length > 0;
|
||||
}
|
||||
|
||||
/** A commission slab request, laid out as the slab and the chart it joins rather than as a money movement. */
|
||||
get isSlabReview(): boolean {
|
||||
return this.entry?.entity === 'COMMISSIONSLAB' && this.fields.length > 0;
|
||||
}
|
||||
|
||||
/** Every slab of the request's type, the requested one marked and any it overlaps flagged. */
|
||||
get slabChart(): any[] {
|
||||
return this.detail?.slabChart || [];
|
||||
}
|
||||
|
||||
get slabOverlaps(): number {
|
||||
return this.slabChart.filter((slab: any) => slab.overlaps).length;
|
||||
}
|
||||
|
||||
/** What an agent earns at the start of the band: "₹50,001 → ₹1,000.02". */
|
||||
get slabExample(): string {
|
||||
const slab = this.slabChart.find((each: any) => each.requested);
|
||||
if (!slab || slab.from === null || slab.from === undefined || slab.rate === null || slab.rate === undefined) {
|
||||
return '';
|
||||
}
|
||||
const commission = (Number(slab.from) * Number(slab.rate)) / 100;
|
||||
return `${this.rupees(slab.from)} → ${this.rupees(commission)}`;
|
||||
}
|
||||
|
||||
private static readonly RUPEES = new Intl.NumberFormat('en-IN', {
|
||||
style: 'currency',
|
||||
currency: 'INR',
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 2
|
||||
});
|
||||
|
||||
rupees(value: any): string {
|
||||
return value === null || value === undefined || value === ''
|
||||
? '—'
|
||||
: DdsApprovalReviewComponent.RUPEES.format(Number(value));
|
||||
}
|
||||
|
||||
percent(value: any): string {
|
||||
return value === null || value === undefined || value === '' ? '—' : `${Number(value)}%`;
|
||||
}
|
||||
|
||||
same(a: any, b: any): boolean {
|
||||
return Number(a) === Number(b);
|
||||
}
|
||||
|
||||
/** The nominees a DDS account request names. */
|
||||
get nominees(): any[] {
|
||||
return this.detail?.nominees || [];
|
||||
@@ -161,7 +206,8 @@ export class DdsApprovalReviewComponent implements OnInit {
|
||||
Agent: 'badge',
|
||||
'Address & Contact': 'location_on',
|
||||
'Branch & Accounts': 'account_balance',
|
||||
Balances: 'balance'
|
||||
Balances: 'balance',
|
||||
'Commission Slab': 'stacked_bar_chart'
|
||||
};
|
||||
|
||||
/** The terms in the sections the server groups them into, in the order it sends them. */
|
||||
|
||||
Reference in New Issue
Block a user