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
/>
updateForm("min_quantity", e.target.value)}
placeholder="0"
fullWidth
/>
updateForm("notes", e.target.value)}
placeholder="Volitelné poznámky"
fullWidth
/>
) : (
{/* Číslo položky */}
Číslo položky
{item?.item_number || "—"}
{/* Název */}
Název
{item?.name || "—"}
{/* Popis */}
Popis
{item?.description || "—"}
{/* Kategorie */}
Kategorie
{item?.category?.name || "—"}
{/* Jednotka */}
Jednotka
{item?.unit || "—"}
{/* Minimální množství */}
Minimální množství
{item?.min_quantity != null ? item.min_quantity : "—"}
{/* Poznámky */}
{item?.notes && (
Poznámky
{item.notes}
)}
)}
{/* Stock info — only in view mode, for existing items */}
{id !== "new" && !editing && item && (
)}
{/* Batches table — only in view mode, for existing items */}
{id !== "new" && !editing && (
Dávky (FIFO)
columns={batchColumns}
rows={batches}
rowKey={(b) => b.id}
empty={}
/>
)}
{/* Locations table — only in view mode, for existing items */}
{id !== "new" && !editing && (
Umístění
columns={locationColumns}
rows={locations}
rowKey={(l) => l.id}
empty={}
/>
)}
{/* Delete confirmation */}
setShowDeleteConfirm(false)}
onConfirm={handleDelete}
title="Smazat položku"
message={`Opravdu chcete smazat položku „${item?.name ?? ""}"?`}
confirmText="Smazat"
confirmVariant="danger"
loading={deleteMutation.isPending}
/>
);
}