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
@@ -6,16 +6,20 @@
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { Component, OnInit, OnDestroy, inject } from '@angular/core';
|
||||
import { Component, OnInit, OnDestroy, inject, Input } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||
import { catchError, debounceTime, distinctUntilChanged } from 'rxjs/operators';
|
||||
import { forkJoin, of, Subject } from 'rxjs';
|
||||
import { MatDialog, MatDialogModule, MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { Router } from '@angular/router';
|
||||
import { CashierService } from '../cashier.service';
|
||||
import { AuthenticationService } from '../../core/authentication/authentication.service';
|
||||
import { ClerkUiStateService, ClerkSavedAccount } from '../clerk-ui-state.service';
|
||||
import { ClerkWithdrawalService } from '../clerk-withdrawal.service';
|
||||
import { SettingsService } from '../../settings/settings.service';
|
||||
|
||||
interface TxnRow {
|
||||
typ: string;
|
||||
@@ -620,15 +624,37 @@ export class ImagePreviewDialogComponent {
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
MatDialogModule
|
||||
MatDialogModule,
|
||||
MatIconModule
|
||||
]
|
||||
})
|
||||
export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
@Input() clerkMode = false;
|
||||
/** When set, the component runs in manager-review mode for that withdrawal request id. */
|
||||
@Input() reviewRequestId: number | null = null;
|
||||
/** When set, the clerk edits & resubmits that (rejected) withdrawal request id. */
|
||||
@Input() editRequestId: number | null = null;
|
||||
|
||||
// Manager review state
|
||||
reviewRequest: any = null;
|
||||
showRejectDialog = false;
|
||||
rejectionReason = '';
|
||||
|
||||
// Clerk edit (rejected resubmit) state
|
||||
editRequest: any = null;
|
||||
|
||||
// Active approved withdrawal (cashier disburse flow)
|
||||
activeWithdrawalRequest: any = null;
|
||||
loadingWithdrawal = false;
|
||||
|
||||
private http = inject(HttpClient);
|
||||
private cashierService = inject(CashierService);
|
||||
private authService = inject(AuthenticationService);
|
||||
private dialog = inject(MatDialog);
|
||||
private clerkUiState = inject(ClerkUiStateService);
|
||||
private clerkWithdrawalService = inject(ClerkWithdrawalService);
|
||||
private router = inject(Router);
|
||||
private settingsService = inject(SettingsService);
|
||||
|
||||
today = '';
|
||||
rows: TxnRow[] = [];
|
||||
@@ -659,6 +685,7 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
|
||||
// Search token — cancels stale async results
|
||||
private searchToken = 0;
|
||||
private lastLoadedAcNo = '';
|
||||
private acNoInput$ = new Subject<{ row: TxnRow; index: number; value: string }>();
|
||||
cashVaultGlId: number | null = null;
|
||||
|
||||
@@ -770,6 +797,16 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
|
||||
this.loadCashVaultGl();
|
||||
|
||||
if (this.reviewMode) {
|
||||
this.loadRequestIntoForm(this.reviewRequestId!, 'review');
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.editMode) {
|
||||
this.loadRequestIntoForm(this.editRequestId!, 'edit');
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isClerk) {
|
||||
const saved = this.clerkUiState.snapshot.savedAccount;
|
||||
if (saved) {
|
||||
@@ -799,6 +836,11 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.persistClerkState();
|
||||
}
|
||||
|
||||
/** Save the full loaded account into shared clerk state so the facility/photo/SI/activity views can read it. */
|
||||
private persistClerkState(): void {
|
||||
if (this.isClerk && this.accountDetails) {
|
||||
this.clerkUiState.saveFullState({
|
||||
accountDetails: this.accountDetails,
|
||||
@@ -1014,7 +1056,7 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
addRow(): void {
|
||||
this.rows.push({
|
||||
const row: TxnRow = {
|
||||
typ: '~',
|
||||
br: 1,
|
||||
trTp: '',
|
||||
@@ -1030,7 +1072,45 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
paymentTypeId: null,
|
||||
bankName: '',
|
||||
routingCode: ''
|
||||
});
|
||||
};
|
||||
this.rows.push(row);
|
||||
// Token is assigned by the backend at actual submission time — not pre-generated here
|
||||
}
|
||||
|
||||
/** Cashier enters a token number → look up the APPROVED withdrawal request for auto-fill. */
|
||||
onTokNoBlur(row: TxnRow): void {
|
||||
if (this.isClerk || !row.tokNo?.trim()) return;
|
||||
const token = row.tokNo.trim();
|
||||
this.loadingWithdrawal = true;
|
||||
this.activeWithdrawalRequest = null;
|
||||
this.clerkWithdrawalService
|
||||
.getByToken(token)
|
||||
.pipe(
|
||||
catchError((err: any) => {
|
||||
this.loadingWithdrawal = false;
|
||||
const msg: string =
|
||||
err?.error?.message ??
|
||||
err?.error?.errors?.[0]?.defaultUserMessage ??
|
||||
`Token '${token}' could not be verified. Please try again.`;
|
||||
this.showAlert('error', `Token '${token}'`, [msg]);
|
||||
return of(null);
|
||||
})
|
||||
)
|
||||
.subscribe((req: any) => {
|
||||
this.loadingWithdrawal = false;
|
||||
if (!req) return;
|
||||
this.activeWithdrawalRequest = req;
|
||||
row.acNo = req.account_no ?? '';
|
||||
row.acTyp = 'SD';
|
||||
row.amount = Number(req.amount) || null;
|
||||
row.dbCr = 'Dr';
|
||||
row.paymentTypeId = req.payment_type_id ?? null;
|
||||
row.description = req.description ?? '';
|
||||
this.selectedRow = row;
|
||||
if (req.account_no) {
|
||||
this.loadAccountDetails(req.account_no.trim(), 'SD');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onAcNoBlur(row: TxnRow, index: number): void {
|
||||
@@ -1041,6 +1121,7 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
|
||||
onAcNoInput(row: TxnRow, index: number, value: string): void {
|
||||
row.acNo = value;
|
||||
this.lastLoadedAcNo = '';
|
||||
this.acNoInput$.next({ row, index, value });
|
||||
}
|
||||
|
||||
@@ -1062,15 +1143,15 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
|
||||
onRowSelect(row: TxnRow, index: number): void {
|
||||
this.selectedRow = row;
|
||||
// Only reload if account number changed or nothing is loaded yet
|
||||
const loadedId = this.accountDetails?.glCode ?? this.accountDetails?.accountNo;
|
||||
const alreadyLoaded = this.accountDetails && loadedId === row.acNo.trim();
|
||||
if (row.acNo.trim() && !alreadyLoaded) {
|
||||
this.loadAccountDetails(row.acNo.trim(), row.acTyp);
|
||||
const acNo = row.acNo.trim();
|
||||
const alreadyLoaded = this.accountDetails && this.lastLoadedAcNo === acNo;
|
||||
if (acNo && !alreadyLoaded) {
|
||||
this.loadAccountDetails(acNo, row.acTyp);
|
||||
}
|
||||
}
|
||||
|
||||
private loadAccountDetails(acNo: string, acTyp: string): void {
|
||||
this.lastLoadedAcNo = acNo;
|
||||
this.loadingAccount = true;
|
||||
this.accountDetails = null;
|
||||
this.clientDetails = null;
|
||||
@@ -1389,6 +1470,8 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
if (blob) {
|
||||
const url = URL.createObjectURL(blob);
|
||||
this.signatureUrl = url;
|
||||
// Signature arrives after the main load — refresh shared state so embedded views get it
|
||||
this.persistClerkState();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1405,6 +1488,9 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
// Today's transactions — savings uses "content", CA uses "pageItems"
|
||||
this.todayTransactions =
|
||||
todayTxns?.pageItems ?? todayTxns?.content ?? (Array.isArray(todayTxns) ? todayTxns : []);
|
||||
|
||||
// Push full state so embedded facility/photo/SI/activity views (e.g. manager review) can read it
|
||||
this.persistClerkState();
|
||||
}
|
||||
);
|
||||
});
|
||||
@@ -1594,7 +1680,26 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
get isClerk(): boolean {
|
||||
return this.authService.isClerkUser();
|
||||
// clerkMode forces the clerk layout (used by the clerk dashboard and the manager review screen)
|
||||
return this.clerkMode || this.authService.isClerkUser();
|
||||
}
|
||||
|
||||
get reviewMode(): boolean {
|
||||
return this.reviewRequestId != null;
|
||||
}
|
||||
|
||||
get editMode(): boolean {
|
||||
return this.editRequestId != null;
|
||||
}
|
||||
|
||||
/** Only REJECTED requests are editable; PENDING/APPROVED open read-only. */
|
||||
get editResubmittable(): boolean {
|
||||
return this.editMode && this.editRequest?.status === 'REJECTED';
|
||||
}
|
||||
|
||||
/** Form is locked in manager review, or when viewing a non-rejected request in edit mode. */
|
||||
get formReadOnly(): boolean {
|
||||
return this.reviewMode || (this.editMode && !this.editResubmittable);
|
||||
}
|
||||
|
||||
get isNonIndividual(): boolean {
|
||||
@@ -1901,12 +2006,12 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
const row = this.selectedRow;
|
||||
if (!row || !row.acNo.trim() || !this.accountDetails) return;
|
||||
|
||||
// Cash payment → amount MUST come from denomination (not manual entry)
|
||||
const selectedPtEarly = this.paymentTypes.find((p: any) => p.id === row.paymentTypeId);
|
||||
const isCash = !!selectedPtEarly?.isCashPayment;
|
||||
const amount = isCash ? this.denominationTotal : (row.amount ?? this.denominationTotal);
|
||||
// Clerk mode: always use manually entered amount; Cashier mode: cash amount comes from denomination
|
||||
const amount = isCash && !this.clerkMode ? this.denominationTotal : (row.amount ?? this.denominationTotal);
|
||||
|
||||
if (isCash && this.denominationTotal <= 0) {
|
||||
if (isCash && this.denominationTotal <= 0 && !this.clerkMode) {
|
||||
this.showAlert('error', 'Denomination Required', [
|
||||
'Cash transaction: Please enter denomination (notes/coins count) first.'
|
||||
]);
|
||||
@@ -1918,6 +2023,47 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
const isDeposit = row.dbCr === 'Cr';
|
||||
|
||||
// Token number is required for all withdrawals (clerk must enter, cashier must enter approved token)
|
||||
if (!this.reviewMode && !isDeposit && !row.tokNo?.trim()) {
|
||||
this.showAlert('error', 'Token Number Required', [
|
||||
this.isClerk
|
||||
? 'Please enter a Token Number (TOK. NO.) for this withdrawal request.'
|
||||
: 'Please enter the approved Token Number (TOK. NO.) to process this withdrawal.'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Cashier direct withdrawal BLOCKED — must use approved maker-checker token
|
||||
if (!this.isClerk && !this.reviewMode && !isDeposit && !this.activeWithdrawalRequest) {
|
||||
this.showAlert('error', 'Direct Withdrawal Not Permitted', [
|
||||
'Withdrawals cannot be processed directly by the cashier.',
|
||||
'All withdrawals must go through the Clerk → Manager approval process.',
|
||||
'Enter the approved Token Number (TOK. NO.) to disburse cash.'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Approved withdrawal: amount/denomination must exactly match
|
||||
if (!this.isClerk && this.activeWithdrawalRequest && !isDeposit) {
|
||||
const approvedAmt = Number(this.activeWithdrawalRequest.amount);
|
||||
if (Math.abs(amount - approvedAmt) > 0.005) {
|
||||
if (isCash) {
|
||||
this.showAlert('error', 'Denomination Mismatch — Approved Withdrawal', [
|
||||
`Cash denomination total ₹${this.denominationTotal.toLocaleString('en-IN', { minimumFractionDigits: 2 })} does not match the approved withdrawal amount ₹${approvedAmt.toLocaleString('en-IN', { minimumFractionDigits: 2 })}.`,
|
||||
`Adjust the notes/coins breakdown to total exactly ₹${approvedAmt.toLocaleString('en-IN', { minimumFractionDigits: 2 })}.`
|
||||
]);
|
||||
} else {
|
||||
this.showAlert('error', 'Amount Mismatch — Approved Withdrawal', [
|
||||
`Amount has been changed from the approved value.`,
|
||||
`Approved: ₹${approvedAmt.toLocaleString('en-IN', { minimumFractionDigits: 2 })}`,
|
||||
`Entered: ₹${amount.toLocaleString('en-IN', { minimumFractionDigits: 2 })}`,
|
||||
'Reset the amount to the approved value before proceeding.'
|
||||
]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
const isWithdrawal = !isDeposit;
|
||||
|
||||
// ── Validation ─────────────────────────────────────────────────────────
|
||||
@@ -2129,10 +2275,17 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
private openConfirmDialog(row: TxnRow, amount: number, isDeposit: boolean, selectedPt: any): void {
|
||||
// ── Confirmation dialog ────────────────────────────────────────────────
|
||||
const balanceAfterTxn = isDeposit ? this.currentBalance + amount : this.currentBalance - amount;
|
||||
// Token-based cashier disbursement: balance was already deducted at manager approval
|
||||
const isDisbursement = !isDeposit && !this.isClerk && !!this.activeWithdrawalRequest;
|
||||
const balanceAfterTxn = isDisbursement
|
||||
? this.currentBalance // no further deduction — just handing cash
|
||||
: isDeposit
|
||||
? this.currentBalance + amount
|
||||
: this.currentBalance - amount;
|
||||
|
||||
this.confirmData = {
|
||||
type: isDeposit ? 'DEPOSIT' : 'WITHDRAWAL',
|
||||
type: isDisbursement ? 'DISBURSEMENT' : isDeposit ? 'DEPOSIT' : 'WITHDRAWAL',
|
||||
isDisbursement,
|
||||
accountNo: this.accountDetails.glCode ?? this.accountDetails.accountNo,
|
||||
clientName: this.clientName,
|
||||
amount,
|
||||
@@ -2140,7 +2293,8 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
balanceAfter: balanceAfterTxn,
|
||||
paymentType: selectedPt?.name ?? 'Cash',
|
||||
chequeNo: row.chequeNo || null,
|
||||
symbol: this.currencySymbol
|
||||
symbol: this.currencySymbol,
|
||||
tokNo: this.activeWithdrawalRequest?.tok_no ?? null
|
||||
};
|
||||
this.showConfirmDialog = true;
|
||||
}
|
||||
@@ -2160,6 +2314,57 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
return;
|
||||
}
|
||||
|
||||
// Clerk mode + withdrawal → route through maker-checker (create PENDING request, no direct deduction)
|
||||
if (this.clerkMode && !isDeposit) {
|
||||
this.submitClerkWithdrawalRequest(row, amount);
|
||||
return;
|
||||
}
|
||||
|
||||
// Hard block: cashier direct withdrawal never reaches here — caught in onSave()
|
||||
// Extra safety net in case confirmSubmit() is ever called directly
|
||||
if (!this.isClerk && !this.reviewMode && !isDeposit && !this.activeWithdrawalRequest) {
|
||||
this.showAlert('error', 'Direct Withdrawal Not Permitted', [
|
||||
'Withdrawals must go through the maker-checker approval process.',
|
||||
'Enter the approved token number to disburse.'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Token-based approved withdrawal → balance already deducted at manager approval
|
||||
// Do NOT post another savings transaction — just record disbursement
|
||||
if (!this.isClerk && this.activeWithdrawalRequest && !isDeposit) {
|
||||
this.saving = true;
|
||||
const denomJson = JSON.stringify(this.denominationCounts);
|
||||
const req = this.activeWithdrawalRequest;
|
||||
this.clerkWithdrawalService
|
||||
.disburse(req.id, denomJson, amount)
|
||||
.pipe(
|
||||
catchError((err: any) => {
|
||||
const msg =
|
||||
err?.error?.message ??
|
||||
err?.error?.errors?.[0]?.defaultUserMessage ??
|
||||
'Disbursement recording failed. Please try again.';
|
||||
this.showAlert('error', 'Disbursement Failed', [msg]);
|
||||
this.saving = false;
|
||||
return of(null);
|
||||
})
|
||||
)
|
||||
.subscribe((res: any) => {
|
||||
this.saving = false;
|
||||
if (res) {
|
||||
this.showAlert('success', 'Cash Disbursed Successfully', [
|
||||
`₹${amount.toLocaleString('en-IN', { minimumFractionDigits: 2 })} disbursed to customer.`,
|
||||
`Token No: ${req.tok_no} · Client: ${req.client_name}`
|
||||
]);
|
||||
this.activeWithdrawalRequest = null;
|
||||
this.clearDenominations();
|
||||
this.loadOpeningPosition();
|
||||
this.loadAccountDetails(row.acNo.trim(), row.acTyp);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const today = new Date();
|
||||
const txnDate = `${String(today.getDate()).padStart(2, '0')}/${String(today.getMonth() + 1).padStart(2, '0')}/${today.getFullYear()}`;
|
||||
|
||||
@@ -2224,6 +2429,235 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
// ── Manager review mode ──────────────────────────────────────────────────
|
||||
|
||||
/** Load a withdrawal request and pre-fill the workspace (manager review or clerk edit). */
|
||||
private loadRequestIntoForm(id: number, mode: 'review' | 'edit'): void {
|
||||
this.clerkWithdrawalService
|
||||
.getById(id)
|
||||
.pipe(catchError(() => of(null)))
|
||||
.subscribe((req: any) => {
|
||||
if (!req) {
|
||||
this.showAlert('error', 'Request Not Found', ['This withdrawal request could not be loaded.']);
|
||||
return;
|
||||
}
|
||||
if (mode === 'review') {
|
||||
this.reviewRequest = req;
|
||||
} else {
|
||||
this.editRequest = req;
|
||||
}
|
||||
|
||||
const acTyp = 'SD';
|
||||
const row = this.rows[0];
|
||||
row.acNo = req.account_no ?? '';
|
||||
row.acTyp = acTyp;
|
||||
row.paymentTypeId = req.payment_type_id ?? null;
|
||||
row.amount = Number(req.amount) || 0;
|
||||
row.description = req.description ?? '';
|
||||
row.chequeNo = req.cheque_no ?? '';
|
||||
row.bankName = req.bank_name ?? '';
|
||||
row.tokNo = req.tok_no ?? '';
|
||||
row.dbCr = 'Dr';
|
||||
this.selectedRow = row;
|
||||
|
||||
try {
|
||||
this.denominationCounts = req.denomination_data ? JSON.parse(req.denomination_data) : {};
|
||||
} catch {
|
||||
this.denominationCounts = {};
|
||||
}
|
||||
|
||||
if (req.account_no) {
|
||||
this.loadAccountDetails(req.account_no, acTyp);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Manager approves → backend posts the real savings withdrawal and deducts the balance. */
|
||||
approveReview(): void {
|
||||
if (!this.reviewRequest || this.saving) return;
|
||||
this.saving = true;
|
||||
this.clerkWithdrawalService
|
||||
.approve(this.reviewRequest.id)
|
||||
.pipe(
|
||||
catchError((err: any) => {
|
||||
const msg =
|
||||
err?.error?.errors?.[0]?.defaultUserMessage ??
|
||||
err?.error?.developerMessage ??
|
||||
err?.error?.defaultUserMessage ??
|
||||
'Approval failed. Please try again.';
|
||||
this.showAlert('error', 'Approval Failed', [msg]);
|
||||
this.saving = false;
|
||||
return of(null);
|
||||
})
|
||||
)
|
||||
.subscribe((res: any) => {
|
||||
this.saving = false;
|
||||
if (res) {
|
||||
this.showAlert(
|
||||
'success',
|
||||
'Withdrawal Approved',
|
||||
[
|
||||
'The withdrawal has been approved and the balance has been deducted.',
|
||||
res.txnId ? `Transaction Ref: ${res.txnId}` : ''
|
||||
].filter(Boolean)
|
||||
);
|
||||
setTimeout(() => this.router.navigate(['/checker-inbox-and-tasks/withdrawal-requests']), 1200);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
openRejectReview(): void {
|
||||
this.rejectionReason = '';
|
||||
this.showRejectDialog = true;
|
||||
}
|
||||
|
||||
cancelRejectReview(): void {
|
||||
this.showRejectDialog = false;
|
||||
this.rejectionReason = '';
|
||||
}
|
||||
|
||||
confirmRejectReview(): void {
|
||||
if (!this.reviewRequest || this.saving) return;
|
||||
if (!this.rejectionReason.trim()) {
|
||||
this.showAlert('error', 'Reason Required', ['Please enter a reason for rejecting this request.']);
|
||||
return;
|
||||
}
|
||||
this.saving = true;
|
||||
this.clerkWithdrawalService
|
||||
.reject(this.reviewRequest.id, this.rejectionReason.trim())
|
||||
.pipe(
|
||||
catchError((err: any) => {
|
||||
const msg =
|
||||
err?.error?.errors?.[0]?.defaultUserMessage ??
|
||||
err?.error?.developerMessage ??
|
||||
err?.error?.defaultUserMessage ??
|
||||
'Rejection failed. Please try again.';
|
||||
this.showAlert('error', 'Rejection Failed', [msg]);
|
||||
this.saving = false;
|
||||
return of(null);
|
||||
})
|
||||
)
|
||||
.subscribe((res: any) => {
|
||||
this.saving = false;
|
||||
this.showRejectDialog = false;
|
||||
if (res) {
|
||||
this.showAlert('success', 'Withdrawal Rejected', [
|
||||
'The request has been sent back to the clerk for correction.'
|
||||
]);
|
||||
setTimeout(() => this.router.navigate(['/checker-inbox-and-tasks/withdrawal-requests']), 1200);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
backToInbox(): void {
|
||||
this.router.navigate(['/checker-inbox-and-tasks/withdrawal-requests']);
|
||||
}
|
||||
|
||||
/** Build the withdrawal-request payload from the current row + loaded account. */
|
||||
private buildWithdrawalPayload(row: TxnRow, amount: number): any {
|
||||
const today = new Date();
|
||||
const txnDateIso = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`;
|
||||
const selectedPt = this.paymentTypes.find((p: any) => p.id === row.paymentTypeId);
|
||||
|
||||
return {
|
||||
accountNo: this.accountDetails.accountNo,
|
||||
accountType: 'SAVINGS',
|
||||
savingsAccountId: this.accountDetails.id,
|
||||
clientId: this.accountDetails.clientId ?? this.clientDetails?.id ?? null,
|
||||
clientName: this.clientDetails?.displayName ?? this.accountDetails?.clientName ?? null,
|
||||
amount,
|
||||
txnDate: txnDateIso,
|
||||
paymentTypeId: row.paymentTypeId || null,
|
||||
paymentTypeName: selectedPt?.name ?? 'Cash',
|
||||
description: row.description || null,
|
||||
chequeNo: row.chequeNo || null,
|
||||
bankName: row.bankName || null,
|
||||
tokNo: row.tokNo?.trim() || null,
|
||||
denominationData: JSON.stringify(this.denominationCounts)
|
||||
};
|
||||
}
|
||||
|
||||
/** Clerk edits a rejected request → resubmit (status back to PENDING) for manager approval. */
|
||||
resubmitEdit(): void {
|
||||
if (!this.editRequest || this.saving) return;
|
||||
const row = this.selectedRow;
|
||||
if (!row || !this.accountDetails) {
|
||||
this.showAlert('error', 'Account Required', ['Please load the account before resubmitting.']);
|
||||
return;
|
||||
}
|
||||
const amount = row.amount ?? 0;
|
||||
if (!amount || amount <= 0) {
|
||||
this.showAlert('error', 'Amount Required', ['Please enter a valid withdrawal amount.']);
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = this.buildWithdrawalPayload(row, amount);
|
||||
this.saving = true;
|
||||
this.clerkWithdrawalService
|
||||
.update(this.editRequest.id, payload)
|
||||
.pipe(
|
||||
catchError((err: any) => {
|
||||
const msg =
|
||||
err?.error?.errors?.[0]?.defaultUserMessage ??
|
||||
err?.error?.developerMessage ??
|
||||
err?.error?.defaultUserMessage ??
|
||||
'Failed to resubmit request. Please try again.';
|
||||
this.showAlert('error', 'Resubmit Failed', [msg]);
|
||||
this.saving = false;
|
||||
return of(null);
|
||||
})
|
||||
)
|
||||
.subscribe((res: any) => {
|
||||
this.saving = false;
|
||||
if (res) {
|
||||
const tok = (this.editRequest as any)?.tok_no ?? row.tokNo?.trim() ?? '—';
|
||||
this.showAlert('success', 'Withdrawal Resubmitted', [
|
||||
`Withdrawal of ₹${amount.toLocaleString('en-IN', { minimumFractionDigits: 2 })} resubmitted for manager approval.`,
|
||||
`Token No: ${tok}`
|
||||
]);
|
||||
setTimeout(() => this.router.navigate(['/clerk/withdrawal-requests']), 1200);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
cancelEdit(): void {
|
||||
this.router.navigate(['/clerk/withdrawal-requests']);
|
||||
}
|
||||
|
||||
/** Clerk maker-checker: create a PENDING withdrawal request instead of deducting balance directly. */
|
||||
private submitClerkWithdrawalRequest(row: TxnRow, amount: number): void {
|
||||
const payload = this.buildWithdrawalPayload(row, amount);
|
||||
|
||||
this.saving = true;
|
||||
this.clerkWithdrawalService
|
||||
.submit(payload)
|
||||
.pipe(
|
||||
catchError((err: any) => {
|
||||
const msg =
|
||||
err?.error?.errors?.[0]?.defaultUserMessage ??
|
||||
err?.error?.developerMessage ??
|
||||
err?.error?.defaultUserMessage ??
|
||||
'Failed to submit withdrawal request. Please try again.';
|
||||
this.showAlert('error', 'Request Failed', [msg]);
|
||||
this.saving = false;
|
||||
return of(null);
|
||||
})
|
||||
)
|
||||
.subscribe((res: any) => {
|
||||
this.saving = false;
|
||||
if (res) {
|
||||
const ref = res.resourceId ?? res.requestId ?? res.id ?? 'N/A';
|
||||
const tok = row.tokNo?.trim() ?? '—';
|
||||
this.showAlert('success', 'Withdrawal Request Submitted', [
|
||||
`Withdrawal of ₹${amount.toLocaleString('en-IN', { minimumFractionDigits: 2 })} sent for manager approval.`,
|
||||
'Balance will be deducted only after approval.',
|
||||
`Token No: ${tok} · Request No: ${ref}`
|
||||
]);
|
||||
this.onCancel();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onCancel(): void {
|
||||
this.clerkUiState.clear();
|
||||
this.accountDetails = null;
|
||||
|
||||
Reference in New Issue
Block a user