feat(inventory): add spare parts management

This commit is contained in:
Schubert Ferenc 2026-07-05 11:32:06 +02:00
parent abc6e308dd
commit fb5c2cc26a
43 changed files with 2950 additions and 2 deletions

View file

@ -0,0 +1,33 @@
import { NextRequest } from "next/server";
import { proxyHermesRequest } from "@/lib/server/hermes-proxy";
import { assertSameOrigin } from "@/lib/server/request-guards";
type Params = {
params: Promise<{ id: string }>;
};
async function proxyCategoryRequest(request: NextRequest, { params }: Params) {
const { id } = await params;
return proxyHermesRequest(request, `/inventory/categories/${id}`);
}
export async function PUT(request: NextRequest, context: Params) {
const originError = assertSameOrigin(request);
if (originError) {
return originError;
}
return proxyCategoryRequest(request, context);
}
export async function DELETE(request: NextRequest, context: Params) {
const originError = assertSameOrigin(request);
if (originError) {
return originError;
}
return proxyCategoryRequest(request, context);
}

View file

@ -0,0 +1,18 @@
import { NextRequest } from "next/server";
import { proxyHermesRequest } from "@/lib/server/hermes-proxy";
import { assertSameOrigin } from "@/lib/server/request-guards";
export async function GET(request: NextRequest) {
return proxyHermesRequest(request, "/inventory/categories");
}
export async function POST(request: NextRequest) {
const originError = assertSameOrigin(request);
if (originError) {
return originError;
}
return proxyHermesRequest(request, "/inventory/categories");
}

View file

@ -0,0 +1,12 @@
import { NextRequest } from "next/server";
import { proxyHermesRequest } from "@/lib/server/hermes-proxy";
type Params = {
params: Promise<{ id: string }>;
};
export async function GET(request: NextRequest, { params }: Params) {
const { id } = await params;
return proxyHermesRequest(request, `/inventory/items/${id}/movements`);
}

View file

@ -0,0 +1,37 @@
import { NextRequest } from "next/server";
import { proxyHermesRequest } from "@/lib/server/hermes-proxy";
import { assertSameOrigin } from "@/lib/server/request-guards";
type Params = {
params: Promise<{ id: string }>;
};
async function proxyItemRequest(request: NextRequest, { params }: Params) {
const { id } = await params;
return proxyHermesRequest(request, `/inventory/items/${id}`);
}
export async function GET(request: NextRequest, context: Params) {
return proxyItemRequest(request, context);
}
export async function PUT(request: NextRequest, context: Params) {
const originError = assertSameOrigin(request);
if (originError) {
return originError;
}
return proxyItemRequest(request, context);
}
export async function DELETE(request: NextRequest, context: Params) {
const originError = assertSameOrigin(request);
if (originError) {
return originError;
}
return proxyItemRequest(request, context);
}

View file

@ -0,0 +1,19 @@
import { NextRequest } from "next/server";
import { proxyHermesRequest } from "@/lib/server/hermes-proxy";
import { assertSameOrigin } from "@/lib/server/request-guards";
type Params = {
params: Promise<{ id: string }>;
};
export async function POST(request: NextRequest, { params }: Params) {
const originError = assertSameOrigin(request);
if (originError) {
return originError;
}
const { id } = await params;
return proxyHermesRequest(request, `/inventory/items/${id}/stock/adjust`);
}

View file

@ -0,0 +1,19 @@
import { NextRequest } from "next/server";
import { proxyHermesRequest } from "@/lib/server/hermes-proxy";
import { assertSameOrigin } from "@/lib/server/request-guards";
type Params = {
params: Promise<{ id: string }>;
};
export async function POST(request: NextRequest, { params }: Params) {
const originError = assertSameOrigin(request);
if (originError) {
return originError;
}
const { id } = await params;
return proxyHermesRequest(request, `/inventory/items/${id}/stock/consume`);
}

View file

@ -0,0 +1,19 @@
import { NextRequest } from "next/server";
import { proxyHermesRequest } from "@/lib/server/hermes-proxy";
import { assertSameOrigin } from "@/lib/server/request-guards";
type Params = {
params: Promise<{ id: string }>;
};
export async function POST(request: NextRequest, { params }: Params) {
const originError = assertSameOrigin(request);
if (originError) {
return originError;
}
const { id } = await params;
return proxyHermesRequest(request, `/inventory/items/${id}/stock/release`);
}

View file

@ -0,0 +1,19 @@
import { NextRequest } from "next/server";
import { proxyHermesRequest } from "@/lib/server/hermes-proxy";
import { assertSameOrigin } from "@/lib/server/request-guards";
type Params = {
params: Promise<{ id: string }>;
};
export async function POST(request: NextRequest, { params }: Params) {
const originError = assertSameOrigin(request);
if (originError) {
return originError;
}
const { id } = await params;
return proxyHermesRequest(request, `/inventory/items/${id}/stock/reserve`);
}

View file

@ -0,0 +1,18 @@
import { NextRequest } from "next/server";
import { proxyHermesRequest } from "@/lib/server/hermes-proxy";
import { assertSameOrigin } from "@/lib/server/request-guards";
export async function GET(request: NextRequest) {
return proxyHermesRequest(request, `/inventory/items${request.nextUrl.search}`);
}
export async function POST(request: NextRequest) {
const originError = assertSameOrigin(request);
if (originError) {
return originError;
}
return proxyHermesRequest(request, "/inventory/items");
}

View file

@ -0,0 +1,33 @@
import { NextRequest } from "next/server";
import { proxyHermesRequest } from "@/lib/server/hermes-proxy";
import { assertSameOrigin } from "@/lib/server/request-guards";
type Params = {
params: Promise<{ id: string }>;
};
async function proxyLocationRequest(request: NextRequest, { params }: Params) {
const { id } = await params;
return proxyHermesRequest(request, `/inventory/locations/${id}`);
}
export async function PUT(request: NextRequest, context: Params) {
const originError = assertSameOrigin(request);
if (originError) {
return originError;
}
return proxyLocationRequest(request, context);
}
export async function DELETE(request: NextRequest, context: Params) {
const originError = assertSameOrigin(request);
if (originError) {
return originError;
}
return proxyLocationRequest(request, context);
}

View file

@ -0,0 +1,18 @@
import { NextRequest } from "next/server";
import { proxyHermesRequest } from "@/lib/server/hermes-proxy";
import { assertSameOrigin } from "@/lib/server/request-guards";
export async function GET(request: NextRequest) {
return proxyHermesRequest(request, "/inventory/locations");
}
export async function POST(request: NextRequest) {
const originError = assertSameOrigin(request);
if (originError) {
return originError;
}
return proxyHermesRequest(request, "/inventory/locations");
}

View file

@ -0,0 +1,33 @@
import { NextRequest } from "next/server";
import { proxyHermesRequest } from "@/lib/server/hermes-proxy";
import { assertSameOrigin } from "@/lib/server/request-guards";
type Params = {
params: Promise<{ id: string }>;
};
async function proxySupplierRequest(request: NextRequest, { params }: Params) {
const { id } = await params;
return proxyHermesRequest(request, `/inventory/suppliers/${id}`);
}
export async function PUT(request: NextRequest, context: Params) {
const originError = assertSameOrigin(request);
if (originError) {
return originError;
}
return proxySupplierRequest(request, context);
}
export async function DELETE(request: NextRequest, context: Params) {
const originError = assertSameOrigin(request);
if (originError) {
return originError;
}
return proxySupplierRequest(request, context);
}

View file

@ -0,0 +1,18 @@
import { NextRequest } from "next/server";
import { proxyHermesRequest } from "@/lib/server/hermes-proxy";
import { assertSameOrigin } from "@/lib/server/request-guards";
export async function GET(request: NextRequest) {
return proxyHermesRequest(request, "/inventory/suppliers");
}
export async function POST(request: NextRequest) {
const originError = assertSameOrigin(request);
if (originError) {
return originError;
}
return proxyHermesRequest(request, "/inventory/suppliers");
}

View file

@ -62,7 +62,7 @@ export default function DashboardPage() {
return <div className="rounded-lg border bg-white p-8 text-red-600">{error || "Keine Dashboarddaten verfügbar"}</div>;
}
const cards = [...summary.customers, ...summary.repairs, ...summary.users, ...summary.roles];
const cards = [...summary.customers, ...summary.repairs, ...summary.inventory, ...summary.users, ...summary.roles];
return (
<div className="space-y-6">

View file

@ -0,0 +1,153 @@
"use client";
import { use, useCallback, useEffect, useState } from "react";
import Link from "next/link";
import { formatDate, formatMoney, getErrorMessage } from "@/components/inventory/inventory-utils";
import { Button } from "@/components/ui/button";
import { api } from "@/lib/api";
import type { InventoryItem, InventoryStockMovement } from "@/types/inventory";
type PageProps = {
params: Promise<{
id: string;
}>;
};
const movementLabels: Record<string, string> = {
initial: "Anfangsbestand",
purchase: "Einkauf",
adjustment: "Anpassung",
reservation: "Reservierung",
release: "Freigabe",
consumption: "Verbrauch",
return: "Rückgabe",
correction: "Korrektur",
};
export default function InventoryItemDetailPage({ params }: PageProps) {
const { id } = use(params);
const [item, setItem] = useState<InventoryItem | null>(null);
const [movements, setMovements] = useState<InventoryStockMovement[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
const loadItem = useCallback(async () => {
setLoading(true);
setError("");
try {
const [itemResponse, movementResponse] = await Promise.all([
api.get<InventoryItem>(`/inventory/items/${id}`),
api.get<InventoryStockMovement[]>(`/inventory/items/${id}/movements`),
]);
setItem(itemResponse.data);
setMovements(movementResponse.data);
} catch (err) {
setError(getErrorMessage(err));
} finally {
setLoading(false);
}
}, [id]);
useEffect(() => {
queueMicrotask(() => {
void loadItem();
});
}, [loadItem]);
if (loading) {
return <div className="rounded-lg border bg-white p-8 text-slate-500">Artikel wird geladen...</div>;
}
if (error || !item) {
return <div className="rounded-lg border bg-white p-8 text-red-600">{error || "Artikel nicht gefunden"}</div>;
}
return (
<div className="space-y-6">
<div className="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
<div>
<p className="text-sm font-medium text-slate-500">{item.sku}</p>
<h1 className="text-3xl font-bold text-slate-950">{item.name}</h1>
<p className="mt-1 text-sm text-slate-500">{item.category?.name ?? "Keine Kategorie"} · {item.is_active ? "Aktiv" : "Inaktiv"}</p>
</div>
<Link href="/inventory"><Button type="button" variant="outline">Zurück zum Lager</Button></Link>
</div>
<div className="grid gap-4 xl:grid-cols-3">
<section className="rounded-lg border bg-white p-5 xl:col-span-2">
<h2 className="font-semibold text-slate-950">Stammdaten</h2>
<dl className="mt-4 grid gap-4 md:grid-cols-2">
<Info label="Beschreibung" value={item.description ?? "Keine Angabe"} />
<Info label="Hersteller" value={item.manufacturer ?? "Keine Angabe"} />
<Info label="Hersteller-Teilenummer" value={item.manufacturer_part_number ?? "Keine Angabe"} />
<Info label="Lieferanten-Teilenummer" value={item.supplier_part_number ?? "Keine Angabe"} />
<Info label="Lieferant" value={item.supplier?.name ?? "Kein Lieferant"} />
<Info label="Lagerort" value={item.location?.name ?? "Kein Lagerort"} />
<Info label="Kompatible Geräte" value={item.compatible_devices ?? "Keine Angabe"} />
<Info label="Notizen" value={item.notes ?? "Keine Notizen"} />
</dl>
</section>
<section className="rounded-lg border bg-white p-5">
<h2 className="font-semibold text-slate-950">Bestand</h2>
<dl className="mt-4 grid gap-3">
<Metric label="Bestand" value={`${item.quantity_on_hand} ${item.unit}`} />
<Metric label="Reserviert" value={`${item.quantity_reserved} ${item.unit}`} />
<Metric label="Verfügbar" value={`${item.quantity_available} ${item.unit}`} />
<Metric label="Mindestbestand" value={`${item.reorder_level} ${item.unit}`} />
</dl>
</section>
</div>
<div className="grid gap-4 xl:grid-cols-3">
<section className="rounded-lg border bg-white p-5">
<h2 className="font-semibold text-slate-950">Preise</h2>
<dl className="mt-4 grid gap-3">
<Metric label="Einkaufspreis" value={formatMoney(item.purchase_price_cents, item.currency)} />
<Metric label="Verkaufspreis" value={formatMoney(item.selling_price_cents, item.currency)} />
<Metric label="MwSt." value={`${item.tax_rate_percent} %`} />
</dl>
</section>
<section className="rounded-lg border bg-white p-5 xl:col-span-2">
<h2 className="font-semibold text-slate-950">Bewegungen</h2>
{movements.length === 0 ? (
<p className="mt-4 text-sm text-slate-500">Noch keine Bewegungen vorhanden.</p>
) : (
<div className="mt-4 divide-y">
{movements.map((movement) => (
<div key={movement.id} className="py-3">
<div className="flex flex-col gap-1 sm:flex-row sm:items-center sm:justify-between">
<p className="font-medium text-slate-950">{movementLabels[movement.movement_type] ?? movement.movement_type} · {movement.quantity}</p>
<time className="text-xs text-slate-500" dateTime={movement.created_at}>{formatDate(movement.created_at)}</time>
</div>
<p className="mt-1 text-sm text-slate-500">{movement.reason}{movement.note ? ` · ${movement.note}` : ""}</p>
{movement.actor_username && <p className="mt-1 text-xs text-slate-400">Gebucht von {movement.actor_username}</p>}
</div>
))}
</div>
)}
</section>
</div>
</div>
);
}
function Info({ label, value }: { label: string; value: string }) {
return (
<div>
<dt className="text-xs font-semibold uppercase text-slate-500">{label}</dt>
<dd className="mt-1 whitespace-pre-wrap text-sm text-slate-950">{value}</dd>
</div>
);
}
function Metric({ label, value }: { label: string; value: string }) {
return (
<div className="rounded-lg border bg-slate-50 p-3">
<dt className="text-xs font-semibold uppercase text-slate-500">{label}</dt>
<dd className="mt-1 text-lg font-semibold text-slate-950">{value}</dd>
</div>
);
}

View file

@ -0,0 +1,322 @@
"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<InventoryItem[]>([]);
const [categories, setCategories] = useState<InventoryCategory[]>([]);
const [locations, setLocations] = useState<InventoryLocation[]>([]);
const [suppliers, setSuppliers] = useState<InventorySupplier[]>([]);
const [currentUser, setCurrentUser] = useState<CurrentUser | null>(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<StockAction>("adjust");
const [selectedItem, setSelectedItem] = useState<InventoryItem | null>(null);
const [deleteItem, setDeleteItem] = useState<InventoryItem | null>(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<InventoryItemListResponse>(`/inventory/items?${params.toString()}`),
api.get<InventoryCategory[]>("/inventory/categories"),
api.get<InventoryLocation[]>("/inventory/locations"),
api.get<InventorySupplier[]>("/inventory/suppliers"),
api.get<CurrentUser>("/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<InventoryItem>[] = [
{ key: "sku", label: "SKU", sortable: true, render: (item) => <span className="font-medium text-slate-950">{item.sku}</span> },
{ 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) => <span className={item.quantity_available <= item.reorder_level ? "font-semibold text-amber-700" : ""}>{item.quantity_available}</span> },
{ 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) => (
<div className="flex justify-end gap-2">
<Link href={`/inventory/items/${item.id}`} className={buttonVariants({ variant: "ghost", size: "icon-sm" })} title="Details"><Eye size={16} /></Link>
{canUpdate && <Button type="button" variant="ghost" size="icon-sm" title="Bearbeiten" onClick={() => { setSelectedItem(item); setFormError(""); setFormOpen(true); }}><Edit size={16} /></Button>}
{canAdjust && <Button type="button" variant="ghost" size="icon-sm" title="Bestand anpassen" onClick={() => openStock(item, "adjust")}><Wrench size={16} /></Button>}
{canReserve && <Button type="button" variant="ghost" size="icon-sm" title="Reservieren" onClick={() => openStock(item, "reserve")}><PackagePlus size={16} /></Button>}
{canConsume && <Button type="button" variant="ghost" size="icon-sm" title="Verbrauch buchen" onClick={() => openStock(item, "consume")}><RotateCcw size={16} /></Button>}
{canDelete && <Button type="button" variant="destructive" size="icon-sm" title="Deaktivieren" onClick={() => { setDeleteItem(item); setDeleteError(""); }}><Trash2 size={16} /></Button>}
</div>
),
},
];
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<InventoryItem>(`/inventory/items/${selectedItem.id}`, payload);
showToast({ type: "success", title: "Artikel aktualisiert", description: selectedItem.sku });
} else {
const response = await api.post<InventoryItem>("/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<InventoryItem>(`/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 (
<div className="space-y-6">
<div className="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
<div>
<h1 className="text-3xl font-bold text-slate-950">Lager / Ersatzteile</h1>
<p className="mt-1 text-sm text-slate-500">{total} Artikel im aktuellen Filter</p>
</div>
{canCreate && <Button type="button" onClick={() => { setSelectedItem(null); setFormError(""); setFormOpen(true); }}><Plus size={16} />Artikel anlegen</Button>}
</div>
<div className="flex flex-wrap gap-2">
{(["items", "categories", "locations", "suppliers"] as const).map((value) => (
<Button key={value} type="button" variant={tab === value ? "default" : "outline"} onClick={() => setTab(value)}>
{{ items: "Artikel", categories: "Kategorien", locations: "Lagerorte", suppliers: "Lieferanten" }[value]}
</Button>
))}
</div>
{tab === "items" ? (
<>
<div className="flex flex-col gap-3 rounded-lg border bg-white p-4 xl:flex-row xl:items-center">
<SearchInput value={search} onChange={(value) => { setSearch(value); setPage(1); }} placeholder="Artikel suchen" />
<select value={categoryFilter} onChange={(event) => { setCategoryFilter(event.target.value); setPage(1); }} className="h-8 rounded-lg border px-2 text-sm">
<option value="all">Alle Kategorien</option>
{categories.map((category) => <option key={category.id} value={category.id}>{category.name}</option>)}
</select>
<select value={locationFilter} onChange={(event) => { setLocationFilter(event.target.value); setPage(1); }} className="h-8 rounded-lg border px-2 text-sm">
<option value="all">Alle Lagerorte</option>
{locations.map((location) => <option key={location.id} value={location.id}>{location.name}</option>)}
</select>
<select value={supplierFilter} onChange={(event) => { setSupplierFilter(event.target.value); setPage(1); }} className="h-8 rounded-lg border px-2 text-sm">
<option value="all">Alle Lieferanten</option>
{suppliers.map((supplier) => <option key={supplier.id} value={supplier.id}>{supplier.name}</option>)}
</select>
<select value={stockFilter} onChange={(event) => { setStockFilter(event.target.value as "all" | "low"); setPage(1); }} className="h-8 rounded-lg border px-2 text-sm">
<option value="all">Alle Bestände</option>
<option value="low">Niedriger Bestand</option>
</select>
<select value={activeFilter} onChange={(event) => { setActiveFilter(event.target.value as "true" | "false" | "all"); setPage(1); }} className="h-8 rounded-lg border px-2 text-sm">
<option value="true">Aktiv</option>
<option value="false">Inaktiv</option>
<option value="all">Alle</option>
</select>
</div>
{!loading && !error && total === 0 && !search ? (
<EmptyState
title="Noch keine Ersatzteile vorhanden."
description="Lege den ersten Artikel an, um Bestand, Preise und Lieferanten zu pflegen."
action={canCreate ? <Button type="button" onClick={() => setFormOpen(true)}><Plus size={16} />Artikel anlegen</Button> : undefined}
/>
) : (
<DataTable
columns={columns}
rows={sortedItems}
rowKey={(item) => item.id}
sortKey={sortKey}
sortDirection={sortDirection}
loading={loading}
error={error}
emptyTitle="Keine Artikel gefunden."
emptyDescription="Passe Suche oder Filter an."
onSort={handleSort}
/>
)}
<div className="flex items-center justify-between text-sm text-slate-600">
<span>Seite {currentPage} von {pageCount}</span>
<div className="flex gap-2">
<Button type="button" variant="outline" disabled={currentPage === 1} onClick={() => setPage((current) => Math.max(1, current - 1))}>Zurück</Button>
<Button type="button" variant="outline" disabled={currentPage === pageCount} onClick={() => setPage((current) => Math.min(pageCount, current + 1))}>Weiter</Button>
</div>
</div>
</>
) : (
<div className="grid gap-4 xl:grid-cols-2">
{tab === "categories" && <MasterDataSection title="Kategorien" kind="categories" entries={categories} canManage={canManageMasterdata} onChanged={() => void loadData()} />}
{tab === "locations" && <MasterDataSection title="Lagerorte" kind="locations" entries={locations} canManage={canManageMasterdata} onChanged={() => void loadData()} />}
{tab === "suppliers" && <MasterDataSection title="Lieferanten" kind="suppliers" entries={suppliers} canManage={canManageMasterdata} onChanged={() => void loadData()} />}
</div>
)}
<InventoryItemDialog key={`item-${selectedItem?.id ?? "new"}-${formOpen}`} open={formOpen} item={selectedItem} categories={categories} locations={locations} suppliers={suppliers} pending={pending} serverError={formError} onOpenChange={setFormOpen} onSubmit={saveItem} />
<StockActionDialog key={`stock-${selectedItem?.id ?? "none"}-${stockAction}-${stockOpen}`} open={stockOpen} item={selectedItem} action={stockAction} pending={pending} serverError={stockError} onOpenChange={setStockOpen} onSubmit={saveStock} />
<ConfirmDialog
open={Boolean(deleteItem)}
title="Artikel deaktivieren"
description="Artikel mit Bewegungen werden nicht hart gelöscht, sondern deaktiviert."
confirmLabel="Deaktivieren"
pending={pending}
onOpenChange={(open) => !open && setDeleteItem(null)}
onConfirm={confirmDelete}
>
<div className="space-y-2">
<p>{deleteItem?.sku} · {deleteItem?.name}</p>
{deleteError && <p className="text-sm text-red-600">{deleteError}</p>}
</div>
</ConfirmDialog>
</div>
);
}