feat(cashier): integrate expected balance, settlement verification, and reconciliation checks

This commit is contained in:
Jaanu423
2026-05-28 22:30:08 +05:30
parent 7ccaffa7f1
commit 6433ae068d
10 changed files with 1362 additions and 201 deletions
@@ -175,6 +175,35 @@
}
</div>
@if (totalVariance !== 0) {
<div
class="settle-variance-banner"
[class.settle-variance-banner--excess]="totalVariance > 0"
[class.settle-variance-banner--short]="totalVariance < 0"
style="margin: 8px 0 0"
>
<mat-icon>{{ totalVariance > 0 ? 'add_circle_outline' : 'remove_circle_outline' }}</mat-icon>
<div class="settle-variance-text">
<strong
>{{ totalVariance > 0 ? 'Excess' : 'Shortage' }}: ₹{{
(totalVariance < 0 ? -totalVariance : totalVariance) | number: '1.2-2'
}}</strong
>
<span>Variance reason is required to save reconciliation.</span>
</div>
<mat-form-field appearance="outline" subscriptSizing="dynamic" style="flex-shrink: 0; width: 300px">
<mat-label>Variance Reason *</mat-label>
<input
matInput
type="text"
[value]="varianceReason"
(input)="varianceReason = $any($event.target).value"
placeholder="e.g. Rounding, soiled note returned"
/>
</mat-form-field>
</div>
}
<div class="pg-total-bar">
<span>
Expected: <strong>₹{{ totalExpected | number: '1.2-2' }}</strong> &nbsp;|&nbsp; Physical:
@@ -184,9 +213,14 @@
</strong>
<span class="badge" [ngClass]="statusClass(statusLabel)">{{ statusLabel }}</span>
</span>
<button type="button" class="pg-btn pg-btn--primary" [disabled]="!valid || saving" (click)="submit()">
<mat-icon>save</mat-icon> Save Reconciliation
</button>
<div style="display: flex; align-items: center; gap: 10px">
@if (totalVariance !== 0 && !varianceReason.trim()) {
<span style="font-size: 12px; color: #d97706; font-weight: 600">Enter variance reason to save</span>
}
<button type="button" class="pg-btn pg-btn--primary" [disabled]="!valid || saving" (click)="submit()">
<mat-icon>save</mat-icon> Save Reconciliation
</button>
</div>
</div>
}
@@ -45,3 +45,69 @@
background: #fee2e2;
color: #991b1b;
}
/* Variance banner (shared style with settlement) */
.settle-variance-banner {
display: flex;
align-items: center;
gap: 14px;
padding: 12px 16px;
border-radius: 8px;
border: 1px solid;
mat-icon {
font-size: 22px;
height: 22px;
width: 22px;
flex-shrink: 0;
}
&--excess {
background: #f0fdf4;
border-color: #86efac;
color: #15803d;
mat-icon {
color: #16a34a;
}
}
&--short {
background: #fef2f2;
border-color: #fca5a5;
color: #991b1b;
mat-icon {
color: #dc2626;
}
}
}
.settle-variance-text {
flex: 1;
min-width: 0;
strong {
display: block;
font-size: 13px;
font-weight: 700;
}
span {
font-size: 12px;
opacity: 0.85;
}
}
.pos-section-label {
font-size: 10px;
font-weight: 800;
text-transform: uppercase;
letter-spacing: 0.06em;
color: #1e40af;
padding: 10px 12px 4px;
&--coin {
color: #92400e;
}
}
@@ -73,6 +73,7 @@ export class ReconciliationComponent implements OnInit {
cashierName = '';
reconDate = '';
note = '';
varianceReason = '';
rows: DenomRow[] = [];
loadingExpected = false;
saving = false;
@@ -162,7 +163,11 @@ export class ReconciliationComponent implements OnInit {
return v === 0 ? 'OK' : v > 0 ? 'EXCESS' : 'SHORT';
}
get valid(): boolean {
return !!this.selectedCashierId && this.rows.length > 0;
return (
!!this.selectedCashierId &&
this.rows.length > 0 &&
(this.totalVariance === 0 || this.varianceReason.trim().length > 0)
);
}
submit(): void {
@@ -173,7 +178,13 @@ export class ReconciliationComponent implements OnInit {
tellerId: this.selectedTellerId,
cashierName: this.cashierName,
reconDate: this.reconDate,
note: this.note || null,
note:
[
this.note,
this.totalVariance !== 0 ? `Variance reason: ${this.varianceReason}` : null
]
.filter(Boolean)
.join(' | ') || null,
denominations: this.rows.map((r) => ({
denominationId: r.denominationId,
denominationValue: r.denominationValue,
@@ -190,6 +201,7 @@ export class ReconciliationComponent implements OnInit {
});
this.rows = [];
this.note = '';
this.varianceReason = '';
this.saving = false;
this.loadHistory();
},