Files
cbsnexora/src/app/fd-interest-posting-report/fd-interest-posting-report.component.html
T
niteshandClaude Sonnet 5 c90528e109 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>
2026-10-08 19:06:57 +05:30

647 lines
25 KiB
HTML

<!--
Copyright since 2025 Mifos Initiative
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
-->
<div class="fd">
<div class="fd-head">
<div>
<h2 class="fd-head-t"><fa-icon icon="percentage"></fa-icon> FD Interest Posting Reports</h2>
<div class="fd-head-s">
Each branch gets its own report for each fixed deposit scheme, generated automatically at 12:00 noon on the
posting date. One user puts the figures up and a second approves them — whoever generated a report cannot
approve it. Approved interest posts when the branch closes its day, and it cannot close until then.
</div>
</div>
<div class="fd-head-a">
<label class="fd-date">
Posting date
<input type="date" [(ngModel)]="postingDate" (change)="load()" />
</label>
<button class="fd-btn" (click)="load()" [disabled]="loading"><fa-icon icon="undo"></fa-icon>Reload</button>
<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>
@if (isHeadOffice) {
<button class="fd-btn" (click)="openProductSummary()" [disabled]="productSummaryLoading">
<fa-icon icon="table"></fa-icon>Product summary
</button>
}
</div>
</div>
@if (!approvalEnabled) {
<div class="fd-alert fd-alert--info">
<fa-icon icon="info-circle"></fa-icon>
<div>
Fixed deposit interest is currently posted directly by the nightly job, so there is nothing to approve here.
Turn on <strong>fd-interest-posting-approval</strong> under System &rarr; Configurations to put deposits behind
this report.
</div>
</div>
}
@if (isHeadOffice) {
<div class="fd-tabs">
<button class="fd-tab" [class.is-active]="tab === 'own'" (click)="switchTab('own')">
<fa-icon icon="building"></fa-icon>My branch{{ myBranch ? ' — ' + myBranch : '' }}
</button>
<button class="fd-tab" [class.is-active]="tab === 'others'" (click)="switchTab('others')">
<fa-icon icon="sitemap"></fa-icon>Other branches
</button>
</div>
@if (tab === 'others') {
<div class="fd-alert fd-alert--info">
<fa-icon icon="info-circle"></fa-icon>
<div>
Each branch generates and approves its own reports. The head office can act here only for a branch that has
not opened the day, and only once every other open branch has closed. Anything it approves is marked (HO).
</div>
</div>
}
}
@if (errorMessage) {
<div class="fd-alert fd-alert--error">
<fa-icon icon="exclamation-triangle"></fa-icon>
<div>{{ errorMessage }}</div>
</div>
}
@if (notice) {
<div class="fd-alert fd-alert--ok">
<fa-icon icon="check"></fa-icon>
<div>{{ notice }}</div>
</div>
}
@if (pendingCount > 0) {
<div class="fd-alert fd-alert--warn">
<fa-icon icon="clock"></fa-icon>
<div>
<strong>{{ pendingCount }} report(s) waiting for approval.</strong> The day cannot be closed until they are
approved, and the user who generated a report cannot be the one to approve it.
</div>
</div>
}
@if (due.length > 0) {
<div class="fd-alert fd-alert--warn">
<fa-icon icon="exclamation-triangle"></fa-icon>
<div class="fd-due-body">
<div>Fixed deposit interest is due but no report has been generated yet for:</div>
@for (d of due; track d.productId + '-' + d.officeId) {
<div class="fd-due-row">
<strong>{{ d.productName }}</strong>
<span class="fd-chip" [ngClass]="fdTypeClass(d.fdType)">{{ fdTypeShort(d.fdType) }}</span>
<span>
&middot; {{ d.officeName }} &middot; {{ d.postingDate }} &middot; {{ d.accountCount }} deposit(s) &middot;
&#8377;{{ d.totalInterest | number: '1.2-2' }}
</span>
@if (d.canAct) {
<button class="fd-btn fd-btn--primary fd-btn--slim" (click)="generate(d)" [disabled]="busy">
<fa-icon icon="sync"></fa-icon>{{ tab === 'others' ? 'Generate on behalf' : 'Generate report' }}
</button>
} @else if (d.blockedReason) {
<span class="fd-blocked">{{ d.blockedReason }}</span>
}
</div>
}
</div>
</div>
}
<!-- Summary strip -->
<div class="fd-summary">
<div class="fd-stat">
<div class="fd-stat__label">Interest due, no report</div>
<div class="fd-stat__value">&#8377;{{ dueTotal | number: '1.2-2' }}</div>
<div class="fd-stat__sub">{{ dueAccountTotal }} deposit(s) &middot; {{ due.length }} product(s)</div>
</div>
<div class="fd-stat fd-stat--warn">
<div class="fd-stat__label">Awaiting approval</div>
<div class="fd-stat__value">&#8377;{{ awaitingTotal | number: '1.2-2' }}</div>
<div class="fd-stat__sub">{{ awaitingReports.length }} report(s) &middot; blocking day close</div>
</div>
<div class="fd-stat fd-stat--ok">
<div class="fd-stat__label">Approved &mdash; posts at close</div>
<div class="fd-stat__value">&#8377;{{ approvedTotal | number: '1.2-2' }}</div>
<div class="fd-stat__sub">{{ approvedReports.length }} report(s)</div>
</div>
<div class="fd-stat fd-stat--posted">
<div class="fd-stat__label">Posted today</div>
<div class="fd-stat__value">&#8377;{{ postedTotal | number: '1.2-2' }}</div>
<div class="fd-stat__sub">{{ postedReports.length }} report(s)</div>
</div>
</div>
@if (showPreview) {
<div class="fd-card">
<div class="fd-detail-head">
<div>
<h3>FD interest preview &mdash; earned up to {{ previewAsOf }}</h3>
<div class="muted">
What each deposit would be paid if interest were posted today. View only &mdash; nothing is saved, approved
or posted.
</div>
</div>
<div class="fd-total">
<span>Total so far</span>
<strong>&#8377;{{ previewTotal | number: '1.2-2' }}</strong>
<span class="muted">{{ previewRows.length }} deposit(s)</span>
</div>
</div>
<div
style="
display: flex;
flex-wrap: wrap;
gap: 10px;
padding: 14px 20px;
border-bottom: 1px solid var(--fd-border-soft);
"
>
<button class="fd-btn fd-btn--slim" (click)="openPreview()" [disabled]="previewLoading">
<fa-icon icon="undo"></fa-icon>Refresh
</button>
<button
class="fd-btn fd-btn--slim"
(click)="exportPreview()"
[disabled]="previewLoading || previewRows.length === 0"
>
<fa-icon icon="file-excel"></fa-icon>Export Excel
</button>
<button class="fd-btn fd-btn--slim" (click)="closePreview()"><fa-icon icon="times"></fa-icon>Close</button>
</div>
@if (previewLoading) {
<div class="fd-empty">Working out what each deposit has earned up to today&hellip;</div>
} @else {
<div class="fd-table-wrap">
<table class="fd-table">
<thead>
<tr>
<th>Branch</th>
<th>Product</th>
<th>Scheme</th>
<th class="num">Deposits</th>
<th class="num">Interest so far</th>
</tr>
</thead>
<tbody>
@for (g of previewGroups; track g.officeName + g.productName) {
<tr>
<td>{{ g.officeName }}</td>
<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">&#8377;{{ g.interest | number: '1.2-2' }}</td>
</tr>
} @empty {
<tr>
<td colspan="5" class="fd-empty">No fixed deposit has interest waiting to be posted.</td>
</tr>
}
</tbody>
</table>
</div>
<div class="fd-table-wrap">
<table class="fd-table">
<thead>
<tr>
<th>Account no</th>
<th>Customer</th>
<th>Scheme</th>
<th>Interest for</th>
<th class="num">Deposit</th>
<th class="num">Rate %</th>
<th>Maturity</th>
<th>Pays into</th>
<th class="num">Interest so far</th>
</tr>
</thead>
<tbody>
@for (r of previewRows; track r.fdAccountId + '-' + r.interestPostingDate) {
<tr>
<td class="fd-mono">{{ r.accountNo }}</td>
<td>{{ r.clientName }}</td>
<td>
<span class="fd-chip" [ngClass]="fdTypeClass(r.fdType)">{{ fdTypeShort(r.fdType) }}</span>
</td>
<td class="fd-mono">{{ r.interestPeriodFrom || '—' }} &rarr; {{ r.interestPostingDate }}</td>
<td class="num">&#8377;{{ r.depositAmount | number: '1.2-2' }}</td>
<td class="num">{{ r.interestRate | number: '1.2-2' }}</td>
<td class="fd-mono">{{ r.maturityDate || '—' }}</td>
<td class="fd-sub">{{ r.payoutAccountNo || 'Stays in the deposit' }}</td>
<td class="num">&#8377;{{ r.interest | number: '1.2-2' }}</td>
</tr>
}
</tbody>
<tfoot>
<tr>
<td colspan="8"><strong>Total</strong></td>
<td class="num">
<strong>&#8377;{{ previewTotal | number: '1.2-2' }}</strong>
</td>
</tr>
</tfoot>
</table>
</div>
}
</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 &mdash; 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>&#8377;{{ 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&hellip;</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">&#8377;{{ 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">&#8377;{{ 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>&#8377;{{ outsideApprovalTotal | number: '1.2-2' }}</strong>
</td>
<td></td>
</tr>
</tfoot>
</table>
</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>&#8377;{{ 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&hellip;</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">&#8377;{{ row.posted | number: '1.2-2' }}</td>
<td class="num">&#8377;{{ row.pending | number: '1.2-2' }}</td>
<td class="num">&#8377;{{ row.maturityPosted | number: '1.2-2' }}</td>
<td class="num">
<strong>&#8377;{{ 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>&#8377;{{ productSummaryGrandTotal.posted | number: '1.2-2' }}</strong>
</td>
<td class="num">
<strong>&#8377;{{ productSummaryGrandTotal.pending | number: '1.2-2' }}</strong>
</td>
<td class="num">
<strong>&#8377;{{ productSummaryGrandTotal.maturityPosted | number: '1.2-2' }}</strong>
</td>
<td class="num">
<strong>&#8377;{{ 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>
<span class="fd-card-sub">One report per scheme, per branch</span>
</div>
<div class="fd-table-wrap">
<table class="fd-table">
<thead>
<tr>
<th>Branch</th>
<th>Scheme</th>
<th>Type</th>
<th>Posting date</th>
<th class="num">Deposits</th>
<th class="num">Total interest</th>
<th>Status</th>
<th>Maker</th>
<th>Checker</th>
<th></th>
</tr>
</thead>
<tbody>
@for (r of reports; track r.id) {
<tr class="is-clickable" [class.is-selected]="selected?.id === r.id" (click)="open(r)">
<td>{{ r.officeName }}</td>
<td>
<strong>{{ r.productName }}</strong>
</td>
<td [title]="fdTypeLabel(r.fdType)">
<span class="fd-chip" [ngClass]="fdTypeClass(r.fdType)">{{ fdTypeShort(r.fdType) }}</span>
</td>
<td class="fd-mono">{{ r.postingDate }}</td>
<td class="num">{{ r.accountCount }}</td>
<td class="num">&#8377;{{ r.totalInterest | number: '1.2-2' }}</td>
<td>
<span class="fd-badge" [ngClass]="statusClass(r.status)">{{ r.status }}</span>
@if (r.failureReason) {
<div class="fd-fail" [title]="r.failureReason">Posting failed: {{ r.failureReason }}</div>
}
</td>
<td class="fd-sub">
{{ r.generatedBy || 'System' }} &middot; {{ r.generationMode }}<br />{{
r.generatedAt | date: 'dd MMM, HH:mm'
}}
</td>
<td class="fd-sub">
@if (r.approvedAt) {
{{ r.approvedBy }}
@if (r.approvedOnBehalf) {
<span class="fd-behalf">(HO)</span>
}
<br />{{ r.approvedAt | date: 'dd MMM, HH:mm' }}
} @else {
&mdash;
}
</td>
<td (click)="$event.stopPropagation()">
<div style="display: flex; flex-wrap: wrap; gap: 6px; justify-content: flex-end">
@if (canRegenerate(r)) {
<button class="fd-btn fd-btn--slim" (click)="regenerate(r)" [disabled]="busy">
<fa-icon icon="sync"></fa-icon>Regenerate
</button>
}
@if (canApprove(r)) {
<button class="fd-btn fd-btn--primary fd-btn--slim" (click)="approve(r)" [disabled]="busy">
<fa-icon icon="check"></fa-icon>{{ tab === 'others' ? 'Approve on behalf' : 'Approve' }}
</button>
} @else if (r.status === 'GENERATED' && r.approveBlockedReason) {
<span class="fd-blocked">{{ r.approveBlockedReason }}</span>
}
</div>
</td>
</tr>
} @empty {
<tr>
<td colspan="10" class="fd-empty">
@if (loading) {
Loading&hellip;
} @else {
No fixed deposit interest posting report for {{ postingDate || 'this date' }}. Reports are generated
at 12:00 noon on a product's posting date.
}
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
@if (selected) {
<div class="fd-card">
<div class="fd-detail-head">
<div>
<h3>
{{ selected.officeName }} &middot; {{ selected.productName }} &mdash; {{ 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>
<div class="muted">
Dr Interest Expense &rarr; Cr {{ selected.payableGlCode }} {{ selected.payableGlName }}; Dr
{{ selected.payableGlCode }} &rarr; Cr {{ selected.paidGlCode }} {{ selected.paidGlName }}; then Dr
{{ selected.paidGlCode }} &rarr; Cr each deposit below
</div>
</div>
<div class="fd-total">
<span>Total interest</span>
<strong>&#8377;{{ selected.totalInterest | number: '1.2-2' }}</strong>
<span class="muted">{{ selected.accountCount }} deposit(s)</span>
</div>
</div>
<div class="fd-table-wrap">
<table class="fd-table">
<thead>
<tr>
<th>Account no</th>
<th>Account holder</th>
<th>Interest for</th>
<th class="num">Deposit</th>
<th class="num">Rate %</th>
<th>Maturity</th>
<th class="num">Interest</th>
<th class="num">Balance before</th>
<th class="num">Balance after</th>
<th>Pays into</th>
<th>Posted txn</th>
</tr>
</thead>
<tbody>
@for (l of selected.lines; track l.fdAccountId + '-' + l.interestPostingDate) {
<tr>
<td class="fd-mono">{{ l.accountNo }}</td>
<td>{{ l.clientName }}</td>
<td class="fd-mono">{{ l.interestPeriodFrom || '—' }} &rarr; {{ l.interestPostingDate }}</td>
<td class="num">&#8377;{{ l.depositAmount | number: '1.2-2' }}</td>
<td class="num">{{ l.interestRate | number: '1.2-2' }}</td>
<td class="fd-mono">{{ l.maturityDate || '—' }}</td>
<td class="num">&#8377;{{ l.interestAmount | number: '1.2-2' }}</td>
<td class="num">&#8377;{{ l.balanceBefore | number: '1.2-2' }}</td>
<td class="num">&#8377;{{ l.balanceAfter | number: '1.2-2' }}</td>
<td class="fd-sub">{{ l.payoutAccountNo || 'Stays in the deposit' }}</td>
<td class="fd-sub">{{ l.fdTransactionId ? '#' + l.fdTransactionId : '—' }}</td>
</tr>
}
</tbody>
<tfoot>
<tr>
<td colspan="6"><strong>Total</strong></td>
<td class="num">
<strong>&#8377;{{ selected.totalInterest | number: '1.2-2' }}</strong>
</td>
<td colspan="4"></td>
</tr>
</tfoot>
</table>
</div>
</div>
}
</div>