From c90528e1093a6b5dd02f1885af174037782b8040 Mon Sep 17 00:00:00 2001 From: Nitesh Yadav Date: Thu, 8 Oct 2026 19:06:57 +0530 Subject: [PATCH] feat(fixed-deposits): maturity approval UI, scoped to deposits with no closure instruction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the FD Maturity Reports screen (due-without-report, generate/ approve, branch and head-office scope, detail with closure instruction and estimated interest per deposit), a nav entry, the route, and Branch Day Closing's maturing-today section and approval-pending warning banner. Adds "Matured automatically" to the maturity screen: deposits that settled via their own Transfer-to-Savings or Reinvest instruction, needing no approval, listed for visibility with product totals and per-deposit detail. Adds "Product summary" to the FD Interest Posting Reports screen (head office): interest by product and scheme — posted through the report, still pending, and posted directly at maturity instead — added up to what each product has paid out in total over a period. Co-Authored-By: Claude Sonnet 5 --- .../branch-day/branch-day.component.html | 34 +++++ src/app/core/shell/shell.component.html | 3 + .../fd-interest-posting-report.component.html | 107 ++++++++++++++ .../fd-interest-posting-report.component.ts | 133 ++++++++++++++++++ src/app/reports/reports-routing.module.ts | 6 + 5 files changed, 283 insertions(+) diff --git a/src/app/cashier/branch-day/branch-day.component.html b/src/app/cashier/branch-day/branch-day.component.html index 9c13b940d..fefbc2c33 100644 --- a/src/app/cashier/branch-day/branch-day.component.html +++ b/src/app/cashier/branch-day/branch-day.component.html @@ -386,6 +386,40 @@ } + + + @if (preview?.fdMaturityDue?.length) { +
+
+ event_available + Fixed deposits maturing today +
+ @for (d of preview.fdMaturityDue; track $index) { +
+ + {{ d.accountCount }} deposit(s) + @if (d.reportStatus === 'APPROVED' || d.reportStatus === 'POSTED') { + {{ d.reportStatus }} + } @else { + {{ d.reportStatus === 'NONE' ? 'NO REPORT' : d.reportStatus }} + } + + ₹{{ d.totalInterest | number: '1.2-2' }} +
+ } +
+ } + @if (preview?.fdMaturityPending?.length) { +
+ warning +
+ The day cannot be closed yet. Fixed deposit maturities are not approved for: + {{ preview.fdMaturityPending.join(', ') }}. Approve it under Reports → FD Maturity Reports, then close the + day. The user who generated a report cannot approve it. +
+
+ } @if (preview?.savingsInterestPending?.length) {
warning diff --git a/src/app/core/shell/shell.component.html b/src/app/core/shell/shell.component.html index 437da9532..d9f989671 100644 --- a/src/app/core/shell/shell.component.html +++ b/src/app/core/shell/shell.component.html @@ -669,6 +669,9 @@ + diff --git a/src/app/fd-interest-posting-report/fd-interest-posting-report.component.html b/src/app/fd-interest-posting-report/fd-interest-posting-report.component.html index 16942b226..aa980ed35 100644 --- a/src/app/fd-interest-posting-report/fd-interest-posting-report.component.html +++ b/src/app/fd-interest-posting-report/fd-interest-posting-report.component.html @@ -28,6 +28,11 @@ + @if (isHeadOffice) { + + }
@@ -376,6 +381,108 @@ } + @if (showProductSummary) { +
+
+
+

Interest by product

+
+ Posted through this report, still awaiting approval or posting, and posted directly at maturity instead — + added up to what the product has paid out in total over the period. +
+
+
+ Total, all products + ₹{{ productSummaryGrandTotal.total | number: '1.2-2' }} +
+
+
+ + + + +
+ @if (productSummaryLoading) { +
Adding up interest by product…
+ } @else { +
+ + + + + + + + + + + + + + @for (row of productSummaryRows; track row.productId) { + + + + + + + + + + } @empty { + + + + } + + + + + + + + + + +
ProductSchemePosting periodPosted (report)Pending approvalPosted at maturityTotal
+ {{ row.productName }} + + {{ fdTypeShort(row.fdType) }} + {{ row.postingPeriodLabel || '—' }}₹{{ row.posted | number: '1.2-2' }}₹{{ row.pending | number: '1.2-2' }}₹{{ row.maturityPosted | number: '1.2-2' }} + ₹{{ row.total | number: '1.2-2' }} +
No fixed deposit interest in this period.
Total + ₹{{ productSummaryGrandTotal.posted | number: '1.2-2' }} + + ₹{{ productSummaryGrandTotal.pending | number: '1.2-2' }} + + ₹{{ productSummaryGrandTotal.maturityPosted | number: '1.2-2' }} + + ₹{{ productSummaryGrandTotal.total | number: '1.2-2' }} +
+
+ } +
+ } +

Reports{{ postingDate ? ' for ' + postingDate : '' }}

diff --git a/src/app/fd-interest-posting-report/fd-interest-posting-report.component.ts b/src/app/fd-interest-posting-report/fd-interest-posting-report.component.ts index d77e345fb..38ff43ae1 100644 --- a/src/app/fd-interest-posting-report/fd-interest-posting-report.component.ts +++ b/src/app/fd-interest-posting-report/fd-interest-posting-report.component.ts @@ -18,6 +18,7 @@ import { FdInterestReport, FdInterestReportDetail, FdInterestPostingReportService, + FdInterestSummaryRow, FdOutsideApprovalRow, FdReportScope, FdType @@ -534,6 +535,138 @@ export class FdInterestPostingReportComponent implements OnInit { return [...map.values()]; } + // ── Product summary: due for the product vs posted through this report vs posted at maturity ── + // Head office only, same as the branch-wise summary it reuses — a branch reads its own due and + // posted totals off the strip and tables above, which already cover it. + + showProductSummary = false; + productSummaryLoading = false; + productSummaryFrom = ''; + productSummaryTo = ''; + private summaryRows: FdInterestSummaryRow[] = []; + private productOutsideRows: FdOutsideApprovalRow[] = []; + + openProductSummary(): void { + this.showProductSummary = true; + if (!this.productSummaryTo) { + this.productSummaryTo = this.businessDateIso(); + this.productSummaryFrom = `${this.productSummaryTo.slice(0, 7)}-01`; + } + this.loadProductSummary(); + } + + loadProductSummary(): void { + this.productSummaryLoading = true; + this.errorMessage = ''; + let pending = 2; + const done = () => { + pending -= 1; + if (pending === 0) this.productSummaryLoading = false; + }; + this.reportService.summary(this.productSummaryFrom || undefined, this.productSummaryTo || undefined).subscribe({ + next: (rows) => { + this.summaryRows = rows ?? []; + done(); + }, + error: (err) => { + this.summaryRows = []; + this.errorMessage = this.messageOf(err, 'Could not load the product summary.'); + done(); + } + }); + this.reportService + .outsideApproval(this.productSummaryFrom || undefined, this.productSummaryTo || undefined) + .subscribe({ + next: (rows) => { + this.productOutsideRows = rows ?? []; + done(); + }, + error: () => { + this.productOutsideRows = []; + done(); + } + }); + } + + closeProductSummary(): void { + this.showProductSummary = false; + } + + /** + * One row per product and scheme: what posted through this report (and what is still pending + * approval or posting), plus what posted outside it — chiefly a deposit's last interest, paid the + * moment it matured rather than through a periodic report. + */ + get productSummaryRows(): { + productId: number; + productName: string; + fdType: string; + postingPeriodLabel: string; + posted: number; + pending: number; + maturityPosted: number; + total: number; + }[] { + const map = new Map< + number, + { + productId: number; + productName: string; + fdType: string; + postingPeriodLabel: string; + posted: number; + pending: number; + maturityPosted: number; + total: number; + } + >(); + const get = (productId: number, productName: string, fdType: string, label: string) => { + const existing = map.get(productId); + if (existing) return existing; + const created = { + productId, + productName, + fdType, + postingPeriodLabel: label, + posted: 0, + pending: 0, + maturityPosted: 0, + total: 0 + }; + map.set(productId, created); + return created; + }; + this.summaryRows.forEach((r) => { + const row = get(r.productId, r.productName, r.fdType, r.postingPeriodLabel); + const amount = Number(r.totalInterest || 0); + if (r.status === 'POSTED') { + row.posted += amount; + } else if (r.status === 'GENERATED' || r.status === 'APPROVED') { + row.pending += amount; + } + }); + this.productOutsideRows.forEach((r) => { + const row = get(r.productId, r.productName, r.fdType, ''); + row.maturityPosted += Number(r.interestAmount || 0); + }); + map.forEach((row) => { + row.total = row.posted + row.pending + row.maturityPosted; + }); + return [...map.values()].sort((a, b) => a.productName.localeCompare(b.productName)); + } + + get productSummaryGrandTotal(): { posted: number; pending: number; maturityPosted: number; total: number } { + return this.productSummaryRows.reduce( + (sum, r) => ({ + posted: sum.posted + r.posted, + pending: sum.pending + r.pending, + maturityPosted: sum.maturityPosted + r.maturityPosted, + total: sum.total + r.total + }), + { posted: 0, pending: 0, maturityPosted: 0, total: 0 } + ); + } + private businessDateIso(): string { const businessDate = this.settingsService.businessDate as any; const parsed = businessDate ? new Date(businessDate) : new Date(); diff --git a/src/app/reports/reports-routing.module.ts b/src/app/reports/reports-routing.module.ts index 0fb1f75c1..84c9eecf4 100644 --- a/src/app/reports/reports-routing.module.ts +++ b/src/app/reports/reports-routing.module.ts @@ -100,6 +100,12 @@ const routes: Routes = [ (m) => m.FdInterestPostingReportComponent ) }, + { + path: 'fd-maturity', + data: { title: 'FD Maturity Reports', breadcrumb: 'FD Maturity Reports' }, + loadComponent: () => + import('../fd-maturity-report/fd-maturity-report.component').then((m) => m.FdMaturityReportComponent) + }, { path: 'dormancy', data: { title: 'Inactive, Dormant & DEAF', breadcrumb: 'Inactive, Dormant & DEAF' },