import { useState } from "react"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useAlert } from "../context/AlertContext"; import { useAuth } from "../context/AuthContext"; import { Link, useNavigate } from "react-router-dom"; import apiFetch from "../utils/api"; import Forbidden from "../components/Forbidden"; import { motion } from "framer-motion"; import ConfirmModal from "../components/ConfirmModal"; import FormModal from "../components/FormModal"; import FormField from "../components/FormField"; import { formatCurrency, formatDate, czechPlural } from "../utils/formatters"; import SortIcon from "../components/SortIcon"; import useTableSort from "../hooks/useTableSort"; import { usePaginatedQuery } from "../hooks/usePaginatedQuery"; import { orderListOptions, orderNextNumberOptions, } from "../lib/queries/orders"; import { offerCustomersOptions } from "../lib/queries/offers"; import { useApiMutation } from "../lib/queries/mutations"; import Pagination from "../components/Pagination"; 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", }; interface Order { id: number; order_number: string; quotation_id: number; quotation_number: string; customer_name: string; status: string; created_at: string; total: number; currency: string; invoice_id?: number; } export default function Orders() { const alert = useAlert(); const { hasPermission } = useAuth(); const { sort, order, handleSort, activeSort } = useTableSort("order_number"); const [search, setSearch] = useState(""); const [page, setPage] = useState(1); const [deleteConfirm, setDeleteConfirm] = useState<{ show: boolean; order: Order | null; }>({ show: false, order: null }); const [deleteFiles, setDeleteFiles] = useState(false); const deleteMutation = useApiMutation< { id: number; delete_files: boolean }, { message?: string; error?: string } >({ url: ({ id }) => `${API_BASE}/orders/${id}`, method: () => "DELETE", invalidate: ["orders", "offers", "projects", "invoices"], onSuccess: (data) => { setDeleteConfirm({ show: false, order: null }); setDeleteFiles(false); alert.success(data?.message || "Objednávka byla smazána"); }, }); const [showCreate, setShowCreate] = useState(false); const [createForm, setCreateForm] = useState({ customer_id: "", customer_order_number: "", currency: "CZK", vat_rate: "21", apply_vat: true, scope_title: "", scope_description: "", notes: "", create_project: true, price: "", quantity: "1", }); const [creating, setCreating] = useState(false); const queryClient = useQueryClient(); const navigate = useNavigate(); const [orderAttachment, setOrderAttachment] = useState(null); const customersQuery = useQuery({ ...offerCustomersOptions(), enabled: showCreate, }); const nextNumberQuery = useQuery({ ...orderNextNumberOptions(), enabled: showCreate, }); const openCreate = () => { setCreateForm({ customer_id: "", customer_order_number: "", currency: "CZK", vat_rate: "21", apply_vat: true, scope_title: "", scope_description: "", notes: "", create_project: true, price: "", quantity: "1", }); setOrderAttachment(null); setShowCreate(true); }; const handleCreate = async () => { setCreating(true); try { const priceNum = createForm.price ? Number(createForm.price) : 0; const qtyNum = createForm.quantity ? Number(createForm.quantity) : 1; const items = priceNum > 0 ? [ { description: createForm.scope_title || "Položka", quantity: qtyNum, unit_price: priceNum, is_included_in_total: true, }, ] : undefined; let fetchOptions: RequestInit; if (orderAttachment) { const fd = new FormData(); if (createForm.customer_id) fd.append("customer_id", createForm.customer_id); fd.append("customer_order_number", createForm.customer_order_number); fd.append("currency", createForm.currency); fd.append("vat_rate", createForm.vat_rate); fd.append("apply_vat", createForm.apply_vat ? "1" : "0"); fd.append("scope_title", createForm.scope_title); fd.append("scope_description", createForm.scope_description); fd.append("notes", createForm.notes); fd.append("create_project", createForm.create_project ? "1" : "0"); if (items) fd.append("items", JSON.stringify(items)); fd.append("attachment", orderAttachment); fetchOptions = { method: "POST", body: fd }; } else { fetchOptions = { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ customer_id: createForm.customer_id ? Number(createForm.customer_id) : null, customer_order_number: createForm.customer_order_number, currency: createForm.currency, vat_rate: createForm.vat_rate, apply_vat: createForm.apply_vat, scope_title: createForm.scope_title, scope_description: createForm.scope_description, notes: createForm.notes, create_project: createForm.create_project, ...(items ? { items } : {}), }), }; } const response = await apiFetch(`${API_BASE}/orders`, fetchOptions); const result = await response.json(); if (result.success) { setShowCreate(false); alert.success(result.message || "Objednávka byla vytvořena"); await queryClient.invalidateQueries({ queryKey: ["orders"] }); await queryClient.invalidateQueries({ queryKey: ["projects"] }); await queryClient.invalidateQueries({ queryKey: ["offers"] }); await queryClient.invalidateQueries({ queryKey: ["invoices"] }); if (result.data?.id) navigate(`/orders/${result.data.id}`); } else { alert.error(result.error || "Nepodařilo se vytvořit objednávku"); } } catch { alert.error("Chyba připojení"); } finally { setCreating(false); } }; const { items: orders, pagination, isPending, isFetching, } = usePaginatedQuery(orderListOptions({ search, sort, order, page })); if (!hasPermission("orders.view")) return ; const handleDelete = async () => { if (!deleteConfirm.order) return; try { await deleteMutation.mutateAsync({ id: deleteConfirm.order.id, delete_files: deleteFiles, }); } catch (e) { alert.error(e instanceof Error ? e.message : "Chyba připojení"); } }; if (isPending) { return (
); } return (

Objednávky

{pagination?.total ?? orders.length}{" "} {czechPlural( pagination?.total ?? orders.length, "objednávka", "objednávky", "objednávek", )}

{hasPermission("orders.create") && (
)}
{ setSearch(e.target.value); setPage(1); }} className="admin-form-input" placeholder="Hledat podle čísla, nabídky, projektu nebo zákazníka..." />
{orders.length === 0 ? (

Zatím nejsou žádné objednávky.

Objednávky se vytvářejí z nabídek.

) : (
{(orders as Order[]).map((o) => ( ))}
handleSort("order_number")} > Číslo{" "} Nabídka Zákazník handleSort("status")} > Stav{" "} handleSort("created_at")} > Datum{" "} Celkem Akce
{o.order_number} {o.quotation_number} {o.customer_name || "—"} {STATUS_LABELS[o.status] || o.status} {formatDate(o.created_at)} {formatCurrency(o.total, o.currency)}
{o.invoice_id ? ( F ) : ( hasPermission("invoices.create") && ( ) )} {hasPermission("orders.delete") && ( )}
)}
{ setDeleteConfirm({ show: false, order: null }); setDeleteFiles(false); }} onConfirm={handleDelete} title="Smazat objednávku" message={ <> Opravdu chcete smazat objednávku " {deleteConfirm.order?.order_number}"? Bude smazán i přidružený projekt. Tato akce je nevratná. } confirmText="Smazat" cancelText="Zrušit" type="danger" loading={deleteMutation.isPending} /> setShowCreate(false)} onSubmit={handleCreate} title="Vytvořit objednávku bez nabídky" submitLabel="Vytvořit" loading={creating} >
setCreateForm({ ...createForm, customer_order_number: e.target.value, }) } className="admin-form-input" />
setCreateForm({ ...createForm, vat_rate: e.target.value }) } className="admin-form-input" min="0" />
setCreateForm({ ...createForm, price: e.target.value }) } className="admin-form-input" min="0" step="0.01" placeholder="0" /> setCreateForm({ ...createForm, quantity: e.target.value }) } className="admin-form-input" min="0" step="1" />
setCreateForm({ ...createForm, scope_title: e.target.value }) } className="admin-form-input" />