diff --git a/src/app/cashier/cash-history/cash-history.component.html b/src/app/cashier/cash-history/cash-history.component.html index 7b7ec4a7c..69c34a4d7 100644 --- a/src/app/cashier/cash-history/cash-history.component.html +++ b/src/app/cashier/cash-history/cash-history.component.html @@ -191,6 +191,48 @@ } + + @if (detail.row.type === 'settlement' && detail.morningTotal > 0) { +
+
+ wb_sunny +
+ Morning Received + ₹{{ detail.morningTotal | number: '1.2-2' }} +
+
+ arrow_forward +
+ account_balance_wallet +
+ This Settlement + ₹{{ detail.row.amount | number: '1.2-2' }} +
+
+
+ {{ + detail.row.amount === detail.morningTotal + ? 'check_circle' + : detail.row.amount > detail.morningTotal + ? 'trending_up' + : 'trending_down' + }} + {{ + detail.row.amount === detail.morningTotal + ? 'Fully Settled' + : (detail.row.amount - detail.morningTotal >= 0 ? '+' : '') + + '₹' + + (detail.row.amount - detail.morningTotal | number: '1.2-2') + }} +
+
+ } +
payments @@ -202,37 +244,102 @@ } @else if (detail.denominations.length === 0) {
No denomination data recorded.
} @else { - - - - - - - - - - @for (d of detail.denominations; track d.denominationValue) { + + @if (notes(detail.denominations).length > 0) { +
Notes
+
DenominationCountTotal
+ - - - + + @if (detail.row.type === 'settlement' && detail.morningTotal > 0) { + + } + + - } - - - - - - - - -
- ₹{{ d.denominationValue }} - {{ d.count }}₹{{ d.totalAmount | number: '1.2-2' }}DenominationMorning{{ detail.row.type === 'settlement' ? 'Settled' : 'Count' }}Total
Total - {{ totalCount(detail.denominations) }} - - ₹{{ detail.row.amount | number: '1.2-2' }} -
+ + + @for (d of notes(detail.denominations); track d.denominationValue) { + + + ₹{{ d.denominationValue }} + + @if (detail.row.type === 'settlement' && detail.morningTotal > 0) { + {{ d.morningCount || '—' }} + } + {{ d.count }} + ₹{{ d.totalAmount | number: '1.2-2' }} + + } + + + + Subtotal + @if (detail.row.type === 'settlement' && detail.morningTotal > 0) { + + } + + {{ totalCount(notes(detail.denominations)) }} + + + ₹{{ subtotal(notes(detail.denominations)) | number: '1.2-2' }} + + + + + } + + + @if (coins(detail.denominations).length > 0) { +
Coins
+ + + + + @if (detail.row.type === 'settlement' && detail.morningTotal > 0) { + + } + + + + + + @for (d of coins(detail.denominations); track d.denominationValue) { + + + @if (detail.row.type === 'settlement' && detail.morningTotal > 0) { + + } + + + + } + + + + + @if (detail.row.type === 'settlement' && detail.morningTotal > 0) { + + } + + + + +
DenominationMorning{{ detail.row.type === 'settlement' ? 'Settled' : 'Count' }}Total
+ ₹{{ d.denominationValue }} + {{ d.morningCount || '—' }}{{ d.count }}₹{{ d.totalAmount | number: '1.2-2' }}
Subtotal + {{ totalCount(coins(detail.denominations)) }} + + ₹{{ subtotal(coins(detail.denominations)) | number: '1.2-2' }} +
+ } + + +
+ Grand Total + {{ totalCount(detail.denominations) }} notes/coins + ₹{{ detail.row.amount | number: '1.2-2' }} +
}
diff --git a/src/app/cashier/cash-history/cash-history.component.scss b/src/app/cashier/cash-history/cash-history.component.scss index ccc7906a4..a759dc8fe 100644 --- a/src/app/cashier/cash-history/cash-history.component.scss +++ b/src/app/cashier/cash-history/cash-history.component.scss @@ -271,8 +271,17 @@ } } +.detail-denom-kind-label { + margin: 4px 20px 6px; + font-size: 10px; + font-weight: 800; + text-transform: uppercase; + letter-spacing: 0.08em; + color: #64748b; +} + .detail-denom-table { - margin: 0 20px 20px; + margin: 0 20px 12px; width: calc(100% - 40px); border: 1px solid #e2e8f0; border-radius: 8px; @@ -284,6 +293,25 @@ border-top: 2px solid #e2e8f0; } +.detail-denom-grand-total { + display: flex; + justify-content: space-between; + align-items: center; + margin: 0 20px 20px; + padding: 10px 14px; + background: #1e40af; + color: #fff; + border-radius: 8px; + font-size: 13px; + font-weight: 700; +} + +.pg-chip--coin { + background: #fef3c7; + color: #92400e; + border-color: #fcd34d; +} + .pg-btn--danger { background: linear-gradient(135deg, #ef4444, #b91c1c); color: #fff; @@ -294,3 +322,117 @@ cursor: not-allowed; } } + +/* ── Settlement: Morning Received vs Settled comparison bar ── */ +.settle-compare-bar { + display: flex; + align-items: center; + gap: 12px; + padding: 12px 20px; + background: linear-gradient(135deg, #fffbeb 0%, #f0fdf4 100%); + border-bottom: 1px solid #e2e8f0; + flex-wrap: wrap; +} + +.settle-compare-item { + display: flex; + align-items: center; + gap: 8px; + + mat-icon { + font-size: 18px; + height: 18px; + width: 18px; + flex-shrink: 0; + } + + &--morning mat-icon { + color: #d97706; + } + + &--settled mat-icon { + color: #1e40af; + } + + strong { + display: block; + font-size: 14px; + font-weight: 800; + color: #0f172a; + } +} + +.settle-compare-label { + display: block; + font-size: 10px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.05em; + color: #64748b; + margin-bottom: 1px; +} + +.settle-compare-arrow { + color: #94a3b8; + font-size: 18px; + height: 18px; + width: 18px; + flex-shrink: 0; +} + +.settle-compare-net { + margin-left: auto; + display: flex; + align-items: center; + gap: 4px; + padding: 4px 10px; + border-radius: 999px; + font-size: 12px; + font-weight: 700; + background: #f1f5f9; + color: #475569; + + mat-icon { + font-size: 16px; + height: 16px; + width: 16px; + } + + &--zero { + background: #dcfce7; + color: #15803d; + + mat-icon { + color: #16a34a; + } + } + + &--pos { + background: #dbeafe; + color: #1e40af; + + mat-icon { + color: #2563eb; + } + } + + &--neg { + background: #fee2e2; + color: #991b1b; + + mat-icon { + color: #dc2626; + } + } +} + +/* ── Morning column in settlement detail table ── */ +.settle-detail-col-morning { + color: #d97706; + font-weight: 700; +} + +.settle-detail-morning { + color: #b45309; + font-weight: 600; +} diff --git a/src/app/cashier/cash-history/cash-history.component.ts b/src/app/cashier/cash-history/cash-history.component.ts index e95a32d4e..cf1aaa7fd 100644 --- a/src/app/cashier/cash-history/cash-history.component.ts +++ b/src/app/cashier/cash-history/cash-history.component.ts @@ -21,6 +21,7 @@ interface TxnRow { date: string; fromParty: string; toParty: string; + fromCashierId?: number; amount: number; note: string; isReversed: boolean; @@ -35,6 +36,8 @@ interface TxnRow { interface DenomDetail { denominationId: number | null; denominationValue: number; + kind: 'note' | 'coin'; + morningCount: number; count: number; totalAmount: number; } @@ -42,6 +45,7 @@ interface DenomDetail { interface DetailView { row: TxnRow; denominations: DenomDetail[]; + morningTotal: number; loading: boolean; } @@ -71,6 +75,8 @@ export class CashHistoryComponent implements OnInit { interCashier: TxnRow[] = []; loading = false; + private denomKindMap = new Map(); + reversingId: number | null = null; reversingType: string | null = null; reverseReason = ''; @@ -78,6 +84,12 @@ export class CashHistoryComponent implements OnInit { detail: DetailView | null = null; ngOnInit(): void { + this.service + .getActiveDenominations() + .pipe(catchError(() => of([]))) + .subscribe((list: any[]) => { + (list || []).forEach((d: any) => this.denomKindMap.set(d.id, d.kind === 'coin' ? 'coin' : 'note')); + }); this.load(); } @@ -107,6 +119,7 @@ export class CashHistoryComponent implements OnInit { id: s.id, date: s.settlementDate, fromParty: s.fromParty, + fromCashierId: s.fromCashierId ?? undefined, toParty: s.toParty, amount: s.settlementAmount, note: s.note, @@ -138,7 +151,31 @@ export class CashHistoryComponent implements OnInit { } viewDetail(row: TxnRow): void { - this.detail = { row, denominations: [], loading: true }; + this.detail = { row, denominations: [], morningTotal: 0, loading: true }; + + if (row.type === 'settlement' && row.fromCashierId) { + forkJoin({ + detail: this.service.getCashSettlementById(row.id).pipe(catchError(() => of(null))), + morning: this.service.getCashierMorningBalance(row.fromCashierId, row.date).pipe(catchError(() => of(null))) + }).subscribe(({ detail, morning }) => { + if (!this.detail) return; + const morningMap = new Map(); + ((morning?.denominations || []) as any[]).forEach((d: any) => + morningMap.set(d.denominationId, d.morningCount ?? 0) + ); + this.detail.morningTotal = Number(morning?.totalMorning ?? 0); + this.detail.denominations = ((detail?.details || []) as any[]).map((d: any) => ({ + denominationId: d.denominationId ?? null, + denominationValue: d.denominationValue, + kind: this.denomKindMap.get(d.denominationId) ?? (d.denominationValue <= 5 ? 'coin' : 'note'), + morningCount: morningMap.get(d.denominationId) ?? 0, + count: d.count, + totalAmount: d.totalAmount + })); + this.detail.loading = false; + }); + return; + } let obs$; if (row.type === 'transfer') obs$ = this.service.getCashTransferById(row.id); @@ -147,9 +184,11 @@ export class CashHistoryComponent implements OnInit { obs$.pipe(catchError(() => of(null))).subscribe((data: any) => { if (this.detail) { - this.detail.denominations = (data?.details || []).map((d: any) => ({ + this.detail.denominations = ((data?.details || []) as any[]).map((d: any) => ({ 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 })); @@ -200,6 +239,18 @@ export class CashHistoryComponent implements OnInit { return type === 'transfer' ? 'Cash Transfer' : type === 'settlement' ? 'Cash Settlement' : 'Inter-Cashier Transfer'; } + notes(denoms: DenomDetail[]): DenomDetail[] { + return denoms.filter((d) => d.kind === 'note').sort((a, b) => b.denominationValue - a.denominationValue); + } + + coins(denoms: DenomDetail[]): DenomDetail[] { + return denoms.filter((d) => d.kind === 'coin').sort((a, b) => b.denominationValue - a.denominationValue); + } + + subtotal(denoms: DenomDetail[]): number { + return denoms.reduce((s, d) => s + Number(d.totalAmount), 0); + } + totalCount(denoms: DenomDetail[]): number { return denoms.reduce((s, d) => s + d.count, 0); } diff --git a/src/app/cashier/cash-settlement/cash-settlement.component.html b/src/app/cashier/cash-settlement/cash-settlement.component.html index ae6ba9116..7364fc900 100644 --- a/src/app/cashier/cash-settlement/cash-settlement.component.html +++ b/src/app/cashier/cash-settlement/cash-settlement.component.html @@ -182,6 +182,45 @@ } + + @if (morningTotal > 0) { +
+
+ wb_sunny + Morning Opening Balance (Cash received from vault today) + ₹{{ morningTotal | number: '1.2-2' }} +
+
+ @if (noteRows.length > 0) { + +
+ @for (row of noteRows; track row.denominationId) { + @if (row.morningCount > 0) { + ₹{{ row.denominationValue }}×{{ row.morningCount }} + } + } +
+ } + @if (coinRows.length > 0) { + +
+ @for (row of coinRows; track row.denominationId) { + @if (row.morningCount > 0) { + ₹{{ row.denominationValue }}×{{ row.morningCount }} + } + } +
+ } +
+
+ } + @if (rows.length > 0) {
@@ -192,6 +231,7 @@ Denomination + Morning System Expected Physical Count Variance @@ -203,6 +243,7 @@ ₹{{ row.denominationValue }} + {{ row.morningCount || '—' }} {{ row.expectedCount }}
@@ -224,9 +265,9 @@ > {{ varianceFor(row) > 0 ? '+' : '' }}{{ varianceFor(row) }} @if (varianceFor(row) !== 0) { - - (₹{{ varianceFor(row) * row.denominationValue | number: '1.0-0' }}) - + (₹{{ varianceFor(row) * row.denominationValue | number: '1.0-0' }}) } @@ -242,6 +283,7 @@ Denomination + Morning System Expected Physical Count Variance @@ -253,6 +295,7 @@ ₹{{ row.denominationValue }} + {{ row.morningCount || '—' }} {{ row.expectedCount }}
@@ -274,9 +317,9 @@ > {{ varianceFor(row) > 0 ? '+' : '' }}{{ varianceFor(row) }} @if (varianceFor(row) !== 0) { - - (₹{{ varianceFor(row) * row.denominationValue | number: '1.0-0' }}) - + (₹{{ varianceFor(row) * row.denominationValue | number: '1.0-0' }}) } diff --git a/src/app/cashier/cash-settlement/cash-settlement.component.scss b/src/app/cashier/cash-settlement/cash-settlement.component.scss index 5b89967c7..f68bd3b6c 100644 --- a/src/app/cashier/cash-settlement/cash-settlement.component.scss +++ b/src/app/cashier/cash-settlement/cash-settlement.component.scss @@ -293,3 +293,111 @@ font-weight: 600; } } + +/* ── Morning Opening Balance panel ── */ +.morning-panel { + background: linear-gradient(135deg, #fffbeb 0%, #fef9ec 100%); + border: 1px solid #fcd34d; + border-radius: 10px; + margin-bottom: 16px; + overflow: hidden; + + &__head { + display: flex; + align-items: center; + gap: 10px; + padding: 12px 16px; + border-bottom: 1px solid #fde68a; + + mat-icon { + color: #d97706; + font-size: 20px; + height: 20px; + width: 20px; + flex-shrink: 0; + } + + span { + flex: 1; + font-size: 13px; + font-weight: 600; + color: #78350f; + + em { + font-weight: 400; + opacity: 0.75; + font-style: normal; + font-size: 12px; + } + } + } + + &__total { + font-size: 15px; + font-weight: 800; + color: #92400e; + white-space: nowrap; + } + + &__body { + padding: 10px 16px 12px; + display: flex; + flex-direction: column; + gap: 6px; + } + + &__section-label { + font-size: 10px; + font-weight: 800; + text-transform: uppercase; + letter-spacing: 0.06em; + color: #92400e; + margin-bottom: 2px; + + &--coin { + color: #78350f; + } + } +} + +.morning-panel__pills { + display: flex; + flex-wrap: wrap; + gap: 6px; + margin-bottom: 4px; +} + +.morning-pill { + display: inline-flex; + align-items: center; + gap: 2px; + padding: 3px 9px; + background: #fef3c7; + border: 1px solid #fcd34d; + border-radius: 999px; + font-size: 12px; + font-weight: 700; + color: #92400e; + + &--coin { + background: #fff7ed; + border-color: #fdba74; + color: #7c2d12; + } +} + +.morning-pill__x { + font-weight: 400; + opacity: 0.75; + margin-left: 1px; +} + +/* ── Morning column in settle table ── */ +.settle-col-morning { + color: #d97706; +} + +.settle-morning { + color: #b45309; + font-weight: 600; +} diff --git a/src/app/cashier/cash-settlement/cash-settlement.component.ts b/src/app/cashier/cash-settlement/cash-settlement.component.ts index 0b543c1a0..c93a31c64 100644 --- a/src/app/cashier/cash-settlement/cash-settlement.component.ts +++ b/src/app/cashier/cash-settlement/cash-settlement.component.ts @@ -25,6 +25,7 @@ interface SettleDenomRow { denominationId: number; denominationValue: number; kind: 'note' | 'coin'; + morningCount: number; expectedCount: number; physicalCount: number; } @@ -77,6 +78,7 @@ export class CashSettlementComponent implements OnInit { supervisorChecking = false; rows: SettleDenomRow[] = []; + morningTotal = 0; settlementDate = ''; // Vault teller to deposit TO @@ -128,8 +130,9 @@ export class CashSettlementComponent implements OnInit { .pipe(catchError(() => of({ alreadySettled: false }))), recon: this.service .checkReconciliation(cashierId, this.settlementDate) - .pipe(catchError(() => of({ reconciled: false }))) - }).subscribe(({ expected, check, recon }) => { + .pipe(catchError(() => of({ reconciled: false }))), + morning: this.service.getCashierMorningBalance(cashierId, this.settlementDate).pipe(catchError(() => of(null))) + }).subscribe(({ expected, check, recon, morning }) => { this.supervisorChecking = false; if (check?.alreadySettled) { this.pageState = 'already-settled'; @@ -139,6 +142,13 @@ export class CashSettlementComponent implements OnInit { this.pageState = 'needs-reconciliation'; return; } + // Build denomination → morningCount map + const morningMap = new Map(); + ((morning?.denominations || []) as any[]).forEach((d: any) => + morningMap.set(d.denominationId, d.morningCount ?? 0) + ); + this.morningTotal = Number(morning?.totalMorning ?? 0); + this.rows = ((expected?.denominations as any[]) || []) .filter((d: any) => (d.expectedCount ?? 0) > 0) .sort((a: any, b: any) => b.denominationValue - a.denominationValue) @@ -146,6 +156,7 @@ export class CashSettlementComponent implements OnInit { denominationId: d.denominationId, denominationValue: d.denominationValue, kind: (d.kind ?? 'note') as 'note' | 'coin', + morningCount: morningMap.get(d.denominationId) ?? 0, expectedCount: d.expectedCount, physicalCount: d.expectedCount })); @@ -187,6 +198,12 @@ export class CashSettlementComponent implements OnInit { get coinRows(): SettleDenomRow[] { return this.rows.filter((r) => r.kind === 'coin'); } + get morningNoteTotal(): number { + return this.noteRows.reduce((s, r) => s + r.morningCount * r.denominationValue, 0); + } + get morningCoinTotal(): number { + return this.coinRows.reduce((s, r) => s + r.morningCount * r.denominationValue, 0); + } varianceFor(row: SettleDenomRow): number { return row.physicalCount - row.expectedCount; diff --git a/src/app/cashier/cash-transfer/cash-transfer.component.ts b/src/app/cashier/cash-transfer/cash-transfer.component.ts index cf6f1bb35..ee3aa8a22 100644 --- a/src/app/cashier/cash-transfer/cash-transfer.component.ts +++ b/src/app/cashier/cash-transfer/cash-transfer.component.ts @@ -171,8 +171,15 @@ export class CashTransferComponent implements OnInit { .map((d) => ({ denominationId: d.id, denominationValue: d.value, count: this.countFor(d) })) }; this.service.createCashTransfer(payload).subscribe({ - next: () => { - this.snackBar.open('Cash transfer recorded', 'Close', { duration: 3000 }); + next: (res: any) => { + if (res?.glWarning) { + this.snackBar.open(`Transfer saved (GL warning: ${res.glWarning})`, 'Close', { + duration: 8000, + panelClass: ['snack-warn'] + }); + } else { + this.snackBar.open('Cash transfer recorded', 'Close', { duration: 3000 }); + } this.clear(); this.note = ''; this.fromParty = ''; diff --git a/src/app/cashier/cashier-position/cashier-position.component.html b/src/app/cashier/cashier-position/cashier-position.component.html index fc405c40f..2142d986c 100644 --- a/src/app/cashier/cashier-position/cashier-position.component.html +++ b/src/app/cashier/cashier-position/cashier-position.component.html @@ -58,11 +58,43 @@ @for (pos of results; track pos.cashierId) {
+
{{ pos.cashierName || labelForCashier(pos.cashierId) }} ₹{{ pos.totalAmount | number: '1.2-2' }}
+ + @if (pos.morningTotal > 0) { +
+
+ wb_sunny +
+ Morning Opening + ₹{{ pos.morningTotal | number: '1.2-2' }} +
+
+ arrow_forward +
+ account_balance_wallet +
+ Current Position + ₹{{ pos.totalAmount | number: '1.2-2' }} +
+
+
+ {{ pos.totalAmount >= pos.morningTotal ? 'trending_up' : 'trending_down' }} + {{ pos.totalAmount - pos.morningTotal >= 0 ? '+' : '' }}₹{{ + pos.totalAmount - pos.morningTotal | number: '1.2-2' + }} +
+
+ } + @if (pos.denominations.length === 0) {
No balance.
} @@ -73,7 +105,10 @@ Denomination - Count + @if (pos.morningTotal > 0) { + Morning + } + Current Count Amount @@ -83,6 +118,9 @@ ₹{{ d.denominationValue }} + @if (pos.morningTotal > 0) { + {{ d.morningCount || '—' }} + } {{ d.count }} ₹{{ d.amount | number: '1.2-2' }} @@ -97,7 +135,10 @@ Denomination - Count + @if (pos.morningTotal > 0) { + Morning + } + Current Count Amount @@ -107,6 +148,9 @@ ₹{{ d.denominationValue }} + @if (pos.morningTotal > 0) { + {{ d.morningCount || '—' }} + } {{ d.count }} ₹{{ d.amount | number: '1.2-2' }} diff --git a/src/app/cashier/cashier-position/cashier-position.component.scss b/src/app/cashier/cashier-position/cashier-position.component.scss index 1807e6902..f9e3ae396 100644 --- a/src/app/cashier/cashier-position/cashier-position.component.scss +++ b/src/app/cashier/cashier-position/cashier-position.component.scss @@ -27,3 +27,114 @@ color: #0f4f3c; font-variant-numeric: tabular-nums; } + +/* ── Morning vs Current comparison bar ── */ +.pos-compare-bar { + display: flex; + align-items: center; + gap: 12px; + padding: 12px 16px; + background: linear-gradient(135deg, #fffbeb 0%, #f0fdf4 100%); + border-bottom: 1px solid #e2e8f0; + flex-wrap: wrap; +} + +.pos-compare-item { + display: flex; + align-items: center; + gap: 8px; + + mat-icon { + font-size: 20px; + height: 20px; + width: 20px; + flex-shrink: 0; + } + + &--morning mat-icon { + color: #d97706; + } + + &--current mat-icon { + color: #1e40af; + } +} + +.pos-compare-label { + display: block; + font-size: 10px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.05em; + color: #64748b; + margin-bottom: 1px; +} + +.pos-compare-val { + font-size: 15px; + font-weight: 800; + color: #0f172a; + font-variant-numeric: tabular-nums; +} + +.pos-compare-arrow { + color: #94a3b8; + font-size: 20px; + height: 20px; + width: 20px; + flex-shrink: 0; +} + +.pos-compare-delta { + margin-left: auto; + display: flex; + align-items: center; + gap: 4px; + padding: 4px 12px; + border-radius: 999px; + font-size: 13px; + font-weight: 700; + background: #f1f5f9; + color: #64748b; + + mat-icon { + font-size: 18px; + height: 18px; + width: 18px; + } + + &--pos { + background: #dcfce7; + color: #15803d; + + mat-icon { + color: #16a34a; + } + } + + &--neg { + background: #fee2e2; + color: #991b1b; + + mat-icon { + color: #dc2626; + } + } +} + +/* ── Morning column in position table ── */ +.pos-col-morning { + color: #d97706; + font-weight: 700; +} + +.pos-morning-val { + color: #b45309; + font-weight: 600; +} + +.pg-chip--coin { + background: #fef3c7; + color: #92400e; + border-color: #fcd34d; +} diff --git a/src/app/cashier/cashier-position/cashier-position.component.ts b/src/app/cashier/cashier-position/cashier-position.component.ts index abb113211..3f5c5ed9d 100644 --- a/src/app/cashier/cashier-position/cashier-position.component.ts +++ b/src/app/cashier/cashier-position/cashier-position.component.ts @@ -13,7 +13,7 @@ import { MatIconModule } from '@angular/material/icon'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatSelectModule } from '@angular/material/select'; import { MatInputModule } from '@angular/material/input'; -import { of } from 'rxjs'; +import { forkJoin, of } from 'rxjs'; import { catchError } from 'rxjs/operators'; import { CashierService } from '../cashier.service'; @@ -21,6 +21,7 @@ interface DenomRow { denominationId: number; denominationValue: number; kind: string; + morningCount: number; count: number; amount: number; } @@ -29,6 +30,7 @@ interface CashierPos { cashierName?: string; asOfDate: string; totalAmount: number; + morningTotal: number; denominations: DenomRow[]; } @@ -91,17 +93,54 @@ export class CashierPositionComponent implements OnInit { this.loading = true; this.results = []; this.errorMsg = ''; - this.service - .cashierPositionReport(this.selectedCashierId || undefined, this.asOfDate) - .pipe(catchError(() => of(null))) - .subscribe((data: any) => { + + if (this.selectedCashierId) { + forkJoin({ + position: this.service + .cashierPositionReport(this.selectedCashierId, this.asOfDate) + .pipe(catchError(() => of(null))), + morning: this.service + .getCashierMorningBalance(this.selectedCashierId, this.asOfDate) + .pipe(catchError(() => of(null))) + }).subscribe(({ position, morning }) => { this.loading = false; - if (!data) { + if (!position) { this.errorMsg = 'Failed to load position data.'; return; } - this.results = Array.isArray(data) ? data : [data]; + const morningMap = new Map(); + ((morning?.denominations || []) as any[]).forEach((d: any) => + morningMap.set(d.denominationId, d.morningCount ?? 0) + ); + const morningTotal = Number(morning?.totalMorning ?? 0); + const arr: any[] = Array.isArray(position) ? position : [position]; + this.results = arr.map((p: any) => ({ + ...p, + morningTotal, + denominations: ((p.denominations || []) as any[]).map((d: any) => ({ + ...d, + morningCount: morningMap.get(d.denominationId) ?? 0 + })) + })); }); + } else { + this.service + .cashierPositionReport(undefined, this.asOfDate) + .pipe(catchError(() => of(null))) + .subscribe((data: any) => { + this.loading = false; + if (!data) { + this.errorMsg = 'Failed to load position data.'; + return; + } + const arr: any[] = Array.isArray(data) ? data : [data]; + this.results = arr.map((p: any) => ({ + ...p, + morningTotal: 0, + denominations: ((p.denominations || []) as any[]).map((d: any) => ({ ...d, morningCount: 0 })) + })); + }); + } } labelForCashier(id: number): string { diff --git a/src/app/cashier/cashier.service.ts b/src/app/cashier/cashier.service.ts index 0d52dd4e7..7a914d5af 100644 --- a/src/app/cashier/cashier.service.ts +++ b/src/app/cashier/cashier.service.ts @@ -133,6 +133,11 @@ export class CashierService { return this.http.get('/cashreconciliation/check', { params }); } + getCashierMorningBalance(cashierId: number, date: string): Observable { + const params = new HttpParams().set('cashierId', String(cashierId)).set('date', date); + return this.http.get('/cashreports/cashier-morning', { params }); + } + getExpectedBalanceForMe(asOfDate?: string): Observable { let params = new HttpParams(); if (asOfDate) params = params.set('asOfDate', asOfDate); diff --git a/src/app/cashier/reconciliation/reconciliation.component.html b/src/app/cashier/reconciliation/reconciliation.component.html index 6a6d90861..55036cb21 100644 --- a/src/app/cashier/reconciliation/reconciliation.component.html +++ b/src/app/cashier/reconciliation/reconciliation.component.html @@ -69,6 +69,47 @@ } @if (!loadingExpected && rows.length > 0) { + + @if (morningTotal > 0) { +
+
+ wb_sunny + Morning Opening Balance (Cash received from vault today) + ₹{{ morningTotal | number: '1.2-2' }} +
+
+ @if (noteRows.length > 0) { + +
+ @for (row of noteRows; track row.denominationId) { + @if (row.morningCount > 0) { + ₹{{ row.denominationValue + }}×{{ row.morningCount }} + } + } +
+ } + @if (coinRows.length > 0) { + +
+ @for (row of coinRows; track row.denominationId) { + @if (row.morningCount > 0) { + ₹{{ row.denominationValue + }}×{{ row.morningCount }} + } + } +
+ } +
+
+ } +
@if (noteRows.length > 0) { @@ -76,6 +117,7 @@ Denomination + Morning Expected Physical Variance @@ -87,6 +129,7 @@ ₹{{ row.denominationValue }} + {{ row.morningCount || '—' }} {{ row.expectedCount }}
@@ -128,6 +171,7 @@ Denomination + Morning Expected Physical Variance @@ -139,6 +183,7 @@ ₹{{ row.denominationValue }} + {{ row.morningCount || '—' }} {{ row.expectedCount }}
diff --git a/src/app/cashier/reconciliation/reconciliation.component.scss b/src/app/cashier/reconciliation/reconciliation.component.scss index 9dfc43133..e690d7b80 100644 --- a/src/app/cashier/reconciliation/reconciliation.component.scss +++ b/src/app/cashier/reconciliation/reconciliation.component.scss @@ -111,3 +111,116 @@ color: #92400e; } } + +.pg-chip--coin { + background: #fef3c7; + color: #92400e; + border-color: #fcd34d; +} + +/* ── Morning Opening Balance panel ── */ +.morning-panel { + background: linear-gradient(135deg, #fffbeb 0%, #fef9ec 100%); + border: 1px solid #fcd34d; + border-radius: 10px; + overflow: hidden; + + &__head { + display: flex; + align-items: center; + gap: 10px; + padding: 12px 16px; + border-bottom: 1px solid #fde68a; + + mat-icon { + color: #d97706; + font-size: 20px; + height: 20px; + width: 20px; + flex-shrink: 0; + } + + span { + flex: 1; + font-size: 13px; + font-weight: 600; + color: #78350f; + + em { + font-weight: 400; + opacity: 0.75; + font-style: normal; + font-size: 12px; + } + } + } + + &__total { + font-size: 15px; + font-weight: 800; + color: #92400e; + white-space: nowrap; + } + + &__body { + padding: 10px 16px 12px; + display: flex; + flex-direction: column; + gap: 6px; + } + + &__section-label { + font-size: 10px; + font-weight: 800; + text-transform: uppercase; + letter-spacing: 0.06em; + color: #92400e; + margin-bottom: 2px; + + &--coin { + color: #78350f; + } + } +} + +.morning-panel__pills { + display: flex; + flex-wrap: wrap; + gap: 6px; + margin-bottom: 4px; +} + +.morning-pill { + display: inline-flex; + align-items: center; + gap: 2px; + padding: 3px 9px; + background: #fef3c7; + border: 1px solid #fcd34d; + border-radius: 999px; + font-size: 12px; + font-weight: 700; + color: #92400e; + + &--coin { + background: #fff7ed; + border-color: #fdba74; + color: #7c2d12; + } +} + +.morning-pill__x { + font-weight: 400; + opacity: 0.75; + margin-left: 1px; +} + +/* ── Morning column in recon table ── */ +.recon-col-morning { + color: #d97706; +} + +.recon-morning { + color: #b45309; + font-weight: 600; +} diff --git a/src/app/cashier/reconciliation/reconciliation.component.ts b/src/app/cashier/reconciliation/reconciliation.component.ts index ca2c76ec3..27b8e3ff5 100644 --- a/src/app/cashier/reconciliation/reconciliation.component.ts +++ b/src/app/cashier/reconciliation/reconciliation.component.ts @@ -15,7 +15,7 @@ import { MatFormFieldModule } from '@angular/material/form-field'; import { MatSelectModule } from '@angular/material/select'; import { MatInputModule } from '@angular/material/input'; import { MatTabsModule } from '@angular/material/tabs'; -import { of } from 'rxjs'; +import { forkJoin, of } from 'rxjs'; import { catchError } from 'rxjs/operators'; import { CashierService } from '../cashier.service'; @@ -23,6 +23,7 @@ interface DenomRow { denominationId: number; denominationValue: number; kind: string; + morningCount: number; expectedCount: number; physicalCount: number; varianceCount: number; @@ -75,6 +76,7 @@ export class ReconciliationComponent implements OnInit { note = ''; varianceReason = ''; rows: DenomRow[] = []; + morningTotal = 0; loadingExpected = false; saving = false; @@ -120,21 +122,33 @@ export class ReconciliationComponent implements OnInit { fetchExpected(): void { if (!this.selectedCashierId) return; this.loadingExpected = true; - this.service - .getExpectedBalance(this.selectedCashierId, this.reconDate) - .pipe(catchError(() => of(null))) - .subscribe((data: any) => { - this.loadingExpected = false; - if (!data) return; - this.rows = (data.denominations || []).map((d: any) => ({ + forkJoin({ + expected: this.service + .getExpectedBalance(this.selectedCashierId, this.reconDate) + .pipe(catchError(() => of(null))), + morning: this.service + .getCashierMorningBalance(this.selectedCashierId, this.reconDate) + .pipe(catchError(() => of(null))) + }).subscribe(({ expected, morning }) => { + this.loadingExpected = false; + const morningMap = new Map(); + ((morning?.denominations || []) as any[]).forEach((d: any) => + morningMap.set(d.denominationId, d.morningCount ?? 0) + ); + this.morningTotal = Number(morning?.totalMorning ?? 0); + if (!expected) return; + this.rows = ((expected.denominations || []) as any[]) + .sort((a: any, b: any) => b.denominationValue - a.denominationValue) + .map((d: any) => ({ denominationId: d.denominationId || null, denominationValue: d.denominationValue, kind: d.kind || 'note', + morningCount: morningMap.get(d.denominationId) ?? 0, expectedCount: d.expectedCount || 0, physicalCount: d.expectedCount || 0, varianceCount: 0 })); - }); + }); } updatePhysical(row: DenomRow, raw: any): void { @@ -148,6 +162,12 @@ export class ReconciliationComponent implements OnInit { get coinRows(): DenomRow[] { return this.rows.filter((r) => r.kind === 'coin'); } + get morningNoteTotal(): number { + return this.noteRows.reduce((s, r) => s + r.morningCount * r.denominationValue, 0); + } + get morningCoinTotal(): number { + return this.coinRows.reduce((s, r) => s + r.morningCount * r.denominationValue, 0); + } get totalExpected(): number { return this.rows.reduce((s, r) => s + r.denominationValue * r.expectedCount, 0);