- 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.
+ }
- 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.
+ }
}
diff --git a/src/app/cashier/branch-day/branch-day.component.scss b/src/app/cashier/branch-day/branch-day.component.scss
index 334ee94ba..a4b1ac062 100644
--- a/src/app/cashier/branch-day/branch-day.component.scss
+++ b/src/app/cashier/branch-day/branch-day.component.scss
@@ -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. */
diff --git a/src/app/cashier/branch-day/branch-day.component.ts b/src/app/cashier/branch-day/branch-day.component.ts
index f6eb1cc20..9abba824f 100644
--- a/src/app/cashier/branch-day/branch-day.component.ts
+++ b/src/app/cashier/branch-day/branch-day.component.ts
@@ -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 = {};
closeCounts: Record = {};
+ /** 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 = {};
+ /** 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 = {};
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 = {};
+ 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 = {};
+ 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. */
diff --git a/src/app/cashier/cashier-shared.scss b/src/app/cashier/cashier-shared.scss
index 6985025e4..90f9984cc 100644
--- a/src/app/cashier/cashier-shared.scss
+++ b/src/app/cashier/cashier-shared.scss
@@ -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;
diff --git a/src/app/cashier/cashier.service.ts b/src/app/cashier/cashier.service.ts
index 7725e5e36..65e4cce0b 100644
--- a/src/app/cashier/cashier.service.ts
+++ b/src/app/cashier/cashier.service.ts
@@ -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 {
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'
});
}