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:
co-authored by
Claude Opus 4.7
parent
634003a75c
commit
12bfc81219
@@ -0,0 +1,466 @@
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user