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:
parent
13f1b524b0
commit
dcb450bd99
3 changed files with 113 additions and 49 deletions
|
|
@ -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' }}>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue