feat(cashier): add end-of-day report, my work, and cashier updates
This commit is contained in:
@@ -10,6 +10,7 @@ import { Component, OnInit, inject } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { Router } from '@angular/router';
|
||||
import { catchError, of } from 'rxjs';
|
||||
import { CashierService } from '../cashier.service';
|
||||
|
||||
@@ -20,7 +21,7 @@ import { CashierService } from '../cashier.service';
|
||||
* the bank shifting its own money, not a customer transaction, and it is read on the Cashier
|
||||
* Position and Closing Cash Report where the drawer itself is the subject.
|
||||
*/
|
||||
type DayBookSection = 'CASHIER' | 'CLERK';
|
||||
type DayBookSection = 'CASHIER' | 'CLERK' | 'SYSTEM';
|
||||
|
||||
interface DayBookRow {
|
||||
voucherNo: string | null;
|
||||
@@ -61,6 +62,18 @@ interface DayBookRow {
|
||||
})
|
||||
export class DayBookComponent implements OnInit {
|
||||
private service = inject(CashierService);
|
||||
private router = inject(Router);
|
||||
|
||||
/**
|
||||
* The heading, taken from the route the screen was opened by.
|
||||
*
|
||||
* One screen serves three menus, and only the Branch Ops reading is a different thing: there it
|
||||
* is the branch's book with the verification sheet an officer signs off, rather than one desk's
|
||||
* view of the day.
|
||||
*/
|
||||
get title(): string {
|
||||
return this.router.url.includes('/branch-ops/') ? 'Day Book & Officer Verification' : 'Day Book';
|
||||
}
|
||||
|
||||
date = '';
|
||||
side: '' | 'CR' | 'DR' = '';
|
||||
@@ -145,7 +158,15 @@ export class DayBookComponent implements OnInit {
|
||||
}[] {
|
||||
const order: { key: DayBookSection; label: string; hint: string }[] = [
|
||||
{ key: 'CASHIER', label: 'Cashier', hint: 'Cash actually taken in and paid out over the counter' },
|
||||
{ key: 'CLERK', label: 'Clerk', hint: 'Requests raised but not yet actioned — no cash has moved' }
|
||||
{ key: 'CLERK', label: 'Clerk', hint: 'Requests raised but not yet actioned — no cash has moved' },
|
||||
// Postings that reached the ledger without a request behind them: interest the nightly run
|
||||
// credited, a charge taken from a balance, a manual journal. These are the day's work too,
|
||||
// and an officer accounting for the day cannot do it from the counter registers alone.
|
||||
{
|
||||
key: 'SYSTEM',
|
||||
label: 'System & Journal',
|
||||
hint: 'Interest, charges and journals posted without a counter request'
|
||||
}
|
||||
];
|
||||
return order
|
||||
.map((s) => {
|
||||
@@ -206,11 +227,90 @@ export class DayBookComponent implements OnInit {
|
||||
return s === 'APPROVED' || s === 'COMPLETED' || s === 'WITHDRAWN';
|
||||
}
|
||||
|
||||
/**
|
||||
* The day totalled per person, for the officer who signs the book off.
|
||||
*
|
||||
* A branch's day book is read two ways. Entry by entry, to trace one voucher — which the sections
|
||||
* below already give. And person by person, to ask what each member of staff handled and whether
|
||||
* their column adds up: that is the reconciliation a principal officer actually performs, and
|
||||
* without it they are adding a day of rows in their head.
|
||||
*
|
||||
* Built from whoever appears in the day's own rows rather than from a list of staff, so it needs
|
||||
* no configuring: one person working alone gets one line, five people get five, and somebody who
|
||||
* handled nothing today is simply absent instead of showing as a row of zeros.
|
||||
*
|
||||
* Only approved money counts, the same rule the section totals use — a pending request is not
|
||||
* cash anybody is answerable for yet. Pending and rejected rows are still counted as ITEMS, so a
|
||||
* person whose work is all awaiting approval does not vanish from the sheet.
|
||||
*
|
||||
* Ordered by the largest turnover first: the columns worth checking hardest are the ones that
|
||||
* moved the most money.
|
||||
*/
|
||||
get byUser(): {
|
||||
name: string;
|
||||
credit: number;
|
||||
debit: number;
|
||||
net: number;
|
||||
items: number;
|
||||
pending: number;
|
||||
}[] {
|
||||
const map = new Map<string, { name: string; credit: number; debit: number; items: number; pending: number }>();
|
||||
|
||||
for (const r of this.filteredRows) {
|
||||
// Rows the registers could not attribute are grouped rather than dropped: an unattributed
|
||||
// amount is exactly what an officer needs to see, and hiding it would leave the sheet's own
|
||||
// total disagreeing with the day's.
|
||||
const name = (r.handledBy ?? '').trim() || 'Unattributed';
|
||||
const entry = map.get(name) ?? { name, credit: 0, debit: 0, items: 0, pending: 0 };
|
||||
|
||||
entry.items++;
|
||||
if (this.countsAsMoney(r)) {
|
||||
const amount = Number(r.amount ?? 0);
|
||||
if (r.side === 'CR') {
|
||||
entry.credit += amount;
|
||||
} else {
|
||||
entry.debit += amount;
|
||||
}
|
||||
} else {
|
||||
entry.pending++;
|
||||
}
|
||||
map.set(name, entry);
|
||||
}
|
||||
|
||||
return Array.from(map.values())
|
||||
.map((e) => ({ ...e, net: e.credit - e.debit }))
|
||||
.sort((a, b) => b.credit + b.debit - (a.credit + a.debit));
|
||||
}
|
||||
|
||||
/** The sheet's own totals, which must equal the day's — that is what makes it verifiable. */
|
||||
get byUserTotals(): { credit: number; debit: number; net: number; items: number; pending: number } {
|
||||
return this.byUser.reduce(
|
||||
(t, u) => ({
|
||||
credit: t.credit + u.credit,
|
||||
debit: t.debit + u.debit,
|
||||
net: t.net + u.net,
|
||||
items: t.items + u.items,
|
||||
pending: t.pending + u.pending
|
||||
}),
|
||||
{ credit: 0, debit: 0, net: 0, items: 0, pending: 0 }
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Approved and paid out are not the same state, and the day book is where the difference matters:
|
||||
* an approved withdrawal is a promise, a withdrawn one is money gone. Both used to render in the
|
||||
* same green, so a reader could not tell from the badge whether the customer had actually been
|
||||
* handed the cash — the one thing this page is read to find out.
|
||||
*/
|
||||
statusClass(status: string): string {
|
||||
switch ((status ?? '').toUpperCase()) {
|
||||
case 'APPROVED':
|
||||
case 'COMPLETED':
|
||||
case 'WITHDRAWN':
|
||||
case 'COMPLETED':
|
||||
case 'CLOSED':
|
||||
// Cash has left the counter. The darker, filled badge — the final state.
|
||||
return 'db-badge db-badge--done';
|
||||
case 'APPROVED':
|
||||
// Passed, but nobody has handed anything over yet.
|
||||
return 'db-badge db-badge--ok';
|
||||
case 'PENDING':
|
||||
return 'db-badge db-badge--pending';
|
||||
|
||||
Reference in New Issue
Block a user