Files
cbsnexora/src/app/clients/clients-view/ckyc-tab/ckyc-tab.component.ts
T
niteshandClaude Sonnet 5 31e99b1363 fix(clients): rebuild CKYC document upload as a popup with full proof details
The CKYC tab's document cards now open a dedicated dialog (with client-side
preview) instead of expanding inline, collecting document type, number, and
issue/expiry dates the same way client creation does. Fixes several bugs
found along the way:

- Uploading a document type that's valid for both identity and address
  (Aadhaar, Passport, etc.) no longer 404s or 500s — the "no such endpoint"
  and case-sensitive ClientIdentifierStatus.valueOf("Active") crashes are
  both resolved.
- Edit Client's Identity-proof and Address-proof steps no longer show each
  other's document when both are dual-purpose OVD types, and no longer show
  stale/mismatched front-back scans after a document type change.
- Address-proof's own document preview never showed a document at all when
  no m_client_kyc record existed yet.

Document number and issue/expiry dates are persisted on the identifier
(documentKey + a shared "Issue date: ..., Expiry date: ..." description
convention) and read back by both the CKYC tab and Edit Client so the same
details appear everywhere.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-14 11:39:14 +05:30

700 lines
27 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, OnDestroy, inject } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Subject, Observable, takeUntil, catchError, of, forkJoin } from 'rxjs';
/** Custom Services */
import { ClientsService } from '../../clients.service';
import { AlertService } from 'app/core/alert/alert.service';
import { MatIcon } from '@angular/material/icon';
import { MatProgressSpinner } from '@angular/material/progress-spinner';
import { MatDialog } from '@angular/material/dialog';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
import { Dates } from 'app/core/utils/dates';
import { ProofType, normalizeProofType } from '../../shared/kyc-proof-types';
import { CkycUploadDialogComponent, CkycUploadDialogResult } from './ckyc-upload-dialog/ckyc-upload-dialog.component';
interface ExistingArtefact {
present: boolean;
name?: string;
source?: string;
previewUrl?: string;
previewUrlBack?: string;
documentId?: string;
documentTypeId?: string;
documentKey?: string;
/** "Issue date: yyyy-MM-dd, Expiry date: yyyy-MM-dd" — same convention client-india-address-step
* already writes/reads for address proof; now shared by the photo/identity proof too. */
description?: string;
fileCount?: number;
}
type ProofKind = 'ADDRESS_PROOF' | 'PHOTO_PROOF';
type CkycDocKind = ProofKind | 'CLIENT_IMAGE' | 'CLIENT_SIGNATURE';
@Component({
selector: 'mifosx-ckyc-tab',
templateUrl: './ckyc-tab.component.html',
styleUrls: ['./ckyc-tab.component.scss'],
standalone: true,
imports: [
...STANDALONE_SHARED_IMPORTS,
MatIcon,
MatProgressSpinner,
FaIconComponent
]
})
export class CkycTabComponent implements OnInit, OnDestroy {
private route = inject(ActivatedRoute);
private clientsService = inject(ClientsService);
private alertService = inject(AlertService);
private dialog = inject(MatDialog);
private dates = inject(Dates);
private destroy$ = new Subject<void>();
/** Top section - read-only client info */
clientId: string;
clientName: string = '';
clientEmail: string = '';
clientMobile: string = '';
cifNumber: string = '';
ckycNumber: string = '';
allAccounts: any[] = [];
selectedAccount: any = null;
selectedAccountType: string = '';
/** State */
loading = true;
ckycData: any = null;
ckycUploadedOn = '';
ckycBatchId = '';
checkingExisting = false;
/** KYC verification status (Aadhaar + PAN) — independent of CERSAI sync */
panVerified = false;
aadhaarVerified = false;
panHolderName = '';
aadhaarHolderName = '';
panNumber = '';
aadhaarMasked = '';
get kycComplete(): boolean {
return this.panVerified && this.aadhaarVerified;
}
/** Pre-existing artefacts detected from client data */
existing: Record<CkycDocKind, ExistingArtefact> = {
ADDRESS_PROOF: { present: false },
PHOTO_PROOF: { present: false },
CLIENT_IMAGE: { present: false },
CLIENT_SIGNATURE: { present: false }
};
/** Document-type options for the proof pickers, fetched once from /kyc/proof-types */
addressProofOptions: ProofType[] = [];
idProofOptions: ProofType[] = [];
/** Document type currently selected in each proof card's picker */
selectedProofType: Record<ProofKind, ProofType | null> = {
ADDRESS_PROOF: null,
PHOTO_PROOF: null
};
/** Front/back files staged for the two proof cards; other cards keep a single file each */
docFiles: Record<ProofKind, { front: File | null; back: File | null }> = {
ADDRESS_PROOF: { front: null, back: null },
PHOTO_PROOF: { front: null, back: null }
};
/** Document number + issue/expiry dates staged from the upload dialog, same fields client
* creation collects for a KYC/address proof — persisted onto the identifier's documentKey
* and description ("Issue date: yyyy-MM-dd, Expiry date: yyyy-MM-dd"). */
private documentDetails: Record<
ProofKind,
{ documentKey: string; issueDate: string | null; expiryDate: string | null }
> = {
ADDRESS_PROOF: { documentKey: '', issueDate: null, expiryDate: null },
PHOTO_PROOF: { documentKey: '', issueDate: null, expiryDate: null }
};
/** File references */
clientImageFile: File | null = null;
clientSignatureFile: File | null = null;
/** Per-document upload state */
uploading: { [key: string]: boolean } = {
ADDRESS_PROOF: false,
PHOTO_PROOF: false,
CLIENT_IMAGE: false,
CLIENT_SIGNATURE: false
};
/** Per-document error messages */
uploadErrors: { [key: string]: string | null } = {
ADDRESS_PROOF: null,
PHOTO_PROOF: null,
CLIENT_IMAGE: null,
CLIENT_SIGNATURE: null
};
ngOnInit(): void {
this.route.parent.params.subscribe((params: any) => {
this.clientId = params['clientId'];
this.loadClientInfo();
this.loadAccounts();
this.loadCkycData();
this.loadExistingArtefacts();
});
}
private loadClientInfo(): void {
this.route.parent.data.subscribe((data: any) => {
const cv = data?.clientViewData;
if (cv) {
this.clientName = cv.displayName || '';
this.clientEmail = cv.emailAddress || '';
this.clientMobile = cv.mobileNo || '';
this.cifNumber = cv.accountNo || cv.externalId || '';
}
});
}
private loadAccounts(): void {
this.clientsService
.getClientAccountData(this.clientId)
.pipe(
takeUntil(this.destroy$),
catchError(() => of(null))
)
.subscribe((data: any) => {
if (data) {
const savings = (data.savingsAccounts || []).map((a: any) => ({
accountNo: a.accountNo,
accountType: 'Savings',
productName: a.productName || '',
id: a.id
}));
const loans = (data.loanAccounts || []).map((a: any) => ({
accountNo: a.accountNo,
accountType: 'Loan',
productName: a.productName || '',
id: a.id
}));
this.allAccounts = [
...savings,
...loans
];
}
});
}
private loadCkycData(): void {
this.loading = true;
// Read CKYC registry data from /clients/{id}/kyc — that's where the
// SFTP upload flow persists ckycNumber / ckycUploadedOn / ckycBatchId.
this.clientsService
.getClientKyc(this.clientId)
.pipe(
takeUntil(this.destroy$),
catchError(() => {
this.loading = false;
return of(null);
})
)
.subscribe((data: any) => {
this.loading = false;
this.ckycData = data;
this.ckycNumber = data?.ckycNumber || '';
this.ckycUploadedOn = this.formatJavaDate(data?.ckycUploadedOn);
this.ckycBatchId = data?.ckycBatchId || '';
this.panVerified = data?.panVerified === true || data?.panVerified === 'true' || data?.panVerified === 1;
this.aadhaarVerified =
data?.aadhaarVerified === true || data?.aadhaarVerified === 'true' || data?.aadhaarVerified === 1;
this.panHolderName = data?.panFullName || data?.panHolderName || '';
this.aadhaarHolderName = data?.aadhaarHolderName || '';
this.panNumber = data?.panNumber || '';
this.aadhaarMasked = data?.aadhaarNumberMasked || '';
});
}
private formatJavaDate(value: any): string {
if (!value) return '';
if (Array.isArray(value) && value.length >= 3) {
const [
y,
m,
d
] = value;
return `${String(d).padStart(2, '0')}-${String(m).padStart(2, '0')}-${y}`;
}
if (typeof value === 'string') return value;
return '';
}
/**
* Fetch photo (profile image), signature (client document),
* ID proof and address proof (client identifiers) so already-uploaded
* artefacts are shown here instead of forcing re-upload.
*/
private loadExistingArtefacts(): void {
this.checkingExisting = true;
forkJoin({
photo: this.clientsService.getClientProfileImage(this.clientId).pipe(catchError(() => of(null))),
documents: this.clientsService.getClientDocuments(this.clientId).pipe(catchError(() => of([]))),
identifiers: this.clientsService.getClientIdentifiers(this.clientId).pipe(catchError(() => of([]))),
proofTypes: this.clientsService.getKycProofTypes().pipe(catchError(() => of({ idProofs: [], addressProofs: [] })))
})
.pipe(takeUntil(this.destroy$))
.subscribe(({ photo, documents, identifiers, proofTypes }) => {
this.checkingExisting = false;
// 1. Photo / image of client — from m_client.image_id
if (photo && typeof photo === 'string' && photo.length > 0) {
this.existing.CLIENT_IMAGE = {
present: true,
source: 'Client registration',
previewUrl: photo,
name: 'Profile photo'
};
}
// 2. Signature — client document with name='clientSignature'. Multiple rows can exist
// for clients who re-cropped/replaced their signature before uploads became single-slot,
// so pick the highest id (latest) rather than the first unordered match.
const signatureDoc = (documents as any[])
?.filter((d: any) => d?.name === 'clientSignature')
?.sort((a: any, b: any) => b.id - a.id)?.[0];
if (signatureDoc) {
this.existing.CLIENT_SIGNATURE = {
present: true,
source: 'Client registration',
name: signatureDoc.fileName || 'Client signature',
documentId: signatureDoc.id
};
// Fetch blob for preview
this.clientsService
.getClientSignatureImage(this.clientId, signatureDoc.id)
.pipe(takeUntil(this.destroy$))
.subscribe({
next: (blob: any) => {
if (blob) {
this.existing.CLIENT_SIGNATURE.previewUrl = URL.createObjectURL(blob);
}
},
error: () => undefined
});
}
// 3 & 4. ID proof + Address proof — client identifiers classified via proof-types
this.idProofOptions = (proofTypes?.idProofs || []).map((p: any) => normalizeProofType(p));
this.addressProofOptions = (proofTypes?.addressProofs || []).map((p: any) => normalizeProofType(p));
const idProofIds = new Set<string>((proofTypes?.idProofs || []).map((p: any) => String(p.id)));
const addressProofIds = new Set<string>((proofTypes?.addressProofs || []).map((p: any) => String(p.id)));
const idList = Array.isArray(identifiers) ? identifiers : (identifiers as any)?.pageItems || [];
for (const identity of idList) {
const docTypeId = String(identity?.documentType?.id ?? '');
const isActive =
!identity?.status ||
identity?.status?.code === 'clientIdentifierStatusType.active' ||
identity?.status === 'clientIdentifierStatusType.active' ||
identity?.status === 'Active';
if (!isActive) continue;
const proofName = identity?.documentType?.name || 'Identifier';
// Address proof documentKey is auto-generated as "<ProofName> - <clientId>"
// in identities-tab; hide it when it's just that synthetic key.
const rawKey = identity?.documentKey || '';
const isSyntheticKey = rawKey.startsWith(`${proofName} - `);
const common = {
present: true,
source: 'Identities section',
name: proofName,
documentKey: isSyntheticKey ? '' : rawKey,
documentId: identity?.id,
documentTypeId: docTypeId,
description: identity?.description || '',
fileCount: (identity?.documents || []).length
};
if (idProofIds.has(docTypeId) && !this.existing.PHOTO_PROOF.present) {
this.existing.PHOTO_PROOF = common;
this.selectedProofType.PHOTO_PROOF = this.idProofOptions.find((p) => String(p.id) === docTypeId) || null;
this.loadIdentifierDocPreview('PHOTO_PROOF', identity.id);
} else if (addressProofIds.has(docTypeId) && !this.existing.ADDRESS_PROOF.present) {
this.existing.ADDRESS_PROOF = common;
this.selectedProofType.ADDRESS_PROOF =
this.addressProofOptions.find((p) => String(p.id) === docTypeId) || null;
this.loadIdentifierDocPreview('ADDRESS_PROOF', identity.id);
}
}
});
}
/**
* The scanned ID/address-proof card only showed its file name and key — never the scan
* itself, unlike the photo and signature cards right beside it. Mirrors those two: fetch
* the identifier's own attached document(s) (m_client_identifier -> m_document, the same
* chain the Identities tab and the create-client stepper read) and download the latest for
* an inline preview. Two-sided proof types (Aadhaar, Passport, Voter ID, Driving Licence)
* store front/back as two separate documents named "<Proof> — Front"/"— Back" (the same
* convention the create-client stepper writes), so both are matched and previewed here;
* a legacy single upload with no such marker just shows as the one preview.
*/
private loadIdentifierDocPreview(kind: ProofKind, identifierId: string): void {
this.clientsService
.getClientIdentificationDocuments(identifierId)
.pipe(
takeUntil(this.destroy$),
catchError(() => of([]))
)
.subscribe((docs: any) => {
const list = Array.isArray(docs) ? docs : [];
if (!list.length) return;
this.existing[kind].fileCount = list.length;
const byNewest = (a: any, b: any) => b.id - a.id;
const label = (d: any) => (d?.name || d?.fileName || '').toString();
const back = [...list].filter((d: any) => /back/i.test(label(d))).sort(byNewest)[0];
const front =
[...list].filter((d: any) => /front/i.test(label(d))).sort(byNewest)[0] ||
[...list].filter((d: any) => d.id !== back?.id).sort(byNewest)[0];
if (front) this.downloadIdentifierDocPreview(identifierId, front.id, kind, 'front');
if (back && back.id !== front?.id) this.downloadIdentifierDocPreview(identifierId, back.id, kind, 'back');
});
}
private downloadIdentifierDocPreview(
identifierId: string,
documentId: string,
kind: ProofKind,
slot: 'front' | 'back'
): void {
this.clientsService
.downloadClientIdentificationDocument(identifierId, documentId)
.pipe(takeUntil(this.destroy$))
.subscribe({
next: (blob: any) => {
if (!blob) return;
const url = URL.createObjectURL(blob);
if (slot === 'front') this.existing[kind].previewUrl = url;
else this.existing[kind].previewUrlBack = url;
},
error: () => undefined
});
}
onAccountSelect(accountNo: string): void {
this.selectedAccount = this.allAccounts.find((a) => a.accountNo === accountNo) || null;
this.selectedAccountType = this.selectedAccount?.accountType || '';
}
getFile(documentType: string): File | null {
switch (documentType) {
case 'CLIENT_IMAGE':
return this.clientImageFile;
case 'CLIENT_SIGNATURE':
return this.clientSignatureFile;
default:
return null;
}
}
private static readonly CARD_TITLES: Record<CkycDocKind, string> = {
ADDRESS_PROOF: 'Address Proof Document',
PHOTO_PROOF: 'Photo Proof Document',
CLIENT_IMAGE: 'Image of Client',
CLIENT_SIGNATURE: 'Signature of Client'
};
/** Opens the picker/preview popup for one card and, once the user confirms a file there, feeds
* the picked type/file(s) into the same upload path the card always used. Picking and preview
* now live entirely in the dialog — this screen only stages the result and re-runs upload. */
openUploadDialog(kind: CkycDocKind): void {
const isProofKind = kind === 'ADDRESS_PROOF' || kind === 'PHOTO_PROOF';
const action = this.existing[kind].present ? 'Replace' : 'Upload';
const existingDates = isProofKind
? this.parseDescriptionDates(this.existing[kind].description)
: { issueDate: null, expiryDate: null };
const ref = this.dialog.open(CkycUploadDialogComponent, {
width: '480px',
maxWidth: '95vw',
data: {
title: `${action} ${CkycTabComponent.CARD_TITLES[kind]}`,
isProofKind,
accept: isProofKind ? '.png,.jpeg,.jpg,.pdf' : '.png,.jpeg,.jpg',
proofOptions:
kind === 'PHOTO_PROOF'
? this.idProofOptions
: kind === 'ADDRESS_PROOF'
? this.addressProofOptions
: undefined,
initialProofType: isProofKind ? this.selectedProofType[kind as ProofKind] : undefined,
initialDocumentKey: isProofKind ? this.existing[kind].documentKey || '' : undefined,
initialIssueDate: existingDates.issueDate,
initialExpiryDate: existingDates.expiryDate
}
});
ref.afterClosed().subscribe((result: CkycUploadDialogResult | null) => {
if (!result) return;
if (isProofKind) {
const proofKind = kind as ProofKind;
this.selectedProofType[proofKind] = result.proofType || null;
this.docFiles[proofKind] = { front: result.front, back: result.back || null };
this.documentDetails[proofKind] = {
documentKey: result.documentKey || '',
issueDate: result.issueDate || null,
expiryDate: result.expiryDate || null
};
} else if (kind === 'CLIENT_IMAGE') {
this.clientImageFile = result.front;
} else {
this.clientSignatureFile = result.front;
}
this.uploadDocument(kind);
});
}
/** Reads back the "Issue date: yyyy-MM-dd, Expiry date: yyyy-MM-dd" convention this tab (and
* client-india-address-step, for address proof) writes into the identifier's description. */
private parseDescriptionDates(description: string | undefined): { issueDate: Date | null; expiryDate: Date | null } {
const issueMatch = (description || '').match(/Issue date:\s*([^,|]+)/i);
const expiryMatch = (description || '').match(/Expiry date:\s*([^,|]+)/i);
return {
issueDate: issueMatch ? this.dates.parseDate(issueMatch[1].trim()) : null,
expiryDate: expiryMatch ? this.dates.parseDate(expiryMatch[1].trim()) : null
};
}
private buildDescription(issueDate: string | null, expiryDate: string | null): string {
const parts: string[] = [];
if (issueDate) parts.push(`Issue date: ${issueDate}`);
if (expiryDate) parts.push(`Expiry date: ${expiryDate}`);
return parts.join(', ');
}
/** Issue/expiry date for display on the card, formatted dd-MM-yyyy — parsed from the same
* description text the upload dialog writes. */
cardDate(kind: ProofKind, which: 'issue' | 'expiry'): string {
const parsed = this.parseDescriptionDates(this.existing[kind].description);
const value = which === 'issue' ? parsed.issueDate : parsed.expiryDate;
return value ? this.dates.formatDate(value, 'dd-MM-yyyy') : '';
}
/**
* Routes each card to whichever real endpoint that artefact is actually stored through — there
* is no single "/clients/{id}/ckyc/documents" resource on the backend, so posting every card to
* one made-up URL 404'd for all four. A photo goes to the image endpoint, a signature to the
* client's own documents, and an ID/address proof to the m_client_identifier it already belongs
* to (the same one the Identities tab and create-client stepper post to) — never a fresh,
* type-less upload, since which identifier a proof belongs to is exactly what makes it that
* proof rather than some other file on the client's record.
*/
uploadDocument(documentType: string): void {
if (documentType === 'PHOTO_PROOF' || documentType === 'ADDRESS_PROOF') {
this.uploadProofDocument(documentType);
return;
}
const file = this.getFile(documentType);
if (!file) return;
this.uploading[documentType] = true;
this.uploadErrors[documentType] = null;
const key = documentType as keyof typeof this.existing;
let request$: Observable<any>;
switch (documentType) {
case 'CLIENT_IMAGE':
request$ = this.clientsService.uploadClientProfileImage(this.clientId, file);
break;
case 'CLIENT_SIGNATURE':
request$ = this.clientsService.uploadClientSignatureImage(this.clientId, file);
break;
default:
return;
}
request$.pipe(takeUntil(this.destroy$)).subscribe({
next: () => {
this.uploading[documentType] = false;
this.alertService.alert({ type: 'success', message: 'Document uploaded successfully.' });
this.refreshPreview(documentType);
},
error: (err: any) => {
this.uploading[documentType] = false;
this.uploadErrors[documentType] = this.extractErrorMessage(err);
}
});
}
/**
* Proof cards work like the create-client KYC step: pick a document type first (its `sides`
* decides whether one image or a front+back pair is required), then upload. Reuses the same
* identifier the Identities tab manages — updates its documentTypeId if the picked type
* changed, or creates a fresh identifier when none exists yet, so this card never depends on
* a separate trip to the Identities tab.
*/
private uploadProofDocument(kind: ProofKind): void {
const proof = this.selectedProofType[kind];
if (!proof) {
this.uploadErrors[kind] = 'Select a document type first.';
return;
}
const files = this.docFiles[kind];
if (!files.front || (proof.sides === 2 && !files.back)) {
this.uploadErrors[kind] =
proof.sides === 2
? 'Both front and back images are required for this document type.'
: 'Select a file to upload.';
return;
}
this.uploading[kind] = true;
this.uploadErrors[kind] = null;
const existingId = this.existing[kind].documentId;
const details = this.documentDetails[kind];
const documentKey = details.documentKey || `${proof.name} - ${this.clientId}`;
const description = this.buildDescription(details.issueDate, details.expiryDate);
const identifier$: Observable<any> = !existingId
? this.clientsService.addClientIdentifier(this.clientId, {
documentTypeId: proof.id,
status: 'ACTIVE',
documentKey,
description: description || 'KYC document'
})
: this.clientsService.updateClientIdentifier(this.clientId, existingId, {
documentTypeId: proof.id,
status: 'ACTIVE',
documentKey,
description
});
identifier$.pipe(takeUntil(this.destroy$)).subscribe({
next: (res: any) => {
const identifierId = existingId || res?.resourceId;
const uploads: Observable<any>[] = [];
const front = new FormData();
front.append('name', proof.sides === 2 ? `${proof.name} — Front` : proof.name);
front.append('file', files.front!);
uploads.push(this.clientsService.uploadClientIdentifierDocument(identifierId, front));
if (proof.sides === 2 && files.back) {
const back = new FormData();
back.append('name', `${proof.name} — Back`);
back.append('file', files.back);
uploads.push(this.clientsService.uploadClientIdentifierDocument(identifierId, back));
}
forkJoin(uploads)
.pipe(takeUntil(this.destroy$))
.subscribe({
next: () => {
this.uploading[kind] = false;
this.existing[kind].present = true;
this.existing[kind].documentId = identifierId;
this.existing[kind].documentTypeId = String(proof.id);
this.existing[kind].documentKey = details.documentKey || '';
this.existing[kind].description = description;
this.existing[kind].name = proof.name;
this.existing[kind].source = this.existing[kind].source || 'Identities section';
this.docFiles[kind] = { front: null, back: null };
this.alertService.alert({ type: 'success', message: 'Document uploaded successfully.' });
this.refreshPreview(kind);
},
error: (err: any) => {
this.uploading[kind] = false;
this.uploadErrors[kind] = this.extractErrorMessage(err);
}
});
},
error: (err: any) => {
this.uploading[kind] = false;
this.uploadErrors[kind] = this.extractErrorMessage(err);
}
});
}
private extractErrorMessage(err: any): string {
return (
err?.error?.errors?.[0]?.defaultUserMessage ||
err?.error?.developerMessage ||
err?.error?.defaultUserMessage ||
err?.message ||
'Failed to upload document.'
);
}
/** Re-fetches whichever artefact was just replaced, so its preview reflects the new file
* immediately instead of the one this screen loaded on entry. */
private refreshPreview(documentType: string): void {
const staleKey = documentType as keyof typeof this.existing;
if (this.existing[staleKey].previewUrl?.startsWith('blob:')) {
URL.revokeObjectURL(this.existing[staleKey].previewUrl!);
}
if (this.existing[staleKey].previewUrlBack?.startsWith('blob:')) {
URL.revokeObjectURL(this.existing[staleKey].previewUrlBack!);
}
switch (documentType) {
case 'CLIENT_IMAGE':
this.clientsService
.getClientProfileImage(this.clientId)
.pipe(takeUntil(this.destroy$))
.subscribe((photo: any) => {
if (photo) this.existing.CLIENT_IMAGE.previewUrl = photo;
});
break;
case 'CLIENT_SIGNATURE':
this.clientsService
.getClientDocuments(this.clientId)
.pipe(takeUntil(this.destroy$))
.subscribe((documents: any) => {
const doc = (documents as any[])
?.filter((d: any) => d?.name === 'clientSignature')
?.sort((a: any, b: any) => b.id - a.id)?.[0];
if (!doc) return;
this.existing.CLIENT_SIGNATURE.documentId = doc.id;
this.clientsService
.getClientSignatureImage(this.clientId, doc.id)
.pipe(takeUntil(this.destroy$))
.subscribe((blob: any) => {
if (blob) this.existing.CLIENT_SIGNATURE.previewUrl = URL.createObjectURL(blob);
});
});
break;
case 'PHOTO_PROOF':
case 'ADDRESS_PROOF':
this.loadIdentifierDocPreview(documentType, this.existing[documentType].documentId!);
break;
}
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
// Release object URLs
Object.values(this.existing).forEach((e) => {
if (e.previewUrl?.startsWith('blob:')) URL.revokeObjectURL(e.previewUrl);
if (e.previewUrlBack?.startsWith('blob:')) URL.revokeObjectURL(e.previewUrlBack);
});
}
}