feat(cashier): complete maker-checker withdrawal disbursement flow

- Clerk manually enters token no with today-date unique validation (backend enforced)
- Cashier token lookup: auto-fill amount/account, lock amount field, status-specific errors
- Confirm dialog shows CASH DISBURSEMENT (blue) with no balance re-deduction warning
- Disbursement denomination saved to m_savings_txn_denomination for vault position update
- Block direct cashier withdrawal without approved maker-checker token
- DISBURSED status shown in My Requests and Checker inbox views with cashier/time info
- Remove getNextToken() service method and m_daily_token_counter table (0326 migration)
- Race condition protection: UPDATE WHERE status=APPROVED prevents double disbursement

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Jaanu423
2026-06-11 15:57:33 +05:30
co-authored by Claude Sonnet 4.6
parent de420bb8ab
commit 61cb09f352
26 changed files with 4822 additions and 248 deletions
@@ -0,0 +1,102 @@
/**
* 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/.
*/
.wr {
display: flex;
flex-direction: column;
min-height: 100%;
}
.wr-body {
flex: 1;
}
/* Mirrors the clerk dashboard nav (shell .cashier-nav) for identical look & feel */
.cashier-nav {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 2px;
background: #1a2744;
padding: 4px 12px;
border-bottom: 2px solid #0ea5e9;
&__item {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 5px 12px;
border-radius: 4px;
font-size: 12px;
font-weight: 600;
color: #94a3b8;
background: transparent;
border: none;
cursor: pointer;
white-space: nowrap;
transition:
background 0.15s,
color 0.15s;
&:hover {
background: #223060;
color: #e2e8f0;
}
&--active {
background: #0ea5e9;
color: #fff;
}
&--primary {
background: #1353a0;
color: #fff;
font-weight: 700;
&:hover {
background: #1a6ac8;
}
&.cashier-nav__item--active {
background: #1353a0;
}
}
&--sec {
font-size: 11.5px;
padding: 4px 10px;
border: 1px solid rgb(255 255 255 / 12%);
}
}
&__esc-hint {
margin-left: auto;
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 11.5px;
font-weight: 600;
color: #cbd5e1;
cursor: pointer;
padding: 4px 12px;
border-radius: 4px;
border: 1px solid #2a3a5e;
transition:
color 0.15s,
border-color 0.15s;
&:hover {
color: #e2e8f0;
border-color: #0ea5e9;
}
}
&--clerk {
border-bottom-color: #e87722;
}
}
@@ -0,0 +1,145 @@
/**
* 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/.
*/
import { Component, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ActivatedRoute, Router } from '@angular/router';
import { TransactionEntryComponent } from '../../cashier/transaction-entry/transaction-entry.component';
import { AccountFacilitiesComponent } from '../account-facilities/account-facilities.component';
import { PhotoSignatureComponent } from '../photo-signature/photo-signature.component';
import { StandingInstructionsComponent } from '../standing-instructions/standing-instructions.component';
import { TodayActivityComponent } from '../today-activity/today-activity.component';
type ReviewTab = 'dashboard' | 'facilities' | 'photo' | 'si' | 'activity';
/**
* Manager review wrapper — full-screen, self-contained. Renders the same workspace + nav the clerk
* sees, pre-filled from the withdrawal request, with Approve/Reject. The header tabs switch panels
* in-page (no navigation away) and show the reviewed account's data via shared clerk state.
*/
@Component({
selector: 'mifosx-withdrawal-review',
standalone: true,
imports: [
CommonModule,
TransactionEntryComponent,
AccountFacilitiesComponent,
PhotoSignatureComponent,
StandingInstructionsComponent,
TodayActivityComponent
],
template: `
<div class="wr">
<nav class="cashier-nav cashier-nav--clerk">
<button
type="button"
class="cashier-nav__item cashier-nav__item--primary"
[class.cashier-nav__item--active]="activeTab === 'dashboard'"
(click)="activeTab = 'dashboard'"
>
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="2" y="4" width="20" height="16" rx="2" />
<circle cx="8" cy="10" r="2" />
<path d="M14 9h4M14 13h4M5 15h7" />
</svg>
Withdrawal Review
</button>
<button
type="button"
class="cashier-nav__item cashier-nav__item--sec"
[class.cashier-nav__item--active]="activeTab === 'facilities'"
(click)="activeTab = 'facilities'"
>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="1" y="4" width="22" height="16" rx="2" />
<line x1="1" y1="10" x2="23" y2="10" />
</svg>
Account Facilities
</button>
<button
type="button"
class="cashier-nav__item cashier-nav__item--sec"
[class.cashier-nav__item--active]="activeTab === 'photo'"
(click)="activeTab = 'photo'"
>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="8" r="4" />
<path d="M4 20c0-4 3.6-7 8-7s8 3 8 7" />
</svg>
Photo &amp; Signature
</button>
<button
type="button"
class="cashier-nav__item cashier-nav__item--sec"
[class.cashier-nav__item--active]="activeTab === 'si'"
(click)="activeTab = 'si'"
>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="17 1 21 5 17 9" />
<path d="M3 11V9a4 4 0 0 1 4-4h14" />
<polyline points="7 23 3 19 7 15" />
<path d="M21 13v2a4 4 0 0 1-4 4H3" />
</svg>
Standing Instructions
</button>
<button
type="button"
class="cashier-nav__item cashier-nav__item--sec"
[class.cashier-nav__item--active]="activeTab === 'activity'"
(click)="activeTab = 'activity'"
>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="4" width="18" height="18" rx="2" />
<line x1="16" y1="2" x2="16" y2="6" />
<line x1="8" y1="2" x2="8" y2="6" />
<line x1="3" y1="10" x2="21" y2="10" />
</svg>
Today's Activity
</button>
<span class="cashier-nav__esc-hint" (click)="backToInbox()"> ← Back to Inbox </span>
</nav>
<div class="wr-body">
<!-- Dashboard stays mounted so it loads once and keeps shared state fresh -->
<div [hidden]="activeTab !== 'dashboard'">
<mifosx-transaction-entry [clerkMode]="true" [reviewRequestId]="requestId"></mifosx-transaction-entry>
</div>
@if (activeTab === 'facilities') {
<mifosx-account-facilities [embedded]="true"></mifosx-account-facilities>
}
@if (activeTab === 'photo') {
<mifosx-photo-signature [embedded]="true"></mifosx-photo-signature>
}
@if (activeTab === 'si') {
<mifosx-standing-instructions [embedded]="true"></mifosx-standing-instructions>
}
@if (activeTab === 'activity') {
<mifosx-today-activity [embedded]="true"></mifosx-today-activity>
}
</div>
</div>
`,
styleUrl: './withdrawal-review.component.scss'
})
export class WithdrawalReviewComponent {
private route = inject(ActivatedRoute);
private router = inject(Router);
requestId: number | null = null;
activeTab: ReviewTab = 'dashboard';
constructor() {
const id = this.route.snapshot.params['id'];
this.requestId = id != null ? Number(id) : null;
}
backToInbox(): void {
this.router.navigate(['/checker-inbox-and-tasks/withdrawal-requests']);
}
}