From 0404197c1b9e49daa47c150ecfb01a34f558f0e3 Mon Sep 17 00:00:00 2001 From: anshul Date: Wed, 14 Oct 2026 15:11:43 +0530 Subject: [PATCH] feat(investment): interest voucher posting UI, home pending alert and day-end reminder --- .../branch-day/branch-day.component.html | 17 ++++ src/app/home/home.component.html | 17 ++++ src/app/home/home.component.scss | 36 ++++++++ src/app/home/home.component.ts | 14 ++++ .../bond-investment.component.html | 82 +++++++++++++++++-- .../bond-investment.component.ts | 74 +++++++++++++++++ src/app/investment/investment.service.ts | 41 ++++++++++ 7 files changed, 273 insertions(+), 8 deletions(-) diff --git a/src/app/cashier/branch-day/branch-day.component.html b/src/app/cashier/branch-day/branch-day.component.html index 9c13b940d..029026b90 100644 --- a/src/app/cashier/branch-day/branch-day.component.html +++ b/src/app/cashier/branch-day/branch-day.component.html @@ -386,6 +386,23 @@ } + + @if (preview?.investmentInterestPending?.length) { +
+ notifications_active +
+ Reminder: investment interest vouchers are pending. + @for (t of preview.investmentInterestPending; track t.purchaseId) { +
+ {{ t.lineType === 'COUPON' ? 'Coupon' : 'Provision' }} · {{ t.scriptDesc }} · deal {{ t.dealNo }} · + {{ t.intDate }} · ₹{{ (t.lineType === 'COUPON' ? t.grossInterest : t.amount) | number: '1.2-2' }} +
+ } + Post them under Investment → Govt Security / Bond → 03 Interest Entry. You can still close the day. +
+
+ } @if (preview?.savingsInterestPending?.length) {
warning diff --git a/src/app/home/home.component.html b/src/app/home/home.component.html index 787b209a1..6cb51900a 100644 --- a/src/app/home/home.component.html +++ b/src/app/home/home.component.html @@ -47,6 +47,23 @@
+ + @if (pendingInvestmentInterest.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 }} + + } + + Open Interest Entry → + + } +
diff --git a/src/app/home/home.component.scss b/src/app/home/home.component.scss index c2048c15f..701542b70 100644 --- a/src/app/home/home.component.scss +++ b/src/app/home/home.component.scss @@ -424,3 +424,39 @@ $accent: #0ea5e9; grid-template-columns: 1fr; } } + +.investment-interest-alert { + display: flex; + align-items: flex-start; + gap: 12px; + margin: 12px 0; + padding: 12px 16px; + border: 1px solid #f5c26b; + border-left: 4px solid #e08e0b; + border-radius: 8px; + background: #fff8e6; + color: #5c3d00; + text-decoration: none; + + .iia-icon { + font-size: 20px; + line-height: 1.2; + } + + .iia-text { + display: flex; + flex: 1; + flex-direction: column; + gap: 2px; + } + + .iia-item { + font-size: 13px; + } + + .iia-action { + align-self: center; + font-weight: 600; + white-space: nowrap; + } +} diff --git a/src/app/home/home.component.ts b/src/app/home/home.component.ts index dd1ccfd03..ac0abd2ed 100644 --- a/src/app/home/home.component.ts +++ b/src/app/home/home.component.ts @@ -26,6 +26,7 @@ import { PopoverService } from '../configuration-wizard/popover/popover.service' import { ConfigurationWizardService } from '../configuration-wizard/configuration-wizard.service'; import { SettingsService } from 'app/settings/settings.service'; import { DscService } from 'app/users/dsc/dsc.service'; +import { InvestmentInterestDueDto, InvestmentService } from 'app/investment/investment.service'; /** Custom Components */ import { NextStepDialogComponent } from '../configuration-wizard/next-step-dialog/next-step-dialog.component'; @@ -109,6 +110,7 @@ export class HomeComponent implements OnInit, AfterViewInit { this.tenant = this.tenantIdentifier(); this.setFilteredActivities(); this.loadDscStatus(); + this.loadPendingInvestmentInterest(); if (!this.authenticationService.hasDialogBeenShown()) { this.dialog.open(WarningDialogComponent, { disableClose: false, @@ -118,6 +120,18 @@ export class HomeComponent implements OnInit, AfterViewInit { } } + /** Investment interest lines due now or within two days, shown on the home screen so nobody has to look for them. */ + pendingInvestmentInterest: InvestmentInterestDueDto[] = []; + private readonly investmentService = inject(InvestmentService); + + /** Silent when the user cannot see investments or nothing is due — the strip simply does not render. */ + private loadPendingInvestmentInterest(): void { + this.investmentService.getDueInterest().subscribe({ + next: (due) => (this.pendingInvestmentInterest = due ?? []), + error: () => (this.pendingInvestmentInterest = []) + }); + } + /** * Surface a small "DSC verified" badge in the welcome banner when the * signed-in user has at least one ACTIVE DSC certificate. Silent on diff --git a/src/app/investment/bond-investment/bond-investment.component.html b/src/app/investment/bond-investment/bond-investment.component.html index ea4c4f67a..8e1c1c440 100644 --- a/src/app/investment/bond-investment/bond-investment.component.html +++ b/src/app/investment/bond-investment/bond-investment.component.html @@ -693,6 +693,39 @@ @if (activeTab === 'interest') {
+ @if (intDueTasks.length) { + + + + + + + + + + + + + + + @for (t of intDueTasks; track t.purchaseId) { + + + + + + + + + + } + +
Deal NoScriptTypeInt DateAmountStatus
+ {{ t.dealNo }} + {{ t.scriptCode }} — {{ t.scriptDesc }}{{ t.lineType === 'COUPON' ? 'Coupon' : 'Provision' }}{{ t.intDate }}₹{{ (t.lineType === 'COUPON' ? t.grossInterest : t.amount) | number: '1.2-2' }}{{ t.overdue ? 'Overdue — post now' : t.postable ? 'Due today' : 'Due on ' + t.intDate }} + +
+ }

Days are 30/360. A provision books the interest earned since the last coupon; the coupon then clears what is - already booked. Display only — nothing is posted to GL yet. + already booked. Post each line with Generate Voucher on or after its date, in date order — the JV voucher is + passed as it is raised.

+
+ + +
@@ -796,10 +845,11 @@ + - @for (r of sc.rows; track r.intDate) { + @for (r of intVisibleRows; track r.type + r.intDate) { + } @empty { - @@ -840,8 +906,8 @@
Already Booked Amount AccountingVoucher
{{ r.type === 'COUPON' ? 'Coupon' : 'Provision' }} @@ -829,10 +879,26 @@
Cr {{ sc.interestGlCode }} Int. Income ₹{{ r.amount | number: '1.0-0' }}
}
+ @if (r.posted) { + Posted · {{ r.voucherNo }} + } @else if (r.postable) { + + } @else if (r.dueSoon) { + 🔔 Due {{ r.intDate }} + } +
+ No interest lines — check the rate, last interest date and maturity date.

- Interest income GL: {{ sc.interestGlCode }} — {{ sc.interestGlName }}. Posting to GL will be enabled after - approval. + Interest income GL: {{ sc.interestGlCode }} — {{ sc.interestGlName }} · Interest receivable GL: + {{ interestReceivableGl.glCode }}.

} @else {

diff --git a/src/app/investment/bond-investment/bond-investment.component.ts b/src/app/investment/bond-investment/bond-investment.component.ts index dbdee702a..72bc94367 100644 --- a/src/app/investment/bond-investment/bond-investment.component.ts +++ b/src/app/investment/bond-investment/bond-investment.component.ts @@ -9,6 +9,7 @@ import { Component, OnInit, inject } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; +import { ActivatedRoute } from '@angular/router'; import { DateDdmmyyyyPickerComponent } from '../date-ddmmyyyy-picker/date-ddmmyyyy-picker.component'; import { SCRIPT_MASTER, @@ -56,6 +57,8 @@ import { import { exportWorkbook } from '../investment-export'; import { InvestmentDailyRateDto, + InvestmentInterestDueDto, + InvestmentInterestRowDto, InvestmentInterestScheduleDto, InvestmentPurchaseDto, InvestmentService, @@ -205,9 +208,14 @@ interface BondSellForm { }) export class BondInvestmentComponent implements OnInit { private readonly investmentService = inject(InvestmentService); + private readonly route = inject(ActivatedRoute); /** Script and party dropdowns offer the masters saved in the backend. */ ngOnInit(): void { + // The home-screen interest alert links here with ?tab=interest. + if (this.route.snapshot.queryParamMap.get('tab') === 'interest') { + this.activeTab = 'interest'; + } this.investmentService.syncSharedMasters().subscribe({ error: (err) => this.toast(`Could not load Script / Party masters: ${apiErrorMessage(err)}`) }); @@ -222,6 +230,7 @@ export class BondInvestmentComponent implements OnInit { error: (err) => this.toast(`Could not load GL accounts: ${apiErrorMessage(err)}`) }); this.loadPurchases(); + this.loadDueInterest(); } /** GL id for a GL code — the purchase API takes ids, the form works with codes. */ @@ -307,6 +316,13 @@ export class BondInvestmentComponent implements OnInit { intAsOnDate = this.asOnDate; intSchedule: InvestmentInterestScheduleDto | null = null; intLoading = false; + /** Interest lines of all holdings due now or within two days — what Jitendra Sir asked to be reminded of. */ + intDueTasks: InvestmentInterestDueDto[] = []; + /** By default the schedule shows only lines up to the end of next year (and anything already posted). */ + intShowFullSchedule = false; + /** Bank a coupon is received into; defaults to the purchase's paying bank. */ + intBankGlCode = ''; + intPosting = false; readonly interestReceivableGl = INTEREST_RECEIVABLE_GL; // Write-off @@ -1052,6 +1068,7 @@ export class BondInvestmentComponent implements OnInit { this.investmentService.getInterestSchedule(this.intPurchaseId, this.intAsOnDate || undefined).subscribe({ next: (schedule) => { this.intSchedule = schedule; + this.intBankGlCode = schedule.bankGlCode; this.intLoading = false; }, error: (err) => { @@ -1062,6 +1079,63 @@ export class BondInvestmentComponent implements OnInit { }); } + loadDueInterest(): void { + this.investmentService.getDueInterest().subscribe({ + next: (tasks) => (this.intDueTasks = tasks), + error: () => (this.intDueTasks = []) + }); + } + + /** Opens a due task's holding in the schedule below. */ + openDueTask(t: InvestmentInterestDueDto): void { + this.intType = ''; + this.intScript = ''; + this.intPurchaseId = t.purchaseId; + this.retrieveInterestSchedule(); + } + + /** Lines shown: up to 31 December of next year, plus anything already posted — unless the full schedule is asked for. */ + get intVisibleRows(): InvestmentInterestRowDto[] { + const rows = this.intSchedule?.rows ?? []; + if (this.intShowFullSchedule) { + return rows; + } + const upTo = new Date(new Date().getFullYear() + 1, 11, 31); + return rows.filter((r) => r.posted || parseDDMMYYYY(r.intDate) <= upTo); + } + + /** Posts one interest line with its JV voucher (passed as it is raised), then refreshes the schedule and tasks. */ + generateVoucher(r: InvestmentInterestRowDto): void { + const sc = this.intSchedule; + if (!sc || this.intPosting) { + return; + } + const bankGlAccountId = + r.type === 'COUPON' ? (this.glIdByCode.get(this.intBankGlCode) ?? sc.bankGlAccountId) : undefined; + const body = { + lineType: r.type, + intDate: r.intDate, + ...(bankGlAccountId ? { bankGlAccountId } : {}), + dateFormat: 'dd-MM-yyyy', + locale: 'en' + }; + this.intPosting = true; + this.investmentService.postInterest(sc.purchaseId, body).subscribe({ + next: (res) => { + this.intPosting = false; + this.toast( + `${r.type === 'COUPON' ? 'Coupon' : 'Provision'} of ${r.intDate} posted — voucher ${res.resourceIdentifier ?? ''}.` + ); + this.retrieveInterestSchedule(); + this.loadDueInterest(); + }, + error: (err) => { + this.intPosting = false; + this.toast(apiErrorMessage(err)); + } + }); + } + /** Total of the lines falling due up to the as-on date. */ get intDueTotal(): number { return (this.intSchedule?.rows ?? []).filter((r) => r.due).reduce((sum, r) => sum + r.amount, 0); diff --git a/src/app/investment/investment.service.ts b/src/app/investment/investment.service.ts index b9723b0e4..076b8f8c6 100644 --- a/src/app/investment/investment.service.ts +++ b/src/app/investment/investment.service.ts @@ -175,6 +175,19 @@ export class InvestmentService { } /** Coupon and 31 Mar / 30 Sep provision lines of a holding up to maturity (calculated, not posted). */ + /** Interest lines of all holdings that are due now or within the next few days. */ + getDueInterest(): Observable { + return this.http.get('/investment/interest-postings/due'); + } + + /** Posts one interest line (coupon / provision) with its JV voucher; the voucher number comes back as resourceIdentifier. */ + postInterest(purchaseId: number, body: object): Observable<{ resourceIdentifier?: string; transactionId?: string }> { + return this.http.post<{ resourceIdentifier?: string; transactionId?: string }>( + `/investment/purchases/${purchaseId}/interest-postings`, + body + ); + } + getInterestSchedule(purchaseId: number, asOnDate?: string): Observable { const params = asOnDate ? `?asOnDate=${encodeURIComponent(asOnDate)}` : ''; return this.http.get( @@ -347,6 +360,30 @@ export interface InvestmentInterestRowDto { alreadyBooked: number; amount: number; due: boolean; + /** Posted to GL: its JV voucher and GL transaction. */ + posted: boolean; + voucherNo?: string; + glTransactionId?: string; + postedOnDate?: string; + /** The next line to post and its date has come. */ + postable: boolean; + /** Unposted and due within the reminder window (or past). */ + dueSoon: boolean; +} + +/** A holding's next interest line that is due (or due within the reminder window) — a task to post. */ +export interface InvestmentInterestDueDto { + purchaseId: number; + dealNo: string; + certNo: string; + scriptCode: string; + scriptDesc: string; + lineType: 'COUPON' | 'PROVISION'; + intDate: string; + grossInterest: number; + amount: number; + postable: boolean; + overdue: boolean; } export interface InvestmentInterestScheduleDto { @@ -369,6 +406,10 @@ export interface InvestmentInterestScheduleDto { asOnDate: string; interestGlCode: string; interestGlName: string; + bankGlAccountId: number; + bankGlCode: string; + bankGlName: string; + status: string; rows: InvestmentInterestRowDto[]; }