From 6890584b16f21952df8f159335313df2fc5b4c82 Mon Sep 17 00:00:00 2001 From: anshul Date: Tue, 20 Oct 2026 16:16:12 +0530 Subject: [PATCH] feat(investment): FD TDS fields, GOI interest to the paisa, compact home reminders, deal no in holdings and settle guard --- src/app/home/home.component.html | 88 ++++++++++++------- src/app/home/home.component.scss | 41 ++++++++- src/app/home/home.component.ts | 15 ++++ .../bond-investment.component.html | 44 +++++++--- .../bond-investment.component.ts | 50 ++++++++--- .../fd-investment.component.html | 64 ++++++++++---- .../fd-investment/fd-investment.component.ts | 47 ++++++++-- src/app/investment/investment-data.ts | 3 + .../investment-masters.component.html | 17 +++- .../investment-masters.component.ts | 23 +++++ src/app/investment/investment.service.ts | 6 ++ 11 files changed, 316 insertions(+), 82 deletions(-) diff --git a/src/app/home/home.component.html b/src/app/home/home.component.html index a3e8fe8c4..878300434 100644 --- a/src/app/home/home.component.html +++ b/src/app/home/home.component.html @@ -47,43 +47,63 @@ - - @if (pendingInvestmentInterest.length) { - + + @if (pendingInvestmentInterest.length || pendingFd.length) { +
🔔 - - {{ pendingInvestmentInterest.length }} investment interest voucher(s) pending - @for (t of pendingInvestmentInterest; track t.purchaseId) { - - {{ t.lineType === 'COUPON' ? 'Coupon' : 'Provision' }} · {{ t.scriptDesc }} · deal {{ t.dealNo }} · - {{ t.overdue ? 'overdue since ' + t.intDate : t.postable ? 'due today' : 'due ' + t.intDate }} - +
+ Investment entries pending + @if (pendingInvestmentInterest.length) { + + @if (showInterestDetails) { + @for (t of pendingInvestmentInterest.slice(0, detailLimit); track t.purchaseId) { + + {{ t.lineType === 'COUPON' ? 'Coupon' : 'Provision' }} · {{ t.scriptDesc }} · deal {{ t.dealNo }} · + {{ t.overdue ? 'overdue since ' + t.intDate : t.postable ? 'due today' : 'due ' + t.intDate }} + + } + @if (pendingInvestmentInterest.length > detailLimit) { + +{{ pendingInvestmentInterest.length - detailLimit }} more in Interest Entry + } + } } - - Open Interest Entry → - - } - - - @if (pendingFd.length) { - - 🔔 - - {{ pendingFd.length }} FD investment entry(ies) pending - @for (t of pendingFd; track t.fdId) { - - {{ t.lineType === 'MATURITY' ? 'Maturity' : t.lineType === 'PAYOUT' ? 'Interest payout' : 'Provision' }} · - {{ t.partyName }} · FD {{ t.dealNo }} · - {{ t.overdue ? 'overdue since ' + t.intDate : t.postable ? 'due today' : 'due ' + t.intDate }} - + @if (pendingFd.length) { + + @if (showFdDetails) { + @for (t of pendingFd.slice(0, detailLimit); track t.fdId + t.lineType + t.intDate) { + + {{ t.lineType === 'MATURITY' ? 'Maturity' : t.lineType === 'PAYOUT' ? 'Interest payout' : 'Provision' }} + · {{ t.partyName }} · FD {{ t.dealNo }} · + {{ t.overdue ? 'overdue since ' + t.intDate : t.postable ? 'due today' : 'due ' + t.intDate }} + + } + @if (pendingFd.length > detailLimit) { + +{{ pendingFd.length - detailLimit }} more in FD Investment + } + } } - - Open FD Investment → - +
+
} diff --git a/src/app/home/home.component.scss b/src/app/home/home.component.scss index 701542b70..98c25b72a 100644 --- a/src/app/home/home.component.scss +++ b/src/app/home/home.component.scss @@ -447,16 +447,55 @@ $accent: #0ea5e9; display: flex; flex: 1; flex-direction: column; - gap: 2px; + gap: 4px; + min-width: 0; + } + + .iia-row { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 4px 12px; + font-size: 14px; + } + + .iia-summary { + flex: 1; + min-width: 0; + } + + .iia-toggle { + padding: 2px 10px; + border: 1px solid #e0a43a; + border-radius: 12px; + background: transparent; + color: inherit; + font-size: 12px; + cursor: pointer; } .iia-item { + padding-left: 12px; font-size: 13px; + color: inherit; + text-decoration: none; + } + + .iia-more { + padding-left: 12px; + font-size: 12px; + font-style: italic; + } + + a.iia-item:hover { + text-decoration: underline; } .iia-action { align-self: center; font-weight: 600; white-space: nowrap; + color: inherit; + text-decoration: none; } } diff --git a/src/app/home/home.component.ts b/src/app/home/home.component.ts index 6b4646feb..4d3c7acaa 100644 --- a/src/app/home/home.component.ts +++ b/src/app/home/home.component.ts @@ -124,8 +124,23 @@ export class HomeComponent implements OnInit, AfterViewInit { pendingInvestmentInterest: InvestmentInterestDueDto[] = []; /** FDs placed with other banks: interest vouchers and maturities due now or within two days. */ pendingFd: InvestmentFdDueDto[] = []; + /** The lists stay folded to one line each; the user opens them when needed. */ + showInterestDetails = false; + showFdDetails = false; + readonly detailLimit = 5; private readonly investmentService = inject(InvestmentService); + /** " · 3 overdue, oldest 14-08-2025" for a list of due entries (dates DD-MM-YYYY); empty when none is overdue. */ + overdueNote(items: Array<{ overdue?: boolean; intDate: string }>): string { + const late = items.filter((t) => t.overdue); + if (!late.length) { + return ''; + } + const key = (d: string) => d.split('-').reverse().join(''); + const oldest = late.map((t) => t.intDate).sort((a, b) => key(a).localeCompare(key(b)))[0]; + return ` · ${late.length} overdue, oldest ${oldest}`; + } + /** Silent when the user cannot see investments or nothing is due — the strip simply does not render. */ private loadPendingInvestmentInterest(): void { this.investmentService.getDueInterest().subscribe({ diff --git a/src/app/investment/bond-investment/bond-investment.component.html b/src/app/investment/bond-investment/bond-investment.component.html index faae95c05..4091e9f7d 100644 --- a/src/app/investment/bond-investment/bond-investment.component.html +++ b/src/app/investment/bond-investment/bond-investment.component.html @@ -131,7 +131,7 @@ - + @@ -150,7 +150,8 @@ @for (h of holdingsForType; track h.certNo) { @@ -169,8 +170,13 @@ } @empty { diff --git a/src/app/investment/bond-investment/bond-investment.component.ts b/src/app/investment/bond-investment/bond-investment.component.ts index fc1bb99c7..2d4e220f2 100644 --- a/src/app/investment/bond-investment/bond-investment.component.ts +++ b/src/app/investment/bond-investment/bond-investment.component.ts @@ -604,7 +604,7 @@ export class BondInvestmentComponent implements OnInit { return this.purchaseBaseAmount; } const rate = f.purchaseRate || 100; - return Math.round((this.purchaseBaseAmount * rate) / 100); + return Math.round(this.purchaseBaseAmount * rate) / 100; } /** Stamp duty + brokerage + other charges — paid with the settlement, booked to expense GLs. */ @@ -627,7 +627,7 @@ export class BondInvestmentComponent implements OnInit { get purchaseNextCouponAmount(): number { const f = this.purchaseForm; - return f.intRate ? Math.round((this.purchaseBaseAmount * f.intRate) / 200) : 0; + return f.intRate ? Math.round((this.purchaseBaseAmount * f.intRate) / 2) / 100 : 0; } get purchaseDaysToNextCoupon(): number { @@ -648,7 +648,7 @@ export class BondInvestmentComponent implements OnInit { if (!f.intRate || !f.lastIntDt || !f.dealDate) { return 0; } - return Math.round((this.purchaseBaseAmount * (f.intRate / 100) * this.purchaseAccruedDays) / 360); + return Math.round((this.purchaseBaseAmount * f.intRate * this.purchaseAccruedDays) / 360) / 100; } get purchaseAccruedDays(): number { @@ -663,7 +663,7 @@ export class BondInvestmentComponent implements OnInit { if (this.isMutualFund || !f.faceValue || !f.purchaseRate) { return 0; } - return Math.round((f.faceValue * (f.purchaseRate - 100)) / 100); + return Math.round(f.faceValue * (f.purchaseRate - 100)) / 100; } /** Re-suggests the last coupon date when the deal date changes. */ @@ -1022,12 +1022,17 @@ export class BondInvestmentComponent implements OnInit { } /** Posts a pending deal to GL dated its settlement date; under maker-checker it waits in the Checker Inbox. */ + /** The purchase being settled; its button stays disabled so it cannot be sent twice. */ + settlingDealId: number | null = null; + settleDeal(h: InvestmentHolding): void { - if (!h.id) { + if (!h.id || this.settlingDealId !== null) { return; } + this.settlingDealId = h.id; this.investmentService.settlePurchase(h.id).subscribe({ next: (res) => { + this.settlingDealId = null; this.toast( res.rollbackTransaction ? `Settlement of deal ${h.dealNo} sent for approval (Checker Inbox).` @@ -1035,7 +1040,10 @@ export class BondInvestmentComponent implements OnInit { ); this.loadPurchases(); }, - error: (err) => this.toast(apiErrorMessage(err)) + error: (err) => { + this.settlingDealId = null; + this.toast(apiErrorMessage(err)); + } }); } @@ -1345,29 +1353,44 @@ export class BondInvestmentComponent implements OnInit { }); } + /** The pending sale being settled or dropped; its buttons stay disabled so it cannot be sent twice. */ + busySaleId: number | null = null; + settleSale(s: InvestmentSaleDto): void { - if (!s.id) { + if (!s.id || this.busySaleId !== null) { return; } + this.busySaleId = s.id; this.investmentService.settleSale(s.id).subscribe({ next: (res) => { + this.busySaleId = null; + this.sales = this.sales.filter((x) => x.id !== s.id); this.toast(`Sale of deal ${s.dealNo} settled — voucher ${res.resourceIdentifier ?? ''}.`); this.loadPurchases(); }, - error: (err) => this.toast(apiErrorMessage(err)) + error: (err) => { + this.busySaleId = null; + this.toast(apiErrorMessage(err)); + } }); } dropSale(s: InvestmentSaleDto): void { - if (!s.id) { + if (!s.id || this.busySaleId !== null) { return; } + this.busySaleId = s.id; this.investmentService.deleteSale(s.id).subscribe({ next: () => { + this.busySaleId = null; + this.sales = this.sales.filter((x) => x.id !== s.id); this.toast(`Pending sale of deal ${s.dealNo} dropped.`); this.loadInterestLog(); }, - error: (err) => this.toast(apiErrorMessage(err)) + error: (err) => { + this.busySaleId = null; + this.toast(apiErrorMessage(err)); + } }); } @@ -1723,7 +1746,7 @@ export class BondInvestmentComponent implements OnInit { for (let k = 1; parseDDMMYYYY(addPeriod(h.lastIntDate, 0, 6 * k)) <= parseDDMMYYYY(asOn); k++) { lastCoupon = addPeriod(h.lastIntDate, 0, 6 * k); } - return Math.round((h.faceValue * (h.interestRate / 100) * days360(lastCoupon, asOn)) / 360); + return Math.round((h.faceValue * h.interestRate * days360(lastCoupon, asOn)) / 360) / 100; } maturity(h: InvestmentHolding): number { @@ -1749,6 +1772,7 @@ export class BondInvestmentComponent implements OnInit { title: `${this.instrumentLabel(this.bondType)} Holdings Register`, subtitle: `As on ${this.asOnDate}`, headers: [ + 'Deal No', 'Cert No', 'Script', 'Party', @@ -1762,7 +1786,8 @@ export class BondInvestmentComponent implements OnInit { 'Status' ], widths: [ - 14, + 16, + 10, 26, 22, 12, @@ -1775,6 +1800,7 @@ export class BondInvestmentComponent implements OnInit { 12 ], rows: this.holdingsForType.map((h) => [ + h.dealNo ?? '', h.certNo, h.scriptName, h.party, diff --git a/src/app/investment/fd-investment/fd-investment.component.html b/src/app/investment/fd-investment/fd-investment.component.html index f29fee509..8100a7073 100644 --- a/src/app/investment/fd-investment/fd-investment.component.html +++ b/src/app/investment/fd-investment/fd-investment.component.html @@ -106,6 +106,9 @@ @if (f.closureTds) { · TDS ₹{{ f.closureTds | number: '1.2-2' }} } + @if (f.closurePenal) { + · penal ₹{{ f.closurePenal | number: '1.2-2' }} + } } @if (f.renewedFromDealNo) { @@ -240,7 +243,14 @@ /> @if (q.payout !== 'MATURITY') {
Cert NoDeal No / Cert No Script Party Units
- {{ h.certNo }} + {{ h.dealNo }} +
Cert {{ h.certNo }}
{{ h.scriptName }} {{ h.party }} @if (h.status === 'PENDING_SETTLEMENT') { - } @else if (h.glTransactionId) { GL {{ h.glTransactionId }} @@ -357,7 +363,7 @@ @if (!isMutualFund) { @if (r.type === 'COUPON') { -
Dr Bank ₹{{ r.grossInterest | number: '1.0-0' }}
+
Dr Bank ₹{{ r.grossInterest | number: '1.2-2' }}
@if (r.alreadyBooked > 0) {
- Cr {{ interestReceivableGl.glCode }} Int. Receivable ₹{{ r.alreadyBooked | number: '1.0-0' }} + Cr {{ interestReceivableGl.glCode }} Int. Receivable ₹{{ r.alreadyBooked | number: '1.2-2' }}
} -
Cr {{ sc.interestGlCode }} Int. Income ₹{{ r.amount | number: '1.0-0' }}
+
Cr {{ sc.interestGlCode }} Int. Income ₹{{ r.amount | number: '1.2-2' }}
} @else { -
Dr {{ interestReceivableGl.glCode }} Int. Receivable ₹{{ r.amount | number: '1.0-0' }}
-
Cr {{ sc.interestGlCode }} Int. Income ₹{{ r.amount | number: '1.0-0' }}
+
Dr {{ interestReceivableGl.glCode }} Int. Receivable ₹{{ r.amount | number: '1.2-2' }}
+
Cr {{ sc.interestGlCode }} Int. Income ₹{{ r.amount | number: '1.2-2' }}
}
@@ -1129,8 +1135,22 @@ ₹{{ s.netAmount | number: '1.2-2' }} ₹{{ s.profitLoss | number: '1.2-2' }} - - + +
+ @if (closure.type === 'RENEWAL' && closureTopUp !== 0 && !closure.diffBankGlAccountId) { +

+ Choose the bank for the {{ closureTopUp > 0 ? 'extra amount' : 'amount taken out' }} to complete the + voucher. +

+ }

Voucher dated {{ q.closeDate }}. The lines follow the amounts entered above.

diff --git a/src/app/investment/fd-investment/fd-investment.component.ts b/src/app/investment/fd-investment/fd-investment.component.ts index 1643c7485..e80a64160 100644 --- a/src/app/investment/fd-investment/fd-investment.component.ts +++ b/src/app/investment/fd-investment/fd-investment.component.ts @@ -139,7 +139,7 @@ export class FdInvestmentComponent implements OnInit { ngOnInit(): void { this.investmentService.getParties().subscribe({ - next: (ps) => (this.fdParties = ps.filter((p) => p.bondType === 'FD' && p.active)), + next: (ps) => (this.fdParties = ps.filter((p) => p.bondType === 'FD' && p.active && !!p.interestGlAccountId)), error: (err) => this.toast(`Could not load FD banks: ${apiErrorMessage(err)}`) }); this.investmentService.getAssetGlAccounts().subscribe({ @@ -162,9 +162,19 @@ export class FdInvestmentComponent implements OnInit { this.activeTab = tab; if (tab === 'purchase') { this.refreshQuote(); + } else if (tab === 'closure' && this.closure.fdId) { + // Interest may have been posted on the other tab meanwhile: work the closure out again. + this.refreshClosureQuote(); + } else if (tab === 'interest' && this.interestFdId) { + this.loadSchedule(); } } + /** A complete DD-MM-YYYY date: half-typed dates are not sent to the server. */ + private static fullDate(d: string | null | undefined): boolean { + return !!d && /^\d{2}-\d{2}-\d{4}$/.test(d); + } + private loadFds(): void { this.investmentService.getFds().subscribe({ next: (list) => (this.fds = list), @@ -196,7 +206,7 @@ export class FdInvestmentComponent implements OnInit { /** Period (years / months / days) sets the maturity date; the maturity date can also be typed directly. */ onPeriodChange(): void { const f = this.form; - if (f.dealDate && (f.periodYears || f.periodMonths || f.periodDays)) { + if (FdInvestmentComponent.fullDate(f.dealDate) && (f.periodYears || f.periodMonths || f.periodDays)) { f.maturityDate = addPeriod(f.dealDate, f.periodYears ?? 0, f.periodMonths ?? 0, f.periodDays ?? 0); } this.refreshQuote(); @@ -206,6 +216,9 @@ export class FdInvestmentComponent implements OnInit { refreshQuote(): void { const f = this.form; this.quoteError = ''; + if (f.dealDate && !FdInvestmentComponent.fullDate(f.dealDate)) { + return; + } const params: Record = { dealDate: f.dealDate || todayDDMMYYYY(), payout: f.payout, @@ -220,7 +233,7 @@ export class FdInvestmentComponent implements OnInit { if (f.intRate) { params['intRate'] = f.intRate; } - if (f.maturityDate) { + if (FdInvestmentComponent.fullDate(f.maturityDate)) { params['maturityDate'] = f.maturityDate; } this.investmentService.quoteFd(params).subscribe({ @@ -315,6 +328,7 @@ export class FdInvestmentComponent implements OnInit { } resetPurchase(): void { + this.quoteError = ''; this.form = this.blankForm(); this.refreshQuote(); } @@ -362,7 +376,8 @@ export class FdInvestmentComponent implements OnInit { if (this.payoutReceived) { body['receivedAmount'] = this.payoutReceived; } - if (this.payoutTds) { + // Blank TDS: the system works it out at the bank's rate; an entered figure (even 0) is used as it is. + if (entered(this.payoutTds)) { body['tds'] = this.payoutTds; } if (this.payoutBankGlAccountId) { @@ -434,14 +449,29 @@ export class FdInvestmentComponent implements OnInit { this.closureQuote = null; return; } + if (c.type === 'PREMATURE' && c.closeDate && !FdInvestmentComponent.fullDate(c.closeDate)) { + return; + } const params: Record = { type: c.type }; if (c.type === 'PREMATURE' && c.closeDate) { params['closeDate'] = c.closeDate; } + if (c.type === 'RENEWAL') { + if (c.diffBankGlAccountId) { + params['diffBankGlAccountId'] = c.diffBankGlAccountId; + } + } else { + if (c.principalBankGlAccountId) { + params['principalBankGlAccountId'] = c.principalBankGlAccountId; + } + if (c.interestBankGlAccountId) { + params['interestBankGlAccountId'] = c.interestBankGlAccountId; + } + } if (c.receivedAmount) { params['receivedAmount'] = c.receivedAmount; } - if (c.tds) { + if (entered(c.tds)) { params['tds'] = c.tds; } if (c.type === 'RENEWAL' && c.renewalAmount) { @@ -464,7 +494,7 @@ export class FdInvestmentComponent implements OnInit { return; } const base = { locale: 'en', dateFormat: 'dd-MM-yyyy', ...(c.remarks.trim() ? { remarks: c.remarks.trim() } : {}) }; - const tds = c.tds ? { tds: c.tds } : {}; + const tds = entered(c.tds) ? { tds: c.tds } : {}; let body: object; if (c.type === 'RENEWAL') { if (!c.newCertNo.trim() || !c.newIntRate || !c.newMaturityDate) { @@ -587,3 +617,8 @@ export class FdInvestmentComponent implements OnInit { this.toastTimer = setTimeout(() => (this.toastVisible = false), 4000); } } + +/** A number box the user filled in (0 counts; blank does not). */ +function entered(v: number | null | undefined | string): boolean { + return v !== null && v !== undefined && v !== ''; +} diff --git a/src/app/investment/investment-data.ts b/src/app/investment/investment-data.ts index 0310b6698..848d7b2b0 100644 --- a/src/app/investment/investment-data.ts +++ b/src/app/investment/investment-data.ts @@ -1910,6 +1910,9 @@ export interface PartyMaster { interestGlAccountId?: number; interestGlCode?: string; interestGlName?: string; + /** Whether this bank deducts TDS from FD interest (some do, some do not), and at what rate (%). */ + tdsApplicable?: boolean; + tdsRate?: number | null; rbiCode?: string; /** Exposure limit for this party (₹). */ limit?: number | null; diff --git a/src/app/investment/investment-masters/investment-masters.component.html b/src/app/investment/investment-masters/investment-masters.component.html index 10b5458ca..8105906e0 100644 --- a/src/app/investment/investment-masters/investment-masters.component.html +++ b/src/app/investment/investment-masters/investment-masters.component.html @@ -157,7 +157,7 @@ * GL A/c No. @@ -172,6 +172,19 @@ } + + @if (partyForm.tdsApplicable) { + + } }