From 918a90368c6731b01803494948faf0d9a633399d Mon Sep 17 00:00:00 2001 From: Jaanu423 Date: Wed, 27 May 2026 11:15:47 +0530 Subject: [PATCH] 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) --- src/app/core/shell/shell.component.html | 4 + .../denomination-master.component.html | 289 +++++++ .../denomination-master.component.scss | 747 ++++++++++++++++++ .../denomination-master.component.ts | 258 ++++++ .../denomination-master.service.ts | 38 + src/app/system/system-routing.module.ts | 11 + 6 files changed, 1347 insertions(+) create mode 100644 src/app/system/denomination-master/denomination-master.component.html create mode 100644 src/app/system/denomination-master/denomination-master.component.scss create mode 100644 src/app/system/denomination-master/denomination-master.component.ts create mode 100644 src/app/system/denomination-master/denomination-master.service.ts diff --git a/src/app/core/shell/shell.component.html b/src/app/core/shell/shell.component.html index 0015c46a0..1c0fecc5b 100644 --- a/src/app/core/shell/shell.component.html +++ b/src/app/core/shell/shell.component.html @@ -404,6 +404,10 @@ OTP Audit Logs + diff --git a/src/app/system/denomination-master/denomination-master.component.html b/src/app/system/denomination-master/denomination-master.component.html new file mode 100644 index 000000000..64ef50df0 --- /dev/null +++ b/src/app/system/denomination-master/denomination-master.component.html @@ -0,0 +1,289 @@ + +
+ +
+ +
+
+
CURRENCY · CONFIGURATION
+

Denomination master

+

+ Define which currency notes and coins the bank accepts. Disable a denomination to phase it out of new + transactions without losing history. +

+
+ +
+ + +
+
+
+ account_balance_wallet + Total denominations +
+
{{ total }}
+
Across notes and coins
+
+ +
+
+ payments + Note types +
+
{{ notes.length }}
+
{{ activeNotes }} currently active
+
+ +
+
+ monetization_on + Coin types +
+
{{ coins.length }}
+
{{ activeCoins }} currently active
+
+ +
+
+ check_circle + Active +
+
{{ activeCount }}
+
{{ disabledCount }} disabled
+
+
+ + +
+
+

Currency notes

+ {{ sectionCount(activeNotes, notes.length) }} +
+
+
+
+
₹{{ item.denomination }}
+
+
{{ item.description || item.denomination + ' Rupee Note' }}
+
Introduced {{ item.introducedOn || '—' }}
+
+
+ +
+ + {{ item.isActive ? 'Active' : 'Discontinued' }} + +
+ + +
+
+ +
+ Discontinued + {{ item.discontinuedOn }} +
+
+
+
+ + +
+
+

Coins

+ {{ sectionCount(activeCoins, coins.length) }} +
+
+
+
+
₹{{ item.denomination }}
+
+
{{ item.description || item.denomination + ' Rupee Coin' }}
+
Introduced {{ item.introducedOn || '—' }}
+
+
+ +
+ + {{ item.isActive ? 'Active' : 'Discontinued' }} + +
+ + +
+
+ +
+ Discontinued + {{ item.discontinuedOn }} +
+
+
+
+
+ + +
+ + + +
diff --git a/src/app/system/denomination-master/denomination-master.component.scss b/src/app/system/denomination-master/denomination-master.component.scss new file mode 100644 index 000000000..724fcb1e1 --- /dev/null +++ b/src/app/system/denomination-master/denomination-master.component.scss @@ -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); + } +} diff --git a/src/app/system/denomination-master/denomination-master.component.ts b/src/app/system/denomination-master/denomination-master.component.ts new file mode 100644 index 000000000..c92218e92 --- /dev/null +++ b/src/app/system/denomination-master/denomination-master.component.ts @@ -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(); + }); + } + }); + } +} diff --git a/src/app/system/denomination-master/denomination-master.service.ts b/src/app/system/denomination-master/denomination-master.service.ts new file mode 100644 index 000000000..6b037b0ad --- /dev/null +++ b/src/app/system/denomination-master/denomination-master.service.ts @@ -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('/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}`); + } +} diff --git a/src/app/system/system-routing.module.ts b/src/app/system/system-routing.module.ts index 6d2ff8cd7..b99efa2ee 100644 --- a/src/app/system/system-routing.module.ts +++ b/src/app/system/system-routing.module.ts @@ -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 + } + ] } ] }