/** * 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, inject } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { MatDialog } from '@angular/material/dialog'; import { MatTooltip } from '@angular/material/tooltip'; import { forkJoin, of, Observable } from 'rxjs'; import { catchError, map, shareReplay, switchMap } from 'rxjs/operators'; import { SharesService } from '../../../../shares/shares.service'; import { ClientsService } from '../../../../clients/clients.service'; import { ShareCertificateDialogComponent } from '../../../../shares/shares-account-view/share-certificate-dialog/share-certificate-dialog.component'; import { downloadShareCertificatePdf } from '../../certificate-pdf'; import { DscSignService } from '../../../dsc-sign-dialog/dsc-sign.service'; import type { ShareClass, ShareCertificateRecord } from '../share-certificate.component'; /** A real share account, kept alongside the raw API payload needed to open the real certificate dialog. */ export interface RealShareAccountRow { record: ShareCertificateRecord; sharesAccountData: any; clientId: string; } @Component({ selector: 'mifosx-real-share-accounts-view', templateUrl: './real-share-accounts-view.component.html', styleUrls: [ '../../certificates.shared.scss', './real-share-accounts-view.component.scss' ], imports: [ CommonModule, FormsModule, FaIconComponent, MatTooltip ] }) export class RealShareAccountsViewComponent implements OnInit { private dialog = inject(MatDialog); private sharesService = inject(SharesService); private clientsService = inject(ClientsService); private dscSign = inject(DscSignService); loading = true; loadError = false; rows: RealShareAccountRow[] = []; // ── Filters (mirrors the demo list above) ── search = ''; classFilter: '' | ShareClass = ''; branchFilter = ''; readonly classOptions: { value: ShareClass; label: string }[] = [ { value: 'ORDINARY', label: 'Ordinary' }, { value: 'PREFERENCE', label: 'Preference' } ]; // Per-client caches so accounts sharing the same client (a common case — one // member can hold several share accounts) only fetch that client once. private clientDataCache = new Map>(); private clientIdentifiersCache = new Map>(); ngOnInit(): void { this.loadRealAccounts(); } private loadRealAccounts(): void { this.loading = true; this.loadError = false; this.clientDataCache.clear(); this.clientIdentifiersCache.clear(); this.sharesService .getAllShareAccounts(0, 200) .pipe( map((page: any) => page?.pageItems || page?.content || (Array.isArray(page) ? page : [])), switchMap((items: any[]) => { if (!items.length) { return of([] as RealShareAccountRow[]); } return forkJoin(items.map((item) => this.buildRow(item))).pipe( map((rows) => rows.filter((r): r is RealShareAccountRow => !!r)) ); }), catchError(() => { this.loadError = true; return of([] as RealShareAccountRow[]); }) ) .subscribe((rows) => { this.rows = rows; this.loading = false; }); } /** * The share-account list already carries clientId/clientName (same mapper as the * single-account read), so the account detail call and the client calls can fire * together instead of waiting on each other — halving the round trips on the * critical path. */ private buildRow(listItem: any): Observable { const clientId = listItem?.clientId != null ? String(listItem.clientId) : null; return forkJoin({ sa: this.sharesService.getSharesAccountData(String(listItem.id), false), client: clientId ? this.getClientDataCached(clientId) : of(null), identifiers: clientId ? this.getClientIdentifiersCached(clientId) : of([]) }).pipe( switchMap(({ sa, client, identifiers }) => { // Defensive fallback if the list payload didn't carry clientId for some reason. if (clientId || !sa?.clientId) { return of({ sa, client, identifiers }); } const fallbackClientId = String(sa.clientId); return forkJoin({ sa: of(sa), client: this.getClientDataCached(fallbackClientId), identifiers: this.getClientIdentifiersCached(fallbackClientId) }); }), map(({ sa, client, identifiers }: any) => ({ record: this.mapToRecord(sa, client, identifiers), sharesAccountData: sa, clientId: String(sa.clientId) })), catchError(() => of(null)) ); } private getClientDataCached(clientId: string): Observable { if (!this.clientDataCache.has(clientId)) { this.clientDataCache.set( clientId, this.clientsService.getClientData(clientId).pipe( catchError(() => of(null)), shareReplay(1) ) ); } return this.clientDataCache.get(clientId)!; } private getClientIdentifiersCached(clientId: string): Observable { if (!this.clientIdentifiersCache.has(clientId)) { this.clientIdentifiersCache.set( clientId, this.clientsService.getClientIdentifiers(clientId).pipe( catchError(() => of([])), shareReplay(1) ) ); } return this.clientIdentifiersCache.get(clientId)!; } private mapToRecord(sa: any, client: any, identifiers: any): ShareCertificateRecord { const approvedShares = sa.summary?.totalApprovedShares || 0; const faceValue = sa.currentMarketPrice || 0; const issueDate = this.toIsoDate(sa.timeline?.activatedDate) || this.toIsoDate(sa.timeline?.approvedDate) || ''; const joiningDate = this.toIsoDate(client?.activationDate) || this.toIsoDate(client?.timeline?.submittedOnDate) || issueDate; const startNum = sa.id * 5000 + 201; const endNum = startNum + approvedShares - 1; const idList = Array.isArray(identifiers) ? identifiers : identifiers?.pageItems || []; const panId = idList.find((i: any) => /pan/i.test(i?.documentType?.name || '')); const memberPan = panId ? panId.documentKey : '—'; const dividendYtd = (sa.dividends || []).reduce((sum: number, d: any) => sum + (d.amount || 0), 0); return { certificateNo: `SC-2026-${sa.accountNo || String(sa.id).padStart(6, '0')}`, folioNo: `F-${client?.accountNo || String(client?.id ?? sa.clientId).padStart(5, '0')}`, memberId: client?.accountNo || '', memberName: sa.clientName || client?.displayName || '', memberPan, // Address isn't shown in this table — fetched on demand when a PDF is downloaded. memberAddress: '', branch: client?.officeName || '', joiningDate, shareClass: /prefer/i.test(sa.productName || '') ? 'PREFERENCE' : 'ORDINARY', shareCount: approvedShares, faceValue, issueDate, shareNoFrom: startNum, shareNoTo: approvedShares > 0 ? endNum : startNum, dividendYtd }; } private toIsoDate(value: any): string { if (!value) { return ''; } if (Array.isArray(value)) { const [ y, m, d ] = value; if (!y) { return ''; } return `${y}-${String(m || 1).padStart(2, '0')}-${String(d || 1).padStart(2, '0')}`; } return String(value); } private formatAddress(addr: any): string { if (!addr) { return ''; } return [ addr.addressLine1, addr.addressLine2, addr.townVillage, addr.city, addr.stateName, addr.countryName, addr.postalCode ] .filter((p) => !!p && String(p).trim()) .join(', '); } // ── Derived list (identical shape to the demo table above) ── get filteredRows(): RealShareAccountRow[] { const q = this.search.trim().toLowerCase(); const branch = this.branchFilter.trim().toLowerCase(); return this.rows.filter(({ record: r }) => { if (this.classFilter && r.shareClass !== this.classFilter) return false; if (branch && !r.branch.toLowerCase().includes(branch)) return false; if (!q) return true; return ( r.certificateNo.toLowerCase().includes(q) || r.folioNo.toLowerCase().includes(q) || r.memberId.toLowerCase().includes(q) || r.memberName.toLowerCase().includes(q) || r.memberPan.toLowerCase().includes(q) ); }); } get branchOptions(): string[] { return Array.from(new Set(this.rows.map((row) => row.record.branch).filter(Boolean))).sort(); } classLabel(c: ShareClass): string { return this.classOptions.find((o) => o.value === c)?.label || c; } totalShares(): number { return this.filteredRows.reduce((s, { record: r }) => s + r.shareCount, 0); } totalShareValue(): number { return this.filteredRows.reduce((s, { record: r }) => s + r.shareCount * r.faceValue, 0); } clearFilters(): void { this.search = ''; this.classFilter = ''; this.branchFilter = ''; } retry(): void { this.loadRealAccounts(); } /** Opens the same certificate dialog used from Client → Share Account → Share Certificate. */ viewCertificate(row: RealShareAccountRow): void { this.dialog.open(ShareCertificateDialogComponent, { data: { sharesAccountData: row.sharesAccountData, clientId: row.clientId }, width: '850px', maxWidth: '95vw' }); } /** Downloads the certificate as a PDF. Address is fetched here (only place it's needed). */ downloadCertificate(row: RealShareAccountRow): void { this.clientsService .getClientAddressData(row.clientId) .pipe(catchError(() => of([]))) .subscribe((addresses: any) => { const addrList = Array.isArray(addresses) ? addresses : []; const permanent = addrList.find((a: any) => /permanent/i.test(a?.addressType || '')) || addrList[0]; const r: ShareCertificateRecord = { ...row.record, memberAddress: permanent ? this.formatAddress(permanent) : row.record.memberAddress }; this.dscSign.promptThenDownload( { title: 'Share Certificate', reference: `${r.certificateNo} · Folio ${r.folioNo} · ${r.memberName}`, filename: `Share-Certificate-${r.certificateNo}.pdf` }, (dsc) => downloadShareCertificatePdf(r, dsc) ); }); } }