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>
This commit is contained in:
@@ -9,27 +9,38 @@
|
||||
/** Angular Imports */
|
||||
import { Component, OnInit, OnDestroy, inject } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { Subject, takeUntil, catchError, of, forkJoin } from 'rxjs';
|
||||
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;
|
||||
expanded?: boolean;
|
||||
}
|
||||
|
||||
type ProofKind = 'ADDRESS_PROOF' | 'PHOTO_PROOF';
|
||||
type CkycDocKind = ProofKind | 'CLIENT_IMAGE' | 'CLIENT_SIGNATURE';
|
||||
|
||||
@Component({
|
||||
selector: 'mifosx-ckyc-tab',
|
||||
templateUrl: './ckyc-tab.component.html',
|
||||
@@ -46,6 +57,8 @@ 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>();
|
||||
|
||||
@@ -80,16 +93,41 @@ export class CkycTabComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
/** Pre-existing artefacts detected from client data */
|
||||
existing: Record<'ADDRESS_PROOF' | 'PHOTO_PROOF' | 'CLIENT_IMAGE' | 'CLIENT_SIGNATURE', ExistingArtefact> = {
|
||||
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 */
|
||||
addressProofFile: File | null = null;
|
||||
photoProofFile: File | null = null;
|
||||
clientImageFile: File | null = null;
|
||||
clientSignatureFile: File | null = null;
|
||||
|
||||
@@ -101,9 +139,6 @@ export class CkycTabComponent implements OnInit, OnDestroy {
|
||||
CLIENT_SIGNATURE: false
|
||||
};
|
||||
|
||||
/** Per-document upload response */
|
||||
uploadResponses: { [key: string]: any } = {};
|
||||
|
||||
/** Per-document error messages */
|
||||
uploadErrors: { [key: string]: string | null } = {
|
||||
ADDRESS_PROOF: null,
|
||||
@@ -262,6 +297,8 @@ export class CkycTabComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
// 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)));
|
||||
|
||||
@@ -287,21 +324,77 @@ export class CkycTabComponent implements OnInit, OnDestroy {
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
toggleReplace(documentType: string): void {
|
||||
const entry = (this.existing as any)[documentType];
|
||||
if (entry) entry.expanded = !entry.expanded;
|
||||
/**
|
||||
* 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 {
|
||||
@@ -309,33 +402,8 @@ export class CkycTabComponent implements OnInit, OnDestroy {
|
||||
this.selectedAccountType = this.selectedAccount?.accountType || '';
|
||||
}
|
||||
|
||||
onFileSelect(event: any, documentType: string): void {
|
||||
const file = event.target?.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
switch (documentType) {
|
||||
case 'ADDRESS_PROOF':
|
||||
this.addressProofFile = file;
|
||||
break;
|
||||
case 'PHOTO_PROOF':
|
||||
this.photoProofFile = file;
|
||||
break;
|
||||
case 'CLIENT_IMAGE':
|
||||
this.clientImageFile = file;
|
||||
break;
|
||||
case 'CLIENT_SIGNATURE':
|
||||
this.clientSignatureFile = file;
|
||||
break;
|
||||
}
|
||||
this.uploadErrors[documentType] = null;
|
||||
}
|
||||
|
||||
getFile(documentType: string): File | null {
|
||||
switch (documentType) {
|
||||
case 'ADDRESS_PROOF':
|
||||
return this.addressProofFile;
|
||||
case 'PHOTO_PROOF':
|
||||
return this.photoProofFile;
|
||||
case 'CLIENT_IMAGE':
|
||||
return this.clientImageFile;
|
||||
case 'CLIENT_SIGNATURE':
|
||||
@@ -345,38 +413,278 @@ export class CkycTabComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('documentType', documentType);
|
||||
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;
|
||||
}
|
||||
|
||||
this.clientsService
|
||||
.uploadCkycDocument(this.clientId, formData)
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe({
|
||||
next: (response: any) => {
|
||||
this.uploading[documentType] = false;
|
||||
this.uploadResponses[documentType] = response;
|
||||
this.alertService.alert({
|
||||
type: 'success',
|
||||
message: response?.message || 'Document uploaded successfully.'
|
||||
});
|
||||
},
|
||||
error: (err: any) => {
|
||||
this.uploading[documentType] = false;
|
||||
this.uploadErrors[documentType] =
|
||||
err?.error?.developerMessage ||
|
||||
err?.error?.defaultUserMessage ||
|
||||
err?.message ||
|
||||
'Failed to upload document.';
|
||||
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 {
|
||||
@@ -385,6 +693,7 @@ export class CkycTabComponent implements OnInit, OnDestroy {
|
||||
// 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user