From 596b0b6f0370a78bf7412b142254df0e0b6838a9 Mon Sep 17 00:00:00 2001 From: adamp Date: Mon, 9 Feb 2026 18:30:10 -0600 Subject: [PATCH] Add walk-in customer name field and bulk restock page When "Walk-in" is selected on the new order form, an optional name input now appears. If filled, a new customer is created and linked to the order; if left blank, the order remains a nameless walk-in as before. Adds a new Restock page that lists all products with their current stock and lets the user enter quantities to add in bulk, using the existing PATCH /products/:id/stock endpoint. Co-Authored-By: Claude Opus 4.6 --- client/src/App.jsx | 2 + client/src/components/Layout.jsx | 1 + client/src/pages/OrderNew.jsx | 15 +++- client/src/pages/Restock.jsx | 116 +++++++++++++++++++++++++++++++ server/routes/orders.js | 9 ++- 5 files changed, 140 insertions(+), 3 deletions(-) create mode 100644 client/src/pages/Restock.jsx diff --git a/client/src/App.jsx b/client/src/App.jsx index b82b9c7..68191ec 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -9,6 +9,7 @@ import Customers from './pages/Customers'; import Orders from './pages/Orders'; import OrderNew from './pages/OrderNew'; import OrderDetail from './pages/OrderDetail'; +import Restock from './pages/Restock'; function AuthGate({ children }) { const [state, setState] = useState({ checked: false, authenticated: false }); @@ -51,6 +52,7 @@ export default function App() { } /> } /> } /> + } /> )} diff --git a/client/src/components/Layout.jsx b/client/src/components/Layout.jsx index 4693398..8d61d16 100644 --- a/client/src/components/Layout.jsx +++ b/client/src/components/Layout.jsx @@ -7,6 +7,7 @@ const navItems = [ { path: '/inventory', label: 'Inventory' }, { path: '/customers', label: 'Customers' }, { path: '/orders', label: 'Orders' }, + { path: '/restock', label: 'Restock' }, ]; export default function Layout({ children, onLogout }) { diff --git a/client/src/pages/OrderNew.jsx b/client/src/pages/OrderNew.jsx index 050dc7c..67fbf1d 100644 --- a/client/src/pages/OrderNew.jsx +++ b/client/src/pages/OrderNew.jsx @@ -10,6 +10,7 @@ export default function OrderNew() { const [customers, setCustomers] = useState([]); const [products, setProducts] = useState([]); const [customerId, setCustomerId] = useState(preselectedCustomerId || ''); + const [walkInName, setWalkInName] = useState(''); const [items, setItems] = useState([]); const [status, setStatus] = useState('pending'); const [notes, setNotes] = useState(''); @@ -60,6 +61,7 @@ export default function OrderNew() { } const payload = { customer_id: customerId ? Number(customerId) : null, + walk_in_name: !customerId && walkInName.trim() ? walkInName.trim() : undefined, status, notes: notes.trim() || null, items: items.map((line) => ({ @@ -100,7 +102,7 @@ export default function OrderNew() { + {customerId === '' && ( +
+ + setWalkInName(e.target.value)} + placeholder="Walk-in customer name" + /> +
+ )}
diff --git a/client/src/pages/Restock.jsx b/client/src/pages/Restock.jsx new file mode 100644 index 0000000..388a730 --- /dev/null +++ b/client/src/pages/Restock.jsx @@ -0,0 +1,116 @@ +import { useEffect, useState } from 'react'; +import { api } from '../api'; + +export default function Restock() { + const [products, setProducts] = useState([]); + const [adjustments, setAdjustments] = useState({}); + const [loading, setLoading] = useState(true); + const [submitting, setSubmitting] = useState(false); + const [error, setError] = useState(null); + const [success, setSuccess] = useState(null); + + function load() { + api('/products') + .then((r) => (r.ok ? r.json() : Promise.reject(new Error('Failed to load')))) + .then((data) => { + setProducts(data); + setAdjustments({}); + }) + .catch((e) => setError(e.message)) + .finally(() => setLoading(false)); + } + + useEffect(() => { + load(); + }, []); + + function handleSubmit(e) { + e.preventDefault(); + const updates = Object.entries(adjustments) + .filter(([, qty]) => Number(qty) > 0) + .map(([id, qty]) => ({ id: Number(id), adjustment: Number(qty) })); + + if (updates.length === 0) { + setError('Enter a quantity for at least one product.'); + return; + } + + setSubmitting(true); + setError(null); + setSuccess(null); + + Promise.all( + updates.map(({ id, adjustment }) => + api(`/products/${id}/stock`, { + method: 'PATCH', + body: { adjustment }, + }).then((r) => { + if (!r.ok) return r.json().then((e) => Promise.reject(new Error(e.error || 'Failed'))); + return r.json(); + }) + ) + ) + .then(() => { + setSuccess(`Restocked ${updates.length} product${updates.length > 1 ? 's' : ''}.`); + load(); + }) + .catch((e) => setError(e.message)) + .finally(() => setSubmitting(false)); + } + + if (loading) return

Loading...

; + + return ( + <> +

Restock

+ {error &&

{error}

} + {success &&

{success}

} + + {products.length === 0 ? ( +

No products in inventory.

+ ) : ( +
+
+
+ + + + + + + + + + {products.map((p) => ( + + + + + + ))} + +
ProductCurrent stockAdd
{p.name}{p.quantity_on_hand} + + setAdjustments((prev) => ({ ...prev, [p.id]: e.target.value })) + } + placeholder="0" + style={{ width: 80 }} + /> +
+
+
+ +
+ +
+
+ )} + + ); +} diff --git a/server/routes/orders.js b/server/routes/orders.js index bcbfad3..8ef1e41 100644 --- a/server/routes/orders.js +++ b/server/routes/orders.js @@ -72,7 +72,7 @@ router.get('/:id', (req, res) => { router.post('/', (req, res) => { try { const db = getDb(); - const { customer_id, status = 'pending', notes, items = [] } = req.body; + const { customer_id, walk_in_name, status = 'pending', notes, items = [] } = req.body; if (!Array.isArray(items) || items.length === 0) { return res.status(400).json({ error: 'At least one order item is required' }); } @@ -84,9 +84,14 @@ router.post('/', (req, res) => { } const createOrder = db.transaction(() => { + let resolvedCustomerId = customer_id || null; + if (!resolvedCustomerId && typeof walk_in_name === 'string' && walk_in_name.trim()) { + const custResult = db.prepare('INSERT INTO customers (name) VALUES (?)').run(walk_in_name.trim()); + resolvedCustomerId = custResult.lastInsertRowid; + } const result = db.prepare( 'INSERT INTO orders (customer_id, status, notes) VALUES (?, ?, ?)' - ).run(customer_id || null, status, notes || null); + ).run(resolvedCustomerId, status, notes || null); const orderId = result.lastInsertRowid; for (const it of items) {