feat(cashier): add morning transfer & previous-day settlement hard block

- validateCashierHasMorningTransfer() blocks deposits & withdrawals when no today's cash transfer received
- validatePreviousDayClosed() blocks all transactions when previous day unsettled
- morningTransferReceived field added to cashier-day-status API response
- Warning banners shown in Savings, Current, FD, RD transaction forms
- Submit button disabled when either condition is active
- getCashierTotalCash SQL fixed: dm.denomination_value -> dm.denomination

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Jaanu423
2026-06-01 13:14:29 +05:30
co-authored by Claude Sonnet 4.6
parent f291e4da5b
commit 4ffeb6ee92
13 changed files with 575 additions and 8 deletions
+3
View File
@@ -125,6 +125,9 @@ export class CashierService {
if (asOfDate) params = params.set('asOfDate', asOfDate);
return this.http.get('/cashreports/cashier-position/me', { params });
}
getCashierDayStatus(): Observable<any> {
return this.http.get('/cashreports/cashier-day-status');
}
checkSettlement(cashierId: number, date: string): Observable<any> {
const params = new HttpParams().set('cashierId', String(cashierId)).set('date', date);
@@ -135,6 +135,40 @@
</div>
</div>
<!-- Previous-day settlement pending banner -->
@if (previousDayUnsettled) {
<div class="sat__day-close-banner sat__day-close-banner--withdrawal">
<mat-icon class="sat__day-close-ico">lock_clock</mat-icon>
<div class="sat__day-close-body">
<strong>Previous day not closed — {{ unsettledSince }}</strong>
<span>
Complete Cash Settlement for {{ unsettledSince }} before processing any transactions.
<b>All deposits and withdrawals are blocked.</b>
Pending cash: ₹{{ pendingAmount | number: '1.2-2' }}
</span>
</div>
<a class="sat__day-close-link" routerLink="/cashier/settlements">
<mat-icon>arrow_forward</mat-icon> Go to Settlement
</a>
</div>
}
<!-- Morning cash transfer not received banner -->
@if (!morningTransferReceived && !previousDayUnsettled) {
<div class="sat__day-close-banner sat__day-close-banner--withdrawal">
<mat-icon class="sat__day-close-ico">account_balance_wallet</mat-icon>
<div class="sat__day-close-body">
<strong>Morning cash transfer not received</strong>
<span>
No cash has been allocated to your drawer today. Request a morning transfer from the vault before processing
any transactions. <b>All deposits and withdrawals are blocked.</b>
</span>
</div>
<a class="sat__day-close-link" routerLink="/cashier/transfers">
<mat-icon>arrow_forward</mat-icon> Request Transfer
</a>
</div>
}
<form [formGroup]="currentAccountTransactionForm" (ngSubmit)="submit()" class="sat__form">
<!-- ─── LEFT COLUMN: denomination breakdown ────────────────────── -->
<section class="sat__panel sat__panel--denom">
@@ -514,7 +548,9 @@
[disabled]="
!currentAccountTransactionForm.valid ||
(denominationTotal > 0 && !denominationMatchesForm) ||
hasWithdrawalDenomOverdraft
hasWithdrawalDenomOverdraft ||
previousDayUnsettled ||
!morningTransferReceived
"
>
<fa-icon icon="check" class="m-r-6"></fa-icon>
@@ -1189,3 +1189,86 @@ $red: #b91c1c;
color: #b91c1c;
}
}
// ── Previous-day settlement pending banner ──
.sat__day-close-banner {
display: flex;
align-items: flex-start;
gap: 14px;
margin: 12px 20px 0;
padding: 14px 18px;
background: #fffbeb;
border: 1.5px solid #fcd34d;
border-left: 5px solid #f59e0b;
border-radius: 10px;
&--withdrawal {
background: #fff5f5;
border-color: #fca5a5;
border-left-color: #ef4444;
.sat__day-close-ico {
color: #ef4444;
}
.sat__day-close-link {
color: #b91c1c;
border-color: #fca5a5;
}
}
}
.sat__day-close-ico {
color: #b45309;
font-size: 22px;
height: 22px;
width: 22px;
flex-shrink: 0;
margin-top: 1px;
}
.sat__day-close-body {
flex: 1;
display: flex;
flex-direction: column;
gap: 3px;
strong {
font-size: 13px;
font-weight: 700;
color: #78350f;
}
span {
font-size: 12px;
color: #92400e;
line-height: 1.5;
}
b {
font-weight: 800;
color: #b91c1c;
}
}
.sat__day-close-link {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 5px 12px;
border-radius: 999px;
border: 1.5px solid #fcd34d;
background: #fff;
color: #92400e;
font-size: 12px;
font-weight: 700;
white-space: nowrap;
text-decoration: none;
flex-shrink: 0;
mat-icon {
font-size: 14px;
height: 14px;
width: 14px;
}
}
@@ -120,6 +120,11 @@ export class CurrentAccountTransactionsComponent implements OnInit {
openingTotal = 0;
openingLoaded = false;
previousDayUnsettled = false;
morningTransferReceived = true;
unsettledSince = '';
pendingAmount = 0;
get openingNotes(): any[] {
return this.openingDenoms.filter((d) => d.kind !== 'coin');
}
@@ -151,6 +156,7 @@ export class CurrentAccountTransactionsComponent implements OnInit {
this.loadDenominations();
this.loadContextStrip();
this.loadOpeningPosition();
this.loadDayStatus();
}
/** Load active denominations from the Denomination Master so the cash
@@ -203,6 +209,21 @@ export class CurrentAccountTransactionsComponent implements OnInit {
});
}
private loadDayStatus(): void {
this.cashierService.getCashierDayStatus().subscribe({
next: (res: any) => {
this.previousDayUnsettled = res?.previousDayUnsettled ?? false;
this.morningTransferReceived = res?.morningTransferReceived ?? true;
this.unsettledSince = res?.unsettledSince ?? '';
this.pendingAmount = Number(res?.pendingAmount ?? 0);
},
error: () => {
this.previousDayUnsettled = false;
this.morningTransferReceived = true;
}
});
}
private loadOpeningPosition(): void {
const today = new Date();
const asOfDate = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`;
@@ -390,7 +411,7 @@ export class CurrentAccountTransactionsComponent implements OnInit {
}
get withdrawalDenomOverdraft(): Array<{ label: string; available: number; requested: number }> {
if (!this.transactionType.withdrawal || !this.openingLoaded || this.openingDenoms.length === 0) return [];
if (!this.transactionType.withdrawal || !this.openingLoaded) return [];
const openMap = new Map<string, number>();
for (const d of this.openingDenoms) {
openMap.set(`${d.kind}:${d.denominationValue}`, Number(d.count ?? 0));
@@ -109,6 +109,40 @@
</div>
</div>
<!-- Previous-day settlement pending banner -->
@if (previousDayUnsettled) {
<div class="sat__day-close-banner sat__day-close-banner--withdrawal">
<mat-icon class="sat__day-close-ico">lock_clock</mat-icon>
<div class="sat__day-close-body">
<strong>Previous day not closed — {{ unsettledSince }}</strong>
<span>
Complete Cash Settlement for {{ unsettledSince }} before processing any transactions.
<b>All deposits and withdrawals are blocked.</b>
Pending cash: ₹{{ pendingAmount | number: '1.2-2' }}
</span>
</div>
<a class="sat__day-close-link" routerLink="/cashier/settlements">
<mat-icon>arrow_forward</mat-icon> Go to Settlement
</a>
</div>
}
<!-- Morning cash transfer not received banner -->
@if (!morningTransferReceived && !previousDayUnsettled) {
<div class="sat__day-close-banner sat__day-close-banner--withdrawal">
<mat-icon class="sat__day-close-ico">account_balance_wallet</mat-icon>
<div class="sat__day-close-body">
<strong>Morning cash transfer not received</strong>
<span>
No cash has been allocated to your drawer today. Request a morning transfer from the vault before processing
any transactions. <b>All deposits and withdrawals are blocked.</b>
</span>
</div>
<a class="sat__day-close-link" routerLink="/cashier/transfers">
<mat-icon>arrow_forward</mat-icon> Request Transfer
</a>
</div>
}
<form [formGroup]="accountTransactionForm" (ngSubmit)="submit()" class="sat__form">
<!-- LEFT: denomination breakdown -->
<section class="sat__panel sat__panel--denom">
@@ -480,7 +514,9 @@
[disabled]="
!accountTransactionForm.valid ||
(denominationTotal > 0 && !denominationMatchesForm) ||
hasWithdrawalDenomOverdraft
hasWithdrawalDenomOverdraft ||
previousDayUnsettled ||
!morningTransferReceived
"
>
<fa-icon icon="check" class="m-r-6"></fa-icon>
@@ -1101,3 +1101,86 @@ $amber-d: #b45309;
color: #b91c1c;
}
}
// ── Previous-day settlement pending banner ──
.sat__day-close-banner {
display: flex;
align-items: flex-start;
gap: 14px;
margin: 12px 20px 0;
padding: 14px 18px;
background: #fffbeb;
border: 1.5px solid #fcd34d;
border-left: 5px solid #f59e0b;
border-radius: 10px;
&--withdrawal {
background: #fff5f5;
border-color: #fca5a5;
border-left-color: #ef4444;
.sat__day-close-ico {
color: #ef4444;
}
.sat__day-close-link {
color: #b91c1c;
border-color: #fca5a5;
}
}
}
.sat__day-close-ico {
color: #b45309;
font-size: 22px;
height: 22px;
width: 22px;
flex-shrink: 0;
margin-top: 1px;
}
.sat__day-close-body {
flex: 1;
display: flex;
flex-direction: column;
gap: 3px;
strong {
font-size: 13px;
font-weight: 700;
color: #78350f;
}
span {
font-size: 12px;
color: #92400e;
line-height: 1.5;
}
b {
font-weight: 800;
color: #b91c1c;
}
}
.sat__day-close-link {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 5px 12px;
border-radius: 999px;
border: 1.5px solid #fcd34d;
background: #fff;
color: #92400e;
font-size: 12px;
font-weight: 700;
white-space: nowrap;
text-decoration: none;
flex-shrink: 0;
mat-icon {
font-size: 14px;
height: 14px;
width: 14px;
}
}
@@ -95,6 +95,11 @@ export class FixedDepositsCashTransactionComponent implements OnInit {
openingTotal = 0;
openingLoaded = false;
previousDayUnsettled = false;
morningTransferReceived = true;
unsettledSince = '';
pendingAmount = 0;
get openingNotes(): any[] {
return this.openingDenoms.filter((d) => d.kind !== 'coin');
}
@@ -130,6 +135,7 @@ export class FixedDepositsCashTransactionComponent implements OnInit {
this.loadDenominations();
this.loadContextStrip();
this.loadOpeningPosition();
this.loadDayStatus();
}
/** Load active denominations from the Denomination Master so the cash
@@ -180,6 +186,21 @@ export class FixedDepositsCashTransactionComponent implements OnInit {
});
}
private loadDayStatus(): void {
this.cashierService.getCashierDayStatus().subscribe({
next: (res: any) => {
this.previousDayUnsettled = res?.previousDayUnsettled ?? false;
this.morningTransferReceived = res?.morningTransferReceived ?? true;
this.unsettledSince = res?.unsettledSince ?? '';
this.pendingAmount = Number(res?.pendingAmount ?? 0);
},
error: () => {
this.previousDayUnsettled = false;
this.morningTransferReceived = true;
}
});
}
private loadOpeningPosition(): void {
const today = new Date();
const asOfDate = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`;
@@ -313,7 +334,7 @@ export class FixedDepositsCashTransactionComponent implements OnInit {
}
get withdrawalDenomOverdraft(): Array<{ label: string; available: number; requested: number }> {
if (!this.transactionType.withdrawal || !this.openingLoaded || this.openingDenoms.length === 0) return [];
if (!this.transactionType.withdrawal || !this.openingLoaded) return [];
const openMap = new Map<string, number>();
for (const d of this.openingDenoms) {
openMap.set(`${d.kind}:${d.denominationValue}`, Number(d.count ?? 0));
@@ -109,6 +109,40 @@
</div>
</div>
<!-- Previous-day settlement pending banner -->
@if (previousDayUnsettled) {
<div class="sat__day-close-banner sat__day-close-banner--withdrawal">
<mat-icon class="sat__day-close-ico">lock_clock</mat-icon>
<div class="sat__day-close-body">
<strong>Previous day not closed — {{ unsettledSince }}</strong>
<span>
Complete Cash Settlement for {{ unsettledSince }} before processing any transactions.
<b>All deposits and withdrawals are blocked.</b>
Pending cash: ₹{{ pendingAmount | number: '1.2-2' }}
</span>
</div>
<a class="sat__day-close-link" routerLink="/cashier/settlements">
<mat-icon>arrow_forward</mat-icon> Go to Settlement
</a>
</div>
}
<!-- Morning cash transfer not received banner -->
@if (!morningTransferReceived && !previousDayUnsettled) {
<div class="sat__day-close-banner sat__day-close-banner--withdrawal">
<mat-icon class="sat__day-close-ico">account_balance_wallet</mat-icon>
<div class="sat__day-close-body">
<strong>Morning cash transfer not received</strong>
<span>
No cash has been allocated to your drawer today. Request a morning transfer from the vault before processing
any transactions. <b>All deposits and withdrawals are blocked.</b>
</span>
</div>
<a class="sat__day-close-link" routerLink="/cashier/transfers">
<mat-icon>arrow_forward</mat-icon> Request Transfer
</a>
</div>
}
<form [formGroup]="depositRecurringDepositForm" (ngSubmit)="submit()" class="sat__form">
<!-- LEFT: denomination breakdown -->
<section class="sat__panel sat__panel--denom">
@@ -486,7 +520,9 @@
[disabled]="
!depositRecurringDepositForm.valid ||
(denominationTotal > 0 && !denominationMatchesForm) ||
hasWithdrawalDenomOverdraft
hasWithdrawalDenomOverdraft ||
previousDayUnsettled ||
!morningTransferReceived
"
>
<fa-icon icon="check" class="m-r-6"></fa-icon>
@@ -1101,3 +1101,86 @@ $amber-d: #b45309;
color: #b91c1c;
}
}
// ── Previous-day settlement pending banner ──
.sat__day-close-banner {
display: flex;
align-items: flex-start;
gap: 14px;
margin: 12px 20px 0;
padding: 14px 18px;
background: #fffbeb;
border: 1.5px solid #fcd34d;
border-left: 5px solid #f59e0b;
border-radius: 10px;
&--withdrawal {
background: #fff5f5;
border-color: #fca5a5;
border-left-color: #ef4444;
.sat__day-close-ico {
color: #ef4444;
}
.sat__day-close-link {
color: #b91c1c;
border-color: #fca5a5;
}
}
}
.sat__day-close-ico {
color: #b45309;
font-size: 22px;
height: 22px;
width: 22px;
flex-shrink: 0;
margin-top: 1px;
}
.sat__day-close-body {
flex: 1;
display: flex;
flex-direction: column;
gap: 3px;
strong {
font-size: 13px;
font-weight: 700;
color: #78350f;
}
span {
font-size: 12px;
color: #92400e;
line-height: 1.5;
}
b {
font-weight: 800;
color: #b91c1c;
}
}
.sat__day-close-link {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 5px 12px;
border-radius: 999px;
border: 1.5px solid #fcd34d;
background: #fff;
color: #92400e;
font-size: 12px;
font-weight: 700;
white-space: nowrap;
text-decoration: none;
flex-shrink: 0;
mat-icon {
font-size: 14px;
height: 14px;
width: 14px;
}
}
@@ -104,6 +104,11 @@ export class DepositRecurringDepositsAccountComponent implements OnInit {
openingDenoms: any[] = [];
openingTotal = 0;
previousDayUnsettled = false;
morningTransferReceived = true;
unsettledSince = '';
pendingAmount = 0;
openingLoaded = false;
get openingNotes(): any[] {
@@ -148,6 +153,7 @@ export class DepositRecurringDepositsAccountComponent implements OnInit {
this.loadDenominations();
this.loadContextStrip();
this.loadOpeningPosition();
this.loadDayStatus();
}
/** Load active denominations from the Denomination Master so the cash
@@ -198,6 +204,21 @@ export class DepositRecurringDepositsAccountComponent implements OnInit {
});
}
private loadDayStatus(): void {
this.cashierService.getCashierDayStatus().subscribe({
next: (res: any) => {
this.previousDayUnsettled = res?.previousDayUnsettled ?? false;
this.morningTransferReceived = res?.morningTransferReceived ?? true;
this.unsettledSince = res?.unsettledSince ?? '';
this.pendingAmount = Number(res?.pendingAmount ?? 0);
},
error: () => {
this.previousDayUnsettled = false;
this.morningTransferReceived = true;
}
});
}
private loadOpeningPosition(): void {
const today = new Date();
const asOfDate = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`;
@@ -334,7 +355,7 @@ export class DepositRecurringDepositsAccountComponent implements OnInit {
}
get withdrawalDenomOverdraft(): Array<{ label: string; available: number; requested: number }> {
if (!this.transactionType.withdrawal || !this.openingLoaded || this.openingDenoms.length === 0) return [];
if (!this.transactionType.withdrawal || !this.openingLoaded) return [];
const openMap = new Map<string, number>();
for (const d of this.openingDenoms) {
openMap.set(`${d.kind}:${d.denominationValue}`, Number(d.count ?? 0));
@@ -135,6 +135,40 @@
</div>
</div>
<!-- Previous-day settlement pending banner -->
@if (previousDayUnsettled) {
<div class="sat__day-close-banner sat__day-close-banner--withdrawal">
<mat-icon class="sat__day-close-ico">lock_clock</mat-icon>
<div class="sat__day-close-body">
<strong>Previous day not closed — {{ unsettledSince }}</strong>
<span>
Complete Cash Settlement for {{ unsettledSince }} before processing any transactions.
<b>All deposits and withdrawals are blocked.</b>
Pending cash: ₹{{ pendingAmount | number: '1.2-2' }}
</span>
</div>
<a class="sat__day-close-link" routerLink="/cashier/settlements">
<mat-icon>arrow_forward</mat-icon> Go to Settlement
</a>
</div>
}
<!-- Morning cash transfer not received banner -->
@if (!morningTransferReceived && !previousDayUnsettled) {
<div class="sat__day-close-banner sat__day-close-banner--withdrawal">
<mat-icon class="sat__day-close-ico">account_balance_wallet</mat-icon>
<div class="sat__day-close-body">
<strong>Morning cash transfer not received</strong>
<span>
No cash has been allocated to your drawer today. Request a morning transfer from the vault before processing
any transactions. <b>All deposits and withdrawals are blocked.</b>
</span>
</div>
<a class="sat__day-close-link" routerLink="/cashier/transfers">
<mat-icon>arrow_forward</mat-icon> Request Transfer
</a>
</div>
}
<form [formGroup]="savingAccountTransactionForm" (ngSubmit)="submit()" class="sat__form">
<!-- ─── LEFT COLUMN: denomination breakdown ────────────────────── -->
<section class="sat__panel sat__panel--denom">
@@ -514,7 +548,9 @@
[disabled]="
!savingAccountTransactionForm.valid ||
(denominationTotal > 0 && !denominationMatchesForm) ||
hasWithdrawalDenomOverdraft
hasWithdrawalDenomOverdraft ||
previousDayUnsettled ||
!morningTransferReceived
"
>
<fa-icon icon="check" class="m-r-6"></fa-icon>
@@ -1216,3 +1216,86 @@ $red: #b91c1c;
}
}
}
// ── Previous-day settlement pending banner ──
.sat__day-close-banner {
display: flex;
align-items: flex-start;
gap: 14px;
margin: 12px 20px 0;
padding: 14px 18px;
background: #fffbeb;
border: 1.5px solid #fcd34d;
border-left: 5px solid #f59e0b;
border-radius: 10px;
&--withdrawal {
background: #fff5f5;
border-color: #fca5a5;
border-left-color: #ef4444;
.sat__day-close-ico {
color: #ef4444;
}
.sat__day-close-link {
color: #b91c1c;
border-color: #fca5a5;
}
}
}
.sat__day-close-ico {
color: #b45309;
font-size: 22px;
height: 22px;
width: 22px;
flex-shrink: 0;
margin-top: 1px;
}
.sat__day-close-body {
flex: 1;
display: flex;
flex-direction: column;
gap: 3px;
strong {
font-size: 13px;
font-weight: 700;
color: #78350f;
}
span {
font-size: 12px;
color: #92400e;
line-height: 1.5;
}
b {
font-weight: 800;
color: #b91c1c;
}
}
.sat__day-close-link {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 5px 12px;
border-radius: 999px;
border: 1.5px solid #fcd34d;
background: #fff;
color: #92400e;
font-size: 12px;
font-weight: 700;
white-space: nowrap;
text-decoration: none;
flex-shrink: 0;
mat-icon {
font-size: 14px;
height: 14px;
width: 14px;
}
}
@@ -140,6 +140,15 @@ export class SavingsAccountTransactionsComponent implements OnInit {
/** True once the opening-position API has responded (success or error). */
openingLoaded = false;
/** True when cashier has an unsettled previous business day. */
previousDayUnsettled = false;
/** False when cashier has not received today's morning cash transfer. */
morningTransferReceived = true;
/** Date string of the earliest unsettled transfer (yyyy-MM-dd). */
unsettledSince = '';
/** Pending cash amount from previous day. */
pendingAmount = 0;
get openingNotes(): any[] {
return this.openingDenoms.filter((d) => d.kind !== 'coin');
}
@@ -171,6 +180,7 @@ export class SavingsAccountTransactionsComponent implements OnInit {
this.loadDenominations();
this.loadContextStrip();
this.loadOpeningPosition();
this.loadDayStatus();
}
/** Load active denominations from the Denomination Master so the cash
@@ -231,6 +241,21 @@ export class SavingsAccountTransactionsComponent implements OnInit {
});
}
private loadDayStatus(): void {
this.cashierService.getCashierDayStatus().subscribe({
next: (res: any) => {
this.previousDayUnsettled = res?.previousDayUnsettled ?? false;
this.morningTransferReceived = res?.morningTransferReceived ?? true;
this.unsettledSince = res?.unsettledSince ?? '';
this.pendingAmount = Number(res?.pendingAmount ?? 0);
},
error: () => {
this.previousDayUnsettled = false;
this.morningTransferReceived = true;
}
});
}
private loadOpeningPosition(): void {
const today = new Date();
const asOfDate = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`;
@@ -457,7 +482,7 @@ export class SavingsAccountTransactionsComponent implements OnInit {
/** Denominations where cashier is giving more than currently available (withdrawal only). */
get withdrawalDenomOverdraft(): Array<{ label: string; available: number; requested: number }> {
if (!this.transactionType.withdrawal || !this.openingLoaded || this.openingDenoms.length === 0) return [];
if (!this.transactionType.withdrawal || !this.openingLoaded) return [];
const openMap = new Map<string, number>();
for (const d of this.openingDenoms) {
openMap.set(`${d.kind}:${d.denominationValue}`, Number(d.count ?? 0));