import { useState } from "react"; import { useAlert } from "../context/AlertContext"; import { useAuth } from "../context/AuthContext"; import { Link, useNavigate } from "react-router-dom"; import { motion, AnimatePresence } from "framer-motion"; import ConfirmModal from "../components/ConfirmModal"; import Forbidden from "../components/Forbidden"; import apiFetch from "../utils/api"; import { formatCurrency, formatDate, czechPlural } from "../utils/formatters"; import SortIcon from "../components/SortIcon"; import useTableSort from "../hooks/useTableSort"; import useListData from "../hooks/useListData"; import useModalLock from "../hooks/useModalLock"; import Pagination from "../components/Pagination"; import FormField from "../components/FormField"; const API_BASE = "/api/admin"; const DRAFT_KEY = "boha_offer_draft"; interface Quotation { id: number; quotation_number: string; project_code: string; customer_name: string; created_at: string; valid_until: string; currency: string; total: number; status: string; order_id?: number; } interface Draft { form: { project_code: string; customer_name: string; created_at: string; valid_until: string; currency: string; }; items: unknown[]; savedAt?: string; } export default function Offers() { const alert = useAlert(); const { hasPermission } = useAuth(); const navigate = useNavigate(); const { sort, order, handleSort, activeSort } = useTableSort("quotation_number"); const [search, setSearch] = useState(""); const [page, setPage] = useState(1); const [deleteConfirm, setDeleteConfirm] = useState<{ show: boolean; quotation: Quotation | null; }>({ show: false, quotation: null }); const [deleting, setDeleting] = useState(false); const [invalidateConfirm, setInvalidateConfirm] = useState<{ show: boolean; quotation: Quotation | null; }>({ show: false, quotation: null }); const [invalidating, setInvalidating] = useState(false); const [duplicating, setDuplicating] = useState(null); const [pdfLoading, setPdfLoading] = useState(null); const [creatingOrder, setCreatingOrder] = useState(null); const [orderModal, setOrderModal] = useState<{ show: boolean; quotation: Quotation | null; }>({ show: false, quotation: null }); useModalLock(orderModal.show); const [customerOrderNumber, setCustomerOrderNumber] = useState(""); const [orderAttachment, setOrderAttachment] = useState(null); const [draft, setDraft] = useState(() => { try { const raw = localStorage.getItem(DRAFT_KEY); if (!raw) return null; const parsed = JSON.parse(raw); if (parsed && parsed.form && Array.isArray(parsed.items)) return parsed; } catch { /* ignore corrupt data */ } return null; }); const { items: quotations, loading, initialLoad, pagination, refetch: fetchData, } = useListData("offers", { search, sort, order, page, errorMsg: "Nepodařilo se načíst nabídky", }); const discardDraft = () => { try { localStorage.removeItem(DRAFT_KEY); } catch { /* ignore */ } setDraft(null); }; const getRowClass = (invalidated: boolean, expired: boolean) => { if (invalidated) return "offers-invalidated-row"; if (expired) return "offers-expired-row"; return ""; }; if (!hasPermission("offers.view")) return ; const handleDuplicate = async (quotation: Quotation) => { setDuplicating(quotation.id); try { const response = await apiFetch( `${API_BASE}/offers/${quotation.id}/duplicate`, { method: "POST", headers: { "Content-Type": "application/json" }, }, ); const result = await response.json(); if (result.success) { alert.success(result.message || "Nabídka byla duplikována"); fetchData(); } else { alert.error(result.error || "Nepodařilo se duplikovat nabídku"); } } catch { alert.error("Chyba připojení"); } finally { setDuplicating(null); } }; const handleCreateOrder = async () => { if (!customerOrderNumber.trim() || !orderModal.quotation) return; setCreatingOrder(orderModal.quotation.id); try { const formData = new FormData(); formData.append("quotationId", String(orderModal.quotation.id)); formData.append("customerOrderNumber", customerOrderNumber.trim()); if (orderAttachment) { formData.append("attachment", orderAttachment); } const response = await apiFetch(`${API_BASE}/orders`, { method: "POST", body: formData, }); const result = await response.json(); if (result.success) { setOrderModal({ show: false, quotation: null }); alert.success(result.message || "Objednávka byla vytvořena"); navigate(`/orders/${result.data.order_id}`); } else { alert.error(result.error || "Nepodařilo se vytvořit objednávku"); } } catch { alert.error("Chyba připojení"); } finally { setCreatingOrder(null); } }; const handleDelete = async () => { if (!deleteConfirm.quotation) return; setDeleting(true); try { const response = await apiFetch( `${API_BASE}/offers/${deleteConfirm.quotation.id}`, { method: "DELETE", }, ); const result = await response.json(); if (result.success) { setDeleteConfirm({ show: false, quotation: null }); alert.success(result.message || "Nabídka byla smazána"); fetchData(); } else { alert.error(result.error || "Nepodařilo se smazat nabídku"); } } catch { alert.error("Chyba připojení"); } finally { setDeleting(false); } }; const handleInvalidate = async () => { if (!invalidateConfirm.quotation) return; setInvalidating(true); try { const response = await apiFetch( `${API_BASE}/offers/${invalidateConfirm.quotation.id}/invalidate`, { method: "POST", }, ); const result = await response.json(); if (result.success) { setInvalidateConfirm({ show: false, quotation: null }); alert.success(result.message || "Nabídka byla zneplatněna"); fetchData(); } else { alert.error(result.error || "Nepodařilo se zneplatnit nabídku"); } } catch { alert.error("Chyba připojení"); } finally { setInvalidating(false); } }; const handlePdf = async (quotation: Quotation) => { if (pdfLoading) return; const newWindow = window.open("", "_blank"); setPdfLoading(quotation.id); try { const response = await apiFetch( `${API_BASE}/offers/${quotation.id}/file`, ); if (response.status === 401) { newWindow?.close(); return; } if (!response.ok) { newWindow?.close(); alert.error("PDF soubor nenalezen — otevřete nabídku a uložte ji"); 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 { setPdfLoading(null); } }; if (initialLoad) { return (
{[0, 1, 2, 3, 4].map((i) => (
))}
); } return (

Nabídky

{pagination?.total ?? quotations.length}{" "} {czechPlural( pagination?.total ?? quotations.length, "nabídka", "nabídky", "nabídek", )}

{hasPermission("settings.manage") && ( Šablony )} {hasPermission("offers.create") && ( Nová nabídka )}
{ setSearch(e.target.value); setPage(1); }} className="admin-form-input" placeholder="Hledat podle čísla, projektu nebo zákazníka..." />
{quotations.length === 0 && !draft ? (

Zatím nejsou žádné nabídky.

{hasPermission("offers.create") && ( Vytvořit první nabídku )}
) : (
{draft && !search && ( )} {(quotations as Quotation[]).map((q) => { const isInvalidated = q.status === "invalidated"; const isExpired = !isInvalidated && !q.order_id && q.valid_until && new Date(q.valid_until) < new Date(new Date().toDateString()); return ( ); })} {quotations.length === 0 && draft && search && ( )}
handleSort("quotation_number")} > Číslo{" "} handleSort("project_code")} > Projekt{" "} Zákazník handleSort("created_at")} > Datum{" "} handleSort("valid_until")} > Platnost{" "} handleSort("currency")} > Měna{" "} Celkem Akce
Koncept {draft.savedAt && ( {" · "} {new Date(draft.savedAt).toLocaleTimeString( "cs-CZ", { hour: "2-digit", minute: "2-digit" }, )} )} {draft.form.project_code || "—"} {draft.form.customer_name || "—"} {draft.form.created_at ? formatDate(draft.form.created_at) : "—"} {draft.form.valid_until ? formatDate(draft.form.valid_until) : "—"} {draft.form.currency || "—"}
{q.quotation_number} {q.project_code || "—"} {q.customer_name || "—"} {formatDate(q.created_at)} {formatDate(q.valid_until)} {q.currency} {formatCurrency(q.total, q.currency)}
{isInvalidated ? ( ) : ( )} {!isInvalidated && hasPermission("offers.create") && ( )} {!isInvalidated && q.order_id ? ( O ) : ( !isInvalidated && hasPermission("orders.create") && ( ) )} {isExpired && !isInvalidated && hasPermission("offers.edit") && ( )} {hasPermission("offers.export") && ( )} {hasPermission("offers.delete") && ( )}
Žádné nabídky odpovídající hledání.
)}
setDeleteConfirm({ show: false, quotation: null })} onConfirm={handleDelete} title="Smazat nabídku" message={`Opravdu chcete smazat nabídku "${deleteConfirm.quotation?.quotation_number}"? Budou smazány i všechny položky a sekce. Tato akce je nevratná.`} confirmText="Smazat" cancelText="Zrušit" type="danger" loading={deleting} /> setInvalidateConfirm({ show: false, quotation: null })} onConfirm={handleInvalidate} title="Zneplatnit nabídku" message={`Opravdu chcete zneplatnit nabídku "${invalidateConfirm.quotation?.quotation_number}"? Nabídka bude pouze pro čtení a nepůjde upravovat.`} confirmText="Zneplatnit" cancelText="Zrušit" type="danger" loading={invalidating} /> {orderModal.show && (
!creatingOrder && setOrderModal({ show: false, quotation: null }) } />

Vytvořit objednávku

Nabídka:{" "} {orderModal.quotation?.quotation_number}

setCustomerOrderNumber(e.target.value)} onKeyDown={(e) => e.key === "Enter" && !creatingOrder && handleCreateOrder() } className="admin-form-input" placeholder="Např. PO-2026-001" autoFocus /> {orderAttachment ? (
{orderAttachment.name}{" "} ({(orderAttachment.size / 1024).toFixed(0)} KB)
) : ( )} Max 10 MB
)}
); }