diff --git a/src/app/cashier/cashier-shared.scss b/src/app/cashier/cashier-shared.scss index 9874d3ade..6985025e4 100644 --- a/src/app/cashier/cashier-shared.scss +++ b/src/app/cashier/cashier-shared.scss @@ -542,9 +542,21 @@ // GL Table .gl-table { .gl-th--voucher { + width: 80px; + } + + .gl-th--date { + width: 100px; + } + + .gl-th--pt { width: 110px; } + .gl-th--teller { + width: 120px; + } + .gl-th--dr { color: #dc2626 !important; } @@ -571,6 +583,51 @@ color: #1e293b; } + .gl-badge { + display: inline-block; + padding: 2px 8px; + border-radius: 4px; + font-size: 10px; + font-weight: 800; + letter-spacing: 0.08em; + background: #dbeafe; + color: #1d4ed8; + text-transform: uppercase; + } + + .gl-date { + font-size: 12px; + color: #64748b; + white-space: nowrap; + } + + .gl-time { + font-size: 11px; + color: #94a3b8; + margin-top: 1px; + } + + .gl-pt-badge { + display: inline-block; + padding: 2px 7px; + border-radius: 4px; + font-size: 11px; + font-weight: 600; + background: #f0fdf4; + color: #15803d; + border: 1px solid #bbf7d0; + } + + .gl-pt-none { + color: #94a3b8; + font-size: 13px; + } + + .gl-teller { + font-size: 12px; + color: #475569; + } + .gl-amt--dr { color: #dc2626; font-weight: 700; diff --git a/src/app/cashier/closing-cash-report/closing-cash-report.component.html b/src/app/cashier/closing-cash-report/closing-cash-report.component.html index 95ae8e8b1..7b6c6e573 100644 --- a/src/app/cashier/closing-cash-report/closing-cash-report.component.html +++ b/src/app/cashier/closing-cash-report/closing-cash-report.component.html @@ -212,8 +212,11 @@ Voucher No + Date A/C No A/C Name + Payment Type + Teller DR (₹) CR (₹) @@ -221,9 +224,29 @@ @for (r of glRows; track $index) { - {{ r.voucherNo }} + + @if (r.txnSource === 'GL') { + GL + } @else { + {{ r.voucherNo }} + } + + +
{{ r.txnDate }}
+ @if (r.txnTime) { +
{{ r.txnTime }}
+ } + {{ r.acNo }} {{ r.acName }} + + @if (r.paymentType) { + {{ r.paymentType }} + } @else { + — + } + + {{ r.tellerName || '—' }} @if (r.dr > 0) { ₹{{ r.dr | number: '1.2-2' }} @@ -239,7 +262,7 @@ - Total + Total ₹{{ glTotalDr | number: '1.2-2' }} ₹{{ glTotalCr | number: '1.2-2' }} diff --git a/src/app/cashier/transaction-entry/transaction-entry.component.html b/src/app/cashier/transaction-entry/transaction-entry.component.html index b5f2de636..6827d7ff6 100644 --- a/src/app/cashier/transaction-entry/transaction-entry.component.html +++ b/src/app/cashier/transaction-entry/transaction-entry.component.html @@ -115,7 +115,12 @@
- @for (a of acTypOptions; track a) { } @@ -156,6 +161,7 @@ type="button" class="ws-seg__btn ws-seg__btn--dr" [class.ws-seg__btn--active-dr]="row.dbCr === 'Dr'" + [disabled]="row.acTyp === 'GL'" (click)="row.dbCr = 'Dr'" > DR @@ -560,7 +566,8 @@
- 1.Current Balance + 1. + {{ accountDetails?.glCode ? 'GL Balance' : 'Current Balance' }} {{ currencySymbol }}{{ currentBalance | number: '1.2-2' }} @@ -685,8 +692,12 @@
}
- {{ clientDetails?.displayName ?? accountDetails?.clientName ?? '—' }} - {{ accountDetails?.accountNo }} + {{ + accountDetails?.glCode + ? (accountDetails?.name ?? '—') + : (clientDetails?.displayName ?? accountDetails?.clientName ?? '—') + }} + {{ accountDetails?.glCode ?? accountDetails?.accountNo }}
@@ -713,7 +724,13 @@ ? 'Fixed Dep.' : selectedRow?.acTyp === 'RD' ? 'Recurring' - : '—') + : selectedRow?.acTyp === 'GL' + ? (accountDetails?.type?.value ?? 'GL Account') + : selectedRow?.acTyp === 'SHR' + ? 'Share' + : selectedRow?.acTyp === 'LA' + ? 'Loan' + : '—') }} Branch {{ officeName ?? '—' }} @@ -1083,7 +1100,11 @@
- @for (a of acTypOptions; track a) { } @@ -1116,6 +1137,7 @@ type="button" class="ws-seg__btn ws-seg__btn--dr" [class.ws-seg__btn--active-dr]="row.dbCr === 'Dr'" + [disabled]="row.acTyp === 'GL'" (click)="$event.stopPropagation(); row.dbCr = 'Dr'" > DR diff --git a/src/app/cashier/transaction-entry/transaction-entry.component.ts b/src/app/cashier/transaction-entry/transaction-entry.component.ts index cca15d147..d781184b3 100644 --- a/src/app/cashier/transaction-entry/transaction-entry.component.ts +++ b/src/app/cashier/transaction-entry/transaction-entry.component.ts @@ -660,6 +660,7 @@ export class TransactionEntryComponent implements OnInit, OnDestroy { // Search token — cancels stale async results private searchToken = 0; private acNoInput$ = new Subject<{ row: TxnRow; index: number; value: string }>(); + cashVaultGlId: number | null = null; // Joint holder expanded details (keyed by index) expandedHolder: number | null = null; @@ -767,6 +768,8 @@ export class TransactionEntryComponent implements OnInit, OnDestroy { } }); + this.loadCashVaultGl(); + if (this.isClerk) { const saved = this.clerkUiState.snapshot.savedAccount; if (saved) { @@ -1041,10 +1044,27 @@ export class TransactionEntryComponent implements OnInit, OnDestroy { this.acNoInput$.next({ row, index, value }); } + private loadCashVaultGl(): void { + // Dynamic: vault name > cash name > first ASSET type — no hardcoded IDs + this.http + .get('/financialactivityaccounts') + .pipe(catchError(() => of([]))) + .subscribe((list: any) => { + const items: any[] = Array.isArray(list) ? list : []; + const byName = (re: RegExp) => items.find((a: any) => re.test(a.financialActivityData?.name ?? '')); + const found = + byName(/vault/i) ?? byName(/cash/i) ?? items.find((a: any) => a.glAccountData?.type?.value === 'ASSET'); + if (found?.glAccountData?.id) { + this.cashVaultGlId = found.glAccountData.id; + } + }); + } + onRowSelect(row: TxnRow, index: number): void { this.selectedRow = row; // Only reload if account number changed or nothing is loaded yet - const alreadyLoaded = this.accountDetails && this.accountDetails.accountNo === row.acNo.trim(); + 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); } @@ -1072,22 +1092,25 @@ export class TransactionEntryComponent implements OnInit, OnDestroy { const token = ++this.searchToken; // Every new search gets a unique token const cfg = this.accountTypeConfig(acTyp); - // Step 1: search by accountNo on the correct endpoint for this account type + // Step 1: search on the correct endpoint for this account type this.http - .get(`${cfg.list}?accountNo=${encodeURIComponent(acNo)}&offset=0&limit=${cfg.maxLimit}`) + .get( + `${cfg.list}?${cfg.listQueryParam}=${encodeURIComponent(acNo)}${cfg.supportsPagination ? `&offset=0&limit=${cfg.maxLimit}` : ''}` + ) .pipe(catchError(() => of(null))) .subscribe((res: any) => { if (token !== this.searchToken) return; // stale result — newer search started const allItems: any[] = res?.pageItems ?? (Array.isArray(res) ? res : []); + const f = cfg.matchField; // Exact match first, then suffix/partial match (backend may return by last-N-digits) - const exactMatch = allItems.find((x: any) => x.accountNo === acNo); + const exactMatch = allItems.find((x: any) => x[f] === acNo); const partialMatch = !exactMatch && allItems.length === 1 - ? allItems.find((x: any) => x.accountNo?.endsWith(acNo) || x.accountNo?.includes(acNo)) + ? allItems.find((x: any) => x[f]?.endsWith(acNo) || x[f]?.includes(acNo)) : null; const match = exactMatch ?? partialMatch ?? null; if (match?.id) { - this.fetchAllDetails(match.id, match.accountNo, cfg, token, acTyp); + this.fetchAllDetails(match.id, match[f] ?? acNo, cfg, token, acTyp); return; } this.loadingAccount = false; @@ -1106,6 +1129,10 @@ export class TransactionEntryComponent implements OnInit, OnDestroy { txnSearchSupported: boolean; maxLimit: number; skipGlobalSearch: boolean; + listQueryParam: string; + matchField: string; + detailParams: string; + supportsPagination: boolean; } { const t = acTyp?.toUpperCase(); if (t === 'LA') { @@ -1118,12 +1145,14 @@ export class TransactionEntryComponent implements OnInit, OnDestroy { txnBase: '/loans', txnSearchSupported: true, maxLimit: 1, - skipGlobalSearch: true + skipGlobalSearch: true, + listQueryParam: 'accountNo', + matchField: 'accountNo', + detailParams: '?associations=all', + supportsPagination: true }; } if (t === 'SHR') { - // Backend ignores accountNo filter on list — fetch all and match client-side - // Global /search?resource=share returns 500 — skip it return { list: '/accounts/share', detail: '/accounts/share', @@ -1133,10 +1162,15 @@ export class TransactionEntryComponent implements OnInit, OnDestroy { txnBase: '/accounts/share', txnSearchSupported: false, maxLimit: 500, - skipGlobalSearch: true + skipGlobalSearch: true, + listQueryParam: 'accountNo', + matchField: 'accountNo', + detailParams: '?associations=all', + supportsPagination: true }; } if (t === 'GL') { + // GL uses searchParam (not accountNo) and matches on glCode field return { list: '/glaccounts', detail: '/glaccounts', @@ -1146,7 +1180,11 @@ export class TransactionEntryComponent implements OnInit, OnDestroy { txnBase: '/glaccounts', txnSearchSupported: false, maxLimit: 500, - skipGlobalSearch: true + skipGlobalSearch: true, + listQueryParam: 'searchParam', + matchField: 'glCode', + detailParams: '?fetchRunningBalance=true', + supportsPagination: false }; } if (t === 'CA' || t === 'CB') { @@ -1159,7 +1197,11 @@ export class TransactionEntryComponent implements OnInit, OnDestroy { txnBase: '/currentaccounts', txnSearchSupported: true, maxLimit: 1, - skipGlobalSearch: true + skipGlobalSearch: true, + listQueryParam: 'accountNo', + matchField: 'accountNo', + detailParams: '?associations=all', + supportsPagination: true }; } if (t === 'FD') { @@ -1172,7 +1214,11 @@ export class TransactionEntryComponent implements OnInit, OnDestroy { txnBase: '/savingsaccounts', txnSearchSupported: true, maxLimit: 1, - skipGlobalSearch: true + skipGlobalSearch: true, + listQueryParam: 'accountNo', + matchField: 'accountNo', + detailParams: '?associations=all', + supportsPagination: true }; } if (t === 'RD') { @@ -1185,7 +1231,11 @@ export class TransactionEntryComponent implements OnInit, OnDestroy { txnBase: '/savingsaccounts', txnSearchSupported: true, maxLimit: 1, - skipGlobalSearch: true + skipGlobalSearch: true, + listQueryParam: 'accountNo', + matchField: 'accountNo', + detailParams: '?associations=all', + supportsPagination: true }; } // SD — savings @@ -1198,7 +1248,11 @@ export class TransactionEntryComponent implements OnInit, OnDestroy { txnBase: '/savingsaccounts', txnSearchSupported: true, maxLimit: 1, - skipGlobalSearch: true + skipGlobalSearch: true, + listQueryParam: 'accountNo', + matchField: 'accountNo', + detailParams: '?associations=all', + supportsPagination: true }; } @@ -1219,12 +1273,13 @@ export class TransactionEntryComponent implements OnInit, OnDestroy { siAccountType: number; txnBase: string; txnSearchSupported: boolean; + detailParams?: string; }, token: number, acTyp: string = '' ): void { this.http - .get(`${cfg.detail}/${accountId}?associations=all`) + .get(`${cfg.detail}/${accountId}${cfg.detailParams ?? '?associations=all'}`) .pipe(catchError(() => of(null))) .subscribe((acc: any) => { if (token !== this.searchToken) return; // stale response — newer search active @@ -1233,17 +1288,18 @@ export class TransactionEntryComponent implements OnInit, OnDestroy { this.accountError = 'Could not load account details.'; return; } - // Cross-check: returned accountNo must match what cashier searched - if (acc.accountNo && acc.accountNo !== acNo) { + // Cross-check: returned identifier must match what cashier searched + const returnedId = acc.glCode ?? acc.accountNo; + if (returnedId && returnedId !== acNo) { this.loadingAccount = false; this.accountError = `Account mismatch. Please re-enter account number and try again.`; return; } this.accountDetails = acc; this.jointHolders = acc.jointHolders ?? []; - // Auto-fill ECS A/C NO. with savings account number (SHR) or account number + // Auto-fill ECS A/C NO.: GL → glCode, SHR → savingsAccountNumber, others → accountNo if (this.selectedRow) { - this.selectedRow.ecsAcNo = acc.savingsAccountNumber ?? acc.accountNo ?? ''; + this.selectedRow.ecsAcNo = acc.glCode ?? acc.savingsAccountNumber ?? acc.accountNo ?? ''; } if (this.isClerk) this.clerkUiState.setFacilities(this.facilities, acNo, acTyp); // Compute same-day compliance indicators from included transactions @@ -1484,7 +1540,7 @@ export class TransactionEntryComponent implements OnInit, OnDestroy { // ── Balance getters ──────────────────────────────────────────────────────── get currentBalance(): number { - return this.accountDetails?.summary?.accountBalance ?? 0; + return this.accountDetails?.summary?.accountBalance ?? this.accountDetails?.organizationRunningBalance ?? 0; } get clearBalance(): number { @@ -1935,6 +1991,55 @@ export class TransactionEntryComponent implements OnInit, OnDestroy { private proceedAfterValidation(row: TxnRow, amount: number, isDeposit: boolean, selectedPtEarly: any): void { const isWithdrawal = !isDeposit; const isCash = !!selectedPtEarly?.isCashPayment; + const isGlType = row.acTyp === 'GL'; + + // GL transactions post as journal entries — skip account-level facility/balance checks + if (isGlType) { + if (!this.cashVaultGlId) { + this.showAlert('error', 'Cash Vault GL Not Configured', [ + 'Financial Activity Account (CASH_AT_TELLER) is not configured.', + 'Please configure it in System → Financial Activity Accounts.' + ]); + return; + } + + // Payment type required (same as savings) + if (!row.paymentTypeId) { + this.showAlert('error', 'Payment Type Required', ['Please select Payment Type (Cash / Cheque / NEFT etc.)']); + return; + } + + // Denomination overdraft check for GL withdrawal (same as savings) + 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; + } + + const AML_GL = 200000; + if (amount > AML_GL && isCash) { + this.showConfirmAlert( + 'warning', + 'AML Alert — Large Cash Transaction', + [ + `Transaction Amount: ₹${amount.toLocaleString('en-IN', { minimumFractionDigits: 2 })}`, + 'This transaction requires manager approval.', + 'Have you obtained approval to proceed?' + ], + 'Yes, Proceed', + () => this.openConfirmDialog(row, amount, isDeposit, selectedPtEarly) + ); + return; + } + this.openConfirmDialog(row, amount, isDeposit, selectedPtEarly); + return; + } // 0d. Withdrawal facility if (isWithdrawal) { @@ -2028,7 +2133,7 @@ export class TransactionEntryComponent implements OnInit, OnDestroy { const balanceAfterTxn = isDeposit ? this.currentBalance + amount : this.currentBalance - amount; this.confirmData = { type: isDeposit ? 'DEPOSIT' : 'WITHDRAWAL', - accountNo: this.accountDetails.accountNo, + accountNo: this.accountDetails.glCode ?? this.accountDetails.accountNo, clientName: this.clientName, amount, currentBalance: this.currentBalance, @@ -2049,6 +2154,12 @@ export class TransactionEntryComponent implements OnInit, OnDestroy { const accountId = this.accountDetails.id; const cfg = this.accountTypeConfig(row.acTyp); + // GL type → post as manual journal entry + if (row.acTyp === 'GL') { + this.submitGlJournalEntry(row, amount, isDeposit); + return; + } + const today = new Date(); const txnDate = `${String(today.getDate()).padStart(2, '0')}/${String(today.getMonth() + 1).padStart(2, '0')}/${today.getFullYear()}`; @@ -2134,6 +2245,102 @@ export class TransactionEntryComponent implements OnInit, OnDestroy { this.addRow(); } + private submitGlJournalEntry(row: TxnRow, amount: number, isDeposit: boolean): void { + if (!this.cashVaultGlId) { + this.showAlert('error', 'Cash Vault GL Not Configured', [ + 'Financial Activity Account (CASH_AT_TELLER) is not configured.', + 'Please configure it in System → Financial Activity Accounts.' + ]); + this.saving = false; + return; + } + + const today = new Date(); + const txnDate = `${String(today.getDate()).padStart(2, '0')}/${String(today.getMonth() + 1).padStart(2, '0')}/${today.getFullYear()}`; + const txnDateIso = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`; + + const selectedGlId = this.accountDetails.id; + const contraGlId = this.cashVaultGlId; + + // isDeposit (CR button) = cashier receives cash → credit selected GL, debit contra (cash vault) + // isWithdrawal (DR button) = cashier pays out cash → debit selected GL, credit contra (cash vault) + const credits = isDeposit ? [{ glAccountId: selectedGlId, amount }] : [{ glAccountId: contraGlId, amount }]; + const debits = isDeposit ? [{ glAccountId: contraGlId, amount }] : [{ glAccountId: selectedGlId, amount }]; + + const payload: any = { + officeId: row.br || 1, + transactionDate: txnDate, + dateFormat: 'dd/MM/yyyy', + locale: 'en', + currencyCode: this.accountDetails?.currency?.code ?? 'INR', + comments: row.description || null, + credits, + debits, + paymentTypeId: row.paymentTypeId + }; + + // Check if cash payment so we can record denomination after journal entry + const selectedPt = this.paymentTypes.find((p: any) => p.id === row.paymentTypeId); + const isCashPayment = !!selectedPt?.isCashPayment; + + this.saving = true; + this.http + .post('/journalentries', payload) + .pipe( + catchError((err: any) => { + const msg = + err?.error?.errors?.[0]?.defaultUserMessage ?? + err?.error?.developerMessage ?? + 'Journal entry failed. Please try again.'; + this.showAlert('error', 'Transaction Failed', [msg]); + this.saving = false; + return of(null); + }) + ) + .subscribe((res: any) => { + this.saving = false; + if (res) { + // Record denomination so cashier position stays accurate + // Same condition as savings: cash + denom entered + denom matches posted amount + if (isCashPayment && this.denominationTotal > 0 && this.denominationTotal === amount) { + const denomBreakdown = [ + ...this.notes, + ...this.coins + ] + .filter((d) => this.countFor(d) > 0) + .map((d) => ({ + denominationId: d.id, + denominationValue: d.value, + kind: d.kind, + count: this.countFor(d), + subtotal: this.subtotalFor(d) + })); + this.http + .post('/cashier/gl-denomination', { + txnId: res.transactionId ?? res.resourceId, + accountNo: this.accountDetails?.glCode ?? row.acNo, + accountName: this.accountDetails?.name ?? '', + txnType: isDeposit ? 'DEPOSIT' : 'WITHDRAWAL', + txnDate: txnDateIso, + paymentTypeName: selectedPt?.name ?? 'Cash', + denominationBreakdown: denomBreakdown + }) + .pipe(catchError(() => of(null))) + .subscribe(); + } + + const ref = res.transactionId ?? res.resourceId ?? 'N/A'; + this.showAlert('success', 'GL Transaction Posted', [ + `Journal entry of ₹${amount.toLocaleString('en-IN', { minimumFractionDigits: 2 })} posted successfully.`, + `Transaction ID: ${ref}` + ]); + this.clearDenominations(); + this.loadOpeningPosition(); + this.loadAccountDetails(row.acNo.trim(), row.acTyp); + } + }); + } + onSearch(): void { if (this.selectedRow?.acNo.trim()) { this.loadAccountDetails(this.selectedRow.acNo.trim(), this.selectedRow.acTyp);