313 lines
11 KiB
TypeScript
313 lines
11 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/.
|
|
*/
|
|
|
|
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<string, Observable<any>>();
|
|
private clientIdentifiersCache = new Map<string, Observable<any>>();
|
|
|
|
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<RealShareAccountRow | null> {
|
|
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<any> {
|
|
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<any> {
|
|
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)
|
|
);
|
|
});
|
|
}
|
|
}
|