diff --git a/src/app/npa/npa-reports/npa-reports.component.html b/src/app/npa/npa-reports/npa-reports.component.html index 0c75cb956..2b6e76c22 100644 --- a/src/app/npa/npa-reports/npa-reports.component.html +++ b/src/app/npa/npa-reports/npa-reports.component.html @@ -38,6 +38,9 @@ + diff --git a/src/app/npa/npa-reports/npa-reports.component.ts b/src/app/npa/npa-reports/npa-reports.component.ts index 5d6eb24e5..887737dda 100644 --- a/src/app/npa/npa-reports/npa-reports.component.ts +++ b/src/app/npa/npa-reports/npa-reports.component.ts @@ -12,6 +12,7 @@ import { FormsModule } from '@angular/forms'; import { ActivatedRoute, Router, RouterModule } from '@angular/router'; import { Subscription } from 'rxjs'; import { AuthenticationService } from '../../core/authentication/authentication.service'; +import { ExportSheetSpec, exportWorkbook } from '../../cashier/cashier-export'; import { LOAN_ACCOUNTS, BRANCHES, @@ -127,6 +128,9 @@ export class NpaReportsComponent implements OnInit, OnDestroy { mode: 'Cash Deposit' }; + /** True while an Export to Excel workbook is being built — disables the button to prevent double-clicks. */ + generating = false; + toastMsg = ''; toastVisible = false; private toastTimer: ReturnType | undefined; @@ -479,6 +483,598 @@ export class NpaReportsComponent implements OnInit, OnDestroy { this.toast(`${this.reportLabel(this.activeReport)} generated as on ${this.asOnDate}.`); } + // -------------------- Export to Excel (whichever report is currently on screen) -------------------- + async exportExcel(): Promise { + this.generating = true; + try { + const sheets = this.buildExportSheets(); + await exportWorkbook(sheets, `${this.reportLabel(this.activeReport)}-${this.scopeName}-${this.asOnDate}`); + } catch (err) { + console.error('NPA export failed', err); + } finally { + this.generating = false; + } + } + + /** Builds the workbook for whichever report is active — every sheet is built from the same + * derived getters the on-screen tables use, so the export can never disagree with the screen. */ + private buildExportSheets(): ExportSheetSpec[] { + const subtitle = `Scope: ${this.scopeName} · As On: ${this.asOnDate}`; + + switch (this.activeReport) { + case 'interest': + return [ + { + sheetName: 'NPA Interest', + title: 'NPA Interest — Income Reversed on Non-Performing Accounts', + subtitle, + headers: [ + 'Account No', + 'Customer', + 'Branch', + 'Product', + 'Stage', + 'Outstanding', + 'Rate %', + 'Overdue Days', + 'Interest Reversed' + ], + widths: [ + 16, + 22, + 16, + 16, + 14, + 16, + 10, + 12, + 18 + ], + rows: [ + ...this.npaAccounts.map((a) => [ + a.accountNo, + a.customerName, + a.branchName, + a.productName, + this.stageLabel(this.stage(a)), + a.outstandingBalance, + a.interestRate, + a.overdueDays, + this.interestRev(a) + ]), + [ + `Total (${this.npaAccounts.length} accounts)`, + '', + '', + '', + '', + '', + '', + '', + this.sumInterest(this.npaAccounts) + ] + ] + } + ]; + + case 'status': + return [ + { + sheetName: 'NPA Status', + title: 'NPA Status Report — Full Non-Performing Book', + subtitle, + headers: [ + 'Account No', + 'Customer', + 'Branch', + 'Product', + 'Secured', + 'Outstanding', + 'Overdue Amount', + 'Overdue Days', + 'NPA Since', + 'Stage', + 'Restructured', + 'Legal Action', + 'Provision Required' + ], + widths: [ + 16, + 22, + 16, + 16, + 10, + 16, + 16, + 12, + 12, + 14, + 12, + 22, + 18 + ], + rows: this.npaAccounts.map((a) => [ + a.accountNo, + a.customerName, + a.branchName, + a.productName, + a.secured ? 'Yes' : 'No', + a.outstandingBalance, + a.overdueAmount, + a.overdueDays, + this.npaSince(a), + this.stageLabel(this.stage(a)), + a.isRestructured ? 'Yes' : 'No', + this.legalActionLabel(a.legalActionStatus), + this.provisionReq(a) + ]) + } + ]; + + case 'recovery': + return [ + { + sheetName: 'NPA Recovery', + title: 'NPA Recovery Register', + subtitle, + headers: [ + 'Date', + 'Account No', + 'Customer', + 'Branch', + 'Mode', + 'Recovered Amount' + ], + widths: [ + 12, + 16, + 22, + 16, + 18, + 18 + ], + rows: [ + ...this.recoveryRows.map((r) => [ + r.date, + r.accountNo, + r.customerName, + r.branchName, + r.mode, + r.amount + ]), + [ + 'Total Recovered', + '', + '', + '', + '', + this.totalRecoveredAmount + ] + ] + } + ]; + + case 'summary': { + const sheets: ExportSheetSpec[] = [ + { + sheetName: 'Stage Summary', + title: 'NPA Summary Report — Stage-wise', + subtitle, + headers: [ + 'Classification', + 'Accounts', + 'Outstanding', + 'Provision Required' + ], + widths: [ + 22, + 12, + 18, + 18 + ], + rows: [ + ...this.stageSummary.map((row) => [ + row.label, + row.count, + row.outstanding, + row.provisionReq + ]), + [ + 'Total', + this.grandTotalAccounts, + this.grandTotalOutstanding, + this.grandTotalProvision + ] + ] + } + ]; + if (this.isHeadOffice && !this.branchFilter) { + sheets.push({ + sheetName: 'Branch Summary', + title: 'Branch-wise NPA Summary — All Branches', + subtitle: `As On: ${this.asOnDate}`, + headers: [ + 'Branch', + 'Total Accounts', + 'Total Advances', + 'NPA Accounts', + 'GNPA Outstanding', + 'Provision Required', + 'GNPA %' + ], + widths: [ + 20, + 14, + 18, + 14, + 18, + 18, + 10 + ], + rows: this.branchSummary.map((row) => [ + row.branchName, + row.totalAccounts, + row.totalAdvances, + row.npaCount, + row.npaOutstanding, + row.provisionReq, + Number(row.gnpaRatio.toFixed(2)) + ]) + }); + } + return sheets; + } + + case 'account-detail': { + const a = this.selectedAccount; + const detailRows: (string | number | null)[][] = a + ? [ + [ + 'Account No', + a.accountNo + ], + [ + 'Customer', + a.customerName + ], + [ + 'Branch', + a.branchName + ], + [ + 'Product', + a.productName + ], + [ + 'Secured', + a.secured ? 'Yes' : 'No' + ], + [ + 'Guarantor', + a.guarantorName + ], + [ + 'Sanctioned Amount', + a.sanctionedAmount + ], + [ + 'Disbursement Date', + a.disbursementDate + ], + [ + 'Interest Rate %', + a.interestRate + ], + [ + 'Outstanding Balance', + a.outstandingBalance + ], + [ + 'Overdue Amount', + a.overdueAmount + ], + [ + 'Overdue Days', + a.overdueDays + ], + [ + 'NPA Since', + this.npaSince(a) + ], + [ + 'Restructured', + a.isRestructured ? 'Yes' : 'No' + ], + [ + 'Last Payment Date', + a.lastPaymentDate + ], + [ + 'Collateral Value', + a.collateralValue + ], + [ + 'Stage', + this.stageLabel(this.stage(a)) + ], + [ + 'Provisioning Rate %', + this.provisionRate(a) + ], + [ + 'Provision Required', + this.provisionReq(a) + ], + [ + 'Provision Held', + this.provisionHeldAmt(a) + ], + [ + 'Interest Reversed', + this.interestRev(a) + ], + [ + 'Total Recovered', + this.recovered(a) + ], + [ + 'Movement (vs Last Quarter)', + this.movement(a) + ], + [ + 'NPA Reason', + a.npaReason + ], + [ + 'Recovery Officer', + a.recoveryOfficer + ], + [ + 'Legal Action Status', + this.legalActionLabel(a.legalActionStatus) + ] + ] + : []; + return [ + { + sheetName: 'Account Detail', + title: a ? `${a.accountNo} — ${a.customerName}` : 'NPA Account Detail Report', + subtitle, + headers: [ + 'Field', + 'Value' + ], + widths: [ + 26, + 32 + ], + rows: detailRows + }, + { + sheetName: 'Recovery History', + title: a ? `Recovery History — ${a.accountNo}` : 'Recovery History', + subtitle, + headers: [ + 'Date', + 'Mode', + 'Amount' + ], + widths: [ + 14, + 20, + 18 + ], + rows: (a?.recoveries || []).map((r) => [ + r.date, + r.mode, + r.amount + ]) + } + ]; + } + + case 'movement': + return [ + { + sheetName: 'NPA Movement', + title: 'Classification Movement — Current Quarter vs Previous Quarter', + subtitle, + headers: [ + 'Account No', + 'Customer', + 'Branch', + 'Previous Stage', + 'Current Stage', + 'Movement', + 'Outstanding' + ], + widths: [ + 16, + 22, + 16, + 16, + 16, + 26, + 16 + ], + rows: this.movedAccounts.map((a) => [ + a.accountNo, + a.customerName, + a.branchName, + this.stageLabel(this.prevStage(a)), + this.stageLabel(this.stage(a)), + this.movement(a), + a.outstandingBalance + ]) + } + ]; + + case 'provision': + return [ + { + sheetName: 'NPA Provision', + title: 'Provision Held vs Required — Full Loan Book', + subtitle, + headers: [ + 'Account No', + 'Customer', + 'Branch', + 'Stage', + 'Outstanding', + 'Rate %', + 'Provision Required', + 'Provision Held', + 'Shortfall / (Excess)' + ], + widths: [ + 16, + 22, + 16, + 14, + 16, + 10, + 18, + 16, + 18 + ], + rows: [ + ...this.provisionRows.map((a) => [ + a.accountNo, + a.customerName, + a.branchName, + this.stageLabel(this.stage(a)), + a.outstandingBalance, + this.provisionRate(a), + this.provisionReq(a), + this.provisionHeldAmt(a), + this.shortfall(a) + ]), + [ + `Total (${this.provisionRows.length} accounts)`, + '', + '', + '', + '', + '', + this.provisionRequiredTotal, + this.provisionHeldTotal, + this.provisionShortfallTotal + ] + ] + } + ]; + + case 'probable': + return [ + { + sheetName: 'Probable NPA', + title: 'Probable NPA — SMA-2 Accounts Likely to Slip Next Quarter', + subtitle, + headers: [ + 'Account No', + 'Customer', + 'Branch', + 'Product', + 'Outstanding', + 'Overdue Amount', + 'Overdue Days', + 'Last Payment', + 'Follow-up' + ], + widths: [ + 16, + 22, + 16, + 16, + 16, + 16, + 12, + 14, + 12 + ], + rows: [ + ...this.probableNpaAccounts.map((a) => [ + a.accountNo, + a.customerName, + a.branchName, + a.productName, + a.outstandingBalance, + a.overdueAmount, + a.overdueDays, + a.lastPaymentDate, + a.overdueDays > 75 ? 'Urgent' : 'Monitor' + ]), + [ + `Total (${this.probableNpaAccounts.length} accounts)`, + '', + '', + '', + this.sumOutstanding(this.probableNpaAccounts), + '', + '', + '', + '' + ] + ] + } + ]; + + case 'sma': + return [ + { + sheetName: 'SMA Report', + title: 'Special Mention Accounts — SMA-0 / SMA-1 / SMA-2', + subtitle, + headers: [ + 'Account No', + 'Customer', + 'Branch', + 'Product', + 'Stage', + 'Outstanding', + 'Overdue Amount', + 'Overdue Days' + ], + widths: [ + 16, + 22, + 16, + 16, + 14, + 16, + 16, + 12 + ], + rows: [ + ...this.smaAccounts.map((a) => [ + a.accountNo, + a.customerName, + a.branchName, + a.productName, + this.stageLabel(this.stage(a)), + a.outstandingBalance, + a.overdueAmount, + a.overdueDays + ]), + [ + `Total (${this.smaAccounts.length} accounts)`, + '', + '', + '', + '', + this.sumOutstanding(this.smaAccounts), + this.sumOverdue(this.smaAccounts), + '' + ] + ] + } + ]; + + default: + return []; + } + } + private toast(msg: string): void { this.toastMsg = msg; this.toastVisible = true;