import { useState, useCallback, useRef } from "react"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useAlert } from "../context/AlertContext"; import { useAuth } from "../context/AuthContext"; import { motion, AnimatePresence } from "framer-motion"; import ConfirmModal from "../components/ConfirmModal"; import FormField from "../components/FormField"; import Forbidden from "../components/Forbidden"; import useModalLock from "../hooks/useModalLock"; import { offerCustomersOptions } from "../lib/queries/offers"; import { Skeleton } from "boneyard-js/react"; import OffersCustomersFixture from "../fixtures/OffersCustomersFixture"; import apiFetch from "../utils/api"; const API_BASE = "/api/admin"; const DEFAULT_CUSTOMER_FIELD_ORDER = [ "street", "city_postal", "country", "company_id", "vat_id", ]; const CUSTOMER_FIELD_LABELS: Record = { street: "Ulice", city_postal: "Město + PSČ", country: "Země", company_id: "IČO", vat_id: "DIČ", }; interface Customer { id: number; name: string; street?: string; city?: string; postal_code?: string; country?: string; company_id?: string; vat_id?: string; quotation_count: number; custom_fields?: CustomField[]; customer_field_order?: string[]; } interface CustomField { name: string; value: string; showLabel: boolean; _key?: string; } interface CustomerForm { name: string; street: string; city: string; postal_code: string; country: string; company_id: string; vat_id: string; } export default function OffersCustomers() { const alert = useAlert(); const { hasPermission } = useAuth(); const queryClient = useQueryClient(); const { data: customers = [], isPending } = useQuery( offerCustomersOptions(), ) as { data: Customer[]; isPending: boolean }; const [search, setSearch] = useState(""); const [showModal, setShowModal] = useState(false); const [editingCustomer, setEditingCustomer] = useState(null); const [saving, setSaving] = useState(false); const [form, setForm] = useState({ name: "", street: "", city: "", postal_code: "", country: "", company_id: "", vat_id: "", }); const [customFields, setCustomFields] = useState([]); const customFieldKeyCounter = useRef(0); const [fieldOrder, setFieldOrder] = useState([ ...DEFAULT_CUSTOMER_FIELD_ORDER, ]); const [deleteConfirm, setDeleteConfirm] = useState<{ show: boolean; customer: Customer | null; }>({ show: false, customer: null }); const [deleting, setDeleting] = useState(false); useModalLock(showModal); const getFullFieldOrder = useCallback(() => { const allBuiltIn = [...DEFAULT_CUSTOMER_FIELD_ORDER]; const order = [...fieldOrder].filter((k) => k !== "name"); for (const f of allBuiltIn) { if (!order.includes(f)) order.push(f); } for (let i = 0; i < customFields.length; i++) { const key = `custom_${i}`; if (!order.includes(key)) order.push(key); } return order.filter((key) => { if (key.startsWith("custom_")) { const idx = parseInt(key.split("_")[1]); return idx < customFields.length; } return true; }); }, [fieldOrder, customFields]); const moveField = (index: number, direction: number) => { const order = getFullFieldOrder(); const newIndex = index + direction; if (newIndex < 0 || newIndex >= order.length) return; const updated = [...order]; [updated[index], updated[newIndex]] = [updated[newIndex], updated[index]]; setFieldOrder(updated); }; const getFieldDisplayName = (key: string) => { if (CUSTOMER_FIELD_LABELS[key]) return CUSTOMER_FIELD_LABELS[key]; if (key.startsWith("custom_")) { const idx = parseInt(key.split("_")[1]); const cf = customFields[idx]; if (cf) return cf.name ? `${cf.name}: ${cf.value || "..."}` : cf.value || `Vlastní pole ${idx + 1}`; } return key; }; // Data fetching moved to useQuery(offerCustomersOptions()) above const openCreateModal = () => { setEditingCustomer(null); setForm({ name: "", street: "", city: "", postal_code: "", country: "", company_id: "", vat_id: "", }); setCustomFields([]); setFieldOrder([...DEFAULT_CUSTOMER_FIELD_ORDER]); setShowModal(true); }; const openEditModal = (customer: Customer) => { setEditingCustomer(customer); setForm({ name: customer.name || "", street: customer.street || "", city: customer.city || "", postal_code: customer.postal_code || "", country: customer.country || "", company_id: customer.company_id || "", vat_id: customer.vat_id || "", }); const cf = Array.isArray(customer.custom_fields) && customer.custom_fields.length > 0 ? customer.custom_fields.map((f) => ({ ...f, _key: `cf-${++customFieldKeyCounter.current}`, })) : []; setCustomFields(cf); if ( Array.isArray(customer.customer_field_order) && customer.customer_field_order.length > 0 ) { setFieldOrder(customer.customer_field_order); } else { setFieldOrder([...DEFAULT_CUSTOMER_FIELD_ORDER]); } setShowModal(true); }; const closeModal = () => { setShowModal(false); setEditingCustomer(null); }; const handleSubmit = async () => { if (!form.name.trim()) { alert.error("Název zákazníka je povinný"); return; } setSaving(true); try { const url = editingCustomer ? `${API_BASE}/customers/${editingCustomer.id}` : `${API_BASE}/customers`; const payload = { ...form, custom_fields: customFields.filter( (f) => f.name.trim() || f.value.trim(), ), customer_field_order: getFullFieldOrder(), }; const response = await apiFetch(url, { method: editingCustomer ? "PUT" : "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); const result = await response.json(); if (result.success) { closeModal(); await new Promise((resolve) => setTimeout(resolve, 300)); alert.success( result.message || (editingCustomer ? "Zákazník byl aktualizován" : "Zákazník byl vytvořen"), ); queryClient.invalidateQueries({ queryKey: ["offer-customers"] }); } else { alert.error(result.error || "Nepodařilo se uložit zákazníka"); } } catch { alert.error("Chyba připojení"); } finally { setSaving(false); } }; const handleDelete = async () => { if (!deleteConfirm.customer) return; setDeleting(true); try { const response = await apiFetch( `${API_BASE}/customers/${deleteConfirm.customer.id}`, { method: "DELETE", }, ); const result = await response.json(); if (result.success) { setDeleteConfirm({ show: false, customer: null }); alert.success(result.message || "Zákazník byl smazán"); queryClient.invalidateQueries({ queryKey: ["offer-customers"] }); } else { alert.error(result.error || "Nepodařilo se smazat zákazníka"); } } catch { alert.error("Chyba připojení"); } finally { setDeleting(false); } }; if (!hasPermission("offers.view")) return ; const filteredCustomers = search ? customers.filter( (c) => (c.name || "").toLowerCase().includes(search.toLowerCase()) || (c.company_id || "").includes(search) || (c.city || "").toLowerCase().includes(search.toLowerCase()), ) : customers; const fullFieldOrder = getFullFieldOrder(); return ( } >

Zákazníci

Správa zákazníků pro nabídky

{hasPermission("offers.create") && ( )}
setSearch(e.target.value)} className="admin-form-input" placeholder="Hledat zákazníky..." />
{filteredCustomers.length === 0 ? (

{search ? "Žádní zákazníci odpovídající hledání." : "Zatím nejsou žádní zákazníci."}

{!search && hasPermission("offers.create") && ( )}
) : (
{filteredCustomers.map((customer) => ( ))}
Název Město IČO DIČ Nabídky Akce
{customer.name}
{customer.street && (
{customer.street}
)}
{customer.city || "—"} {customer.company_id || "—"} {customer.vat_id || "—"} {customer.quotation_count || 0}
{hasPermission("offers.edit") && ( )} {hasPermission("offers.delete") && ( )}
)}
{/* Create/Edit Modal */} {showModal && (

{editingCustomer ? "Upravit zákazníka" : "Nový zákazník"}

setForm((prev) => ({ ...prev, name: e.target.value })) } className="admin-form-input" placeholder="Název firmy / jméno" /> setForm((prev) => ({ ...prev, street: e.target.value, })) } className="admin-form-input" />
setForm((prev) => ({ ...prev, city: e.target.value, })) } className="admin-form-input" /> setForm((prev) => ({ ...prev, postal_code: e.target.value, })) } className="admin-form-input" />
setForm((prev) => ({ ...prev, country: e.target.value, })) } className="admin-form-input" />
setForm((prev) => ({ ...prev, company_id: e.target.value, })) } className="admin-form-input" /> setForm((prev) => ({ ...prev, vat_id: e.target.value, })) } className="admin-form-input" />
{/* Dynamic custom fields */}
{customFields.map((field, idx) => (
{ const updated = [...customFields]; updated[idx] = { ...updated[idx], name: e.target.value, }; setCustomFields(updated); }} className="admin-form-input" placeholder="Např. Kontakt" />
{ const updated = [...customFields]; updated[idx] = { ...updated[idx], value: e.target.value, }; setCustomFields(updated); }} className="admin-form-input" style={{ flex: 1 }} />
))}
{/* Field order for PDF */}
Určuje pořadí řádků v adresním bloku zákazníka na PDF nabídce.
{fullFieldOrder.map((key, index) => (
{getFieldDisplayName(key)}
))}
)} {/* Delete Confirm Modal */} setDeleteConfirm({ show: false, customer: null })} onConfirm={handleDelete} title="Smazat zákazníka" message={`Opravdu chcete smazat zákazníka "${deleteConfirm.customer?.name}"? Tato akce je nevratná.`} confirmText="Smazat" cancelText="Zrušit" type="danger" loading={deleting} />
); }