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 } 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 apiFetch from "../utils/api"; import { issuedOrderListOptions, type IssuedOrder, } from "../lib/queries/issued-orders"; import { Card, DataTable, Pagination, ConfirmDialog, TextField, Select, StatusChip, FilterBar, EmptyState, LoadingState, type DataColumn, } from "../ui"; import { ISSUED_ORDER_STATUS, statusLabel, statusColor, statusOptions, } from "../lib/documentStatus"; const API_BASE = "/api/admin"; const STATUS_OPTIONS = statusOptions(ISSUED_ORDER_STATUS, { value: "", label: "Všechny stavy", }); const ViewIcon = ( ); const PdfIcon = ( ); const DeleteIcon = ( ); interface IssuedOrdersProps { month: number; year: number; } export default function IssuedOrders({ month, year }: IssuedOrdersProps) { 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, month, year, }), ); 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í"); } }; const handleExportPdf = async (o: IssuedOrder) => { // Authenticated open: apiFetch attaches the access token, then we hand the // resulting PDF blob to a window. A raw window.open(url) would be an // unauthenticated top-level navigation and 401 on the token-guarded route. const newWindow = window.open("", "_blank"); try { const response = await apiFetch( `${API_BASE}/issued-orders-pdf/${o.id}?lang=cs`, ); if (!response.ok) { newWindow?.close(); alert.error("Nepodařilo se vygenerovat PDF"); return; } const blob = await response.blob(); const url = URL.createObjectURL(blob); if (newWindow) newWindow.location.href = url; setTimeout(() => URL.revokeObjectURL(url), 60000); } catch { newWindow?.close(); alert.error("Chyba při generování PDF"); } }; if (isPending) { return ; } 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") && ( handleExportPdf(o)} aria-label="PDF" title="PDF" > {PdfIcon} )} {hasPermission("orders.delete") && ( setDeleteConfirm({ show: true, order: o })} aria-label="Smazat" title="Smazat" > {DeleteIcon} )} ), }, ]; return ( <> { setSearch(e.target.value); setPage(1); }} placeholder="Hledat podle čísla nebo dodavatele..." fullWidth />