diff --git a/src/admin/pages/Users.tsx b/src/admin/pages/Users.tsx index 4dad719..bfa819b 100644 --- a/src/admin/pages/Users.tsx +++ b/src/admin/pages/Users.tsx @@ -1,18 +1,32 @@ import { useState } from "react"; import { useQuery } from "@tanstack/react-query"; import { motion } from "framer-motion"; +import Avatar from "@mui/material/Avatar"; +import Box from "@mui/material/Box"; +import Typography from "@mui/material/Typography"; +import IconButton from "@mui/material/IconButton"; import { useAuth } from "../context/AuthContext"; import { useAlert } from "../context/AlertContext"; -import ConfirmModal from "../components/ConfirmModal"; -import FormField from "../components/FormField"; import Forbidden from "../components/Forbidden"; -import FormModal from "../components/FormModal"; import { useApiMutation } from "../lib/queries/mutations"; import { userListOptions, roleListOptions, type User, } from "../lib/queries/users"; +import { + Button, + Card, + DataTable, + Modal, + ConfirmDialog, + Field, + TextField, + SwitchField, + Select, + StatusChip, + type DataColumn, +} from "../ui"; const API_BASE = "/api/admin"; @@ -22,10 +36,59 @@ interface FormData { password: string; first_name: string; last_name: string; - role_id: number | string; + role_id: string; is_active: boolean; } +// Payload sent to the API. The kit Select is string-based, so the form keeps +// role_id as a string and we convert it to a number at the submit boundary +// (the server expects a numeric role_id). +type UserPayload = Omit & { role_id: number }; + +const PlusIcon = ( + + + + +); +const EditIcon = ( + + + + +); +const DeleteIcon = ( + + + + +); + export default function Users() { const { user: currentUser, updateUser, hasPermission } = useAuth(); const alert = useAlert(); @@ -39,6 +102,7 @@ export default function Users() { user: User | null; }>({ isOpen: false, user: null }); const [saving, setSaving] = useState(false); + const [errors, setErrors] = useState>({}); const [formData, setFormData] = useState({ username: "", email: "", @@ -58,8 +122,8 @@ export default function Users() { "projects", ]; - const saveUser = useApiMutation({ - url: (input) => + const saveUser = useApiMutation({ + url: () => editingUser ? `${API_BASE}/users/${editingUser.id}` : `${API_BASE}/users`, method: () => (editingUser ? "PUT" : "POST"), invalidate: USER_INVALIDATE, @@ -115,9 +179,10 @@ export default function Users() { password: "", first_name: "", last_name: "", - role_id: roles[0]?.id || "", + role_id: roles[0]?.id ? String(roles[0].id) : "", is_active: true, }); + setErrors({}); setShowModal(true); }; @@ -129,9 +194,10 @@ export default function Users() { password: "", first_name: user.first_name, last_name: user.last_name, - role_id: user.role_id, + role_id: String(user.role_id), is_active: user.is_active, }); + setErrors({}); setShowModal(true); }; @@ -141,9 +207,22 @@ export default function Users() { }; const handleSubmit = async () => { + const newErrors: Record = {}; + if (!formData.first_name) newErrors.first_name = "Zadejte jméno"; + if (!formData.last_name) newErrors.last_name = "Zadejte příjmení"; + if (!formData.username) newErrors.username = "Zadejte uživatelské jméno"; + if (!formData.email) newErrors.email = "Zadejte e-mail"; + if (!editingUser && !formData.password) + newErrors.password = "Zadejte heslo"; + setErrors(newErrors); + if (Object.keys(newErrors).length > 0) return; + setSaving(true); try { - await saveUser.mutateAsync(formData); + await saveUser.mutateAsync({ + ...formData, + role_id: Number(formData.role_id), + }); } catch (e) { alert.error(e instanceof Error ? e.message : "Chyba připojení"); } finally { @@ -151,14 +230,6 @@ export default function Users() { } }; - const openDeleteModal = (user: User) => { - setDeleteModal({ isOpen: true, user }); - }; - - const closeDeleteModal = () => { - setDeleteModal({ isOpen: false, user: null }); - }; - const handleDelete = async () => { if (!deleteModal.user) return; try { @@ -175,15 +246,6 @@ export default function Users() { }); }; - const getRoleBadgeClass = (roleName: string): string => { - switch (roleName) { - case "admin": - return "admin-badge admin-badge-admin"; - default: - return "admin-badge admin-badge-viewer"; - } - }; - if (isPending) { return (
@@ -192,278 +254,231 @@ export default function Users() { ); } + const columns: DataColumn[] = [ + { + key: "user", + header: "Uživatel", + render: (u) => ( + + + {(u.first_name || u.username).charAt(0).toUpperCase()} + + + + {u.first_name} {u.last_name} + + + @{u.username} + + + + ), + }, + { key: "email", header: "E-mail", render: (u) => u.email }, + { + key: "role", + header: "Role", + render: (u) => ( + + ), + }, + { + key: "status", + header: "Stav", + render: (u) => ( + toggleActive(u)} + /> + ), + }, + { + key: "actions", + header: "Akce", + align: "right", + render: (u) => ( + + openEditModal(u)} + aria-label="Upravit" + title="Upravit" + > + {EditIcon} + + {u.id !== currentUser?.id && ( + setDeleteModal({ isOpen: true, user: u })} + aria-label="Smazat" + title="Smazat" + > + {DeleteIcon} + + )} + + ), + }, + ]; + return ( -
+ -
-

Uživatelé

-

- Správa uživatelských účtů a oprávnění -

-
- + Správa uživatelů + +
-
-
- - - - - - - - - - - - {users.map((user) => ( - - - - - - - - ))} - -
UživatelE-mailRoleStavAkce
-
-
- {(user.first_name || user.username) - .charAt(0) - .toUpperCase()} -
-
-
- {user.first_name} {user.last_name} -
-
- @{user.username} -
-
-
-
{user.email} - - {user.roles?.display_name || user.roles?.name || "—"} - - - - -
- - {user.id !== currentUser?.id && ( - - )} -
-
-
-
+ + + columns={columns} + rows={users} + rowKey={(u) => u.id} + rowInactive={(u) => !u.is_active} + empty={ + + + Zatím nejsou žádní uživatelé. + + + + } + /> +
- -
-
- - + + + - setFormData({ - ...formData, - first_name: e.target.value, - }) - } - required - className="admin-form-input" + error={!!errors.first_name} + onChange={(e) => { + setFormData({ ...formData, first_name: e.target.value }); + setErrors((prev) => ({ ...prev, first_name: "" })); + }} /> - - - + + + + - setFormData({ - ...formData, - last_name: e.target.value, - }) - } - required - className="admin-form-input" + error={!!errors.last_name} + onChange={(e) => { + setFormData({ ...formData, last_name: e.target.value }); + setErrors((prev) => ({ ...prev, last_name: "" })); + }} /> - -
+ + + - - - setFormData({ ...formData, username: e.target.value }) - } - required - className="admin-form-input" - /> - + + { + setFormData({ ...formData, username: e.target.value }); + setErrors((prev) => ({ ...prev, username: "" })); + }} + /> + - - - setFormData({ ...formData, email: e.target.value }) - } - required - className="admin-form-input" - /> - + + { + setFormData({ ...formData, email: e.target.value }); + setErrors((prev) => ({ ...prev, email: "" })); + }} + /> + - - - setFormData({ ...formData, password: e.target.value }) - } - required={!editingUser} - className="admin-form-input" - /> - + + { + setFormData({ ...formData, password: e.target.value }); + setErrors((prev) => ({ ...prev, password: "" })); + }} + /> + - - - + + - setFormData({ - ...formData, - is_active: e.target.checked, - }) - } - /> - Účet je aktivní - -
-
+ setFormData({ ...formData, is_active: v })} + /> + - setDeleteModal({ isOpen: false, user: null })} onConfirm={handleDelete} title="Smazat uživatele" - message={`Opravdu chcete smazat uživatele "${deleteModal.user?.first_name} ${deleteModal.user?.last_name}"? Tato akce je nevratná.`} + message={ + deleteModal.user + ? `Opravdu chcete smazat uživatele "${deleteModal.user.first_name} ${deleteModal.user.last_name}"? Tato akce je nevratná.` + : "" + } confirmText="Smazat" - cancelText="Zrušit" - type="danger" - loading={deleteUser.isPending} + confirmVariant="danger" /> -
+ ); }