/** * 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, PageEvent } from '@angular/material/paginator'; import { MatSort, Sort, MatSortHeader } from '@angular/material/sort'; import { MatTableDataSource, MatTable, MatColumnDef, MatHeaderCellDef, MatHeaderCell, MatCellDef, MatCell, MatHeaderRowDef, MatHeaderRow, MatRowDef, MatRow } from '@angular/material/table'; /** Custom Services */ import { environment } from '../../environments/environment'; import { ClientsService } from './clients.service'; import { NgClass } from '@angular/common'; import { forkJoin, of } from 'rxjs'; import { catchError } from 'rxjs/operators'; import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { MatProgressBar } from '@angular/material/progress-bar'; import { AccountNumberComponent } from '../shared/account-number/account-number.component'; import { StatusLookupPipe } from '../pipes/status-lookup.pipe'; import { MatIconButton, MatButton } from '@angular/material/button'; import { MatIcon } from '@angular/material/icon'; import { MatTooltip } from '@angular/material/tooltip'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; @Component({ selector: 'mifosx-clients', templateUrl: './clients.component.html', styleUrls: ['./clients.component.scss'], imports: [ ...STANDALONE_SHARED_IMPORTS, FaIconComponent, MatProgressBar, MatTable, MatSort, MatColumnDef, MatHeaderCellDef, MatHeaderCell, MatSortHeader, MatCellDef, MatCell, AccountNumberComponent, NgClass, MatHeaderRowDef, MatHeaderRow, MatRowDef, MatRow, MatPaginator, StatusLookupPipe, MatIconButton, MatButton, MatIcon, MatTooltip ] }) export class ClientsComponent implements OnInit { private clientService = inject(ClientsService); /** Returns true if client data masking is enabled */ get hideClientData(): boolean { return environment.complianceHideClientData; } /** Mask a client name */ maskName(name: string): string { if (!name) return ''; return name .split(' ') .map((part) => (part.length > 1 ? part[0] + '*'.repeat(part.length - 1) : part)) .join(' '); } displayedColumns = [ 'displayName', 'cifNumber', 'legalForm', 'status', 'profileCompletion', 'actions' ]; dataSource: MatTableDataSource = new MatTableDataSource(); existsClientsToFilter = false; notExistsClientsToFilter = false; totalRows: number; isLoading = false; pageSize = 50; currentPage = 0; filterText = ''; sortAttribute = ''; sortDirection = ''; statusFilter = ''; legalFormFilter = ''; ckycFilter = ''; private allRows: any[] = []; @ViewChild(MatPaginator) paginator: MatPaginator; @ViewChild(MatSort) sort: MatSort; ngOnInit() { if (environment.preloadClients) { this.getClients(); } } /** * Searches server for query and resource. */ search(value: string) { this.filterText = value; this.resetPaginator(); this.getClients(); } private getClients() { this.isLoading = true; this.clientService .searchByText( this.filterText, this.currentPage, this.pageSize, this.sortAttribute, this.sortDirection, false, '', true ) .subscribe( (data: any) => { this.allRows = data.content || []; this.totalRows = data.totalElements; this.existsClientsToFilter = data.numberOfElements > 0; this.notExistsClientsToFilter = !this.existsClientsToFilter; this.isLoading = false; this.applyClientFilters(); // Fetch KYC & address status for visible rows this.loadProfileStatus(this.allRows); }, (error: any) => { this.isLoading = false; } ); } /** * Fetches KYC and address status for each client row to update profile dots. */ private loadProfileStatus(clients: any[]) { if (!clients?.length) return; // Use individual subscriptions instead of forkJoin to avoid EMPTY observable issues clients.forEach((client, i) => { this.clientService .getClientData(client.id?.toString(), true) .pipe(catchError(() => of(null))) .subscribe((data: any) => { if (data?.legalForm) { clients[i].legalForm = data.legalForm; this.applyClientFilters(); } }); this.clientService .getClientKyc(client.id?.toString(), true) .pipe(catchError(() => of(null))) .subscribe((kyc: any) => { if (kyc) { clients[i]._kycDone = kyc.kycStatus === 300; clients[i]._ckycDone = !!kyc.ckycNumber; clients[i]._ckycNumber = kyc.ckycNumber || ''; this.applyClientFilters(); } }); this.clientService .getClientAddressData(client.id?.toString(), true) .pipe(catchError(() => of(null))) .subscribe((addr: any) => { if (addr && Array.isArray(addr) && addr.length > 0) { clients[i]._addressDone = true; this.applyClientFilters(); } }); }); } applyClientFilters() { const status = this.statusFilter; const legal = this.legalFormFilter; const ckyc = this.ckycFilter; const filtered = (this.allRows || []).filter((row) => { const code = (row?.status?.code || '').toLowerCase(); if (status && !code.includes(status)) { return false; } if (legal && String(row?.legalForm?.id ?? '') !== legal) { return false; } if (ckyc === 'done' && !row._ckycDone) { return false; } if (ckyc === 'pending' && row._ckycDone) { return false; } return true; }); this.dataSource.data = filtered; } pageChanged(event: PageEvent) { this.pageSize = event.pageSize; this.currentPage = event.pageIndex; this.getClients(); } sortChanged(event: Sort) { if (event.direction === '') { this.sortDirection = ''; this.sortAttribute = ''; } else { this.sortAttribute = event.active; this.sortDirection = event.direction; } this.resetPaginator(); this.getClients(); } /** * Returns profile completion steps for a client row. * Each step: { label, done } */ getProfileSteps(row: any): { label: string; done: boolean }[] { return [ { label: 'General', done: !!(row.firstname || row.displayName) && !!row.officeName }, { label: 'KYC', done: !!row._kycDone }, { label: 'Address', done: !!row._addressDone } ]; } getCompletedCount(row: any): number { return this.getProfileSteps(row).filter((s) => s.done).length; } private resetPaginator() { this.currentPage = 0; this.paginator.firstPage(); } }