feat(cashier+checker): add cashier deposit maker-checker flow and voucher system
- Add cashier deposit service and submit flow via maker-checker - New CashierDepositService: submit, approve, reject, list endpoints - Transaction-entry: cashier SD deposit goes to deposit-requests instead of direct credit - Checker inbox: new Deposit Requests tab with approve/reject review dialog - Cashier nav: My Deposit Requests tracking page - Withdrawal requests: show voucher_no from backend instead of JS-padded id - clerk-withdrawal.service: disburse denominationData accepts null for non-cash - Role-based checker enforcement: only manager/head-cashier can approve/reject - Maker cannot approve own request (self-approval blocked at backend) - Denomination total validation: breakdown must match transaction amount Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
61cb09f352
commit
e5d6cd32dc
@@ -0,0 +1,248 @@
|
||||
<!--
|
||||
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/.
|
||||
-->
|
||||
|
||||
<div class="pg">
|
||||
<!-- ── HEADER ── -->
|
||||
<header class="pg-head">
|
||||
<div>
|
||||
<div class="pg-eyebrow">CASHIER · DEPOSIT REQUESTS</div>
|
||||
<h1 class="pg-title">My Deposit Requests</h1>
|
||||
<p class="pg-sub">Track the status of your submitted deposit requests awaiting manager approval.</p>
|
||||
</div>
|
||||
<button type="button" class="pg-btn pg-btn--primary" (click)="newDeposit()">
|
||||
<mat-icon>add</mat-icon> New Deposit
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<!-- ── FILTER ROW ── -->
|
||||
<div class="pg-filter-row">
|
||||
<select class="mdr-select" [(ngModel)]="filterStatus" (change)="onFilterChange()">
|
||||
@for (opt of statusOptions; track opt.value) {
|
||||
<option [value]="opt.value">{{ opt.label }}</option>
|
||||
}
|
||||
</select>
|
||||
|
||||
<span class="pg-fb-sep"></span>
|
||||
|
||||
<div class="pg-presets">
|
||||
@for (p of datePresets; track p.key) {
|
||||
<button
|
||||
type="button"
|
||||
class="pg-preset-chip"
|
||||
[class.pg-preset-chip--active]="activePreset === p.key"
|
||||
(click)="applyPreset(p.key)"
|
||||
>
|
||||
{{ p.label }}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
<span class="pg-fb-sep"></span>
|
||||
|
||||
<div class="pg-date-range">
|
||||
<div class="pg-date-box">
|
||||
<span class="pg-date-box__lbl">From</span>
|
||||
<input
|
||||
class="pg-date-box__input"
|
||||
[matDatepicker]="fromPicker"
|
||||
[(ngModel)]="fromDate"
|
||||
[max]="maxDate"
|
||||
(dateChange)="onFromDateChange()"
|
||||
readonly
|
||||
/>
|
||||
<button type="button" class="pg-date-box__btn" (click)="fromPicker.open()">
|
||||
<mat-icon>event</mat-icon>
|
||||
</button>
|
||||
<mat-datepicker #fromPicker></mat-datepicker>
|
||||
</div>
|
||||
<span class="pg-date-range__sep">—</span>
|
||||
<div class="pg-date-box">
|
||||
<span class="pg-date-box__lbl">To</span>
|
||||
<input
|
||||
class="pg-date-box__input"
|
||||
[matDatepicker]="toPicker"
|
||||
[(ngModel)]="toDate"
|
||||
[min]="fromDate"
|
||||
[max]="maxDate"
|
||||
(dateChange)="onToDateChange()"
|
||||
readonly
|
||||
/>
|
||||
<button type="button" class="pg-date-box__btn" (click)="toPicker.open()">
|
||||
<mat-icon>event</mat-icon>
|
||||
</button>
|
||||
<mat-datepicker #toPicker></mat-datepicker>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="button" class="mdr-btn-refresh" (click)="load()" [disabled]="loading">
|
||||
<mat-icon>refresh</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- ── TABLE CARD ── -->
|
||||
<div class="pg-card">
|
||||
@if (loading) {
|
||||
<div class="pg-empty">Loading requests…</div>
|
||||
} @else if (loaded && requests.length === 0) {
|
||||
<div class="pg-empty">
|
||||
<mat-icon style="font-size: 36px; color: #cbd5e1; display: block; text-align: center">inbox</mat-icon>
|
||||
<p style="margin-top: 8px; color: #94a3b8">No deposit requests found.</p>
|
||||
<button type="button" class="pg-btn pg-btn--primary" (click)="newDeposit()" style="margin-top: 12px">
|
||||
<mat-icon>add</mat-icon> New Deposit
|
||||
</button>
|
||||
</div>
|
||||
} @else {
|
||||
<table class="pg-table mdr-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Voucher No</th>
|
||||
<th>Account</th>
|
||||
<th>Client</th>
|
||||
<th class="num">Amount</th>
|
||||
<th>Payment Mode</th>
|
||||
<th>Status</th>
|
||||
<th>Submitted</th>
|
||||
<th>Reviewed By</th>
|
||||
<th>Rejection Reason</th>
|
||||
<th class="mdr-th-action">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (r of requests; track r.id) {
|
||||
<tr
|
||||
class="mdr-row"
|
||||
[class.mdr-row--pending]="r.status === 'PENDING'"
|
||||
[class.mdr-row--approved]="r.status === 'APPROVED'"
|
||||
[class.mdr-row--rejected]="r.status === 'REJECTED'"
|
||||
>
|
||||
<td class="cell-mono mdr-voucher">{{ r.voucher_no || '—' }}</td>
|
||||
<td class="cell-mono">{{ r.account_no }}</td>
|
||||
<td>{{ r.client_name }}</td>
|
||||
<td class="num">₹{{ r.amount | number: '1.2-2' }}</td>
|
||||
<td>{{ r.payment_type_name || 'Cash' }}</td>
|
||||
<td>
|
||||
<span class="mdr-status" [ngClass]="statusClass(r.status)">
|
||||
{{ statusLabel(r.status) }}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ formatDateTime(r.submitted_at) }}</td>
|
||||
<td>{{ r.manager_name || r.manager_username || '—' }}</td>
|
||||
<td class="mdr-rejection">
|
||||
@if (r.status === 'REJECTED' && r.rejection_reason) {
|
||||
<span class="mdr-rejection-text" [title]="r.rejection_reason">
|
||||
{{ r.rejection_reason | slice: 0 : 60 }}{{ r.rejection_reason?.length > 60 ? '…' : '' }}
|
||||
</span>
|
||||
} @else {
|
||||
<span class="mdr-none">—</span>
|
||||
}
|
||||
</td>
|
||||
<td class="mdr-actions-cell">
|
||||
<button type="button" class="pg-btn mdr-btn-view" (click)="view(r)">
|
||||
<mat-icon>visibility</mat-icon> View
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<mat-paginator
|
||||
[length]="totalRecords"
|
||||
[pageSize]="pageSize"
|
||||
[pageSizeOptions]="pageSizeOptions"
|
||||
[pageIndex]="pageIndex"
|
||||
showFirstLastButtons
|
||||
(page)="onPageChange($event)"
|
||||
></mat-paginator>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── VIEW MODAL ── -->
|
||||
@if (viewRequest) {
|
||||
<div class="mdr-modal-overlay" (click)="closeView()">
|
||||
<div class="mdr-modal" (click)="$event.stopPropagation()">
|
||||
<div class="mdr-modal__header">
|
||||
<div>
|
||||
<div class="mdr-modal__eyebrow">DEPOSIT REQUEST · VOUCHER {{ viewRequest.voucher_no || '—' }}</div>
|
||||
<div class="mdr-modal__title">
|
||||
₹{{ viewRequest.amount | number: '1.2-2' }} — {{ viewRequest.client_name }}
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="mdr-modal__close" (click)="closeView()">
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
<div class="mdr-modal__body">
|
||||
<div class="mdr-detail-grid">
|
||||
<div class="mdr-detail">
|
||||
<span class="mdr-detail__lbl">Account No</span>
|
||||
<span class="mdr-detail__val cell-mono">{{ viewRequest.account_no }}</span>
|
||||
</div>
|
||||
<div class="mdr-detail">
|
||||
<span class="mdr-detail__lbl">Payment Mode</span>
|
||||
<span class="mdr-detail__val">{{ viewRequest.payment_type_name || 'Cash' }}</span>
|
||||
</div>
|
||||
<div class="mdr-detail">
|
||||
<span class="mdr-detail__lbl">Status</span>
|
||||
<span class="mdr-detail__val">
|
||||
<span class="mdr-status" [ngClass]="statusClass(viewRequest.status)">
|
||||
{{ statusLabel(viewRequest.status) }}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="mdr-detail">
|
||||
<span class="mdr-detail__lbl">Submitted</span>
|
||||
<span class="mdr-detail__val">{{ formatDateTime(viewRequest.submitted_at) }}</span>
|
||||
</div>
|
||||
@if (viewRequest.manager_name || viewRequest.manager_username) {
|
||||
<div class="mdr-detail">
|
||||
<span class="mdr-detail__lbl">Reviewed By</span>
|
||||
<span class="mdr-detail__val">{{ viewRequest.manager_name || viewRequest.manager_username }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (viewRequest.actioned_at) {
|
||||
<div class="mdr-detail">
|
||||
<span class="mdr-detail__lbl">Actioned At</span>
|
||||
<span class="mdr-detail__val">{{ formatDateTime(viewRequest.actioned_at) }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (viewRequest.txn_id) {
|
||||
<div class="mdr-detail">
|
||||
<span class="mdr-detail__lbl">Transaction Ref</span>
|
||||
<span class="mdr-detail__val cell-mono">{{ viewRequest.txn_id }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (viewRequest.description) {
|
||||
<div class="mdr-detail mdr-detail--wide">
|
||||
<span class="mdr-detail__lbl">Description</span>
|
||||
<span class="mdr-detail__val">{{ viewRequest.description }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (viewRequest.status === 'REJECTED' && viewRequest.rejection_reason) {
|
||||
<div class="mdr-detail mdr-detail--wide">
|
||||
<span class="mdr-detail__lbl">Rejection Reason</span>
|
||||
<span class="mdr-detail__val mdr-detail__val--reject">{{ viewRequest.rejection_reason }}</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@if (viewRequest.status === 'REJECTED') {
|
||||
<div class="mdr-hint">This request was rejected. Enter a fresh deposit at Transaction Entry to resubmit.</div>
|
||||
}
|
||||
</div>
|
||||
<div class="mdr-modal__actions">
|
||||
<button type="button" class="pg-btn" (click)="closeView()">Close</button>
|
||||
@if (viewRequest.status === 'REJECTED') {
|
||||
<button type="button" class="pg-btn pg-btn--primary" (click)="newDeposit()">
|
||||
<mat-icon>add</mat-icon> New Deposit
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
Reference in New Issue
Block a user