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:
+136
-43
@@ -7,19 +7,51 @@
|
||||
-->
|
||||
|
||||
<div class="psp-tab-content">
|
||||
@if (dataSource.data.length > 0) {
|
||||
<div class="psp-action-bar">
|
||||
<mat-form-field appearance="outline" class="psp-search-field">
|
||||
<mat-icon matPrefix>search</mat-icon>
|
||||
<input
|
||||
#filterInput
|
||||
matInput
|
||||
placeholder="Search by agent, client, branch or account"
|
||||
(keyup)="applyFilter(filterInput.value)"
|
||||
/>
|
||||
</mat-form-field>
|
||||
<!-- Agents or commission slabs, one at a time; agents first, as most requests are theirs. -->
|
||||
<div class="aa-bar">
|
||||
<div class="aa-switch" role="tablist">
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
class="aa-switch__btn"
|
||||
[class.aa-switch__btn--active]="listView === 'agents'"
|
||||
[attr.aria-selected]="listView === 'agents'"
|
||||
(click)="show('agents')"
|
||||
>
|
||||
<fa-icon icon="user-tie"></fa-icon>
|
||||
Agents
|
||||
<span class="aa-switch__count">{{ dataSource.data.length }}</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
class="aa-switch__btn"
|
||||
[class.aa-switch__btn--active]="listView === 'slabs'"
|
||||
[attr.aria-selected]="listView === 'slabs'"
|
||||
(click)="show('slabs')"
|
||||
>
|
||||
<fa-icon icon="layer-group"></fa-icon>
|
||||
Commission Slabs
|
||||
<span class="aa-switch__count">{{ slabSource.data.length }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<mat-form-field appearance="outline" class="psp-search-field aa-search">
|
||||
<mat-icon matPrefix>search</mat-icon>
|
||||
<input
|
||||
#filterInput
|
||||
matInput
|
||||
[placeholder]="
|
||||
listView === 'agents' ? 'Search by agent, client, branch or account' : 'Search by slab, band or submitter'
|
||||
"
|
||||
(keyup)="applyFilter(filterInput.value)"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<!-- ── Agents ─────────────────────────────────────────────────────────────────────────────── -->
|
||||
@if (listView !== 'agents') {
|
||||
<!-- shown under Commission Slabs -->
|
||||
} @else if (dataSource.data.length > 0) {
|
||||
<div class="table-container">
|
||||
<table mat-table [dataSource]="dataSource" class="modern-table">
|
||||
<ng-container matColumnDef="request">
|
||||
@@ -80,30 +112,7 @@
|
||||
<ng-container matColumnDef="actions">
|
||||
<th mat-header-cell *matHeaderCellDef>Actions</th>
|
||||
<td mat-cell *matCellDef="let row">
|
||||
<div class="aa-actions">
|
||||
<button mat-stroked-button class="psp-btn psp-btn-view" (click)="view(row)">
|
||||
<fa-icon icon="eye" class="m-r-10"></fa-icon>View
|
||||
</button>
|
||||
<!-- Not offered to whoever made the request - another user with checker rights has to act. -->
|
||||
<button
|
||||
mat-stroked-button
|
||||
class="psp-btn psp-btn-approve"
|
||||
[disabled]="!canAct(row)"
|
||||
[title]="isMine(row) ? 'You made this request, so another user has to approve it' : ''"
|
||||
(click)="approve(row)"
|
||||
>
|
||||
<fa-icon icon="check" class="m-r-10"></fa-icon>Approve
|
||||
</button>
|
||||
<button
|
||||
mat-stroked-button
|
||||
class="psp-btn psp-btn-reject"
|
||||
[disabled]="!canAct(row)"
|
||||
[title]="isMine(row) ? 'You made this request, so another user has to reject it' : ''"
|
||||
(click)="reject(row)"
|
||||
>
|
||||
<fa-icon icon="ban" class="m-r-10"></fa-icon>Reject
|
||||
</button>
|
||||
</div>
|
||||
<ng-container *ngTemplateOutlet="actionsTpl; context: { row: row }"></ng-container>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
@@ -111,15 +120,99 @@
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns" class="table-row"></tr>
|
||||
</table>
|
||||
</div>
|
||||
} @else if (loading) {
|
||||
<div class="empty-state">
|
||||
<p class="empty-title">Loading agent requests…</p>
|
||||
} @else {
|
||||
<p class="aa-empty">{{ loading ? 'Loading agent requests…' : 'No agent requests waiting.' }}</p>
|
||||
}
|
||||
|
||||
<!-- ── Commission slabs ───────────────────────────────────────────────────────────────────── -->
|
||||
@if (listView !== 'slabs') {
|
||||
<!-- shown under Agents -->
|
||||
} @else if (slabSource.data.length > 0) {
|
||||
<div class="table-container">
|
||||
<table mat-table [dataSource]="slabSource" class="modern-table">
|
||||
<ng-container matColumnDef="request">
|
||||
<th mat-header-cell *matHeaderCellDef>Request</th>
|
||||
<td mat-cell *matCellDef="let row">
|
||||
<span class="status-badge status-badge--action">{{ requestLabel(row) }}</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="slabType">
|
||||
<th mat-header-cell *matHeaderCellDef>Slab</th>
|
||||
<td mat-cell *matCellDef="let row">
|
||||
<strong>{{ slabTypeLabel(row.slabType) }}</strong>
|
||||
<div class="cell-mono">{{ row.resourceId ? '#' + row.resourceId : 'Number on approval' }}</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="band">
|
||||
<th mat-header-cell *matHeaderCellDef>Collection Band</th>
|
||||
<td mat-cell *matCellDef="let row">{{ rupees(row.slabFrom) }} – {{ rupees(row.slabTo) }}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="commission">
|
||||
<th mat-header-cell *matHeaderCellDef>Commission</th>
|
||||
<td mat-cell *matCellDef="let row">
|
||||
<strong>{{ percent(row.commissionPercent) }}</strong>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="maker">
|
||||
<th mat-header-cell *matHeaderCellDef>Submitted By</th>
|
||||
<td mat-cell *matCellDef="let row">{{ row.makerName || row.maker }}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="madeOn">
|
||||
<th mat-header-cell *matHeaderCellDef>Submitted On</th>
|
||||
<td mat-cell *matCellDef="let row">{{ formatDate(row.submittedOn) }}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="status">
|
||||
<th mat-header-cell *matHeaderCellDef>Status</th>
|
||||
<td mat-cell *matCellDef="let row">
|
||||
<span class="status-badge status-badge--pending">{{ row.status }}</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="actions">
|
||||
<th mat-header-cell *matHeaderCellDef>Actions</th>
|
||||
<td mat-cell *matCellDef="let row">
|
||||
<ng-container *ngTemplateOutlet="actionsTpl; context: { row: row }"></ng-container>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="slabColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: slabColumns" class="table-row"></tr>
|
||||
</table>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="empty-state">
|
||||
<fa-icon icon="user-tie" class="empty-icon"></fa-icon>
|
||||
<p class="empty-title">No agent requests waiting</p>
|
||||
<p class="empty-text">There are no agents awaiting approval at this time.</p>
|
||||
</div>
|
||||
<p class="aa-empty">{{ loading ? 'Loading slab requests…' : 'No commission slab requests waiting.' }}</p>
|
||||
}
|
||||
|
||||
<ng-template #actionsTpl let-row="row">
|
||||
<div class="aa-actions">
|
||||
<button mat-stroked-button class="psp-btn psp-btn-view" (click)="view(row)">
|
||||
<fa-icon icon="eye" class="m-r-10"></fa-icon>View
|
||||
</button>
|
||||
<!-- Not offered to whoever made the request - another user with checker rights has to act. -->
|
||||
<button
|
||||
mat-stroked-button
|
||||
class="psp-btn psp-btn-approve"
|
||||
[disabled]="!canAct(row)"
|
||||
[title]="isMine(row) ? 'You made this request, so another user has to approve it' : ''"
|
||||
(click)="approve(row)"
|
||||
>
|
||||
<fa-icon icon="check" class="m-r-10"></fa-icon>Approve
|
||||
</button>
|
||||
<button
|
||||
mat-stroked-button
|
||||
class="psp-btn psp-btn-reject"
|
||||
[disabled]="!canAct(row)"
|
||||
[title]="isMine(row) ? 'You made this request, so another user has to reject it' : ''"
|
||||
(click)="reject(row)"
|
||||
>
|
||||
<fa-icon icon="ban" class="m-r-10"></fa-icon>Reject
|
||||
</button>
|
||||
</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
|
||||
+87
@@ -28,3 +28,90 @@ td.mat-column-actions {
|
||||
width: 1%;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
// Agents or commission slabs, picked by the two buttons at the top, with the search beside them.
|
||||
.aa-bar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.aa-search {
|
||||
flex: 0 1 360px;
|
||||
}
|
||||
|
||||
.aa-switch {
|
||||
display: inline-flex;
|
||||
gap: 4px;
|
||||
padding: 4px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 12px;
|
||||
background: #f8fafc;
|
||||
|
||||
&__btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: 9px;
|
||||
background: transparent;
|
||||
color: #475569;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background 0.15s,
|
||||
color 0.15s;
|
||||
|
||||
fa-icon {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
&--active {
|
||||
background: #0f4f3c;
|
||||
color: #fff;
|
||||
box-shadow: 0 1px 3px rgb(15 23 42 / 18%);
|
||||
|
||||
fa-icon {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.aa-switch__count {
|
||||
background: rgb(255 255 255 / 20%);
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__count {
|
||||
min-width: 22px;
|
||||
padding: 1px 8px;
|
||||
border-radius: 999px;
|
||||
background: #e6f4f0;
|
||||
color: #0f4f3c;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.aa-empty {
|
||||
margin: 0 0 8px;
|
||||
padding: 14px 16px;
|
||||
border: 1px dashed #e2e8f0;
|
||||
border-radius: 10px;
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
+71
-6
@@ -33,8 +33,9 @@ import { DdsApprovalReviewComponent } from 'app/deposits/dds-approvals/dds-appro
|
||||
import { ddsMadeOn } from '../../product-entities';
|
||||
|
||||
/**
|
||||
* Approvals › Agent Approval: every DDS agent request waiting on a checker - an agent being opened,
|
||||
* changed or closed.
|
||||
* Approvals › Agent & Slab Approval: every DDS agent request waiting on a checker - an agent being
|
||||
* opened, changed or closed - and every commission slab being added or changed, since the slabs are
|
||||
* what an agent is paid by.
|
||||
*
|
||||
* An agent has no voucher and no amount, so the money columns of DDS Approvals say nothing about one.
|
||||
* This lists them by what identifies an agent - who they are, which branch, which savings account the
|
||||
@@ -73,8 +74,28 @@ export class AgentApprovalsComponent implements OnInit {
|
||||
private readonly currentUserId = this.authenticationService.getCredentials()?.userId;
|
||||
|
||||
dataSource = new MatTableDataSource<any>([]);
|
||||
/** Commission slab requests: a slab is what an agent's commission is worked out by. */
|
||||
slabSource = new MatTableDataSource<any>([]);
|
||||
loading = true;
|
||||
|
||||
/** Which list is on show: agents by default, commission slabs from the button beside it. */
|
||||
listView: 'agents' | 'slabs' = 'agents';
|
||||
|
||||
show(view: 'agents' | 'slabs'): void {
|
||||
this.listView = view;
|
||||
}
|
||||
|
||||
slabColumns: string[] = [
|
||||
'request',
|
||||
'slabType',
|
||||
'band',
|
||||
'commission',
|
||||
'maker',
|
||||
'madeOn',
|
||||
'status',
|
||||
'actions'
|
||||
];
|
||||
|
||||
displayedColumns: string[] = [
|
||||
'request',
|
||||
'agent',
|
||||
@@ -104,6 +125,18 @@ export class AgentApprovalsComponent implements OnInit {
|
||||
.join(' ')
|
||||
.toLowerCase()
|
||||
.includes(filter);
|
||||
this.slabSource.filterPredicate = (row: any, filter: string) =>
|
||||
[
|
||||
row.slabType,
|
||||
row.details,
|
||||
row.type,
|
||||
row.maker,
|
||||
row.makerName
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
.toLowerCase()
|
||||
.includes(filter);
|
||||
this.load();
|
||||
}
|
||||
|
||||
@@ -113,19 +146,51 @@ export class AgentApprovalsComponent implements OnInit {
|
||||
.getDdsApprovals('PENDING')
|
||||
.pipe(catchError(() => of([])))
|
||||
.subscribe((rows: any) => {
|
||||
const agents = (Array.isArray(rows) ? rows : []).filter((row: any) => row?.entity === 'AGENT');
|
||||
this.dataSource.data = agents.map((row: any) => ({ ...row, submittedOn: ddsMadeOn(row.madeOn) }));
|
||||
const all = (Array.isArray(rows) ? rows : []).map((row: any) => ({
|
||||
...row,
|
||||
submittedOn: ddsMadeOn(row.madeOn)
|
||||
}));
|
||||
this.dataSource.data = all.filter((row: any) => row?.entity === 'AGENT');
|
||||
this.slabSource.data = all.filter((row: any) => row?.entity === 'COMMISSIONSLAB');
|
||||
this.loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
applyFilter(value: string = ''): void {
|
||||
this.dataSource.filter = value.trim().toLowerCase();
|
||||
this.slabSource.filter = value.trim().toLowerCase();
|
||||
}
|
||||
|
||||
/** What the request is, as the badge reads it. */
|
||||
get hasRequests(): boolean {
|
||||
return this.dataSource.data.length > 0 || this.slabSource.data.length > 0;
|
||||
}
|
||||
|
||||
/** What the request is, as the badge reads it: "New Agent", "Edit Slab", "Close Agent". */
|
||||
requestLabel(row: any): string {
|
||||
return row.action === 'CREATE' ? 'New' : row.action === 'CLOSE' ? 'Close' : 'Edit';
|
||||
const verb = row.action === 'CREATE' ? 'New' : row.action === 'CLOSE' ? 'Close' : 'Edit';
|
||||
return `${verb} ${row.entity === 'COMMISSIONSLAB' ? 'Slab' : 'Agent'}`;
|
||||
}
|
||||
|
||||
/** ACCOUNT → Account. */
|
||||
slabTypeLabel(type: string | null | undefined): string {
|
||||
return type ? type.charAt(0).toUpperCase() + type.slice(1).toLowerCase() : '—';
|
||||
}
|
||||
|
||||
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 === ''
|
||||
? '—'
|
||||
: AgentApprovalsComponent.RUPEES.format(Number(value));
|
||||
}
|
||||
|
||||
percent(value: any): string {
|
||||
return value === null || value === undefined || value === '' ? '—' : `${Number(value)}%`;
|
||||
}
|
||||
|
||||
isMine(row: any): boolean {
|
||||
|
||||
Reference in New Issue
Block a user