Files
cbsnexora/src/app/fiu/fiu.component.ts
T
AdityaandClaude Opus 4.7 12bfc81219 feat: add 11 banking compliance & accounting report modules
Adds dialog components and feature modules for RBI/banking compliance,
KYC, and accounting reports. Wires them into the shell menu (RBI,
General Ledger, CKYC Mgmt, Others, Insurance) and dashboard cards.

Dialog components (lazy-loaded MatDialog):
- PSL Returns (Form III) — dashboard card + RBI menu
- ALM Statement of Structural Liquidity (Form ALM-1) — RBI menu
- CIMS / XBRL Returns (OSS-2 / PSA / RAL / RCP-UCB) — RBI menu, shared
  dialog with form-pill switcher
- CIC DQR (CIBIL / Experian / Equifax / CRIF, 72-col TUDF) — Others menu
  + dashboard card
- CKYC Compliance Reports (Re-KYC / Risk Review / CERSAI) — CKYC Mgmt
- Accounting Reports (P&L / Balance Sheet / Comparative B/S / GL DR/CR
  / Misc Expense Ledger) — General Ledger menu

Feature modules (Route.withShell):
- DEAF (/deaf): Form I, Form II, Annex 4, accounts register
- DICGC (/dicgc): Slab Report, SAHAY Depositor List
- FIU-IND (/fiu): FINnet 2.0 Statement of Financial Transactions
- SFT (/sft): Income Tax Section 285BA SFT
- CKYC Reports (/ckyc/ckyc-reports): standalone dashboard

UI:
- Theme-colored icons on 22 wired menu links across 5 menus —
  visually distinguishes wired (colored) from placeholder (gray) links.
- Backdrop classes per dialog in main.scss.

All using dummy data (Bharat Urban Bank Ltd, Karthik Kamath as Principal
Officer, dummy customer names) — no real bank or customer references.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 18:59:50 +05:30

548 lines
16 KiB
TypeScript

/**
* 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 } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import {
CtrTransaction,
CTR_TXNS,
FIU_BANK,
KC1_INDIVIDUALS,
KC2_ENTITIES,
KycEntity,
KycIndividual,
StrStatus,
StrTransaction,
STR_TXNS
} from './fiu-data';
@Component({
selector: 'mifosx-fiu',
templateUrl: './fiu.component.html',
styleUrls: ['./fiu.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
FaIconComponent,
FormsModule
]
})
export class FiuComponent {
/** Active tab */
activeTab: 'dashboard' | 'ctr' | 'str' | 'customers' | 'files' = 'dashboard';
bank = FIU_BANK;
ctrTxns: CtrTransaction[] = CTR_TXNS;
strTxns: StrTransaction[] = STR_TXNS;
kc1: KycIndividual[] = KC1_INDIVIDUALS;
kc2: KycEntity[] = KC2_ENTITIES;
/** CTR filters */
ctrFilterMode: '' | 'Deposit' | 'Withdrawal' = '';
ctrFilterAccount = '';
ctrFilterMinAmount = 0;
/** STR filters */
strFilterStatus: '' | StrStatus = '';
strFilterRisk: string = '';
/** Toast */
toastMsg = '';
toastVisible = false;
private toastTimer: any;
showTab(tab: 'dashboard' | 'ctr' | 'str' | 'customers' | 'files'): void {
this.activeTab = tab;
}
// -------------------- KPIs --------------------
get totalReports(): number {
return new Set(this.ctrTxns.map((t) => t.reportRefNo)).size;
}
get totalCtrTxns(): number {
return this.ctrTxns.length;
}
get totalCtrAmount(): number {
return this.ctrTxns.reduce((s, t) => s + t.txnAmount, 0);
}
get pendingStr(): number {
return this.strTxns.filter((s) => s.status === 'draft' || s.status === 'po-review').length;
}
get filedStr(): number {
return this.strTxns.filter((s) => s.status === 'filed').length;
}
get totalCustomers(): number {
return this.kc1.length + this.kc2.length;
}
// -------------------- Filtered lists --------------------
get filteredCtr(): CtrTransaction[] {
const acc = this.ctrFilterAccount.trim();
return this.ctrTxns.filter((t) => {
if (this.ctrFilterMode && t.depositOrWithdrawal !== this.ctrFilterMode) return false;
if (acc && !t.accountNumber.includes(acc)) return false;
if (this.ctrFilterMinAmount > 0 && t.txnAmount < this.ctrFilterMinAmount) return false;
return true;
});
}
get filteredStr(): StrTransaction[] {
return this.strTxns.filter((s) => {
if (this.strFilterStatus && s.status !== this.strFilterStatus) return false;
if (this.strFilterRisk && s.riskCategory !== this.strFilterRisk) return false;
return true;
});
}
resetCtrFilters(): void {
this.ctrFilterMode = '';
this.ctrFilterAccount = '';
this.ctrFilterMinAmount = 0;
}
resetStrFilters(): void {
this.strFilterStatus = '';
this.strFilterRisk = '';
}
// -------------------- Helpers --------------------
statusLabel(s: StrStatus): string {
return s === 'draft' ? 'Draft' : s === 'po-review' ? 'PO Review' : s === 'filed' ? 'Filed' : 'Rejected';
}
riskLabel(c: StrTransaction['riskCategory']): string {
const map: Record<StrTransaction['riskCategory'], string> = {
structuring: 'Structuring',
'dormant-revival': 'Dormant Revival',
'high-risk-country': 'High-Risk Country',
'unusual-pattern': 'Unusual Pattern',
'pep-related': 'PEP Related',
'sanctions-hit': 'Sanctions Hit'
};
return map[c];
}
// -------------------- Exports --------------------
/** TC2 — Branch / cash transaction CSV */
exportTc2(): void {
const headers = [
'Report Type',
'Report Reference Number',
'Branch Code of Account',
'Account Number',
'Transaction Branch Code',
'Deposit/Withdrawal',
'Transaction ID',
'Transaction Date',
'Transaction Time',
'Transaction Amount',
'Instrument Type',
'Instrument ID (Card Number/ Cash/ Cheque/ DD/ Others)',
'Non-Customer Reference Number',
'Relationship Flag',
'Third Party PAN (Non-Customer)',
'Declaration (If Third Party PAN (Non-Customer) is not available)'
];
const rows = [headers];
for (const t of this.filteredCtr) {
rows.push([
t.reportType,
t.reportRefNo.toString(),
t.branchCode,
t.accountNumber,
t.txnBranchCode,
t.depositOrWithdrawal,
t.txnId.toString(),
t.txnDate.replace(/-/g, '/'),
t.txnTime,
t.txnAmount.toString(),
t.instrumentType,
t.instrumentId,
t.nonCustomerRef,
t.relationshipFlag,
t.thirdPartyPan,
t.declaration
]);
}
this.saveCsv(rows, 'TC2.csv');
this.toast(`TC2 exported (${this.filteredCtr.length} rows)`);
}
/** TC1 header-only template (UI doesn't track ATM/card txns) */
exportTc1(): void {
const headers = [
'Relationship Flag',
'Transaction Status',
'Transaction ID',
'Transaction Date',
'Transaction Time',
'Deposit/Withdrawal',
'Transaction Amount',
'Account Number',
'Account Type',
'Card Type',
'Card Number',
'Card Issuing Country',
'Card Issuing Bank',
'Report Reference Number',
'Switch Provider Transaction ID',
'ATM ID/ CAM ID',
'Branch Code of Account',
'Report Type'
];
this.saveCsv([headers], 'TC1.csv');
this.toast('TC1 template exported (header only)');
}
/** KC1 — Individual customers */
exportKc1(): void {
const headers = [
'Report Reference Number',
'UCIC',
'Customer ID',
'PAN',
'Declaration (If PAN is not available)',
'CKYC Number',
'Declaration (If CKYC is not available)',
'Passport Number',
'Voter ID',
'Other Occupation',
'Drivers License Number',
'NREGA Card',
'DIN / DPIN',
'First Name',
'Middle Name',
'Last Name',
'Declaration (If Last name is not available)',
'Name of Father',
'Name of Mother',
'Spouse/Partner Name',
'Gender',
'Date of Birth',
'Nationality',
'Mobile Number',
'Alternate Mobile Number',
'Telephone Number',
'Email ID',
'Primary Address 1',
'Primary Address Country',
'Primary Address Pin Code',
'Primary Address Locality',
'Primary Address State',
'Primary Address District',
'Primary Address City',
'Customer Type',
'Annual Income (INR)',
'Occupation',
'Date of Customer On-boarding',
'Date of Last KYC / re-KYC',
'Customer Risk Level',
'Report Type',
'Other Identifier Name',
'Other Identifier Number',
'Primary Entity'
];
const rows = [headers];
for (const k of this.kc1) {
rows.push([
k.reportRefNo.toString(),
k.ucic,
k.ucic,
k.pan,
k.pan ? '' : 'NA',
k.ckycNumber,
k.ckycNumber ? '' : 'NA',
'',
'',
'',
'',
'',
'',
k.firstName,
k.middleName,
k.lastName,
'',
k.fatherName,
k.motherName,
'',
k.gender,
k.dob.replace(/-/g, '/'),
'97',
k.mobileNumber,
'',
'',
k.email,
k.primaryAddress,
'95',
k.pincode,
k.primaryAddress.split(' ').slice(0, 4).join(' '),
k.state,
'501',
k.city,
k.customerType,
k.annualIncome.toString(),
k.occupation,
k.onboardingDate.replace(/-/g, '/'),
k.lastKycDate.replace(/-/g, '/'),
k.riskLevel,
k.reportType,
k.otherIdentifierName || '',
k.otherIdentifierNumber || '',
'Yes'
]);
}
this.saveCsv(rows, 'KC1.csv');
this.toast(`KC1 exported (${this.kc1.length} individuals)`);
}
/** KC2 — Entity customers */
exportKc2(): void {
const headers = [
'Report Reference Number',
'UCIC',
'Entity Name',
'Company ID Number',
'Company ID Type',
'PAN Number',
'Declaration (If PAN is not available)',
'TAN',
'GSTIN',
'IEC',
'Declaration (If IEC is not available)',
'No identifier available',
'Telephone Number',
'Mobile Number',
'Email ID',
'Company Website',
'Country',
'Address Line 1',
'PIN Code',
'Locality',
'State',
'District',
'City / Village / Town',
'Date Of Incorporation',
'Customer Type',
'Last KYC Date',
'Onboarding Date',
'Customer Risk Level',
'Report Type',
'Sole Proprietor Firm',
'UCIC of Owner of Sole Proprietor Firm',
'Name of Owner of Sole Proprietor Firm'
];
const rows = [headers];
for (const k of this.kc2) {
rows.push([
k.reportRefNo.toString(),
k.ucic,
k.entityName,
'',
'',
k.panNumber,
'',
'',
k.gstin,
'',
'NA',
'',
'',
k.mobileNumber,
'',
'',
'95',
k.address,
k.pincode,
k.address.split(' ').slice(0, 4).join(' '),
k.state,
'501',
k.city,
k.dateOfIncorporation.replace(/-/g, '/'),
k.customerType,
k.lastKycDate.replace(/-/g, '/'),
k.onboardingDate.replace(/-/g, '/'),
k.riskLevel,
k.reportType,
k.isSoleProprietor ? 'Yes' : 'No',
k.ownerUcic,
k.ownerName
]);
}
this.saveCsv(rows, 'KC2.csv');
this.toast(`KC2 exported (${this.kc2.length} entities)`);
}
/** Account_Detail.csv — per-report account aggregates (12-month) */
exportAccountDetail(): void {
const headers = [
'Report Type',
'Report Reference Number',
'Account Type',
'Account Number',
'Branch Code of Account',
'Date of Account Opening',
'Date of Account Closing',
'Account Status',
'Reason for Account Freeze',
'No Of Debits (In last 12 months)',
'Total Debit Amount (In last 12 months)',
'No Of Credits (In last 12 months)',
'Total Credit Amount (In last 12 months)',
'No Of Cash Transaction (In last 12 months)',
'Total Cash Deposit (In last 12 months)',
'Total Cash Withdrawal (In last 12 months) Amount'
];
const rows = [headers];
// Build one aggregate row per (reportRefNo, accountNumber) pair
const seen = new Set<string>();
for (const t of this.ctrTxns) {
const key = `${t.reportRefNo}|${t.accountNumber}`;
if (seen.has(key)) continue;
seen.add(key);
const sameKey = this.ctrTxns.filter(
(x) => x.reportRefNo === t.reportRefNo && x.accountNumber === t.accountNumber
);
const debits = sameKey.filter((x) => x.depositOrWithdrawal === 'Withdrawal');
const credits = sameKey.filter((x) => x.depositOrWithdrawal === 'Deposit');
const cashTxns = sameKey.filter((x) => x.instrumentType === 'Cash');
rows.push([
t.reportType,
t.reportRefNo.toString(),
'212',
t.accountNumber,
t.branchCode,
'20/05/2018',
'',
'ACTIVE',
'',
debits.length.toString(),
debits.reduce((s, x) => s + x.txnAmount, 0).toString(),
credits.length.toString(),
credits.reduce((s, x) => s + x.txnAmount, 0).toString(),
cashTxns.length.toString(),
cashTxns
.filter((x) => x.depositOrWithdrawal === 'Deposit')
.reduce((s, x) => s + x.txnAmount, 0)
.toString(),
cashTxns
.filter((x) => x.depositOrWithdrawal === 'Withdrawal')
.reduce((s, x) => s + x.txnAmount, 0)
.toString()
]);
}
this.saveCsv(rows, 'Account_Detail.csv');
this.toast(`Account_Detail exported (${rows.length - 1} accounts)`);
}
/** Account_Person_Relation.csv — account ↔ customer mapping */
exportAccountPersonRelation(): void {
const headers = [
'Report Type',
'Report Reference Number',
'Branch Code of Account',
'Account Number',
'Relationship Type',
'Unique Reference Number',
'Individual / Non-individual',
'Name of Non-customer'
];
const rows = [headers];
const seen = new Set<string>();
for (const t of this.ctrTxns) {
const key = `${t.reportRefNo}|${t.accountNumber}`;
if (seen.has(key)) continue;
seen.add(key);
// Find matching KC1 by reportRefNo (best-effort dummy mapping)
const k = this.kc1.find((x) => x.reportRefNo === t.reportRefNo);
rows.push([
t.reportType,
t.reportRefNo.toString(),
t.branchCode,
t.accountNumber,
'1',
k ? k.ucic : '0',
'Individual',
t.relationshipFlag === 'Non-Customer' ? t.customerName || '' : ''
]);
}
this.saveCsv(rows, 'Account_Person_Relation.csv');
this.toast(`Account_Person_Relation exported (${rows.length - 1} rows)`);
}
/** STR XML stub (FINnet 2.0 STR XML) */
exportStrXml(): void {
const xml: string[] = [];
xml.push('<?xml version="1.0" encoding="UTF-8"?>');
xml.push(`<STR xmlns="http://fiuind.gov.in/finnet2/str" reportingMonth="${this.bank.reportingMonth}">`);
xml.push(` <REBank buid="${this.bank.reBuid}" name="${this.bank.name}" />`);
xml.push(` <PrincipalOfficer name="${this.bank.principalOfficer}" email="${this.bank.principalOfficerEmail}" />`);
for (const s of this.filteredStr) {
xml.push(` <Suspect ref="${s.reportRefNo}" status="${s.status}">`);
xml.push(` <Account>${s.accountNumber}</Account>`);
xml.push(
` <Transaction id="${s.txnId}" date="${s.txnDate}" time="${s.txnTime}" amount="${s.txnAmount}" type="${s.depositOrWithdrawal}" instrument="${s.instrumentType}" />`
);
xml.push(` <RiskCategory>${s.riskCategory}</RiskCategory>`);
xml.push(` <Detection date="${s.detectedOn}" by="${this.escapeXml(s.flaggedBy)}" />`);
xml.push(` <Narrative>${this.escapeXml(s.narrative)}</Narrative>`);
if (s.poRemarks) xml.push(` <PORemarks>${this.escapeXml(s.poRemarks)}</PORemarks>`);
xml.push(' </Suspect>');
}
xml.push('</STR>');
const blob = new Blob([xml.join('\n')], { type: 'application/xml;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `STR-${this.bank.reportingMonth.replace(/\s+/g, '-')}.xml`;
a.click();
URL.revokeObjectURL(url);
this.toast(`STR XML exported (${this.filteredStr.length} suspects)`);
}
/** Bundle: all 6 CSVs zipped — emitted as separate downloads sequentially */
exportAll(): void {
setTimeout(() => this.exportTc1(), 0);
setTimeout(() => this.exportTc2(), 200);
setTimeout(() => this.exportKc1(), 400);
setTimeout(() => this.exportKc2(), 600);
setTimeout(() => this.exportAccountDetail(), 800);
setTimeout(() => this.exportAccountPersonRelation(), 1000);
}
private escapeXml(s: string): string {
return (s || '')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;');
}
private saveCsv(rows: string[][], filename: string): void {
const escape = (v: string): string => {
const s = (v || '').replace(/"/g, '""');
if (s.includes(',') || s.includes('"') || s.includes('\n')) return `"${s}"`;
return s;
};
const csv = rows.map((r) => r.map(escape).join(',')).join('\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 = filename;
a.click();
URL.revokeObjectURL(url);
}
toast(msg: string): void {
this.toastMsg = msg;
this.toastVisible = true;
if (this.toastTimer) clearTimeout(this.toastTimer);
this.toastTimer = setTimeout(() => (this.toastVisible = false), 2800);
}
}