feat(cashier,savings,tasks): clerk-manager-cashier account closure flow
Replaces the one-click Close action on the savings account page (removed from the Danger Zone menu) with a maker-checker flow, since closing an account pays real cash out of a teller's drawer. Clerk: an Action button on Transaction Entry, shown once an ACTIVE savings account is loaded. Ticking "Close this account" previews the payout -- balance plus interest calculated to today by the same code the closure itself runs -- and Save drops that figure into the slip's AMOUNT. The clerk then fills TOK. NO. on the slip and presses Save, which raises the request. Token and narration deliberately live on the slip, not in the popup, so the closure is entered like any other transaction. Manager: a Closure Requests tab in the Approval Center, reviewed in a popup dialog (matching Disbursement Requests) rather than a separate route. Cashier: the closure token is recognised at the counter alongside withdrawal and dividend tokens, auto-filling the rounded cash figure, locking it, and closing the account once the denomination is counted. Save-path fixes, all of which silently blocked a valid closure: - amount came from the denomination total whenever clerkMode was false -- which it is for any route other than the Clerk Dashboard wrapper -- so the calculated payout was replaced by zero - the balance check rejected the payout because it legitimately exceeds today's withdrawable amount: it includes interest close() posts before withdrawing. Min-balance, withdrawal-facility and clerk-side denomination checks do not apply to a closure either - on the cashier side the account is already CLOSED, so accountDetails is a stub with no balance and the same check reported "Withdrawable 0.00" - the closure branch had to precede the plain clerk-withdrawal branch: a closure is a full-balance withdrawal, so that branch would have swallowed it and the account would never have closed - onSave's one silent return now says what is missing, instead of leaving Save looking like a broken button Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -236,14 +236,14 @@
|
||||
[title]="row.acTyp === 'SHR' ? 'Number of Shares' : 'Transaction Amount'"
|
||||
>
|
||||
{{ row.acTyp === 'SHR' ? 'SHARE NO.' : 'AMOUNT' }}
|
||||
@if (!isClerk && activeWithdrawalRequest) {
|
||||
<span class="ws-amt-locked-badge" title="Amount locked — approved withdrawal">🔒</span>
|
||||
@if (!isClerk && (activeWithdrawalRequest || activeClosureRequest)) {
|
||||
<span class="ws-amt-locked-badge" title="Amount locked — approved request">🔒</span>
|
||||
}
|
||||
</label>
|
||||
<div
|
||||
class="ws-amt-wrap"
|
||||
[class.ws-amt-wrap--cash]="isCashPayment && selectedRow === row"
|
||||
[class.ws-amt-wrap--locked]="!isClerk && !!activeWithdrawalRequest"
|
||||
[class.ws-amt-wrap--locked]="!isClerk && !!(activeWithdrawalRequest || activeClosureRequest)"
|
||||
>
|
||||
@if (row.acTyp !== 'SHR') {
|
||||
<span class="ws-amt-prefix">₹</span>
|
||||
@@ -255,8 +255,8 @@
|
||||
(input)="row.amount = +$any($event.target).value; syncShareLinkedAmounts()"
|
||||
(click)="$event.stopPropagation()"
|
||||
[placeholder]="row.acTyp === 'SHR' ? '0' : '0.00'"
|
||||
[readonly]="!isClerk && !!activeWithdrawalRequest"
|
||||
[class.ws-amt-input--locked]="!isClerk && !!activeWithdrawalRequest"
|
||||
[readonly]="!isClerk && !!(activeWithdrawalRequest || activeClosureRequest)"
|
||||
[class.ws-amt-input--locked]="!isClerk && !!(activeWithdrawalRequest || activeClosureRequest)"
|
||||
/>
|
||||
</div>
|
||||
@if (row.acTyp === 'SHR' && rowSharePrices[row.acNo]) {
|
||||
@@ -311,8 +311,8 @@
|
||||
<input
|
||||
class="ws-f__ctl"
|
||||
[(ngModel)]="row.tokNo"
|
||||
[readonly]="!isClerk && !!activeWithdrawalRequest"
|
||||
[class.ws-f__ctl--readonly]="!isClerk && !!activeWithdrawalRequest"
|
||||
[readonly]="!isClerk && !!(activeWithdrawalRequest || activeClosureRequest)"
|
||||
[class.ws-f__ctl--readonly]="!isClerk && !!(activeWithdrawalRequest || activeClosureRequest)"
|
||||
placeholder="Enter token"
|
||||
(blur)="onTokNoBlur(row)"
|
||||
(click)="$event.stopPropagation()"
|
||||
@@ -436,6 +436,73 @@
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
@if (isClerk && pendingClosure) {
|
||||
<div class="ws-withdrawal-banner ws-withdrawal-banner--closure">
|
||||
<mat-icon class="ws-withdrawal-banner__icon">lock</mat-icon>
|
||||
<div class="ws-withdrawal-banner__content">
|
||||
<div class="ws-withdrawal-banner__title">Closing This Account</div>
|
||||
<div class="ws-withdrawal-banner__details">
|
||||
@if (closurePreview) {
|
||||
<span
|
||||
>Balance: <strong>₹{{ closurePreview.balance | number: '1.2-2' }}</strong></span
|
||||
>
|
||||
<span class="ws-wb-sep">·</span>
|
||||
<span
|
||||
>Interest: <strong>+₹{{ closurePreview.interest | number: '1.2-2' }}</strong></span
|
||||
>
|
||||
<span class="ws-wb-sep">·</span>
|
||||
<span
|
||||
>Payable: <strong>₹{{ closurePreview.total | number: '1.2-2' }}</strong></span
|
||||
>
|
||||
}
|
||||
</div>
|
||||
<div class="ws-withdrawal-banner__note">
|
||||
Enter <strong>TOK. NO.</strong> below and press <strong>Save</strong> to send this for the Bank Manager's
|
||||
approval. The account closes when the cashier hands over the cash.
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="ws-withdrawal-banner__clear" (click)="clearPendingClosure()" title="Cancel closure">
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
@if (!isClerk && activeClosureRequest) {
|
||||
<div class="ws-withdrawal-banner">
|
||||
<mat-icon class="ws-withdrawal-banner__icon">lock</mat-icon>
|
||||
<div class="ws-withdrawal-banner__content">
|
||||
<div class="ws-withdrawal-banner__title">Approved Account Closure Linked</div>
|
||||
<div class="ws-withdrawal-banner__details">
|
||||
<span
|
||||
>Token: <strong>{{ activeClosureRequest.tok_no }}</strong></span
|
||||
>
|
||||
<span class="ws-wb-sep">·</span>
|
||||
<span
|
||||
>Cash to Hand Over:
|
||||
<strong>₹{{ activeClosureRequest.cashier_disbursed_amount | number: '1.2-2' }}</strong></span
|
||||
>
|
||||
<span class="ws-wb-sep">·</span>
|
||||
<span
|
||||
>Client: <strong>{{ activeClosureRequest.client_name }}</strong></span
|
||||
>
|
||||
<span class="ws-wb-sep">·</span>
|
||||
<span>By Clerk: {{ activeClosureRequest.clerk_name }}</span>
|
||||
<span class="ws-wb-sep">·</span>
|
||||
<span>Approved by: {{ activeClosureRequest.manager_name || '—' }}</span>
|
||||
</div>
|
||||
<div class="ws-withdrawal-banner__note">
|
||||
@if (activeClosureRequest.rounding_amount && activeClosureRequest.rounding_amount !== 0) {
|
||||
Exact closing balance was ₹{{ activeClosureRequest.closing_amount | number: '1.2-2' }} — rounded
|
||||
{{ activeClosureRequest.rounding_amount > 0 ? 'up' : 'down' }} to whole rupees for cash.
|
||||
} @else {
|
||||
The account is already closed. Hand over the cash to complete the request.
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="ws-withdrawal-banner__clear" (click)="activeClosureRequest = null" title="Unlink">
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- ── STATUS ────────────────────────────────────────────────────────── -->
|
||||
@if (loadingAccount) {
|
||||
@@ -1443,14 +1510,16 @@
|
||||
<div class="ws-f ws-f--amt">
|
||||
<label class="ws-f__lbl">
|
||||
{{ row.acTyp === 'SHR' ? 'SHARE NO.' : 'AMOUNT' }}
|
||||
@if (!isClerk && activeWithdrawalRequest) {
|
||||
<span class="ws-amt-locked-badge" title="Amount locked — approved withdrawal">🔒</span>
|
||||
@if (!isClerk && (activeWithdrawalRequest || activeClosureRequest)) {
|
||||
<span class="ws-amt-locked-badge" title="Amount locked — approved request">🔒</span>
|
||||
}
|
||||
</label>
|
||||
<div
|
||||
class="ws-amt-wrap"
|
||||
[class.ws-amt-wrap--cash]="isCashPayment && selectedRow === row"
|
||||
[class.ws-amt-wrap--locked]="!isClerk && !!activeWithdrawalRequest"
|
||||
[class.ws-amt-wrap--locked]="
|
||||
!isClerk && !!(activeWithdrawalRequest || activeClosureRequest)
|
||||
"
|
||||
>
|
||||
@if (row.acTyp !== 'SHR') {
|
||||
<span class="ws-amt-prefix">₹</span>
|
||||
@@ -1462,8 +1531,10 @@
|
||||
(input)="row.amount = +$any($event.target).value; syncShareLinkedAmounts()"
|
||||
(click)="$event.stopPropagation()"
|
||||
[placeholder]="row.acTyp === 'SHR' ? '0' : '0.00'"
|
||||
[readonly]="!isClerk && !!activeWithdrawalRequest"
|
||||
[class.ws-amt-input--locked]="!isClerk && !!activeWithdrawalRequest"
|
||||
[readonly]="!isClerk && !!(activeWithdrawalRequest || activeClosureRequest)"
|
||||
[class.ws-amt-input--locked]="
|
||||
!isClerk && !!(activeWithdrawalRequest || activeClosureRequest)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
@if (row.acTyp === 'SHR' && rowSharePrices[row.acNo]) {
|
||||
@@ -1504,8 +1575,10 @@
|
||||
<input
|
||||
class="ws-f__ctl"
|
||||
[(ngModel)]="row.tokNo"
|
||||
[readonly]="!isClerk && !!activeWithdrawalRequest"
|
||||
[class.ws-f__ctl--readonly]="!isClerk && !!activeWithdrawalRequest"
|
||||
[readonly]="!isClerk && !!(activeWithdrawalRequest || activeClosureRequest)"
|
||||
[class.ws-f__ctl--readonly]="
|
||||
!isClerk && !!(activeWithdrawalRequest || activeClosureRequest)
|
||||
"
|
||||
placeholder="Enter token"
|
||||
(blur)="onTokNoBlur(row)"
|
||||
/>
|
||||
@@ -1673,6 +1746,19 @@
|
||||
My Disbursements
|
||||
</button>
|
||||
}
|
||||
@if (canShowAccountActions) {
|
||||
<button
|
||||
class="ws-btn ws-btn--action"
|
||||
type="button"
|
||||
(click)="openAccountActions()"
|
||||
title="Account actions for the loaded account"
|
||||
>
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
|
||||
<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2" />
|
||||
</svg>
|
||||
Action
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
<div class="ws-footer__actions">
|
||||
<button
|
||||
@@ -1694,6 +1780,81 @@
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- ── ACCOUNT ACTIONS (clerk) ──────────────────────────────────────────── -->
|
||||
@if (showAccountActions) {
|
||||
<div class="te-confirm-overlay" (click)="closeAccountActions()">
|
||||
<div class="te-confirm-box te-aa-box" (click)="$event.stopPropagation()">
|
||||
<div class="te-confirm-header te-confirm-header--action">
|
||||
<span class="te-confirm-icon">⚡</span>
|
||||
<span>Account Actions</span>
|
||||
</div>
|
||||
<div class="te-confirm-body">
|
||||
<div class="te-aa-account">
|
||||
<span class="te-aa-account__no">{{ accountDetails?.accountNo }}</span>
|
||||
<span class="te-aa-account__name">{{ clientName }}</span>
|
||||
</div>
|
||||
|
||||
<label class="te-aa-check">
|
||||
<input type="checkbox" [(ngModel)]="closeAccountChecked" (change)="onCloseAccountToggle()" />
|
||||
<span>
|
||||
<strong>Close this account</strong>
|
||||
<small>Pays out the full balance in cash and closes the account, after manager approval.</small>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
@if (closeAccountChecked) {
|
||||
@if (closurePreviewLoading) {
|
||||
<div class="te-aa-calc te-aa-calc--loading">Calculating interest to date…</div>
|
||||
} @else if (closurePreviewError) {
|
||||
<div class="te-aa-calc te-aa-calc--error">{{ closurePreviewError }}</div>
|
||||
} @else if (closurePreview) {
|
||||
<div class="te-aa-calc">
|
||||
<div class="te-aa-calc__row">
|
||||
<span>Current Balance</span>
|
||||
<span>₹{{ closurePreview.balance | number: '1.2-2' }}</span>
|
||||
</div>
|
||||
<div class="te-aa-calc__row">
|
||||
<span>Interest to date</span>
|
||||
<span>+ ₹{{ closurePreview.interest | number: '1.2-2' }}</span>
|
||||
</div>
|
||||
<div class="te-aa-calc__row te-aa-calc__row--total">
|
||||
<span>Payable on Closure</span>
|
||||
<span>₹{{ closurePreview.total | number: '1.2-2' }}</span>
|
||||
</div>
|
||||
@if (!closurePreview.upToDate) {
|
||||
<div class="te-aa-calc__warn">
|
||||
Interest could not be calculated just now, so this shows the current balance only — the actual payout
|
||||
will be higher once interest is added at approval.
|
||||
</div>
|
||||
}
|
||||
<div class="te-aa-calc__note">
|
||||
Interest calculated to {{ closurePreview.asOfDate }} using the same calculation the closure itself
|
||||
performs. Cash is handed over rounded to whole rupees.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="te-aa-next">
|
||||
This amount will be filled into <strong>AMOUNT</strong> on the slip. Enter <strong>TOK. NO.</strong> and
|
||||
press <strong>Save</strong> to send it for the Bank Manager's approval.
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
<div class="te-confirm-actions">
|
||||
<button class="te-btn" type="button" (click)="closeAccountActions()">Cancel</button>
|
||||
<button
|
||||
class="te-btn te-btn--primary"
|
||||
type="button"
|
||||
[disabled]="closeAccountChecked && (!closurePreview || closurePreviewLoading)"
|
||||
(click)="applyCloseAccount()"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- ── REJECT DIALOG (manager review) ───────────────────────────────────── -->
|
||||
@if (showRejectDialog) {
|
||||
<div class="te-confirm-overlay" (click)="cancelRejectReview()">
|
||||
|
||||
Reference in New Issue
Block a user