import { useState } from "react"; import { useQuery } from "@tanstack/react-query"; import Box from "@mui/material/Box"; import IconButton from "@mui/material/IconButton"; import { useAlert } from "../context/AlertContext"; import { useAuth } from "../context/AuthContext"; import Forbidden from "../components/Forbidden"; import { warehouseLocationListOptions, type WarehouseLocation, } from "../lib/queries/warehouse"; import { useApiMutation } from "../lib/queries/mutations"; import { Button, Card, DataTable, Modal, ConfirmDialog, Field, TextField, StatusChip, PageHeader, PageEnter, EmptyState, LoadingState, type DataColumn, } from "../ui"; const API_BASE = "/api/admin/warehouse/locations"; interface LocationForm { code: string; name: string; description: string; } const PlusIcon = ( ); const EditIcon = ( ); const DeleteIcon = ( ); export default function WarehouseLocations() { const alert = useAlert(); const { hasPermission } = useAuth(); const { data: locations = [], isPending } = useQuery( warehouseLocationListOptions({ active: "all" }), ); const [showModal, setShowModal] = useState(false); const [editingLocation, setEditingLocation] = useState(null); const [form, setForm] = useState({ code: "", name: "", description: "", }); const [errors, setErrors] = useState>({}); const [deleteConfirm, setDeleteConfirm] = useState<{ show: boolean; location: WarehouseLocation | null; }>({ show: false, location: null }); // useApiMutation JSON.stringifies the whole TIn as the request body, so // TIn must match the backend schema (CreateLocationSchema / UpdateLocationSchema) // directly — NOT a wrapper that nests the body. id is captured in the URL closure. const createLocationMutation = useApiMutation< LocationForm, { message?: string; error?: string } >({ url: () => API_BASE, method: () => "POST", invalidate: ["warehouse"], onSuccess: (data) => { setShowModal(false); alert.success(data?.message || "Umístění bylo vytvořeno"); }, }); const updateLocationMutation = useApiMutation< { id: number; code?: string; name?: string; description?: string }, { message?: string; error?: string } >({ url: ({ id }) => `${API_BASE}/${id}`, method: () => "PUT", invalidate: ["warehouse"], onSuccess: (data) => { setShowModal(false); alert.success(data?.message || "Umístění bylo aktualizováno"); }, }); const deleteMutation = useApiMutation< { id: number }, { message?: string; error?: string } >({ url: ({ id }) => `${API_BASE}/${id}`, method: () => "DELETE", invalidate: ["warehouse"], onSuccess: (data) => { setDeleteConfirm({ show: false, location: null }); alert.success(data?.message || "Umístění bylo smazáno"); }, }); const toggleMutation = useApiMutation< { id: number; is_active: boolean }, { message?: string; error?: string } >({ url: ({ id }) => `${API_BASE}/${id}`, method: () => "PUT", invalidate: ["warehouse"], }); if (!hasPermission("warehouse.manage")) return ; const openCreateModal = () => { setEditingLocation(null); setForm({ code: "", name: "", description: "", }); setErrors({}); setShowModal(true); }; const openEditModal = (location: WarehouseLocation) => { setEditingLocation(location); setForm({ code: location.code, name: location.name, description: location.description || "", }); setErrors({}); setShowModal(true); }; const handleSubmit = async () => { const newErrors: Record = {}; if (!form.code.trim()) newErrors.code = "Zadejte kód umístění"; if (!form.name.trim()) newErrors.name = "Zadejte název umístění"; setErrors(newErrors); if (Object.keys(newErrors).length > 0) return; try { if (editingLocation) { await updateLocationMutation.mutateAsync({ id: editingLocation.id, code: form.code, name: form.name, description: form.description, }); } else { await createLocationMutation.mutateAsync({ code: form.code, name: form.name, description: form.description, }); } } catch (e) { alert.error(e instanceof Error ? e.message : "Chyba připojení"); } }; const handleDelete = async () => { if (!deleteConfirm.location) return; try { await deleteMutation.mutateAsync({ id: deleteConfirm.location.id }); } catch (e) { alert.error(e instanceof Error ? e.message : "Chyba připojení"); } }; const toggleActive = async (location: WarehouseLocation) => { try { await toggleMutation.mutateAsync({ id: location.id, is_active: !location.is_active, }); alert.success( location.is_active ? "Umístění bylo deaktivováno" : "Umístění bylo aktivováno", ); } catch (e) { alert.error(e instanceof Error ? e.message : "Chyba připojení"); } }; if (isPending) { return ; } const total = locations.length; const subtitle = `${total} ${ total === 1 ? "umístění" : total >= 2 && total <= 4 ? "umístění" : "umístění" }`; const columns: DataColumn[] = [ { key: "code", header: "Kód", width: "15%", mono: true, bold: true, render: (l) => l.code, }, { key: "name", header: "Název", width: "25%", render: (l) => l.name, }, { key: "description", header: "Popis", width: "35%", render: (l) => l.description || "—", }, { key: "status", header: "Stav", width: "15%", render: (l) => ( toggleActive(l)} /> ), }, { key: "actions", header: "Akce", width: "10%", align: "right", render: (l) => ( openEditModal(l)} aria-label="Upravit" title="Upravit" > {EditIcon} setDeleteConfirm({ show: true, location: l })} aria-label="Smazat" title="Smazat" > {DeleteIcon} ), }, ]; return ( Přidat umístění } /> columns={columns} rows={locations} rowKey={(l) => l.id} rowInactive={(l) => !l.is_active} empty={ Přidat první umístění } /> } /> {/* Add/Edit Modal */} setShowModal(false)} onSubmit={handleSubmit} title={editingLocation ? "Upravit umístění" : "Přidat umístění"} loading={ createLocationMutation.isPending || updateLocationMutation.isPending } > { setForm({ ...form, code: e.target.value.toUpperCase() }); setErrors((prev) => ({ ...prev, code: "" })); }} placeholder="A-01" /> { setForm({ ...form, name: e.target.value }); setErrors((prev) => ({ ...prev, name: "" })); }} placeholder="Regál A, polička 1" /> setForm({ ...form, description: e.target.value })} placeholder="Volitelný popis umístění" /> {/* Delete Confirmation */} setDeleteConfirm({ show: false, location: null })} onConfirm={handleDelete} title="Smazat umístění" message={ deleteConfirm.location ? `Opravdu chcete smazat umístění "${deleteConfirm.location.code} - ${deleteConfirm.location.name}"? Pokud umístění obsahuje zásoby, nelze jej smazat.` : "" } confirmText="Smazat" confirmVariant="danger" loading={deleteMutation.isPending} /> ); }