- Add Balance Sheet report and disabled-features accounting config - Point menu-access tenant header and env.js/environment.ts tenant identifier at env config instead of hardcoded 'default'/'lincpay' literals - Surface the real backend error (not a generic "no reference id") when an OTP send fails in contact-otp-block - Fix branch-day "₹null" display bug (null vs undefined) and add a guard blocking a branch's first-ever day open before cash denomination migration - Dynamically size the denomination count input and block re-opening an already-open business day from Migrate Cash Denomination - Lock the denomination-managed cash GL row in Migrate Opening Balances (read-only, with a link back to Migrate Cash Denomination) - Remove duplicate Cancel/Submit action bars on the Share Product and Savings Account creation preview steps; align Previous into the same row - Hide "Apply Collection" on a share account once its advance collection is already settled (further purchases go through Apply Additional Shares) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
744 lines
30 KiB
HTML
744 lines
30 KiB
HTML
<!--
|
||
Copyright since 2025 Mifos Initiative
|
||
|
||
This Source Code Form is subject to the terms of the Mozilla Public
|
||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||
-->
|
||
|
||
<div class="pg">
|
||
<header class="pg-head">
|
||
<div>
|
||
<div class="pg-eyebrow">BRANCH OPERATIONS · {{ missedOnly ? 'MISSED CLOSE' : 'DAY REGISTER' }}</div>
|
||
<h1 class="pg-title">{{ missedOnly ? 'Missed Close Register' : 'Day Open / Day Close' }}</h1>
|
||
<p class="pg-sub">
|
||
@if (missedOnly) {
|
||
Business days that were opened and never closed. While a day stays open its closing balance never carries
|
||
forward, a back-dated entry can still be posted into it, and any cash difference on that date goes unnoticed.
|
||
} @else {
|
||
The branch's business day, opened each morning and closed each evening. A day left open never carries its
|
||
closing balance forward, and any cash difference on that date goes unnoticed — so the ones still hanging are
|
||
listed first.
|
||
}
|
||
</p>
|
||
</div>
|
||
<div class="pg-toolbar">
|
||
@if (canOpenToday && !missedOnly) {
|
||
<button type="button" class="pg-btn pg-btn--primary" (click)="startOpen()">
|
||
<mat-icon>lock_open</mat-icon> Open Day
|
||
</button>
|
||
}
|
||
<button type="button" class="pg-btn" (click)="load()" [disabled]="loading">
|
||
<mat-icon>refresh</mat-icon> Reload
|
||
</button>
|
||
</div>
|
||
</header>
|
||
|
||
@if (errorMsg) {
|
||
<div class="pg-alert pg-alert--error">{{ errorMsg }}</div>
|
||
}
|
||
@if (successMsg) {
|
||
<div class="bd-success">{{ successMsg }}</div>
|
||
}
|
||
|
||
@if (!canOperate && loaded) {
|
||
<div class="ve-note">
|
||
<mat-icon>info</mat-icon>
|
||
You can see the register but not open or close a day — a Clerk, Head Cashier or Bank Manager does that, since a
|
||
cashier works inside the day and cannot declare it closed.
|
||
</div>
|
||
}
|
||
|
||
@if (firstOpenNeedsCashMigration && !missedOnly) {
|
||
<div class="bd-locked">
|
||
<mat-icon>lock</mat-icon>
|
||
<span>
|
||
This branch has never opened a business day, and its Cash at Vault GL account has no opening balance yet. Count
|
||
the vault and open the first business day together from
|
||
<a [routerLink]="['/accounting/migrate-cash-denomination']">Migrate Cash Denomination</a> — Open Day here is
|
||
locked until that is done, so today's cash tracking never starts out of step with the ledger.
|
||
</span>
|
||
</div>
|
||
}
|
||
|
||
<!-- Missed closes first: these are the ones that need action. -->
|
||
@if (missed.length > 0) {
|
||
<div class="db-exceptions">
|
||
<div class="db-exceptions__head">
|
||
<mat-icon>warning</mat-icon>
|
||
<strong>{{ missed.length }} business day{{ missed.length === 1 ? '' : 's' }} never closed</strong>
|
||
</div>
|
||
@for (m of missed; track m.id) {
|
||
<div class="bd-missed">
|
||
<span class="pg-chip">{{ m.businessDate }}</span>
|
||
<strong class="bd-missed__age"> open {{ m.daysOpen }} day{{ m.daysOpen === 1 ? '' : 's' }} </strong>
|
||
<span class="db-muted">
|
||
opened {{ timeOnly(m.openedAt) }} by {{ m.openedByName }} · opening ₹{{ m.openingCash | number: '1.2-2' }}
|
||
</span>
|
||
@if (canOperate) {
|
||
<button type="button" class="pg-btn pg-btn--warn bd-missed__act" (click)="startClose(m)">
|
||
Close this day
|
||
</button>
|
||
}
|
||
</div>
|
||
}
|
||
</div>
|
||
}
|
||
|
||
<!-- On the missed-close screen, nothing to report is itself the answer — and has to be stated,
|
||
or the page looks like it failed to load. -->
|
||
@if (missedOnly && loaded && missed.length === 0) {
|
||
<div class="pg-empty">
|
||
<mat-icon>check_circle</mat-icon>
|
||
<p>No missed closes. Every business day opened for this branch has been closed.</p>
|
||
</div>
|
||
}
|
||
|
||
<!-- Current day -->
|
||
@if (!missedOnly && openDay) {
|
||
<div class="bd-current">
|
||
<div class="bd-current__main">
|
||
<span class="db-badge db-badge--pending">DAY OPEN</span>
|
||
<strong class="bd-current__date">{{ openDay.businessDate }}</strong>
|
||
<span class="db-muted">
|
||
opened {{ timeOnly(openDay.openedAt) }} by {{ openDay.openedByName }} · opening cash ₹{{
|
||
openDay.openingCash | number: '1.2-2'
|
||
}}
|
||
</span>
|
||
</div>
|
||
@if (canOperate) {
|
||
<button type="button" class="pg-btn pg-btn--primary" (click)="startClose(openDay)">
|
||
<mat-icon>lock</mat-icon> Close Day
|
||
</button>
|
||
}
|
||
</div>
|
||
} @else if (loaded && canOperate) {
|
||
<div class="bd-current bd-current--shut">
|
||
<div class="bd-current__main">
|
||
<span class="db-badge">NO DAY OPEN</span>
|
||
<span class="db-muted">Open the business day before any cash transaction is accepted.</span>
|
||
</div>
|
||
</div>
|
||
}
|
||
|
||
<!-- Open-day form -->
|
||
@if (showOpenForm) {
|
||
<div class="db-panel bd-panel">
|
||
<div class="db-panel__head">
|
||
<strong>Open business day</strong>
|
||
<button type="button" class="db-icon-btn" (click)="cancelOpen()" title="Close">
|
||
<mat-icon>close</mat-icon>
|
||
</button>
|
||
</div>
|
||
<div class="db-panel__body">
|
||
<label class="pg-field">
|
||
<span>Business date</span>
|
||
<input type="date" [(ngModel)]="openDate" />
|
||
</label>
|
||
<label class="pg-field">
|
||
<span>Opening cash (leave blank to carry forward)</span>
|
||
<input
|
||
type="number"
|
||
step="0.01"
|
||
[(ngModel)]="openingCash"
|
||
[disabled]="openDenomTotal > 0"
|
||
placeholder="Carried from last close"
|
||
/>
|
||
</label>
|
||
</div>
|
||
<p class="bd-hint">
|
||
@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>
|
||
<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; flex-direction: column; align-items: 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>
|
||
@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>
|
||
}
|
||
@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; flex-direction: column; align-items: 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>
|
||
@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>
|
||
}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
@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]="!canSubmitOpen">
|
||
Open Day
|
||
</button>
|
||
<button type="button" class="pg-btn" (click)="cancelOpen()">Cancel</button>
|
||
</div>
|
||
</div>
|
||
}
|
||
|
||
<!-- Close-day form -->
|
||
@if (closeTarget) {
|
||
<div class="db-panel bd-panel" [class.bd-panel--late]="isLateClose">
|
||
<div class="db-panel__head">
|
||
<strong>Close business day — {{ closeTarget.businessDate }}</strong>
|
||
<button type="button" class="db-icon-btn" (click)="cancelClose()" title="Close">
|
||
<mat-icon>close</mat-icon>
|
||
</button>
|
||
</div>
|
||
|
||
@if (needsForceReason) {
|
||
<p class="db-panel__warn">
|
||
This day is being closed after its own date. It will be recorded as a
|
||
<strong>force close</strong>, with the reason below kept permanently against it.
|
||
</p>
|
||
}
|
||
|
||
@if (previewLoading) {
|
||
<p class="db-muted">Working out the day's figure…</p>
|
||
}
|
||
|
||
<!-- The arithmetic, spelled out. Whoever closes the day can see where the expected figure
|
||
comes from instead of being asked to agree with a number they cannot check. -->
|
||
@if (preview) {
|
||
<div class="bd-calc">
|
||
<div class="bd-calc__row">
|
||
<span>Opening cash (carried forward)</span>
|
||
<span class="bd-calc__amt">₹{{ preview.openingCash | number: '1.2-2' }}</span>
|
||
</div>
|
||
<div class="bd-calc__row bd-calc__row--in">
|
||
<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--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--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 (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) {
|
||
<div class="db-cmp db-cmp--good">
|
||
<span class="db-cmp__label">Difference</span>
|
||
<span class="db-cmp__value">Tallied</span>
|
||
</div>
|
||
} @else {
|
||
<div class="db-cmp db-cmp--bad">
|
||
<span class="db-cmp__label">Difference</span>
|
||
<span class="db-cmp__value">
|
||
{{ liveDifference > 0 ? 'Excess' : 'Short' }} ₹{{
|
||
liveDifference < 0 ? -liveDifference : (liveDifference | number: '1.2-2')
|
||
}}
|
||
</span>
|
||
</div>
|
||
}
|
||
}
|
||
@if (preview && liveDifference !== 0) {
|
||
<button type="button" class="pg-btn" (click)="useExpected()">Use expected figure</button>
|
||
}
|
||
</div>
|
||
|
||
<p class="bd-hint">
|
||
@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">
|
||
<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; flex-direction: column; align-items: 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>
|
||
@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>
|
||
}
|
||
@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; flex-direction: column; align-items: 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>
|
||
@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>
|
||
}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
@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>
|
||
}
|
||
|
||
<!-- Shown for an unreconciled cashier too, not only a late close. The backend blocks a close
|
||
over a cashier who took cash on this date and never reconciled it, and accepts a reason as
|
||
the override — but this field only appeared for a late close, so a same-day close in that
|
||
state had nowhere to type the reason and could not be completed at all until the date had
|
||
passed. -->
|
||
@if (isLateClose || hasUnreconciledOrUnsettledCashiers) {
|
||
<label class="pg-field bd-panel__wide">
|
||
<span>
|
||
@if (isLateClose) {
|
||
Why was this day left open? (required)
|
||
} @else {
|
||
Why close over a cashier who has not reconciled? (required)
|
||
}
|
||
</span>
|
||
<input
|
||
type="text"
|
||
[(ngModel)]="forceReason"
|
||
placeholder="e.g. cashier on leave, cash not settled that evening"
|
||
/>
|
||
</label>
|
||
}
|
||
|
||
<label class="pg-field bd-panel__wide">
|
||
<span>Remarks (optional)</span>
|
||
<input type="text" [(ngModel)]="closeRemarks" placeholder="Anything worth recording about this day" />
|
||
</label>
|
||
|
||
<p class="bd-hint">
|
||
The figure above is stored against the record, so a difference stays readable later even if a transaction is
|
||
reversed afterwards.
|
||
</p>
|
||
|
||
@if (hasUnreconciledOrUnsettledCashiers) {
|
||
<!-- A wall on the day itself, a warning on a past one. Today the cashier is still here and
|
||
the cash can be counted, so nothing excuses closing over it. On a past date it cannot be
|
||
counted for that date at all, and blocking would leave a day nobody could ever close —
|
||
so it goes through on the reason a late close already requires, with the names recorded
|
||
against the day. -->
|
||
<div
|
||
class="pg-alert bd-reconcile-block"
|
||
[class.pg-alert--error]="blocksSameDayClose"
|
||
[class.bd-reconcile-block--warn]="!blocksSameDayClose"
|
||
>
|
||
<mat-icon>{{ blocksSameDayClose ? 'block' : 'warning' }}</mat-icon>
|
||
<div>
|
||
@if (blocksSameDayClose) {
|
||
<strong
|
||
>Cannot close today — cashiers still holding cash have not been reconciled or settled.</strong
|
||
>
|
||
} @else {
|
||
<strong>This day's cash was never reconciled or settled.</strong>
|
||
}
|
||
<ul class="bd-reconcile-block__list">
|
||
@for (detail of unreconciledOrUnsettledCashierDetails; track detail.name) {
|
||
<li>
|
||
<strong>{{ detail.name }}</strong> —
|
||
@if (detail.notReconciled && detail.notSettled) {
|
||
Not Reconciled, Not Settled
|
||
} @else if (detail.notReconciled) {
|
||
Not Reconciled
|
||
} @else {
|
||
Not Settled
|
||
}
|
||
</li>
|
||
}
|
||
</ul>
|
||
@if (blocksSameDayClose) {
|
||
<span class="db-muted"
|
||
>Each cashier must count their drawer, reconcile it, and settle the cash back to the vault. Then this
|
||
day can be closed.</span
|
||
>
|
||
} @else {
|
||
<span class="db-muted"
|
||
>The cash cannot be counted for a date that has passed. Record why above — their names are kept
|
||
against the record.</span
|
||
>
|
||
}
|
||
</div>
|
||
</div>
|
||
}
|
||
|
||
<div class="db-panel__actions">
|
||
<button type="button" class="pg-btn pg-btn--primary" (click)="submitClose()" [disabled]="!canSubmitClose">
|
||
{{ needsForceReason ? 'Force Close Day' : 'Close Day' }}
|
||
</button>
|
||
<button type="button" class="pg-btn" (click)="cancelClose()">Cancel</button>
|
||
</div>
|
||
</div>
|
||
}
|
||
|
||
@if (loading) {
|
||
<div class="pg-empty">Loading…</div>
|
||
}
|
||
|
||
@if (!loading && loaded && days.length === 0 && !missedOnly) {
|
||
<div class="pg-empty">
|
||
<mat-icon>event_note</mat-icon>
|
||
<p>No business day has been opened for this branch yet.</p>
|
||
</div>
|
||
}
|
||
|
||
<!-- The register -->
|
||
@if (days.length > 0 && !missedOnly) {
|
||
<div class="pg-card">
|
||
<table class="pg-table">
|
||
<thead>
|
||
<tr>
|
||
<th>Business Date</th>
|
||
<th>Status</th>
|
||
<th>Opened By</th>
|
||
<th class="num">Opening ₹</th>
|
||
<th>Closed By</th>
|
||
<th class="num">Closing ₹</th>
|
||
<th class="num">Book ₹</th>
|
||
<th>Difference</th>
|
||
<th>Note</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
@for (d of days; track d.id) {
|
||
<tr>
|
||
<td>
|
||
<span class="pg-chip">{{ d.businessDate }}</span>
|
||
</td>
|
||
<td>
|
||
@if (d.status === 'OPEN') {
|
||
<span class="db-badge db-badge--pending">OPEN</span>
|
||
} @else if (d.forceClosed) {
|
||
<span class="db-badge db-badge--rejected" [title]="d.forceCloseReason ?? ''">FORCE CLOSED</span>
|
||
} @else {
|
||
<span class="db-badge db-badge--ok">CLOSED</span>
|
||
}
|
||
</td>
|
||
<td>
|
||
{{ d.openedByName ?? '—' }}
|
||
<span class="db-muted">· {{ timeOnly(d.openedAt) }}</span>
|
||
</td>
|
||
<td class="num">₹{{ d.openingCash | number: '1.2-2' }}</td>
|
||
<td>
|
||
{{ d.closedByName ?? '—' }}
|
||
@if (d.closedAt) {
|
||
<span class="db-muted">· {{ timeOnly(d.closedAt) }}</span>
|
||
}
|
||
</td>
|
||
<td class="num">{{ hasValue(d.closingCash) ? '₹' + (d.closingCash | number: '1.2-2') : '—' }}</td>
|
||
<td class="num">{{ hasValue(d.bookCash) ? '₹' + (d.bookCash | number: '1.2-2') : '—' }}</td>
|
||
<td>
|
||
@if (!hasValue(d.cashDifference)) {
|
||
—
|
||
} @else if (d.cashDifference === 0) {
|
||
<span class="db-badge db-badge--ok">Tallied</span>
|
||
} @else {
|
||
<span class="db-badge db-badge--rejected">{{ differenceLabel(d.cashDifference) }}</span>
|
||
}
|
||
</td>
|
||
<td class="bd-note">
|
||
@if (d.forceCloseReason) {
|
||
<strong>Left open:</strong> {{ d.forceCloseReason }}
|
||
}
|
||
@if (d.remarks) {
|
||
<span class="db-muted">{{ d.remarks }}</span>
|
||
}
|
||
</td>
|
||
</tr>
|
||
}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
}
|
||
</div>
|