feat(cashier): add full-screen cashier teller workstation dashboard
- Add Transaction Entry screen with full account details panel (personal info, joint holders, nominees, KYC, balances) - Role-based layout: CASHIER role hides nav/sidebar/ticker, redirects to /cashier/transaction-entry on login - Denomination entry with opening/closing position preview (notes first then coins, stepper +/- round button inputs) - Auto-fetch savings/current/FD/RD accounts with associations, client KYC, nominees, addresses, family members (S/D/W/H/O) - Submit deposit/withdrawal via POST /transactions with denominationBreakdown - Cashier position auto-loads for CASHIER role (no filter dropdowns) - Pagination added to cash-history tabs and currency-exchange-detail - Cashier menu simplified for cashier role permissions Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
c8e9ab8f84
commit
9094b9a0eb
@@ -0,0 +1,655 @@
|
||||
/**
|
||||
* Copyright since 2025 Mifos Initiative
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { Component, OnInit, inject } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||
import { catchError } from 'rxjs/operators';
|
||||
import { forkJoin, of } from 'rxjs';
|
||||
import { CashierService } from '../cashier.service';
|
||||
|
||||
interface TxnRow {
|
||||
typ: string;
|
||||
br: number;
|
||||
trTp: string;
|
||||
acTyp: string;
|
||||
acNo: string;
|
||||
ecsAcNo: string;
|
||||
dbCr: string;
|
||||
chequeNo: string;
|
||||
amount: number | null;
|
||||
description: string;
|
||||
tokNo: string;
|
||||
cheqDate: string;
|
||||
}
|
||||
|
||||
interface Denomination {
|
||||
id: number;
|
||||
value: number;
|
||||
kind: 'note' | 'coin';
|
||||
label: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'mifosx-transaction-entry',
|
||||
templateUrl: './transaction-entry.component.html',
|
||||
styleUrls: ['./transaction-entry.component.scss'],
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule
|
||||
]
|
||||
})
|
||||
export class TransactionEntryComponent implements OnInit {
|
||||
private http = inject(HttpClient);
|
||||
private cashierService = inject(CashierService);
|
||||
|
||||
today = '';
|
||||
rows: TxnRow[] = [];
|
||||
selectedRow: TxnRow | null = null;
|
||||
loadingAccount = false;
|
||||
accountError = '';
|
||||
|
||||
// Full account + client data
|
||||
accountDetails: any = null; // savings account API response
|
||||
clientDetails: any = null; // client personal details
|
||||
kycDetails: any = null; // KYC (PAN, Aadhaar, CKYC)
|
||||
nominees: any[] = []; // nominee list
|
||||
jointHolders: any[] = []; // joint holders
|
||||
clientAddresses: any[] = []; // client addresses
|
||||
|
||||
acTypOptions = [
|
||||
'SD',
|
||||
'CA',
|
||||
'FD',
|
||||
'RD',
|
||||
'SHR',
|
||||
'CC'
|
||||
];
|
||||
dbCrOptions = [
|
||||
'Dr',
|
||||
'Cr'
|
||||
];
|
||||
trTpOptions = [
|
||||
'C',
|
||||
'D',
|
||||
'T'
|
||||
];
|
||||
|
||||
// ── Denomination ──────────────────────────────────────────────────────────
|
||||
notes: Denomination[] = [];
|
||||
coins: Denomination[] = [];
|
||||
denominationCounts: Record<string, number> = {};
|
||||
|
||||
// ── Opening / Closing position ────────────────────────────────────────────
|
||||
openingDenoms: any[] = [];
|
||||
openingTotal = 0;
|
||||
openingLoaded = false;
|
||||
|
||||
// ── Transaction saving state ──────────────────────────────────────────────
|
||||
saving = false;
|
||||
|
||||
ngOnInit(): void {
|
||||
const d = new Date();
|
||||
this.today = `${String(d.getDate()).padStart(2, '0')}/${String(d.getMonth() + 1).padStart(2, '0')}/${d.getFullYear()}`;
|
||||
this.addRow();
|
||||
this.loadDenominations();
|
||||
this.loadOpeningPosition();
|
||||
}
|
||||
|
||||
private loadDenominations(): void {
|
||||
this.cashierService
|
||||
.getActiveDenominations()
|
||||
.pipe(catchError(() => of([])))
|
||||
.subscribe((list: any[]) => {
|
||||
const rows: Denomination[] = (list || []).map((d: any) => ({
|
||||
id: d.id,
|
||||
value: d.denomination ?? d.value,
|
||||
kind: d.kind as 'note' | 'coin',
|
||||
label: '₹' + (d.denomination ?? d.value)
|
||||
}));
|
||||
const order = (a: Denomination, b: Denomination) => b.value - a.value;
|
||||
this.notes = rows.filter((r) => r.kind === 'note').sort(order);
|
||||
this.coins = rows.filter((r) => r.kind === 'coin').sort(order);
|
||||
});
|
||||
}
|
||||
|
||||
private loadOpeningPosition(): void {
|
||||
const d = new Date();
|
||||
const asOfDate = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
||||
this.cashierService
|
||||
.getCashierPositionForMe(asOfDate)
|
||||
.pipe(catchError(() => of(null)))
|
||||
.subscribe((res: any) => {
|
||||
this.openingDenoms = res?.denominations ?? [];
|
||||
this.openingTotal = Number(res?.totalAmount ?? 0);
|
||||
this.openingLoaded = true;
|
||||
});
|
||||
}
|
||||
|
||||
// ── Denomination helpers ──────────────────────────────────────────────────
|
||||
|
||||
denomKey(d: Denomination): string {
|
||||
return `${d.kind}:${d.value}`;
|
||||
}
|
||||
|
||||
countFor(d: Denomination): number {
|
||||
return this.denominationCounts[this.denomKey(d)] || 0;
|
||||
}
|
||||
|
||||
subtotalFor(d: Denomination): number {
|
||||
return this.countFor(d) * d.value;
|
||||
}
|
||||
|
||||
onDenomChange(d: Denomination, raw: any): void {
|
||||
this.denominationCounts[this.denomKey(d)] = Math.max(0, Math.floor(Number(raw) || 0));
|
||||
}
|
||||
|
||||
bumpDenom(d: Denomination, delta: number): void {
|
||||
this.denominationCounts[this.denomKey(d)] = Math.max(0, this.countFor(d) + delta);
|
||||
}
|
||||
|
||||
get notesTotal(): number {
|
||||
return this.notes.reduce((s, d) => s + this.subtotalFor(d), 0);
|
||||
}
|
||||
get coinsTotal(): number {
|
||||
return this.coins.reduce((s, d) => s + this.subtotalFor(d), 0);
|
||||
}
|
||||
get denominationTotal(): number {
|
||||
return this.notesTotal + this.coinsTotal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Denomination Entry order: ALL notes (desc) first, then ALL coins (desc).
|
||||
* This matches how cashiers physically count cash — notes first, coins after.
|
||||
* Opening/Closing panels use value-based interleaving (reporting format).
|
||||
*/
|
||||
get allDenominations(): Denomination[] {
|
||||
const desc = (a: Denomination, b: Denomination) => b.value - a.value;
|
||||
return [
|
||||
...[...this.notes].sort(desc),
|
||||
...[...this.coins].sort(desc)
|
||||
];
|
||||
}
|
||||
|
||||
// ── Opening / Closing ─────────────────────────────────────────────────────
|
||||
|
||||
get openingNotes(): any[] {
|
||||
return this.openingDenoms.filter((d) => d.kind !== 'coin');
|
||||
}
|
||||
get openingCoins(): any[] {
|
||||
return this.openingDenoms.filter((d) => d.kind === 'coin');
|
||||
}
|
||||
|
||||
/** Opening sorted: all notes (desc) then all coins (desc) */
|
||||
get sortedOpeningDenoms(): any[] {
|
||||
const desc = (a: any, b: any) => b.denominationValue - a.denominationValue;
|
||||
const notes = this.openingDenoms.filter((d) => d.kind !== 'coin').sort(desc);
|
||||
const coins = this.openingDenoms.filter((d) => d.kind === 'coin').sort(desc);
|
||||
return [
|
||||
...notes,
|
||||
...coins
|
||||
];
|
||||
}
|
||||
|
||||
/** Closing sorted: all notes (desc) then all coins (desc) */
|
||||
get sortedClosingDenoms(): any[] {
|
||||
const desc = (a: any, b: any) => b.denominationValue - a.denominationValue;
|
||||
const notes = this.closingDenoms.filter((d) => d.kind !== 'coin').sort(desc);
|
||||
const coins = this.closingDenoms.filter((d) => d.kind === 'coin').sort(desc);
|
||||
return [
|
||||
...notes,
|
||||
...coins
|
||||
];
|
||||
}
|
||||
|
||||
get closingTotal(): number {
|
||||
const row = this.selectedRow;
|
||||
const sign = row?.dbCr === 'Cr' ? 1 : -1; // Cr = Deposit (+), Dr = Withdrawal (-)
|
||||
return this.openingTotal + this.denominationTotal * sign;
|
||||
}
|
||||
|
||||
get closingDenoms(): any[] {
|
||||
const openMap = new Map<string, any>();
|
||||
for (const d of this.openingDenoms) {
|
||||
openMap.set(`${d.kind}:${d.denominationValue}`, d);
|
||||
}
|
||||
const sign = this.selectedRow?.dbCr === 'Cr' ? 1 : -1;
|
||||
const result: any[] = [];
|
||||
for (const denom of [
|
||||
...this.notes,
|
||||
...this.coins
|
||||
]) {
|
||||
const key = this.denomKey(denom);
|
||||
const open = openMap.get(key);
|
||||
const openCount = open?.count ?? 0;
|
||||
const entered = this.countFor(denom);
|
||||
if (openCount === 0 && entered === 0) continue;
|
||||
result.push({
|
||||
denominationValue: denom.value,
|
||||
kind: denom.kind,
|
||||
openCount,
|
||||
entered,
|
||||
closingCount: openCount + entered * sign
|
||||
});
|
||||
}
|
||||
return result.sort((a, b) => b.denominationValue - a.denominationValue);
|
||||
}
|
||||
|
||||
applyDenominationTotal(): void {
|
||||
if (this.selectedRow) {
|
||||
this.selectedRow.amount = this.denominationTotal;
|
||||
}
|
||||
}
|
||||
|
||||
clearDenominations(): void {
|
||||
this.denominationCounts = {};
|
||||
}
|
||||
|
||||
addRow(): void {
|
||||
this.rows.push({
|
||||
typ: '~',
|
||||
br: 1,
|
||||
trTp: '',
|
||||
acTyp: 'SD',
|
||||
acNo: '',
|
||||
ecsAcNo: '',
|
||||
dbCr: '',
|
||||
chequeNo: '',
|
||||
amount: null,
|
||||
description: '',
|
||||
tokNo: '',
|
||||
cheqDate: ''
|
||||
});
|
||||
}
|
||||
|
||||
onAcNoBlur(row: TxnRow, index: number): void {
|
||||
if (!row.acNo.trim()) return;
|
||||
this.selectedRow = row;
|
||||
this.loadAccountDetails(row.acNo.trim(), row.acTyp);
|
||||
}
|
||||
|
||||
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();
|
||||
if (row.acNo.trim() && !alreadyLoaded) {
|
||||
this.loadAccountDetails(row.acNo.trim(), row.acTyp);
|
||||
}
|
||||
}
|
||||
|
||||
private loadAccountDetails(acNo: string, acTyp: string): void {
|
||||
this.loadingAccount = true;
|
||||
this.accountDetails = null;
|
||||
this.clientDetails = null;
|
||||
this.kycDetails = null;
|
||||
this.nominees = [];
|
||||
this.jointHolders = [];
|
||||
this.clientAddresses = [];
|
||||
this.accountError = '';
|
||||
|
||||
const cfg = this.accountTypeConfig(acTyp);
|
||||
|
||||
// Step 1: search by accountNo on the correct endpoint for this account type
|
||||
this.http
|
||||
.get<any>(`${cfg.list}?accountNo=${encodeURIComponent(acNo)}&offset=0&limit=1`)
|
||||
.pipe(catchError(() => of(null)))
|
||||
.subscribe((res: any) => {
|
||||
const items: any[] = res?.pageItems ?? (Array.isArray(res) ? res : []);
|
||||
if (items.length > 0 && items[0].id) {
|
||||
this.fetchAllDetails(items[0].id, acNo, cfg);
|
||||
return;
|
||||
}
|
||||
// Fallback: global search
|
||||
this.http
|
||||
.get<any>(`/search?query=${encodeURIComponent(acNo)}&resource=${cfg.search}`)
|
||||
.pipe(catchError(() => of([])))
|
||||
.subscribe((found: any) => {
|
||||
const list: any[] = Array.isArray(found) ? found : [];
|
||||
if (list.length > 0 && list[0].entityId) {
|
||||
this.fetchAllDetails(list[0].entityId, acNo, cfg);
|
||||
} else {
|
||||
this.loadingAccount = false;
|
||||
this.accountError = 'Account not found for: ' + acNo;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/** Maps UI account type → { listEndpoint, detailEndpoint, nomineeType, searchResource } */
|
||||
private accountTypeConfig(acTyp: string): { list: string; detail: string; nominee: string; search: string } {
|
||||
const t = acTyp?.toUpperCase();
|
||||
if (t === 'CA' || t === 'CB' || t === 'CC') {
|
||||
return { list: '/currentaccounts', detail: '/currentaccounts', nominee: 'CB', search: 'currentaccount' };
|
||||
}
|
||||
if (t === 'FD') {
|
||||
return { list: '/fixeddepositaccounts', detail: '/fixeddepositaccounts', nominee: 'FD', search: 'fixeddeposit' };
|
||||
}
|
||||
if (t === 'RD') {
|
||||
return {
|
||||
list: '/recurringdepositaccounts',
|
||||
detail: '/recurringdepositaccounts',
|
||||
nominee: 'RD',
|
||||
search: 'recurringdeposit'
|
||||
};
|
||||
}
|
||||
// Default: SD / SB — Savings
|
||||
return { list: '/savingsaccounts', detail: '/savingsaccounts', nominee: 'SB', search: 'saving' };
|
||||
}
|
||||
|
||||
/** Maps UI account type code → backend accountType code for nominee filter */
|
||||
private toNomineeAccountType(acTyp: string): string {
|
||||
return this.accountTypeConfig(acTyp).nominee;
|
||||
}
|
||||
|
||||
/** Parallel fetch: account (with associations) + client + KYC + nominees */
|
||||
private fetchAllDetails(
|
||||
accountId: number,
|
||||
acNo: string,
|
||||
cfg: { list: string; detail: string; nominee: string; search: string }
|
||||
): void {
|
||||
this.http
|
||||
.get<any>(`${cfg.detail}/${accountId}?associations=all`)
|
||||
.pipe(catchError(() => of(null)))
|
||||
.subscribe((acc: any) => {
|
||||
if (!acc) {
|
||||
this.loadingAccount = false;
|
||||
this.accountError = 'Could not load account details.';
|
||||
return;
|
||||
}
|
||||
this.accountDetails = acc;
|
||||
this.jointHolders = acc.jointHolders ?? [];
|
||||
|
||||
const clientId = acc.clientId?.toString();
|
||||
if (!clientId) {
|
||||
this.loadingAccount = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const acType = cfg.nominee;
|
||||
|
||||
// Parallel: client + KYC + nominees + addresses + family members
|
||||
forkJoin({
|
||||
client: this.http.get<any>(`/clients/${clientId}`).pipe(catchError(() => of(null))),
|
||||
kyc: this.http.get<any>(`/clients/${clientId}/kyc`).pipe(catchError(() => of([]))),
|
||||
nominees: this.http
|
||||
.get<any>(`/clients/${clientId}/nominees?accountNo=${acNo}&accountType=${acType}`)
|
||||
.pipe(catchError(() => of([]))),
|
||||
addresses: this.http.get<any>(`/client/${clientId}/addresses`).pipe(catchError(() => of([]))),
|
||||
family: this.http.get<any>(`/clients/${clientId}/familymembers`).pipe(catchError(() => of([])))
|
||||
}).subscribe(({ client, kyc, nominees, addresses, family }) => {
|
||||
this.loadingAccount = false;
|
||||
this.clientDetails = { ...client, familyMembers: Array.isArray(family) ? family : [] };
|
||||
const kycArr: any[] = Array.isArray(kyc) ? kyc : kyc ? [kyc] : [];
|
||||
this.kycDetails = kycArr[0] ?? null;
|
||||
this.nominees = Array.isArray(nominees) ? nominees : [];
|
||||
this.clientAddresses = Array.isArray(addresses) ? addresses : [];
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ── Balance getters ────────────────────────────────────────────────────────
|
||||
|
||||
get currentBalance(): number {
|
||||
return this.accountDetails?.summary?.accountBalance ?? 0;
|
||||
}
|
||||
|
||||
get clearBalance(): number {
|
||||
return this.accountDetails?.summary?.availableBalance ?? this.currentBalance;
|
||||
}
|
||||
|
||||
get todayClgAmount(): number {
|
||||
return this.accountDetails?.summary?.totalDepositsPostMaturityLevel ?? 0;
|
||||
}
|
||||
|
||||
get freezeAmount(): number {
|
||||
return this.accountDetails?.summary?.holdAmountBalance ?? 0;
|
||||
}
|
||||
|
||||
get withdrawableAmount(): number {
|
||||
const avail = this.accountDetails?.summary?.availableBalance ?? this.currentBalance;
|
||||
return Math.max(0, avail - this.freezeAmount);
|
||||
}
|
||||
|
||||
get currencySymbol(): string {
|
||||
return this.accountDetails?.currency?.displaySymbol ?? '₹';
|
||||
}
|
||||
|
||||
get modeOfOperationLabel(): string {
|
||||
const modeMap: Record<string, string> = {
|
||||
FORMER_OR_SURVIVOR: 'Former or Survivor',
|
||||
EITHER_OR_SURVIVOR: 'Either or Survivor',
|
||||
ANYONE_OR_SURVIVOR: 'Any One or Survivor',
|
||||
JOINTLY: 'Both Jointly',
|
||||
SINGLE: 'Single',
|
||||
OTHER: 'Other'
|
||||
};
|
||||
const mode = this.accountDetails?.modeOfOperation ?? '';
|
||||
return modeMap[mode] ?? mode.replace(/_/g, ' ');
|
||||
}
|
||||
|
||||
// ── Client detail getters ─────────────────────────────────────────────────
|
||||
|
||||
get clientName(): string {
|
||||
return this.clientDetails?.displayName ?? this.accountDetails?.clientName ?? '—';
|
||||
}
|
||||
|
||||
get guardianName(): string {
|
||||
// S/D/W/H/O derived from family members
|
||||
// API returns: firstName, middleName, lastName, relationship
|
||||
const family: any[] = this.clientDetails?.familyMembers ?? [];
|
||||
if (!family.length) return this.clientDetails?.guardianClientName ?? '—';
|
||||
|
||||
const priority = [
|
||||
'FATHER',
|
||||
'MOTHER',
|
||||
'HUSBAND',
|
||||
'WIFE',
|
||||
'SPOUSE'
|
||||
];
|
||||
const prefixMap: Record<string, string> = {
|
||||
FATHER: 'S/O',
|
||||
MOTHER: 'D/O',
|
||||
HUSBAND: 'W/O',
|
||||
WIFE: 'H/O',
|
||||
SPOUSE: 'W/O'
|
||||
};
|
||||
|
||||
for (const rel of priority) {
|
||||
const member = family.find((m: any) => (m.relationship ?? m.relationshipName ?? '').toUpperCase() === rel);
|
||||
if (member) {
|
||||
// API uses camelCase: firstName, middleName, lastName
|
||||
const name = [
|
||||
member.firstName,
|
||||
member.middleName,
|
||||
member.lastName
|
||||
]
|
||||
.filter((v) => v && v.trim())
|
||||
.join(' ')
|
||||
.trim();
|
||||
return name ? `${prefixMap[rel] ?? rel} ${name}` : (prefixMap[rel] ?? rel);
|
||||
}
|
||||
}
|
||||
// Fallback: first family member
|
||||
const m = family[0];
|
||||
const name = [
|
||||
m.firstName,
|
||||
m.middleName,
|
||||
m.lastName
|
||||
]
|
||||
.filter((v) => v && v.trim())
|
||||
.join(' ')
|
||||
.trim();
|
||||
return name || '—';
|
||||
}
|
||||
|
||||
get clientAddress(): string {
|
||||
// From /client/{id}/addresses — AddressData fields: addressLine1, townVillage, city, stateName, postalCode
|
||||
if (this.clientAddresses.length > 0) {
|
||||
const a = this.clientAddresses[0];
|
||||
return [
|
||||
a.addressLine1,
|
||||
a.addressLine2,
|
||||
a.addressLine3,
|
||||
a.townVillage,
|
||||
a.city,
|
||||
a.countyDistrict,
|
||||
a.stateName ?? a.stateProvinceLabel,
|
||||
a.postalCode
|
||||
]
|
||||
.filter((v) => v && v.toString().trim())
|
||||
.join(', ');
|
||||
}
|
||||
return '—';
|
||||
}
|
||||
|
||||
get clientPhone(): string {
|
||||
const mob = this.clientDetails?.mobileNo ?? this.accountDetails?.clientMobileNo;
|
||||
const alt = this.clientDetails?.alternateMobileNo;
|
||||
return (
|
||||
[
|
||||
mob,
|
||||
alt
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' / ') || '—'
|
||||
);
|
||||
}
|
||||
|
||||
get clientEmail(): string {
|
||||
return this.clientDetails?.emailAddress ?? '—';
|
||||
}
|
||||
|
||||
get officeName(): string {
|
||||
return this.accountDetails?.officeName ?? this.clientDetails?.officeName ?? '—';
|
||||
}
|
||||
|
||||
get openingDate(): string {
|
||||
const d = this.accountDetails?.timeline?.submittedOnDate;
|
||||
return d ? `${d[2]}/${d[1]}/${d[0]}` : '—';
|
||||
}
|
||||
|
||||
get lastTransactionDate(): string {
|
||||
const d = this.accountDetails?.lastActiveTransactionDate;
|
||||
return d ? `${d[2]}/${d[1]}/${d[0]}` : '—';
|
||||
}
|
||||
|
||||
get panNumber(): string {
|
||||
return this.kycDetails?.panNumber ?? this.kycDetails?.pan_number ?? '—';
|
||||
}
|
||||
|
||||
get aadhaarNumber(): string {
|
||||
return this.kycDetails?.aadhaarNumberMasked ?? this.kycDetails?.aadhaar_number_masked ?? 'XXXX/XXXX/XXXX';
|
||||
}
|
||||
|
||||
get ckycNumber(): string {
|
||||
return this.kycDetails?.ckycNumber ?? this.kycDetails?.ckyc_number ?? '—';
|
||||
}
|
||||
|
||||
get kycStatus(): string {
|
||||
const s = this.kycDetails?.kycStatus ?? this.kycDetails?.kyc_status;
|
||||
if (!s) return '—';
|
||||
const map: Record<number, string> = { 100: 'Pending', 200: 'In Progress', 300: 'Verified', 400: 'Rejected' };
|
||||
return map[s] ?? `KYC-${s}`;
|
||||
}
|
||||
|
||||
get cifNumber(): string {
|
||||
return this.clientDetails?.cifNumber ?? this.clientDetails?.cif_number ?? '—';
|
||||
}
|
||||
|
||||
onSave(): void {
|
||||
const row = this.selectedRow;
|
||||
if (!row || !row.acNo.trim() || !this.accountDetails) return;
|
||||
|
||||
const amount = row.amount ?? this.denominationTotal;
|
||||
if (!amount || amount <= 0) {
|
||||
alert('Please enter amount or denomination.');
|
||||
return;
|
||||
}
|
||||
|
||||
const isDeposit = row.dbCr === 'Cr';
|
||||
const command = isDeposit ? 'deposit' : 'withdrawal';
|
||||
const accountId = this.accountDetails.id;
|
||||
const cfg = this.accountTypeConfig(row.acTyp);
|
||||
|
||||
const today = new Date();
|
||||
const txnDate = `${String(today.getDate()).padStart(2, '0')}/${String(today.getMonth() + 1).padStart(2, '0')}/${today.getFullYear()}`;
|
||||
|
||||
const payload: any = {
|
||||
transactionDate: txnDate,
|
||||
transactionAmount: amount,
|
||||
dateFormat: 'dd/MM/yyyy',
|
||||
locale: 'en',
|
||||
note: row.description || null
|
||||
};
|
||||
|
||||
if (row.chequeNo) payload['checkNumber'] = row.chequeNo;
|
||||
|
||||
if (this.denominationTotal > 0) {
|
||||
payload['denominationBreakdown'] = [
|
||||
...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.saving = true;
|
||||
const params = new HttpParams().set('command', command);
|
||||
this.http
|
||||
.post(`${cfg.detail}/${accountId}/transactions`, payload, { params })
|
||||
.pipe(
|
||||
catchError((err) => {
|
||||
const msg =
|
||||
err?.error?.errors?.[0]?.defaultUserMessage ?? err?.error?.developerMessage ?? 'Transaction failed.';
|
||||
alert(msg);
|
||||
this.saving = false;
|
||||
return of(null);
|
||||
})
|
||||
)
|
||||
.subscribe((res: any) => {
|
||||
this.saving = false;
|
||||
if (res) {
|
||||
alert(`Transaction posted. Ref: ${res.resourceId ?? res.transactionId ?? 'N/A'}`);
|
||||
this.clearDenominations();
|
||||
this.loadOpeningPosition();
|
||||
this.loadAccountDetails(row.acNo.trim(), row.acTyp);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onCancel(): void {
|
||||
this.accountDetails = null;
|
||||
this.clientDetails = null;
|
||||
this.kycDetails = null;
|
||||
this.nominees = [];
|
||||
this.jointHolders = [];
|
||||
this.clientAddresses = [];
|
||||
this.accountError = '';
|
||||
this.selectedRow = null;
|
||||
this.rows = [];
|
||||
this.addRow();
|
||||
}
|
||||
|
||||
onSearch(): void {
|
||||
if (this.selectedRow?.acNo.trim()) {
|
||||
this.loadAccountDetails(this.selectedRow.acNo.trim(), this.selectedRow.acTyp);
|
||||
}
|
||||
}
|
||||
|
||||
onExit(): void {
|
||||
history.back();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user