import { useState, useRef } from "react"; 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 AresAdornment, { type AresCompany } from "../components/AresLookup"; import useDebounce from "../hooks/useDebounce"; import { usePaginatedQuery } from "../hooks/usePaginatedQuery"; import { warehouseSupplierListOptions, type WarehouseSupplier, type WarehouseSupplierCustomField, } from "../lib/queries/warehouse"; import { useApiMutation } from "../lib/queries/mutations"; import { Button, Card, DataTable, Pagination, Modal, ConfirmDialog, Field, TextField, CheckboxField, StatusChip, PageHeader, PageEnter, FilterBar, EmptyState, LoadingState, type DataColumn, } from "../ui"; const API_BASE = "/api/admin/warehouse/suppliers"; // Mirror of the customers modal (minus the PDF field-order picker): the same // company fields + "Vlastní pole"; contact person/e-mail/phone live as custom // fields since the dedicated columns were dropped. interface SupplierForm { name: string; ico: string; dic: string; street: string; city: string; postal_code: string; country: string; } const EMPTY_SUPPLIER_FORM: SupplierForm = { name: "", ico: "", dic: "", street: "", city: "", postal_code: "", country: "", }; const PER_PAGE = 20; const PlusIcon = ( ); const EditIcon = ( ); const DeleteIcon = ( ); const SmallPlusIcon = ( ); const RemoveIcon = ( ); export default function WarehouseSuppliers() { const alert = useAlert(); const { hasPermission } = useAuth(); const [page, setPage] = useState(1); const [search, setSearch] = useState(""); const debouncedSearch = useDebounce(search, 300); const { items: suppliers, pagination, isPending, isFetching, } = usePaginatedQuery( warehouseSupplierListOptions({ search: debouncedSearch || undefined, page, perPage: PER_PAGE, }), ); const [showModal, setShowModal] = useState(false); const [editingSupplier, setEditingSupplier] = useState(null); const [form, setForm] = useState({ ...EMPTY_SUPPLIER_FORM }); const [customFields, setCustomFields] = useState< WarehouseSupplierCustomField[] >([]); const customFieldKeyCounter = useRef(0); const [errors, setErrors] = useState>({}); const [deactivateConfirm, setDeactivateConfirm] = useState<{ show: boolean; supplier: WarehouseSupplier | null; }>({ show: false, supplier: null }); const submitMutation = useApiMutation< SupplierForm & { custom_fields: Array<{ name: string; value: string; showLabel?: boolean; }>; }, { id?: number; message?: string } >({ url: () => editingSupplier ? `${API_BASE}/${editingSupplier.id}` : API_BASE, method: () => (editingSupplier ? "PUT" : "POST"), // issued-orders included: the PO form's supplier picker caches under // ["issued-orders","suppliers"] and must see supplier CRUD immediately. invalidate: ["warehouse", "issued-orders"], onSuccess: (data) => { setShowModal(false); alert.success(data?.message || "Dodavatel byl uložen"); }, }); const deleteMutation = useApiMutation({ url: (id) => `${API_BASE}/${id}`, method: () => "DELETE", // issued-orders included: the PO form's supplier picker caches under // ["issued-orders","suppliers"] and must see supplier CRUD immediately. invalidate: ["warehouse", "issued-orders"], onSuccess: (data) => { setDeactivateConfirm({ show: false, supplier: null }); alert.success(data?.message || "Dodavatel byl smazán"); }, }); // id is captured from the mutation variable (built into the URL), so it stays // correct even when called immediately after a state update. The non-strict // UpdateSupplierSchema strips the `id` field from the body. const toggleActiveMutation = useApiMutation< { id: number; is_active: boolean }, { message?: string } >({ url: ({ id }) => `${API_BASE}/${id}`, method: () => "PUT", // issued-orders included: the PO form's supplier picker caches under // ["issued-orders","suppliers"] and must see supplier CRUD immediately. invalidate: ["warehouse", "issued-orders"], }); if (!hasPermission("warehouse.manage")) return ; // 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, ico: c.ico, dic: c.dic || "", street: c.street, city: c.city, postal_code: c.postal_code, country: c.country, })); setErrors((prev) => ({ ...prev, name: "" })); }; const openCreateModal = () => { setEditingSupplier(null); setForm({ ...EMPTY_SUPPLIER_FORM }); setCustomFields([]); setErrors({}); setShowModal(true); }; const openEditModal = (supplier: WarehouseSupplier) => { setEditingSupplier(supplier); setForm({ name: supplier.name, ico: supplier.ico || "", dic: supplier.dic || "", street: supplier.street || "", city: supplier.city || "", postal_code: supplier.postal_code || "", country: supplier.country || "", }); setCustomFields( Array.isArray(supplier.custom_fields) && supplier.custom_fields.length > 0 ? supplier.custom_fields.map((f) => ({ ...f, _key: `cf-${++customFieldKeyCounter.current}`, })) : [], ); setErrors({}); setShowModal(true); }; const handleSubmit = async () => { const newErrors: Record = {}; if (!form.name.trim()) newErrors.name = "Zadejte název dodavatele"; setErrors(newErrors); if (Object.keys(newErrors).length > 0) return; try { await submitMutation.mutateAsync({ ...form, custom_fields: customFields .filter((f) => f.name.trim() || f.value.trim()) .map((f) => ({ name: f.name, value: f.value, showLabel: f.showLabel, })), }); } catch (e) { alert.error(e instanceof Error ? e.message : "Chyba připojení"); } }; const handleDeactivate = async () => { if (!deactivateConfirm.supplier) return; try { await deleteMutation.mutateAsync(deactivateConfirm.supplier.id); } catch (e) { alert.error(e instanceof Error ? e.message : "Chyba připojení"); } }; const toggleActive = async (supplier: WarehouseSupplier) => { try { await toggleActiveMutation.mutateAsync({ id: supplier.id, is_active: !supplier.is_active, }); alert.success( supplier.is_active ? "Dodavatel byl deaktivován" : "Dodavatel byl aktivován", ); } catch (e) { alert.error(e instanceof Error ? e.message : "Chyba připojení"); } }; if (isPending) { return ; } const total = pagination?.total ?? suppliers.length; const subtitle = `${total} ${ total === 1 ? "dodavatel" : total >= 2 && total <= 4 ? "dodavatelé" : "dodavatelů" }`; const columns: DataColumn[] = [ { key: "name", header: "Název", width: "20%", bold: true, render: (s) => s.name, }, { key: "ico", header: "IČO", width: "11%", mono: true, render: (s) => s.ico || "—", }, { key: "dic", header: "DIČ", width: "11%", mono: true, render: (s) => s.dic || "—", }, { key: "street", header: "Ulice", width: "18%", render: (s) => s.street || "—", }, { key: "city", header: "Město", width: "14%", render: (s) => s.city || "—", }, { key: "status", header: "Stav", width: "8%", render: (s) => ( toggleActive(s)} title="Kliknutím přepnete stav" /> ), }, { key: "actions", header: "Akce", width: "10%", align: "right", render: (s) => ( openEditModal(s)} aria-label="Upravit" title="Upravit" > {EditIcon} setDeactivateConfirm({ show: true, supplier: s })} aria-label={s.is_active ? "Deaktivovat" : "Smazat"} title={s.is_active ? "Deaktivovat" : "Smazat"} > {DeleteIcon} ), }, ]; return ( Přidat dodavatele } /> { setSearch(e.target.value); setPage(1); }} placeholder="Hledat dodavatele..." fullWidth /> columns={columns} rows={suppliers} rowKey={(s) => s.id} rowInactive={(s) => !s.is_active} empty={ search ? ( ) : ( Přidat prvního dodavatele } /> ) } /> {/* Add/Edit Modal — mirror of the customers modal (minus the PDF field-order picker) */} setShowModal(false)} onSubmit={handleSubmit} title={editingSupplier ? "Upravit dodavatele" : "Přidat dodavatele"} loading={submitMutation.isPending} maxWidth="md" > { setForm({ ...form, name: e.target.value }); setErrors((prev) => ({ ...prev, name: "" })); }} placeholder="Název firmy / jméno" InputProps={{ endAdornment: ( ), }} /> setForm({ ...form, street: e.target.value })} /> setForm({ ...form, city: e.target.value })} /> setForm({ ...form, postal_code: e.target.value }) } /> setForm({ ...form, country: e.target.value })} /> setForm({ ...form, ico: e.target.value })} InputProps={{ endAdornment: ( ), }} /> setForm({ ...form, dic: e.target.value })} /> {/* Dynamic custom fields — same editor as the customers modal */} 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 }} /> 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); }} /> ))} {/* Deactivate/Delete Confirmation */} setDeactivateConfirm({ show: false, supplier: null })} onConfirm={handleDeactivate} title={ deactivateConfirm.supplier?.is_active ? "Deaktivovat dodavatele" : "Smazat dodavatele" } message={ deactivateConfirm.supplier ? deactivateConfirm.supplier.is_active ? `Opravdu chcete deaktivovat dodavatele "${deactivateConfirm.supplier.name}"?` : `Opravdu chcete smazat dodavatele "${deactivateConfirm.supplier.name}"? Tato akce je nevratná.` : "" } confirmText={ deactivateConfirm.supplier?.is_active ? "Deaktivovat" : "Smazat" } confirmVariant="danger" loading={deleteMutation.isPending} /> ); }