feat(cashier): add end-of-day report, my work, and cashier updates
This commit is contained in:
@@ -9,11 +9,10 @@
|
||||
<div class="pg">
|
||||
<header class="pg-head">
|
||||
<div>
|
||||
<div class="pg-eyebrow">CASHIER · DAY BOOK</div>
|
||||
<h1 class="pg-title">Day Book</h1>
|
||||
<h1 class="pg-title">{{ title }}</h1>
|
||||
<p class="pg-sub">
|
||||
Every voucher raised on the selected date — cash deposits, withdrawals, disbursements, collections and bank
|
||||
transfers, with credit and debit totals.
|
||||
Every voucher raised on the selected date — cash deposits, withdrawals, disbursements, collections, interest and
|
||||
journals, with each person's own credit, debit and net.
|
||||
</p>
|
||||
</div>
|
||||
<div class="pg-toolbar">
|
||||
@@ -103,11 +102,90 @@
|
||||
Totals count approved vouchers only — pending and rejected rows are listed but never move the money.
|
||||
</p>
|
||||
|
||||
<!-- The day per person, for the officer who signs the book off. The sections below trace one
|
||||
voucher at a time; this is the other reading a branch needs — what each member of staff
|
||||
handled, and whether their column adds up. Built from whoever appears in the day's rows, so
|
||||
one person working alone gets one line and five people get five. -->
|
||||
@if (byUser.length > 0) {
|
||||
<div class="pg-card db-section db-byuser">
|
||||
<div class="db-section__head">
|
||||
<strong>Officer Verification Sheet</strong>
|
||||
<span class="db-muted">Each person's own credit, debit and net for the day</span>
|
||||
<span class="db-section__count"> {{ byUser.length }} {{ byUser.length === 1 ? 'person' : 'people' }} </span>
|
||||
</div>
|
||||
<div class="pg-table-wrap">
|
||||
<table class="pg-table db-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Handled by</th>
|
||||
<th class="num">Items</th>
|
||||
<th class="num">Credit (CR)</th>
|
||||
<th class="num">Debit (DR)</th>
|
||||
<th class="num">Net</th>
|
||||
<th class="num">Pending</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (u of byUser; track u.name) {
|
||||
<tr>
|
||||
<td class="db-byuser__name">{{ u.name }}</td>
|
||||
<td class="num">{{ u.items }}</td>
|
||||
<td class="num db-byuser__cr">₹{{ u.credit | number: '1.2-2' }}</td>
|
||||
<td class="num db-byuser__dr">₹{{ u.debit | number: '1.2-2' }}</td>
|
||||
<td
|
||||
class="num db-byuser__net"
|
||||
[class.db-byuser__net--pos]="u.net >= 0"
|
||||
[class.db-byuser__net--neg]="u.net < 0"
|
||||
>
|
||||
₹{{ u.net | number: '1.2-2' }}
|
||||
</td>
|
||||
<td class="num">
|
||||
@if (u.pending > 0) {
|
||||
<span class="db-badge db-badge--pending">{{ u.pending }}</span>
|
||||
} @else {
|
||||
—
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
<!-- The sheet's own total, which must equal the day's totals above. That equality is
|
||||
what makes the sheet verifiable rather than just informative. -->
|
||||
<tfoot>
|
||||
<tr class="db-byuser__total">
|
||||
<td>All staff</td>
|
||||
<td class="num">{{ byUserTotals.items }}</td>
|
||||
<td class="num db-byuser__cr">₹{{ byUserTotals.credit | number: '1.2-2' }}</td>
|
||||
<td class="num db-byuser__dr">₹{{ byUserTotals.debit | number: '1.2-2' }}</td>
|
||||
<td
|
||||
class="num db-byuser__net"
|
||||
[class.db-byuser__net--pos]="byUserTotals.net >= 0"
|
||||
[class.db-byuser__net--neg]="byUserTotals.net < 0"
|
||||
>
|
||||
₹{{ byUserTotals.net | number: '1.2-2' }}
|
||||
</td>
|
||||
<td class="num">
|
||||
@if (byUserTotals.pending > 0) {
|
||||
{{ byUserTotals.pending }}
|
||||
} @else {
|
||||
—
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- 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">
|
||||
<!-- The section's own colour on the left edge: cash that actually crossed the counter, work
|
||||
still waiting to be actioned, and postings the system made are three different things to
|
||||
a person closing the day. -->
|
||||
<div [class]="'pg-card db-section db-section--' + s.key">
|
||||
<div class="db-section__head">
|
||||
<strong>{{ s.label }}</strong>
|
||||
<span class="db-muted">{{ s.hint }}</span>
|
||||
|
||||
@@ -36,6 +36,36 @@
|
||||
from the others, so the three are not added up as though they were one list. */
|
||||
.db-section {
|
||||
margin-bottom: 14px;
|
||||
border-left: 3px solid #cbd5e1;
|
||||
|
||||
/* Cash that actually crossed the counter — the section a drawer is checked against, so it
|
||||
carries the darker green. */
|
||||
&--CASHIER {
|
||||
border-left-color: #15803d;
|
||||
|
||||
.db-section__head {
|
||||
background: #f0fdf4;
|
||||
}
|
||||
}
|
||||
|
||||
/* Raised but not actioned: no cash has moved yet, so the lighter shade of the same green. */
|
||||
&--CLERK {
|
||||
border-left-color: #86efac;
|
||||
|
||||
.db-section__head {
|
||||
background: #f7fef9;
|
||||
}
|
||||
}
|
||||
|
||||
/* Postings with no counter behind them — interest, charges, journals. Not the branch's cash at
|
||||
all, so a different colour rather than a third green that would read as a third cash state. */
|
||||
&--SYSTEM {
|
||||
border-left-color: #6366f1;
|
||||
|
||||
.db-section__head {
|
||||
background: #f5f5ff;
|
||||
}
|
||||
}
|
||||
|
||||
&__head {
|
||||
display: flex;
|
||||
@@ -110,11 +140,19 @@
|
||||
background: #f1f5f9;
|
||||
color: #475569;
|
||||
|
||||
/* Passed, but nothing handed over yet — the lighter green. */
|
||||
&--ok {
|
||||
background: #dcfce7;
|
||||
color: #15803d;
|
||||
}
|
||||
|
||||
/* Cash has actually left the counter. Filled and dark, so the final state is the one the eye
|
||||
lands on: on this page the difference between "approved" and "paid out" is the whole question. */
|
||||
&--done {
|
||||
background: #15803d;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&--pending {
|
||||
background: #fef3c7;
|
||||
color: #b45309;
|
||||
@@ -352,3 +390,52 @@
|
||||
color: #334155;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
/* Officer Verification Sheet — the day totalled per person, for whoever signs the book off.
|
||||
Numeric cells use the shared `.num` from cashier-shared.scss rather than a class of their own:
|
||||
that one already right-aligns and tabular-figures both the heading and the cell, which is what
|
||||
lines a money column up. A local class only styled the cells, leaving every heading left-aligned
|
||||
over right-aligned figures. */
|
||||
.db-byuser {
|
||||
/* Not one of the three sections below, so no section colour: this is the whole day summed by
|
||||
person, and giving it one of their edges would read as a fourth section. */
|
||||
border-left-color: #94a3b8;
|
||||
|
||||
&__name {
|
||||
font-weight: 600;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
/* Received green, paid-out red — the convention a passbook already uses, so the two columns are
|
||||
told apart without reading the headings. */
|
||||
&__cr {
|
||||
color: #15803d;
|
||||
}
|
||||
|
||||
&__dr {
|
||||
color: #b91c1c;
|
||||
}
|
||||
|
||||
&__net {
|
||||
font-weight: 600;
|
||||
|
||||
&--pos {
|
||||
color: #15803d;
|
||||
}
|
||||
|
||||
&--neg {
|
||||
color: #b91c1c;
|
||||
}
|
||||
}
|
||||
|
||||
/* The sheet's own total sits against a tinted band so the eye lands on the line that has to
|
||||
agree with the day's figures above. */
|
||||
&__total {
|
||||
background: #f8fafc;
|
||||
font-weight: 600;
|
||||
|
||||
td {
|
||||
border-top: 2px solid #cbd5e1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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