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>
156 lines
5.0 KiB
TypeScript
156 lines
5.0 KiB
TypeScript
/**
|
|
* 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);
|
|
}
|
|
}
|