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
+338
-1
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user