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>
548 lines
16 KiB
TypeScript
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, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''');
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|