diff --git a/src/app/investment/bond-investment/bond-investment.component.ts b/src/app/investment/bond-investment/bond-investment.component.ts index c50ebea8f..e489fceda 100644 --- a/src/app/investment/bond-investment/bond-investment.component.ts +++ b/src/app/investment/bond-investment/bond-investment.component.ts @@ -74,7 +74,11 @@ import { InvestmentNonSlrLimitDto, InvestmentService, apiErrorMessage, - toHolding + toHolding, + toInterestTxn, + toPurchaseTxn, + toSaleTxn, + toTransferTxn } from '../investment.service'; type BondType = 'GOVT_SECURITY' | 'NON_SLR' | 'BOND' | 'MUTUAL_FUND'; @@ -222,7 +226,7 @@ export class BondInvestmentComponent implements OnInit { this.investmentService.getPurchases().subscribe({ next: (purchases) => { this.holdings = purchases.map(toHolding); - this.txns = purchases.map((d) => this.purchaseTxn(d)); + this.txns = purchases.map(toPurchaseTxn); this.purchaseForm = this.blankPurchaseForm(); this.loadNonSlrLimit(); this.loadInterestLog(); @@ -253,115 +257,13 @@ export class BondInvestmentComponent implements OnInit { ); this.txns = [ ...others, - ...postings.map((x) => this.interestTxn(x)), - ...sales.map((x) => this.saleTxn(x)), - ...transfers.map((x) => this.transferTxn(x)) + ...postings.map(toInterestTxn), + ...sales.map(toSaleTxn), + ...transfers.map(toTransferTxn) ].sort((a, b) => parseDDMMYYYY(b.txnDate).getTime() - parseDDMMYYYY(a.txnDate).getTime()); }); } - /** A security transfer shows the book value carried to the new security. */ - private transferTxn(x: InvestmentSecurityTransferDto): InvestmentTxn { - return { - certNo: x.certNo, - instrumentType: x.bondType as InstrumentType, - txnType: 'TRANSFER', - voucherNo: x.voucherNo ?? `TRF/${x.dealNo}/${x.id}`, - txnDate: x.transferDate, - amount: x.bookValue, - particulars: `GOI category transfer - Deal ${x.dealNo}: ${x.fromScriptCode} ${x.fromScriptDesc} → ${x.toScriptCode} ${x.toScriptDesc}`, - glCode: x.toGlCode, - glName: x.glTransactionId - ? `${x.toGlName} / Cr ${x.fromGlCode} (txn ${x.glTransactionId})` - : `${x.toGlName} (same GL, no entry)` - }; - } - - /** A sale or redemption shows the money into the bank; a pending sale is not in the GL yet. */ - private saleTxn(x: InvestmentSaleDto): InvestmentTxn { - if (x.saleType === 'AMORTISE') { - return { - certNo: x.certNo, - instrumentType: x.bondType as InstrumentType, - txnType: 'AMORTIZE', - voucherNo: x.voucherNo ?? `AMZ/${x.dealNo}/${x.id}`, - txnDate: x.settlementDate, - amount: x.bookValueSold, - particulars: `${x.remarks ?? 'Premium amortisation'} - Deal ${x.dealNo} - ${x.scriptDesc} (${x.accruedDays} days)`, - glCode: '500604', - glName: `Amortisation of Premium / Cr Premium on Investments (txn ${x.glTransactionId})` - }; - } - if (x.saleType === 'WRITEOFF') { - return { - certNo: x.certNo, - instrumentType: x.bondType as InstrumentType, - txnType: 'WRITE_OFF', - voucherNo: x.voucherNo ?? `WOF/${x.dealNo}/${x.id}`, - txnDate: x.settlementDate, - amount: x.bookValueSold, - particulars: `Written off - Deal ${x.dealNo} - ${x.scriptDesc}${x.remarks ? ' - ' + x.remarks : ''}`, - glCode: '500605', - glName: `Loss on Investments / Cr ${x.investmentGlCode} (txn ${x.glTransactionId})` - }; - } - const redeem = x.saleType === 'REDEEM'; - const pl = - x.profitLoss >= 0 - ? `profit ₹${x.profitLoss}` - : redeem - ? `premium written off ₹${-x.profitLoss}` - : `loss ₹${-x.profitLoss}`; - return { - certNo: x.certNo, - instrumentType: x.bondType as InstrumentType, - txnType: x.saleType, - voucherNo: x.voucherNo ?? `SALE/${x.dealNo}/${x.id}`, - txnDate: x.settlementDate, - amount: x.netAmount, - particulars: redeem - ? `Redeemed at maturity - Deal ${x.dealNo} - ${x.scriptDesc} (${pl})` - : `Sold ${x.units} units @ ${x.saleRate} to ${x.partyName ?? ''} - Deal ${x.dealNo} - ${x.scriptDesc} (${pl})`, - glCode: x.status === 'SETTLED' ? x.bankGlCode : '', - glName: - x.status === 'SETTLED' - ? `${x.bankGlName} / Cr ${x.investmentGlCode} (txn ${x.glTransactionId})` - : 'Not posted (pending settlement)' - }; - } - - /** A coupon shows the cash received into the bank; a provision the interest booked as receivable. */ - private interestTxn(x: InvestmentInterestPostingDto): InvestmentTxn { - const coupon = x.lineType === 'COUPON'; - return { - certNo: x.certNo, - instrumentType: x.bondType as InstrumentType, - txnType: 'INTEREST', - voucherNo: x.voucherNo, - txnDate: x.intDate, - amount: coupon ? x.grossInterest : x.amount, - particulars: `${coupon ? 'Coupon' : 'Provision'} ${x.fromDate} to ${x.toDate} - Deal ${x.dealNo} - ${x.scriptDesc}`, - glCode: x.drGlCode, - glName: `${x.drGlName} / Cr ${x.incomeGlCode} (txn ${x.glTransactionId})` - }; - } - - /** A purchase reaches the GL only when it is settled, dated the settlement date. */ - private purchaseTxn(d: InvestmentPurchaseDto): InvestmentTxn { - const posted = !!d.glTransactionId; - return { - certNo: d.certNo, - instrumentType: d.bondType, - txnType: 'PURCHASE', - voucherNo: `INV/PUR/${d.dealNo}`, - txnDate: d.settlementDate, - amount: d.settlementAmount, - particulars: `Purchase Deal ${d.dealNo} - ${d.scriptDesc} - ${d.partyName} [${d.category}]`, - glCode: posted ? d.investmentGlCode : '', - glName: posted ? `${d.investmentGlName} (txn ${d.glTransactionId})` : 'Not posted (pending settlement)' - }; - } - activeTab: BondTab = 'holdings'; /** Purchase Entry shows only the mandatory fields; the rest sit under "More Details". */ showMoreDetails = false; diff --git a/src/app/investment/investment-dashboard/investment-dashboard.component.html b/src/app/investment/investment-dashboard/investment-dashboard.component.html index 51673f91b..9d5cdc2d3 100644 --- a/src/app/investment/investment-dashboard/investment-dashboard.component.html +++ b/src/app/investment/investment-dashboard/investment-dashboard.component.html @@ -12,34 +12,50 @@
TREASURY · INVESTMENT

Investment Portfolio Dashboard

- 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 }}.

- +
+ @if (error) { +
+

{{ error }}

+
+ } +
- Total Holdings + Holdings {{ totalHoldings }} - {{ activeCount }} active · {{ maturedCount }} matured + {{ maturingSoon.length }} due within 90 days{{ overdueCount ? ' (' + overdueCount + ' overdue)' : '' }}
- Total Invested (Face Value) - ₹{{ totalFaceValue | number: '1.2-2' }} + Total Invested (Book Value) + ₹{{ totalBookValue | number: '1.2-2' }} + Face value ₹{{ totalFaceValue | number: '1.2-2' }}
- Accrued Interest / Income + Interest Receivable ₹{{ totalAccruedInterest | number: '1.2-2' }} + Earned till today, not received · ₹{{ totalProvisioned | number: '1.2-2' }} provisioned
- Projected Value at Maturity + Value at Maturity ₹{{ totalMaturityValue | number: '1.2-2' }} + Securities at face value, FDs with interest
@@ -51,7 +67,9 @@ Instrument Type Holdings Face Value - Accrued Interest + Book Value + Share + Interest Receivable Maturity Value @@ -62,6 +80,8 @@ {{ s.label }} {{ s.count }} ₹{{ s.faceValue | number: '1.2-2' }} + ₹{{ s.bookValue | number: '1.2-2' }} + {{ s.sharePct | number: '1.1-1' }}% ₹{{ s.accruedInterest | number: '1.2-2' }} ₹{{ s.maturityValue | number: '1.2-2' }} @@ -75,6 +95,8 @@ Total {{ totalHoldings }} ₹{{ totalFaceValue | number: '1.2-2' }} + ₹{{ totalBookValue | number: '1.2-2' }} + {{ totalBookValue > 0 ? '100%' : '—' }} ₹{{ totalAccruedInterest | number: '1.2-2' }} ₹{{ totalMaturityValue | number: '1.2-2' }} @@ -84,93 +106,70 @@
-
Portfolio Allocation — Target vs Actual
-

- 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. -

- - - - - - - - - - - - - - @for (a of allocationVsTarget; track a.type) { +
RBI Limits
+ @if (limitNote) { +

{{ limitNote }}

+ } + @if (limits.length) { +
Instrument TypeTarget BasisTarget %Actual %Actual ValueVarianceStatus
+ - - - - - - - + + + + + + - } - - - - - - - - - - -
{{ a.label }} - - {{ a.isRbiLinked ? 'RBI SLR / Non-SLR Norm' : 'Board Policy' }} - - {{ a.targetPct }}%{{ a.actualPct }}%₹{{ a.actualValue | number: '1.2-2' }} - {{ a.variancePct > 0 ? '+' : '' }}{{ a.variancePct }}% - - - {{ a.statusLabel }} - - LimitBasisAllowedUsedUsed %Status
Total100%100%₹{{ totalFaceValue | number: '1.2-2' }}
+ + + @for (l of limits; track l.label) { + + {{ l.label }} + {{ l.basis }} + ₹{{ l.limit | number: '1.2-2' }} + ₹{{ l.used | number: '1.2-2' }} + {{ l.usedPct === null ? '—' : (l.usedPct | number: '1.1-1') + '%' }} + + + {{ l.within ? 'Within' : 'Above limit' }} + + + + } + + + }
-
Due-Date Diary — Maturing Within 90 Days
+
Maturing Within 90 Days
@if (maturingSoon.length === 0) {
No holdings maturing in the next 90 days.
} @else {
- @for (h of maturingSoon; track h.certNo) { + @for (m of maturingSoon; track m.dealNo) {
- {{ h.certNo }} - {{ h.scriptName }} — {{ h.party }} ({{ instrumentLabel(h.instrumentType) }}) + {{ m.dealNo }} + {{ m.particulars }} ({{ m.instrument }}) - Matures {{ h.maturityDate }} · + Matures {{ m.maturityDate }} · ₹{{ m.amount | number: '1.2-2' }} · - {{ daysLeft(h) }} days + {{ m.days < 0 ? 'Overdue ' + -m.days + ' days' : m.days + ' days' }}
}
+
+ Open Due Date Diary +
}
@@ -179,19 +178,17 @@
Fixed Deposit - {{ summaries[0].count }} - Purchase / Interest / Sell / Write-off + {{ countOf('FD') }} + Purchase / Interest / Maturity / Renewal Govt Security / Non-SLR / Bonds / MF - {{ - summaries[1].count + summaries[2].count + summaries[3].count + summaries[4].count - }} + {{ countOf('GOVT_SECURITY', 'NON_SLR', 'BOND', 'MUTUAL_FUND') }} Purchase / Interest / Sell / Amortize Share Entry - {{ summaries[5].count }} + {{ countOf('SHARE') }} Purchase / Dividend / Sell / Write-off diff --git a/src/app/investment/investment-dashboard/investment-dashboard.component.ts b/src/app/investment/investment-dashboard/investment-dashboard.component.ts index 15d6adb79..bbfc8c4d3 100644 --- a/src/app/investment/investment-dashboard/investment-dashboard.component.ts +++ b/src/app/investment/investment-dashboard/investment-dashboard.component.ts @@ -6,21 +6,23 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { Component } from '@angular/core'; +import { Component, OnInit, inject } from '@angular/core'; import { CommonModule } from '@angular/common'; import { RouterLink } from '@angular/router'; -import { - ALL_HOLDINGS, - ALLOCATION_TARGET_IS_RBI_LINKED, - INSTRUMENT_LABELS, - InstrumentType, - InvestmentHolding, - TARGET_ALLOCATION_PCT, - accruedInterest, - daysToMaturity, - maturityAmount -} from '../investment-data'; +import { forkJoin } from 'rxjs'; +import { INSTRUMENT_LABELS, InstrumentType, parseDDMMYYYY, todayDDMMYYYY } from '../investment-data'; import { exportWorkbook } from '../investment-export'; +import { + InvestmentBucketReportDto, + InvestmentBucketRowDto, + InvestmentFdDto, + InvestmentFdLimitDto, + InvestmentInterestReportDto, + InvestmentNonSlrLimitDto, + InvestmentPurchaseDto, + InvestmentService, + apiErrorMessage +} from '../investment.service'; interface InstrumentSummary { type: InstrumentType; @@ -28,25 +30,45 @@ interface InstrumentSummary { route: string; count: number; faceValue: number; + bookValue: number; accruedInterest: number; maturityValue: number; + sharePct: number; } -interface AllocationRow { - type: InstrumentType; +interface LimitRow { label: string; - targetPct: number; - actualPct: number; - actualValue: number; - variancePct: number; - statusLabel: 'Within Target' | 'Over-Allocated' | 'Under-Allocated'; - /** True when the target itself traces to an RBI SLR/Non-SLR rule rather than being a - * board-only policy choice — see TARGET_ALLOCATION_PCT's doc comment in investment-data.ts. */ - isRbiLinked: boolean; + basis: string; + limit: number | null; + used: number; + usedPct: number | null; + within: boolean; } -/** 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. */ +/** A holding maturing soon, or past maturity and not yet redeemed / closed. */ +interface MaturingRow { + dealNo: string; + particulars: string; + instrument: string; + maturityDate: string; + days: number; + amount: number; +} + +const ROUTE: Record = { + FD: '/investment/fd', + GOVT_SECURITY: '/investment/bonds', + NON_SLR: '/investment/bonds', + BOND: '/investment/bonds', + MUTUAL_FUND: '/investment/bonds', + SHARE: '/investment/share' +}; + +/** + * Cross-instrument portfolio dashboard on live data: what is held today by instrument (face value, book value, + * interest earned and not yet received, value at maturity), where the portfolio stands against the RBI limits, and + * what matures in the next 90 days. + */ @Component({ selector: 'mifosx-investment-dashboard', templateUrl: './investment-dashboard.component.html', @@ -60,107 +82,216 @@ interface AllocationRow { RouterLink ] }) -export class InvestmentDashboardComponent { - asOnDate = ''; +export class InvestmentDashboardComponent implements OnInit { + private readonly investmentService = inject(InvestmentService); + + asOnDate = todayDDMMYYYY(); + loading = false; + error = ''; generating = false; toastMsg = ''; toastVisible = false; private toastTimer: ReturnType | undefined; - 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()}`; + summaries: InstrumentSummary[] = []; + limits: LimitRow[] = []; + limitNote = ''; + maturingSoon: MaturingRow[] = []; + totalReceivable = 0; + totalProvisioned = 0; + + ngOnInit(): void { + this.load(); } - 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) - }; + load(): void { + const today = this.asOnDate; + const d = parseDDMMYYYY(today); + const fyStart = `01-04-${d.getMonth() >= 3 ? d.getFullYear() : d.getFullYear() - 1}`; + this.loading = true; + this.error = ''; + forkJoin({ + bucket: this.investmentService.getBucketReport(today), + interest: this.investmentService.getInterestReport(fyStart, today), + purchases: this.investmentService.getPurchases(), + fds: this.investmentService.getFds(), + fdQuote: this.investmentService.quoteFd({ amount: 0, intRate: 0, dealDate: today }), + nonSlr: this.investmentService.getNonSlrLimit(null, today, 0) + }).subscribe({ + next: (r) => { + this.build(r.bucket, r.interest, r.purchases, r.fds, r.fdQuote.limit ?? null, r.nonSlr); + this.loading = false; + }, + error: (err: unknown) => { + this.loading = false; + this.error = apiErrorMessage(err); + } }); } - /** 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, - isRbiLinked: ALLOCATION_TARGET_IS_RBI_LINKED[type] - }; - }); + private build( + bucket: InvestmentBucketReportDto, + interest: InvestmentInterestReportDto, + purchases: InvestmentPurchaseDto[], + fds: InvestmentFdDto[], + fdLimit: InvestmentFdLimitDto | null, + nonSlr: InvestmentNonSlrLimitDto + ): void { + const purchaseById = new Map( + purchases.map((p) => [ + p.id, + p + ]) + ); + const fdById = new Map( + fds + .filter((f) => f.id != null) + .map((f) => [ + f.id as number, + f + ]) + ); + const receivable = new Map( + interest.rows.map((x) => [ + x.instrument + ':' + x.refId, + x.receivable + ]) + ); + this.totalReceivable = interest.totalReceivable; + this.totalProvisioned = interest.totalProvisioned; + + // Bucket rows are the Govt Securities, bonds and FDs held today; the purchase tells a Non-SLR from a Bond. + const typeOf = (r: InvestmentBucketRowDto): InstrumentType => + r.instrument === 'FD' ? 'FD' : (purchaseById.get(r.refId)?.bondType ?? 'BOND'); + const acc = new Map(); + const sum = (type: InstrumentType): InstrumentSummary => { + let s = acc.get(type); + if (!s) { + s = { + type, + label: INSTRUMENT_LABELS[type], + route: ROUTE[type], + count: 0, + faceValue: 0, + bookValue: 0, + accruedInterest: 0, + maturityValue: 0, + sharePct: 0 + }; + acc.set(type, s); + } + return s; + }; + for (const r of bucket.rows) { + const s = sum(typeOf(r)); + s.count += 1; + s.faceValue += r.faceValue; + s.bookValue += r.bookValue; + s.accruedInterest += receivable.get((r.instrument === 'FD' ? 'FD' : 'SECURITY') + ':' + r.refId) ?? 0; + // A security is redeemed at face value; an FD pays its maturity amount (just the principal on a payout FD). + s.maturityValue += + r.instrument === 'FD' ? (fdById.get(r.refId)?.expectedMaturityAmount ?? r.faceValue) : r.faceValue; + } + // Mutual funds and shares have no maturity, so they are not in the buckets; their units are held at cost. + for (const p of purchases) { + if ((p.bondType === 'MUTUAL_FUND' || p.bondType === 'SHARE') && p.status === 'ACTIVE') { + const s = sum(p.bondType); + s.count += 1; + s.faceValue += p.faceValue; + s.bookValue += p.bookValue; + s.maturityValue += p.bookValue; + } + } + const order: InstrumentType[] = [ + 'GOVT_SECURITY', + 'FD', + 'NON_SLR', + 'BOND', + 'MUTUAL_FUND', + 'SHARE' + ]; + const total = [...acc.values()].reduce((t, s) => t + s.bookValue, 0); + this.summaries = order + .map((t) => sum(t)) + .map((s) => ({ ...s, sharePct: total > 0 ? (s.bookValue / total) * 100 : 0 })); + + this.limits = []; + this.limitNote = ''; + if (fdLimit?.grossLimit != null) { + const used = fdLimit.fdOutstanding + fdLimit.bankBalances; + this.limits.push( + this.limitRow( + 'Deposits with other banks', + 'FDs + current accounts, ≤ 20% of deposits', + fdLimit.grossLimit, + used + ) + ); + } + if (nonSlr.nonSlrLimit != null) { + this.limits.push( + this.limitRow( + 'Non-SLR investments', + 'Bonds, CP / CD, MF, ≤ 10% of deposits', + nonSlr.nonSlrLimit, + nonSlr.nonSlrOutstanding + ) + ); + this.limits.push( + this.limitRow( + 'Unlisted non-SLR', + `≤ 10% of non-SLR held on ${nonSlr.asOnDate}`, + nonSlr.unlistedLimit, + nonSlr.unlistedOutstanding + ) + ); + } else { + this.limitNote = `Total deposits as on ${nonSlr.asOnDate} are not entered yet; enter them on the FD screen to see the RBI limits.`; + } + + this.maturingSoon = bucket.rows + .filter((r) => r.daysToMaturity <= 90) + .map((r) => ({ + dealNo: r.dealNo, + particulars: r.particulars, + instrument: INSTRUMENT_LABELS[typeOf(r)], + maturityDate: r.maturityDate, + days: r.daysToMaturity, + amount: r.instrument === 'FD' ? (fdById.get(r.refId)?.expectedMaturityAmount ?? r.faceValue) : r.faceValue + })); + } + + private limitRow(label: string, basis: string, limit: number, used: number): LimitRow { + return { label, basis, limit, used, usedPct: limit > 0 ? (used / limit) * 100 : null, within: used <= limit }; } get totalHoldings(): number { - return ALL_HOLDINGS.length; + return this.summaries.reduce((t, s) => t + s.count, 0); } get totalFaceValue(): number { - return ALL_HOLDINGS.reduce((s, h) => s + h.faceValue, 0); + return this.summaries.reduce((t, s) => t + s.faceValue, 0); + } + + get totalBookValue(): number { + return this.summaries.reduce((t, s) => t + s.bookValue, 0); } get totalAccruedInterest(): number { - return ALL_HOLDINGS.reduce((s, h) => s + accruedInterest(h, this.asOnDate), 0); + return this.summaries.reduce((t, s) => t + s.accruedInterest, 0); } get totalMaturityValue(): number { - return ALL_HOLDINGS.reduce((s, h) => s + maturityAmount(h), 0); + return this.summaries.reduce((t, s) => t + s.maturityValue, 0); } - get activeCount(): number { - return ALL_HOLDINGS.filter((h) => h.status === 'ACTIVE').length; + get overdueCount(): number { + return this.maturingSoon.filter((m) => m.days < 0).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]; + /** Holdings of one or more instrument types, for the Quick Entry cards. */ + countOf(...types: InstrumentType[]): number { + return this.summaries.filter((s) => types.includes(s.type)).reduce((t, s) => t + s.count, 0); } async exportExcel(): Promise { @@ -176,54 +307,97 @@ export class InvestmentDashboardComponent { 'Instrument Type', 'Holdings', 'Face Value', - 'Accrued Interest', + 'Book Value', + 'Share %', + 'Interest Receivable', 'Maturity Value' ], widths: [ - 26, - 12, + 22, + 10, 18, 18, + 10, + 18, 18 ], - rows: this.summaries.map((s) => [ - s.label, - s.count, - s.faceValue, - s.accruedInterest, - s.maturityValue - ]) + rows: [ + ...this.summaries.map((s) => [ + s.label, + s.count, + s.faceValue, + s.bookValue, + Number(s.sharePct.toFixed(2)), + s.accruedInterest, + s.maturityValue + ]), + [ + 'Total', + this.totalHoldings, + this.totalFaceValue, + this.totalBookValue, + 100, + this.totalAccruedInterest, + this.totalMaturityValue + ] + ] }, { - sheetName: 'Portfolio Allocation', - title: 'Portfolio Allocation — Target vs Actual', + sheetName: 'RBI Limits', + title: 'RBI Limits', subtitle: `As on ${this.asOnDate}`, headers: [ - 'Instrument Type', - 'Target Basis', - 'Target %', - 'Actual %', - 'Actual Value', - 'Variance %', + 'Limit', + 'Basis', + 'Allowed', + 'Used', + 'Used %', 'Status' ], widths: [ 26, - 20, - 12, - 12, + 40, 18, + 18, + 10, + 14 + ], + rows: this.limits.map((l) => [ + l.label, + l.basis, + l.limit, + l.used, + l.usedPct === null ? null : Number(l.usedPct.toFixed(2)), + l.within ? 'Within' : 'Above limit' + ]) + }, + { + sheetName: 'Maturing in 90 days', + title: 'Maturing Within 90 Days (and overdue)', + subtitle: `As on ${this.asOnDate}`, + headers: [ + 'Deal No', + 'Investment', + 'Particulars', + 'Maturity', + 'Days', + 'Amount' + ], + widths: [ + 16, + 16, + 40, 12, + 10, 18 ], - rows: this.allocationVsTarget.map((a) => [ - a.label, - a.isRbiLinked ? 'RBI SLR / Non-SLR Norm' : 'Board Policy', - a.targetPct, - a.actualPct, - a.actualValue, - a.variancePct, - a.statusLabel + rows: this.maturingSoon.map((m) => [ + m.dealNo, + m.instrument, + m.particulars, + m.maturityDate, + m.days, + m.amount ]) } ], diff --git a/src/app/investment/investment-reports/investment-reports.component.html b/src/app/investment/investment-reports/investment-reports.component.html index f8c91431b..29c989aee 100644 --- a/src/app/investment/investment-reports/investment-reports.component.html +++ b/src/app/investment/investment-reports/investment-reports.component.html @@ -12,17 +12,26 @@
TREASURY · INVESTMENT · REPORTS

Investment Reports

- Balance reports, ledgers, registers, maturity lists and the consolidated portfolio — all derived live from the - same holdings/transactions used by the Entry screens. + Balance reports, ledgers, registers, maturity lists and the consolidated portfolio from the saved deals, FDs and + their GL entries, as on {{ asOnDate }}.

- +
+ @if (error) { +
+

{{ error }}

+
+ } +