diff --git a/ApiCore-Frontend/src/app/(app)/equipment/[id]/page.tsx b/ApiCore-Frontend/src/app/(app)/equipment/[id]/page.tsx
new file mode 100644
index 0000000..c18bf8c
--- /dev/null
+++ b/ApiCore-Frontend/src/app/(app)/equipment/[id]/page.tsx
@@ -0,0 +1,94 @@
+"use client";
+
+import { useParams } from "next/navigation";
+import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
+import { fetchEquipment, updateEquipment } from "@/lib/equipment/api";
+import { EquipmentCondition } from "@/lib/equipment/types";
+import { statusTone } from "@/lib/status-tone";
+import { Badge } from "@/components/ui/badge";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+
+const CONDITIONS: EquipmentCondition[] = ["NEW", "GOOD", "WORN", "DEFECTIVE"];
+
+export default function EquipmentDetailPage() {
+ const params = useParams<{ id: string }>();
+ const queryClient = useQueryClient();
+ const { data: equipment, isLoading, isError } = useQuery({
+ queryKey: ["equipment", params.id],
+ queryFn: () => fetchEquipment(params.id),
+ });
+
+ const updateMutation = useMutation({
+ mutationFn: (condition: EquipmentCondition) =>
+ updateEquipment(params.id, { condition }),
+ onSuccess: () => {
+ void queryClient.invalidateQueries({ queryKey: ["equipment", params.id] });
+ void queryClient.invalidateQueries({ queryKey: ["equipment"] });
+ },
+ });
+
+ if (isLoading) {
+ return
Lädt…
;
+ }
+ if (isError || !equipment) {
+ return (
+
+ Gegenstand konnte nicht geladen werden.
+
+ );
+ }
+
+ return (
+
+
+
+ {equipment.name}
+ {equipment.condition}
+
+
+
+ Inventarnummer: {equipment.inventoryNumber}
+
+ {equipment.manufacturer ? Hersteller: {equipment.manufacturer}
: null}
+ {equipment.location ? Standort: {equipment.location}
: null}
+ {equipment.purchaseDate ? (
+ Kaufdatum: {new Date(equipment.purchaseDate).toLocaleDateString("de-DE")}
+ ) : null}
+ {equipment.maintenanceDate ? (
+
+ Wartungsdatum:{" "}
+ {new Date(equipment.maintenanceDate).toLocaleDateString("de-DE")}
+
+ ) : null}
+
+ Zustand ändern
+
+
+
+
+
+ );
+}
diff --git a/ApiCore-Frontend/src/app/(app)/equipment/new/page.tsx b/ApiCore-Frontend/src/app/(app)/equipment/new/page.tsx
new file mode 100644
index 0000000..aab866b
--- /dev/null
+++ b/ApiCore-Frontend/src/app/(app)/equipment/new/page.tsx
@@ -0,0 +1,122 @@
+"use client";
+
+import { FormEvent, useState } from "react";
+import { useRouter } from "next/navigation";
+import { useMutation, useQueryClient } from "@tanstack/react-query";
+import { createEquipment } from "@/lib/equipment/api";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+
+export default function NewEquipmentPage() {
+ const router = useRouter();
+ const queryClient = useQueryClient();
+ const [inventoryNumber, setInventoryNumber] = useState("");
+ const [name, setName] = useState("");
+ const [manufacturer, setManufacturer] = useState("");
+ const [purchaseDate, setPurchaseDate] = useState("");
+ const [location, setLocation] = useState("");
+ const [maintenanceDate, setMaintenanceDate] = useState("");
+ const [error, setError] = useState(null);
+
+ const mutation = useMutation({
+ mutationFn: createEquipment,
+ onSuccess: (equipment) => {
+ void queryClient.invalidateQueries({ queryKey: ["equipment"] });
+ router.push(`/equipment/${equipment.id}`);
+ },
+ onError: () => {
+ setError("Gegenstand konnte nicht angelegt werden.");
+ },
+ });
+
+ function handleSubmit(event: FormEvent) {
+ event.preventDefault();
+ setError(null);
+ mutation.mutate({
+ inventoryNumber,
+ name,
+ manufacturer: manufacturer || undefined,
+ purchaseDate: purchaseDate ? new Date(purchaseDate).toISOString() : undefined,
+ location: location || undefined,
+ maintenanceDate: maintenanceDate
+ ? new Date(maintenanceDate).toISOString()
+ : undefined,
+ });
+ }
+
+ return (
+
+
+
+ Neuer Inventargegenstand
+
+
+
+
+
+
+ );
+}
diff --git a/ApiCore-Frontend/src/app/(app)/equipment/page.tsx b/ApiCore-Frontend/src/app/(app)/equipment/page.tsx
new file mode 100644
index 0000000..1b56fe5
--- /dev/null
+++ b/ApiCore-Frontend/src/app/(app)/equipment/page.tsx
@@ -0,0 +1,73 @@
+"use client";
+
+import Link from "next/link";
+import { useQuery } from "@tanstack/react-query";
+import { fetchEquipmentList } from "@/lib/equipment/api";
+import { statusTone } from "@/lib/status-tone";
+import { Badge } from "@/components/ui/badge";
+import { Button } from "@/components/ui/button";
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/components/ui/table";
+
+export default function EquipmentPage() {
+ const { data: equipmentList, isLoading, isError } = useQuery({
+ queryKey: ["equipment"],
+ queryFn: () => fetchEquipmentList(),
+ });
+
+ return (
+
+
+
Inventar
+ }>
+ + Neuer Gegenstand
+
+
+
+ {isLoading ? Lädt…
: null}
+ {isError ? (
+
+ Inventar konnte nicht geladen werden.
+
+ ) : null}
+
+ {equipmentList ? (
+
+
+
+ Inventarnummer
+ Bezeichnung
+ Standort
+ Zustand
+
+
+
+ {equipmentList.map((item) => (
+
+
+
+ {item.inventoryNumber}
+
+
+ {item.name}
+ {item.location ?? "—"}
+
+ {item.condition}
+
+
+ ))}
+
+
+ ) : null}
+
+ );
+}
diff --git a/ApiCore-Frontend/src/app/(app)/layout.tsx b/ApiCore-Frontend/src/app/(app)/layout.tsx
index 4a5f4a3..e0a0578 100644
--- a/ApiCore-Frontend/src/app/(app)/layout.tsx
+++ b/ApiCore-Frontend/src/app/(app)/layout.tsx
@@ -32,6 +32,7 @@ const PRIMARY_LINKS = [
const MORE_LINKS = [
{ label: "Produkte", href: "/products" },
{ label: "Abfüllungen", href: "/bottling/new" },
+ { label: "Inventar", href: "/equipment" },
];
const ADMIN_LINKS = [
diff --git a/ApiCore-Frontend/src/lib/equipment/api.ts b/ApiCore-Frontend/src/lib/equipment/api.ts
new file mode 100644
index 0000000..255f51f
--- /dev/null
+++ b/ApiCore-Frontend/src/lib/equipment/api.ts
@@ -0,0 +1,35 @@
+import { apiFetch } from "@/lib/auth/api-client";
+import {
+ CreateEquipmentInput,
+ Equipment,
+ EquipmentFilter,
+ UpdateEquipmentInput,
+} from "./types";
+
+export function fetchEquipmentList(filter: EquipmentFilter = {}): Promise {
+ const params = new URLSearchParams();
+ if (filter.condition) params.set("condition", filter.condition);
+ const query = params.toString();
+ return apiFetch(`/equipment${query ? `?${query}` : ""}`);
+}
+
+export function fetchEquipment(id: string): Promise {
+ return apiFetch(`/equipment/${id}`);
+}
+
+export function createEquipment(input: CreateEquipmentInput): Promise {
+ return apiFetch("/equipment", {
+ method: "POST",
+ body: JSON.stringify(input),
+ });
+}
+
+export function updateEquipment(
+ id: string,
+ input: UpdateEquipmentInput,
+): Promise {
+ return apiFetch(`/equipment/${id}`, {
+ method: "PATCH",
+ body: JSON.stringify(input),
+ });
+}
diff --git a/ApiCore-Frontend/src/lib/equipment/types.ts b/ApiCore-Frontend/src/lib/equipment/types.ts
new file mode 100644
index 0000000..2c4edf9
--- /dev/null
+++ b/ApiCore-Frontend/src/lib/equipment/types.ts
@@ -0,0 +1,31 @@
+export type EquipmentCondition = "NEW" | "GOOD" | "WORN" | "DEFECTIVE";
+
+export interface Equipment {
+ id: string;
+ inventoryNumber: string;
+ name: string;
+ manufacturer: string | null;
+ purchaseDate: string | null;
+ condition: EquipmentCondition;
+ location: string | null;
+ maintenanceDate: string | null;
+ createdAt: string;
+ updatedAt: string;
+}
+
+export interface CreateEquipmentInput {
+ inventoryNumber: string;
+ name: string;
+ manufacturer?: string;
+ purchaseDate?: string;
+ location?: string;
+ maintenanceDate?: string;
+}
+
+export interface UpdateEquipmentInput extends Partial {
+ condition?: EquipmentCondition;
+}
+
+export interface EquipmentFilter {
+ condition?: EquipmentCondition;
+}
diff --git a/ApiCore-Frontend/src/lib/status-tone.ts b/ApiCore-Frontend/src/lib/status-tone.ts
index 433d8f4..406b1b5 100644
--- a/ApiCore-Frontend/src/lib/status-tone.ts
+++ b/ApiCore-Frontend/src/lib/status-tone.ts
@@ -5,16 +5,19 @@ const WARNING_STATUSES = new Set([
"CONSPICUOUS",
"QUEENLESS",
"LOW_STOCK",
+ "WORN",
]);
-const DESTRUCTIVE_STATUSES = new Set(["DISSOLVED", "DECEASED", "SOLD"]);
+const DESTRUCTIVE_STATUSES = new Set(["DISSOLVED", "DECEASED", "SOLD", "DEFECTIVE"]);
const NEUTRAL_STATUSES = new Set(["MERGED", "CONSUMED"]);
+const POSITIVE_STATUSES = new Set(["ACTIVE", "NEW", "GOOD"]);
+
export function statusTone(status: string): StatusTone {
if (WARNING_STATUSES.has(status)) return "warning";
if (DESTRUCTIVE_STATUSES.has(status)) return "destructive";
if (NEUTRAL_STATUSES.has(status)) return "outline";
- if (status === "ACTIVE") return "default";
+ if (POSITIVE_STATUSES.has(status)) return "default";
return "secondary";
}
diff --git a/TASKS.md b/TASKS.md
index 8afcdc1..9723d8b 100644
--- a/TASKS.md
+++ b/TASKS.md
@@ -196,7 +196,7 @@ Reihenfolge = Bau-Reihenfolge. Gleiche Haken-Regel wie MVP: erst setzen, wenn Li
- Endpunkte: `GET/POST /api/v1/equipment`, `GET/PATCH /api/v1/equipment/:id`
- Frontend: `/equipment`, `/equipment/new`, `/equipment/:id`
- [x] Backend-Modul fertig, getestet, grün, committet
-- [ ] Frontend-Views fertig, getestet, grün, committet
+- [x] Frontend-Views fertig, getestet, grün, committet
### 25. Kalender