Merge branch 'dev' into feature/bugfix

Conflicts resolved in:
- src/app/clients/clients-view/ckyc-tab/ckyc-tab.component.html
- src/app/clients/clients-view/ckyc-tab/ckyc-tab.component.ts

Kept dev's CKYC architecture (upload dialog, real per-artefact endpoints)
and layered feature/bugfix's fixes on top: upload file validation,
missing-file badge, silent preview download, and image/PDF view button.
Also includes the CKYC "Image of Client" upload-state fix and the
client-change state-leak fix in the same component.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-16 16:55:32 +05:30
co-authored by Claude Sonnet 5
105 changed files with 7140 additions and 1902 deletions
@@ -82,12 +82,13 @@
<strong class="kyc-doc-alert__title">{{ kycDocumentAlertTitle }}</strong>
<p class="kyc-doc-alert__text">{{ kycDocumentAlertMessage }}</p>
</div>
<!-- Same dialog as the document's own row: the banner is asking for exactly that. -->
<button
mat-flat-button
color="primary"
type="button"
class="kyc-doc-alert__action"
(click)="toggleEditMode()"
(click)="updateKycDocument()"
>
Update Document
</button>
@@ -470,7 +471,11 @@
}
</span>
<span class="kv-table__col kv-table__col--action">
<button mat-stroked-button class="kv-action-btn" type="button" (click)="toggleEditMode()">Update</button>
<!-- The document is re-collected in its own dialog, the way PAN and Aadhaar are dealt
with on their rows, rather than by opening the whole KYC form for editing. -->
<button mat-stroked-button class="kv-action-btn" type="button" (click)="updateKycDocument()">
Update
</button>
</span>
</div>
}
@@ -1188,15 +1193,81 @@
</mat-form-field>
</div>
<!-- The scans live with the document itself, on the identifier the customer's
original was attached to. Re-scanning is done there rather than duplicated
here, so both sides, the cropper and the replace-in-place all behave the
same way they did when the document was first collected. -->
<a class="kyc-doc-scans-link" [routerLink]="['/clients', clientId, 'edit']">
<mat-icon>photo_library</mat-icon>
<span>Replace the scanned front / back of this document</span>
<mat-icon class="kyc-doc-scans-link__go">chevron_right</mat-icon>
</a>
<!-- The scan, where the document it belongs to is. Re-collecting an expiring
document is what this panel is for, so sending the officer to the client
edit screen to change the image made them leave the record they were
working on and find the same document again. -->
<div class="kyc-doc-scans">
<div class="kyc-doc-scans__head">
<mat-icon>photo_library</mat-icon>
<span>Scanned document</span>
@if (docScansLoading) {
<mat-progress-spinner diameter="16" mode="indeterminate"></mat-progress-spinner>
}
</div>
@if (!docScansLoading && !docScans.front && !docScans.back) {
<div class="kyc-doc-scans__empty">No scan is on file for this document. Upload one below.</div>
}
<div class="kyc-doc-scans__sides">
@for (slot of docScanSlots; track slot) {
<div class="kyc-doc-side">
<div class="kyc-doc-side__label">{{ slot === 'front' ? 'Front' : 'Back' }}</div>
<div class="kyc-doc-side__frame">
@if (docScanOf(slot)?.previewUrl) {
<img
[src]="docScanOf(slot)!.previewUrl"
[alt]="slot === 'front' ? 'Front of document' : 'Back of document'"
/>
} @else if (docScanOf(slot)) {
<!-- On file but not an image (a PDF, say) — named rather than shown. -->
<div class="kyc-doc-side__file">
<mat-icon>description</mat-icon>
<span>{{ docScanOf(slot)!.fileName }}</span>
</div>
} @else {
<div class="kyc-doc-side__none">Not on file</div>
}
</div>
<button
type="button"
mat-stroked-button
class="kyc-doc-side__btn"
[disabled]="!!docScanUploading || docScansLoading"
(click)="scanInput.click()"
>
@if (docScanUploading === slot) {
<mat-progress-spinner diameter="16" mode="indeterminate"></mat-progress-spinner>
Uploading…
} @else {
{{ docScanOf(slot) ? 'Replace' : 'Upload' }}
}
</button>
<input
#scanInput
type="file"
hidden
accept="image/*,application/pdf"
(change)="onDocScanSelected($event, slot)"
/>
</div>
}
</div>
@if (docScanError) {
<div class="kyc-doc-scans__msg kyc-doc-scans__msg--err">
<mat-icon>error_outline</mat-icon><span>{{ docScanError }}</span>
</div>
}
@if (docScanSuccess) {
<div class="kyc-doc-scans__msg kyc-doc-scans__msg--ok">
<mat-icon>check_circle</mat-icon><span>{{ docScanSuccess }}</span>
</div>
}
</div>
</div>
}