/** * 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, OnDestroy, OnInit, inject } from '@angular/core'; import { Router } from '@angular/router'; import { FormControl, ReactiveFormsModule } from '@angular/forms'; import { NgClass } from '@angular/common'; import { MatPaginator, PageEvent } from '@angular/material/paginator'; import { MatCell, MatCellDef, MatColumnDef, MatHeaderCell, MatHeaderCellDef, MatHeaderRow, MatHeaderRowDef, MatRow, MatRowDef, MatTable } from '@angular/material/table'; import { MatTooltipModule } from '@angular/material/tooltip'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { Subject, merge } from 'rxjs'; import { debounceTime, distinctUntilChanged, takeUntil } from 'rxjs/operators'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; import { FormatNumberPipe } from 'app/pipes/format-number.pipe'; import { OrganizationService } from 'app/organization/organization.service'; /** * The DDS accounts, listed the way Savings Accounts lists savings: account number, client, product, * balance and status, searched by account number or client and narrowed by opening date. Each opens * onto the account's own page and its transactions. * * Read-only. Accounts are opened and changed from DDS Party Master; money moves through collections, * the cashier and the batch runs. */ @Component({ selector: 'mifosx-dds-accounts', templateUrl: './dds-accounts.component.html', // The Savings Accounts list's own look, so the two screens read as one family. styleUrls: [ '../../accounts/accounts-list/accounts-list.component.scss', './dds-accounts.component.scss' ], standalone: true, imports: [ ...STANDALONE_SHARED_IMPORTS, ReactiveFormsModule, NgClass, FaIconComponent, MatTable, MatColumnDef, MatHeaderCellDef, MatHeaderCell, MatCellDef, MatCell, MatHeaderRowDef, MatHeaderRow, MatRowDef, MatRow, MatPaginator, MatTooltipModule, MatProgressSpinnerModule, FormatNumberPipe ] }) export class DdsAccountsComponent implements OnInit, OnDestroy { private organizationService = inject(OrganizationService); private router = inject(Router); private destroy$ = new Subject(); displayedColumns = [ 'accountNo', 'depositType', 'clientName', 'productName', 'balance', 'status', 'actions' ]; /** Every DDS account, fetched once; search, dates and paging work on this. */ private allAccounts: any[] = []; /** The accounts matching the search and dates, whatever their status - what the status counts read. */ private beforeStatus: any[] = []; /** The accounts matching the search, dates and status, before paging. */ private matching: any[] = []; /** Which accounts are shown: all of them, only the open ones, or only the closed. */ statusFilter: 'ALL' | 'ACTIVE' | 'CLOSED' = 'ALL'; /** The page on screen. */ rows: any[] = []; pageSize = 20; pageIndex = 0; isLoading = false; searchControl = new FormControl(''); searchType: 'accountNo' | 'clientName' = 'accountNo'; fromDateControl = new FormControl(''); toDateControl = new FormControl(''); asOnDateControl = new FormControl(''); readonly today = new Date().toISOString().split('T')[0]; get totalRecords(): number { return this.matching.length; } get activeCount(): number { return this.beforeStatus.filter((account) => account.status === 'ACTIVE').length; } get closedCount(): number { return this.beforeStatus.filter((account) => account.status === 'CLOSED').length; } get allCount(): number { return this.beforeStatus.length; } setStatusFilter(status: 'ALL' | 'ACTIVE' | 'CLOSED'): void { this.statusFilter = status; this.pageIndex = 0; this.applyFilters(); } get hasDateFilters(): boolean { return !!((this.fromDateControl.value && this.toDateControl.value) || this.asOnDateControl.value); } ngOnInit(): void { this.loadAccounts(); this.searchControl.valueChanges .pipe(debounceTime(300), distinctUntilChanged(), takeUntil(this.destroy$)) .subscribe(() => { this.pageIndex = 0; this.applyFilters(); }); merge(this.fromDateControl.valueChanges, this.toDateControl.valueChanges, this.asOnDateControl.valueChanges) .pipe(debounceTime(300), takeUntil(this.destroy$)) .subscribe(() => { this.pageIndex = 0; this.applyFilters(); }); } ngOnDestroy(): void { this.destroy$.next(); this.destroy$.complete(); } loadAccounts(): void { this.isLoading = true; this.organizationService .getParties() .pipe(takeUntil(this.destroy$)) .subscribe({ next: (parties: any) => { // Newest first, as the savings list shows them. this.allAccounts = (Array.isArray(parties) ? parties : parties?.pageItems || []).sort( (a: any, b: any) => Number(b.id) - Number(a.id) ); this.isLoading = false; this.applyFilters(); }, error: () => { this.allAccounts = []; this.isLoading = false; this.applyFilters(); } }); } /** Search by account number or client, and by the day the account was opened. */ private applyFilters(): void { const term = String(this.searchControl.value || '') .trim() .toLowerCase(); const fromVal = this.fromDateControl.value; const toVal = this.toDateControl.value; const bothDates = !!(fromVal && toVal); const asOn = this.asOnDateControl.value; this.beforeStatus = this.allAccounts.filter((account: any) => { if (term) { const haystack = this.searchType === 'accountNo' ? `${account.accountNo || ''} ${account.partyCode || ''}` : `${account.clientName || ''} ${account.partyName || ''}`; if (!haystack.toLowerCase().includes(term)) { return false; } } const opened = String(account.openingDate || ''); if (bothDates && opened && (opened < String(fromVal) || opened > String(toVal))) { return false; } // As on a date: the accounts that had been opened by then. if (asOn && opened && opened > String(asOn)) { return false; } return true; }); this.matching = this.statusFilter === 'ALL' ? this.beforeStatus : this.beforeStatus.filter((account: any) => account.status === this.statusFilter); this.showPage(); } private showPage(): void { const start = this.pageIndex * this.pageSize; this.rows = this.matching.slice(start, start + this.pageSize); } onPageChange(event: PageEvent): void { this.pageIndex = event.pageIndex; this.pageSize = event.pageSize; this.showPage(); } toggleSearchType(): void { this.searchType = this.searchType === 'accountNo' ? 'clientName' : 'accountNo'; this.pageIndex = 0; this.applyFilters(); } clearSearch(): void { this.searchControl.setValue(''); } clearDateFilters(): void { this.fromDateControl.setValue('', { emitEvent: false }); this.toDateControl.setValue('', { emitEvent: false }); this.asOnDateControl.setValue('', { emitEvent: false }); this.pageIndex = 0; this.applyFilters(); } viewAccount(account: any): void { this.router.navigate([ '/accounts/dds-accounts', account.id ]); } getStatusClass(account: any): string { switch (account?.status) { case 'ACTIVE': return 'status--active'; case 'CLOSED': return 'status--closed'; default: return 'status--default'; } } }