Cash claims now require a token at claim submission; a cashier redeems it at Transaction Entry to record physical hand-over, mirroring the existing clerk-withdrawal-request flow -- no double-disbursement, and the manager never hands over cash directly at approval time. Also: dividend-claim tokens fall back through the same TOK. NO. lookup used for withdrawal requests, silenced against the global error interceptor since the caller already handles every outcome; the account summary card now shows "Dividend Claim" instead of a misleading "A/c Savings" label when resolving one; and cash amounts are floored to whole rupees client-side to match cash's whole-rupee denominations. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
417 lines
16 KiB
HTML
417 lines
16 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/.
|
|
-->
|
|
|
|
@if (summary) {
|
|
<div class="dd-summary-grid">
|
|
<div class="dd-stat-card">
|
|
<div class="dd-stat-label">Declarations</div>
|
|
<div class="dd-stat-value">{{ summary.totalDeclarations }}</div>
|
|
<div class="dd-stat-sub">{{ summary.pendingApprovalDeclarations }} pending approval</div>
|
|
</div>
|
|
<div class="dd-stat-card">
|
|
<div class="dd-stat-label">Members Affected</div>
|
|
<div class="dd-stat-value">{{ summary.membersAffected }}</div>
|
|
<div class="dd-stat-sub">{{ summary.totalMemberDividends }} member-dividends total</div>
|
|
</div>
|
|
<div class="dd-stat-card">
|
|
<div class="dd-stat-label">Total Amount</div>
|
|
<div class="dd-stat-value">{{ summary.totalAmount | number: '1.2-2' }}</div>
|
|
<div class="dd-stat-sub">across all declared dividends</div>
|
|
</div>
|
|
<div class="dd-stat-card">
|
|
<div class="dd-stat-label">Currently Locked</div>
|
|
<div class="dd-stat-value">{{ summary.lockedCount }}</div>
|
|
<div class="dd-stat-sub">{{ summary.lockedAmount | number: '1.2-2' }}</div>
|
|
</div>
|
|
<div class="dd-stat-card">
|
|
<div class="dd-stat-label">Applied to Loans</div>
|
|
<div class="dd-stat-value">{{ summary.appliedToLoanCount }}</div>
|
|
<div class="dd-stat-sub">{{ summary.appliedToLoanAmount | number: '1.2-2' }}</div>
|
|
</div>
|
|
<div class="dd-stat-card">
|
|
<div class="dd-stat-label">Claimed by Member</div>
|
|
<div class="dd-stat-value">{{ summary.claimedCount }}</div>
|
|
<div class="dd-stat-sub">{{ summary.claimedAmount | number: '1.2-2' }}</div>
|
|
</div>
|
|
<div class="dd-stat-card dd-stat-card--forfeited">
|
|
<div class="dd-stat-label">Forfeited to Profit</div>
|
|
<div class="dd-stat-value">{{ summary.forfeitedCount }}</div>
|
|
<div class="dd-stat-sub">{{ summary.forfeitedAmount | number: '1.2-2' }}</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
<div class="pg-card">
|
|
<div class="pos-section-label">Dividend Declarations — Every Dividend Ever Declared, Tracked to Completion</div>
|
|
<div class="inv-form-grid" style="padding: 12px 16px">
|
|
<label class="inv-form-field" style="align-self: end">
|
|
<button type="button" class="pg-btn" [disabled]="declarationsLoading" (click)="loadDeclarations()">
|
|
Refresh
|
|
</button>
|
|
</label>
|
|
</div>
|
|
<table class="pg-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Product</th>
|
|
<th>Period</th>
|
|
<th class="num">Declared Amount</th>
|
|
<th class="num">Members</th>
|
|
<th>Status</th>
|
|
<th>Approved On</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@if (declarationsLoading) {
|
|
<tr>
|
|
<td colspan="6" class="pg-empty">Loading…</td>
|
|
</tr>
|
|
} @else if (declarationsLoaded && declarations.length === 0) {
|
|
<tr>
|
|
<td colspan="6" class="pg-empty">No dividends have been declared yet.</td>
|
|
</tr>
|
|
} @else {
|
|
@for (d of declarations; track d.declarationId) {
|
|
<tr>
|
|
<td>{{ d.productName }}</td>
|
|
<td>{{ d.periodStartDate }} → {{ d.periodEndDate }}</td>
|
|
<td class="num">{{ d.declaredAmount | number: '1.2-2' }}</td>
|
|
<td class="num">{{ d.totalMembers }}</td>
|
|
<td>
|
|
<span class="dd-badge" [ngClass]="declarationStatusClass(d.status)">{{
|
|
declarationStatusLabel(d.status)
|
|
}}</span>
|
|
</td>
|
|
<td>{{ d.approvedOn ? (d.approvedOn | date: 'medium') : '—' }}</td>
|
|
</tr>
|
|
}
|
|
}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="pg-card" style="margin-top: 16px">
|
|
<div class="pos-section-label">Dividend Dashboard — Locked Dividends for Members with Overdue EMI</div>
|
|
<div class="inv-form-grid" style="padding: 12px 16px">
|
|
<label class="inv-form-field" style="align-self: end">
|
|
<button type="button" class="pg-btn" [disabled]="loading" (click)="load()">Refresh</button>
|
|
</label>
|
|
</div>
|
|
<table class="pg-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Client</th>
|
|
<th class="num">Locked Dividend</th>
|
|
<th>Locked Date</th>
|
|
<th>Release Due</th>
|
|
<th class="num">Total Overdue EMI</th>
|
|
<th></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@if (loading) {
|
|
<tr>
|
|
<td colspan="6" class="pg-empty">Loading…</td>
|
|
</tr>
|
|
} @else if (loaded && rows.length === 0) {
|
|
<tr>
|
|
<td colspan="6" class="pg-empty">No members with a locked dividend and an overdue EMI right now.</td>
|
|
</tr>
|
|
} @else {
|
|
@for (row of rows; track row.lockId) {
|
|
<tr>
|
|
<td>{{ row.clientName }}</td>
|
|
<td class="num">{{ row.lockedAmount | number: '1.2-2' }}</td>
|
|
<td>{{ row.lockedDate }}</td>
|
|
<td>{{ row.releaseDueDate }}</td>
|
|
<td class="num">{{ totalOverdue(row) | number: '1.2-2' }}</td>
|
|
<td>
|
|
<button type="button" class="pg-btn" (click)="toggleApply(row)">
|
|
{{ expandedLockId === row.lockId ? 'Cancel' : 'Apply to Loan' }}
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
@if (expandedLockId === row.lockId) {
|
|
<tr class="dd-apply-row">
|
|
<td colspan="6">
|
|
<div class="dd-apply-panel">
|
|
<label class="inv-form-field">
|
|
<span>Loan</span>
|
|
<select [(ngModel)]="selectedLoanId[row.lockId]" (change)="onLoanChange(row)">
|
|
@for (loan of row.overdueLoans; track loan.loanId) {
|
|
<option [ngValue]="loan.loanId">
|
|
{{ loan.accountNo }} — Overdue {{ loan.overdueAmount | number: '1.2-2' }}
|
|
</option>
|
|
}
|
|
</select>
|
|
</label>
|
|
<label class="inv-form-field">
|
|
<span>Amount to Apply (max {{ maxAmountFor(row) | number: '1.2-2' }})</span>
|
|
<input
|
|
type="number"
|
|
[(ngModel)]="applyAmount[row.lockId]"
|
|
[max]="maxAmountFor(row)"
|
|
min="0.01"
|
|
step="0.01"
|
|
/>
|
|
</label>
|
|
@if (applyError) {
|
|
<div class="dd-apply-error">{{ applyError }}</div>
|
|
}
|
|
<button
|
|
type="button"
|
|
class="pg-btn dd-apply-confirm"
|
|
[disabled]="applying"
|
|
(click)="submitApply(row)"
|
|
>
|
|
{{ applying ? 'Applying…' : 'Confirm' }}
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
}
|
|
}
|
|
}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="pg-card" style="margin-top: 16px">
|
|
<div class="pos-section-label">All Dividend Locks — Full 3-Year Lock History</div>
|
|
<div class="inv-form-grid" style="padding: 12px 16px; gap: 8px">
|
|
<div class="dd-filter-group">
|
|
@for (opt of ['ALL', 'UNCLAIMED', 'CLAIMED', 'FORFEITED']; track opt) {
|
|
<button
|
|
type="button"
|
|
class="dd-filter-btn"
|
|
[class.dd-filter-btn--active]="lockStatusFilter === opt"
|
|
(click)="setLockStatusFilter(opt)"
|
|
>
|
|
{{ opt === 'ALL' ? 'All' : (opt | titlecase) }}
|
|
</button>
|
|
}
|
|
</div>
|
|
<label class="inv-form-field" style="align-self: end; margin-left: auto">
|
|
<button type="button" class="pg-btn" [disabled]="locksLoading" (click)="loadLocks()">Refresh</button>
|
|
</label>
|
|
</div>
|
|
<table class="pg-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Client</th>
|
|
<th>Savings Account</th>
|
|
<th class="num">Amount</th>
|
|
<th>Locked Date</th>
|
|
<th>Release Due</th>
|
|
<th>Status</th>
|
|
<th>Applied Loan</th>
|
|
<th></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@if (locksLoading) {
|
|
<tr>
|
|
<td colspan="8" class="pg-empty">Loading…</td>
|
|
</tr>
|
|
} @else if (locksLoaded && locks.length === 0) {
|
|
<tr>
|
|
<td colspan="8" class="pg-empty">No dividend locks match this filter.</td>
|
|
</tr>
|
|
} @else {
|
|
@for (l of locks; track l.lockId) {
|
|
<tr>
|
|
<td>{{ l.clientName }}</td>
|
|
<td>
|
|
<span class="pg-chip">{{ l.savingsAccountNo }}</span>
|
|
</td>
|
|
<td class="num">{{ l.amount | number: '1.2-2' }}</td>
|
|
<td>{{ l.lockedDate }}</td>
|
|
<td>{{ l.releaseDueDate }}</td>
|
|
<td>
|
|
<span class="dd-badge" [ngClass]="lockStatusClass(l.status)">{{ l.status | titlecase }}</span>
|
|
</td>
|
|
<td>{{ l.appliedLoanAccountNo || '—' }}</td>
|
|
<td>
|
|
@if (l.status === 'UNCLAIMED') {
|
|
<button type="button" class="pg-btn" (click)="toggleClaim(l)">
|
|
{{ expandedClaimLockId === l.lockId ? 'Cancel' : 'Claim' }}
|
|
</button>
|
|
}
|
|
</td>
|
|
</tr>
|
|
@if (expandedClaimLockId === l.lockId) {
|
|
<tr class="dd-apply-row">
|
|
<td colspan="8">
|
|
<div class="dd-apply-panel dd-apply-panel--wrap">
|
|
<div class="dd-apply-panel-title">
|
|
Unclaimed amount: {{ l.amount | number: '1.2-2' }} — split it below (loan, savings, cash)
|
|
</div>
|
|
@if (claimOverdueLoans[l.lockId] && claimOverdueLoans[l.lockId].length > 0) {
|
|
<label class="inv-form-field">
|
|
<span>Apply to Overdue Loan</span>
|
|
<select [(ngModel)]="claimLoanId[l.lockId]">
|
|
@for (loan of claimOverdueLoans[l.lockId]; track loan.loanId) {
|
|
<option [ngValue]="loan.loanId">
|
|
{{ loan.accountNo }} — Overdue {{ loan.overdueAmount | number: '1.2-2' }}
|
|
</option>
|
|
}
|
|
</select>
|
|
</label>
|
|
<label class="inv-form-field">
|
|
<span>Loan Amount</span>
|
|
<input type="number" [(ngModel)]="claimLoanAmount[l.lockId]" min="0" step="0.01" />
|
|
</label>
|
|
} @else {
|
|
<div class="pg-empty-inline">No overdue loan for this client.</div>
|
|
}
|
|
<label class="inv-form-field">
|
|
<span>Credit to Savings</span>
|
|
<input type="number" [(ngModel)]="claimSavingsAmount[l.lockId]" min="0" step="0.01" />
|
|
</label>
|
|
@if (!l.savingsAccountId && claimSavingsAmount[l.lockId] > 0) {
|
|
<label class="inv-form-field">
|
|
<span>Savings Account ID (no linked account — enter one to credit)</span>
|
|
<input type="number" [(ngModel)]="claimSavingsAccountOverride[l.lockId]" min="1" step="1" />
|
|
</label>
|
|
}
|
|
<label class="inv-form-field">
|
|
<span>Cash</span>
|
|
<input type="number" [(ngModel)]="claimCashAmount[l.lockId]" min="0" step="0.01" />
|
|
</label>
|
|
@if (claimCashAmount[l.lockId] > 0) {
|
|
<label class="inv-form-field">
|
|
<span>Token No. (for cashier hand-over)</span>
|
|
<input type="text" [(ngModel)]="claimTokNo[l.lockId]" />
|
|
</label>
|
|
}
|
|
<div class="dd-apply-panel-total">
|
|
Total: {{ claimTotal(l) | number: '1.2-2' }} / {{ l.amount | number: '1.2-2' }}
|
|
</div>
|
|
@if (claimError) {
|
|
<div class="dd-apply-error">{{ claimError }}</div>
|
|
}
|
|
<button
|
|
type="button"
|
|
class="pg-btn dd-apply-confirm"
|
|
[disabled]="claimSubmitting"
|
|
(click)="submitClaim(l)"
|
|
>
|
|
{{ claimSubmitting ? 'Submitting…' : 'Submit Claim Request' }}
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
}
|
|
}
|
|
}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="pg-card" style="margin-top: 16px">
|
|
<div class="pos-section-label">Dividend Claim Requests — Pending Review</div>
|
|
<div class="inv-form-grid" style="padding: 12px 16px">
|
|
<label class="inv-form-field" style="align-self: end">
|
|
<button type="button" class="pg-btn" [disabled]="claimRequestsLoading" (click)="loadClaimRequests()">
|
|
Refresh
|
|
</button>
|
|
</label>
|
|
</div>
|
|
<table class="pg-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Client</th>
|
|
<th>Savings Account</th>
|
|
<th class="num">To Loan</th>
|
|
<th class="num">To Savings</th>
|
|
<th class="num">Cash</th>
|
|
<th class="num">Unclaimed Balance</th>
|
|
<th>Client Loan Status</th>
|
|
<th></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@if (claimRequestsLoading) {
|
|
<tr>
|
|
<td colspan="8" class="pg-empty">Loading…</td>
|
|
</tr>
|
|
} @else if (claimRequestsLoaded && claimRequests.length === 0) {
|
|
<tr>
|
|
<td colspan="8" class="pg-empty">No dividend claim requests pending review.</td>
|
|
</tr>
|
|
} @else {
|
|
@for (req of claimRequests; track req.id) {
|
|
<tr>
|
|
<td>{{ req.client_name }}</td>
|
|
<td>
|
|
<span class="pg-chip">{{ req.savings_account_no }}</span>
|
|
</td>
|
|
<td class="num">
|
|
{{
|
|
req.loan_apply_amount > 0
|
|
? (req.loan_apply_amount | number: '1.2-2') + ' (' + req.loan_account_no + ')'
|
|
: '—'
|
|
}}
|
|
</td>
|
|
<td class="num">{{ req.savings_apply_amount > 0 ? (req.savings_apply_amount | number: '1.2-2') : '—' }}</td>
|
|
<td class="num">
|
|
{{ req.cash_apply_amount > 0 ? (req.cash_apply_amount | number: '1.2-2') : '—' }}
|
|
@if (req.cash_apply_amount > 0 && req.tok_no) {
|
|
<div class="pg-empty-inline">Tok: {{ req.tok_no }}</div>
|
|
}
|
|
</td>
|
|
<td class="num">{{ req.locked_amount | number: '1.2-2' }}</td>
|
|
<td>
|
|
@if (req.has_overdue_loan) {
|
|
<span class="dd-badge dd-badge--failed">Has Overdue EMI</span>
|
|
} @else {
|
|
<span class="dd-badge dd-badge--posted">No Overdue Loan</span>
|
|
}
|
|
</td>
|
|
<td>
|
|
<button type="button" class="pg-btn" [disabled]="claimActioningId === req.id" (click)="approveClaim(req)">
|
|
{{ claimActioningId === req.id ? 'Approving…' : 'Approve' }}
|
|
</button>
|
|
<button type="button" class="pg-btn" (click)="toggleRejectClaim(req)">
|
|
{{ expandedRejectClaimId === req.id ? 'Cancel' : 'Reject' }}
|
|
</button>
|
|
@if (claimActionError[req.id]) {
|
|
<div class="dd-retry-error">{{ claimActionError[req.id] }}</div>
|
|
}
|
|
</td>
|
|
</tr>
|
|
@if (expandedRejectClaimId === req.id) {
|
|
<tr class="dd-apply-row">
|
|
<td colspan="8">
|
|
<div class="dd-apply-panel">
|
|
<label class="inv-form-field">
|
|
<span>Rejection Reason</span>
|
|
<input
|
|
type="text"
|
|
[(ngModel)]="rejectReason[req.id]"
|
|
placeholder="Why is this claim being rejected?"
|
|
/>
|
|
</label>
|
|
<button
|
|
type="button"
|
|
class="pg-btn dd-apply-confirm"
|
|
[disabled]="claimActioningId === req.id"
|
|
(click)="rejectClaim(req)"
|
|
>
|
|
{{ claimActioningId === req.id ? 'Rejecting…' : 'Confirm Reject' }}
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
}
|
|
}
|
|
}
|
|
</tbody>
|
|
</table>
|
|
</div>
|