From 4caf307fd1caadf9d8ed7197b9b0deb72a692840 Mon Sep 17 00:00:00 2001 From: jtricerolph Date: Wed, 1 Jul 2026 22:15:42 +0000 Subject: [PATCH] Auto-fetch Newbook on date load; add transaction breakdown checklist MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Newbook payments now fetched automatically when a date loads (in parallel with the cash up fetch), not just on button click - Refresh button still available for manual re-fetch - Transaction breakdown section below reconciliation table: three collapsible groups (Reception Manual, Reception Gateway, Restaurant/Bar) each showing individual transactions with click-to-tick checkboxes for tracking down discrepancies — local state, clears on refresh Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/pages/DailyCashUp.tsx | 139 ++++++++++++++++++++++++++++- frontend/src/types.ts | 14 +++ 2 files changed, 151 insertions(+), 2 deletions(-) diff --git a/frontend/src/pages/DailyCashUp.tsx b/frontend/src/pages/DailyCashUp.tsx index e63d743..53b5235 100644 --- a/frontend/src/pages/DailyCashUp.tsx +++ b/frontend/src/pages/DailyCashUp.tsx @@ -6,6 +6,7 @@ import { GBP_DENOMINATIONS, fmtGBP, today, type User, type CashUp, type Denomination, type CardMachine, type PaymentTotals, type ReconciliationRow, type TillPayment, type Attachment, + type TransactionBreakdown, type TransactionItem, } from '../types' const MACHINES = ['Front Desk', 'Restaurant / Bar'] @@ -45,6 +46,8 @@ export function DailyCashUp({ user: _user }: Props) { const [, setAttachments] = useState([]) const [newbookTotals, setNewbookTotals] = useState(null) const [tillPayments, setTillPayments] = useState([]) + const [transactionBreakdown, setTransactionBreakdown] = useState(null) + const [checkedItems, setCheckedItems] = useState>(new Set()) const [fetching, setFetching] = useState(false) const [saving, setSaving] = useState(false) const [msg, setMsg] = useState<{ text: string; ok: boolean } | null>(null) @@ -92,7 +95,7 @@ export function DailyCashUp({ user: _user }: Props) { setPageState(data.cash_up.status === 'final' ? 'locked' : 'editing') } - // Auto-check on mount and whenever date changes + // Auto-check on mount and whenever date changes; auto-fetch Newbook in parallel useEffect(() => { setPageState('checking') setCashUp(null) @@ -103,6 +106,8 @@ export function DailyCashUp({ user: _user }: Props) { setAttachments([]) setNewbookTotals(null) setTillPayments([]) + setTransactionBreakdown(null) + setCheckedItems(new Set()) api.get<{ cash_up: CashUp; denominations: Denomination[]; card_machines: CardMachine[]; @@ -113,6 +118,15 @@ export function DailyCashUp({ user: _user }: Props) { if (e instanceof Error && e.message === 'Not found') setPageState('empty') else { flash('Failed to load data for this date.', false); setPageState('empty') } }) + + // Auto-fetch Newbook — soft failure, user can retry with the button + api.post<{ count: number; totals: PaymentTotals; till_payments: TillPayment[]; transaction_breakdown: TransactionBreakdown }>( + '/newbook/payments', { date } + ).then(data => { + setNewbookTotals(data.totals) + setTillPayments(data.till_payments || []) + setTransactionBreakdown(data.transaction_breakdown || null) + }).catch(() => { /* silently ignore — button still available */ }) }, [date]) function updateDenom(list: Denomination[], setList: (d: Denomination[]) => void, idx: number, field: 'quantity' | 'value_entered', raw: string) { @@ -144,10 +158,12 @@ export function DailyCashUp({ user: _user }: Props) { setFetching(true) try { const data = await api.post<{ - count: number; totals: PaymentTotals; till_payments: TillPayment[] + count: number; totals: PaymentTotals; till_payments: TillPayment[]; transaction_breakdown: TransactionBreakdown }>('/newbook/payments', { date }) setNewbookTotals(data.totals) setTillPayments(data.till_payments || []) + setTransactionBreakdown(data.transaction_breakdown || null) + setCheckedItems(new Set()) flash(`Fetched ${data.count} payment(s) from Newbook.`) } catch (e: unknown) { flash(e instanceof Error ? e.message : 'Failed to fetch Newbook data.', false) @@ -371,6 +387,17 @@ export function DailyCashUp({ user: _user }: Props) { )} + {transactionBreakdown && ( + setCheckedItems(prev => { + const next = new Set(prev) + next.has(key) ? next.delete(key) : next.add(key) + return next + })} + /> + )} )} @@ -465,6 +492,114 @@ const inputSt: React.CSSProperties = { background: 'white', } +const SECTION_LABELS: Record = { + reception_manual: 'Reception — Manual', + reception_gateway: 'Reception — Gateway', + restaurant_bar: 'Restaurant / Bar', +} + +function TransactionChecklist({ breakdown, checked, onToggle }: { + breakdown: TransactionBreakdown + checked: Set + onToggle: (key: string) => void +}) { + const [openSections, setOpenSections] = useState>(new Set()) + + const allItems: Array<{ key: string; section: string; category: string; item: TransactionItem }> = [] + for (const [section, categories] of Object.entries(breakdown) as [keyof TransactionBreakdown, Record][]) { + for (const [category, items] of Object.entries(categories)) { + items.forEach((item, i) => allItems.push({ key: `${section}:${category}:${i}`, section, category, item })) + } + } + + const totalItems = allItems.length + const checkedCount = allItems.filter(a => checked.has(a.key)).length + + if (totalItems === 0) return null + + const toggleSection = (s: string) => setOpenSections(prev => { + const next = new Set(prev); next.has(s) ? next.delete(s) : next.add(s); return next + }) + + return ( +
+
+

Transaction Breakdown

+ + {checkedCount}/{totalItems} checked + +
+ + {(Object.entries(breakdown) as [keyof TransactionBreakdown, Record][]).map(([section, categories]) => { + const sectionItems = Object.values(categories).flat() + if (sectionItems.length === 0) return null + const sectionKeys = Object.entries(categories).flatMap(([cat, items]) => + items.map((_, i) => `${section}:${cat}:${i}`) + ) + const sectionChecked = sectionKeys.filter(k => checked.has(k)).length + const isOpen = openSections.has(section) + + return ( +
+ + + {isOpen && ( +
+ {Object.entries(categories).map(([category, items]) => ( +
+
+ {category} +
+ {items.map((item, i) => { + const key = `${section}:${category}:${i}` + const isDone = checked.has(key) + return ( +
onToggle(key)} style={{ + display: 'grid', gridTemplateColumns: '20px 1fr auto auto', + gap: '0.5rem', alignItems: 'center', + padding: '0.35rem 0.75rem', cursor: 'pointer', + background: isDone ? '#f0fdf4' : 'white', + borderBottom: '1px solid var(--card-border)', + opacity: isDone ? 0.6 : 1, + }}> + + {isDone ? '✓' : '○'} + +
+ + {item.details || item.payment_type} + + {item.time && ( + {item.time.slice(11, 16)} + )} +
+ {item.payment_type} + + {item.is_voided ? '-' : ''}{fmtGBP(Math.abs(item.amount))} + +
+ ) + })} +
+ ))} +
+ )} +
+ ) + })} +
+ ) +} + function MoneyInput({ label, value, onChange, disabled }: { label: string; value: number; onChange: (v: string) => void; disabled: boolean }) { diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 93ec7f4..da075ea 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -60,6 +60,20 @@ export interface TillPayment { total_value: number } +export interface TransactionItem { + time: string + payment_type: string + details: string + amount: number + is_voided: boolean +} + +export interface TransactionBreakdown { + reception_manual: Record + reception_gateway: Record + restaurant_bar: Record +} + export interface Attachment { id: number cash_up_id: number