- 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>
241 lines
7.1 KiB
TypeScript
241 lines
7.1 KiB
TypeScript
/**
|
||
* Copyright since 2025 Mifos Initiative
|
||
*
|
||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||
*/
|
||
|
||
import { Component, OnInit, inject } from '@angular/core';
|
||
import {
|
||
MatTableDataSource,
|
||
MatTable,
|
||
MatColumnDef,
|
||
MatHeaderCellDef,
|
||
MatHeaderCell,
|
||
MatCellDef,
|
||
MatCell,
|
||
MatHeaderRowDef,
|
||
MatHeaderRow,
|
||
MatRowDef,
|
||
MatRow
|
||
} from '@angular/material/table';
|
||
import { MatDialog } from '@angular/material/dialog';
|
||
import { MatIcon } from '@angular/material/icon';
|
||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||
import { of } from 'rxjs';
|
||
import { catchError } from 'rxjs/operators';
|
||
|
||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||
import { OrganizationService } from 'app/organization/organization.service';
|
||
import { AuthenticationService } from 'app/core/authentication/authentication.service';
|
||
import { DdsApprovalReviewComponent } from 'app/deposits/dds-approvals/dds-approval-review/dds-approval-review.component';
|
||
import { ddsMadeOn } from '../../product-entities';
|
||
|
||
/**
|
||
* 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
|
||
* commission goes into, how many guarantors - and opens the same review the DDS queue uses, which
|
||
* shows the agent and their guarantors in full and approves or rejects through the DDS queue.
|
||
*/
|
||
@Component({
|
||
selector: 'mifosx-agent-approvals',
|
||
templateUrl: './agent-approvals.component.html',
|
||
// The same look as Product Approval, so the tabs of this screen read alike.
|
||
styleUrls: [
|
||
'../pending-savings-products/pending-savings-products.component.scss',
|
||
'./agent-approvals.component.scss'
|
||
],
|
||
imports: [
|
||
...STANDALONE_SHARED_IMPORTS,
|
||
FaIconComponent,
|
||
MatTable,
|
||
MatColumnDef,
|
||
MatHeaderCellDef,
|
||
MatHeaderCell,
|
||
MatCellDef,
|
||
MatCell,
|
||
MatHeaderRowDef,
|
||
MatHeaderRow,
|
||
MatRowDef,
|
||
MatRow,
|
||
MatIcon
|
||
]
|
||
})
|
||
export class AgentApprovalsComponent implements OnInit {
|
||
private organizationService = inject(OrganizationService);
|
||
private authenticationService = inject(AuthenticationService);
|
||
private dialog = inject(MatDialog);
|
||
|
||
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',
|
||
'client',
|
||
'branch',
|
||
'savingAccount',
|
||
'guarantors',
|
||
'maker',
|
||
'madeOn',
|
||
'status',
|
||
'actions'
|
||
];
|
||
|
||
ngOnInit(): void {
|
||
this.dataSource.filterPredicate = (row: any, filter: string) =>
|
||
[
|
||
row.agentName,
|
||
row.agentCode,
|
||
row.clientName,
|
||
row.officeName,
|
||
row.savingAccountNo,
|
||
row.type,
|
||
row.maker,
|
||
row.makerName
|
||
]
|
||
.filter(Boolean)
|
||
.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();
|
||
}
|
||
|
||
load(): void {
|
||
this.loading = true;
|
||
this.organizationService
|
||
.getDdsApprovals('PENDING')
|
||
.pipe(catchError(() => of([])))
|
||
.subscribe((rows: any) => {
|
||
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();
|
||
}
|
||
|
||
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 {
|
||
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 {
|
||
return !!this.currentUserId && Number(row.makerId) === Number(this.currentUserId);
|
||
}
|
||
|
||
formatDate(date: Date | null): string {
|
||
return date ? date.toLocaleString('en-IN') : '';
|
||
}
|
||
|
||
/** Opens the agent in full - the client, address, branch, accounts and guarantors the request carries. */
|
||
view(row: any): void {
|
||
this.dialog
|
||
.open(DdsApprovalReviewComponent, {
|
||
data: { entry: row, currentUserId: this.currentUserId },
|
||
autoFocus: false
|
||
})
|
||
.afterClosed()
|
||
.subscribe((done: boolean) => {
|
||
if (done) {
|
||
this.load();
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Whether this user may approve or reject the request. It is the same act the review and DDS
|
||
* Approvals perform, through the DDS queue, which refuses whoever made the request - so it is not
|
||
* offered to them here either.
|
||
*/
|
||
canAct(row: any): boolean {
|
||
return !this.isMine(row);
|
||
}
|
||
|
||
/**
|
||
* Approve and Reject open the agent in full rather than acting at once: a request is never actioned
|
||
* without its client, address, branch, accounts and guarantors on screen. The review carries the
|
||
* Approve and Reject buttons, and asks before rejecting.
|
||
*/
|
||
approve(row: any): void {
|
||
this.view(row);
|
||
}
|
||
|
||
reject(row: any): void {
|
||
this.view(row);
|
||
}
|
||
}
|