275 lines
7.1 KiB
TypeScript
275 lines
7.1 KiB
TypeScript
/**
|
|
* 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<any> = 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();
|
|
}
|
|
}
|