Lesson 11 Lab — The Order Create/Edit Form¶
Right now an order can be viewed and advanced through its workflow, but not created from the UI — you've been relying on seed data. Build the Order form and close that gap. It's the shared create/edit form from Lesson 7, with one twist that uses the Context you just built: the Staff dropdown is pre-set to the signed-in user and disabled — you record orders as yourself.
Prerequisite: your API on the HTTP profile with Staff + Orders seed data, and you are signed in (Lesson 11) — the form reads the current user from
useStaffContext().
The form fields¶
- Table Number — required,
valueAsNumber. - Notes — optional textarea.
- Status — a
<select>(Placed … Cancelled) defaulting to PLACED, disabled on create and editable on edit: a new order is always Placed, and day to day the status advances through the Lesson 9 workflow buttons, not this form. (Same rule PRS's Request form uses — Status disabled on Create, editable on Edit.) - Staff — an FK
<select>of staff (options fromstaffAPI.list()), pre-filled from the signed-in user and disabled — you place orders as yourself.
Steps¶
The form is the Lesson 7 pattern; the genuinely new parts are the context pre-fill, the disabled fields, and redirecting to the new record (every form so far went back to a list, where the new id didn't matter).
- Add
post(order)andput(order)toOrderAPI.ts(findand the workflow endpoints already exist). OrderForm—useForm<IOrder>, read:idwithuseParams, and read the signed-in user at the top of the component:const { staff } = useStaffContext();. It's a hook, so it can't go inside thedefaultValuescallback — read it up top, use it below. You also need the dropdown's options in state, as in the Lesson 7 form:Start from this blank order (const [staffList, setStaffList] = useState<IStaff[]>([]); async function loadStaff() { setStaffList(await staffAPI.list()); }IOrderpicked upcancellationReasonin Lesson 8 andorderItemsin Lesson 10):Then in asynclet emptyOrder: IOrder = { id: undefined, tableNumber: undefined, notes: undefined, status: "PLACED", cancellationReason: undefined, total: 0, orderedAt: new Date().toISOString(), staffId: undefined, orderItems: [], };defaultValues:await loadStaff()(options for the dropdown), setemptyOrder.staffId = staff?.id, and returnemptyOrder(create) ororderAPI.find(Number(id))(edit).save— both branches land on the order's detail page, but only edit already knows the id. On create you have to read it back off the POST response:- Table Number (
required,valueAsNumber) and Notes register like any form fields, each with theis-invalid+invalid-feedbackdisplay from Lesson 7. - Staff
<select>— options from yourstaffList, plusdisabled(it's pre-set to you): - Status
<select>— the five status options anddefaultValue="PLACED", disabled on create only:const isEdit = Boolean(id);thendisabled={!isEdit}. Both disabled fields still submit their values — react-hook-form sends what's in its state, not what the DOM would post — so a created order arrives as PLACED, assigned to you. (Stretch #3 digs into why that differs from a plain HTML form.) - A Cancel
Linkback to/ordersand a Save order submit button, as on every form since Lesson 7. - Lay the fields out with flex utilities (
d-flex flex-wrap gap-4,w-75/w-50) — the only custom CSS in this app is the skeleton and Sign In rules you already added. - Thin
OrderCreatePage/OrderEditPage(heading +<OrderForm />) and routesorders/createandorders/edit/:idunderLayout. - Wire it up — add an Add Order button on
OrdersPage(→/orders/create), and the order Edit affordances (there was nowhere for them to go until now): an ⋮ → Edit item onOrderRowand a pencilLinkon Order Detail (both →/orders/edit/:id). - ✅ Checkpoint: Add Order → the form opens with your name filled and disabled in Staff and PLACED disabled in Status; enter a table number, Save → you land on the new order's detail page (201), and it appears in the Orders list. ⋮ → Edit pre-fills.
Verify in the browser¶
With your API running, npm run dev up, and signed in:
/orders→ Add Order → the form; Staff shows your name (disabled), Status PLACED (disabled). Save with Table Number empty → inline error. Fill it, Save → the new order's detail page, 201POST, and a new row in the list.- ⋮ → Edit an order → the form pre-fills and Status is now editable (it was disabled on
create); change Notes, Save → 200
PUT. - Console clean; Network shows the
POST/PUT. - Open the order you just created and advance it to Preparing — Cancel Order is
enabled, because the order is yours. That's the guide's
isOwnOrder || isManagercheck, now demonstrable on a record you own rather than on seed data.
This is the last CRUD screen — TableServe is now a complete app. On PRS this is the
Request create/edit form (a User FK instead of Staff, description / justification
fields, status likewise set by the workflow).
Stretch challenges¶
Optional — for when you finish early. Not needed for the capstone. [Reinforce] builds on what you've done; [Reach] goes past it and needs some research.
- Finish role-gating the maintenance screens — [Reinforce] — the payoff of Lesson 11's role
flags. The guide wrapped the Staff nav link in
{staff?.isAdmin && …}; do the same for Menu Items and Categories, then for the Add and Edit buttons on those three pages (a non-admin shouldn't see a create button for a screen they can't navigate to). Sign in as an admin vs. a non-admin from the seed data and confirm the maintenance UI appears only for admins. (This is the exact client-side role rule PRS uses — admins get the maintenance pages; reviewers get Approve/Reject.) - Cancel returns to the list — [Reinforce] — confirm the form's Cancel link goes back to
/orders(not the detail page) on create, and consider sending edit's Cancel to the order's detail instead — a small UX call about where "cancel" should land. - Order the staff dropdown — [Reach] — even though it's disabled, sort
staffListby last name before rendering so the (pre-selected) option reads naturally. Then work out why yourstaffIdstill reaches the server: MDN says a disabled control's value is not submitted, yet your POST carries it. (Hint: react-hook-form submits its own form state, not the DOM's — and note that RHF'sregister(name, { disabled: true })option behaves differently from the plain HTML attribute you used.) References: MDN — thedisabledattribute and react-hook-form —registeroptions.
Finished these and want more? See stretch-react-challenges.md for bigger challenges that span the whole React pass.