feat: dynamic denomination breakdown in deposit forms + savings menu scroller

Deposit forms (Current/Savings/FD/RD) now load cash denominations from
the Denomination Master instead of a hardcoded list:
- active + discontinued denominations fetched via /denominationmaster
- discontinued rows hidden by default behind a "Show discontinued" toggle,
  rendered faded and disabled (cannot be counted) when revealed
- rows sorted by value descending; redesigned as a Denom/Qty/Amount table
  with circular coin chips and per-row amount (faded at zero)
Also: savings account-actions menu now scrolls (max-height + grouped
sections mirror the current-account menu) and renders all action items.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jaanu423
2026-05-27 13:19:23 +05:30
co-authored by Claude Opus 4.7
parent 918a90368c
commit 89e17f561f
16 changed files with 1006 additions and 548 deletions
@@ -142,68 +142,47 @@
<fa-icon icon="money-bill-wave" class="sat__panel-ico"></fa-icon> <fa-icon icon="money-bill-wave" class="sat__panel-ico"></fa-icon>
<h3 class="sat__panel-title">Cash Denomination Breakdown</h3> <h3 class="sat__panel-title">Cash Denomination Breakdown</h3>
<span class="sat__panel-spacer"></span> <span class="sat__panel-spacer"></span>
@if (hasInactive) {
<button type="button" mat-stroked-button class="sat__panel-action" (click)="showInactive = !showInactive">
{{ showInactive ? 'Hide discontinued' : 'Show discontinued' }}
</button>
}
<button type="button" mat-stroked-button class="sat__panel-action" (click)="clearDenominations()"> <button type="button" mat-stroked-button class="sat__panel-action" (click)="clearDenominations()">
<fa-icon icon="undo" class="m-r-6"></fa-icon> Clear <fa-icon icon="undo" class="m-r-6"></fa-icon> Clear
</button> </button>
</header> </header>
<!-- Banknotes --> <!-- Denomination table (Denom · Qty · Amount) -->
<div class="sat__denom-group"> <div class="sat__dt">
<div class="sat__denom-group-head"> <div class="sat__dt-head">
<span class="sat__denom-group-title">Banknotes</span> <span class="sat__dt-h-denom">Denom</span>
<span class="sat__denom-group-total">₹{{ notesTotal | number: '1.2-2' }}</span> <span class="sat__dt-h-qty">Qty</span>
<span class="sat__dt-h-amount">Amount</span>
</div> </div>
<div class="sat__denom-grid">
@for (d of notes; track d.value) {
<div class="sat__denom" [class.sat__denom--active]="countFor(d) > 0">
<div class="sat__denom-top">
<span class="sat__denom-chip sat__denom-chip--note">{{ d.label }}</span>
<span class="sat__denom-amount">₹{{ subtotalFor(d) | number: '1.2-2' }}</span>
</div>
<div class="sat__denom-stepper">
<button
type="button"
class="sat__denom-step"
(click)="bumpDenom(d, -1)"
[disabled]="countFor(d) === 0"
aria-label="Decrease"
>
−
</button>
<input
type="number"
min="0"
[value]="countFor(d)"
(input)="onDenomChange(d, $any($event.target).value)"
inputmode="numeric"
class="sat__denom-count"
/>
<button type="button" class="sat__denom-step" (click)="bumpDenom(d, 1)" aria-label="Increase">+</button>
</div>
</div>
}
</div>
</div>
<!-- Coins --> <!-- Bank notes -->
<div class="sat__denom-group"> <div class="sat__dt-section">
<div class="sat__denom-group-head"> <span class="sat__dt-section-title">Bank Notes</span>
<span class="sat__denom-group-title">Coins</span> <span class="sat__dt-section-total">₹{{ notesTotal | number: '1.2-2' }}</span>
<span class="sat__denom-group-total">₹{{ coinsTotal | number: '1.2-2' }}</span>
</div> </div>
<div class="sat__denom-grid"> @for (d of notes; track d.value) {
@for (d of coins; track d.value) { <div
<div class="sat__denom" [class.sat__denom--active]="countFor(d) > 0"> class="sat__dt-row"
<div class="sat__denom-top"> [class.sat__dt-row--active]="countFor(d) > 0"
<span class="sat__denom-chip sat__denom-chip--coin">{{ d.label }}</span> [style.display]="!d.active && !showInactive ? 'none' : null"
<span class="sat__denom-amount">₹{{ subtotalFor(d) | number: '1.2-2' }}</span> [style.opacity]="d.active ? null : 0.55"
</div> [attr.title]="d.active ? null : 'Discontinued — not active for new deposits'"
>
<div class="sat__dt-denom">
<span class="sat__denom-chip sat__denom-chip--note">{{ d.label }}</span>
</div>
<div class="sat__dt-qty">
<div class="sat__denom-stepper"> <div class="sat__denom-stepper">
<button <button
type="button" type="button"
class="sat__denom-step" class="sat__denom-step"
(click)="bumpDenom(d, -1)" (click)="bumpDenom(d, -1)"
[disabled]="countFor(d) === 0" [disabled]="countFor(d) === 0 || !d.active"
aria-label="Decrease" aria-label="Decrease"
> >
− −
@@ -213,14 +192,79 @@
min="0" min="0"
[value]="countFor(d)" [value]="countFor(d)"
(input)="onDenomChange(d, $any($event.target).value)" (input)="onDenomChange(d, $any($event.target).value)"
[disabled]="!d.active"
inputmode="numeric" inputmode="numeric"
class="sat__denom-count" class="sat__denom-count"
/> />
<button type="button" class="sat__denom-step" (click)="bumpDenom(d, 1)" aria-label="Increase">+</button> <button
type="button"
class="sat__denom-step"
(click)="bumpDenom(d, 1)"
[disabled]="!d.active"
aria-label="Increase"
>
+
</button>
</div> </div>
</div> </div>
} <div class="sat__dt-amount" [class.sat__dt-amount--zero]="countFor(d) === 0">
₹{{ subtotalFor(d) | number: '1.2-2' }}
</div>
</div>
}
<!-- Coins -->
<div class="sat__dt-section">
<span class="sat__dt-section-title">Coins</span>
<span class="sat__dt-section-total">₹{{ coinsTotal | number: '1.2-2' }}</span>
</div> </div>
@for (d of coins; track d.value) {
<div
class="sat__dt-row"
[class.sat__dt-row--active]="countFor(d) > 0"
[style.display]="!d.active && !showInactive ? 'none' : null"
[style.opacity]="d.active ? null : 0.55"
[attr.title]="d.active ? null : 'Discontinued — not active for new deposits'"
>
<div class="sat__dt-denom">
<span class="sat__denom-chip sat__denom-chip--coin sat__denom-chip--circle">{{ d.label }}</span>
</div>
<div class="sat__dt-qty">
<div class="sat__denom-stepper">
<button
type="button"
class="sat__denom-step"
(click)="bumpDenom(d, -1)"
[disabled]="countFor(d) === 0 || !d.active"
aria-label="Decrease"
>
−
</button>
<input
type="number"
min="0"
[value]="countFor(d)"
(input)="onDenomChange(d, $any($event.target).value)"
[disabled]="!d.active"
inputmode="numeric"
class="sat__denom-count"
/>
<button
type="button"
class="sat__denom-step"
(click)="bumpDenom(d, 1)"
[disabled]="!d.active"
aria-label="Increase"
>
+
</button>
</div>
</div>
<div class="sat__dt-amount" [class.sat__dt-amount--zero]="countFor(d) === 0">
₹{{ subtotalFor(d) | number: '1.2-2' }}
</div>
</div>
}
</div> </div>
<!-- Tally + apply --> <!-- Tally + apply -->
@@ -431,22 +431,42 @@ $red: #b91c1c;
} }
/* ─── Denomination groups ─────────────────────────────────────────── */ /* ─── Denomination groups ─────────────────────────────────────────── */
.sat__denom-group {
padding: 8px 14px 4px;
& + & { /* ── Denomination table: Denom · Qty · Amount ── */
border-top: 1px solid #f1f5f9; .sat__dt {
} padding: 4px 0 6px;
} }
.sat__denom-group-head { .sat__dt-head {
display: grid;
grid-template-columns: 1fr 140px 120px;
align-items: center;
gap: 10px;
padding: 2px 16px 8px;
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
color: #94a3b8;
border-bottom: 1px solid $border;
}
.sat__dt-h-qty {
text-align: center;
}
.sat__dt-h-amount {
text-align: right;
}
.sat__dt-section {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: baseline; align-items: baseline;
margin-bottom: 5px; padding: 12px 16px 4px;
} }
.sat__denom-group-title { .sat__dt-section-title {
font-size: 10px; font-size: 10px;
font-weight: 800; font-weight: 800;
text-transform: uppercase; text-transform: uppercase;
@@ -454,46 +474,52 @@ $red: #b91c1c;
color: $slate; color: $slate;
} }
.sat__denom-group-total { .sat__dt-section-total {
font-size: 12px; font-size: 12px;
font-weight: 700; font-weight: 700;
color: $navy; color: $navy;
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
} }
.sat__denom-grid { .sat__dt-row {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); grid-template-columns: 1fr 140px 120px;
gap: 6px; align-items: center;
} gap: 10px;
padding: 7px 16px;
.sat__denom { border-bottom: 1px dotted #e2e8f0;
display: flex; transition: background 0.12s ease;
flex-direction: column;
gap: 5px;
padding: 6px 9px;
border-radius: 8px;
background: #fff;
border: 1px solid $border;
transition:
border-color 0.15s ease,
background 0.15s ease,
box-shadow 0.15s ease;
min-width: 0;
&--active { &--active {
border-color: #86efac;
background: #f0fdf4; background: #f0fdf4;
box-shadow: 0 1px 3px -1px rgb(4 120 87 / 18%);
} }
} }
.sat__denom-top { .sat__dt-denom {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; }
gap: 6px;
min-width: 0; .sat__dt-qty {
display: flex;
justify-content: center;
.sat__denom-stepper {
width: 130px;
}
}
.sat__dt-amount {
text-align: right;
font-size: 13px;
font-weight: 700;
color: $navy;
font-variant-numeric: tabular-nums;
&--zero {
color: #cbd5e1;
font-weight: 600;
}
} }
.sat__denom-chip { .sat__denom-chip {
@@ -521,6 +547,14 @@ $red: #b91c1c;
color: #854d0e; color: #854d0e;
border: 1px solid #fcd34d; border: 1px solid #fcd34d;
} }
&--circle {
min-width: 36px;
width: 36px;
height: 36px;
padding: 0;
border-radius: 50%;
}
} }
.sat__denom-amount { .sat__denom-amount {
@@ -19,6 +19,7 @@ import { Router, ActivatedRoute, RouterLink } from '@angular/router';
/** Custom Services */ /** Custom Services */
import { CurrentAccountsService } from '../../current-accounts.service'; import { CurrentAccountsService } from '../../current-accounts.service';
import { DenominationMasterService } from 'app/system/denomination-master/denomination-master.service';
import { ClientsService } from 'app/clients/clients.service'; import { ClientsService } from 'app/clients/clients.service';
import { SettingsService } from 'app/settings/settings.service'; import { SettingsService } from 'app/settings/settings.service';
import { Dates } from 'app/core/utils/dates'; import { Dates } from 'app/core/utils/dates';
@@ -37,6 +38,7 @@ interface Denomination {
value: number; value: number;
kind: 'note' | 'coin'; kind: 'note' | 'coin';
label: string; label: string;
active: boolean;
} }
/** /**
@@ -63,6 +65,7 @@ export class CurrentAccountTransactionsComponent implements OnInit {
private clientsService = inject(ClientsService); private clientsService = inject(ClientsService);
private settingsService = inject(SettingsService); private settingsService = inject(SettingsService);
private dialog = inject(MatDialog); private dialog = inject(MatDialog);
private denominationMasterService = inject(DenominationMasterService);
/** Minimum Due Date allowed. */ /** Minimum Due Date allowed. */
minDate = new Date(2000, 0, 1); minDate = new Date(2000, 0, 1);
@@ -88,24 +91,14 @@ export class CurrentAccountTransactionsComponent implements OnInit {
currentAccountId: string; currentAccountId: string;
currency: Currency | null = null; currency: Currency | null = null;
/** Indian banknote denominations (descending). */ /** Banknote denominations — loaded dynamically from the Denomination Master (active only). */
readonly notes: Denomination[] = [ notes: Denomination[] = [];
{ value: 500, kind: 'note', label: '₹500' },
{ value: 200, kind: 'note', label: '₹200' },
{ value: 100, kind: 'note', label: '₹100' },
{ value: 50, kind: 'note', label: '₹50' },
{ value: 20, kind: 'note', label: '₹20' },
{ value: 10, kind: 'note', label: '₹10' }
];
/** Indian coin denominations (descending). */ /** Coin denominations — loaded dynamically from the Denomination Master. */
readonly coins: Denomination[] = [ coins: Denomination[] = [];
{ value: 20, kind: 'coin', label: '₹20' },
{ value: 10, kind: 'coin', label: '₹10' }, /** When false, discontinued (inactive) denominations stay hidden in the grid. */
{ value: 5, kind: 'coin', label: '₹5' }, showInactive = false;
{ value: 2, kind: 'coin', label: '₹2' },
{ value: 1, kind: 'coin', label: 'Re 1' }
];
/** Quantity per denomination — keyed by `${kind}:${value}`. */ /** Quantity per denomination — keyed by `${kind}:${value}`. */
denominationCounts: Record<string, number> = {}; denominationCounts: Record<string, number> = {};
@@ -135,9 +128,40 @@ export class CurrentAccountTransactionsComponent implements OnInit {
ngOnInit() { ngOnInit() {
this.maxDate = this.settingsService.businessDate; this.maxDate = this.settingsService.businessDate;
this.createCurrentAccountTransactionForm(); this.createCurrentAccountTransactionForm();
this.loadDenominations();
this.loadContextStrip(); this.loadContextStrip();
} }
/** Load active denominations from the Denomination Master so the cash
* breakdown grid reflects the bank's current configuration. */
private loadDenominations(): void {
this.denominationMasterService.getAll().subscribe({
next: (list: any[]) => {
const rows: Denomination[] = (list || []).map((d) => ({
value: d.denomination,
kind: d.kind as 'note' | 'coin',
label: '₹' + d.denomination,
active: !!d.isActive
}));
const order = (a: Denomination, b: Denomination) => b.value - a.value;
this.notes = rows.filter((r) => r.kind === 'note').sort(order);
this.coins = rows.filter((r) => r.kind === 'coin').sort(order);
},
error: () => {
this.notes = [];
this.coins = [];
}
});
}
/** True when at least one discontinued denomination exists. */
get hasInactive(): boolean {
return [
...this.notes,
...this.coins
].some((d) => !d.active);
}
private loadContextStrip(): void { private loadContextStrip(): void {
if (!this.currentAccountId) return; if (!this.currentAccountId) return;
this.currentAccountsService.getCurrentAccountData(this.currentAccountId).subscribe({ this.currentAccountsService.getCurrentAccountData(this.currentAccountId).subscribe({
@@ -116,66 +116,47 @@
<fa-icon icon="money-bill-wave" class="sat__panel-ico"></fa-icon> <fa-icon icon="money-bill-wave" class="sat__panel-ico"></fa-icon>
<h3 class="sat__panel-title">Cash Denomination Breakdown</h3> <h3 class="sat__panel-title">Cash Denomination Breakdown</h3>
<span class="sat__panel-spacer"></span> <span class="sat__panel-spacer"></span>
@if (hasInactive) {
<button type="button" mat-stroked-button class="sat__panel-action" (click)="showInactive = !showInactive">
{{ showInactive ? 'Hide discontinued' : 'Show discontinued' }}
</button>
}
<button type="button" mat-stroked-button class="sat__panel-action" (click)="clearDenominations()"> <button type="button" mat-stroked-button class="sat__panel-action" (click)="clearDenominations()">
<fa-icon icon="undo" class="m-r-6"></fa-icon> Clear <fa-icon icon="undo" class="m-r-6"></fa-icon> Clear
</button> </button>
</header> </header>
<div class="sat__denom-group"> <!-- Denomination table (Denom · Qty · Amount) -->
<div class="sat__denom-group-head"> <div class="sat__dt">
<span class="sat__denom-group-title">Banknotes</span> <div class="sat__dt-head">
<span class="sat__denom-group-total">₹{{ notesTotal | number: '1.2-2' }}</span> <span class="sat__dt-h-denom">Denom</span>
<span class="sat__dt-h-qty">Qty</span>
<span class="sat__dt-h-amount">Amount</span>
</div> </div>
<div class="sat__denom-grid">
@for (d of notes; track d.value) {
<div class="sat__denom" [class.sat__denom--active]="countFor(d) > 0">
<div class="sat__denom-top">
<span class="sat__denom-chip sat__denom-chip--note">{{ d.label }}</span>
<span class="sat__denom-amount">₹{{ subtotalFor(d) | number: '1.2-2' }}</span>
</div>
<div class="sat__denom-stepper">
<button
type="button"
class="sat__denom-step"
(click)="bumpDenom(d, -1)"
[disabled]="countFor(d) === 0"
aria-label="Decrease"
>
−
</button>
<input
type="number"
min="0"
[value]="countFor(d)"
(input)="onDenomChange(d, $any($event.target).value)"
inputmode="numeric"
class="sat__denom-count"
/>
<button type="button" class="sat__denom-step" (click)="bumpDenom(d, 1)" aria-label="Increase">+</button>
</div>
</div>
}
</div>
</div>
<div class="sat__denom-group"> <!-- Bank notes -->
<div class="sat__denom-group-head"> <div class="sat__dt-section">
<span class="sat__denom-group-title">Coins</span> <span class="sat__dt-section-title">Bank Notes</span>
<span class="sat__denom-group-total">₹{{ coinsTotal | number: '1.2-2' }}</span> <span class="sat__dt-section-total">₹{{ notesTotal | number: '1.2-2' }}</span>
</div> </div>
<div class="sat__denom-grid"> @for (d of notes; track d.value) {
@for (d of coins; track d.value) { <div
<div class="sat__denom" [class.sat__denom--active]="countFor(d) > 0"> class="sat__dt-row"
<div class="sat__denom-top"> [class.sat__dt-row--active]="countFor(d) > 0"
<span class="sat__denom-chip sat__denom-chip--coin">{{ d.label }}</span> [style.display]="!d.active && !showInactive ? 'none' : null"
<span class="sat__denom-amount">₹{{ subtotalFor(d) | number: '1.2-2' }}</span> [style.opacity]="d.active ? null : 0.55"
</div> [attr.title]="d.active ? null : 'Discontinued — not active for new deposits'"
>
<div class="sat__dt-denom">
<span class="sat__denom-chip sat__denom-chip--note">{{ d.label }}</span>
</div>
<div class="sat__dt-qty">
<div class="sat__denom-stepper"> <div class="sat__denom-stepper">
<button <button
type="button" type="button"
class="sat__denom-step" class="sat__denom-step"
(click)="bumpDenom(d, -1)" (click)="bumpDenom(d, -1)"
[disabled]="countFor(d) === 0" [disabled]="countFor(d) === 0 || !d.active"
aria-label="Decrease" aria-label="Decrease"
> >
− −
@@ -185,14 +166,79 @@
min="0" min="0"
[value]="countFor(d)" [value]="countFor(d)"
(input)="onDenomChange(d, $any($event.target).value)" (input)="onDenomChange(d, $any($event.target).value)"
[disabled]="!d.active"
inputmode="numeric" inputmode="numeric"
class="sat__denom-count" class="sat__denom-count"
/> />
<button type="button" class="sat__denom-step" (click)="bumpDenom(d, 1)" aria-label="Increase">+</button> <button
type="button"
class="sat__denom-step"
(click)="bumpDenom(d, 1)"
[disabled]="!d.active"
aria-label="Increase"
>
+
</button>
</div> </div>
</div> </div>
} <div class="sat__dt-amount" [class.sat__dt-amount--zero]="countFor(d) === 0">
₹{{ subtotalFor(d) | number: '1.2-2' }}
</div>
</div>
}
<!-- Coins -->
<div class="sat__dt-section">
<span class="sat__dt-section-title">Coins</span>
<span class="sat__dt-section-total">₹{{ coinsTotal | number: '1.2-2' }}</span>
</div> </div>
@for (d of coins; track d.value) {
<div
class="sat__dt-row"
[class.sat__dt-row--active]="countFor(d) > 0"
[style.display]="!d.active && !showInactive ? 'none' : null"
[style.opacity]="d.active ? null : 0.55"
[attr.title]="d.active ? null : 'Discontinued — not active for new deposits'"
>
<div class="sat__dt-denom">
<span class="sat__denom-chip sat__denom-chip--coin sat__denom-chip--circle">{{ d.label }}</span>
</div>
<div class="sat__dt-qty">
<div class="sat__denom-stepper">
<button
type="button"
class="sat__denom-step"
(click)="bumpDenom(d, -1)"
[disabled]="countFor(d) === 0 || !d.active"
aria-label="Decrease"
>
−
</button>
<input
type="number"
min="0"
[value]="countFor(d)"
(input)="onDenomChange(d, $any($event.target).value)"
[disabled]="!d.active"
inputmode="numeric"
class="sat__denom-count"
/>
<button
type="button"
class="sat__denom-step"
(click)="bumpDenom(d, 1)"
[disabled]="!d.active"
aria-label="Increase"
>
+
</button>
</div>
</div>
<div class="sat__dt-amount" [class.sat__dt-amount--zero]="countFor(d) === 0">
₹{{ subtotalFor(d) | number: '1.2-2' }}
</div>
</div>
}
</div> </div>
<div class="sat__tally"> <div class="sat__tally">
@@ -383,22 +383,41 @@ $amber-d: #b45309;
gap: 3px; gap: 3px;
} }
.sat__denom-group { /* ── Denomination table: Denom · Qty · Amount ── */
padding: 8px 14px 4px; .sat__dt {
padding: 4px 0 6px;
& + & {
border-top: 1px solid #f1f5f9;
}
} }
.sat__denom-group-head { .sat__dt-head {
display: grid;
grid-template-columns: 1fr 140px 120px;
align-items: center;
gap: 10px;
padding: 2px 16px 8px;
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
color: #94a3b8;
border-bottom: 1px solid $border;
}
.sat__dt-h-qty {
text-align: center;
}
.sat__dt-h-amount {
text-align: right;
}
.sat__dt-section {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: baseline; align-items: baseline;
margin-bottom: 5px; padding: 12px 16px 4px;
} }
.sat__denom-group-title { .sat__dt-section-title {
font-size: 10px; font-size: 10px;
font-weight: 800; font-weight: 800;
text-transform: uppercase; text-transform: uppercase;
@@ -406,45 +425,52 @@ $amber-d: #b45309;
color: $slate; color: $slate;
} }
.sat__denom-group-total { .sat__dt-section-total {
font-size: 12px; font-size: 12px;
font-weight: 700; font-weight: 700;
color: $navy; color: $navy;
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
} }
.sat__denom-grid { .sat__dt-row {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); grid-template-columns: 1fr 140px 120px;
gap: 6px; align-items: center;
} gap: 10px;
padding: 7px 16px;
.sat__denom { border-bottom: 1px dotted #e2e8f0;
display: flex; transition: background 0.12s ease;
flex-direction: column;
gap: 5px;
padding: 6px 9px;
border-radius: 8px;
background: #fff;
border: 1px solid $border;
transition:
border-color 0.15s ease,
background 0.15s ease;
min-width: 0;
&--active { &--active {
border-color: #86efac;
background: #f0fdf4; background: #f0fdf4;
box-shadow: 0 1px 3px -1px rgb(4 120 87 / 18%);
} }
} }
.sat__denom-top { .sat__dt-denom {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; }
gap: 6px;
min-width: 0; .sat__dt-qty {
display: flex;
justify-content: center;
.sat__denom-stepper {
width: 130px;
}
}
.sat__dt-amount {
text-align: right;
font-size: 13px;
font-weight: 700;
color: $navy;
font-variant-numeric: tabular-nums;
&--zero {
color: #cbd5e1;
font-weight: 600;
}
} }
.sat__denom-chip { .sat__denom-chip {
@@ -470,6 +496,14 @@ $amber-d: #b45309;
color: #854d0e; color: #854d0e;
border: 1px solid #fcd34d; border: 1px solid #fcd34d;
} }
&--circle {
min-width: 36px;
width: 36px;
height: 36px;
padding: 0;
border-radius: 50%;
}
} }
.sat__denom-amount { .sat__denom-amount {
@@ -15,6 +15,7 @@ import {
ReactiveFormsModule ReactiveFormsModule
} from '@angular/forms'; } from '@angular/forms';
import { FixedDepositsService } from '../../fixed-deposits.service'; import { FixedDepositsService } from '../../fixed-deposits.service';
import { DenominationMasterService } from 'app/system/denomination-master/denomination-master.service';
import { ClientsService } from 'app/clients/clients.service'; import { ClientsService } from 'app/clients/clients.service';
import { SettingsService } from 'app/settings/settings.service'; import { SettingsService } from 'app/settings/settings.service';
import { Dates } from 'app/core/utils/dates'; import { Dates } from 'app/core/utils/dates';
@@ -34,6 +35,7 @@ interface Denomination {
value: number; value: number;
kind: 'note' | 'coin'; kind: 'note' | 'coin';
label: string; label: string;
active: boolean;
} }
@Component({ @Component({
@@ -57,6 +59,7 @@ export class FixedDepositsCashTransactionComponent implements OnInit {
private clientsService = inject(ClientsService); private clientsService = inject(ClientsService);
private settingsService = inject(SettingsService); private settingsService = inject(SettingsService);
private dialog = inject(MatDialog); private dialog = inject(MatDialog);
private denominationMasterService = inject(DenominationMasterService);
minDate = new Date(2000, 0, 1); minDate = new Date(2000, 0, 1);
maxDate = new Date(); maxDate = new Date();
@@ -69,22 +72,12 @@ export class FixedDepositsCashTransactionComponent implements OnInit {
accountId: string; accountId: string;
currency: Currency; currency: Currency;
readonly notes: Denomination[] = [ notes: Denomination[] = [];
{ value: 500, kind: 'note', label: '₹500' },
{ value: 200, kind: 'note', label: '₹200' },
{ value: 100, kind: 'note', label: '₹100' },
{ value: 50, kind: 'note', label: '₹50' },
{ value: 20, kind: 'note', label: '₹20' },
{ value: 10, kind: 'note', label: '₹10' }
];
readonly coins: Denomination[] = [ coins: Denomination[] = [];
{ value: 20, kind: 'coin', label: '₹20' },
{ value: 10, kind: 'coin', label: '₹10' }, /** When false, discontinued (inactive) denominations stay hidden in the grid. */
{ value: 5, kind: 'coin', label: '₹5' }, showInactive = false;
{ value: 2, kind: 'coin', label: '₹2' },
{ value: 1, kind: 'coin', label: 'Re 1' }
];
denominationCounts: Record<string, number> = {}; denominationCounts: Record<string, number> = {};
@@ -114,9 +107,40 @@ export class FixedDepositsCashTransactionComponent implements OnInit {
ngOnInit() { ngOnInit() {
this.maxDate = this.settingsService.businessDate; this.maxDate = this.settingsService.businessDate;
this.createSavingAccountTransactionForm(); this.createSavingAccountTransactionForm();
this.loadDenominations();
this.loadContextStrip(); this.loadContextStrip();
} }
/** Load active denominations from the Denomination Master so the cash
* breakdown grid reflects the bank's current configuration. */
private loadDenominations(): void {
this.denominationMasterService.getAll().subscribe({
next: (list: any[]) => {
const rows: Denomination[] = (list || []).map((d) => ({
value: d.denomination,
kind: d.kind as 'note' | 'coin',
label: '₹' + d.denomination,
active: !!d.isActive
}));
const order = (a: Denomination, b: Denomination) => b.value - a.value;
this.notes = rows.filter((r) => r.kind === 'note').sort(order);
this.coins = rows.filter((r) => r.kind === 'coin').sort(order);
},
error: () => {
this.notes = [];
this.coins = [];
}
});
}
/** True when at least one discontinued denomination exists. */
get hasInactive(): boolean {
return [
...this.notes,
...this.coins
].some((d) => !d.active);
}
private loadContextStrip(): void { private loadContextStrip(): void {
if (!this.accountId) return; if (!this.accountId) return;
this.fixedDepositsService.getFixedDepositsAccountData(this.accountId).subscribe({ this.fixedDepositsService.getFixedDepositsAccountData(this.accountId).subscribe({
@@ -116,66 +116,47 @@
<fa-icon icon="money-bill-wave" class="sat__panel-ico"></fa-icon> <fa-icon icon="money-bill-wave" class="sat__panel-ico"></fa-icon>
<h3 class="sat__panel-title">Cash Denomination Breakdown</h3> <h3 class="sat__panel-title">Cash Denomination Breakdown</h3>
<span class="sat__panel-spacer"></span> <span class="sat__panel-spacer"></span>
@if (hasInactive) {
<button type="button" mat-stroked-button class="sat__panel-action" (click)="showInactive = !showInactive">
{{ showInactive ? 'Hide discontinued' : 'Show discontinued' }}
</button>
}
<button type="button" mat-stroked-button class="sat__panel-action" (click)="clearDenominations()"> <button type="button" mat-stroked-button class="sat__panel-action" (click)="clearDenominations()">
<fa-icon icon="undo" class="m-r-6"></fa-icon> Clear <fa-icon icon="undo" class="m-r-6"></fa-icon> Clear
</button> </button>
</header> </header>
<div class="sat__denom-group"> <!-- Denomination table (Denom · Qty · Amount) -->
<div class="sat__denom-group-head"> <div class="sat__dt">
<span class="sat__denom-group-title">Banknotes</span> <div class="sat__dt-head">
<span class="sat__denom-group-total">₹{{ notesTotal | number: '1.2-2' }}</span> <span class="sat__dt-h-denom">Denom</span>
<span class="sat__dt-h-qty">Qty</span>
<span class="sat__dt-h-amount">Amount</span>
</div> </div>
<div class="sat__denom-grid">
@for (d of notes; track d.value) {
<div class="sat__denom" [class.sat__denom--active]="countFor(d) > 0">
<div class="sat__denom-top">
<span class="sat__denom-chip sat__denom-chip--note">{{ d.label }}</span>
<span class="sat__denom-amount">₹{{ subtotalFor(d) | number: '1.2-2' }}</span>
</div>
<div class="sat__denom-stepper">
<button
type="button"
class="sat__denom-step"
(click)="bumpDenom(d, -1)"
[disabled]="countFor(d) === 0"
aria-label="Decrease"
>
−
</button>
<input
type="number"
min="0"
[value]="countFor(d)"
(input)="onDenomChange(d, $any($event.target).value)"
inputmode="numeric"
class="sat__denom-count"
/>
<button type="button" class="sat__denom-step" (click)="bumpDenom(d, 1)" aria-label="Increase">+</button>
</div>
</div>
}
</div>
</div>
<div class="sat__denom-group"> <!-- Bank notes -->
<div class="sat__denom-group-head"> <div class="sat__dt-section">
<span class="sat__denom-group-title">Coins</span> <span class="sat__dt-section-title">Bank Notes</span>
<span class="sat__denom-group-total">₹{{ coinsTotal | number: '1.2-2' }}</span> <span class="sat__dt-section-total">₹{{ notesTotal | number: '1.2-2' }}</span>
</div> </div>
<div class="sat__denom-grid"> @for (d of notes; track d.value) {
@for (d of coins; track d.value) { <div
<div class="sat__denom" [class.sat__denom--active]="countFor(d) > 0"> class="sat__dt-row"
<div class="sat__denom-top"> [class.sat__dt-row--active]="countFor(d) > 0"
<span class="sat__denom-chip sat__denom-chip--coin">{{ d.label }}</span> [style.display]="!d.active && !showInactive ? 'none' : null"
<span class="sat__denom-amount">₹{{ subtotalFor(d) | number: '1.2-2' }}</span> [style.opacity]="d.active ? null : 0.55"
</div> [attr.title]="d.active ? null : 'Discontinued — not active for new deposits'"
>
<div class="sat__dt-denom">
<span class="sat__denom-chip sat__denom-chip--note">{{ d.label }}</span>
</div>
<div class="sat__dt-qty">
<div class="sat__denom-stepper"> <div class="sat__denom-stepper">
<button <button
type="button" type="button"
class="sat__denom-step" class="sat__denom-step"
(click)="bumpDenom(d, -1)" (click)="bumpDenom(d, -1)"
[disabled]="countFor(d) === 0" [disabled]="countFor(d) === 0 || !d.active"
aria-label="Decrease" aria-label="Decrease"
> >
− −
@@ -185,14 +166,79 @@
min="0" min="0"
[value]="countFor(d)" [value]="countFor(d)"
(input)="onDenomChange(d, $any($event.target).value)" (input)="onDenomChange(d, $any($event.target).value)"
[disabled]="!d.active"
inputmode="numeric" inputmode="numeric"
class="sat__denom-count" class="sat__denom-count"
/> />
<button type="button" class="sat__denom-step" (click)="bumpDenom(d, 1)" aria-label="Increase">+</button> <button
type="button"
class="sat__denom-step"
(click)="bumpDenom(d, 1)"
[disabled]="!d.active"
aria-label="Increase"
>
+
</button>
</div> </div>
</div> </div>
} <div class="sat__dt-amount" [class.sat__dt-amount--zero]="countFor(d) === 0">
₹{{ subtotalFor(d) | number: '1.2-2' }}
</div>
</div>
}
<!-- Coins -->
<div class="sat__dt-section">
<span class="sat__dt-section-title">Coins</span>
<span class="sat__dt-section-total">₹{{ coinsTotal | number: '1.2-2' }}</span>
</div> </div>
@for (d of coins; track d.value) {
<div
class="sat__dt-row"
[class.sat__dt-row--active]="countFor(d) > 0"
[style.display]="!d.active && !showInactive ? 'none' : null"
[style.opacity]="d.active ? null : 0.55"
[attr.title]="d.active ? null : 'Discontinued — not active for new deposits'"
>
<div class="sat__dt-denom">
<span class="sat__denom-chip sat__denom-chip--coin sat__denom-chip--circle">{{ d.label }}</span>
</div>
<div class="sat__dt-qty">
<div class="sat__denom-stepper">
<button
type="button"
class="sat__denom-step"
(click)="bumpDenom(d, -1)"
[disabled]="countFor(d) === 0 || !d.active"
aria-label="Decrease"
>
−
</button>
<input
type="number"
min="0"
[value]="countFor(d)"
(input)="onDenomChange(d, $any($event.target).value)"
[disabled]="!d.active"
inputmode="numeric"
class="sat__denom-count"
/>
<button
type="button"
class="sat__denom-step"
(click)="bumpDenom(d, 1)"
[disabled]="!d.active"
aria-label="Increase"
>
+
</button>
</div>
</div>
<div class="sat__dt-amount" [class.sat__dt-amount--zero]="countFor(d) === 0">
₹{{ subtotalFor(d) | number: '1.2-2' }}
</div>
</div>
}
</div> </div>
<div class="sat__tally"> <div class="sat__tally">
@@ -383,22 +383,41 @@ $amber-d: #b45309;
gap: 3px; gap: 3px;
} }
.sat__denom-group { /* ── Denomination table: Denom · Qty · Amount ── */
padding: 8px 14px 4px; .sat__dt {
padding: 4px 0 6px;
& + & {
border-top: 1px solid #f1f5f9;
}
} }
.sat__denom-group-head { .sat__dt-head {
display: grid;
grid-template-columns: 1fr 140px 120px;
align-items: center;
gap: 10px;
padding: 2px 16px 8px;
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
color: #94a3b8;
border-bottom: 1px solid $border;
}
.sat__dt-h-qty {
text-align: center;
}
.sat__dt-h-amount {
text-align: right;
}
.sat__dt-section {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: baseline; align-items: baseline;
margin-bottom: 5px; padding: 12px 16px 4px;
} }
.sat__denom-group-title { .sat__dt-section-title {
font-size: 10px; font-size: 10px;
font-weight: 800; font-weight: 800;
text-transform: uppercase; text-transform: uppercase;
@@ -406,45 +425,52 @@ $amber-d: #b45309;
color: $slate; color: $slate;
} }
.sat__denom-group-total { .sat__dt-section-total {
font-size: 12px; font-size: 12px;
font-weight: 700; font-weight: 700;
color: $navy; color: $navy;
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
} }
.sat__denom-grid { .sat__dt-row {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); grid-template-columns: 1fr 140px 120px;
gap: 6px; align-items: center;
} gap: 10px;
padding: 7px 16px;
.sat__denom { border-bottom: 1px dotted #e2e8f0;
display: flex; transition: background 0.12s ease;
flex-direction: column;
gap: 5px;
padding: 6px 9px;
border-radius: 8px;
background: #fff;
border: 1px solid $border;
transition:
border-color 0.15s ease,
background 0.15s ease;
min-width: 0;
&--active { &--active {
border-color: #86efac;
background: #f0fdf4; background: #f0fdf4;
box-shadow: 0 1px 3px -1px rgb(4 120 87 / 18%);
} }
} }
.sat__denom-top { .sat__dt-denom {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; }
gap: 6px;
min-width: 0; .sat__dt-qty {
display: flex;
justify-content: center;
.sat__denom-stepper {
width: 130px;
}
}
.sat__dt-amount {
text-align: right;
font-size: 13px;
font-weight: 700;
color: $navy;
font-variant-numeric: tabular-nums;
&--zero {
color: #cbd5e1;
font-weight: 600;
}
} }
.sat__denom-chip { .sat__denom-chip {
@@ -470,6 +496,14 @@ $amber-d: #b45309;
color: #854d0e; color: #854d0e;
border: 1px solid #fcd34d; border: 1px solid #fcd34d;
} }
&--circle {
min-width: 36px;
width: 36px;
height: 36px;
padding: 0;
border-radius: 50%;
}
} }
.sat__denom-amount { .sat__denom-amount {
@@ -19,6 +19,7 @@ import { ActivatedRoute, Router, RouterLink } from '@angular/router';
/** Custom Services */ /** Custom Services */
import { RecurringDepositsService } from '../../recurring-deposits.service'; import { RecurringDepositsService } from '../../recurring-deposits.service';
import { DenominationMasterService } from 'app/system/denomination-master/denomination-master.service';
import { ClientsService } from 'app/clients/clients.service'; import { ClientsService } from 'app/clients/clients.service';
import { SettingsService } from 'app/settings/settings.service'; import { SettingsService } from 'app/settings/settings.service';
import { Dates } from 'app/core/utils/dates'; import { Dates } from 'app/core/utils/dates';
@@ -37,6 +38,7 @@ interface Denomination {
value: number; value: number;
kind: 'note' | 'coin'; kind: 'note' | 'coin';
label: string; label: string;
active: boolean;
} }
/** /**
@@ -63,6 +65,7 @@ export class DepositRecurringDepositsAccountComponent implements OnInit {
private clientsService = inject(ClientsService); private clientsService = inject(ClientsService);
private settingsService = inject(SettingsService); private settingsService = inject(SettingsService);
private dialog = inject(MatDialog); private dialog = inject(MatDialog);
private denominationMasterService = inject(DenominationMasterService);
@Input() currency: Currency; @Input() currency: Currency;
@@ -80,22 +83,12 @@ export class DepositRecurringDepositsAccountComponent implements OnInit {
transactionCommand: TransactionCommand; transactionCommand: TransactionCommand;
transactionType: TransactionTypeFlags = { deposit: false, withdrawal: false }; transactionType: TransactionTypeFlags = { deposit: false, withdrawal: false };
readonly notes: Denomination[] = [ notes: Denomination[] = [];
{ value: 500, kind: 'note', label: '₹500' },
{ value: 200, kind: 'note', label: '₹200' },
{ value: 100, kind: 'note', label: '₹100' },
{ value: 50, kind: 'note', label: '₹50' },
{ value: 20, kind: 'note', label: '₹20' },
{ value: 10, kind: 'note', label: '₹10' }
];
readonly coins: Denomination[] = [ coins: Denomination[] = [];
{ value: 20, kind: 'coin', label: '₹20' },
{ value: 10, kind: 'coin', label: '₹10' }, /** When false, discontinued (inactive) denominations stay hidden in the grid. */
{ value: 5, kind: 'coin', label: '₹5' }, showInactive = false;
{ value: 2, kind: 'coin', label: '₹2' },
{ value: 1, kind: 'coin', label: 'Re 1' }
];
denominationCounts: Record<string, number> = {}; denominationCounts: Record<string, number> = {};
@@ -132,9 +125,40 @@ export class DepositRecurringDepositsAccountComponent implements OnInit {
ngOnInit() { ngOnInit() {
this.maxDate = this.settingsService.businessDate; this.maxDate = this.settingsService.businessDate;
this.createdepositRecurringDepositForm(); this.createdepositRecurringDepositForm();
this.loadDenominations();
this.loadContextStrip(); this.loadContextStrip();
} }
/** Load active denominations from the Denomination Master so the cash
* breakdown grid reflects the bank's current configuration. */
private loadDenominations(): void {
this.denominationMasterService.getAll().subscribe({
next: (list: any[]) => {
const rows: Denomination[] = (list || []).map((d) => ({
value: d.denomination,
kind: d.kind as 'note' | 'coin',
label: '₹' + d.denomination,
active: !!d.isActive
}));
const order = (a: Denomination, b: Denomination) => b.value - a.value;
this.notes = rows.filter((r) => r.kind === 'note').sort(order);
this.coins = rows.filter((r) => r.kind === 'coin').sort(order);
},
error: () => {
this.notes = [];
this.coins = [];
}
});
}
/** True when at least one discontinued denomination exists. */
get hasInactive(): boolean {
return [
...this.notes,
...this.coins
].some((d) => !d.active);
}
private loadContextStrip(): void { private loadContextStrip(): void {
if (!this.accountId) return; if (!this.accountId) return;
this.recurringDepositsService.getRecurringDepositsAccountData(this.accountId).subscribe({ this.recurringDepositsService.getRecurringDepositsAccountData(this.accountId).subscribe({
@@ -142,68 +142,47 @@
<fa-icon icon="money-bill-wave" class="sat__panel-ico"></fa-icon> <fa-icon icon="money-bill-wave" class="sat__panel-ico"></fa-icon>
<h3 class="sat__panel-title">Cash Denomination Breakdown</h3> <h3 class="sat__panel-title">Cash Denomination Breakdown</h3>
<span class="sat__panel-spacer"></span> <span class="sat__panel-spacer"></span>
@if (hasInactive) {
<button type="button" mat-stroked-button class="sat__panel-action" (click)="showInactive = !showInactive">
{{ showInactive ? 'Hide discontinued' : 'Show discontinued' }}
</button>
}
<button type="button" mat-stroked-button class="sat__panel-action" (click)="clearDenominations()"> <button type="button" mat-stroked-button class="sat__panel-action" (click)="clearDenominations()">
<fa-icon icon="undo" class="m-r-6"></fa-icon> Clear <fa-icon icon="undo" class="m-r-6"></fa-icon> Clear
</button> </button>
</header> </header>
<!-- Banknotes --> <!-- Denomination table (Denom · Qty · Amount) -->
<div class="sat__denom-group"> <div class="sat__dt">
<div class="sat__denom-group-head"> <div class="sat__dt-head">
<span class="sat__denom-group-title">Banknotes</span> <span class="sat__dt-h-denom">Denom</span>
<span class="sat__denom-group-total">₹{{ notesTotal | number: '1.2-2' }}</span> <span class="sat__dt-h-qty">Qty</span>
<span class="sat__dt-h-amount">Amount</span>
</div> </div>
<div class="sat__denom-grid">
@for (d of notes; track d.value) {
<div class="sat__denom" [class.sat__denom--active]="countFor(d) > 0">
<div class="sat__denom-top">
<span class="sat__denom-chip sat__denom-chip--note">{{ d.label }}</span>
<span class="sat__denom-amount">₹{{ subtotalFor(d) | number: '1.2-2' }}</span>
</div>
<div class="sat__denom-stepper">
<button
type="button"
class="sat__denom-step"
(click)="bumpDenom(d, -1)"
[disabled]="countFor(d) === 0"
aria-label="Decrease"
>
−
</button>
<input
type="number"
min="0"
[value]="countFor(d)"
(input)="onDenomChange(d, $any($event.target).value)"
inputmode="numeric"
class="sat__denom-count"
/>
<button type="button" class="sat__denom-step" (click)="bumpDenom(d, 1)" aria-label="Increase">+</button>
</div>
</div>
}
</div>
</div>
<!-- Coins --> <!-- Bank notes -->
<div class="sat__denom-group"> <div class="sat__dt-section">
<div class="sat__denom-group-head"> <span class="sat__dt-section-title">Bank Notes</span>
<span class="sat__denom-group-title">Coins</span> <span class="sat__dt-section-total">₹{{ notesTotal | number: '1.2-2' }}</span>
<span class="sat__denom-group-total">₹{{ coinsTotal | number: '1.2-2' }}</span>
</div> </div>
<div class="sat__denom-grid"> @for (d of notes; track d.value) {
@for (d of coins; track d.value) { <div
<div class="sat__denom" [class.sat__denom--active]="countFor(d) > 0"> class="sat__dt-row"
<div class="sat__denom-top"> [class.sat__dt-row--active]="countFor(d) > 0"
<span class="sat__denom-chip sat__denom-chip--coin">{{ d.label }}</span> [style.display]="!d.active && !showInactive ? 'none' : null"
<span class="sat__denom-amount">₹{{ subtotalFor(d) | number: '1.2-2' }}</span> [style.opacity]="d.active ? null : 0.55"
</div> [attr.title]="d.active ? null : 'Discontinued — not active for new deposits'"
>
<div class="sat__dt-denom">
<span class="sat__denom-chip sat__denom-chip--note">{{ d.label }}</span>
</div>
<div class="sat__dt-qty">
<div class="sat__denom-stepper"> <div class="sat__denom-stepper">
<button <button
type="button" type="button"
class="sat__denom-step" class="sat__denom-step"
(click)="bumpDenom(d, -1)" (click)="bumpDenom(d, -1)"
[disabled]="countFor(d) === 0" [disabled]="countFor(d) === 0 || !d.active"
aria-label="Decrease" aria-label="Decrease"
> >
− −
@@ -213,14 +192,79 @@
min="0" min="0"
[value]="countFor(d)" [value]="countFor(d)"
(input)="onDenomChange(d, $any($event.target).value)" (input)="onDenomChange(d, $any($event.target).value)"
[disabled]="!d.active"
inputmode="numeric" inputmode="numeric"
class="sat__denom-count" class="sat__denom-count"
/> />
<button type="button" class="sat__denom-step" (click)="bumpDenom(d, 1)" aria-label="Increase">+</button> <button
type="button"
class="sat__denom-step"
(click)="bumpDenom(d, 1)"
[disabled]="!d.active"
aria-label="Increase"
>
+
</button>
</div> </div>
</div> </div>
} <div class="sat__dt-amount" [class.sat__dt-amount--zero]="countFor(d) === 0">
₹{{ subtotalFor(d) | number: '1.2-2' }}
</div>
</div>
}
<!-- Coins -->
<div class="sat__dt-section">
<span class="sat__dt-section-title">Coins</span>
<span class="sat__dt-section-total">₹{{ coinsTotal | number: '1.2-2' }}</span>
</div> </div>
@for (d of coins; track d.value) {
<div
class="sat__dt-row"
[class.sat__dt-row--active]="countFor(d) > 0"
[style.display]="!d.active && !showInactive ? 'none' : null"
[style.opacity]="d.active ? null : 0.55"
[attr.title]="d.active ? null : 'Discontinued — not active for new deposits'"
>
<div class="sat__dt-denom">
<span class="sat__denom-chip sat__denom-chip--coin sat__denom-chip--circle">{{ d.label }}</span>
</div>
<div class="sat__dt-qty">
<div class="sat__denom-stepper">
<button
type="button"
class="sat__denom-step"
(click)="bumpDenom(d, -1)"
[disabled]="countFor(d) === 0 || !d.active"
aria-label="Decrease"
>
−
</button>
<input
type="number"
min="0"
[value]="countFor(d)"
(input)="onDenomChange(d, $any($event.target).value)"
[disabled]="!d.active"
inputmode="numeric"
class="sat__denom-count"
/>
<button
type="button"
class="sat__denom-step"
(click)="bumpDenom(d, 1)"
[disabled]="!d.active"
aria-label="Increase"
>
+
</button>
</div>
</div>
<div class="sat__dt-amount" [class.sat__dt-amount--zero]="countFor(d) === 0">
₹{{ subtotalFor(d) | number: '1.2-2' }}
</div>
</div>
}
</div> </div>
<!-- Tally + apply --> <!-- Tally + apply -->
@@ -453,22 +453,42 @@ $red: #b91c1c;
} }
/* ─── Denomination groups ─────────────────────────────────────────── */ /* ─── Denomination groups ─────────────────────────────────────────── */
.sat__denom-group {
padding: 8px 14px 4px;
& + & { /* ── Denomination table: Denom · Qty · Amount ── */
border-top: 1px solid #f1f5f9; .sat__dt {
} padding: 4px 0 6px;
} }
.sat__denom-group-head { .sat__dt-head {
display: grid;
grid-template-columns: 1fr 140px 120px;
align-items: center;
gap: 10px;
padding: 2px 16px 8px;
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
color: #94a3b8;
border-bottom: 1px solid $border;
}
.sat__dt-h-qty {
text-align: center;
}
.sat__dt-h-amount {
text-align: right;
}
.sat__dt-section {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: baseline; align-items: baseline;
margin-bottom: 5px; padding: 12px 16px 4px;
} }
.sat__denom-group-title { .sat__dt-section-title {
font-size: 10px; font-size: 10px;
font-weight: 800; font-weight: 800;
text-transform: uppercase; text-transform: uppercase;
@@ -476,51 +496,52 @@ $red: #b91c1c;
color: $slate; color: $slate;
} }
.sat__denom-group-total { .sat__dt-section-total {
font-size: 12px; font-size: 12px;
font-weight: 700; font-weight: 700;
color: $navy; color: $navy;
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
} }
.sat__denom-grid { .sat__dt-row {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); grid-template-columns: 1fr 140px 120px;
gap: 6px; align-items: center;
} gap: 10px;
padding: 7px 16px;
/* ── Single denomination tile ── border-bottom: 1px dotted #e2e8f0;
Two-row layout: top has the denomination chip + running subtotal, transition: background 0.12s ease;
bottom has a full-width stepper (− / count / +). Easier to read,
gives the cashier obvious tap targets, and never clips on narrow
columns. */
.sat__denom {
display: flex;
flex-direction: column;
gap: 5px;
padding: 6px 9px;
border-radius: 8px;
background: #fff;
border: 1px solid $border;
transition:
border-color 0.15s ease,
background 0.15s ease,
box-shadow 0.15s ease;
min-width: 0;
&--active { &--active {
border-color: #86efac;
background: #f0fdf4; background: #f0fdf4;
box-shadow: 0 1px 3px -1px rgb(4 120 87 / 18%);
} }
} }
.sat__denom-top { .sat__dt-denom {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; }
gap: 6px;
min-width: 0; .sat__dt-qty {
display: flex;
justify-content: center;
.sat__denom-stepper {
width: 130px;
}
}
.sat__dt-amount {
text-align: right;
font-size: 13px;
font-weight: 700;
color: $navy;
font-variant-numeric: tabular-nums;
&--zero {
color: #cbd5e1;
font-weight: 600;
}
} }
.sat__denom-chip { .sat__denom-chip {
@@ -548,6 +569,14 @@ $red: #b91c1c;
color: #854d0e; color: #854d0e;
border: 1px solid #fcd34d; border: 1px solid #fcd34d;
} }
&--circle {
min-width: 36px;
width: 36px;
height: 36px;
padding: 0;
border-radius: 50%;
}
} }
.sat__denom-amount { .sat__denom-amount {
@@ -19,6 +19,7 @@ import { Router, ActivatedRoute, RouterLink } from '@angular/router';
/** Custom Services */ /** Custom Services */
import { SavingsService } from '../../savings.service'; import { SavingsService } from '../../savings.service';
import { DenominationMasterService } from 'app/system/denomination-master/denomination-master.service';
import { ClientsService } from 'app/clients/clients.service'; import { ClientsService } from 'app/clients/clients.service';
import { SettingsService } from 'app/settings/settings.service'; import { SettingsService } from 'app/settings/settings.service';
import { Dates } from 'app/core/utils/dates'; import { Dates } from 'app/core/utils/dates';
@@ -37,6 +38,7 @@ interface Denomination {
value: number; value: number;
kind: 'note' | 'coin'; kind: 'note' | 'coin';
label: string; label: string;
active: boolean;
} }
/** /**
@@ -68,6 +70,7 @@ export class SavingsAccountTransactionsComponent implements OnInit {
private clientsService = inject(ClientsService); private clientsService = inject(ClientsService);
private settingsService = inject(SettingsService); private settingsService = inject(SettingsService);
private dialog = inject(MatDialog); private dialog = inject(MatDialog);
private denominationMasterService = inject(DenominationMasterService);
/** Minimum Due Date allowed. */ /** Minimum Due Date allowed. */
minDate = new Date(2000, 0, 1); minDate = new Date(2000, 0, 1);
@@ -93,24 +96,14 @@ export class SavingsAccountTransactionsComponent implements OnInit {
savingAccountId: string; savingAccountId: string;
currency: Currency | null = null; currency: Currency | null = null;
/** Indian banknote denominations (descending). */ /** Banknote denominations — loaded dynamically from the Denomination Master (active only). */
readonly notes: Denomination[] = [ notes: Denomination[] = [];
{ value: 500, kind: 'note', label: '₹500' },
{ value: 200, kind: 'note', label: '₹200' },
{ value: 100, kind: 'note', label: '₹100' },
{ value: 50, kind: 'note', label: '₹50' },
{ value: 20, kind: 'note', label: '₹20' },
{ value: 10, kind: 'note', label: '₹10' }
];
/** Indian coin denominations (descending). */ /** Coin denominations — loaded dynamically from the Denomination Master. */
readonly coins: Denomination[] = [ coins: Denomination[] = [];
{ value: 20, kind: 'coin', label: '₹20' },
{ value: 10, kind: 'coin', label: '₹10' }, /** When false, discontinued (inactive) denominations stay hidden in the grid. */
{ value: 5, kind: 'coin', label: '₹5' }, showInactive = false;
{ value: 2, kind: 'coin', label: '₹2' },
{ value: 1, kind: 'coin', label: 'Re 1' }
];
/** Quantity per denomination — keyed by `${kind}:${value}`. */ /** Quantity per denomination — keyed by `${kind}:${value}`. */
denominationCounts: Record<string, number> = {}; denominationCounts: Record<string, number> = {};
@@ -152,9 +145,40 @@ export class SavingsAccountTransactionsComponent implements OnInit {
ngOnInit() { ngOnInit() {
this.maxDate = this.settingsService.businessDate; this.maxDate = this.settingsService.businessDate;
this.createSavingAccountTransactionForm(); this.createSavingAccountTransactionForm();
this.loadDenominations();
this.loadContextStrip(); this.loadContextStrip();
} }
/** Load active denominations from the Denomination Master so the cash
* breakdown grid reflects the bank's current configuration. */
private loadDenominations(): void {
this.denominationMasterService.getAll().subscribe({
next: (list: any[]) => {
const rows: Denomination[] = (list || []).map((d) => ({
value: d.denomination,
kind: d.kind as 'note' | 'coin',
label: '₹' + d.denomination,
active: !!d.isActive
}));
const order = (a: Denomination, b: Denomination) => b.value - a.value;
this.notes = rows.filter((r) => r.kind === 'note').sort(order);
this.coins = rows.filter((r) => r.kind === 'coin').sort(order);
},
error: () => {
this.notes = [];
this.coins = [];
}
});
}
/** True when at least one discontinued denomination exists. */
get hasInactive(): boolean {
return [
...this.notes,
...this.coins
].some((d) => !d.active);
}
/** /**
* Pull the savings account + client to populate the cashier's context * Pull the savings account + client to populate the cashier's context
* strip (account number, client name, KYC status, risk level, same-day * strip (account number, client name, KYC status, risk level, same-day
@@ -104,155 +104,172 @@
</div> </div>
</header> </header>
<!-- Cash movement (Deposit / Withdrawal / Hold Amount) --> <div class="sav-menu__body">
<div class="sav-menu__group"> <!-- Transact: singleButtons + options of kind 'transact' -->
<div class="sav-menu__group-title">Transact</div> @if (hasKind('transact')) {
@for (item of buttonConfig.singleButtons; track item.name) { <div class="sav-menu__group">
@if (kindOf(item.name) === 'transact') { <div class="sav-menu__group-title">Transact</div>
<button @for (item of buttonConfig.singleButtons; track item.name) {
mat-menu-item @if (kindOf(item.name) === 'transact') {
class="sav-menu__item sav-menu__item--transact" <button
*mifosxHasPermission="item.taskPermissionName" mat-menu-item
(click)="doAction(item.name)" class="sav-menu__item sav-menu__item--transact"
> *mifosxHasPermission="item.taskPermissionName"
<span class="sav-menu__ico"> (click)="doAction(item.name)"
<fa-icon [icon]="iconOf(item.name, item.icon)"></fa-icon> >
</span> <span class="sav-menu__ico"><fa-icon [icon]="iconOf(item.name, item.icon)"></fa-icon></span>
<span class="sav-menu__label">{{ 'labels.menus.' + item.name | translate }}</span> <span class="sav-menu__label">{{ 'labels.menus.' + item.name | translate }}</span>
</button> </button>
} }
}
@for (option of buttonConfig.options; track option.name) {
@if (kindOf(option.name) === 'transact') {
<button
mat-menu-item
class="sav-menu__item sav-menu__item--transact"
*mifosxHasPermission="option.taskPermissionName"
(click)="doAction(option.name)"
>
<span class="sav-menu__ico"><fa-icon [icon]="iconOf(option.name)"></fa-icon></span>
<span class="sav-menu__label">{{ 'labels.menus.' + option.name | translate }}</span>
</button>
}
}
</div>
}
<!-- Restrict: singleButtons + options of kind 'block' -->
@if (hasKind('block')) {
<div class="sav-menu__group">
<div class="sav-menu__group-title">Restrict</div>
@for (item of buttonConfig.singleButtons; track item.name) {
@if (kindOf(item.name) === 'block') {
<button
mat-menu-item
class="sav-menu__item sav-menu__item--block"
*mifosxHasPermission="item.taskPermissionName"
(click)="doAction(item.name)"
>
<span class="sav-menu__ico"><fa-icon [icon]="iconOf(item.name, item.icon)"></fa-icon></span>
<span class="sav-menu__label">{{ 'labels.menus.' + item.name | translate }}</span>
</button>
}
}
@for (option of buttonConfig.options; track option.name) {
@if (kindOf(option.name) === 'block') {
<button
mat-menu-item
class="sav-menu__item sav-menu__item--block"
*mifosxHasPermission="option.taskPermissionName"
(click)="doAction(option.name)"
>
<span class="sav-menu__ico"><fa-icon [icon]="iconOf(option.name)"></fa-icon></span>
<span class="sav-menu__label">{{ 'labels.menus.' + option.name | translate }}</span>
</button>
}
}
</div>
}
<!-- Interest: singleButtons + options of kind 'interest' -->
@if (hasKind('interest')) {
<div class="sav-menu__group">
<div class="sav-menu__group-title">Interest</div>
@for (item of buttonConfig.singleButtons; track item.name) {
@if (kindOf(item.name) === 'interest') {
<button
mat-menu-item
class="sav-menu__item sav-menu__item--interest"
*mifosxHasPermission="item.taskPermissionName"
(click)="doAction(item.name)"
>
<span class="sav-menu__ico"><fa-icon [icon]="iconOf(item.name, item.icon)"></fa-icon></span>
<span class="sav-menu__label">{{ 'labels.menus.' + item.name | translate }}</span>
</button>
}
}
@for (option of buttonConfig.options; track option.name) {
@if (kindOf(option.name) === 'interest') {
<button
mat-menu-item
class="sav-menu__item sav-menu__item--interest"
*mifosxHasPermission="option.taskPermissionName"
(click)="doAction(option.name)"
>
<span class="sav-menu__ico"><fa-icon [icon]="iconOf(option.name)"></fa-icon></span>
<span class="sav-menu__label">{{ 'labels.menus.' + option.name | translate }}</span>
</button>
}
}
</div>
}
<!-- Admin: singleButtons + options of kind 'admin', hidden when blocked -->
@if (hasKind('admin') && !savingsAccountData.subStatus.block) {
<div class="sav-menu__group">
<div class="sav-menu__group-title">Admin</div>
@for (item of buttonConfig.singleButtons; track item.name) {
@if (kindOf(item.name) === 'admin') {
<button
mat-menu-item
class="sav-menu__item sav-menu__item--admin"
*mifosxHasPermission="item.taskPermissionName"
(click)="doAction(item.name)"
>
<span class="sav-menu__ico"><fa-icon [icon]="iconOf(item.name, item.icon)"></fa-icon></span>
<span class="sav-menu__label">{{ 'labels.menus.' + item.name | translate }}</span>
</button>
}
}
@for (option of buttonConfig.options; track option.name) {
@if (kindOf(option.name) === 'admin') {
<button
mat-menu-item
class="sav-menu__item sav-menu__item--admin"
*mifosxHasPermission="option.taskPermissionName"
(click)="doAction(option.name)"
>
<span class="sav-menu__ico"><fa-icon [icon]="iconOf(option.name)"></fa-icon></span>
<span class="sav-menu__label">{{ 'labels.menus.' + option.name | translate }}</span>
</button>
}
}
</div>
}
<!-- Danger zone: singleButtons + options of kind 'danger' -->
@if (hasKind('danger')) {
<div class="sav-menu__group sav-menu__group--danger">
<div class="sav-menu__group-title">Danger zone</div>
@for (item of buttonConfig.singleButtons; track item.name) {
@if (kindOf(item.name) === 'danger') {
<button
mat-menu-item
class="sav-menu__item sav-menu__item--danger"
*mifosxHasPermission="item.taskPermissionName"
(click)="doAction(item.name)"
>
<span class="sav-menu__ico"><fa-icon [icon]="iconOf(item.name, item.icon)"></fa-icon></span>
<span class="sav-menu__label">{{ 'labels.menus.' + item.name | translate }}</span>
</button>
}
}
@for (option of buttonConfig.options; track option.name) {
@if (kindOf(option.name) === 'danger') {
<button
mat-menu-item
class="sav-menu__item sav-menu__item--danger"
*mifosxHasPermission="option.taskPermissionName"
(click)="doAction(option.name)"
>
<span class="sav-menu__ico"><fa-icon [icon]="iconOf(option.name)"></fa-icon></span>
<span class="sav-menu__label">{{ 'labels.menus.' + option.name | translate }}</span>
</button>
}
}
</div>
} }
</div> </div>
<!-- Restrictions (Block / Unblock variants) -->
<div class="sav-menu__group">
<div class="sav-menu__group-title">Restrict</div>
@for (item of buttonConfig.singleButtons; track item.name) {
@if (kindOf(item.name) === 'block') {
<button
mat-menu-item
class="sav-menu__item sav-menu__item--block"
*mifosxHasPermission="item.taskPermissionName"
(click)="doAction(item.name)"
>
<span class="sav-menu__ico">
<fa-icon [icon]="iconOf(item.name, item.icon)"></fa-icon>
</span>
<span class="sav-menu__label">{{ 'labels.menus.' + item.name | translate }}</span>
</button>
}
}
</div>
<!-- Interest -->
<div class="sav-menu__group">
<div class="sav-menu__group-title">Interest</div>
@for (item of buttonConfig.singleButtons; track item.name) {
@if (kindOf(item.name) === 'interest') {
<button
mat-menu-item
class="sav-menu__item sav-menu__item--interest"
*mifosxHasPermission="item.taskPermissionName"
(click)="doAction(item.name)"
>
<span class="sav-menu__ico">
<fa-icon [icon]="iconOf(item.name, item.icon)"></fa-icon>
</span>
<span class="sav-menu__label">{{ 'labels.menus.' + item.name | translate }}</span>
</button>
}
}
</div>
<!-- Admin (approval/staff/charges) — drawn from BOTH primary
buttons and the historical "More" options. -->
@if (buttonConfig.options && !savingsAccountData.subStatus.block) {
<div class="sav-menu__group">
<div class="sav-menu__group-title">Admin</div>
@for (item of buttonConfig.singleButtons; track item.name) {
@if (kindOf(item.name) === 'admin') {
<button
mat-menu-item
class="sav-menu__item sav-menu__item--admin"
*mifosxHasPermission="item.taskPermissionName"
(click)="doAction(item.name)"
>
<span class="sav-menu__ico">
<fa-icon [icon]="iconOf(item.name, item.icon)"></fa-icon>
</span>
<span class="sav-menu__label">{{ 'labels.menus.' + item.name | translate }}</span>
</button>
}
}
@for (option of buttonConfig.options; track option.name) {
@if (kindOf(option.name) === 'admin') {
<button
mat-menu-item
class="sav-menu__item sav-menu__item--admin"
*mifosxHasPermission="option.taskPermissionName"
(click)="doAction(option.name)"
>
<span class="sav-menu__ico">
<fa-icon [icon]="iconOf(option.name)"></fa-icon>
</span>
<span class="sav-menu__label">{{ 'labels.menus.' + option.name | translate }}</span>
</button>
}
}
@for (option of buttonConfig.options; track option.name) {
@if (kindOf(option.name) === 'interest') {
<button
mat-menu-item
class="sav-menu__item sav-menu__item--interest"
*mifosxHasPermission="option.taskPermissionName"
(click)="doAction(option.name)"
>
<span class="sav-menu__ico">
<fa-icon [icon]="iconOf(option.name)"></fa-icon>
</span>
<span class="sav-menu__label">{{ 'labels.menus.' + option.name | translate }}</span>
</button>
}
}
@for (option of buttonConfig.options; track option.name) {
@if (kindOf(option.name) === 'transact') {
<button
mat-menu-item
class="sav-menu__item sav-menu__item--transact"
*mifosxHasPermission="option.taskPermissionName"
(click)="doAction(option.name)"
>
<span class="sav-menu__ico">
<fa-icon [icon]="iconOf(option.name)"></fa-icon>
</span>
<span class="sav-menu__label">{{ 'labels.menus.' + option.name | translate }}</span>
</button>
}
}
</div>
}
<!-- Danger (Close / Reject / Delete) -->
@if (buttonConfig.options && !savingsAccountData.subStatus.block) {
<div class="sav-menu__group sav-menu__group--danger">
<div class="sav-menu__group-title">Danger zone</div>
@for (option of buttonConfig.options; track option.name) {
@if (kindOf(option.name) === 'danger') {
<button
mat-menu-item
class="sav-menu__item sav-menu__item--danger"
*mifosxHasPermission="option.taskPermissionName"
(click)="doAction(option.name)"
>
<span class="sav-menu__ico">
<fa-icon [icon]="iconOf(option.name)"></fa-icon>
</span>
<span class="sav-menu__label">{{ 'labels.menus.' + option.name | translate }}</span>
</button>
}
}
</div>
}
</div> </div>
</mat-menu> </mat-menu>
</header> </header>
@@ -170,6 +170,14 @@ export class SavingsAccountViewComponent implements OnInit {
return this.ACTION_ICON[name] ?? fallback ?? 'circle'; return this.ACTION_ICON[name] ?? fallback ?? 'circle';
} }
hasKind(kind: 'transact' | 'block' | 'interest' | 'admin' | 'danger'): boolean {
const all = [
...(this.buttonConfig?.singleButtons ?? []),
...(this.buttonConfig?.options ?? [])
];
return all.some((item) => this.kindOf(item.name) === kind);
}
/** /**
* Fetches savings account data from `resolve` * Fetches savings account data from `resolve`
* @param {ActivatedRoute} route Activated Route * @param {ActivatedRoute} route Activated Route
@@ -16,6 +16,10 @@ export class DenominationMasterService {
return this.http.get<any[]>('/denominationmaster'); return this.http.get<any[]>('/denominationmaster');
} }
getActive() {
return this.http.get<any[]>('/denominationmaster/active');
}
create(data: any) { create(data: any) {
return this.http.post('/denominationmaster', data); return this.http.post('/denominationmaster', data);
} }
+22
View File
@@ -1061,12 +1061,34 @@ mat-card {
.sav-action-menu .mat-mdc-menu-content { .sav-action-menu .mat-mdc-menu-content {
padding: 0 !important; padding: 0 !important;
overflow: hidden !important;
} }
.sav-menu { .sav-menu {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
font-family: Inter, 'DM Sans', Roboto, sans-serif; font-family: Inter, 'DM Sans', Roboto, sans-serif;
max-height: 440px;
overflow: hidden;
}
.sav-menu__body {
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: #cbd5e1 transparent;
}
.sav-menu__body::-webkit-scrollbar {
width: 4px;
}
.sav-menu__body::-webkit-scrollbar-track {
background: transparent;
}
.sav-menu__body::-webkit-scrollbar-thumb {
background: #cbd5e1;
border-radius: 4px;
} }
.sav-menu__head { .sav-menu__head {