feat(investment): add Treasury Investment module (FD, Govt Sec/Non-SLR/Bond/MF, Share)
Adds full frontend-only Investment module mirroring the legacy AutoBank CBS Investment menu: Dashboard, FD/Bond/Share entry screens (purchase, interest, sell/redeem, write-off, amortize and more), 10 master screens, an Interest calculation/register screen, and ~21 reports. All figures are derived from a single holdings/transactions dataset so entry, interest, and reports can never drift out of sync. Wires the new "Investment" mega-menu into the shell and routes the previously inert Investment stub buttons.
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
<!--
|
||||
Copyright since 2025 Mifos Initiative
|
||||
|
||||
This Source Code Form is subject to the terms of the Mozilla Public
|
||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
-->
|
||||
|
||||
<div class="pg">
|
||||
<header class="pg-head">
|
||||
<div>
|
||||
<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
|
||||
{{ asOnDate }}.
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="inv-kpis">
|
||||
<div class="inv-kpi">
|
||||
<span class="inv-kpi__label">Total Holdings</span>
|
||||
<span class="inv-kpi__value">{{ totalHoldings }}</span>
|
||||
<span class="inv-kpi__sub">{{ activeCount }} active · {{ maturedCount }} matured</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>
|
||||
</div>
|
||||
<div class="inv-kpi">
|
||||
<span class="inv-kpi__label">Accrued Interest / Income</span>
|
||||
<span class="inv-kpi__value col-in">₹{{ totalAccruedInterest | number: '1.2-2' }}</span>
|
||||
</div>
|
||||
<div class="inv-kpi">
|
||||
<span class="inv-kpi__label">Projected Value at Maturity</span>
|
||||
<span class="inv-kpi__value">₹{{ totalMaturityValue | number: '1.2-2' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pg-card">
|
||||
<div class="pos-section-label">Instrument-wise Break-up</div>
|
||||
<table class="pg-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<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 col-net">Maturity Value</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (s of summaries; track s.type) {
|
||||
<tr>
|
||||
<td>{{ s.label }}</td>
|
||||
<td class="num">{{ s.count }}</td>
|
||||
<td class="num">₹{{ s.faceValue | number: '1.2-2' }}</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>
|
||||
<a class="pg-btn pg-btn--ghost" [routerLink]="s.route">Open</a>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="cctr-total-row">
|
||||
<td>Total</td>
|
||||
<td class="num">{{ totalHoldings }}</td>
|
||||
<td class="num">₹{{ totalFaceValue | number: '1.2-2' }}</td>
|
||||
<td class="num">₹{{ totalAccruedInterest | number: '1.2-2' }}</td>
|
||||
<td class="num">₹{{ totalMaturityValue | number: '1.2-2' }}</td>
|
||||
<td></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) {
|
||||
<div class="pg-empty">No holdings maturing in the next 90 days.</div>
|
||||
} @else {
|
||||
<div>
|
||||
@for (h of maturingSoon; track h.certNo) {
|
||||
<div class="inv-maturity-row">
|
||||
<span>
|
||||
<span class="pg-chip">{{ h.certNo }}</span>
|
||||
{{ h.scriptName }} — {{ h.party }} ({{ instrumentLabel(h.instrumentType) }})
|
||||
</span>
|
||||
<span>
|
||||
Matures {{ h.maturityDate }} ·
|
||||
<span
|
||||
class="inv-maturity-days"
|
||||
[class.inv-maturity-days--soon]="daysLeft(h) <= 30"
|
||||
[class.inv-maturity-days--ok]="daysLeft(h) > 30"
|
||||
>
|
||||
{{ daysLeft(h) }} days
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="pg-card">
|
||||
<div class="pos-section-label">Quick Entry</div>
|
||||
<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>
|
||||
</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__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__value">Purchase / Dividend / Sell / Write-off</span>
|
||||
</a>
|
||||
<a class="inv-nav-card" routerLink="/investment/interest">
|
||||
<span class="inv-nav-card__label">Interest</span>
|
||||
<span class="inv-nav-card__count">5</span>
|
||||
<span class="inv-nav-card__value">FD / Bond Calculation & Registers</span>
|
||||
</a>
|
||||
<a class="inv-nav-card" routerLink="/investment/masters">
|
||||
<span class="inv-nav-card__label">Masters</span>
|
||||
<span class="inv-nav-card__count">10</span>
|
||||
<span class="inv-nav-card__value">Script / Broker / Party / Control Params</span>
|
||||
</a>
|
||||
<a class="inv-nav-card" routerLink="/investment/reports">
|
||||
<span class="inv-nav-card__label">Reports</span>
|
||||
<span class="inv-nav-card__count">20+</span>
|
||||
<span class="inv-nav-card__value">Balance / Ledger / Maturity / Portfolio</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,120 @@
|
||||
/**
|
||||
* Copyright since 2025 Mifos Initiative
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { RouterLink } from '@angular/router';
|
||||
import {
|
||||
ALL_HOLDINGS,
|
||||
INSTRUMENT_LABELS,
|
||||
InstrumentType,
|
||||
InvestmentHolding,
|
||||
accruedInterest,
|
||||
daysToMaturity,
|
||||
maturityAmount
|
||||
} from '../investment-data';
|
||||
|
||||
interface InstrumentSummary {
|
||||
type: InstrumentType;
|
||||
label: string;
|
||||
route: string;
|
||||
count: number;
|
||||
faceValue: number;
|
||||
accruedInterest: number;
|
||||
maturityValue: number;
|
||||
}
|
||||
|
||||
/** 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({
|
||||
selector: 'mifosx-investment-dashboard',
|
||||
templateUrl: './investment-dashboard.component.html',
|
||||
styleUrls: [
|
||||
'../../cashier/cashier-shared.scss',
|
||||
'../investment-shared.scss'
|
||||
],
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
RouterLink
|
||||
]
|
||||
})
|
||||
export class InvestmentDashboardComponent {
|
||||
asOnDate = '';
|
||||
private readonly types: Array<{ type: InstrumentType; route: string }> = [
|
||||
{ type: 'FD', route: '/investment/fd' },
|
||||
{ type: 'GOVT_SECURITY', route: '/investment/bonds' },
|
||||
{ type: 'NON_SLR', route: '/investment/bonds' },
|
||||
{ type: 'BOND', route: '/investment/bonds' },
|
||||
{ type: 'MUTUAL_FUND', route: '/investment/bonds' },
|
||||
{ type: 'SHARE', route: '/investment/share' }
|
||||
];
|
||||
|
||||
constructor() {
|
||||
const d = new Date();
|
||||
this.asOnDate = `${String(d.getDate()).padStart(2, '0')}-${String(d.getMonth() + 1).padStart(2, '0')}-${d.getFullYear()}`;
|
||||
}
|
||||
|
||||
private holdingsOf(type: InstrumentType): InvestmentHolding[] {
|
||||
return ALL_HOLDINGS.filter((h) => h.instrumentType === type);
|
||||
}
|
||||
|
||||
get summaries(): InstrumentSummary[] {
|
||||
return this.types.map(({ type, route }) => {
|
||||
const holdings = this.holdingsOf(type);
|
||||
return {
|
||||
type,
|
||||
label: INSTRUMENT_LABELS[type],
|
||||
route,
|
||||
count: holdings.length,
|
||||
faceValue: holdings.reduce((s, h) => s + h.faceValue, 0),
|
||||
accruedInterest: holdings.reduce((s, h) => s + accruedInterest(h, this.asOnDate), 0),
|
||||
maturityValue: holdings.reduce((s, h) => s + maturityAmount(h), 0)
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
get totalHoldings(): number {
|
||||
return ALL_HOLDINGS.length;
|
||||
}
|
||||
|
||||
get totalFaceValue(): number {
|
||||
return ALL_HOLDINGS.reduce((s, h) => s + h.faceValue, 0);
|
||||
}
|
||||
|
||||
get totalAccruedInterest(): number {
|
||||
return ALL_HOLDINGS.reduce((s, h) => s + accruedInterest(h, this.asOnDate), 0);
|
||||
}
|
||||
|
||||
get totalMaturityValue(): number {
|
||||
return ALL_HOLDINGS.reduce((s, h) => s + maturityAmount(h), 0);
|
||||
}
|
||||
|
||||
get activeCount(): number {
|
||||
return ALL_HOLDINGS.filter((h) => h.status === 'ACTIVE').length;
|
||||
}
|
||||
|
||||
get maturedCount(): number {
|
||||
return ALL_HOLDINGS.filter((h) => h.status === 'MATURED').length;
|
||||
}
|
||||
|
||||
/** Holdings maturing within the next 90 days — a "due-date diary" style widget. */
|
||||
get maturingSoon(): InvestmentHolding[] {
|
||||
return ALL_HOLDINGS.filter((h) => h.status === 'ACTIVE' && daysToMaturity(h, this.asOnDate) <= 90).sort(
|
||||
(a, b) => daysToMaturity(a, this.asOnDate) - daysToMaturity(b, this.asOnDate)
|
||||
);
|
||||
}
|
||||
|
||||
daysLeft(h: InvestmentHolding): number {
|
||||
return daysToMaturity(h, this.asOnDate);
|
||||
}
|
||||
|
||||
instrumentLabel(type: InstrumentType): string {
|
||||
return INSTRUMENT_LABELS[type];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user