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 <noreply@anthropic.com>
This commit is contained in:
parent
39b2ce73da
commit
596b0b6f03
@ -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() {
|
||||
<Route path="/orders" element={<Orders />} />
|
||||
<Route path="/orders/new" element={<OrderNew />} />
|
||||
<Route path="/orders/:id" element={<OrderDetail />} />
|
||||
<Route path="/restock" element={<Restock />} />
|
||||
</Routes>
|
||||
</Layout>
|
||||
)}
|
||||
|
||||
@ -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 }) {
|
||||
|
||||
@ -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() {
|
||||
<label>Customer</label>
|
||||
<select
|
||||
value={customerId}
|
||||
onChange={(e) => setCustomerId(e.target.value)}
|
||||
onChange={(e) => { setCustomerId(e.target.value); setWalkInName(''); }}
|
||||
>
|
||||
<option value="">Walk-in</option>
|
||||
{customers.map((c) => (
|
||||
@ -108,6 +110,17 @@ export default function OrderNew() {
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
{customerId === '' && (
|
||||
<div className="form-group">
|
||||
<label>Name (optional)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={walkInName}
|
||||
onChange={(e) => setWalkInName(e.target.value)}
|
||||
placeholder="Walk-in customer name"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="form-row">
|
||||
<div className="form-group">
|
||||
<label>Status</label>
|
||||
|
||||
116
client/src/pages/Restock.jsx
Normal file
116
client/src/pages/Restock.jsx
Normal file
@ -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 <p className="text-muted">Loading...</p>;
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1 className="page-title">Restock</h1>
|
||||
{error && <p className="error">{error}</p>}
|
||||
{success && <p style={{ color: 'var(--color-success, green)', marginBottom: '1rem' }}>{success}</p>}
|
||||
|
||||
{products.length === 0 ? (
|
||||
<p className="text-muted">No products in inventory.</p>
|
||||
) : (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="card-list table-layout">
|
||||
<div className="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Product</th>
|
||||
<th>Current stock</th>
|
||||
<th>Add</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{products.map((p) => (
|
||||
<tr key={p.id}>
|
||||
<td data-label="Product">{p.name}</td>
|
||||
<td data-label="Current stock">{p.quantity_on_hand}</td>
|
||||
<td data-label="Add">
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
value={adjustments[p.id] || ''}
|
||||
onChange={(e) =>
|
||||
setAdjustments((prev) => ({ ...prev, [p.id]: e.target.value }))
|
||||
}
|
||||
placeholder="0"
|
||||
style={{ width: 80 }}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: '1rem' }}>
|
||||
<button type="submit" disabled={submitting}>
|
||||
{submitting ? 'Restocking…' : 'Submit restock'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -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) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user