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:
@@ -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. */
|
||||
|
||||
Reference in New Issue
Block a user