# 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) 1. **Chip opens a menu** of valid next states (not single-click-advance) → ConfirmDialog per pick, danger styling + cascade notes on destructive/irreversible ones. 2. **Reopen allowed**: `dokonceny/zruseny → aktivni` (projects), `dokoncena/stornovana → v_realizaci` (received orders). **Reopening never cascades** to the linked record. 3. **Sync scope: finish + cancel, both directions.** Project `dokonceny` ⇄ order `dokoncena`; project `zruseny` ⇄ order `stornovana`. 4. 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 to `ordered` — 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 → `dokoncena` **iff** order status ∈ {prijata, v_realizaci} - project → `zruseny` ⇒ linked order → `stornovana` **iff** order status ∈ {prijata, v_realizaci} - reopen ⇒ no order change; completed/cancelled orders are never resurrected by a cascade - direct `tx` write (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_TRANSITIONS` gains reopen: `dokoncena: [v_realizaci]`, `stornovana: [v_realizaci]` - `syncProjectStatus` becomes 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.