feat: implement cashier dashboard, dividend dashboards, share accounts views, and checker tasks

This commit is contained in:
2026-08-25 11:20:42 +05:30
parent 7ef75c902d
commit 0b79b0b0d3
77 changed files with 7396 additions and 426 deletions
@@ -200,11 +200,47 @@
<button type="button" class="pg-btn" [disabled]="locksLoading" (click)="loadLocks()">Refresh</button>
</label>
</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>
<th>Savings Account</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>
@@ -218,16 +254,23 @@
<tr>
<td colspan="8" class="pg-empty">Loading…</td>
</tr>
} @else if (locksLoaded && locks.length === 0) {
} @else if (locksLoaded && visibleLocks.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) {
@for (l of visibleLocks; track l.lockId) {
<tr>
<td>{{ l.clientName }}</td>
<td>
<span class="pg-chip">{{ l.savingsAccountNo }}</span>
@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>
@@ -249,48 +292,33 @@
<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)
Unclaimed amount: {{ l.amount | number: '1.2-2' }}
@if (l.financialYear) {
<span> (FY {{ l.financialYear }} dividend)</span>
}
— issue as a Demand Draft
</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>
}
<!-- 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>Credit to Savings</span>
<input type="number" [(ngModel)]="claimSavingsAmount[l.lockId]" min="0" step="0.01" />
<span>DD Amount</span>
<input type="number" [(ngModel)]="claimDdAmount[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" />
<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>
@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' }}
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>
@@ -315,6 +343,21 @@
<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()">
@@ -326,53 +369,44 @@
<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>
<!-- 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>Client Loan Status</th>
<th>Requested By</th>
<th></th>
</tr>
</thead>
<tbody>
@if (claimRequestsLoading) {
<tr>
<td colspan="8" class="pg-empty">Loading…</td>
<td colspan="7" 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>
<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>
<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>
@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>
@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>{{ req.clerk_name }}</td>
<td>
<button type="button" class="pg-btn" [disabled]="claimActioningId === req.id" (click)="approveClaim(req)">
{{ claimActioningId === req.id ? 'Approving…' : 'Approve' }}
@@ -387,7 +421,7 @@
</tr>
@if (expandedRejectClaimId === req.id) {
<tr class="dd-apply-row">
<td colspan="8">
<td colspan="7">
<div class="dd-apply-panel">
<label class="inv-form-field">
<span>Rejection Reason</span>