import { useState } from "react"; import { useQuery } from "@tanstack/react-query"; import { Link as RouterLink, useNavigate } from "react-router-dom"; import { motion } from "framer-motion"; import Box from "@mui/material/Box"; import Typography from "@mui/material/Typography"; import IconButton from "@mui/material/IconButton"; import { useAlert } from "../context/AlertContext"; import { useAuth } from "../context/AuthContext"; import Forbidden from "../components/Forbidden"; import { formatDate } from "../utils/formatters"; import useTableSort from "../hooks/useTableSort"; import { usePaginatedQuery } from "../hooks/usePaginatedQuery"; import { projectListOptions, projectNextNumberOptions, } from "../lib/queries/projects"; import { offerCustomersOptions } from "../lib/queries/offers"; import { userListOptions } from "../lib/queries/users"; import { useApiMutation } from "../lib/queries/mutations"; import { Button, Card, DataTable, Pagination, Modal, ConfirmDialog, Field, TextField, Select, DateField, StatusChip, CheckboxField, type DataColumn, } from "../ui"; const API_BASE = "/api/admin"; const STATUS_LABELS: Record = { aktivni: "Aktivní", dokonceny: "Dokončený", zruseny: "Zrušený", }; const STATUS_COLORS: Record< string, "default" | "success" | "error" | "warning" | "info" > = { aktivni: "success", dokonceny: "info", zruseny: "default", }; interface Project { id: number; project_number: string; name: string; customer_name: string; responsible_user_name: string; status: string; start_date: string; end_date: string; order_id?: number; order_number?: string; } const PlusIcon = ( ); const EditIcon = ( ); const DeleteIcon = ( ); export default function Projects() { const alert = useAlert(); const navigate = useNavigate(); const { hasPermission } = useAuth(); const { sort, order, handleSort } = useTableSort("project_number"); const [search, setSearch] = useState(""); const [page, setPage] = useState(1); const [deleteTarget, setDeleteTarget] = useState(null); const [deleteFiles, setDeleteFiles] = useState(false); const deleteMutation = useApiMutation< { id: number; delete_files: boolean }, { message?: string; error?: string } >({ url: ({ id }) => `${API_BASE}/projects/${id}`, method: () => "DELETE", invalidate: [ "projects", "warehouse", "orders", "offers", "invoices", "attendance", ], onSuccess: (data) => { alert.success(data?.message || "Projekt byl smazán"); setDeleteTarget(null); setDeleteFiles(false); }, }); const [showCreate, setShowCreate] = useState(false); const [createForm, setCreateForm] = useState({ name: "", customer_id: "", responsible_user_id: "", status: "aktivni", start_date: "", end_date: "", notes: "", }); const [createErrors, setCreateErrors] = useState>({}); const [creating, setCreating] = useState(false); const customersQuery = useQuery({ ...offerCustomersOptions(), enabled: showCreate, }); // Responsible-person picker: only users whose role can view projects // (projects.view, filtered server-side) and who are active. const usersQuery = useQuery({ ...userListOptions("projects.view"), enabled: showCreate, }); const nextNumberQuery = useQuery({ ...projectNextNumberOptions(), enabled: showCreate, }); const createMutation = useApiMutation({ url: () => `${API_BASE}/projects`, method: () => "POST", invalidate: ["projects", "orders", "offers", "invoices", "attendance"], onSuccess: () => { setShowCreate(false); alert.success("Projekt byl vytvořen"); }, }); const openCreate = () => { setCreateForm({ name: "", customer_id: "", responsible_user_id: "", status: "aktivni", start_date: "", end_date: "", notes: "", }); setCreateErrors({}); setShowCreate(true); }; const handleCreate = async () => { const errs: Record = {}; if (!createForm.name.trim()) errs.name = "Zadejte název projektu"; setCreateErrors(errs); if (Object.keys(errs).length > 0) return; setCreating(true); try { await createMutation.mutateAsync(createForm); } catch (e) { alert.error(e instanceof Error ? e.message : "Chyba připojení"); } finally { setCreating(false); } }; const { items: projects, pagination, isPending, isFetching, } = usePaginatedQuery( projectListOptions({ search, sort, order, page }), ); if (!hasPermission("projects.view")) return ; const handleDelete = async () => { if (!deleteTarget) return; try { await deleteMutation.mutateAsync({ id: deleteTarget.id, delete_files: deleteFiles, }); } catch (e) { alert.error(e instanceof Error ? e.message : "Chyba připojení"); setDeleteTarget(null); setDeleteFiles(false); } }; if (isPending) { return (
); } const columns: DataColumn[] = [ { key: "project_number", header: "Číslo", sortKey: "project_number", mono: true, render: (p) => p.project_number, }, { key: "name", header: "Název", sortKey: "name", bold: true, render: (p) => p.name || "—", }, { key: "customer", header: "Zákazník", render: (p) => p.customer_name || "—", }, { key: "responsible", header: "Zodpovědná osoba", render: (p) => p.responsible_user_name || "—", }, { key: "status", header: "Stav", sortKey: "status", render: (p) => ( ), }, { key: "order", header: "Objednávka", render: (p) => p.order_id ? ( {p.order_number} ) : ( "—" ), }, { key: "start_date", header: "Začátek", mono: true, render: (p) => formatDate(p.start_date), }, { key: "end_date", header: "Konec", mono: true, render: (p) => formatDate(p.end_date), }, { key: "actions", header: "Akce", align: "right", render: (p) => ( navigate(`/projects/${p.id}`)} aria-label="Upravit" title="Upravit" > {EditIcon} {hasPermission("projects.delete") && !p.order_id && ( setDeleteTarget(p)} aria-label="Smazat" title="Smazat projekt" > {DeleteIcon} )} ), }, ]; return ( Projekty {hasPermission("projects.create") && ( )} { setSearch(e.target.value); setPage(1); }} placeholder="Hledat podle čísla, názvu nebo zákazníka..." fullWidth /> columns={columns} rows={projects} rowKey={(p) => p.id} sortBy={sort} sortDir={order} onSort={handleSort} empty={ Zatím nejsou žádné projekty. Projekty vznikají z objednávek nebo je můžete vytvořit ručně tlačítkem „Přidat projekt". } /> { setDeleteTarget(null); setDeleteFiles(false); }} onConfirm={handleDelete} title="Smazat projekt" message={ deleteTarget ? `Opravdu chcete smazat projekt ${deleteTarget.project_number}?` : "" } confirmText="Smazat" confirmVariant="danger" loading={deleteMutation.isPending} > setShowCreate(false)} onSubmit={handleCreate} title="Přidat projekt" subtitle={`Číslo projektu: ${ nextNumberQuery.data?.number ?? nextNumberQuery.data?.next_number ?? "…" }`} submitText="Vytvořit" loading={creating} > { setCreateForm({ ...createForm, name: e.target.value }); setCreateErrors((p) => ({ ...p, name: "" })); }} /> setCreateForm({ ...createForm, responsible_user_id: value }) } options={[ { value: "", label: "— nepřiřazeno —" }, ...(usersQuery.data ?? []) .filter((u) => u.is_active) .map((u) => ({ value: String(u.id), label: `${u.first_name} ${u.last_name}`, })), ]} /> setCreateForm({ ...createForm, start_date: val }) } /> setCreateForm({ ...createForm, end_date: val }) } /> setCreateForm({ ...createForm, notes: e.target.value }) } /> ); }