Files
app/docs/superpowers/specs/2026-07-04-status-quick-actions-sync-design.md
2026-07-04 03:49:46 +02:00

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)

  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.