/** * 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, inject } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { MatIconModule } from '@angular/material/icon'; import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar'; import { CashierService } from '../cashier.service'; interface Denom { id: number; value: number; kind: 'note' | 'coin'; label: string; } /** Cashier cash-exchange entry: customer gives some denominations and takes equal-value change (net 0). */ @Component({ selector: 'mifosx-cash-exchange-entry', templateUrl: './cash-exchange-entry.component.html', styleUrls: ['./cash-exchange-entry.component.scss'], standalone: true, imports: [ CommonModule, FormsModule, MatIconModule, MatSnackBarModule ] }) export class CashExchangeEntryComponent implements OnInit { private service = inject(CashierService); private snackBar = inject(MatSnackBar); notes: Denom[] = []; coins: Denom[] = []; receivedCounts: Record = {}; givenCounts: Record = {}; exchangeDate = ''; note = ''; saving = false; // ── Customer info ── isAccountHolder = false; accountNo = ''; customerIdProof = ''; // ── Cashier position (for denomination overdraft check) ── private availableMap = new Map(); positionLoaded = false; /** All denominations (notes then coins) */ get denoms(): Denom[] { return [ ...this.notes, ...this.coins ]; } ngOnInit(): void { const d = new Date(); this.exchangeDate = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; this.service.getActiveDenominations().subscribe({ next: (list) => { const rows: Denom[] = (list || []).map((x: any) => ({ id: x.id, value: x.denomination, kind: x.kind, label: '₹' + x.denomination })); this.notes = rows.filter((r) => r.kind === 'note').sort((a, b) => b.value - a.value); this.coins = rows.filter((r) => r.kind === 'coin').sort((a, b) => b.value - a.value); }, error: () => { this.notes = []; this.coins = []; } }); this.service.getCashierPositionForMe().subscribe({ next: (res: any) => { this.availableMap.clear(); ((res?.denominations || []) as any[]).forEach((d: any) => { if (d.denominationId) this.availableMap.set(Number(d.denominationId), Number(d.count ?? 0)); }); this.positionLoaded = true; }, error: () => { this.positionLoaded = true; } }); } countFor(side: 'r' | 'g', d: Denom): number { return (side === 'r' ? this.receivedCounts : this.givenCounts)[d.id] || 0; } subtotal(side: 'r' | 'g', d: Denom): number { return this.countFor(side, d) * d.value; } onChange(side: 'r' | 'g', d: Denom, raw: any): void { (side === 'r' ? this.receivedCounts : this.givenCounts)[d.id] = Math.max(0, Math.floor(Number(raw) || 0)); } bump(side: 'r' | 'g', d: Denom, delta: number): void { const map = side === 'r' ? this.receivedCounts : this.givenCounts; map[d.id] = Math.max(0, (map[d.id] || 0) + delta); } get receivedTotal(): number { return this.denoms.reduce((s, d) => s + this.subtotal('r', d), 0); } get givenTotal(): number { return this.denoms.reduce((s, d) => s + this.subtotal('g', d), 0); } get difference(): number { return this.receivedTotal - this.givenTotal; } get matches(): boolean { return this.receivedTotal > 0 && this.receivedTotal === this.givenTotal; } /** Above ₹50,000 PAN/ID is mandatory per RBI guideline */ get needsIdProof(): boolean { return this.receivedTotal >= 50000; } /** Denominations where cashier is giving more than currently available. */ get givenOverdraftItems(): Array<{ label: string; available: number; requested: number }> { if (!this.positionLoaded) return []; return this.denoms .map((d) => ({ label: d.label, available: this.availableMap.get(d.id) ?? 0, requested: this.countFor('g', d) })) .filter((item) => item.requested > 0 && item.requested > item.available); } get hasGivenOverdraft(): boolean { return this.givenOverdraftItems.length > 0; } get valid(): boolean { return this.matches && (!this.needsIdProof || this.customerIdProof.trim().length > 0) && !this.hasGivenOverdraft; } toggleAccountHolder(): void { this.isAccountHolder = !this.isAccountHolder; if (!this.isAccountHolder) this.accountNo = ''; } clear(): void { this.receivedCounts = {}; this.givenCounts = {}; this.note = ''; this.accountNo = ''; this.customerIdProof = ''; this.isAccountHolder = false; } submit(): void { if (!this.valid || this.saving) return; this.saving = true; const build = (side: 'r' | 'g') => this.denoms .filter((d) => this.countFor(side, d) > 0) .map((d) => ({ denominationId: d.id, denominationValue: d.value, count: this.countFor(side, d) })); const payload: any = { exchangeDate: this.exchangeDate, note: this.note || null, customerIdProof: this.customerIdProof.trim() || null, accountNo: this.isAccountHolder && this.accountNo.trim() ? this.accountNo.trim() : null, received: build('r'), given: build('g') }; this.service.createCashExchange(payload).subscribe({ next: () => { this.snackBar.open('Cash exchange recorded', 'Close', { duration: 3000 }); this.clear(); this.saving = false; }, error: () => { this.saving = false; } }); } }