# 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);`then`await 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 ;` - **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[]`, 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"}`, `onClick` → `toggleActive(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 `Modal`** — `Field` + 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) ` 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.