feat(cashier): add end-of-day report, my work, and cashier updates

This commit is contained in:
2026-09-08 10:38:59 +05:30
parent fbce73fd17
commit 035b11fa96
20 changed files with 3146 additions and 124 deletions
@@ -9,9 +9,8 @@
<div class="pg">
<header class="pg-head">
<div>
<div class="pg-eyebrow">CASHIER · REPORT</div>
<h1 class="pg-title">Closing Cash Report</h1>
<p class="pg-sub">Denomination-wise deposit, withdrawal and net cash position for the selected day.</p>
<p class="pg-sub">How much cash you started with, what came in and went out, and how much you must hand back.</p>
</div>
<div class="pg-toolbar">
<label class="pg-field">
@@ -27,79 +26,71 @@
</div>
</header>
<!-- Shown whenever the day has any cash at all, not only when a denomination breakdown exists:
a cash Demand Draft is recorded without one, and gating on rows hid its money entirely. -->
@if (!showGl && (rows.length > 0 || ddCashTotal > 0)) {
<!-- Summary totals bar -->
<div class="closing-summary">
<!-- The day's own cash, leg by leg, ending in what should physically be in the drawer.
A cashier carries no balance: they draw from the vault in the morning and hand back at close,
so this is the figure a physical count is checked against.
This used to sit under a second strip reading Deposits − Withdrawals = Net Counter Movement.
That strip stated the same two figures again and then a net the cashier has no use for: it
excludes the vault legs, so on a day the vault gave ₹600 and customer business cancelled out
it read ₹0 while the drawer held ₹600. Two panels, four repeated figures, and the one that
came first was the one to ignore. -->
@if (!showGl && scopedToCashier && hasTodayLegs) {
<!-- Says what the strip is before the reader meets five figures and a running sum. Somebody
opening this screen for the first time has no way to know that the last tile is the one
that matters. -->
<p class="closing-lede">Your cash for the day, step by step</p>
<div class="closing-summary closing-summary--balance">
<div class="closing-summary__item closing-summary__item--in">
<span class="closing-summary__label">Total Deposits (IN)</span>
<span class="closing-summary__label">Cash you got this morning</span>
<span class="closing-summary__value">₹{{ todayVaultIn | number: '1.2-2' }}</span>
</div>
<div class="closing-summary__sep">+</div>
<div class="closing-summary__item closing-summary__item--in">
<span class="closing-summary__label">Cash customers gave you</span>
<span class="closing-summary__value">₹{{ depositTotalWithDd | number: '1.2-2' }}</span>
@if (ddCashTotal > 0) {
<!-- Named on the tile as well as in the table: this part of the total carries no
denomination, so without saying so the figures read as failing to add up. -->
<span class="closing-summary__note">
incl. ₹{{ ddCashTotal | number: '1.2-2' }} not itemised ({{ ddCashCount }} cash DD{{
₹{{ ddCashTotal | number: '1.2-2' }} of this was for {{ ddCashCount }} draft{{
ddCashCount === 1 ? '' : 's'
}})
}}, notes not listed
</span>
}
</div>
<div class="closing-summary__sep">−</div>
<div class="closing-summary__item closing-summary__item--out">
<span class="closing-summary__label">Total Withdrawals (OUT)</span>
<span class="closing-summary__label">Cash you gave customers</span>
<span class="closing-summary__value">₹{{ withdrawalTotal | number: '1.2-2' }}</span>
</div>
<div class="closing-summary__sep">=</div>
<div
class="closing-summary__item"
[class.closing-summary__item--net-pos]="netTotal >= 0"
[class.closing-summary__item--net-neg]="netTotal < 0"
>
<!-- Named for what it is: the counter's own movement. It was labelled "Net Closing Cash",
which it is not -- it carries nothing forward from yesterday and leaves out vault and
inter-cashier legs, so it can differ from the drawer by a large amount. The real
closing figure is in the balance strip below. -->
<span class="closing-summary__label">Net Counter Movement</span>
<span class="closing-summary__value">₹{{ netTotal | number: '1.2-2' }}</span>
</div>
</div>
}
<!-- Opening → closing, the figure a physical count is checked against. Shown only for a
cashier-scoped report; a branch-wide one has no single drawer to open and close. -->
@if (!showGl && scopedToCashier && closingBalance !== null) {
<div class="closing-summary closing-summary--balance">
<div class="closing-summary__item">
<span class="closing-summary__label">Opening Balance</span>
<span class="closing-summary__value">₹{{ openingBalance | number: '1.2-2' }}</span>
</div>
<div class="closing-summary__sep">+</div>
<div
class="closing-summary__item"
[class.closing-summary__item--in]="(cashMovement ?? 0) >= 0"
[class.closing-summary__item--out]="(cashMovement ?? 0) < 0"
>
<span class="closing-summary__label">Cash Movement</span>
<span class="closing-summary__value">₹{{ cashMovement | number: '1.2-2' }}</span>
<div class="closing-summary__sep">−</div>
<div class="closing-summary__item closing-summary__item--out">
<span class="closing-summary__label">Cash already returned</span>
<span class="closing-summary__value">₹{{ todaySettledOut | number: '1.2-2' }}</span>
</div>
<div class="closing-summary__sep">=</div>
<!-- The figure the day ends on. Named for what the cashier does with it — hand this back to
the vault — rather than "closing balance", which reads as something they keep. -->
<div
class="closing-summary__item"
[class.closing-summary__item--net-pos]="(closingBalance ?? 0) >= 0"
[class.closing-summary__item--net-neg]="(closingBalance ?? 0) < 0"
[class.closing-summary__item--net-pos]="expectedInDrawer >= 0"
[class.closing-summary__item--net-neg]="expectedInDrawer < 0"
>
<span class="closing-summary__label">Closing Balance</span>
<span class="closing-summary__value">₹{{ closingBalance | number: '1.2-2' }}</span>
<span class="closing-summary__label">Cash left with you now</span>
<span class="closing-summary__value">₹{{ expectedInDrawer | number: '1.2-2' }}</span>
</div>
</div>
@if (cashMovement !== null && netTotal !== cashMovement) {
<!-- The gap between the two is not an error: it is the vault, inter-cashier and exchange
traffic that never crossed the counter. Said plainly, because two different "net"
figures on one screen otherwise read as a bug. -->
<p class="closing-note">
Count your cash now &mdash; it should come to ₹{{ expectedInDrawer | number: '1.2-2' }}. Hand that amount back and
your day is done.
</p>
<!-- Cashier-to-cashier hand-overs are left out of the row above to keep it to the four legs a
teller sees, and stated here when they happened rather than silently folded in. -->
@if ((todayInterCashierIn ?? 0) > 0 || (todayInterCashierOut ?? 0) > 0) {
<p class="closing-note">
Cash movement differs from counter movement by ₹{{ cashMovement - netTotal | number: '1.2-2' }} — vault,
inter-cashier and exchange legs that did not pass over the counter.
This already counts cash swapped with another cashier &mdash; ₹{{ todayInterCashierIn | number: '1.2-2' }} taken
from them, ₹{{ todayInterCashierOut | number: '1.2-2' }} given to them.
</p>
}
}
@@ -111,13 +102,16 @@
<table class="pg-table">
<thead>
<tr>
<th>Denomination</th>
<th class="num col-in">Deposit Pcs</th>
<th class="num col-in">Deposit Amt</th>
<th class="num col-out">Withdrawal Pcs</th>
<th class="num col-out">Withdrawal Amt</th>
<th class="num col-net">Net Pcs</th>
<th class="num col-net">Net Amount</th>
<th>Note</th>
<!-- Counted in pieces, because that is what a teller does with notes. Nine columns of
pieces-and-amount for every leg was a reconciliation nobody could read at a
counter; these five are the sentence the day actually makes: this many came in
from the vault, this many over the counter, this many went out, this many are
left, and that is worth this much. -->
<th class="num col-in">Came in</th>
<th class="num col-out">Went out</th>
<th class="num col-net">Left with you</th>
<th class="num col-net">Worth</th>
</tr>
</thead>
<tbody>
@@ -126,15 +120,16 @@
<td>
<span class="pg-chip">₹{{ r.denominationValue }}</span>
</td>
<td class="num col-in">{{ r.depositCount }}</td>
<td class="num col-in">₹{{ r.depositAmount | number: '1.2-2' }}</td>
<td class="num col-out">{{ r.withdrawalCount }}</td>
<td class="num col-out">₹{{ r.withdrawalAmount | number: '1.2-2' }}</td>
<!-- Vault and counter added together: to a teller both are notes arriving, and
splitting them made the reader do the sum. A dash rather than a zero where
nothing happened, so the eye passes over it. -->
<td class="num col-in">{{ r.vaultInCount + r.depositCount || '—' }}</td>
<td class="num col-out">{{ r.withdrawalCount + r.settledOutCount || '—' }}</td>
<td
class="num col-net"
[style.color]="r.netCount > 0 ? '#16a34a' : r.netCount < 0 ? '#dc2626' : '#94a3b8'"
>
{{ r.netCount > 0 ? '+' : '' }}{{ r.netCount }}
{{ r.netCount }}
</td>
<td
class="num col-net"
@@ -153,15 +148,13 @@
@if (ddCashTotal > 0) {
<tr>
<td>
<span class="pg-chip">Not itemised</span>
<span class="pg-chip">Notes not listed</span>
<span class="closing-row__hint"> {{ ddCashCount }} cash DD{{ ddCashCount === 1 ? '' : 's' }} </span>
</td>
<td class="num col-in">—</td>
<td class="num col-in">₹{{ ddCashTotal | number: '1.2-2' }}</td>
<td class="num col-out">—</td>
<td class="num col-out">₹0.00</td>
<td class="num col-net">—</td>
<td class="num col-net" style="color: #16a34a">+₹{{ ddCashTotal | number: '1.2-2' }}</td>
<td class="num col-net" style="color: #16a34a">₹{{ ddCashTotal | number: '1.2-2' }}</td>
</tr>
}
</tbody>
@@ -170,13 +163,11 @@
<tfoot>
<tr>
<td>Total</td>
<td class="num col-in">{{ noteDepositCount }}</td>
<td class="num col-in">₹{{ noteDepositAmount | number: '1.2-2' }}</td>
<td class="num col-out">{{ noteWithdrawalCount }}</td>
<td class="num col-out">₹{{ noteWithdrawalAmount | number: '1.2-2' }}</td>
<td class="num col-net"></td>
<td class="num col-in">{{ noteVaultInCount + noteDepositCount || '—' }}</td>
<td class="num col-out">{{ noteWithdrawalCount + noteSettledOutCount || '—' }}</td>
<td class="num col-net">{{ noteNetCount }}</td>
<td class="num col-net">
{{ noteNetAmount >= 0 ? '+' : '−' }}₹{{
{{ noteNetAmount >= 0 ? '' : '−' }}₹{{
(noteNetAmount < 0 ? -noteNetAmount : noteNetAmount) | number: '1.2-2'
}}
</td>
@@ -190,13 +181,11 @@
<table class="pg-table">
<thead>
<tr>
<th>Denomination</th>
<th class="num col-in">Deposit Pcs</th>
<th class="num col-in">Deposit Amt</th>
<th class="num col-out">Withdrawal Pcs</th>
<th class="num col-out">Withdrawal Amt</th>
<th class="num col-net">Net Pcs</th>
<th class="num col-net">Net Amount</th>
<th>Coin</th>
<th class="num col-in">Came in</th>
<th class="num col-out">Went out</th>
<th class="num col-net">Left with you</th>
<th class="num col-net">Worth</th>
</tr>
</thead>
<tbody>
@@ -205,15 +194,16 @@
<td>
<span class="pg-chip pg-chip--coin">₹{{ r.denominationValue }}</span>
</td>
<td class="num col-in">{{ r.depositCount }}</td>
<td class="num col-in">₹{{ r.depositAmount | number: '1.2-2' }}</td>
<td class="num col-out">{{ r.withdrawalCount }}</td>
<td class="num col-out">₹{{ r.withdrawalAmount | number: '1.2-2' }}</td>
<!-- Vault and counter added together: to a teller both are notes arriving, and
splitting them made the reader do the sum. A dash rather than a zero where
nothing happened, so the eye passes over it. -->
<td class="num col-in">{{ r.vaultInCount + r.depositCount || '—' }}</td>
<td class="num col-out">{{ r.withdrawalCount + r.settledOutCount || '—' }}</td>
<td
class="num col-net"
[style.color]="r.netCount > 0 ? '#16a34a' : r.netCount < 0 ? '#dc2626' : '#94a3b8'"
>
{{ r.netCount > 0 ? '+' : '' }}{{ r.netCount }}
{{ r.netCount }}
</td>
<td
class="num col-net"
@@ -236,9 +226,9 @@
whatever they held yesterday, and that balance is shown in the strip above. Saying
only "no data" here read as though the drawer were empty. -->
<div class="pg-empty">
@if (scopedToCashier && closingBalance !== null) {
No cash crossed the counter on this date. The drawer still holds ₹{{ closingBalance | number: '1.2-2' }},
carried forward.
@if (scopedToCashier && hasTodayLegs && expectedInDrawer !== 0) {
No cash crossed the counter on this date. The drawer should hold ₹{{ expectedInDrawer | number: '1.2-2' }}
from the day's vault and settlement legs.
} @else {
No cash denomination data for this date.
}
@@ -65,6 +65,19 @@ export class ClosingCashReportComponent implements OnInit {
*/
openingBalance: number | null = null;
closingBalance: number | null = null;
/**
* The day's own cash legs, which is what a cashier reconciles against the drawer.
*
* A cashier carries no balance: they draw from the vault in the morning and hand back at close.
* The cumulative opening/closing figures answer a question nobody at the counter asks, and one
* carrying years of history answers it with a number that cannot be counted against anything on
* the desk.
*/
todayVaultIn: number | null = null;
todaySettledOut: number | null = null;
todayInterCashierIn: number | null = null;
todayInterCashierOut: number | null = null;
/** Every leg of the day's cash movement, which is wider than deposits less withdrawals. */
cashMovement: number | null = null;
/** Whether the figures above are one cashier's drawer or the whole branch's counter work. */
@@ -95,6 +108,29 @@ export class ClosingCashReportComponent implements OnInit {
selectedTellerId: number | null = null;
selectedCashierId: number | null = null;
/**
* What should be in the drawer at the end of the day, from the day's own legs.
*
* Drawn from the vault, plus anything taken over the counter and from another cashier, less what
* went back out. This is the figure a cashier counts against — it does not carry a balance
* forward, because a cashier does not.
*/
get expectedInDrawer(): number {
return (
Number(this.todayVaultIn ?? 0) +
Number(this.todayInterCashierIn ?? 0) +
Number(this.depositTotalWithDd ?? 0) -
Number(this.withdrawalTotal ?? 0) -
Number(this.todaySettledOut ?? 0) -
Number(this.todayInterCashierOut ?? 0)
);
}
/** True when any of the day's own legs carries a figure, so the panel is worth showing. */
get hasTodayLegs(): boolean {
return this.todayVaultIn !== null;
}
get noteRows(): any[] {
return this.rows.filter((r) => r.kind !== 'coin');
}
@@ -122,8 +158,35 @@ export class ClosingCashReportComponent implements OnInit {
return this.noteRows.reduce((s, r) => s + Number(r.withdrawalAmount ?? 0), 0);
}
get noteVaultInCount(): number {
return this.noteRows.reduce((s, r) => s + Number(r.vaultInCount ?? 0), 0);
}
get noteVaultInAmount(): number {
return this.noteRows.reduce((s, r) => s + Number(r.vaultInAmount ?? 0), 0);
}
get noteSettledOutCount(): number {
return this.noteRows.reduce((s, r) => s + Number(r.settledOutCount ?? 0), 0);
}
get noteSettledOutAmount(): number {
return this.noteRows.reduce((s, r) => s + Number(r.settledOutAmount ?? 0), 0);
}
get noteNetCount(): number {
return this.noteVaultInCount + this.noteDepositCount - this.noteWithdrawalCount - this.noteSettledOutCount;
}
/**
* What is left in the drawer in notes: everything that came in, less everything that went out.
*
* Was deposits minus withdrawals, which is the counter's movement rather than the drawer's
* contents — it left out the cash drawn from the vault, so a teller holding a ₹500 note saw a net
* of zero and had nothing to count against.
*/
get noteNetAmount(): number {
return this.noteDepositAmount - this.noteWithdrawalAmount;
return this.noteVaultInAmount + this.noteDepositAmount - this.noteWithdrawalAmount - this.noteSettledOutAmount;
}
get coinRows(): any[] {
return this.rows.filter((r) => r.kind === 'coin');
@@ -160,6 +223,10 @@ export class ClosingCashReportComponent implements OnInit {
this.openingBalance = res?.openingBalance == null ? null : Number(res.openingBalance);
this.closingBalance = res?.closingBalance == null ? null : Number(res.closingBalance);
this.cashMovement = res?.cashMovement == null ? null : Number(res.cashMovement);
this.todayVaultIn = res?.todayVaultIn == null ? null : Number(res.todayVaultIn);
this.todaySettledOut = res?.todaySettledOut == null ? null : Number(res.todaySettledOut);
this.todayInterCashierIn = res?.todayInterCashierIn == null ? null : Number(res.todayInterCashierIn);
this.todayInterCashierOut = res?.todayInterCashierOut == null ? null : Number(res.todayInterCashierOut);
this.loading = false;
this.loaded = true;
},
@@ -176,6 +243,10 @@ export class ClosingCashReportComponent implements OnInit {
this.openingBalance = null;
this.closingBalance = null;
this.cashMovement = null;
this.todayVaultIn = null;
this.todaySettledOut = null;
this.todayInterCashierIn = null;
this.todayInterCashierOut = null;
this.loading = false;
this.loaded = true;
}