feat(investment,npa): RBI-grounded targets, GL-tracked interest posting, date/search pickers

- Correct Investment portfolio allocation and Loan Origination product-mix targets against
  RBI's UCB SLR/Non-SLR framework and PSL circulars, with a Target Basis column distinguishing
  RBI-mandated vs board-policy rows (Excel export included).
- Expand Investment holdings dataset with more near-term maturities so the Due-Date Diary
  reflects a realistic spread across FD/Govt Security/Non-SLR/Bond.
- Add From/To period-based Interest Calculation & Post (with GL selection for both posting and
  receiving) across the Investment Interest module, FD Entry and Bond Entry screens; fix a bug
  where a posted period kept re-appearing as due since accrual wasn't tracked per-holding.
- Add a reusable DD-MM-YYYY date-picker component and roll it out across all Investment date
  fields.
- NPA Summary Report: drop SMA-0/1/2 from the stage-wise table (kept in Day-wise Overdue only).
- NPA Recovery Report: add Account Type / NPA Category / Target-vs-Actual columns, a shared
  Account Type filter driving both the account picker and the register, and a searchable
  (fixed-position, not clipped by card overflow) account select for long lists.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-02 19:26:19 +05:30
co-authored by Claude Sonnet 5
parent d7f5bfe5f5
commit ebe67cc410
20 changed files with 1401 additions and 226 deletions
@@ -264,16 +264,22 @@
<div class="pos-section-label">Record a Recovery Against an NPA Account</div>
<div class="inv-form-grid">
<label class="inv-form-field">
<span>* NPA Account</span>
<select [(ngModel)]="newRecovery.accountNo">
<option value="">-- Select --</option>
@for (a of recoveryOptions; track a.accountNo) {
<option [value]="a.accountNo">
{{ a.accountNo }} — {{ a.customerName }} ({{ stageLabel(stage(a)) }})
</option>
<span>Account Type</span>
<select [ngModel]="recoveryProductFilter" (ngModelChange)="setRecoveryProductFilter($event)">
<option value="">-- All Account Types --</option>
@for (p of recoveryProductOptions; track p) {
<option [value]="p">{{ p }}</option>
}
</select>
</label>
<label class="inv-form-field">
<span>* NPA Account</span>
<mifosx-search-select
[options]="recoveryAccountSearchOptions"
[(ngModel)]="newRecovery.accountNo"
placeholder="Search account no. or customer name…"
></mifosx-search-select>
</label>
<label class="inv-form-field">
<span>* Recovery Date</span>
<input type="text" [(ngModel)]="newRecovery.date" placeholder="DD-MM-YYYY" />
@@ -299,45 +305,66 @@
</div>
<div class="pg-card">
<div class="pos-section-label">
NPA Recovery Register — ₹{{ totalRecoveredAmount | number: '1.0-0' }} recovered against ₹{{
gnpaAmount | number: '1.0-0'
}}
GNPA outstanding
<div
class="pos-section-label"
style="display: flex; align-items: center; justify-content: space-between; gap: 8px; flex-wrap: wrap"
>
<span>
NPA Recovery Register — ₹{{ totalRecoveredAmount | number: '1.0-0' }} recovered against ₹{{
gnpaAmount | number: '1.0-0'
}}
GNPA outstanding
</span>
@if (recoveryProductFilter) {
<span class="pg-chip">Filtered: {{ recoveryProductFilter }}</span>
}
</div>
<p class="pg-sub" style="padding: 8px 16px 12px">
Target Recovery is the account's contractual due this month (EMI / interest-servicing / bullet amount) — same
basis as the Recovery Management Dashboard.
</p>
<table class="pg-table">
<thead>
<tr>
<th>Date</th>
<th>Account Type</th>
<th>NPA Category</th>
<th>Account No</th>
<th>Customer</th>
<th>Branch</th>
<th>Mode</th>
<th class="num col-in">Recovered Amount</th>
<th class="num">Target Recovery</th>
<th class="num col-in">Recovered Amount (Actual)</th>
</tr>
</thead>
<tbody>
@for (r of paged(recoveryRows); track r.accountNo + r.date + r.amount) {
<tr>
<td>{{ r.date }}</td>
<td>{{ r.productName }}</td>
<td>
<span [class]="r.npaCategoryClass">{{ r.npaCategory }}</span>
</td>
<td>
<span class="pg-chip">{{ r.accountNo }}</span>
</td>
<td>{{ r.customerName }}</td>
<td>{{ r.branchName }}</td>
<td>{{ r.mode }}</td>
<td class="num">₹{{ r.targetRecovery | number: '1.0-0' }}</td>
<td class="num col-in">₹{{ r.amount | number: '1.0-0' }}</td>
</tr>
} @empty {
<tr>
<td colspan="6" class="pg-empty">No recoveries recorded yet.</td>
<td colspan="9" class="pg-empty">No recoveries recorded yet.</td>
</tr>
}
</tbody>
<tfoot>
<tr class="cctr-total-row">
<td colspan="5">Total Recovered</td>
<td class="num">₹{{ totalRecoveredAmount | number: '1.0-0' }}</td>
<td colspan="7">Total (Target once per account · Actual across all entries shown)</td>
<td class="num">₹{{ recoveryTargetTotal | number: '1.0-0' }}</td>
<td class="num">₹{{ recoveryActualTotal | number: '1.0-0' }}</td>
</tr>
</tfoot>
</table>