import { useState, useEffect, useRef } from "react"; import { useQuery } from "@tanstack/react-query"; import { useParams, useNavigate, Link as RouterLink } from "react-router-dom"; import Box from "@mui/material/Box"; import Typography from "@mui/material/Typography"; import MenuItem from "@mui/material/MenuItem"; import { useAlert } from "../context/AlertContext"; import { useAuth } from "../context/AuthContext"; import Forbidden from "../components/Forbidden"; import useModalLock from "../hooks/useModalLock"; import { formatCurrency, formatDate } from "../utils/formatters"; import { warehouseItemDetailOptions, warehouseCategoryListOptions, type WarehouseItem, } from "../lib/queries/warehouse"; import { useApiMutation } from "../lib/queries/mutations"; import { Button, Card, DataTable, Field, TextField, Select, StatCard, EmptyState, LoadingState, PageHeader, ConfirmDialog, PageEnter, headerActionsSx, type DataColumn, } from "../ui"; interface Batch { id: number; quantity: number; original_qty: number; unit_price: number; received_at: string | null; is_consumed: boolean; } interface ItemLocation { id: number; quantity: number; location: { id: number; code: string; name: string; } | null; } interface ItemDetail extends WarehouseItem { batches?: Batch[]; item_locations?: ItemLocation[]; } const API_BASE = "/api/admin/warehouse/items"; // Must match UnitEnum in src/schemas/warehouse.schema.ts — the server rejects // any other value (CreateItemSchema/UpdateItemSchema), so free text here would // make the save 400. const UNIT_OPTIONS = ["ks", "m", "kg", "bal", "sada", "m2", "m3", "l"] as const; interface ItemForm { item_number: string; name: string; description: string; category_id: string; unit: string; min_quantity: string; notes: string; } const EditIcon = ( ); const BackIcon = ( ); export default function WarehouseItemDetail() { const { id } = useParams(); const alert = useAlert(); const { hasPermission } = useAuth(); const navigate = useNavigate(); const [editing, setEditing] = useState(false); const [form, setForm] = useState({ item_number: "", name: "", description: "", category_id: "", unit: "ks", min_quantity: "", notes: "", }); const [errors, setErrors] = useState>({}); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const isNew = id === "new"; const itemQuery = useQuery(warehouseItemDetailOptions(id, !!id && !isNew)); const item = itemQuery.data as ItemDetail | undefined; const isPending = itemQuery.isPending; const { data: categories = [] } = useQuery(warehouseCategoryListOptions()); const formInitialized = useRef(false); useEffect(() => { formInitialized.current = false; }, [id]); useEffect(() => { if (item && !formInitialized.current) { setForm({ item_number: item.item_number || "", name: item.name, description: item.description || "", category_id: item.category_id ? String(item.category_id) : "", unit: item.unit, min_quantity: item.min_quantity != null ? String(item.min_quantity) : "", notes: item.notes || "", }); formInitialized.current = true; } }, [item]); useEffect(() => { if (itemQuery.error && !isNew) { alert.error("Položka nenalezena"); navigate("/warehouse/items"); } }, [itemQuery.error]); // eslint-disable-line react-hooks/exhaustive-deps useModalLock(editing); const saveMutation = useApiMutation< Record, { id?: number; message?: string } >({ url: () => (isNew ? API_BASE : `${API_BASE}/${id}`), method: () => (isNew ? "POST" : "PUT"), invalidate: ["warehouse"], onSuccess: (data) => { setEditing(false); alert.success(data?.message || "Položka byla uložena"); if (isNew && data?.id) { navigate(`/warehouse/items/${data.id}`, { replace: true }); } }, }); const deleteMutation = useApiMutation({ url: () => `${API_BASE}/${id ?? ""}`, method: () => "DELETE", invalidate: ["warehouse"], onSuccess: () => { navigate("/warehouse/items"); setTimeout(() => alert.success("Položka byla deaktivována"), 300); }, }); // All hooks above this line — permission gate is the last thing before render. if (!hasPermission("warehouse.view")) return ; const canManage = hasPermission("warehouse.manage"); const updateForm = (field: keyof ItemForm, value: string | boolean) => setForm((prev) => ({ ...prev, [field]: value })); const handleSave = async () => { const newErrors: Record = {}; if (!form.name.trim()) newErrors.name = "Zadejte název položky"; if (!form.unit.trim()) newErrors.unit = "Zadejte jednotku"; setErrors(newErrors); if (Object.keys(newErrors).length > 0) return; const payload: Record = { item_number: form.item_number.trim() || null, name: form.name.trim(), description: form.description.trim() || null, category_id: form.category_id ? Number(form.category_id) : null, unit: form.unit.trim(), min_quantity: form.min_quantity ? Number(form.min_quantity) : null, notes: form.notes.trim() || null, }; try { await saveMutation.mutateAsync(payload); } catch (e) { alert.error(e instanceof Error ? e.message : "Chyba připojení"); } }; const handleDelete = async () => { if (!item) return; try { await deleteMutation.mutateAsync(undefined); } catch (e) { alert.error(e instanceof Error ? e.message : "Chyba připojení"); } }; const saving = saveMutation.isPending; if (isPending && !isNew) { return ; } const batches: Batch[] = item?.batches ?? []; const locations: ItemLocation[] = item?.item_locations ?? []; const title = isNew ? "Nová položka" : item ? item.name : "Detail položky"; const subtitle = item?.item_number || undefined; // Edit / view mode action buttons const headerActions = canManage ? ( {editing || isNew ? ( <> {!isNew && ( )} ) : ( <> {item && ( )} )} ) : undefined; // Batch table columns const batchColumns: DataColumn[] = [ { key: "received_at", header: "Datum příjmu", width: "22%", mono: true, render: (b) => formatDate(b.received_at), }, { key: "original_qty", header: "Původní množství", width: "20%", mono: true, render: (b) => String(Number(b.original_qty)), }, { key: "quantity", header: "Zůstatek", width: "18%", mono: true, bold: true, render: (b) => String(Number(b.quantity)), }, { key: "unit_price", header: "Cena za jednotku", width: "20%", mono: true, render: (b) => formatCurrency(Number(b.unit_price), "CZK"), }, { key: "value", header: "Hodnota", width: "20%", mono: true, render: (b) => formatCurrency(Number(b.quantity) * Number(b.unit_price), "CZK"), }, ]; // Location table columns const locationColumns: DataColumn[] = [ { key: "code", header: "Kód", width: "30%", mono: true, bold: true, render: (l) => l.location?.code || "—", }, { key: "name", header: "Název", width: "50%", render: (l) => l.location?.name || "—", }, { key: "quantity", header: "Množství", width: "20%", mono: true, bold: true, render: (l) => String(Number(l.quantity)), }, ]; return ( {/* Header */} {headerActions} } /> {/* Basic info */} Základní údaje {editing || isNew ? ( { updateForm("item_number", e.target.value); setErrors((prev) => ({ ...prev, item_number: "" })); }} placeholder="Např. SKL-001" fullWidth /> { updateForm("name", e.target.value); setErrors((prev) => ({ ...prev, name: "" })); }} placeholder="Název položky" fullWidth /> updateForm("description", e.target.value)} placeholder="Volitelný popis položky" fullWidth />