feat: Denomination Master admin UI (modern fintech design)
Standalone Angular page to manage currency notes & coins: hero + stat tiles, notes/coins card grids, slide-in drawer for add/edit. Supports CRUD, enable/disable, and client-side duplicate (denomination+kind) validation with snackbar feedback. Added route and Admin menu link. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
33b01d667d
commit
918a90368c
@@ -404,6 +404,10 @@
|
||||
<fa-icon icon="key" class="menu-item-icon" style="color: #7c3aed"></fa-icon>
|
||||
OTP Audit Logs
|
||||
</button>
|
||||
<button mat-menu-item routerLink="/system/denomination-master" tabindex="0">
|
||||
<fa-icon icon="coins" class="menu-item-icon" style="color: #7c3aed"></fa-icon>
|
||||
Denomination Master
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Audit & Compliance -->
|
||||
|
||||
@@ -0,0 +1,289 @@
|
||||
<!--
|
||||
Copyright since 2025 Mifos Initiative
|
||||
|
||||
This Source Code Form is subject to the terms of the Mozilla Public
|
||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
-->
|
||||
<div class="denom-page">
|
||||
<!-- LEFT CONTENT COLUMN -->
|
||||
<div class="denom-content">
|
||||
<!-- HERO -->
|
||||
<header class="hero">
|
||||
<div class="hero-text">
|
||||
<div class="eyebrow">CURRENCY · CONFIGURATION</div>
|
||||
<h1 class="hero-title">Denomination master</h1>
|
||||
<p class="hero-sub">
|
||||
Define which currency notes and coins the bank accepts. Disable a denomination to phase it out of new
|
||||
transactions without losing history.
|
||||
</p>
|
||||
</div>
|
||||
<button class="hero-add-btn" (click)="openAdd()" aria-label="Add denomination">
|
||||
<mat-icon>add</mat-icon>
|
||||
Add denomination
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<!-- STAT TILES -->
|
||||
<section class="stat-grid">
|
||||
<div class="stat-tile">
|
||||
<div class="stat-head">
|
||||
<span class="stat-icon ic-dark"><mat-icon>account_balance_wallet</mat-icon></span>
|
||||
<span class="stat-label">Total denominations</span>
|
||||
</div>
|
||||
<div class="stat-value">{{ total }}</div>
|
||||
<div class="stat-foot">Across notes and coins</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-tile">
|
||||
<div class="stat-head">
|
||||
<span class="stat-icon ic-note"><mat-icon>payments</mat-icon></span>
|
||||
<span class="stat-label">Note types</span>
|
||||
</div>
|
||||
<div class="stat-value">{{ notes.length }}</div>
|
||||
<div class="stat-foot">{{ activeNotes }} currently active</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-tile">
|
||||
<div class="stat-head">
|
||||
<span class="stat-icon ic-coin"><mat-icon>monetization_on</mat-icon></span>
|
||||
<span class="stat-label">Coin types</span>
|
||||
</div>
|
||||
<div class="stat-value">{{ coins.length }}</div>
|
||||
<div class="stat-foot">{{ activeCoins }} currently active</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-tile">
|
||||
<div class="stat-head">
|
||||
<span class="stat-icon ic-ok"><mat-icon>check_circle</mat-icon></span>
|
||||
<span class="stat-label">Active</span>
|
||||
</div>
|
||||
<div class="stat-value">{{ activeCount }}</div>
|
||||
<div class="stat-foot">{{ disabledCount }} disabled</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- CURRENCY NOTES -->
|
||||
<section class="denom-section" *ngIf="notes.length > 0">
|
||||
<div class="section-head">
|
||||
<h2 class="section-title">Currency notes</h2>
|
||||
<span class="section-count">{{ sectionCount(activeNotes, notes.length) }}</span>
|
||||
</div>
|
||||
<div class="card-grid">
|
||||
<article class="denom-card" *ngFor="let item of notes" [class.inactive]="!item.isActive">
|
||||
<div class="card-top">
|
||||
<div class="bill-swatch" [style.background]="noteColor(item.denomination)">₹{{ item.denomination }}</div>
|
||||
<div class="card-info">
|
||||
<div class="card-desc">{{ item.description || item.denomination + ' Rupee Note' }}</div>
|
||||
<div class="card-meta">Introduced {{ item.introducedOn || '—' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-actions">
|
||||
<span
|
||||
class="status-pill"
|
||||
[class.is-active]="item.isActive"
|
||||
[class.is-inactive]="!item.isActive"
|
||||
[attr.aria-label]="'Status: ' + (item.isActive ? 'Active' : 'Discontinued')"
|
||||
>
|
||||
<span class="status-dot"></span>{{ item.isActive ? 'Active' : 'Discontinued' }}
|
||||
</span>
|
||||
<div class="icon-btns">
|
||||
<button class="icon-btn" (click)="openEdit(item)" aria-label="Edit denomination">
|
||||
<mat-icon>edit</mat-icon>
|
||||
</button>
|
||||
<div class="menu-wrap">
|
||||
<button class="icon-btn" (click)="toggleMenu(item, $event)" aria-label="More actions">
|
||||
<mat-icon>more_horiz</mat-icon>
|
||||
</button>
|
||||
<div class="ctx-menu" *ngIf="openMenuId === item.id" (click)="$event.stopPropagation()">
|
||||
<button class="ctx-item" (click)="toggleStatus(item)">
|
||||
<mat-icon>{{ item.isActive ? 'block' : 'check_circle' }}</mat-icon>
|
||||
{{ item.isActive ? 'Disable' : 'Enable' }}
|
||||
</button>
|
||||
<button class="ctx-item ctx-danger" (click)="deleteDenomination(item)">
|
||||
<mat-icon>delete_outline</mat-icon>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="disc-row" *ngIf="item.discontinuedOn">
|
||||
<span class="disc-label">Discontinued</span>
|
||||
<span class="disc-value">{{ item.discontinuedOn }}</span>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- COINS -->
|
||||
<section class="denom-section" *ngIf="coins.length > 0">
|
||||
<div class="section-head">
|
||||
<h2 class="section-title">Coins</h2>
|
||||
<span class="section-count">{{ sectionCount(activeCoins, coins.length) }}</span>
|
||||
</div>
|
||||
<div class="card-grid">
|
||||
<article class="denom-card" *ngFor="let item of coins" [class.inactive]="!item.isActive">
|
||||
<div class="card-top">
|
||||
<div class="coin-disc" [style.background]="coinColor(item.denomination)">₹{{ item.denomination }}</div>
|
||||
<div class="card-info">
|
||||
<div class="card-desc">{{ item.description || item.denomination + ' Rupee Coin' }}</div>
|
||||
<div class="card-meta">Introduced {{ item.introducedOn || '—' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-actions">
|
||||
<span
|
||||
class="status-pill"
|
||||
[class.is-active]="item.isActive"
|
||||
[class.is-inactive]="!item.isActive"
|
||||
[attr.aria-label]="'Status: ' + (item.isActive ? 'Active' : 'Discontinued')"
|
||||
>
|
||||
<span class="status-dot"></span>{{ item.isActive ? 'Active' : 'Discontinued' }}
|
||||
</span>
|
||||
<div class="icon-btns">
|
||||
<button class="icon-btn" (click)="openEdit(item)" aria-label="Edit denomination">
|
||||
<mat-icon>edit</mat-icon>
|
||||
</button>
|
||||
<div class="menu-wrap">
|
||||
<button class="icon-btn" (click)="toggleMenu(item, $event)" aria-label="More actions">
|
||||
<mat-icon>more_horiz</mat-icon>
|
||||
</button>
|
||||
<div class="ctx-menu" *ngIf="openMenuId === item.id" (click)="$event.stopPropagation()">
|
||||
<button class="ctx-item" (click)="toggleStatus(item)">
|
||||
<mat-icon>{{ item.isActive ? 'block' : 'check_circle' }}</mat-icon>
|
||||
{{ item.isActive ? 'Disable' : 'Enable' }}
|
||||
</button>
|
||||
<button class="ctx-item ctx-danger" (click)="deleteDenomination(item)">
|
||||
<mat-icon>delete_outline</mat-icon>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="disc-row" *ngIf="item.discontinuedOn">
|
||||
<span class="disc-label">Discontinued</span>
|
||||
<span class="disc-value">{{ item.discontinuedOn }}</span>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- DRAWER BACKDROP -->
|
||||
<div class="drawer-backdrop" *ngIf="drawerOpen" (click)="closeDrawer()"></div>
|
||||
|
||||
<!-- RIGHT DRAWER -->
|
||||
<aside class="drawer" [class.open]="drawerOpen" role="dialog" aria-modal="true" aria-label="Add denomination">
|
||||
<div class="drawer-head">
|
||||
<div>
|
||||
<h3 class="drawer-title">{{ editingId ? 'Edit denomination' : 'Add denomination' }}</h3>
|
||||
<p class="drawer-sub">{{ editingId ? 'Update an existing note or coin' : 'Define a new note or coin' }}</p>
|
||||
</div>
|
||||
<button class="icon-btn close-btn" (click)="closeDrawer()" aria-label="Close"><mat-icon>close</mat-icon></button>
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="drawerOpen">
|
||||
<form class="drawer-form" (ngSubmit)="save()">
|
||||
<!-- Type segmented control -->
|
||||
<div class="field">
|
||||
<label class="field-label">Type</label>
|
||||
<div class="segmented" role="radiogroup" aria-label="Denomination type">
|
||||
<button
|
||||
type="button"
|
||||
class="seg-btn"
|
||||
role="radio"
|
||||
[attr.aria-checked]="model.kind === 'note'"
|
||||
[class.active]="model.kind === 'note'"
|
||||
(click)="setType('note')"
|
||||
>
|
||||
<mat-icon>payments</mat-icon> Note
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="seg-btn"
|
||||
role="radio"
|
||||
[attr.aria-checked]="model.kind === 'coin'"
|
||||
[class.active]="model.kind === 'coin'"
|
||||
(click)="setType('coin')"
|
||||
>
|
||||
<mat-icon>monetization_on</mat-icon> Coin
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Denomination -->
|
||||
<div class="field">
|
||||
<label class="field-label" for="denomInput">Denomination (₹)</label>
|
||||
<input
|
||||
id="denomInput"
|
||||
class="text-input"
|
||||
type="number"
|
||||
min="1"
|
||||
step="1"
|
||||
placeholder="e.g. 500"
|
||||
[(ngModel)]="model.denomination"
|
||||
name="denomination"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<div class="field">
|
||||
<label class="field-label" for="descInput">Description</label>
|
||||
<input
|
||||
id="descInput"
|
||||
class="text-input"
|
||||
type="text"
|
||||
placeholder="500 Rupee Note"
|
||||
[(ngModel)]="model.description"
|
||||
name="description"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Introduced on -->
|
||||
<div class="field">
|
||||
<label class="field-label" for="dateInput">Introduced on</label>
|
||||
<input
|
||||
id="dateInput"
|
||||
class="text-input"
|
||||
type="date"
|
||||
[max]="today"
|
||||
[(ngModel)]="model.introducedOn"
|
||||
name="introducedOn"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Active toggle -->
|
||||
<div class="toggle-block">
|
||||
<div class="toggle-text">
|
||||
<span class="toggle-title">Active</span>
|
||||
<span class="toggle-help">Available for transactions</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="pill-toggle"
|
||||
[class.on]="model.isActive"
|
||||
(click)="toggleActive()"
|
||||
role="switch"
|
||||
[attr.aria-checked]="model.isActive"
|
||||
aria-label="Active"
|
||||
>
|
||||
<span class="pill-dot"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="drawer-actions">
|
||||
<button type="button" class="btn btn-cancel" (click)="closeDrawer()">Cancel</button>
|
||||
<button type="submit" class="btn btn-save" [disabled]="!formValid || saving">
|
||||
{{ editingId ? 'Update' : 'Save' }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</ng-container>
|
||||
</aside>
|
||||
</div>
|
||||
@@ -0,0 +1,747 @@
|
||||
/**
|
||||
* Copyright since 2025 Mifos Initiative
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
|
||||
|
||||
:host {
|
||||
/* Colors */
|
||||
--bg-page: #faf8f4;
|
||||
--bg-card: #fff;
|
||||
--bg-soft: #f2ede0;
|
||||
--border: #ece6d8;
|
||||
--border-strong: #e5e0d6;
|
||||
--text-primary: #1a1a1a;
|
||||
--text-secondary: #5f5c57;
|
||||
--text-muted: #8a867f;
|
||||
--accent-dark: #1a1a1a;
|
||||
--accent-eyebrow: #0f4f3c;
|
||||
--status-active-bg: #e8f3e8;
|
||||
--status-active-text: #1f6f3d;
|
||||
--status-inactive-bg: #f4ede0;
|
||||
--status-inactive-text: #8a6a2e;
|
||||
--icon-bg-dark: #1a1a1a;
|
||||
--icon-fg-dark: #faf8f4;
|
||||
--icon-bg-note: #eef0f8;
|
||||
--icon-fg-note: #3b4f8f;
|
||||
--icon-bg-coin: #fbf1d6;
|
||||
--icon-fg-coin: #8a6a2e;
|
||||
--icon-bg-ok: #e8f3e8;
|
||||
--icon-fg-ok: #1f6f3d;
|
||||
--r-sm: 8px;
|
||||
--r-md: 10px;
|
||||
--r-lg: 14px;
|
||||
--r-xl: 16px;
|
||||
--r-pill: 999px;
|
||||
|
||||
display: block;
|
||||
font-family:
|
||||
Inter,
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
'Segoe UI',
|
||||
Roboto,
|
||||
sans-serif;
|
||||
color: var(--text-primary);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
.denom-page {
|
||||
position: relative;
|
||||
min-height: 100%;
|
||||
background: var(--bg-page);
|
||||
padding: 36px 40px;
|
||||
overflow-x: clip;
|
||||
}
|
||||
|
||||
.denom-content {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* ---------- HERO ---------- */
|
||||
.hero {
|
||||
margin-bottom: 32px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.hero-text {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.hero-add-btn {
|
||||
flex: 0 0 auto;
|
||||
align-self: flex-start;
|
||||
width: auto;
|
||||
height: 38px;
|
||||
margin-top: 4px;
|
||||
padding: 0 16px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 7px;
|
||||
font-family: inherit;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.01em;
|
||||
white-space: nowrap;
|
||||
border: none;
|
||||
border-radius: var(--r-pill);
|
||||
cursor: pointer;
|
||||
color: #fff;
|
||||
background: linear-gradient(135deg, #1f7a4d 0%, #0f4f3c 100%);
|
||||
box-shadow: 0 4px 14px -4px rgb(15 79 60 / 45%);
|
||||
transition:
|
||||
transform 0.15s ease,
|
||||
box-shadow 0.15s ease,
|
||||
filter 0.15s ease;
|
||||
}
|
||||
|
||||
.hero-add-btn:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 8px 22px -6px rgb(15 79 60 / 55%);
|
||||
filter: brightness(1.06);
|
||||
}
|
||||
|
||||
.hero-add-btn:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.hero-add-btn mat-icon {
|
||||
font-size: 17px;
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
line-height: 17px;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.12em;
|
||||
color: var(--accent-eyebrow);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 34px;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.02em;
|
||||
color: var(--text-primary);
|
||||
margin: 0 0 12px;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.hero-sub {
|
||||
font-size: 15px;
|
||||
color: var(--text-secondary);
|
||||
max-width: 520px;
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* ---------- STAT TILES ---------- */
|
||||
.stat-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 12px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.stat-tile {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--r-xl);
|
||||
padding: 20px;
|
||||
min-height: 120px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.stat-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: var(--r-sm);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.stat-icon mat-icon {
|
||||
font-size: 16px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.ic-dark {
|
||||
background: var(--icon-bg-dark);
|
||||
color: var(--icon-fg-dark);
|
||||
}
|
||||
|
||||
.ic-note {
|
||||
background: var(--icon-bg-note);
|
||||
color: var(--icon-fg-note);
|
||||
}
|
||||
|
||||
.ic-coin {
|
||||
background: var(--icon-bg-coin);
|
||||
color: var(--icon-fg-coin);
|
||||
}
|
||||
|
||||
.ic-ok {
|
||||
background: var(--icon-bg-ok);
|
||||
color: var(--icon-fg-ok);
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 40px;
|
||||
font-weight: 500;
|
||||
letter-spacing: -0.03em;
|
||||
font-variant-numeric: tabular-nums;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.stat-foot {
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* ---------- SECTION ---------- */
|
||||
.denom-section {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.section-head {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.01em;
|
||||
color: var(--text-primary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.section-count {
|
||||
font-size: 13px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* ---------- CARD GRID ---------- */
|
||||
.card-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.denom-card {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--r-lg);
|
||||
padding: 18px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
transition: opacity 0.15s ease;
|
||||
}
|
||||
|
||||
.denom-card.inactive {
|
||||
opacity: 0.65;
|
||||
}
|
||||
|
||||
.card-top {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.bill-swatch {
|
||||
width: 64px;
|
||||
height: 36px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid rgb(0 0 0 / 8%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
font-variant-numeric: tabular-nums;
|
||||
color: #2a2a2a;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.coin-disc {
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: #5c3a00;
|
||||
flex: 0 0 auto;
|
||||
box-shadow:
|
||||
inset 0 -2px 0 rgb(0 0 0 / 10%),
|
||||
0 1px 3px rgb(0 0 0 / 8%);
|
||||
}
|
||||
|
||||
.card-info {
|
||||
min-width: 0;
|
||||
padding-top: 2px;
|
||||
}
|
||||
|
||||
.card-desc {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.card-meta {
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.status-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
padding: 3px 9px;
|
||||
border-radius: var(--r-pill);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.status-pill.is-active {
|
||||
background: var(--status-active-bg);
|
||||
color: var(--status-active-text);
|
||||
}
|
||||
|
||||
.status-pill.is-inactive {
|
||||
background: var(--status-inactive-bg);
|
||||
color: var(--status-inactive-text);
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: currentcolor;
|
||||
display: inline-block;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.icon-btns {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.icon-btn {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--bg-page);
|
||||
color: var(--text-secondary);
|
||||
border-radius: var(--r-sm);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
transition:
|
||||
background 0.12s ease,
|
||||
color 0.12s ease;
|
||||
}
|
||||
|
||||
.icon-btn:hover {
|
||||
background: var(--bg-soft);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.icon-btn mat-icon {
|
||||
font-size: 16px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.menu-wrap {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ctx-menu {
|
||||
position: absolute;
|
||||
top: 34px;
|
||||
right: 0;
|
||||
z-index: 30;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border-strong);
|
||||
border-radius: var(--r-md);
|
||||
box-shadow: 0 8px 24px -8px rgb(0 0 0 / 18%);
|
||||
padding: 4px;
|
||||
min-width: 140px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.ctx-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
text-align: left;
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 8px 10px;
|
||||
font-size: 13px;
|
||||
color: var(--text-primary);
|
||||
border-radius: var(--r-sm);
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.ctx-item:hover {
|
||||
background: var(--bg-soft);
|
||||
}
|
||||
|
||||
.ctx-item mat-icon {
|
||||
font-size: 16px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.ctx-danger {
|
||||
color: #c0392b;
|
||||
}
|
||||
|
||||
.ctx-danger:hover {
|
||||
background: #fbe9e7;
|
||||
}
|
||||
|
||||
.disc-row {
|
||||
border-top: 1px solid #f2ede0;
|
||||
padding-top: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.disc-label {
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.disc-value {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
/* ---------- DRAWER ---------- */
|
||||
.drawer-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgb(26 26 26 / 18%);
|
||||
z-index: 1000;
|
||||
animation: fade-in 0.18s ease;
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.drawer {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 380px;
|
||||
background: var(--bg-card);
|
||||
border-left: 1px solid var(--border);
|
||||
padding: 32px 28px;
|
||||
box-shadow: -20px 0 40px -20px rgb(0 0 0 / 18%);
|
||||
overflow-y: auto;
|
||||
z-index: 1001;
|
||||
transform: translateX(100%);
|
||||
transition: transform 0.24s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.drawer.open {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.drawer-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.drawer-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.drawer-sub {
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
margin: 4px 0 0;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.drawer-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.field-label {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.text-input {
|
||||
box-sizing: border-box;
|
||||
height: 42px;
|
||||
padding: 0 14px;
|
||||
border: 1px solid var(--border-strong);
|
||||
border-radius: var(--r-md);
|
||||
background: var(--bg-page);
|
||||
font-size: 14px;
|
||||
color: var(--text-primary);
|
||||
font-family: inherit;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.text-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent-dark);
|
||||
background: var(--bg-card);
|
||||
}
|
||||
|
||||
.text-input::placeholder {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* segmented control */
|
||||
.segmented {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
padding: 3px;
|
||||
background: var(--bg-soft);
|
||||
border: 1px solid var(--border-strong);
|
||||
border-radius: var(--r-md);
|
||||
}
|
||||
|
||||
.seg-btn {
|
||||
flex: 1;
|
||||
height: 36px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
border-radius: 7px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.seg-btn mat-icon {
|
||||
font-size: 16px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.seg-btn.active {
|
||||
background: var(--bg-card);
|
||||
color: var(--text-primary);
|
||||
box-shadow: 0 1px 2px rgb(0 0 0 / 6%);
|
||||
}
|
||||
|
||||
/* active toggle block */
|
||||
.toggle-block {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: var(--bg-page);
|
||||
border: 1px solid var(--border-strong);
|
||||
border-radius: var(--r-md);
|
||||
padding: 12px 14px;
|
||||
}
|
||||
|
||||
.toggle-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.toggle-title {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.toggle-help {
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.pill-toggle {
|
||||
width: 36px;
|
||||
height: 20px;
|
||||
border-radius: var(--r-pill);
|
||||
background: #d9d3c5;
|
||||
border: none;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
flex: 0 0 auto;
|
||||
transition: background 0.16s ease;
|
||||
}
|
||||
|
||||
.pill-toggle.on {
|
||||
background: var(--accent-dark);
|
||||
}
|
||||
|
||||
.pill-dot {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
background: #fff;
|
||||
transition: transform 0.16s ease;
|
||||
}
|
||||
|
||||
.pill-toggle.on .pill-dot {
|
||||
transform: translateX(16px);
|
||||
}
|
||||
|
||||
/* drawer actions */
|
||||
.drawer-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
flex: 1;
|
||||
height: 42px;
|
||||
padding: 0 18px;
|
||||
border-radius: var(--r-pill);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.btn mat-icon {
|
||||
font-size: 18px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.btn-cancel {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border-strong);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.btn-save {
|
||||
background: var(--accent-dark);
|
||||
border: 1px solid var(--accent-dark);
|
||||
color: var(--bg-page);
|
||||
}
|
||||
|
||||
.btn-save:disabled {
|
||||
opacity: 0.45;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* ---------- RESPONSIVE ---------- */
|
||||
@media (width <= 1100px) {
|
||||
.drawer {
|
||||
width: min(380px, 90vw);
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 820px) {
|
||||
.hero {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.stat-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.card-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
/**
|
||||
* Copyright since 2025 Mifos Initiative
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
import { Component, OnInit, HostListener, inject } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { MatDialog, MatDialogModule } from '@angular/material/dialog';
|
||||
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { DenominationMasterService } from './denomination-master.service';
|
||||
import { DeleteDialogComponent } from 'app/shared/delete-dialog/delete-dialog.component';
|
||||
|
||||
interface Denomination {
|
||||
id: number;
|
||||
denomination: number;
|
||||
kind: 'note' | 'coin';
|
||||
description: string;
|
||||
isActive: boolean;
|
||||
introducedOn: string | null;
|
||||
discontinuedOn: string | null;
|
||||
}
|
||||
|
||||
const NOTE_COLORS: { [k: number]: string } = {
|
||||
2000: '#E8C6F4',
|
||||
1000: '#F5C6C6',
|
||||
500: '#C9D5F2',
|
||||
200: '#F5DDA9',
|
||||
100: '#C7DCC7',
|
||||
50: '#D9C8F2',
|
||||
20: '#F5C9A3',
|
||||
10: '#C7D4DC'
|
||||
};
|
||||
const COIN_COLORS: { [k: number]: string } = {
|
||||
20: '#E5C04A',
|
||||
10: '#D4A949',
|
||||
5: '#C9A24F',
|
||||
2: '#BD9B58',
|
||||
1: '#B89460'
|
||||
};
|
||||
|
||||
@Component({
|
||||
selector: 'mifosx-denomination-master',
|
||||
templateUrl: './denomination-master.component.html',
|
||||
styleUrls: ['./denomination-master.component.scss'],
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
MatDialogModule,
|
||||
MatSnackBarModule,
|
||||
MatIconModule
|
||||
]
|
||||
})
|
||||
export class DenominationMasterComponent implements OnInit {
|
||||
private service = inject(DenominationMasterService);
|
||||
private dialog = inject(MatDialog);
|
||||
private snackBar = inject(MatSnackBar);
|
||||
|
||||
notes: Denomination[] = [];
|
||||
coins: Denomination[] = [];
|
||||
allItems: Denomination[] = [];
|
||||
|
||||
drawerOpen = false;
|
||||
editingId: number | null = null;
|
||||
saving = false;
|
||||
openMenuId: number | null = null;
|
||||
|
||||
model = {
|
||||
kind: 'note' as 'note' | 'coin',
|
||||
denomination: null as number | null,
|
||||
description: '',
|
||||
introducedOn: '',
|
||||
isActive: true
|
||||
};
|
||||
|
||||
ngOnInit(): void {
|
||||
this.load();
|
||||
}
|
||||
|
||||
load(): void {
|
||||
this.service.getAll().subscribe((data: Denomination[]) => {
|
||||
this.allItems = data || [];
|
||||
this.notes = this.allItems.filter((d) => d.kind === 'note').sort((a, b) => b.denomination - a.denomination);
|
||||
this.coins = this.allItems.filter((d) => d.kind === 'coin').sort((a, b) => b.denomination - a.denomination);
|
||||
});
|
||||
}
|
||||
|
||||
// ---- derived stats ----
|
||||
get total(): number {
|
||||
return this.allItems.length;
|
||||
}
|
||||
get activeNotes(): number {
|
||||
return this.notes.filter((n) => n.isActive).length;
|
||||
}
|
||||
get activeCoins(): number {
|
||||
return this.coins.filter((c) => c.isActive).length;
|
||||
}
|
||||
get activeCount(): number {
|
||||
return this.allItems.filter((i) => i.isActive).length;
|
||||
}
|
||||
get disabledCount(): number {
|
||||
return this.allItems.filter((i) => !i.isActive).length;
|
||||
}
|
||||
|
||||
// ---- colors ----
|
||||
noteColor(value: number): string {
|
||||
return NOTE_COLORS[value] || '#E5E0D6';
|
||||
}
|
||||
coinColor(value: number): string {
|
||||
return COIN_COLORS[value] || '#D4A949';
|
||||
}
|
||||
|
||||
sectionCount(active: number, count: number): string {
|
||||
if (count > 0 && active === count) {
|
||||
return `${count} types · all active`;
|
||||
}
|
||||
return `${count} types · ${active} active`;
|
||||
}
|
||||
|
||||
// ---- drawer ----
|
||||
openAdd(): void {
|
||||
this.editingId = null;
|
||||
this.model = { kind: 'note', denomination: null, description: '', introducedOn: '', isActive: true };
|
||||
this.drawerOpen = true;
|
||||
}
|
||||
|
||||
openEdit(item: Denomination): void {
|
||||
this.editingId = item.id;
|
||||
this.openMenuId = null;
|
||||
this.model = {
|
||||
kind: item.kind,
|
||||
denomination: item.denomination,
|
||||
description: item.description || '',
|
||||
introducedOn: item.introducedOn || '',
|
||||
isActive: item.isActive
|
||||
};
|
||||
this.drawerOpen = true;
|
||||
}
|
||||
|
||||
closeDrawer(): void {
|
||||
this.drawerOpen = false;
|
||||
this.editingId = null;
|
||||
}
|
||||
|
||||
setType(kind: 'note' | 'coin'): void {
|
||||
this.model.kind = kind;
|
||||
}
|
||||
toggleActive(): void {
|
||||
this.model.isActive = !this.model.isActive;
|
||||
}
|
||||
|
||||
get today(): string {
|
||||
return new Date().toISOString().split('T')[0];
|
||||
}
|
||||
|
||||
get formValid(): boolean {
|
||||
const v = this.model.denomination;
|
||||
const denomOk = v != null && Number.isInteger(+v) && +v > 0;
|
||||
const descOk = this.model.description.trim().length > 0;
|
||||
const dateOk = !!this.model.introducedOn && this.model.introducedOn <= this.today;
|
||||
return denomOk && descOk && dateOk;
|
||||
}
|
||||
|
||||
save(): void {
|
||||
if (!this.formValid || this.saving) {
|
||||
return;
|
||||
}
|
||||
|
||||
const denom = +this.model.denomination!;
|
||||
const kind = this.model.kind;
|
||||
|
||||
// duplicate guard: same denomination + same kind cannot exist twice
|
||||
const duplicate = this.allItems.find((i) => i.kind === kind && i.denomination === denom && i.id !== this.editingId);
|
||||
if (duplicate) {
|
||||
const kindLabel = kind === 'note' ? 'note' : 'coin';
|
||||
this.snackBar.open(`A ₹${denom} ${kindLabel} already exists. Duplicate denominations are not allowed.`, 'Close', {
|
||||
duration: 5000
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.saving = true;
|
||||
const payload = {
|
||||
denomination: this.model.denomination,
|
||||
kind: this.model.kind,
|
||||
description: this.model.description.trim(),
|
||||
isActive: this.model.isActive,
|
||||
introducedOn: this.model.introducedOn
|
||||
};
|
||||
const obs = this.editingId ? this.service.update(this.editingId, payload) : this.service.create(payload);
|
||||
const wasEdit = this.editingId != null;
|
||||
obs.subscribe({
|
||||
next: () => {
|
||||
this.snackBar.open(wasEdit ? 'Denomination updated' : 'Denomination added', 'Close', { duration: 3000 });
|
||||
this.closeDrawer();
|
||||
this.load();
|
||||
this.saving = false;
|
||||
},
|
||||
error: () => {
|
||||
this.saving = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ---- card actions ----
|
||||
toggleMenu(item: Denomination, event: MouseEvent): void {
|
||||
event.stopPropagation();
|
||||
this.openMenuId = this.openMenuId === item.id ? null : item.id;
|
||||
}
|
||||
|
||||
@HostListener('document:keydown.escape')
|
||||
onEsc(): void {
|
||||
if (this.openMenuId != null) {
|
||||
this.openMenuId = null;
|
||||
} else if (this.drawerOpen) {
|
||||
this.closeDrawer();
|
||||
}
|
||||
}
|
||||
|
||||
@HostListener('document:click')
|
||||
onDocumentClick(): void {
|
||||
if (this.openMenuId != null) {
|
||||
this.openMenuId = null;
|
||||
}
|
||||
}
|
||||
|
||||
closeMenu(): void {
|
||||
this.openMenuId = null;
|
||||
}
|
||||
|
||||
toggleStatus(item: Denomination): void {
|
||||
this.openMenuId = null;
|
||||
const obs = item.isActive ? this.service.deactivate(item.id) : this.service.activate(item.id);
|
||||
obs.subscribe(() => {
|
||||
this.snackBar.open(item.isActive ? 'Denomination disabled' : 'Denomination enabled', 'Close', { duration: 3000 });
|
||||
this.load();
|
||||
});
|
||||
}
|
||||
|
||||
deleteDenomination(item: Denomination): void {
|
||||
this.openMenuId = null;
|
||||
const ref = this.dialog.open(DeleteDialogComponent, {
|
||||
data: { deleteContext: `₹${item.denomination} ${item.kind}` }
|
||||
});
|
||||
ref.afterClosed().subscribe((res: any) => {
|
||||
if (res?.delete) {
|
||||
this.service.delete(item.id).subscribe(() => {
|
||||
this.snackBar.open('Denomination deleted', 'Close', { duration: 3000 });
|
||||
this.load();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Copyright since 2025 Mifos Initiative
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class DenominationMasterService {
|
||||
private http = inject(HttpClient);
|
||||
|
||||
getAll() {
|
||||
return this.http.get<any[]>('/denominationmaster');
|
||||
}
|
||||
|
||||
create(data: any) {
|
||||
return this.http.post('/denominationmaster', data);
|
||||
}
|
||||
|
||||
update(id: number, data: any) {
|
||||
return this.http.put(`/denominationmaster/${id}`, data);
|
||||
}
|
||||
|
||||
activate(id: number) {
|
||||
return this.http.put(`/denominationmaster/${id}?command=activate`, {});
|
||||
}
|
||||
|
||||
deactivate(id: number) {
|
||||
return this.http.put(`/denominationmaster/${id}?command=deactivate`, {});
|
||||
}
|
||||
|
||||
delete(id: number) {
|
||||
return this.http.delete(`/denominationmaster/${id}`);
|
||||
}
|
||||
}
|
||||
@@ -56,6 +56,7 @@ import { ViewRoleComponent } from './roles-and-permissions/view-role/view-role.c
|
||||
import { SystemComponent } from './system.component';
|
||||
import { SystemInformationComponent } from './system-information/system-information.component';
|
||||
import { AboutUsComponent } from './about-us/about-us.component';
|
||||
import { DenominationMasterComponent } from './denomination-master/denomination-master.component';
|
||||
|
||||
/** Custom Resolvers */
|
||||
import { AccountNumberPreferencesResolver } from './account-number-preferences/account-number-preferences.resolver';
|
||||
@@ -614,6 +615,16 @@ const routes: Routes = [
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: 'denomination-master',
|
||||
data: { title: 'Denomination Master', breadcrumb: 'Denomination Master' },
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
component: DenominationMasterComponent
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user