Cashup: configurable card machines — settings + inline Z-report layout

Settings: add Card Machines section to configure number of PDQ terminals
and their names (1–6 machines, defaults to Front Desk + Restaurant / Bar).
Names are stored under the 'card_machines' settings key.

DailyCashUp: machine names now load from settings instead of hardcoded
const. Card Machines section changed from a 2-column grid to a vertical
list; each machine row shows the amount inputs on the left and its PDQ
Z-report photo uploader on the right. Separate PDQ Z-Reports card removed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
jtricerolph 2026-07-07 11:32:37 +00:00
parent 13f1b524b0
commit dcb450bd99
3 changed files with 113 additions and 49 deletions

View file

@ -9,7 +9,7 @@ import {
type TransactionBreakdown, type TransactionItem,
} from '../types'
const MACHINES = ['Front Desk', 'Restaurant / Bar']
const DEFAULT_MACHINE_NAMES = ['Front Desk', 'Restaurant / Bar']
function initDenominations(countType: 'takings' | 'float'): Denomination[] {
return GBP_DENOMINATIONS.map(d => ({
@ -22,8 +22,8 @@ function initDenominations(countType: 'takings' | 'float'): Denomination[] {
}))
}
function initMachines(): CardMachine[] {
return MACHINES.map(name => ({ machine_name: name, total_amount: 0, amex_amount: 0, visa_mc_amount: 0 }))
function initMachines(names: string[]): CardMachine[] {
return names.map(name => ({ machine_name: name, total_amount: 0, amex_amount: 0, visa_mc_amount: 0 }))
}
function denomTotal(denoms: Denomination[]) {
@ -42,7 +42,8 @@ export function DailyCashUp({ user }: Props) {
const [cashUp, setCashUp] = useState<CashUp | null>(null)
const [takings, setTakings] = useState<Denomination[]>(initDenominations('takings'))
const [float, setFloat] = useState<Denomination[]>(initDenominations('float'))
const [machines, setMachines] = useState<CardMachine[]>(initMachines())
const machineNamesRef = useRef<string[]>(DEFAULT_MACHINE_NAMES)
const [machines, setMachines] = useState<CardMachine[]>(() => initMachines(DEFAULT_MACHINE_NAMES))
const [notes, setNotes] = useState('')
const [attachments, setAttachments] = useState<Attachment[]>([])
const [newbookTotals, setNewbookTotals] = useState<PaymentTotals | null>(null)
@ -91,7 +92,7 @@ export function DailyCashUp({ user }: Props) {
setFloat(rebuild('float'))
if (data.card_machines.length) {
setMachines(MACHINES.map(name => {
setMachines(machineNamesRef.current.map(name => {
const m = data.card_machines.find(c => c.machine_name === name)
if (!m) return { machine_name: name, total_amount: 0, amex_amount: 0, visa_mc_amount: 0 }
return {
@ -133,10 +134,24 @@ export function DailyCashUp({ user }: Props) {
return () => clearInterval(interval)
}, [pageState])
// Fetch till float target once on mount
// Fetch settings once on mount (float target + machine names)
useEffect(() => {
api.get<{ till_float_target?: string }>('/settings')
.then(s => setTillFloatTarget(parseFloat(s.till_float_target || '0') || 0))
api.get<{ till_float_target?: string; card_machines?: string }>('/settings')
.then(s => {
setTillFloatTarget(parseFloat(s.till_float_target || '0') || 0)
try {
const names = JSON.parse(s.card_machines || '[]') as string[]
if (names.length > 0) {
machineNamesRef.current = names
// Only reinit machines if they're still all-zero (not yet touched)
setMachines(prev =>
prev.every(m => m.total_amount === 0 && m.amex_amount === 0)
? initMachines(names)
: prev
)
}
} catch {}
})
.catch(() => {})
}, [])
@ -146,7 +161,7 @@ export function DailyCashUp({ user }: Props) {
setCashUp(null)
setTakings(initDenominations('takings'))
setFloat(initDenominations('float'))
setMachines(initMachines())
setMachines(initMachines(machineNamesRef.current))
setNotes('')
setAttachments([])
setNewbookTotals(null)
@ -376,59 +391,60 @@ export function DailyCashUp({ user }: Props) {
<DenomGrid denoms={takings} onChange={(i, f, v) => updateDenom(takings, setTakings, i, f, v)} disabled={isFinal} tabBase={0} />
</Card>
{/* Card Machines */}
{/* Card Machines (PDQ) — inputs + Z-report uploads side by side per machine */}
<Card style={{ marginBottom: '1rem' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1rem' }}>
<h2 style={{ fontSize: '1rem', fontWeight: 700 }}>Card Machines (PDQ)</h2>
<span style={{ fontWeight: 700, fontSize: '1.1rem' }}>Total: {fmtGBP(totalPdq)}</span>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem' }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
{machines.map((m, i) => (
<div key={m.machine_name} style={{ border: '1px solid var(--card-border)', borderRadius: '8px', padding: '1rem' }}>
<h3 style={{ fontSize: '0.875rem', fontWeight: 600, marginBottom: '0.75rem', color: 'var(--text-mid)' }}>
{m.machine_name}
</h3>
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
<MoneyInput label="Total" value={m.total_amount}
onChange={v => updateMachine(i, 'total_amount', v)} disabled={isFinal} />
<MoneyInput label="Amex" value={m.amex_amount}
onChange={v => updateMachine(i, 'amex_amount', v)} disabled={isFinal} />
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '0.875rem', paddingTop: '0.25rem' }}>
<span style={{ color: 'var(--text-mid)' }}>Visa / MC</span>
<span style={{ fontWeight: 600 }}>{fmtGBP(m.visa_mc_amount)}</span>
<div key={m.machine_name} style={{
display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem',
border: '1px solid var(--card-border)', borderRadius: '8px', padding: '1rem',
}}>
{/* Left: amount inputs */}
<div>
<h3 style={{ fontSize: '0.875rem', fontWeight: 600, marginBottom: '0.75rem', color: 'var(--text-mid)' }}>
{m.machine_name}
</h3>
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
<MoneyInput label="Total" value={m.total_amount}
onChange={v => updateMachine(i, 'total_amount', v)} disabled={isFinal} />
<MoneyInput label="Amex" value={m.amex_amount}
onChange={v => updateMachine(i, 'amex_amount', v)} disabled={isFinal} />
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '0.875rem', paddingTop: '0.25rem' }}>
<span style={{ color: 'var(--text-mid)' }}>Visa / MC</span>
<span style={{ fontWeight: 600 }}>{fmtGBP(m.visa_mc_amount)}</span>
</div>
</div>
</div>
{/* Right: PDQ Z-report upload */}
<div>
<div style={{ fontSize: '0.75rem', color: 'var(--text-mid)', marginBottom: '0.5rem', fontWeight: 600 }}>
Z-Report
</div>
{cashUp ? (
<PhotoUploader
cashUpId={cashUp.id}
attachmentType="pdq_z_report"
label={m.machine_name}
attachments={attachments}
onAdded={a => setAttachments(prev => [...prev, a])}
onRemoved={id => setAttachments(prev => prev.filter(a => a.id !== id))}
disabled={isFinal}
/>
) : (
<p style={{ fontSize: '0.75rem', color: 'var(--text-mid)', fontStyle: 'italic' }}>
Save a draft first to upload photos.
</p>
)}
</div>
</div>
))}
</div>
</Card>
{/* PDQ Z-Reports — one upload area per machine */}
{cashUp && (
<Card style={{ marginBottom: '1rem' }}>
<h2 style={{ fontSize: '1rem', fontWeight: 700, marginBottom: '0.75rem' }}>PDQ Z-Reports</h2>
<p style={{ fontSize: '0.75rem', color: 'var(--text-mid)', marginBottom: '1rem' }}>
Upload the end-of-day Z-report printout for each card machine.
</p>
<div style={{ display: 'flex', flexDirection: 'column', gap: '1.25rem' }}>
{MACHINES.map(name => (
<div key={name}>
<div style={{ fontSize: '0.8rem', fontWeight: 600, marginBottom: '0.5rem' }}>{name}</div>
<PhotoUploader
cashUpId={cashUp.id}
attachmentType="pdq_z_report"
label={name}
attachments={attachments}
onAdded={a => setAttachments(prev => [...prev, a])}
onRemoved={id => setAttachments(prev => prev.filter(a => a.id !== id))}
disabled={isFinal}
/>
</div>
))}
</div>
</Card>
)}
{/* Newbook + Reconciliation */}
<Card style={{ marginBottom: '1rem' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1rem' }}>