Files
cbsnexora/src/app/sft/sft.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

467 lines
14 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 {
SFT_ACCOUNT_RECORDS,
SFT_BANK,
SFT_INT_DIV_RECORDS,
SFT_PERSON_RECORDS,
SftAccountRecord,
SftCode,
SftInterestDividendRecord,
SftPersonRecord
} from './sft-data';
@Component({
selector: 'mifosx-sft',
templateUrl: './sft.component.html',
styleUrls: ['./sft.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
FaIconComponent,
FormsModule
]
})
export class SftComponent {
/** Active tab */
activeTab: 'dashboard' | 'person' | 'account' | 'intdiv' = 'dashboard';
bank = SFT_BANK;
personRecords: SftPersonRecord[] = SFT_PERSON_RECORDS;
accountRecords: SftAccountRecord[] = SFT_ACCOUNT_RECORDS;
intDivRecords: SftInterestDividendRecord[] = SFT_INT_DIV_RECORDS;
/** Person tab — switch which SFT code is being shown */
personSftFilter: 'ALL' | 'SFT-001' | 'SFT-002' | 'SFT-004' = 'ALL';
/** Int/Div tab — switch interest vs dividend */
intDivFilter: 'ALL' | 'SFT-015' | 'SFT-016' = 'ALL';
toastMsg = '';
toastVisible = false;
private toastTimer: any;
showTab(tab: 'dashboard' | 'person' | 'account' | 'intdiv'): void {
this.activeTab = tab;
}
// -------------------- KPIs --------------------
get totalRecords(): number {
return this.personRecords.length + this.accountRecords.length + this.intDivRecords.length;
}
get sft001Count(): number {
return this.personRecords.filter((r) => r.sftCode === 'SFT-001').length;
}
get sft002Count(): number {
return this.personRecords.filter((r) => r.sftCode === 'SFT-002').length;
}
get sft003Count(): number {
return this.accountRecords.length;
}
get sft004Count(): number {
return this.personRecords.filter((r) => r.sftCode === 'SFT-004').length;
}
get sft015Count(): number {
return this.intDivRecords.filter((r) => r.sftCode === 'SFT-015').length;
}
get sft016Count(): number {
return this.intDivRecords.filter((r) => r.sftCode === 'SFT-016').length;
}
get totalReportableAmount(): number {
return (
this.personRecords.reduce((s, r) => s + r.aggGrossReceived, 0) +
this.accountRecords.reduce((s, r) => s + r.aggGrossCashCredited, 0) +
this.intDivRecords.reduce((s, r) => s + r.amount, 0)
);
}
// -------------------- Filters --------------------
get filteredPersons(): SftPersonRecord[] {
if (this.personSftFilter === 'ALL') return this.personRecords;
return this.personRecords.filter((r) => r.sftCode === this.personSftFilter);
}
get filteredIntDiv(): SftInterestDividendRecord[] {
if (this.intDivFilter === 'ALL') return this.intDivRecords;
return this.intDivRecords.filter((r) => r.sftCode === this.intDivFilter);
}
// -------------------- Helpers --------------------
productLabel(t: string): string {
return t.replace(/_/g, ' ');
}
sftLabel(c: SftCode): string {
const map: Record<SftCode, string> = {
'SFT-001': 'Cash Drafts/PO',
'SFT-002': 'Time Deposits',
'SFT-003': 'Cash CA',
'SFT-004': 'Cash SB',
'SFT-015': 'Dividend',
'SFT-016': 'Interest Paid'
};
return map[c];
}
// -------------------- CSV exports --------------------
/** SFT-001/002/004 — Person-wise CSV (comma-separated, all 43 columns) */
exportPersonCsv(filter?: SftCode): void {
const list = filter ? this.personRecords.filter((r) => r.sftCode === filter) : this.filteredPersons;
const headers = [
'Report Serial Number',
'Original Report Serial Number',
'Person Name',
'Person Type',
'Customer Identity',
'Gender',
'Father\u2019s Name',
'PAN',
'Aadhaar Number',
'Form 60 Acknowledgment',
'Identification Type',
'Identification Number',
'Date of birth/Incorporation',
'Nationality/Country of Incorporation',
'AddressType',
'Fix Address',
'City/Town',
'PostCode',
'State',
'CountryCode',
'Business or occupation',
'Primary STD Code',
'Primary Phone No',
'Primary Mobile No',
'Secondary STD Code',
'Secondary Phone No',
'Secondary Mobile No',
'Email',
'Remarks',
'Product Type',
'Aggregate gross amount received from the person',
'Aggregate gross amount received from the person in cash',
'Aggregate gross amount paid to the person',
'TransactionSummry Remarks',
'Product Identifier',
'Last Date of Transaction',
'Amount Received From Person',
'Cash Amount Received From Person',
'Amount Paid To Person',
'Related Account Number',
'Related Institute Name',
'Related Institute Ref Number',
'Financial TransactionDtls Remarks'
];
const rows: string[][] = [headers];
if (list.length === 0) {
rows.push(['NIL Report']);
} else {
for (const r of list) {
rows.push([
r.reportSerialNumber.toString(),
r.originalReportSerialNumber.toString(),
r.personName,
r.personType,
r.customerIdentity,
r.gender,
r.fatherName,
r.pan,
r.aadhaar,
r.form60Ack,
r.identificationType,
r.identificationNumber,
r.dobOrIncorporation.replace(/-/g, '-'),
r.nationality,
r.addressType,
r.fixAddress,
r.city,
r.postCode,
r.state,
r.countryCode,
r.businessOrOccupation,
r.primaryStdCode,
r.primaryPhoneNo,
r.primaryMobileNo,
'',
'',
'',
r.email,
r.remarks,
r.productType,
r.aggGrossReceived.toString(),
r.aggGrossReceivedCash.toString(),
r.aggGrossPaid.toString(),
'',
r.productIdentifier,
r.lastDateOfTransaction,
r.amountReceivedFromPerson.toString(),
r.cashAmountReceivedFromPerson.toString(),
r.amountPaidToPerson.toString(),
r.relatedAccountNumber,
r.relatedInstituteName,
r.relatedInstituteRefNumber,
''
]);
}
}
const fileTag = filter || 'ALL';
this.saveCsv(rows, `${fileTag.replace('-', '_')}.csv`);
this.toast(`${fileTag} exported (${list.length} rows)`);
}
/** SFT-003 — Account-wise CSV with embedded Person Card */
exportAccountCsv(): void {
const headers = [
'Report Serial Number',
'Original Report Serial Number',
'Account Type',
'Account Number',
'Account Holder Name',
'Account Status',
'Branch Reference Number',
'Branch Name',
'Branch Address',
'Branch City/Town',
'Branch Postal Code',
'Branch State',
'Branch Country',
'Branch STD Code',
'Branch Phone Number',
'Branch Mobile Number',
'Branch Fax STD Code',
'Branch Fax Phone No',
'Branch Email',
'Branch Remarks',
'Aggregate gross amount credited to the account in cash',
'Aggregate gross amount debited to the account in cash',
'Amount credited to the account before demonetization',
'Amount credited to the account after demonetization',
'Account Remarks',
'Account Relationship (PC)',
'Person Name (PC)',
'Person Type (PC)',
'Customer Identity (PC)',
'Gender (PC)',
'Father\u2019s Name (PC)',
'PAN (PC)',
'Aadhaar Number (PC)',
'Form 60 Acknowledgement (PC)',
'Identification Type (PC)',
'Identification Number (PC)',
'Date of Birth/ Incorporation (PC)',
'Nationality/Country of Incorporation (PC)',
'Business or Occupation (PC)',
'Address Type (PC-L)',
'Address (PC-L)',
'City/Town (PC-L)',
'Pin Code (PC-L)',
'State (PC-L)',
'Country (PC-L)',
'Primary STD Code (PC)',
'Primary Telephone Number (PC)',
'Primary Mobile Number (PC)',
'Secondary STD Code (PC)',
'Secondary Telephone Number (PC)',
'Secondary Mobile Number (PC)',
'Email (PC)',
'Remarks (PC)'
];
const rows: string[][] = [headers];
if (this.accountRecords.length === 0) {
rows.push(['NIL Report']);
} else {
for (const r of this.accountRecords) {
rows.push([
r.reportSerialNumber.toString(),
r.originalReportSerialNumber.toString(),
r.accountType,
r.accountNumber,
r.accountHolderName,
r.accountStatus,
r.branchReferenceNumber,
r.branchName,
r.branchAddress,
r.branchCity,
r.branchPostalCode,
r.branchState,
r.branchCountry,
r.branchStdCode,
r.branchPhone,
r.branchMobile,
r.branchFaxStd,
r.branchFaxPhone,
r.branchEmail,
'',
r.aggGrossCashCredited.toString(),
r.aggGrossCashDebited.toString(),
r.amountCreditedBeforeDemonetization.toString(),
r.amountCreditedAfterDemonetization.toString(),
'',
r.pcAccountRelationship,
r.pcPersonName,
r.pcPersonType,
r.pcCustomerIdentity,
r.pcGender,
r.pcFatherName,
r.pcPan,
r.pcAadhaar,
r.pcForm60Ack,
r.pcIdentificationType,
r.pcIdentificationNumber,
r.pcDob,
r.pcNationality,
r.pcBusiness,
r.pcAddressType,
r.pcAddress,
r.pcCity,
r.pcPin,
r.pcState,
r.pcCountry,
r.pcPrimaryStd,
r.pcPrimaryPhone,
r.pcPrimaryMobile,
'',
'',
'',
r.pcEmail,
''
]);
}
}
this.saveCsv(rows, 'SFT_003.csv');
this.toast(`SFT-003 exported (${this.accountRecords.length} accounts)`);
}
/** SFT-015 / SFT-016 — Pipe-delimited */
exportIntDivCsv(filter?: 'SFT-015' | 'SFT-016'): void {
const list = filter ? this.intDivRecords.filter((r) => r.sftCode === filter) : this.filteredIntDiv;
const fileTag = filter || 'BOTH';
const lines: string[] = [];
if (filter === 'SFT-015' || (!filter && list.some((r) => r.sftCode === 'SFT-015'))) {
// Dividend file header
const headers15 = [
'RSN',
'FY',
'Name',
'PAN',
'Aadhaar',
'Address',
'Pincode',
'Mobile',
'Email',
'Dividend',
''
];
const div15 = list.filter((r) => r.sftCode === 'SFT-015');
if (filter === 'SFT-015' || div15.length > 0) {
if (filter !== 'SFT-016') {
lines.push(headers15.join('|'));
for (const r of div15) {
lines.push(
[
r.rsn.toString(),
r.fy,
r.name,
r.pan,
r.aadhaar,
r.address || '',
r.pincode || '',
r.mobile,
r.email,
r.amount.toString(),
''
].join('|')
);
}
}
}
}
if (filter === 'SFT-016' || (!filter && list.some((r) => r.sftCode === 'SFT-016'))) {
const headers16 = [
'RSN',
'FY',
'Name',
'PAN',
'Aadhaar',
'Mobile',
'Email',
'Account Number',
'Type of Account',
'Interest',
''
];
const int16 = list.filter((r) => r.sftCode === 'SFT-016');
if (filter !== 'SFT-015' && int16.length > 0) {
if (lines.length > 0) lines.push('');
lines.push(headers16.join('|'));
for (const r of int16) {
lines.push(
[
r.rsn.toString(),
r.fy,
r.name,
r.pan,
r.aadhaar,
r.mobile,
r.email,
r.accountNumber || '',
r.typeOfAccount || '',
r.amount.toString(),
''
].join('|')
);
}
}
}
const filename = filter === 'SFT-015' ? 'SFT15.csv' : filter === 'SFT-016' ? 'SFT16.csv' : 'SFT15_SFT16.csv';
this.saveText(lines.join('\n') + '\n', filename, 'text/csv');
this.toast(`${fileTag} exported (${list.length} rows)`);
}
/** Bundle: all SFT files (one per SFT code) */
exportAll(): void {
setTimeout(() => this.exportPersonCsv('SFT-001'), 0);
setTimeout(() => this.exportPersonCsv('SFT-002'), 200);
setTimeout(() => this.exportAccountCsv(), 400);
setTimeout(() => this.exportPersonCsv('SFT-004'), 600);
setTimeout(() => this.exportIntDivCsv('SFT-015'), 800);
setTimeout(() => this.exportIntDivCsv('SFT-016'), 1000);
}
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') + '\n';
this.saveText(csv, filename, 'text/csv');
}
private saveText(content: string, filename: string, mime: string): void {
const blob = new Blob([content], { type: `${mime};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);
}
}