Files
cbsnexora/src/app/cashier/cash-exchange-entry/cash-exchange-entry.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

489 lines
19 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="cx">
<header class="cx-head">
<div>
<div class="cx-eyebrow">CASHIER · CASH EXCHANGE</div>
<h1 class="cx-title">Cash Exchange Entry</h1>
<p class="cx-sub">Customer gives notes/coins and takes equal-value change. Net amount must be ₹0.</p>
</div>
<div class="cx-toolbar">
<label class="cx-field">
<span>Date</span>
<input type="date" [value]="exchangeDate" (change)="exchangeDate = $any($event.target).value" />
</label>
<button type="button" class="cx-btn cx-btn--ghost" (click)="clear()"><mat-icon>undo</mat-icon> Clear</button>
</div>
</header>
<!-- ── Cashier: whose drawer takes the customer's notes and gives the change ── -->
@if (cashier) {
<section class="cx-cashier">
<div class="cx-avatar cx-avatar--sm">{{ cashierInitials }}</div>
<div class="cx-cashier__id">
<div class="cx-cashier__label">Cashier at the counter</div>
<div class="cx-cashier__name">{{ cashier.name || '—' }}</div>
<div class="cx-cashier__flow">
<span><mat-icon>south</mat-icon> Receives from customer</span>
<span><mat-icon>north</mat-icon> Gives to customer</span>
</div>
</div>
<div class="cx-cashier__stats">
<div>
<span>Counter</span><strong>{{ cashier.tellerName || '—' }}</strong>
</div>
<div>
<span>Branch</span><strong>{{ cashier.officeName || '—' }}</strong>
</div>
<div>
<span>Allocated today</span
><strong>{{
cashier.allocatedAmount === null ? '—' : '₹' + (cashier.allocatedAmount | number: '1.2-2')
}}</strong>
</div>
<div>
<span>Cash in hand</span><strong>₹{{ cashier.totalAmount | number: '1.2-2' }}</strong>
</div>
</div>
</section>
} @else if (noCashier) {
<section class="cx-cashier cx-cashier--none">
<mat-icon>person_off</mat-icon>
<div>
<div class="cx-cashier__name">Your login is not linked to a cashier</div>
<div class="cx-cashier__label">
A cash exchange moves notes in and out of a cashier's drawer, so it can only be recorded by a cashier. Log in
as a cashier to continue.
</div>
</div>
</section>
}
<!-- ── Customer: an existing client (details from their record) or a walk-in identified at the counter ── -->
<section class="cx-cust">
<header class="cx-cust__head">
<div class="cx-cust__title">
<span class="cx-cust__title-icon"><mat-icon>groups</mat-icon></span>
<div>
<div class="cx-cust__title-text">Customer</div>
<div class="cx-cust__title-sub">Who is exchanging cash at the counter</div>
</div>
</div>
<div class="cx-cust__modes" role="tablist" aria-label="Customer type">
<button
type="button"
role="tab"
class="cx-cust__mode"
[class.cx-cust__mode--active]="customerMode === 'existing'"
[attr.aria-selected]="customerMode === 'existing'"
(click)="setCustomerMode('existing')"
>
<mat-icon>badge</mat-icon> Existing Customer
</button>
<button
type="button"
role="tab"
class="cx-cust__mode"
[class.cx-cust__mode--active]="customerMode === 'walkin'"
[attr.aria-selected]="customerMode === 'walkin'"
(click)="setCustomerMode('walkin')"
>
<mat-icon>person_outline</mat-icon> Walk-in Customer
</button>
</div>
</header>
<div class="cx-cust__body">
<!-- Who -->
<div class="cx-cust__who">
@if (customerMode === 'existing') {
@if (loadingCustomer) {
<div class="cx-cust__empty">
<mat-icon class="cx-cust__empty-icon cx-spin">autorenew</mat-icon>
<div class="cx-cust__empty-title">Loading customer details…</div>
</div>
} @else if (!selectedCustomer) {
<div class="cx-cust__empty">
<mat-icon class="cx-cust__empty-icon">person_search</mat-icon>
<div class="cx-cust__empty-title">No customer selected</div>
<div class="cx-cust__empty-sub">Search by name, account no., PAN or Aadhaar</div>
<button type="button" class="cx-btn cx-btn--primary" (click)="openCustomerSearch()">
<mat-icon>search</mat-icon> Search Customer
</button>
</div>
} @else {
<div class="cx-cust__card">
<div class="cx-cust__card-top">
<div class="cx-avatar">{{ customerInitials }}</div>
<div class="cx-cust__card-id">
<div class="cx-cust__name">{{ selectedCustomer.displayName }}</div>
<div class="cx-cust__badges">
<span
class="cx-kyc"
[class.cx-kyc--ok]="selectedCustomer.kycStatus === 'Verified'"
[class.cx-kyc--warn]="selectedCustomer.kycStatus !== 'Verified'"
>
<mat-icon>{{
selectedCustomer.kycStatus === 'Verified' ? 'verified_user' : 'gpp_maybe'
}}</mat-icon>
KYC: {{ selectedCustomer.kycStatus }}
</span>
@if (selectedCustomer.accountType) {
<span class="cx-chip-tag">{{ selectedCustomer.accountType }}</span>
}
</div>
</div>
<div class="cx-cust__card-actions">
<button type="button" class="cx-link-btn" (click)="openCustomerSearch()">
<mat-icon>swap_horiz</mat-icon> Change
</button>
<button type="button" class="cx-link-btn cx-link-btn--danger" (click)="clearCustomer()">
<mat-icon>close</mat-icon> Remove
</button>
</div>
</div>
<div class="cx-cust__grid">
<div>
<span>CIF No.</span><strong>{{ selectedCustomer.cifNumber || '—' }}</strong>
</div>
<div>
<span>Client ID</span><strong>{{ selectedCustomer.clientId }}</strong>
</div>
<div>
<span>Mobile</span><strong>{{ selectedCustomer.mobileNo || '—' }}</strong>
</div>
<div>
<span>Branch</span><strong>{{ selectedCustomer.officeName || '—' }}</strong>
</div>
<div>
<span>Account</span><strong>{{ selectedCustomer.accountNo || 'No account' }}</strong>
</div>
<div>
<span>PAN on record</span>
<strong>
{{ selectedCustomer.kycPan || 'Not on record' }}
@if (selectedCustomer.kycPan) {
<span
class="cx-kyc cx-kyc--sm"
[class.cx-kyc--ok]="selectedCustomer.panVerified"
[class.cx-kyc--warn]="!selectedCustomer.panVerified"
>{{ selectedCustomer.panVerified ? 'Verified' : 'Not verified' }}</span
>
}
</strong>
</div>
</div>
</div>
}
} @else {
<div class="cx-cust__walkin">
<label class="cx-field">
<span>Customer Name{{ needsPan ? ' *' : '' }}</span>
<input
type="text"
[value]="walkInName"
(input)="walkInName = $any($event.target).value"
placeholder="Full name as on ID"
maxlength="150"
autocomplete="off"
/>
</label>
<label class="cx-field">
<span>Mobile</span>
<input
type="tel"
inputmode="numeric"
[value]="walkInMobile"
(input)="walkInMobile = $any($event.target).value"
placeholder="10-digit mobile number"
maxlength="15"
autocomplete="off"
/>
</label>
</div>
}
</div>
<!-- PAN -->
<div
class="cx-pan"
[class.cx-pan--required]="panMissing"
[class.cx-pan--invalid]="panFormatInvalid"
[class.cx-pan--valid]="panValue && !panFormatInvalid"
>
<div class="cx-pan__head">
<mat-icon>credit_card</mat-icon>
<span class="cx-pan__label">PAN Card</span>
<span class="cx-pan__tag" [class.cx-pan__tag--on]="needsPan">{{ needsPan ? 'Mandatory' : 'Optional' }}</span>
</div>
<div class="cx-pan__input">
<input
type="text"
[value]="panNumber"
(input)="panNumber = $any($event.target).value.toUpperCase()"
placeholder="ABCDE1234F"
maxlength="10"
autocomplete="off"
spellcheck="false"
aria-label="PAN number"
/>
@if (panValue && !panFormatInvalid) {
<mat-icon class="cx-pan__state cx-pan__state--ok">check_circle</mat-icon>
} @else if (panFormatInvalid) {
<mat-icon class="cx-pan__state cx-pan__state--bad">error</mat-icon>
}
</div>
<div class="cx-pan__hint">
@if (panFormatInvalid) {
PAN is 5 letters, 4 digits, 1 letter (e.g. ABCDE1234F).
} @else if (panFromKyc) {
<mat-icon>link</mat-icon> Fetched from the customer's KYC record.
} @else if (panMissing) {
Required: this exchange is above ₹50,000 (Income Tax Rule 114B / RBI).
} @else {
Mandatory for exchanges above ₹50,000.
}
</div>
</div>
</div>
</section>
<div class="cx-cols">
<!-- The cashier's count of a denomination and what this exchange does to it, updated as counts are typed. -->
<ng-template #drawerCell let-d="d">
@if (availableFor(d) === null) {
—
} @else {
In drawer <strong>{{ availableFor(d) }}</strong>
@if (netFor(d) !== 0) {
<span class="cx-after">
<mat-icon>{{ netFor(d) > 0 ? 'trending_up' : 'trending_down' }}</mat-icon>
<strong>{{ drawerAfter(d) }}</strong>
<small>({{ netFor(d) > 0 ? '+' : '' }}{{ netFor(d) }})</small>
</span>
}
}
</ng-template>
<!-- RECEIVED -->
<section class="cx-col cx-col--received">
<header class="cx-col-head">
<mat-icon>south</mat-icon>
<span class="cx-col-title">Received from customer</span>
<span class="cx-col-total">₹{{ receivedTotal | number: '1.2-2' }}</span>
</header>
<div class="cx-list">
@if (notes.length) {
<div class="cx-subhead">Bank Notes</div>
}
@for (d of notes; track d.id) {
<div class="cx-row cx-row--stock">
<span class="cx-chip">{{ d.label }}</span>
<div class="cx-step">
<button type="button" (click)="bump('r', d, -1)" [disabled]="countFor('r', d) === 0">−</button>
<input
type="number"
min="0"
[value]="countFor('r', d)"
(input)="onChange('r', d, $any($event.target).value)"
/>
<button type="button" (click)="bump('r', d, 1)">+</button>
</div>
<span class="cx-avail" [class.cx-avail--up]="netFor(d) > 0" [class.cx-avail--down]="netFor(d) < 0">
<ng-container *ngTemplateOutlet="drawerCell; context: { d: d }"></ng-container>
</span>
<span class="cx-amt" [class.cx-amt--zero]="countFor('r', d) === 0"
>₹{{ subtotal('r', d) | number: '1.2-2' }}</span
>
</div>
}
@if (coins.length) {
<div class="cx-subhead">Coins</div>
}
@for (d of coins; track d.id) {
<div class="cx-row cx-row--stock">
<span class="cx-chip cx-chip--coin">{{ d.label }}</span>
<div class="cx-step">
<button type="button" (click)="bump('r', d, -1)" [disabled]="countFor('r', d) === 0">−</button>
<input
type="number"
min="0"
[value]="countFor('r', d)"
(input)="onChange('r', d, $any($event.target).value)"
/>
<button type="button" (click)="bump('r', d, 1)">+</button>
</div>
<span class="cx-avail" [class.cx-avail--up]="netFor(d) > 0" [class.cx-avail--down]="netFor(d) < 0">
<ng-container *ngTemplateOutlet="drawerCell; context: { d: d }"></ng-container>
</span>
<span class="cx-amt" [class.cx-amt--zero]="countFor('r', d) === 0"
>₹{{ subtotal('r', d) | number: '1.2-2' }}</span
>
</div>
}
</div>
</section>
<!-- GIVEN -->
<section class="cx-col cx-col--given">
<header class="cx-col-head">
<mat-icon>north</mat-icon>
<span class="cx-col-title">Given to customer</span>
<span class="cx-col-total">₹{{ givenTotal | number: '1.2-2' }}</span>
</header>
<div class="cx-list">
@if (notes.length) {
<div class="cx-subhead">Bank Notes</div>
}
@for (d of notes; track d.id) {
<div class="cx-row cx-row--stock" [class.cx-row--over]="exceedsAvailable(d)">
<span class="cx-chip">{{ d.label }}</span>
<div class="cx-step">
<button type="button" (click)="bump('g', d, -1)" [disabled]="countFor('g', d) === 0">−</button>
<input
type="number"
min="0"
[value]="countFor('g', d)"
(input)="onChange('g', d, $any($event.target).value)"
/>
<button type="button" (click)="bump('g', d, 1)" [disabled]="!canGiveMore(d)">+</button>
</div>
<span
class="cx-avail"
[class.cx-avail--up]="netFor(d) > 0"
[class.cx-avail--down]="netFor(d) < 0"
[class.cx-avail--over]="exceedsAvailable(d)"
>
<ng-container *ngTemplateOutlet="drawerCell; context: { d: d }"></ng-container>
</span>
<span class="cx-amt" [class.cx-amt--zero]="countFor('g', d) === 0"
>₹{{ subtotal('g', d) | number: '1.2-2' }}</span
>
</div>
}
@if (coins.length) {
<div class="cx-subhead">Coins</div>
}
@for (d of coins; track d.id) {
<div class="cx-row cx-row--stock" [class.cx-row--over]="exceedsAvailable(d)">
<span class="cx-chip cx-chip--coin">{{ d.label }}</span>
<div class="cx-step">
<button type="button" (click)="bump('g', d, -1)" [disabled]="countFor('g', d) === 0">−</button>
<input
type="number"
min="0"
[value]="countFor('g', d)"
(input)="onChange('g', d, $any($event.target).value)"
/>
<button type="button" (click)="bump('g', d, 1)" [disabled]="!canGiveMore(d)">+</button>
</div>
<span
class="cx-avail"
[class.cx-avail--up]="netFor(d) > 0"
[class.cx-avail--down]="netFor(d) < 0"
[class.cx-avail--over]="exceedsAvailable(d)"
>
<ng-container *ngTemplateOutlet="drawerCell; context: { d: d }"></ng-container>
</span>
<span class="cx-amt" [class.cx-amt--zero]="countFor('g', d) === 0"
>₹{{ subtotal('g', d) | number: '1.2-2' }}</span
>
</div>
}
</div>
</section>
</div>
<!-- What this exchange does to the cashier's drawer, denomination by denomination. -->
@if (impactRows.length) {
<div class="cx-impact">
<div class="cx-impact__title"><mat-icon>inventory_2</mat-icon> Drawer after this exchange</div>
<div class="cx-impact__chips">
@for (r of impactRows; track r.id) {
<span
class="cx-impact__chip"
[class.cx-impact__chip--up]="r.net > 0"
[class.cx-impact__chip--down]="r.net < 0"
>
<b>{{ r.label }}</b>
{{ r.before }} → {{ r.after }}
<em>({{ r.net > 0 ? '+' : '' }}{{ r.net }})</em>
</span>
}
</div>
<div class="cx-impact__note">
The branch's total for each denomination moves by the same amount; the vault is not touched (Branch Ops → Branch
Cash Position).
</div>
</div>
}
<div class="cx-summary">
<div class="cx-sum">
<span>Received</span><strong>₹{{ receivedTotal | number: '1.2-2' }}</strong>
</div>
<mat-icon>swap_horiz</mat-icon>
<div class="cx-sum">
<span>Given</span><strong>₹{{ givenTotal | number: '1.2-2' }}</strong>
</div>
<div class="cx-sum">
<span>Difference</span>
<strong [class.cx-bad]="difference !== 0" [class.cx-good]="matches">₹{{ difference | number: '1.2-2' }}</strong>
</div>
</div>
@if (receivedTotal > 0 && !matches) {
<div class="cx-warn"><mat-icon>warning</mat-icon> Received and Given totals must match (net ₹0).</div>
}
@if (hasGivenOverdraft) {
<div class="cx-overdraft-card">
<div class="cx-overdraft-card__title">
<mat-icon class="cx-overdraft-card__ico">block</mat-icon>
<span>Insufficient stock — cannot give to customer</span>
</div>
<div class="cx-overdraft-card__rows">
@for (item of givenOverdraftItems; track item.label) {
<div class="cx-overdraft-card__row">
<span class="cx-overdraft-card__chip">{{ item.label }}</span>
<span class="cx-overdraft-card__stat"
><span class="cx-overdraft-card__lbl">Need</span><strong>{{ item.requested }}</strong></span
>
<span class="cx-overdraft-card__dot">·</span>
<span class="cx-overdraft-card__stat"
><span class="cx-overdraft-card__lbl">Available</span><strong>{{ item.available }}</strong></span
>
<span class="cx-overdraft-card__short">Short: {{ item.requested - item.available }}</span>
</div>
}
</div>
</div>
}
@if (panMissing) {
<div class="cx-warn cx-warn--pan">
<mat-icon>badge</mat-icon> Enter the customer's PAN to proceed (amount above ₹50,000).
</div>
}
@if (!customerIdentified && !loadingCustomer) {
<div class="cx-warn cx-warn--pan">
<mat-icon>person_search</mat-icon>
{{ customerMode === 'existing' ? 'Search and select the customer.' : "Enter the walk-in customer's name." }}
</div>
}
<div class="cx-actions">
<label class="cx-field cx-field--note">
<span>Note</span>
<input type="text" [value]="note" (input)="note = $any($event.target).value" placeholder="Optional" />
</label>
<button type="button" class="cx-btn cx-btn--primary" [disabled]="!valid || saving" (click)="submit()">
<mat-icon>swap_horiz</mat-icon> {{ saving ? 'Saving…' : 'Confirm Exchange' }}
</button>
</div>
</div>