408 lines
15 KiB
HTML
408 lines
15 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>
|
|
}
|
|
|
|
<!-- One list at a time. The three answer unrelated questions and a clerk is only ever in one of
|
|
them; stacked, the one they came for was usually below the fold. Counts ride on the tabs so a
|
|
list worth opening — a blocked member, a lock nearing forfeiture — is visible without opening
|
|
it. -->
|
|
<div class="dd-tabs" role="tablist">
|
|
<button
|
|
type="button"
|
|
role="tab"
|
|
class="dd-tab"
|
|
[class.dd-tab--active]="activeTab === 'declarations'"
|
|
[attr.aria-selected]="activeTab === 'declarations'"
|
|
(click)="activeTab = 'declarations'"
|
|
>
|
|
Declarations
|
|
<span class="dd-tab__count">{{ declarations.length }}</span>
|
|
</button>
|
|
<button
|
|
type="button"
|
|
role="tab"
|
|
class="dd-tab"
|
|
[class.dd-tab--active]="activeTab === 'overdue'"
|
|
[attr.aria-selected]="activeTab === 'overdue'"
|
|
(click)="activeTab = 'overdue'"
|
|
>
|
|
Overdue EMI
|
|
<span class="dd-tab__count" [class.dd-tab__count--alert]="rows.length > 0">{{ rows.length }}</span>
|
|
</button>
|
|
<button
|
|
type="button"
|
|
role="tab"
|
|
class="dd-tab"
|
|
[class.dd-tab--active]="activeTab === 'locks'"
|
|
[attr.aria-selected]="activeTab === 'locks'"
|
|
(click)="activeTab = 'locks'"
|
|
>
|
|
Lock History
|
|
<span class="dd-tab__count">{{ locks.length }}</span>
|
|
</button>
|
|
</div>
|
|
|
|
<div class="pg-card" [hidden]="activeTab !== 'declarations'">
|
|
<div class="pos-section-label">Dividend Declarations — Every Dividend Ever Declared, Tracked to Completion</div>
|
|
<div class="dd-toolbar">
|
|
<div class="dd-search">
|
|
<input
|
|
type="search"
|
|
class="dd-search__input"
|
|
placeholder="Search product, period or status…"
|
|
[(ngModel)]="declarationSearch"
|
|
name="declarationSearch"
|
|
/>
|
|
@if (declarationSearch) {
|
|
<button type="button" class="dd-search__clear" (click)="declarationSearch = ''" aria-label="Clear search">
|
|
×
|
|
</button>
|
|
}
|
|
</div>
|
|
<span class="dd-toolbar__count">
|
|
{{ visibleDeclarations.length }} of {{ declarations.length }} declaration{{
|
|
declarations.length === 1 ? '' : 's'
|
|
}}
|
|
</span>
|
|
<button type="button" class="pg-btn" [disabled]="declarationsLoading" (click)="loadDeclarations()">Refresh</button>
|
|
</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 if (visibleDeclarations.length === 0) {
|
|
<tr>
|
|
<td colspan="6" class="pg-empty">No declaration matches “{{ declarationSearch }}”.</td>
|
|
</tr>
|
|
} @else {
|
|
@for (d of visibleDeclarations; 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" [hidden]="activeTab !== 'overdue'">
|
|
<div class="pos-section-label">Dividend Dashboard — Locked Dividends for Members with Overdue EMI</div>
|
|
<div class="dd-toolbar">
|
|
<div class="dd-search">
|
|
<input
|
|
type="search"
|
|
class="dd-search__input"
|
|
placeholder="Search member name or share account no…"
|
|
[(ngModel)]="overdueSearch"
|
|
name="overdueSearch"
|
|
/>
|
|
@if (overdueSearch) {
|
|
<button type="button" class="dd-search__clear" (click)="overdueSearch = ''" aria-label="Clear search">
|
|
×
|
|
</button>
|
|
}
|
|
</div>
|
|
<span class="dd-toolbar__count">
|
|
{{ visibleOverdueRows.length }} of {{ rows.length }} member{{ rows.length === 1 ? '' : 's' }}
|
|
</span>
|
|
<button type="button" class="pg-btn" [disabled]="loading" (click)="load()">Refresh</button>
|
|
</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 if (visibleOverdueRows.length === 0) {
|
|
<tr>
|
|
<td colspan="6" class="pg-empty">No member matches “{{ overdueSearch }}”.</td>
|
|
</tr>
|
|
} @else {
|
|
@for (row of visibleOverdueRows; track row.lockId) {
|
|
<tr>
|
|
<td>
|
|
{{ row.clientName }}
|
|
@if (row.shareAccountNo) {
|
|
<div class="dd-account-no">{{ row.shareAccountNo }}</div>
|
|
}
|
|
</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" [hidden]="activeTab !== 'locks'">
|
|
<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>
|
|
<div class="dd-search" style="margin-left: auto">
|
|
<input
|
|
type="search"
|
|
class="dd-search__input"
|
|
placeholder="Search member name or share account no…"
|
|
[(ngModel)]="lockSearch"
|
|
name="lockSearch"
|
|
/>
|
|
@if (lockSearch) {
|
|
<button type="button" class="dd-search__clear" (click)="lockSearch = ''" aria-label="Clear search">
|
|
×
|
|
</button>
|
|
}
|
|
</div>
|
|
<button type="button" class="pg-btn" [disabled]="locksLoading" (click)="loadLocks()">Refresh</button>
|
|
</div>
|
|
|
|
<!-- Financial-year chips, built from the locks themselves: a new declaration adds its year here
|
|
on its own, and a year with nothing left to show stops being offered. Shown even for a single
|
|
year, so the filter is visibly there before a second year exists to use it on. -->
|
|
@if (lockYears.length > 0) {
|
|
<div class="dd-year-filter">
|
|
<span class="dd-year-filter__label">Financial year</span>
|
|
<div class="dd-filter-group">
|
|
<button
|
|
type="button"
|
|
class="dd-filter-btn"
|
|
[class.dd-filter-btn--active]="lockYearFilter === 'ALL'"
|
|
(click)="setLockYearFilter('ALL')"
|
|
>
|
|
All years
|
|
</button>
|
|
@for (year of lockYears; track year) {
|
|
<button
|
|
type="button"
|
|
class="dd-filter-btn"
|
|
[class.dd-filter-btn--active]="lockYearFilter === year"
|
|
(click)="setLockYearFilter(year)"
|
|
>
|
|
FY {{ year }}
|
|
</button>
|
|
}
|
|
</div>
|
|
<span class="dd-year-filter__total">
|
|
{{ visibleLocks.length }} lock{{ visibleLocks.length === 1 ? '' : 's' }} ·
|
|
{{ visibleLocksTotal | number: '1.2-2' }}
|
|
</span>
|
|
</div>
|
|
}
|
|
<table class="pg-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Client</th>
|
|
<!-- Which declaration this lock came from. A member can hold several unclaimed dividends at
|
|
once — this year's plus an earlier year they never collected — and amount alone cannot
|
|
tell them apart. -->
|
|
<th>Dividend</th>
|
|
<th class="num">Amount</th>
|
|
<th>Locked Date</th>
|
|
<th>Release Due</th>
|
|
<th>Status</th>
|
|
<th>Applied Loan</th>
|
|
<th>Demand Draft</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@if (locksLoading) {
|
|
<tr>
|
|
<td colspan="8" class="pg-empty">Loading…</td>
|
|
</tr>
|
|
} @else if (locksLoaded && visibleLocks.length === 0) {
|
|
<tr>
|
|
<td colspan="8" class="pg-empty">
|
|
@if (lockSearch) {
|
|
No dividend lock matches “{{ lockSearch }}”.
|
|
} @else {
|
|
No dividend locks match this filter.
|
|
}
|
|
</td>
|
|
</tr>
|
|
} @else {
|
|
@for (l of visibleLocks; track l.lockId) {
|
|
<tr>
|
|
<td>
|
|
{{ l.clientName }}
|
|
@if (l.shareAccountNo) {
|
|
<div class="dd-account-no">{{ l.shareAccountNo }}</div>
|
|
}
|
|
</td>
|
|
<td>
|
|
@if (l.financialYear) {
|
|
<div class="dd-dividend-year">FY {{ l.financialYear }}</div>
|
|
<div class="dd-dividend-period">
|
|
{{ l.productName }} · {{ l.periodStartDate }} → {{ l.periodEndDate }}
|
|
</div>
|
|
} @else {
|
|
—
|
|
}
|
|
</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>
|
|
<!-- Once a draft has been raised the clerk needs its number, not another Claim button:
|
|
it is what goes on the counterfoil and what the member asks about when they return.
|
|
Until then, the button hands off to the DD inventory to raise one. -->
|
|
<td>
|
|
@if (l.claimDdNumber && l.claimStatus === 'APPROVED') {
|
|
<span class="dd-dd-number">{{ l.claimDdNumber }}</span>
|
|
} @else if (l.claimStatus === 'PENDING') {
|
|
<span class="dd-badge dd-badge--pending">DD {{ l.claimDdNumber }} — awaiting approval</span>
|
|
} @else if (l.status === 'UNCLAIMED') {
|
|
<button type="button" class="pg-btn dd-apply-confirm" (click)="claimAsDemandDraft(l)">
|
|
Claim by DD
|
|
</button>
|
|
}
|
|
</td>
|
|
</tr>
|
|
}
|
|
}
|
|
</tbody>
|
|
</table>
|
|
</div>
|