/** * 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, inject, OnInit } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { MatDialog } from '@angular/material/dialog'; import { MatTooltip } from '@angular/material/tooltip'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; import { ImpsEntryComponent } from '../imps-entry/imps-entry.component'; /** Person-to-Account, Person-to-Person (MMID+Mobile), Person-to-Aadhaar. */ export type ImpsTxnType = 'P2A' | 'P2P' | 'P2AADHAAR'; export type ImpsState = 'INITIATED' | 'SENT' | 'SUCCESS' | 'PENDING' | 'FAILED' | 'REVERSED'; export interface ImpsTxn { rrn: string; utr: string; remitterAcc: string; remitterBranch: string; remitterUser: string; remitterChannel: 'BRANCH' | 'MOBILE'; /** P2A populates these. */ beneAcc?: string; beneIfsc?: string; /** P2P populates these. */ beneMmid?: string; beneMobile?: string; /** P2AADHAAR populates this. */ beneAadhaar?: string; beneName: string; beneBank: string; txnType: ImpsTxnType; amount: number; valueDate: string; purposeCode: string; state: ImpsState; failureCode?: string; failureReason?: string; sentTimestamp: string; } type ReportTab = 'trends' | 'bank' | 'date' | 'pending' | 'channel' | 'user'; interface BankRow { bank: string; count: number; amount: number; success: number; pending: number; failed: number; } interface DateRow { date: string; count: number; amount: number; p2a: number; p2p: number; } interface UserRow { user: string; count: number; amount: number; branch: string; } interface ChannelRow { channel: 'BRANCH' | 'MOBILE'; count: number; amount: number; share: number; } @Component({ selector: 'mifosx-imps-report', templateUrl: './imps-report.component.html', styleUrls: ['../neft-rtgs-report/neft-rtgs-report.component.scss'], imports: [ ...STANDALONE_SHARED_IMPORTS, FormsModule, FaIconComponent, MatTooltip ] }) export class ImpsReportComponent implements OnInit { private dialog = inject(MatDialog); activeTab: ReportTab = 'trends'; search = ''; typeFilter: '' | ImpsTxnType = ''; stateFilter: '' | ImpsState = ''; txns: ImpsTxn[] = []; readonly tabs: { id: ReportTab; label: string; icon: string }[] = [ { id: 'trends', label: 'Trends', icon: 'chart-line' }, { id: 'bank', label: 'Bank-wise', icon: 'university' }, { id: 'date', label: 'Date-wise', icon: 'calendar-alt' }, { id: 'pending', label: 'Success / Pending / Failed', icon: 'tasks' }, { id: 'channel', label: 'Branch / Mobile', icon: 'mobile-alt' }, { id: 'user', label: 'User-wise', icon: 'user-tie' } ]; ngOnInit(): void { this.txns = seedImpsTransactions(); } // ───────────────────── KPIs ───────────────────── get totalCount(): number { return this.txns.length; } get totalAmount(): number { return this.txns.reduce((s, t) => s + t.amount, 0); } get successCount(): number { return this.txns.filter((t) => t.state === 'SUCCESS').length; } get pendingCount(): number { return this.txns.filter((t) => [ 'INITIATED', 'SENT', 'PENDING' ].includes(t.state)).length; } get failedCount(): number { return this.txns.filter((t) => t.state === 'FAILED' || t.state === 'REVERSED').length; } get p2aCount(): number { return this.txns.filter((t) => t.txnType === 'P2A').length; } get p2pCount(): number { return this.txns.filter((t) => t.txnType === 'P2P').length; } get successRate(): number { if (this.totalCount === 0) return 0; return (this.successCount / this.totalCount) * 100; } get avgTicket(): number { if (this.totalCount === 0) return 0; return this.totalAmount / this.totalCount; } // ───────────────────── Filtered list ───────────────────── get filteredTxns(): ImpsTxn[] { const q = this.search.trim().toLowerCase(); return this.txns.filter((t) => { if (this.typeFilter && t.txnType !== this.typeFilter) return false; if (this.stateFilter && t.state !== this.stateFilter) return false; if (!q) return true; return ( t.rrn.toLowerCase().includes(q) || t.utr.toLowerCase().includes(q) || (t.beneAcc || '').toLowerCase().includes(q) || (t.beneIfsc || '').toLowerCase().includes(q) || (t.beneMmid || '').toLowerCase().includes(q) || (t.beneMobile || '').toLowerCase().includes(q) || t.beneName.toLowerCase().includes(q) || t.remitterAcc.toLowerCase().includes(q) ); }); } // ───────────────────── Aggregations ───────────────────── get bankRows(): BankRow[] { const map = new Map(); for (const t of this.txns) { const key = t.beneBank; if (!map.has(key)) { map.set(key, { bank: key, count: 0, amount: 0, success: 0, pending: 0, failed: 0 }); } const r = map.get(key)!; r.count++; r.amount += t.amount; if (t.state === 'SUCCESS') r.success++; else if (t.state === 'FAILED' || t.state === 'REVERSED') r.failed++; else r.pending++; } return Array.from(map.values()).sort((a, b) => b.amount - a.amount); } get dateRows(): DateRow[] { const map = new Map(); for (const t of this.txns) { if (!map.has(t.valueDate)) { map.set(t.valueDate, { date: t.valueDate, count: 0, amount: 0, p2a: 0, p2p: 0 }); } const r = map.get(t.valueDate)!; r.count++; r.amount += t.amount; if (t.txnType === 'P2A') r.p2a++; else if (t.txnType === 'P2P') r.p2p++; } return Array.from(map.values()).sort((a, b) => (a.date < b.date ? 1 : -1)); } get userRows(): UserRow[] { const map = new Map(); for (const t of this.txns) { const key = `${t.remitterUser}|${t.remitterBranch}`; if (!map.has(key)) { map.set(key, { user: t.remitterUser, branch: t.remitterBranch, count: 0, amount: 0 }); } const r = map.get(key)!; r.count++; r.amount += t.amount; } return Array.from(map.values()).sort((a, b) => b.amount - a.amount); } get channelRows(): ChannelRow[] { const total = this.totalAmount || 1; const map = new Map<'BRANCH' | 'MOBILE', ChannelRow>(); for (const t of this.txns) { if (!map.has(t.remitterChannel)) { map.set(t.remitterChannel, { channel: t.remitterChannel, count: 0, amount: 0, share: 0 }); } const r = map.get(t.remitterChannel)!; r.count++; r.amount += t.amount; } map.forEach((r) => (r.share = (r.amount / total) * 100)); return Array.from(map.values()); } get trendBuckets(): DateRow[] { return this.dateRows.slice(0, 7).reverse(); } get trendMaxAmount(): number { const buckets = this.trendBuckets; if (!buckets.length) return 1; return Math.max(...buckets.map((b) => b.amount)); } get failureCodeRows(): { code: string; reason: string; count: number; amount: number }[] { const map = new Map(); for (const t of this.txns) { if (!(t.state === 'FAILED' || t.state === 'REVERSED') || !t.failureCode) continue; if (!map.has(t.failureCode)) { map.set(t.failureCode, { code: t.failureCode, reason: t.failureReason || '', count: 0, amount: 0 }); } const r = map.get(t.failureCode)!; r.count++; r.amount += t.amount; } return Array.from(map.values()).sort((a, b) => b.count - a.count); } // ───────────────────── Actions ───────────────────── showTab(tab: ReportTab): void { this.activeTab = tab; } openInitiateDialog(): void { const ref = this.dialog.open(ImpsEntryComponent, { width: '880px', maxWidth: '94vw', maxHeight: '88vh', disableClose: true, autoFocus: false, restoreFocus: true, panelClass: 'nr-entry-dialog', backdropClass: 'nr-entry-backdrop' }); ref.afterClosed().subscribe((result) => { if (result && result.submitted && result.payload) { this.txns = [ this.toTxn(result.payload), ...this.txns ]; } }); } exportCsv(): void { const headers = [ 'RRN', 'UTR', 'Remitter Acc', 'Bene Acc / MMID / Aadhaar', 'Bene IFSC / Mobile', 'Bene Name', 'Txn Type', 'Amount', 'Value Date', 'Purpose Code', 'State', 'Failure Code', 'Sent Timestamp' ]; const rows: string[][] = [headers]; for (const t of this.filteredTxns) { rows.push([ t.rrn, t.utr, t.remitterAcc, t.beneAcc || t.beneMmid || t.beneAadhaar || '', t.beneIfsc || t.beneMobile || '', t.beneName, t.txnType, t.amount.toFixed(2), t.valueDate, t.purposeCode, t.state, t.failureCode || '', t.sentTimestamp ]); } const escape = (v: string): string => { const s = (v || '').replace(/"/g, '""'); return /[,"\n]/.test(s) ? `"${s}"` : s; }; const csv = rows.map((r) => r.map(escape).join(',')).join('\n') + '\n'; const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `IMPS_Report_${new Date().toISOString().slice(0, 10)}.csv`; a.click(); URL.revokeObjectURL(url); } clearFilters(): void { this.search = ''; this.typeFilter = ''; this.stateFilter = ''; } // ───────────────────── Helpers ───────────────────── stateColor(state: ImpsState): string { switch (state) { case 'SUCCESS': return 'green'; case 'FAILED': case 'REVERSED': return 'red'; case 'SENT': return 'blue'; case 'INITIATED': return 'teal'; case 'PENDING': return 'amber'; } } trendBarHeight(amount: number): number { return Math.max(6, Math.round((amount / this.trendMaxAmount) * 100)); } private toTxn(p: any): ImpsTxn { const valueDate = p.valueDate ? new Date(p.valueDate).toISOString().slice(0, 10) : new Date().toISOString().slice(0, 10); const ts = new Date(); const pad = (n: number) => String(n).padStart(2, '0'); return { rrn: 'IMPS' + Date.now().toString().slice(-10), utr: 'UTR' + Date.now().toString().slice(-11), remitterAcc: p.senderAccount || '', remitterBranch: 'Main Branch', remitterUser: 'Current User', remitterChannel: 'BRANCH', beneAcc: p.beneficiaryAccount, beneIfsc: (p.ifscCode || '').toUpperCase() || undefined, beneMmid: p.beneMmid, beneMobile: p.beneMobile, beneAadhaar: p.beneAadhaar, beneName: p.beneficiaryName || '', beneBank: this.bankFromIfsc(p.ifscCode || ''), txnType: p.txnType || 'P2A', amount: Number(p.amount) || 0, valueDate, purposeCode: this.purposeCodeFor(p.purposeOfRemittance), state: 'INITIATED', sentTimestamp: ts.getFullYear() + '-' + pad(ts.getMonth() + 1) + '-' + pad(ts.getDate()) + ' ' + pad(ts.getHours()) + ':' + pad(ts.getMinutes()) + ':' + pad(ts.getSeconds()) }; } private bankFromIfsc(ifsc: string): string { const code = ifsc.slice(0, 4).toUpperCase(); const map: Record = { HDFC: 'HDFC Bank', ICIC: 'ICICI Bank', SBIN: 'State Bank of India', KKBK: 'Kotak Mahindra Bank', AXIS: 'Axis Bank', UTIB: 'Axis Bank', PUNB: 'Punjab National Bank', BARB: 'Bank of Baroda', CNRB: 'Canara Bank', YESB: 'YES Bank', IDFB: 'IDFC FIRST Bank' }; return map[code] || code + ' Bank'; } private purposeCodeFor(p: string): string { const map: Record = { CAPITAL_TRANSFER: 'P0102', FAMILY_MAINTENANCE: 'P1301', TRADE_SETTLEMENT: 'P0801', SALARY_PAYMENT: 'P1006', LOAN_REPAYMENT: 'P0103', TAX_PAYMENT: 'P1099', VENDOR_PAYMENT: 'P0802', INVESTMENT: 'P0202', GIFT: 'P1302', OTHER: 'P0099' }; return map[p] || 'P0099'; } // ───────────────────── Mock seed data ───────────────────── /** @deprecated Use the exported `seedImpsTransactions()` instead. */ private seedTransactions(): ImpsTxn[] { return seedImpsTransactions(); } } // ───────────────────── Shared mock seed (top-level export) ───────────────────── /** * Mock IMPS history. Exported so the entry form (and any per-sender history dialogs) * can render from the same dataset. Replace with an API call when the back-end is wired. */ export function seedImpsTransactions(): ImpsTxn[] { const today = new Date(); const dt = (offset: number, h = 10, m = 0): string => { const d = new Date(today); d.setDate(d.getDate() - offset); const pad = (n: number) => String(n).padStart(2, '0'); return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()) + ' ' + pad(h) + ':' + pad(m) + ':00'; }; const vd = (offset: number): string => dt(offset).slice(0, 10); return [ { rrn: 'IMPS2608517721', utr: 'UTRKKBK52026081', remitterAcc: '50200012345678', remitterBranch: 'Mumbai — BKC', remitterUser: 'Anita Desai', remitterChannel: 'MOBILE', beneMmid: '9234567', beneMobile: '9821234567', beneName: 'Rohan S. Mehta', beneBank: 'HDFC Bank', txnType: 'P2P', amount: 12500, valueDate: vd(0), purposeCode: 'P1301', state: 'SUCCESS', sentTimestamp: dt(0, 14, 5) }, { rrn: 'IMPS2608517722', utr: 'UTRHDFC2608712', remitterAcc: '50200012345678', remitterBranch: 'Mumbai — BKC', remitterUser: 'Anita Desai', remitterChannel: 'BRANCH', beneAcc: '345678901234', beneIfsc: 'ICIC0000567', beneName: 'Bluestone Trading Co.', beneBank: 'ICICI Bank', txnType: 'P2A', amount: 78500, valueDate: vd(0), purposeCode: 'P0802', state: 'SENT', sentTimestamp: dt(0, 14, 6) }, { rrn: 'IMPS2608517723', utr: 'UTRSBIN2608902', remitterAcc: '50200099887766', remitterBranch: 'Bengaluru — MG Road', remitterUser: 'Karthik Rao', remitterChannel: 'MOBILE', beneMmid: '9128831', beneMobile: '9845112233', beneName: 'Anita Verma', beneBank: 'State Bank of India', txnType: 'P2P', amount: 4500, valueDate: vd(1), purposeCode: 'P1301', state: 'SUCCESS', sentTimestamp: dt(1, 11, 22) }, { rrn: 'IMPS2608517724', utr: 'UTRAXIS2608811', remitterAcc: '50200099887766', remitterBranch: 'Bengaluru — MG Road', remitterUser: 'Karthik Rao', remitterChannel: 'MOBILE', beneAcc: '6789012345', beneIfsc: 'UTIB0000456', beneName: 'Rakesh Industries', beneBank: 'Axis Bank', txnType: 'P2A', amount: 195000, valueDate: vd(1), purposeCode: 'P0801', state: 'FAILED', failureCode: 'E03', failureReason: 'Beneficiary Bank Not Available', sentTimestamp: dt(1, 11, 25) }, { rrn: 'IMPS2608517725', utr: 'UTRPUNB2608619', remitterAcc: '50100221122334', remitterBranch: 'Delhi — Connaught', remitterUser: 'Sushma Iyer', remitterChannel: 'BRANCH', beneAcc: '50100998877665', beneIfsc: 'PUNB0123400', beneName: 'Madhavi Singh', beneBank: 'Punjab National Bank', txnType: 'P2A', amount: 42000, valueDate: vd(2), purposeCode: 'P1006', state: 'SUCCESS', sentTimestamp: dt(2, 9, 35) }, { rrn: 'IMPS2608517726', utr: 'UTRYESB2608520', remitterAcc: '50100221122334', remitterBranch: 'Delhi — Connaught', remitterUser: 'Sushma Iyer', remitterChannel: 'BRANCH', beneMmid: '9001122', beneMobile: '9818877665', beneName: 'Sigma Constructions', beneBank: 'YES Bank', txnType: 'P2P', amount: 18500, valueDate: vd(2), purposeCode: 'P0102', state: 'PENDING', sentTimestamp: dt(2, 9, 38) }, { rrn: 'IMPS2608517727', utr: 'UTRCNRB2608417', remitterAcc: '50100887766554', remitterBranch: 'Chennai — T. Nagar', remitterUser: 'Ramesh Krishnan', remitterChannel: 'MOBILE', beneMmid: '9889911', beneMobile: '9840012345', beneName: 'Padma Rao', beneBank: 'Canara Bank', txnType: 'P2P', amount: 2800, valueDate: vd(3), purposeCode: 'P1302', state: 'SUCCESS', sentTimestamp: dt(3, 16, 15) }, { rrn: 'IMPS2608517728', utr: 'UTRBARB2608318', remitterAcc: '50100887766554', remitterBranch: 'Chennai — T. Nagar', remitterUser: 'Ramesh Krishnan', remitterChannel: 'BRANCH', beneAcc: '50100123456987', beneIfsc: 'BARB0VJVATA', beneName: 'Vijaya Traders', beneBank: 'Bank of Baroda', txnType: 'P2A', amount: 125000, valueDate: vd(3), purposeCode: 'P0802', state: 'INITIATED', sentTimestamp: dt(3, 16, 20) }, { rrn: 'IMPS2608517729', utr: 'UTRIDFB2608215', remitterAcc: '50200445566778', remitterBranch: 'Pune — Koregaon Park', remitterUser: 'Vikram Joshi', remitterChannel: 'BRANCH', beneAadhaar: 'XXXX-XXXX-7891', beneName: 'Pinnacle Realty Pvt Ltd', beneBank: 'IDFC FIRST Bank', txnType: 'P2AADHAAR', amount: 145000, valueDate: vd(4), purposeCode: 'P0202', state: 'SUCCESS', sentTimestamp: dt(4, 12, 45) }, { rrn: 'IMPS2608517730', utr: 'UTRHDFC2608116', remitterAcc: '50200445566778', remitterBranch: 'Pune — Koregaon Park', remitterUser: 'Vikram Joshi', remitterChannel: 'MOBILE', beneMmid: '9650432', beneMobile: '9890065432', beneName: 'Neha Kapoor', beneBank: 'HDFC Bank', txnType: 'P2P', amount: 6500, valueDate: vd(4), purposeCode: 'P1301', state: 'SUCCESS', sentTimestamp: dt(4, 12, 50) }, { rrn: 'IMPS2608517731', utr: 'UTRKKBK2608013', remitterAcc: '50200778899001', remitterBranch: 'Hyderabad — Banjara', remitterUser: 'Lakshmi Naidu', remitterChannel: 'BRANCH', beneAcc: '12345678900011', beneIfsc: 'KKBK0001899', beneName: 'Aarav Enterprises', beneBank: 'Kotak Mahindra Bank', txnType: 'P2A', amount: 67000, valueDate: vd(5), purposeCode: 'P0802', state: 'REVERSED', failureCode: 'E11', failureReason: 'Beneficiary Account Closed', sentTimestamp: dt(5, 10, 5) }, { rrn: 'IMPS2608517732', utr: 'UTRSBIN2607914', remitterAcc: '50200778899001', remitterBranch: 'Hyderabad — Banjara', remitterUser: 'Lakshmi Naidu', remitterChannel: 'BRANCH', beneMmid: '9445566', beneMobile: '9849988776', beneName: 'Spectra Components', beneBank: 'State Bank of India', txnType: 'P2P', amount: 9800, valueDate: vd(5), purposeCode: 'P0801', state: 'SUCCESS', sentTimestamp: dt(5, 10, 10) }, { rrn: 'IMPS2608517733', utr: 'UTRICIC2607812', remitterAcc: '50100334455667', remitterBranch: 'Kolkata — Park Street', remitterUser: 'Arjun Banerjee', remitterChannel: 'MOBILE', beneAcc: '88990011223344', beneIfsc: 'ICIC0001234', beneName: 'Eastern Logistics', beneBank: 'ICICI Bank', txnType: 'P2A', amount: 88000, valueDate: vd(6), purposeCode: 'P0802', state: 'SUCCESS', sentTimestamp: dt(6, 14, 30) }, { rrn: 'IMPS2608517734', utr: 'UTRAXIS2607711', remitterAcc: '50100334455667', remitterBranch: 'Kolkata — Park Street', remitterUser: 'Arjun Banerjee', remitterChannel: 'BRANCH', beneMmid: '9009988', beneMobile: '9831100225', beneName: 'Indrani Sen', beneBank: 'Axis Bank', txnType: 'P2P', amount: 2200, valueDate: vd(6), purposeCode: 'P1302', state: 'SUCCESS', sentTimestamp: dt(6, 14, 32) } ]; }