feat(npa): add Undebited Interest Recovery Report, fix note text width

Adds a 10th NPA report tracking memorandum "undebited interest" per IRAC
norms (accrued vs cash-recovered vs balance), wired into Export to Excel
and the NPA mega-menu. Also fixes a narrow-width text wrap on the report's
description note (new .npa-report-note class overriding pg-sub's 560px cap).
This commit is contained in:
2026-08-02 08:34:03 +05:30
parent 90beea64cb
commit 27cdaf7fc2
5 changed files with 220 additions and 10 deletions
@@ -784,7 +784,97 @@
</div>
}
<!-- ===================== 08 PROBABLE NPA ===================== -->
<!-- ===================== 08 UNDEBITED INTEREST RECOVERY REPORT ===================== -->
@if (activeReport === 'undebited-interest') {
<div class="pg-card">
<div class="pos-section-label">
Undebited Interest Recovery Report — Memorandum Interest Held Outside the Loan Account (IRAC Norms)
</div>
<p class="pg-sub npa-report-note">
Once an account turns NPA, interest is no longer debited to the borrower's loan account or recognised as income
— it is tracked here as memorandum "undebited interest" and recognised only when actually recovered in cash.
Cash recovered is appropriated to this bucket first; any balance beyond it reduces principal.
</p>
<div class="pg-table-scroll">
<table class="pg-table">
<thead>
<tr>
<th>Account No</th>
<th>Customer</th>
<th>Branch</th>
<th>Stage</th>
<th>NPA Since</th>
<th class="num">Days NPA</th>
<th class="num">Rate</th>
<th class="num col-out">Undebited Interest Accrued</th>
<th class="num col-in">Interest Recovered (Cash)</th>
<th class="num">Undebited Interest Balance</th>
</tr>
</thead>
<tbody>
@for (a of paged(npaAccounts); track a.accountNo) {
<tr>
<td>
<span class="pg-chip">{{ a.accountNo }}</span>
</td>
<td>{{ a.customerName }}</td>
<td>{{ a.branchName }}</td>
<td>
<span [class]="badgeClass(stage(a))">{{ stageLabel(stage(a)) }}</span>
</td>
<td>{{ npaSince(a) }}</td>
<td class="num">{{ daysSinceNpaOf(a) }}</td>
<td class="num">{{ a.interestRate }}%</td>
<td class="num col-out">₹{{ interestRev(a) | number: '1.0-0' }}</td>
<td class="num col-in">₹{{ interestRecoveredCashOf(a) | number: '1.0-0' }}</td>
<td class="num">₹{{ undebitedInterestBalanceOf(a) | number: '1.0-0' }}</td>
</tr>
} @empty {
<tr>
<td colspan="10" class="pg-empty">No NPA accounts in this branch.</td>
</tr>
}
</tbody>
<tfoot>
<tr class="cctr-total-row">
<td colspan="7">Total ({{ npaAccounts.length }} accounts)</td>
<td class="num">₹{{ undebitedInterestAccruedTotal | number: '1.0-0' }}</td>
<td class="num">₹{{ interestRecoveredCashTotal | number: '1.0-0' }}</td>
<td class="num">₹{{ undebitedInterestBalanceTotal | number: '1.0-0' }}</td>
</tr>
</tfoot>
</table>
</div>
@if (npaAccounts.length > pageSize) {
<div class="pg-pagination">
<span class="pg-pagination__info"
>Showing {{ rangeStart(npaAccounts) }}–{{ rangeEnd(npaAccounts) }} of {{ npaAccounts.length }}</span
>
<div class="pg-pagination__controls">
<button
type="button"
class="pg-btn pg-btn--ghost"
[disabled]="currentPage === 1"
(click)="goToPage(currentPage - 1, npaAccounts)"
>
‹ Prev
</button>
<span class="pg-pagination__page">Page {{ currentPage }} of {{ pageCount(npaAccounts) }}</span>
<button
type="button"
class="pg-btn pg-btn--ghost"
[disabled]="currentPage === pageCount(npaAccounts)"
(click)="goToPage(currentPage + 1, npaAccounts)"
>
Next ›
</button>
</div>
</div>
}
</div>
}
<!-- ===================== 09 PROBABLE NPA ===================== -->
@if (activeReport === 'probable') {
<div class="pg-card">
<div class="pos-section-label">
@@ -871,7 +961,7 @@
</div>
}
<!-- ===================== 09 SMA REPORT ===================== -->
<!-- ===================== 10 SMA REPORT ===================== -->
@if (activeReport === 'sma') {
<div class="pg-card">
<div class="pos-section-label">Special Mention Accounts — SMA-0 (1–30d) / SMA-1 (31–60d) / SMA-2 (61–90d)</div>
@@ -36,6 +36,9 @@ import {
movementLabel,
stageBadgeClass,
npaSinceDate,
daysSinceNpa,
interestRecoveredCash,
undebitedInterestBalance,
todayDDMMYYYY,
LegalActionStatus,
LEGAL_ACTION_LABELS
@@ -268,6 +271,18 @@ export class NpaReportsComponent implements OnInit, OnDestroy {
return npaSinceDate(a);
}
daysSinceNpaOf(a: LoanAccount): number {
return daysSinceNpa(a);
}
interestRecoveredCashOf(a: LoanAccount): number {
return interestRecoveredCash(a);
}
undebitedInterestBalanceOf(a: LoanAccount): number {
return undebitedInterestBalance(a);
}
legalActionLabel(status: LegalActionStatus): string {
return LEGAL_ACTION_LABELS[status];
}
@@ -359,6 +374,19 @@ export class NpaReportsComponent implements OnInit, OnDestroy {
return this.npaAccounts.reduce((s, a) => s + this.recovered(a), 0);
}
// -------------------- Undebited Interest Recovery Report --------------------
get undebitedInterestAccruedTotal(): number {
return this.npaAccounts.reduce((s, a) => s + this.interestRev(a), 0);
}
get interestRecoveredCashTotal(): number {
return this.npaAccounts.reduce((s, a) => s + this.interestRecoveredCashOf(a), 0);
}
get undebitedInterestBalanceTotal(): number {
return this.npaAccounts.reduce((s, a) => s + this.undebitedInterestBalanceOf(a), 0);
}
get restructuredAccounts(): LoanAccount[] {
return this.allAccounts.filter((a) => a.isRestructured);
}
@@ -964,6 +992,65 @@ export class NpaReportsComponent implements OnInit, OnDestroy {
}
];
case 'undebited-interest':
return [
{
sheetName: 'Undebited Interest',
title: 'Undebited Interest Recovery Report — Memorandum Interest (IRAC Norms)',
subtitle,
headers: [
'Account No',
'Customer',
'Branch',
'Stage',
'NPA Since',
'Days NPA',
'Rate %',
'Undebited Interest Accrued',
'Interest Recovered (Cash)',
'Undebited Interest Balance'
],
widths: [
16,
22,
16,
14,
12,
10,
10,
22,
20,
22
],
rows: [
...this.npaAccounts.map((a) => [
a.accountNo,
a.customerName,
a.branchName,
this.stageLabel(this.stage(a)),
this.npaSince(a),
this.daysSinceNpaOf(a),
a.interestRate,
this.interestRev(a),
this.interestRecoveredCashOf(a),
this.undebitedInterestBalanceOf(a)
]),
[
`Total (${this.npaAccounts.length} accounts)`,
'',
'',
'',
'',
'',
'',
this.undebitedInterestAccruedTotal,
this.interestRecoveredCashTotal,
this.undebitedInterestBalanceTotal
]
]
}
];
case 'probable':
return [
{