/** * 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([]); /** Commission slab requests: a slab is what an agent's commission is worked out by. */ slabSource = new MatTableDataSource([]); 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); } }