feat(ctsclearing): single-panel signature verification for inward cheques
Publish Image in Docker Hub / build (push) Canceled after 0s
build-run / Run Lint, Build and Deploy (push) Canceled after 0s
Playwright E2E Tests / Run Playwright Tests (push) Canceled after 0s
Tests / testRigor (push) Canceled after 0s

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:
Jaanu423
2026-07-22 12:25:20 +05:30
co-authored by Claude Sonnet 5
parent 6d205c0ed2
commit 0207cf85a3
8 changed files with 1170 additions and 17 deletions
@@ -29,7 +29,7 @@
.mode-toggle {
display: flex;
gap: 8px;
margin-bottom: 12px;
margin: 16px 0 12px;
button.active {
background: rgb(25 118 210 / 12%);
@@ -39,3 +39,340 @@
mat-form-field {
width: 100%;
}
.section-title {
margin: 20px 0 8px;
font-size: 0.95rem;
}
.muted {
color: rgb(0 0 0 / 50%);
font-size: 0.82rem;
margin-bottom: 8px;
}
.image-row {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.cheque-image {
max-width: 260px;
max-height: 140px;
object-fit: contain;
border: 1px solid #ddd;
border-radius: 4px;
cursor: pointer;
background: #fafafa;
}
// the overlay boxes are positioned absolute relative to THIS wrapper, not the (possibly flex-centered)
// .cheque-stage — otherwise, once the stage centers the image via flexbox in fullscreen mode, the overlay's
// left/top coordinates (measured from the stage's own top-left corner) land nowhere near the actual image
.cheque-image-wrap {
position: relative;
display: inline-block;
line-height: 0;
transform-origin: center center;
}
.cheque-stage {
position: relative;
display: inline-block;
line-height: 0;
// fullscreen compare mode: the stage itself becomes a fixed, full-viewport backdrop so the cheque and
// every draggable signature overlay are shown as large as possible for a real side-by-side comparison
&.fullscreen-stage {
position: fixed;
inset: 0;
z-index: 2000;
display: flex;
align-items: center;
justify-content: center;
width: 100vw;
height: 100vh;
max-width: none;
overflow: auto;
background: rgb(0 0 0 / 92%);
.cheque-image {
width: auto;
max-width: 82vw;
max-height: 88vh;
cursor: zoom-in;
}
}
// matches the native fullscreen state directly (independent of Angular's class-binding timing), so the
// stage fills correctly the instant the browser promotes it to the top layer
&:fullscreen {
width: 100vw;
height: 100vh;
max-width: none;
display: flex;
align-items: center;
justify-content: center;
background: rgb(0 0 0 / 92%);
}
}
.fullscreen-close {
position: absolute;
top: 16px;
right: 16px;
z-index: 2010;
width: 36px;
height: 36px;
border-radius: 50%;
border: none;
background: rgb(255 255 255 / 15%);
color: #fff;
font-size: 16px;
cursor: pointer;
&:hover {
background: rgb(255 255 255 / 25%);
}
}
.fullscreen-zoom {
position: absolute;
top: 16px;
left: 50%;
transform: translateX(-50%);
z-index: 2010;
display: flex;
align-items: center;
gap: 6px;
padding: 4px 10px;
border-radius: 20px;
background: rgb(255 255 255 / 15%);
color: #fff;
font-size: 12px;
span {
min-width: 40px;
text-align: center;
font-variant-numeric: tabular-nums;
}
button {
width: 26px;
height: 26px;
border-radius: 50%;
border: none;
background: rgb(255 255 255 / 15%);
color: #fff;
font-size: 14px;
line-height: 1;
cursor: pointer;
&:hover {
background: rgb(255 255 255 / 25%);
}
}
}
.fullscreen-shelf {
position: absolute;
top: 16px;
left: 16px;
z-index: 2010;
display: flex;
flex-direction: column;
gap: 8px;
max-height: 88vh;
overflow-y: auto;
padding-right: 4px;
}
.shelf-item {
width: 150px;
padding: 4px;
border: 2px solid transparent;
border-radius: 4px;
background: #fff;
cursor: pointer;
text-align: center;
&.active {
border-color: #1976d2;
}
img {
display: block;
width: 100%;
height: 54px;
object-fit: contain;
}
span {
display: block;
margin-top: 2px;
color: #333;
font-size: 10px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
.sig-overlay {
position: absolute;
// must beat the fullscreen shelf/close-button (z-index: 2010) — they share the same stacking context
// (the fullscreen-stage), so a low z-index here meant the overlay silently rendered BEHIND the shelf
// wherever the two visually overlapped, e.g. when dragging it toward the top-left where the shelf sits
z-index: 2020;
box-sizing: border-box;
border: 1px dashed rgb(25 118 210 / 70%);
background: rgb(255 255 255 / 40%);
cursor: move;
user-select: none;
}
.sig-overlay-handle {
display: flex;
align-items: center;
justify-content: space-between;
gap: 4px;
padding: 1px 4px;
background: rgb(25 118 210 / 85%);
color: #fff;
font-size: 10px;
line-height: 1.6;
cursor: move;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.sig-overlay-close {
background: none;
border: none;
color: #fff;
cursor: pointer;
font-size: 11px;
line-height: 1;
padding: 0 2px;
}
.sig-overlay-img {
display: block;
width: 100%;
height: auto;
pointer-events: none;
}
.sig-overlay-resize {
position: absolute;
right: 0;
bottom: 0;
width: 12px;
height: 12px;
cursor: nwse-resize;
background: repeating-linear-gradient(135deg, rgb(25 118 210 / 80%) 0 2px, transparent 2px 4px);
}
.sig-overlay-opacity {
width: 44px;
height: 10px;
margin: 0 4px;
cursor: pointer;
}
.no-image {
padding: 12px;
border: 1px dashed #ccc;
border-radius: 4px;
color: rgb(0 0 0 / 40%);
font-size: 0.82rem;
&.small {
padding: 6px 10px;
width: 120px;
}
}
.check-badges {
display: flex;
gap: 6px;
flex-wrap: wrap;
margin-bottom: 6px;
}
.validation-detail {
margin-bottom: 8px;
}
.mode-label {
font-weight: 500;
}
.signer-row {
display: flex;
align-items: center;
gap: 14px;
padding: 8px 0;
border-bottom: 1px solid rgb(0 0 0 / 6%);
&:last-of-type {
border-bottom: none;
}
}
.signer-signature {
flex-shrink: 0;
}
.signature-image {
width: 140px;
height: 60px;
object-fit: contain;
border: 1px solid #ddd;
border-radius: 4px;
cursor: pointer;
background: #fafafa;
}
.signer-info {
flex: 1;
min-width: 0;
}
.signer-name {
font-weight: 500;
display: flex;
align-items: center;
gap: 8px;
}
.gate-warning {
color: #d32f2f;
font-size: 0.82rem;
margin-bottom: 8px;
}
.pill {
font-size: 11px;
padding: 2px 8px;
border-radius: 6px;
font-weight: 600;
white-space: nowrap;
&.ok {
background: #dff5e8;
color: #28c07a;
}
&.bad {
background: #fde3e3;
color: #d32f2f;
}
&.neutral {
background: #e0e0e0;
color: #757575;
}
}