From a22f8e7e454a7c71e0b440a771616d87f476fbfc Mon Sep 17 00:00:00 2001 From: Jaanu423 Date: Wed, 22 Jul 2026 18:29:18 +0530 Subject: [PATCH] feat(ctsclearing): validation comparison popup, decided-item read-only view, and dialog-close fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../inward-batch-detail.component.html | 56 +++-- .../inward-batch-detail.component.ts | 9 + .../item-decision-dialog.component.html | 231 ++++++++++++++---- .../item-decision-dialog.component.scss | 64 +++++ .../item-decision-dialog.component.ts | 43 +++- .../validation-detail-dialog.component.html | 199 +++++++++++++++ .../validation-detail-dialog.component.scss | 144 +++++++++++ .../validation-detail-dialog.component.ts | 126 ++++++++++ 8 files changed, 799 insertions(+), 73 deletions(-) create mode 100644 src/app/ctsclearing/inward-batch-detail/item-decision-dialog/validation-detail-dialog/validation-detail-dialog.component.html create mode 100644 src/app/ctsclearing/inward-batch-detail/item-decision-dialog/validation-detail-dialog/validation-detail-dialog.component.scss create mode 100644 src/app/ctsclearing/inward-batch-detail/item-decision-dialog/validation-detail-dialog/validation-detail-dialog.component.ts diff --git a/src/app/ctsclearing/inward-batch-detail/inward-batch-detail.component.html b/src/app/ctsclearing/inward-batch-detail/inward-batch-detail.component.html index bb99accff..f820cdea8 100644 --- a/src/app/ctsclearing/inward-batch-detail/inward-batch-detail.component.html +++ b/src/app/ctsclearing/inward-batch-detail/inward-batch-detail.component.html @@ -160,24 +160,31 @@ {{ 'labels.inputs.Validation' | translate }} -
- @for (rule of rules; track rule.key) { - {{ rule.label }} + @if (it.decision) { + + {{ hasFailedChecks(it) ? 'Some checks failed' : 'All checks passed' }} + +
See View → Comparison Details for the full breakdown
+ } @else { +
+ @for (rule of rules; track rule.key) { + {{ rule.label }} + } +
+ @if (it.validationDetail) { +
{{ it.validationDetail }}
+ } + @if (it.iqaStatus && it.iqaStatus !== 'NA') { + IQA: {{ it.iqaStatus }} + } + @if (it.iqaDetail) { +
{{ it.iqaDetail }}
+ } + @if (it.iqaSubChecks) { +
{{ it.iqaSubChecks }}
} -
- @if (it.validationDetail) { -
{{ it.validationDetail }}
- } - @if (it.iqaStatus && it.iqaStatus !== 'NA') { - IQA: {{ it.iqaStatus }} - } - @if (it.iqaDetail) { -
{{ it.iqaDetail }}
- } - @if (it.iqaSubChecks) { -
{{ it.iqaSubChecks }}
}
@@ -192,6 +199,17 @@ @if (it.returnReasonCode) {
{{ it.returnReasonCode }}
} + @if (it.decisionedOn) { +
{{ it.decisionedOn | dateFormat }}
+ } + @if (it.decisionReason) { +
{{ it.decisionReason }}
+ } + @if (it.settlementBatchId) { +
Settlement #{{ it.settlementBatchId }}
+ } @else if (it.decision) { +
Awaiting settlement
+ } @@ -225,7 +243,9 @@ - + diff --git a/src/app/ctsclearing/inward-batch-detail/inward-batch-detail.component.ts b/src/app/ctsclearing/inward-batch-detail/inward-batch-detail.component.ts index 14da7f8cc..b076a35f5 100644 --- a/src/app/ctsclearing/inward-batch-detail/inward-batch-detail.component.ts +++ b/src/app/ctsclearing/inward-batch-detail/inward-batch-detail.component.ts @@ -11,6 +11,7 @@ import { Component, ElementRef, OnDestroy, OnInit, ViewChild, inject } from '@an import { ActivatedRoute, RouterLink } from '@angular/router'; import { MatButtonModule } from '@angular/material/button'; import { MatTableModule } from '@angular/material/table'; +import { MatTooltipModule } from '@angular/material/tooltip'; import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar'; import { MatDialog } from '@angular/material/dialog'; import { FaIconComponent } from '@fortawesome/angular-fontawesome'; @@ -42,6 +43,7 @@ import { pillClass, clearingError } from '../../clearing-network/clearing-status MatButtonModule, MatTableModule, MatSnackBarModule, + MatTooltipModule, FaIconComponent ] }) @@ -200,6 +202,13 @@ export class InwardBatchDetailComponent implements OnInit, OnDestroy { return good ? 'ok' : 'bad'; } + /** Once a cheque is decided, the clerk doesn't need the full rule-by-rule checklist sitting in the table + * row forever — a one-line outcome is enough, with the full breakdown one click away via View -> Comparison + * Details. Kept for PENDING items, where the full checklist is exactly what's needed to decide. */ + hasFailedChecks(item: any): boolean { + return this.rules.some((rule) => this.ruleClass(item, rule) === 'bad') || item?.iqaStatus === 'FAIL'; + } + runValidation(): void { if (this.busy) { return; diff --git a/src/app/ctsclearing/inward-batch-detail/item-decision-dialog/item-decision-dialog.component.html b/src/app/ctsclearing/inward-batch-detail/item-decision-dialog/item-decision-dialog.component.html index 55a4e89cb..f0c920fac 100644 --- a/src/app/ctsclearing/inward-batch-detail/item-decision-dialog/item-decision-dialog.component.html +++ b/src/app/ctsclearing/inward-batch-detail/item-decision-dialog/item-decision-dialog.component.html @@ -22,14 +22,34 @@ {{ 'labels.inputs.Account' | translate }}{{ item.accountNo || item.accountId || '–' }} + @if (item.clientName) { +
+ Account Holder{{ item.clientName }} +
+ } + @if (accountInfo?.availableBalance !== null && accountInfo?.availableBalance !== undefined) { +
+ Available Balance + + {{ accountInfo!.availableBalance }} + @if (balanceSufficient !== null) { + + {{ balanceSufficient ? 'Sufficient' : 'Insufficient' }} + + } + +
+ } + @if ( + accountInfo?.onHoldAmount !== null && accountInfo?.onHoldAmount !== undefined && accountInfo!.onHoldAmount !== 0 + ) { +
+ Existing Holds on Account{{ accountInfo!.onHoldAmount }} +
+ }
Hold Placed{{ item.holdPlaced ? (item.holdAmount ?? item.amount) : '–' }}
- @if (accountInfo?.availableBalance !== null && accountInfo?.availableBalance !== undefined) { -
- Available Balance{{ accountInfo!.availableBalance }} -
- }

Cheque Image

@@ -142,12 +162,73 @@ } -

Automatic Validation

+

Instrument Details

+
+ @if (item.micrCode) { +
+ MICR Code{{ item.micrCode }} +
+ } + @if (item.ifsc) { +
+ IFSC{{ item.ifsc }} +
+ } + @if (item.instrumentDate) { +
+ Instrument Date{{ item.instrumentDate | dateFormat }} +
+ } + @if (item.amountInWords) { +
+ Amount in Words{{ item.amountInWords }} +
+ } + @if (item.drawerAccountNumberMasked) { +
+ Drawer A/C (as on cheque){{ item.drawerAccountNumberMasked }} +
+ } + @if (item.draweeBankName) { +
+ Drawee Bank{{ item.draweeBankName }} +
+ } + @if (item.instrumentTypeDescription || item.instrumentType) { +
+ Instrument Type{{ item.instrumentTypeDescription || item.instrumentType }} +
+ } + @if (item.ctsReferenceNumber) { +
+ CTS Reference{{ item.ctsReferenceNumber }} +
+ } +
+ +
+

Automatic Validation

+ +
+ @if (item.instrumentStopPaymentFlag) { +
Stop payment was flagged on this instrument by the depositing bank.
+ }
@for (rule of validationRules; track rule.key) { {{ rule.label }} } + @if (item.iqaStatus && item.iqaStatus !== 'NA') { + IQA: {{ item.iqaStatus }} + }
+ @if (item.iqaDetail) { +
{{ item.iqaDetail }}
+ } + @if (item.iqaSubChecks) { +
{{ item.iqaSubChecks }}
+ } @if (item.validationDetail) {
{{ item.validationDetail }}
} @@ -202,59 +283,105 @@ } } -
- - -
- - @if (mode === 'PASS') { -
- @if (!signatureVerified) { -
Signature verification required above before this cheque can be passed.
+ @if (item.decision) { +

Decision

+
+
+ {{ item.decision }} + @if (item.decisionedOn) { + on {{ item.decisionedOn | dateFormat }} + } +
+ @if (item.returnReasonCode) { +
+ Return Reason Code{{ item.returnReasonCode }} +
} - - {{ 'labels.inputs.Note' | translate }} - - - + @if (item.decisionReason) { +
+ Note{{ item.decisionReason }} +
+ } + @if (item.settlementBatchId) { +
+ Settlement Batch#{{ item.settlementBatchId }} +
+ } @else if (item.decision === 'PASS') { +
+ SettlementAwaiting settlement +
+ } +
+ This cheque has already been decided and cannot be decided again — this view is read-only. +
+
} @else { -
- - {{ 'labels.inputs.Return Reason Code' | translate }} - - @for (c of reasonCodes; track c.id) { - {{ c.code }} — {{ c.description }} - } - - - - {{ 'labels.inputs.Reason' | translate }} - - -
+
+ + +
+ + @if (mode === 'PASS') { +
+ @if (!signatureVerified) { +
Signature verification required above before this cheque can be passed.
+ } + + {{ 'labels.inputs.Note' | translate }} + + +
+ } @else { +
+ + {{ 'labels.inputs.Return Reason Code' | translate }} + + @for (c of reasonCodes; track c.id) { + {{ c.code }} — {{ c.description }} + } + + + + {{ 'labels.inputs.Reason' | translate }} + + +
+ } }
- - + @if (!item.decision) { + + }
diff --git a/src/app/ctsclearing/inward-batch-detail/item-decision-dialog/item-decision-dialog.component.scss b/src/app/ctsclearing/inward-batch-detail/item-decision-dialog/item-decision-dialog.component.scss index cf95aacaa..7285f830a 100644 --- a/src/app/ctsclearing/inward-batch-detail/item-decision-dialog/item-decision-dialog.component.scss +++ b/src/app/ctsclearing/inward-batch-detail/item-decision-dialog/item-decision-dialog.component.scss @@ -24,6 +24,11 @@ color: rgb(0 0 0 / 50%); margin-bottom: 2px; } + + .pill { + margin-left: 6px; + vertical-align: middle; + } } .mode-toggle { @@ -36,6 +41,54 @@ } } +.decided-summary { + padding: 14px 16px; + border-radius: 8px; + border-left: 4px solid rgb(0 0 0 / 30%); + background: rgb(0 0 0 / 4%); + + &.ok { + border-left-color: #1a9c63; + background: #e8f8f0; + } + + &.bad { + border-left-color: #d32f2f; + background: #fdeaea; + } +} + +.decided-headline { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 10px; +} + +.compare-row { + display: grid; + grid-template-columns: minmax(120px, 38%) 1fr; + gap: 6px 16px; + padding: 4px 0; + border-top: 1px solid rgb(255 255 255 / 60%); + + &:first-of-type { + border-top: none; + } +} + +.compare-label { + color: rgb(0 0 0 / 55%); + font-size: 0.72rem; + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.compare-value { + font-size: 0.85rem; + word-break: break-word; +} + mat-form-field { width: 100%; } @@ -45,6 +98,17 @@ mat-form-field { font-size: 0.95rem; } +.section-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + + .section-title { + margin: 20px 0 8px; + } +} + .muted { color: rgb(0 0 0 / 50%); font-size: 0.82rem; diff --git a/src/app/ctsclearing/inward-batch-detail/item-decision-dialog/item-decision-dialog.component.ts b/src/app/ctsclearing/inward-batch-detail/item-decision-dialog/item-decision-dialog.component.ts index 0853bd33e..53a7aebc1 100644 --- a/src/app/ctsclearing/inward-batch-detail/item-decision-dialog/item-decision-dialog.component.ts +++ b/src/app/ctsclearing/inward-batch-detail/item-decision-dialog/item-decision-dialog.component.ts @@ -10,14 +10,17 @@ import { Component, ElementRef, HostListener, OnDestroy, OnInit, ViewChild, inject } from '@angular/core'; import { FormsModule, UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; import { + MatDialog, MatDialogRef, MAT_DIALOG_DATA, MatDialogTitle, MatDialogContent, - MatDialogActions + MatDialogActions, + MatDialogClose } from '@angular/material/dialog'; import { CdkScrollable } from '@angular/cdk/scrolling'; import { MatButtonModule } from '@angular/material/button'; +import { MatTooltipModule } from '@angular/material/tooltip'; import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatSnackBar } from '@angular/material/snack-bar'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; @@ -31,7 +34,8 @@ import type { GalleryItem } from 'lightgallery/lg-utils'; import { CtsClearingService } from '../../ctsclearing.service'; import { ClearingService } from '../../../clearing-network/clearing.service'; import { SignatoryService, AccountSignatoryInfo, JointHolder } from '../../../clearing-network/signatory.service'; -import { clearingError } from '../../../clearing-network/clearing-status'; +import { clearingError, pillClass } from '../../../clearing-network/clearing-status'; +import { ValidationDetailDialogComponent } from './validation-detail-dialog/validation-detail-dialog.component'; interface RequiredSigner { clientId: number; @@ -72,8 +76,10 @@ const OVERLAY_DEFAULT_WIDTH = 160; CdkScrollable, MatDialogContent, MatDialogActions, + MatDialogClose, MatButtonModule, - MatCheckboxModule + MatCheckboxModule, + MatTooltipModule ] }) export class ItemDecisionDialogComponent implements OnInit, OnDestroy { @@ -84,6 +90,7 @@ export class ItemDecisionDialogComponent implements OnInit, OnDestroy { private clearingService = inject(ClearingService); private signatoryService = inject(SignatoryService); private snackBar = inject(MatSnackBar); + private dialog = inject(MatDialog); @ViewChild('lightboxRoot', { static: true }) lightboxRoot: ElementRef; @ViewChild('chequeStage') chequeStageEl?: ElementRef; @@ -165,6 +172,9 @@ export class ItemDecisionDialogComponent implements OnInit, OnDestroy { this.destroyLightbox(); } + /** Shared status-string -> pill-color mapping, same one the batch list table already uses for iqaStatus/decision. */ + pillClass = pillClass; + /** ok / bad / neutral for a single boolean validation rule, respecting whether true is the good outcome. */ ruleClass(rule: { key: string; goodWhenTrue: boolean }): 'ok' | 'bad' | 'neutral' { const value = this.item?.[rule.key]; @@ -175,6 +185,17 @@ export class ItemDecisionDialogComponent implements OnInit, OnDestroy { return good ? 'ok' : 'bad'; } + /** What each automatic rule actually compared (amount vs words, instrument date vs the valid-until window, + * balance vs amount, etc.) instead of just the pass/fail pill — opened on demand, not always-on clutter. */ + openValidationDetail(): void { + this.dialog.open(ValidationDetailDialogComponent, { + data: { item: this.item, accountInfo: this.accountInfo }, + width: '560px', + maxWidth: '95vw', + maxHeight: '85vh' + }); + } + private loadImages(): void { this.ctsService.getInwardItemImage(this.item.id, 'front').subscribe({ next: (url: string) => (this.frontImageUrl = url), @@ -558,6 +579,22 @@ export class ItemDecisionDialogComponent implements OnInit, OnDestroy { }; } + /** null when the account's balance couldn't be resolved at all — the automatic "Balance" check pill already + * covers that outcome, this just makes the actual sufficiency legible at a glance rather than requiring + * the clerk to mentally subtract Available Balance minus Amount themselves. */ + get balanceSufficient(): boolean | null { + const available = this.accountInfo?.availableBalance; + if ( + available === null || + available === undefined || + this.item?.amount === null || + this.item?.amount === undefined + ) { + return null; + } + return available >= this.item.amount; + } + get modeOfOperationLabel(): string { const labels: { [key: string]: string } = { EITHER_OR_SURVIVOR: 'Either or Survivor — any one signatory', diff --git a/src/app/ctsclearing/inward-batch-detail/item-decision-dialog/validation-detail-dialog/validation-detail-dialog.component.html b/src/app/ctsclearing/inward-batch-detail/item-decision-dialog/validation-detail-dialog/validation-detail-dialog.component.html new file mode 100644 index 000000000..2221cf77b --- /dev/null +++ b/src/app/ctsclearing/inward-batch-detail/item-decision-dialog/validation-detail-dialog/validation-detail-dialog.component.html @@ -0,0 +1,199 @@ + + +
+
+

Validation Comparison

+
Cheque {{ item.chequeNumber }} · what each automatic check actually compared
+
+ +
+ +
+
+
+ + Account +
+
+
+ Account Type{{ item.accountType || '–' }} +
+
+ Account Number{{ item.accountNo || item.accountId || '–' }} +
+
+ Account Holder{{ item.clientName || '–' }} +
+
+
+ +
+
+ + Balance +
+
+
+ Available Balance{{ accountInfo?.availableBalance ?? 'Not available' }} +
+
+ Cheque Amount{{ item.amount }} +
+
+ Result{{ + balanceSufficient === null ? 'Unknown' : balanceSufficient ? 'Sufficient' : 'Insufficient' + }} +
+
+
+ +
+
+ + Duplicate +
+
+
+ Checked AgainstSame cheque number + account + amount already PASSed, and the cheque leaf's own USED status +
+
+ Cheque Number{{ item.chequeNumber || '–' }} +
+
+
+ +
+
+ + Stop Payment +
+
+
+ Checked AgainstThe matching cheque leaf's own stop-payment status +
+ @if (item.instrumentStopPaymentFlag) { +
+ Depositing-Bank FlagStop payment was flagged on this instrument by the depositing bank +
+ } +
+
+ +
+
+ + Amount Match +
+
+
+ Amount (Figures){{ item.amount }} +
+
+ Amount (Words){{ item.amountInWords || 'Not captured' }} +
+ @if (amountWordsDigits) { +
+ Digits Extracted From Words{{ amountWordsDigits }} +
+ } +
+
+ +
+
+ + Stale / Postdated +
+
+
+ Instrument Date{{ item.instrumentDate ? (item.instrumentDate | dateFormat) : '–' }} +
+
+ Valid Until (+3 months){{ instrumentValidUntil ? (instrumentValidUntil | dateFormat) : '–' }} +
+
+ {{ asOfLabel }}{{ asOfDate | dateFormat }} +
+
+
+ +
+
+ + Positive Pay +
+
+
+ RBI PPS Threshold{{ positivePayThreshold }} +
+
+ Cheque Amount{{ item.amount }} +
+
+ Applicable{{ + positivePayApplicable ? 'Yes — at or above threshold' : 'No — below threshold' + }} +
+
+
+ + @if (item.iqaStatus && item.iqaStatus !== 'NA') { +
+
+ + Image Quality — {{ item.iqaStatus }} +
+ @if (item.iqaDetail || item.iqaSubChecks) { +
+ @if (item.iqaDetail) { +
+ Detail{{ item.iqaDetail }} +
+ } + @if (item.iqaSubChecks) { +
+ Sub-checks{{ item.iqaSubChecks }} +
+ } +
+ } +
+ } + + @if (item.validationDetail) { +
+ Backend Validation Notes +

{{ item.validationDetail }}

+
+ } +
+ +
+ +
diff --git a/src/app/ctsclearing/inward-batch-detail/item-decision-dialog/validation-detail-dialog/validation-detail-dialog.component.scss b/src/app/ctsclearing/inward-batch-detail/item-decision-dialog/validation-detail-dialog/validation-detail-dialog.component.scss new file mode 100644 index 000000000..85fed616b --- /dev/null +++ b/src/app/ctsclearing/inward-batch-detail/item-decision-dialog/validation-detail-dialog/validation-detail-dialog.component.scss @@ -0,0 +1,144 @@ +/** + * 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/. + */ + +$ok: #1a9c63; +$ok-bg: #e8f8f0; +$bad: #d32f2f; +$bad-bg: #fdeaea; +$neutral: #6b6b6b; +$neutral-bg: #f0f0f0; + +.dialog-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 12px; + padding: 20px 12px 4px 24px; + + [mat-dialog-title] { + margin: 0; + font-size: 1.15rem; + font-weight: 600; + } +} + +.dialog-subtitle { + margin-top: 2px; + color: rgb(0 0 0 / 55%); + font-size: 0.8rem; +} + +.header-close { + flex-shrink: 0; + color: rgb(0 0 0 / 45%); +} + +.rule-card { + margin-bottom: 12px; + padding: 14px 16px; + border-radius: 8px; + border-left: 4px solid $neutral; + background: $neutral-bg; + + &:last-of-type { + margin-bottom: 0; + } + + &.ok { + border-left-color: $ok; + background: $ok-bg; + + .rule-card-header { + color: $ok; + } + } + + &.bad { + border-left-color: $bad; + background: $bad-bg; + + .rule-card-header { + color: $bad; + } + } + + &.neutral { + .rule-card-header { + color: $neutral; + } + } +} + +.rule-card-header { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 10px; + font-size: 0.9rem; + font-weight: 600; + + fa-icon { + font-size: 0.95rem; + } +} + +.compare-list { + display: flex; + flex-direction: column; + gap: 6px; +} + +.compare-row { + display: grid; + grid-template-columns: minmax(120px, 38%) 1fr; + gap: 6px 16px; + padding: 4px 0; + border-top: 1px solid rgb(255 255 255 / 60%); + + &:first-child { + border-top: none; + } +} + +.compare-label { + color: rgb(0 0 0 / 55%); + font-size: 0.72rem; + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.compare-value { + font-size: 0.85rem; + word-break: break-word; + + &.warn-text { + color: $bad; + font-weight: 600; + } +} + +.backend-notes { + margin-top: 4px; + padding: 10px 14px; + border-radius: 8px; + background: rgb(0 0 0 / 4%); + font-size: 0.85rem; + + strong { + display: block; + margin-bottom: 4px; + color: rgb(0 0 0 / 55%); + font-size: 0.72rem; + text-transform: uppercase; + letter-spacing: 0.04em; + } + + p { + margin: 0; + } +} diff --git a/src/app/ctsclearing/inward-batch-detail/item-decision-dialog/validation-detail-dialog/validation-detail-dialog.component.ts b/src/app/ctsclearing/inward-batch-detail/item-decision-dialog/validation-detail-dialog/validation-detail-dialog.component.ts new file mode 100644 index 000000000..3eefcd764 --- /dev/null +++ b/src/app/ctsclearing/inward-batch-detail/item-decision-dialog/validation-detail-dialog/validation-detail-dialog.component.ts @@ -0,0 +1,126 @@ +/** + * 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/. + */ + +/** Angular Imports */ +import { Component, inject } from '@angular/core'; +import { + MatDialogRef, + MAT_DIALOG_DATA, + MatDialogTitle, + MatDialogContent, + MatDialogActions, + MatDialogClose +} from '@angular/material/dialog'; +import { CdkScrollable } from '@angular/cdk/scrolling'; +import { MatButtonModule } from '@angular/material/button'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { FaIconComponent } from '@fortawesome/angular-fontawesome'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; +import { pillClass } from '../../../../clearing-network/clearing-status'; +import { AccountSignatoryInfo } from '../../../../clearing-network/signatory.service'; + +/** RBI Positive Pay System threshold — mirrors InwardClearingValidationService.POSITIVE_PAY_THRESHOLD. */ +const POSITIVE_PAY_THRESHOLD = 50000; +/** Stale-instrument window — mirrors InwardClearingValidationService.STALE_MONTHS. */ +const STALE_MONTHS = 3; + +/** + * Shows exactly what each automatic validation rule actually compared, not just its pass/fail outcome — + * e.g. the amount in figures next to the amount in words, or the instrument date next to its valid-until + * date — mirroring the real values InwardClearingValidationService.java checks server-side. + */ +@Component({ + selector: 'mifosx-validation-detail-dialog', + templateUrl: './validation-detail-dialog.component.html', + styleUrls: ['./validation-detail-dialog.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + MatDialogTitle, + CdkScrollable, + MatDialogContent, + MatDialogActions, + MatDialogClose, + MatButtonModule, + MatTooltipModule, + FaIconComponent + ] +}) +export class ValidationDetailDialogComponent { + dialogRef = inject>(MatDialogRef); + data = inject(MAT_DIALOG_DATA); + + item: any = this.data?.item || {}; + accountInfo: AccountSignatoryInfo | null = this.data?.accountInfo || null; + + pillClass = pillClass; + + ruleClass(value: boolean | null | undefined, goodWhenTrue = true): 'ok' | 'bad' | 'neutral' { + if (value === null || value === undefined) { + return 'neutral'; + } + const good = goodWhenTrue ? value === true : value === false; + return good ? 'ok' : 'bad'; + } + + statusIcon(status: 'ok' | 'bad' | 'neutral'): string { + return status === 'ok' ? 'check-circle' : status === 'bad' ? 'times-circle' : 'minus-circle'; + } + + get balanceSufficient(): boolean | null { + const available = this.accountInfo?.availableBalance; + if ( + available === null || + available === undefined || + this.item?.amount === null || + this.item?.amount === undefined + ) { + return null; + } + return available >= this.item.amount; + } + + /** The digits InwardClearingValidationService.amountWordsFiguresMatch() extracts from amountInWords, for a + * transparent side-by-side with the figure amount rather than just trusting the pass/fail pill. */ + get amountWordsDigits(): string | null { + const words: string | undefined = this.item?.amountInWords; + if (!words) { + return null; + } + const digits = words.replace(/[^0-9]/g, ''); + return digits || null; + } + + get instrumentValidUntil(): Date | null { + if (!this.item?.instrumentDate) { + return null; + } + const d = new Date(this.item.instrumentDate); + d.setMonth(d.getMonth() + STALE_MONTHS); + return d; + } + + /** + * The date the stale/postdated check actually ran against — for an already-decided item that's when it was + * decided (decisionedOn), NOT today. Showing today's live date here for a cheque passed 50 days ago would + * make it look like the comparison is being silently re-run against the current date, which it isn't — the + * decision already happened, using whatever "today" was back then. + */ + get asOfDate(): Date { + return this.item?.decisionedOn ? new Date(this.item.decisionedOn) : new Date(); + } + + get asOfLabel(): string { + return this.item?.decisionedOn ? 'As Of Decision' : 'Today'; + } + + get positivePayApplicable(): boolean { + return (this.item?.amount ?? 0) >= POSITIVE_PAY_THRESHOLD; + } + + readonly positivePayThreshold = POSITIVE_PAY_THRESHOLD; +}