diff --git a/src/app/cashier/cash-exchange-entry/cash-exchange-entry.component.html b/src/app/cashier/cash-exchange-entry/cash-exchange-entry.component.html index 4950989e4..e08dc73b0 100644 --- a/src/app/cashier/cash-exchange-entry/cash-exchange-entry.component.html +++ b/src/app/cashier/cash-exchange-entry/cash-exchange-entry.component.html @@ -22,6 +22,49 @@ + +
+
+ {{ isAccountHolder ? 'check_box' : 'check_box_outline_blank' }} + Account Holder +
+ + @if (isAccountHolder) { + + } @else { + person_outline Walk-in Customer + } + + @if (needsIdProof) { +
+ warning + Amount ≥ ₹50,000 — Customer PAN / Aadhaar required (RBI) +
+ + } +
+
@@ -147,14 +190,42 @@ @if (receivedTotal > 0 && !matches) {
warning Received and Given totals must match (net ₹0).
} + @if (hasGivenOverdraft) { +
+
+ block + Insufficient stock — cannot give to customer +
+
+ @for (item of givenOverdraftItems; track item.label) { +
+ {{ item.label }} + Need{{ item.requested }} + · + Available{{ item.available }} + Short: {{ item.requested - item.available }} +
+ } +
+
+ } + @if (needsIdProof && !customerIdProof.trim()) { +
+ badge Enter customer PAN / Aadhaar to proceed (amount ≥ ₹50,000). +
+ }
-
diff --git a/src/app/cashier/cash-exchange-entry/cash-exchange-entry.component.scss b/src/app/cashier/cash-exchange-entry/cash-exchange-entry.component.scss index 687d4ac0f..9c4277912 100644 --- a/src/app/cashier/cash-exchange-entry/cash-exchange-entry.component.scss +++ b/src/app/cashier/cash-exchange-entry/cash-exchange-entry.component.scss @@ -73,6 +73,208 @@ } } +/* ── Customer info bar ── */ +.cx-customer-bar { + display: flex; + align-items: center; + gap: 12px; + padding: 10px 14px; + background: #f8fafc; + border: 1px solid #e2e8f0; + border-radius: 10px; + flex-wrap: wrap; +} + +.cx-customer-toggle { + display: flex; + align-items: center; + gap: 6px; + font-size: 13px; + font-weight: 600; + color: #475569; + cursor: pointer; + user-select: none; + + mat-icon { + font-size: 20px; + height: 20px; + width: 20px; + color: #94a3b8; + } + + &--active { + color: #1e40af; + + mat-icon { + color: #1e40af; + } + } +} + +.cx-walk-in { + display: flex; + align-items: center; + gap: 4px; + font-size: 12px; + color: #64748b; + + mat-icon { + font-size: 16px; + height: 16px; + width: 16px; + } +} + +.cx-pan-required { + display: flex; + align-items: center; + gap: 6px; + padding: 4px 10px; + background: #fffbeb; + border: 1px solid #fcd34d; + border-radius: 6px; + font-size: 12px; + font-weight: 600; + color: #92400e; + + mat-icon { + font-size: 16px; + height: 16px; + width: 16px; + color: #d97706; + } +} + +.cx-field--inline { + flex-direction: row; + align-items: center; + gap: 8px; + + span { + white-space: nowrap; + } + + input { + width: 180px; + } +} + +.cx-field--pan input { + width: 160px; + border-color: #fcd34d; + + &:focus { + outline: none; + border-color: #d97706; + } +} + +.cx-warn--pan { + background: #eff6ff; + border-color: #bfdbfe; + color: #1e40af; + + mat-icon { + color: #2563eb; + } +} + +.cx-overdraft-card { + background: #fff5f5; + border: 1.5px solid #fca5a5; + border-left: 4px solid #ef4444; + border-radius: 10px; + overflow: hidden; + + &__title { + display: flex; + align-items: center; + gap: 8px; + padding: 9px 14px; + font-size: 13px; + font-weight: 700; + color: #7f1d1d; + background: #fef2f2; + border-bottom: 1px solid #fecaca; + } + + &__ico { + color: #ef4444; + font-size: 18px; + height: 18px; + width: 18px; + flex-shrink: 0; + } + + &__rows { + display: flex; + flex-direction: column; + padding: 4px 0; + } + + &__row { + display: flex; + align-items: center; + gap: 10px; + padding: 7px 14px; + font-size: 12.5px; + color: #7f1d1d; + + &:not(:last-child) { + border-bottom: 1px dashed #fecaca; + } + } + + &__chip { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 3px 10px; + background: linear-gradient(135deg, #fee2e2, #fecaca); + border: 1px solid #fca5a5; + border-radius: 6px; + font-size: 12px; + font-weight: 800; + color: #991b1b; + min-width: 52px; + letter-spacing: 0.01em; + } + + &__stat { + display: flex; + align-items: center; + gap: 4px; + + strong { + font-weight: 800; + font-variant-numeric: tabular-nums; + } + } + + &__lbl { + font-size: 11px; + color: #b91c1c; + font-weight: 600; + } + + &__dot { + color: #fca5a5; + font-weight: 700; + font-size: 14px; + } + + &__short { + margin-left: auto; + padding: 2px 9px; + background: #fff; + border: 1.5px solid #f87171; + border-radius: 999px; + font-size: 11px; + font-weight: 800; + color: #b91c1c; + } +} + .cx-cols { display: grid; grid-template-columns: 1fr 1fr; diff --git a/src/app/cashier/cash-exchange-entry/cash-exchange-entry.component.ts b/src/app/cashier/cash-exchange-entry/cash-exchange-entry.component.ts index 5d1bc6428..383cb57d6 100644 --- a/src/app/cashier/cash-exchange-entry/cash-exchange-entry.component.ts +++ b/src/app/cashier/cash-exchange-entry/cash-exchange-entry.component.ts @@ -45,7 +45,16 @@ export class CashExchangeEntryComponent implements OnInit { note = ''; saving = false; - /** All denominations (notes then coins) — used for totals and the submit payload. */ + // ── Customer info ── + isAccountHolder = false; + accountNo = ''; + customerIdProof = ''; + + // ── Cashier position (for denomination overdraft check) ── + private availableMap = new Map(); + positionLoaded = false; + + /** All denominations (notes then coins) */ get denoms(): Denom[] { return [ ...this.notes, @@ -58,7 +67,7 @@ export class CashExchangeEntryComponent implements OnInit { this.exchangeDate = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; this.service.getActiveDenominations().subscribe({ next: (list) => { - const rows: Denom[] = (list || []).map((x) => ({ + const rows: Denom[] = (list || []).map((x: any) => ({ id: x.id, value: x.denomination, kind: x.kind, @@ -72,6 +81,18 @@ export class CashExchangeEntryComponent implements OnInit { this.coins = []; } }); + this.service.getCashierPositionForMe().subscribe({ + next: (res: any) => { + this.availableMap.clear(); + ((res?.denominations || []) as any[]).forEach((d: any) => { + if (d.denominationId) this.availableMap.set(Number(d.denominationId), Number(d.count ?? 0)); + }); + this.positionLoaded = true; + }, + error: () => { + this.positionLoaded = true; + } + }); } countFor(side: 'r' | 'g', d: Denom): number { @@ -107,23 +128,53 @@ export class CashExchangeEntryComponent implements OnInit { return this.receivedTotal > 0 && this.receivedTotal === this.givenTotal; } + /** Above ₹50,000 PAN/ID is mandatory per RBI guideline */ + get needsIdProof(): boolean { + return this.receivedTotal >= 50000; + } + + /** Denominations where cashier is giving more than currently available. */ + get givenOverdraftItems(): Array<{ label: string; available: number; requested: number }> { + if (!this.positionLoaded) return []; + return this.denoms + .map((d) => ({ label: d.label, available: this.availableMap.get(d.id) ?? 0, requested: this.countFor('g', d) })) + .filter((item) => item.requested > 0 && item.requested > item.available); + } + + get hasGivenOverdraft(): boolean { + return this.givenOverdraftItems.length > 0; + } + + get valid(): boolean { + return this.matches && (!this.needsIdProof || this.customerIdProof.trim().length > 0) && !this.hasGivenOverdraft; + } + + toggleAccountHolder(): void { + this.isAccountHolder = !this.isAccountHolder; + if (!this.isAccountHolder) this.accountNo = ''; + } + clear(): void { this.receivedCounts = {}; this.givenCounts = {}; + this.note = ''; + this.accountNo = ''; + this.customerIdProof = ''; + this.isAccountHolder = false; } submit(): void { - if (!this.matches || this.saving) { - return; - } + if (!this.valid || this.saving) return; this.saving = true; const build = (side: 'r' | 'g') => this.denoms .filter((d) => this.countFor(side, d) > 0) .map((d) => ({ denominationId: d.id, denominationValue: d.value, count: this.countFor(side, d) })); - const payload = { + const payload: any = { exchangeDate: this.exchangeDate, note: this.note || null, + customerIdProof: this.customerIdProof.trim() || null, + accountNo: this.isAccountHolder && this.accountNo.trim() ? this.accountNo.trim() : null, received: build('r'), given: build('g') }; @@ -131,7 +182,6 @@ export class CashExchangeEntryComponent implements OnInit { next: () => { this.snackBar.open('Cash exchange recorded', 'Close', { duration: 3000 }); this.clear(); - this.note = ''; this.saving = false; }, error: () => { diff --git a/src/app/cashier/cash-history/cash-history.component.html b/src/app/cashier/cash-history/cash-history.component.html index 69c34a4d7..ffb27f148 100644 --- a/src/app/cashier/cash-history/cash-history.component.html +++ b/src/app/cashier/cash-history/cash-history.component.html @@ -52,6 +52,12 @@ + + +
+ +
+
@@ -122,7 +128,13 @@
- {{ detail.row.type === 'settlement' ? 'account_balance_wallet' : 'swap_horiz' }} + {{ + detail.row.type === 'settlement' + ? 'account_balance_wallet' + : detail.row.type === 'exchange' + ? 'currency_exchange' + : 'swap_horiz' + }}
{{ typeLabel(detail.row.type) }} #{{ detail.row.id }} @@ -241,8 +253,163 @@ @if (detail.loading) {
Loading denomination details…
- } @else if (detail.denominations.length === 0) { + } @else if ( + detail.row.type === 'exchange' && + detail.receivedDenominations.length === 0 && + detail.givenDenominations.length === 0 + ) {
No denomination data recorded.
+ } @else if (detail.row.type !== 'exchange' && detail.denominations.length === 0) { +
No denomination data recorded.
+ } @else if (detail.row.type === 'exchange') { + +
south Received from Customer
+ @if (notes(detail.receivedDenominations).length > 0) { +
Notes
+ + + + + + + + + + @for (d of notes(detail.receivedDenominations); track d.denominationValue) { + + + + + + } + + + + + + + + +
DenominationCountTotal
+ ₹{{ d.denominationValue }} + {{ d.count }}₹{{ d.totalAmount | number: '1.2-2' }}
Subtotal + {{ totalCount(notes(detail.receivedDenominations)) }} + + ₹{{ subtotal(notes(detail.receivedDenominations)) | number: '1.2-2' }} +
+ } + @if (coins(detail.receivedDenominations).length > 0) { +
Coins
+ + + + + + + + + + @for (d of coins(detail.receivedDenominations); track d.denominationValue) { + + + + + + } + + + + + + + + +
DenominationCountTotal
+ ₹{{ d.denominationValue }} + {{ d.count }}₹{{ d.totalAmount | number: '1.2-2' }}
Subtotal + {{ totalCount(coins(detail.receivedDenominations)) }} + + ₹{{ subtotal(coins(detail.receivedDenominations)) | number: '1.2-2' }} +
+ } + + +
north Given to Customer
+ @if (notes(detail.givenDenominations).length > 0) { +
Notes
+ + + + + + + + + + @for (d of notes(detail.givenDenominations); track d.denominationValue) { + + + + + + } + + + + + + + + +
DenominationCountTotal
+ ₹{{ d.denominationValue }} + {{ d.count }}₹{{ d.totalAmount | number: '1.2-2' }}
Subtotal + {{ totalCount(notes(detail.givenDenominations)) }} + + ₹{{ subtotal(notes(detail.givenDenominations)) | number: '1.2-2' }} +
+ } + @if (coins(detail.givenDenominations).length > 0) { +
Coins
+ + + + + + + + + + @for (d of coins(detail.givenDenominations); track d.denominationValue) { + + + + + + } + + + + + + + + +
DenominationCountTotal
+ ₹{{ d.denominationValue }} + {{ d.count }}₹{{ d.totalAmount | number: '1.2-2' }}
Subtotal + {{ totalCount(coins(detail.givenDenominations)) }} + + ₹{{ subtotal(coins(detail.givenDenominations)) | number: '1.2-2' }} +
+ } + + +
+ Grand Total + {{ totalCount(detail.receivedDenominations) }} notes/coins + ₹{{ detail.row.amount | number: '1.2-2' }} +
} @else { @if (notes(detail.denominations).length > 0) { diff --git a/src/app/cashier/cash-history/cash-history.component.scss b/src/app/cashier/cash-history/cash-history.component.scss index a759dc8fe..1db02c17d 100644 --- a/src/app/cashier/cash-history/cash-history.component.scss +++ b/src/app/cashier/cash-history/cash-history.component.scss @@ -312,6 +312,38 @@ border-color: #fcd34d; } +// Cash exchange direction labels +.cx-dir-label { + display: flex; + align-items: center; + gap: 6px; + margin: 8px 20px 4px; + padding: 6px 10px; + border-radius: 6px; + font-size: 11px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.06em; + + mat-icon { + font-size: 14px; + height: 14px; + width: 14px; + } + + &--received { + background: #eff6ff; + color: #1d4ed8; + border: 1px solid #bfdbfe; + } + + &--given { + background: #fef3c7; + color: #92400e; + border: 1px solid #fcd34d; + } +} + .pg-btn--danger { background: linear-gradient(135deg, #ef4444, #b91c1c); color: #fff; diff --git a/src/app/cashier/cash-history/cash-history.component.ts b/src/app/cashier/cash-history/cash-history.component.ts index cf1aaa7fd..40f291e78 100644 --- a/src/app/cashier/cash-history/cash-history.component.ts +++ b/src/app/cashier/cash-history/cash-history.component.ts @@ -30,7 +30,7 @@ interface TxnRow { reversedBy: number | null; reversedAt: string | null; reverseReason: string | null; - type: 'transfer' | 'settlement' | 'inter'; + type: 'transfer' | 'settlement' | 'inter' | 'exchange'; } interface DenomDetail { @@ -45,6 +45,8 @@ interface DenomDetail { interface DetailView { row: TxnRow; denominations: DenomDetail[]; + receivedDenominations: DenomDetail[]; + givenDenominations: DenomDetail[]; morningTotal: number; loading: boolean; } @@ -73,6 +75,7 @@ export class CashHistoryComponent implements OnInit { transfers: TxnRow[] = []; settlements: TxnRow[] = []; interCashier: TxnRow[] = []; + exchanges: TxnRow[] = []; loading = false; private denomKindMap = new Map(); @@ -98,8 +101,9 @@ export class CashHistoryComponent implements OnInit { forkJoin({ transfers: this.service.getCashTransfers().pipe(catchError(() => of([]))), settlements: this.service.getCashSettlements().pipe(catchError(() => of([]))), - inter: this.service.getInterCashierTransfers().pipe(catchError(() => of([]))) - }).subscribe(({ transfers, settlements, inter }) => { + inter: this.service.getInterCashierTransfers().pipe(catchError(() => of([]))), + exchanges: this.service.getCashExchanges().pipe(catchError(() => of([]))) + }).subscribe(({ transfers, settlements, inter, exchanges }) => { this.transfers = (transfers as any[]).map((t) => ({ id: t.id, date: t.transferDate, @@ -146,12 +150,34 @@ export class CashHistoryComponent implements OnInit { reverseReason: i.reverseReason ?? null, type: 'inter' })); + this.exchanges = (exchanges as any[]).map((e) => ({ + id: e.id, + date: e.exchangeDate, + fromParty: e.accountNo ? `Account ${e.accountNo}` : 'Walk-in Customer', + toParty: e.performedByUsername || '—', + amount: e.exchangeAmount, + note: e.note, + isReversed: e.isReversed ?? false, + performedByUsername: e.performedByUsername, + createdAt: e.createdAt, + reversedBy: e.reversedBy ?? null, + reversedAt: e.reversedAt ?? null, + reverseReason: e.reverseReason ?? null, + type: 'exchange' + })); this.loading = false; }); } viewDetail(row: TxnRow): void { - this.detail = { row, denominations: [], morningTotal: 0, loading: true }; + this.detail = { + row, + denominations: [], + receivedDenominations: [], + givenDenominations: [], + morningTotal: 0, + loading: true + }; if (row.type === 'settlement' && row.fromCashierId) { forkJoin({ @@ -180,18 +206,27 @@ export class CashHistoryComponent implements OnInit { let obs$; if (row.type === 'transfer') obs$ = this.service.getCashTransferById(row.id); else if (row.type === 'settlement') obs$ = this.service.getCashSettlementById(row.id); + else if (row.type === 'exchange') obs$ = this.service.getCashExchange(row.id); else obs$ = this.service.getInterCashierById(row.id); obs$.pipe(catchError(() => of(null))).subscribe((data: any) => { if (this.detail) { - this.detail.denominations = ((data?.details || []) as any[]).map((d: any) => ({ + const mapDenom = (d: any): DenomDetail => ({ denominationId: d.denominationId ?? null, denominationValue: d.denominationValue, kind: this.denomKindMap.get(d.denominationId) ?? (d.denominationValue <= 5 ? 'coin' : 'note'), morningCount: 0, count: d.count, totalAmount: d.totalAmount - })); + }); + if (row.type === 'exchange') { + const all: any[] = data?.details || []; + this.detail.receivedDenominations = all.filter((d: any) => d.direction === 'RECEIVED').map(mapDenom); + this.detail.givenDenominations = all.filter((d: any) => d.direction === 'GIVEN').map(mapDenom); + this.detail.denominations = this.detail.receivedDenominations; + } else { + this.detail.denominations = (data?.details || []).map(mapDenom); + } this.detail.loading = false; } }); @@ -223,6 +258,7 @@ export class CashHistoryComponent implements OnInit { let obs$; if (type === 'transfer') obs$ = this.service.reverseCashTransfer(id, reason); else if (type === 'settlement') obs$ = this.service.reverseCashSettlement(id, reason); + else if (type === 'exchange') obs$ = this.service.reverseCashExchange(id, reason); else obs$ = this.service.reverseInterCashierTransfer(id, reason); obs$.pipe(catchError(() => of(null))).subscribe((res: any) => { @@ -236,7 +272,10 @@ export class CashHistoryComponent implements OnInit { } typeLabel(type: string): string { - return type === 'transfer' ? 'Cash Transfer' : type === 'settlement' ? 'Cash Settlement' : 'Inter-Cashier Transfer'; + if (type === 'transfer') return 'Cash Transfer'; + if (type === 'settlement') return 'Cash Settlement'; + if (type === 'exchange') return 'Cash Exchange'; + return 'Inter-Cashier Transfer'; } notes(denoms: DenomDetail[]): DenomDetail[] { diff --git a/src/app/cashier/cashier-shared.scss b/src/app/cashier/cashier-shared.scss index 3fc434cb8..f544f9a0f 100644 --- a/src/app/cashier/cashier-shared.scss +++ b/src/app/cashier/cashier-shared.scss @@ -373,3 +373,58 @@ font-variant-numeric: tabular-nums; } } + +/* ── Cash Exchange section header ───────────────────────────────────── */ +.closing-exchange-head { + display: flex; + align-items: flex-start; + gap: 10px; + padding: 12px 16px 10px; + border-bottom: 1px solid #e2e8f0; + margin-bottom: 2px; + + mat-icon { + color: #0f4f3c; + font-size: 22px; + height: 22px; + width: 22px; + margin-top: 2px; + flex-shrink: 0; + } +} + +.closing-exchange-title { + font-size: 14px; + font-weight: 700; + color: #1a1a1a; +} + +.closing-exchange-sub { + font-size: 12px; + color: #64748b; + margin-top: 2px; +} + +// Excess Transaction Report — type badges +.etr-badge { + display: inline-flex; + align-items: center; + height: 22px; + padding: 0 8px; + border-radius: 5px; + font-size: 11px; + font-weight: 700; + white-space: nowrap; + + &--savings { + background: #f0fdf4; + color: #15803d; + border: 1px solid #bbf7d0; + } + + &--exchange { + background: #eff6ff; + color: #1d4ed8; + border: 1px solid #bfdbfe; + } +} diff --git a/src/app/cashier/cashier.service.ts b/src/app/cashier/cashier.service.ts index 7a914d5af..e1be9b5c3 100644 --- a/src/app/cashier/cashier.service.ts +++ b/src/app/cashier/cashier.service.ts @@ -30,6 +30,9 @@ export class CashierService { getCashExchange(id: number): Observable { return this.http.get(`/cashexchange/${id}`); } + reverseCashExchange(id: number, reason: string): Observable { + return this.http.post(`/cashexchange/${id}/reverse`, { reason }); + } // ── Tellers / Cashiers ── getTellers(): Observable { diff --git a/src/app/cashier/closing-cash-report/closing-cash-report.component.html b/src/app/cashier/closing-cash-report/closing-cash-report.component.html index 607d3662d..2576bf0e7 100644 --- a/src/app/cashier/closing-cash-report/closing-cash-report.component.html +++ b/src/app/cashier/closing-cash-report/closing-cash-report.component.html @@ -133,8 +133,109 @@ } - @if (loaded && rows.length === 0) { + @if (loaded && rows.length === 0 && exchangeRows.length === 0) {
No cash denomination data for this date.
}
+ + + @if (exchangeRows.length > 0) { +
+
+ swap_horiz +
+
Cash Exchanges
+
Denomination swaps recorded on this date (net cash impact: ₹0.00)
+
+
+ + @if (exchangeNoteRows.length > 0) { + + + + + + + + + + + + + + + @for (r of exchangeNoteRows; track r.denominationId) { + + + + + + + + + + } + +
DenominationReceived PcsReceived AmtGiven PcsGiven AmtNet PcsNet Amount
+ ₹{{ r.denominationValue }} + {{ r.receivedCount }}₹{{ r.receivedAmount | number: '1.2-2' }}{{ r.givenCount }}₹{{ r.givenAmount | number: '1.2-2' }} + {{ r.netCount > 0 ? '+' : '' }}{{ r.netCount }} + + {{ r.netCount >= 0 ? '' : '−' }}₹{{ + (r.netAmount < 0 ? -r.netAmount : r.netAmount) | number: '1.2-2' + }} +
+ } + + @if (exchangeCoinRows.length > 0) { + + + + + + + + + + + + + + + @for (r of exchangeCoinRows; track r.denominationId) { + + + + + + + + + + } + +
DenominationReceived PcsReceived AmtGiven PcsGiven AmtNet PcsNet Amount
+ ₹{{ r.denominationValue }} + {{ r.receivedCount }}₹{{ r.receivedAmount | number: '1.2-2' }}{{ r.givenCount }}₹{{ r.givenAmount | number: '1.2-2' }} + {{ r.netCount > 0 ? '+' : '' }}{{ r.netCount }} + + {{ r.netCount >= 0 ? '' : '−' }}₹{{ + (r.netAmount < 0 ? -r.netAmount : r.netAmount) | number: '1.2-2' + }} +
+ } +
+ }
diff --git a/src/app/cashier/closing-cash-report/closing-cash-report.component.ts b/src/app/cashier/closing-cash-report/closing-cash-report.component.ts index 175710636..01a468490 100644 --- a/src/app/cashier/closing-cash-report/closing-cash-report.component.ts +++ b/src/app/cashier/closing-cash-report/closing-cash-report.component.ts @@ -29,6 +29,7 @@ export class ClosingCashReportComponent implements OnInit { date = ''; rows: any[] = []; + exchangeRows: any[] = []; depositTotal = 0; withdrawalTotal = 0; netTotal = 0; @@ -41,6 +42,12 @@ export class ClosingCashReportComponent implements OnInit { get coinRows(): any[] { return this.rows.filter((r) => r.kind === 'coin'); } + get exchangeNoteRows(): any[] { + return this.exchangeRows.filter((r) => r.kind !== 'coin'); + } + get exchangeCoinRows(): any[] { + return this.exchangeRows.filter((r) => r.kind === 'coin'); + } ngOnInit(): void { const d = new Date(); @@ -53,6 +60,7 @@ export class ClosingCashReportComponent implements OnInit { this.service.closingCashReport(this.date).subscribe({ next: (res: any) => { this.rows = res?.rows || []; + this.exchangeRows = res?.exchangeRows || []; this.depositTotal = Number(res?.depositTotal || 0); this.withdrawalTotal = Number(res?.withdrawalTotal || 0); this.netTotal = Number(res?.netTotal || 0); @@ -61,6 +69,7 @@ export class ClosingCashReportComponent implements OnInit { }, error: () => { this.rows = []; + this.exchangeRows = []; this.depositTotal = 0; this.withdrawalTotal = 0; this.netTotal = 0; diff --git a/src/app/cashier/currency-exchange-detail/currency-exchange-detail.component.html b/src/app/cashier/currency-exchange-detail/currency-exchange-detail.component.html index 790f31737..69f80ffe3 100644 --- a/src/app/cashier/currency-exchange-detail/currency-exchange-detail.component.html +++ b/src/app/cashier/currency-exchange-detail/currency-exchange-detail.component.html @@ -18,6 +18,10 @@ + @if (loading) { +
Loading…
+ } +
diff --git a/src/app/cashier/excess-transaction-report/excess-transaction-report.component.html b/src/app/cashier/excess-transaction-report/excess-transaction-report.component.html index 84270cd17..6615b1e79 100644 --- a/src/app/cashier/excess-transaction-report/excess-transaction-report.component.html +++ b/src/app/cashier/excess-transaction-report/excess-transaction-report.component.html @@ -36,26 +36,36 @@
+ + - @for (r of rows; track r.txnId) { + @for (r of rows; track $index) { + + } @if (loaded && rows.length === 0) { - + } diff --git a/src/app/current-accounts/current-account-actions/current-account-transactions/current-account-transactions.component.html b/src/app/current-accounts/current-account-actions/current-account-transactions/current-account-transactions.component.html index d5bed2bd6..038c62eb4 100644 --- a/src/app/current-accounts/current-account-actions/current-account-transactions/current-account-transactions.component.html +++ b/src/app/current-accounts/current-account-actions/current-account-transactions/current-account-transactions.component.html @@ -326,6 +326,31 @@ Use ₹{{ denominationTotal | number: '1.2-2' }} as Transaction Amount + + @if (transactionType.withdrawal && hasWithdrawalDenomOverdraft) { +
+
+ + Insufficient denomination stock — cannot proceed +
+
+ @for (item of withdrawalDenomOverdraft; track item.label) { +
+ {{ item.label }} + Need{{ item.requested }} + · + Available{{ item.available }} + Short: {{ item.requested - item.available }} +
+ } +
+
+ } + @if (openingLoaded && openingDenoms.length > 0) {
{{ 'labels.buttons.Submit' | translate }} diff --git a/src/app/current-accounts/current-account-actions/current-account-transactions/current-account-transactions.component.scss b/src/app/current-accounts/current-account-actions/current-account-transactions/current-account-transactions.component.scss index 84588d28c..9211b069a 100644 --- a/src/app/current-accounts/current-account-actions/current-account-transactions/current-account-transactions.component.scss +++ b/src/app/current-accounts/current-account-actions/current-account-transactions/current-account-transactions.component.scss @@ -1094,3 +1094,98 @@ $red: #b91c1c; font-variant-numeric: tabular-nums; color: #0d1b2a; } + +.sat__denom-warn { + margin: 8px 14px; + background: #fff5f5; + border: 1.5px solid #fca5a5; + border-left: 4px solid #ef4444; + border-radius: 10px; + overflow: hidden; + + &-title { + display: flex; + align-items: center; + gap: 8px; + padding: 9px 14px; + font-size: 13px; + font-weight: 700; + color: #7f1d1d; + background: #fef2f2; + border-bottom: 1px solid #fecaca; + } + + &-ico { + color: #ef4444; + font-size: 14px; + flex-shrink: 0; + } + + &-rows { + display: flex; + flex-direction: column; + padding: 4px 0; + } + + &-row { + display: flex; + align-items: center; + gap: 10px; + padding: 7px 14px; + font-size: 12.5px; + color: #7f1d1d; + + &:not(:last-child) { + border-bottom: 1px dashed #fecaca; + } + } + + &-chip { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 3px 10px; + background: linear-gradient(135deg, #fee2e2, #fecaca); + border: 1px solid #fca5a5; + border-radius: 6px; + font-size: 12px; + font-weight: 800; + color: #991b1b; + min-width: 52px; + letter-spacing: 0.01em; + } + + &-stat { + display: flex; + align-items: center; + gap: 4px; + + strong { + font-weight: 800; + font-variant-numeric: tabular-nums; + } + } + + &-lbl { + font-size: 11px; + color: #b91c1c; + font-weight: 600; + } + + &-dot { + color: #fca5a5; + font-weight: 700; + font-size: 14px; + } + + &-short { + margin-left: auto; + padding: 2px 9px; + background: #fff; + border: 1.5px solid #f87171; + border-radius: 999px; + font-size: 11px; + font-weight: 800; + color: #b91c1c; + } +} diff --git a/src/app/current-accounts/current-account-actions/current-account-transactions/current-account-transactions.component.ts b/src/app/current-accounts/current-account-actions/current-account-transactions/current-account-transactions.component.ts index d18a28fc4..413e198f5 100644 --- a/src/app/current-accounts/current-account-actions/current-account-transactions/current-account-transactions.component.ts +++ b/src/app/current-accounts/current-account-actions/current-account-transactions/current-account-transactions.component.ts @@ -389,6 +389,24 @@ export class CurrentAccountTransactionsComponent implements OnInit { return this.denominationTotal > 0 && formAmt === this.denominationTotal; } + get withdrawalDenomOverdraft(): Array<{ label: string; available: number; requested: number }> { + if (!this.transactionType.withdrawal || !this.openingLoaded || this.openingDenoms.length === 0) return []; + const openMap = new Map(); + for (const d of this.openingDenoms) { + openMap.set(`${d.kind}:${d.denominationValue}`, Number(d.count ?? 0)); + } + return [ + ...this.notes, + ...this.coins + ] + .map((d) => ({ label: d.label, available: openMap.get(this.denomKey(d)) ?? 0, requested: this.countFor(d) })) + .filter((item) => item.requested > 0 && item.requested > item.available); + } + + get hasWithdrawalDenomOverdraft(): boolean { + return this.withdrawalDenomOverdraft.length > 0; + } + applyDenominationTotal(): void { this.currentAccountTransactionForm.controls['transactionAmount'].setValue(this.denominationTotal); this.currentAccountTransactionForm.controls['transactionAmount'].markAsDirty(); diff --git a/src/app/deposits/fixed-deposits/fixed-deposits-account-actions/fixed-deposits-cash-transaction/fixed-deposits-cash-transaction.component.html b/src/app/deposits/fixed-deposits/fixed-deposits-account-actions/fixed-deposits-cash-transaction/fixed-deposits-cash-transaction.component.html index 50b747955..ac9b16a5b 100644 --- a/src/app/deposits/fixed-deposits/fixed-deposits-account-actions/fixed-deposits-cash-transaction/fixed-deposits-cash-transaction.component.html +++ b/src/app/deposits/fixed-deposits/fixed-deposits-account-actions/fixed-deposits-cash-transaction/fixed-deposits-cash-transaction.component.html @@ -299,6 +299,31 @@ Use ₹{{ denominationTotal | number: '1.2-2' }} as Transaction Amount
+ + @if (transactionType.withdrawal && hasWithdrawalDenomOverdraft) { +
+
+ + Insufficient denomination stock — cannot proceed +
+
+ @for (item of withdrawalDenomOverdraft; track item.label) { +
+ {{ item.label }} + Need{{ item.requested }} + · + Available{{ item.available }} + Short: {{ item.requested - item.available }} +
+ } +
+
+ } + @if (openingLoaded && openingDenoms.length > 0) {
{{ 'labels.buttons.Submit' | translate }} diff --git a/src/app/deposits/fixed-deposits/fixed-deposits-account-actions/fixed-deposits-cash-transaction/fixed-deposits-cash-transaction.component.scss b/src/app/deposits/fixed-deposits/fixed-deposits-account-actions/fixed-deposits-cash-transaction/fixed-deposits-cash-transaction.component.scss index 515771824..ca898f512 100644 --- a/src/app/deposits/fixed-deposits/fixed-deposits-account-actions/fixed-deposits-cash-transaction/fixed-deposits-cash-transaction.component.scss +++ b/src/app/deposits/fixed-deposits/fixed-deposits-account-actions/fixed-deposits-cash-transaction/fixed-deposits-cash-transaction.component.scss @@ -1006,3 +1006,98 @@ $amber-d: #b45309; font-variant-numeric: tabular-nums; color: #0d1b2a; } + +.sat__denom-warn { + margin: 8px 14px; + background: #fff5f5; + border: 1.5px solid #fca5a5; + border-left: 4px solid #ef4444; + border-radius: 10px; + overflow: hidden; + + &-title { + display: flex; + align-items: center; + gap: 8px; + padding: 9px 14px; + font-size: 13px; + font-weight: 700; + color: #7f1d1d; + background: #fef2f2; + border-bottom: 1px solid #fecaca; + } + + &-ico { + color: #ef4444; + font-size: 14px; + flex-shrink: 0; + } + + &-rows { + display: flex; + flex-direction: column; + padding: 4px 0; + } + + &-row { + display: flex; + align-items: center; + gap: 10px; + padding: 7px 14px; + font-size: 12.5px; + color: #7f1d1d; + + &:not(:last-child) { + border-bottom: 1px dashed #fecaca; + } + } + + &-chip { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 3px 10px; + background: linear-gradient(135deg, #fee2e2, #fecaca); + border: 1px solid #fca5a5; + border-radius: 6px; + font-size: 12px; + font-weight: 800; + color: #991b1b; + min-width: 52px; + letter-spacing: 0.01em; + } + + &-stat { + display: flex; + align-items: center; + gap: 4px; + + strong { + font-weight: 800; + font-variant-numeric: tabular-nums; + } + } + + &-lbl { + font-size: 11px; + color: #b91c1c; + font-weight: 600; + } + + &-dot { + color: #fca5a5; + font-weight: 700; + font-size: 14px; + } + + &-short { + margin-left: auto; + padding: 2px 9px; + background: #fff; + border: 1.5px solid #f87171; + border-radius: 999px; + font-size: 11px; + font-weight: 800; + color: #b91c1c; + } +} diff --git a/src/app/deposits/fixed-deposits/fixed-deposits-account-actions/fixed-deposits-cash-transaction/fixed-deposits-cash-transaction.component.ts b/src/app/deposits/fixed-deposits/fixed-deposits-account-actions/fixed-deposits-cash-transaction/fixed-deposits-cash-transaction.component.ts index 6ae14b86b..79538fd5b 100644 --- a/src/app/deposits/fixed-deposits/fixed-deposits-account-actions/fixed-deposits-cash-transaction/fixed-deposits-cash-transaction.component.ts +++ b/src/app/deposits/fixed-deposits/fixed-deposits-account-actions/fixed-deposits-cash-transaction/fixed-deposits-cash-transaction.component.ts @@ -312,6 +312,24 @@ export class FixedDepositsCashTransactionComponent implements OnInit { return this.denominationTotal > 0 && formAmt === this.denominationTotal; } + get withdrawalDenomOverdraft(): Array<{ label: string; available: number; requested: number }> { + if (!this.transactionType.withdrawal || !this.openingLoaded || this.openingDenoms.length === 0) return []; + const openMap = new Map(); + for (const d of this.openingDenoms) { + openMap.set(`${d.kind}:${d.denominationValue}`, Number(d.count ?? 0)); + } + return [ + ...this.notes, + ...this.coins + ] + .map((d) => ({ label: d.label, available: openMap.get(this.denomKey(d)) ?? 0, requested: this.countFor(d) })) + .filter((item) => item.requested > 0 && item.requested > item.available); + } + + get hasWithdrawalDenomOverdraft(): boolean { + return this.withdrawalDenomOverdraft.length > 0; + } + applyDenominationTotal(): void { this.accountTransactionForm.controls['transactionAmount'].setValue(this.denominationTotal); this.accountTransactionForm.controls['transactionAmount'].markAsDirty(); diff --git a/src/app/deposits/recurring-deposits/recurring-deposits-account-actions/deposit-recurring-deposits-account/deposit-recurring-deposits-account.component.html b/src/app/deposits/recurring-deposits/recurring-deposits-account-actions/deposit-recurring-deposits-account/deposit-recurring-deposits-account.component.html index 6787d4a0a..3f4c8da95 100644 --- a/src/app/deposits/recurring-deposits/recurring-deposits-account-actions/deposit-recurring-deposits-account/deposit-recurring-deposits-account.component.html +++ b/src/app/deposits/recurring-deposits/recurring-deposits-account-actions/deposit-recurring-deposits-account/deposit-recurring-deposits-account.component.html @@ -299,6 +299,31 @@ Use ₹{{ denominationTotal | number: '1.2-2' }} as Transaction Amount
+ + @if (transactionType.withdrawal && hasWithdrawalDenomOverdraft) { +
+
+ + Insufficient denomination stock — cannot proceed +
+
+ @for (item of withdrawalDenomOverdraft; track item.label) { +
+ {{ item.label }} + Need{{ item.requested }} + · + Available{{ item.available }} + Short: {{ item.requested - item.available }} +
+ } +
+
+ } + @if (openingLoaded && openingDenoms.length > 0) {
{{ 'labels.buttons.Submit' | translate }} diff --git a/src/app/deposits/recurring-deposits/recurring-deposits-account-actions/deposit-recurring-deposits-account/deposit-recurring-deposits-account.component.scss b/src/app/deposits/recurring-deposits/recurring-deposits-account-actions/deposit-recurring-deposits-account/deposit-recurring-deposits-account.component.scss index 515771824..ca898f512 100644 --- a/src/app/deposits/recurring-deposits/recurring-deposits-account-actions/deposit-recurring-deposits-account/deposit-recurring-deposits-account.component.scss +++ b/src/app/deposits/recurring-deposits/recurring-deposits-account-actions/deposit-recurring-deposits-account/deposit-recurring-deposits-account.component.scss @@ -1006,3 +1006,98 @@ $amber-d: #b45309; font-variant-numeric: tabular-nums; color: #0d1b2a; } + +.sat__denom-warn { + margin: 8px 14px; + background: #fff5f5; + border: 1.5px solid #fca5a5; + border-left: 4px solid #ef4444; + border-radius: 10px; + overflow: hidden; + + &-title { + display: flex; + align-items: center; + gap: 8px; + padding: 9px 14px; + font-size: 13px; + font-weight: 700; + color: #7f1d1d; + background: #fef2f2; + border-bottom: 1px solid #fecaca; + } + + &-ico { + color: #ef4444; + font-size: 14px; + flex-shrink: 0; + } + + &-rows { + display: flex; + flex-direction: column; + padding: 4px 0; + } + + &-row { + display: flex; + align-items: center; + gap: 10px; + padding: 7px 14px; + font-size: 12.5px; + color: #7f1d1d; + + &:not(:last-child) { + border-bottom: 1px dashed #fecaca; + } + } + + &-chip { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 3px 10px; + background: linear-gradient(135deg, #fee2e2, #fecaca); + border: 1px solid #fca5a5; + border-radius: 6px; + font-size: 12px; + font-weight: 800; + color: #991b1b; + min-width: 52px; + letter-spacing: 0.01em; + } + + &-stat { + display: flex; + align-items: center; + gap: 4px; + + strong { + font-weight: 800; + font-variant-numeric: tabular-nums; + } + } + + &-lbl { + font-size: 11px; + color: #b91c1c; + font-weight: 600; + } + + &-dot { + color: #fca5a5; + font-weight: 700; + font-size: 14px; + } + + &-short { + margin-left: auto; + padding: 2px 9px; + background: #fff; + border: 1.5px solid #f87171; + border-radius: 999px; + font-size: 11px; + font-weight: 800; + color: #b91c1c; + } +} diff --git a/src/app/deposits/recurring-deposits/recurring-deposits-account-actions/deposit-recurring-deposits-account/deposit-recurring-deposits-account.component.ts b/src/app/deposits/recurring-deposits/recurring-deposits-account-actions/deposit-recurring-deposits-account/deposit-recurring-deposits-account.component.ts index 98b9c593c..9c48fa7ba 100644 --- a/src/app/deposits/recurring-deposits/recurring-deposits-account-actions/deposit-recurring-deposits-account/deposit-recurring-deposits-account.component.ts +++ b/src/app/deposits/recurring-deposits/recurring-deposits-account-actions/deposit-recurring-deposits-account/deposit-recurring-deposits-account.component.ts @@ -333,6 +333,24 @@ export class DepositRecurringDepositsAccountComponent implements OnInit { return this.denominationTotal > 0 && formAmt === this.denominationTotal; } + get withdrawalDenomOverdraft(): Array<{ label: string; available: number; requested: number }> { + if (!this.transactionType.withdrawal || !this.openingLoaded || this.openingDenoms.length === 0) return []; + const openMap = new Map(); + for (const d of this.openingDenoms) { + openMap.set(`${d.kind}:${d.denominationValue}`, Number(d.count ?? 0)); + } + return [ + ...this.notes, + ...this.coins + ] + .map((d) => ({ label: d.label, available: openMap.get(this.denomKey(d)) ?? 0, requested: this.countFor(d) })) + .filter((item) => item.requested > 0 && item.requested > item.available); + } + + get hasWithdrawalDenomOverdraft(): boolean { + return this.withdrawalDenomOverdraft.length > 0; + } + applyDenominationTotal(): void { this.depositRecurringDepositForm.controls['transactionAmount'].setValue(this.denominationTotal); this.depositRecurringDepositForm.controls['transactionAmount'].markAsDirty(); diff --git a/src/app/savings/saving-account-actions/savings-account-transactions/savings-account-transactions.component.html b/src/app/savings/saving-account-actions/savings-account-transactions/savings-account-transactions.component.html index 793502a94..143c88ec1 100644 --- a/src/app/savings/saving-account-actions/savings-account-transactions/savings-account-transactions.component.html +++ b/src/app/savings/saving-account-actions/savings-account-transactions/savings-account-transactions.component.html @@ -326,6 +326,31 @@ Use ₹{{ denominationTotal | number: '1.2-2' }} as Transaction Amount
+ + @if (transactionType.withdrawal && hasWithdrawalDenomOverdraft) { +
+
+ + Insufficient denomination stock — cannot proceed +
+
+ @for (item of withdrawalDenomOverdraft; track item.label) { +
+ {{ item.label }} + Need{{ item.requested }} + · + Available{{ item.available }} + Short: {{ item.requested - item.available }} +
+ } +
+
+ } + @if (openingLoaded && openingDenoms.length > 0) {
{{ 'labels.buttons.Submit' | translate }} diff --git a/src/app/savings/saving-account-actions/savings-account-transactions/savings-account-transactions.component.scss b/src/app/savings/saving-account-actions/savings-account-transactions/savings-account-transactions.component.scss index afca13297..e6a0990b0 100644 --- a/src/app/savings/saving-account-actions/savings-account-transactions/savings-account-transactions.component.scss +++ b/src/app/savings/saving-account-actions/savings-account-transactions/savings-account-transactions.component.scss @@ -827,6 +827,103 @@ $red: #b91c1c; } } +/* ─── Denomination overdraft warning (withdrawal) ─────────────────── */ + +.sat__denom-warn { + margin: 8px 14px; + background: #fff5f5; + border: 1.5px solid #fca5a5; + border-left: 4px solid #ef4444; + border-radius: 10px; + overflow: hidden; + + &-title { + display: flex; + align-items: center; + gap: 8px; + padding: 9px 14px; + font-size: 13px; + font-weight: 700; + color: #7f1d1d; + background: #fef2f2; + border-bottom: 1px solid #fecaca; + } + + &-ico { + color: #ef4444; + font-size: 14px; + flex-shrink: 0; + } + + &-rows { + display: flex; + flex-direction: column; + padding: 4px 0; + } + + &-row { + display: flex; + align-items: center; + gap: 10px; + padding: 7px 14px; + font-size: 12.5px; + color: #7f1d1d; + + &:not(:last-child) { + border-bottom: 1px dashed #fecaca; + } + } + + &-chip { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 3px 10px; + background: linear-gradient(135deg, #fee2e2, #fecaca); + border: 1px solid #fca5a5; + border-radius: 6px; + font-size: 12px; + font-weight: 800; + color: #991b1b; + min-width: 52px; + letter-spacing: 0.01em; + } + + &-stat { + display: flex; + align-items: center; + gap: 4px; + + strong { + font-weight: 800; + font-variant-numeric: tabular-nums; + } + } + + &-lbl { + font-size: 11px; + color: #b91c1c; + font-weight: 600; + } + + &-dot { + color: #fca5a5; + font-weight: 700; + font-size: 14px; + } + + &-short { + margin-left: auto; + padding: 2px 9px; + background: #fff; + border: 1.5px solid #f87171; + border-radius: 999px; + font-size: 11px; + font-weight: 800; + color: #b91c1c; + } +} + /* ─── Opening / Closing position cards ───────────────────────────── */ .sat__pos-card { margin: 0 14px 12px; diff --git a/src/app/savings/saving-account-actions/savings-account-transactions/savings-account-transactions.component.ts b/src/app/savings/saving-account-actions/savings-account-transactions/savings-account-transactions.component.ts index ef84b5151..e1a1946d4 100644 --- a/src/app/savings/saving-account-actions/savings-account-transactions/savings-account-transactions.component.ts +++ b/src/app/savings/saving-account-actions/savings-account-transactions/savings-account-transactions.component.ts @@ -455,6 +455,25 @@ export class SavingsAccountTransactionsComponent implements OnInit { this.denominationCounts = {}; } + /** Denominations where cashier is giving more than currently available (withdrawal only). */ + get withdrawalDenomOverdraft(): Array<{ label: string; available: number; requested: number }> { + if (!this.transactionType.withdrawal || !this.openingLoaded || this.openingDenoms.length === 0) return []; + const openMap = new Map(); + for (const d of this.openingDenoms) { + openMap.set(`${d.kind}:${d.denominationValue}`, Number(d.count ?? 0)); + } + return [ + ...this.notes, + ...this.coins + ] + .map((d) => ({ label: d.label, available: openMap.get(this.denomKey(d)) ?? 0, requested: this.countFor(d) })) + .filter((item) => item.requested > 0 && item.requested > item.available); + } + + get hasWithdrawalDenomOverdraft(): boolean { + return this.withdrawalDenomOverdraft.length > 0; + } + /** * Open the identity-verify pop-up. Only meaningful for withdrawals * (where the cashier needs to match the customer at the counter
Type Txn # Account No CIF Date AmountID Proof
+ @if (r.type === 'EXCHANGE') { + Exchange + } @else { + Txn + } + {{ r.txnId }} {{ r.accountNo || '—' }} {{ r.cifNo || '—' }} {{ r.txnDate || '—' }} ₹{{ r.totalAmount | number: '1.2-2' }}{{ r.idProof || '—' }}
No transactions at or above ₹{{ threshold | number }}.No transactions at or above ₹{{ threshold | number }}.