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" />
|
<input type="text" [(ngModel)]="asOnDate" placeholder="DD-MM-YYYY" />
|
||||||
</label>
|
</label>
|
||||||
<button type="button" class="pg-btn pg-btn--primary" (click)="generate()">Generate</button>
|
<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>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { FormsModule } from '@angular/forms';
|
|||||||
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
|
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
|
||||||
import { Subscription } from 'rxjs';
|
import { Subscription } from 'rxjs';
|
||||||
import { AuthenticationService } from '../../core/authentication/authentication.service';
|
import { AuthenticationService } from '../../core/authentication/authentication.service';
|
||||||
|
import { ExportSheetSpec, exportWorkbook } from '../../cashier/cashier-export';
|
||||||
import {
|
import {
|
||||||
LOAN_ACCOUNTS,
|
LOAN_ACCOUNTS,
|
||||||
BRANCHES,
|
BRANCHES,
|
||||||
@@ -127,6 +128,9 @@ export class NpaReportsComponent implements OnInit, OnDestroy {
|
|||||||
mode: 'Cash Deposit'
|
mode: 'Cash Deposit'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** True while an Export to Excel workbook is being built — disables the button to prevent double-clicks. */
|
||||||
|
generating = false;
|
||||||
|
|
||||||
toastMsg = '';
|
toastMsg = '';
|
||||||
toastVisible = false;
|
toastVisible = false;
|
||||||
private toastTimer: ReturnType<typeof setTimeout> | undefined;
|
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}.`);
|
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 {
|
private toast(msg: string): void {
|
||||||
this.toastMsg = msg;
|
this.toastMsg = msg;
|
||||||
this.toastVisible = true;
|
this.toastVisible = true;
|
||||||
|
|||||||
Reference in New Issue
Block a user