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>
467 lines
14 KiB
TypeScript
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);
|
|
}
|
|
}
|