diff --git a/src/app/clients/client-stepper/client-general-step/client-general-step.component.html b/src/app/clients/client-stepper/client-general-step/client-general-step.component.html
index fc4fa013b..3483e695c 100644
--- a/src/app/clients/client-stepper/client-general-step/client-general-step.component.html
+++ b/src/app/clients/client-stepper/client-general-step/client-general-step.component.html
@@ -365,7 +365,7 @@
}
-
+
@if (isMinor && createClientForm.get('legalFormId')?.value === LegalFormId.PERSON) {
@@ -1035,69 +1035,67 @@
Education & Social Details
-
- @if (!isMinor) {
-
- Education Level
-
-
- @if (getSelectedEducationLevel()) {
-
- {{
- getEducationLevelIcon(getSelectedEducationLevel().name)
- }}
- {{ getSelectedEducationLevel().name }}
-
- }
-
- @for (el of educationLevelOptions; track el) {
-
-
- {{ getEducationLevelIcon(el.name) }}
- {{ el.name }}
-
-
+
+
+ Education Level
+
+
+ @if (getSelectedEducationLevel()) {
+
+ {{
+ getEducationLevelIcon(getSelectedEducationLevel().name)
+ }}
+ {{ getSelectedEducationLevel().name }}
+
}
-
-
+
+ @for (el of educationLevelOptions; track el) {
+
+
+ {{ getEducationLevelIcon(el.name) }}
+ {{ el.name }}
+
+
+ }
+
+
-
-
- Degree / Qualification
-
-
- @if (getSelectedDegreeQualification()) {
-
- {{
- getDegreeQualificationIcon(getSelectedDegreeQualification().name)
- }}
- {{ getSelectedDegreeQualification().name }}
-
- }
-
- @for (dq of filteredDegreeQualificationOptions; track dq) {
-
-
- {{ getDegreeQualificationIcon(dq.name) }}
- {{ dq.name }}
-
-
+
+
+ Degree / Qualification
+
+
+ @if (getSelectedDegreeQualification()) {
+
+ {{
+ getDegreeQualificationIcon(getSelectedDegreeQualification().name)
+ }}
+ {{ getSelectedDegreeQualification().name }}
+
}
- @if (!getSelectedEducationLevel()) {
-
- Please select Education Level first
-
- } @else if (filteredDegreeQualificationOptions.length === 0) {
-
- No options available
-
- }
-
-
- }
+
+ @for (dq of filteredDegreeQualificationOptions; track dq) {
+
+
+ {{ getDegreeQualificationIcon(dq.name) }}
+ {{ dq.name }}
+
+
+ }
+ @if (!getSelectedEducationLevel()) {
+
+ Please select Education Level first
+
+ } @else if (filteredDegreeQualificationOptions.length === 0) {
+
+ No options available
+
+ }
+
+
@@ -1322,6 +1320,8 @@
@if (seniorCitizenProofFile) {
{{ seniorCitizenProofFile.name }}
+ } @else if (existingSeniorCitizenProofDoc) {
+ {{ existingSeniorCitizenProofDoc.name }} (uploaded)
} @else {
Upload Senior Citizen Proof (Age / ID Proof) — optional
}
@@ -1330,6 +1330,13 @@
+ } @else if (existingSeniorCitizenProofDoc) {
+
+
}
}
@@ -1354,12 +1361,16 @@
diff --git a/src/app/clients/client-stepper/client-general-step/client-general-step.component.ts b/src/app/clients/client-stepper/client-general-step/client-general-step.component.ts
index 46edf0be4..21c7bfa4d 100644
--- a/src/app/clients/client-stepper/client-general-step/client-general-step.component.ts
+++ b/src/app/clients/client-stepper/client-general-step/client-general-step.component.ts
@@ -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) {
diff --git a/src/app/clients/client-stepper/client-india-address-step/client-india-address-step.component.ts b/src/app/clients/client-stepper/client-india-address-step/client-india-address-step.component.ts
index e02b48b3f..fdf2f9250 100644
--- a/src/app/clients/client-stepper/client-india-address-step/client-india-address-step.component.ts
+++ b/src/app/clients/client-stepper/client-india-address-step/client-india-address-step.component.ts
@@ -15,9 +15,11 @@ import { MatCheckbox } from '@angular/material/checkbox';
import { MatTabsModule } from '@angular/material/tabs';
import { MatIcon } from '@angular/material/icon';
import { MatIconButton, MatButton } from '@angular/material/button';
+import { MatDialog } from '@angular/material/dialog';
import { ClientsService } from '../../clients.service';
import { ProofType, normalizeProofType, groupProofsByCategory } from '../../shared/kyc-proof-types';
import { KycUploadHelperService } from '../../shared/kyc-upload-helper.service';
+import { DocumentPreviewDialogComponent } from '../../shared/document-preview-dialog/document-preview-dialog.component';
@Component({
selector: 'mifosx-client-india-address-step',
@@ -38,6 +40,7 @@ export class ClientIndiaAddressStepComponent implements OnInit, OnChanges {
private http = inject(HttpClient);
private clientsService = inject(ClientsService);
private kycUpload = inject(KycUploadHelperService);
+ private dialog = inject(MatDialog);
/** Client ID — when set, loads existing addresses for edit mode */
@Input() clientId: string = '';
@@ -633,33 +636,17 @@ export class ClientIndiaAddressStepComponent implements OnInit, OnChanges {
});
}
- /**
- * Open the saved address proof in a new browser tab.
- *
- * For images we already hold a base64 data URL in `previewUrl` from
- * `loadExistingAddressProof`, so we can hand it straight to `window.open`.
- * For PDFs / other types we need to fetch the binary blob and create a
- * temporary object URL so the browser's PDF viewer can render it.
- */
+ /** Open the saved address proof in an in-app preview dialog. */
viewExistingAddressProof(): void {
if (!this.existingAddressProofIdentifierId || this.existingAddressProofDocuments.length === 0) {
return;
}
const doc = this.existingAddressProofDocuments[0];
- if (doc.previewUrl) {
- window.open(doc.previewUrl, '_blank', 'noopener');
- return;
- }
this.clientsService.downloadClientIdentificationDocument(this.existingAddressProofIdentifierId, doc.id).subscribe({
- next: (blob: Blob) => {
- const url = URL.createObjectURL(blob);
- const w = window.open(url, '_blank', 'noopener');
- // Revoke after the tab has had time to load — browsers vary, so
- // err on the side of a generous wait.
- if (w) {
- setTimeout(() => URL.revokeObjectURL(url), 60_000);
- }
- },
+ next: (blob: Blob) =>
+ this.dialog.open(DocumentPreviewDialogComponent, {
+ data: { blob, title: doc.fileName || doc.name, fileName: doc.fileName || doc.name }
+ }),
error: () => {
// Silent fail — UI shows the saved name so user knows it exists.
}
diff --git a/src/app/clients/client-stepper/client-kyc-step/client-kyc-step.component.ts b/src/app/clients/client-stepper/client-kyc-step/client-kyc-step.component.ts
index 3a6f0b97a..85cb2c115 100644
--- a/src/app/clients/client-stepper/client-kyc-step/client-kyc-step.component.ts
+++ b/src/app/clients/client-stepper/client-kyc-step/client-kyc-step.component.ts
@@ -32,6 +32,7 @@ import { ProofType, normalizeProofType, groupProofsByCategory } from '../../shar
import { KycUploadHelperService } from '../../shared/kyc-upload-helper.service';
import { CaptureImageDialogComponent } from '../../clients-view/custom-dialogs/capture-image-dialog/capture-image-dialog.component';
import { dataUrlToFile } from 'app/shared/utils/data-url.util';
+import { DocumentPreviewDialogComponent } from '../../shared/document-preview-dialog/document-preview-dialog.component';
@Component({
selector: 'mifosx-client-kyc-step',
@@ -695,30 +696,17 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
this.kycDocMergedPreview = null;
}
- /**
- * Open the saved KYC ID-proof document in a new browser tab.
- *
- * Same flow as the address-step variant: images use the already-loaded
- * base64 data URL; PDFs (or anything without a cached preview) are fetched
- * fresh and rendered via a temporary blob URL.
- */
+ /** Open the saved KYC ID-proof document in an in-app preview dialog. */
viewExistingKycDoc(): void {
if (!this.existingKycIdentifierId || this.existingKycDocuments.length === 0) {
return;
}
const doc = this.existingKycDocuments[0];
- if (doc.previewUrl) {
- window.open(doc.previewUrl, '_blank', 'noopener');
- return;
- }
this.clientsService.downloadClientIdentificationDocument(this.existingKycIdentifierId, doc.id).subscribe({
- next: (blob: Blob) => {
- const url = URL.createObjectURL(blob);
- const w = window.open(url, '_blank', 'noopener');
- if (w) {
- setTimeout(() => URL.revokeObjectURL(url), 60_000);
- }
- },
+ next: (blob: Blob) =>
+ this.dialog.open(DocumentPreviewDialogComponent, {
+ data: { blob, title: doc.fileName || doc.name, fileName: doc.fileName || doc.name }
+ }),
error: () => {
/* silent — UI still shows the saved filename */
}
diff --git a/src/app/clients/client-stepper/client-preview-step/client-preview-step.component.html b/src/app/clients/client-stepper/client-preview-step/client-preview-step.component.html
index eefd2a892..00857b355 100644
--- a/src/app/clients/client-stepper/client-preview-step/client-preview-step.component.html
+++ b/src/app/clients/client-stepper/client-preview-step/client-preview-step.component.html
@@ -195,7 +195,7 @@
client.isMinor ? 'preview-badge preview-badge--danger' : 'preview-badge preview-badge--verified'
"
>
- {{ client.isMinor ? 'Yes (Age < 10)' : 'No' }}
+ {{ client.isMinor ? 'Yes (Age < 18)' : 'No' }}
diff --git a/src/app/clients/clients-view/personal-data-tab/personal-data-tab.component.html b/src/app/clients/clients-view/personal-data-tab/personal-data-tab.component.html
index 7086ffacb..42dcb79ba 100644
--- a/src/app/clients/clients-view/personal-data-tab/personal-data-tab.component.html
+++ b/src/app/clients/clients-view/personal-data-tab/personal-data-tab.component.html
@@ -105,7 +105,7 @@
[class.pd-verify-badge--verified]="!clientViewData.isMinor"
[class.pd-verify-badge--pending]="clientViewData.isMinor"
>
- {{ clientViewData.isMinor ? 'Yes (Age < 10)' : 'No' }}
+ {{ clientViewData.isMinor ? 'Yes (Age < 18)' : 'No' }}
diff --git a/src/app/clients/edit-client/edit-client.component.ts b/src/app/clients/edit-client/edit-client.component.ts
index b9c9f641e..f253ba7e9 100644
--- a/src/app/clients/edit-client/edit-client.component.ts
+++ b/src/app/clients/edit-client/edit-client.component.ts
@@ -440,6 +440,26 @@ export class EditClientComponent {
this.savingClient = false;
this.tabEditMode[tabIndex] = false;
+ // Senior Citizen / PWD supporting documents — same pattern as create-client's
+ // post-creation upload, triggered here since these are plain file inputs on the
+ // General step, not part of the JSON update payload.
+ if (this.clientGeneralStep?.seniorCitizenProofFile) {
+ const seniorCitizenProofForm = new FormData();
+ seniorCitizenProofForm.append('file', this.clientGeneralStep.seniorCitizenProofFile);
+ seniorCitizenProofForm.append('name', 'seniorCitizenProof');
+ seniorCitizenProofForm.append('description', 'Senior Citizen Proof (Age/ID Proof)');
+ this.clientsService
+ .uploadClientDocument(this.clientId, seniorCitizenProofForm)
+ .subscribe({ error: () => {} });
+ }
+ if (this.clientGeneralStep?.pwdCertificateFile) {
+ const pwdCertificateForm = new FormData();
+ pwdCertificateForm.append('file', this.clientGeneralStep.pwdCertificateFile);
+ pwdCertificateForm.append('name', 'pwdCertificate');
+ pwdCertificateForm.append('description', 'Disability Certificate');
+ this.clientsService.uploadClientDocument(this.clientId, pwdCertificateForm).subscribe({ error: () => {} });
+ }
+
// Log mobile/email changes as client notes for risk audit
this.logFieldChangeNotes(changedFields);
diff --git a/src/app/clients/shared/document-preview-dialog/document-preview-dialog.component.html b/src/app/clients/shared/document-preview-dialog/document-preview-dialog.component.html
new file mode 100644
index 000000000..bd1345b7e
--- /dev/null
+++ b/src/app/clients/shared/document-preview-dialog/document-preview-dialog.component.html
@@ -0,0 +1,24 @@
+
+
+
+ @if (isImage) {
+
![]()
+ } @else if (isPdf) {
+
+ } @else {
+
Preview isn't available for this file type — use Download to view it.
+ }
+
+
+