- 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>
489 lines
19 KiB
HTML
489 lines
19 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="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>
|