Skip to main content

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:

FieldRequiredNotes
NameYesWhat the chef sees, up to 200 characters; must be unique
CategoryNoUp to 100 characters; drives catalog grouping and filtering
Ordering unitYescase, lb, each — the unit quantities are expressed in
Pack sizeNoe.g. 6x10oz; display only
DescriptionNoUp to 2,000 characters
ActiveYesDefaults 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.

TypeChannelConfig fields
syscoX12 850 EDI file over SFTPOutbound / inbound / archive directories, ISA05–ISA08 qualifiers and ids, EDI version, product-id qualifier, and Vault secret names
costcoStructured HTML order emailEmail address, optional CC list
central_kitchenPDF purchase order attached to an emailEmail 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.

Credentials are never stored here

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.

FieldRequiredPurpose
Catalog itemYesThe item being mapped; unique across mappings
Primary supplierYesWho normally fulfils this item
Supplier SKUYesThe supplier's own product code — this is what goes on the wire
Product codeNoAn additional supplier reference
Alternate supplierNoWho catches this item when the primary back-orders it
Mapping is activeYesAn 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.

caution

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 itemPrimarySupplier SKUAlternate
Roma tomatoes, case 6x10ozSysco4471102Central Kitchen
Whole milk, case 4x1galCostco Business Center18864Sysco
House stock base, 5 lb tubCentral KitchenCK-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:

  1. create the user in Zitadel and grant them the chef role;
  2. have them sign in to the Order Center once;
  3. 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:

  • partial orders — some lines reached their supplier and some did not; the failed lines are the ones to act on.
  • dispatch_failed lines — the channel failed after all retries. An alert email has already gone out, and the audit trail records every attempt.
  • backorder_no_alternate lines — 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.

  1. Open the order from either order list and find the line.
  2. 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.
  3. Confirm. The system appends BACKORDER_REROUTED against 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