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>
This commit is contained in:
@@ -45,6 +45,12 @@
|
|||||||
@if (showReceiveForm) {
|
@if (showReceiveForm) {
|
||||||
<mat-card class="add-form">
|
<mat-card class="add-form">
|
||||||
<mat-card-content>
|
<mat-card-content>
|
||||||
|
<div class="global-allocation-note">
|
||||||
|
{{ 'labels.text.Highest leaf serial allocated so far, across every series' | translate }}:
|
||||||
|
<strong>{{ globalHighestAllocated }}</strong>
|
||||||
|
— {{ 'labels.text.next available start' | translate }}:
|
||||||
|
<strong>{{ globalHighestAllocated + 1 }}</strong>
|
||||||
|
</div>
|
||||||
<form [formGroup]="receiveForm" (ngSubmit)="submitReceive()">
|
<form [formGroup]="receiveForm" (ngSubmit)="submitReceive()">
|
||||||
<div class="form-grid">
|
<div class="form-grid">
|
||||||
<mat-form-field appearance="fill">
|
<mat-form-field appearance="fill">
|
||||||
@@ -82,7 +88,12 @@
|
|||||||
<mat-form-field appearance="fill">
|
<mat-form-field appearance="fill">
|
||||||
<mat-label>{{ 'labels.inputs.Range Start' | translate }}</mat-label>
|
<mat-label>{{ 'labels.inputs.Range Start' | translate }}</mat-label>
|
||||||
<input matInput type="number" formControlName="rangeStart" required />
|
<input matInput type="number" formControlName="rangeStart" required />
|
||||||
<mat-hint>{{ 'labels.text.Auto-suggested to avoid overlap; editable' | translate }}</mat-hint>
|
<mat-hint
|
||||||
|
>{{ 'labels.text.Auto-suggested to avoid overlap across every series; editable' | translate }} ({{
|
||||||
|
globalHighestAllocated
|
||||||
|
}}
|
||||||
|
+ 1)</mat-hint
|
||||||
|
>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<mat-form-field appearance="fill">
|
<mat-form-field appearance="fill">
|
||||||
<mat-label>{{ 'labels.inputs.Range End' | translate }}</mat-label>
|
<mat-label>{{ 'labels.inputs.Range End' | translate }}</mat-label>
|
||||||
|
|||||||
@@ -92,6 +92,19 @@
|
|||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.global-allocation-note {
|
||||||
|
margin-bottom: 14px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: rgb(25 118 210 / 8%);
|
||||||
|
color: rgb(0 0 0 / 70%);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
|
||||||
|
strong {
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.form-grid {
|
.form-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||||
|
|||||||
@@ -281,17 +281,22 @@ export class StockManagementComponent implements OnInit {
|
|||||||
return start + leaves - 1;
|
return start + leaves - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Highest leaf-serial allocated so far, across every series/office — the actual number the "Range Start"
|
||||||
|
* suggestion is based on. Shown directly in the form (not just silently applied to the field) so the operator
|
||||||
|
* can see and trust the reasoning instead of taking a pre-filled number on faith. */
|
||||||
|
get globalHighestAllocated(): number {
|
||||||
|
return this.allStockLots.reduce((m, l) => Math.max(m, l.rangeEnd || 0), 0);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Next unused serial WITHIN this series, so a fresh receipt continues that series' own numbering —
|
* Next unused serial across EVERY series, not just the one being received — a real bank allocates leaf-serial
|
||||||
* each series is numbered independently (a separate stationery order from the press), so a different
|
* numbers from one bank-wide numbering authority, so two different cheque designs never legitimately reuse the
|
||||||
* series' stock is not relevant here.
|
* same range. Suggesting a start that only avoids the same series' own lots is exactly what let two series end
|
||||||
|
* up overlapping (and, once both got issued to the same account, an unresolvable serial collision) before the
|
||||||
|
* backend's own overlap check was widened to match (ChequeStockLotRepository.countOverlappingAnySeries).
|
||||||
*/
|
*/
|
||||||
private nextFreeSerial(): number {
|
private nextFreeSerial(): number {
|
||||||
const seriesId = this.receiveForm.get('seriesId')?.value;
|
return this.globalHighestAllocated + 1;
|
||||||
const maxEnd = this.allStockLots
|
|
||||||
.filter((l) => l.seriesId === seriesId)
|
|
||||||
.reduce((m, l) => Math.max(m, l.rangeEnd || 0), 0);
|
|
||||||
return maxEnd + 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onReceiveSeriesChange(): void {
|
onReceiveSeriesChange(): void {
|
||||||
|
|||||||
@@ -160,7 +160,7 @@
|
|||||||
<ng-container matColumnDef="checks">
|
<ng-container matColumnDef="checks">
|
||||||
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Validation' | translate }}</th>
|
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Validation' | translate }}</th>
|
||||||
<td mat-cell *matCellDef="let it">
|
<td mat-cell *matCellDef="let it">
|
||||||
@if (it.decision) {
|
@if (isDecided(it)) {
|
||||||
<span class="pill" [ngClass]="hasFailedChecks(it) ? 'bad' : 'ok'">
|
<span class="pill" [ngClass]="hasFailedChecks(it) ? 'bad' : 'ok'">
|
||||||
{{ hasFailedChecks(it) ? 'Some checks failed' : 'All checks passed' }}
|
{{ hasFailedChecks(it) ? 'Some checks failed' : 'All checks passed' }}
|
||||||
</span>
|
</span>
|
||||||
@@ -191,7 +191,7 @@
|
|||||||
<ng-container matColumnDef="decision">
|
<ng-container matColumnDef="decision">
|
||||||
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Decision' | translate }}</th>
|
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Decision' | translate }}</th>
|
||||||
<td mat-cell *matCellDef="let it">
|
<td mat-cell *matCellDef="let it">
|
||||||
@if (it.decision) {
|
@if (isDecided(it)) {
|
||||||
<span class="pill" [ngClass]="pillClass(it.decision)">{{ it.decision }}</span>
|
<span class="pill" [ngClass]="pillClass(it.decision)">{{ it.decision }}</span>
|
||||||
} @else {
|
} @else {
|
||||||
<span class="pill neutral">{{ it.itemStatus || 'PENDING' }}</span>
|
<span class="pill neutral">{{ it.itemStatus || 'PENDING' }}</span>
|
||||||
@@ -199,16 +199,18 @@
|
|||||||
@if (it.returnReasonCode) {
|
@if (it.returnReasonCode) {
|
||||||
<div class="muted">{{ it.returnReasonCode }}</div>
|
<div class="muted">{{ it.returnReasonCode }}</div>
|
||||||
}
|
}
|
||||||
@if (it.decisionedOn) {
|
@if (isDecided(it) && it.decisionedOn) {
|
||||||
<div class="muted">{{ it.decisionedOn | dateFormat }}</div>
|
<div class="muted">{{ it.decisionedOn | dateFormat }}</div>
|
||||||
}
|
}
|
||||||
@if (it.decisionReason) {
|
@if (isDecided(it) && it.decisionReason) {
|
||||||
<div class="muted">{{ it.decisionReason }}</div>
|
<div class="muted">{{ it.decisionReason }}</div>
|
||||||
}
|
}
|
||||||
@if (it.settlementBatchId) {
|
@if (isDecided(it)) {
|
||||||
<div class="muted">Settlement #{{ it.settlementBatchId }}</div>
|
@if (it.settlementBatchId) {
|
||||||
} @else if (it.decision) {
|
<div class="muted">Settlement #{{ it.settlementBatchId }}</div>
|
||||||
<div class="muted">Awaiting settlement</div>
|
} @else {
|
||||||
|
<div class="muted">Awaiting settlement</div>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</td>
|
</td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
@@ -244,7 +246,7 @@
|
|||||||
<th mat-header-cell *matHeaderCellDef></th>
|
<th mat-header-cell *matHeaderCellDef></th>
|
||||||
<td mat-cell *matCellDef="let it">
|
<td mat-cell *matCellDef="let it">
|
||||||
<button mat-button color="primary" (click)="decide(it)">
|
<button mat-button color="primary" (click)="decide(it)">
|
||||||
{{ (it.decision ? 'labels.buttons.View' : 'labels.buttons.Decide') | translate }}
|
{{ (isDecided(it) ? 'labels.buttons.View' : 'labels.buttons.Decide') | translate }}
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|||||||
@@ -209,6 +209,14 @@ export class InwardBatchDetailComponent implements OnInit, OnDestroy {
|
|||||||
return this.rules.some((rule) => this.ruleClass(item, rule) === 'bad') || item?.iqaStatus === 'FAIL';
|
return this.rules.some((rule) => this.ruleClass(item, rule) === 'bad') || item?.iqaStatus === 'FAIL';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** item.decision is NEVER null/empty — InwardClearingItem.java sets it to the literal string "PENDING" at
|
||||||
|
* creation, then "PASS"/"RETURN" once decided. A plain truthy check on it.decision is always true, which
|
||||||
|
* silently broke every "is this item decided yet" branch (View-vs-Decide, read-only summary, collapsed
|
||||||
|
* validation) into always showing the decided state, even for a still-PENDING item. */
|
||||||
|
isDecided(item: any): boolean {
|
||||||
|
return item?.decision === 'PASS' || item?.decision === 'RETURN';
|
||||||
|
}
|
||||||
|
|
||||||
runValidation(): void {
|
runValidation(): void {
|
||||||
if (this.busy) {
|
if (this.busy) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
+3
-3
@@ -283,7 +283,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@if (item.decision) {
|
@if (isDecided) {
|
||||||
<h3 class="section-title">Decision</h3>
|
<h3 class="section-title">Decision</h3>
|
||||||
<div class="decided-summary" [ngClass]="pillClass(item.decision)">
|
<div class="decided-summary" [ngClass]="pillClass(item.decision)">
|
||||||
<div class="decided-headline">
|
<div class="decided-headline">
|
||||||
@@ -372,9 +372,9 @@
|
|||||||
|
|
||||||
<div mat-dialog-actions align="end">
|
<div mat-dialog-actions align="end">
|
||||||
<button mat-raised-button type="button" mat-dialog-close>
|
<button mat-raised-button type="button" mat-dialog-close>
|
||||||
{{ (item.decision ? 'labels.buttons.Close' : 'labels.buttons.Cancel') | translate }}
|
{{ (isDecided ? 'labels.buttons.Close' : 'labels.buttons.Cancel') | translate }}
|
||||||
</button>
|
</button>
|
||||||
@if (!item.decision) {
|
@if (!isDecided) {
|
||||||
<button
|
<button
|
||||||
mat-raised-button
|
mat-raised-button
|
||||||
[color]="mode === 'RETURN' ? 'warn' : 'primary'"
|
[color]="mode === 'RETURN' ? 'warn' : 'primary'"
|
||||||
|
|||||||
+8
@@ -185,6 +185,14 @@ export class ItemDecisionDialogComponent implements OnInit, OnDestroy {
|
|||||||
return good ? 'ok' : 'bad';
|
return good ? 'ok' : 'bad';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** item.decision is NEVER null — InwardClearingItem.java sets it to the literal string "PENDING" at creation,
|
||||||
|
* then "PASS"/"RETURN" once decided. A plain truthy check on it is always true, which silently made this
|
||||||
|
* dialog show the read-only decision summary (and hide the Pass/Return form) for every item, including ones
|
||||||
|
* still genuinely PENDING. */
|
||||||
|
get isDecided(): boolean {
|
||||||
|
return this.item?.decision === 'PASS' || this.item?.decision === 'RETURN';
|
||||||
|
}
|
||||||
|
|
||||||
/** What each automatic rule actually compared (amount vs words, instrument date vs the valid-until window,
|
/** 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. */
|
* balance vs amount, etc.) instead of just the pass/fail pill — opened on demand, not always-on clutter. */
|
||||||
openValidationDetail(): void {
|
openValidationDetail(): void {
|
||||||
|
|||||||
@@ -3319,6 +3319,9 @@
|
|||||||
"Only series applicable to this account type are shown": "Only series applicable to this account type are shown",
|
"Only series applicable to this account type are shown": "Only series applicable to this account type are shown",
|
||||||
"Leaves handed over = books x the series book size": "Leaves handed over = books × the series book size",
|
"Leaves handed over = books x the series book size": "Leaves handed over = books × the series book size",
|
||||||
"Auto-suggested to avoid overlap; editable": "Auto-suggested to avoid overlap; editable",
|
"Auto-suggested to avoid overlap; editable": "Auto-suggested to avoid overlap; editable",
|
||||||
|
"Auto-suggested to avoid overlap across every series; editable": "Auto-suggested to avoid overlap across every series; editable",
|
||||||
|
"Highest leaf serial allocated so far, across every series": "Highest leaf serial allocated so far, across every series",
|
||||||
|
"next available start": "next available start",
|
||||||
"Calculated from books x book size": "Calculated from books × book size",
|
"Calculated from books x book size": "Calculated from books × book size",
|
||||||
"Searching...": "Searching...",
|
"Searching...": "Searching...",
|
||||||
"Type at least 4 digits of the account number": "Type at least 4 digits of the account number",
|
"Type at least 4 digits of the account number": "Type at least 4 digits of the account number",
|
||||||
|
|||||||
@@ -9,8 +9,8 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
export default {
|
export default {
|
||||||
'mifos_x': {
|
'mifos_x': {
|
||||||
'version': '260721',
|
'version': '260723',
|
||||||
'hash': '6d205c0e'
|
'hash': 'ca85bae4'
|
||||||
},
|
},
|
||||||
'allow_switching_backend_instance': true
|
'allow_switching_backend_instance': true
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user