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
@@ -0,0 +1,24 @@
<!--
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/.
-->
<h1 mat-dialog-title>{{ data.title || data.fileName || 'Document Preview' }}</h1>
<div mat-dialog-content class="doc-preview-content">
@if (isImage) {
<img [src]="previewUrl" [alt]="data.fileName || 'Document'" class="doc-preview-image" />
} @else if (isPdf) {
<iframe [src]="safeUrl" class="doc-preview-pdf" title="Document preview"></iframe>
} @else {
<p>Preview isn't available for this file type — use Download to view it.</p>
}
</div>
<mat-dialog-actions align="end">
<button mat-raised-button (click)="download()">Download</button>
<button mat-raised-button mat-dialog-close color="primary">Close</button>
</mat-dialog-actions>
@@ -0,0 +1,27 @@
/**
* 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/.
*/
.doc-preview-content {
display: flex;
justify-content: center;
align-items: center;
min-width: 320px;
max-width: 80vw;
}
.doc-preview-image {
max-width: 100%;
max-height: 70vh;
object-fit: contain;
}
.doc-preview-pdf {
width: 70vw;
height: 70vh;
border: none;
}
@@ -0,0 +1,81 @@
/**
* 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/.
*/
/** Angular Imports */
import { Component, OnDestroy, inject } from '@angular/core';
import {
MatDialogRef,
MAT_DIALOG_DATA,
MatDialogTitle,
MatDialogContent,
MatDialogActions,
MatDialogClose
} from '@angular/material/dialog';
import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
import { CdkScrollable } from '@angular/cdk/scrolling';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
export interface DocumentPreviewDialogData {
blob: Blob;
title?: string;
fileName?: string;
}
/**
* Generic in-app document preview dialog — shows an image inline or a PDF in an embedded
* viewer, with a Download fallback for anything else. Used in place of window.open() for
* saved KYC/address/senior-citizen/PWD documents: opening a new browser tab is subject to
* popup blockers (fires only after the async download resolves) and, for images, Chrome/Edge
* frequently fail to paint a huge base64 data: URL until the tab is manually refreshed.
*/
@Component({
selector: 'mifosx-document-preview-dialog',
templateUrl: './document-preview-dialog.component.html',
styleUrls: ['./document-preview-dialog.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
MatDialogTitle,
CdkScrollable,
MatDialogContent,
MatDialogActions,
MatDialogClose
]
})
export class DocumentPreviewDialogComponent implements OnDestroy {
dialogRef = inject<MatDialogRef<DocumentPreviewDialogComponent>>(MatDialogRef);
private sanitizer = inject(DomSanitizer);
data = inject<DocumentPreviewDialogData>(MAT_DIALOG_DATA);
private readonly objectUrl: string;
readonly safeUrl: SafeResourceUrl;
readonly isImage: boolean;
readonly isPdf: boolean;
constructor() {
this.objectUrl = URL.createObjectURL(this.data.blob);
this.safeUrl = this.sanitizer.bypassSecurityTrustResourceUrl(this.objectUrl);
const type = this.data.blob.type || '';
this.isImage = type.startsWith('image/');
this.isPdf = type === 'application/pdf';
}
ngOnDestroy(): void {
URL.revokeObjectURL(this.objectUrl);
}
get previewUrl(): string {
return this.objectUrl;
}
download(): void {
const a = document.createElement('a');
a.href = this.objectUrl;
a.download = this.data.fileName || 'document';
a.click();
}
}