feat(fixed-deposits): maturity approval UI, scoped to deposits with no closure instruction
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 <noreply@anthropic.com>
This commit is contained in:
@@ -386,6 +386,40 @@
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Deposits whose term ends today — status, final interest and any renewal/transfer instruction
|
||||
all run together the moment this report is approved and the day closes. -->
|
||||
@if (preview?.fdMaturityDue?.length) {
|
||||
<div class="bd-calc bd-calc--fd">
|
||||
<div class="bd-calc__head">
|
||||
<mat-icon>event_available</mat-icon>
|
||||
<strong>Fixed deposits maturing today</strong>
|
||||
</div>
|
||||
@for (d of preview.fdMaturityDue; track $index) {
|
||||
<div class="bd-calc__row">
|
||||
<span>
|
||||
{{ d.accountCount }} deposit(s)
|
||||
@if (d.reportStatus === 'APPROVED' || d.reportStatus === 'POSTED') {
|
||||
<span class="bd-fd-ok">{{ d.reportStatus }}</span>
|
||||
} @else {
|
||||
<span class="bd-fd-pending">{{ d.reportStatus === 'NONE' ? 'NO REPORT' : d.reportStatus }}</span>
|
||||
}
|
||||
</span>
|
||||
<span class="bd-calc__amt">₹{{ d.totalInterest | number: '1.2-2' }}</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@if (preview?.fdMaturityPending?.length) {
|
||||
<div class="bd-warn">
|
||||
<mat-icon>warning</mat-icon>
|
||||
<div>
|
||||
<strong>The day cannot be closed yet.</strong> 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.
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@if (preview?.savingsInterestPending?.length) {
|
||||
<div class="bd-warn">
|
||||
<mat-icon>warning</mat-icon>
|
||||
|
||||
@@ -669,6 +669,9 @@
|
||||
<button mat-menu-item routerLink="/reports/fd-interest-posting" tabindex="0">
|
||||
<i class="fa fa-clipboard-check menu-item-icon"></i> FD Interest Posting Reports
|
||||
</button>
|
||||
<button mat-menu-item routerLink="/reports/fd-maturity" tabindex="0">
|
||||
<i class="fa fa-calendar-check menu-item-icon"></i> FD Maturity Reports
|
||||
</button>
|
||||
<button mat-menu-item routerLink="/reports/interest-posting-summary" tabindex="0">
|
||||
<i class="fa fa-table menu-item-icon"></i> Interest Posting Summary (Branch-wise)
|
||||
</button>
|
||||
|
||||
@@ -28,6 +28,11 @@
|
||||
<button class="fd-btn" (click)="openOutsideApproval()" [disabled]="outsideApprovalLoading">
|
||||
<fa-icon icon="bolt"></fa-icon>Posted outside approval
|
||||
</button>
|
||||
@if (isHeadOffice) {
|
||||
<button class="fd-btn" (click)="openProductSummary()" [disabled]="productSummaryLoading">
|
||||
<fa-icon icon="table"></fa-icon>Product summary
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -376,6 +381,108 @@
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (showProductSummary) {
|
||||
<div class="fd-card">
|
||||
<div class="fd-detail-head">
|
||||
<div>
|
||||
<h3>Interest by product</h3>
|
||||
<div class="muted">
|
||||
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.
|
||||
</div>
|
||||
</div>
|
||||
<div class="fd-total">
|
||||
<span>Total, all products</span>
|
||||
<strong>₹{{ productSummaryGrandTotal.total | number: '1.2-2' }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style="
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-end;
|
||||
gap: 10px;
|
||||
padding: 14px 20px;
|
||||
border-bottom: 1px solid var(--fd-border-soft);
|
||||
"
|
||||
>
|
||||
<label class="fd-date">
|
||||
From
|
||||
<input type="date" [(ngModel)]="productSummaryFrom" (change)="loadProductSummary()" />
|
||||
</label>
|
||||
<label class="fd-date">
|
||||
To
|
||||
<input type="date" [(ngModel)]="productSummaryTo" (change)="loadProductSummary()" />
|
||||
</label>
|
||||
<button class="fd-btn fd-btn--slim" (click)="loadProductSummary()" [disabled]="productSummaryLoading">
|
||||
<fa-icon icon="undo"></fa-icon>Refresh
|
||||
</button>
|
||||
<button class="fd-btn fd-btn--slim" (click)="closeProductSummary()">
|
||||
<fa-icon icon="times"></fa-icon>Close
|
||||
</button>
|
||||
</div>
|
||||
@if (productSummaryLoading) {
|
||||
<div class="fd-empty">Adding up interest by product…</div>
|
||||
} @else {
|
||||
<div class="fd-table-wrap">
|
||||
<table class="fd-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Product</th>
|
||||
<th>Scheme</th>
|
||||
<th>Posting period</th>
|
||||
<th class="num">Posted (report)</th>
|
||||
<th class="num">Pending approval</th>
|
||||
<th class="num">Posted at maturity</th>
|
||||
<th class="num">Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (row of productSummaryRows; track row.productId) {
|
||||
<tr>
|
||||
<td>
|
||||
<strong>{{ row.productName }}</strong>
|
||||
</td>
|
||||
<td>
|
||||
<span class="fd-chip" [ngClass]="fdTypeClass(row.fdType)">{{ fdTypeShort(row.fdType) }}</span>
|
||||
</td>
|
||||
<td class="fd-sub">{{ row.postingPeriodLabel || '—' }}</td>
|
||||
<td class="num">₹{{ row.posted | number: '1.2-2' }}</td>
|
||||
<td class="num">₹{{ row.pending | number: '1.2-2' }}</td>
|
||||
<td class="num">₹{{ row.maturityPosted | number: '1.2-2' }}</td>
|
||||
<td class="num">
|
||||
<strong>₹{{ row.total | number: '1.2-2' }}</strong>
|
||||
</td>
|
||||
</tr>
|
||||
} @empty {
|
||||
<tr>
|
||||
<td colspan="7" class="fd-empty">No fixed deposit interest in this period.</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colspan="3"><strong>Total</strong></td>
|
||||
<td class="num">
|
||||
<strong>₹{{ productSummaryGrandTotal.posted | number: '1.2-2' }}</strong>
|
||||
</td>
|
||||
<td class="num">
|
||||
<strong>₹{{ productSummaryGrandTotal.pending | number: '1.2-2' }}</strong>
|
||||
</td>
|
||||
<td class="num">
|
||||
<strong>₹{{ productSummaryGrandTotal.maturityPosted | number: '1.2-2' }}</strong>
|
||||
</td>
|
||||
<td class="num">
|
||||
<strong>₹{{ productSummaryGrandTotal.total | number: '1.2-2' }}</strong>
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="fd-card">
|
||||
<div class="fd-card-h">
|
||||
<h2>Reports{{ postingDate ? ' for ' + postingDate : '' }}</h2>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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' },
|
||||
|
||||
Reference in New Issue
Block a user