Files
cbsnexora/src/app/clients/clients-view/ckyc-tab/ckyc-tab.component.html
T
anshulandClaude Sonnet 5 cda7c52d11 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>
2026-09-16 16:55:32 +05:30

559 lines
22 KiB
HTML

<!--
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="ckyc-tab-container">
<!-- Loading State -->
@if (loading) {
<div class="ckyc-loading">
<mat-progress-spinner mode="indeterminate" diameter="48"></mat-progress-spinner>
<p>{{ 'labels.inputs.CKYC' | translate }}...</p>
</div>
}
@if (!loading) {
<!-- Header -->
<div class="ckyc-tab-header">
<div class="ckyc-tab-title">
<h3>{{ 'labels.heading.CKYC Verification' | translate }}</h3>
<span class="ckyc-badge">CKYC</span>
</div>
</div>
<!-- CERSAI Sync Status hero -->
<div class="cersai-hero" [class.cersai-hero--synced]="ckycNumber" [class.cersai-hero--pending]="!ckycNumber">
<div class="cersai-hero__left">
<div class="cersai-hero__icon">
@if (ckycNumber) {
<fa-icon icon="check-double"></fa-icon>
} @else {
<fa-icon icon="hourglass-half"></fa-icon>
}
</div>
<div class="cersai-hero__text">
<div class="cersai-hero__status">
@if (ckycNumber) {
<span class="cersai-hero__tag">SYNCED WITH CERSAI</span>
} @else {
<span class="cersai-hero__tag cersai-hero__tag--pending">PENDING CERSAI SYNC</span>
}
</div>
@if (ckycNumber) {
<div class="cersai-hero__ckyc-label">CKYC Registry Number</div>
<div class="cersai-hero__ckyc-number">{{ ckycNumber }}</div>
<div class="cersai-hero__meta">
@if (ckycUploadedOn) {
<span>
<fa-icon icon="calendar-check"></fa-icon>
Uploaded on <strong>{{ ckycUploadedOn }}</strong>
</span>
}
@if (ckycBatchId) {
<span class="cersai-hero__sep">·</span>
<span>
<fa-icon icon="file-archive"></fa-icon>
Batch <strong>{{ ckycBatchId }}</strong>
</span>
}
</div>
} @else {
<div class="cersai-hero__ckyc-label">This client has not yet been synced to the CERSAI registry</div>
<div class="cersai-hero__hint">
Collect all 4 artefacts below, then create a batch from the Record Master to upload.
</div>
}
</div>
</div>
@if (ckycNumber) {
<div class="cersai-hero__pulse">
<span class="cersai-hero__pulse-dot"></span>
Live
</div>
}
</div>
<!-- KYC Verification Status (Aadhaar + PAN) -->
<div class="kyc-status-card" [class.kyc-status-card--complete]="kycComplete">
<div class="kyc-status-card__head">
<div class="kyc-status-card__title">
<fa-icon [icon]="kycComplete ? 'shield-alt' : 'user-shield'" class="kyc-status-card__icon"></fa-icon>
<span>KYC Verification</span>
</div>
@if (kycComplete) {
<span class="kyc-status-card__badge kyc-status-card__badge--ok">
<fa-icon icon="check-circle"></fa-icon>
KYC Done
</span>
} @else if (panVerified || aadhaarVerified) {
<span class="kyc-status-card__badge kyc-status-card__badge--partial">
<fa-icon icon="clock"></fa-icon>
Partially Verified
</span>
} @else {
<span class="kyc-status-card__badge kyc-status-card__badge--pending">
<fa-icon icon="exclamation-circle"></fa-icon>
Not Verified
</span>
}
</div>
<div class="kyc-status-card__body">
<div class="kyc-status-row">
<div class="kyc-status-row__label">
<fa-icon icon="id-card"></fa-icon>
<span>PAN</span>
</div>
@if (panVerified) {
<div class="kyc-status-row__value">
<span class="kyc-status-row__id">{{ panNumber || '—' }}</span>
@if (panHolderName) {
<span class="kyc-status-row__name">{{ panHolderName }}</span>
}
</div>
<span class="kyc-pill kyc-pill--ok"><fa-icon icon="check-circle"></fa-icon> Verified</span>
} @else {
<span class="kyc-status-row__value kyc-status-row__value--missing">{{ panNumber || 'Not provided' }}</span>
<span class="kyc-pill kyc-pill--pending"><fa-icon icon="times-circle"></fa-icon> Pending</span>
}
</div>
<div class="kyc-status-row">
<div class="kyc-status-row__label">
<fa-icon icon="fingerprint"></fa-icon>
<span>Aadhaar</span>
</div>
@if (aadhaarVerified) {
<div class="kyc-status-row__value">
<span class="kyc-status-row__id">{{ aadhaarMasked || '—' }}</span>
@if (aadhaarHolderName) {
<span class="kyc-status-row__name">{{ aadhaarHolderName }}</span>
}
</div>
<span class="kyc-pill kyc-pill--ok"><fa-icon icon="check-circle"></fa-icon> Verified</span>
} @else {
<span class="kyc-status-row__value kyc-status-row__value--missing">{{
aadhaarMasked || 'Not provided'
}}</span>
<span class="kyc-pill kyc-pill--pending"><fa-icon icon="times-circle"></fa-icon> Pending</span>
}
</div>
</div>
</div>
<!-- Client basic info -->
<div class="ckyc-info-grid">
<div class="ckyc-info-item">
<span class="ckyc-info-label"> <fa-icon icon="hashtag"></fa-icon> CIF Number </span>
<span class="ckyc-info-value ckyc-info-value--mono">{{ cifNumber || clientId }}</span>
</div>
<div class="ckyc-info-item">
<span class="ckyc-info-label"> <fa-icon icon="user"></fa-icon> Full Name </span>
<span class="ckyc-info-value">{{ clientName || '—' }}</span>
</div>
<div class="ckyc-info-item">
<span class="ckyc-info-label"> <fa-icon icon="envelope"></fa-icon> Email </span>
@if (clientEmail) {
<span class="ckyc-info-value">{{ clientEmail }}</span>
} @else {
<span class="ckyc-info-value ckyc-info-value--missing">No email on file</span>
}
</div>
<div class="ckyc-info-item">
<span class="ckyc-info-label"> <fa-icon icon="phone"></fa-icon> Mobile </span>
@if (clientMobile) {
<span class="ckyc-info-value">{{ clientMobile }}</span>
} @else {
<span class="ckyc-info-value ckyc-info-value--missing">No mobile on file</span>
}
</div>
</div>
<!-- Document Upload Section -->
<h4 class="ckyc-section-title">{{ 'labels.heading.CKYC Documents' | translate }}</h4>
@if (checkingExisting) {
<div class="ckyc-existing-loading">
<mat-progress-spinner diameter="18" mode="indeterminate"></mat-progress-spinner>
<span>Checking for already-uploaded artefacts…</span>
</div>
}
<div class="ckyc-upload-grid">
<!-- Address Proof Document -->
<mat-card class="ckyc-upload-card">
<mat-card-content>
<div class="ckyc-upload-header">
<mat-icon class="ckyc-upload-icon">home</mat-icon>
<span>{{ 'labels.inputs.Address Proof Document' | translate }}</span>
</div>
@if (existing.ADDRESS_PROOF.present) {
<div class="ckyc-existing">
<div class="ckyc-existing__head">
<fa-icon icon="check-circle" class="ckyc-existing__ok"></fa-icon>
<div>
<strong>Already available</strong>
<span class="ckyc-existing__source">from {{ existing.ADDRESS_PROOF.source }}</span>
@if (existing.ADDRESS_PROOF.previewUnavailable) {
<span class="ckyc-existing__warn">stored file unavailable</span>
}
</div>
</div>
@if (
(existing.ADDRESS_PROOF.previewUrl || existing.ADDRESS_PROOF.previewUrlBack) && isImage('ADDRESS_PROOF')
) {
<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>
}
@if (existing.ADDRESS_PROOF.previewUrl) {
<button
mat-stroked-button
type="button"
class="ckyc-existing__view"
(click)="openDocument('ADDRESS_PROOF')"
>
<fa-icon icon="eye"></fa-icon>
&nbsp;View document
</button>
}
<div class="ckyc-existing__meta">
<fa-icon icon="file-alt"></fa-icon>
<span>{{ existing.ADDRESS_PROOF.name }}</span>
@if (existing.ADDRESS_PROOF.documentKey) {
<span class="ckyc-existing__key">{{ existing.ADDRESS_PROOF.documentKey }}</span>
}
@if (existing.ADDRESS_PROOF.fileCount && existing.ADDRESS_PROOF.fileCount > 0) {
<span class="ckyc-existing__count">
{{ existing.ADDRESS_PROOF.fileCount }}
{{ existing.ADDRESS_PROOF.fileCount === 1 ? 'file' : 'files' }}
</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)="openUploadDialog('ADDRESS_PROOF')"
>
<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 (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>
}
</mat-card-content>
</mat-card>
<!-- Photo Proof Document -->
<mat-card class="ckyc-upload-card">
<mat-card-content>
<div class="ckyc-upload-header">
<mat-icon class="ckyc-upload-icon">photo_camera</mat-icon>
<span>{{ 'labels.inputs.Photo Proof Document' | translate }}</span>
</div>
@if (existing.PHOTO_PROOF.present) {
<div class="ckyc-existing">
<div class="ckyc-existing__head">
<fa-icon icon="check-circle" class="ckyc-existing__ok"></fa-icon>
<div>
<strong>Already available</strong>
<span class="ckyc-existing__source">from {{ existing.PHOTO_PROOF.source }}</span>
@if (existing.PHOTO_PROOF.previewUnavailable) {
<span class="ckyc-existing__warn">stored file unavailable</span>
}
</div>
</div>
@if ((existing.PHOTO_PROOF.previewUrl || existing.PHOTO_PROOF.previewUrlBack) && isImage('PHOTO_PROOF')) {
<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>
}
@if (existing.PHOTO_PROOF.previewUrl) {
<button
mat-stroked-button
type="button"
class="ckyc-existing__view"
(click)="openDocument('PHOTO_PROOF')"
>
<fa-icon icon="eye"></fa-icon>
&nbsp;View document
</button>
}
<div class="ckyc-existing__meta">
<fa-icon icon="id-card"></fa-icon>
<span>{{ existing.PHOTO_PROOF.name }}</span>
@if (existing.PHOTO_PROOF.documentKey) {
<span class="ckyc-existing__key">{{ existing.PHOTO_PROOF.documentKey }}</span>
}
@if (existing.PHOTO_PROOF.fileCount && existing.PHOTO_PROOF.fileCount > 0) {
<span class="ckyc-existing__count">
{{ existing.PHOTO_PROOF.fileCount }} {{ existing.PHOTO_PROOF.fileCount === 1 ? 'file' : 'files' }}
</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)="openUploadDialog('PHOTO_PROOF')"
>
<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 (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>
}
</mat-card-content>
</mat-card>
<!-- Client Image -->
<mat-card class="ckyc-upload-card">
<mat-card-content>
<div class="ckyc-upload-header">
<mat-icon class="ckyc-upload-icon">person</mat-icon>
<span>{{ 'labels.inputs.Image of Client' | translate }}</span>
</div>
@if (existing.CLIENT_IMAGE.present) {
<div class="ckyc-existing">
<div class="ckyc-existing__head">
<fa-icon icon="check-circle" class="ckyc-existing__ok"></fa-icon>
<div>
<strong>Already available</strong>
<span class="ckyc-existing__source">from {{ existing.CLIENT_IMAGE.source }}</span>
@if (existing.CLIENT_IMAGE.previewUnavailable) {
<span class="ckyc-existing__warn">stored file unavailable</span>
}
</div>
</div>
@if (existing.CLIENT_IMAGE.previewUrl && isImage('CLIENT_IMAGE')) {
<img [src]="existing.CLIENT_IMAGE.previewUrl" alt="Client photo" class="ckyc-existing__thumb" />
}
@if (existing.CLIENT_IMAGE.previewUrl) {
<button
mat-stroked-button
type="button"
class="ckyc-existing__view"
(click)="openDocument('CLIENT_IMAGE')"
>
<fa-icon icon="eye"></fa-icon>
&nbsp;View document
</button>
}
<button
mat-stroked-button
type="button"
class="ckyc-existing__replace"
(click)="openUploadDialog('CLIENT_IMAGE')"
>
<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 (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>
}
</mat-card-content>
</mat-card>
<!-- Client Signature -->
<mat-card class="ckyc-upload-card">
<mat-card-content>
<div class="ckyc-upload-header">
<mat-icon class="ckyc-upload-icon">draw</mat-icon>
<span>{{ 'labels.inputs.Signature of Client' | translate }}</span>
</div>
@if (existing.CLIENT_SIGNATURE.present) {
<div class="ckyc-existing">
<div class="ckyc-existing__head">
<fa-icon icon="check-circle" class="ckyc-existing__ok"></fa-icon>
<div>
<strong>Already available</strong>
<span class="ckyc-existing__source">from {{ existing.CLIENT_SIGNATURE.source }}</span>
@if (existing.CLIENT_SIGNATURE.previewUnavailable) {
<span class="ckyc-existing__warn">stored file unavailable</span>
}
</div>
</div>
@if (existing.CLIENT_SIGNATURE.previewUrl && isImage('CLIENT_SIGNATURE')) {
<img
[src]="existing.CLIENT_SIGNATURE.previewUrl"
alt="Client signature"
class="ckyc-existing__thumb ckyc-existing__thumb--wide"
/>
}
@if (existing.CLIENT_SIGNATURE.previewUrl) {
<button
mat-stroked-button
type="button"
class="ckyc-existing__view"
(click)="openDocument('CLIENT_SIGNATURE')"
>
<fa-icon icon="eye"></fa-icon>
&nbsp;View document
</button>
}
<div class="ckyc-existing__meta">
<fa-icon icon="file-signature"></fa-icon>
<span>{{ existing.CLIENT_SIGNATURE.name }}</span>
</div>
<button
mat-stroked-button
type="button"
class="ckyc-existing__replace"
(click)="openUploadDialog('CLIENT_SIGNATURE')"
>
<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 (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>
}
</mat-card-content>
</mat-card>
</div>
}
</div>