/** * 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 { MatFormFieldModule } from '@angular/material/form-field'; import { MatSelectModule } from '@angular/material/select'; import { MatInputModule } from '@angular/material/input'; import { of } from 'rxjs'; import { catchError } from 'rxjs/operators'; import { CashierService } from '../cashier.service'; import { businessToday } from 'app/core/utils/business-today'; interface Denom { id: number; value: number; kind: 'note' | 'coin'; label: string; } interface Cashier { id: number; label: string; } /** One side of the transfer, from GET /cashintercashier/cashier-summary. */ interface CashierSummary { cashierId: number; cashierName: string; tellerName: string; officeName: string; cashAllocated: boolean; allocatedAmount: number; totalAmount: number; /** Count held per denomination id. */ held: Record; } /** Move cash denomination-wise between two cashiers under the same teller. */ @Component({ selector: 'mifosx-inter-cashier-transfer', templateUrl: './inter-cashier-transfer.component.html', styleUrls: [ '../cashier-shared.scss', './inter-cashier-transfer.component.scss' ], standalone: true, imports: [ CommonModule, FormsModule, MatIconModule, MatSnackBarModule, MatFormFieldModule, MatSelectModule, MatInputModule ] }) export class InterCashierTransferComponent implements OnInit { private service = inject(CashierService); private snackBar = inject(MatSnackBar); notes: Denom[] = []; coins: Denom[] = []; counts: Record = {}; transferDate = ''; note = ''; saving = false; /** * The sending cashier is always the one logged in: cash is handed over from their own drawer, so there is * no one else to pick on that side. Only the receiving cashier is chosen. */ ownCashierMissing = false; tellerName = ''; allCashiers: Cashier[] = []; selectedTellerId: number | null = null; fromCashierId: number | null = null; toCashierId: number | null = null; fromParty = ''; toParty = ''; loadingCashiers = false; /** Details and denomination-wise cash of each side, so both are seen before the transfer is recorded. */ fromSummary: CashierSummary | null = null; toSummary: CashierSummary | null = null; loadingFrom = false; loadingTo = false; get denoms(): Denom[] { return [ ...this.notes, ...this.coins ]; } /** Everyone under the same teller except the sender. */ get availableTo(): Cashier[] { return this.allCashiers.filter((c) => c.id !== this.fromCashierId); } ngOnInit(): void { const d = businessToday(); this.transferDate = `${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 = []; } }); // The sender is the cashier behind this login; their counter then gives the receivers to pick from. this.service.getCurrentCashierInfo().subscribe((info) => { if (!info) { this.ownCashierMissing = true; return; } this.fromCashierId = info.cashierId; this.fromParty = info.cashierName; this.loadSummary('from'); }); } /** The other cashiers at this counter, one of whom receives the cash. */ private loadReceivers(tellerId: number): void { this.loadingCashiers = true; this.service .getCashiersForTeller(tellerId) .pipe(catchError(() => of({ cashiers: [] }))) .subscribe((r: any) => { this.allCashiers = (r?.cashiers || []).map((c: any) => ({ id: c.id, label: c.staffName })); this.loadingCashiers = false; }); } onToSelect(cashierId: number): void { this.toCashierId = cashierId; const c = this.allCashiers.find((x) => x.id === cashierId); this.toParty = c ? c.label : ''; this.loadSummary('to'); } onDateChange(value: string): void { this.transferDate = value; this.loadSummary('from'); this.loadSummary('to'); } /** Fetch one side's details, vault allocation and denomination-wise cash for the transfer date. */ private loadSummary(side: 'from' | 'to'): void { const cashierId = side === 'from' ? this.fromCashierId : this.toCashierId; if (side === 'from') { this.fromSummary = null; } else { this.toSummary = null; } if (!cashierId || !this.transferDate) return; this.setLoading(side, true); this.service .getInterCashierCashierSummary(cashierId, this.transferDate) .pipe(catchError(() => of(null))) .subscribe((r: any) => { this.setLoading(side, false); // A late answer for a cashier no longer selected on that side is dropped. const current = side === 'from' ? this.fromCashierId : this.toCashierId; if (!r || current !== cashierId) return; // The sender's summary also tells which counter they are at, hence who they can hand cash to. if (side === 'from' && r.tellerId && this.selectedTellerId !== Number(r.tellerId)) { this.selectedTellerId = Number(r.tellerId); this.tellerName = r.tellerName || ''; this.loadReceivers(this.selectedTellerId); } const held: Record = {}; for (const d of r.denominations || []) { held[d.denominationId] = (held[d.denominationId] || 0) + Number(d.count || 0); } const summary: CashierSummary = { cashierId: r.cashierId, cashierName: r.cashierName, tellerName: r.tellerName, officeName: r.officeName, cashAllocated: !!r.cashAllocated, allocatedAmount: Number(r.allocatedAmount || 0), totalAmount: Number(r.totalAmount || 0), held }; if (side === 'from') { this.fromSummary = summary; } else { this.toSummary = summary; } }); } private setLoading(side: 'from' | 'to', loading: boolean): void { if (side === 'from') { this.loadingFrom = loading; } else { this.loadingTo = loading; } } /** Count of the denomination the cashier holds, or null while their figures are not loaded. */ heldBy(summary: CashierSummary | null, d: Denom): number | null { return summary ? summary.held[d.id] || 0 : null; } /** The sending cashier is asked to hand on more of this denomination than they hold. */ exceedsHeld(d: Denom): boolean { return !!this.fromSummary && this.countFor(d) > (this.fromSummary.held[d.id] || 0); } get anyExceedsHeld(): boolean { return this.denoms.some((d) => this.exceedsHeld(d)); } /** Both sides must have been allocated cash from the vault for the day before they can transfer. */ get bothAllocated(): boolean { return !!this.fromSummary?.cashAllocated && !!this.toSummary?.cashAllocated; } countFor(d: Denom): number { return this.counts[d.id] || 0; } subtotal(d: Denom): number { return this.countFor(d) * d.value; } onChange(d: Denom, raw: any): void { this.counts[d.id] = Math.max(0, Math.floor(Number(raw) || 0)); } bump(d: Denom, delta: number): void { this.counts[d.id] = Math.max(0, (this.counts[d.id] || 0) + delta); } get total(): number { return this.denoms.reduce((s, d) => s + this.subtotal(d), 0); } get valid(): boolean { return ( this.total > 0 && !!this.selectedTellerId && !!this.fromCashierId && !!this.toCashierId && this.bothAllocated && !this.anyExceedsHeld ); } clear(): void { this.counts = {}; } submit(): void { if (!this.valid || this.saving) return; this.saving = true; const payload = { transferDate: this.transferDate, tellerId: this.selectedTellerId, fromCashierId: this.fromCashierId, toCashierId: this.toCashierId, fromParty: this.fromParty.trim(), toParty: this.toParty.trim(), note: this.note || null, denominations: this.denoms .filter((d) => this.countFor(d) > 0) .map((d) => ({ denominationId: d.id, denominationValue: d.value, count: this.countFor(d) })) }; this.service.createInterCashierTransfer(payload).subscribe({ next: () => { this.snackBar.open('Inter-cashier transfer recorded', 'Close', { duration: 3000 }); this.clear(); this.note = ''; // The sender stays the logged-in cashier; the receiver is picked afresh, and the sender's holdings // are read again now that part of them has gone. this.toCashierId = null; this.toParty = ''; this.toSummary = null; this.loadSummary('from'); this.saving = false; }, error: () => { this.saving = false; } }); } }