/** * 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/. */ /** Angular Imports */ import { Component, OnInit, inject } from '@angular/core'; import { UntypedFormGroup, UntypedFormBuilder, Validators, UntypedFormControl, ReactiveFormsModule } from '@angular/forms'; 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 { CashierService } from 'app/cashier/cashier.service'; import { Dates } from 'app/core/utils/dates'; import { Currency } from 'app/shared/models/general.model'; import { InputAmountComponent } from '../../../shared/input-amount/input-amount.component'; import { MatSlideToggle } from '@angular/material/slide-toggle'; import { CdkTextareaAutosize } from '@angular/cdk/text-field'; import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { MatDialog } from '@angular/material/dialog'; import { TransactionConfirmDialogComponent } from '../../../savings/saving-account-actions/savings-account-transactions/transaction-confirm-dialog/transaction-confirm-dialog.component'; import { IdentityVerifyDialogComponent } from '../../../savings/saving-account-actions/savings-account-transactions/identity-verify-dialog/identity-verify-dialog.component'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; /** Single Indian denomination row (note or coin). */ interface Denomination { id: number; value: number; kind: 'note' | 'coin'; label: string; active: boolean; } /** * Create current account transactions component (Deposit / Withdraw). */ @Component({ selector: 'mifosx-current-account-transactions', templateUrl: './current-account-transactions.component.html', styleUrls: ['./current-account-transactions.component.scss'], imports: [ ...STANDALONE_SHARED_IMPORTS, InputAmountComponent, MatSlideToggle, CdkTextareaAutosize, FaIconComponent ] }) export class CurrentAccountTransactionsComponent implements OnInit { private formBuilder = inject(UntypedFormBuilder); private route = inject(ActivatedRoute); private router = inject(Router); private dateUtils = inject(Dates); private currentAccountsService = inject(CurrentAccountsService); private clientsService = inject(ClientsService); private settingsService = inject(SettingsService); private dialog = inject(MatDialog); private denominationMasterService = inject(DenominationMasterService); private cashierService = inject(CashierService); /** Minimum Due Date allowed. */ minDate = new Date(2000, 0, 1); /** Maximum Due Date allowed. */ maxDate = new Date(); /** Current account transaction form. */ currentAccountTransactionForm: UntypedFormGroup; /** current account transaction payment options. */ paymentTypeOptions: { id: number; name: string; description: string; isCashPayment: boolean; position: number; }[]; /** Flag to enable payment details fields. */ addPaymentDetailsFlag: Boolean = false; /** transaction type flag to render required UI */ transactionType: { deposit: boolean; withdrawal: boolean } = { deposit: false, withdrawal: false }; /** transaction command for submit request */ transactionCommand: string; /** current account's Id */ currentAccountId: string; currency: Currency | null = null; /** Banknote denominations — loaded dynamically from the Denomination Master (active only). */ notes: Denomination[] = []; /** 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 = {}; // ── Context strip data ────────────────────────────────────────────── accountNo: string | null = null; clientName: string | null = null; clientId: number | null = null; kycStatus: 'Verified' | 'Partial' | 'Pending' = 'Pending'; sameDayNeftRtgs = 0; sameDayChequeClearing = 0; availableBalance = 0; identityVerified = false; openingDenoms: any[] = []; openingTotal = 0; openingLoaded = false; previousDayUnsettled = false; morningTransferReceived = true; unsettledSince = ''; pendingAmount = 0; get openingNotes(): any[] { return this.openingDenoms.filter((d) => d.kind !== 'coin'); } get openingCoins(): any[] { return this.openingDenoms.filter((d) => d.kind === 'coin'); } get closingNotes(): any[] { return this.closingDenoms.filter((d) => d.kind !== 'coin'); } get closingCoins(): any[] { return this.closingDenoms.filter((d) => d.kind === 'coin'); } constructor() { this.route.data.subscribe((data: { currentAccountActionData: any }) => { this.paymentTypeOptions = data.currentAccountActionData.paymentTypeOptions; if (data.currentAccountActionData.currency) { this.currency = data.currentAccountActionData.currency; } }); this.transactionCommand = this.route.snapshot.params['name'].toLowerCase(); this.transactionType[this.transactionCommand as 'deposit' | 'withdrawal'] = true; this.currentAccountId = this.route.snapshot.params['currentAccountId']; } ngOnInit() { this.maxDate = this.settingsService.businessDate; this.createCurrentAccountTransactionForm(); this.loadDenominations(); this.loadContextStrip(); this.loadOpeningPosition(); this.loadDayStatus(); } /** 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) => ({ id: d.id, 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({ next: (account: any) => { this.accountNo = account?.accountNo ?? null; this.clientName = account?.clientName ?? account?.groupName ?? null; this.clientId = account?.clientId ?? null; if (!this.currency && account?.currency) this.currency = account.currency; this.availableBalance = Number(account?.summary?.availableBalance ?? account?.summary?.accountBalance ?? 0); this.computeSameDayNeftRtgs(account?.transactions); this.computeSameDayChequeClearing(account?.transactions); if (account?.clientId) { this.fetchClientKyc(String(account.clientId)); } }, error: () => {} }); } private loadDayStatus(): void { this.cashierService.getCashierDayStatus().subscribe({ next: (res: any) => { this.previousDayUnsettled = res?.previousDayUnsettled ?? false; this.morningTransferReceived = res?.morningTransferReceived ?? true; this.unsettledSince = res?.unsettledSince ?? ''; this.pendingAmount = Number(res?.pendingAmount ?? 0); }, error: () => { this.previousDayUnsettled = false; this.morningTransferReceived = true; } }); } private loadOpeningPosition(): void { const today = new Date(); const asOfDate = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`; this.cashierService.getCashierPositionForMe(asOfDate).subscribe({ next: (res: any) => { this.openingDenoms = res?.denominations || []; this.openingTotal = Number(res?.totalAmount || 0); this.openingLoaded = true; }, error: () => { this.openingDenoms = []; this.openingTotal = 0; this.openingLoaded = true; } }); } get closingDenoms(): Array { const openMap = new Map(); for (const d of this.openingDenoms) { openMap.set(`${d.kind}:${d.denominationValue}`, d); } const keySet = new Set(); for (const d of [ ...this.notes, ...this.coins ]) keySet.add(this.denomKey(d)); for (const k of openMap.keys()) keySet.add(k); const sign = this.transactionType.deposit ? 1 : -1; const result: any[] = []; for (const key of keySet) { const open = openMap.get(key); const denom = [ ...this.notes, ...this.coins ].find((d) => this.denomKey(d) === key); const openCount = open?.count ?? 0; const enteredCount = denom ? this.countFor(denom) : 0; const delta = enteredCount * sign; const closingCount = openCount + delta; const denomValue = open?.denominationValue ?? denom?.value ?? 0; const kind = (open?.kind ?? denom?.kind ?? 'note') as string; if (openCount !== 0 || enteredCount !== 0) { result.push({ denominationId: open?.denominationId ?? denom?.id ?? 0, denominationValue: denomValue, kind, openCount, delta, closingCount, closingAmount: closingCount * denomValue }); } } return result.sort((a, b) => b.denominationValue - a.denominationValue || a.kind.localeCompare(b.kind)); } get closingTotal(): number { const sign = this.transactionType.deposit ? 1 : -1; return this.openingTotal + this.denominationTotal * sign; } private fetchClientKyc(clientId: string): void { this.clientsService.getClientKyc(clientId).subscribe({ next: (kyc: any) => { const pan = !!kyc?.panVerified; const aadhaar = !!kyc?.aadhaarVerified; this.kycStatus = pan && aadhaar ? 'Verified' : pan || aadhaar ? 'Partial' : 'Pending'; }, error: () => {} }); } private computeSameDayNeftRtgs(transactions: any[] | undefined): void { if (!Array.isArray(transactions) || transactions.length === 0) { this.sameDayNeftRtgs = 0; return; } const today = this.settingsService.businessDate ?? new Date(); const sameDay = (d: any) => { if (!d) return false; const t = Array.isArray(d) && d.length >= 3 ? new Date(d[0], d[1] - 1, d[2]) : new Date(d); return ( t.getFullYear() === today.getFullYear() && t.getMonth() === today.getMonth() && t.getDate() === today.getDate() ); }; const isNeftRtgs = (t: any) => { const txnType = (t?.transactionType?.value ?? t?.transactionType?.code ?? '').toUpperCase(); const payType = (t?.paymentDetailData?.paymentType?.name ?? '').toUpperCase(); return /NEFT|RTGS/.test(txnType) || /NEFT|RTGS/.test(payType); }; this.sameDayNeftRtgs = transactions .filter((t: any) => !t.reversed && sameDay(t.date) && isNeftRtgs(t)) .reduce((sum, t: any) => sum + (Number(t.amount) || 0), 0); } private computeSameDayChequeClearing(transactions: any[] | undefined): void { if (!Array.isArray(transactions) || transactions.length === 0) { this.sameDayChequeClearing = 0; return; } const today = this.settingsService.businessDate ?? new Date(); const sameDay = (d: any) => { if (!d) return false; const t = Array.isArray(d) && d.length >= 3 ? new Date(d[0], d[1] - 1, d[2]) : new Date(d); return ( t.getFullYear() === today.getFullYear() && t.getMonth() === today.getMonth() && t.getDate() === today.getDate() ); }; const isCheque = (t: any) => { const txnType = (t?.transactionType?.value ?? t?.transactionType?.code ?? '').toUpperCase(); const payType = (t?.paymentDetailData?.paymentType?.name ?? '').toUpperCase(); const hasCheckNo = !!t?.paymentDetailData?.checkNumber; return hasCheckNo || /CHEQUE|CHECK/.test(txnType) || /CHEQUE|CHECK/.test(payType); }; const isPending = (t: any) => { const cleared = t?.transactionType?.bankTransfer === true || t?.transactionType?.deposit === true; return !cleared; }; this.sameDayChequeClearing = transactions .filter((t: any) => !t.reversed && sameDay(t.date) && isCheque(t) && isPending(t)) .reduce((sum, t: any) => sum + (Number(t.amount) || 0), 0); } createCurrentAccountTransactionForm() { this.currentAccountTransactionForm = this.formBuilder.group({ transactionDate: [ this.settingsService.businessDate, Validators.required ], transactionAmount: [ 0, Validators.required ], paymentTypeId: [ '', Validators.required ], note: [''] }); } // ── Denomination helpers ──────────────────────────────────────────── denomKey(d: Denomination): string { return `${d.kind}:${d.value}`; } countFor(d: Denomination): number { return this.denominationCounts[this.denomKey(d)] || 0; } subtotalFor(d: Denomination): number { return this.countFor(d) * d.value; } onDenomChange(d: Denomination, raw: any): void { const n = Math.max(0, Math.floor(Number(raw) || 0)); this.denominationCounts[this.denomKey(d)] = n; } bumpDenom(d: Denomination, delta: number): void { const next = Math.max(0, this.countFor(d) + delta); this.denominationCounts[this.denomKey(d)] = next; } get notesTotal(): number { return this.notes.reduce((s, d) => s + this.subtotalFor(d), 0); } get coinsTotal(): number { return this.coins.reduce((s, d) => s + this.subtotalFor(d), 0); } get denominationTotal(): number { return this.notesTotal + this.coinsTotal; } get totalPieces(): number { return Object.values(this.denominationCounts).reduce((s, n) => s + (n || 0), 0); } get denominationMatchesForm(): boolean { const formAmt = Number(this.currentAccountTransactionForm?.controls?.['transactionAmount']?.value || 0); return this.denominationTotal > 0 && formAmt === this.denominationTotal; } get withdrawalDenomOverdraft(): Array<{ label: string; available: number; requested: number }> { if (!this.transactionType.withdrawal || !this.openingLoaded) return []; const openMap = new Map(); for (const d of this.openingDenoms) { openMap.set(`${d.kind}:${d.denominationValue}`, Number(d.count ?? 0)); } return [ ...this.notes, ...this.coins ] .map((d) => ({ label: d.label, available: openMap.get(this.denomKey(d)) ?? 0, requested: this.countFor(d) })) .filter((item) => item.requested > 0 && item.requested > item.available); } get hasWithdrawalDenomOverdraft(): boolean { return this.withdrawalDenomOverdraft.length > 0; } applyDenominationTotal(): void { this.currentAccountTransactionForm.controls['transactionAmount'].setValue(this.denominationTotal); this.currentAccountTransactionForm.controls['transactionAmount'].markAsDirty(); } clearDenominations(): void { this.denominationCounts = {}; } openIdentityVerify(): void { if (!this.clientId) return; this.identityVerified = true; this.dialog.open(IdentityVerifyDialogComponent, { width: '500px', maxWidth: '92vw', panelClass: 'idv-panel', autoFocus: false, data: { clientId: this.clientId, clientName: this.clientName, accountNo: this.accountNo } }); } /** * Method to add payment detail fields to the UI. */ addPaymentDetails() { this.addPaymentDetailsFlag = !this.addPaymentDetailsFlag; if (this.addPaymentDetailsFlag) { this.currentAccountTransactionForm.addControl('accountNumber', new UntypedFormControl('')); this.currentAccountTransactionForm.addControl('checkNumber', new UntypedFormControl('')); this.currentAccountTransactionForm.addControl('routingCode', new UntypedFormControl('')); this.currentAccountTransactionForm.addControl('receiptNumber', new UntypedFormControl('')); this.currentAccountTransactionForm.addControl('bankNumber', new UntypedFormControl('')); } else { this.currentAccountTransactionForm.removeControl('accountNumber'); this.currentAccountTransactionForm.removeControl('checkNumber'); this.currentAccountTransactionForm.removeControl('routingCode'); this.currentAccountTransactionForm.removeControl('receiptNumber'); this.currentAccountTransactionForm.removeControl('bankNumber'); } } /** * Opens a confirmation dialog then posts the transaction. */ submit() { const formAmount = Number(this.currentAccountTransactionForm.value.transactionAmount) || 0; const paymentTypeId = this.currentAccountTransactionForm.value.paymentTypeId; const paymentTypeLabel = this.paymentTypeOptions?.find((p) => p.id === paymentTypeId)?.name ?? null; const ref = this.dialog.open(TransactionConfirmDialogComponent, { width: '480px', maxWidth: '92vw', panelClass: 'tcd-panel', autoFocus: false, data: { kind: this.transactionType.deposit ? 'deposit' : 'withdrawal', clientName: this.clientName, accountNo: this.accountNo, currentBalance: this.availableBalance, transactionAmount: formAmount, paymentTypeLabel } }); ref.afterClosed().subscribe((result: any) => { if (result?.confirm) { this.postTransaction(); } }); } private postTransaction(): void { const currentAccountTransactionFormData = this.currentAccountTransactionForm.value; const locale = this.settingsService.language.code; const dateFormat = this.settingsService.dateFormat; const prevTransactionDate: Date = this.currentAccountTransactionForm.value.transactionDate; if (currentAccountTransactionFormData.transactionDate instanceof Date) { currentAccountTransactionFormData.transactionDate = this.dateUtils.formatDate(prevTransactionDate, dateFormat); } const data = { ...currentAccountTransactionFormData, dateFormat, locale }; data['transactionAmount'] = data['transactionAmount'] * 1; if (this.denominationTotal > 0) { const breakdown = [ ...this.notes, ...this.coins ] .map((d) => ({ denominationId: d.id, denominationValue: d.value, kind: d.kind, count: this.countFor(d), subtotal: this.subtotalFor(d) })) .filter((row) => row.count > 0); data['denominationBreakdown'] = breakdown; } this.currentAccountsService .executeCurrentAccountTransactionsCommand(this.currentAccountId, this.transactionCommand, data) .subscribe(() => { this.router.navigate(['../../transactions'], { relativeTo: this.route }); }); } }