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:
2026-09-14 11:39:14 +05:30
co-authored by Claude Sonnet 5
parent ab58d7f74c
commit 31e99b1363
13 changed files with 1364 additions and 371 deletions
@@ -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,78 +235,51 @@
</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>
&nbsp;{{ '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>
&nbsp;{{ 'labels.buttons.Browse' | translate }}
</button>
@if (uploading['ADDRESS_PROOF']) {
<div class="ckyc-uploading">
<mat-progress-spinner diameter="18" mode="indeterminate"></mat-progress-spinner>
<span>Uploading…</span>
</div>
}
@if (uploadErrors['ADDRESS_PROOF']) {
<div class="ckyc-upload-error">
<mat-icon>error_outline</mat-icon>
<span>{{ uploadErrors['ADDRESS_PROOF'] }}</span>
</div>
<button
mat-raised-button
color="primary"
type="button"
[disabled]="!addressProofFile || uploading['ADDRESS_PROOF']"
(click)="uploadDocument('ADDRESS_PROOF')"
>
@if (uploading['ADDRESS_PROOF']) {
<mat-progress-spinner diameter="18" mode="indeterminate"></mat-progress-spinner>
} @else {
{{ 'labels.buttons.Upload' | translate }}
}
</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,78 +331,49 @@
</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>
&nbsp;{{ '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>
&nbsp;{{ 'labels.buttons.Browse' | translate }}
</button>
@if (uploading['PHOTO_PROOF']) {
<div class="ckyc-uploading">
<mat-progress-spinner diameter="18" mode="indeterminate"></mat-progress-spinner>
<span>Uploading…</span>
</div>
}
@if (uploadErrors['PHOTO_PROOF']) {
<div class="ckyc-upload-error">
<mat-icon>error_outline</mat-icon>
<span>{{ uploadErrors['PHOTO_PROOF'] }}</span>
</div>
<button
mat-raised-button
color="primary"
type="button"
[disabled]="!photoProofFile || uploading['PHOTO_PROOF']"
(click)="uploadDocument('PHOTO_PROOF')"
>
@if (uploading['PHOTO_PROOF']) {
<mat-progress-spinner diameter="18" mode="indeterminate"></mat-progress-spinner>
} @else {
{{ 'labels.buttons.Upload' | translate }}
}
</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,74 +402,33 @@
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>
&nbsp;{{ '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>
&nbsp;{{ 'labels.buttons.Browse' | translate }}
</button>
@if (uploading['CLIENT_IMAGE']) {
<div class="ckyc-uploading">
<mat-progress-spinner diameter="18" mode="indeterminate"></mat-progress-spinner>
<span>Uploading…</span>
</div>
}
@if (uploadErrors['CLIENT_IMAGE']) {
<div class="ckyc-upload-error">
<mat-icon>error_outline</mat-icon>
<span>{{ uploadErrors['CLIENT_IMAGE'] }}</span>
</div>
<button
mat-raised-button
color="primary"
type="button"
[disabled]="!clientImageFile || uploading['CLIENT_IMAGE']"
(click)="uploadDocument('CLIENT_IMAGE')"
>
@if (uploading['CLIENT_IMAGE']) {
<mat-progress-spinner diameter="18" mode="indeterminate"></mat-progress-spinner>
} @else {
{{ 'labels.buttons.Upload' | translate }}
}
</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,75 +465,33 @@
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>
&nbsp;{{ '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>
&nbsp;{{ 'labels.buttons.Browse' | translate }}
</button>
@if (uploading['CLIENT_SIGNATURE']) {
<div class="ckyc-uploading">
<mat-progress-spinner diameter="18" mode="indeterminate"></mat-progress-spinner>
<span>Uploading…</span>
</div>
}
@if (uploadErrors['CLIENT_SIGNATURE']) {
<div class="ckyc-upload-error">
<mat-icon>error_outline</mat-icon>
<span>{{ uploadErrors['CLIENT_SIGNATURE'] }}</span>
</div>
<button
mat-raised-button
color="primary"
type="button"
[disabled]="!clientSignatureFile || uploading['CLIENT_SIGNATURE']"
(click)="uploadDocument('CLIENT_SIGNATURE')"
>
@if (uploading['CLIENT_SIGNATURE']) {
<mat-progress-spinner diameter="18" mode="indeterminate"></mat-progress-spinner>
} @else {
{{ 'labels.buttons.Upload' | translate }}
}
</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>