fix(clients): minor-age threshold to 18, senior/PWD doc reload, in-app document preview dialog

- Raise isMinor threshold from age <10 to <18 (legal definition), cascading correctly to
  guardian requirement, KYC document filtering, and Education Level/Degree Qualification
  visibility (now shown for all ages, not hidden for minors).
- Load and display previously-uploaded Senior Citizen proof / PWD certificate documents on
  edit (view/replace/remove), instead of always showing an empty upload prompt.
- Fix seniorPwdDocsComplete to accept an existing certificate, not just a freshly-picked file.
- Add DocumentPreviewDialogComponent: a reusable in-app modal (image/PDF/download) used in
  place of window.open() for saved KYC/address/senior-citizen/PWD documents — avoids popup
  blockers (fired only after the async download resolved) and Chrome/Edge's failure to paint
  large base64 data: URLs on first open.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-28 15:49:53 +05:30
co-authored by Claude Sonnet 5
parent c1fc95d9b7
commit 79fedb66e5
10 changed files with 347 additions and 107 deletions
@@ -46,6 +46,7 @@ import { MatSelectTrigger } from '@angular/material/select';
import { MatTooltip } from '@angular/material/tooltip';
import { MatDialog, MAT_DIALOG_DATA, MatDialogContent, MatDialogClose } from '@angular/material/dialog';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
import { DocumentPreviewDialogComponent } from '../../shared/document-preview-dialog/document-preview-dialog.component';
import { DateFormatPipe } from '../../../pipes/date-format.pipe';
import { SanctionsVerifyDialogComponent } from './sanctions-verify-dialog.component';
import { DipVerifyDialogComponent } from './dip-verify-dialog.component';
@@ -98,6 +99,11 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
seniorCitizenProofFile: File | null = null;
pwdCertificateFile: File | null = null;
/** Previously-uploaded documents, loaded in edit mode so the certificate doesn't appear to
* "disappear" on reopening — the raw File properties above only ever hold a fresh selection. */
existingSeniorCitizenProofDoc: { id: string; name: string } | null = null;
existingPwdCertificateDoc: { id: string; name: string } | null = null;
/** Disability categories (per the Rights of Persons with Disabilities Act, 2016) — fetched from
* m_code_value (code "PwdCategory") via the client template, same as occupationCategoryOptions.
* Admins can add/reorder/deactivate entries from System -> Manage Codes without a code change. */
@@ -111,7 +117,8 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
get seniorPwdDocsComplete(): boolean {
const pwdOk =
!this.createClientForm.get('isPwd')?.value ||
(!!this.pwdCertificateFile && !!this.createClientForm.get('pwdCategoryId')?.value);
((!!this.pwdCertificateFile || !!this.existingPwdCertificateDoc) &&
!!this.createClientForm.get('pwdCategoryId')?.value);
return pwdOk;
}
@@ -141,6 +148,93 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
this.pwdCertificateFile = null;
}
/** Loads previously-uploaded senior-citizen-proof / PWD-certificate documents in edit mode —
* mirrors the KYC step's loadExistingSignature() pattern so re-opening edit shows what's
* already on file instead of an empty upload prompt. */
private loadExistingSeniorPwdDocuments(): void {
const clientId = this.clientData?.id;
if (!clientId) {
return;
}
this.clientService.getClientDocuments(clientId.toString()).subscribe({
next: (documents: any[]) => {
const seniorDoc = documents
?.filter((d: any) => d.name === 'seniorCitizenProof')
?.sort((a: any, b: any) => b.id - a.id)?.[0];
if (seniorDoc?.id) {
this.existingSeniorCitizenProofDoc = {
id: seniorDoc.id.toString(),
name: seniorDoc.fileName || 'Senior Citizen Proof'
};
}
const pwdDoc = documents
?.filter((d: any) => d.name === 'pwdCertificate')
?.sort((a: any, b: any) => b.id - a.id)?.[0];
if (pwdDoc?.id) {
this.existingPwdCertificateDoc = {
id: pwdDoc.id.toString(),
name: pwdDoc.fileName || 'Disability Certificate'
};
}
},
error: () => {}
});
}
viewExistingSeniorCitizenProof(): void {
const clientId = this.clientData?.id;
if (!clientId || !this.existingSeniorCitizenProofDoc) {
return;
}
this.clientService.downloadClientDocument(clientId.toString(), this.existingSeniorCitizenProofDoc.id).subscribe({
next: (blob: any) =>
this.dialog.open(DocumentPreviewDialogComponent, {
data: {
blob,
title: this.existingSeniorCitizenProofDoc?.name,
fileName: this.existingSeniorCitizenProofDoc?.name
}
}),
error: () => {}
});
}
removeExistingSeniorCitizenProof(): void {
const clientId = this.clientData?.id;
if (!clientId || !this.existingSeniorCitizenProofDoc) {
return;
}
this.clientService.deleteClientDocument(clientId.toString(), this.existingSeniorCitizenProofDoc.id).subscribe({
next: () => (this.existingSeniorCitizenProofDoc = null),
error: () => {}
});
}
viewExistingPwdCertificate(): void {
const clientId = this.clientData?.id;
if (!clientId || !this.existingPwdCertificateDoc) {
return;
}
this.clientService.downloadClientDocument(clientId.toString(), this.existingPwdCertificateDoc.id).subscribe({
next: (blob: any) =>
this.dialog.open(DocumentPreviewDialogComponent, {
data: { blob, title: this.existingPwdCertificateDoc?.name, fileName: this.existingPwdCertificateDoc?.name }
}),
error: () => {}
});
}
removeExistingPwdCertificate(): void {
const clientId = this.clientData?.id;
if (!clientId || !this.existingPwdCertificateDoc) {
return;
}
this.clientService.deleteClientDocument(clientId.toString(), this.existingPwdCertificateDoc.id).subscribe({
next: () => (this.existingPwdCertificateDoc = null),
error: () => {}
});
}
/** Minimum date allowed. */
minDate = new Date(2000, 0, 1);
/** Maximum date allowed. */
@@ -203,7 +297,7 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
];
/** Calculated age detail string (Y/M/D) */
calculatedAgeDetail: string | null = null;
/** Whether client is a minor (age < 10) */
/** Whether client is a minor (age < 18) */
isMinor = false;
/** Whether client is a senior citizen (age >= 60) — auto-detected from Date of Birth */
isSeniorCitizen = false;
@@ -600,6 +694,7 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
*/
private prefillForEdit() {
const data = this.clientData;
this.loadExistingSeniorPwdDocuments();
// Patch all base controls that exist on the form
this.createClientForm.patchValue({
@@ -980,7 +1075,7 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
this.calculatedAgeDetail = `${years} yr${years !== 1 ? 's' : ''} ${months} mo${months !== 1 ? 's' : ''} ${days} day${days !== 1 ? 's' : ''}`;
this.ageBreakdown = { years, months, days };
const wasMinor = this.isMinor;
this.isMinor = years < 10;
this.isMinor = years < 18;
this.isSeniorCitizen = years >= 60;
// Add/remove guardian form group based on minor status
if (this.isMinor && !wasMinor) {