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>
This commit is contained in:
Aditya
2026-04-29 18:59:50 +05:30
co-authored by Claude Opus 4.7
parent 634003a75c
commit 12bfc81219
61 changed files with 22207 additions and 725 deletions
+560
View File
@@ -0,0 +1,560 @@
/**
* 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/.
*/
/**
* Sample FIU-IND data — schema mirrors the FINnet 2.0 CSV submission templates
* (TC1, TC2, KC1, KC2, Account_Detail, Account_Person_Relation).
* Bank/customer names are dummy placeholders.
*
* Reports filed with FIU-IND under PMLA 2002:
* CTR — Cash Transaction Report (≥ ₹10 L cash, monthly by 15th)
* STR — Suspicious Transaction Report (within 7 days of detection)
* CCR — Counterfeit Currency Report
* NTR — NPO Transaction Report (≥ ₹10 L, NGO/NPO clients)
* CBWTR — Cross-Border Wire Transfer Report (≥ ₹5 L)
*/
export type ReportType = 'CTR' | 'STR' | 'CCR' | 'NTR' | 'CBWTR';
export type StrStatus = 'draft' | 'po-review' | 'filed' | 'rejected';
/** TC2 — Branch / cash transaction record */
export interface CtrTransaction {
reportType: ReportType;
reportRefNo: number;
branchCode: string;
accountNumber: string;
txnBranchCode: string;
depositOrWithdrawal: 'Deposit' | 'Withdrawal';
txnId: number;
txnDate: string;
txnTime: string;
txnAmount: number;
instrumentType: 'Cash' | 'Cheque' | 'DD' | 'NEFT' | 'RTGS' | 'Others';
instrumentId: string;
nonCustomerRef: string;
relationshipFlag: 'Customer' | 'Non-Customer';
thirdPartyPan: string;
declaration: string;
/** Customer name resolved from KC1 (UI convenience, not in actual TC2 schema) */
customerName?: string;
}
/** STR record — workflow extension on top of TC2 */
export interface StrTransaction extends CtrTransaction {
detectedOn: string;
flaggedBy: string;
riskCategory:
| 'structuring'
| 'dormant-revival'
| 'high-risk-country'
| 'unusual-pattern'
| 'pep-related'
| 'sanctions-hit';
narrative: string;
status: StrStatus;
poRemarks?: string;
}
/** KC1 — Individual customer card (subset of the 70+ fields, only what UI shows) */
export interface KycIndividual {
reportRefNo: number;
ucic: string;
pan: string;
ckycNumber: string;
firstName: string;
middleName: string;
lastName: string;
fatherName: string;
motherName: string;
gender: 'Male' | 'Female' | 'Other';
dob: string;
mobileNumber: string;
email: string;
primaryAddress: string;
state: string;
city: string;
pincode: string;
customerType: string;
annualIncome: number;
occupation: string;
onboardingDate: string;
lastKycDate: string;
riskLevel: 'Low' | 'Medium' | 'High';
reportType: ReportType;
otherIdentifierName?: string;
otherIdentifierNumber?: string;
}
/** KC2 — Entity / non-individual customer card */
export interface KycEntity {
reportRefNo: number;
ucic: string;
entityName: string;
panNumber: string;
gstin: string;
mobileNumber: string;
address: string;
state: string;
city: string;
pincode: string;
dateOfIncorporation: string;
customerType: string;
riskLevel: 'Low' | 'Medium' | 'High';
onboardingDate: string;
lastKycDate: string;
isSoleProprietor: boolean;
ownerUcic: string;
ownerName: string;
reportType: ReportType;
}
export const FIU_BANK = {
name: 'Bharat Urban Bank Ltd',
branch: 'Head Office',
reBankingFlag: 'SCB',
reportingMonth: 'January 2026',
reBuid: 'REBANK-2031',
principalOfficer: 'Karthik Kamath',
principalOfficerEmail: 'po.fiu@bharaturbanbank.example',
filerUserId: 'PO-001',
cashThreshold: 1000000, // ₹10 L per single cash txn
monthlyAggThreshold: 1000000, // aggregate ≥ ₹10 L in a month
cbwtrThreshold: 500000 // CBWTR threshold ₹5 L
};
/** ── Sample CTR transactions (FINnet TC2-style) ── */
export const CTR_TXNS: CtrTransaction[] = [
{
reportType: 'CTR',
reportRefNo: 1,
branchCode: '1',
accountNumber: '110001101000838',
txnBranchCode: '1',
depositOrWithdrawal: 'Withdrawal',
txnId: 1,
txnDate: '20-01-2026',
txnTime: '16:51:28',
txnAmount: 1500000,
instrumentType: 'Cheque',
instrumentId: '24304',
nonCustomerRef: '',
relationshipFlag: 'Customer',
thirdPartyPan: '',
declaration: 'NA',
customerName: 'RAVI KUMAR SHARMA'
},
{
reportType: 'CTR',
reportRefNo: 1,
branchCode: '1',
accountNumber: '110001101000838',
txnBranchCode: '1',
depositOrWithdrawal: 'Withdrawal',
txnId: 2,
txnDate: '27-01-2026',
txnTime: '16:14:22',
txnAmount: 932000,
instrumentType: 'Cheque',
instrumentId: '24305',
nonCustomerRef: '',
relationshipFlag: 'Customer',
thirdPartyPan: '',
declaration: 'NA',
customerName: 'RAVI KUMAR SHARMA'
},
{
reportType: 'CTR',
reportRefNo: 2,
branchCode: '1',
accountNumber: '110001352000357',
txnBranchCode: '1',
depositOrWithdrawal: 'Withdrawal',
txnId: 3,
txnDate: '02-01-2026',
txnTime: '15:05:36',
txnAmount: 600000,
instrumentType: 'Cheque',
instrumentId: '100039',
nonCustomerRef: '',
relationshipFlag: 'Customer',
thirdPartyPan: '',
declaration: 'NA',
customerName: 'PRIYA PATEL'
},
{
reportType: 'CTR',
reportRefNo: 2,
branchCode: '1',
accountNumber: '110001352000357',
txnBranchCode: '1',
depositOrWithdrawal: 'Withdrawal',
txnId: 4,
txnDate: '07-01-2026',
txnTime: '14:14:17',
txnAmount: 700000,
instrumentType: 'Cheque',
instrumentId: '100040',
nonCustomerRef: '',
relationshipFlag: 'Customer',
thirdPartyPan: '',
declaration: 'NA',
customerName: 'PRIYA PATEL'
},
{
reportType: 'CTR',
reportRefNo: 3,
branchCode: '7',
accountNumber: '110001352000420',
txnBranchCode: '7',
depositOrWithdrawal: 'Deposit',
txnId: 5,
txnDate: '12-01-2026',
txnTime: '11:28:50',
txnAmount: 1200000,
instrumentType: 'Cash',
instrumentId: 'CASH',
nonCustomerRef: '',
relationshipFlag: 'Customer',
thirdPartyPan: '',
declaration: 'NA',
customerName: 'VIKRAM REDDY'
},
{
reportType: 'CTR',
reportRefNo: 4,
branchCode: '4',
accountNumber: '110001352000511',
txnBranchCode: '4',
depositOrWithdrawal: 'Deposit',
txnId: 6,
txnDate: '18-01-2026',
txnTime: '10:14:05',
txnAmount: 2500000,
instrumentType: 'Cash',
instrumentId: 'CASH',
nonCustomerRef: '',
relationshipFlag: 'Customer',
thirdPartyPan: '',
declaration: 'NA',
customerName: 'NIRAV SHAH'
},
{
reportType: 'CTR',
reportRefNo: 5,
branchCode: '11',
accountNumber: '110001352000604',
txnBranchCode: '11',
depositOrWithdrawal: 'Withdrawal',
txnId: 7,
txnDate: '24-01-2026',
txnTime: '13:42:11',
txnAmount: 1100000,
instrumentType: 'Cheque',
instrumentId: '88123',
nonCustomerRef: 'NCR-001',
relationshipFlag: 'Non-Customer',
thirdPartyPan: 'AKHPV2244R',
declaration: 'NA',
customerName: 'KAVITA VERMA'
}
];
/** ── Sample STR transactions ── */
export const STR_TXNS: StrTransaction[] = [
{
reportType: 'STR',
reportRefNo: 101,
branchCode: '1',
accountNumber: '110001101000841',
txnBranchCode: '1',
depositOrWithdrawal: 'Deposit',
txnId: 8,
txnDate: '15-01-2026',
txnTime: '11:32:00',
txnAmount: 950000,
instrumentType: 'Cash',
instrumentId: 'CASH',
nonCustomerRef: '',
relationshipFlag: 'Customer',
thirdPartyPan: '',
declaration: 'NA',
customerName: 'ARJUN DESHMUKH',
detectedOn: '16-01-2026',
flaggedBy: 'AML Engine — Rule R-007',
riskCategory: 'structuring',
narrative:
'Three sub-₹10L cash deposits within 5 days totalling ₹28.4L; pattern indicative of structuring to avoid CTR threshold.',
status: 'po-review'
},
{
reportType: 'STR',
reportRefNo: 102,
branchCode: '7',
accountNumber: '110001101000922',
txnBranchCode: '7',
depositOrWithdrawal: 'Deposit',
txnId: 9,
txnDate: '21-01-2026',
txnTime: '14:18:30',
txnAmount: 4800000,
instrumentType: 'NEFT',
instrumentId: 'N1264821',
nonCustomerRef: '',
relationshipFlag: 'Customer',
thirdPartyPan: '',
declaration: 'NA',
customerName: 'LAKSHMI IYER',
detectedOn: '22-01-2026',
flaggedBy: 'Branch Compliance Officer',
riskCategory: 'dormant-revival',
narrative:
'Account dormant for 18 months suddenly received ₹48L NEFT followed by 4 immediate cash withdrawals of ₹2L each.',
status: 'filed',
poRemarks: 'STR filed with FIU-IND. Acknowledgement no: STR-2026-9821.'
},
{
reportType: 'STR',
reportRefNo: 103,
branchCode: '2',
accountNumber: '110001302001012',
txnBranchCode: '2',
depositOrWithdrawal: 'Withdrawal',
txnId: 10,
txnDate: '23-01-2026',
txnTime: '15:42:18',
txnAmount: 1800000,
instrumentType: 'RTGS',
instrumentId: 'R889013',
nonCustomerRef: '',
relationshipFlag: 'Customer',
thirdPartyPan: '',
declaration: 'NA',
customerName: 'SAMEER KAPOOR',
detectedOn: '24-01-2026',
flaggedBy: 'AML Engine — Rule R-014',
riskCategory: 'high-risk-country',
narrative:
'RTGS transfer to beneficiary in jurisdiction listed under FATF grey list (Cayman Islands intermediary). KYC documentation incomplete.',
status: 'draft'
},
{
reportType: 'STR',
reportRefNo: 104,
branchCode: '13',
accountNumber: '110001352001145',
txnBranchCode: '13',
depositOrWithdrawal: 'Deposit',
txnId: 11,
txnDate: '08-01-2026',
txnTime: '12:55:42',
txnAmount: 6500000,
instrumentType: 'Cash',
instrumentId: 'CASH',
nonCustomerRef: '',
relationshipFlag: 'Customer',
thirdPartyPan: '',
declaration: 'NA',
customerName: 'MANMOHAN SINGH GILL',
detectedOn: '09-01-2026',
flaggedBy: 'AML Engine — Rule R-022',
riskCategory: 'unusual-pattern',
narrative:
'Account avg monthly turnover ₹1.5L; received ₹65L cash in single deposit, source-of-funds documentation pending.',
status: 'po-review'
}
];
/** ── KC1 sample individual customers ── */
export const KC1_INDIVIDUALS: KycIndividual[] = [
{
reportRefNo: 1,
ucic: '10078',
pan: 'AAQPS8560J',
ckycNumber: '60011225033445',
firstName: 'RAVI',
middleName: 'KUMAR',
lastName: 'SHARMA',
fatherName: 'RAJESH KUMAR SHARMA',
motherName: 'SUNITA DEVI',
gender: 'Male',
dob: '10-08-1985',
mobileNumber: '91-9892011001',
email: 'ravi.sharma@example.com',
primaryAddress: 'FLAT 402 SUNRISE TOWERS MAROL ANDHERI EAST',
state: 'MH',
city: 'MUMBAI',
pincode: '400069',
customerType: 'Individual',
annualIncome: 750000,
occupation: 'Service',
onboardingDate: '15-03-2020',
lastKycDate: '12-04-2025',
riskLevel: 'Medium',
reportType: 'CTR',
otherIdentifierName: 'AADHAAR',
otherIdentifierNumber: 'XXXXXXXX1234'
},
{
reportRefNo: 2,
ucic: '10079',
pan: 'AKLPS9933P',
ckycNumber: '60022144558899',
firstName: 'PRIYA',
middleName: '',
lastName: 'PATEL',
fatherName: 'HARISH PATEL',
motherName: 'MEENA PATEL',
gender: 'Female',
dob: '22-03-1990',
mobileNumber: '91-9822055002',
email: 'priya.patel@example.com',
primaryAddress: 'B-12 SHIVAJI NAGAR FC ROAD',
state: 'MH',
city: 'PUNE',
pincode: '411005',
customerType: 'Individual',
annualIncome: 480000,
occupation: 'Self-Employed',
onboardingDate: '10-09-2023',
lastKycDate: '15-03-2025',
riskLevel: 'Low',
reportType: 'CTR',
otherIdentifierName: 'AADHAAR',
otherIdentifierNumber: 'XXXXXXXX2210'
},
{
reportRefNo: 3,
ucic: '20101',
pan: 'AAEPR9352L',
ckycNumber: '',
firstName: 'VIKRAM',
middleName: '',
lastName: 'REDDY',
fatherName: 'SURESH REDDY',
motherName: 'LAKSHMI REDDY',
gender: 'Male',
dob: '07-07-1982',
mobileNumber: '91-9845022004',
email: '',
primaryAddress: 'NO 23 4TH CROSS JAYANAGAR 5TH BLOCK',
state: 'KA',
city: 'BENGALURU',
pincode: '560041',
customerType: 'Individual',
annualIncome: 950000,
occupation: 'Profession',
onboardingDate: '12-11-2021',
lastKycDate: '20-02-2025',
riskLevel: 'Medium',
reportType: 'CTR',
otherIdentifierName: 'AADHAAR',
otherIdentifierNumber: 'XXXXXXXX6622'
},
{
reportRefNo: 4,
ucic: '40301',
pan: 'AHKPS5544Z',
ckycNumber: '60040301229988',
firstName: 'NIRAV',
middleName: '',
lastName: 'SHAH',
fatherName: 'JAYESH SHAH',
motherName: 'RITA SHAH',
gender: 'Male',
dob: '18-05-1992',
mobileNumber: '91-9824012010',
email: 'nirav.shah@example.com',
primaryAddress: 'A-401 ATLANTIS APARTMENTS SATELLITE ROAD',
state: 'GJ',
city: 'AHMEDABAD',
pincode: '380015',
customerType: 'Individual',
annualIncome: 1800000,
occupation: 'Service',
onboardingDate: '14-02-2018',
lastKycDate: '20-12-2024',
riskLevel: 'High',
reportType: 'CTR',
otherIdentifierName: 'PASSPORT',
otherIdentifierNumber: 'M5544321'
},
{
reportRefNo: 5,
ucic: '50402',
pan: 'AKHPV2244R',
ckycNumber: '',
firstName: 'KAVITA',
middleName: '',
lastName: 'VERMA',
fatherName: 'DEEPAK VERMA',
motherName: 'USHA VERMA',
gender: 'Female',
dob: '01-04-1988',
mobileNumber: '91-9415055014',
email: '',
primaryAddress: '45 CIVIL LINES MALL ROAD',
state: 'UP',
city: 'KANPUR',
pincode: '208001',
customerType: 'Individual',
annualIncome: 540000,
occupation: 'Service',
onboardingDate: '08-09-2022',
lastKycDate: '15-09-2024',
riskLevel: 'Low',
reportType: 'CTR',
otherIdentifierName: 'AADHAAR',
otherIdentifierNumber: 'XXXXXXXX2244'
}
];
/** ── KC2 sample entity customers ── */
export const KC2_ENTITIES: KycEntity[] = [
{
reportRefNo: 11,
ucic: 'E10101',
entityName: 'BHARAT TRADING COMPANY',
panNumber: 'ABCFB1234N',
gstin: '27ABCFB1234N1ZT',
mobileNumber: '91-9892011010',
address: 'GALA 22 LOWER PAREL',
state: 'MH',
city: 'MUMBAI',
pincode: '400013',
dateOfIncorporation: '24-03-2008',
customerType: 'Sole Proprietor',
riskLevel: 'High',
onboardingDate: '24-03-2008',
lastKycDate: '03-09-2025',
isSoleProprietor: true,
ownerUcic: '10078',
ownerName: 'RAVI KUMAR SHARMA',
reportType: 'CTR'
},
{
reportRefNo: 12,
ucic: 'E10102',
entityName: 'NEXTGEN TECH PVT LTD',
panNumber: 'AABCN5566K',
gstin: '27AABCN5566K1Z2',
mobileNumber: '91-9824012011',
address: 'TOWER B SUNRISE BUSINESS PARK ANDHERI EAST',
state: 'MH',
city: 'MUMBAI',
pincode: '400093',
dateOfIncorporation: '18-09-2015',
customerType: 'Private Limited',
riskLevel: 'Medium',
onboardingDate: '18-09-2015',
lastKycDate: '12-08-2025',
isSoleProprietor: false,
ownerUcic: '',
ownerName: '',
reportType: 'CTR'
}
];
+29
View File
@@ -0,0 +1,29 @@
/**
* 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 { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { Route } from '../core/route/route.service';
import { FiuComponent } from './fiu.component';
const routes: Routes = [
Route.withShell([
{
path: 'fiu',
component: FiuComponent,
data: { title: 'FIU-IND', breadcrumb: 'FIU-IND' }
}
])
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class FiuRoutingModule {}
+480
View File
@@ -0,0 +1,480 @@
<!--
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/.
-->
<div class="fiu-root">
<!-- HEADER -->
<div class="fiu-head">
<div class="fiu-head-left">
<fa-icon icon="shield-alt" class="fiu-head-ic"></fa-icon>
<div>
<h2 class="fiu-head-t">Financial Intelligence Unit – India (FIU-IND)</h2>
<div class="fiu-head-s">
{{ bank.name }} &middot; {{ bank.branch }} &middot; Reporting Month <strong>{{ bank.reportingMonth }}</strong>
&middot; PMLA 2002 / FINnet 2.0
</div>
</div>
</div>
<div class="fiu-head-meta">
<div>
<span class="fiu-meta-l">Principal Officer</span><span class="fiu-meta-v">{{ bank.principalOfficer }}</span>
</div>
<div>
<span class="fiu-meta-l">REBANK BUID</span><span class="fiu-meta-v mono">{{ bank.reBuid }}</span>
</div>
</div>
</div>
<!-- TABS -->
<div class="fiu-tabs">
<button class="fiu-tab" [class.active]="activeTab === 'dashboard'" (click)="showTab('dashboard')">
<fa-icon icon="chart-pie"></fa-icon> Dashboard
</button>
<button class="fiu-tab" [class.active]="activeTab === 'ctr'" (click)="showTab('ctr')">
<fa-icon icon="coins"></fa-icon> CTR — Cash Transactions
</button>
<button class="fiu-tab" [class.active]="activeTab === 'str'" (click)="showTab('str')">
<fa-icon icon="exclamation-triangle"></fa-icon> STR — Suspicious
</button>
<button class="fiu-tab" [class.active]="activeTab === 'customers'" (click)="showTab('customers')">
<fa-icon icon="users"></fa-icon> Customer Cards
</button>
<button class="fiu-tab" [class.active]="activeTab === 'files'" (click)="showTab('files')">
<fa-icon icon="file-csv"></fa-icon> FINnet Files
</button>
</div>
<!-- DASHBOARD -->
@if (activeTab === 'dashboard') {
<div class="fiu-section">
<div class="fiu-kpis">
<div class="fiu-kpi fiu-kpi--blue">
<div class="fiu-kpi-ic"><fa-icon icon="file-alt"></fa-icon></div>
<div>
<div class="fiu-kpi-n">{{ totalReports | number }}</div>
<div class="fiu-kpi-l">CTR Reports</div>
<div class="fiu-kpi-sub">{{ totalCtrTxns | number }} txns this cycle</div>
</div>
</div>
<div class="fiu-kpi fiu-kpi--teal">
<div class="fiu-kpi-ic"><fa-icon icon="coins"></fa-icon></div>
<div>
<div class="fiu-kpi-n">&#8377;{{ totalCtrAmount | number: '1.0-0' }}</div>
<div class="fiu-kpi-l">Total CTR Amount</div>
<div class="fiu-kpi-sub">aggregate cash movement</div>
</div>
</div>
<div class="fiu-kpi fiu-kpi--amber">
<div class="fiu-kpi-ic"><fa-icon icon="exclamation-triangle"></fa-icon></div>
<div>
<div class="fiu-kpi-n">{{ pendingStr | number }}</div>
<div class="fiu-kpi-l">STR Pending</div>
<div class="fiu-kpi-sub">draft / PO review</div>
</div>
</div>
<div class="fiu-kpi fiu-kpi--green">
<div class="fiu-kpi-ic"><fa-icon icon="check-circle"></fa-icon></div>
<div>
<div class="fiu-kpi-n">{{ filedStr | number }}</div>
<div class="fiu-kpi-l">STR Filed</div>
<div class="fiu-kpi-sub">acknowledged by FIU</div>
</div>
</div>
<div class="fiu-kpi fiu-kpi--purple">
<div class="fiu-kpi-ic"><fa-icon icon="users"></fa-icon></div>
<div>
<div class="fiu-kpi-n">{{ totalCustomers | number }}</div>
<div class="fiu-kpi-l">Customers in Cycle</div>
<div class="fiu-kpi-sub">{{ kc1.length }} ind &middot; {{ kc2.length }} entity</div>
</div>
</div>
</div>
<!-- Compliance cards -->
<div class="fiu-compliance">
<div class="dc-card">
<div class="dc-card-h">
<fa-icon icon="check-circle" class="dc-ok"></fa-icon><span>CTR — Cash Transaction Report</span>
</div>
<div class="dc-meta">
Threshold: <strong>&#8377;{{ bank.cashThreshold | number }}</strong> per single txn / aggregate per month
</div>
<div class="dc-meta">Due: <strong>15 Feb 2026</strong></div>
<button class="dc-btn" (click)="showTab('ctr')"><fa-icon icon="external-link-alt"></fa-icon> Open</button>
</div>
<div class="dc-card">
<div class="dc-card-h">
<fa-icon icon="exclamation-triangle" class="dc-warn"></fa-icon><span>STR — Suspicious Transaction</span>
</div>
<div class="dc-meta">Filing window: <strong>within 7 days of detection</strong></div>
<div class="dc-meta">
Pending: <strong>{{ pendingStr }}</strong> &middot; Filed: <strong>{{ filedStr }}</strong>
</div>
<button class="dc-btn" (click)="showTab('str')"><fa-icon icon="external-link-alt"></fa-icon> Open</button>
</div>
<div class="dc-card">
<div class="dc-card-h"><fa-icon icon="file-csv" class="dc-info"></fa-icon><span>FINnet 2.0 Files</span></div>
<div class="dc-meta">6 CSVs: TC1 / TC2 / KC1 / KC2 / Account_Detail / Account_Person_Relation</div>
<div class="dc-meta">+ STR XML for suspicious filings</div>
<button class="dc-btn" (click)="showTab('files')"><fa-icon icon="external-link-alt"></fa-icon> Open</button>
</div>
</div>
</div>
}
<!-- CTR -->
@if (activeTab === 'ctr') {
<div class="fiu-section">
<div class="fiu-form-meta">
<div class="fiu-form-title">
<h3>CTR — Cash Transaction Report</h3>
<p>
All cash transactions ≥ &#8377;{{ bank.cashThreshold | number }} (single or aggregate per month per account)
</p>
</div>
<button class="fiu-btn fiu-btn-csv" (click)="exportTc2()">
<fa-icon icon="file-csv"></fa-icon> Download TC2.csv
</button>
</div>
<div class="fiu-filters">
<div class="fiu-filter">
<label>Type</label>
<select class="fiu-inp" [(ngModel)]="ctrFilterMode">
<option value="">All</option>
<option value="Deposit">Deposit</option>
<option value="Withdrawal">Withdrawal</option>
</select>
</div>
<div class="fiu-filter">
<label>Account contains</label>
<input class="fiu-inp" type="text" [(ngModel)]="ctrFilterAccount" placeholder="A/c partial" />
</div>
<div class="fiu-filter">
<label>Min Amount (&#8377;)</label>
<input class="fiu-inp" type="number" [(ngModel)]="ctrFilterMinAmount" placeholder="0" />
</div>
<button class="fiu-btn fiu-btn-g" (click)="resetCtrFilters()">Reset</button>
</div>
<div class="fiu-tbl-wrap">
<table class="fiu-tbl">
<thead>
<tr>
<th class="ta-c">Ref</th>
<th>Account</th>
<th>Customer</th>
<th class="ta-c">Type</th>
<th>Date / Time</th>
<th class="ta-r">Amount</th>
<th>Instrument</th>
<th>Branch</th>
<th class="ta-c">Relationship</th>
</tr>
</thead>
<tbody>
@for (t of filteredCtr; track t.txnId) {
<tr>
<td class="ta-c">{{ t.reportRefNo }}</td>
<td class="mono">{{ t.accountNumber }}</td>
<td>{{ t.customerName }}</td>
<td class="ta-c">
<span
class="dir-pill"
[class.deposit]="t.depositOrWithdrawal === 'Deposit'"
[class.withdrawal]="t.depositOrWithdrawal === 'Withdrawal'"
>
{{ t.depositOrWithdrawal }}
</span>
</td>
<td>
{{ t.txnDate }} <span class="muted">{{ t.txnTime }}</span>
</td>
<td class="ta-r mono">
<strong>&#8377;{{ t.txnAmount | number: '1.0-0' }}</strong>
</td>
<td>
{{ t.instrumentType }} <span class="muted">{{ t.instrumentId }}</span>
</td>
<td class="mono">{{ t.branchCode }}</td>
<td class="ta-c">
<span class="rel-pill" [class.non-cust]="t.relationshipFlag === 'Non-Customer'">{{
t.relationshipFlag
}}</span>
</td>
</tr>
} @empty {
<tr>
<td colspan="9" class="fiu-empty">No transactions match the filters.</td>
</tr>
}
</tbody>
</table>
</div>
</div>
}
<!-- STR -->
@if (activeTab === 'str') {
<div class="fiu-section">
<div class="fiu-form-meta">
<div class="fiu-form-title">
<h3>STR — Suspicious Transaction Report</h3>
<p>Detected by AML rules engine + branch compliance officers; filed within 7 days</p>
</div>
<button class="fiu-btn fiu-btn-xml" (click)="exportStrXml()">
<fa-icon icon="file-alt"></fa-icon> Download STR XML
</button>
</div>
<div class="fiu-filters">
<div class="fiu-filter">
<label>Status</label>
<select class="fiu-inp" [(ngModel)]="strFilterStatus">
<option value="">All</option>
<option value="draft">Draft</option>
<option value="po-review">PO Review</option>
<option value="filed">Filed</option>
<option value="rejected">Rejected</option>
</select>
</div>
<div class="fiu-filter">
<label>Risk Category</label>
<select class="fiu-inp" [(ngModel)]="strFilterRisk">
<option value="">All</option>
<option value="structuring">Structuring</option>
<option value="dormant-revival">Dormant Revival</option>
<option value="high-risk-country">High-Risk Country</option>
<option value="unusual-pattern">Unusual Pattern</option>
<option value="pep-related">PEP Related</option>
<option value="sanctions-hit">Sanctions Hit</option>
</select>
</div>
<button class="fiu-btn fiu-btn-g" (click)="resetStrFilters()">Reset</button>
</div>
<div class="fiu-str-list">
@for (s of filteredStr; track s.reportRefNo) {
<div class="str-card" [class.str-card--filed]="s.status === 'filed'">
<div class="str-card-h">
<div class="str-card-id">
<span class="str-ref">STR-{{ s.reportRefNo }}</span>
<span class="status-pill" [class]="'st-' + s.status">{{ statusLabel(s.status) }}</span>
<span class="risk-pill" [class]="'rk-' + s.riskCategory">{{ riskLabel(s.riskCategory) }}</span>
</div>
<div class="str-card-amt">
<span class="muted">{{ s.depositOrWithdrawal }}</span>
<strong>&#8377;{{ s.txnAmount | number: '1.0-0' }}</strong>
</div>
</div>
<div class="str-card-body">
<div class="str-meta">
<div>
<label>Customer</label><span>{{ s.customerName }}</span>
</div>
<div>
<label>Account</label><span class="mono">{{ s.accountNumber }}</span>
</div>
<div>
<label>Txn Date</label><span>{{ s.txnDate }} {{ s.txnTime }}</span>
</div>
<div>
<label>Detected</label><span>{{ s.detectedOn }}</span>
</div>
<div>
<label>Flagged By</label><span>{{ s.flaggedBy }}</span>
</div>
<div>
<label>Instrument</label><span>{{ s.instrumentType }} {{ s.instrumentId }}</span>
</div>
</div>
<div class="str-narrative">
<label>Narrative</label>
<p>{{ s.narrative }}</p>
</div>
@if (s.poRemarks) {
<div class="str-po-remarks">
<label><fa-icon icon="check-circle"></fa-icon> PO Remarks</label>
<p>{{ s.poRemarks }}</p>
</div>
}
</div>
</div>
} @empty {
<div class="fiu-empty-card">No STR records match the filters.</div>
}
</div>
</div>
}
<!-- CUSTOMER CARDS -->
@if (activeTab === 'customers') {
<div class="fiu-section">
<div class="fiu-form-meta">
<div class="fiu-form-title">
<h3>Customer Cards — KC1 (Individuals) &amp; KC2 (Entities)</h3>
<p>Customers attached to reports filed in this cycle</p>
</div>
<div class="fiu-action-group">
<button class="fiu-btn fiu-btn-csv" (click)="exportKc1()"><fa-icon icon="file-csv"></fa-icon> KC1.csv</button>
<button class="fiu-btn fiu-btn-csv" (click)="exportKc2()"><fa-icon icon="file-csv"></fa-icon> KC2.csv</button>
</div>
</div>
<div class="fiu-tbl-wrap">
<div class="fiu-tbl-h">KC1 — Individuals</div>
<table class="fiu-tbl">
<thead>
<tr>
<th class="ta-c">Ref</th>
<th>UCIC</th>
<th>Name</th>
<th class="ta-c">Gender</th>
<th>DOB</th>
<th>PAN</th>
<th>Mobile</th>
<th>City</th>
<th class="ta-r">Annual Income</th>
<th class="ta-c">Risk</th>
</tr>
</thead>
<tbody>
@for (k of kc1; track k.ucic) {
<tr>
<td class="ta-c">{{ k.reportRefNo }}</td>
<td class="mono">{{ k.ucic }}</td>
<td>{{ k.firstName }} {{ k.middleName }} {{ k.lastName }}</td>
<td class="ta-c">{{ k.gender }}</td>
<td>{{ k.dob }}</td>
<td class="mono">{{ k.pan }}</td>
<td class="mono">{{ k.mobileNumber }}</td>
<td>{{ k.city }}, {{ k.state }}</td>
<td class="ta-r mono">&#8377;{{ k.annualIncome | number }}</td>
<td class="ta-c">
<span class="risk-tag" [class]="'rk-tag-' + k.riskLevel.toLowerCase()">{{ k.riskLevel }}</span>
</td>
</tr>
}
</tbody>
</table>
</div>
<div class="fiu-tbl-wrap">
<div class="fiu-tbl-h">KC2 — Entities</div>
<table class="fiu-tbl">
<thead>
<tr>
<th class="ta-c">Ref</th>
<th>UCIC</th>
<th>Entity Name</th>
<th>PAN</th>
<th>GSTIN</th>
<th>City</th>
<th>Type</th>
<th>Owner (if Sole Prop)</th>
<th class="ta-c">Risk</th>
</tr>
</thead>
<tbody>
@for (k of kc2; track k.ucic) {
<tr>
<td class="ta-c">{{ k.reportRefNo }}</td>
<td class="mono">{{ k.ucic }}</td>
<td>{{ k.entityName }}</td>
<td class="mono">{{ k.panNumber }}</td>
<td class="mono">{{ k.gstin }}</td>
<td>{{ k.city }}, {{ k.state }}</td>
<td>{{ k.customerType }}</td>
<td>{{ k.isSoleProprietor ? k.ownerName : '—' }}</td>
<td class="ta-c">
<span class="risk-tag" [class]="'rk-tag-' + k.riskLevel.toLowerCase()">{{ k.riskLevel }}</span>
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
}
<!-- FINNET FILES -->
@if (activeTab === 'files') {
<div class="fiu-section">
<div class="fiu-form-meta">
<div class="fiu-form-title">
<h3>FINnet 2.0 Submission Files</h3>
<p>6 CSV files in FIU-IND prescribed schema, plus STR XML</p>
</div>
<button class="fiu-btn fiu-btn-xls" (click)="exportAll()">
<fa-icon icon="file-csv"></fa-icon> Download All
</button>
</div>
<div class="files-grid">
<div class="file-card">
<div class="file-card-h"><fa-icon icon="file-csv"></fa-icon>TC1.csv</div>
<p>ATM / card transaction template (header only — no card txns this cycle)</p>
<button class="fiu-btn" (click)="exportTc1()"><fa-icon icon="file-csv"></fa-icon> Download</button>
</div>
<div class="file-card">
<div class="file-card-h"><fa-icon icon="file-csv"></fa-icon>TC2.csv</div>
<p>Branch / cash transactions ({{ ctrTxns.length }} rows)</p>
<button class="fiu-btn fiu-btn-csv" (click)="exportTc2()">
<fa-icon icon="file-csv"></fa-icon> Download
</button>
</div>
<div class="file-card">
<div class="file-card-h"><fa-icon icon="file-csv"></fa-icon>KC1.csv</div>
<p>Individual customer cards ({{ kc1.length }} rows)</p>
<button class="fiu-btn fiu-btn-csv" (click)="exportKc1()">
<fa-icon icon="file-csv"></fa-icon> Download
</button>
</div>
<div class="file-card">
<div class="file-card-h"><fa-icon icon="file-csv"></fa-icon>KC2.csv</div>
<p>Entity customer cards ({{ kc2.length }} rows)</p>
<button class="fiu-btn fiu-btn-csv" (click)="exportKc2()">
<fa-icon icon="file-csv"></fa-icon> Download
</button>
</div>
<div class="file-card">
<div class="file-card-h"><fa-icon icon="file-csv"></fa-icon>Account_Detail.csv</div>
<p>Per-account 12-month aggregates derived from CTR list</p>
<button class="fiu-btn fiu-btn-csv" (click)="exportAccountDetail()">
<fa-icon icon="file-csv"></fa-icon> Download
</button>
</div>
<div class="file-card">
<div class="file-card-h"><fa-icon icon="file-csv"></fa-icon>Account_Person_Relation.csv</div>
<p>Account ↔ customer relationship mapping</p>
<button class="fiu-btn fiu-btn-csv" (click)="exportAccountPersonRelation()">
<fa-icon icon="file-csv"></fa-icon> Download
</button>
</div>
<div class="file-card file-card--xml">
<div class="file-card-h"><fa-icon icon="file-alt"></fa-icon>STR.xml</div>
<p>Suspicious Transaction Report ({{ filedStr + pendingStr }} suspects)</p>
<button class="fiu-btn fiu-btn-xml" (click)="exportStrXml()">
<fa-icon icon="file-alt"></fa-icon> Download XML
</button>
</div>
</div>
<div class="fiu-note">
<fa-icon icon="info-circle"></fa-icon>
<span
><strong>Note:</strong> Files are emitted in FIU-IND FINnet 2.0 CSV schema. Submit via the FINnet portal under
{{ bank.principalOfficer }}'s digital signature. CTR is due by the 15th of the following month; STR within 7
days of detection.</span
>
</div>
</div>
}
<!-- TOAST -->
<div class="fiu-toast" [class.show]="toastVisible">{{ toastMsg }}</div>
</div>
+894
View File
@@ -0,0 +1,894 @@
/**
* 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/.
*/
:host {
--f-navy: #0b1e3d;
--f-blue: #1d4ed8;
--f-blue-l: #eff6ff;
--f-teal: #0e7e6b;
--f-teal-l: #e0f4f0;
--f-green: #15803d;
--f-green-l: #dcfce7;
--f-amber: #d97706;
--f-amber-l: #fef3c7;
--f-red: #c0392b;
--f-red-l: #fdecea;
--f-purple: #6d28d9;
--f-purple-l: #ede9fe;
--f-g50: #f8fafc;
--f-g100: #f1f5f9;
--f-g200: #e2e8f0;
--f-g300: #cbd5e1;
--f-g500: #64748b;
--f-g700: #334155;
--f-g900: #0f172a;
display: block;
font-family:
'DM Sans',
system-ui,
-apple-system,
sans-serif;
background: var(--f-g50);
color: var(--f-g900);
font-size: 14px;
line-height: 1.5;
}
.fiu-root {
padding: 18px 22px;
max-width: 1500px;
margin: 0 auto;
}
.fiu-head {
display: flex;
justify-content: space-between;
align-items: center;
gap: 16px;
flex-wrap: wrap;
margin-bottom: 16px;
}
.fiu-head-left {
display: flex;
align-items: center;
gap: 14px;
}
.fiu-head-ic {
font-size: 28px;
color: var(--f-red);
}
.fiu-head-t {
margin: 0;
font-size: 18px;
font-weight: 700;
color: var(--f-g900);
}
.fiu-head-s {
font-size: 12px;
color: var(--f-g500);
}
.fiu-head-meta {
display: flex;
gap: 22px;
> div {
display: flex;
flex-direction: column;
.fiu-meta-l {
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--f-g500);
}
.fiu-meta-v {
font-size: 14px;
font-weight: 600;
color: var(--f-g900);
}
}
}
/* TABS */
.fiu-tabs {
display: flex;
gap: 4px;
background: #fff;
border: 1px solid var(--f-g200);
border-radius: 10px;
padding: 4px;
margin-bottom: 16px;
overflow-x: auto;
}
.fiu-tab {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 9px 14px;
border: none;
background: transparent;
font-size: 13px;
font-weight: 500;
color: var(--f-g500);
cursor: pointer;
border-radius: 7px;
white-space: nowrap;
transition: all 0.15s;
font-family: inherit;
&:hover {
color: var(--f-g900);
}
&.active {
background: var(--f-red);
color: #fff;
box-shadow: 0 2px 6px rgb(192 57 43 / 25%);
}
}
.fiu-section {
display: flex;
flex-direction: column;
gap: 16px;
}
/* KPIs */
.fiu-kpis {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 12px;
}
.fiu-kpi {
background: #fff;
border: 1px solid var(--f-g200);
border-radius: 12px;
padding: 14px 16px;
display: flex;
align-items: center;
gap: 12px;
box-shadow: 0 1px 3px rgb(0 0 0 / 5%);
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 14px rgb(0 0 0 / 10%);
}
}
.fiu-kpi-ic {
width: 42px;
height: 42px;
border-radius: 11px;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
flex-shrink: 0;
}
.fiu-kpi--blue .fiu-kpi-ic {
background: var(--f-blue-l);
color: var(--f-blue);
}
.fiu-kpi--teal .fiu-kpi-ic {
background: var(--f-teal-l);
color: var(--f-teal);
}
.fiu-kpi--amber .fiu-kpi-ic {
background: var(--f-amber-l);
color: var(--f-amber);
}
.fiu-kpi--green .fiu-kpi-ic {
background: var(--f-green-l);
color: var(--f-green);
}
.fiu-kpi--purple .fiu-kpi-ic {
background: var(--f-purple-l);
color: var(--f-purple);
}
.fiu-kpi-n {
font-size: 18px;
font-weight: 700;
color: var(--f-g900);
line-height: 1.1;
}
.fiu-kpi-l {
font-size: 12px;
color: var(--f-g700);
font-weight: 500;
margin-top: 2px;
}
.fiu-kpi-sub {
font-size: 11px;
color: var(--f-g500);
margin-top: 1px;
}
/* COMPLIANCE CARDS */
.fiu-compliance {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 12px;
}
.dc-card {
background: #fff;
border: 1px solid var(--f-g200);
border-radius: 12px;
padding: 14px 18px;
box-shadow: 0 1px 3px rgb(0 0 0 / 4%);
}
.dc-card-h {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
font-weight: 600;
color: var(--f-g900);
margin-bottom: 8px;
}
.dc-ok {
color: var(--f-green);
}
.dc-warn {
color: var(--f-amber);
}
.dc-info {
color: var(--f-blue);
}
.dc-meta {
font-size: 12px;
color: var(--f-g500);
margin: 2px 0;
strong {
color: var(--f-g900);
font-weight: 600;
}
}
.dc-btn {
margin-top: 10px;
padding: 6px 12px;
border: 1px solid var(--f-red);
background: var(--f-red-l);
color: var(--f-red);
border-radius: 8px;
font-size: 12px;
font-weight: 600;
cursor: pointer;
font-family: inherit;
display: inline-flex;
align-items: center;
gap: 6px;
&:hover {
background: var(--f-red);
color: #fff;
}
}
/* FORM HEADER */
.fiu-form-meta {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 14px;
flex-wrap: wrap;
}
.fiu-form-title {
h3 {
margin: 0;
font-size: 16px;
font-weight: 700;
color: var(--f-g900);
}
p {
margin: 2px 0 0;
font-size: 12px;
color: var(--f-g500);
}
}
.fiu-action-group {
display: flex;
gap: 8px;
}
/* BUTTONS */
.fiu-btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 14px;
border: 1px solid var(--f-g300);
background: #fff;
border-radius: 8px;
font-size: 13px;
font-weight: 500;
color: var(--f-g700);
cursor: pointer;
font-family: inherit;
transition: all 0.15s;
&:hover {
background: var(--f-g50);
border-color: var(--f-g500);
}
}
.fiu-btn-csv {
background: #1d4ed8;
border-color: #1d4ed8;
color: #fff;
&:hover {
background: #1e40af;
border-color: #1e40af;
color: #fff;
}
}
.fiu-btn-xls {
background: #16a34a;
border-color: #16a34a;
color: #fff;
&:hover {
background: #15803d;
border-color: #15803d;
color: #fff;
}
}
.fiu-btn-xml {
background: var(--f-purple);
border-color: var(--f-purple);
color: #fff;
&:hover {
background: #5b21b6;
border-color: #5b21b6;
color: #fff;
}
}
.fiu-btn-g {
align-self: end;
}
/* INPUTS */
.fiu-inp {
width: 100%;
padding: 8px 12px;
border: 1px solid var(--f-g200);
border-radius: 8px;
font-size: 13px;
background: #fff;
font-family: inherit;
&:focus {
outline: none;
border-color: var(--f-red);
box-shadow: 0 0 0 3px rgb(192 57 43 / 15%);
}
}
/* FILTERS */
.fiu-filters {
background: #fff;
border: 1px solid var(--f-g200);
border-radius: 12px;
padding: 14px;
display: grid;
grid-template-columns: 1fr 1fr 1fr auto;
gap: 12px;
align-items: end;
}
.fiu-filter {
display: flex;
flex-direction: column;
gap: 4px;
label {
font-size: 11px;
font-weight: 600;
color: var(--f-g500);
text-transform: uppercase;
letter-spacing: 0.05em;
}
}
/* TABLE */
.fiu-tbl-wrap {
background: #fff;
border: 1px solid var(--f-g200);
border-radius: 12px;
overflow: auto;
box-shadow: 0 1px 3px rgb(0 0 0 / 4%);
}
.fiu-tbl-h {
background: var(--f-blue-l);
color: var(--f-blue);
padding: 10px 16px;
font-size: 12px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
border-bottom: 1px solid var(--f-g200);
}
.fiu-tbl {
width: 100%;
border-collapse: collapse;
th {
text-align: left;
padding: 10px 12px;
font-size: 11px;
font-weight: 600;
color: var(--f-g500);
text-transform: uppercase;
letter-spacing: 0.04em;
background: var(--f-g50);
border-bottom: 1px solid var(--f-g200);
}
td {
padding: 10px 12px;
border-bottom: 1px solid var(--f-g100);
font-size: 13px;
color: var(--f-g700);
}
tr:last-child td {
border-bottom: none;
}
tr:hover td {
background: var(--f-g50);
}
}
.ta-c {
text-align: center;
}
.ta-r {
text-align: right;
}
.mono {
font-family: 'DM Mono', monospace;
font-size: 12px;
letter-spacing: 0.02em;
}
.muted {
color: var(--f-g500);
font-size: 11px;
}
.fiu-empty {
text-align: center;
padding: 30px;
color: var(--f-g500);
font-style: italic;
}
.fiu-empty-card {
background: #fff;
border: 1px dashed var(--f-g300);
border-radius: 12px;
padding: 50px;
text-align: center;
color: var(--f-g500);
font-size: 13px;
}
/* PILLS */
.dir-pill {
display: inline-block;
padding: 2px 9px;
border-radius: 999px;
font-size: 11px;
font-weight: 600;
background: var(--f-g100);
color: var(--f-g700);
&.deposit {
background: var(--f-green-l);
color: var(--f-green);
}
&.withdrawal {
background: var(--f-red-l);
color: var(--f-red);
}
}
.rel-pill {
display: inline-block;
padding: 2px 9px;
border-radius: 999px;
font-size: 11px;
font-weight: 600;
background: var(--f-blue-l);
color: var(--f-blue);
&.non-cust {
background: var(--f-amber-l);
color: var(--f-amber);
}
}
.status-pill {
display: inline-block;
padding: 2px 10px;
border-radius: 999px;
font-size: 11px;
font-weight: 600;
&.st-draft {
background: var(--f-g100);
color: var(--f-g700);
}
&.st-po-review {
background: var(--f-amber-l);
color: var(--f-amber);
}
&.st-filed {
background: var(--f-green-l);
color: var(--f-green);
}
&.st-rejected {
background: var(--f-red-l);
color: var(--f-red);
}
}
.risk-pill {
display: inline-block;
padding: 2px 10px;
border-radius: 999px;
font-size: 11px;
font-weight: 600;
background: var(--f-purple-l);
color: var(--f-purple);
&.rk-structuring {
background: #fff7ed;
color: #9a3412;
}
&.rk-dormant-revival {
background: #ecfeff;
color: #155e75;
}
&.rk-high-risk-country {
background: var(--f-red-l);
color: var(--f-red);
}
&.rk-unusual-pattern {
background: var(--f-amber-l);
color: var(--f-amber);
}
&.rk-pep-related {
background: var(--f-purple-l);
color: var(--f-purple);
}
&.rk-sanctions-hit {
background: #fee2e2;
color: #991b1b;
}
}
.risk-tag {
display: inline-block;
padding: 2px 8px;
border-radius: 6px;
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
&.rk-tag-low {
background: var(--f-green-l);
color: var(--f-green);
}
&.rk-tag-medium {
background: var(--f-amber-l);
color: var(--f-amber);
}
&.rk-tag-high {
background: var(--f-red-l);
color: var(--f-red);
}
}
/* STR cards */
.fiu-str-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.str-card {
background: #fff;
border: 1px solid var(--f-g200);
border-radius: 12px;
overflow: hidden;
box-shadow: 0 1px 3px rgb(0 0 0 / 4%);
&--filed {
border-color: #86efac;
}
}
.str-card-h {
padding: 12px 16px;
background: var(--f-g50);
border-bottom: 1px solid var(--f-g200);
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
}
.str-card-id {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.str-ref {
font-family: 'DM Mono', monospace;
font-size: 13px;
font-weight: 700;
color: var(--f-g900);
}
.str-card-amt {
display: flex;
align-items: baseline;
gap: 6px;
strong {
font-size: 16px;
color: var(--f-g900);
font-family: 'DM Mono', monospace;
}
}
.str-card-body {
padding: 14px 16px;
display: flex;
flex-direction: column;
gap: 12px;
}
.str-meta {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px 18px;
> div {
display: flex;
flex-direction: column;
label {
font-size: 10px;
font-weight: 600;
color: var(--f-g500);
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 1px;
}
span {
font-size: 13px;
color: var(--f-g900);
}
}
}
.str-narrative,
.str-po-remarks {
background: var(--f-g50);
border-radius: 8px;
padding: 10px 14px;
label {
font-size: 10px;
font-weight: 700;
color: var(--f-g500);
text-transform: uppercase;
letter-spacing: 0.05em;
display: flex;
align-items: center;
gap: 5px;
margin-bottom: 4px;
}
p {
margin: 0;
font-size: 13px;
color: var(--f-g700);
line-height: 1.5;
}
}
.str-po-remarks {
background: var(--f-green-l);
label {
color: var(--f-green);
}
p {
color: #14532d;
}
}
/* FILES TAB */
.files-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
}
.file-card {
background: #fff;
border: 1px solid var(--f-g200);
border-radius: 12px;
padding: 16px;
display: flex;
flex-direction: column;
gap: 8px;
box-shadow: 0 1px 3px rgb(0 0 0 / 4%);
&--xml {
border-color: var(--f-purple);
background: linear-gradient(135deg, #fff 0%, #faf5ff 100%);
}
}
.file-card-h {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
font-weight: 700;
color: var(--f-g900);
font-family: 'DM Mono', monospace;
}
.file-card p {
margin: 0;
font-size: 12px;
color: var(--f-g500);
flex: 1;
}
.file-card .fiu-btn {
align-self: flex-start;
}
/* NOTE */
.fiu-note {
background: var(--f-blue-l);
border: 1px solid #bfdbfe;
border-radius: 10px;
padding: 12px 16px;
display: flex;
gap: 10px;
font-size: 12px;
color: #1e3a8a;
line-height: 1.5;
fa-icon {
font-size: 14px;
flex-shrink: 0;
margin-top: 2px;
}
}
/* TOAST */
.fiu-toast {
position: fixed;
bottom: 32px;
left: 50%;
transform: translateX(-50%) translateY(20px);
background: var(--f-navy);
color: #fff;
padding: 12px 22px;
border-radius: 10px;
font-size: 13px;
font-weight: 500;
box-shadow: 0 6px 20px rgb(0 0 0 / 25%);
opacity: 0;
pointer-events: none;
transition: all 0.25s;
z-index: 1500;
&.show {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
}
@media (width <= 1100px) {
.fiu-kpis {
grid-template-columns: repeat(3, 1fr);
}
.fiu-compliance {
grid-template-columns: 1fr;
}
.fiu-filters {
grid-template-columns: 1fr 1fr;
}
.files-grid {
grid-template-columns: 1fr 1fr;
}
.str-meta {
grid-template-columns: 1fr 1fr;
}
}
@media (width <= 720px) {
.fiu-kpis {
grid-template-columns: 1fr 1fr;
}
.files-grid {
grid-template-columns: 1fr;
}
.str-meta {
grid-template-columns: 1fr;
}
}
+547
View File
@@ -0,0 +1,547 @@
/**
* 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);
}
}
+22
View File
@@ -0,0 +1,22 @@
/**
* 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 { NgModule } from '@angular/core';
import { SharedModule } from '../shared/shared.module';
import { FiuRoutingModule } from './fiu-routing.module';
import { FiuComponent } from './fiu.component';
@NgModule({
imports: [
SharedModule,
FiuRoutingModule,
FiuComponent
]
})
export class FiuModule {}