Files
app/docs/superpowers/plans/2026-06-06-mui-migration-phase-5-projects.md
BOHA fbcd5a8939 docs(plan): MUI migration Phase 5 (Projects page)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 22:23:36 +02:00

8.1 KiB

MUI Migration — Phase 5: Projects (Projekty) — Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: superpowers:subagent-driven-development. Steps use checkbox (- [ ]) syntax.

Goal: Migrate src/admin/pages/Projects.tsx onto the MUI kit. It's a paginated, server-sorted list with a create modal (customer/user Selects, DateFields, multiline notes) and a delete confirm that has an extra "delete files from disk" checkbox. Mirror Vehicles.tsx/Users.tsx; preserve ALL data logic.

Prereq kit tweak (Task 1): ConfirmDialog needs to accept optional extra content below the message (for the delete-files checkbox), since message is a string.

Tech Stack: MUI v7 kit (src/admin/ui/): Button, Card, DataTable (now sortable), Pagination, Modal, ConfirmDialog, Field, TextField, Select, DateField, StatusChip, CheckboxField, type DataColumn. MUI Box/Typography/IconButton direct.

Reference: Vehicles.tsx + Users.tsx (migrated). The Projects data layer is authoritative — keep it.

Gates: npx tsc -b --noEmit, npm run build, npx vitest run (148). Visual check is the user's.


Behavior to preserve VERBATIM (from the scout)

  • List state: const { sort, order, handleSort, activeSort } = useTableSort("project_number"); search/page state; reset setPage(1) when search changes. usePaginatedQuery<Project>(projectListOptions({ search, sort, order, page })){ items, pagination, isPending, isFetching }.
  • isFetching dimming: the list Card gets sx={{ opacity: isFetching ? 0.6 : 1, transition: "opacity .2s" }}.
  • Create-only (edit navigates to the /projects/:id detail route — NOT a modal). createForm = { name, customer_id, responsible_user_id, status: "aktivni", start_date, end_date, notes }, createErrors, creating. Dependent queries enabled only while the modal is open: offerCustomersOptions(), userListOptions("projects.view") (filter to is_active), projectNextNumberOptions(). createMutation: POST /projects, invalidate: ["projects","orders","offers","invoices","attendance"], onSuccess close + "Projekt byl vytvořen". Validation: only name required ("Zadejte název projektu").
  • Delete: deleteTarget/deleteFiles state. deleteMutation: DELETE /projects/:id, invalidate: ["projects","warehouse","orders","offers","invoices","attendance"], called as mutateAsync({ id, delete_files: deleteFiles }), onSuccess toast data?.message || "Projekt byl smazán" + reset.
  • Permissions: projects.view<Forbidden/>; projects.create hides the add button; projects.delete hides the per-row delete; delete also hidden when p.order_id (order-linked projects can't be manually deleted).
  • Status constants: STATUS_LABELS = { aktivni:"Aktivní", dokonceny:"Dokončený", zruseny:"Zrušený" }. Map to StatusChip colors: aktivnisuccess, dokoncenyinfo, zrusenydefault.
  • Next number: nextNumberQuery.data?.number ?? nextNumberQuery.data?.next_number ?? "…".

Task 1: Extend ConfirmDialog with optional extra content

Files: Modify src/admin/ui/ConfirmDialog.tsx.

  • Step 1: Add children?: ReactNode to ConfirmDialogProps (import type { ReactNode }). Render it in DialogContent BELOW the message (live — not frozen, so the checkbox stays interactive):
<DialogContent>
  <DialogContentText>{shown.message}</DialogContentText>
  {children}
</DialogContent>
  • Step 2: Verify tsc -b clean; build:client OK; existing callers (Vozidla, Users) unaffected (no children → unchanged). Commit (feat(mui): ConfirmDialog optional children (extra content below message); + trailer).

Task 2: Migrate Projects.tsx

Files: Modify src/admin/pages/Projects.tsx (read it first — authoritative for the logic above).

  • Step 1: Read src/admin/pages/Projects.tsx fully + Users.tsx/Vehicles.tsx. Keep ALL hooks/queries/mutations/handlers/permission-guards/state. Change only presentation.

  • Step 2: Imports — from ../ui: Button, Card, DataTable, Pagination, Modal, ConfirmDialog, Field, TextField, Select, DateField, StatusChip, CheckboxField, type DataColumn. MUI Box, Typography, IconButton direct. Keep motion, useNavigate, the query options, useApiMutation, useAlert, useAuth, Forbidden. Remove FormModal/ConfirmModal/FormField/AdminDatePicker/legacy Pagination imports.

  • Step 3: Page shell — Box + motion.div header (Typography variant="h4" "Projekty" + Button startIcon={PlusIcon} "Nový projekt", hidden without projects.create). A search TextField (no Field wrapper) that sets search + setPage(1).

  • Step 4: ListCard (with the isFetching opacity sx) wrapping a sortable DataTable<Project>:

    • Wire sort: sortBy={sort} sortDir={order} onSort={handleSort}. Give sortable columns a sortKey (e.g. project_number, name — match the server's accepted sort keys from the original SortIcon usage).
    • Columns: project number (mono, sortKey), name (sortKey), customer, responsible user, status (StatusChip per the color map), linked order number (if order_id), start/end dates (mono, via the existing date formatter), actions — a detail link/IconButton that navigate(\/projects/${p.id}`)for edit, and a deleteIconButton color="error"shown only whenhasPermission("projects.delete") && !p.order_id`.
    • Below the table: <Pagination page={page} pageCount={pagination?.totalPages ?? 1} onChange={setPage} /> (use the actual page-count field from pagination — check the shape).
  • Step 5: Create Modal (isOpen={showCreate}, loading={creating}, title "Nový projekt", subtitle/hint with the next number): Field+TextField for name (required, error from createErrors); Select (in a Field) for customer (offerCustomersOptions) and responsible user (active users) — options { value: String(id), label }, convert at the boundary; Select for status (the STATUS_LABELS options); two DateFields for start/end (string in/out — matches createForm.start_date); TextField multiline minRows={3} for notes. Validation: only name required.

  • Step 6: Delete ConfirmDialog — title "Smazat projekt", message with the project name, confirmVariant="danger", and as children a CheckboxField "Smazat i soubory z disku" bound to deleteFiles. onConfirm calls the existing delete handler (mutateAsync({ id, delete_files: deleteFiles })).

  • Step 7: Verify tsc -b clean; npm run build OK; npx vitest run (148). Drop admin-* classes. (Projects has no own CSS file; it uses shared classes.)

  • Step 8: Commit (feat(mui): migrate Projects (Projekty) page onto MUI kit; + trailer).

  • Step 9: MANUAL (USER) — deferred: list renders + sorts (click headers) + paginates + dims while fetching; create modal (customer/user/status selects, date pickers, notes; name-required validation; next number shown) saves; delete confirm with the files checkbox works; order-linked projects show no delete; permission gating; dark+light.


Task 3: Review + finish

  • Spec-compliance review (server sort/pagination wiring, the create invalidate arrays, name-only validation, delete {id, delete_files} body + the checkbox, permission/order-link gating, status color map, the Select string boundaries) → code-quality review.
  • finishing-a-development-branch after the user's visual check.

Self-review notes

  • This is the first page to exercise the sortable DataTable + Pagination + DateField + Select together — confirm the sort keys match what the server accepts and the pagination page-count field name is correct (read the query options / a working paginated page).
  • Select is string-based; convert customer/user/status ids at the form boundary (String(...) for options/value; send what the server expects).
  • The delete-files checkbox rides in ConfirmDialog children (Task 1) — it's live (not frozen), so it toggles while open; it resets via the caller on close.
  • Edit is a route navigation (/projects/:id), NOT a modal — don't add an edit modal.