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:
+44
-12
@@ -221,6 +221,22 @@
|
||||
<mat-datepicker #addrIssueDatePicker></mat-datepicker>
|
||||
</mat-form-field>
|
||||
}
|
||||
<mat-form-field appearance="outline" class="addr-ff">
|
||||
<mat-label>Expiry date (optional)</mat-label>
|
||||
<span matPrefix class="addr-prefix"><mat-icon>event_busy</mat-icon></span>
|
||||
<input
|
||||
matInput
|
||||
[matDatepicker]="addrExpiryDatePicker"
|
||||
formControlName="expiryDate"
|
||||
[readonly]="readOnly || isAddressProofLinkedToKyc"
|
||||
/>
|
||||
<mat-datepicker-toggle
|
||||
matSuffix
|
||||
[for]="addrExpiryDatePicker"
|
||||
[disabled]="readOnly || isAddressProofLinkedToKyc"
|
||||
></mat-datepicker-toggle>
|
||||
<mat-datepicker #addrExpiryDatePicker></mat-datepicker>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="addr-upload-grid" [class.addr-upload-grid--two]="selectedAddrProof.sides === 2">
|
||||
@@ -238,14 +254,18 @@
|
||||
@if (existingAddressProofDocuments.length > 0 && !addrFiles.front && readOnly) {
|
||||
<div class="addr-dropzone addr-dropzone--has addr-dropzone--locked">
|
||||
<div class="addr-dropzone__existing">
|
||||
@if (addrMergedPreview) {
|
||||
<img [src]="addrMergedPreview" alt="Saved address proof" class="addr-dropzone__thumb" />
|
||||
@if (existingAddrFrontSlotPreview) {
|
||||
<img
|
||||
[src]="existingAddrFrontSlotPreview"
|
||||
alt="Saved address proof"
|
||||
class="addr-dropzone__thumb"
|
||||
/>
|
||||
} @else {
|
||||
<mat-icon class="addr-dropzone__doc-ic">description</mat-icon>
|
||||
}
|
||||
<div class="addr-dropzone__existing-meta">
|
||||
<span class="addr-dropzone__existing-name">
|
||||
{{ existingAddressProofDocuments[0].fileName || existingAddressProofDocuments[0].name }}
|
||||
{{ existingAddrFrontSlotName }}
|
||||
</span>
|
||||
<button type="button" class="addr-dropzone__view" (click)="viewExistingAddressProof()">
|
||||
<mat-icon>visibility</mat-icon>
|
||||
@@ -278,14 +298,18 @@
|
||||
<!-- Edit mode WITH existing doc — let the user view it OR
|
||||
click anywhere to replace. -->
|
||||
<div class="addr-dropzone__existing">
|
||||
@if (addrMergedPreview) {
|
||||
<img [src]="addrMergedPreview" alt="Saved address proof" class="addr-dropzone__thumb" />
|
||||
@if (existingAddrFrontSlotPreview) {
|
||||
<img
|
||||
[src]="existingAddrFrontSlotPreview"
|
||||
alt="Saved address proof"
|
||||
class="addr-dropzone__thumb"
|
||||
/>
|
||||
} @else {
|
||||
<mat-icon class="addr-dropzone__doc-ic">description</mat-icon>
|
||||
}
|
||||
<div class="addr-dropzone__existing-meta">
|
||||
<span class="addr-dropzone__existing-name">
|
||||
{{ existingAddressProofDocuments[0].fileName || existingAddressProofDocuments[0].name }}
|
||||
{{ existingAddrFrontSlotName }}
|
||||
</span>
|
||||
<div class="addr-dropzone__existing-actions">
|
||||
<button
|
||||
@@ -327,14 +351,18 @@
|
||||
@if (existingAddressProofDocuments.length > 0 && !addrFiles.back && readOnly) {
|
||||
<div class="addr-dropzone addr-dropzone--has addr-dropzone--locked">
|
||||
<div class="addr-dropzone__existing">
|
||||
@if (addrMergedPreview) {
|
||||
<img [src]="addrMergedPreview" alt="Saved address proof" class="addr-dropzone__thumb" />
|
||||
@if (existingAddrBackSlotPreview) {
|
||||
<img
|
||||
[src]="existingAddrBackSlotPreview"
|
||||
alt="Saved address proof"
|
||||
class="addr-dropzone__thumb"
|
||||
/>
|
||||
} @else {
|
||||
<mat-icon class="addr-dropzone__doc-ic">description</mat-icon>
|
||||
}
|
||||
<div class="addr-dropzone__existing-meta">
|
||||
<span class="addr-dropzone__existing-name">
|
||||
{{ existingAddressProofDocuments[0].fileName || existingAddressProofDocuments[0].name }}
|
||||
{{ existingAddrBackSlotName }}
|
||||
</span>
|
||||
<button type="button" class="addr-dropzone__view" (click)="viewExistingAddressProof()">
|
||||
<mat-icon>visibility</mat-icon>
|
||||
@@ -367,14 +395,18 @@
|
||||
<!-- Edit mode WITH existing combined doc — same view + replace
|
||||
pattern as the front slot, anchored to the saved file. -->
|
||||
<div class="addr-dropzone__existing">
|
||||
@if (addrMergedPreview) {
|
||||
<img [src]="addrMergedPreview" alt="Saved address proof" class="addr-dropzone__thumb" />
|
||||
@if (existingAddrBackSlotPreview) {
|
||||
<img
|
||||
[src]="existingAddrBackSlotPreview"
|
||||
alt="Saved address proof"
|
||||
class="addr-dropzone__thumb"
|
||||
/>
|
||||
} @else {
|
||||
<mat-icon class="addr-dropzone__doc-ic">description</mat-icon>
|
||||
}
|
||||
<div class="addr-dropzone__existing-meta">
|
||||
<span class="addr-dropzone__existing-name">
|
||||
{{ existingAddressProofDocuments[0].fileName || existingAddressProofDocuments[0].name }}
|
||||
{{ existingAddrBackSlotName }}
|
||||
</span>
|
||||
<div class="addr-dropzone__existing-actions">
|
||||
<button
|
||||
|
||||
+103
-11
@@ -102,7 +102,8 @@ export class ClientIndiaAddressStepComponent implements OnInit, OnChanges {
|
||||
Validators.required
|
||||
],
|
||||
documentKey: [''],
|
||||
issueDate: ['']
|
||||
issueDate: [''],
|
||||
expiryDate: ['']
|
||||
});
|
||||
|
||||
currentForm: UntypedFormGroup = this.fb.group({
|
||||
@@ -222,6 +223,12 @@ export class ClientIndiaAddressStepComponent implements OnInit, OnChanges {
|
||||
size: number;
|
||||
previewUrl: string | null;
|
||||
}[] = [];
|
||||
/** Set only when the saved documents are separately-uploaded "<Proof> — Front"/"— Back"
|
||||
* files (the CKYC tab / client-kyc-step convention) rather than this component's own single
|
||||
* merged-PNG upload — lets the two slots show their own file instead of both showing
|
||||
* whichever one happened to be first. */
|
||||
private existingAddrFrontPreview: string | null = null;
|
||||
private existingAddrBackPreview: string | null = null;
|
||||
|
||||
residenceTypes = [
|
||||
'Owned',
|
||||
@@ -307,11 +314,10 @@ export class ClientIndiaAddressStepComponent implements OnInit, OnChanges {
|
||||
this.existingAddressProofDocuments = [];
|
||||
if (this.existingKycDocSides.front) this.existingAddressProofDocuments.push(this.existingKycDocSides.front);
|
||||
if (this.existingKycDocSides.back) this.existingAddressProofDocuments.push(this.existingKycDocSides.back);
|
||||
if (this.existingKycDocSides.front?.previewUrl) {
|
||||
this.addrMergedPreview = this.existingKycDocSides.front.previewUrl;
|
||||
} else if (this.existingKycDocSides.back?.previewUrl) {
|
||||
this.addrMergedPreview = this.existingKycDocSides.back.previewUrl;
|
||||
}
|
||||
// These already carry their own per-side preview from the KYC step's own mapping —
|
||||
// keep the two slots distinct rather than collapsing them into one shared preview.
|
||||
this.existingAddrFrontPreview = this.existingKycDocSides.front?.previewUrl || null;
|
||||
this.existingAddrBackPreview = this.existingKycDocSides.back?.previewUrl || null;
|
||||
this.existingAddressProofIdentifierId = this.existingKycIdentifierId;
|
||||
}
|
||||
} else if (this.isAddressProofLinkedToKyc) {
|
||||
@@ -327,6 +333,8 @@ export class ClientIndiaAddressStepComponent implements OnInit, OnChanges {
|
||||
this.currentForm.patchValue({ documentKey: '', issueDate: null });
|
||||
this.addrFiles = { front: null, back: null };
|
||||
this.addrMergedPreview = null;
|
||||
this.existingAddrFrontPreview = null;
|
||||
this.existingAddrBackPreview = null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -356,6 +364,8 @@ export class ClientIndiaAddressStepComponent implements OnInit, OnChanges {
|
||||
this.addrFiles = { front: null, back: null };
|
||||
this.addrMergedFile = null;
|
||||
this.addrMergedPreview = null;
|
||||
this.existingAddrFrontPreview = null;
|
||||
this.existingAddrBackPreview = null;
|
||||
this.existingAddressProofIdentifierId = null;
|
||||
this.existingAddressProofDocuments = [];
|
||||
this.addressLoaded = false;
|
||||
@@ -488,6 +498,7 @@ export class ClientIndiaAddressStepComponent implements OnInit, OnChanges {
|
||||
proofId: any;
|
||||
documentKey: string;
|
||||
issueDate: string;
|
||||
expiryDate: string;
|
||||
files: { slot: string; file: File }[];
|
||||
} {
|
||||
const files: { slot: string; file: File }[] = [];
|
||||
@@ -500,11 +511,13 @@ export class ClientIndiaAddressStepComponent implements OnInit, OnChanges {
|
||||
}
|
||||
}
|
||||
const rawIssueDate = this.permanentForm.get('issueDate')?.value;
|
||||
const rawExpiryDate = this.permanentForm.get('expiryDate')?.value;
|
||||
return {
|
||||
proof: this.selectedAddrProof,
|
||||
proofId: this.permanentForm.get('proofId')?.value || '',
|
||||
documentKey: this.permanentForm.get('documentKey')?.value || '',
|
||||
issueDate: rawIssueDate ? this.dates.getDate(rawIssueDate) : '',
|
||||
expiryDate: rawExpiryDate ? this.dates.getDate(rawExpiryDate) : '',
|
||||
files
|
||||
};
|
||||
}
|
||||
@@ -644,7 +657,14 @@ export class ClientIndiaAddressStepComponent implements OnInit, OnChanges {
|
||||
this.tryLinkKycData();
|
||||
return;
|
||||
}
|
||||
const identifier = identifiers.find((i: any) => this.findMatchingProof(i?.documentType?.name));
|
||||
// Aadhaar, Passport, Voter ID and Driving Licence are valid OVDs for both this slot and
|
||||
// the separate identity-proof one (client-kyc-step) — when a client has two such
|
||||
// dual-purpose identifiers, that step always runs first and picks the OLDEST match (the
|
||||
// one on file first, historically the identity proof), so picking the NEWEST match here
|
||||
// lands this step on the other row without the two steps needing to coordinate.
|
||||
const identifier = [...identifiers]
|
||||
.sort((a: any, b: any) => (b?.id ?? 0) - (a?.id ?? 0))
|
||||
.find((i: any) => this.findMatchingProof(i?.documentType?.name));
|
||||
if (!identifier?.id) {
|
||||
this.existingAddressProofCheckComplete = true;
|
||||
this.tryLinkKycData();
|
||||
@@ -664,14 +684,19 @@ export class ClientIndiaAddressStepComponent implements OnInit, OnChanges {
|
||||
proofId: matchedProof.id,
|
||||
documentKey: identifier.documentKey || ''
|
||||
});
|
||||
// The description carries `Issue date: yyyy-MM-dd` when set on create (see
|
||||
// create-client.saveAddressProof) — pull it out so the form's issueDate field
|
||||
// shows what was previously entered.
|
||||
// The description carries "Issue date: yyyy-MM-dd, Expiry date: yyyy-MM-dd" when set on
|
||||
// create (see create-client.saveAddressProof / CkycTabComponent) — pull both out so the
|
||||
// form's date fields show what was previously entered. Each match stops at the next
|
||||
// comma so adding expiry after issue doesn't get swallowed into the issue-date capture.
|
||||
const desc: string = identifier.description || '';
|
||||
const issueMatch = desc.match(/Issue date:\s*(.+)$/i);
|
||||
const issueMatch = desc.match(/Issue date:\s*([^,|]+)/i);
|
||||
const expiryMatch = desc.match(/Expiry date:\s*([^,|]+)/i);
|
||||
if (issueMatch) {
|
||||
this.permanentForm.patchValue({ issueDate: this.dates.parseDate(issueMatch[1].trim()) });
|
||||
}
|
||||
if (expiryMatch) {
|
||||
this.permanentForm.patchValue({ expiryDate: this.dates.parseDate(expiryMatch[1].trim()) });
|
||||
}
|
||||
}
|
||||
|
||||
this.clientsService.getClientIdentificationDocuments(this.existingAddressProofIdentifierId).subscribe({
|
||||
@@ -685,6 +710,29 @@ export class ClientIndiaAddressStepComponent implements OnInit, OnChanges {
|
||||
size: d.size || 0,
|
||||
previewUrl: null as string | null
|
||||
}));
|
||||
|
||||
const sideOf = (d: { name: string; fileName: string }): 'front' | 'back' | null => {
|
||||
const text = `${d.name || ''} ${d.fileName || ''}`.toLowerCase();
|
||||
if (text.includes('back')) return 'back';
|
||||
if (text.includes('front')) return 'front';
|
||||
return null;
|
||||
};
|
||||
// Changing the document type (e.g. Aadhaar -> Passport) keeps the old front/back
|
||||
// files attached to this same identifier rather than removing them, so there can be
|
||||
// more than one "— Front"/"— Back" pair on file — sort newest-first before matching
|
||||
// or this would keep showing the very first scan ever uploaded here.
|
||||
const byNewest = [...this.existingAddressProofDocuments].sort((a, b) => Number(b.id) - Number(a.id));
|
||||
const frontDoc = byNewest.find((d) => sideOf(d) === 'front');
|
||||
const backDoc = byNewest.find((d) => sideOf(d) === 'back');
|
||||
|
||||
if (frontDoc || backDoc) {
|
||||
// Separately-uploaded front/back — each side gets its own preview.
|
||||
if (frontDoc) this.downloadAddrSidePreview(frontDoc, 'front');
|
||||
if (backDoc) this.downloadAddrSidePreview(backDoc, 'back');
|
||||
return;
|
||||
}
|
||||
|
||||
// Legacy/single-file case: one combined (or unlabelled) document covers both slots.
|
||||
const firstImage = this.existingAddressProofDocuments.find((d) =>
|
||||
(d.type || '').toLowerCase().startsWith('image/')
|
||||
);
|
||||
@@ -715,6 +763,50 @@ export class ClientIndiaAddressStepComponent implements OnInit, OnChanges {
|
||||
});
|
||||
}
|
||||
|
||||
private downloadAddrSidePreview(doc: { id: string; fileName: string; name: string }, slot: 'front' | 'back'): void {
|
||||
this.clientsService.downloadClientIdentificationDocument(this.existingAddressProofIdentifierId!, doc.id).subscribe({
|
||||
next: async (blob: any) => {
|
||||
const buffer = await blob.arrayBuffer();
|
||||
const bytes = new Uint8Array(buffer);
|
||||
let binary = '';
|
||||
for (let i = 0; i < bytes.length; i++) {
|
||||
binary += String.fromCharCode(bytes[i]);
|
||||
}
|
||||
const url = 'data:' + blob.type + ';base64,' + btoa(binary);
|
||||
if (slot === 'front') this.existingAddrFrontPreview = url;
|
||||
else this.existingAddrBackPreview = url;
|
||||
},
|
||||
error: () => {}
|
||||
});
|
||||
}
|
||||
|
||||
/** What the Front slot should show: its own saved file when front/back were uploaded
|
||||
* separately, falling back to the single combined preview for legacy/merged uploads. */
|
||||
get existingAddrFrontSlotPreview(): string | null {
|
||||
return this.existingAddrFrontPreview || this.addrMergedPreview;
|
||||
}
|
||||
|
||||
get existingAddrFrontSlotName(): string {
|
||||
const front = this.existingAddressProofDocuments.find((d) =>
|
||||
`${d.name} ${d.fileName}`.toLowerCase().includes('front')
|
||||
);
|
||||
const doc = front || this.existingAddressProofDocuments[0];
|
||||
return doc?.fileName || doc?.name || '';
|
||||
}
|
||||
|
||||
/** Same as above, for the Back slot. */
|
||||
get existingAddrBackSlotPreview(): string | null {
|
||||
return this.existingAddrBackPreview || this.addrMergedPreview;
|
||||
}
|
||||
|
||||
get existingAddrBackSlotName(): string {
|
||||
const back = this.existingAddressProofDocuments.find((d) =>
|
||||
`${d.name} ${d.fileName}`.toLowerCase().includes('back')
|
||||
);
|
||||
const doc = back || this.existingAddressProofDocuments[0];
|
||||
return doc?.fileName || doc?.name || '';
|
||||
}
|
||||
|
||||
onPinInput(tab: string) {
|
||||
const form = this.getForm(tab);
|
||||
const pin = form.get('pinCode')?.value?.replace(/\D/g, '') || '';
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
*/
|
||||
|
||||
import { Component, Input, OnInit, OnChanges, OnDestroy, SimpleChanges, inject } from '@angular/core';
|
||||
import { forkJoin, catchError, of } from 'rxjs';
|
||||
import { UntypedFormBuilder, UntypedFormGroup, Validators, FormsModule } from '@angular/forms';
|
||||
import {
|
||||
MatDialog,
|
||||
@@ -1266,8 +1267,14 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
||||
this.loadExistingKycIdentifier(data?.kycDocumentName);
|
||||
},
|
||||
error: () => {
|
||||
// No existing KYC — that's fine for new clients
|
||||
// No m_client_kyc row yet (e.g. CERSAI sync hasn't run) — that's fine, but the client
|
||||
// can still have a KYC-type identifier already on file (uploaded via the CKYC tab,
|
||||
// which writes straight to client_identifier and never touches this record). Look it
|
||||
// up the same way the success branch does, just without a document-name hint.
|
||||
this.kycLoaded = true;
|
||||
this.loadExistingPhoto();
|
||||
this.loadExistingSignature();
|
||||
this.loadExistingKycIdentifier(undefined);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1283,8 +1290,13 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
||||
*/
|
||||
private loadExistingKycIdentifier(kycDocumentName: string | undefined): void {
|
||||
if (!this.clientId) return;
|
||||
this.clientsService.getClientIdentifiers(this.clientId).subscribe({
|
||||
next: (identifiers: any[]) => {
|
||||
forkJoin({
|
||||
identifiers: this.clientsService.getClientIdentifiers(this.clientId),
|
||||
// Only needed to spot the dual-purpose OVD types (Aadhaar/Passport/Voter ID/Driving
|
||||
// Licence) below — a failure here just means that disambiguation step is skipped.
|
||||
proofTypes: this.clientsService.getKycProofTypes().pipe(catchError(() => of({ addressProofs: [] })))
|
||||
}).subscribe({
|
||||
next: ({ identifiers, proofTypes }: { identifiers: any[]; proofTypes: any }) => {
|
||||
if (!identifiers || identifiers.length === 0) return;
|
||||
const cleanStr = (s: string) => (s || '').toLowerCase().replace(/[^a-z0-9]/g, '');
|
||||
const isKycType = (i: any) => {
|
||||
@@ -1297,6 +1309,13 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
||||
})
|
||||
);
|
||||
};
|
||||
// Aadhaar, Passport, Voter ID and Driving Licence are valid OVDs for BOTH this slot and
|
||||
// the separate address-proof one (client-india-address-step) — a client with, say, a PAN
|
||||
// identity proof and an Aadhaar address proof has two client_identifier rows that both
|
||||
// pass isKycType. Without a name to go on, prefer whichever candidate is NOT also an
|
||||
// address-proof type, so this slot doesn't grab the other step's file.
|
||||
const addressTypeNames = new Set<string>((proofTypes?.addressProofs || []).map((p: any) => cleanStr(p?.name)));
|
||||
const isAddressType = (i: any) => addressTypeNames.has(cleanStr(i?.documentType?.name));
|
||||
// Re-uploading a KYC document creates a new identifier row rather than updating the
|
||||
// old one in place (same pattern as OVD identifiers), so sort newest-first before
|
||||
// picking — otherwise a repeat upload of the same document type would keep showing
|
||||
@@ -1311,7 +1330,15 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
||||
return name === kName || name.includes(kName) || kName.includes(name);
|
||||
})
|
||||
: null;
|
||||
const identifier = match || sorted.find(isKycType);
|
||||
// Last resort, when EVERY kyc-type candidate is also address-eligible (e.g. a client
|
||||
// whose identity proof and address proof are both Aadhaar/Passport/Voter ID/Driving
|
||||
// Licence): this stepper always runs before the address step in both the create and
|
||||
// edit wizards, so the identity proof is the one that was on file first — pick the
|
||||
// OLDEST match here, and the address step (client-india-address-step) picks the NEWEST
|
||||
// among ITS own address-eligible candidates, so the two steps land on different rows
|
||||
// without needing to coordinate at runtime.
|
||||
const identifier =
|
||||
match || sorted.find((i) => isKycType(i) && !isAddressType(i)) || [...sorted].reverse().find(isKycType);
|
||||
if (!identifier?.id) return;
|
||||
|
||||
this.existingKycIdentifierId = identifier.id.toString();
|
||||
@@ -1327,6 +1354,19 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
||||
kycDocumentNumber: identifier.documentKey || ''
|
||||
});
|
||||
}
|
||||
// Same for issue/expiry date — the CKYC tab (and client-india-address-step, for address
|
||||
// proof) write "Issue date: yyyy-MM-dd, Expiry date: yyyy-MM-dd" into the identifier's
|
||||
// description; read it back here only to fill in whatever the KYC record itself doesn't
|
||||
// already have, same as the documentKey backfill above.
|
||||
const desc: string = identifier?.description || '';
|
||||
const issueMatch = desc.match(/Issue date:\s*([^,|]+)/i);
|
||||
const expiryMatch = desc.match(/Expiry date:\s*([^,|]+)/i);
|
||||
if (issueMatch && !this.kycForm.get('kycDocIssueDate')?.value) {
|
||||
this.kycForm.patchValue({ kycDocIssueDate: this.parseApiDate(issueMatch[1].trim()) });
|
||||
}
|
||||
if (expiryMatch && !this.kycForm.get('kycDocExpiryDate')?.value) {
|
||||
this.kycForm.patchValue({ kycDocExpiryDate: this.parseApiDate(expiryMatch[1].trim()) });
|
||||
}
|
||||
|
||||
this.clientsService.getClientIdentificationDocuments(this.existingKycIdentifierId).subscribe({
|
||||
next: (docs: any[]) => {
|
||||
|
||||
@@ -204,6 +204,24 @@
|
||||
<span class="ckyc-existing__source">from {{ existing.ADDRESS_PROOF.source }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@if (existing.ADDRESS_PROOF.previewUrl || existing.ADDRESS_PROOF.previewUrlBack) {
|
||||
<div class="ckyc-existing__thumbs">
|
||||
@if (existing.ADDRESS_PROOF.previewUrl) {
|
||||
<img
|
||||
[src]="existing.ADDRESS_PROOF.previewUrl"
|
||||
alt="Address proof scan - front"
|
||||
class="ckyc-existing__thumb"
|
||||
/>
|
||||
}
|
||||
@if (existing.ADDRESS_PROOF.previewUrlBack) {
|
||||
<img
|
||||
[src]="existing.ADDRESS_PROOF.previewUrlBack"
|
||||
alt="Address proof scan - back"
|
||||
class="ckyc-existing__thumb"
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<div class="ckyc-existing__meta">
|
||||
<fa-icon icon="file-alt"></fa-icon>
|
||||
<span>{{ existing.ADDRESS_PROOF.name }}</span>
|
||||
@@ -217,79 +235,52 @@
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
@if (cardDate('ADDRESS_PROOF', 'issue') || cardDate('ADDRESS_PROOF', 'expiry')) {
|
||||
<div class="ckyc-existing__dates">
|
||||
@if (cardDate('ADDRESS_PROOF', 'issue')) {
|
||||
<span
|
||||
><fa-icon icon="calendar-check"></fa-icon> Issued {{ cardDate('ADDRESS_PROOF', 'issue') }}</span
|
||||
>
|
||||
}
|
||||
@if (cardDate('ADDRESS_PROOF', 'expiry')) {
|
||||
<span
|
||||
><fa-icon icon="calendar-check"></fa-icon> Expires {{ cardDate('ADDRESS_PROOF', 'expiry') }}</span
|
||||
>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<button
|
||||
mat-stroked-button
|
||||
type="button"
|
||||
class="ckyc-existing__replace"
|
||||
(click)="toggleReplace('ADDRESS_PROOF')"
|
||||
(click)="openUploadDialog('ADDRESS_PROOF')"
|
||||
>
|
||||
<fa-icon [icon]="existing.ADDRESS_PROOF.expanded ? 'times' : 'upload'"></fa-icon>
|
||||
{{ existing.ADDRESS_PROOF.expanded ? 'Cancel replace' : 'Upload new version' }}
|
||||
<fa-icon icon="upload"></fa-icon>
|
||||
Upload new version
|
||||
</button>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="ckyc-missing">
|
||||
<p class="ckyc-upload-hint">Not uploaded yet. Accepted formats: PNG, JPEG, JPG, PDF</p>
|
||||
<button mat-raised-button color="primary" type="button" (click)="openUploadDialog('ADDRESS_PROOF')">
|
||||
<fa-icon icon="upload"></fa-icon>
|
||||
{{ 'labels.buttons.Upload' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!existing.ADDRESS_PROOF.present || existing.ADDRESS_PROOF.expanded) {
|
||||
<p class="ckyc-upload-hint">{{ 'labels.inputs.Accepted formats' | translate }}: PNG, JPEG, JPG, PDF</p>
|
||||
<div class="ckyc-file-row">
|
||||
<input
|
||||
type="file"
|
||||
#addressProofInput
|
||||
accept=".png,.jpeg,.jpg,.pdf"
|
||||
(change)="onFileSelect($event, 'ADDRESS_PROOF')"
|
||||
[style.display]="'none'"
|
||||
/>
|
||||
<mat-form-field appearance="outline" class="ckyc-file-name-field">
|
||||
<input
|
||||
matInput
|
||||
[readonly]="true"
|
||||
[value]="addressProofFile?.name || ''"
|
||||
[placeholder]="'labels.text.NoFileSelected' | translate"
|
||||
/>
|
||||
</mat-form-field>
|
||||
<button mat-stroked-button type="button" (click)="addressProofInput.click()">
|
||||
<fa-icon icon="folder-open" size="lg"></fa-icon>
|
||||
{{ 'labels.buttons.Browse' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
mat-raised-button
|
||||
color="primary"
|
||||
type="button"
|
||||
[disabled]="!addressProofFile || uploading['ADDRESS_PROOF']"
|
||||
(click)="uploadDocument('ADDRESS_PROOF')"
|
||||
>
|
||||
@if (uploading['ADDRESS_PROOF']) {
|
||||
<div class="ckyc-uploading">
|
||||
<mat-progress-spinner diameter="18" mode="indeterminate"></mat-progress-spinner>
|
||||
} @else {
|
||||
{{ 'labels.buttons.Upload' | translate }}
|
||||
<span>Uploading…</span>
|
||||
</div>
|
||||
}
|
||||
</button>
|
||||
@if (uploadErrors['ADDRESS_PROOF']) {
|
||||
<div class="ckyc-upload-error">
|
||||
<mat-icon>error_outline</mat-icon>
|
||||
<span>{{ uploadErrors['ADDRESS_PROOF'] }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (uploadResponses['ADDRESS_PROOF']) {
|
||||
<div class="ckyc-upload-response">
|
||||
<p>
|
||||
<b>{{ 'labels.inputs.Status' | translate }}:</b> {{ uploadResponses['ADDRESS_PROOF'].status }}
|
||||
</p>
|
||||
<p>
|
||||
<b>{{ 'labels.inputs.Request ID' | translate }}:</b> {{ uploadResponses['ADDRESS_PROOF'].requestId }}
|
||||
</p>
|
||||
<p>
|
||||
<b>{{ 'labels.inputs.Acknowledgement Number' | translate }}:</b>
|
||||
{{ uploadResponses['ADDRESS_PROOF'].acknowledgementNumber }}
|
||||
</p>
|
||||
<p>
|
||||
<b>{{ 'labels.inputs.Processing Status' | translate }}:</b>
|
||||
{{ uploadResponses['ADDRESS_PROOF'].processingStatus }}
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
|
||||
@@ -310,6 +301,24 @@
|
||||
<span class="ckyc-existing__source">from {{ existing.PHOTO_PROOF.source }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@if (existing.PHOTO_PROOF.previewUrl || existing.PHOTO_PROOF.previewUrlBack) {
|
||||
<div class="ckyc-existing__thumbs">
|
||||
@if (existing.PHOTO_PROOF.previewUrl) {
|
||||
<img
|
||||
[src]="existing.PHOTO_PROOF.previewUrl"
|
||||
alt="ID proof scan - front"
|
||||
class="ckyc-existing__thumb"
|
||||
/>
|
||||
}
|
||||
@if (existing.PHOTO_PROOF.previewUrlBack) {
|
||||
<img
|
||||
[src]="existing.PHOTO_PROOF.previewUrlBack"
|
||||
alt="ID proof scan - back"
|
||||
class="ckyc-existing__thumb"
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<div class="ckyc-existing__meta">
|
||||
<fa-icon icon="id-card"></fa-icon>
|
||||
<span>{{ existing.PHOTO_PROOF.name }}</span>
|
||||
@@ -322,79 +331,50 @@
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
@if (cardDate('PHOTO_PROOF', 'issue') || cardDate('PHOTO_PROOF', 'expiry')) {
|
||||
<div class="ckyc-existing__dates">
|
||||
@if (cardDate('PHOTO_PROOF', 'issue')) {
|
||||
<span><fa-icon icon="calendar-check"></fa-icon> Issued {{ cardDate('PHOTO_PROOF', 'issue') }}</span>
|
||||
}
|
||||
@if (cardDate('PHOTO_PROOF', 'expiry')) {
|
||||
<span
|
||||
><fa-icon icon="calendar-check"></fa-icon> Expires {{ cardDate('PHOTO_PROOF', 'expiry') }}</span
|
||||
>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<button
|
||||
mat-stroked-button
|
||||
type="button"
|
||||
class="ckyc-existing__replace"
|
||||
(click)="toggleReplace('PHOTO_PROOF')"
|
||||
(click)="openUploadDialog('PHOTO_PROOF')"
|
||||
>
|
||||
<fa-icon [icon]="existing.PHOTO_PROOF.expanded ? 'times' : 'upload'"></fa-icon>
|
||||
{{ existing.PHOTO_PROOF.expanded ? 'Cancel replace' : 'Upload new version' }}
|
||||
<fa-icon icon="upload"></fa-icon>
|
||||
Upload new version
|
||||
</button>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="ckyc-missing">
|
||||
<p class="ckyc-upload-hint">Not uploaded yet. Accepted formats: PNG, JPEG, JPG, PDF</p>
|
||||
<button mat-raised-button color="primary" type="button" (click)="openUploadDialog('PHOTO_PROOF')">
|
||||
<fa-icon icon="upload"></fa-icon>
|
||||
{{ 'labels.buttons.Upload' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!existing.PHOTO_PROOF.present || existing.PHOTO_PROOF.expanded) {
|
||||
<p class="ckyc-upload-hint">{{ 'labels.inputs.Accepted formats' | translate }}: PNG, JPEG, JPG, PDF</p>
|
||||
<div class="ckyc-file-row">
|
||||
<input
|
||||
type="file"
|
||||
#photoProofInput
|
||||
accept=".png,.jpeg,.jpg,.pdf"
|
||||
(change)="onFileSelect($event, 'PHOTO_PROOF')"
|
||||
[style.display]="'none'"
|
||||
/>
|
||||
<mat-form-field appearance="outline" class="ckyc-file-name-field">
|
||||
<input
|
||||
matInput
|
||||
[readonly]="true"
|
||||
[value]="photoProofFile?.name || ''"
|
||||
[placeholder]="'labels.text.NoFileSelected' | translate"
|
||||
/>
|
||||
</mat-form-field>
|
||||
<button mat-stroked-button type="button" (click)="photoProofInput.click()">
|
||||
<fa-icon icon="folder-open" size="lg"></fa-icon>
|
||||
{{ 'labels.buttons.Browse' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
mat-raised-button
|
||||
color="primary"
|
||||
type="button"
|
||||
[disabled]="!photoProofFile || uploading['PHOTO_PROOF']"
|
||||
(click)="uploadDocument('PHOTO_PROOF')"
|
||||
>
|
||||
@if (uploading['PHOTO_PROOF']) {
|
||||
<div class="ckyc-uploading">
|
||||
<mat-progress-spinner diameter="18" mode="indeterminate"></mat-progress-spinner>
|
||||
} @else {
|
||||
{{ 'labels.buttons.Upload' | translate }}
|
||||
<span>Uploading…</span>
|
||||
</div>
|
||||
}
|
||||
</button>
|
||||
@if (uploadErrors['PHOTO_PROOF']) {
|
||||
<div class="ckyc-upload-error">
|
||||
<mat-icon>error_outline</mat-icon>
|
||||
<span>{{ uploadErrors['PHOTO_PROOF'] }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (uploadResponses['PHOTO_PROOF']) {
|
||||
<div class="ckyc-upload-response">
|
||||
<p>
|
||||
<b>{{ 'labels.inputs.Status' | translate }}:</b> {{ uploadResponses['PHOTO_PROOF'].status }}
|
||||
</p>
|
||||
<p>
|
||||
<b>{{ 'labels.inputs.Request ID' | translate }}:</b> {{ uploadResponses['PHOTO_PROOF'].requestId }}
|
||||
</p>
|
||||
<p>
|
||||
<b>{{ 'labels.inputs.Acknowledgement Number' | translate }}:</b>
|
||||
{{ uploadResponses['PHOTO_PROOF'].acknowledgementNumber }}
|
||||
</p>
|
||||
<p>
|
||||
<b>{{ 'labels.inputs.Processing Status' | translate }}:</b>
|
||||
{{ uploadResponses['PHOTO_PROOF'].processingStatus }}
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
|
||||
@@ -422,75 +402,34 @@
|
||||
mat-stroked-button
|
||||
type="button"
|
||||
class="ckyc-existing__replace"
|
||||
(click)="toggleReplace('CLIENT_IMAGE')"
|
||||
(click)="openUploadDialog('CLIENT_IMAGE')"
|
||||
>
|
||||
<fa-icon [icon]="existing.CLIENT_IMAGE.expanded ? 'times' : 'upload'"></fa-icon>
|
||||
{{ existing.CLIENT_IMAGE.expanded ? 'Cancel replace' : 'Upload new version' }}
|
||||
<fa-icon icon="upload"></fa-icon>
|
||||
Upload new version
|
||||
</button>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="ckyc-missing">
|
||||
<p class="ckyc-upload-hint">Not uploaded yet. Accepted formats: PNG, JPEG, JPG</p>
|
||||
<button mat-raised-button color="primary" type="button" (click)="openUploadDialog('CLIENT_IMAGE')">
|
||||
<fa-icon icon="upload"></fa-icon>
|
||||
{{ 'labels.buttons.Upload' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!existing.CLIENT_IMAGE.present || existing.CLIENT_IMAGE.expanded) {
|
||||
<p class="ckyc-upload-hint">{{ 'labels.inputs.Accepted formats' | translate }}: PNG, JPEG, JPG</p>
|
||||
<div class="ckyc-file-row">
|
||||
<input
|
||||
type="file"
|
||||
#clientImageInput
|
||||
accept=".png,.jpeg,.jpg"
|
||||
(change)="onFileSelect($event, 'CLIENT_IMAGE')"
|
||||
[style.display]="'none'"
|
||||
/>
|
||||
<mat-form-field appearance="outline" class="ckyc-file-name-field">
|
||||
<input
|
||||
matInput
|
||||
[readonly]="true"
|
||||
[value]="clientImageFile?.name || ''"
|
||||
[placeholder]="'labels.text.NoFileSelected' | translate"
|
||||
/>
|
||||
</mat-form-field>
|
||||
<button mat-stroked-button type="button" (click)="clientImageInput.click()">
|
||||
<fa-icon icon="folder-open" size="lg"></fa-icon>
|
||||
{{ 'labels.buttons.Browse' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
mat-raised-button
|
||||
color="primary"
|
||||
type="button"
|
||||
[disabled]="!clientImageFile || uploading['CLIENT_IMAGE']"
|
||||
(click)="uploadDocument('CLIENT_IMAGE')"
|
||||
>
|
||||
@if (uploading['CLIENT_IMAGE']) {
|
||||
<div class="ckyc-uploading">
|
||||
<mat-progress-spinner diameter="18" mode="indeterminate"></mat-progress-spinner>
|
||||
} @else {
|
||||
{{ 'labels.buttons.Upload' | translate }}
|
||||
<span>Uploading…</span>
|
||||
</div>
|
||||
}
|
||||
</button>
|
||||
@if (uploadErrors['CLIENT_IMAGE']) {
|
||||
<div class="ckyc-upload-error">
|
||||
<mat-icon>error_outline</mat-icon>
|
||||
<span>{{ uploadErrors['CLIENT_IMAGE'] }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (uploadResponses['CLIENT_IMAGE']) {
|
||||
<div class="ckyc-upload-response">
|
||||
<p>
|
||||
<b>{{ 'labels.inputs.Status' | translate }}:</b> {{ uploadResponses['CLIENT_IMAGE'].status }}
|
||||
</p>
|
||||
<p>
|
||||
<b>{{ 'labels.inputs.Request ID' | translate }}:</b> {{ uploadResponses['CLIENT_IMAGE'].requestId }}
|
||||
</p>
|
||||
<p>
|
||||
<b>{{ 'labels.inputs.Acknowledgement Number' | translate }}:</b>
|
||||
{{ uploadResponses['CLIENT_IMAGE'].acknowledgementNumber }}
|
||||
</p>
|
||||
<p>
|
||||
<b>{{ 'labels.inputs.Processing Status' | translate }}:</b>
|
||||
{{ uploadResponses['CLIENT_IMAGE'].processingStatus }}
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
|
||||
@@ -526,76 +465,34 @@
|
||||
mat-stroked-button
|
||||
type="button"
|
||||
class="ckyc-existing__replace"
|
||||
(click)="toggleReplace('CLIENT_SIGNATURE')"
|
||||
(click)="openUploadDialog('CLIENT_SIGNATURE')"
|
||||
>
|
||||
<fa-icon [icon]="existing.CLIENT_SIGNATURE.expanded ? 'times' : 'upload'"></fa-icon>
|
||||
{{ existing.CLIENT_SIGNATURE.expanded ? 'Cancel replace' : 'Upload new version' }}
|
||||
<fa-icon icon="upload"></fa-icon>
|
||||
Upload new version
|
||||
</button>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="ckyc-missing">
|
||||
<p class="ckyc-upload-hint">Not uploaded yet. Accepted formats: PNG, JPEG, JPG</p>
|
||||
<button mat-raised-button color="primary" type="button" (click)="openUploadDialog('CLIENT_SIGNATURE')">
|
||||
<fa-icon icon="upload"></fa-icon>
|
||||
{{ 'labels.buttons.Upload' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!existing.CLIENT_SIGNATURE.present || existing.CLIENT_SIGNATURE.expanded) {
|
||||
<p class="ckyc-upload-hint">{{ 'labels.inputs.Accepted formats' | translate }}: PNG, JPEG, JPG</p>
|
||||
<div class="ckyc-file-row">
|
||||
<input
|
||||
type="file"
|
||||
#clientSignatureInput
|
||||
accept=".png,.jpeg,.jpg"
|
||||
(change)="onFileSelect($event, 'CLIENT_SIGNATURE')"
|
||||
[style.display]="'none'"
|
||||
/>
|
||||
<mat-form-field appearance="outline" class="ckyc-file-name-field">
|
||||
<input
|
||||
matInput
|
||||
[readonly]="true"
|
||||
[value]="clientSignatureFile?.name || ''"
|
||||
[placeholder]="'labels.text.NoFileSelected' | translate"
|
||||
/>
|
||||
</mat-form-field>
|
||||
<button mat-stroked-button type="button" (click)="clientSignatureInput.click()">
|
||||
<fa-icon icon="folder-open" size="lg"></fa-icon>
|
||||
{{ 'labels.buttons.Browse' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
mat-raised-button
|
||||
color="primary"
|
||||
type="button"
|
||||
[disabled]="!clientSignatureFile || uploading['CLIENT_SIGNATURE']"
|
||||
(click)="uploadDocument('CLIENT_SIGNATURE')"
|
||||
>
|
||||
@if (uploading['CLIENT_SIGNATURE']) {
|
||||
<div class="ckyc-uploading">
|
||||
<mat-progress-spinner diameter="18" mode="indeterminate"></mat-progress-spinner>
|
||||
} @else {
|
||||
{{ 'labels.buttons.Upload' | translate }}
|
||||
<span>Uploading…</span>
|
||||
</div>
|
||||
}
|
||||
</button>
|
||||
@if (uploadErrors['CLIENT_SIGNATURE']) {
|
||||
<div class="ckyc-upload-error">
|
||||
<mat-icon>error_outline</mat-icon>
|
||||
<span>{{ uploadErrors['CLIENT_SIGNATURE'] }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (uploadResponses['CLIENT_SIGNATURE']) {
|
||||
<div class="ckyc-upload-response">
|
||||
<p>
|
||||
<b>{{ 'labels.inputs.Status' | translate }}:</b> {{ uploadResponses['CLIENT_SIGNATURE'].status }}
|
||||
</p>
|
||||
<p>
|
||||
<b>{{ 'labels.inputs.Request ID' | translate }}:</b>
|
||||
{{ uploadResponses['CLIENT_SIGNATURE'].requestId }}
|
||||
</p>
|
||||
<p>
|
||||
<b>{{ 'labels.inputs.Acknowledgement Number' | translate }}:</b>
|
||||
{{ uploadResponses['CLIENT_SIGNATURE'].acknowledgementNumber }}
|
||||
</p>
|
||||
<p>
|
||||
<b>{{ 'labels.inputs.Processing Status' | translate }}:</b>
|
||||
{{ uploadResponses['CLIENT_SIGNATURE'].processingStatus }}
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
|
||||
@@ -553,15 +553,21 @@
|
||||
margin: 0 0 12px;
|
||||
}
|
||||
|
||||
.ckyc-file-row {
|
||||
.ckyc-missing {
|
||||
margin-top: 14px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.ckyc-file-name-field {
|
||||
flex: 1;
|
||||
.ckyc-uploading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 10px;
|
||||
font-size: 12px;
|
||||
color: #5f6368;
|
||||
}
|
||||
|
||||
.ckyc-upload-error {
|
||||
@@ -583,19 +589,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.ckyc-upload-response {
|
||||
margin-top: 12px;
|
||||
padding: 10px 14px;
|
||||
background-color: #e8f5e9;
|
||||
border: 1px solid #a5d6a7;
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
|
||||
p {
|
||||
margin: 4px 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 900px) {
|
||||
.ckyc-upload-grid {
|
||||
grid-template-columns: 1fr;
|
||||
@@ -610,14 +603,6 @@
|
||||
.ckyc-upload-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.ckyc-file-row {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.ckyc-file-name-field {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===================== Already-available banners ===================== */
|
||||
@@ -731,6 +716,34 @@
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
&__thumbs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
&__dates {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 11px;
|
||||
color: #0f6e56;
|
||||
flex-wrap: wrap;
|
||||
|
||||
span {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
fa-icon {
|
||||
color: #1d9e75;
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
&__thumb {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
|
||||
@@ -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) => {
|
||||
request$.pipe(takeUntil(this.destroy$)).subscribe({
|
||||
next: () => {
|
||||
this.uploading[documentType] = false;
|
||||
this.uploadResponses[documentType] = response;
|
||||
this.alertService.alert({
|
||||
type: 'success',
|
||||
message: response?.message || 'Document uploaded successfully.'
|
||||
});
|
||||
this.alertService.alert({ type: 'success', message: 'Document uploaded successfully.' });
|
||||
this.refreshPreview(documentType);
|
||||
},
|
||||
error: (err: any) => {
|
||||
this.uploading[documentType] = false;
|
||||
this.uploadErrors[documentType] =
|
||||
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.';
|
||||
'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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+167
@@ -0,0 +1,167 @@
|
||||
<!--
|
||||
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/.
|
||||
-->
|
||||
|
||||
<div class="cud-header">
|
||||
<div class="cud-header__badge">
|
||||
<fa-icon icon="folder-open"></fa-icon>
|
||||
</div>
|
||||
<div class="cud-header__text">
|
||||
<h2 mat-dialog-title class="cud-title">{{ data.title }}</h2>
|
||||
<p class="cud-subtitle">Fill in the document details, then attach the scan.</p>
|
||||
</div>
|
||||
<button mat-icon-button mat-dialog-close class="cud-close" aria-label="Close">
|
||||
<fa-icon icon="times"></fa-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<mat-dialog-content class="cud-body">
|
||||
@if (data.isProofKind) {
|
||||
<div class="cud-section">
|
||||
<span class="cud-section__label"><fa-icon icon="id-card"></fa-icon> Document type</span>
|
||||
<mat-form-field appearance="outline" class="cud-field" subscriptSizing="dynamic">
|
||||
<mat-select
|
||||
[value]="selectedProofType"
|
||||
[compareWith]="compareProofType"
|
||||
placeholder="Select document type"
|
||||
(selectionChange)="onProofTypeChange($event.value)"
|
||||
>
|
||||
@for (p of data.proofOptions; track p.id) {
|
||||
<mat-option [value]="p">{{ p.name }}</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<p class="cud-hint">Upload requirements will update automatically based on the type you pick.</p>
|
||||
</div>
|
||||
|
||||
@if (selectedProofType) {
|
||||
@if (selectedProofType.info) {
|
||||
<div class="cud-banner">
|
||||
<fa-icon icon="check-circle"></fa-icon>
|
||||
<span>{{ selectedProofType.info }}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="cud-section">
|
||||
<span class="cud-section__label">
|
||||
<fa-icon icon="hashtag"></fa-icon>
|
||||
{{ selectedProofType.documentKeyLabel || 'Document number' }}
|
||||
</span>
|
||||
<mat-form-field appearance="outline" class="cud-field" subscriptSizing="dynamic">
|
||||
<input
|
||||
matInput
|
||||
[(ngModel)]="documentKey"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
[placeholder]="selectedProofType.documentKeyPlaceholder || 'Enter document number'"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="cud-section cud-section--dates">
|
||||
<div class="cud-date-field">
|
||||
<span class="cud-section__label"><fa-icon icon="calendar-check"></fa-icon> Issue date</span>
|
||||
<mat-form-field appearance="outline" class="cud-field" subscriptSizing="dynamic">
|
||||
<input
|
||||
matInput
|
||||
readonly
|
||||
[matDatepicker]="issuePicker"
|
||||
[(ngModel)]="issueDate"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
placeholder="Optional"
|
||||
(click)="issuePicker.open()"
|
||||
/>
|
||||
<mat-datepicker-toggle matSuffix [for]="issuePicker"></mat-datepicker-toggle>
|
||||
<mat-datepicker #issuePicker></mat-datepicker>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="cud-date-field">
|
||||
<span class="cud-section__label"><fa-icon icon="calendar-check"></fa-icon> Expiry date</span>
|
||||
<mat-form-field appearance="outline" class="cud-field" subscriptSizing="dynamic">
|
||||
<input
|
||||
matInput
|
||||
readonly
|
||||
[matDatepicker]="expiryPicker"
|
||||
[(ngModel)]="expiryDate"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
placeholder="Optional"
|
||||
(click)="expiryPicker.open()"
|
||||
/>
|
||||
<mat-datepicker-toggle matSuffix [for]="expiryPicker"></mat-datepicker-toggle>
|
||||
<mat-datepicker #expiryPicker></mat-datepicker>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cud-divider"><span>Scan</span></div>
|
||||
|
||||
<div class="cud-grid" [class.cud-grid--two]="sides === 2">
|
||||
<div class="cud-slot">
|
||||
<span class="cud-slot__label">{{ sides === 2 ? 'Front side' : 'Document' }}</span>
|
||||
<label class="cud-dropzone" [class.cud-dropzone--has]="front">
|
||||
<input type="file" [attr.accept]="data.accept" hidden (change)="onFilePick('front', $event)" />
|
||||
@if (frontPreview) {
|
||||
<img [src]="frontPreview" alt="Front side preview" class="cud-dropzone__img" />
|
||||
} @else if (front) {
|
||||
<fa-icon icon="file-alt" class="cud-dropzone__icon"></fa-icon>
|
||||
<p class="cud-dropzone__name">{{ front.name }}</p>
|
||||
} @else {
|
||||
<fa-icon icon="cloud-upload-alt" class="cud-dropzone__icon"></fa-icon>
|
||||
<p class="cud-dropzone__text">Click to choose file</p>
|
||||
}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@if (sides === 2) {
|
||||
<div class="cud-slot">
|
||||
<span class="cud-slot__label">Back side</span>
|
||||
<label class="cud-dropzone" [class.cud-dropzone--has]="back">
|
||||
<input type="file" [attr.accept]="data.accept" hidden (change)="onFilePick('back', $event)" />
|
||||
@if (backPreview) {
|
||||
<img [src]="backPreview" alt="Back side preview" class="cud-dropzone__img" />
|
||||
} @else if (back) {
|
||||
<fa-icon icon="file-alt" class="cud-dropzone__icon"></fa-icon>
|
||||
<p class="cud-dropzone__name">{{ back.name }}</p>
|
||||
} @else {
|
||||
<fa-icon icon="cloud-upload-alt" class="cud-dropzone__icon"></fa-icon>
|
||||
<p class="cud-dropzone__text">Click to choose file</p>
|
||||
}
|
||||
</label>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
} @else {
|
||||
<div class="cud-grid">
|
||||
<div class="cud-slot">
|
||||
<span class="cud-slot__label">Document</span>
|
||||
<label class="cud-dropzone" [class.cud-dropzone--has]="front">
|
||||
<input type="file" [attr.accept]="data.accept" hidden (change)="onFilePick('front', $event)" />
|
||||
@if (frontPreview) {
|
||||
<img [src]="frontPreview" alt="Preview" class="cud-dropzone__img" />
|
||||
} @else if (front) {
|
||||
<fa-icon icon="file-alt" class="cud-dropzone__icon"></fa-icon>
|
||||
<p class="cud-dropzone__name">{{ front.name }}</p>
|
||||
} @else {
|
||||
<fa-icon icon="cloud-upload-alt" class="cud-dropzone__icon"></fa-icon>
|
||||
<p class="cud-dropzone__text">Click to choose file</p>
|
||||
}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</mat-dialog-content>
|
||||
|
||||
<mat-dialog-actions class="cud-actions" align="end">
|
||||
<button mat-stroked-button type="button" (click)="cancel()">
|
||||
<fa-icon icon="times"></fa-icon>
|
||||
Cancel
|
||||
</button>
|
||||
<button mat-flat-button color="primary" type="button" [disabled]="!canSubmit" (click)="submit()">
|
||||
<fa-icon icon="upload"></fa-icon>
|
||||
Upload
|
||||
</button>
|
||||
</mat-dialog-actions>
|
||||
+270
@@ -0,0 +1,270 @@
|
||||
/**
|
||||
* 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/.
|
||||
*/
|
||||
|
||||
:host {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* ===================== Header ===================== */
|
||||
.cud-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
padding: 20px 24px 16px;
|
||||
border-bottom: 1px solid #eef1f4;
|
||||
|
||||
&__badge {
|
||||
flex-shrink: 0;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 9px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 15px;
|
||||
color: #fff;
|
||||
background: linear-gradient(135deg, #1a73e8, #4285f4);
|
||||
box-shadow: 0 3px 10px rgb(26 115 232 / 28%);
|
||||
}
|
||||
|
||||
&__text {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.cud-title {
|
||||
margin: 0 !important;
|
||||
padding: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #1a1d21;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.cud-subtitle {
|
||||
margin: 2px 0 0;
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.cud-close {
|
||||
flex-shrink: 0;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin: -4px -4px 0 0;
|
||||
color: #9aa0a6;
|
||||
|
||||
&:hover {
|
||||
color: #3c4043;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===================== Body / sections ===================== */
|
||||
.cud-body.mat-mdc-dialog-content {
|
||||
padding: 18px 24px !important;
|
||||
max-height: 70vh;
|
||||
}
|
||||
|
||||
.cud-section {
|
||||
margin-bottom: 14px;
|
||||
|
||||
&--dates {
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
|
||||
.cud-date-field {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: #475569;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
margin-bottom: 6px;
|
||||
|
||||
fa-icon {
|
||||
color: #1a73e8;
|
||||
font-size: 10.5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cud-field {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.cud-hint {
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
margin: 6px 0 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.cud-banner {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 9px;
|
||||
padding: 10px 14px;
|
||||
margin-bottom: 14px;
|
||||
background: #f0fbf6;
|
||||
border: 1px solid #cfecdd;
|
||||
border-radius: 10px;
|
||||
font-size: 12.5px;
|
||||
color: #085041;
|
||||
line-height: 1.45;
|
||||
|
||||
fa-icon {
|
||||
margin-top: 2px;
|
||||
color: #1d9e75;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.cud-divider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin: 16px 0 12px;
|
||||
font-size: 10.5px;
|
||||
font-weight: 700;
|
||||
color: #9aa0a6;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
content: '';
|
||||
flex: 1;
|
||||
height: 1px;
|
||||
background: #eef1f4;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===================== Front/back scan grid ===================== */
|
||||
.cud-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 12px;
|
||||
|
||||
&--two {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.cud-slot {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
|
||||
&__label {
|
||||
font-size: 10.5px;
|
||||
font-weight: 700;
|
||||
color: #5f6368;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
}
|
||||
|
||||
.cud-dropzone {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
min-height: 130px;
|
||||
padding: 16px;
|
||||
border: 2px dashed #cbd5e1;
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
background: #fafbfc;
|
||||
transition:
|
||||
border-color 0.15s,
|
||||
background 0.15s,
|
||||
box-shadow 0.15s;
|
||||
|
||||
&:hover {
|
||||
border-color: #1a73e8;
|
||||
background: #f5f9ff;
|
||||
box-shadow: 0 2px 10px rgb(26 115 232 / 10%);
|
||||
}
|
||||
|
||||
&--has {
|
||||
border-style: solid;
|
||||
border-color: #1d9e75;
|
||||
background: #f0fbf6;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
font-size: 22px;
|
||||
color: #9aa5b1;
|
||||
}
|
||||
|
||||
&--has &__icon {
|
||||
color: #1d9e75;
|
||||
}
|
||||
|
||||
&__text {
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
margin: 0;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&__name {
|
||||
font-size: 11.5px;
|
||||
color: #085041;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
word-break: break-all;
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
&__img {
|
||||
max-width: 100%;
|
||||
max-height: 150px;
|
||||
border-radius: 8px;
|
||||
object-fit: contain;
|
||||
box-shadow: 0 2px 8px rgb(0 0 0 / 8%);
|
||||
}
|
||||
}
|
||||
|
||||
/* ===================== Actions ===================== */
|
||||
.cud-actions {
|
||||
padding: 12px 24px !important;
|
||||
margin: 0 !important;
|
||||
border-top: 1px solid #eef1f4;
|
||||
|
||||
fa-icon {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 560px) {
|
||||
:host {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.cud-section--dates {
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.cud-grid--two {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
+155
@@ -0,0 +1,155 @@
|
||||
/**
|
||||
* 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, OnDestroy, inject } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import {
|
||||
MatDialogRef,
|
||||
MAT_DIALOG_DATA,
|
||||
MatDialogTitle,
|
||||
MatDialogContent,
|
||||
MatDialogActions,
|
||||
MatDialogClose
|
||||
} from '@angular/material/dialog';
|
||||
import { MatIconButton } from '@angular/material/button';
|
||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
import { Dates } from 'app/core/utils/dates';
|
||||
import { ProofType } from '../../../shared/kyc-proof-types';
|
||||
|
||||
export interface CkycUploadDialogData {
|
||||
title: string;
|
||||
isProofKind: boolean;
|
||||
accept: string;
|
||||
proofOptions?: ProofType[];
|
||||
initialProofType?: ProofType | null;
|
||||
initialDocumentKey?: string;
|
||||
initialIssueDate?: Date | null;
|
||||
initialExpiryDate?: Date | null;
|
||||
}
|
||||
|
||||
export interface CkycUploadDialogResult {
|
||||
proofType?: ProofType | null;
|
||||
front: File;
|
||||
back?: File;
|
||||
/** Document number (Aadhaar/PAN/Passport number, etc.) — same field client creation collects. */
|
||||
documentKey?: string;
|
||||
/** 'yyyy-MM-dd', same as the rest of this app's date fields. */
|
||||
issueDate?: string | null;
|
||||
expiryDate?: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* A focused popup for replacing/uploading one CKYC artefact at a time — opened from each card on
|
||||
* the CKYC tab instead of expanding a form inline underneath it. Collects the same details
|
||||
* client creation does for an identity/address proof (document type, number, issue + expiry
|
||||
* date) and shows a client-side preview of whatever image is picked before it's ever sent to the
|
||||
* server; the actual upload still happens back in CkycTabComponent (this dialog only collects
|
||||
* the type, number, dates and file(s), then hands them back).
|
||||
*/
|
||||
@Component({
|
||||
selector: 'mifosx-ckyc-upload-dialog',
|
||||
templateUrl: './ckyc-upload-dialog.component.html',
|
||||
styleUrls: ['./ckyc-upload-dialog.component.scss'],
|
||||
imports: [
|
||||
...STANDALONE_SHARED_IMPORTS,
|
||||
FormsModule,
|
||||
MatDialogTitle,
|
||||
MatDialogContent,
|
||||
MatDialogActions,
|
||||
MatDialogClose,
|
||||
MatIconButton,
|
||||
FaIconComponent
|
||||
]
|
||||
})
|
||||
export class CkycUploadDialogComponent implements OnDestroy {
|
||||
private dates = inject(Dates);
|
||||
dialogRef = inject<MatDialogRef<CkycUploadDialogComponent, CkycUploadDialogResult | null>>(MatDialogRef);
|
||||
data = inject<CkycUploadDialogData>(MAT_DIALOG_DATA);
|
||||
|
||||
selectedProofType: ProofType | null = this.data.initialProofType || null;
|
||||
documentKey = this.data.initialDocumentKey || '';
|
||||
issueDate: Date | null = this.data.initialIssueDate || null;
|
||||
expiryDate: Date | null = this.data.initialExpiryDate || null;
|
||||
|
||||
front: File | null = null;
|
||||
back: File | null = null;
|
||||
frontPreview: string | null = null;
|
||||
backPreview: string | null = null;
|
||||
|
||||
get sides(): number {
|
||||
return this.data.isProofKind ? (this.selectedProofType?.sides ?? 1) : 1;
|
||||
}
|
||||
|
||||
get canSubmit(): boolean {
|
||||
if (this.data.isProofKind) {
|
||||
if (!this.selectedProofType) return false;
|
||||
if (!this.documentKey.trim()) return false;
|
||||
}
|
||||
if (!this.front) return false;
|
||||
if (this.sides === 2 && !this.back) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
compareProofType = (a: ProofType | null, b: ProofType | null): boolean => (a?.id ?? null) === (b?.id ?? null);
|
||||
|
||||
onProofTypeChange(proof: ProofType | null): void {
|
||||
this.selectedProofType = proof;
|
||||
this.clearSlot('front');
|
||||
this.clearSlot('back');
|
||||
}
|
||||
|
||||
onFilePick(slot: 'front' | 'back', event: any): void {
|
||||
const file = event?.target?.files?.[0];
|
||||
if (event?.target) event.target.value = '';
|
||||
if (!file) return;
|
||||
|
||||
this.clearSlot(slot);
|
||||
const preview = file.type?.startsWith('image/') ? URL.createObjectURL(file) : null;
|
||||
if (slot === 'front') {
|
||||
this.front = file;
|
||||
this.frontPreview = preview;
|
||||
} else {
|
||||
this.back = file;
|
||||
this.backPreview = preview;
|
||||
}
|
||||
}
|
||||
|
||||
private clearSlot(slot: 'front' | 'back'): void {
|
||||
if (slot === 'front') {
|
||||
if (this.frontPreview) URL.revokeObjectURL(this.frontPreview);
|
||||
this.front = null;
|
||||
this.frontPreview = null;
|
||||
} else {
|
||||
if (this.backPreview) URL.revokeObjectURL(this.backPreview);
|
||||
this.back = null;
|
||||
this.backPreview = null;
|
||||
}
|
||||
}
|
||||
|
||||
submit(): void {
|
||||
if (!this.canSubmit) return;
|
||||
this.dialogRef.close({
|
||||
proofType: this.selectedProofType,
|
||||
front: this.front!,
|
||||
back: this.sides === 2 ? this.back! : undefined,
|
||||
documentKey: this.data.isProofKind ? this.documentKey.trim() : undefined,
|
||||
issueDate: this.issueDate ? this.dates.getDate(this.issueDate) : null,
|
||||
expiryDate: this.expiryDate ? this.dates.getDate(this.expiryDate) : null
|
||||
});
|
||||
}
|
||||
|
||||
cancel(): void {
|
||||
this.dialogRef.close(null);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
if (this.frontPreview) URL.revokeObjectURL(this.frontPreview);
|
||||
if (this.backPreview) URL.revokeObjectURL(this.backPreview);
|
||||
}
|
||||
}
|
||||
@@ -229,7 +229,7 @@ export class IdentitiesTabComponent implements OnDestroy {
|
||||
}
|
||||
const identifierData = {
|
||||
documentTypeId,
|
||||
status: 'Active',
|
||||
status: 'ACTIVE',
|
||||
documentKey: kind === 'id' ? section.documentKey : `${proofName} - ${this.clientId}`,
|
||||
description: kind === 'address' && section.address ? section.address : description
|
||||
};
|
||||
|
||||
@@ -840,6 +840,15 @@ export class CreateClientComponent {
|
||||
this.saveAddressProof(clientId);
|
||||
}
|
||||
|
||||
/** "Issue date: yyyy-MM-dd, Expiry date: yyyy-MM-dd" — same convention the CKYC tab and
|
||||
* client-india-address-step's own "load existing" parsing both read back. */
|
||||
private buildProofDescription(issueDate: string | null | undefined, expiryDate: string | null | undefined): string {
|
||||
const parts: string[] = [];
|
||||
if (issueDate) parts.push(`Issue date: ${issueDate}`);
|
||||
if (expiryDate) parts.push(`Expiry date: ${expiryDate}`);
|
||||
return parts.join(', ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the address-proof picked on the permanent tab as a client identifier
|
||||
* + upload each selected file as a document attached to that identifier.
|
||||
@@ -855,9 +864,9 @@ export class CreateClientComponent {
|
||||
const proofName = payload.proof?.name || 'Address Proof';
|
||||
const identifierData: any = {
|
||||
documentTypeId: payload.proofId,
|
||||
status: 'Active',
|
||||
status: 'ACTIVE',
|
||||
documentKey: payload.documentKey || `${proofName} - ${clientId}`,
|
||||
description: payload.issueDate ? `Issue date: ${payload.issueDate}` : ''
|
||||
description: this.buildProofDescription(payload.issueDate, payload.expiryDate)
|
||||
};
|
||||
|
||||
this.clientsService.addClientIdentifier(clientId, identifierData).subscribe({
|
||||
@@ -904,7 +913,7 @@ export class CreateClientComponent {
|
||||
}
|
||||
const identifierData: any = {
|
||||
documentTypeId,
|
||||
status: 'Active',
|
||||
status: 'ACTIVE',
|
||||
documentKey: payload.documentKey || `${proofName} - ${clientId}`,
|
||||
description: 'KYC document'
|
||||
};
|
||||
|
||||
@@ -1035,6 +1035,15 @@ export class EditClientComponent {
|
||||
this.saveAddressProof(clientId);
|
||||
}
|
||||
|
||||
/** "Issue date: yyyy-MM-dd, Expiry date: yyyy-MM-dd" — same convention the CKYC tab and
|
||||
* client-india-address-step's own "load existing" parsing both read back. */
|
||||
private buildProofDescription(issueDate: string | null | undefined, expiryDate: string | null | undefined): string {
|
||||
const parts: string[] = [];
|
||||
if (issueDate) parts.push(`Issue date: ${issueDate}`);
|
||||
if (expiryDate) parts.push(`Expiry date: ${expiryDate}`);
|
||||
return parts.join(', ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the address-proof picked on the permanent tab as a client identifier
|
||||
* + upload each selected file as a document attached to that identifier.
|
||||
@@ -1050,9 +1059,9 @@ export class EditClientComponent {
|
||||
const proofName = payload.proof?.name || 'Address Proof';
|
||||
const identifierData: any = {
|
||||
documentTypeId: payload.proofId,
|
||||
status: 'Active',
|
||||
status: 'ACTIVE',
|
||||
documentKey: payload.documentKey || `${proofName} - ${clientId}`,
|
||||
description: payload.issueDate ? `Issue date: ${payload.issueDate}` : ''
|
||||
description: this.buildProofDescription(payload.issueDate, payload.expiryDate)
|
||||
};
|
||||
|
||||
this.clientsService.addClientIdentifier(clientId, identifierData).subscribe({
|
||||
@@ -1105,7 +1114,7 @@ export class EditClientComponent {
|
||||
const existingId = this.clientKycStep?.existingKycIdentifierId;
|
||||
if (existingId) {
|
||||
this.resolveDocumentTypeId(payload.proofId, proofName).subscribe((documentTypeId: number | null) => {
|
||||
const update: any = { documentKey: payload.documentKey || `${proofName} - ${clientId}`, status: 'Active' };
|
||||
const update: any = { documentKey: payload.documentKey || `${proofName} - ${clientId}`, status: 'ACTIVE' };
|
||||
if (documentTypeId != null) {
|
||||
update.documentTypeId = documentTypeId;
|
||||
}
|
||||
@@ -1128,7 +1137,7 @@ export class EditClientComponent {
|
||||
}
|
||||
const identifierData: any = {
|
||||
documentTypeId,
|
||||
status: 'Active',
|
||||
status: 'ACTIVE',
|
||||
documentKey: payload.documentKey || `${proofName} - ${clientId}`,
|
||||
description: 'KYC document'
|
||||
};
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
/* tslint:disable */
|
||||
export default {
|
||||
'mifos_x': {
|
||||
'version': '260911',
|
||||
'hash': '7afa9c7a'
|
||||
'version': '260914',
|
||||
'hash': 'ab58d7f7'
|
||||
},
|
||||
'allow_switching_backend_instance': true
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user