diff --git a/src/app/cashier/clerk-withdrawal.service.ts b/src/app/cashier/clerk-withdrawal.service.ts index 27db75c4d..c137da4b4 100644 --- a/src/app/cashier/clerk-withdrawal.service.ts +++ b/src/app/cashier/clerk-withdrawal.service.ts @@ -7,8 +7,9 @@ */ import { Injectable, inject } from '@angular/core'; -import { HttpClient, HttpParams } from '@angular/common/http'; +import { HttpClient, HttpContext, HttpParams } from '@angular/common/http'; import { Observable } from 'rxjs'; +import { SILENT_HTTP_ERROR } from '../core/http/silent-error.token'; /** HTTP service for Clerk Withdrawal Request maker-checker workflow. */ @Injectable({ providedIn: 'root' }) @@ -50,8 +51,12 @@ export class ClerkWithdrawalService { return this.http.post(`/clerk/withdrawal-requests/${id}/reject`, { reason }); } + // Silenced: onTokNoBlur() probes this expecting a possible legitimate 404 (token not from this + // pool, could be a dividend claim's instead) and shows its own tailored message either way. getByToken(token: string): Observable { - return this.http.get(`/clerk/withdrawal-requests/by-token/${encodeURIComponent(token)}`); + return this.http.get(`/clerk/withdrawal-requests/by-token/${encodeURIComponent(token)}`, { + context: new HttpContext().set(SILENT_HTTP_ERROR, true) + }); } disburse(id: number, denominationData: string | null, transactionAmount?: number): Observable { diff --git a/src/app/cashier/transaction-entry/transaction-entry.component.html b/src/app/cashier/transaction-entry/transaction-entry.component.html index 4cd957977..12498d5b3 100644 --- a/src/app/cashier/transaction-entry/transaction-entry.component.html +++ b/src/app/cashier/transaction-entry/transaction-entry.component.html @@ -925,42 +925,50 @@
{{ - accountDetails?.subStatus?.value === 'Dormant' ? 'Dormant' : (accountDetails?.status?.value ?? '—') + activeDividendClaimRequest + ? 'Awaiting Disbursement' + : accountDetails?.subStatus?.value === 'Dormant' + ? 'Dormant' + : (accountDetails?.status?.value ?? '—') }} A/c {{ accountDetails?.depositType?.value || - (selectedRow?.acTyp === 'CA' - ? 'Current' - : selectedRow?.acTyp === 'SD' - ? 'Savings' - : selectedRow?.acTyp === 'FD' - ? 'Fixed Dep.' - : selectedRow?.acTyp === 'RD' - ? 'Recurring' - : selectedRow?.acTyp === 'GL' - ? (accountDetails?.type?.value ?? 'GL Account') - : selectedRow?.acTyp === 'SHR' - ? 'Share' - : selectedRow?.acTyp === 'LA' - ? 'Loan' - : '—') + (activeDividendClaimRequest + ? 'Dividend Claim' + : selectedRow?.acTyp === 'CA' + ? 'Current' + : selectedRow?.acTyp === 'SD' + ? 'Savings' + : selectedRow?.acTyp === 'FD' + ? 'Fixed Dep.' + : selectedRow?.acTyp === 'RD' + ? 'Recurring' + : selectedRow?.acTyp === 'GL' + ? (accountDetails?.type?.value ?? 'GL Account') + : selectedRow?.acTyp === 'SHR' + ? 'Share' + : selectedRow?.acTyp === 'LA' + ? 'Loan' + : '—') }} Branch {{ officeName ?? '—' }} Product {{ - accountDetails?.savingsProductName || - accountDetails?.depositProductName || - accountDetails?.productName || - '—' + activeDividendClaimRequest + ? 'Dividend Cash Payout' + : accountDetails?.savingsProductName || + accountDetails?.depositProductName || + accountDetails?.productName || + '—' }} { + // Not a withdrawal-request token at all (as opposed to PENDING/WITHDRAWN/REJECTED, + // which are legitimate matches with their own specific error) — try the dividend + // cash-claim token pool before giving up, since both share the same TOK. NO. field. + if (err?.status === 404) { + return this.dividendClaimRequestService.getByToken(token).pipe( + catchError((divErr: any) => { + this.loadingWithdrawal = false; + const msg: string = + divErr?.error?.message ?? + divErr?.error?.errors?.[0]?.defaultUserMessage ?? + `Token '${token}' could not be verified. Please try again.`; + this.showAlert('error', `Token '${token}'`, [msg]); + return of(null); + }), + map((divReq: any) => (divReq ? { __dividendClaim: true, ...divReq } : null)) + ); + } this.loadingWithdrawal = false; const msg: string = err?.error?.message ?? @@ -1747,6 +1768,24 @@ export class TransactionEntryComponent implements OnInit, OnDestroy { .subscribe((req: any) => { this.loadingWithdrawal = false; if (!req) return; + + if (req.__dividendClaim) { + this.activeDividendClaimRequest = req; + // No real savings/current account backs a dividend cash claim (it's a pure GL + // liability settled at claim-approval time) — stub just enough of accountDetails + // and kycDetails for onSave()'s generic guards to pass without stale/unrelated data. + this.accountDetails = { id: null, accountNo: 'DIVIDEND-CLAIM-' + req.id, clientName: req.client_name }; + this.kycDetails = null; + this.clientDetails = null; + this.accountError = ''; + row.acNo = req.client_name ?? 'Dividend Claim #' + req.id; + row.acTyp = 'SD'; + row.amount = Number(req.amount) || null; + row.dbCr = 'Dr'; + this.selectedRow = row; + return; + } + this.activeWithdrawalRequest = req; row.acNo = req.account_no ?? ''; row.acTyp = 'SD'; @@ -3012,7 +3051,13 @@ export class TransactionEntryComponent implements OnInit, OnDestroy { } // Cashier direct withdrawal BLOCKED — must use approved maker-checker token - if (!this.isClerk && !this.reviewMode && !isDeposit && !this.activeWithdrawalRequest) { + if ( + !this.isClerk && + !this.reviewMode && + !isDeposit && + !this.activeWithdrawalRequest && + !this.activeDividendClaimRequest + ) { 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.', @@ -3021,9 +3066,15 @@ export class TransactionEntryComponent implements OnInit, OnDestroy { return; } - // Approved withdrawal: amount/denomination must exactly match - if (!this.isClerk && this.activeWithdrawalRequest && !isDeposit) { - const approvedAmt = Number(this.activeWithdrawalRequest.amount); + // Approved withdrawal / dividend cash claim: amount/denomination must exactly match + if (!this.isClerk && (this.activeWithdrawalRequest || this.activeDividendClaimRequest) && !isDeposit) { + const rawApprovedAmt = Number(this.activeWithdrawalRequest?.amount ?? this.activeDividendClaimRequest?.amount); + // A dividend cash claim's liability is stored with paise precision (a proportional profit + // share), but cash can only be handed over in whole-rupee notes/coins -- compare against + // the floored amount so an exact denomination match is actually achievable. Ordinary + // withdrawal-request amounts are already whole rupees by convention, so this only changes + // behavior for dividend claims. + const approvedAmt = this.activeDividendClaimRequest ? Math.floor(rawApprovedAmt) : rawApprovedAmt; if (Math.abs(amount - approvedAmt) > 0.005) { if (isCash) { this.showAlert('error', 'Denomination Mismatch — Approved Withdrawal', [ @@ -3116,6 +3167,30 @@ export class TransactionEntryComponent implements OnInit, OnDestroy { const isCash = !!selectedPtEarly?.isCashPayment; const isGlType = row.acTyp === 'GL'; + // Dividend cash claim token disbursement: the GL entry (Dr Dividend Payable / Cr Cash at + // Main Vault) already posted at claim-approval time — there is no real savings/current + // account behind this row, so skip balance/min-balance/KYC/dormant/facility checks entirely + // (none apply to a GL-only liability payout) and go straight to the confirm dialog. + if (this.activeDividendClaimRequest) { + if (!row.paymentTypeId) { + this.showAlert('error', 'Payment Type Required', ['Please select Payment Type (Cash / Cheque / NEFT etc.)']); + return; + } + if (isWithdrawal && this.denominationTotal > 0 && this.hasWithdrawalDenomOverdraft) { + const lines = this.denominationOverdrafts.map( + (o) => `${o.label}: Need ${o.requested}, Available ${o.available} (Short by ${o.shortfall})` + ); + this.showAlert('error', 'Insufficient Denomination Stock', [ + 'Your cash drawer does not have enough notes/coins:', + ...lines, + 'Adjust denomination or request vault replenishment.' + ]); + return; + } + this.openConfirmDialog(row, amount, isDeposit, selectedPtEarly); + return; + } + // GL transactions post as journal entries — skip account-level facility/balance checks if (isGlType) { if (!this.cashVaultGlId) { @@ -3261,7 +3336,8 @@ export class TransactionEntryComponent implements OnInit, OnDestroy { private openConfirmDialog(row: TxnRow, amount: number, isDeposit: boolean, selectedPt: any): void { // Token-based cashier disbursement: balance was already deducted at manager approval - const isDisbursement = !isDeposit && !this.isClerk && !!this.activeWithdrawalRequest; + const isDisbursement = + !isDeposit && !this.isClerk && (!!this.activeWithdrawalRequest || !!this.activeDividendClaimRequest); // Cashier deposit (savings/current/GL) goes via maker-checker: balance credits only after manager approval const isDepositRequest = isDeposit && @@ -3292,7 +3368,7 @@ export class TransactionEntryComponent implements OnInit, OnDestroy { paymentType: selectedPt?.name ?? 'Cash', chequeNo: row.chequeNo || null, symbol: this.currencySymbol, - tokNo: this.activeWithdrawalRequest?.tok_no ?? null, + tokNo: this.activeWithdrawalRequest?.tok_no ?? this.activeDividendClaimRequest?.tok_no ?? null, isDdCashGl: this.isDdCashGlRow(row), ddFaceValue: row.amount ?? 0, ddCharges: row.ddCharges ?? 0, @@ -3354,9 +3430,52 @@ export class TransactionEntryComponent implements OnInit, OnDestroy { return; } + // Dividend cash claim token disbursement → GL entry already posted at claim-approval time. + // Do NOT post any new transaction/journal entry — just record the physical hand-over via + // DividendClaimRequestService, mirroring the activeWithdrawalRequest branch just below. + if (!this.isClerk && this.activeDividendClaimRequest && !isDeposit) { + this.saving = true; + const selectedPtDisb = this.paymentTypes.find((p: any) => p.id === row.paymentTypeId); + const denomJson = + selectedPtDisb?.isCashPayment && this.denominationTotal > 0 ? JSON.stringify(this.denominationCounts) : null; + const req = this.activeDividendClaimRequest; + this.dividendClaimRequestService + .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.activeDividendClaimRequest = null; + this.clearDenominations(); + this.loadOpeningPosition(); + } + }); + 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) { + if ( + !this.isClerk && + !this.reviewMode && + !isDeposit && + !this.activeWithdrawalRequest && + !this.activeDividendClaimRequest + ) { this.showAlert('error', 'Direct Withdrawal Not Permitted', [ 'Withdrawals must go through the maker-checker approval process.', 'Enter the approved token number to disburse.' diff --git a/src/app/clerk/dividend-claim-request.service.ts b/src/app/clerk/dividend-claim-request.service.ts index 81c208966..2f87ead92 100644 --- a/src/app/clerk/dividend-claim-request.service.ts +++ b/src/app/clerk/dividend-claim-request.service.ts @@ -7,8 +7,9 @@ */ import { Injectable, inject } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; +import { HttpClient, HttpContext } from '@angular/common/http'; import { Observable } from 'rxjs'; +import { SILENT_HTTP_ERROR } from '../core/http/silent-error.token'; /** * HTTP service for the maker-checker "claim my dividend early" workflow — a clerk submits a claim @@ -27,6 +28,7 @@ export class DividendClaimRequestService { savingsAccountId?: number; savingsApplyAmount?: number; cashApplyAmount?: number; + tokNo?: string; } ): Observable { const body: any = { lockId }; @@ -45,9 +47,27 @@ export class DividendClaimRequestService { if (breakdown.cashApplyAmount != null) { body.cashApplyAmount = String(breakdown.cashApplyAmount); } + if (breakdown.tokNo != null) { + body.tokNo = breakdown.tokNo; + } return this.http.post('/clerk/dividend-claim-requests', body); } + // Silenced: onTokNoBlur() in Transaction Entry probes this only after the withdrawal-request + // token pool 404s, and shows its own tailored message either way. + getByToken(token: string): Observable { + return this.http.get(`/clerk/dividend-claim-requests/by-token/${encodeURIComponent(token)}`, { + context: new HttpContext().set(SILENT_HTTP_ERROR, true) + }); + } + + disburse(id: number, denominationData: string | null, transactionAmount?: number): Observable { + return this.http.post(`/clerk/dividend-claim-requests/${id}/disburse`, { + denominationData, + transactionAmount: transactionAmount != null ? String(transactionAmount) : null + }); + } + getOverdueLoans(clientId: number): Observable { return this.http.get(`/clerk/dividend-claim-requests/overdue-loans/${clientId}`); } diff --git a/src/app/clerk/dividend-dashboard/dividend-dashboard.component.html b/src/app/clerk/dividend-dashboard/dividend-dashboard.component.html index 449483871..9871b11eb 100644 --- a/src/app/clerk/dividend-dashboard/dividend-dashboard.component.html +++ b/src/app/clerk/dividend-dashboard/dividend-dashboard.component.html @@ -283,6 +283,12 @@ Cash + @if (claimCashAmount[l.lockId] > 0) { + + }
Total: {{ claimTotal(l) | number: '1.2-2' }} / {{ l.amount | number: '1.2-2' }}
@@ -353,7 +359,12 @@ }} {{ req.savings_apply_amount > 0 ? (req.savings_apply_amount | number: '1.2-2') : '—' }} - {{ req.cash_apply_amount > 0 ? (req.cash_apply_amount | number: '1.2-2') : '—' }} + + {{ req.cash_apply_amount > 0 ? (req.cash_apply_amount | number: '1.2-2') : '—' }} + @if (req.cash_apply_amount > 0 && req.tok_no) { +
Tok: {{ req.tok_no }}
+ } + {{ req.locked_amount | number: '1.2-2' }} @if (req.has_overdue_loan) { diff --git a/src/app/clerk/dividend-dashboard/dividend-dashboard.component.ts b/src/app/clerk/dividend-dashboard/dividend-dashboard.component.ts index df1540926..5ddb98147 100644 --- a/src/app/clerk/dividend-dashboard/dividend-dashboard.component.ts +++ b/src/app/clerk/dividend-dashboard/dividend-dashboard.component.ts @@ -68,6 +68,7 @@ export class DividendDashboardComponent implements OnInit { claimSavingsAmount: Record = {}; claimSavingsAccountOverride: Record = {}; claimCashAmount: Record = {}; + claimTokNo: Record = {}; claimSubmitting = false; claimError: string | null = null; @@ -99,6 +100,7 @@ export class DividendDashboardComponent implements OnInit { this.claimSavingsAmount[lock.lockId] = Number(lock.amount); this.claimSavingsAccountOverride[lock.lockId] = null; this.claimCashAmount[lock.lockId] = null; + this.claimTokNo[lock.lockId] = ''; this.claimRequestService .getOverdueLoans(lock.clientId) .pipe(catchError(() => of([]))) @@ -117,14 +119,17 @@ export class DividendDashboardComponent implements OnInit { return ( Number(this.claimLoanAmount[lock.lockId] || 0) + Number(this.claimSavingsAmount[lock.lockId] || 0) + - Number(this.claimCashAmount[lock.lockId] || 0) + Math.floor(Number(this.claimCashAmount[lock.lockId] || 0)) ); } submitClaim(lock: any): void { const loanAmount = Number(this.claimLoanAmount[lock.lockId] || 0); const savingsAmount = Number(this.claimSavingsAmount[lock.lockId] || 0); - const cashAmount = Number(this.claimCashAmount[lock.lockId] || 0); + // Cash can only be handed over in whole-rupee notes/coins (no denomination below ₹1), while a + // dividend share often works out to paise — floor to the nearest rupee so the denomination + // total the cashier counts can always match exactly; any leftover paise stays unclaimed. + const cashAmount = Math.floor(Number(this.claimCashAmount[lock.lockId] || 0)); const total = loanAmount + savingsAmount + cashAmount; if (total <= 0) { this.claimError = 'Enter at least one amount (loan, savings, or cash), greater than zero.'; @@ -142,6 +147,11 @@ export class DividendDashboardComponent implements OnInit { this.claimError = 'This share account has no linked savings account — enter a Savings Account ID to credit.'; return; } + const tokNo = this.claimTokNo[lock.lockId]?.trim(); + if (cashAmount > 0 && !tokNo) { + this.claimError = 'Enter a Token No. for the cash portion — the cashier will use it to hand over the cash.'; + return; + } this.claimSubmitting = true; this.claimError = null; this.claimRequestService @@ -151,7 +161,8 @@ export class DividendDashboardComponent implements OnInit { savingsAccountId: savingsAmount > 0 && !lock.savingsAccountId ? this.claimSavingsAccountOverride[lock.lockId] : undefined, savingsApplyAmount: savingsAmount > 0 ? savingsAmount : undefined, - cashApplyAmount: cashAmount > 0 ? cashAmount : undefined + cashApplyAmount: cashAmount > 0 ? cashAmount : undefined, + tokNo: cashAmount > 0 ? tokNo : undefined }) .subscribe({ next: () => { diff --git a/src/environments/.env.ts b/src/environments/.env.ts index 408f0a803..a08b20b4a 100644 --- a/src/environments/.env.ts +++ b/src/environments/.env.ts @@ -10,7 +10,7 @@ export default { 'mifos_x': { 'version': '260811', - 'hash': '7b895a0e' + 'hash': 'f379b241' }, 'allow_switching_backend_instance': true };