/** * 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, OnDestroy, ViewChild, inject } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { FormControl, ReactiveFormsModule } from '@angular/forms'; import { MatPaginator, PageEvent } from '@angular/material/paginator'; import { MatSort, MatSortHeader, Sort } from '@angular/material/sort'; import { MatTableDataSource, MatTable, MatColumnDef, MatHeaderCellDef, MatHeaderCell, MatCellDef, MatCell, MatHeaderRowDef, MatHeaderRow, MatRowDef, MatRow } from '@angular/material/table'; import { Subject, merge } from 'rxjs'; import { debounceTime, distinctUntilChanged, takeUntil } from 'rxjs/operators'; import { NgClass } from '@angular/common'; import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { MatTooltipModule } from '@angular/material/tooltip'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { FormatNumberPipe } from '../../pipes/format-number.pipe'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; import { AccountsService } from '../accounts.service'; export type AccountType = 'savings' | 'current' | 'fixed-deposits' | 'recurring-deposits'; interface AccountConfig { label: string; icon: string; accentClass: string; typeLabel: string; typeBadgeClass: string; } @Component({ selector: 'mifosx-accounts-list', templateUrl: './accounts-list.component.html', styleUrls: ['./accounts-list.component.scss'], standalone: true, imports: [ ...STANDALONE_SHARED_IMPORTS, ReactiveFormsModule, FaIconComponent, MatTable, MatSort, MatSortHeader, MatColumnDef, MatHeaderCellDef, MatHeaderCell, MatCellDef, MatCell, MatHeaderRowDef, MatHeaderRow, MatRowDef, MatRow, MatPaginator, NgClass, MatTooltipModule, MatProgressSpinnerModule, FormatNumberPipe ] }) export class AccountsListComponent implements OnInit, OnDestroy { private route = inject(ActivatedRoute); private router = inject(Router); private accountsService = inject(AccountsService); private destroy$ = new Subject(); accountType: AccountType = 'savings'; displayedColumns = [ 'accountNo', 'depositType', 'clientName', 'productName', 'balance', 'status', 'actions' ]; dataSource = new MatTableDataSource([]); totalRecords = 0; pageSize = 20; pageIndex = 0; isLoading = false; searchControl = new FormControl(''); searchType: 'accountNo' | 'clientName' = 'accountNo'; fromDateControl = new FormControl(''); toDateControl = new FormControl(''); asOnDateControl = new FormControl(''); @ViewChild(MatPaginator) paginator!: MatPaginator; @ViewChild(MatSort) sort!: MatSort; readonly configs: Record = { savings: { label: 'Savings Accounts', icon: 'piggy-bank', accentClass: 'accent--saving', typeLabel: 'SB', typeBadgeClass: 'dtype--sb' }, current: { label: 'Current Accounts', icon: 'building', accentClass: 'accent--current', typeLabel: 'CA', typeBadgeClass: 'dtype--ca' }, 'fixed-deposits': { label: 'Fixed Deposit Accounts', icon: 'lock', accentClass: 'accent--fd', typeLabel: 'FD', typeBadgeClass: 'dtype--fd' }, 'recurring-deposits': { label: 'Recurring Deposit Accounts', icon: 'sync', accentClass: 'accent--rd', typeLabel: 'RD', typeBadgeClass: 'dtype--rd' } }; get config(): AccountConfig { return this.configs[this.accountType]; } ngOnInit() { this.route.data.pipe(takeUntil(this.destroy$)).subscribe((data) => { this.accountType = data['accountType'] as AccountType; this.pageIndex = 0; this.searchControl.setValue('', { emitEvent: false }); this.loadAccounts(); }); this.searchControl.valueChanges .pipe(debounceTime(300), distinctUntilChanged(), takeUntil(this.destroy$)) .subscribe(() => { this.pageIndex = 0; this.loadAccounts(); }); merge(this.fromDateControl.valueChanges, this.toDateControl.valueChanges) .pipe(debounceTime(400), takeUntil(this.destroy$)) .subscribe(() => { const from = this.fromDateControl.value; const to = this.toDateControl.value; if ((from && to) || (!from && !to)) { this.pageIndex = 0; this.loadAccounts(); } }); this.asOnDateControl.valueChanges.pipe(debounceTime(300), takeUntil(this.destroy$)).subscribe(() => { this.pageIndex = 0; this.loadAccounts(); }); } ngOnDestroy() { this.destroy$.next(); this.destroy$.complete(); } loadAccounts() { this.isLoading = true; const search = (this.searchControl.value || '').trim(); const fromVal = this.fromDateControl.value; const toVal = this.toDateControl.value; const bothDatesSet = !!(fromVal && toVal); const params: import('../accounts.service').AccountListParams = { offset: this.pageIndex * this.pageSize, limit: this.pageSize, accountNo: this.searchType === 'accountNo' ? search || undefined : undefined, clientName: this.searchType === 'clientName' ? search || undefined : undefined, orderBy: 'id', sortOrder: 'DESC', status: 'active', ...(bothDatesSet && { fromDate: fromVal! }), ...(bothDatesSet && { toDate: toVal! }), ...(this.asOnDateControl.value && { asOnDate: this.asOnDateControl.value }) }; const obs$ = this.accountType === 'savings' ? this.accountsService.getSavingsAccounts(params) : this.accountType === 'current' ? this.accountsService.getCurrentAccounts(params) : this.accountType === 'fixed-deposits' ? this.accountsService.getFixedDepositAccounts(params) : this.accountsService.getRecurringDepositAccounts(params); obs$.pipe(takeUntil(this.destroy$)).subscribe({ next: (res) => { let items: any[] = Array.isArray(res) ? res : (res?.pageItems ?? []); if (this.accountType === 'savings') { items = items.filter((a: any) => a?.depositType?.id === 100); } items = items.filter((a: any) => (a?.status?.id ?? a?.statusId) === 300); const searchTerm = search.toLowerCase(); if (searchTerm) { items = items.filter((a: any) => this.searchType === 'accountNo' ? (a?.accountNo || '').toLowerCase().includes(searchTerm) : (a?.clientName || '').toLowerCase().includes(searchTerm) ); } if (bothDatesSet || this.asOnDateControl.value) { const fromDate = bothDatesSet ? new Date(fromVal!) : null; const toDate = bothDatesSet ? new Date(toVal! + 'T23:59:59') : null; const asOnDate = this.asOnDateControl.value ? new Date(this.asOnDateControl.value + 'T23:59:59') : null; items = items.filter((a: any) => { const d = this.parseFinDate(a?.activationDate ?? a?.submittedOnDate); if (!d) return true; if (fromDate && d < fromDate) return false; if (toDate && d > toDate) return false; if (asOnDate && d > asOnDate) return false; return true; }); } this.totalRecords = Array.isArray(res) ? items.length : (res?.totalFilteredRecords ?? items.length); this.dataSource.data = items; this.isLoading = false; }, error: () => { this.isLoading = false; } }); } onPageChange(event: PageEvent) { this.pageIndex = event.pageIndex; this.pageSize = event.pageSize; this.loadAccounts(); } private parseFinDate(d: any): Date | null { if (!d) return null; if (Array.isArray(d) && d.length >= 3) return new Date(d[0], d[1] - 1, d[2]); if (typeof d === 'string') return new Date(d); return null; } readonly today = new Date().toISOString().split('T')[0]; get hasDateFilters(): boolean { return !!((this.fromDateControl.value && this.toDateControl.value) || this.asOnDateControl.value); } clearDateFilters() { this.fromDateControl.setValue(null, { emitEvent: false }); this.toDateControl.setValue(null, { emitEvent: false }); this.asOnDateControl.setValue(null, { emitEvent: false }); this.pageIndex = 0; this.loadAccounts(); } onSortChange(_sort: Sort) { this.pageIndex = 0; this.loadAccounts(); } toggleSearchType() { this.searchType = this.searchType === 'accountNo' ? 'clientName' : 'accountNo'; if (this.searchControl.value) this.loadAccounts(); } clearSearch() { this.searchControl.setValue(''); } viewAccount(account: any) { const id = account.id; const routes: Record = { savings: [ '/clients', account.clientId, 'savings-accounts', id, 'general' ], current: [ '/clients', account.clientId, 'current-accounts', id, 'general' ], 'fixed-deposits': [ '/clients', account.clientId, 'fixed-deposits-accounts', id, 'general' ], 'recurring-deposits': [ '/clients', account.clientId, 'recurring-deposits-accounts', id, 'general' ] }; this.router.navigate(routes[this.accountType]); } getBalance(account: any): number { return account?.summary?.accountBalance ?? account?.accountBalance ?? 0; } getCurrencySymbol(account: any): string { return account?.currency?.displaySymbol || account?.currency?.code || '₹'; } getStatusClass(account: any): string { const id = account?.status?.id ?? account?.statusId; const map: Record = { 100: 'status--submitted', 200: 'status--approved', 300: 'status--active', 400: 'status--withdrawn', 500: 'status--rejected', 600: 'status--closed' }; return map[id] ?? 'status--default'; } getStatusLabel(account: any): string { return account?.status?.value ?? account?.statusValue ?? 'Unknown'; } }