WEB-751 feat: add draw signature capability for clients (#3212)

This commit is contained in:
Shubham Kumar
2026-02-22 19:05:22 -06:00
committed by GitHub
parent f0db7491ab
commit 2e0d403a79
20 changed files with 206 additions and 0 deletions
@@ -18,6 +18,7 @@ import { UnassignStaffDialogComponent } from './custom-dialogs/unassign-staff-di
import { UploadSignatureDialogComponent } from './custom-dialogs/upload-signature-dialog/upload-signature-dialog.component';
import { ViewSignatureDialogComponent } from './custom-dialogs/view-signature-dialog/view-signature-dialog.component';
import { DeleteSignatureDialogComponent } from './custom-dialogs/delete-signature-dialog/delete-signature-dialog.component';
import { DrawSignatureDialogComponent } from './custom-dialogs/draw-signature-dialog/draw-signature-dialog.component';
import { DeleteDialogComponent } from 'app/shared/delete-dialog/delete-dialog.component';
import { UploadImageDialogComponent } from './custom-dialogs/upload-image-dialog/upload-image-dialog.component';
import { CaptureImageDialogComponent } from './custom-dialogs/capture-image-dialog/capture-image-dialog.component';
@@ -313,6 +314,8 @@ export class ClientsViewComponent implements OnInit {
viewSignatureDialogRef.afterClosed().subscribe((response: any) => {
if (response.upload) {
this.uploadSignature();
} else if (response.draw) {
this.drawSignature();
} else if (response.delete) {
this.deleteSignature();
}
@@ -334,6 +337,20 @@ export class ClientsViewComponent implements OnInit {
});
}
/**
* Opens draw pad for client signature
*/
private drawSignature() {
const drawSignatureDialogRef = this.dialog.open(DrawSignatureDialogComponent);
drawSignatureDialogRef.afterClosed().subscribe((signature: File) => {
if (signature) {
this.clientsService.uploadClientSignatureImage(this.clientViewData.id, signature).subscribe(() => {
this.reload();
});
}
});
}
/**
* Deletes client signature
*/
@@ -0,0 +1,28 @@
<!--
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/.
-->
<div class="layout-column gap-2px">
<h1 mat-dialog-title>{{ 'labels.heading.Draw Client Signature' | translate }}</h1>
<div class="signature-canvas-container">
<canvas #signatureCanvas class="signature-canvas"></canvas>
</div>
<div align="center">
<button mat-stroked-button color="primary" (click)="clear()">
{{ 'labels.buttons.Clear' | translate }}
</button>
</div>
<mat-dialog-actions align="end">
<button mat-raised-button mat-dialog-close>{{ 'labels.buttons.Cancel' | translate }}</button>
<button mat-raised-button color="primary" [disabled]="isEmpty" (click)="confirm()">
{{ 'labels.buttons.Confirm' | translate }}
</button>
</mat-dialog-actions>
</div>
@@ -0,0 +1,21 @@
/**
* 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/.
*/
.signature-canvas-container {
border: 1px solid #ccc;
border-radius: 4px;
margin: 0 auto;
width: 100%;
max-width: 500px;
}
.signature-canvas {
width: 100%;
height: 200px;
display: block;
}
@@ -0,0 +1,90 @@
/**
* 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, ViewChild, ElementRef, AfterViewInit, OnDestroy, inject } from '@angular/core';
import { MatDialogRef, MatDialogTitle, MatDialogActions, MatDialogClose } from '@angular/material/dialog';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
/** External Imports */
import SignaturePad from 'signature_pad';
/**
* Draw signature dialog component.
*/
@Component({
selector: 'mifosx-draw-signature-dialog',
templateUrl: './draw-signature-dialog.component.html',
styleUrls: ['./draw-signature-dialog.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
MatDialogTitle,
MatDialogActions,
MatDialogClose
]
})
export class DrawSignatureDialogComponent implements AfterViewInit, OnDestroy {
dialogRef = inject<MatDialogRef<DrawSignatureDialogComponent>>(MatDialogRef);
/** Canvas element reference */
@ViewChild('signatureCanvas', { static: true }) signatureCanvas: ElementRef<HTMLCanvasElement>;
/** Signature pad instance */
private signaturePad: SignaturePad;
/** Whether the pad has content */
isEmpty = true;
ngAfterViewInit() {
const canvas = this.signatureCanvas.nativeElement;
this.signaturePad = new SignaturePad(canvas, {
backgroundColor: 'rgb(255, 255, 255)'
});
this.signaturePad.addEventListener('endStroke', () => {
this.isEmpty = this.signaturePad.isEmpty();
});
this.resizeCanvas();
}
ngOnDestroy() {
if (this.signaturePad) {
this.signaturePad.off();
}
}
/** Resizes canvas to match its display size for crisp rendering */
private resizeCanvas() {
const canvas = this.signatureCanvas.nativeElement;
const ratio = Math.max(window.devicePixelRatio || 1, 1);
canvas.width = canvas.offsetWidth * ratio;
canvas.height = canvas.offsetHeight * ratio;
canvas.getContext('2d').scale(ratio, ratio);
this.signaturePad.clear();
}
/** Clears the signature pad */
clear() {
this.signaturePad.clear();
this.isEmpty = true;
}
/** Converts the drawn signature to a File and closes the dialog */
confirm() {
if (this.signaturePad.isEmpty()) {
return;
}
const canvas = this.signatureCanvas.nativeElement;
canvas.toBlob((blob: Blob) => {
const file = new File([blob], 'clientSignature.png', { type: 'image/png' });
this.dialogRef.close(file);
}, 'image/png');
}
}
@@ -31,5 +31,8 @@
<button mat-raised-button color="primary" [mat-dialog-close]="{ upload: true }">
{{ 'labels.buttons.Upload' | translate }}
</button>
<button mat-raised-button color="primary" [mat-dialog-close]="{ draw: true }">
{{ 'labels.buttons.Draw' | translate }}
</button>
}
</mat-dialog-actions>