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:
Jaanu423
2026-06-05 10:59:34 +05:30
co-authored by Claude Sonnet 4.6
parent c8e9ab8f84
commit 9094b9a0eb
18 changed files with 2828 additions and 190 deletions
@@ -12,6 +12,7 @@ import { FormsModule } from '@angular/forms';
import { MatIconModule } from '@angular/material/icon';
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
import { MatTabsModule } from '@angular/material/tabs';
import { MatPaginatorModule, PageEvent } from '@angular/material/paginator';
import { forkJoin, of } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { CashierService } from '../cashier.service';
@@ -65,7 +66,8 @@ interface DetailView {
FormsModule,
MatIconModule,
MatSnackBarModule,
MatTabsModule
MatTabsModule,
MatPaginatorModule
]
})
export class CashHistoryComponent implements OnInit {
@@ -78,6 +80,23 @@ export class CashHistoryComponent implements OnInit {
exchanges: TxnRow[] = [];
loading = false;
// Pagination — per tab
pageSize = 20;
pageSizeOptions = [
10,
20,
50,
100
];
transfersTotal = 0;
transfersOffset = 0;
settlementsTotal = 0;
settlementsOffset = 0;
interTotal = 0;
interOffset = 0;
exchangesTotal = 0;
exchangesOffset = 0;
private denomKindMap = new Map<number, 'note' | 'coin'>();
reversingId: number | null = null;
@@ -96,15 +115,55 @@ export class CashHistoryComponent implements OnInit {
this.load();
}
onPageChange(tab: 'transfers' | 'settlements' | 'inter' | 'exchanges', event: PageEvent): void {
this.pageSize = event.pageSize;
if (tab === 'transfers') {
this.transfersOffset = event.pageIndex * event.pageSize;
this.loadTransfers();
}
if (tab === 'settlements') {
this.settlementsOffset = event.pageIndex * event.pageSize;
this.loadSettlements();
}
if (tab === 'inter') {
this.interOffset = event.pageIndex * event.pageSize;
this.loadInter();
}
if (tab === 'exchanges') {
this.exchangesOffset = event.pageIndex * event.pageSize;
this.loadExchanges();
}
}
load(): void {
this.transfersOffset = 0;
this.settlementsOffset = 0;
this.interOffset = 0;
this.exchangesOffset = 0;
this.loading = true;
forkJoin({
transfers: this.service.getCashTransfers().pipe(catchError(() => of([]))),
settlements: this.service.getCashSettlements().pipe(catchError(() => of([]))),
inter: this.service.getInterCashierTransfers().pipe(catchError(() => of([]))),
exchanges: this.service.getCashExchanges().pipe(catchError(() => of([])))
transfers: this.service
.getCashTransfers(0, this.pageSize)
.pipe(catchError(() => of({ pageItems: [], totalFilteredRecords: 0 }))),
settlements: this.service
.getCashSettlements(0, this.pageSize)
.pipe(catchError(() => of({ pageItems: [], totalFilteredRecords: 0 }))),
inter: this.service
.getInterCashierTransfers(0, this.pageSize)
.pipe(catchError(() => of({ pageItems: [], totalFilteredRecords: 0 }))),
exchanges: this.service
.getCashExchanges(0, this.pageSize)
.pipe(catchError(() => of({ pageItems: [], totalFilteredRecords: 0 })))
}).subscribe(({ transfers, settlements, inter, exchanges }) => {
this.transfers = (transfers as any[]).map((t) => ({
this.transfersTotal = transfers.totalFilteredRecords ?? 0;
this.settlementsTotal = settlements.totalFilteredRecords ?? 0;
this.interTotal = inter.totalFilteredRecords ?? 0;
this.exchangesTotal = exchanges.totalFilteredRecords ?? 0;
const transferItems = transfers.pageItems ?? [];
const settlementItems = settlements.pageItems ?? [];
const interItems = inter.pageItems ?? [];
const exchangeItems = exchanges.pageItems ?? [];
this.transfers = (transferItems as any[]).map((t) => ({
id: t.id,
date: t.transferDate,
fromParty: t.fromParty,
@@ -119,7 +178,7 @@ export class CashHistoryComponent implements OnInit {
reverseReason: t.reverseReason ?? null,
type: 'transfer'
}));
this.settlements = (settlements as any[]).map((s) => ({
this.settlements = (settlementItems as any[]).map((s) => ({
id: s.id,
date: s.settlementDate,
fromParty: s.fromParty,
@@ -135,7 +194,7 @@ export class CashHistoryComponent implements OnInit {
reverseReason: s.reverseReason ?? null,
type: 'settlement'
}));
this.interCashier = (inter as any[]).map((i) => ({
this.interCashier = (interItems as any[]).map((i) => ({
id: i.id,
date: i.transferDate,
fromParty: i.fromParty,
@@ -150,7 +209,7 @@ export class CashHistoryComponent implements OnInit {
reverseReason: i.reverseReason ?? null,
type: 'inter'
}));
this.exchanges = (exchanges as any[]).map((e) => ({
this.exchanges = (exchangeItems as any[]).map((e) => ({
id: e.id,
date: e.exchangeDate,
fromParty: e.accountNo ? `Account ${e.accountNo}` : 'Walk-in Customer',
@@ -169,6 +228,103 @@ export class CashHistoryComponent implements OnInit {
});
}
private loadTransfers(): void {
this.service
.getCashTransfers(this.transfersOffset, this.pageSize)
.pipe(catchError(() => of({ pageItems: [], totalFilteredRecords: 0 })))
.subscribe((res: any) => {
this.transfersTotal = res.totalFilteredRecords ?? 0;
this.transfers = (res.pageItems ?? []).map((t: any) => ({
id: t.id,
date: t.transferDate,
fromParty: t.fromParty,
toParty: t.toParty,
amount: t.transferAmount,
note: t.note,
isReversed: t.isReversed,
performedByUsername: t.performedByUsername,
createdAt: t.createdAt,
reversedBy: t.reversedBy ?? null,
reversedAt: t.reversedAt ?? null,
reverseReason: t.reverseReason ?? null,
type: 'transfer'
}));
});
}
private loadSettlements(): void {
this.service
.getCashSettlements(this.settlementsOffset, this.pageSize)
.pipe(catchError(() => of({ pageItems: [], totalFilteredRecords: 0 })))
.subscribe((res: any) => {
this.settlementsTotal = res.totalFilteredRecords ?? 0;
this.settlements = (res.pageItems ?? []).map((s: any) => ({
id: s.id,
date: s.settlementDate,
fromParty: s.fromParty,
fromCashierId: s.fromCashierId ?? undefined,
toParty: s.toParty,
amount: s.settlementAmount,
note: s.note,
isReversed: s.isReversed,
performedByUsername: s.performedByUsername,
createdAt: s.createdAt,
reversedBy: s.reversedBy ?? null,
reversedAt: s.reversedAt ?? null,
reverseReason: s.reverseReason ?? null,
type: 'settlement'
}));
});
}
private loadInter(): void {
this.service
.getInterCashierTransfers(this.interOffset, this.pageSize)
.pipe(catchError(() => of({ pageItems: [], totalFilteredRecords: 0 })))
.subscribe((res: any) => {
this.interTotal = res.totalFilteredRecords ?? 0;
this.interCashier = (res.pageItems ?? []).map((i: any) => ({
id: i.id,
date: i.transferDate,
fromParty: i.fromParty,
toParty: i.toParty,
amount: i.transferAmount,
note: i.note,
isReversed: i.isReversed,
performedByUsername: i.performedByUsername,
createdAt: i.createdAt,
reversedBy: i.reversedBy ?? null,
reversedAt: i.reversedAt ?? null,
reverseReason: i.reverseReason ?? null,
type: 'inter'
}));
});
}
private loadExchanges(): void {
this.service
.getCashExchanges(this.exchangesOffset, this.pageSize)
.pipe(catchError(() => of({ pageItems: [], totalFilteredRecords: 0 })))
.subscribe((res: any) => {
this.exchangesTotal = res.totalFilteredRecords ?? 0;
this.exchanges = (res.pageItems ?? []).map((e: any) => ({
id: e.id,
date: e.exchangeDate,
fromParty: e.accountNo ? `Account ${e.accountNo}` : 'Walk-in Customer',
toParty: e.performedByUsername || '—',
amount: e.exchangeAmount,
note: e.note,
isReversed: e.isReversed ?? false,
performedByUsername: e.performedByUsername,
createdAt: e.createdAt,
reversedBy: e.reversedBy ?? null,
reversedAt: e.reversedAt ?? null,
reverseReason: e.reverseReason ?? null,
type: 'exchange'
}));
});
}
viewDetail(row: TxnRow): void {
this.detail = {
row,