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>
<h3 class="sat__panel-title">Cash Denomination Breakdown</h3>
<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()">
<fa-icon icon="undo" class="m-r-6"></fa-icon> Clear
</button>
</header>
<!-- Banknotes -->
<div class="sat__denom-group">
<div class="sat__denom-group-head">
<span class="sat__denom-group-title">Banknotes</span>
<span class="sat__denom-group-total">₹{{ notesTotal | number: '1.2-2' }}</span>
<!-- Denomination table (Denom · Qty · Amount) -->
<div class="sat__dt">
<div class="sat__dt-head">
<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 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 -->
<div class="sat__denom-group">
<div class="sat__denom-group-head">
<span class="sat__denom-group-title">Coins</span>
<span class="sat__denom-group-total">₹{{ coinsTotal | number: '1.2-2' }}</span>
<!-- Bank notes -->
<div class="sat__dt-section">
<span class="sat__dt-section-title">Bank Notes</span>
<span class="sat__dt-section-total">₹{{ notesTotal | number: '1.2-2' }}</span>
</div>
<div class="sat__denom-grid">
@for (d of coins; 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--coin">{{ d.label }}</span>
<span class="sat__denom-amount">₹{{ subtotalFor(d) | number: '1.2-2' }}</span>
</div>
@for (d of notes; 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--note">{{ 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"
[disabled]="countFor(d) === 0 || !d.active"
aria-label="Decrease"
>
−
@@ -213,14 +192,79 @@
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)" 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 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>
@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>
<!-- Tally + apply -->
@@ -431,22 +431,42 @@ $red: #b91c1c;
}
/* ─── Denomination groups ─────────────────────────────────────────── */
.sat__denom-group {
padding: 8px 14px 4px;
& + & {
border-top: 1px solid #f1f5f9;
}
/* ── Denomination table: Denom · Qty · Amount ── */
.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;
justify-content: space-between;
align-items: baseline;
margin-bottom: 5px;
padding: 12px 16px 4px;
}
.sat__denom-group-title {
.sat__dt-section-title {
font-size: 10px;
font-weight: 800;
text-transform: uppercase;
@@ -454,46 +474,52 @@ $red: #b91c1c;
color: $slate;
}
.sat__denom-group-total {
.sat__dt-section-total {
font-size: 12px;
font-weight: 700;
color: $navy;
font-variant-numeric: tabular-nums;
}
.sat__denom-grid {
.sat__dt-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 6px;
}
.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;
grid-template-columns: 1fr 140px 120px;
align-items: center;
gap: 10px;
padding: 7px 16px;
border-bottom: 1px dotted #e2e8f0;
transition: background 0.12s ease;
&--active {
border-color: #86efac;
background: #f0fdf4;
box-shadow: 0 1px 3px -1px rgb(4 120 87 / 18%);
}
}
.sat__denom-top {
.sat__dt-denom {
display: flex;
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 {
@@ -521,6 +547,14 @@ $red: #b91c1c;
color: #854d0e;
border: 1px solid #fcd34d;
}
&--circle {
min-width: 36px;
width: 36px;
height: 36px;
padding: 0;
border-radius: 50%;
}
}
.sat__denom-amount {
@@ -19,6 +19,7 @@ import { Router, ActivatedRoute, RouterLink } from '@angular/router';
/** Custom Services */
import { CurrentAccountsService } from '../../current-accounts.service';
import { DenominationMasterService } from 'app/system/denomination-master/denomination-master.service';
import { ClientsService } from 'app/clients/clients.service';
import { SettingsService } from 'app/settings/settings.service';
import { Dates } from 'app/core/utils/dates';
@@ -37,6 +38,7 @@ interface Denomination {
value: number;
kind: 'note' | 'coin';
label: string;
active: boolean;
}
/**
@@ -63,6 +65,7 @@ export class CurrentAccountTransactionsComponent implements OnInit {
private clientsService = inject(ClientsService);
private settingsService = inject(SettingsService);
private dialog = inject(MatDialog);
private denominationMasterService = inject(DenominationMasterService);
/** Minimum Due Date allowed. */
minDate = new Date(2000, 0, 1);
@@ -88,24 +91,14 @@ export class CurrentAccountTransactionsComponent implements OnInit {
currentAccountId: string;
currency: Currency | null = null;
/** Indian banknote denominations (descending). */
readonly 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' }
];
/** Banknote denominations — loaded dynamically from the Denomination Master (active only). */
notes: Denomination[] = [];
/** Indian coin denominations (descending). */
readonly coins: Denomination[] = [
{ value: 20, kind: 'coin', label: '₹20' },
{ value: 10, kind: 'coin', label: '₹10' },
{ value: 5, kind: 'coin', label: '₹5' },
{ value: 2, kind: 'coin', label: '₹2' },
{ value: 1, kind: 'coin', label: 'Re 1' }
];
/** Coin denominations — loaded dynamically from the Denomination Master. */
coins: Denomination[] = [];
/** When false, discontinued (inactive) denominations stay hidden in the grid. */
showInactive = false;
/** Quantity per denomination — keyed by `${kind}:${value}`. */
denominationCounts: Record<string, number> = {};
@@ -135,9 +128,40 @@ export class CurrentAccountTransactionsComponent implements OnInit {
ngOnInit() {
this.maxDate = this.settingsService.businessDate;
this.createCurrentAccountTransactionForm();
this.loadDenominations();
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 {
if (!this.currentAccountId) return;
this.currentAccountsService.getCurrentAccountData(this.currentAccountId).subscribe({
@@ -116,66 +116,47 @@
<fa-icon icon="money-bill-wave" class="sat__panel-ico"></fa-icon>
<h3 class="sat__panel-title">Cash Denomination Breakdown</h3>
<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()">
<fa-icon icon="undo" class="m-r-6"></fa-icon> Clear
</button>
</header>
<div class="sat__denom-group">
<div class="sat__denom-group-head">
<span class="sat__denom-group-title">Banknotes</span>
<span class="sat__denom-group-total">₹{{ notesTotal | number: '1.2-2' }}</span>
<!-- Denomination table (Denom · Qty · Amount) -->
<div class="sat__dt">
<div class="sat__dt-head">
<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 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">
<div class="sat__denom-group-head">
<span class="sat__denom-group-title">Coins</span>
<span class="sat__denom-group-total">₹{{ coinsTotal | number: '1.2-2' }}</span>
<!-- Bank notes -->
<div class="sat__dt-section">
<span class="sat__dt-section-title">Bank Notes</span>
<span class="sat__dt-section-total">₹{{ notesTotal | number: '1.2-2' }}</span>
</div>
<div class="sat__denom-grid">
@for (d of coins; 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--coin">{{ d.label }}</span>
<span class="sat__denom-amount">₹{{ subtotalFor(d) | number: '1.2-2' }}</span>
</div>
@for (d of notes; 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--note">{{ 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"
[disabled]="countFor(d) === 0 || !d.active"
aria-label="Decrease"
>
−
@@ -185,14 +166,79 @@
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)" 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 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>
@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 class="sat__tally">
@@ -383,22 +383,41 @@ $amber-d: #b45309;
gap: 3px;
}
.sat__denom-group {
padding: 8px 14px 4px;
& + & {
border-top: 1px solid #f1f5f9;
}
/* ── Denomination table: Denom · Qty · Amount ── */
.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;
justify-content: space-between;
align-items: baseline;
margin-bottom: 5px;
padding: 12px 16px 4px;
}
.sat__denom-group-title {
.sat__dt-section-title {
font-size: 10px;
font-weight: 800;
text-transform: uppercase;
@@ -406,45 +425,52 @@ $amber-d: #b45309;
color: $slate;
}
.sat__denom-group-total {
.sat__dt-section-total {
font-size: 12px;
font-weight: 700;
color: $navy;
font-variant-numeric: tabular-nums;
}
.sat__denom-grid {
.sat__dt-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 6px;
}
.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;
min-width: 0;
grid-template-columns: 1fr 140px 120px;
align-items: center;
gap: 10px;
padding: 7px 16px;
border-bottom: 1px dotted #e2e8f0;
transition: background 0.12s ease;
&--active {
border-color: #86efac;
background: #f0fdf4;
box-shadow: 0 1px 3px -1px rgb(4 120 87 / 18%);
}
}
.sat__denom-top {
.sat__dt-denom {
display: flex;
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 {
@@ -470,6 +496,14 @@ $amber-d: #b45309;
color: #854d0e;
border: 1px solid #fcd34d;
}
&--circle {
min-width: 36px;
width: 36px;
height: 36px;
padding: 0;
border-radius: 50%;
}
}
.sat__denom-amount {
@@ -15,6 +15,7 @@ import {
ReactiveFormsModule
} from '@angular/forms';
import { FixedDepositsService } from '../../fixed-deposits.service';
import { DenominationMasterService } from 'app/system/denomination-master/denomination-master.service';
import { ClientsService } from 'app/clients/clients.service';
import { SettingsService } from 'app/settings/settings.service';
import { Dates } from 'app/core/utils/dates';
@@ -34,6 +35,7 @@ interface Denomination {
value: number;
kind: 'note' | 'coin';
label: string;
active: boolean;
}
@Component({
@@ -57,6 +59,7 @@ export class FixedDepositsCashTransactionComponent implements OnInit {
private clientsService = inject(ClientsService);
private settingsService = inject(SettingsService);
private dialog = inject(MatDialog);
private denominationMasterService = inject(DenominationMasterService);
minDate = new Date(2000, 0, 1);
maxDate = new Date();
@@ -69,22 +72,12 @@ export class FixedDepositsCashTransactionComponent implements OnInit {
accountId: string;
currency: Currency;
readonly 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' }
];
notes: Denomination[] = [];
readonly coins: Denomination[] = [
{ value: 20, kind: 'coin', label: '₹20' },
{ value: 10, kind: 'coin', label: '₹10' },
{ value: 5, kind: 'coin', label: '₹5' },
{ value: 2, kind: 'coin', label: '₹2' },
{ value: 1, kind: 'coin', label: 'Re 1' }
];
coins: Denomination[] = [];
/** When false, discontinued (inactive) denominations stay hidden in the grid. */
showInactive = false;
denominationCounts: Record<string, number> = {};
@@ -114,9 +107,40 @@ export class FixedDepositsCashTransactionComponent implements OnInit {
ngOnInit() {
this.maxDate = this.settingsService.businessDate;
this.createSavingAccountTransactionForm();
this.loadDenominations();
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 {
if (!this.accountId) return;
this.fixedDepositsService.getFixedDepositsAccountData(this.accountId).subscribe({
@@ -116,66 +116,47 @@
<fa-icon icon="money-bill-wave" class="sat__panel-ico"></fa-icon>
<h3 class="sat__panel-title">Cash Denomination Breakdown</h3>
<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()">
<fa-icon icon="undo" class="m-r-6"></fa-icon> Clear
</button>
</header>
<div class="sat__denom-group">
<div class="sat__denom-group-head">
<span class="sat__denom-group-title">Banknotes</span>
<span class="sat__denom-group-total">₹{{ notesTotal | number: '1.2-2' }}</span>
<!-- Denomination table (Denom · Qty · Amount) -->
<div class="sat__dt">
<div class="sat__dt-head">
<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 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">
<div class="sat__denom-group-head">
<span class="sat__denom-group-title">Coins</span>
<span class="sat__denom-group-total">₹{{ coinsTotal | number: '1.2-2' }}</span>
<!-- Bank notes -->
<div class="sat__dt-section">
<span class="sat__dt-section-title">Bank Notes</span>
<span class="sat__dt-section-total">₹{{ notesTotal | number: '1.2-2' }}</span>
</div>
<div class="sat__denom-grid">
@for (d of coins; 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--coin">{{ d.label }}</span>
<span class="sat__denom-amount">₹{{ subtotalFor(d) | number: '1.2-2' }}</span>
</div>
@for (d of notes; 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--note">{{ 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"
[disabled]="countFor(d) === 0 || !d.active"
aria-label="Decrease"
>
−
@@ -185,14 +166,79 @@
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)" 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 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>
@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 class="sat__tally">
@@ -383,22 +383,41 @@ $amber-d: #b45309;
gap: 3px;
}
.sat__denom-group {
padding: 8px 14px 4px;
& + & {
border-top: 1px solid #f1f5f9;
}
/* ── Denomination table: Denom · Qty · Amount ── */
.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;
justify-content: space-between;
align-items: baseline;
margin-bottom: 5px;
padding: 12px 16px 4px;
}
.sat__denom-group-title {
.sat__dt-section-title {
font-size: 10px;
font-weight: 800;
text-transform: uppercase;
@@ -406,45 +425,52 @@ $amber-d: #b45309;
color: $slate;
}
.sat__denom-group-total {
.sat__dt-section-total {
font-size: 12px;
font-weight: 700;
color: $navy;
font-variant-numeric: tabular-nums;
}
.sat__denom-grid {
.sat__dt-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 6px;
}
.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;
min-width: 0;
grid-template-columns: 1fr 140px 120px;
align-items: center;
gap: 10px;
padding: 7px 16px;
border-bottom: 1px dotted #e2e8f0;
transition: background 0.12s ease;
&--active {
border-color: #86efac;
background: #f0fdf4;
box-shadow: 0 1px 3px -1px rgb(4 120 87 / 18%);
}
}
.sat__denom-top {
.sat__dt-denom {
display: flex;
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 {
@@ -470,6 +496,14 @@ $amber-d: #b45309;
color: #854d0e;
border: 1px solid #fcd34d;
}
&--circle {
min-width: 36px;
width: 36px;
height: 36px;
padding: 0;
border-radius: 50%;
}
}
.sat__denom-amount {
@@ -19,6 +19,7 @@ import { ActivatedRoute, Router, RouterLink } from '@angular/router';
/** Custom Services */
import { RecurringDepositsService } from '../../recurring-deposits.service';
import { DenominationMasterService } from 'app/system/denomination-master/denomination-master.service';
import { ClientsService } from 'app/clients/clients.service';
import { SettingsService } from 'app/settings/settings.service';
import { Dates } from 'app/core/utils/dates';
@@ -37,6 +38,7 @@ interface Denomination {
value: number;
kind: 'note' | 'coin';
label: string;
active: boolean;
}
/**
@@ -63,6 +65,7 @@ export class DepositRecurringDepositsAccountComponent implements OnInit {
private clientsService = inject(ClientsService);
private settingsService = inject(SettingsService);
private dialog = inject(MatDialog);
private denominationMasterService = inject(DenominationMasterService);
@Input() currency: Currency;
@@ -80,22 +83,12 @@ export class DepositRecurringDepositsAccountComponent implements OnInit {
transactionCommand: TransactionCommand;
transactionType: TransactionTypeFlags = { deposit: false, withdrawal: false };
readonly 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' }
];
notes: Denomination[] = [];
readonly coins: Denomination[] = [
{ value: 20, kind: 'coin', label: '₹20' },
{ value: 10, kind: 'coin', label: '₹10' },
{ value: 5, kind: 'coin', label: '₹5' },
{ value: 2, kind: 'coin', label: '₹2' },
{ value: 1, kind: 'coin', label: 'Re 1' }
];
coins: Denomination[] = [];
/** When false, discontinued (inactive) denominations stay hidden in the grid. */
showInactive = false;
denominationCounts: Record<string, number> = {};
@@ -132,9 +125,40 @@ export class DepositRecurringDepositsAccountComponent implements OnInit {
ngOnInit() {
this.maxDate = this.settingsService.businessDate;
this.createdepositRecurringDepositForm();
this.loadDenominations();
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 {
if (!this.accountId) return;
this.recurringDepositsService.getRecurringDepositsAccountData(this.accountId).subscribe({
@@ -142,68 +142,47 @@
<fa-icon icon="money-bill-wave" class="sat__panel-ico"></fa-icon>
<h3 class="sat__panel-title">Cash Denomination Breakdown</h3>
<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()">
<fa-icon icon="undo" class="m-r-6"></fa-icon> Clear
</button>
</header>
<!-- Banknotes -->
<div class="sat__denom-group">
<div class="sat__denom-group-head">
<span class="sat__denom-group-title">Banknotes</span>
<span class="sat__denom-group-total">₹{{ notesTotal | number: '1.2-2' }}</span>
<!-- Denomination table (Denom · Qty · Amount) -->
<div class="sat__dt">
<div class="sat__dt-head">
<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 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 -->
<div class="sat__denom-group">
<div class="sat__denom-group-head">
<span class="sat__denom-group-title">Coins</span>
<span class="sat__denom-group-total">₹{{ coinsTotal | number: '1.2-2' }}</span>
<!-- Bank notes -->
<div class="sat__dt-section">
<span class="sat__dt-section-title">Bank Notes</span>
<span class="sat__dt-section-total">₹{{ notesTotal | number: '1.2-2' }}</span>
</div>
<div class="sat__denom-grid">
@for (d of coins; 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--coin">{{ d.label }}</span>
<span class="sat__denom-amount">₹{{ subtotalFor(d) | number: '1.2-2' }}</span>
</div>
@for (d of notes; 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--note">{{ 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"
[disabled]="countFor(d) === 0 || !d.active"
aria-label="Decrease"
>
−
@@ -213,14 +192,79 @@
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)" 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 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>
@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>
<!-- Tally + apply -->
@@ -453,22 +453,42 @@ $red: #b91c1c;
}
/* ─── Denomination groups ─────────────────────────────────────────── */
.sat__denom-group {
padding: 8px 14px 4px;
& + & {
border-top: 1px solid #f1f5f9;
}
/* ── Denomination table: Denom · Qty · Amount ── */
.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;
justify-content: space-between;
align-items: baseline;
margin-bottom: 5px;
padding: 12px 16px 4px;
}
.sat__denom-group-title {
.sat__dt-section-title {
font-size: 10px;
font-weight: 800;
text-transform: uppercase;
@@ -476,51 +496,52 @@ $red: #b91c1c;
color: $slate;
}
.sat__denom-group-total {
.sat__dt-section-total {
font-size: 12px;
font-weight: 700;
color: $navy;
font-variant-numeric: tabular-nums;
}
.sat__denom-grid {
.sat__dt-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 6px;
}
/* ── Single denomination tile ──
Two-row layout: top has the denomination chip + running subtotal,
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;
grid-template-columns: 1fr 140px 120px;
align-items: center;
gap: 10px;
padding: 7px 16px;
border-bottom: 1px dotted #e2e8f0;
transition: background 0.12s ease;
&--active {
border-color: #86efac;
background: #f0fdf4;
box-shadow: 0 1px 3px -1px rgb(4 120 87 / 18%);
}
}
.sat__denom-top {
.sat__dt-denom {
display: flex;
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 {
@@ -548,6 +569,14 @@ $red: #b91c1c;
color: #854d0e;
border: 1px solid #fcd34d;
}
&--circle {
min-width: 36px;
width: 36px;
height: 36px;
padding: 0;
border-radius: 50%;
}
}
.sat__denom-amount {
@@ -19,6 +19,7 @@ import { Router, ActivatedRoute, RouterLink } from '@angular/router';
/** Custom Services */
import { SavingsService } from '../../savings.service';
import { DenominationMasterService } from 'app/system/denomination-master/denomination-master.service';
import { ClientsService } from 'app/clients/clients.service';
import { SettingsService } from 'app/settings/settings.service';
import { Dates } from 'app/core/utils/dates';
@@ -37,6 +38,7 @@ interface Denomination {
value: number;
kind: 'note' | 'coin';
label: string;
active: boolean;
}
/**
@@ -68,6 +70,7 @@ export class SavingsAccountTransactionsComponent implements OnInit {
private clientsService = inject(ClientsService);
private settingsService = inject(SettingsService);
private dialog = inject(MatDialog);
private denominationMasterService = inject(DenominationMasterService);
/** Minimum Due Date allowed. */
minDate = new Date(2000, 0, 1);
@@ -93,24 +96,14 @@ export class SavingsAccountTransactionsComponent implements OnInit {
savingAccountId: string;
currency: Currency | null = null;
/** Indian banknote denominations (descending). */
readonly 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' }
];
/** Banknote denominations — loaded dynamically from the Denomination Master (active only). */
notes: Denomination[] = [];
/** Indian coin denominations (descending). */
readonly coins: Denomination[] = [
{ value: 20, kind: 'coin', label: '₹20' },
{ value: 10, kind: 'coin', label: '₹10' },
{ value: 5, kind: 'coin', label: '₹5' },
{ value: 2, kind: 'coin', label: '₹2' },
{ value: 1, kind: 'coin', label: 'Re 1' }
];
/** Coin denominations — loaded dynamically from the Denomination Master. */
coins: Denomination[] = [];
/** When false, discontinued (inactive) denominations stay hidden in the grid. */
showInactive = false;
/** Quantity per denomination — keyed by `${kind}:${value}`. */
denominationCounts: Record<string, number> = {};
@@ -152,9 +145,40 @@ export class SavingsAccountTransactionsComponent implements OnInit {
ngOnInit() {
this.maxDate = this.settingsService.businessDate;
this.createSavingAccountTransactionForm();
this.loadDenominations();
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
* strip (account number, client name, KYC status, risk level, same-day
@@ -104,155 +104,172 @@
</div>
</header>
<!-- Cash movement (Deposit / Withdrawal / Hold Amount) -->
<div class="sav-menu__group">
<div class="sav-menu__group-title">Transact</div>
@for (item of buttonConfig.singleButtons; track item.name) {
@if (kindOf(item.name) === 'transact') {
<button
mat-menu-item
class="sav-menu__item sav-menu__item--transact"
*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 class="sav-menu__body">
<!-- Transact: singleButtons + options of kind 'transact' -->
@if (hasKind('transact')) {
<div class="sav-menu__group">
<div class="sav-menu__group-title">Transact</div>
@for (item of buttonConfig.singleButtons; track item.name) {
@if (kindOf(item.name) === 'transact') {
<button
mat-menu-item
class="sav-menu__item sav-menu__item--transact"
*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) === '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>
<!-- 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>
</mat-menu>
</header>
@@ -170,6 +170,14 @@ export class SavingsAccountViewComponent implements OnInit {
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`
* @param {ActivatedRoute} route Activated Route
@@ -16,6 +16,10 @@ export class DenominationMasterService {
return this.http.get<any[]>('/denominationmaster');
}
getActive() {
return this.http.get<any[]>('/denominationmaster/active');
}
create(data: any) {
return this.http.post('/denominationmaster', data);
}
+22
View File
@@ -1061,12 +1061,34 @@ mat-card {
.sav-action-menu .mat-mdc-menu-content {
padding: 0 !important;
overflow: hidden !important;
}
.sav-menu {
display: flex;
flex-direction: column;
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 {