import { useState } from "react"; import { useParams, useNavigate, Link as RouterLink } from "react-router-dom"; import { useQuery } from "@tanstack/react-query"; import Box from "@mui/material/Box"; import Typography from "@mui/material/Typography"; import IconButton from "@mui/material/IconButton"; import Link from "@mui/material/Link"; import { useAlert } from "../context/AlertContext"; import { useAuth } from "../context/AuthContext"; import Forbidden from "../components/Forbidden"; import { apiFetch } from "../utils/api"; import { formatCurrency, formatDate } from "../utils/formatters"; import { warehouseReceiptDetailOptions, type WarehouseReceipt, type WarehouseReceiptItem, type WarehouseReceiptAttachment, } from "../lib/queries/warehouse"; import { useApiMutation } from "../lib/queries/mutations"; import { Button, Card, DataTable, StatusChip, EmptyState, LoadingState, PageHeader, ConfirmDialog, PageEnter, headerActionsSx, type DataColumn, } from "../ui"; const STATUS_COLOR: Record< string, "default" | "success" | "error" | "warning" | "info" > = { DRAFT: "warning", CONFIRMED: "success", CANCELLED: "error", }; const STATUS_LABEL: Record = { DRAFT: "Návrh", CONFIRMED: "Potvrzeno", CANCELLED: "Zrušeno", }; function formatFileSize(bytes: number): string { if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} kB`; return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; } const BackIcon = ( ); const TrashIcon = ( ); export default function WarehouseReceiptDetail() { const { id } = useParams(); const alert = useAlert(); const { hasPermission } = useAuth(); const navigate = useNavigate(); const [cancelConfirm, setCancelConfirm] = useState(false); const { data: receipt, isPending, error, } = useQuery(warehouseReceiptDetailOptions(id)); const confirmMutation = useApiMutation({ url: (receiptId) => `/api/admin/warehouse/receipts/${receiptId}/confirm`, method: () => "POST", invalidate: ["warehouse"], onSuccess: () => { alert.success("Doklad byl potvrzen"); }, }); const cancelMutation = useApiMutation({ url: (receiptId) => `/api/admin/warehouse/receipts/${receiptId}/cancel`, method: () => "POST", invalidate: ["warehouse"], onSuccess: () => { setCancelConfirm(false); alert.success("Doklad byl zrušen"); }, }); const deleteAttachmentMutation = useApiMutation< { receiptId: number; attachmentId: number }, void >({ url: ({ receiptId, attachmentId }) => `/api/admin/warehouse/receipts/${receiptId}/attachments/${attachmentId}`, method: () => "DELETE", invalidate: ["warehouse"], onSuccess: () => { alert.success("Příloha byla smazána"); }, }); // All hooks above this line — permission gate is the last thing before render. if (!hasPermission("warehouse.view")) return ; const canOperate = hasPermission("warehouse.operate"); const handleConfirm = async () => { if (!receipt) return; try { await confirmMutation.mutateAsync(receipt.id); } catch (e) { alert.error(e instanceof Error ? e.message : "Chyba připojení"); } }; const handleCancel = async () => { if (!receipt) return; try { await cancelMutation.mutateAsync(receipt.id); } catch (e) { alert.error(e instanceof Error ? e.message : "Chyba připojení"); } }; const handleDeleteAttachment = async (attachmentId: number) => { if (!receipt) return; try { await deleteAttachmentMutation.mutateAsync({ receiptId: receipt.id, attachmentId, }); } catch (e) { alert.error(e instanceof Error ? e.message : "Chyba připojení"); } }; // A plain cannot send the Authorization header, so attachments are // fetched via apiFetch and handed to the browser as a temporary blob URL. const handleDownloadAttachment = async (att: WarehouseReceiptAttachment) => { if (!receipt) return; try { const response = await apiFetch( `/api/admin/warehouse/receipts/${receipt.id}/attachments/${att.id}`, ); if (!response.ok) { const result = await response.json().catch(() => ({})); alert.error(result.error || "Nepodařilo se stáhnout přílohu"); return; } const blob = await response.blob(); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = att.file_name; document.body.appendChild(a); a.click(); document.body.removeChild(a); setTimeout(() => URL.revokeObjectURL(url), 60000); } catch (e) { alert.error(e instanceof Error ? e.message : "Chyba připojení"); } }; const confirming = confirmMutation.isPending; const cancelling = cancelMutation.isPending; if (isPending) { return ; } if (error || !receipt) { return ( Zpět } /> ); } const r = receipt as WarehouseReceipt; const items = r.items ?? []; const attachments = r.attachments ?? []; const itemColumns: DataColumn[] = [ { key: "item", header: "Položka", width: "25%", bold: true, render: (row) => row.item?.name || `ID: ${row.item_id}`, }, { key: "quantity", header: "Množství", width: "12%", mono: true, render: (row) => String(Number(row.quantity)), }, { key: "unit_price", header: "Cena/ks", width: "16%", mono: true, render: (row) => formatCurrency(Number(row.unit_price), "CZK"), }, { key: "total", header: "Celkem", width: "16%", mono: true, bold: true, render: (row) => formatCurrency(Number(row.quantity) * Number(row.unit_price), "CZK"), }, { key: "location", header: "Lokace", width: "16%", mono: true, render: (row) => row.location?.code || "—", }, { key: "notes", header: "Poznámka", width: "15%", render: (row) => row.notes || "—", }, ]; const attachmentColumns: DataColumn[] = [ { key: "file_name", header: "Název souboru", width: "45%", bold: true, render: (att) => ( handleDownloadAttachment(att)} sx={{ textAlign: "left", font: "inherit" }} > {att.file_name} ), }, { key: "file_size", header: "Velikost", width: "15%", mono: true, render: (att) => formatFileSize(att.file_size), }, { key: "created_at", header: "Datum", width: "20%", mono: true, render: (att) => formatDate(att.created_at), }, { key: "actions", header: "Akce", width: "20%", render: (att) => r.status === "DRAFT" && canOperate ? ( handleDeleteAttachment(att.id)} title="Smazat přílohu" aria-label="Smazat přílohu" color="error" > {TrashIcon} ) : null, }, ]; return ( {/* Header */} {canOperate && r.status === "DRAFT" && ( <> )} {canOperate && r.status === "CONFIRMED" && ( )} } /> {/* Basic info */} Základní údaje Číslo dokladu {r.receipt_number || "—"} Dodavatel {r.supplier?.name || "—"} Číslo dodacího listu {r.delivery_note_number || "—"} Datum dodacího listu {formatDate(r.delivery_note_date)} Přijal {r.received_by_user ? `${r.received_by_user.first_name} ${r.received_by_user.last_name}` : "—"} Vytvořeno {formatDate(r.created_at)} {r.notes && ( Poznámky {r.notes} )} {/* Lines table */} Položky columns={itemColumns} rows={items} rowKey={(row) => row.id} empty={} /> {/* Attachments */} Přílohy columns={attachmentColumns} rows={attachments} rowKey={(att) => att.id} empty={} /> {/* Cancel confirmation dialog */} setCancelConfirm(false)} onConfirm={handleCancel} title="Zrušit doklad" message={`Opravdu chcete zrušit příjmový doklad „${r.receipt_number || r.id}"?`} confirmText="Zrušit doklad" confirmVariant="danger" loading={cancelling} /> ); }