Files
cbsnexora/src/app/ctsclearing/inward-batch-detail/item-decision-dialog/item-decision-dialog.component.html
T
niteshandClaude Sonnet 5 97d0545f07
build-run / Run Lint, Build and Deploy (push) Canceled after 0s
Publish Image in Docker Hub / build (push) Canceled after 0s
Playwright E2E Tests / Run Playwright Tests (push) Canceled after 0s
Tests / testRigor (push) Canceled after 0s
fix(chequebook,ctsclearing): surface global stock allocation in UI; fix decided-state check
Receive Stock form now displays the actual highest-allocated serial across
every series (not just silently prefilling the field), so the operator
can see and trust the auto-suggested range start rather than taking it
on faith.

Fixes a real bug where item.decision (which InwardClearingItem.java always
sets to the literal string "PENDING" at creation, never null) was checked
with a plain truthy test everywhere a "has this been decided yet" branch
was needed. Since "PENDING" is a non-empty string, that check was always
true — silently breaking the Decide/View button label, the collapsed
validation summary, the "Awaiting settlement" text, and the decision
dialog itself (which never showed the actual Pass/Return form, even for
still-pending items). Replaced with a proper isDecided() check everywhere.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 17:08:10 +05:30

388 lines
13 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!--
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/.
-->
<h1 mat-dialog-title>{{ 'labels.inputs.Cheque' | translate }} {{ item.chequeNumber }}</h1>
<div mat-dialog-content>
<div class="info-grid">
<div>
<span class="k">{{ 'labels.inputs.Payee Name' | translate }}</span
><span>{{ item.payeeName || '–' }}</span>
</div>
<div>
<span class="k">{{ 'labels.inputs.Amount' | translate }}</span
><span>{{ item.amount }}</span>
</div>
<div>
<span class="k">{{ 'labels.inputs.Account' | translate }}</span
><span>{{ item.accountNo || item.accountId || '–' }}</span>
</div>
@if (item.clientName) {
<div>
<span class="k">Account Holder</span><span>{{ item.clientName }}</span>
</div>
}
@if (accountInfo?.availableBalance !== null && accountInfo?.availableBalance !== undefined) {
<div>
<span class="k">Available Balance</span>
<span>
{{ accountInfo!.availableBalance }}
@if (balanceSufficient !== null) {
<span class="pill" [ngClass]="balanceSufficient ? 'ok' : 'bad'">
{{ balanceSufficient ? 'Sufficient' : 'Insufficient' }}
</span>
}
</span>
</div>
}
@if (
accountInfo?.onHoldAmount !== null && accountInfo?.onHoldAmount !== undefined && accountInfo!.onHoldAmount !== 0
) {
<div>
<span class="k">Existing Holds on Account</span><span>{{ accountInfo!.onHoldAmount }}</span>
</div>
}
<div>
<span class="k">Hold Placed</span><span>{{ item.holdPlaced ? (item.holdAmount ?? item.amount) : '–' }}</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">Instrument Details</h3>
<div class="info-grid">
@if (item.micrCode) {
<div>
<span class="k">MICR Code</span><span>{{ item.micrCode }}</span>
</div>
}
@if (item.ifsc) {
<div>
<span class="k">IFSC</span><span>{{ item.ifsc }}</span>
</div>
}
@if (item.instrumentDate) {
<div>
<span class="k">Instrument Date</span><span>{{ item.instrumentDate | dateFormat }}</span>
</div>
}
@if (item.amountInWords) {
<div>
<span class="k">Amount in Words</span><span>{{ item.amountInWords }}</span>
</div>
}
@if (item.drawerAccountNumberMasked) {
<div>
<span class="k">Drawer A/C (as on cheque)</span><span>{{ item.drawerAccountNumberMasked }}</span>
</div>
}
@if (item.draweeBankName) {
<div>
<span class="k">Drawee Bank</span><span>{{ item.draweeBankName }}</span>
</div>
}
@if (item.instrumentTypeDescription || item.instrumentType) {
<div>
<span class="k">Instrument Type</span><span>{{ item.instrumentTypeDescription || item.instrumentType }}</span>
</div>
}
@if (item.ctsReferenceNumber) {
<div>
<span class="k">CTS Reference</span><span>{{ item.ctsReferenceNumber }}</span>
</div>
}
</div>
<div class="section-header">
<h3 class="section-title">Automatic Validation</h3>
<button type="button" mat-stroked-button (click)="openValidationDetail()">Comparison Details</button>
</div>
@if (item.instrumentStopPaymentFlag) {
<div class="gate-warning">Stop payment was flagged on this instrument by the depositing bank.</div>
}
<div class="check-badges">
@for (rule of validationRules; track rule.key) {
<span class="pill" [ngClass]="ruleClass(rule)" [title]="rule.label">{{ rule.label }}</span>
}
@if (item.iqaStatus && item.iqaStatus !== 'NA') {
<span class="pill" [ngClass]="pillClass(item.iqaStatus)" matTooltip="Image Quality Assessment"
>IQA: {{ item.iqaStatus }}</span
>
}
</div>
@if (item.iqaDetail) {
<div class="muted">{{ item.iqaDetail }}</div>
}
@if (item.iqaSubChecks) {
<div class="muted">{{ item.iqaSubChecks }}</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>
}
}
}
@if (isDecided) {
<h3 class="section-title">Decision</h3>
<div class="decided-summary" [ngClass]="pillClass(item.decision)">
<div class="decided-headline">
<span class="pill" [ngClass]="pillClass(item.decision)">{{ item.decision }}</span>
@if (item.decisionedOn) {
<span class="muted">on {{ item.decisionedOn | dateFormat }}</span>
}
</div>
@if (item.returnReasonCode) {
<div class="compare-row">
<span class="compare-label">Return Reason Code</span
><span class="compare-value">{{ item.returnReasonCode }}</span>
</div>
}
@if (item.decisionReason) {
<div class="compare-row">
<span class="compare-label">Note</span><span class="compare-value">{{ item.decisionReason }}</span>
</div>
}
@if (item.settlementBatchId) {
<div class="compare-row">
<span class="compare-label">Settlement Batch</span
><span class="compare-value">#{{ item.settlementBatchId }}</span>
</div>
} @else if (item.decision === 'PASS') {
<div class="compare-row">
<span class="compare-label">Settlement</span><span class="compare-value">Awaiting settlement</span>
</div>
}
<div class="muted">
This cheque has already been decided and cannot be decided again — this view is read-only.
</div>
</div>
} @else {
<div class="mode-toggle">
<button
mat-stroked-button
type="button"
[class.active]="mode === 'PASS'"
color="primary"
(click)="setMode('PASS')"
>
{{ 'labels.buttons.Pass' | translate }}
</button>
<button
mat-stroked-button
type="button"
[class.active]="mode === 'RETURN'"
color="warn"
(click)="setMode('RETURN')"
>
{{ 'labels.buttons.Return' | translate }}
</button>
</div>
@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" />
</mat-form-field>
</form>
} @else {
<form [formGroup]="returnForm">
<mat-form-field appearance="fill">
<mat-label>{{ 'labels.inputs.Return Reason Code' | translate }}</mat-label>
<mat-select formControlName="returnReasonCode" required>
@for (c of reasonCodes; track c.id) {
<mat-option [value]="c.code">{{ c.code }} — {{ c.description }}</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field appearance="fill">
<mat-label>{{ 'labels.inputs.Reason' | translate }}</mat-label>
<input matInput formControlName="reason" />
</mat-form-field>
</form>
}
}
<div #lightboxRoot style="display: none"></div>
</div>
<div mat-dialog-actions align="end">
<button mat-raised-button type="button" mat-dialog-close>
{{ (isDecided ? 'labels.buttons.Close' : 'labels.buttons.Cancel') | translate }}
</button>
@if (!isDecided) {
<button
mat-raised-button
[color]="mode === 'RETURN' ? 'warn' : 'primary'"
(click)="submit()"
[disabled]="busy || (mode === 'RETURN' ? returnForm.invalid : !signatureVerified)"
>
{{ (mode === 'PASS' ? 'labels.buttons.Pass' : 'labels.buttons.Return') | translate }}
</button>
}
</div>