Administrator guide
There is exactly one administrator role, and it is global: every restaurant, every order, the full catalog with supplier detail, and all supplier configuration. There is no per-restaurant admin.
The administrator's screens live under /admin in the same application chefs use, and the
Administration group only appears in the sidebar for an admin. Because the admin role also
satisfies the chef guard, an administrator can use the chef screens too — browse the catalog, build a
cart and place an order against any active restaurant. Admins are deliberately not given
user_restaurants rows; their account is listed against every active location instead.
Admin routes are gated twice: a chef who reaches an /admin/... URL is shown the /403 page, and the
API refuses the call regardless of what the browser sends. Only the second gate is a security control.
1. Catalog items
Where: /admin/catalog
Each row shows the item and its supplier mapping — primary supplier, alternate, supplier SKU — because an unmapped item cannot be ordered at all, and that is the most important fact about a row. Rows carry a Not mapped badge when there is no mapping and an Inactive badge when the item is deactivated. Search by name, and toggle Show inactive items (on by default).
Creating an item
New item opens a dialog with these fields:
| Field | Required | Notes |
|---|---|---|
| Name | Yes | What the chef sees, up to 200 characters; must be unique |
| Category | No | Up to 100 characters; drives catalog grouping and filtering |
| Ordering unit | Yes | case, lb, each — the unit quantities are expressed in |
| Pack size | No | e.g. 6x10oz; display only |
| Description | No | Up to 2,000 characters |
| Active | Yes | Defaults to on |
A saved item does not appear in the chef catalog until it has an active mapping to an active supplier — the chef catalog only lists orderable items. Use the Map action on the row (it becomes Mapping once one exists) to create it without leaving the page.
Editing and deactivating
Editing is a straight update of the same fields, including the active toggle. Deactivate is a soft
delete: the active flag is cleared, the item disappears from the chef catalog immediately, and every
historical order that referenced it keeps working. The confirmation dialog says exactly that, and
reactivating is just an edit. Nothing in this application hard-deletes catalog data, because order
history has to stay readable.
2. Suppliers
Where: /admin/suppliers
Each row shows the supplier's name, its channel type, and how many catalog items are mapped to it. A supplier record is a name, a type, an active flag and a type-specific connection config. The type selects which worker handler runs, so it is not a casual change.
| Type | Channel | Config fields |
|---|---|---|
sysco | X12 850 EDI file over SFTP | Outbound / inbound / archive directories, ISA05–ISA08 qualifiers and ids, EDI version, product-id qualifier, and Vault secret names |
costco | Structured HTML order email | Email address, optional CC list |
central_kitchen | PDF purchase order attached to an email | Email address, optional CC list, optional facility name for the PDF header |
The form renders only the fields that belong to the selected type, rather than a union of everything.
The Sysco config holds the name of a Vault secret — never a host password, a private key or a
passphrase. The worker resolves those names against Vault at run time. The API enforces this: anything
that looks like a plaintext credential is rejected with a 422. Treat everything you type into a config
field as readable by any administrator.
Deactivate refuses with a 422 while the supplier is still the primary supplier of any active
mapping, and the dialog lists the exact catalog items you need to re-map first — deactivating anyway
would turn each of them into a silent dispatch failure. Being named only as an alternate does not
block deactivation: losing a fallback degrades a back order to backorder_no_alternate, which is a
modelled outcome rather than a broken order.
3. SKU mappings
Where: /admin/mappings, and inline from the catalog list
This is the routing engine's configuration and the most consequential screen in the admin area. One catalog item has exactly one mapping. Filter the list by supplier — the filter matches mappings where that supplier is either the primary or the alternate.
| Field | Required | Purpose |
|---|---|---|
| Catalog item | Yes | The item being mapped; unique across mappings |
| Primary supplier | Yes | Who normally fulfils this item |
| Supplier SKU | Yes | The supplier's own product code — this is what goes on the wire |
| Product code | No | An additional supplier reference |
| Alternate supplier | No | Who catches this item when the primary back-orders it |
| Mapping is active | Yes | An inactive mapping makes the item un-orderable |
The supplier SKU is the value that ends up in the Sysco EDI PO1 segment, in the Costco order email
and on the Central Kitchen PDF. Get it wrong and the order transmits successfully and is then filled
wrongly — worse than a failure. Check it against the supplier's own catalog.
The alternate supplier is what makes automatic back-order recovery possible. Without one, a
back-ordered line stops at backorder_no_alternate and waits for you.
The alternate must differ from the primary. The dialog blocks it before you submit and the API rejects it as well, using the same message from the shared package — rerouting a back order straight back to the supplier that just back-ordered it would silently defeat the whole automation.
Delete is a soft delete that is never blocked, but it makes the catalog item un-orderable. The API
returns that consequence as a warning string, which the confirmation dialog shows before the fact and
the success toast repeats afterwards. Both a deletion and an edit are recorded as MAPPING_UPDATED in
the audit trail.
A worked example
| Catalog item | Primary | Supplier SKU | Alternate |
|---|---|---|---|
| Roma tomatoes, case 6x10oz | Sysco | 4471102 | Central Kitchen |
| Whole milk, case 4x1gal | Costco Business Center | 18864 | Sysco |
| House stock base, 5 lb tub | Central Kitchen | CK-STOCK-05 | — |
The third row has no alternate on purpose. Central Kitchen is Lucille's own facility; if it cannot fill an item there is nowhere sensible to reroute to, and a human decision is the right outcome.
4. Restaurants and chef assignment
Where: /admin/restaurants
Create and edit locations — name (up to 200 characters), an optional location string, and an active
toggle. Deactivating a location stops new orders against it (placement returns a 409) without
touching its history. Each row shows how many chefs are assigned.
Chefs opens the assignment dialog: the chefs currently assigned, each with an Unassign button, and a field to assign another. Assignment is many-to-many — a chef can hold several restaurants and a restaurant can have several chefs.
Assignment asks for a user id, not a name from a dropdown, and that is not an oversight: there is no user-directory endpoint. Users are provisioned in Zitadel and mirrored into the application database on their first authenticated call, so before someone has signed in once there is nothing to enumerate and nothing to assign. The order of operations is therefore:
- create the user in Zitadel and grant them the
chefrole; - have them sign in to the Order Center once;
- assign them here, using the id from Zitadel.
The dialog surfaces the two failures verbatim: a 404 explaining that a user who has never signed in
cannot be assigned yet, and a 422 if the id belongs to an administrator — admin access is global and
is deliberately not expressed as an assignment row. Assigning the same chef twice returns a 409.
Assignments and removals are audited as CHEF_ASSIGNED and CHEF_UNASSIGNED, and take effect on the
chef's next request because assignments are re-read from the database every time.
5. Order oversight and the audit feed
Where: /admin/orders
Two tabs on one screen, each paginated independently.
Orders spans every restaurant. Filter by restaurant, status and date range. Alongside the usual
columns there is a Needs attention column that badges the count of line items in each of the four
statuses worth chasing — backorder_no_alternate, dispatch_failed, backordered and rerouted — so
you can triage the list without opening every row. Clicking a row opens the same order detail page
chefs use, except that as an admin you also get supplier names on lines and dispatches, unredacted
audit payloads, and the reroute action.
Audit feed is the global trail. Filter by any of the 22 event types, expand any row's payload as JSON, and jump straight to the order an event belongs to. The trail is append-only and enforced by a database trigger, which is what makes it usable as evidence when a delivery is disputed.
Three things are worth watching for, with the order lifecycle statuses in mind:
partialorders — some lines reached their supplier and some did not; the failed lines are the ones to act on.dispatch_failedlines — the channel failed after all retries. An alert email has already gone out, and the audit trail records every attempt.backorder_no_alternatelines — a back order with nowhere to go. This is your queue.
6. Manual reroute
Rerouting is done from the order detail page, not from a separate screen. As an admin, each line in the line-items table gets a Reroute button.
- Open the order from either order list and find the line.
- Click Reroute. The confirmation dialog names the item and warns that the original supplier is not cancelled automatically — check with them if the line may still ship.
- Confirm. The system appends
BACKORDER_REROUTEDagainst your user id and queues a reroute job; the worker creates the new dispatch and sends it.
The destination is the alternate supplier on the item's SKU mapping. The screen does not offer a
supplier picker — the API endpoint accepts an explicit alternate, but no UI exposes it yet, so if the
mapping has no alternate the call returns a 422 and the fix is to add one to the mapping first. A
manual reroute follows exactly the same dispatch path as an automatic one, including retries and the
idempotency check, so it is safe to trigger and safe to retry. What it does not do is fix the
configuration behind the problem.
Where to go next
- Roles and permissions — the route-to-guard tables.
- Order lifecycle — how statuses advance.
- Supplier integrations — what actually happens on the wire.