feat(frontend): Lagerbehälter-Sektion und Umlagerungshistorie (Modul 32)
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.
This commit is contained in:
1 parent
59a320ae36
commit
15f5ab57ae
4 files changed
+199
No files matched your search
@@ -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<string | null>(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<HTMLFormElement>) {
|
||||||
|
event.preventDefault();
|
||||||
|
setError(null);
|
||||||
|
if (!storageContainerId) {
|
||||||
|
setError("Bitte einen Lagerbehälter auswählen.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mutation.mutate({ storageContainerId, notes: notes || undefined });
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="max-w-lg">
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between">
|
||||||
|
<CardTitle>Lagerbehälter</CardTitle>
|
||||||
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
|
<DialogTrigger render={<Button size="sm">Umlagern</Button>} />
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Honigcharge umlagern</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<form className="flex flex-col gap-4" onSubmit={handleSubmit}>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Label htmlFor="relocate-container">Ziel-Lagerbehälter</Label>
|
||||||
|
<Select
|
||||||
|
value={storageContainerId}
|
||||||
|
onValueChange={(value) => setStorageContainerId(value ?? "")}
|
||||||
|
>
|
||||||
|
<SelectTrigger id="relocate-container">
|
||||||
|
<SelectValue placeholder="Lagerbehälter wählen" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{containers?.map((container) => (
|
||||||
|
<SelectItem key={container.id} value={container.id}>
|
||||||
|
{container.code}
|
||||||
|
{container.location ? ` · ${container.location}` : ""}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Label htmlFor="relocate-notes">Notizen</Label>
|
||||||
|
<Input
|
||||||
|
id="relocate-notes"
|
||||||
|
value={notes}
|
||||||
|
onChange={(event) => setNotes(event.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{error ? (
|
||||||
|
<p role="alert" className="text-sm text-destructive">
|
||||||
|
{error}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
<DialogFooter>
|
||||||
|
<Button type="submit" disabled={mutation.isPending}>
|
||||||
|
{mutation.isPending ? "Wird umgelagert…" : "Umlagern"}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="flex flex-col gap-3 text-sm">
|
||||||
|
<p>
|
||||||
|
Aktuell:{" "}
|
||||||
|
{honeyBatch.storageContainer
|
||||||
|
? `${honeyBatch.storageContainer.code}${
|
||||||
|
honeyBatch.storageContainer.location
|
||||||
|
? ` · ${honeyBatch.storageContainer.location}`
|
||||||
|
: ""
|
||||||
|
}`
|
||||||
|
: "Kein Lagerbehälter zugewiesen"}
|
||||||
|
</p>
|
||||||
|
{isLoading ? <p className="text-muted-foreground">Lädt…</p> : null}
|
||||||
|
{!isLoading && (!relocations || relocations.length === 0) ? (
|
||||||
|
<EmptyState
|
||||||
|
icon={History}
|
||||||
|
title="Noch keine Umlagerung"
|
||||||
|
description="Ordne die Charge einem Lagerbehälter zu, um die Historie zu starten."
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
{relocations?.map((relocation) => (
|
||||||
|
<div key={relocation.id} className="rounded-md border p-3">
|
||||||
|
<p className="font-medium">
|
||||||
|
{new Date(relocation.relocatedAt).toLocaleDateString("de-DE")} ·{" "}
|
||||||
|
{relocation.fromStorageContainer ? relocation.fromStorageContainer.code : "—"} →{" "}
|
||||||
|
{relocation.toStorageContainer.code}
|
||||||
|
</p>
|
||||||
|
{relocation.notes ? <p className="text-muted-foreground">{relocation.notes}</p> : null}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ import { exportHoneyBatchTraceabilityPdf } from "@/lib/export/api";
|
|||||||
import { QrCodeDialog } from "@/components/qr-code-dialog";
|
import { QrCodeDialog } from "@/components/qr-code-dialog";
|
||||||
import { DocumentUploadCard } from "@/components/document-upload-card";
|
import { DocumentUploadCard } from "@/components/document-upload-card";
|
||||||
import { EmptyState } from "@/components/empty-state";
|
import { EmptyState } from "@/components/empty-state";
|
||||||
|
import { HoneyBatchStorageCard } from "./honey-batch-storage-card";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
@@ -136,6 +137,7 @@ export default function HoneyBatchDetailPage() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
<HoneyBatchStorageCard honeyBatch={honeyBatch} />
|
||||||
<DocumentUploadCard entityType="HONEY_BATCH" entityId={honeyBatch.id} />
|
<DocumentUploadCard entityType="HONEY_BATCH" entityId={honeyBatch.id} />
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ import {
|
|||||||
CreateHoneyBatchInput,
|
CreateHoneyBatchInput,
|
||||||
CreateHoneyMeasurementInput,
|
CreateHoneyMeasurementInput,
|
||||||
HoneyBatch,
|
HoneyBatch,
|
||||||
|
HoneyBatchRelocation,
|
||||||
HoneyBatchStatus,
|
HoneyBatchStatus,
|
||||||
|
RelocateHoneyBatchInput,
|
||||||
Traceability,
|
Traceability,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
@@ -42,3 +44,17 @@ export function addHoneyMeasurement(
|
|||||||
body: JSON.stringify(input),
|
body: JSON.stringify(input),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function relocateHoneyBatch(
|
||||||
|
id: string,
|
||||||
|
input: RelocateHoneyBatchInput,
|
||||||
|
): Promise<HoneyBatch> {
|
||||||
|
return apiFetch<HoneyBatch>(`/honey-batches/${id}/relocate`, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(input),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchHoneyBatchRelocations(id: string): Promise<HoneyBatchRelocation[]> {
|
||||||
|
return apiFetch<HoneyBatchRelocation[]>(`/honey-batches/${id}/relocations`);
|
||||||
|
}
|
||||||
@@ -17,6 +17,12 @@ export interface HoneyMeasurement {
|
|||||||
notes: string | null;
|
notes: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface StorageContainerRef {
|
||||||
|
id: string;
|
||||||
|
code: string;
|
||||||
|
location: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
export interface HoneyBatch {
|
export interface HoneyBatch {
|
||||||
id: string;
|
id: string;
|
||||||
batchNumber: string;
|
batchNumber: string;
|
||||||
@@ -25,11 +31,27 @@ export interface HoneyBatch {
|
|||||||
amount: number | null;
|
amount: number | null;
|
||||||
waterContent: number | null;
|
waterContent: number | null;
|
||||||
storageContainerId: string | null;
|
storageContainerId: string | null;
|
||||||
|
storageContainer: StorageContainerRef | null;
|
||||||
status: HoneyBatchStatus;
|
status: HoneyBatchStatus;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
measurements: HoneyMeasurement[];
|
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 {
|
export interface Traceability {
|
||||||
honeyBatch: {
|
honeyBatch: {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
Reference in new issue
Block a user