feat(cashier): denomination overdraft validation, exchange detail fix, cash exchange in reports

- Savings/Current/FD/RD withdrawal forms: card-style overdraft warning when
  requested denomination count exceeds cashier stock; submit blocked on overdraft
- Cash History detail view: show both RECEIVED and GIVEN denomination breakdowns
  for Cash Exchange (previously only RECEIVED side was displayed)
- Closing Cash Report: add Cash Exchanges section (denomination-wise received/given/net)
- Excess Transaction Report: include cash exchanges >= threshold with Type badge
  (Exchange/Txn) and ID Proof column for RBI/AML compliance
- cashier-shared.scss: etr-badge and closing-exchange-head styles
- Cash Exchange entry: overdraft warning redesigned to cx-overdraft-card BEM layout

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Jaanu423
2026-06-01 10:10:17 +05:30
co-authored by Claude Sonnet 4.6
parent 51495f8eb8
commit f291e4da5b
24 changed files with 1339 additions and 25 deletions
@@ -326,6 +326,31 @@
Use ₹{{ denominationTotal | number: '1.2-2' }} as Transaction Amount
</button>
</div>
<!-- Denomination overdraft warning (withdrawal only) -->
@if (transactionType.withdrawal && hasWithdrawalDenomOverdraft) {
<div class="sat__denom-warn">
<div class="sat__denom-warn-title">
<fa-icon icon="exclamation-triangle" class="sat__denom-warn-ico"></fa-icon>
<span>Insufficient denomination stock — cannot proceed</span>
</div>
<div class="sat__denom-warn-rows">
@for (item of withdrawalDenomOverdraft; track item.label) {
<div class="sat__denom-warn-row">
<span class="sat__denom-warn-chip">{{ item.label }}</span>
<span class="sat__denom-warn-stat"
><span class="sat__denom-warn-lbl">Need</span><strong>{{ item.requested }}</strong></span
>
<span class="sat__denom-warn-dot">·</span>
<span class="sat__denom-warn-stat"
><span class="sat__denom-warn-lbl">Available</span><strong>{{ item.available }}</strong></span
>
<span class="sat__denom-warn-short">Short: {{ item.requested - item.available }}</span>
</div>
}
</div>
</div>
}
<!-- Closing cash position -->
@if (openingLoaded && openingDenoms.length > 0) {
<div
@@ -486,7 +511,11 @@
mat-flat-button
type="submit"
class="sat__submit"
[disabled]="!currentAccountTransactionForm.valid || (denominationTotal > 0 && !denominationMatchesForm)"
[disabled]="
!currentAccountTransactionForm.valid ||
(denominationTotal > 0 && !denominationMatchesForm) ||
hasWithdrawalDenomOverdraft
"
>
<fa-icon icon="check" class="m-r-6"></fa-icon>
{{ 'labels.buttons.Submit' | translate }}
@@ -1094,3 +1094,98 @@ $red: #b91c1c;
font-variant-numeric: tabular-nums;
color: #0d1b2a;
}
.sat__denom-warn {
margin: 8px 14px;
background: #fff5f5;
border: 1.5px solid #fca5a5;
border-left: 4px solid #ef4444;
border-radius: 10px;
overflow: hidden;
&-title {
display: flex;
align-items: center;
gap: 8px;
padding: 9px 14px;
font-size: 13px;
font-weight: 700;
color: #7f1d1d;
background: #fef2f2;
border-bottom: 1px solid #fecaca;
}
&-ico {
color: #ef4444;
font-size: 14px;
flex-shrink: 0;
}
&-rows {
display: flex;
flex-direction: column;
padding: 4px 0;
}
&-row {
display: flex;
align-items: center;
gap: 10px;
padding: 7px 14px;
font-size: 12.5px;
color: #7f1d1d;
&:not(:last-child) {
border-bottom: 1px dashed #fecaca;
}
}
&-chip {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 3px 10px;
background: linear-gradient(135deg, #fee2e2, #fecaca);
border: 1px solid #fca5a5;
border-radius: 6px;
font-size: 12px;
font-weight: 800;
color: #991b1b;
min-width: 52px;
letter-spacing: 0.01em;
}
&-stat {
display: flex;
align-items: center;
gap: 4px;
strong {
font-weight: 800;
font-variant-numeric: tabular-nums;
}
}
&-lbl {
font-size: 11px;
color: #b91c1c;
font-weight: 600;
}
&-dot {
color: #fca5a5;
font-weight: 700;
font-size: 14px;
}
&-short {
margin-left: auto;
padding: 2px 9px;
background: #fff;
border: 1.5px solid #f87171;
border-radius: 999px;
font-size: 11px;
font-weight: 800;
color: #b91c1c;
}
}
@@ -389,6 +389,24 @@ export class CurrentAccountTransactionsComponent implements OnInit {
return this.denominationTotal > 0 && formAmt === this.denominationTotal;
}
get withdrawalDenomOverdraft(): Array<{ label: string; available: number; requested: number }> {
if (!this.transactionType.withdrawal || !this.openingLoaded || this.openingDenoms.length === 0) return [];
const openMap = new Map<string, number>();
for (const d of this.openingDenoms) {
openMap.set(`${d.kind}:${d.denominationValue}`, Number(d.count ?? 0));
}
return [
...this.notes,
...this.coins
]
.map((d) => ({ label: d.label, available: openMap.get(this.denomKey(d)) ?? 0, requested: this.countFor(d) }))
.filter((item) => item.requested > 0 && item.requested > item.available);
}
get hasWithdrawalDenomOverdraft(): boolean {
return this.withdrawalDenomOverdraft.length > 0;
}
applyDenominationTotal(): void {
this.currentAccountTransactionForm.controls['transactionAmount'].setValue(this.denominationTotal);
this.currentAccountTransactionForm.controls['transactionAmount'].markAsDirty();