diff --git a/backend/src/routes/settings.js b/backend/src/routes/settings.js index b730301..9c64e29 100644 --- a/backend/src/routes/settings.js +++ b/backend/src/routes/settings.js @@ -5,6 +5,7 @@ import { testConnection, fetchGlAccountsGrouped } from '../lib/newbook.js' const ALL_KEYS = [ 'default_report_days', 'petty_cash_float', 'till_float_target', 'sales_breakdown_columns', 'change_tin_breakdown', 'change_tin_notes', + 'card_machines', ] export async function settingsRoutes(app) { diff --git a/frontend/src/pages/DailyCashUp.tsx b/frontend/src/pages/DailyCashUp.tsx index a9d302e..35bcce0 100644 --- a/frontend/src/pages/DailyCashUp.tsx +++ b/frontend/src/pages/DailyCashUp.tsx @@ -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(null) const [takings, setTakings] = useState(initDenominations('takings')) const [float, setFloat] = useState(initDenominations('float')) - const [machines, setMachines] = useState(initMachines()) + const machineNamesRef = useRef(DEFAULT_MACHINE_NAMES) + const [machines, setMachines] = useState(() => initMachines(DEFAULT_MACHINE_NAMES)) const [notes, setNotes] = useState('') const [attachments, setAttachments] = useState([]) const [newbookTotals, setNewbookTotals] = useState(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) { updateDenom(takings, setTakings, i, f, v)} disabled={isFinal} tabBase={0} /> - {/* Card Machines */} + {/* Card Machines (PDQ) — inputs + Z-report uploads side by side per machine */}

Card Machines (PDQ)

Total: {fmtGBP(totalPdq)}
-
+
{machines.map((m, i) => ( -
-

- {m.machine_name} -

-
- updateMachine(i, 'total_amount', v)} disabled={isFinal} /> - updateMachine(i, 'amex_amount', v)} disabled={isFinal} /> -
- Visa / MC - {fmtGBP(m.visa_mc_amount)} +
+ {/* Left: amount inputs */} +
+

+ {m.machine_name} +

+
+ updateMachine(i, 'total_amount', v)} disabled={isFinal} /> + updateMachine(i, 'amex_amount', v)} disabled={isFinal} /> +
+ Visa / MC + {fmtGBP(m.visa_mc_amount)} +
+ {/* Right: PDQ Z-report upload */} +
+
+ Z-Report +
+ {cashUp ? ( + setAttachments(prev => [...prev, a])} + onRemoved={id => setAttachments(prev => prev.filter(a => a.id !== id))} + disabled={isFinal} + /> + ) : ( +

+ Save a draft first to upload photos. +

+ )} +
))}
- {/* PDQ Z-Reports — one upload area per machine */} - {cashUp && ( - -

PDQ Z-Reports

-

- Upload the end-of-day Z-report printout for each card machine. -

-
- {MACHINES.map(name => ( -
-
{name}
- setAttachments(prev => [...prev, a])} - onRemoved={id => setAttachments(prev => prev.filter(a => a.id !== id))} - disabled={isFinal} - /> -
- ))} -
-
- )} - {/* Newbook + Reconciliation */}
diff --git a/frontend/src/pages/Settings.tsx b/frontend/src/pages/Settings.tsx index 9d7cf76..61f27c1 100644 --- a/frontend/src/pages/Settings.tsx +++ b/frontend/src/pages/Settings.tsx @@ -3,6 +3,8 @@ import { api } from '../api' import { PageHeader, Card, Btn } from '../components/Layout' import { can, fmtGBP, type User } from '../types' +const DEFAULT_MACHINE_NAMES = ['Front Desk', 'Restaurant / Bar'] + interface SettingsData { default_report_days: string petty_cash_float: string @@ -10,6 +12,7 @@ interface SettingsData { sales_breakdown_columns: string change_tin_breakdown: string change_tin_notes: string + card_machines: string } // Banknotes use individual note qty; coins use sealed bag qty @@ -31,6 +34,7 @@ interface GlColumn { gl_code: string; display_name: string; enabled: boolean; so export function SettingsPage({ user }: { user: User }) { const [settings, setSettings] = useState>({}) const [columns, setColumns] = useState([]) + const [machineNames, setMachineNames] = useState(DEFAULT_MACHINE_NAMES) const [tinTargetUnits, setTinTargetUnits] = useState>({}) const [tinDenomNotes, setTinDenomNotes] = useState>({}) const [loading, setLoading] = useState(true) @@ -48,6 +52,10 @@ export function SettingsPage({ user }: { user: User }) { api.get('/settings').then(s => { setSettings(s) try { setColumns(JSON.parse(s.sales_breakdown_columns || '[]')) } catch { setColumns([]) } + try { + const names = JSON.parse(s.card_machines || '[]') as string[] + setMachineNames(names.length ? names : DEFAULT_MACHINE_NAMES) + } catch { setMachineNames(DEFAULT_MACHINE_NAMES) } try { const breakdown = JSON.parse(s.change_tin_breakdown || '{}') const units: Record = {} @@ -78,6 +86,7 @@ export function SettingsPage({ user }: { user: User }) { sales_breakdown_columns: JSON.stringify(columns), change_tin_breakdown: JSON.stringify(tinBreakdown), change_tin_notes: JSON.stringify(tinDenomNotes), + card_machines: JSON.stringify(machineNames), }) flash('Settings saved.') } catch (e: unknown) { @@ -186,6 +195,44 @@ export function SettingsPage({ user }: { user: User }) {
+ {/* Card machines */} + +
+
+

Card Machines (PDQ)

+

+ Define the PDQ terminals used each day. Names appear on the cash up form. +

+
+ {machineNames.length < 6 && ( + setMachineNames(prev => [...prev, `Machine ${prev.length + 1}`])}> + + Add + + )} +
+
+ {machineNames.map((name, i) => ( +
+ {i + 1}. + setMachineNames(prev => prev.map((n, j) => j === i ? e.target.value : n))} + placeholder={`Machine ${i + 1}`} + style={{ ...inpSt, flex: 1 }} + /> + {machineNames.length > 1 && ( + + )} +
+ ))} +
+
+ {/* Change tin target breakdown */}

Change Tin Target