feat(ctsclearing): validation comparison popup, decided-item read-only view, and dialog-close fixes
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

Adds a "Comparison Details" popup on the inward decision dialog showing
exactly what each automatic rule compared (amount figures vs words,
instrument date vs its valid-until window, balance vs amount, RBI
Positive Pay threshold, IQA sub-checks) instead of just the pass/fail
pill, mirroring InwardClearingValidationService.java's actual logic.
For an already-decided item, the stale/postdated "as of" date now shows
the decision date rather than today's live date.

Once a cheque is decided, the batch table's Validation column collapses
to a single outcome pill instead of permanently showing all 7 rule
badges, and the item-decision dialog itself switches to a read-only
decision summary (outcome, decisioned-on, note, settlement status)
instead of a re-openable Pass/Return form — the "Decide" action becomes
"View" for decided rows.

Also fixes two silently-broken directives: matTooltip and mat-dialog-close
were used throughout the decision dialogs without MatTooltipModule /
MatDialogClose being imported into the standalone components, so every
tooltip and every close button did nothing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Jaanu423
2026-07-22 18:29:18 +05:30
co-authored by Claude Sonnet 5
parent 0207cf85a3
commit a22f8e7e45
8 changed files with 799 additions and 73 deletions
@@ -22,14 +22,34 @@
<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>
@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>
@@ -142,12 +162,73 @@
}
</div>
<h3 class="section-title">Automatic Validation</h3>
<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>
}
@@ -202,59 +283,105 @@
}
}
<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>
@if (item.decision) {
<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>
}
<mat-form-field appearance="fill">
<mat-label>{{ 'labels.inputs.Note' | translate }}</mat-label>
<input matInput formControlName="note" />
</mat-form-field>
</form>
@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 {
<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 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>{{ 'labels.buttons.Cancel' | translate }}</button>
<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 mat-raised-button type="button" mat-dialog-close>
{{ (item.decision ? 'labels.buttons.Close' : 'labels.buttons.Cancel') | translate }}
</button>
@if (!item.decision) {
<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>