From 15f5ab57ae461c35862b081db61aa3612f73f446 Mon Sep 17 00:00:00 2001 From: Nick Szaunig Date: Mon, 24 Aug 2026 17:06:54 +0200 Subject: [PATCH] =?UTF-8?q?feat(frontend):=20Lagerbeh=C3=A4lter-Sektion=20?= =?UTF-8?q?und=20Umlagerungshistorie=20(Modul=2032)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Neue HoneyBatchStorageCard-Komponente auf der Honigcharge-Detailseite: zeigt den aktuell zugeordneten Lagerbehälter, einen "Umlagern"-Dialog (Ziel-Lagerbehälter wählen, optionale Notiz) und die vollständige Umlagerungshistorie (von/nach, Zeitpunkt, Notiz) im etablierten EmptyState-Stil. HoneyBatch-Typ um storageContainer erweitert, neue HoneyBatchRelocation/RelocateHoneyBatchInput-Typen und passende API-Funktionen ergänzt. Live gegen das Backend verifiziert: Umlagerung zwischen zwei echten Lagerbehältern aktualisiert die Anzeige und die Historie korrekt, zero Konsolenfehler. --- .../[id]/honey-batch-storage-card.tsx | 159 ++++++++++++++++++ .../src/app/(app)/honey-batches/[id]/page.tsx | 2 + ApiCore-Frontend/src/lib/honey-batches/api.ts | 16 ++ .../src/lib/honey-batches/types.ts | 22 +++ 4 files changed, 199 insertions(+) create mode 100644 ApiCore-Frontend/src/app/(app)/honey-batches/[id]/honey-batch-storage-card.tsx diff --git a/ApiCore-Frontend/src/app/(app)/honey-batches/[id]/honey-batch-storage-card.tsx b/ApiCore-Frontend/src/app/(app)/honey-batches/[id]/honey-batch-storage-card.tsx new file mode 100644 index 0000000..6cbc769 --- /dev/null +++ b/ApiCore-Frontend/src/app/(app)/honey-batches/[id]/honey-batch-storage-card.tsx @@ -0,0 +1,159 @@ +"use client"; + +import { FormEvent, useState } from "react"; +import { History } from "lucide-react"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { fetchHoneyBatchRelocations, relocateHoneyBatch } from "@/lib/honey-batches/api"; +import { HoneyBatch } from "@/lib/honey-batches/types"; +import { fetchStorageContainers } from "@/lib/storage/api"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { EmptyState } from "@/components/empty-state"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; + +export function HoneyBatchStorageCard({ honeyBatch }: { honeyBatch: HoneyBatch }) { + const queryClient = useQueryClient(); + const [open, setOpen] = useState(false); + const [storageContainerId, setStorageContainerId] = useState(""); + const [notes, setNotes] = useState(""); + const [error, setError] = useState(null); + + const { data: containers } = useQuery({ + queryKey: ["storage-containers"], + queryFn: fetchStorageContainers, + }); + + const { data: relocations, isLoading } = useQuery({ + queryKey: ["honey-batches", honeyBatch.id, "relocations"], + queryFn: () => fetchHoneyBatchRelocations(honeyBatch.id), + }); + + const mutation = useMutation({ + mutationFn: (input: { storageContainerId: string; notes?: string }) => + relocateHoneyBatch(honeyBatch.id, input), + onSuccess: () => { + void queryClient.invalidateQueries({ queryKey: ["honey-batches", honeyBatch.id] }); + void queryClient.invalidateQueries({ + queryKey: ["honey-batches", honeyBatch.id, "relocations"], + }); + setOpen(false); + setStorageContainerId(""); + setNotes(""); + setError(null); + }, + onError: () => { + setError("Umlagerung konnte nicht gespeichert werden."); + }, + }); + + function handleSubmit(event: FormEvent) { + event.preventDefault(); + setError(null); + if (!storageContainerId) { + setError("Bitte einen Lagerbehälter auswählen."); + return; + } + mutation.mutate({ storageContainerId, notes: notes || undefined }); + } + + return ( + + + Lagerbehälter + + Umlagern} /> + + + Honigcharge umlagern + +
+
+ + +
+
+ + setNotes(event.target.value)} + /> +
+ {error ? ( +

+ {error} +

+ ) : null} + + + +
+
+
+
+ +

+ Aktuell:{" "} + {honeyBatch.storageContainer + ? `${honeyBatch.storageContainer.code}${ + honeyBatch.storageContainer.location + ? ` · ${honeyBatch.storageContainer.location}` + : "" + }` + : "Kein Lagerbehälter zugewiesen"} +

+ {isLoading ?

Lädt…

: null} + {!isLoading && (!relocations || relocations.length === 0) ? ( + + ) : null} + {relocations?.map((relocation) => ( +
+

+ {new Date(relocation.relocatedAt).toLocaleDateString("de-DE")} ·{" "} + {relocation.fromStorageContainer ? relocation.fromStorageContainer.code : "—"} →{" "} + {relocation.toStorageContainer.code} +

+ {relocation.notes ?

{relocation.notes}

: null} +
+ ))} +
+
+ ); +} diff --git a/ApiCore-Frontend/src/app/(app)/honey-batches/[id]/page.tsx b/ApiCore-Frontend/src/app/(app)/honey-batches/[id]/page.tsx index 23a0602..1f09260 100644 --- a/ApiCore-Frontend/src/app/(app)/honey-batches/[id]/page.tsx +++ b/ApiCore-Frontend/src/app/(app)/honey-batches/[id]/page.tsx @@ -9,6 +9,7 @@ import { exportHoneyBatchTraceabilityPdf } from "@/lib/export/api"; import { QrCodeDialog } from "@/components/qr-code-dialog"; import { DocumentUploadCard } from "@/components/document-upload-card"; import { EmptyState } from "@/components/empty-state"; +import { HoneyBatchStorageCard } from "./honey-batch-storage-card"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; @@ -136,6 +137,7 @@ export default function HoneyBatchDetailPage() { + ); diff --git a/ApiCore-Frontend/src/lib/honey-batches/api.ts b/ApiCore-Frontend/src/lib/honey-batches/api.ts index 7c64a6a..faf1cfa 100644 --- a/ApiCore-Frontend/src/lib/honey-batches/api.ts +++ b/ApiCore-Frontend/src/lib/honey-batches/api.ts @@ -3,7 +3,9 @@ import { CreateHoneyBatchInput, CreateHoneyMeasurementInput, HoneyBatch, + HoneyBatchRelocation, HoneyBatchStatus, + RelocateHoneyBatchInput, Traceability, } from "./types"; @@ -42,3 +44,17 @@ export function addHoneyMeasurement( body: JSON.stringify(input), }); } + +export function relocateHoneyBatch( + id: string, + input: RelocateHoneyBatchInput, +): Promise { + return apiFetch(`/honey-batches/${id}/relocate`, { + method: "POST", + body: JSON.stringify(input), + }); +} + +export function fetchHoneyBatchRelocations(id: string): Promise { + return apiFetch(`/honey-batches/${id}/relocations`); +} diff --git a/ApiCore-Frontend/src/lib/honey-batches/types.ts b/ApiCore-Frontend/src/lib/honey-batches/types.ts index 7cf6010..1bd9f88 100644 --- a/ApiCore-Frontend/src/lib/honey-batches/types.ts +++ b/ApiCore-Frontend/src/lib/honey-batches/types.ts @@ -17,6 +17,12 @@ export interface HoneyMeasurement { notes: string | null; } +export interface StorageContainerRef { + id: string; + code: string; + location: string | null; +} + export interface HoneyBatch { id: string; batchNumber: string; @@ -25,11 +31,27 @@ export interface HoneyBatch { amount: number | null; waterContent: number | null; storageContainerId: string | null; + storageContainer: StorageContainerRef | null; status: HoneyBatchStatus; createdAt: string; measurements: HoneyMeasurement[]; } +export interface HoneyBatchRelocation { + id: string; + fromStorageContainerId: string | null; + fromStorageContainer: StorageContainerRef | null; + toStorageContainerId: string; + toStorageContainer: StorageContainerRef; + notes: string | null; + relocatedAt: string; +} + +export interface RelocateHoneyBatchInput { + storageContainerId: string; + notes?: string; +} + export interface Traceability { honeyBatch: { id: string;