Files
cbsnexora/src/app/clerk/dividend-dashboard/dividend-dashboard.component.html
T
keshav 9f95d01196
build-run / Run Lint, Build and Deploy (push) Canceled after 0s
Publish Image in Docker Hub / build (push) Canceled after 0s
Playwright E2E Tests / Run Playwright Tests (push) Canceled after 0s
Tests / testRigor (push) Canceled after 0s
done
2026-08-30 11:09:14 +05:30

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">
&times;
</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">
&times;
</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">
&times;
</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>