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
@@ -18,34 +18,44 @@
</header>
<div class="pg-toolbar pg-toolbar--mat">
<mat-form-field appearance="outline" subscriptSizing="dynamic">
<mat-label>Teller (optional)</mat-label>
<mat-select [value]="selectedTellerId" (selectionChange)="onTellerSelect($event.value)">
<mat-option [value]="null">All Tellers</mat-option>
@for (t of tellers; track t.id) {
<mat-option [value]="t.id">{{ t.name }}</mat-option>
}
</mat-select>
</mat-form-field>
<!-- Teller + Cashier filters — hidden for cashier role (auto-loads own position) -->
@if (!isCashierMode) {
<mat-form-field appearance="outline" subscriptSizing="dynamic">
<mat-label>Teller (optional)</mat-label>
<mat-select [value]="selectedTellerId" (selectionChange)="onTellerSelect($event.value)">
<mat-option [value]="null">All Tellers</mat-option>
@for (t of tellers; track t.id) {
<mat-option [value]="t.id">{{ t.name }}</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" subscriptSizing="dynamic">
<mat-label>Cashier (optional)</mat-label>
<mat-select [value]="selectedCashierId" (selectionChange)="selectedCashierId = $event.value">
<mat-option [value]="null">All Cashiers</mat-option>
@for (c of cashiers; track c.id) {
<mat-option [value]="c.id">{{ c.label }}</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" subscriptSizing="dynamic">
<mat-label>Cashier (optional)</mat-label>
<mat-select [value]="selectedCashierId" (selectionChange)="selectedCashierId = $event.value">
<mat-option [value]="null">All Cashiers</mat-option>
@for (c of cashiers; track c.id) {
<mat-option [value]="c.id">{{ c.label }}</mat-option>
}
</mat-select>
</mat-form-field>
}
<mat-form-field appearance="outline" subscriptSizing="dynamic">
<mat-label>As of Date</mat-label>
<input matInput type="date" [value]="asOfDate" (change)="asOfDate = $any($event.target).value" />
</mat-form-field>
<button type="button" class="pg-btn pg-btn--primary" (click)="load()" [disabled]="loading">
<mat-icon>bar_chart</mat-icon> Load Position
</button>
@if (isCashierMode) {
<!-- Cashier mode: just a refresh button -->
<button type="button" class="pg-btn pg-btn--primary" (click)="loadMe()" [disabled]="loading">
<mat-icon>refresh</mat-icon> Refresh
</button>
} @else {
<button type="button" class="pg-btn pg-btn--primary" (click)="load()" [disabled]="loading">
<mat-icon>bar_chart</mat-icon> Load Position
</button>
}
</div>
@if (errorMsg) {
@@ -16,6 +16,7 @@ import { MatInputModule } from '@angular/material/input';
import { forkJoin, of } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { CashierService } from '../cashier.service';
import { AuthenticationService } from '../../core/authentication/authentication.service';
interface DenomRow {
denominationId: number;
@@ -54,6 +55,7 @@ interface CashierPos {
})
export class CashierPositionComponent implements OnInit {
private service = inject(CashierService);
private authService = inject(AuthenticationService);
tellers: { id: number; name: string }[] = [];
cashiers: { id: number; label: string }[] = [];
@@ -65,15 +67,64 @@ export class CashierPositionComponent implements OnInit {
results: CashierPos[] = [];
errorMsg = '';
// True when logged-in user is a regular cashier — hides teller/cashier filters
isCashierMode = false;
ngOnInit(): void {
const d = new Date();
this.asOfDate = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
this.service
.getTellers()
.pipe(catchError(() => of([] as any[])))
.subscribe((list) => {
this.tellers = (list || []).map((t: any) => ({ id: t.id, name: t.name }));
});
this.isCashierMode = this.authService.isCashierUser();
if (this.isCashierMode) {
// Auto-load current cashier's own position immediately
this.loadMe();
} else {
this.service
.getTellers()
.pipe(catchError(() => of([] as any[])))
.subscribe((list) => {
this.tellers = (list || []).map((t: any) => ({ id: t.id, name: t.name }));
});
}
}
/** Load position for the currently authenticated cashier via /me endpoint. */
loadMe(): void {
this.loading = true;
this.results = [];
this.errorMsg = '';
forkJoin({
position: this.service.getCashierPositionForMe(this.asOfDate).pipe(catchError(() => of(null))),
morning: this.service.getCashierMorningBalance(0, this.asOfDate).pipe(catchError(() => of(null)))
}).subscribe(({ position, morning }) => {
this.loading = false;
if (!position || !position.cashierId) {
this.errorMsg = 'No cashier record linked to your account. Contact your administrator.';
return;
}
// Reload morning balance using the resolved cashier ID
this.service
.getCashierMorningBalance(position.cashierId, this.asOfDate)
.pipe(catchError(() => of(null)))
.subscribe((morningData: any) => {
const morningMap = new Map<number, number>();
((morningData?.denominations || []) as any[]).forEach((d: any) =>
morningMap.set(d.denominationId, d.morningCount ?? 0)
);
const morningTotal = Number(morningData?.totalMorning ?? 0);
this.results = [
{
...position,
morningTotal,
denominations: ((position.denominations || []) as any[]).map((d: any) => ({
...d,
morningCount: morningMap.get(d.denominationId) ?? 0
}))
}
];
});
});
}
onTellerSelect(tellerId: number): void {