feat(ctsclearing): single-panel signature verification for inward cheques
Adds a rich decide dialog for clerks verifying inward CTS cheques: cheque images, all automatic validation results, and a mandatory signature-check gate built from the account's mode of operation and joint holders (backed by a new SignatoryService reusing the existing signature/document fetch pattern from the cashier module). Also adds a fullscreen compare mode: the cheque opens large with a shelf of every mandatory signer's stored signature, which can be dragged onto the cheque, resized, made semi-transparent, and zoomed, for a real side-by-side signature match instead of squinting at a thumbnail. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
6d205c0ed2
commit
0207cf85a3
+184
-1
@@ -22,8 +22,186 @@
|
||||
<span class="k">{{ 'labels.inputs.Account' | translate }}</span
|
||||
><span>{{ item.accountNo || item.accountId || '–' }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="k">Hold Placed</span><span>{{ item.holdPlaced ? (item.holdAmount ?? item.amount) : '–' }}</span>
|
||||
</div>
|
||||
@if (accountInfo?.availableBalance !== null && accountInfo?.availableBalance !== undefined) {
|
||||
<div>
|
||||
<span class="k">Available Balance</span><span>{{ accountInfo!.availableBalance }}</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<h3 class="section-title">Cheque Image</h3>
|
||||
<div class="muted">
|
||||
Click the cheque to go fullscreen, then drag any signer's stored signature from the shelf onto it and resize for a
|
||||
close match.
|
||||
</div>
|
||||
<div class="image-row">
|
||||
<div #chequeStage class="cheque-stage" [class.fullscreen-stage]="fullscreenActive">
|
||||
<div
|
||||
#chequeImageWrap
|
||||
class="cheque-image-wrap"
|
||||
[style.transform]="fullscreenActive ? 'scale(' + chequeZoom + ')' : null"
|
||||
(wheel)="onChequeWheel($event)"
|
||||
>
|
||||
@if (frontImageUrl) {
|
||||
<img
|
||||
[src]="frontImageUrl"
|
||||
class="cheque-image"
|
||||
alt="front"
|
||||
matTooltip="Click to compare fullscreen"
|
||||
(click)="toggleFullscreen()"
|
||||
/>
|
||||
} @else {
|
||||
<div class="no-image">No front image</div>
|
||||
}
|
||||
@if (fullscreenActive) {
|
||||
@for (signer of requiredSigners; track $index) {
|
||||
@if (signer.overlayVisible && signer.signatureUrl) {
|
||||
<div
|
||||
class="sig-overlay"
|
||||
[style.left.px]="signer.overlayX"
|
||||
[style.top.px]="signer.overlayY"
|
||||
[style.width.px]="signer.overlayWidth"
|
||||
[style.opacity]="signer.overlayOpacity"
|
||||
(mousedown)="startOverlayDrag($event, signer)"
|
||||
>
|
||||
<div class="sig-overlay-handle">
|
||||
<span>{{ signer.name }}</span>
|
||||
<input
|
||||
type="range"
|
||||
class="sig-overlay-opacity"
|
||||
min="0.3"
|
||||
max="1"
|
||||
step="0.05"
|
||||
[(ngModel)]="signer.overlayOpacity"
|
||||
(mousedown)="$event.stopPropagation()"
|
||||
matTooltip="Overlay opacity"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="sig-overlay-close"
|
||||
(mousedown)="$event.stopPropagation()"
|
||||
(click)="toggleOverlay(signer)"
|
||||
matTooltip="Hide overlay"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
<img [src]="signer.signatureUrl" class="sig-overlay-img" alt="signature overlay" draggable="false" />
|
||||
<div
|
||||
class="sig-overlay-resize"
|
||||
(mousedown)="startOverlayResize($event, signer)"
|
||||
matTooltip="Drag to resize"
|
||||
></div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
</div>
|
||||
@if (fullscreenActive) {
|
||||
<button type="button" class="fullscreen-close" (click)="toggleFullscreen()" matTooltip="Exit fullscreen (Esc)">
|
||||
✕
|
||||
</button>
|
||||
<div class="fullscreen-zoom">
|
||||
<button type="button" (click)="zoomOut()" matTooltip="Zoom out">−</button>
|
||||
<span>{{ chequeZoom * 100 | number: '1.0-0' }}%</span>
|
||||
<button type="button" (click)="zoomIn()" matTooltip="Zoom in">+</button>
|
||||
@if (chequeZoom !== 1) {
|
||||
<button type="button" (click)="resetZoom()" matTooltip="Reset zoom">⟲</button>
|
||||
}
|
||||
</div>
|
||||
<div class="fullscreen-shelf">
|
||||
@for (signer of requiredSigners; track $index) {
|
||||
@if (signer.signatureUrl) {
|
||||
<div
|
||||
class="shelf-item"
|
||||
[class.active]="signer.overlayVisible"
|
||||
(mousedown)="onShelfItemMouseDown($event, signer)"
|
||||
matTooltip="Drag onto the cheque"
|
||||
>
|
||||
<img [src]="signer.signatureUrl" alt="signature" draggable="false" />
|
||||
<span>{{ signer.name }}</span>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@if (backImageUrl) {
|
||||
<img
|
||||
[src]="backImageUrl"
|
||||
class="cheque-image"
|
||||
alt="back"
|
||||
matTooltip="Back — click to zoom"
|
||||
(click)="openImageZoom('back')"
|
||||
/>
|
||||
} @else {
|
||||
<div class="no-image">No back image</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<h3 class="section-title">Automatic Validation</h3>
|
||||
<div class="check-badges">
|
||||
@for (rule of validationRules; track rule.key) {
|
||||
<span class="pill" [ngClass]="ruleClass(rule)" [title]="rule.label">{{ rule.label }}</span>
|
||||
}
|
||||
</div>
|
||||
@if (item.validationDetail) {
|
||||
<div class="validation-detail muted">{{ item.validationDetail }}</div>
|
||||
}
|
||||
|
||||
<h3 class="section-title">Signature Verification</h3>
|
||||
@if (loadingAccountInfo) {
|
||||
<div class="muted">Loading account signatories…</div>
|
||||
} @else {
|
||||
@if (accountInfo) {
|
||||
<div class="mode-label muted">Mode of Operation: {{ modeOfOperationLabel }}</div>
|
||||
}
|
||||
@if (showManualOverride) {
|
||||
<div class="signer-row">
|
||||
<mat-checkbox [(ngModel)]="manualOverrideChecked">
|
||||
Account/signatory details unavailable — I have manually verified this signature
|
||||
</mat-checkbox>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="muted">
|
||||
{{
|
||||
signerSelectionMode === 'any'
|
||||
? 'Any one signatory below may sign — tick the one that matches.'
|
||||
: 'Every mandatory signatory below must be individually confirmed.'
|
||||
}}
|
||||
</div>
|
||||
@for (signer of requiredSigners; track $index) {
|
||||
<div class="signer-row">
|
||||
<div class="signer-signature">
|
||||
@if (signer.loadingSignature) {
|
||||
<div class="no-image small">Loading…</div>
|
||||
} @else if (signer.signatureUrl) {
|
||||
<img
|
||||
[src]="signer.signatureUrl"
|
||||
class="signature-image"
|
||||
alt="signature"
|
||||
matTooltip="Click to zoom"
|
||||
(click)="openSignatureZoom(signer)"
|
||||
/>
|
||||
} @else {
|
||||
<div class="no-image small">No signature on file</div>
|
||||
}
|
||||
</div>
|
||||
<div class="signer-info">
|
||||
<div class="signer-name">
|
||||
{{ signer.name }}
|
||||
<span class="pill" [ngClass]="signer.mandatory ? 'bad' : 'neutral'">{{ signer.role }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<mat-checkbox [(ngModel)]="signer.checked">Signature Matches</mat-checkbox>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
<div class="mode-toggle">
|
||||
<button mat-stroked-button type="button" [class.active]="mode === 'PASS'" color="primary" (click)="setMode('PASS')">
|
||||
{{ 'labels.buttons.Pass' | translate }}
|
||||
@@ -41,6 +219,9 @@
|
||||
|
||||
@if (mode === 'PASS') {
|
||||
<form [formGroup]="passForm">
|
||||
@if (!signatureVerified) {
|
||||
<div class="gate-warning">Signature verification required above before this cheque can be passed.</div>
|
||||
}
|
||||
<mat-form-field appearance="fill">
|
||||
<mat-label>{{ 'labels.inputs.Note' | translate }}</mat-label>
|
||||
<input matInput formControlName="note" />
|
||||
@@ -62,6 +243,8 @@
|
||||
</mat-form-field>
|
||||
</form>
|
||||
}
|
||||
|
||||
<div #lightboxRoot style="display: none"></div>
|
||||
</div>
|
||||
|
||||
<div mat-dialog-actions align="end">
|
||||
@@ -70,7 +253,7 @@
|
||||
mat-raised-button
|
||||
[color]="mode === 'RETURN' ? 'warn' : 'primary'"
|
||||
(click)="submit()"
|
||||
[disabled]="busy || (mode === 'RETURN' && returnForm.invalid)"
|
||||
[disabled]="busy || (mode === 'RETURN' ? returnForm.invalid : !signatureVerified)"
|
||||
>
|
||||
{{ (mode === 'PASS' ? 'labels.buttons.Pass' : 'labels.buttons.Return') | translate }}
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user