feat(investment): add portfolio allocation targets and interest receivable/post/receive lifecycle
Adds a Portfolio Allocation (Target vs Actual %) section to the Investment Dashboard, and reworks Investment Interest into the full accrual lifecycle: Interest Receivable, FD/Govt./Bond Interest Calculation & Post, and Interest Receive, plus the existing registers. Interest Calculation & Post now tracks cumulative posted/received amounts per holding instead of recomputing full accrued interest on every click (which previously double-counted on repeat posting). Holdings classified Non-Performing (Sub-Standard/Doubtful/Loss) are excluded from accrual posting per RBI norms — their interest accrues in memo/suspense only and is recognised solely on actual cash receipt via Interest Receive. Also bumps the production initial-bundle budget in angular.json (13/14mb -> 15/17mb) since the growing set of eagerly-loaded feature modules (Investment, NPA, Reconciliation, GL Reports, OSS Returns) pushed the build past the old limit.
This commit is contained in:
@@ -78,6 +78,58 @@
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="pg-card">
|
||||
<div class="pos-section-label">Portfolio Allocation — Target vs Actual</div>
|
||||
<table class="pg-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Instrument Type</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) {
|
||||
<tr>
|
||||
<td>{{ a.label }}</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>
|
||||
</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>
|
||||
</div>
|
||||
|
||||
<div class="pg-card">
|
||||
<div class="pos-section-label">Due-Date Diary — Maturing Within 90 Days</div>
|
||||
@if (maturingSoon.length === 0) {
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
INSTRUMENT_LABELS,
|
||||
InstrumentType,
|
||||
InvestmentHolding,
|
||||
TARGET_ALLOCATION_PCT,
|
||||
accruedInterest,
|
||||
daysToMaturity,
|
||||
maturityAmount
|
||||
@@ -29,6 +30,16 @@ interface InstrumentSummary {
|
||||
maturityValue: number;
|
||||
}
|
||||
|
||||
interface AllocationRow {
|
||||
type: InstrumentType;
|
||||
label: string;
|
||||
targetPct: number;
|
||||
actualPct: number;
|
||||
actualValue: number;
|
||||
variancePct: number;
|
||||
statusLabel: 'Within Target' | 'Over-Allocated' | 'Under-Allocated';
|
||||
}
|
||||
|
||||
/** Cross-instrument portfolio dashboard — every figure is derived from ALL_HOLDINGS, never
|
||||
* hand-duplicated, so it can never drift from the per-instrument screens or Reports. */
|
||||
@Component({
|
||||
@@ -79,6 +90,30 @@ export class InvestmentDashboardComponent {
|
||||
});
|
||||
}
|
||||
|
||||
/** Board-approved policy target % per instrument type vs the portfolio's actual allocation
|
||||
* (by invested/face value) — a ±2 percentage-point band is treated as "Within Target" since
|
||||
* a treasury desk can't rebalance to the exact decimal every day. */
|
||||
get allocationVsTarget(): AllocationRow[] {
|
||||
const total = this.totalFaceValue;
|
||||
return this.types.map(({ type }) => {
|
||||
const value = this.holdingsOf(type).reduce((s, h) => s + h.faceValue, 0);
|
||||
const actualPct = total ? (value / total) * 100 : 0;
|
||||
const targetPct = TARGET_ALLOCATION_PCT[type];
|
||||
const variancePct = +(actualPct - targetPct).toFixed(1);
|
||||
const statusLabel: AllocationRow['statusLabel'] =
|
||||
Math.abs(variancePct) <= 2 ? 'Within Target' : variancePct > 0 ? 'Over-Allocated' : 'Under-Allocated';
|
||||
return {
|
||||
type,
|
||||
label: INSTRUMENT_LABELS[type],
|
||||
targetPct,
|
||||
actualPct: +actualPct.toFixed(1),
|
||||
actualValue: value,
|
||||
variancePct,
|
||||
statusLabel
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
get totalHoldings(): number {
|
||||
return ALL_HOLDINGS.length;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user