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
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user