Files
app/docs/superpowers/plans/2026-06-06-mui-migration-phase-4-users.md
BOHA 55a2de3a04 docs(plan): MUI migration Phase 4 (Users page)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 21:39:37 +02:00

6.5 KiB

MUI Migration — Phase 4: Users (Uživatelé) — Implementation Plan

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

Goal: Migrate src/admin/pages/Users.tsx onto the MUI kit, mirroring the already-migrated Vehicles.tsx, preserving ALL data logic. Presentation only changes.

Tech Stack: MUI v7 kit (src/admin/ui/): Button, Card, DataTable, Modal, ConfirmDialog, Field, TextField, SwitchField, Select (new), StatusChip (new). MUI Avatar/IconButton used directly (as Vehicles uses IconButton).

Reference: src/admin/pages/Vehicles.tsx is the canonical migrated page — copy its structure (Box + motion.div header, Button startIcon, Card + DataTable, Modal with Field/TextField, ConfirmDialog). The Users data layer already exists; do not change it.

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


Behavior to preserve VERBATIM (from the scout)

  • Three mutations + invalidation: USER_INVALIDATE = ["users","trips","attendance","leave-requests","leave","projects"]. saveUser (PUT if editingUser else POST), deleteUser (DELETE), toggleUser (PUT {id,is_active}). Keep all onSuccess messages.
  • Self-edit AuthContext sync (critical): in saveUser.onSuccess, when editingUser && currentUser && Number(editingUser.id)===Number(currentUser.id), call updateUser({ username, email, fullName: \${first_name} ${last_name}`.trim() }). Then setShowModal(false); setEditingUser(null);thenawait new Promise(r=>setTimeout(r,300))` (intentional delay — keep it) before the success toast.
  • Auth guard: const { user: currentUser, updateUser, hasPermission } = useAuth();if (!hasPermission("users.view")) return <Forbidden/>;
  • Self-protection: the delete action is hidden and the status toggle is disabled when user.id === currentUser?.id. These reference the live currentUser — keep them in the column render closures.
  • Password: required only on create (!editingUser); empty string on edit means "keep existing".
  • Role dropdown: options from roleListOptions() query; default to roles[0]?.id on create. role_id is number|string in form state.
  • FormData shape: { username, email, password, first_name, last_name, role_id, is_active }.

Task 1: Migrate Users.tsx

Files: Modify src/admin/pages/Users.tsx (read it first; it's the source of truth for the logic above).

  • Step 1: Read src/admin/pages/Users.tsx fully and src/admin/pages/Vehicles.tsx (the template). Keep ALL hooks, mutations, state, handlers, and the guard exactly; change only the rendered JSX.

  • Step 2: Imports — from ../ui: Button, Card, DataTable, Modal, ConfirmDialog, Field, TextField, SwitchField, Select, StatusChip, type DataColumn. From @mui/material: Box, Typography, Avatar, IconButton. Keep motion (framer-motion), useQuery, roleListOptions, useApiMutation, useAlert, useAuth, Forbidden. Remove old imports: FormModal, ConfirmModal, FormField.

  • Step 3: Page shell — mirror Vehicles: outer Box; motion.div header (Typography variant="h4" "Správa uživatelů" + Button startIcon={PlusIcon} "Přidat uživatele"); motion.div + Card wrapping the DataTable. Loading state keeps the admin-spinner markup.

  • Step 4: DataTable columns (DataColumn<User>[], defined inside the component so they close over currentUser/handlers):

    • Uživatel: composite render — MUI Avatar (initial of full_name/username, bgcolor:"primary.main") + name (Typography) + @username (Typography variant="caption" color="text.secondary").
    • Email: render: (u) => u.email.
    • Role: StatusChip colored by role — map "admin"color="warning", else "default"; label = roleDisplay/role name.
    • Stav: clickable StatusChip (color={u.is_active?"success":"default"}, onClicktoggleActive(u)), but non-clickable / no onClick when u.id===currentUser?.id (self-protection).
    • Akce: IconButton edit (always); IconButton delete (color="error") only when u.id !== currentUser?.id. Use the same edit/delete SVGs as Vehicles.
  • Step 5: Add/Edit ModalField + kit TextField for username/email/first_name/last_name; password TextField type="password" (label notes optional on edit); Select (inside a Field label="Role") for role_id with options from the roles query (value: String(r.id), label: r.display_name||r.name) — note Select is string-based, so store/compare role_id as string or String(...) at the boundary; SwitchField for is_active. Add a local errors state + validation in handleSubmit (required: username, email, first_name, last_name; password required when !editingUser) mirroring Vehicles, before saveUser.mutateAsync.

  • Step 6: Delete ConfirmDialog — drop-in replacement for ConfirmModal (title "Smazat uživatele", message with the user's name, confirmVariant="danger").

  • Step 7: Verify npx tsc -b --noEmit clean; npm run build OK; npx vitest run (148). Drop all admin-* class names.

  • Step 8: Commit (feat(mui): migrate Users (Uživatelé) page onto MUI kit; + Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> trailer).

  • Step 9: MANUAL (USER, dev server) — deferred: list renders (avatar/role/status); status toggle works + is disabled for self; delete hidden for self; add (password required) + edit (password optional, role Select) save; self-edit updates the sidebar name; validation; dark+light; no console errors.


Task 2: Review + finish

  • Spec-compliance review (esp. the self-edit AuthContext sync, self-protection, password-on-create-only, role Select string boundary, USER_INVALIDATE) then code-quality review.
  • finishing-a-development-branch after the user's visual check.

Self-review notes

  • The role Select is string-based (per the Phase 3 review fix); convert role_id at the boundary (String(r.id) for options/value; the server accepts the string or coerce on submit if the API needs a number — check how the existing code sends it and match).
  • Self-protection guards and the self-edit updateUser sync are the highest-risk items — the spec reviewer must confirm both survive the column-render refactor.
  • No users.css exists; Users uses shared classes — nothing to delete, it just stops using them.