"use client"; import { useCallback, useEffect, useMemo, useState } from "react"; import Link from "next/link"; import { Edit, Eye, PackagePlus, Plus, RotateCcw, Trash2, Wrench } from "lucide-react"; import ConfirmDialog from "@/components/common/ConfirmDialog"; import DataTable, { type DataTableColumn } from "@/components/common/DataTable"; import EmptyState from "@/components/common/EmptyState"; import SearchInput from "@/components/common/SearchInput"; import { useToast } from "@/components/common/ToastProvider"; import InventoryItemDialog from "@/components/inventory/InventoryItemDialog"; import MasterDataSection from "@/components/inventory/MasterDataSection"; import StockActionDialog, { type StockAction } from "@/components/inventory/StockActionDialog"; import { formatMoney, getErrorMessage } from "@/components/inventory/inventory-utils"; import { Button, buttonVariants } from "@/components/ui/button"; import { api } from "@/lib/api"; import { hasPermission } from "@/lib/permissions"; import type { InventoryCategory, InventoryItem, InventoryItemListResponse, InventoryItemPayload, InventoryLocation, InventoryStockPayload, InventorySupplier, } from "@/types/inventory"; import type { CurrentUser } from "@/types/rbac"; const pageSize = 10; export default function InventoryPage() { const { showToast } = useToast(); const [items, setItems] = useState([]); const [categories, setCategories] = useState([]); const [locations, setLocations] = useState([]); const [suppliers, setSuppliers] = useState([]); const [currentUser, setCurrentUser] = useState(null); const [total, setTotal] = useState(0); const [loading, setLoading] = useState(true); const [error, setError] = useState(""); const [formError, setFormError] = useState(""); const [stockError, setStockError] = useState(""); const [deleteError, setDeleteError] = useState(""); const [pending, setPending] = useState(false); const [tab, setTab] = useState<"items" | "categories" | "locations" | "suppliers">("items"); const [search, setSearch] = useState(""); const [categoryFilter, setCategoryFilter] = useState("all"); const [locationFilter, setLocationFilter] = useState("all"); const [supplierFilter, setSupplierFilter] = useState("all"); const [stockFilter, setStockFilter] = useState<"all" | "low">("all"); const [activeFilter, setActiveFilter] = useState<"true" | "false" | "all">("true"); const [page, setPage] = useState(1); const [sortKey, setSortKey] = useState("name"); const [sortDirection, setSortDirection] = useState<"asc" | "desc">("asc"); const [formOpen, setFormOpen] = useState(false); const [stockOpen, setStockOpen] = useState(false); const [stockAction, setStockAction] = useState("adjust"); const [selectedItem, setSelectedItem] = useState(null); const [deleteItem, setDeleteItem] = useState(null); const loadData = useCallback(async () => { setLoading(true); setError(""); try { const params = new URLSearchParams(); params.set("limit", String(pageSize)); params.set("offset", String((page - 1) * pageSize)); if (search.trim()) params.set("q", search.trim()); if (categoryFilter !== "all") params.set("category_id", categoryFilter); if (locationFilter !== "all") params.set("location_id", locationFilter); if (supplierFilter !== "all") params.set("supplier_id", supplierFilter); if (stockFilter === "low") params.set("low_stock", "true"); if (activeFilter !== "all") params.set("active", activeFilter); const [itemResponse, categoryResponse, locationResponse, supplierResponse, meResponse] = await Promise.all([ api.get(`/inventory/items?${params.toString()}`), api.get("/inventory/categories"), api.get("/inventory/locations"), api.get("/inventory/suppliers"), api.get("/me"), ]); setItems(itemResponse.data.items); setTotal(itemResponse.data.total); setCategories(categoryResponse.data); setLocations(locationResponse.data); setSuppliers(supplierResponse.data); setCurrentUser(meResponse.data); } catch (err) { setError(getErrorMessage(err)); } finally { setLoading(false); } }, [activeFilter, categoryFilter, locationFilter, page, search, stockFilter, supplierFilter]); useEffect(() => { queueMicrotask(() => { void loadData(); }); }, [loadData]); const canCreate = hasPermission(currentUser, "inventory.create"); const canUpdate = hasPermission(currentUser, "inventory.update"); const canDelete = hasPermission(currentUser, "inventory.delete"); const canAdjust = hasPermission(currentUser, "inventory.stock.adjust"); const canReserve = hasPermission(currentUser, "inventory.stock.reserve"); const canConsume = hasPermission(currentUser, "inventory.stock.consume"); const canManageMasterdata = hasPermission(currentUser, "inventory.manage.masterdata"); const pageCount = Math.max(1, Math.ceil(total / pageSize)); const currentPage = Math.min(page, pageCount); const sortedItems = useMemo(() => [...items].sort((first, second) => { const firstValue = String(first[sortKey as keyof InventoryItem] ?? "").toLowerCase(); const secondValue = String(second[sortKey as keyof InventoryItem] ?? "").toLowerCase(); if (firstValue === secondValue) return 0; const result = firstValue > secondValue ? 1 : -1; return sortDirection === "asc" ? result : -result; }), [items, sortDirection, sortKey]); const columns: DataTableColumn[] = [ { key: "sku", label: "SKU", sortable: true, render: (item) => {item.sku} }, { key: "name", label: "Name", sortable: true, render: (item) => item.name }, { key: "category", label: "Kategorie", render: (item) => item.category?.name ?? "Keine" }, { key: "manufacturer", label: "Hersteller", sortable: true, render: (item) => item.manufacturer ?? "Keine Angabe" }, { key: "quantity_on_hand", label: "Bestand", sortable: true, render: (item) => item.quantity_on_hand }, { key: "quantity_reserved", label: "Reserviert", sortable: true, render: (item) => item.quantity_reserved }, { key: "quantity_available", label: "Verfügbar", sortable: true, render: (item) => {item.quantity_available} }, { key: "reorder_level", label: "Mindestbestand", sortable: true, render: (item) => item.reorder_level }, { key: "location", label: "Lagerort", render: (item) => item.location?.name ?? "Kein Lagerort" }, { key: "selling_price_cents", label: "VK", sortable: true, render: (item) => formatMoney(item.selling_price_cents, item.currency) }, { key: "is_active", label: "Status", sortable: true, render: (item) => item.is_active ? "Aktiv" : "Inaktiv" }, { key: "actions", label: "Aktionen", className: "px-4 py-3 text-right", render: (item) => (
{canUpdate && } {canAdjust && } {canReserve && } {canConsume && } {canDelete && }
), }, ]; function openStock(item: InventoryItem, action: StockAction) { setSelectedItem(item); setStockAction(action); setStockError(""); setStockOpen(true); } function handleSort(key: string) { if (sortKey === key) { setSortDirection((current) => (current === "asc" ? "desc" : "asc")); return; } setSortKey(key); setSortDirection("asc"); } async function saveItem(payload: InventoryItemPayload) { setPending(true); setFormError(""); try { if (selectedItem) { await api.put(`/inventory/items/${selectedItem.id}`, payload); showToast({ type: "success", title: "Artikel aktualisiert", description: selectedItem.sku }); } else { const response = await api.post("/inventory/items", payload); showToast({ type: "success", title: "Artikel angelegt", description: response.data.sku }); } setFormOpen(false); await loadData(); } catch (err) { const message = getErrorMessage(err); setFormError(message); showToast({ type: "error", title: "Artikel konnte nicht gespeichert werden", description: message }); } finally { setPending(false); } } async function saveStock(payload: InventoryStockPayload) { if (!selectedItem) return; setPending(true); setStockError(""); try { await api.post(`/inventory/items/${selectedItem.id}/stock/${stockAction}`, payload); showToast({ type: "success", title: "Bestand gebucht", description: selectedItem.sku }); setStockOpen(false); await loadData(); } catch (err) { const message = getErrorMessage(err); setStockError(message); showToast({ type: "error", title: "Bestand konnte nicht gebucht werden", description: message }); } finally { setPending(false); } } async function confirmDelete() { if (!deleteItem) return; setPending(true); setDeleteError(""); try { await api.delete(`/inventory/items/${deleteItem.id}`); showToast({ type: "success", title: "Artikel deaktiviert", description: deleteItem.sku }); setDeleteItem(null); await loadData(); } catch (err) { const message = getErrorMessage(err); setDeleteError(message); showToast({ type: "error", title: "Artikel konnte nicht deaktiviert werden", description: message }); } finally { setPending(false); } } return (

Lager / Ersatzteile

{total} Artikel im aktuellen Filter

{canCreate && }
{(["items", "categories", "locations", "suppliers"] as const).map((value) => ( ))}
{tab === "items" ? ( <>
{ setSearch(value); setPage(1); }} placeholder="Artikel suchen" />
{!loading && !error && total === 0 && !search ? ( setFormOpen(true)}>Artikel anlegen : undefined} /> ) : ( item.id} sortKey={sortKey} sortDirection={sortDirection} loading={loading} error={error} emptyTitle="Keine Artikel gefunden." emptyDescription="Passe Suche oder Filter an." onSort={handleSort} /> )}
Seite {currentPage} von {pageCount}
) : (
{tab === "categories" && void loadData()} />} {tab === "locations" && void loadData()} />} {tab === "suppliers" && void loadData()} />}
)} !open && setDeleteItem(null)} onConfirm={confirmDelete} >

{deleteItem?.sku} · {deleteItem?.name}

{deleteError &&

{deleteError}

}
); }