feat: add investment reports, bond components, dashboard, and service implementation

This commit is contained in:
2026-10-23 16:49:50 +05:30
parent 731ab5a0e0
commit 8e98077158
6 changed files with 865 additions and 497 deletions
@@ -12,34 +12,50 @@
<div class="pg-eyebrow">TREASURY · INVESTMENT</div>
<h1 class="pg-title">Investment Portfolio Dashboard</h1>
<p class="pg-sub">
Consolidated position across FD, Govt Security, Non-SLR, Bonds, Mutual Fund and Share holdings as on
What the bank holds today across Govt Securities, FDs with banks, Non-SLR, Bonds, Mutual Funds and Shares, as on
{{ asOnDate }}.
</p>
</div>
<div class="pg-toolbar">
<button type="button" class="pg-btn pg-btn--ghost" [disabled]="generating" (click)="exportExcel()">
<button type="button" class="pg-btn pg-btn--ghost" [disabled]="loading" (click)="load()">
{{ loading ? 'Loading…' : 'Refresh' }}
</button>
<button type="button" class="pg-btn pg-btn--ghost" [disabled]="generating || loading" (click)="exportExcel()">
{{ generating ? 'Exporting…' : 'Export to Excel' }}
</button>
</div>
</header>
@if (error) {
<div class="pg-card">
<p class="inv-hint inv-hint--bad">{{ error }}</p>
</div>
}
<div class="inv-kpis">
<div class="inv-kpi">
<span class="inv-kpi__label">Total Holdings</span>
<span class="inv-kpi__label">Holdings</span>
<span class="inv-kpi__value">{{ totalHoldings }}</span>
<span class="inv-kpi__sub">{{ activeCount }} active · {{ maturedCount }} matured</span>
<span class="inv-kpi__sub"
>{{ maturingSoon.length }} due within 90 days{{ overdueCount ? ' (' + overdueCount + ' overdue)' : '' }}</span
>
</div>
<div class="inv-kpi">
<span class="inv-kpi__label">Total Invested (Face Value)</span>
<span class="inv-kpi__value">₹{{ totalFaceValue | number: '1.2-2' }}</span>
<span class="inv-kpi__label">Total Invested (Book Value)</span>
<span class="inv-kpi__value">₹{{ totalBookValue | number: '1.2-2' }}</span>
<span class="inv-kpi__sub">Face value ₹{{ totalFaceValue | number: '1.2-2' }}</span>
</div>
<div class="inv-kpi">
<span class="inv-kpi__label">Accrued Interest / Income</span>
<span class="inv-kpi__label">Interest Receivable</span>
<span class="inv-kpi__value col-in">₹{{ totalAccruedInterest | number: '1.2-2' }}</span>
<span class="inv-kpi__sub"
>Earned till today, not received · ₹{{ totalProvisioned | number: '1.2-2' }} provisioned</span
>
</div>
<div class="inv-kpi">
<span class="inv-kpi__label">Projected Value at Maturity</span>
<span class="inv-kpi__label">Value at Maturity</span>
<span class="inv-kpi__value">₹{{ totalMaturityValue | number: '1.2-2' }}</span>
<span class="inv-kpi__sub">Securities at face value, FDs with interest</span>
</div>
</div>
@@ -51,7 +67,9 @@
<th>Instrument Type</th>
<th class="num">Holdings</th>
<th class="num">Face Value</th>
<th class="num col-in">Accrued Interest</th>
<th class="num">Book Value</th>
<th class="num">Share</th>
<th class="num col-in">Interest Receivable</th>
<th class="num col-net">Maturity Value</th>
<th></th>
</tr>
@@ -62,6 +80,8 @@
<td>{{ s.label }}</td>
<td class="num">{{ s.count }}</td>
<td class="num">₹{{ s.faceValue | number: '1.2-2' }}</td>
<td class="num">₹{{ s.bookValue | number: '1.2-2' }}</td>
<td class="num">{{ s.sharePct | number: '1.1-1' }}%</td>
<td class="num col-in">₹{{ s.accruedInterest | number: '1.2-2' }}</td>
<td class="num col-net">₹{{ s.maturityValue | number: '1.2-2' }}</td>
<td>
@@ -75,6 +95,8 @@
<td>Total</td>
<td class="num">{{ totalHoldings }}</td>
<td class="num">₹{{ totalFaceValue | number: '1.2-2' }}</td>
<td class="num">₹{{ totalBookValue | number: '1.2-2' }}</td>
<td class="num">{{ totalBookValue > 0 ? '100%' : '—' }}</td>
<td class="num">₹{{ totalAccruedInterest | number: '1.2-2' }}</td>
<td class="num">₹{{ totalMaturityValue | number: '1.2-2' }}</td>
<td></td>
@@ -84,93 +106,70 @@
</div>
<div class="pg-card">
<div class="pos-section-label">Portfolio Allocation — Target vs Actual</div>
<p class="pg-sub">
Govt Security, Non-SLR, Mutual Fund and Share targets trace to RBI's SLR floor (18% of NDTL) and Non-SLR ceiling
(≤10% of deposits) for Urban Co-operative Banks; Fixed Deposit and Bond are board-approved policy choices within
the room that framework leaves.
</p>
<table class="pg-table">
<thead>
<tr>
<th>Instrument Type</th>
<th>Target Basis</th>
<th class="num">Target %</th>
<th class="num">Actual %</th>
<th class="num">Actual Value</th>
<th class="num">Variance</th>
<th>Status</th>
</tr>
</thead>
<tbody>
@for (a of allocationVsTarget; track a.type) {
<div class="pos-section-label">RBI Limits</div>
@if (limitNote) {
<p class="inv-hint inv-hint--bad">{{ limitNote }}</p>
}
@if (limits.length) {
<table class="pg-table">
<thead>
<tr>
<td>{{ a.label }}</td>
<td>
<span class="inv-badge" [class.inv-badge--matured]="a.isRbiLinked">
{{ a.isRbiLinked ? 'RBI SLR / Non-SLR Norm' : 'Board Policy' }}
</span>
</td>
<td class="num">{{ a.targetPct }}%</td>
<td class="num">{{ a.actualPct }}%</td>
<td class="num">₹{{ a.actualValue | number: '1.2-2' }}</td>
<td
class="num"
[class.col-out]="a.statusLabel !== 'Within Target'"
[class.col-in]="a.statusLabel === 'Within Target'"
>
{{ a.variancePct > 0 ? '+' : '' }}{{ a.variancePct }}%
</td>
<td>
<span
class="inv-badge"
[class.inv-badge--active]="a.statusLabel === 'Within Target'"
[class.inv-badge--sold]="a.statusLabel === 'Over-Allocated'"
[class.inv-badge--written_off]="a.statusLabel === 'Under-Allocated'"
>
{{ a.statusLabel }}
</span>
</td>
<th>Limit</th>
<th>Basis</th>
<th class="num">Allowed</th>
<th class="num">Used</th>
<th class="num">Used %</th>
<th>Status</th>
</tr>
}
</tbody>
<tfoot>
<tr class="cctr-total-row">
<td>Total</td>
<td class="num">100%</td>
<td class="num">100%</td>
<td class="num">₹{{ totalFaceValue | number: '1.2-2' }}</td>
<td colspan="2"></td>
</tr>
</tfoot>
</table>
</thead>
<tbody>
@for (l of limits; track l.label) {
<tr>
<td>{{ l.label }}</td>
<td>{{ l.basis }}</td>
<td class="num">₹{{ l.limit | number: '1.2-2' }}</td>
<td class="num">₹{{ l.used | number: '1.2-2' }}</td>
<td class="num">{{ l.usedPct === null ? '—' : (l.usedPct | number: '1.1-1') + '%' }}</td>
<td>
<span class="inv-badge" [class.inv-badge--active]="l.within" [class.inv-badge--written_off]="!l.within">
{{ l.within ? 'Within' : 'Above limit' }}
</span>
</td>
</tr>
}
</tbody>
</table>
}
</div>
<div class="pg-card">
<div class="pos-section-label">Due-Date Diary — Maturing Within 90 Days</div>
<div class="pos-section-label">Maturing Within 90 Days</div>
@if (maturingSoon.length === 0) {
<div class="pg-empty">No holdings maturing in the next 90 days.</div>
} @else {
<div>
@for (h of maturingSoon; track h.certNo) {
@for (m of maturingSoon; track m.dealNo) {
<div class="inv-maturity-row">
<span>
<span class="pg-chip">{{ h.certNo }}</span>
{{ h.scriptName }} — {{ h.party }} ({{ instrumentLabel(h.instrumentType) }})
<span class="pg-chip">{{ m.dealNo }}</span>
{{ m.particulars }} ({{ m.instrument }})
</span>
<span>
Matures {{ h.maturityDate }} ·
Matures {{ m.maturityDate }} · ₹{{ m.amount | number: '1.2-2' }} ·
<span
class="inv-maturity-days"
[class.inv-maturity-days--soon]="daysLeft(h) <= 30"
[class.inv-maturity-days--ok]="daysLeft(h) > 30"
[class.inv-maturity-days--soon]="m.days <= 30"
[class.inv-maturity-days--ok]="m.days > 30"
>
{{ daysLeft(h) }} days
{{ m.days < 0 ? 'Overdue ' + -m.days + ' days' : m.days + ' days' }}
</span>
</span>
</div>
}
</div>
<div class="inv-form-actions" style="padding: 8px 16px">
<a class="pg-btn pg-btn--ghost" routerLink="/investment/diary">Open Due Date Diary</a>
</div>
}
</div>
@@ -179,19 +178,17 @@
<div class="inv-nav-grid" style="padding: 16px">
<a class="inv-nav-card" routerLink="/investment/fd">
<span class="inv-nav-card__label">Fixed Deposit</span>
<span class="inv-nav-card__count">{{ summaries[0].count }}</span>
<span class="inv-nav-card__value">Purchase / Interest / Sell / Write-off</span>
<span class="inv-nav-card__count">{{ countOf('FD') }}</span>
<span class="inv-nav-card__value">Purchase / Interest / Maturity / Renewal</span>
</a>
<a class="inv-nav-card" routerLink="/investment/bonds">
<span class="inv-nav-card__label">Govt Security / Non-SLR / Bonds / MF</span>
<span class="inv-nav-card__count">{{
summaries[1].count + summaries[2].count + summaries[3].count + summaries[4].count
}}</span>
<span class="inv-nav-card__count">{{ countOf('GOVT_SECURITY', 'NON_SLR', 'BOND', 'MUTUAL_FUND') }}</span>
<span class="inv-nav-card__value">Purchase / Interest / Sell / Amortize</span>
</a>
<a class="inv-nav-card" routerLink="/investment/share">
<span class="inv-nav-card__label">Share Entry</span>
<span class="inv-nav-card__count">{{ summaries[5].count }}</span>
<span class="inv-nav-card__count">{{ countOf('SHARE') }}</span>
<span class="inv-nav-card__value">Purchase / Dividend / Sell / Write-off</span>
</a>
<a class="inv-nav-card" routerLink="/investment/interest">