Add customer search dialog and sponsor DD/reconciliation modules
Adds sponsor-dd and sponsor-dd-reconciliation feature modules, a shared customer search dialog, and wires them into routing/shell/ organization screens alongside cashier and deposit-request updates.
This commit is contained in:
@@ -22,6 +22,11 @@ import { ClerkWithdrawalService } from '../clerk-withdrawal.service';
|
||||
import { ClerkDisbursementService } from '../clerk-disbursement.service';
|
||||
import { CashierDepositService } from '../cashier-deposit.service';
|
||||
import { SettingsService } from '../../settings/settings.service';
|
||||
import {
|
||||
CustomerSearchDialogComponent,
|
||||
CustomerSearchResult
|
||||
} from '../../shared/customer-search-dialog/customer-search-dialog.component';
|
||||
import { SponsorDDService } from '../../sponsor-dd/sponsor-dd.service';
|
||||
|
||||
interface TxnRow {
|
||||
typ: string;
|
||||
@@ -39,6 +44,8 @@ interface TxnRow {
|
||||
paymentTypeId: number | null;
|
||||
bankName: string;
|
||||
routingCode: string;
|
||||
/** DD issuance charges/commission — only used when TYPE=DD, TXN TP=Cash, AC.TP=GL */
|
||||
ddCharges: number | null;
|
||||
}
|
||||
|
||||
interface Denomination {
|
||||
@@ -659,6 +666,12 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
private cashierDepositService = inject(CashierDepositService);
|
||||
readonly router = inject(Router);
|
||||
private settingsService = inject(SettingsService);
|
||||
private sponsorDDService = inject(SponsorDDService);
|
||||
|
||||
/** GST % on DD issuance charges — loaded from the admin GST/GL Settings (sponsor-dd module). */
|
||||
ddGstPercentage = 18;
|
||||
/** Admin-configured DD limit (sponsor-dd module) — caps a single DD's face value. Null = no limit. */
|
||||
ddLimitAmount: number | null = null;
|
||||
|
||||
today = '';
|
||||
rows: TxnRow[] = [];
|
||||
@@ -804,6 +817,7 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
|
||||
this.loadCashVaultGl();
|
||||
this.loadDdGstPercentage();
|
||||
|
||||
if (this.reviewMode) {
|
||||
this.loadRequestIntoForm(this.reviewRequestId!, 'review');
|
||||
@@ -888,6 +902,18 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
/** Admin GST rate (set via GST / GL Settings in the sponsor-dd module) applied to DD issuance charges. */
|
||||
private loadDdGstPercentage(): void {
|
||||
this.sponsorDDService
|
||||
.getGlMappings()
|
||||
.pipe(catchError(() => of([])))
|
||||
.subscribe((mappings: any[]) => {
|
||||
const mapping = mappings && mappings.length ? mappings[0] : null;
|
||||
this.ddGstPercentage = Number(mapping?.gstPercentage ?? 18);
|
||||
this.ddLimitAmount = mapping?.ddLimitAmount != null ? Number(mapping.ddLimitAmount) : null;
|
||||
});
|
||||
}
|
||||
|
||||
private loadDayStatus(): void {
|
||||
this.cashierService
|
||||
.getCashierDayStatus()
|
||||
@@ -1054,7 +1080,9 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
applyDenominationTotal(): void {
|
||||
if (this.selectedRow) {
|
||||
// DD + Cash + GL: amount is derived from Face Value + Charges + GST, not from counted cash —
|
||||
// overwriting it here would feed back into the Charges/GST sum and compound on every click.
|
||||
if (this.selectedRow && !this.isDdCashGlRow(this.selectedRow)) {
|
||||
this.selectedRow.amount = this.denominationTotal;
|
||||
}
|
||||
}
|
||||
@@ -1082,7 +1110,8 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
tokNo: '',
|
||||
paymentTypeId: this.findTransferPaymentTypeId(),
|
||||
bankName: '',
|
||||
routingCode: ''
|
||||
routingCode: '',
|
||||
ddCharges: null
|
||||
};
|
||||
this.rows.push(mirror);
|
||||
this.selectedRow = mirror;
|
||||
@@ -1103,7 +1132,8 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
tokNo: '',
|
||||
paymentTypeId: null,
|
||||
bankName: '',
|
||||
routingCode: ''
|
||||
routingCode: '',
|
||||
ddCharges: null
|
||||
};
|
||||
this.rows.push(row);
|
||||
}
|
||||
@@ -1142,6 +1172,40 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
return pt?.id ?? null;
|
||||
}
|
||||
|
||||
/** TYPE=DD + TXN TP=Cash + AC.TP=GL — the only combination that collects DD charges + GST from the customer. */
|
||||
isDdCashGlRow(row: TxnRow): boolean {
|
||||
const pt = this.paymentTypes.find((p: any) => p.id === row.paymentTypeId);
|
||||
return row.typ === 'DD' && !!pt?.isCashPayment && row.acTyp === 'GL';
|
||||
}
|
||||
|
||||
/** GST on DD charges only (never on the DD face value), at the admin-configured rate. */
|
||||
ddGstAmountFor(row: TxnRow): number {
|
||||
const charges = Number(row.ddCharges) || 0;
|
||||
if (charges <= 0) return 0;
|
||||
return Math.round(charges * this.ddGstPercentage) / 100;
|
||||
}
|
||||
|
||||
/** Amount + Charges + GST, rounded UP to the next whole rupee — the total collected from the customer. */
|
||||
ddTotalPayableFor(row: TxnRow): number {
|
||||
const amount = Number(row.amount) || 0;
|
||||
const charges = Number(row.ddCharges) || 0;
|
||||
const gst = this.ddGstAmountFor(row);
|
||||
return Math.ceil(amount + charges + gst - 1e-9);
|
||||
}
|
||||
|
||||
get isSelectedRowDdCashGl(): boolean {
|
||||
return !!this.selectedRow && this.isDdCashGlRow(this.selectedRow);
|
||||
}
|
||||
|
||||
/** Cash the cashier must physically count for the selected DD row — Face Value + Charges + GST. */
|
||||
get ddTargetTotal(): number {
|
||||
return this.selectedRow ? this.ddTotalPayableFor(this.selectedRow) : 0;
|
||||
}
|
||||
|
||||
ddDenomMatchesTarget(): boolean {
|
||||
return !!this.selectedRow && Math.abs(this.denominationTotal - this.ddTargetTotal) < 0.005;
|
||||
}
|
||||
|
||||
/**
|
||||
* Submits a clerk double-entry request (maker-checker, PENDING until manager approves).
|
||||
* Disbursement: 1 DR [GL] source → N CR savings/current/GL destinations.
|
||||
@@ -1418,6 +1482,34 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly SEARCH_ACCOUNT_TYPE_TO_AC_TYP: Record<string, string> = {
|
||||
SAVINGS: 'SD',
|
||||
CURRENT: 'CA',
|
||||
FIXED_DEPOSIT: 'FD',
|
||||
RECURRING_DEPOSIT: 'RD'
|
||||
};
|
||||
|
||||
openCustomerSearch(): void {
|
||||
const dialogRef = this.dialog.open(CustomerSearchDialogComponent, {
|
||||
width: '960px',
|
||||
maxWidth: '95vw'
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe((result: CustomerSearchResult | undefined) => {
|
||||
if (!result || !result.accountNo) return;
|
||||
// selectedRow is only set once the clerk types into A/C NO. themselves - on a fresh screen
|
||||
// it's still null, so fall back to the first (only) row that addRow() created at startup.
|
||||
const targetRow = this.selectedRow ?? this.rows[0];
|
||||
if (!targetRow) return;
|
||||
|
||||
const acTyp = TransactionEntryComponent.SEARCH_ACCOUNT_TYPE_TO_AC_TYP[result.accountType || ''] || 'SD';
|
||||
targetRow.acTyp = acTyp;
|
||||
targetRow.acNo = result.accountNo;
|
||||
this.selectedRow = targetRow;
|
||||
this.loadAccountDetails(result.accountNo, acTyp);
|
||||
});
|
||||
}
|
||||
|
||||
private loadAccountDetails(acNo: string, acTyp: string): void {
|
||||
this.lastLoadedAcNo = acNo;
|
||||
this.loadingAccount = true;
|
||||
@@ -2430,20 +2522,55 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
|
||||
const selectedPtEarly = this.paymentTypes.find((p: any) => p.id === row.paymentTypeId);
|
||||
const isCash = !!selectedPtEarly?.isCashPayment;
|
||||
const isDdCashGl = this.isDdCashGlRow(row);
|
||||
// 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);
|
||||
// DD + Cash + GL: amount is DD face value + charges + GST (rounded up), not the counted denomination
|
||||
const amount = isDdCashGl
|
||||
? this.ddTotalPayableFor(row)
|
||||
: isCash && !this.clerkMode
|
||||
? this.denominationTotal
|
||||
: (row.amount ?? this.denominationTotal);
|
||||
|
||||
if (isCash && this.denominationTotal <= 0 && !this.clerkMode) {
|
||||
if (isCash && this.denominationTotal <= 0 && !this.clerkMode && !isDdCashGl) {
|
||||
this.showAlert('error', 'Denomination Required', [
|
||||
'Cash transaction: Please enter denomination (notes/coins count) first.'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
// DD + Cash + GL: physical cash counted must reconcile with Face Value + Charges + GST
|
||||
if (isDdCashGl && !this.clerkMode) {
|
||||
if (this.denominationTotal <= 0) {
|
||||
this.showAlert('error', 'Denomination Required', [
|
||||
`Cash transaction: Please count notes/coins totalling ₹${amount.toLocaleString('en-IN', { minimumFractionDigits: 2 })} (DD Face Value + Charges + GST).`
|
||||
]);
|
||||
return;
|
||||
}
|
||||
if (Math.abs(this.denominationTotal - amount) > 0.005) {
|
||||
this.showAlert('error', 'Denomination Mismatch', [
|
||||
`Cash counted: ₹${this.denominationTotal.toLocaleString('en-IN', { minimumFractionDigits: 2 })}`,
|
||||
`Total To Collect: ₹${amount.toLocaleString('en-IN', { minimumFractionDigits: 2 })} (Face Value + Charges + GST)`,
|
||||
'Adjust the notes/coins breakdown to match the Total To Collect exactly.'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!amount || amount <= 0) {
|
||||
this.showAlert('error', 'Amount Required', ['Please enter a valid transaction amount.']);
|
||||
return;
|
||||
}
|
||||
|
||||
// DD + Cash + GL: reject at cash intake, before the cashier even accepts the money, if the DD
|
||||
// Face Value exceeds the admin-configured DD limit (same limit enforced again at DD issuance).
|
||||
if (isDdCashGl && this.ddLimitAmount != null && this.ddLimitAmount > 0 && Number(row.amount) > this.ddLimitAmount) {
|
||||
this.showAlert('error', 'DD Limit Exceeded', [
|
||||
`DD Face Value (₹${Number(row.amount).toLocaleString('en-IN', { minimumFractionDigits: 2 })}) exceeds the configured DD limit of ₹${this.ddLimitAmount.toLocaleString('en-IN', { minimumFractionDigits: 2 })}.`,
|
||||
'Reduce the amount, or issue this as separate DDs within the limit each.'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
const isDeposit = row.dbCr === 'Cr';
|
||||
|
||||
// Clerk cash deposit is not permitted — cash handling (notes/coins) is cashier's responsibility
|
||||
@@ -2738,7 +2865,12 @@ 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 ?? null,
|
||||
isDdCashGl: this.isDdCashGlRow(row),
|
||||
ddFaceValue: row.amount ?? 0,
|
||||
ddCharges: row.ddCharges ?? 0,
|
||||
ddGstPercentage: this.ddGstPercentage,
|
||||
ddGstAmount: this.ddGstAmountFor(row)
|
||||
};
|
||||
this.showConfirmDialog = true;
|
||||
}
|
||||
@@ -2746,7 +2878,7 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
confirmSubmit(): void {
|
||||
this.showConfirmDialog = false;
|
||||
const row = this.selectedRow!;
|
||||
const amount = row.amount ?? this.denominationTotal;
|
||||
const amount = this.isDdCashGlRow(row) ? this.ddTotalPayableFor(row) : (row.amount ?? this.denominationTotal);
|
||||
const isDeposit = row.dbCr === 'Cr';
|
||||
const command = isDeposit ? 'deposit' : 'withdrawal';
|
||||
const accountId = this.accountDetails.id;
|
||||
@@ -3151,7 +3283,17 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
|
||||
description: row.description || null,
|
||||
chequeNo: row.chequeNo || null,
|
||||
bankName: row.bankName || null,
|
||||
denominationData: isCash && this.denominationTotal > 0 ? JSON.stringify(this.denominationCounts) : null
|
||||
denominationData: isCash && this.denominationTotal > 0 ? JSON.stringify(this.denominationCounts) : null,
|
||||
// DD + Cash + GL breakdown — persisted so the Sponsor DD issuance screen can read the face
|
||||
// value/charges/GST back once this request is approved, instead of retyping them.
|
||||
...(this.isDdCashGlRow(row)
|
||||
? {
|
||||
ddFaceValue: row.amount ?? 0,
|
||||
ddCharges: row.ddCharges ?? 0,
|
||||
ddGstPercentage: this.ddGstPercentage,
|
||||
ddGstAmount: this.ddGstAmountFor(row)
|
||||
}
|
||||
: {})
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user