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 Forbidden from "../components/Forbidden"; 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"; interface FormData { username: string; email: string; password: string; first_name: string; last_name: 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(); const { data: usersData, isPending } = useQuery(userListOptions()); const users = usersData ?? []; const { data: roles = [] } = useQuery(roleListOptions()); const [showModal, setShowModal] = useState(false); const [editingUser, setEditingUser] = useState(null); const [deleteModal, setDeleteModal] = useState<{ isOpen: boolean; user: User | null; }>({ isOpen: false, user: null }); const [saving, setSaving] = useState(false); const [errors, setErrors] = useState>({}); const [formData, setFormData] = useState({ username: "", email: "", password: "", first_name: "", last_name: "", role_id: "", is_active: true, }); const USER_INVALIDATE = [ "users", "trips", "attendance", "leave-requests", "leave", "projects", ]; const saveUser = useApiMutation({ url: () => editingUser ? `${API_BASE}/users/${editingUser.id}` : `${API_BASE}/users`, method: () => (editingUser ? "PUT" : "POST"), invalidate: USER_INVALIDATE, onSuccess: async (_data, input) => { if ( editingUser && currentUser && Number(editingUser.id) === Number(currentUser.id) ) { updateUser({ username: input.username, email: input.email, fullName: `${input.first_name} ${input.last_name}`.trim(), }); } setShowModal(false); setEditingUser(null); await new Promise((resolve) => setTimeout(resolve, 300)); alert.success( editingUser ? "Uživatel byl upraven" : "Uživatel byl vytvořen", ); }, }); const deleteUser = useApiMutation({ url: (id) => `${API_BASE}/users/${id}`, method: () => "DELETE", invalidate: USER_INVALIDATE, onSuccess: () => { setDeleteModal({ isOpen: false, user: null }); alert.success("Uživatel byl smazán"); }, }); const toggleUser = useApiMutation<{ id: number; is_active: boolean }, void>({ url: (input) => `${API_BASE}/users/${input.id}`, method: () => "PUT", invalidate: USER_INVALIDATE, onSuccess: (_data, input) => { alert.success( input.is_active ? "Uživatel byl aktivován" : "Uživatel byl deaktivován", ); }, }); if (!hasPermission("users.view")) return ; const openCreateModal = () => { setEditingUser(null); setFormData({ username: "", email: "", password: "", first_name: "", last_name: "", role_id: roles[0]?.id ? String(roles[0].id) : "", is_active: true, }); setErrors({}); setShowModal(true); }; const openEditModal = (user: User) => { setEditingUser(user); setFormData({ username: user.username, email: user.email, password: "", first_name: user.first_name, last_name: user.last_name, role_id: String(user.role_id), is_active: user.is_active, }); setErrors({}); setShowModal(true); }; const closeModal = () => { setShowModal(false); setEditingUser(null); }; 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, role_id: Number(formData.role_id), }); } catch (e) { alert.error(e instanceof Error ? e.message : "Chyba připojení"); } finally { setSaving(false); } }; const handleDelete = async () => { if (!deleteModal.user) return; try { await deleteUser.mutateAsync(deleteModal.user.id); } catch (e) { alert.error(e instanceof Error ? e.message : "Chyba připojení"); } }; const toggleActive = (user: User) => { toggleUser.mutate({ id: user.id, is_active: !user.is_active, }); }; if (isPending) { return (
); } 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 ( Správa uživatelů 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 }); setErrors((prev) => ({ ...prev, first_name: "" })); }} /> { setFormData({ ...formData, last_name: e.target.value }); setErrors((prev) => ({ ...prev, last_name: "" })); }} /> { setFormData({ ...formData, username: e.target.value }); setErrors((prev) => ({ ...prev, username: "" })); }} /> { setFormData({ ...formData, email: e.target.value }); setErrors((prev) => ({ ...prev, email: "" })); }} /> { setFormData({ ...formData, password: e.target.value }); setErrors((prev) => ({ ...prev, password: "" })); }} />