import { useState, useCallback, useRef } from "react"; 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 { useAlert } from "../context/AlertContext"; import { useAuth } from "../context/AuthContext"; import Forbidden from "../components/Forbidden"; import { offerCustomersOptions, type Customer, type CustomField, } from "../lib/queries/offers"; import { useApiMutation } from "../lib/queries/mutations"; import AresAdornment, { type AresCompany } from "../components/AresLookup"; import { Button, Card, DataTable, Modal, ConfirmDialog, Field, TextField, CheckboxField, StatusChip, PageHeader, PageEnter, FilterBar, EmptyState, LoadingState, type DataColumn, } from "../ui"; 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 CustomerForm { name: string; street: string; city: string; postal_code: string; country: string; company_id: string; vat_id: string; } const PlusIcon = ( ); const SmallPlusIcon = ( ); const EditIcon = ( ); const DeleteIcon = ( ); const RemoveIcon = ( ); const UpIcon = ( ); const DownIcon = ( ); export default function OffersCustomers() { const alert = useAlert(); const { hasPermission } = useAuth(); const { data: customers = [], isPending } = useQuery(offerCustomersOptions()); 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); const submitMutation = useApiMutation< CustomerForm & { custom_fields: Array<{ name: string; value: string; showLabel?: boolean; }>; customer_field_order: string[]; }, { message?: string; error?: string } >({ url: () => editingCustomer ? `${API_BASE}/customers/${editingCustomer.id}` : `${API_BASE}/customers`, method: () => (editingCustomer ? "PUT" : "POST"), // Broad domain key — covers the ["offers","customers"] picker query too. invalidate: ["offers"], onSuccess: (data) => { closeModal(); setTimeout( () => alert.success(data?.message || "Zákazník byl uložen"), 300, ); }, }); const deleteMutation = useApiMutation< number, { message?: string; error?: string } >({ url: (id) => `${API_BASE}/customers/${id}`, method: () => "DELETE", // Broad domain key — covers the ["offers","customers"] picker query too. invalidate: ["offers"], onSuccess: (data) => { setDeleteConfirm({ show: false, customer: null }); alert.success(data?.message || "Zákazník byl smazán"); }, }); 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], 10); 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], 10); const cf = customFields[idx]; if (cf) return cf.name ? `${cf.name}: ${cf.value || "..."}` : cf.value || `Vlastní pole ${idx + 1}`; } return key; }; // ARES prefill — overwrite the company fields with registry data (the // button is an explicit user action, so overwriting is the expected UX). const fillFromAres = (c: AresCompany) => { setForm((prev) => ({ ...prev, name: c.name || prev.name, street: c.street, city: c.city, postal_code: c.postal_code, country: c.country, company_id: c.ico, vat_id: c.dic || "", })); }; 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; } const payload = { ...form, custom_fields: customFields .filter((f) => f.name.trim() || f.value.trim()) .map((f) => ({ name: f.name, value: f.value, showLabel: f.showLabel, })), customer_field_order: getFullFieldOrder(), }; setSaving(true); try { await submitMutation.mutateAsync(payload); } catch (e) { alert.error(e instanceof Error ? e.message : "Chyba připojení"); } finally { setSaving(false); } }; const handleDelete = async () => { if (!deleteConfirm.customer) return; setDeleting(true); try { await deleteMutation.mutateAsync(deleteConfirm.customer.id); } catch (e) { alert.error(e instanceof Error ? e.message : "Chyba připojení"); } finally { setDeleting(false); } }; if (!hasPermission("customers.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(); if (isPending) { return ; } const columns: DataColumn[] = [ { key: "name", header: "Název", width: "26%", render: (c) => ( {c.name} {c.street && ( {c.street} )} ), }, { key: "city", header: "Město", width: "18%", render: (c) => c.city || "—", }, { key: "company_id", header: "IČO", width: "13%", mono: true, render: (c) => c.company_id || "—", }, { key: "vat_id", header: "DIČ", width: "13%", mono: true, render: (c) => c.vat_id || "—", }, { key: "quotation_count", header: "Nabídky", width: "10%", render: (c) => ( ), }, { key: "actions", header: "Akce", width: "10%", align: "right", render: (c) => { const cannotDelete = c.quotation_count > 0; return ( {hasPermission("customers.edit") && ( openEditModal(c)} aria-label="Upravit" title="Upravit" > {EditIcon} )} {hasPermission("customers.delete") && ( // Disabled buttons don't fire title/tooltip, so wrap in a titled // span when delete is blocked (customer still has quotations). setDeleteConfirm({ show: true, customer: c })} aria-label={ cannotDelete ? "Nelze smazat zákazníka s nabídkami" : "Smazat" } disabled={cannotDelete} > {DeleteIcon} )} ); }, }, ]; return ( Přidat zákazníka ) : undefined } /> setSearch(e.target.value)} placeholder="Hledat zákazníky..." fullWidth /> columns={columns} rows={filteredCustomers} rowKey={(c) => c.id} empty={ Přidat prvního zákazníka ) : undefined } /> } /> {/* Create/Edit Modal */} setForm((prev) => ({ ...prev, name: e.target.value })) } placeholder="Název firmy / jméno" InputProps={{ endAdornment: ( ), }} /> setForm((prev) => ({ ...prev, street: e.target.value })) } /> setForm((prev) => ({ ...prev, city: e.target.value })) } /> setForm((prev) => ({ ...prev, postal_code: e.target.value, })) } /> setForm((prev) => ({ ...prev, country: e.target.value })) } /> setForm((prev) => ({ ...prev, company_id: e.target.value, })) } InputProps={{ endAdornment: ( ), }} /> setForm((prev) => ({ ...prev, vat_id: e.target.value })) } /> {/* Dynamic custom fields */} Vlastní pole {customFields.map((field, idx) => ( { const updated = [...customFields]; updated[idx] = { ...updated[idx], name: e.target.value, }; setCustomFields(updated); }} placeholder="Např. Kontakt" /> { const updated = [...customFields]; updated[idx] = { ...updated[idx], value: e.target.value, }; setCustomFields(updated); }} sx={{ flex: 1 }} /> { const key = `custom_${idx}`; setFieldOrder((prev) => { return prev .filter((k) => k !== key) .map((k) => { if (k.startsWith("custom_")) { const ki = parseInt(k.split("_")[1], 10); if (ki > idx) return `custom_${ki - 1}`; } return k; }); }); setCustomFields(customFields.filter((_, i) => i !== idx)); }} title="Odebrat pole" aria-label="Odebrat pole" > {RemoveIcon} Zobrazit název v PDF } checked={field.showLabel !== false} onChange={(checked) => { const updated = [...customFields]; updated[idx] = { ...updated[idx], showLabel: checked, }; setCustomFields(updated); }} /> ))} {/* Field order for PDF */} Pořadí polí v PDF Určuje pořadí řádků v adresním bloku zákazníka na PDF nabídce. {fullFieldOrder.map((key, index) => ( moveField(index, -1)} disabled={index === 0} title="Nahoru" aria-label="Nahoru" sx={{ p: 0.25 }} > {UpIcon} moveField(index, 1)} disabled={index === fullFieldOrder.length - 1} title="Dolů" aria-label="Dolů" sx={{ p: 0.25 }} > {DownIcon} {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" confirmVariant="danger" loading={deleting} /> ); }