import { useState, useEffect, useCallback, useMemo, type ReactNode, } from "react"; import DOMPurify from "dompurify"; import { useAlert } from "../context/AlertContext"; import { useAuth } from "../context/AuthContext"; import { useParams, useNavigate, Link } from "react-router-dom"; import { motion } from "framer-motion"; import ConfirmModal from "../components/ConfirmModal"; import OrderConfirmationModal from "../components/OrderConfirmationModal"; import FormField from "../components/FormField"; import Forbidden from "../components/Forbidden"; import apiFetch from "../utils/api"; import { formatCurrency, formatDate } from "../utils/formatters"; const API_BASE = "/api/admin"; const STATUS_LABELS: Record = { prijata: "Přijatá", v_realizaci: "V realizaci", dokoncena: "Dokončená", stornovana: "Stornována", }; const STATUS_CLASSES: Record = { prijata: "admin-badge-order-prijata", v_realizaci: "admin-badge-order-realizace", dokoncena: "admin-badge-order-dokoncena", stornovana: "admin-badge-order-stornovana", }; const TRANSITION_LABELS: Record = { v_realizaci: "Zahájit realizaci", dokoncena: "Dokončit", }; const TRANSITION_CLASSES: Record = { v_realizaci: "admin-btn admin-btn-primary", dokoncena: "admin-btn admin-btn-primary", }; interface OrderItem { id?: number; description: string; item_description?: string; quantity: number; unit: string; unit_price: number; is_included_in_total: number | boolean; } interface OrderSection { id?: number; title: string; title_cz?: string; content: string; } interface Invoice { id: number; invoice_number: string; } interface Project { id: number; project_number: string; name: string; has_nas_folder?: boolean; } interface OrderData { id: number; order_number: string; quotation_id: number; quotation_number: string; project_code?: string; customer_name: string; customer_order_number: string; currency: string; created_at: string; status: string; notes: string; attachment_name?: string; apply_vat: number | boolean; vat_rate: number; language?: string; items: OrderItem[]; sections: OrderSection[]; scope_title?: string; scope_description?: string; valid_transitions?: string[]; invoice?: Invoice; project?: Project; } export default function OrderDetail() { const { id } = useParams(); const alert = useAlert(); const { hasPermission } = useAuth(); const navigate = useNavigate(); const [loading, setLoading] = useState(true); const [order, setOrder] = useState(null); const [notes, setNotes] = useState(""); const [saving, setSaving] = useState(false); const [statusChanging, setStatusChanging] = useState(null); const [statusConfirm, setStatusConfirm] = useState<{ show: boolean; status: string | null; }>({ show: false, status: null }); const [attachmentLoading, setAttachmentLoading] = useState(false); const [deleteConfirm, setDeleteConfirm] = useState(false); const [deleting, setDeleting] = useState(false); const [deleteFiles, setDeleteFiles] = useState(false); const [showConfirmationModal, setShowConfirmationModal] = useState(false); const [confirmationLoading, setConfirmationLoading] = useState(false); const initialNotesRef = useRef(null); const hasSetInitialSnapshot = useRef(false); const fetchDetail = useCallback(async () => { try { const response = await apiFetch(`${API_BASE}/orders/${id}`); if (response.status === 401) return; const result = await response.json(); if (result.success) { setOrder(result.data); setNotes(result.data.notes || ""); } else { alert.error(result.error || "Nepodařilo se načíst objednávku"); navigate("/orders"); } } catch { alert.error("Chyba připojení"); navigate("/orders"); } finally { setLoading(false); } }, [id, alert, navigate]); useEffect(() => { fetchDetail(); }, [fetchDetail]); useEffect(() => { if (loading) { hasSetInitialSnapshot.current = false; return; } if (!hasSetInitialSnapshot.current) { initialNotesRef.current = notes; hasSetInitialSnapshot.current = true; } }, [loading, notes]); const isDirty = useMemo(() => { if (!initialNotesRef.current) return false; return notes !== initialNotesRef.current; }, [notes]); useEffect(() => { if (!isDirty) return; const handler = (e: BeforeUnloadEvent) => { e.preventDefault(); e.returnValue = ""; }; window.addEventListener("beforeunload", handler); return () => window.removeEventListener("beforeunload", handler); }, [isDirty]); const totals = useMemo(() => { if (!order?.items) return { subtotal: 0, vatAmount: 0, total: 0 }; const subtotal = order.items.reduce((sum, item) => { if (Number(item.is_included_in_total)) { return ( sum + (Number(item.quantity) || 0) * (Number(item.unit_price) || 0) ); } return sum; }, 0); const vatAmount = Number(order.apply_vat) ? subtotal * ((Number(order.vat_rate) || 0) / 100) : 0; return { subtotal, vatAmount, total: subtotal + vatAmount }; }, [order]); if (!hasPermission("orders.view")) return ; const handleStatusChange = async () => { if (!statusConfirm.status) return; setStatusChanging(statusConfirm.status); setStatusConfirm({ show: false, status: null }); try { const response = await apiFetch(`${API_BASE}/orders/${id}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: statusConfirm.status }), }); const result = await response.json(); if (result.success) { alert.success(result.message || "Stav byl změněn"); fetchDetail(); } else { alert.error(result.error || "Nepodařilo se změnit stav"); } } catch { alert.error("Chyba připojení"); } finally { setStatusChanging(null); } }; const handleSaveNotes = async () => { setSaving(true); try { const response = await apiFetch(`${API_BASE}/orders/${id}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ notes: notes }), }); const result = await response.json(); if (result.success) { alert.success("Poznámky byly uloženy"); initialNotesRef.current = notes; } else { alert.error(result.error || "Nepodařilo se uložit poznámky"); } } catch { alert.error("Chyba připojení"); } finally { setSaving(false); } }; const handleViewAttachment = async () => { const newWindow = window.open("", "_blank"); setAttachmentLoading(true); try { const response = await apiFetch(`${API_BASE}/orders/${id}/attachment`); if (!response.ok) { newWindow?.close(); alert.error("Nepodařilo se stáhnout přílohu"); 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řipojení"); } finally { setAttachmentLoading(false); } }; const handleGenerateConfirmation = async ( lang: string, applyVat: boolean, customItems?: Array<{ description: string; quantity: number; unit: string; unit_price: number; is_included_in_total: boolean; vat_rate: number; }>, ) => { setConfirmationLoading(true); try { const response = await apiFetch( `${API_BASE}/orders-pdf/${id}/confirmation`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ lang, applyVat, items: customItems }), }, ); if (!response.ok) { const result = await response.json().catch(() => ({})); alert.error(result.error || "Nepodařilo se vygenerovat PDF"); return; } const blob = await response.blob(); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `Potvrzeni-${order?.order_number || String(id)}.pdf`; document.body.appendChild(a); a.click(); document.body.removeChild(a); setTimeout(() => URL.revokeObjectURL(url), 60000); } catch { alert.error("Chyba připojení"); } finally { setConfirmationLoading(false); } }; const handleDelete = async () => { setDeleting(true); try { const response = await apiFetch(`${API_BASE}/orders/${id}`, { method: "DELETE", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ delete_files: deleteFiles }), }); const result = await response.json(); if (result.success) { alert.success(result.message || "Objednávka byla smazána"); navigate("/orders"); } else { alert.error(result.error || "Nepodařilo se smazat objednávku"); } } catch { alert.error("Chyba připojení"); } finally { setDeleting(false); setDeleteConfirm(false); } }; if (loading) { return (
{[0, 1, 2, 3].map((i) => (
))}
); } if (!order) return null; return (
{/* Header */}

Objednávka {order.order_number} {STATUS_LABELS[order.status] || order.status}

{order.invoice ? ( Faktura {order.invoice.invoice_number} ) : ( hasPermission("invoices.create") && order.status === "dokoncena" && ( Vytvořit fakturu ) )} {hasPermission("orders.edit") && order.valid_transitions?.filter((s) => s !== "stornovana").length! > 0 && order .valid_transitions!.filter((s) => s !== "stornovana") .map((status) => ( ))} {hasPermission("orders.delete") && ( )}
{/* Info card */}

Informace

{order.quotation_number} {order.project_code && ( ({order.project_code}) )}
{order.project ? ( {order.project.project_number} — {order.project.name} ) : ( "—" )}
{order.customer_name || "—"}
{order.customer_order_number || "—"}
{order.currency}
{formatDate(order.created_at)}
{order.attachment_name ? ( ) : ( "—" )}
{/* Items (read-only) */}

Položky

{order.items?.length > 0 ? (
{order.items.map((item, index) => { const lineTotal = (Number(item.quantity) || 0) * (Number(item.unit_price) || 0); return ( ); })}
# Popis Množství Jednotka Jedn. cena V ceně Celkem
{index + 1}
{item.description || "—"}
{item.item_description && (
{item.item_description}
)}
{item.quantity} {item.unit || "—"} {formatCurrency(item.unit_price, order.currency)} {Number(item.is_included_in_total) ? "Ano" : "Ne"} {formatCurrency(lineTotal, order.currency)}
) : (

Žádné položky.

)} {/* Totals */}
Mezisoučet: {formatCurrency(totals.subtotal, order.currency)}
{Number(order.apply_vat) > 0 && (
DPH ({order.vat_rate}%): {formatCurrency(totals.vatAmount, order.currency)}
)}
Celkem k úhradě: {formatCurrency(totals.total, order.currency)}
{/* Sections (read-only) */} {order.sections?.length > 0 && (

Rozsah projektu

{order.scope_title && (
{order.scope_title}
)} {order.scope_description && (
{order.scope_description}
)}
{order.sections.map((section, index) => (
{index + 1}. {(order.language === "CZ" ? section.title_cz || section.title : section.title || section.title_cz) || `Sekce ${index + 1}`}
{section.content && (
)}
))}
)} {/* Notes (editable) */}

Poznámky