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>
388 lines
13 KiB
HTML
388 lines
13 KiB
HTML
<!--
|
||
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>
|