feat(fixed-deposits): show the posting period and GL-reconciled maturity interest
Each report line now shows the span its interest covers — "01 Oct to 31 Dec" rather than just the end date — and the report header shows the product's posting cycle (Monthly, Quarterly, Half-yearly, Yearly). Adds a "Posted outside approval" view: deposit interest posted without this report's approval, chiefly a deposit that matured before its own posting cycle completed. Product-wise totals plus a detail list with when each posting actually reached the account. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -25,6 +25,9 @@
|
||||
<button class="fd-btn fd-btn--primary" (click)="openPreview()" [disabled]="previewLoading">
|
||||
<fa-icon icon="eye"></fa-icon>Preview interest (today)
|
||||
</button>
|
||||
<button class="fd-btn" (click)="openOutsideApproval()" [disabled]="outsideApprovalLoading">
|
||||
<fa-icon icon="bolt"></fa-icon>Posted outside approval
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -227,7 +230,7 @@
|
||||
<td>
|
||||
<span class="fd-chip" [ngClass]="fdTypeClass(r.fdType)">{{ fdTypeShort(r.fdType) }}</span>
|
||||
</td>
|
||||
<td class="fd-mono">{{ r.interestPostingDate }}</td>
|
||||
<td class="fd-mono">{{ r.interestPeriodFrom || '—' }} → {{ r.interestPostingDate }}</td>
|
||||
<td class="num">₹{{ r.depositAmount | number: '1.2-2' }}</td>
|
||||
<td class="num">{{ r.interestRate | number: '1.2-2' }}</td>
|
||||
<td class="fd-mono">{{ r.maturityDate || '—' }}</td>
|
||||
@@ -250,6 +253,129 @@
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (showOutsideApproval) {
|
||||
<div class="fd-card">
|
||||
<div class="fd-detail-head">
|
||||
<div>
|
||||
<h3>Fixed deposit interest posted outside this report</h3>
|
||||
<div class="muted">
|
||||
A deposit that matured before its own posting cycle completed has its last period settled directly, the
|
||||
moment it matures — before any report here could be generated or approved for it. Listed here so
|
||||
nothing paid to a customer is invisible, approved or not.
|
||||
</div>
|
||||
</div>
|
||||
<div class="fd-total">
|
||||
<span>Total</span>
|
||||
<strong>₹{{ outsideApprovalTotal | number: '1.2-2' }}</strong>
|
||||
<span class="muted">{{ outsideApprovalRows.length }} posting(s)</span>
|
||||
</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)]="outsideApprovalFrom" (change)="loadOutsideApproval()" />
|
||||
</label>
|
||||
<label class="fd-date">
|
||||
To
|
||||
<input type="date" [(ngModel)]="outsideApprovalTo" (change)="loadOutsideApproval()" />
|
||||
</label>
|
||||
<button class="fd-btn fd-btn--slim" (click)="loadOutsideApproval()" [disabled]="outsideApprovalLoading">
|
||||
<fa-icon icon="undo"></fa-icon>Refresh
|
||||
</button>
|
||||
<button class="fd-btn fd-btn--slim" (click)="closeOutsideApproval()">
|
||||
<fa-icon icon="times"></fa-icon>Close
|
||||
</button>
|
||||
</div>
|
||||
@if (outsideApprovalLoading) {
|
||||
<div class="fd-empty">Looking for deposit interest posted outside this report…</div>
|
||||
} @else {
|
||||
<div class="fd-table-wrap">
|
||||
<table class="fd-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Product</th>
|
||||
<th>Scheme</th>
|
||||
<th class="num">Deposits</th>
|
||||
<th class="num">Interest</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (g of outsideApprovalGroups; track g.productName) {
|
||||
<tr>
|
||||
<td>
|
||||
<strong>{{ g.productName }}</strong>
|
||||
</td>
|
||||
<td>
|
||||
<span class="fd-chip" [ngClass]="fdTypeClass(g.fdType)">{{ fdTypeShort(g.fdType) }}</span>
|
||||
</td>
|
||||
<td class="num">{{ g.accounts }}</td>
|
||||
<td class="num">₹{{ g.interest | number: '1.2-2' }}</td>
|
||||
</tr>
|
||||
} @empty {
|
||||
<tr>
|
||||
<td colspan="4" class="fd-empty">
|
||||
No fixed deposit interest was posted outside this report in this period.
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@if (outsideApprovalRows.length > 0) {
|
||||
<div class="fd-table-wrap">
|
||||
<table class="fd-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Account no</th>
|
||||
<th>Customer</th>
|
||||
<th>Product</th>
|
||||
<th>Scheme</th>
|
||||
<th>Interest for</th>
|
||||
<th class="num">Interest</th>
|
||||
<th>Posted at</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (r of outsideApprovalRows; track r.transactionId) {
|
||||
<tr>
|
||||
<td class="fd-mono">{{ r.accountNo }}</td>
|
||||
<td>{{ r.clientName }}</td>
|
||||
<td>{{ r.productName }}</td>
|
||||
<td>
|
||||
<span class="fd-chip" [ngClass]="fdTypeClass(r.fdType)">{{ fdTypeShort(r.fdType) }}</span>
|
||||
</td>
|
||||
<td class="fd-mono">{{ r.interestFor }}</td>
|
||||
<td class="num">₹{{ r.interestAmount | number: '1.2-2' }}</td>
|
||||
<td class="fd-sub">{{ r.postedAt | date: 'dd MMM yyyy, HH:mm' }}</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colspan="5"><strong>Total</strong></td>
|
||||
<td class="num">
|
||||
<strong>₹{{ outsideApprovalTotal | number: '1.2-2' }}</strong>
|
||||
</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="fd-card">
|
||||
<div class="fd-card-h">
|
||||
<h2>Reports{{ postingDate ? ' for ' + postingDate : '' }}</h2>
|
||||
@@ -347,6 +473,7 @@
|
||||
<h3>
|
||||
{{ selected.officeName }} · {{ selected.productName }} — {{ selected.postingDate }}
|
||||
<span class="fd-chip" [ngClass]="fdTypeClass(selected.fdType)">{{ fdTypeShort(selected.fdType) }}</span>
|
||||
<span class="fd-badge fd-badge--muted">{{ selected.postingPeriodLabel }}</span>
|
||||
<span class="fd-badge" [ngClass]="statusClass(selected.status)">{{ selected.status }}</span>
|
||||
</h3>
|
||||
<div class="muted">{{ fdTypeLabel(selected.fdType) }}</div>
|
||||
@@ -384,7 +511,7 @@
|
||||
<tr>
|
||||
<td class="fd-mono">{{ l.accountNo }}</td>
|
||||
<td>{{ l.clientName }}</td>
|
||||
<td class="fd-mono">{{ l.interestPostingDate }}</td>
|
||||
<td class="fd-mono">{{ l.interestPeriodFrom || '—' }} → {{ l.interestPostingDate }}</td>
|
||||
<td class="num">₹{{ l.depositAmount | number: '1.2-2' }}</td>
|
||||
<td class="num">{{ l.interestRate | number: '1.2-2' }}</td>
|
||||
<td class="fd-mono">{{ l.maturityDate || '—' }}</td>
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
FdInterestReport,
|
||||
FdInterestReportDetail,
|
||||
FdInterestPostingReportService,
|
||||
FdOutsideApprovalRow,
|
||||
FdReportScope,
|
||||
FdType
|
||||
} from './fd-interest-posting-report.service';
|
||||
@@ -478,6 +479,61 @@ export class FdInterestPostingReportComponent implements OnInit {
|
||||
);
|
||||
}
|
||||
|
||||
// ── Outside approval: deposit interest posted without this report, chiefly at maturity ──
|
||||
|
||||
showOutsideApproval = false;
|
||||
outsideApprovalLoading = false;
|
||||
outsideApprovalRows: FdOutsideApprovalRow[] = [];
|
||||
outsideApprovalFrom = '';
|
||||
outsideApprovalTo = '';
|
||||
|
||||
openOutsideApproval(): void {
|
||||
this.showOutsideApproval = true;
|
||||
if (!this.outsideApprovalTo) {
|
||||
this.outsideApprovalTo = this.businessDateIso();
|
||||
this.outsideApprovalFrom = `${this.outsideApprovalTo.slice(0, 7)}-01`;
|
||||
}
|
||||
this.loadOutsideApproval();
|
||||
}
|
||||
|
||||
loadOutsideApproval(): void {
|
||||
this.outsideApprovalLoading = true;
|
||||
this.errorMessage = '';
|
||||
this.reportService
|
||||
.outsideApproval(this.outsideApprovalFrom || undefined, this.outsideApprovalTo || undefined, undefined, this.tab)
|
||||
.subscribe({
|
||||
next: (rows) => {
|
||||
this.outsideApprovalRows = rows ?? [];
|
||||
this.outsideApprovalLoading = false;
|
||||
},
|
||||
error: (err) => {
|
||||
this.outsideApprovalLoading = false;
|
||||
this.outsideApprovalRows = [];
|
||||
this.errorMessage = this.messageOf(err, 'Could not load deposit interest posted outside this report.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
closeOutsideApproval(): void {
|
||||
this.showOutsideApproval = false;
|
||||
}
|
||||
|
||||
get outsideApprovalTotal(): number {
|
||||
return this.outsideApprovalRows.reduce((sum, r) => sum + Number(r.interestAmount || 0), 0);
|
||||
}
|
||||
|
||||
/** Totals by product and scheme, so a branch can see at a glance where its unapproved interest sits. */
|
||||
get outsideApprovalGroups(): { productName: string; fdType: string; accounts: number; interest: number }[] {
|
||||
const map = new Map<string, { productName: string; fdType: string; accounts: number; interest: number }>();
|
||||
this.outsideApprovalRows.forEach((r) => {
|
||||
const g = map.get(r.productName) ?? { productName: r.productName, fdType: r.fdType, accounts: 0, interest: 0 };
|
||||
g.accounts += 1;
|
||||
g.interest += Number(r.interestAmount || 0);
|
||||
map.set(r.productName, g);
|
||||
});
|
||||
return [...map.values()];
|
||||
}
|
||||
|
||||
private businessDateIso(): string {
|
||||
const businessDate = this.settingsService.businessDate as any;
|
||||
const parsed = businessDate ? new Date(businessDate) : new Date();
|
||||
|
||||
@@ -37,6 +37,8 @@ export interface FdInterestReport {
|
||||
approvedOnBehalf: boolean;
|
||||
postedAt: string | null;
|
||||
failureReason: string | null;
|
||||
/** The product's posting cycle in words — Monthly, Quarterly, Half-yearly, Yearly. */
|
||||
postingPeriodLabel: string;
|
||||
/** Whether this user may regenerate it: their own branch, or the head office for a branch that did not open. */
|
||||
canAct: boolean;
|
||||
blockedReason: string | null;
|
||||
@@ -49,6 +51,12 @@ export interface FdInterestReportLine {
|
||||
fdAccountId: number;
|
||||
accountNo: string;
|
||||
clientName: string;
|
||||
/**
|
||||
* The day after the deposit's last paid period ended, or its activation date for a first-ever
|
||||
* posting — null for a line posted before this was tracked. Paired with interestPostingDate, it is
|
||||
* the span the interest was earned over.
|
||||
*/
|
||||
interestPeriodFrom: string | null;
|
||||
/**
|
||||
* The period this interest is for, as the interest engine dated it — not the day the branch closes.
|
||||
* A deposit owed two unpaid periods appears on two lines, one per date.
|
||||
@@ -103,6 +111,28 @@ export interface FdInterestSummaryRow {
|
||||
approvedOnBehalf: boolean;
|
||||
approvedAt: string | null;
|
||||
postedAt: string | null;
|
||||
postingPeriodLabel: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* One fixed deposit interest transaction that reached the customer's account without this report's
|
||||
* approval — chiefly a deposit that matured before its own posting cycle completed, whose last period
|
||||
* is settled directly by the maturity job. View only, for visibility.
|
||||
*/
|
||||
export interface FdOutsideApprovalRow {
|
||||
transactionId: number;
|
||||
fdAccountId: number;
|
||||
accountNo: string;
|
||||
clientName: string;
|
||||
officeId: number;
|
||||
officeName: string;
|
||||
productId: number;
|
||||
productName: string;
|
||||
fdType: FdType;
|
||||
interestFor: string;
|
||||
interestAmount: number;
|
||||
/** When the posting actually reached the account — not the period it is for. */
|
||||
postedAt: string;
|
||||
}
|
||||
|
||||
export interface FdInterestPreviewRow {
|
||||
@@ -114,6 +144,8 @@ export interface FdInterestPreviewRow {
|
||||
productId: number;
|
||||
productName: string;
|
||||
fdType: FdType;
|
||||
/** The day after the deposit's last paid period ended, or its activation date for the first one. */
|
||||
interestPeriodFrom: string | null;
|
||||
/** The period this interest belongs to, as the engine dated it. */
|
||||
interestPostingDate: string;
|
||||
depositAmount: number | null;
|
||||
@@ -230,4 +262,19 @@ export class FdInterestPostingReportService {
|
||||
if (productId) params = params.set('productId', String(productId));
|
||||
return this.http.get<FdInterestSummaryRow[]>(`${this.base}/summary`, { params });
|
||||
}
|
||||
|
||||
/** Deposit interest posted without this report's approval — a deposit maturing mid-cycle, chiefly. */
|
||||
outsideApproval(
|
||||
fromDate?: string,
|
||||
toDate?: string,
|
||||
officeId?: number,
|
||||
scope?: FdReportScope
|
||||
): Observable<FdOutsideApprovalRow[]> {
|
||||
let params = new HttpParams();
|
||||
if (scope) params = params.set('scope', scope);
|
||||
if (fromDate) params = params.set('fromDate', fromDate);
|
||||
if (toDate) params = params.set('toDate', toDate);
|
||||
if (officeId) params = params.set('officeId', String(officeId));
|
||||
return this.http.get<FdOutsideApprovalRow[]>(`${this.base}/outside-approval`, { params });
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user