feat(shares): route dividend cash claims through cashier token disbursement
Cash claims now require a token at claim submission; a cashier redeems it at Transaction Entry to record physical hand-over, mirroring the existing clerk-withdrawal-request flow -- no double-disbursement, and the manager never hands over cash directly at approval time. Also: dividend-claim tokens fall back through the same TOK. NO. lookup used for withdrawal requests, silenced against the global error interceptor since the caller already handles every outcome; the account summary card now shows "Dividend Claim" instead of a misleading "A/c Savings" label when resolving one; and cash amounts are floored to whole rupees client-side to match cash's whole-rupee denominations. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -7,8 +7,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Injectable, inject } from '@angular/core';
|
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 { Observable } from 'rxjs';
|
||||||
|
import { SILENT_HTTP_ERROR } from '../core/http/silent-error.token';
|
||||||
|
|
||||||
/** HTTP service for Clerk Withdrawal Request maker-checker workflow. */
|
/** HTTP service for Clerk Withdrawal Request maker-checker workflow. */
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
@@ -50,8 +51,12 @@ export class ClerkWithdrawalService {
|
|||||||
return this.http.post(`/clerk/withdrawal-requests/${id}/reject`, { reason });
|
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<any> {
|
getByToken(token: string): Observable<any> {
|
||||||
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<any> {
|
disburse(id: number, denominationData: string | null, transactionAmount?: number): Observable<any> {
|
||||||
|
|||||||
@@ -925,42 +925,50 @@
|
|||||||
<div class="te-acs-meta">
|
<div class="te-acs-meta">
|
||||||
<span
|
<span
|
||||||
class="te-acs-tag"
|
class="te-acs-tag"
|
||||||
[class.te-acs-tag--active]="accountDetails?.status?.value === 'Active'"
|
[class.te-acs-tag--active]="activeDividendClaimRequest || accountDetails?.status?.value === 'Active'"
|
||||||
[class.te-acs-tag--blocked]="accountDetails?.status?.value === 'Blocked'"
|
[class.te-acs-tag--blocked]="accountDetails?.status?.value === 'Blocked'"
|
||||||
[class.te-acs-tag--dormant]="accountDetails?.subStatus?.value === 'Dormant'"
|
[class.te-acs-tag--dormant]="accountDetails?.subStatus?.value === 'Dormant'"
|
||||||
>{{
|
>{{
|
||||||
accountDetails?.subStatus?.value === 'Dormant' ? 'Dormant' : (accountDetails?.status?.value ?? '—')
|
activeDividendClaimRequest
|
||||||
|
? 'Awaiting Disbursement'
|
||||||
|
: accountDetails?.subStatus?.value === 'Dormant'
|
||||||
|
? 'Dormant'
|
||||||
|
: (accountDetails?.status?.value ?? '—')
|
||||||
}}</span
|
}}</span
|
||||||
>
|
>
|
||||||
<span class="te-acs-kv"
|
<span class="te-acs-kv"
|
||||||
><b>A/c</b>
|
><b>A/c</b>
|
||||||
{{
|
{{
|
||||||
accountDetails?.depositType?.value ||
|
accountDetails?.depositType?.value ||
|
||||||
(selectedRow?.acTyp === 'CA'
|
(activeDividendClaimRequest
|
||||||
? 'Current'
|
? 'Dividend Claim'
|
||||||
: selectedRow?.acTyp === 'SD'
|
: selectedRow?.acTyp === 'CA'
|
||||||
? 'Savings'
|
? 'Current'
|
||||||
: selectedRow?.acTyp === 'FD'
|
: selectedRow?.acTyp === 'SD'
|
||||||
? 'Fixed Dep.'
|
? 'Savings'
|
||||||
: selectedRow?.acTyp === 'RD'
|
: selectedRow?.acTyp === 'FD'
|
||||||
? 'Recurring'
|
? 'Fixed Dep.'
|
||||||
: selectedRow?.acTyp === 'GL'
|
: selectedRow?.acTyp === 'RD'
|
||||||
? (accountDetails?.type?.value ?? 'GL Account')
|
? 'Recurring'
|
||||||
: selectedRow?.acTyp === 'SHR'
|
: selectedRow?.acTyp === 'GL'
|
||||||
? 'Share'
|
? (accountDetails?.type?.value ?? 'GL Account')
|
||||||
: selectedRow?.acTyp === 'LA'
|
: selectedRow?.acTyp === 'SHR'
|
||||||
? 'Loan'
|
? 'Share'
|
||||||
: '—')
|
: selectedRow?.acTyp === 'LA'
|
||||||
|
? 'Loan'
|
||||||
|
: '—')
|
||||||
}}</span
|
}}</span
|
||||||
>
|
>
|
||||||
<span class="te-acs-kv"><b>Branch</b> {{ officeName ?? '—' }}</span>
|
<span class="te-acs-kv"><b>Branch</b> {{ officeName ?? '—' }}</span>
|
||||||
<span class="te-acs-kv"
|
<span class="te-acs-kv"
|
||||||
><b>Product</b>
|
><b>Product</b>
|
||||||
{{
|
{{
|
||||||
accountDetails?.savingsProductName ||
|
activeDividendClaimRequest
|
||||||
accountDetails?.depositProductName ||
|
? 'Dividend Cash Payout'
|
||||||
accountDetails?.productName ||
|
: accountDetails?.savingsProductName ||
|
||||||
'—'
|
accountDetails?.depositProductName ||
|
||||||
|
accountDetails?.productName ||
|
||||||
|
'—'
|
||||||
}}</span
|
}}</span
|
||||||
>
|
>
|
||||||
<span class="te-acs-kv"
|
<span class="te-acs-kv"
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { CommonModule } from '@angular/common';
|
|||||||
import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
|
import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||||
import { catchError, debounceTime, distinctUntilChanged } from 'rxjs/operators';
|
import { catchError, debounceTime, distinctUntilChanged, map } from 'rxjs/operators';
|
||||||
import { forkJoin, of, Subject } from 'rxjs';
|
import { forkJoin, of, Subject } from 'rxjs';
|
||||||
import { MatDialog, MatDialogModule, MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
import { MatDialog, MatDialogModule, MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||||
import { MatIconModule } from '@angular/material/icon';
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
@@ -20,6 +20,7 @@ import { CashierService } from '../cashier.service';
|
|||||||
import { AuthenticationService } from '../../core/authentication/authentication.service';
|
import { AuthenticationService } from '../../core/authentication/authentication.service';
|
||||||
import { ClerkUiStateService, ClerkSavedAccount } from '../clerk-ui-state.service';
|
import { ClerkUiStateService, ClerkSavedAccount } from '../clerk-ui-state.service';
|
||||||
import { ClerkWithdrawalService } from '../clerk-withdrawal.service';
|
import { ClerkWithdrawalService } from '../clerk-withdrawal.service';
|
||||||
|
import { DividendClaimRequestService } from '../../clerk/dividend-claim-request.service';
|
||||||
import { ClerkDisbursementService } from '../clerk-disbursement.service';
|
import { ClerkDisbursementService } from '../clerk-disbursement.service';
|
||||||
import { ClerkShareWithdrawalService } from '../clerk-share-withdrawal.service';
|
import { ClerkShareWithdrawalService } from '../clerk-share-withdrawal.service';
|
||||||
import { CashierDepositService } from '../cashier-deposit.service';
|
import { CashierDepositService } from '../cashier-deposit.service';
|
||||||
@@ -749,6 +750,7 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
// Active approved withdrawal (cashier disburse flow)
|
// Active approved withdrawal (cashier disburse flow)
|
||||||
activeWithdrawalRequest: any = null;
|
activeWithdrawalRequest: any = null;
|
||||||
|
activeDividendClaimRequest: any = null;
|
||||||
loadingWithdrawal = false;
|
loadingWithdrawal = false;
|
||||||
|
|
||||||
private http = inject(HttpClient);
|
private http = inject(HttpClient);
|
||||||
@@ -757,6 +759,7 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
|||||||
private dialog = inject(MatDialog);
|
private dialog = inject(MatDialog);
|
||||||
private clerkUiState = inject(ClerkUiStateService);
|
private clerkUiState = inject(ClerkUiStateService);
|
||||||
private clerkWithdrawalService = inject(ClerkWithdrawalService);
|
private clerkWithdrawalService = inject(ClerkWithdrawalService);
|
||||||
|
private dividendClaimRequestService = inject(DividendClaimRequestService);
|
||||||
private clerkDisbursementService = inject(ClerkDisbursementService);
|
private clerkDisbursementService = inject(ClerkDisbursementService);
|
||||||
private clerkShareWithdrawalService = inject(ClerkShareWithdrawalService);
|
private clerkShareWithdrawalService = inject(ClerkShareWithdrawalService);
|
||||||
private cashierDepositService = inject(CashierDepositService);
|
private cashierDepositService = inject(CashierDepositService);
|
||||||
@@ -1731,10 +1734,28 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
|||||||
const token = row.tokNo.trim();
|
const token = row.tokNo.trim();
|
||||||
this.loadingWithdrawal = true;
|
this.loadingWithdrawal = true;
|
||||||
this.activeWithdrawalRequest = null;
|
this.activeWithdrawalRequest = null;
|
||||||
|
this.activeDividendClaimRequest = null;
|
||||||
this.clerkWithdrawalService
|
this.clerkWithdrawalService
|
||||||
.getByToken(token)
|
.getByToken(token)
|
||||||
.pipe(
|
.pipe(
|
||||||
catchError((err: any) => {
|
catchError((err: any) => {
|
||||||
|
// 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;
|
this.loadingWithdrawal = false;
|
||||||
const msg: string =
|
const msg: string =
|
||||||
err?.error?.message ??
|
err?.error?.message ??
|
||||||
@@ -1747,6 +1768,24 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
|||||||
.subscribe((req: any) => {
|
.subscribe((req: any) => {
|
||||||
this.loadingWithdrawal = false;
|
this.loadingWithdrawal = false;
|
||||||
if (!req) return;
|
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;
|
this.activeWithdrawalRequest = req;
|
||||||
row.acNo = req.account_no ?? '';
|
row.acNo = req.account_no ?? '';
|
||||||
row.acTyp = 'SD';
|
row.acTyp = 'SD';
|
||||||
@@ -3012,7 +3051,13 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Cashier direct withdrawal BLOCKED — must use approved maker-checker token
|
// 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', [
|
this.showAlert('error', 'Direct Withdrawal Not Permitted', [
|
||||||
'Withdrawals cannot be processed directly by the cashier.',
|
'Withdrawals cannot be processed directly by the cashier.',
|
||||||
'All withdrawals must go through the Clerk → Manager approval process.',
|
'All withdrawals must go through the Clerk → Manager approval process.',
|
||||||
@@ -3021,9 +3066,15 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Approved withdrawal: amount/denomination must exactly match
|
// Approved withdrawal / dividend cash claim: amount/denomination must exactly match
|
||||||
if (!this.isClerk && this.activeWithdrawalRequest && !isDeposit) {
|
if (!this.isClerk && (this.activeWithdrawalRequest || this.activeDividendClaimRequest) && !isDeposit) {
|
||||||
const approvedAmt = Number(this.activeWithdrawalRequest.amount);
|
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 (Math.abs(amount - approvedAmt) > 0.005) {
|
||||||
if (isCash) {
|
if (isCash) {
|
||||||
this.showAlert('error', 'Denomination Mismatch — Approved Withdrawal', [
|
this.showAlert('error', 'Denomination Mismatch — Approved Withdrawal', [
|
||||||
@@ -3116,6 +3167,30 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
|||||||
const isCash = !!selectedPtEarly?.isCashPayment;
|
const isCash = !!selectedPtEarly?.isCashPayment;
|
||||||
const isGlType = row.acTyp === 'GL';
|
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
|
// GL transactions post as journal entries — skip account-level facility/balance checks
|
||||||
if (isGlType) {
|
if (isGlType) {
|
||||||
if (!this.cashVaultGlId) {
|
if (!this.cashVaultGlId) {
|
||||||
@@ -3261,7 +3336,8 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
private openConfirmDialog(row: TxnRow, amount: number, isDeposit: boolean, selectedPt: any): void {
|
private openConfirmDialog(row: TxnRow, amount: number, isDeposit: boolean, selectedPt: any): void {
|
||||||
// Token-based cashier disbursement: balance was already deducted at manager approval
|
// 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
|
// Cashier deposit (savings/current/GL) goes via maker-checker: balance credits only after manager approval
|
||||||
const isDepositRequest =
|
const isDepositRequest =
|
||||||
isDeposit &&
|
isDeposit &&
|
||||||
@@ -3292,7 +3368,7 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
|||||||
paymentType: selectedPt?.name ?? 'Cash',
|
paymentType: selectedPt?.name ?? 'Cash',
|
||||||
chequeNo: row.chequeNo || null,
|
chequeNo: row.chequeNo || null,
|
||||||
symbol: this.currencySymbol,
|
symbol: this.currencySymbol,
|
||||||
tokNo: this.activeWithdrawalRequest?.tok_no ?? null,
|
tokNo: this.activeWithdrawalRequest?.tok_no ?? this.activeDividendClaimRequest?.tok_no ?? null,
|
||||||
isDdCashGl: this.isDdCashGlRow(row),
|
isDdCashGl: this.isDdCashGlRow(row),
|
||||||
ddFaceValue: row.amount ?? 0,
|
ddFaceValue: row.amount ?? 0,
|
||||||
ddCharges: row.ddCharges ?? 0,
|
ddCharges: row.ddCharges ?? 0,
|
||||||
@@ -3354,9 +3430,52 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
|||||||
return;
|
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()
|
// Hard block: cashier direct withdrawal never reaches here — caught in onSave()
|
||||||
// Extra safety net in case confirmSubmit() is ever called directly
|
// 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', [
|
this.showAlert('error', 'Direct Withdrawal Not Permitted', [
|
||||||
'Withdrawals must go through the maker-checker approval process.',
|
'Withdrawals must go through the maker-checker approval process.',
|
||||||
'Enter the approved token number to disburse.'
|
'Enter the approved token number to disburse.'
|
||||||
|
|||||||
@@ -7,8 +7,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Injectable, inject } from '@angular/core';
|
import { Injectable, inject } from '@angular/core';
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient, HttpContext } from '@angular/common/http';
|
||||||
import { Observable } from 'rxjs';
|
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
|
* 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;
|
savingsAccountId?: number;
|
||||||
savingsApplyAmount?: number;
|
savingsApplyAmount?: number;
|
||||||
cashApplyAmount?: number;
|
cashApplyAmount?: number;
|
||||||
|
tokNo?: string;
|
||||||
}
|
}
|
||||||
): Observable<any> {
|
): Observable<any> {
|
||||||
const body: any = { lockId };
|
const body: any = { lockId };
|
||||||
@@ -45,9 +47,27 @@ export class DividendClaimRequestService {
|
|||||||
if (breakdown.cashApplyAmount != null) {
|
if (breakdown.cashApplyAmount != null) {
|
||||||
body.cashApplyAmount = String(breakdown.cashApplyAmount);
|
body.cashApplyAmount = String(breakdown.cashApplyAmount);
|
||||||
}
|
}
|
||||||
|
if (breakdown.tokNo != null) {
|
||||||
|
body.tokNo = breakdown.tokNo;
|
||||||
|
}
|
||||||
return this.http.post('/clerk/dividend-claim-requests', body);
|
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<any> {
|
||||||
|
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<any> {
|
||||||
|
return this.http.post(`/clerk/dividend-claim-requests/${id}/disburse`, {
|
||||||
|
denominationData,
|
||||||
|
transactionAmount: transactionAmount != null ? String(transactionAmount) : null
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
getOverdueLoans(clientId: number): Observable<any> {
|
getOverdueLoans(clientId: number): Observable<any> {
|
||||||
return this.http.get(`/clerk/dividend-claim-requests/overdue-loans/${clientId}`);
|
return this.http.get(`/clerk/dividend-claim-requests/overdue-loans/${clientId}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -283,6 +283,12 @@
|
|||||||
<span>Cash</span>
|
<span>Cash</span>
|
||||||
<input type="number" [(ngModel)]="claimCashAmount[l.lockId]" min="0" step="0.01" />
|
<input type="number" [(ngModel)]="claimCashAmount[l.lockId]" min="0" step="0.01" />
|
||||||
</label>
|
</label>
|
||||||
|
@if (claimCashAmount[l.lockId] > 0) {
|
||||||
|
<label class="inv-form-field">
|
||||||
|
<span>Token No. (for cashier hand-over)</span>
|
||||||
|
<input type="text" [(ngModel)]="claimTokNo[l.lockId]" />
|
||||||
|
</label>
|
||||||
|
}
|
||||||
<div class="dd-apply-panel-total">
|
<div class="dd-apply-panel-total">
|
||||||
Total: {{ claimTotal(l) | number: '1.2-2' }} / {{ l.amount | number: '1.2-2' }}
|
Total: {{ claimTotal(l) | number: '1.2-2' }} / {{ l.amount | number: '1.2-2' }}
|
||||||
</div>
|
</div>
|
||||||
@@ -353,7 +359,12 @@
|
|||||||
}}
|
}}
|
||||||
</td>
|
</td>
|
||||||
<td class="num">{{ req.savings_apply_amount > 0 ? (req.savings_apply_amount | number: '1.2-2') : '—' }}</td>
|
<td class="num">{{ req.savings_apply_amount > 0 ? (req.savings_apply_amount | number: '1.2-2') : '—' }}</td>
|
||||||
<td class="num">{{ req.cash_apply_amount > 0 ? (req.cash_apply_amount | number: '1.2-2') : '—' }}</td>
|
<td class="num">
|
||||||
|
{{ req.cash_apply_amount > 0 ? (req.cash_apply_amount | number: '1.2-2') : '—' }}
|
||||||
|
@if (req.cash_apply_amount > 0 && req.tok_no) {
|
||||||
|
<div class="pg-empty-inline">Tok: {{ req.tok_no }}</div>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
<td class="num">{{ req.locked_amount | number: '1.2-2' }}</td>
|
<td class="num">{{ req.locked_amount | number: '1.2-2' }}</td>
|
||||||
<td>
|
<td>
|
||||||
@if (req.has_overdue_loan) {
|
@if (req.has_overdue_loan) {
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ export class DividendDashboardComponent implements OnInit {
|
|||||||
claimSavingsAmount: Record<number, number | null> = {};
|
claimSavingsAmount: Record<number, number | null> = {};
|
||||||
claimSavingsAccountOverride: Record<number, number | null> = {};
|
claimSavingsAccountOverride: Record<number, number | null> = {};
|
||||||
claimCashAmount: Record<number, number | null> = {};
|
claimCashAmount: Record<number, number | null> = {};
|
||||||
|
claimTokNo: Record<number, string> = {};
|
||||||
claimSubmitting = false;
|
claimSubmitting = false;
|
||||||
claimError: string | null = null;
|
claimError: string | null = null;
|
||||||
|
|
||||||
@@ -99,6 +100,7 @@ export class DividendDashboardComponent implements OnInit {
|
|||||||
this.claimSavingsAmount[lock.lockId] = Number(lock.amount);
|
this.claimSavingsAmount[lock.lockId] = Number(lock.amount);
|
||||||
this.claimSavingsAccountOverride[lock.lockId] = null;
|
this.claimSavingsAccountOverride[lock.lockId] = null;
|
||||||
this.claimCashAmount[lock.lockId] = null;
|
this.claimCashAmount[lock.lockId] = null;
|
||||||
|
this.claimTokNo[lock.lockId] = '';
|
||||||
this.claimRequestService
|
this.claimRequestService
|
||||||
.getOverdueLoans(lock.clientId)
|
.getOverdueLoans(lock.clientId)
|
||||||
.pipe(catchError(() => of([])))
|
.pipe(catchError(() => of([])))
|
||||||
@@ -117,14 +119,17 @@ export class DividendDashboardComponent implements OnInit {
|
|||||||
return (
|
return (
|
||||||
Number(this.claimLoanAmount[lock.lockId] || 0) +
|
Number(this.claimLoanAmount[lock.lockId] || 0) +
|
||||||
Number(this.claimSavingsAmount[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 {
|
submitClaim(lock: any): void {
|
||||||
const loanAmount = Number(this.claimLoanAmount[lock.lockId] || 0);
|
const loanAmount = Number(this.claimLoanAmount[lock.lockId] || 0);
|
||||||
const savingsAmount = Number(this.claimSavingsAmount[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;
|
const total = loanAmount + savingsAmount + cashAmount;
|
||||||
if (total <= 0) {
|
if (total <= 0) {
|
||||||
this.claimError = 'Enter at least one amount (loan, savings, or cash), greater than zero.';
|
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.';
|
this.claimError = 'This share account has no linked savings account — enter a Savings Account ID to credit.';
|
||||||
return;
|
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.claimSubmitting = true;
|
||||||
this.claimError = null;
|
this.claimError = null;
|
||||||
this.claimRequestService
|
this.claimRequestService
|
||||||
@@ -151,7 +161,8 @@ export class DividendDashboardComponent implements OnInit {
|
|||||||
savingsAccountId:
|
savingsAccountId:
|
||||||
savingsAmount > 0 && !lock.savingsAccountId ? this.claimSavingsAccountOverride[lock.lockId] : undefined,
|
savingsAmount > 0 && !lock.savingsAccountId ? this.claimSavingsAccountOverride[lock.lockId] : undefined,
|
||||||
savingsApplyAmount: savingsAmount > 0 ? savingsAmount : undefined,
|
savingsApplyAmount: savingsAmount > 0 ? savingsAmount : undefined,
|
||||||
cashApplyAmount: cashAmount > 0 ? cashAmount : undefined
|
cashApplyAmount: cashAmount > 0 ? cashAmount : undefined,
|
||||||
|
tokNo: cashAmount > 0 ? tokNo : undefined
|
||||||
})
|
})
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
export default {
|
export default {
|
||||||
'mifos_x': {
|
'mifos_x': {
|
||||||
'version': '260811',
|
'version': '260811',
|
||||||
'hash': '7b895a0e'
|
'hash': 'f379b241'
|
||||||
},
|
},
|
||||||
'allow_switching_backend_instance': true
|
'allow_switching_backend_instance': true
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user