feat(npa): add Export to Excel across all 9 NPA reports
Every report screen (Interest, Status, Recovery, Summary, Account Detail, Movement, Provision, Probable NPA, SMA) now has an Export to Excel button that builds a workbook from the same derived getters the on-screen tables use, reusing the shared cashier-export.ts builder.
This commit is contained in:
@@ -38,6 +38,9 @@
|
||||
<input type="text" [(ngModel)]="asOnDate" placeholder="DD-MM-YYYY" />
|
||||
</label>
|
||||
<button type="button" class="pg-btn pg-btn--primary" (click)="generate()">Generate</button>
|
||||
<button type="button" class="pg-btn pg-btn--ghost" [disabled]="generating" (click)="exportExcel()">
|
||||
{{ generating ? 'Exporting…' : 'Export to Excel' }}
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
@@ -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<typeof setTimeout> | 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<void> {
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user