/** * 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/. */ /** Angular Imports */ import { Component, OnInit, ViewChild, inject } from '@angular/core'; import { MatPaginator } from '@angular/material/paginator'; import { MatSort, MatSortHeader } from '@angular/material/sort'; import { MatTable, MatTableDataSource, MatColumnDef, MatHeaderCellDef, MatHeaderCell, MatCellDef, MatCell, MatHeaderRowDef, MatHeaderRow, MatRowDef, MatRow } from '@angular/material/table'; import { MatTooltip } from '@angular/material/tooltip'; import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; import { MatDialog } from '@angular/material/dialog'; /** Custom Services */ import { OrganizationService } from 'app/organization/organization.service'; /** Custom Components */ import { NomineeListDialogComponent } from './nominee-list-dialog/nominee-list-dialog.component'; /** * Party Master list. * * The DDS accounts on file - each one a customer's daily deposit account, opened under a scheme * (Sub Type) and assigned to the agent who collects for it. Follows the same list -> create/edit * shape as Agent Master and the product lists, and shares their stylesheet. */ @Component({ selector: 'mifosx-party-master', templateUrl: './party-master.component.html', styleUrls: ['./party-master.component.scss'], imports: [ ...STANDALONE_SHARED_IMPORTS, FaIconComponent, MatTable, MatSort, MatColumnDef, MatHeaderCellDef, MatHeaderCell, MatSortHeader, MatCellDef, MatCell, MatHeaderRowDef, MatHeaderRow, MatRowDef, MatRow, MatPaginator, MatTooltip ] }) export class PartyMasterComponent implements OnInit { private organizationService = inject(OrganizationService); private dialog = inject(MatDialog); displayedColumns: string[] = [ 'partyCode', 'partyName', 'subType', 'agentName', 'savingsAccountNo', 'dailyDepositAmount', 'currentBalance', 'nominees', 'status', 'actions' ]; /** * Who a party is nominated to, without opening it for editing. * * Asked often enough to belong on the list, and answered from what the list already holds - the * parties come back with their nominees on them, so this opens rather than fetches. */ showNominees(party: any): void { this.dialog.open(NomineeListDialogComponent, { data: { partyCode: party.partyCode, partyName: party.partyName, splitMode: party.nomineeSplitMode, nominees: party.nominees || [] }, autoFocus: false }); } /** How many people are named on an account, which is what the list column shows. */ nomineeCount(party: any): number { return (party?.nominees || []).length; } /** * Matured accounts still open, and what they add up to. Shown here because this is the screen an * officer already opens - money the bank owes a customer should not wait on somebody thinking to * run a report. */ dueForClosureCount = 0; dueForClosurePayable = 0; dataSource: MatTableDataSource = new MatTableDataSource([]); parties: any[] = []; filteredCount = 0; paginator: MatPaginator; sort: MatSort; // The table sits inside an @if, so these only exist once the list has loaded. Read statically they // were never there, the table was never paged, and the paginator read "0 of 0" over a full list. @ViewChild(MatPaginator) set matPaginator(paginator: MatPaginator) { this.paginator = paginator; if (paginator) { this.dataSource.paginator = paginator; } } @ViewChild(MatSort) set matSort(sort: MatSort) { this.sort = sort; if (sort) { this.dataSource.sort = sort; } } get totalCount(): number { return this.parties.length; } get activeCount(): number { return this.parties.filter((party) => party.status !== 'CLOSED').length; } /** What the whole book is worth today - every open account's running balance added up. */ get totalCurrentBalance(): number { return this.parties.reduce((sum, party) => sum + Number(party.currentBalance || 0), 0); } /** What the agents should be collecting between them on a full collection day. */ get totalDailyDue(): number { return this.parties .filter((party) => party.status !== 'CLOSED') .reduce((sum, party) => sum + Number(party.dailyDepositAmount || 0), 0); } ngOnInit(): void { this.organizationService.getParties().subscribe((parties: any[]) => { this.parties = parties || []; this.dataSource = new MatTableDataSource(this.parties); this.dataSource.paginator = this.paginator; this.dataSource.sort = this.sort; this.filteredCount = this.parties.length; this.dataSource.connect().subscribe((rows) => (this.filteredCount = rows.length)); }); this.loadDueForClosure(); } /** * Matured accounts still open. A separate call rather than filtered out of the list above, because * the server decides what counts as matured - comparing dates here would drift from it the first * time either changed. */ private loadDueForClosure(): void { this.organizationService.getPartiesDueForClosure().subscribe({ next: (due: any) => { const rows = Array.isArray(due) ? due : due?.pageItems || []; this.dueForClosureCount = rows.length; this.dueForClosurePayable = rows.reduce( (sum: number, party: any) => sum + Number(party.currentBalance || 0), 0 ); }, // The party list is the point of this screen; a failed banner must not take it down with it. error: () => { this.dueForClosureCount = 0; this.dueForClosurePayable = 0; } }); } applyFilter(filterValue: string): void { this.dataSource.filter = filterValue.trim().toLowerCase(); } }