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>
|
||||||
</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) {
|
@if (preview?.savingsInterestPending?.length) {
|
||||||
<div class="bd-warn">
|
<div class="bd-warn">
|
||||||
<mat-icon>warning</mat-icon>
|
<mat-icon>warning</mat-icon>
|
||||||
|
|||||||
@@ -669,6 +669,9 @@
|
|||||||
<button mat-menu-item routerLink="/reports/fd-interest-posting" tabindex="0">
|
<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
|
<i class="fa fa-clipboard-check menu-item-icon"></i> FD Interest Posting Reports
|
||||||
</button>
|
</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">
|
<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)
|
<i class="fa fa-table menu-item-icon"></i> Interest Posting Summary (Branch-wise)
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -28,6 +28,11 @@
|
|||||||
<button class="fd-btn" (click)="openOutsideApproval()" [disabled]="outsideApprovalLoading">
|
<button class="fd-btn" (click)="openOutsideApproval()" [disabled]="outsideApprovalLoading">
|
||||||
<fa-icon icon="bolt"></fa-icon>Posted outside approval
|
<fa-icon icon="bolt"></fa-icon>Posted outside approval
|
||||||
</button>
|
</button>
|
||||||
|
@if (isHeadOffice) {
|
||||||
|
<button class="fd-btn" (click)="openProductSummary()" [disabled]="productSummaryLoading">
|
||||||
|
<fa-icon icon="table"></fa-icon>Product summary
|
||||||
|
</button>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -376,6 +381,108 @@
|
|||||||
</div>
|
</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">
|
||||||
<div class="fd-card-h">
|
<div class="fd-card-h">
|
||||||
<h2>Reports{{ postingDate ? ' for ' + postingDate : '' }}</h2>
|
<h2>Reports{{ postingDate ? ' for ' + postingDate : '' }}</h2>
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
FdInterestReport,
|
FdInterestReport,
|
||||||
FdInterestReportDetail,
|
FdInterestReportDetail,
|
||||||
FdInterestPostingReportService,
|
FdInterestPostingReportService,
|
||||||
|
FdInterestSummaryRow,
|
||||||
FdOutsideApprovalRow,
|
FdOutsideApprovalRow,
|
||||||
FdReportScope,
|
FdReportScope,
|
||||||
FdType
|
FdType
|
||||||
@@ -534,6 +535,138 @@ export class FdInterestPostingReportComponent implements OnInit {
|
|||||||
return [...map.values()];
|
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 {
|
private businessDateIso(): string {
|
||||||
const businessDate = this.settingsService.businessDate as any;
|
const businessDate = this.settingsService.businessDate as any;
|
||||||
const parsed = businessDate ? new Date(businessDate) : new Date();
|
const parsed = businessDate ? new Date(businessDate) : new Date();
|
||||||
|
|||||||
@@ -100,6 +100,12 @@ const routes: Routes = [
|
|||||||
(m) => m.FdInterestPostingReportComponent
|
(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',
|
path: 'dormancy',
|
||||||
data: { title: 'Inactive, Dormant & DEAF', breadcrumb: 'Inactive, Dormant & DEAF' },
|
data: { title: 'Inactive, Dormant & DEAF', breadcrumb: 'Inactive, Dormant & DEAF' },
|
||||||
|
|||||||
Reference in New Issue
Block a user