Files
cbsnexora/src/app/cashier/reconciliation/reconciliation.component.html
T
niteshandClaude Sonnet 5.5 915deddb74 feat(cashier): cashier-only cash operations, exchange PAN/denomination checks and Branch Cash Position
- Cash Exchange Entry: customer details with PAN (mandatory above Rs 50,000), cashier card,
  denomination-wise validation against the master and real-time impact preview.
- Inter-Cashier Transfer: sender fixed to the logged-in cashier, both sides' details and
  denomination holdings shown, blocked until both are allocated cash.
- Reconciliation, exchange and inter-cashier operate on the logged-in cashier only.
- New Branch Cash Position screen (vault + cashier drawers, denomination-wise).
- Customer search dialog shows the KYC PAN.
- Cashier menu cleanup: Excess/Closing report and Exchange Detail removed, nav compacted to one row.
- Reverse actions removed from cash transfer, settlement, exchange and inter-cashier.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-13 18:41:24 +05:30

322 lines
14 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!--
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">CASHIER · RECONCILIATION</div>
<h1 class="pg-title">Cash Reconciliation</h1>
<p class="pg-sub">Compare physical denomination count with system expected balance. Highlights variance.</p>
</div>
</header>
<mat-tab-group animationDuration="150ms">
<!-- ── Entry Tab ── -->
<mat-tab label="New Reconciliation">
<div class="pg-tab-body">
@if (ownCashierMissing) {
<div class="pg-empty">
Your login is not linked to a cashier, so there is no cash drawer to reconcile. Ask an administrator to link
your user to a cashier.
</div>
}
<div class="pg-toolbar pg-toolbar--mat">
<!-- A reconciliation is the cashier's own count of their own drawer: nothing to pick. -->
<mat-form-field appearance="outline" subscriptSizing="dynamic">
<mat-label>Teller</mat-label>
<input matInput type="text" [value]="ownTellerName || '—'" readonly />
</mat-form-field>
<mat-form-field appearance="outline" subscriptSizing="dynamic">
<mat-label>Cashier (You)</mat-label>
<input matInput type="text" [value]="cashierName || '—'" readonly />
</mat-form-field>
<mat-form-field appearance="outline" subscriptSizing="dynamic">
<mat-label>Date</mat-label>
<input
matInput
type="date"
[value]="reconDate"
(change)="reconDate = $any($event.target).value; fetchExpected()"
/>
</mat-form-field>
<mat-form-field appearance="outline" subscriptSizing="dynamic" class="pg-note-field">
<mat-label>Note</mat-label>
<input
matInput
type="text"
[value]="note"
(input)="note = $any($event.target).value"
placeholder="Optional"
/>
</mat-form-field>
</div>
@if (loadingExpected) {
<div class="pg-empty">Loading expected balances…</div>
}
@if (!loadingExpected && rows.length > 0) {
<!-- Morning Opening Balance panel -->
@if (morningTotal > 0) {
<div class="morning-panel">
<div class="morning-panel__head">
<mat-icon>wb_sunny</mat-icon>
<span>Morning Opening Balance <em>(Cash received from vault today)</em></span>
<strong class="morning-panel__total">₹{{ morningTotal | number: '1.2-2' }}</strong>
</div>
<div class="morning-panel__body">
@if (noteRows.length > 0) {
<div class="morning-panel__section-label">Notes — ₹{{ morningNoteTotal | number: '1.0-0' }}</div>
<div class="morning-panel__pills">
@for (row of noteRows; track row.denominationId) {
@if (row.morningCount > 0) {
<span class="morning-pill"
>₹{{ row.denominationValue
}}<span class="morning-pill__x">×{{ row.morningCount }}</span></span
>
}
}
</div>
}
@if (coinRows.length > 0) {
<div class="morning-panel__section-label morning-panel__section-label--coin">
Coins — ₹{{ morningCoinTotal | number: '1.0-0' }}
</div>
<div class="morning-panel__pills">
@for (row of coinRows; track row.denominationId) {
@if (row.morningCount > 0) {
<span class="morning-pill morning-pill--coin"
>₹{{ row.denominationValue
}}<span class="morning-pill__x">×{{ row.morningCount }}</span></span
>
}
}
</div>
}
</div>
</div>
}
<div class="pg-card">
@if (noteRows.length > 0) {
<div class="pos-section-label">Notes</div>
<table class="pg-table">
<thead>
<tr>
<th>Denomination</th>
<th class="num recon-col-morning" title="Cash received from vault this morning">Morning</th>
<th style="text-align: center">Expected</th>
<th style="text-align: center">Physical</th>
<th class="num">Variance</th>
</tr>
</thead>
<tbody>
@for (row of noteRows; track row.denominationId) {
<tr [class.row-excess]="row.varianceCount > 0" [class.row-short]="row.varianceCount < 0">
<td>
<span class="pg-chip">₹{{ row.denominationValue }}</span>
</td>
<td class="num recon-morning">{{ row.morningCount || '—' }}</td>
<td style="text-align: center; color: #64748b; font-weight: 600">{{ row.expectedCount }}</td>
<td style="text-align: center">
<div class="pg-step" style="margin: 0 auto">
<button
type="button"
(click)="updatePhysical(row, row.physicalCount - 1)"
[disabled]="row.physicalCount === 0"
>
−
</button>
<input
type="number"
min="0"
[value]="row.physicalCount"
(input)="updatePhysical(row, $any($event.target).value)"
/>
<button type="button" (click)="updatePhysical(row, row.physicalCount + 1)">+</button>
</div>
</td>
<td
class="num"
[style.color]="
row.varianceCount > 0 ? '#16a34a' : row.varianceCount < 0 ? '#dc2626' : '#94a3b8'
"
>
{{ row.varianceCount > 0 ? '+' : '' }}{{ row.varianceCount }} (₹{{
row.varianceCount * row.denominationValue | number: '1.0-0'
}})
</td>
</tr>
}
</tbody>
</table>
}
@if (coinRows.length > 0) {
<div class="pos-section-label pos-section-label--coin">Coins</div>
<table class="pg-table">
<thead>
<tr>
<th>Denomination</th>
<th class="num recon-col-morning" title="Cash received from vault this morning">Morning</th>
<th style="text-align: center">Expected</th>
<th style="text-align: center">Physical</th>
<th class="num">Variance</th>
</tr>
</thead>
<tbody>
@for (row of coinRows; track row.denominationId) {
<tr [class.row-excess]="row.varianceCount > 0" [class.row-short]="row.varianceCount < 0">
<td>
<span class="pg-chip pg-chip--coin">₹{{ row.denominationValue }}</span>
</td>
<td class="num recon-morning">{{ row.morningCount || '—' }}</td>
<td style="text-align: center; color: #64748b; font-weight: 600">{{ row.expectedCount }}</td>
<td style="text-align: center">
<div class="pg-step" style="margin: 0 auto">
<button
type="button"
(click)="updatePhysical(row, row.physicalCount - 1)"
[disabled]="row.physicalCount === 0"
>
−
</button>
<input
type="number"
min="0"
[value]="row.physicalCount"
(input)="updatePhysical(row, $any($event.target).value)"
/>
<button type="button" (click)="updatePhysical(row, row.physicalCount + 1)">+</button>
</div>
</td>
<td
class="num"
[style.color]="
row.varianceCount > 0 ? '#16a34a' : row.varianceCount < 0 ? '#dc2626' : '#94a3b8'
"
>
{{ row.varianceCount > 0 ? '+' : '' }}{{ row.varianceCount }} (₹{{
row.varianceCount * row.denominationValue | number: '1.0-0'
}})
</td>
</tr>
}
</tbody>
</table>
}
</div>
@if (totalVariance !== 0) {
<div
class="settle-variance-banner"
[class.settle-variance-banner--excess]="totalVariance > 0"
[class.settle-variance-banner--short]="totalVariance < 0"
style="margin: 8px 0 0"
>
<mat-icon>{{ totalVariance > 0 ? 'add_circle_outline' : 'remove_circle_outline' }}</mat-icon>
<div class="settle-variance-text">
<strong
>{{ totalVariance > 0 ? 'Excess' : 'Shortage' }}: ₹{{
(totalVariance < 0 ? -totalVariance : totalVariance) | number: '1.2-2'
}}</strong
>
<span>Variance reason is required to save reconciliation.</span>
</div>
<mat-form-field appearance="outline" subscriptSizing="dynamic" style="flex-shrink: 0; width: 300px">
<mat-label>Variance Reason *</mat-label>
<input
matInput
type="text"
[value]="varianceReason"
(input)="varianceReason = $any($event.target).value"
placeholder="e.g. Rounding, soiled note returned"
/>
</mat-form-field>
</div>
}
<div class="pg-total-bar">
<span>
Expected: <strong>₹{{ totalExpected | number: '1.2-2' }}</strong> &nbsp;|&nbsp; Physical:
<strong>₹{{ totalPhysical | number: '1.2-2' }}</strong> &nbsp;|&nbsp; Variance:
<strong [style.color]="totalVariance === 0 ? '#16a34a' : '#dc2626'">
{{ totalVariance >= 0 ? '+' : '' }}₹{{ totalVariance | number: '1.2-2' }}
</strong>
<span class="badge" [ngClass]="statusClass(statusLabel)">{{ statusLabel }}</span>
</span>
<div style="display: flex; align-items: center; gap: 10px">
@if (totalVariance !== 0 && !varianceReason.trim()) {
<span style="font-size: 12px; color: #d97706; font-weight: 600">Enter variance reason to save</span>
}
<button type="button" class="pg-btn pg-btn--primary" [disabled]="!valid || saving" (click)="submit()">
<mat-icon>save</mat-icon> Save Reconciliation
</button>
</div>
</div>
}
@if (!loadingExpected && rows.length === 0 && selectedCashierId) {
<div class="pg-empty">No balance data found for this cashier.</div>
}
@if (!selectedCashierId) {
<div class="pg-empty">Select a teller and cashier to load expected balances.</div>
}
</div>
</mat-tab>
<!-- ── History Tab ── -->
<mat-tab label="History">
<div class="pg-tab-body">
@if (loadingHistory) {
<div class="pg-empty">Loading…</div>
}
@if (!loadingHistory && history.length === 0) {
<div class="pg-empty">No reconciliation records yet.</div>
}
@if (!loadingHistory && history.length > 0) {
<div class="pg-card">
<table class="pg-table">
<thead>
<tr>
<th>Date</th>
<th>Cashier</th>
<th class="num">Expected</th>
<th class="num">Physical</th>
<th class="num">Variance</th>
<th style="text-align: center">Status</th>
<th>By</th>
</tr>
</thead>
<tbody>
@for (r of history; track r.id) {
<tr>
<td>{{ r.reconDate }}</td>
<td>{{ r.cashierName || r.cashierId }}</td>
<td class="num">₹{{ r.expectedAmount | number: '1.2-2' }}</td>
<td class="num">₹{{ r.physicalAmount | number: '1.2-2' }}</td>
<td class="num" [style.color]="r.variance > 0 ? '#16a34a' : r.variance < 0 ? '#dc2626' : '#94a3b8'">
{{ r.variance >= 0 ? '+' : '' }}₹{{ r.variance | number: '1.2-2' }}
</td>
<td style="text-align: center">
<span class="badge" [ngClass]="statusClass(r.status)">{{ r.status }}</span>
</td>
<td>{{ r.performedByUsername }}</td>
</tr>
}
</tbody>
</table>
</div>
}
</div>
</mat-tab>
</mat-tab-group>
</div>