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}
/>
);
}