/** * 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 { DeafEligibleAccount, DeafForm, DeafFormRow, ELIGIBLE_ACCOUNTS, FORM_I, FORM_II, REFUND_CLAIMS } from './deaf-data'; @Component({ selector: 'mifosx-deaf', templateUrl: './deaf.component.html', styleUrls: ['./deaf.component.scss'], imports: [ ...STANDALONE_SHARED_IMPORTS, FaIconComponent, FormsModule ] }) export class DeafComponent { /** Active tab */ activeTab: 'dashboard' | 'form1' | 'form2' | 'refund' | 'eligible' = 'dashboard'; formI: DeafForm = FORM_I; formII: DeafForm = FORM_II; refund = REFUND_CLAIMS; accounts: DeafEligibleAccount[] = ELIGIBLE_ACCOUNTS; /** Eligible-list filters */ filterStatus: '' | DeafEligibleAccount['status'] = ''; filterBranch = ''; filterBucket: '' | DeafEligibleAccount['bucket'] = ''; /** Toast */ toastMsg = ''; toastVisible = false; private toastTimer: any; // -------------------- Tabs -------------------- showTab(tab: 'dashboard' | 'form1' | 'form2' | 'refund' | 'eligible'): void { this.activeTab = tab; } // -------------------- KPIs -------------------- /** Total amount currently with the DEAF Fund (closing balance from Form II row 6) */ get totalWithFund(): number { return this.formII.rows[5]?.totalAmount || 0; } get totalAccountsWithFund(): number { return this.formII.rows[5]?.totalAccounts || 0; } get transferredThisMonth(): number { return this.formII.rows[2]?.totalAmount || 0; } get accountsTransferredThisMonth(): number { return this.formII.rows[2]?.totalAccounts || 0; } get refundedThisCycle(): number { return this.refund.rows.reduce((s, r) => s + r.grandTotalAmount, 0); } get refundAccountsThisCycle(): number { return this.refund.rows.reduce((s, r) => s + r.grandTotalAccounts, 0); } get accountsCrossing10Y(): number { return this.accounts.filter((a) => a.status === 'eligible-this-month').length; } get pendingClaims(): number { return this.accounts.filter((a) => a.status === 'claim-pending').length; } // -------------------- Eligible filter -------------------- get filteredAccounts(): DeafEligibleAccount[] { const branch = this.filterBranch.trim().toLowerCase(); return this.accounts.filter((a) => { if (this.filterStatus && a.status !== this.filterStatus) return false; if (this.filterBucket && a.bucket !== this.filterBucket) return false; if (branch && !a.branch.toLowerCase().includes(branch)) return false; return true; }); } resetFilters(): void { this.filterStatus = ''; this.filterBranch = ''; this.filterBucket = ''; } statusLabel(s: DeafEligibleAccount['status']): string { switch (s) { case 'eligible-this-month': return 'Eligible'; case 'transferred': return 'Transferred'; case 'claim-pending': return 'Claim Pending'; case 'refunded': return 'Refunded'; } } // -------------------- Excel export -------------------- /** Builds Form I / Form II rows as a 2-D array — same layout as the RBI return */ private buildFormRows(form: DeafForm, label: 'Form I' | 'Form II'): string[][] { const rows: string[][] = []; rows.push([ label, 'Monthly Return of Unclaimed Deposits Transferred to DEAF Fund' ]); rows.push([]); rows.push([ 'Name of the Bank', form.bankName ]); rows.push([ 'Bank DEAF Code allotted by RBI', form.bankDeafCode ]); rows.push([ 'If remitted through Sponsor Bank', form.remittedThroughSponsor ]); rows.push([ 'Name of the Sponsor Bank', form.sponsorBankName ]); rows.push([ 'Month / Year', form.monthYear ]); rows.push([ 'Date of Transfer to the Fund', form.dateOfTransfer ]); rows.push([]); rows.push(['Particulars (Amount in Rupees)']); rows.push([ 'Sr.No', 'Particulars', 'IBD No. of A/c', 'IBD Amount', 'NIBD No. of A/c', 'NIBD Amount', 'OC No. of A/c', 'OC Amount', 'Total No. of A/c', 'Total Amount' ]); for (const r of form.rows) { rows.push([ r.sl.toString(), r.particulars, r.ibdAccounts.toString(), r.ibdAmount.toFixed(2), r.nibdAccounts.toString(), r.nibdAmount.toFixed(2), r.ocAccounts.toString(), r.ocAmount.toFixed(2), r.totalAccounts.toString(), r.totalAmount.toFixed(2) ]); } rows.push([]); rows.push([ 'Authorised Officer', form.officer ]); rows.push([ 'Designation', form.officerDesignation ]); rows.push([ 'Concurrent Auditor', form.auditor ]); rows.push([ 'Auditor Address', form.auditorAddress ]); return rows; } private buildRefundRows(): string[][] { const c = this.refund; const rows: string[][] = []; rows.push([ 'Annex 4', 'Monthly Return claiming refund from DEA Fund' ]); rows.push([]); rows.push([ 'Name of the Bank', c.bankName ]); rows.push([ 'Bank DEAF Code allotted by RBI', c.bankDeafCode ]); rows.push([ 'Current A/c with RBI', `${c.currentAcWithRbi} via ${c.sponsorBank}` ]); rows.push([ 'Month / Year', `${c.month} ${c.year}` ]); rows.push([]); rows.push(['Details of claims made during the month (Amount in Rupees)']); rows.push([ 'Sr.No', 'IBD Principal', 'IBD Interest', 'IBD Total', 'NIBD No. of A/c', 'NIBD Amount', 'OC No. of A/c', 'OC Amount', 'Grand Total No. of A/c', 'Grand Total Amount' ]); for (const r of c.rows) { rows.push([ r.slNo.toString(), r.ibdPrincipal.toFixed(2), r.ibdInterest.toFixed(2), r.ibdTotal.toFixed(2), r.nibdAccounts.toString(), r.nibdAmount.toFixed(2), r.ocAccounts.toString(), r.ocAmount.toFixed(2), r.grandTotalAccounts.toString(), r.grandTotalAmount.toFixed(2) ]); } rows.push([]); rows.push([ 'Note', 'No individual customer/depositor wise details should be furnished.' ]); rows.push([ 'First Authorised Signatory', `${c.firstSignatory} (${c.firstDesignation})` ]); rows.push([ 'Second Authorised Signatory', `${c.secondSignatory} (${c.secondDesignation})` ]); rows.push([ 'Concurrent Auditor', c.auditor ]); return rows; } private buildEligibleRows(): string[][] { const rows: string[][] = []; rows.push(['DEAF — Accounts Register']); rows.push([]); rows.push([ 'A/c No', 'Customer Name', 'A/c Type', 'Branch', 'Opened', 'Last Txn', 'Inactive (yrs)', 'Bucket', 'Principal', 'Interest', 'Total', 'Status', 'Transferred On' ]); for (const a of this.filteredAccounts) { rows.push([ a.acNo, a.customerName, a.acType, a.branch, a.openedDate, a.lastTxnDate, a.inactiveYears.toString(), a.bucket, a.principal.toFixed(2), a.interest.toFixed(2), a.total.toFixed(2), this.statusLabel(a.status), a.transferredOn || '' ]); } return rows; } exportFormI(): void { this.saveTsv(this.buildFormRows(this.formI, 'Form I'), `DEAF-FormI-${this.fileTag(this.formI.monthYear)}.xls`); this.toast('Form I downloaded'); } exportFormII(): void { this.saveTsv(this.buildFormRows(this.formII, 'Form II'), `DEAF-FormII-${this.fileTag(this.formII.monthYear)}.xls`); this.toast('Form II downloaded'); } exportRefund(): void { this.saveTsv(this.buildRefundRows(), `DEAF-Annex4-${this.refund.month}-${this.refund.year}.xls`); this.toast('Annex 4 (Refund Claim) downloaded'); } exportEligible(): void { this.saveTsv(this.buildEligibleRows(), `DEAF-Accounts-${this.dateStamp()}.xls`); this.toast(`${this.filteredAccounts.length} accounts exported`); } private saveTsv(rows: string[][], filename: string): void { const tsv = rows.map((r) => r.map((v) => (v || '').replace(/\t/g, ' ')).join('\t')).join('\n'); const blob = new Blob([tsv], { type: 'application/vnd.ms-excel;charset=utf-8' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; a.click(); URL.revokeObjectURL(url); } private fileTag(monthYear: string): string { return monthYear.replace('/', '-'); } private dateStamp(): string { const d = new Date(); const dd = String(d.getDate()).padStart(2, '0'); const mm = String(d.getMonth() + 1).padStart(2, '0'); return `${dd}${mm}${d.getFullYear()}`; } /** Coerce a row's raw amounts for display — handles negatives in Form I row 5 */ amountClass(r: DeafFormRow): string { if (r.sl === 5 && r.totalAmount < 0) return 'amt-neg'; if (r.sl === 6) return 'amt-bold'; return ''; } toast(msg: string): void { this.toastMsg = msg; this.toastVisible = true; if (this.toastTimer) clearTimeout(this.toastTimer); this.toastTimer = setTimeout(() => (this.toastVisible = false), 2800); } }