/** * 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 = { '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); } }