done
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

This commit is contained in:
2026-08-30 11:09:14 +05:30
parent 07a4c8b8e3
commit 9f95d01196
57 changed files with 5705 additions and 681 deletions
@@ -46,14 +46,69 @@
</div>
}
<div class="pg-card">
<!-- 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="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 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>
@@ -75,8 +130,12 @@
<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 declarations; track d.declarationId) {
@for (d of visibleDeclarations; track d.declarationId) {
<tr>
<td>{{ d.productName }}</td>
<td>{{ d.periodStartDate }} → {{ d.periodEndDate }}</td>
@@ -95,12 +154,27 @@
</table>
</div>
<div class="pg-card" style="margin-top: 16px">
<div class="pg-card" [hidden]="activeTab !== 'overdue'">
<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 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>
@@ -122,10 +196,19 @@
<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 rows; track row.lockId) {
@for (row of visibleOverdueRows; track row.lockId) {
<tr>
<td>{{ row.clientName }}</td>
<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>
@@ -181,7 +264,7 @@
</table>
</div>
<div class="pg-card" style="margin-top: 16px">
<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">
@@ -196,9 +279,21 @@
</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 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
@@ -246,7 +341,7 @@
<th>Release Due</th>
<th>Status</th>
<th>Applied Loan</th>
<th></th>
<th>Demand Draft</th>
</tr>
</thead>
<tbody>
@@ -256,12 +351,23 @@
</tr>
} @else if (locksLoaded && visibleLocks.length === 0) {
<tr>
<td colspan="8" class="pg-empty">No dividend locks match this filter.</td>
<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 }}</td>
<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>
@@ -279,170 +385,21 @@
<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.status === 'UNCLAIMED') {
<button type="button" class="pg-btn" (click)="toggleClaim(l)">
{{ expandedClaimLockId === l.lockId ? 'Cancel' : 'Claim' }}
@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>
@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' }}
@if (l.financialYear) {
<span> (FY {{ l.financialYear }} dividend)</span>
}
— issue as a Demand Draft
</div>
<!-- A dividend is collected only as a Demand Draft. Amount defaults to the full
unclaimed balance; anything left unclaimed stays on the lock for a later visit. -->
<label class="inv-form-field">
<span>DD Amount</span>
<input type="number" [(ngModel)]="claimDdAmount[l.lockId]" min="0" step="0.01" />
</label>
<label class="inv-form-field">
<span>Beneficiary Name</span>
<input type="text" [(ngModel)]="claimDdBeneficiary[l.lockId]" />
</label>
<label class="inv-form-field">
<span>Payable At — Branch</span>
<input type="text" [(ngModel)]="claimDdPayableAtBranch[l.lockId]" />
</label>
<label class="inv-form-field">
<span>Payable At — City</span>
<input type="text" [(ngModel)]="claimDdPayableAtCity[l.lockId]" />
</label>
<div class="dd-apply-panel-total">
Claiming: {{ claimDdAmount[l.lockId] || 0 | 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>
<!-- The draft just drawn. Approving refreshes the list, so the row it came from is gone by the
time the clerk looks — and the DD number is exactly what they need to write on the
counterfoil and hand over. -->
@if (lastIssuedDd) {
<div class="dd-issued-banner">
<div class="dd-issued-banner__main">
Demand Draft <strong>{{ lastIssuedDd.ddNumber }}</strong> issued to {{ lastIssuedDd.clientName }} for
{{ lastIssuedDd.amount | number: '1.2-2' }}
</div>
<button type="button" class="dd-issued-banner__close" (click)="lastIssuedDd = null" aria-label="Dismiss">
&times;
</button>
</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>
<!-- A dividend is collected only as a Demand Draft, so what the checker needs to see is the
draft about to be drawn: for how much, in whose favour, payable where. -->
<th class="num">DD Amount</th>
<th>Beneficiary</th>
<th>Payable At</th>
<th class="num">Unclaimed Balance</th>
<th>Requested By</th>
<th></th>
</tr>
</thead>
<tbody>
@if (claimRequestsLoading) {
<tr>
<td colspan="7" class="pg-empty">Loading…</td>
</tr>
} @else if (claimRequestsLoaded && claimRequests.length === 0) {
<tr>
<td colspan="7" 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 class="num">{{ req.dd_apply_amount | number: '1.2-2' }}</td>
<td>{{ req.dd_beneficiary_name || '—' }}</td>
<td>
@if (req.dd_payable_at_branch || req.dd_payable_at_city) {
{{ req.dd_payable_at_branch }}
@if (req.dd_payable_at_branch && req.dd_payable_at_city) {
,
}
{{ req.dd_payable_at_city }}
} @else {
—
}
</td>
<td class="num">{{ req.locked_amount | number: '1.2-2' }}</td>
<td>{{ req.clerk_name }}</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="7">
<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>