feat: cash-only registers, day book sections and DD amendments
This commit is contained in:
@@ -65,42 +65,6 @@
|
||||
@if (errorMsg) {
|
||||
<div class="pg-alert pg-alert--error">{{ errorMsg }}</div>
|
||||
}
|
||||
|
||||
<!-- Exceptions surface at the top of the day, not only as a badge buried in their own row: a
|
||||
flag raised for investigation is useless if someone has to scroll to find it. -->
|
||||
@if (!loading && exceptionRows.length > 0) {
|
||||
<div class="db-exceptions">
|
||||
<div class="db-exceptions__head">
|
||||
<mat-icon>warning</mat-icon>
|
||||
<strong>
|
||||
{{ exceptionRows.length }} voucher{{ exceptionRows.length === 1 ? '' : 's' }} awaiting investigation
|
||||
</strong>
|
||||
@if (resolvedExceptionCount > 0) {
|
||||
<span class="db-muted">({{ resolvedExceptionCount }} already closed)</span>
|
||||
}
|
||||
<button type="button" class="pg-btn pg-btn--warn" (click)="toggleExceptionsOnly()">
|
||||
{{ exceptionsOnly ? 'Show all vouchers' : 'Show only these' }}
|
||||
</button>
|
||||
</div>
|
||||
@for (e of exceptionRows; track e.voucherNo) {
|
||||
<div class="db-exceptions__item">
|
||||
<span class="pg-chip">{{ e.voucherNo }}</span>
|
||||
<span class="db-exceptions__amt">
|
||||
System ₹{{ e.amount | number: '1.2-2' }}
|
||||
@if (e.slipAmount != null && e.slipAmount !== e.amount) {
|
||||
· Slip ₹{{ e.slipAmount | number: '1.2-2' }}
|
||||
<strong class="db-exceptions__diff"
|
||||
>(diff ₹{{ e.slipAmount - (e.amount ?? 0) | number: '1.2-2' }})</strong
|
||||
>
|
||||
}
|
||||
</span>
|
||||
<span class="db-exceptions__why">{{ e.verifyRemarks }}</span>
|
||||
<span class="db-muted">— {{ e.verifiedByName }}, {{ timeOf(e.verifiedAt) }}</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (loading) {
|
||||
<div class="pg-empty">Loading…</div>
|
||||
}
|
||||
@@ -139,252 +103,87 @@
|
||||
Totals count approved vouchers only — pending and rejected rows are listed but never move the money.
|
||||
</p>
|
||||
|
||||
<div class="pg-card">
|
||||
<table class="pg-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Voucher No</th>
|
||||
<th>Dr/Cr</th>
|
||||
<th>Type</th>
|
||||
<th>Account</th>
|
||||
<th>Party</th>
|
||||
<th class="num">Amount</th>
|
||||
<th>Payment</th>
|
||||
<th>Status</th>
|
||||
<th>By</th>
|
||||
<th>Approved By</th>
|
||||
<th>Slip Check</th>
|
||||
<th>Time</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (r of filteredRows; track r.voucherNo ?? r.submittedAt) {
|
||||
<tr [class.db-row--cr]="r.side === 'CR'" [class.db-row--dr]="r.side === 'DR'">
|
||||
<td>
|
||||
<span class="pg-chip">{{ r.voucherNo ?? '—' }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="db-side" [class.db-side--cr]="r.side === 'CR'" [class.db-side--dr]="r.side === 'DR'">
|
||||
{{ r.side }}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ r.kind }}</td>
|
||||
<td>
|
||||
{{ r.accountNo ?? '—' }}
|
||||
@if (r.accountType) {
|
||||
<span class="db-muted">· {{ r.accountType }}</span>
|
||||
}
|
||||
</td>
|
||||
<td>{{ r.party ?? '—' }}</td>
|
||||
<td class="num">₹{{ r.amount | number: '1.2-2' }}</td>
|
||||
<td>{{ r.paymentTypeName ?? '—' }}</td>
|
||||
<td>
|
||||
<span [class]="statusClass(r.status)">{{ r.status }}</span>
|
||||
</td>
|
||||
<td>{{ r.handledBy ?? '—' }}</td>
|
||||
<td>
|
||||
{{ r.approvedBy ?? '—' }}
|
||||
@if (r.approvedBy && r.actionedAt) {
|
||||
<span class="db-muted">· {{ timeOf(r.actionedAt) }}</span>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
@if (r.verifyStatus === 'VERIFIED') {
|
||||
<span class="db-badge db-badge--ok" [title]="'Verified by ' + (r.verifiedByName ?? '')"
|
||||
>✓ MATCHED</span
|
||||
>
|
||||
} @else if (r.verifyStatus === 'EXCEPTION' && r.resolutionStatus) {
|
||||
<!-- Closed: the flag is history now, so it reads as settled rather than pending. -->
|
||||
<span
|
||||
class="db-badge db-badge--ok"
|
||||
[title]="resolutionLabel(r.resolutionStatus) + ' — ' + (r.resolutionNotes ?? '')"
|
||||
>
|
||||
✓ RESOLVED
|
||||
</span>
|
||||
} @else if (r.verifyStatus === 'EXCEPTION') {
|
||||
<span
|
||||
class="db-badge db-badge--rejected"
|
||||
[title]="'Slip ₹' + r.slipAmount + ' vs system ₹' + r.amount + ' — ' + (r.verifyRemarks ?? '')"
|
||||
>
|
||||
⚠ EXCEPTION
|
||||
</span>
|
||||
} @else {
|
||||
<span class="db-muted">Not checked</span>
|
||||
}
|
||||
</td>
|
||||
<td>{{ timeOf(r.submittedAt) }}</td>
|
||||
<td class="db-actions">
|
||||
<button type="button" class="db-icon-btn" (click)="openVerify(r)" title="Check against the paper slip">
|
||||
<mat-icon>fact_check</mat-icon>
|
||||
</button>
|
||||
<button type="button" class="db-icon-btn" (click)="openAttach(r)" title="Attach or view scanned slip">
|
||||
<mat-icon>attach_file</mat-icon>
|
||||
@if (r.attachmentCount > 0) {
|
||||
<span class="db-icon-btn__count">{{ r.attachmentCount }}</span>
|
||||
}
|
||||
</button>
|
||||
</td>
|
||||
<!-- One table per source register rather than one long list. The two read differently: the
|
||||
cashier's rows are cash that actually crossed the counter, the clerk's are requests nobody
|
||||
has actioned yet. Mixed together they invited the wrong sum. -->
|
||||
@for (s of sections; track s.key) {
|
||||
<div class="pg-card db-section">
|
||||
<div class="db-section__head">
|
||||
<strong>{{ s.label }}</strong>
|
||||
<span class="db-muted">{{ s.hint }}</span>
|
||||
<span class="db-section__count">{{ s.rows.length }} row{{ s.rows.length === 1 ? '' : 's' }}</span>
|
||||
</div>
|
||||
<table class="pg-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Voucher No</th>
|
||||
<th>Dr/Cr</th>
|
||||
<th>Type</th>
|
||||
<th>Account</th>
|
||||
<th>Party</th>
|
||||
<th class="num">Amount</th>
|
||||
<th>Payment</th>
|
||||
<th>Status</th>
|
||||
<th>By</th>
|
||||
<th>Approved By</th>
|
||||
<th>Time</th>
|
||||
</tr>
|
||||
|
||||
<!-- A flagged voucher spells out its reason in the table itself. A tooltip is not
|
||||
enough: it cannot be printed, exported, or read without a mouse. -->
|
||||
@if (r.verifyStatus === 'EXCEPTION' && verifyRow !== r) {
|
||||
<tr class="db-exc-row" [class.db-exc-row--resolved]="r.resolutionStatus">
|
||||
<td colspan="13">
|
||||
<mat-icon>{{ r.resolutionStatus ? 'task_alt' : 'warning' }}</mat-icon>
|
||||
<strong>Exception</strong>
|
||||
@if (r.slipAmount != null && r.slipAmount !== r.amount) {
|
||||
— slip ₹{{ r.slipAmount | number: '1.2-2' }} vs system ₹{{ r.amount | number: '1.2-2' }} (difference
|
||||
₹{{ r.slipAmount - (r.amount ?? 0) | number: '1.2-2' }})
|
||||
}
|
||||
— {{ r.verifyRemarks }}
|
||||
<span class="db-muted">· raised by {{ r.verifiedByName }} at {{ timeOf(r.verifiedAt) }}</span>
|
||||
<!-- Both halves of the record: what the cashier found, and how it was settled. -->
|
||||
@if (r.resolutionStatus) {
|
||||
<div class="db-exc-row__resolution">
|
||||
<strong>Closed — {{ resolutionLabel(r.resolutionStatus) }}:</strong>
|
||||
{{ r.resolutionNotes }}
|
||||
<span class="db-muted">· by {{ r.resolvedByName }} at {{ timeOf(r.resolvedAt) }}</span>
|
||||
</div>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (r of s.rows; track r.voucherNo ?? r.section + r.txnId) {
|
||||
<tr [class.db-row--cr]="r.side === 'CR'" [class.db-row--dr]="r.side === 'DR'">
|
||||
<td>
|
||||
<span class="pg-chip">{{ r.voucherNo ?? '—' }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="db-side" [class.db-side--cr]="r.side === 'CR'" [class.db-side--dr]="r.side === 'DR'">
|
||||
{{ r.side }}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ r.kind }}</td>
|
||||
<td>
|
||||
{{ r.accountNo ?? '—' }}
|
||||
@if (r.accountType) {
|
||||
<span class="db-muted">· {{ r.accountType }}</span>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
|
||||
<!-- Verification panel, opened inline under its own voucher so the two amounts sit
|
||||
next to the row they describe. -->
|
||||
@if (verifyRow === r) {
|
||||
<tr class="db-panel-row">
|
||||
<td colspan="13">
|
||||
<div class="db-panel">
|
||||
<div class="db-panel__head">
|
||||
<strong>Slip check — {{ r.voucherNo }}</strong>
|
||||
<button type="button" class="db-icon-btn" (click)="closeVerify()" title="Close">
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
<div class="db-panel__body">
|
||||
<div class="db-cmp">
|
||||
<span class="db-cmp__label">System recorded</span>
|
||||
<span class="db-cmp__value">₹{{ r.amount | number: '1.2-2' }}</span>
|
||||
</div>
|
||||
<label class="pg-field">
|
||||
<span>Amount written on slip</span>
|
||||
<input type="number" step="0.01" [(ngModel)]="verifySlipAmount" />
|
||||
</label>
|
||||
@if (verifySlipBlank) {
|
||||
<div class="db-cmp">
|
||||
<span class="db-cmp__label">Difference</span>
|
||||
<span class="db-cmp__value">—</span>
|
||||
</div>
|
||||
} @else if (verifyMismatch) {
|
||||
<div class="db-cmp db-cmp--bad">
|
||||
<span class="db-cmp__label">Difference</span>
|
||||
<span class="db-cmp__value">₹{{ verifyDifference | number: '1.2-2' }}</span>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="db-cmp db-cmp--good">
|
||||
<span class="db-cmp__label">Match</span>
|
||||
<span class="db-cmp__value">Amounts agree</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<label class="pg-field db-panel__remarks">
|
||||
<span>{{ verifyMismatch ? 'What did you find? (required)' : 'Remarks (optional)' }}</span>
|
||||
<input
|
||||
type="text"
|
||||
[(ngModel)]="verifyRemarks"
|
||||
placeholder="e.g. slip written as 500, system posted 400 — teller keyed it wrong"
|
||||
/>
|
||||
</label>
|
||||
@if (verifyMismatch) {
|
||||
<p class="db-panel__warn">
|
||||
Saving this marks the voucher as an <strong>exception</strong> for investigation — it does not
|
||||
change the posted amount.
|
||||
</p>
|
||||
}
|
||||
<div class="db-panel__actions">
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--primary"
|
||||
(click)="saveVerify()"
|
||||
[disabled]="verifySaving || verifySlipBlank"
|
||||
>
|
||||
{{ verifyMismatch ? 'Mark Exception' : 'Mark Verified' }}
|
||||
</button>
|
||||
<!-- Raising an exception on an already-matched voucher stays available: a
|
||||
slip cleared in haste, or a difference noticed later, has to be
|
||||
recordable without erasing the earlier verdict. -->
|
||||
@if (!verifyMismatch) {
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--warn"
|
||||
(click)="raiseException()"
|
||||
[disabled]="verifySaving"
|
||||
>
|
||||
Raise Exception
|
||||
</button>
|
||||
}
|
||||
<button type="button" class="pg-btn" (click)="closeVerify()">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
<td>{{ r.party ?? '—' }}</td>
|
||||
<td class="num">₹{{ r.amount | number: '1.2-2' }}</td>
|
||||
<td>{{ r.paymentTypeName ?? '—' }}</td>
|
||||
<td>
|
||||
<span [class]="statusClass(r.status)">{{ r.status }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
|
||||
<!-- Attachment panel for this voucher. -->
|
||||
@if (attachRow === r) {
|
||||
<tr class="db-panel-row">
|
||||
<td colspan="13">
|
||||
<div class="db-panel">
|
||||
<div class="db-panel__head">
|
||||
<strong>Attachments — {{ r.voucherNo }}</strong>
|
||||
<button type="button" class="db-icon-btn" (click)="closeAttach()" title="Close">
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
<label class="db-upload">
|
||||
<input type="file" (change)="onFileSelected($event)" [disabled]="attachUploading" />
|
||||
<span class="db-muted">Scanned slip, cheque image or ID proof · max 10 MB</span>
|
||||
</label>
|
||||
@if (attachUploading) {
|
||||
<p class="db-muted">Uploading…</p>
|
||||
}
|
||||
@if (attachLoading) {
|
||||
<p class="db-muted">Loading…</p>
|
||||
}
|
||||
@if (!attachLoading && attachments.length === 0) {
|
||||
<p class="db-muted">Nothing attached to this voucher yet.</p>
|
||||
}
|
||||
@for (a of attachments; track a.id) {
|
||||
<div class="db-file">
|
||||
<mat-icon>description</mat-icon>
|
||||
<span class="db-file__name">{{ a.fileName }}</span>
|
||||
<span class="db-muted">{{ a.fileSize }} bytes · {{ a.uploadedByName }}</span>
|
||||
<button type="button" class="db-icon-btn" (click)="downloadAttachment(a)" title="Download">
|
||||
<mat-icon>download</mat-icon>
|
||||
</button>
|
||||
<button type="button" class="db-icon-btn" (click)="deleteAttachment(a)" title="Delete">
|
||||
<mat-icon>delete</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<td>{{ r.handledBy ?? '—' }}</td>
|
||||
<td>
|
||||
{{ r.approvedBy ?? '—' }}
|
||||
@if (r.approvedBy && r.actionedAt) {
|
||||
<span class="db-muted">· {{ timeOf(r.actionedAt) }}</span>
|
||||
}
|
||||
</td>
|
||||
<td>{{ timeOf(r.submittedAt) }}</td>
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colspan="5">Showing {{ filteredRows.length }} of {{ rows.length }} voucher(s)</td>
|
||||
<td class="num">₹{{ shownCredit - shownDebit | number: '1.2-2' }}</td>
|
||||
<td colspan="7"></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colspan="5">{{ s.label }} subtotal</td>
|
||||
<td class="num">
|
||||
@if (s.credit > 0 && s.debit > 0) {
|
||||
₹{{ s.credit | number: '1.2-2' }} CR · ₹{{ s.debit | number: '1.2-2' }} DR
|
||||
} @else if (s.credit > 0) {
|
||||
₹{{ s.credit | number: '1.2-2' }} CR
|
||||
} @else if (s.debit > 0) {
|
||||
₹{{ s.debit | number: '1.2-2' }} DR
|
||||
} @else {
|
||||
—
|
||||
}
|
||||
</td>
|
||||
<td colspan="5"></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@if (!loading && loaded && rows.length > 0 && filteredRows.length === 0) {
|
||||
|
||||
Reference in New Issue
Block a user