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:
co-authored by
Claude Sonnet 4.6
parent
de420bb8ab
commit
61cb09f352
@@ -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 & 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']);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user