feat(cashier,organization): denomination-aware vault UI, Bank/Branch Profile screens, popup disbursement review
Branch Day open/close now captures a denomination-wise count (carried forward automatically to the next day), and shows an auto-fetched EOD summary at close time -- vault + every cashier's position, a branch-total reconciliation against the book figure, and each cashier's reconciliation/settlement status and transaction count -- so closing the day no longer means separately checking Vault Position and every cashier's own screen by hand. New Vault Position screen (branch-ops menu) mirrors Cashier Position for the teller/vault. Cash Transfer now shows the vault's live per-denomination availability while counting a handout, with a client-side warning before it can be over-counted. New Organization screens for Bank Profile, Branch Profile and Clearing GL Configuration, wired into the Organization landing page and the previously-dead "Bank Profile" admin menu button. Cashier Transaction Entry: TYPE is now Receipt/Payment only (cash-only TXN TP.) on the cashier side, with no change to the clerk dashboard's own Withdrawal Slip/Cheque/Disbursement/Enter Bank Transfer/DD flow beyond adding a new Fund Transfer type alongside Disbursement for a GL credit that isn't a loan payout. Neither TYPE nor TXN TP. offers a blank placeholder option anymore -- a new row starts on a real default instead. Footer no longer polls the business-date config for cashier/clerk roles, who never have permission to read it. Disbursement & Transfer Requests: "Review" now opens the review screen as a popup instead of navigating to a separate route, refreshing the list on close; the review screen recognises the new Fund Transfer request type too, and its non-functional "+ Disbursement Review" tab-styled label is now a plain heading. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -125,13 +125,102 @@
|
||||
</label>
|
||||
<label class="pg-field">
|
||||
<span>Opening cash (leave blank to carry forward)</span>
|
||||
<input type="number" step="0.01" [(ngModel)]="openingCash" placeholder="Carried from last close" />
|
||||
<input
|
||||
type="number"
|
||||
step="0.01"
|
||||
[(ngModel)]="openingCash"
|
||||
[disabled]="openDenomTotal > 0"
|
||||
placeholder="Carried from last close"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<p class="bd-hint">
|
||||
Left blank, yesterday's closing figure carries forward automatically — which is what keeps the opening balance
|
||||
from drifting.
|
||||
Left blank with no notes/coins counted below, yesterday's closing figure carries forward automatically — which
|
||||
is what keeps the opening balance from drifting. Counting the drawer below is what actually records the
|
||||
note/coin stock the branch can check a vault handout against later.
|
||||
</p>
|
||||
|
||||
<div class="pg-card bd-denom">
|
||||
<table class="pg-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Denomination</th>
|
||||
<th style="text-align: center">Count</th>
|
||||
<th class="num">Amount</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@if (notes.length) {
|
||||
<tr class="pg-section">
|
||||
<td colspan="3">Bank Notes</td>
|
||||
</tr>
|
||||
}
|
||||
@for (d of notes; track d.id) {
|
||||
<tr>
|
||||
<td>
|
||||
<span class="pg-chip">{{ d.label }}</span>
|
||||
</td>
|
||||
<td style="display: flex; justify-content: center">
|
||||
<div class="pg-step">
|
||||
<button
|
||||
type="button"
|
||||
(click)="bumpCount(openCounts, d, -1)"
|
||||
[disabled]="countFor(openCounts, d) === 0"
|
||||
>
|
||||
−
|
||||
</button>
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
[value]="countFor(openCounts, d)"
|
||||
(input)="onCountChange(openCounts, d, $any($event.target).value)"
|
||||
/>
|
||||
<button type="button" (click)="bumpCount(openCounts, d, 1)">+</button>
|
||||
</div>
|
||||
</td>
|
||||
<td class="num">₹{{ subtotalFor(openCounts, d) | number: '1.2-2' }}</td>
|
||||
</tr>
|
||||
}
|
||||
@if (coins.length) {
|
||||
<tr class="pg-section">
|
||||
<td colspan="3">Coins</td>
|
||||
</tr>
|
||||
}
|
||||
@for (d of coins; track d.id) {
|
||||
<tr>
|
||||
<td>
|
||||
<span class="pg-chip pg-chip--coin">{{ d.label }}</span>
|
||||
</td>
|
||||
<td style="display: flex; justify-content: center">
|
||||
<div class="pg-step">
|
||||
<button
|
||||
type="button"
|
||||
(click)="bumpCount(openCounts, d, -1)"
|
||||
[disabled]="countFor(openCounts, d) === 0"
|
||||
>
|
||||
−
|
||||
</button>
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
[value]="countFor(openCounts, d)"
|
||||
(input)="onCountChange(openCounts, d, $any($event.target).value)"
|
||||
/>
|
||||
<button type="button" (click)="bumpCount(openCounts, d, 1)">+</button>
|
||||
</div>
|
||||
</td>
|
||||
<td class="num">₹{{ subtotalFor(openCounts, d) | number: '1.2-2' }}</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@if (openDenomTotal > 0) {
|
||||
<p class="bd-hint">
|
||||
Denomination total: <strong>₹{{ openDenomTotal | number: '1.2-2' }}</strong>
|
||||
</p>
|
||||
}
|
||||
|
||||
<div class="db-panel__actions">
|
||||
<button type="button" class="pg-btn pg-btn--primary" (click)="submitOpen()" [disabled]="opening || !openDate">
|
||||
Open Day
|
||||
@@ -174,29 +263,133 @@
|
||||
<span>+ Cash deposits taken (approved)</span>
|
||||
<span class="bd-calc__amt">₹{{ preview.deposits | number: '1.2-2' }}</span>
|
||||
</div>
|
||||
<div class="bd-calc__row bd-calc__row--in">
|
||||
<span>+ Cash drawn from vault</span>
|
||||
<span class="bd-calc__amt">₹{{ preview.vaultIn | number: '1.2-2' }}</span>
|
||||
</div>
|
||||
<div class="bd-calc__row bd-calc__row--out">
|
||||
<span>− Cash paid out (approved withdrawals)</span>
|
||||
<span class="bd-calc__amt">₹{{ preview.withdrawals | number: '1.2-2' }}</span>
|
||||
</div>
|
||||
<div class="bd-calc__row bd-calc__row--out">
|
||||
<span>− Cash settled back to vault</span>
|
||||
<span class="bd-calc__amt">₹{{ preview.settledOut | number: '1.2-2' }}</span>
|
||||
</div>
|
||||
<div class="bd-calc__row bd-calc__row--total">
|
||||
<span>Cash the branch should be holding</span>
|
||||
<span class="bd-calc__amt">₹{{ preview.expectedCash | number: '1.2-2' }}</span>
|
||||
</div>
|
||||
@if (preview.vaultIn > 0 || preview.settledOut > 0) {
|
||||
<div class="bd-calc__memo">
|
||||
Vault activity today — moves cash within the branch, not counted in the total above: ₹{{
|
||||
preview.vaultIn | number: '1.2-2'
|
||||
}}
|
||||
handed to a cashier, ₹{{ preview.settledOut | number: '1.2-2' }} settled back to the vault.
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- What every cash point under this branch is actually holding right now, fetched
|
||||
automatically — so closing the day doesn't mean separately checking Vault Position and
|
||||
every cashier's own Cashier Position screen by hand. -->
|
||||
@if (eodLoading) {
|
||||
<p class="db-muted">Fetching vault and cashier positions…</p>
|
||||
}
|
||||
|
||||
<!-- The question that actually matters at close: does the WHOLE branch tally, not just the
|
||||
vault. Only shown once both the book preview and every teller/cashier position have
|
||||
loaded, since it adds them together. -->
|
||||
@if (branchTotalSummary; as bt) {
|
||||
<div class="bd-eod bd-eod--total">
|
||||
<div class="bd-eod__head">
|
||||
<mat-icon>account_balance_wallet</mat-icon>
|
||||
<strong>Branch Total (Vault + All Cashiers)</strong>
|
||||
</div>
|
||||
<div class="bd-eod__totals">
|
||||
<span>Vault ₹{{ bt.vaultTotal | number: '1.2-2' }}</span>
|
||||
<span>+ Cashiers ₹{{ bt.cashierTotal | number: '1.2-2' }}</span>
|
||||
<span class="bd-eod__totals-sum">= ₹{{ bt.grandTotal | number: '1.2-2' }}</span>
|
||||
<span class="db-muted">vs. book ₹{{ bt.expected | number: '1.2-2' }}</span>
|
||||
</div>
|
||||
@if (bt.difference === 0) {
|
||||
<span class="db-badge db-badge--ok">Branch Tallied</span>
|
||||
} @else {
|
||||
<span class="db-badge db-badge--rejected">
|
||||
{{ bt.difference > 0 ? 'Excess' : 'Short' }} ₹{{ absValue(bt.difference) | number: '1.2-2' }}
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@for (t of eodTellers; track t.tellerId) {
|
||||
<div class="bd-eod">
|
||||
<div class="bd-eod__head">
|
||||
<mat-icon>account_balance</mat-icon>
|
||||
<strong>{{ t.tellerName }}</strong>
|
||||
<span class="db-muted">
|
||||
vault holds
|
||||
{{ t.vault?.hasBreakdown ? '₹' + (t.vault.totalAmount | number: '1.2-2') : 'no breakdown recorded' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@if (t.vault?.hasBreakdown && t.vault.denominations.length > 0) {
|
||||
<div class="bd-eod__denoms">
|
||||
@for (d of t.vault.denominations; track d.denominationId) {
|
||||
<span class="bd-eod__chip">₹{{ d.denominationValue }} × {{ d.count }}</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (t.cashiers.length === 0) {
|
||||
<p class="db-muted bd-eod__empty">No cashiers allocated under this vault.</p>
|
||||
}
|
||||
@for (c of t.cashiers; track c.cashierId) {
|
||||
<div class="bd-eod__cashier">
|
||||
<div class="bd-eod__cashier-head">
|
||||
<span>{{ c.cashierName }}</span>
|
||||
<span class="db-muted">
|
||||
morning ₹{{ c.morningTotal | number: '1.2-2' }} → current ₹{{ c.currentTotal | number: '1.2-2' }} ·
|
||||
{{ c.txnCount ?? '—' }} txn{{ c.txnCount === 1 ? '' : 's' }} today
|
||||
</span>
|
||||
</div>
|
||||
<div class="bd-eod__status">
|
||||
@if (c.reconciled === true) {
|
||||
<span class="db-badge db-badge--ok">Reconciled</span>
|
||||
} @else if (c.reconciled === false) {
|
||||
<span class="db-badge db-badge--pending">Not Reconciled</span>
|
||||
}
|
||||
@if (c.alreadySettled === true) {
|
||||
<span class="db-badge db-badge--ok">Settled to Vault</span>
|
||||
} @else if (c.alreadySettled === false) {
|
||||
<span class="db-badge">Not Settled</span>
|
||||
}
|
||||
</div>
|
||||
@if (c.denominations.length > 0) {
|
||||
<div class="bd-eod__denoms">
|
||||
@for (d of c.denominations; track d.denominationId) {
|
||||
<span
|
||||
class="bd-eod__chip"
|
||||
[class.bd-eod__chip--up]="d.delta > 0"
|
||||
[class.bd-eod__chip--down]="d.delta < 0"
|
||||
>
|
||||
₹{{ d.denominationValue }}: {{ d.morningCount }} → {{ d.count }}
|
||||
@if (d.delta !== 0) {
|
||||
({{ d.delta > 0 ? '+' : '' }}{{ d.delta }})
|
||||
}
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
} @else {
|
||||
<span class="db-muted bd-eod__empty">No balance.</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="db-panel__body">
|
||||
<label class="pg-field">
|
||||
<span>Physical cash counted</span>
|
||||
<input type="number" step="0.01" [(ngModel)]="closingCash" placeholder="0.00" />
|
||||
<span>Physical cash counted (or count denominations below)</span>
|
||||
<input
|
||||
type="number"
|
||||
step="0.01"
|
||||
[(ngModel)]="closingCash"
|
||||
[disabled]="closeDenomTotal > 0"
|
||||
placeholder="0.00"
|
||||
/>
|
||||
</label>
|
||||
@if (preview && liveDifference !== null) {
|
||||
@if (liveDifference === 0) {
|
||||
@@ -222,9 +415,91 @@
|
||||
|
||||
<p class="bd-hint">
|
||||
Pre-filled with what the books expect — change it only if the drawer counted something different, and that
|
||||
difference is then recorded against the day.
|
||||
difference is then recorded against the day. Counting the denominations below overrides the typed figure with
|
||||
what was actually counted, note by note.
|
||||
</p>
|
||||
|
||||
<div class="pg-card bd-denom">
|
||||
<table class="pg-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Denomination</th>
|
||||
<th style="text-align: center">Count</th>
|
||||
<th class="num">Amount</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@if (notes.length) {
|
||||
<tr class="pg-section">
|
||||
<td colspan="3">Bank Notes</td>
|
||||
</tr>
|
||||
}
|
||||
@for (d of notes; track d.id) {
|
||||
<tr>
|
||||
<td>
|
||||
<span class="pg-chip">{{ d.label }}</span>
|
||||
</td>
|
||||
<td style="display: flex; justify-content: center">
|
||||
<div class="pg-step">
|
||||
<button
|
||||
type="button"
|
||||
(click)="bumpCount(closeCounts, d, -1)"
|
||||
[disabled]="countFor(closeCounts, d) === 0"
|
||||
>
|
||||
−
|
||||
</button>
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
[value]="countFor(closeCounts, d)"
|
||||
(input)="onCountChange(closeCounts, d, $any($event.target).value)"
|
||||
/>
|
||||
<button type="button" (click)="bumpCount(closeCounts, d, 1)">+</button>
|
||||
</div>
|
||||
</td>
|
||||
<td class="num">₹{{ subtotalFor(closeCounts, d) | number: '1.2-2' }}</td>
|
||||
</tr>
|
||||
}
|
||||
@if (coins.length) {
|
||||
<tr class="pg-section">
|
||||
<td colspan="3">Coins</td>
|
||||
</tr>
|
||||
}
|
||||
@for (d of coins; track d.id) {
|
||||
<tr>
|
||||
<td>
|
||||
<span class="pg-chip pg-chip--coin">{{ d.label }}</span>
|
||||
</td>
|
||||
<td style="display: flex; justify-content: center">
|
||||
<div class="pg-step">
|
||||
<button
|
||||
type="button"
|
||||
(click)="bumpCount(closeCounts, d, -1)"
|
||||
[disabled]="countFor(closeCounts, d) === 0"
|
||||
>
|
||||
−
|
||||
</button>
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
[value]="countFor(closeCounts, d)"
|
||||
(input)="onCountChange(closeCounts, d, $any($event.target).value)"
|
||||
/>
|
||||
<button type="button" (click)="bumpCount(closeCounts, d, 1)">+</button>
|
||||
</div>
|
||||
</td>
|
||||
<td class="num">₹{{ subtotalFor(closeCounts, d) | number: '1.2-2' }}</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@if (closeDenomTotal > 0) {
|
||||
<p class="bd-hint">
|
||||
Denomination total: <strong>₹{{ closeDenomTotal | number: '1.2-2' }}</strong>
|
||||
</p>
|
||||
}
|
||||
|
||||
@if (isLateClose) {
|
||||
<label class="pg-field bd-panel__wide">
|
||||
<span>Why was this day left open? (required)</span>
|
||||
|
||||
@@ -157,6 +157,14 @@
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__memo {
|
||||
margin-top: 8px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px dashed #e2e8f0;
|
||||
font-size: 11px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
}
|
||||
|
||||
.bd-hint {
|
||||
@@ -170,3 +178,105 @@
|
||||
font-size: 11px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* The denomination grid inside the open/close panels — same table chrome as pg-card elsewhere,
|
||||
just tightened to sit inside the panel instead of spanning the page. */
|
||||
.bd-denom {
|
||||
margin: 4px 0 0;
|
||||
max-width: 520px;
|
||||
}
|
||||
|
||||
/* Auto-fetched EOD summary: what the vault and every cashier under it currently hold. */
|
||||
.bd-eod {
|
||||
margin: 0 0 12px;
|
||||
padding: 10px 14px;
|
||||
background: #fff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
|
||||
&__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 6px;
|
||||
|
||||
mat-icon {
|
||||
font-size: 16px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: #0ea5e9;
|
||||
}
|
||||
}
|
||||
|
||||
&__denoms {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin: 4px 0 8px;
|
||||
}
|
||||
|
||||
&__chip {
|
||||
padding: 2px 8px;
|
||||
background: #f1f5f9;
|
||||
border-radius: 999px;
|
||||
font-size: 11px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
color: #334155;
|
||||
|
||||
&--up {
|
||||
background: #dcfce7;
|
||||
color: #15803d;
|
||||
}
|
||||
|
||||
&--down {
|
||||
background: #fee2e2;
|
||||
color: #b91c1c;
|
||||
}
|
||||
}
|
||||
|
||||
&__cashier {
|
||||
padding: 6px 0;
|
||||
border-top: 1px dashed #e2e8f0;
|
||||
|
||||
&-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
font-weight: 600;
|
||||
color: #0d1b2a;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
&__empty {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
&__status {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
margin: 2px 0 6px;
|
||||
}
|
||||
|
||||
&--total {
|
||||
border-left: 4px solid #0ea5e9;
|
||||
background: #f0f9ff;
|
||||
}
|
||||
|
||||
&__totals {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: baseline;
|
||||
gap: 10px;
|
||||
margin-bottom: 6px;
|
||||
font-size: 13px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
|
||||
&-sum {
|
||||
font-weight: 800;
|
||||
font-size: 15px;
|
||||
color: #0d1b2a;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,9 +11,16 @@ import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { catchError, of } from 'rxjs';
|
||||
import { catchError, forkJoin, map, of, switchMap } from 'rxjs';
|
||||
import { CashierService } from '../cashier.service';
|
||||
|
||||
interface Denom {
|
||||
id: number;
|
||||
value: number;
|
||||
kind: 'note' | 'coin';
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface BranchDay {
|
||||
id: number;
|
||||
officeId: number;
|
||||
@@ -92,9 +99,75 @@ export class BranchDayComponent implements OnInit {
|
||||
forceReason = '';
|
||||
closing = false;
|
||||
|
||||
// Denomination breakdown, shared by both the open and close forms — a physical note/coin count
|
||||
// is the true source of the lump-sum figure, not a typed-in guess. Counted separately per form
|
||||
// since opening and closing are two independent physical counts.
|
||||
notes: Denom[] = [];
|
||||
coins: Denom[] = [];
|
||||
openCounts: Record<number, number> = {};
|
||||
closeCounts: Record<number, number> = {};
|
||||
|
||||
get allDenoms(): Denom[] {
|
||||
return [
|
||||
...this.notes,
|
||||
...this.coins
|
||||
];
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.missedOnly = !!this.route.snapshot.data['missedOnly'];
|
||||
this.load();
|
||||
this.service.getActiveDenominations().subscribe({
|
||||
next: (list) => {
|
||||
const rows: Denom[] = (list || []).map((x: any) => ({
|
||||
id: x.id,
|
||||
value: x.denomination,
|
||||
kind: x.kind,
|
||||
label: '₹' + x.denomination
|
||||
}));
|
||||
this.notes = rows.filter((r) => r.kind === 'note').sort((a, b) => b.value - a.value);
|
||||
this.coins = rows.filter((r) => r.kind === 'coin').sort((a, b) => b.value - a.value);
|
||||
},
|
||||
error: () => {
|
||||
this.notes = [];
|
||||
this.coins = [];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
countFor(counts: Record<number, number>, d: Denom): number {
|
||||
return counts[d.id] || 0;
|
||||
}
|
||||
|
||||
subtotalFor(counts: Record<number, number>, d: Denom): number {
|
||||
return this.countFor(counts, d) * d.value;
|
||||
}
|
||||
|
||||
onCountChange(counts: Record<number, number>, d: Denom, raw: any): void {
|
||||
counts[d.id] = Math.max(0, Math.floor(Number(raw) || 0));
|
||||
}
|
||||
|
||||
bumpCount(counts: Record<number, number>, d: Denom, delta: number): void {
|
||||
counts[d.id] = Math.max(0, (counts[d.id] || 0) + delta);
|
||||
}
|
||||
|
||||
totalOf(counts: Record<number, number>): number {
|
||||
return this.allDenoms.reduce((s, d) => s + this.subtotalFor(counts, d), 0);
|
||||
}
|
||||
|
||||
/** Denominations with a positive count, shaped for the API payload. */
|
||||
private denominationPayload(counts: Record<number, number>) {
|
||||
return this.allDenoms
|
||||
.filter((d) => this.countFor(counts, d) > 0)
|
||||
.map((d) => ({ denominationId: d.id, denominationValue: d.value, count: this.countFor(counts, d) }));
|
||||
}
|
||||
|
||||
get openDenomTotal(): number {
|
||||
return this.totalOf(this.openCounts);
|
||||
}
|
||||
|
||||
get closeDenomTotal(): number {
|
||||
return this.totalOf(this.closeCounts);
|
||||
}
|
||||
|
||||
load(): void {
|
||||
@@ -138,6 +211,7 @@ export class BranchDayComponent implements OnInit {
|
||||
// Left blank on purpose: the backend carries yesterday's closing forward, and typing a figure
|
||||
// over it is how a carried balance drifts.
|
||||
this.openingCash = null;
|
||||
this.openCounts = {};
|
||||
this.successMsg = '';
|
||||
this.errorMsg = '';
|
||||
}
|
||||
@@ -150,8 +224,9 @@ export class BranchDayComponent implements OnInit {
|
||||
if (!this.openDate) return;
|
||||
this.opening = true;
|
||||
this.errorMsg = '';
|
||||
const denominations = this.denominationPayload(this.openCounts);
|
||||
this.service
|
||||
.openBranchDay(this.openDate, this.openingCash ?? undefined)
|
||||
.openBranchDay(this.openDate, this.openingCash ?? undefined, undefined, denominations)
|
||||
.pipe(
|
||||
catchError((err: any) => {
|
||||
this.errorMsg =
|
||||
@@ -176,6 +251,7 @@ export class BranchDayComponent implements OnInit {
|
||||
startClose(day: BranchDay): void {
|
||||
this.closeTarget = day;
|
||||
this.closingCash = null;
|
||||
this.closeCounts = {};
|
||||
this.closeRemarks = '';
|
||||
this.forceReason = '';
|
||||
this.successMsg = '';
|
||||
@@ -196,12 +272,125 @@ export class BranchDayComponent implements OnInit {
|
||||
this.closingCash = Number(res.expectedCash);
|
||||
}
|
||||
});
|
||||
|
||||
this.loadEodSummary(day);
|
||||
}
|
||||
|
||||
// ── EOD denomination summary: what's in the vault, and what every cashier under it is
|
||||
// currently holding, fetched automatically so closing the day doesn't require separately
|
||||
// checking Vault Position and every cashier's own Cashier Position screen by hand. ──
|
||||
|
||||
eodLoading = false;
|
||||
eodTellers: any[] = [];
|
||||
|
||||
/**
|
||||
* Vault + every cashier's current holding, added up across the whole branch and checked against
|
||||
* the book figure — the question a manager actually needs answered at close: does the BRANCH
|
||||
* tally, not just the vault alone. Computed on read rather than stored, so it never has to be
|
||||
* kept in sync across the two independent loads (the book preview and the EOD summary) that feed
|
||||
* it.
|
||||
*/
|
||||
get branchTotalSummary(): {
|
||||
vaultTotal: number;
|
||||
cashierTotal: number;
|
||||
grandTotal: number;
|
||||
expected: number;
|
||||
difference: number;
|
||||
} | null {
|
||||
if (!this.preview || this.eodTellers.length === 0) return null;
|
||||
let vaultTotal = 0;
|
||||
let cashierTotal = 0;
|
||||
for (const t of this.eodTellers) {
|
||||
vaultTotal += Number(t.vault?.totalAmount ?? 0);
|
||||
for (const c of t.cashiers) {
|
||||
cashierTotal += Number(c.currentTotal ?? 0);
|
||||
}
|
||||
}
|
||||
const grandTotal = vaultTotal + cashierTotal;
|
||||
const expected = Number(this.preview.expectedCash ?? 0);
|
||||
return { vaultTotal, cashierTotal, grandTotal, expected, difference: grandTotal - expected };
|
||||
}
|
||||
|
||||
private loadEodSummary(day: BranchDay): void {
|
||||
this.eodLoading = true;
|
||||
this.eodTellers = [];
|
||||
this.service
|
||||
.getTellers(day.officeId)
|
||||
.pipe(
|
||||
switchMap((tellers) => {
|
||||
const list = tellers || [];
|
||||
if (!list.length) return of([]);
|
||||
return forkJoin(list.map((t: any) => this.loadTellerSummary(t, day.businessDate)));
|
||||
}),
|
||||
catchError(() => of([]))
|
||||
)
|
||||
.subscribe((results: any[]) => {
|
||||
this.eodLoading = false;
|
||||
this.eodTellers = results;
|
||||
});
|
||||
}
|
||||
|
||||
private loadTellerSummary(teller: any, date: string) {
|
||||
return forkJoin({
|
||||
vault: this.service.vaultPosition(teller.id, date).pipe(catchError(() => of(null))),
|
||||
cashiers: this.service.getCashiersForTeller(teller.id).pipe(catchError(() => of({ cashiers: [] })))
|
||||
}).pipe(
|
||||
switchMap(({ vault, cashiers }) => {
|
||||
const cashierList = cashiers?.cashiers || [];
|
||||
if (!cashierList.length) {
|
||||
return of({ tellerId: teller.id, tellerName: teller.name, vault, cashiers: [] });
|
||||
}
|
||||
return forkJoin(cashierList.map((c: any) => this.loadCashierSummary(c, date))).pipe(
|
||||
map((cashierSummaries: any[]) => ({
|
||||
tellerId: teller.id,
|
||||
tellerName: teller.name,
|
||||
vault,
|
||||
cashiers: cashierSummaries
|
||||
}))
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private loadCashierSummary(cashier: any, date: string) {
|
||||
return forkJoin({
|
||||
position: this.service.cashierPositionReport(cashier.id, date).pipe(catchError(() => of(null))),
|
||||
morning: this.service.getCashierMorningBalance(cashier.id, date).pipe(catchError(() => of(null))),
|
||||
reconciliation: this.service.checkReconciliation(cashier.id, date).pipe(catchError(() => of(null))),
|
||||
settlement: this.service.checkSettlement(cashier.id, date).pipe(catchError(() => of(null))),
|
||||
glEntries: this.service.getGlEntries(date, cashier.id, undefined, 0, 1).pipe(catchError(() => of(null)))
|
||||
}).pipe(
|
||||
map(({ position, morning, reconciliation, settlement, glEntries }: any) => {
|
||||
const morningMap = new Map<number, number>();
|
||||
(morning?.denominations || []).forEach((d: any) => morningMap.set(d.denominationId, d.morningCount ?? 0));
|
||||
return {
|
||||
cashierId: cashier.id,
|
||||
cashierName: cashier.staffName,
|
||||
morningTotal: Number(morning?.totalMorning ?? 0),
|
||||
currentTotal: Number(position?.totalAmount ?? 0),
|
||||
reconciled: reconciliation?.reconciled ?? null,
|
||||
alreadySettled: settlement?.alreadySettled ?? null,
|
||||
txnCount: glEntries?.totalFilteredRecords ?? null,
|
||||
denominations: ((position?.denominations || []) as any[]).map((d: any) => ({
|
||||
...d,
|
||||
morningCount: morningMap.get(d.denominationId) ?? 0,
|
||||
delta: d.count - (morningMap.get(d.denominationId) ?? 0)
|
||||
}))
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/** The counted figure the form would submit right now — the denomination total when any note/coin
|
||||
* has been counted, otherwise the plain typed-in amount. */
|
||||
get effectiveClosingFigure(): number | null {
|
||||
return this.closeDenomTotal > 0 ? this.closeDenomTotal : this.closingCash;
|
||||
}
|
||||
|
||||
/** Difference the form would record right now, so it is visible before saving. */
|
||||
get liveDifference(): number | null {
|
||||
if (!this.preview || this.closingCash == null) return null;
|
||||
return Number(this.closingCash) - Number(this.preview.expectedCash ?? 0);
|
||||
if (!this.preview || this.effectiveClosingFigure == null) return null;
|
||||
return Number(this.effectiveClosingFigure) - Number(this.preview.expectedCash ?? 0);
|
||||
}
|
||||
|
||||
/** Puts the expected figure back after an edit. */
|
||||
@@ -213,6 +402,7 @@ export class BranchDayComponent implements OnInit {
|
||||
|
||||
cancelClose(): void {
|
||||
this.closeTarget = null;
|
||||
this.eodTellers = [];
|
||||
}
|
||||
|
||||
/** A date earlier than today is a late close, and has to say why it was left open. */
|
||||
@@ -221,22 +411,27 @@ export class BranchDayComponent implements OnInit {
|
||||
}
|
||||
|
||||
get canSubmitClose(): boolean {
|
||||
if (!this.closeTarget || this.closingCash == null || this.closing) return false;
|
||||
const hasFigure = this.closingCash != null || this.closeDenomTotal > 0;
|
||||
if (!this.closeTarget || !hasFigure || this.closing) return false;
|
||||
return !this.isLateClose || !!this.forceReason.trim();
|
||||
}
|
||||
|
||||
submitClose(): void {
|
||||
const day = this.closeTarget;
|
||||
if (!day || this.closingCash == null) return;
|
||||
const denominations = this.denominationPayload(this.closeCounts);
|
||||
const figure = denominations.length ? this.closeDenomTotal : this.closingCash;
|
||||
if (!day || figure == null) return;
|
||||
|
||||
this.closing = true;
|
||||
this.errorMsg = '';
|
||||
this.service
|
||||
.closeBranchDay(
|
||||
day.businessDate,
|
||||
Number(this.closingCash),
|
||||
Number(figure),
|
||||
this.closeRemarks.trim() || undefined,
|
||||
this.isLateClose ? this.forceReason.trim() : undefined
|
||||
this.isLateClose ? this.forceReason.trim() : undefined,
|
||||
undefined,
|
||||
denominations
|
||||
)
|
||||
.pipe(
|
||||
catchError((err: any) => {
|
||||
@@ -259,6 +454,10 @@ export class BranchDayComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
absValue(n: number): number {
|
||||
return Math.abs(n);
|
||||
}
|
||||
|
||||
/** Short/excess wording, since a bare signed number reads ambiguously on a cash figure. */
|
||||
differenceLabel(d: number | null): string {
|
||||
if (d == null) return '—';
|
||||
|
||||
Reference in New Issue
Block a user