From efaf49e9df0ecceb9c70b977219c8275e055349f Mon Sep 17 00:00:00 2001 From: BOHA Date: Sat, 6 Jun 2026 20:43:04 +0200 Subject: [PATCH] docs(plan): MUI migration Phase 2 (data/form kit + Vozidla pilot) Co-Authored-By: Claude Opus 4.8 (1M context) --- ...026-06-06-mui-migration-phase-2-vozidla.md | 395 ++++++++++++++++++ 1 file changed, 395 insertions(+) create mode 100644 docs/superpowers/plans/2026-06-06-mui-migration-phase-2-vozidla.md diff --git a/docs/superpowers/plans/2026-06-06-mui-migration-phase-2-vozidla.md b/docs/superpowers/plans/2026-06-06-mui-migration-phase-2-vozidla.md new file mode 100644 index 0000000..96a2c41 --- /dev/null +++ b/docs/superpowers/plans/2026-06-06-mui-migration-phase-2-vozidla.md @@ -0,0 +1,395 @@ +# MUI Migration — Phase 2: Data/Form Kit + Vozidla Pilot — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development. Steps use checkbox (`- [ ]`) syntax. + +**Goal:** Build the data + form foundation components (`DataTable`, `Modal`, `ConfirmDialog`, field controls) and migrate the **Vozidla (Vehicles)** page fully onto them — the pilot that battle-tests the kit. Behavior stays identical; styling becomes MUI (Soft-SaaS shell + dense table). + +**Architecture:** New wrappers in `src/admin/ui/` expose stable, app-friendly APIs (Czech labels) over MUI `Dialog`/`Table`/form controls, so pages import from `ui/` not `@mui`. `Modal`/`ConfirmDialog` preserve the existing `FormModal`/`ConfirmModal` prop shapes so page churn is minimal. `DataTable` is a small dense table that takes a `columns` config + `rows` + optional row actions. Vozidla is then rewritten to use them; its data hooks (`useQuery(vehicleListOptions)`, `useApiMutation`) are unchanged. + +**Tech Stack:** React 18.3, MUI v7 (`@mui/material`), Emotion, framer-motion (page-entry animations kept), TanStack Query v5, TypeScript strict. + +**Spec:** `docs/superpowers/specs/2026-06-06-css-mui-migration-design.md` (§5 kit, §5.1 DataTable, §7 dense-table aesthetic, §8 Phase 2). + +**Authoritative gates:** `npx tsc -b --noEmit`, `npm run build`, `npx vitest run` (expect 148). Live visual check (dev server) is the USER's; implementers do build/typecheck/tests only and must NOT start the dev server. + +**Reference (current Vozidla behavior to preserve):** `src/admin/pages/Vehicles.tsx` — list table (SPZ, Název, Značka/Model, Počáteční km, Aktuální km, Počet jízd, Stav, Akce) with `formatKm`; inactive rows dimmed; status badge toggles active/inactive via `toggleVehicle`; row actions edit/delete; add/edit `FormModal` with SPZ (uppercased)/Název (required) + Značka/Model/Počáteční km/aktivní checkbox + validation (`Zadejte SPZ`/`Zadejte název`); delete `ConfirmModal`; permission gate `vehicles.manage` → ``; empty state. + +--- + +## File Structure + +| File | Responsibility | Action | +| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ------ | +| `src/admin/ui/Modal.tsx` | MUI `Dialog` form modal; preserves FormModal API (`isOpen`/`onClose`/`onSubmit`/`title`/`subtitle`/`loading`/`submitDisabled`/`submitText`/`children`) | Create | +| `src/admin/ui/ConfirmDialog.tsx` | MUI `Dialog` confirm; preserves ConfirmModal API (`isOpen`/`onClose`/`onConfirm`/`title`/`message`/`confirmText`/`confirmVariant`/`loading`) | Create | +| `src/admin/ui/DataTable.tsx` | Dense MUI `Table` from a `columns` config + `rows`; right-aligned/mono numeric cells; row hover; optional `rowInactive` + actions column | Create | +| `src/admin/ui/Field.tsx` | Labeled field wrapper (label + required + error helper) for composing inputs; `SwitchField` for booleans | Create | +| `src/admin/ui/index.ts` | Export the new components | Modify | +| `src/admin/pages/Vehicles.tsx` | Rewrite to use the kit | Modify | + +Import paths from `src/admin/ui/`: `../context/AlertContext`, `../theme`, etc. MUI via deep imports. + +--- + +## Task 1: `Modal` + `ConfirmDialog` (MUI Dialog wrappers) + +**Files:** Create `src/admin/ui/Modal.tsx`, `src/admin/ui/ConfirmDialog.tsx`; Modify `src/admin/ui/index.ts`. + +- [ ] **Step 1: `Modal.tsx`** — MUI `Dialog` preserving the existing `FormModal` API: + +```tsx +import type { ReactNode } from "react"; +import Dialog from "@mui/material/Dialog"; +import DialogTitle from "@mui/material/DialogTitle"; +import DialogContent from "@mui/material/DialogContent"; +import DialogActions from "@mui/material/DialogActions"; +import Typography from "@mui/material/Typography"; +import MuiButton from "@mui/material/Button"; + +export interface ModalProps { + isOpen: boolean; + onClose: () => void; + onSubmit: () => void; + title: string; + subtitle?: string; + children: ReactNode; + loading?: boolean; + submitDisabled?: boolean; + submitText?: string; + cancelText?: string; + maxWidth?: "xs" | "sm" | "md" | "lg"; +} + +export default function Modal({ + isOpen, + onClose, + onSubmit, + title, + subtitle, + children, + loading = false, + submitDisabled = false, + submitText = "Uložit", + cancelText = "Zrušit", + maxWidth = "sm", +}: ModalProps) { + return ( + + + {title} + {subtitle && ( + + {subtitle} + + )} + + {children} + + + {cancelText} + + + {loading ? "Ukládám…" : submitText} + + + + ); +} +``` + +- [ ] **Step 2: `ConfirmDialog.tsx`** — MUI `Dialog` preserving the `ConfirmModal` API: + +```tsx +import Dialog from "@mui/material/Dialog"; +import DialogTitle from "@mui/material/DialogTitle"; +import DialogContent from "@mui/material/DialogContent"; +import DialogContentText from "@mui/material/DialogContentText"; +import DialogActions from "@mui/material/DialogActions"; +import MuiButton from "@mui/material/Button"; + +export interface ConfirmDialogProps { + isOpen: boolean; + onClose: () => void; + onConfirm: () => void; + title: string; + message: string; + confirmText?: string; + cancelText?: string; + confirmVariant?: "primary" | "danger"; + loading?: boolean; +} + +export default function ConfirmDialog({ + isOpen, + onClose, + onConfirm, + title, + message, + confirmText = "Potvrdit", + cancelText = "Zrušit", + confirmVariant = "primary", + loading = false, +}: ConfirmDialogProps) { + return ( + + {title} + + {message} + + + + {cancelText} + + + {loading ? "Pracuji…" : confirmText} + + + + ); +} +``` + +- [ ] **Step 3:** add `export { default as Modal } from "./Modal";` and `export { default as ConfirmDialog } from "./ConfirmDialog";` to `src/admin/ui/index.ts`. + +- [ ] **Step 4: Verify** `npx tsc -b --noEmit` clean; `npm run build:client` succeeds. +- [ ] **Step 5: Commit** (msg `feat(mui): Modal + ConfirmDialog (MUI Dialog wrappers)`; + `Co-Authored-By: Claude Opus 4.8 (1M context) ` trailer). + +--- + +## Task 2: `DataTable` (dense MUI Table) + +**Files:** Create `src/admin/ui/DataTable.tsx`; Modify `src/admin/ui/index.ts`. + +- [ ] **Step 1: Create `DataTable.tsx`** — a generic dense table: + +```tsx +import type { ReactNode } from "react"; +import Table from "@mui/material/Table"; +import TableBody from "@mui/material/TableBody"; +import TableCell from "@mui/material/TableCell"; +import TableContainer from "@mui/material/TableContainer"; +import TableHead from "@mui/material/TableHead"; +import TableRow from "@mui/material/TableRow"; +import Box from "@mui/material/Box"; + +export interface DataColumn { + key: string; + header: string; + align?: "left" | "right"; + mono?: boolean; // DM Mono numeric cell + render: (row: T) => ReactNode; +} + +export interface DataTableProps { + columns: DataColumn[]; + rows: T[]; + rowKey: (row: T) => string | number; + rowInactive?: (row: T) => boolean; + empty?: ReactNode; +} + +export default function DataTable({ + columns, + rows, + rowKey, + rowInactive, + empty, +}: DataTableProps) { + if (rows.length === 0 && empty) return <>{empty}; + return ( + + + + + {columns.map((c) => ( + + {c.header} + + ))} + + + + {rows.map((row) => ( + + {columns.map((c) => ( + + {c.render(row)} + + ))} + + ))} + +
+
+ ); +} +``` + +(Numeric cells: pass `align="right" mono` and render via the existing `formatKm`/`formatCurrency` — the table only styles, never re-formats.) + +- [ ] **Step 2:** add `export { default as DataTable } from "./DataTable"; export type { DataColumn } from "./DataTable";` to `index.ts`. +- [ ] **Step 3: Verify** `npx tsc -b --noEmit` clean; `npm run build:client` succeeds. +- [ ] **Step 4: Commit** (msg `feat(mui): DataTable — dense MUI table with columns config`; + trailer). + +--- + +## Task 3: Field controls (`Field` + `SwitchField`) + +**Files:** Create `src/admin/ui/Field.tsx`; Modify `src/admin/ui/index.ts`. + +- [ ] **Step 1: Create `Field.tsx`** with a labeled wrapper and a boolean switch: + +```tsx +import type { ReactNode } from "react"; +import Box from "@mui/material/Box"; +import Typography from "@mui/material/Typography"; +import FormControlLabel from "@mui/material/FormControlLabel"; +import Switch from "@mui/material/Switch"; + +export function Field({ + label, + required, + error, + hint, + children, +}: { + label: string; + required?: boolean; + error?: string; + hint?: string; + children: ReactNode; +}) { + return ( + + + {label} + {required && ( + + * + + )} + + {children} + {error ? ( + + {error} + + ) : hint ? ( + + {hint} + + ) : null} + + ); +} + +export function SwitchField({ + label, + checked, + onChange, +}: { + label: string; + checked: boolean; + onChange: (v: boolean) => void; +}) { + return ( + onChange(e.target.checked)} + /> + } + label={label} + /> + ); +} +``` + +- [ ] **Step 2:** add `export { Field, SwitchField } from "./Field";` to `index.ts`. (`Button`, `Card`, `TextField` are already exported.) +- [ ] **Step 3: Verify** `npx tsc -b --noEmit` clean; `npm run build:client` succeeds. +- [ ] **Step 4: Commit** (msg `feat(mui): Field + SwitchField form controls`; + trailer). + +--- + +## Task 4: Migrate `Vehicles.tsx` onto the kit + +**Files:** Modify `src/admin/pages/Vehicles.tsx`. + +- [ ] **Step 1: Rewrite the page** keeping ALL existing data logic (the `useQuery(vehicleListOptions)`, `saveVehicle`/`deleteVehicle`/`toggleVehicle` `useApiMutation`s, validation, handlers, permission gate) UNCHANGED, and swapping only the presentation: + - Permission gate + loading + empty state: keep behavior; render loading with the existing `admin-spinner` (still in base.css), or a MUI `CircularProgress` — keep `admin-spinner` for now. + - Page header: an MUI `Box` (flex, title `Typography variant="h4"` "Správa vozidel" + the kit `Button` "Přidat vozidlo" with the plus SVG). Keep the framer-motion entry animation if desired (optional). + - List: kit `Card` wrapping a `DataTable` with columns: SPZ (`mono`), Název, Značka/Model (the existing `brand/model` join with `—` fallback), Počáteční km (`align right`, `mono`, `formatKm(...) + " km"`), Aktuální km (right/mono), Počet jízd (right/mono), Stav (a clickable MUI `Chip` colored `success`/`default` toggling `toggleActive`), Akce (edit + delete `IconButton`s with the existing SVGs). `rowInactive: (v) => !v.is_active`. `empty` = the existing empty-state block (can keep markup or render a simple MUI message + Button). + - Add/Edit modal: kit `Modal` (`isOpen={showModal}`, `onClose`, `onSubmit={handleSubmit}`, `title`, `loading={saving}`) containing `Field`-wrapped kit `TextField`s for SPZ (uppercase onChange)/Název/Značka/Model, a number `TextField` (type="number") for Počáteční km with the hint, and a `SwitchField` for `is_active`. Wire `errors`/`setErrors` exactly as today. + - Delete: kit `ConfirmDialog` (`isOpen={deleteConfirm.show}`, `onConfirm={handleDelete}`, `confirmVariant="danger"`, the existing message). + - Remove imports of the old `FormModal`/`ConfirmModal`/`FormField`; import from `../ui`. + +- [ ] **Step 2: Verify** — `npx tsc -b --noEmit` clean; `npm run build:client` succeeds; `npx vitest run` (148 pass — no test touches this page, but confirm nothing broke). +- [ ] **Step 3: Commit** (msg `feat(mui): migrate Vozidla (Vehicles) page onto MUI kit`; + trailer). +- [ ] **Step 4: MANUAL (USER, dev server) — deferred:** list renders (dense, mono numerics, inactive dimmed); status chip toggles active/inactive; add/edit modal validates (SPZ/Název required) + saves; SPZ uppercases; delete confirm works; empty state; permission gate; dark + light; no console errors. + +--- + +## Task 5: Phase-2 verification + final review + +- [ ] **Step 1:** `npx tsc -b --noEmit` clean; `npm run build` (server+client); `npx vitest run` (148). +- [ ] **Step 2:** Final whole-Phase-2 code review (kit components + Vozidla migration; behavior parity vs the original Vehicles.tsx). +- [ ] **Step 3:** finishing-a-development-branch (merge to master after the user's visual check). + +--- + +## Self-review notes (for the controller) + +- **Spec coverage:** §5 kit (Modal, ConfirmDialog, DataTable, field controls — Tasks 1–3), §5.1 dense DataTable (Task 2), §8 Phase 2 Vozidla pilot (Task 4). Pages still import only from `ui/`. +- **Parity risks:** Vozidla validation (`Zadejte SPZ`/`Zadejte název`) + SPZ uppercasing + the three mutations' invalidate keys (`["vehicles","trips"]`) must be preserved verbatim. The status-toggle was a button styled as a badge → now a clickable `Chip`; behavior (toggle active) identical. `Modal`'s `onClose` is disabled while `loading` (matches FormModal's guard). +- **No CSS file to delete:** Vozidla uses shared classes (forms/tables/buttons/components.css), not a `vehicles.css`; those shared files remain for not-yet-migrated pages. This page simply stops using them. +- **Deferred:** all live visual checks (no dev server in implementer scope).