feat(clients): draw-signature option for Specimen Signature, drop its webcam capture

DrawSignatureDialogComponent (a mouse/touchpad signature pad) already
existed and was used from the client profile view, but wasn't wired into
the KYC step used during client creation/edit — a stale comment there
even referred to a "draw-pad" option as if it were already present.
Wired it in as a third capture method alongside Choose & Crop, then
removed Use Webcam from the signature box specifically (kept for Client
Photograph) per follow-up feedback — a signature is drawn or scanned, not
photographed.

Refactored the file-picker/webcam/draw-pad paths' duplicated stage+upload
logic into shared applySignatureFile()/uploadSignatureFile() helpers.
This commit is contained in:
2026-07-30 12:28:08 +05:30
parent 29f1cdc646
commit 387bd060e1
2 changed files with 44 additions and 48 deletions
@@ -1481,8 +1481,8 @@
<input type="file" accept="image/*" hidden (change)="onSignatureSelected($event)" />
<fa-icon icon="crop"></fa-icon> Choose &amp; Crop
</label>
<button type="button" class="upload-box__btn upload-box__btn--secondary" (click)="captureWebcamSignature()">
<fa-icon icon="video"></fa-icon> Use Webcam
<button type="button" class="upload-box__btn upload-box__btn--secondary" (click)="drawSignature()">
<fa-icon icon="pen"></fa-icon> Draw Signature
</button>
</div>
@if (photoSigTouched) {
@@ -31,6 +31,7 @@ import { Dates } from 'app/core/utils/dates';
import { ProofType, normalizeProofType, groupProofsByCategory } from '../../shared/kyc-proof-types';
import { KycUploadHelperService } from '../../shared/kyc-upload-helper.service';
import { CaptureImageDialogComponent } from '../../clients-view/custom-dialogs/capture-image-dialog/capture-image-dialog.component';
import { DrawSignatureDialogComponent } from '../../clients-view/custom-dialogs/draw-signature-dialog/draw-signature-dialog.component';
import { dataUrlToFile } from 'app/shared/utils/data-url.util';
import { DocumentPreviewDialogComponent } from '../../shared/document-preview-dialog/document-preview-dialog.component';
@@ -1657,20 +1658,6 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
await this.openSignatureCropDialog(file);
}
/** Webcam capture entry point for the specimen signature — photographs a paper
* signature, then reuses the same crop dialog + staging/upload path as the file-picker
* flow above (additive to the existing draw-pad / upload options, not a replacement). */
async captureWebcamSignature(): Promise<void> {
const dialogRef = this.dialog.open(CaptureImageDialogComponent, {
data: { captureTarget: 'signature' }
});
dialogRef.afterClosed().subscribe(async (imageDataUrl: string) => {
if (!imageDataUrl) return;
const file = dataUrlToFile(imageDataUrl, 'webcam-signature.jpg');
await this.openSignatureCropDialog(file);
});
}
private async openSignatureCropDialog(file: File): Promise<void> {
const { ImageCropDialogComponent } = await import('./image-crop-dialog/image-crop-dialog.component');
const dialogRef = this.dialog.open(ImageCropDialogComponent, {
@@ -1680,42 +1667,51 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
});
dialogRef.afterClosed().subscribe((result: any) => {
if (result) {
this.signatureFile = result.croppedFile;
this.signaturePreview = result.previewUrl;
// In edit mode, delete old signature doc then upload new one
if (this.clientId) {
if (this.existingSignatureDocId) {
this.clientsService.deleteClientDocument(this.clientId, this.existingSignatureDocId).subscribe({
next: () => {
this.clientsService.uploadClientSignatureImage(this.clientId, result.croppedFile).subscribe({
next: (res: any) => {
if (res?.resourceId) this.existingSignatureDocId = res.resourceId.toString();
},
error: () => {}
});
},
error: () => {
this.clientsService.uploadClientSignatureImage(this.clientId, result.croppedFile).subscribe({
next: (res: any) => {
if (res?.resourceId) this.existingSignatureDocId = res.resourceId.toString();
},
error: () => {}
});
}
});
} else {
this.clientsService.uploadClientSignatureImage(this.clientId, result.croppedFile).subscribe({
next: (res: any) => {
if (res?.resourceId) this.existingSignatureDocId = res.resourceId.toString();
},
error: () => {}
});
}
}
this.applySignatureFile(result.croppedFile, result.previewUrl);
}
});
}
/** Draw-with-mouse/touchpad entry point for the specimen signature — same staging/upload
* path as the file-picker and webcam flows, minus the crop step (the pad's canvas is
* already the right shape/size). */
drawSignature(): void {
const dialogRef = this.dialog.open(DrawSignatureDialogComponent);
dialogRef.afterClosed().subscribe((file: File) => {
if (file) {
this.applySignatureFile(file, URL.createObjectURL(file));
}
});
}
/** Stages the signature locally and, in edit mode, persists it — replacing any existing
* signature doc first so a client never ends up with two. Shared by the file-picker, webcam
* and draw-pad entry points. */
private applySignatureFile(file: File, previewUrl: string): void {
this.signatureFile = file;
this.signaturePreview = previewUrl;
if (!this.clientId) {
return;
}
if (this.existingSignatureDocId) {
this.clientsService.deleteClientDocument(this.clientId, this.existingSignatureDocId).subscribe({
next: () => this.uploadSignatureFile(file),
error: () => this.uploadSignatureFile(file)
});
} else {
this.uploadSignatureFile(file);
}
}
private uploadSignatureFile(file: File): void {
this.clientsService.uploadClientSignatureImage(this.clientId, file).subscribe({
next: (res: any) => {
if (res?.resourceId) this.existingSignatureDocId = res.resourceId.toString();
},
error: () => {}
});
}
removePhoto() {
this.photographFile = null;
this.photographPreview = null;