diff --git a/src/admin/pages/IssuedOrders.tsx b/src/admin/pages/IssuedOrders.tsx new file mode 100644 index 0000000..5add6a9 --- /dev/null +++ b/src/admin/pages/IssuedOrders.tsx @@ -0,0 +1,371 @@ +import { useState } from "react"; +import { useNavigate, Link as RouterLink } from "react-router-dom"; +import Box from "@mui/material/Box"; +import IconButton from "@mui/material/IconButton"; +import { useAlert } from "../context/AlertContext"; +import { useAuth } from "../context/AuthContext"; +import Forbidden from "../components/Forbidden"; +import { formatCurrency, formatDate, czechPlural } from "../utils/formatters"; +import useTableSort from "../hooks/useTableSort"; +import useDebounce from "../hooks/useDebounce"; +import { usePaginatedQuery } from "../hooks/usePaginatedQuery"; +import { useApiMutation } from "../lib/queries/mutations"; +import { + issuedOrderListOptions, + type IssuedOrder, +} from "../lib/queries/issued-orders"; +import { + Button, + Card, + DataTable, + Pagination, + ConfirmDialog, + TextField, + Select, + StatusChip, + PageHeader, + PageEnter, + FilterBar, + EmptyState, + LoadingState, + type DataColumn, +} from "../ui"; + +const API_BASE = "/api/admin"; + +const STATUS_LABELS: Record = { + draft: "Koncept", + sent: "Odeslaná", + confirmed: "Potvrzená", + completed: "Dokončená", + cancelled: "Stornovaná", +}; + +const STATUS_COLORS: Record< + string, + "default" | "success" | "error" | "warning" | "info" +> = { + draft: "default", + sent: "info", + confirmed: "warning", + completed: "success", + cancelled: "error", +}; + +const STATUS_OPTIONS = [ + { value: "", label: "Všechny stavy" }, + ...Object.entries(STATUS_LABELS).map(([value, label]) => ({ value, label })), +]; + +const PlusIcon = ( + + + + +); +const ViewIcon = ( + + + + +); +const PdfIcon = ( + + + + + + +); +const DeleteIcon = ( + + + + +); + +export default function IssuedOrders() { + const alert = useAlert(); + const { hasPermission } = useAuth(); + const navigate = useNavigate(); + + const { sort, order, handleSort } = useTableSort("po_number"); + const [search, setSearch] = useState(""); + const debouncedSearch = useDebounce(search, 300); + const [status, setStatus] = useState(""); + const [page, setPage] = useState(1); + + const [deleteConfirm, setDeleteConfirm] = useState<{ + show: boolean; + order: IssuedOrder | null; + }>({ show: false, order: null }); + + const deleteMutation = useApiMutation< + { id: number }, + { message?: string; error?: string } + >({ + url: ({ id }) => `${API_BASE}/issued-orders/${id}`, + method: () => "DELETE", + invalidate: ["issued-orders"], + onSuccess: (data) => { + setDeleteConfirm({ show: false, order: null }); + alert.success(data?.message || "Objednávka byla smazána"); + }, + }); + + const { + items: orders, + pagination, + isPending, + isFetching, + } = usePaginatedQuery( + issuedOrderListOptions({ + search: debouncedSearch, + sort, + order, + page, + perPage: 20, + status: status || undefined, + }), + ); + + if (!hasPermission("orders.view")) return ; + + const handleDelete = async () => { + if (!deleteConfirm.order) return; + try { + await deleteMutation.mutateAsync({ id: deleteConfirm.order.id }); + } catch (e) { + alert.error(e instanceof Error ? e.message : "Chyba připojení"); + } + }; + + if (isPending) { + return ; + } + + const total = pagination?.total ?? orders.length; + const subtitle = `${total} ${czechPlural( + total, + "objednávka", + "objednávky", + "objednávek", + )}`; + + const columns: DataColumn[] = [ + { + key: "po_number", + header: "Číslo", + width: "16%", + sortKey: "po_number", + mono: true, + render: (o) => ( + + {o.po_number || "—"} + + ), + }, + { + key: "customer_name", + header: "Dodavatel", + width: "24%", + render: (o) => o.customer_name || "—", + }, + { + key: "status", + header: "Stav", + width: "14%", + sortKey: "status", + render: (o) => ( + + ), + }, + { + key: "order_date", + header: "Datum", + width: "13%", + sortKey: "order_date", + mono: true, + render: (o) => (o.order_date ? formatDate(o.order_date) : "—"), + }, + { + key: "total", + header: "Celkem", + width: "14%", + align: "right", + mono: true, + bold: true, + render: (o) => formatCurrency(o.total, o.currency ?? "CZK"), + }, + { + key: "actions", + header: "Akce", + width: "14%", + align: "right", + render: (o) => ( + + navigate(`/orders/issued/${o.id}`)} + aria-label={hasPermission("orders.edit") ? "Upravit" : "Detail"} + title={hasPermission("orders.edit") ? "Upravit" : "Detail"} + > + {ViewIcon} + + {hasPermission("orders.export") && ( + + window.open( + `${API_BASE}/issued-orders-pdf/${o.id}?lang=cs`, + "_blank", + ) + } + aria-label="PDF" + title="PDF" + > + {PdfIcon} + + )} + {hasPermission("orders.delete") && ( + setDeleteConfirm({ show: true, order: o })} + aria-label="Smazat" + title="Smazat" + > + {DeleteIcon} + + )} + + ), + }, + ]; + + return ( + + navigate("/orders/issued/new")} + > + Vytvořit objednávku vydanou + + ) : undefined + } + /> + + + + { + setSearch(e.target.value); + setPage(1); + }} + placeholder="Hledat podle čísla nebo dodavatele..." + fullWidth + /> + + + - setCreateForm({ ...createForm, customer_id: value }) - } - options={[ - { value: "", label: "— bez zákazníka —" }, - ...(customersQuery.data ?? []).map((c) => ({ - value: String(c.id), - label: c.name, - })), - ]} - /> - - - - - - setCreateForm({ - ...createForm, - customer_order_number: e.target.value, - }) - } - /> - - - - - - - - + setCreateForm({ ...createForm, customer_id: value }) + } + options={[ + { value: "", label: "— bez zákazníka —" }, + ...(customersQuery.data ?? []).map((c) => ({ + value: String(c.id), + label: c.name, + })), + ]} + /> + + + + + + setCreateForm({ + ...createForm, + customer_order_number: e.target.value, + }) + } + /> + + + + + + + +