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:
+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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user