Show Newbook fetch timestamp with staleness warning on Daily Cash Up

Staff were seeing false 'out of balance' readings when the Newbook
totals shown were fetched at page load (or last manual click) and
had gone stale during the count, then balanced once re-checked
later against fresh Newbook data. Surfacing when the data was last
fetched, with a warning past 3 minutes, lets staff catch this before
submitting instead of assuming a real shortfall.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
jtricerolph 2026-08-11 17:31:20 +00:00
parent b4810aa2c1
commit b750004714

View file

@ -49,6 +49,7 @@ export function DailyCashUp({ user }: Props) {
const [notes, setNotes] = useState('') const [notes, setNotes] = useState('')
const [attachments, setAttachments] = useState<Attachment[]>([]) const [attachments, setAttachments] = useState<Attachment[]>([])
const [newbookTotals, setNewbookTotals] = useState<PaymentTotals | null>(null) const [newbookTotals, setNewbookTotals] = useState<PaymentTotals | null>(null)
const [newbookFetchedAt, setNewbookFetchedAt] = useState<Date | null>(null)
const [tillPayments, setTillPayments] = useState<TillPayment[]>([]) const [tillPayments, setTillPayments] = useState<TillPayment[]>([])
const [transactionBreakdown, setTransactionBreakdown] = useState<TransactionBreakdown | null>(null) const [transactionBreakdown, setTransactionBreakdown] = useState<TransactionBreakdown | null>(null)
const [checkedItems, setCheckedItems] = useState<Set<string>>(new Set()) const [checkedItems, setCheckedItems] = useState<Set<string>>(new Set())
@ -174,6 +175,7 @@ export function DailyCashUp({ user }: Props) {
setNotes('') setNotes('')
setAttachments([]) setAttachments([])
setNewbookTotals(null) setNewbookTotals(null)
setNewbookFetchedAt(null)
setTillPayments([]) setTillPayments([])
setTransactionBreakdown(null) setTransactionBreakdown(null)
setCheckedItems(new Set()) setCheckedItems(new Set())
@ -193,6 +195,7 @@ export function DailyCashUp({ user }: Props) {
'/newbook/payments', { date } '/newbook/payments', { date }
).then(data => { ).then(data => {
setNewbookTotals(data.totals) setNewbookTotals(data.totals)
setNewbookFetchedAt(new Date())
setTillPayments(data.till_payments || []) setTillPayments(data.till_payments || [])
setTransactionBreakdown(data.transaction_breakdown || null) setTransactionBreakdown(data.transaction_breakdown || null)
}).catch(() => { /* silently ignore — button still available */ }) }).catch(() => { /* silently ignore — button still available */ })
@ -230,6 +233,7 @@ export function DailyCashUp({ user }: Props) {
count: number; totals: PaymentTotals; till_payments: TillPayment[]; transaction_breakdown: TransactionBreakdown count: number; totals: PaymentTotals; till_payments: TillPayment[]; transaction_breakdown: TransactionBreakdown
}>('/newbook/payments', { date }) }>('/newbook/payments', { date })
setNewbookTotals(data.totals) setNewbookTotals(data.totals)
setNewbookFetchedAt(new Date())
setTillPayments(data.till_payments || []) setTillPayments(data.till_payments || [])
setTransactionBreakdown(data.transaction_breakdown || null) setTransactionBreakdown(data.transaction_breakdown || null)
flash(`Fetched ${data.count} payment(s) from Newbook.`) flash(`Fetched ${data.count} payment(s) from Newbook.`)
@ -492,10 +496,22 @@ export function DailyCashUp({ user }: Props) {
<Card style={{ marginBottom: '1rem' }}> <Card style={{ marginBottom: '1rem' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1rem' }}> <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1rem' }}>
<h2 style={{ fontSize: '1rem', fontWeight: 700 }}>Newbook Reconciliation</h2> <h2 style={{ fontSize: '1rem', fontWeight: 700 }}>Newbook Reconciliation</h2>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem' }}>
{newbookFetchedAt && (() => {
const ageMin = (Date.now() - newbookFetchedAt.getTime()) / 60000
const stale = ageMin >= 3
return (
<span style={{ fontSize: '0.75rem', color: stale ? '#b45309' : 'var(--text-mid)', fontWeight: stale ? 600 : 400 }}>
Fetched {newbookFetchedAt.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' })}
{stale ? ' — re-fetch before submitting' : ''}
</span>
)
})()}
<Btn onClick={fetchNewbook} disabled={fetching || isFinal} small> <Btn onClick={fetchNewbook} disabled={fetching || isFinal} small>
{fetching ? <><Loader size={13} style={{ animation: 'spin 1s linear infinite' }} /> Fetching</> : <><RefreshCw size={13} /> Fetch Payments</>} {fetching ? <><Loader size={13} style={{ animation: 'spin 1s linear infinite' }} /> Fetching</> : <><RefreshCw size={13} /> Fetch Payments</>}
</Btn> </Btn>
</div> </div>
</div>
{newbookTotals && ( {newbookTotals && (
<> <>