fix(branchday): block day close when cashiers holding cash are not reconciled or settled
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

This commit is contained in:
2026-09-02 12:06:08 +05:30
parent 63134f172a
commit d90719ad8d
4 changed files with 102 additions and 2 deletions
@@ -580,6 +580,33 @@
reversed afterwards. reversed afterwards.
</p> </p>
@if (hasUnreconciledOrUnsettledCashiers) {
<div class="pg-alert pg-alert--error bd-reconcile-block">
<mat-icon>block</mat-icon>
<div>
<strong>Cannot close the day — cashiers still holding cash have not been reconciled or settled.</strong>
<ul class="bd-reconcile-block__list">
@for (detail of unreconciledOrUnsettledCashierDetails; track detail.name) {
<li>
<strong>{{ detail.name }}</strong> —
@if (detail.notReconciled && detail.notSettled) {
Not Reconciled, Not Settled
} @else if (detail.notReconciled) {
Not Reconciled
} @else {
Not Settled
}
</li>
}
</ul>
<span class="db-muted"
>Each cashier must reconcile their cash and settle it back to the vault before the branch day can be
closed.</span
>
</div>
</div>
}
<div class="db-panel__actions"> <div class="db-panel__actions">
<button type="button" class="pg-btn pg-btn--primary" (click)="submitClose()" [disabled]="!canSubmitClose"> <button type="button" class="pg-btn pg-btn--primary" (click)="submitClose()" [disabled]="!canSubmitClose">
{{ isLateClose ? 'Force Close Day' : 'Close Day' }} {{ isLateClose ? 'Force Close Day' : 'Close Day' }}
@@ -295,3 +295,36 @@
} }
} }
} }
/* Alert shown when cashiers holding cash have not been reconciled or settled — blocks day close. */
.bd-reconcile-block {
display: flex;
align-items: flex-start;
gap: 10px;
margin: 14px 0;
padding: 12px 16px;
background: #fef2f2;
border: 1px solid #fecaca;
border-left: 4px solid #dc2626;
border-radius: 8px;
color: #991b1b;
font-size: 12px;
mat-icon {
font-size: 20px;
width: 20px;
height: 20px;
color: #dc2626;
flex-shrink: 0;
margin-top: 1px;
}
&__list {
margin: 6px 0 6px 16px;
padding: 0;
list-style: disc;
font-size: 12px;
color: #b91c1c;
line-height: 1.6;
}
}
@@ -539,13 +539,53 @@ export class BranchDayComponent implements OnInit {
return !!this.closeTarget && this.closeTarget.businessDate < this.today; return !!this.closeTarget && this.closeTarget.businessDate < this.today;
} }
/**
* True when any cashier who was given cash (currentTotal > 0) is still NOT reconciled
* or NOT settled — meaning outstanding cash hasn't been accounted for yet.
*/
get hasUnreconciledOrUnsettledCashiers(): boolean {
for (const t of this.eodTellers) {
for (const c of t.cashiers) {
if (c.currentTotal > 0 && (c.reconciled === false || c.alreadySettled === false)) {
return true;
}
}
}
return false;
}
/** Names of cashiers blocking the close, for the error message. */
get unreconciledOrUnsettledCashierDetails(): { name: string; notReconciled: boolean; notSettled: boolean }[] {
const result: { name: string; notReconciled: boolean; notSettled: boolean }[] = [];
for (const t of this.eodTellers) {
for (const c of t.cashiers) {
if (c.currentTotal > 0 && (c.reconciled === false || c.alreadySettled === false)) {
result.push({
name: c.cashierName,
notReconciled: c.reconciled === false,
notSettled: c.alreadySettled === false
});
}
}
}
return result;
}
get canSubmitClose(): boolean { get canSubmitClose(): boolean {
const hasFigure = this.closingCash != null || this.closeDenomTotal > 0; const hasFigure = this.closingCash != null || this.closeDenomTotal > 0;
if (!this.closeTarget || !hasFigure || this.closing) return false; if (!this.closeTarget || !hasFigure || this.closing) return false;
// Block while cashier/vault data is still loading — the reconciliation check can't run yet.
if (this.eodLoading || this.previewLoading) return false;
if (this.hasUnreconciledOrUnsettledCashiers) return false;
return !this.isLateClose || !!this.forceReason.trim(); return !this.isLateClose || !!this.forceReason.trim();
} }
submitClose(): void { submitClose(): void {
// Safety: re-check reconciliation even if the button was somehow enabled.
if (this.hasUnreconciledOrUnsettledCashiers || this.eodLoading || this.previewLoading) {
this.errorMsg = 'Cannot close — cashiers holding cash have not been reconciled or settled.';
return;
}
const day = this.closeTarget; const day = this.closeTarget;
const denominations = this.denominationPayload(this.closeCounts); const denominations = this.denominationPayload(this.closeCounts);
const figure = denominations.length ? this.closeDenomTotal : this.closingCash; const figure = denominations.length ? this.closeDenomTotal : this.closingCash;
+2 -2
View File
@@ -9,8 +9,8 @@
/* tslint:disable */ /* tslint:disable */
export default { export default {
'mifos_x': { 'mifos_x': {
'version': '260901', 'version': '260902',
'hash': '03f2f1d7' 'hash': '63134f17'
}, },
'allow_switching_backend_instance': true 'allow_switching_backend_instance': true
}; };