feat(cashier): maker-checker on cash transfer, real branches, compact Approval Center

- Cash Transfer now raises a request ("Send for approval") instead of posting. A My Requests button in the
  page header opens the caller's requests with their status, and a Cash Transfers approvals page (review
  popup with the full denomination breakdown, approve, reject with a reason) lets a different manager release
  the cash.
- Transaction Entry's Branch dropdown lists the installation's real offices instead of a fixed list. A login
  that cannot read offices is offered only its own branch (read-only), so no "Unauthorized Request" popup.
- Approval Center header and tab bar made compact so more tabs fit on a 100% window.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-15 12:27:39 +05:30
co-authored by Claude Sonnet 5.5
parent 7f3c97a205
commit 7ee230e58a
10 changed files with 1317 additions and 55 deletions
@@ -129,11 +129,16 @@
</div>
<div class="ws-f ws-f--branch">
<label class="ws-f__lbl" title="Branch">BRANCH</label>
<select class="ws-f__ctl" [(ngModel)]="row.br" (click)="$event.stopPropagation()">
<option [ngValue]="1">001 · Main</option>
<option [ngValue]="2">002 · M.G. Road</option>
<option [ngValue]="14">014 · Station Rd</option>
<option [ngValue]="27">027 · Industrial</option>
<select
class="ws-f__ctl"
[(ngModel)]="row.br"
[disabled]="branches.length <= 1"
[title]="branches.length <= 1 ? 'Your branch' : 'Branch'"
(click)="$event.stopPropagation()"
>
@for (b of branches; track b.id) {
<option [ngValue]="b.id">{{ b.label }}</option>
}
</select>
</div>
@if (row.typ === 'DD') {
@@ -1584,11 +1589,16 @@
</div>
<div class="ws-f ws-f--branch">
<label class="ws-f__lbl">BRANCH</label>
<select class="ws-f__ctl" [(ngModel)]="row.br" (click)="$event.stopPropagation()">
<option [ngValue]="1">001 · Main</option>
<option [ngValue]="2">002 · M.G. Road</option>
<option [ngValue]="14">014 · Station Rd</option>
<option [ngValue]="27">027 · Industrial</option>
<select
class="ws-f__ctl"
[(ngModel)]="row.br"
[disabled]="branches.length <= 1"
[title]="branches.length <= 1 ? 'Your branch' : 'Branch'"
(click)="$event.stopPropagation()"
>
@for (b of branches; track b.id) {
<option [ngValue]="b.id">{{ b.label }}</option>
}
</select>
</div>
@if (row.typ === 'DD') {
@@ -1877,6 +1877,7 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
ngOnInit(): void {
const d = businessToday();
this.today = `${String(d.getDate()).padStart(2, '0')}/${String(d.getMonth() + 1).padStart(2, '0')}/${d.getFullYear()}`;
this.loadBranches();
this.addRow();
this.loadDenominations();
this.loadOpeningPosition();
@@ -2309,7 +2310,7 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
// anyway, just without the extra click to get there.
const row: TxnRow = {
typ: this.isClerk ? 'Withdrawal Slip' : 'Receipt',
br: 1,
br: this.defaultBranchId,
trTp: '',
acTyp: 'SD',
acNo: '',
@@ -2655,14 +2656,47 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
return !!this.selectedRow && Math.abs(this.denominationTotal - this.ddTargetTotal) < 0.005;
}
/**
* The branches an entry can be posted against: the real offices of this installation. Read from the offices
* themselves rather than from a fixed list, so a branch that is added or renamed shows up here without a code change.
*/
branches: { id: number; label: string }[] = [];
private loadBranches(): void {
const own = this.authService.getCredentials();
const ownBranch = own?.officeId
? [{ id: own.officeId, label: this.branchLabel(own.officeId, own.officeName) }]
: [];
// The office list is only asked for by a login allowed to read it. A cashier or clerk is not, and asking anyway
// makes the server refuse and the screen raise an "Unauthorized Request" popup; such a login works in its own
// office, which is the one branch it is offered.
const permissions = own?.permissions ?? [];
const mayReadOffices = permissions.includes('ALL_FUNCTIONS') || permissions.includes('READ_OFFICE');
if (!mayReadOffices) {
this.branches = ownBranch;
return;
}
this.http
.get('/offices')
.pipe(catchError(() => of([])))
.subscribe((list: any) => {
const offices: any[] = Array.isArray(list) ? list : [];
this.branches =
offices.length > 0 ? offices.map((o) => ({ id: o.id, label: this.branchLabel(o.id, o.name) })) : ownBranch;
});
}
private branchLabel(id: number, name: string | null | undefined): string {
return `${String(id).padStart(3, '0')} · ${name ?? 'Branch ' + id}`;
}
/** The office the signed-in user works in, which a new entry is posted against unless they pick another. */
get defaultBranchId(): number {
return this.authService.getCredentials()?.officeId ?? 1;
}
branchNameFor(branchId: number): string {
const map: Record<number, string> = {
1: '001 · Main',
2: '002 · M.G. Road',
14: '014 · Station Rd',
27: '027 · Industrial'
};
return map[branchId] || `Branch ${branchId}`;
return this.branches.find((b) => b.id === branchId)?.label ?? `Branch ${branchId}`;
}
/**