feat: implement cashier dashboard, dividend dashboards, share accounts views, and checker tasks
This commit is contained in:
@@ -20,37 +20,29 @@ import { SILENT_HTTP_ERROR } from '../core/http/silent-error.token';
|
||||
export class DividendClaimRequestService {
|
||||
private http = inject(HttpClient);
|
||||
|
||||
/**
|
||||
* Records a claim for an unclaimed dividend, collected as a Demand Draft.
|
||||
*
|
||||
* A dividend is only ever paid out as a draft — the savings-credit, cash and loan-set-off routes
|
||||
* this endpoint once accepted are retired, so the payload carries just what a draft needs: how
|
||||
* much, drawn in whose favour, and payable where.
|
||||
*/
|
||||
submit(
|
||||
lockId: number,
|
||||
breakdown: {
|
||||
loanId?: number;
|
||||
loanApplyAmount?: number;
|
||||
savingsAccountId?: number;
|
||||
savingsApplyAmount?: number;
|
||||
cashApplyAmount?: number;
|
||||
tokNo?: string;
|
||||
claim: {
|
||||
ddApplyAmount: number;
|
||||
ddBeneficiaryName: string;
|
||||
ddPayableAtBranch: string;
|
||||
ddPayableAtCity: string;
|
||||
}
|
||||
): Observable<any> {
|
||||
const body: any = { lockId };
|
||||
if (breakdown.loanId != null) {
|
||||
body.loanId = breakdown.loanId;
|
||||
}
|
||||
if (breakdown.loanApplyAmount != null) {
|
||||
body.loanApplyAmount = String(breakdown.loanApplyAmount);
|
||||
}
|
||||
if (breakdown.savingsAccountId != null) {
|
||||
body.savingsAccountId = breakdown.savingsAccountId;
|
||||
}
|
||||
if (breakdown.savingsApplyAmount != null) {
|
||||
body.savingsApplyAmount = String(breakdown.savingsApplyAmount);
|
||||
}
|
||||
if (breakdown.cashApplyAmount != null) {
|
||||
body.cashApplyAmount = String(breakdown.cashApplyAmount);
|
||||
}
|
||||
if (breakdown.tokNo != null) {
|
||||
body.tokNo = breakdown.tokNo;
|
||||
}
|
||||
return this.http.post('/clerk/dividend-claim-requests', body);
|
||||
return this.http.post('/clerk/dividend-claim-requests', {
|
||||
lockId,
|
||||
ddApplyAmount: String(claim.ddApplyAmount),
|
||||
ddBeneficiaryName: claim.ddBeneficiaryName,
|
||||
ddPayableAtBranch: claim.ddPayableAtBranch,
|
||||
ddPayableAtCity: claim.ddPayableAtCity
|
||||
});
|
||||
}
|
||||
|
||||
// Silenced: onTokNoBlur() in Transaction Entry probes this only after the withdrawal-request
|
||||
@@ -68,10 +60,6 @@ export class DividendClaimRequestService {
|
||||
});
|
||||
}
|
||||
|
||||
getOverdueLoans(clientId: number): Observable<any> {
|
||||
return this.http.get(`/clerk/dividend-claim-requests/overdue-loans/${clientId}`);
|
||||
}
|
||||
|
||||
list(status = 'PENDING'): Observable<any> {
|
||||
return this.http.get('/clerk/dividend-claim-requests', { params: { status } });
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
×
|
||||
</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>
|
||||
|
||||
@@ -169,3 +169,78 @@
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
/* Which declaration a lock belongs to. The financial year leads because that is how a member asks
|
||||
for it ("my 2026-27 dividend"); the product and exact period sit under it for the clerk who needs
|
||||
to be certain, without crowding the row. */
|
||||
.dd-dividend-year {
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
color: #1e293b;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dd-dividend-period {
|
||||
font-size: 11.5px;
|
||||
color: #64748b;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Financial-year chips. Sits below the status chips as a second, narrower axis over the same list —
|
||||
status is fetched from the server, year filters what came back. */
|
||||
.dd-year-filter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
padding: 0 16px 12px;
|
||||
|
||||
&__label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #64748b;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
|
||||
&__total {
|
||||
margin-left: auto;
|
||||
font-size: 12.5px;
|
||||
color: #475569;
|
||||
font-variant-numeric: tabular-nums;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
/* The DD number just drawn. Approving refreshes the pending list, so the row it came from is gone
|
||||
by the time the clerk reads it — and that number is what goes on the counterfoil. */
|
||||
.dd-issued-banner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin: 12px 16px 0;
|
||||
padding: 10px 14px;
|
||||
border: 1px solid #86efac;
|
||||
border-radius: 6px;
|
||||
background: #f0fdf4;
|
||||
color: #166534;
|
||||
font-size: 13.5px;
|
||||
|
||||
&__main {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&__close {
|
||||
border: none;
|
||||
background: none;
|
||||
color: #166534;
|
||||
font-size: 20px;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
padding: 0 4px;
|
||||
|
||||
&:hover {
|
||||
color: #14532d;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,19 +56,30 @@ export class DividendDashboardComponent implements OnInit {
|
||||
declarationsLoading = false;
|
||||
declarationsLoaded = false;
|
||||
|
||||
/** Every lock the status filter returned, before the year filter narrows it. */
|
||||
locks: any[] = [];
|
||||
locksLoading = false;
|
||||
locksLoaded = false;
|
||||
lockStatusFilter = 'ALL';
|
||||
|
||||
/**
|
||||
* Financial years that actually have locks, newest first — derived from the data rather than
|
||||
* listed anywhere, so a new declaration adds its year on its own and a year whose locks are all
|
||||
* gone stops being offered. No maintenance as years roll over.
|
||||
*/
|
||||
lockYears: string[] = [];
|
||||
lockYearFilter = 'ALL';
|
||||
|
||||
expandedClaimLockId: number | null = null;
|
||||
claimOverdueLoans: Record<number, any[]> = {};
|
||||
claimLoanId: Record<number, number | null> = {};
|
||||
claimLoanAmount: Record<number, number | null> = {};
|
||||
claimSavingsAmount: Record<number, number | null> = {};
|
||||
claimSavingsAccountOverride: Record<number, number | null> = {};
|
||||
claimCashAmount: Record<number, number | null> = {};
|
||||
claimTokNo: Record<number, string> = {};
|
||||
/**
|
||||
* A dividend is collected as a Demand Draft and nothing else — the savings-credit, cash and
|
||||
* loan-set-off routes are retired, so the panel only gathers what a draft needs: how much, drawn
|
||||
* in whose favour, and payable where.
|
||||
*/
|
||||
claimDdAmount: Record<number, number | null> = {};
|
||||
claimDdBeneficiary: Record<number, string> = {};
|
||||
claimDdPayableAtBranch: Record<number, string> = {};
|
||||
claimDdPayableAtCity: Record<number, string> = {};
|
||||
claimSubmitting = false;
|
||||
claimError: string | null = null;
|
||||
|
||||
@@ -77,6 +88,13 @@ export class DividendDashboardComponent implements OnInit {
|
||||
claimRequestsLoaded = false;
|
||||
claimActioningId: number | null = null;
|
||||
claimActionError: Record<number, string> = {};
|
||||
|
||||
/**
|
||||
* The Demand Draft just drawn by approving a claim. Held so the clerk can read off the number
|
||||
* after the row it came from has left the pending list — it is what goes on the counterfoil and
|
||||
* into the member's hand, and there is nowhere else on this screen to find it.
|
||||
*/
|
||||
lastIssuedDd: { ddNumber: string; clientName: string; amount: number } | null = null;
|
||||
expandedRejectClaimId: number | null = null;
|
||||
rejectReason: Record<number, string> = {};
|
||||
|
||||
@@ -95,74 +113,45 @@ export class DividendDashboardComponent implements OnInit {
|
||||
}
|
||||
this.expandedClaimLockId = lock.lockId;
|
||||
this.claimError = null;
|
||||
this.claimLoanId[lock.lockId] = null;
|
||||
this.claimLoanAmount[lock.lockId] = null;
|
||||
this.claimSavingsAmount[lock.lockId] = Number(lock.amount);
|
||||
this.claimSavingsAccountOverride[lock.lockId] = null;
|
||||
this.claimCashAmount[lock.lockId] = null;
|
||||
this.claimTokNo[lock.lockId] = '';
|
||||
this.claimRequestService
|
||||
.getOverdueLoans(lock.clientId)
|
||||
.pipe(catchError(() => of([])))
|
||||
.subscribe((loans: any[]) => {
|
||||
this.claimOverdueLoans[lock.lockId] = loans ?? [];
|
||||
if (loans && loans.length > 0) {
|
||||
this.claimLoanId[lock.lockId] = loans[0].loanId;
|
||||
const loanAmt = Math.min(Number(lock.amount), Number(loans[0].overdueAmount));
|
||||
this.claimLoanAmount[lock.lockId] = loanAmt;
|
||||
this.claimSavingsAmount[lock.lockId] = Number((Number(lock.amount) - loanAmt).toFixed(2));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
claimTotal(lock: any): number {
|
||||
return (
|
||||
Number(this.claimLoanAmount[lock.lockId] || 0) +
|
||||
Number(this.claimSavingsAmount[lock.lockId] || 0) +
|
||||
Math.floor(Number(this.claimCashAmount[lock.lockId] || 0))
|
||||
);
|
||||
// Defaults to the whole unclaimed balance and the member's own name, which is what a member
|
||||
// asks for almost every time; the clerk overrides only when the member wants otherwise.
|
||||
this.claimDdAmount[lock.lockId] = Number(lock.amount);
|
||||
this.claimDdBeneficiary[lock.lockId] = lock.clientName ?? '';
|
||||
this.claimDdPayableAtBranch[lock.lockId] = '';
|
||||
this.claimDdPayableAtCity[lock.lockId] = '';
|
||||
}
|
||||
|
||||
submitClaim(lock: any): void {
|
||||
const loanAmount = Number(this.claimLoanAmount[lock.lockId] || 0);
|
||||
const savingsAmount = Number(this.claimSavingsAmount[lock.lockId] || 0);
|
||||
// Cash can only be handed over in whole-rupee notes/coins (no denomination below ₹1), while a
|
||||
// dividend share often works out to paise — floor to the nearest rupee so the denomination
|
||||
// total the cashier counts can always match exactly; any leftover paise stays unclaimed.
|
||||
const cashAmount = Math.floor(Number(this.claimCashAmount[lock.lockId] || 0));
|
||||
const total = loanAmount + savingsAmount + cashAmount;
|
||||
if (total <= 0) {
|
||||
this.claimError = 'Enter at least one amount (loan, savings, or cash), greater than zero.';
|
||||
const ddAmount = Number(this.claimDdAmount[lock.lockId] || 0);
|
||||
const beneficiary = this.claimDdBeneficiary[lock.lockId]?.trim();
|
||||
const payableAtBranch = this.claimDdPayableAtBranch[lock.lockId]?.trim();
|
||||
const payableAtCity = this.claimDdPayableAtCity[lock.lockId]?.trim();
|
||||
|
||||
if (ddAmount <= 0) {
|
||||
this.claimError = 'Enter a DD amount greater than zero.';
|
||||
return;
|
||||
}
|
||||
if (total > Number(lock.amount)) {
|
||||
this.claimError = `Total (${total}) cannot exceed the unclaimed amount of ${lock.amount}.`;
|
||||
if (ddAmount > Number(lock.amount)) {
|
||||
this.claimError = `DD amount (${ddAmount}) cannot exceed the unclaimed amount of ${lock.amount}.`;
|
||||
return;
|
||||
}
|
||||
if (loanAmount > 0 && !this.claimLoanId[lock.lockId]) {
|
||||
this.claimError = 'Select a loan to apply the loan amount towards.';
|
||||
if (!beneficiary) {
|
||||
this.claimError = 'Enter the beneficiary name — a Demand Draft cannot be drawn without a payee.';
|
||||
return;
|
||||
}
|
||||
if (savingsAmount > 0 && !lock.savingsAccountId && !this.claimSavingsAccountOverride[lock.lockId]) {
|
||||
this.claimError = 'This share account has no linked savings account — enter a Savings Account ID to credit.';
|
||||
return;
|
||||
}
|
||||
const tokNo = this.claimTokNo[lock.lockId]?.trim();
|
||||
if (cashAmount > 0 && !tokNo) {
|
||||
this.claimError = 'Enter a Token No. for the cash portion — the cashier will use it to hand over the cash.';
|
||||
if (!payableAtBranch || !payableAtCity) {
|
||||
this.claimError = 'Enter both the branch and city the Demand Draft is payable at.';
|
||||
return;
|
||||
}
|
||||
|
||||
this.claimSubmitting = true;
|
||||
this.claimError = null;
|
||||
this.claimRequestService
|
||||
.submit(lock.lockId, {
|
||||
loanId: loanAmount > 0 ? this.claimLoanId[lock.lockId] : undefined,
|
||||
loanApplyAmount: loanAmount > 0 ? loanAmount : undefined,
|
||||
savingsAccountId:
|
||||
savingsAmount > 0 && !lock.savingsAccountId ? this.claimSavingsAccountOverride[lock.lockId] : undefined,
|
||||
savingsApplyAmount: savingsAmount > 0 ? savingsAmount : undefined,
|
||||
cashApplyAmount: cashAmount > 0 ? cashAmount : undefined,
|
||||
tokNo: cashAmount > 0 ? tokNo : undefined
|
||||
ddApplyAmount: ddAmount,
|
||||
ddBeneficiaryName: beneficiary,
|
||||
ddPayableAtBranch: payableAtBranch,
|
||||
ddPayableAtCity: payableAtCity
|
||||
})
|
||||
.subscribe({
|
||||
next: () => {
|
||||
@@ -193,8 +182,18 @@ export class DividendDashboardComponent implements OnInit {
|
||||
this.claimActioningId = req.id;
|
||||
delete this.claimActionError[req.id];
|
||||
this.claimRequestService.approve(req.id).subscribe({
|
||||
next: () => {
|
||||
next: (res: any) => {
|
||||
this.claimActioningId = null;
|
||||
// The draft number is the one thing the clerk needs off this action — it is what they write
|
||||
// on the counterfoil and hand to the member. Approving refreshes the list, so the row this
|
||||
// came from disappears; without surfacing it here the number would be lost.
|
||||
if (res?.ddNumber) {
|
||||
this.lastIssuedDd = {
|
||||
ddNumber: res.ddNumber,
|
||||
clientName: req.client_name,
|
||||
amount: Number(res.totalDisbursed) || Number(req.dd_apply_amount) || 0
|
||||
};
|
||||
}
|
||||
this.loadClaimRequests();
|
||||
this.loadLocks();
|
||||
this.loadSummary();
|
||||
@@ -269,16 +268,49 @@ export class DividendDashboardComponent implements OnInit {
|
||||
.pipe(catchError(() => of([])))
|
||||
.subscribe((res: any[]) => {
|
||||
this.locks = res ?? [];
|
||||
this.rebuildLockYears();
|
||||
this.locksLoading = false;
|
||||
this.locksLoaded = true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuilds the year chips from whatever locks came back, newest first.
|
||||
*
|
||||
* A year the current status filter has no locks for disappears from the chips, so the selected
|
||||
* year is reset to ALL when that happens — otherwise the table would show nothing with no
|
||||
* indication why.
|
||||
*/
|
||||
private rebuildLockYears(): void {
|
||||
this.lockYears = [...new Set(this.locks.map((l) => l.financialYear).filter((y) => !!y))].sort((a, b) =>
|
||||
b.localeCompare(a)
|
||||
);
|
||||
if (this.lockYearFilter !== 'ALL' && !this.lockYears.includes(this.lockYearFilter)) {
|
||||
this.lockYearFilter = 'ALL';
|
||||
}
|
||||
}
|
||||
|
||||
/** Locks the table actually renders — the status filter is applied server-side, the year here. */
|
||||
get visibleLocks(): any[] {
|
||||
return this.lockYearFilter === 'ALL'
|
||||
? this.locks
|
||||
: this.locks.filter((l) => l.financialYear === this.lockYearFilter);
|
||||
}
|
||||
|
||||
/** Total of what is on screen, so a year's worth of dividend can be read off directly. */
|
||||
get visibleLocksTotal(): number {
|
||||
return this.visibleLocks.reduce((sum, l) => sum + (Number(l.amount) || 0), 0);
|
||||
}
|
||||
|
||||
setLockStatusFilter(status: string): void {
|
||||
this.lockStatusFilter = status;
|
||||
this.loadLocks();
|
||||
}
|
||||
|
||||
setLockYearFilter(year: string): void {
|
||||
this.lockYearFilter = year;
|
||||
}
|
||||
|
||||
lockStatusClass(status: string): string {
|
||||
switch (status) {
|
||||
case 'UNCLAIMED':
|
||||
|
||||
Reference in New Issue
Block a user