5.6 KiB
Status quick actions + project⇄order sync
Date: 2026-07-04 · Status: Approved (interactive design w/ owner) · Scope: offers, received orders, projects
Problem
Status changes are inconsistent: the Invoices list has a quick chip action (click → confirm → paid),
but Offers/ReceivedOrders/Projects tables have inert chips — every change needs the detail page.
ProjectDetail edits status via a free combobox in the form (no transitions, no confirm), unlike
OfferDetail/OrderDetail's transition buttons. Projects have no status machine at all (free-text
column). Order→project completion sync exists one-way (syncProjectStatus), but finishing a
project leaves its linked order untouched.
Decisions (owner)
- Chip opens a menu of valid next states (not single-click-advance) → ConfirmDialog per pick, danger styling + cascade notes on destructive/irreversible ones.
- Reopen allowed:
dokonceny/zruseny → aktivni(projects),dokoncena/stornovana → v_realizaci(received orders). Reopening never cascades to the linked record. - Sync scope: finish + cancel, both directions. Project
dokonceny⇄ orderdokoncena; projectzruseny⇄ orderstornovana. - Offers quick menu:
Aktivovat(draft — same number-assignment + PDF-archive semantics as the detail),Zneplatnit(danger), and "Vytvořit objednávku…" which opens the existing create-order modal (never a raw label flip toordered— that state is owned by the create-order flow).
Design
Backend
Projects gain a status machine (src/services/projects.service.ts):
VALID_TRANSITIONS = { aktivni: [dokonceny, zruseny], dokonceny: [aktivni], zruseny: [aktivni] }.
updateProject validates status changes (token invalid_transition → Czech 400 in the route);
a legacy/unknown current status may transition to any canonical value (tolerant). getProject
returns valid_transitions (same contract as offers/orders).
Project → order cascade (new), inside one transaction with the project update:
- project →
dokonceny⇒ linked order →dokoncenaiff order status ∈ {prijata, v_realizaci} - project →
zruseny⇒ linked order →stornovanaiff order status ∈ {prijata, v_realizaci} - reopen ⇒ no order change; completed/cancelled orders are never resurrected by a cascade
- direct
txwrite (no service recursion); sibling projects of the same order are NOT touched (multi-project orders are a rare schema possibility, not a flow; avoid surprise mass updates) - the service returns what cascaded so the route writes an audit row for the order change too
Received orders (src/services/orders.service.ts):
VALID_TRANSITIONSgains reopen:dokoncena: [v_realizaci],stornovana: [v_realizaci]syncProjectStatusbecomes reopen-aware: given the previous status, a transition OUT of a terminal state (reopen) skips project sync entirely (decision 2). Forward transitions keep the existing mapping (v_realizaci→aktivni no-op in practice, dokoncena→dokonceny, stornovana→zruseny), and the cascaded project change now gets an audit row.- item/section edit guards stay status-based, so a reopened order is editable again (intended).
No DB migration (status columns are strings already).
Frontend
Shared StatusChipMenu (src/admin/components/StatusChipMenu.tsx): renders a StatusChip;
click opens a small MUI Menu of actions; each action either opens a ConfirmDialog
(danger variant + loading during the request; cascade note in the message) or runs a plain
onClick (the modal-opening offer item). Hidden affordance fixed via title tooltip. Chip is
plain (non-clickable) when the user lacks the edit permission or no actions exist.
Offers list: draft → Aktivovat (confirm notes the number assignment; after success the same
fire-and-forget PDF-archive call the detail does); active → Vytvořit objednávku… (opens the
existing modal) + Zneplatnit (danger); ordered → Zneplatnit (danger).
ReceivedOrders list: prijata → Zahájit realizaci, Stornovat (danger, note: linked project
will be cancelled); v_realizaci → Dokončit (note: linked project will be completed),
Stornovat (danger); dokoncena/stornovana → Obnovit (reopen, no cascade note).
Projects list: aktivni → Dokončit (note when order_id present: linked order will be
completed), Zrušit (danger, order-storno note); dokonceny/zruseny → Obnovit.
ProjectDetail: status Select removed from the form (and status removed from the save
payload); header gains transition buttons rendered from valid_transitions
(Dokončit projekt / Zrušit projekt danger / Obnovit projekt), each via ConfirmDialog with
cascade notes, as a status-only PUT with its own mutation
(invalidate: projects, orders, offers, invoices, warehouse). OrderDetail keeps its pattern;
its transition label shows Obnovit when reopening from a terminal state.
Invalidation for all new status mutations: ["orders","offers","projects","invoices"]
(+["warehouse"] on project mutations, matching the page's existing set).
Tests
Service-level (real app_test DB): project transition validation (all legal/illegal edges incl.
legacy-status tolerance), project→order cascade both mappings + the guard (no cascade onto
dokoncena/stornovana orders), reopen-no-cascade both directions, order reopen transitions, and
regression: order→project sync still fires on forward transitions.
Out of scope
Offer machine changes (unchanged: draft→active→ordered/invalidated), issued orders, sibling-project propagation, ReceivedOrders detail-page button changes beyond the automatic reopen button.