feat(cashier): carry forward yesterday's denomination count into Day Open, warn on a mismatch
The Day Open form's note/coin table started at zero even though the branch's own closing count from the day before was already on record -- whoever opened the day had to re-key numbers that were already known. It now auto-fills from that closing count, flags any row a manual edit moves away from it, and shows a live Tallied/Short/Excess comparison against it -- so a short or excess opening count is visible before the day is opened, not discovered later. A reason is required once a mismatch exists, matching the new backend enforcement. Also fixes the register table's date chip wrapping onto two lines when the column narrows. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -135,11 +135,32 @@
|
||||
</label>
|
||||
</div>
|
||||
<p class="bd-hint">
|
||||
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.
|
||||
@if (hasAutoFilledOpenCounts) {
|
||||
Auto-filled below from yesterday's own closing count (₹{{ carriedForwardTotal | number: '1.2-2' }}) — correct
|
||||
only the rows where today's physical count differs before opening.
|
||||
} @else {
|
||||
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>
|
||||
|
||||
@if (hasAutoFilledOpenCounts && openLiveDifference !== null) {
|
||||
@if (openLiveDifference === 0) {
|
||||
<div class="db-cmp db-cmp--good">
|
||||
<span class="db-cmp__label">vs. yesterday's close</span>
|
||||
<span class="db-cmp__value">Tallied</span>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="db-cmp db-cmp--bad">
|
||||
<span class="db-cmp__label">vs. yesterday's close</span>
|
||||
<span class="db-cmp__value">
|
||||
{{ openLiveDifference > 0 ? 'Excess' : 'Short' }} ₹{{ absValue(openLiveDifference) | number: '1.2-2' }}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
<div class="pg-card bd-denom">
|
||||
<table class="pg-table">
|
||||
<thead>
|
||||
@@ -160,7 +181,7 @@
|
||||
<td>
|
||||
<span class="pg-chip">{{ d.label }}</span>
|
||||
</td>
|
||||
<td style="display: flex; justify-content: center">
|
||||
<td style="display: flex; flex-direction: column; align-items: center">
|
||||
<div class="pg-step">
|
||||
<button
|
||||
type="button"
|
||||
@@ -177,6 +198,9 @@
|
||||
/>
|
||||
<button type="button" (click)="bumpCount(openCounts, d, 1)">+</button>
|
||||
</div>
|
||||
@if (hasAutoFilledOpenCounts && countFor(openCounts, d) !== autoOpenCountFor(d)) {
|
||||
<span class="db-muted bd-denom__auto">yesterday closed {{ autoOpenCountFor(d) }}</span>
|
||||
}
|
||||
</td>
|
||||
<td class="num">₹{{ subtotalFor(openCounts, d) | number: '1.2-2' }}</td>
|
||||
</tr>
|
||||
@@ -191,7 +215,7 @@
|
||||
<td>
|
||||
<span class="pg-chip pg-chip--coin">{{ d.label }}</span>
|
||||
</td>
|
||||
<td style="display: flex; justify-content: center">
|
||||
<td style="display: flex; flex-direction: column; align-items: center">
|
||||
<div class="pg-step">
|
||||
<button
|
||||
type="button"
|
||||
@@ -208,6 +232,9 @@
|
||||
/>
|
||||
<button type="button" (click)="bumpCount(openCounts, d, 1)">+</button>
|
||||
</div>
|
||||
@if (hasAutoFilledOpenCounts && countFor(openCounts, d) !== autoOpenCountFor(d)) {
|
||||
<span class="db-muted bd-denom__auto">yesterday closed {{ autoOpenCountFor(d) }}</span>
|
||||
}
|
||||
</td>
|
||||
<td class="num">₹{{ subtotalFor(openCounts, d) | number: '1.2-2' }}</td>
|
||||
</tr>
|
||||
@@ -218,11 +245,27 @@
|
||||
@if (openDenomTotal > 0) {
|
||||
<p class="bd-hint">
|
||||
Denomination total: <strong>₹{{ openDenomTotal | number: '1.2-2' }}</strong>
|
||||
@if (hasAutoFilledOpenCounts) {
|
||||
<button type="button" class="pg-btn bd-denom__reset" (click)="resetOpenToAutoFill()">
|
||||
Reset to yesterday's count
|
||||
</button>
|
||||
}
|
||||
</p>
|
||||
}
|
||||
|
||||
@if (openLiveDifference !== null && openLiveDifference !== 0) {
|
||||
<label class="pg-field bd-panel__wide">
|
||||
<span>Why does this differ from yesterday's closing count? (required)</span>
|
||||
<input
|
||||
type="text"
|
||||
[(ngModel)]="openDifferenceReason"
|
||||
placeholder="e.g. notes exchanged for smaller denominations, extra cash added from head office"
|
||||
/>
|
||||
</label>
|
||||
}
|
||||
|
||||
<div class="db-panel__actions">
|
||||
<button type="button" class="pg-btn pg-btn--primary" (click)="submitOpen()" [disabled]="opening || !openDate">
|
||||
<button type="button" class="pg-btn pg-btn--primary" (click)="submitOpen()" [disabled]="!canSubmitOpen">
|
||||
Open Day
|
||||
</button>
|
||||
<button type="button" class="pg-btn" (click)="cancelOpen()">Cancel</button>
|
||||
@@ -414,9 +457,14 @@
|
||||
</div>
|
||||
|
||||
<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. Counting the denominations below overrides the typed figure with
|
||||
what was actually counted, note by note.
|
||||
@if (hasAutoFilledCounts) {
|
||||
Auto-filled below from the vault and every cashier's position shown above — correct only the rows where the
|
||||
physical count differs, note by note.
|
||||
} @else {
|
||||
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. Counting the denominations below overrides the typed figure with
|
||||
what was actually counted, note by note.
|
||||
}
|
||||
</p>
|
||||
|
||||
<div class="pg-card bd-denom">
|
||||
@@ -439,7 +487,7 @@
|
||||
<td>
|
||||
<span class="pg-chip">{{ d.label }}</span>
|
||||
</td>
|
||||
<td style="display: flex; justify-content: center">
|
||||
<td style="display: flex; flex-direction: column; align-items: center">
|
||||
<div class="pg-step">
|
||||
<button
|
||||
type="button"
|
||||
@@ -456,6 +504,9 @@
|
||||
/>
|
||||
<button type="button" (click)="bumpCount(closeCounts, d, 1)">+</button>
|
||||
</div>
|
||||
@if (hasAutoFilledCounts && countFor(closeCounts, d) !== autoCountFor(d)) {
|
||||
<span class="db-muted bd-denom__auto">system counted {{ autoCountFor(d) }}</span>
|
||||
}
|
||||
</td>
|
||||
<td class="num">₹{{ subtotalFor(closeCounts, d) | number: '1.2-2' }}</td>
|
||||
</tr>
|
||||
@@ -470,7 +521,7 @@
|
||||
<td>
|
||||
<span class="pg-chip pg-chip--coin">{{ d.label }}</span>
|
||||
</td>
|
||||
<td style="display: flex; justify-content: center">
|
||||
<td style="display: flex; flex-direction: column; align-items: center">
|
||||
<div class="pg-step">
|
||||
<button
|
||||
type="button"
|
||||
@@ -487,6 +538,9 @@
|
||||
/>
|
||||
<button type="button" (click)="bumpCount(closeCounts, d, 1)">+</button>
|
||||
</div>
|
||||
@if (hasAutoFilledCounts && countFor(closeCounts, d) !== autoCountFor(d)) {
|
||||
<span class="db-muted bd-denom__auto">system counted {{ autoCountFor(d) }}</span>
|
||||
}
|
||||
</td>
|
||||
<td class="num">₹{{ subtotalFor(closeCounts, d) | number: '1.2-2' }}</td>
|
||||
</tr>
|
||||
@@ -497,6 +551,11 @@
|
||||
@if (closeDenomTotal > 0) {
|
||||
<p class="bd-hint">
|
||||
Denomination total: <strong>₹{{ closeDenomTotal | number: '1.2-2' }}</strong>
|
||||
@if (hasAutoFilledCounts) {
|
||||
<button type="button" class="pg-btn bd-denom__reset" (click)="resetToAutoFill()">
|
||||
Reset to auto-filled count
|
||||
</button>
|
||||
}
|
||||
</p>
|
||||
}
|
||||
|
||||
|
||||
@@ -184,6 +184,21 @@
|
||||
.bd-denom {
|
||||
margin: 4px 0 0;
|
||||
max-width: 520px;
|
||||
|
||||
/* Flags a row where the hand-count no longer matches what the vault/cashier positions reported. */
|
||||
&__auto {
|
||||
display: block;
|
||||
margin-top: 2px;
|
||||
font-size: 10px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__reset {
|
||||
margin-left: 10px;
|
||||
height: 24px;
|
||||
padding: 0 10px;
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Auto-fetched EOD summary: what the vault and every cashier under it currently hold. */
|
||||
|
||||
@@ -90,6 +90,9 @@ export class BranchDayComponent implements OnInit {
|
||||
showOpenForm = false;
|
||||
openDate = '';
|
||||
openingCash: number | null = null;
|
||||
/** Required only when the counted/typed opening figure differs from yesterday's own closing
|
||||
* figure — the backend rejects the open without it in that case. */
|
||||
openDifferenceReason = '';
|
||||
opening = false;
|
||||
|
||||
// Close-day form. The date is fixed by which day is being closed, never typed.
|
||||
@@ -106,6 +109,15 @@ export class BranchDayComponent implements OnInit {
|
||||
coins: Denom[] = [];
|
||||
openCounts: Record<number, number> = {};
|
||||
closeCounts: Record<number, number> = {};
|
||||
/** What yesterday's own close counted, note by note — the open form starts from this instead of
|
||||
* zero. The backend already carries this figure forward on its own when nothing is submitted for
|
||||
* the day being opened; showing it here is only about letting whoever opens the day see and
|
||||
* correct it, not a second place the carry-forward logic lives. */
|
||||
openCountsAuto: Record<number, number> = {};
|
||||
/** What the vault + every cashier's live position added up to when the close form opened — the
|
||||
* physical-count table starts from this instead of zero, since the EOD summary already fetched
|
||||
* it. Kept separately from closeCounts so a manual recount can be told apart from it and undone. */
|
||||
closeCountsAuto: Record<number, number> = {};
|
||||
|
||||
get allDenoms(): Denom[] {
|
||||
return [
|
||||
@@ -212,21 +224,98 @@ export class BranchDayComponent implements OnInit {
|
||||
// over it is how a carried balance drifts.
|
||||
this.openingCash = null;
|
||||
this.openCounts = {};
|
||||
this.openCountsAuto = {};
|
||||
this.openDifferenceReason = '';
|
||||
this.successMsg = '';
|
||||
this.errorMsg = '';
|
||||
this.loadCarriedForwardDenominations();
|
||||
}
|
||||
|
||||
cancelOpen(): void {
|
||||
this.showOpenForm = false;
|
||||
this.openCountsAuto = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Pulls yesterday's own closing note/coin count so the open form shows what is about to be
|
||||
* carried forward instead of a blank table the caller has to trust blindly. Only the most
|
||||
* recently closed day is used — the same day the backend itself falls back to when the caller
|
||||
* submits no denominations and no lump sum.
|
||||
*/
|
||||
private loadCarriedForwardDenominations(): void {
|
||||
const lastClosed = this.days.find((d) => d.status === 'CLOSED' && d.closingCash != null);
|
||||
if (!lastClosed) return;
|
||||
this.service
|
||||
.branchDayDenominations(lastClosed.officeId, lastClosed.businessDate)
|
||||
.pipe(catchError(() => of(null)))
|
||||
.subscribe((res: any) => {
|
||||
const closing = res?.closing || [];
|
||||
if (!closing.length) return;
|
||||
const totals: Record<number, number> = {};
|
||||
for (const d of closing) {
|
||||
totals[d.denominationId] = (totals[d.denominationId] || 0) + Number(d.count || 0);
|
||||
}
|
||||
this.openCountsAuto = totals;
|
||||
// Only overwrite the table if nothing has been typed/counted since the form opened.
|
||||
if (this.openDenomTotal === 0 && this.openingCash == null) {
|
||||
this.openCounts = { ...totals };
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
get hasAutoFilledOpenCounts(): boolean {
|
||||
return Object.keys(this.openCountsAuto).length > 0;
|
||||
}
|
||||
|
||||
autoOpenCountFor(d: Denom): number {
|
||||
return this.openCountsAuto[d.id] || 0;
|
||||
}
|
||||
|
||||
resetOpenToAutoFill(): void {
|
||||
this.openCounts = { ...this.openCountsAuto };
|
||||
}
|
||||
|
||||
get carriedForwardTotal(): number {
|
||||
return this.totalOf(this.openCountsAuto);
|
||||
}
|
||||
|
||||
/** The figure the form would actually submit right now — the denomination total when any note/coin
|
||||
* has been counted, otherwise the plain typed-in amount. Mirrors {@link effectiveClosingFigure}. */
|
||||
get effectiveOpeningFigure(): number | null {
|
||||
return this.openDenomTotal > 0 ? this.openDenomTotal : this.openingCash;
|
||||
}
|
||||
|
||||
/** Difference between what is about to be opened with and what yesterday actually closed with —
|
||||
* visible before saving, the same way the close form shows a live difference against the book
|
||||
* figure, so a short/excess opening count isn't discovered only after the day is already running. */
|
||||
get openLiveDifference(): number | null {
|
||||
if (!this.hasAutoFilledOpenCounts || this.effectiveOpeningFigure == null) return null;
|
||||
return this.effectiveOpeningFigure - this.carriedForwardTotal;
|
||||
}
|
||||
|
||||
/** True once a reason is needed but hasn't been typed — the same "explain the gap before it's
|
||||
* allowed" gate the close form already applies to a late close. */
|
||||
get needsOpenDifferenceReason(): boolean {
|
||||
return this.openLiveDifference != null && this.openLiveDifference !== 0 && !this.openDifferenceReason.trim();
|
||||
}
|
||||
|
||||
get canSubmitOpen(): boolean {
|
||||
return !!this.openDate && !this.opening && !this.needsOpenDifferenceReason;
|
||||
}
|
||||
|
||||
submitOpen(): void {
|
||||
if (!this.openDate) return;
|
||||
if (!this.openDate || this.needsOpenDifferenceReason) return;
|
||||
this.opening = true;
|
||||
this.errorMsg = '';
|
||||
const denominations = this.denominationPayload(this.openCounts);
|
||||
this.service
|
||||
.openBranchDay(this.openDate, this.openingCash ?? undefined, undefined, denominations)
|
||||
.openBranchDay(
|
||||
this.openDate,
|
||||
this.openingCash ?? undefined,
|
||||
undefined,
|
||||
denominations,
|
||||
this.openDifferenceReason.trim() || undefined
|
||||
)
|
||||
.pipe(
|
||||
catchError((err: any) => {
|
||||
this.errorMsg =
|
||||
@@ -252,6 +341,7 @@ export class BranchDayComponent implements OnInit {
|
||||
this.closeTarget = day;
|
||||
this.closingCash = null;
|
||||
this.closeCounts = {};
|
||||
this.closeCountsAuto = {};
|
||||
this.closeRemarks = '';
|
||||
this.forceReason = '';
|
||||
this.successMsg = '';
|
||||
@@ -327,9 +417,47 @@ export class BranchDayComponent implements OnInit {
|
||||
.subscribe((results: any[]) => {
|
||||
this.eodLoading = false;
|
||||
this.eodTellers = results;
|
||||
this.autoFillCloseCounts();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Seeds the physical-count table from what the vault and every cashier are already shown to be
|
||||
* holding, instead of leaving it at zero and making whoever closes the day re-key numbers that
|
||||
* are already on screen above. It stays a starting point, not a lock — any row can still be
|
||||
* corrected against what was actually counted, and {@link resetToAutoFill} undoes a correction.
|
||||
*/
|
||||
private autoFillCloseCounts(): void {
|
||||
const totals: Record<number, number> = {};
|
||||
for (const t of this.eodTellers) {
|
||||
for (const d of t.vault?.denominations || []) {
|
||||
totals[d.denominationId] = (totals[d.denominationId] || 0) + Number(d.count || 0);
|
||||
}
|
||||
for (const c of t.cashiers || []) {
|
||||
for (const d of c.denominations || []) {
|
||||
totals[d.denominationId] = (totals[d.denominationId] || 0) + Number(d.count || 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (Object.keys(totals).length === 0) return;
|
||||
this.closeCountsAuto = totals;
|
||||
this.closeCounts = { ...totals };
|
||||
}
|
||||
|
||||
get hasAutoFilledCounts(): boolean {
|
||||
return Object.keys(this.closeCountsAuto).length > 0;
|
||||
}
|
||||
|
||||
/** The auto-fetched count for one denomination, so a row that was hand-edited away from it can
|
||||
* still show what the vault + cashier positions said. */
|
||||
autoCountFor(d: Denom): number {
|
||||
return this.closeCountsAuto[d.id] || 0;
|
||||
}
|
||||
|
||||
resetToAutoFill(): void {
|
||||
this.closeCounts = { ...this.closeCountsAuto };
|
||||
}
|
||||
|
||||
private loadTellerSummary(teller: any, date: string) {
|
||||
return forkJoin({
|
||||
vault: this.service.vaultPosition(teller.id, date).pipe(catchError(() => of(null))),
|
||||
@@ -403,6 +531,7 @@ export class BranchDayComponent implements OnInit {
|
||||
cancelClose(): void {
|
||||
this.closeTarget = null;
|
||||
this.eodTellers = [];
|
||||
this.closeCountsAuto = {};
|
||||
}
|
||||
|
||||
/** A date earlier than today is a late close, and has to say why it was left open. */
|
||||
|
||||
@@ -206,6 +206,7 @@
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
background: linear-gradient(135deg, #ecfeff, #cffafe);
|
||||
color: #155e75;
|
||||
border: 1px solid #a5f3fc;
|
||||
|
||||
@@ -229,17 +229,20 @@ export class CashierService {
|
||||
return this.http.get('/branchday/close-preview', { params });
|
||||
}
|
||||
|
||||
/** openingDifferenceReason is required when the opening figure differs from yesterday's closing. */
|
||||
openBranchDay(
|
||||
businessDate: string,
|
||||
openingCash?: number,
|
||||
officeId?: number,
|
||||
denominations?: { denominationId: number; denominationValue: number; count: number }[]
|
||||
denominations?: { denominationId: number; denominationValue: number; count: number }[],
|
||||
openingDifferenceReason?: string
|
||||
): Observable<any> {
|
||||
return this.http.post('/branchday/open', {
|
||||
businessDate,
|
||||
openingCash: openingCash != null ? String(openingCash) : null,
|
||||
officeId: officeId ?? null,
|
||||
denominations: denominations?.length ? denominations : null,
|
||||
openingDifferenceReason: openingDifferenceReason ?? null,
|
||||
locale: 'en'
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user