323 lines
12 KiB
HTML
323 lines
12 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="ipr">
|
|
<div class="ipr-head">
|
|
<div>
|
|
<h2 class="ipr-head-t"><fa-icon icon="percentage"></fa-icon> Interest Posting Reports</h2>
|
|
<div class="ipr-head-s">
|
|
Each branch gets its own report for each savings product, generated automatically at 12:00 PM on the posting
|
|
date and open to generating or regenerating at any time. The branch approves it, and its approved interest is
|
|
posted when the branch closes the day — it cannot close until then. The head office closes last.
|
|
</div>
|
|
</div>
|
|
<div class="ipr-head-a">
|
|
<label class="ipr-date">
|
|
Posting date
|
|
<input type="date" [(ngModel)]="postingDate" (change)="load()" />
|
|
</label>
|
|
<button class="ipr-btn" (click)="load()" [disabled]="loading"><fa-icon icon="undo"></fa-icon> Reload</button>
|
|
<button class="ipr-btn ipr-btn--primary" (click)="openPreview()" [disabled]="previewLoading">
|
|
<fa-icon icon="eye"></fa-icon> Preview interest (today)
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
@if (isHeadOffice) {
|
|
<div class="ipr-tabs">
|
|
<button class="ipr-tab" [class.is-active]="tab === 'own'" (click)="switchTab('own')">
|
|
<fa-icon icon="building"></fa-icon> My branch{{ myBranch ? ' — ' + myBranch : '' }}
|
|
</button>
|
|
<button class="ipr-tab" [class.is-active]="tab === 'others'" (click)="switchTab('others')">
|
|
<fa-icon icon="sitemap"></fa-icon> Other branches
|
|
</button>
|
|
</div>
|
|
@if (tab === 'others') {
|
|
<div class="ipr-alert ipr-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 (showPreview) {
|
|
<div class="ipr-card">
|
|
<div class="ipr-detail-head">
|
|
<div>
|
|
<h3>Interest preview — earned up to {{ previewAsOf }}</h3>
|
|
<div class="muted">
|
|
What each account would be paid if interest were posted today. View only — nothing is saved, approved or
|
|
posted.
|
|
</div>
|
|
</div>
|
|
<div class="ipr-total">
|
|
<span>Total so far</span>
|
|
<strong>₹{{ previewTotal | number: '1.2-2' }}</strong>
|
|
<span class="muted">{{ previewRows.length }} account(s)</span>
|
|
</div>
|
|
</div>
|
|
<div class="ipr-preview-actions">
|
|
<button class="ipr-btn ipr-btn--slim" (click)="openPreview()" [disabled]="previewLoading">
|
|
<fa-icon icon="undo"></fa-icon> Refresh
|
|
</button>
|
|
<button
|
|
class="ipr-btn ipr-btn--slim"
|
|
(click)="exportPreview()"
|
|
[disabled]="previewLoading || previewRows.length === 0"
|
|
>
|
|
<fa-icon icon="file-excel"></fa-icon> Export Excel
|
|
</button>
|
|
<button class="ipr-btn ipr-btn--slim" (click)="closePreview()"><fa-icon icon="times"></fa-icon> Close</button>
|
|
</div>
|
|
@if (previewLoading) {
|
|
<div class="ipr-empty">Calculating interest up to today…</div>
|
|
} @else {
|
|
<table class="ipr-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Branch</th>
|
|
<th>Product</th>
|
|
<th>Posting date</th>
|
|
<th class="num">Accounts</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>{{ g.postingDate }}</td>
|
|
<td class="num">{{ g.accounts }}</td>
|
|
<td class="num">₹{{ g.interest | number: '1.2-2' }}</td>
|
|
</tr>
|
|
} @empty {
|
|
<tr>
|
|
<td colspan="5" class="ipr-empty">No savings account has interest accrued in its current cycle.</td>
|
|
</tr>
|
|
}
|
|
</tbody>
|
|
</table>
|
|
@if (previewRows.length > 0) {
|
|
<table class="ipr-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Branch</th>
|
|
<th>Product</th>
|
|
<th>Account no</th>
|
|
<th>Account holder</th>
|
|
<th class="num">Balance</th>
|
|
<th class="num">Rate %</th>
|
|
<th>Cycle</th>
|
|
<th class="num">Interest so far</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@for (r of previewRows; track r.savingsAccountId) {
|
|
<tr>
|
|
<td>{{ r.officeName }}</td>
|
|
<td>{{ r.productName }}</td>
|
|
<td>{{ r.accountNo }}</td>
|
|
<td>{{ r.clientName }}</td>
|
|
<td class="num">₹{{ r.balance | number: '1.2-2' }}</td>
|
|
<td class="num">{{ r.rate | number: '1.2-2' }}</td>
|
|
<td class="muted">{{ r.cycleStart }} → {{ r.postingDate }}</td>
|
|
<td class="num">₹{{ r.interest | number: '1.2-2' }}</td>
|
|
</tr>
|
|
}
|
|
</tbody>
|
|
<tfoot>
|
|
<tr>
|
|
<td colspan="7"><strong>Total</strong></td>
|
|
<td class="num">
|
|
<strong>₹{{ previewTotal | number: '1.2-2' }}</strong>
|
|
</td>
|
|
</tr>
|
|
</tfoot>
|
|
</table>
|
|
}
|
|
}
|
|
</div>
|
|
}
|
|
|
|
@if (errorMessage) {
|
|
<div class="ipr-alert ipr-alert--error">
|
|
<fa-icon icon="exclamation-triangle"></fa-icon>
|
|
<div>{{ errorMessage }}</div>
|
|
</div>
|
|
}
|
|
@if (notice) {
|
|
<div class="ipr-alert ipr-alert--ok">
|
|
<fa-icon icon="check"></fa-icon>
|
|
<div>{{ notice }}</div>
|
|
</div>
|
|
}
|
|
@if (pendingCount > 0) {
|
|
<div class="ipr-alert ipr-alert--warn">
|
|
<fa-icon icon="clock"></fa-icon>
|
|
<div>{{ pendingCount }} report(s) waiting for approval. The day cannot be closed until they are approved.</div>
|
|
</div>
|
|
}
|
|
|
|
@if (due.length > 0) {
|
|
<div class="ipr-alert ipr-alert--warn ipr-due">
|
|
<fa-icon icon="exclamation-triangle"></fa-icon>
|
|
<div class="ipr-due-body">
|
|
<div>Interest is due but no report has been generated yet for:</div>
|
|
@for (d of due; track d.productId + '-' + d.officeId) {
|
|
<div class="ipr-due-row">
|
|
<strong>{{ d.productName }}</strong> <span>· {{ d.officeName }} · {{ d.postingDate }}</span>
|
|
@if (d.canAct) {
|
|
<button class="ipr-btn ipr-btn--primary ipr-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="ipr-blocked">{{ d.blockedReason }}</span>
|
|
}
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
<div class="ipr-card">
|
|
<table class="ipr-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Branch</th>
|
|
<th>Product</th>
|
|
<th>Posting date</th>
|
|
<th class="num">Accounts</th>
|
|
<th class="num">Total interest</th>
|
|
<th>Status</th>
|
|
<th>Generated</th>
|
|
<th>Approved</th>
|
|
<th></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@for (r of reports; track r.id) {
|
|
<tr [class.is-selected]="selected?.id === r.id" (click)="open(r)">
|
|
<td>{{ r.officeName }}</td>
|
|
<td>
|
|
<strong>{{ r.productName }}</strong>
|
|
</td>
|
|
<td>{{ r.postingDate }}</td>
|
|
<td class="num">{{ r.accountCount }}</td>
|
|
<td class="num">₹{{ r.totalInterest | number: '1.2-2' }}</td>
|
|
<td>
|
|
<span class="ipr-badge" [ngClass]="statusClass(r.status)">{{ r.status }}</span>
|
|
@if (r.failureReason) {
|
|
<div class="ipr-fail" [title]="r.failureReason">Posting failed: {{ r.failureReason }}</div>
|
|
}
|
|
</td>
|
|
<td class="muted">
|
|
{{ r.generatedBy || 'System' }} · {{ r.generationMode }}<br />{{ r.generatedAt | date: 'dd MMM, HH:mm' }}
|
|
</td>
|
|
<td class="muted">
|
|
@if (r.approvedAt) {
|
|
{{ r.approvedBy }}
|
|
@if (r.approvedOnBehalf) {
|
|
<span class="ipr-behalf">(HO)</span>
|
|
}
|
|
<br />{{ r.approvedAt | date: 'dd MMM, HH:mm' }}
|
|
} @else {
|
|
—
|
|
}
|
|
</td>
|
|
<td class="actions" (click)="$event.stopPropagation()">
|
|
@if (canRegenerate(r)) {
|
|
<button class="ipr-btn ipr-btn--slim" (click)="regenerate(r)" [disabled]="busy">
|
|
<fa-icon icon="sync"></fa-icon> Regenerate
|
|
</button>
|
|
}
|
|
@if (canApprove(r)) {
|
|
<button class="ipr-btn ipr-btn--primary ipr-btn--slim" (click)="approve(r)" [disabled]="busy">
|
|
<fa-icon icon="check"></fa-icon> {{ tab === 'others' ? 'Approve on behalf' : 'Approve' }}
|
|
</button>
|
|
}
|
|
@if (!r.canAct && r.blockedReason && (r.status === 'GENERATED' || r.status === 'APPROVED')) {
|
|
<span class="ipr-blocked">{{ r.blockedReason }}</span>
|
|
}
|
|
</td>
|
|
</tr>
|
|
} @empty {
|
|
<tr>
|
|
<td colspan="9" class="ipr-empty">
|
|
@if (loading) {
|
|
Loading…
|
|
} @else {
|
|
No interest posting report for {{ postingDate || 'this date' }}. Reports are generated at 12:00 PM on a
|
|
product's posting date.
|
|
}
|
|
</td>
|
|
</tr>
|
|
}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
@if (selected) {
|
|
<div class="ipr-card">
|
|
<div class="ipr-detail-head">
|
|
<div>
|
|
<h3>{{ selected.officeName }} · {{ selected.productName }} — {{ selected.postingDate }}</h3>
|
|
<div class="muted">
|
|
Dr {{ selected.expenseGlCode }} {{ selected.expenseGlName }} → Cr {{ selected.payableGlCode }}
|
|
{{ selected.payableGlName }}; Dr {{ selected.payableGlCode }} → Cr {{ selected.paidGlCode }}
|
|
{{ selected.paidGlName }}; then Dr {{ selected.paidGlCode }} → Cr each account below
|
|
</div>
|
|
</div>
|
|
<div class="ipr-total">
|
|
<span>Total interest</span>
|
|
<strong>₹{{ selected.totalInterest | number: '1.2-2' }}</strong>
|
|
<span class="muted">{{ selected.accountCount }} account(s)</span>
|
|
</div>
|
|
</div>
|
|
<table class="ipr-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Account no</th>
|
|
<th>Account holder</th>
|
|
<th class="num">Interest</th>
|
|
<th class="num">Balance before posting</th>
|
|
<th class="num">Balance after posting</th>
|
|
<th>Posted txn</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@for (l of selected.lines; track l.savingsAccountId) {
|
|
<tr>
|
|
<td>{{ l.accountNo }}</td>
|
|
<td>{{ l.clientName }}</td>
|
|
<td class="num">₹{{ l.interestAmount | number: '1.2-2' }}</td>
|
|
<td class="num">₹{{ l.balanceBefore | number: '1.2-2' }}</td>
|
|
<td class="num">₹{{ l.balanceAfter | number: '1.2-2' }}</td>
|
|
<td class="muted">{{ l.savingsTransactionId ? '#' + l.savingsTransactionId : '—' }}</td>
|
|
</tr>
|
|
}
|
|
</tbody>
|
|
<tfoot>
|
|
<tr>
|
|
<td colspan="2"><strong>Total</strong></td>
|
|
<td class="num">
|
|
<strong>₹{{ selected.totalInterest | number: '1.2-2' }}</strong>
|
|
</td>
|
|
<td colspan="3"></td>
|
|
</tr>
|
|
</tfoot>
|
|
</table>
|
|
</div>
|
|
}
|
|
</div>
|