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;