feat(ui): honey batch page with one main action, traceability errors, confirmed removal

- DetailHeader: "Abfüllung anlegen" as the main action, PDF export in
  "Mehr", state as icon + word, retry and way back on load errors.
- Traceability says what it shows and offers a retry on errors instead
  of "Lädt…" forever.
- Removing a container allocation asks first and says what changes.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
Nick SzaunigandClaude Opus 5.5 committed 2026-10-01 18:37:37 +02:00
1 parent 86f9291ee3
commit c3dc596170
2 files changed
+69 -56

No files matched your search

@@ -1,7 +1,6 @@
"use client"; "use client";
import { FormEvent, useState } from "react"; import { FormEvent, useState } from "react";
import { Trash2 } from "lucide-react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { toast } from "sonner"; import { toast } from "sonner";
import { import {
@@ -15,6 +14,7 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { EmptyState } from "@/components/empty-state"; import { EmptyState } from "@/components/empty-state";
import { ConfirmDialog } from "@/components/confirm-dialog";
import { Boxes } from "lucide-react"; import { Boxes } from "lucide-react";
import { import {
Select, Select,
@@ -132,16 +132,16 @@ export function HoneyBatchContainerAllocationsCard({ honeyBatch }: { honeyBatch:
</p> </p>
<p className="text-muted-foreground">{allocation.amount} kg</p> <p className="text-muted-foreground">{allocation.amount} kg</p>
</div> </div>
<Button <ConfirmDialog
type="button" triggerLabel="Entfernen"
variant="ghost" triggerVariant="ghost"
size="icon" triggerDisabled={removeMutation.isPending}
disabled={removeMutation.isPending} title="Zuweisung entfernen?"
onClick={() => removeMutation.mutate(allocation.storageContainerId)} description={`${allocation.amount} kg dieser Charge gelten danach nicht mehr als in Behälter ${allocation.storageContainer.code} gelagert. Der Honig selbst bleibt in der Charge.`}
aria-label="Zuweisung entfernen" confirmLabel="Entfernen"
> isPending={removeMutation.isPending}
<Trash2 className="size-4" /> onConfirm={() => removeMutation.mutate(allocation.storageContainerId)}
</Button> />
</div> </div>
))} ))}
@@ -2,7 +2,7 @@
import Link from "next/link"; import Link from "next/link";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
import { Droplet, FlaskConical } from "lucide-react"; import { Droplet, FileText, FlaskConical } from "lucide-react";
import { useMutation, useQuery } from "@tanstack/react-query"; import { useMutation, useQuery } from "@tanstack/react-query";
import { toast } from "sonner"; import { toast } from "sonner";
import { fetchHoneyBatch, fetchHoneyBatchTraceability } from "@/lib/honey-batches/api"; import { fetchHoneyBatch, fetchHoneyBatchTraceability } from "@/lib/honey-batches/api";
@@ -11,9 +11,11 @@ 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 { ErrorState, LoadingState } from "@/components/query-state";
import { DetailHeader, DetailQueryFallback, DetailStatus } from "@/components/detail-header";
import { errorMessage } from "@/lib/forms/error-message";
import { HoneyBatchStorageCard } from "./honey-batch-storage-card"; import { HoneyBatchStorageCard } from "./honey-batch-storage-card";
import { HoneyBatchContainerAllocationsCard } from "./honey-batch-container-allocations-card"; import { HoneyBatchContainerAllocationsCard } from "./honey-batch-container-allocations-card";
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";
import { AddMeasurementDialog } from "./add-measurement-dialog"; import { AddMeasurementDialog } from "./add-measurement-dialog";
@@ -21,50 +23,68 @@ import { EditMeasurementDialog } from "./edit-measurement-dialog";
import { ExtractionEditDialog } from "./extraction-edit-dialog"; import { ExtractionEditDialog } from "./extraction-edit-dialog";
import { HoneyBatchEditDialog } from "./honey-batch-edit-dialog"; import { HoneyBatchEditDialog } from "./honey-batch-edit-dialog";
const BACK = { backHref: "/honey-batches", backLabel: "Alle Honigchargen" };
export default function HoneyBatchDetailPage() { export default function HoneyBatchDetailPage() {
const params = useParams<{ id: string }>(); const params = useParams<{ id: string }>();
const { data: honeyBatch, isLoading, isError } = useQuery({ const honeyBatchQuery = useQuery({
queryKey: ["honey-batches", params.id], queryKey: ["honey-batches", params.id],
queryFn: () => fetchHoneyBatch(params.id), queryFn: () => fetchHoneyBatch(params.id),
}); });
const { data: traceability } = useQuery({ const honeyBatch = honeyBatchQuery.data;
const traceabilityQuery = useQuery({
queryKey: ["honey-batches", params.id, "traceability"], queryKey: ["honey-batches", params.id, "traceability"],
queryFn: () => fetchHoneyBatchTraceability(params.id), queryFn: () => fetchHoneyBatchTraceability(params.id),
}); });
const traceability = traceabilityQuery.data;
const exportPdfMutation = useMutation({ const exportPdfMutation = useMutation({
mutationFn: () => exportHoneyBatchTraceabilityPdf(params.id), mutationFn: () => exportHoneyBatchTraceabilityPdf(params.id),
onError: () => toast.error("PDF-Bericht konnte nicht exportiert werden"), onError: (error) =>
toast.error(errorMessage(error, "PDF-Bericht konnte nicht exportiert werden")),
}); });
if (isLoading) { if (honeyBatchQuery.isLoading || honeyBatchQuery.isError || !honeyBatch) {
return <p className="p-4 text-sm text-muted-foreground sm:p-6">Lädt…</p>; return <DetailQueryFallback query={honeyBatchQuery} what="Daten der Honigcharge" {...BACK} />;
}
if (isError || !honeyBatch) {
return (
<p role="alert" className="p-4 text-sm text-destructive sm:p-6">
Honigcharge konnte nicht geladen werden.
</p>
);
} }
return ( return (
<main className="flex flex-col gap-4 p-4 sm:p-6"> <main className="flex flex-col gap-4 p-4 sm:p-6">
<DetailHeader
icon={Droplet}
title={honeyBatch.batchNumber}
subtitle={honeyBatch.honeyType ?? undefined}
status={
<DetailStatus
status={honeyBatch.status}
label={HONEY_BATCH_STATUS_LABELS[honeyBatch.status]}
/>
}
primaryAction={
<Button
size="touch"
nativeButton={false}
render={<Link href={`/bottling/new?honeyBatchId=${honeyBatch.id}`} />}
>
Abfüllung anlegen
</Button>
}
moreActions={[
{
label: "Rückverfolgung als PDF",
icon: FileText,
onSelect: () => exportPdfMutation.mutate(),
disabled: exportPdfMutation.isPending,
},
]}
{...BACK}
/>
<div className="grid grid-cols-1 items-start gap-4 lg:grid-cols-3"> <div className="grid grid-cols-1 items-start gap-4 lg:grid-cols-3">
<div className="flex flex-col gap-4 lg:col-span-1"> <div className="flex flex-col gap-4 lg:col-span-1">
<Card> <Card>
<CardHeader className="flex flex-row items-center justify-between"> <CardHeader className="flex flex-row flex-wrap items-center justify-between gap-2">
<div className="flex items-center gap-2.5"> <CardTitle>Angaben</CardTitle>
<span <div className="flex flex-wrap items-center gap-2">
className="flex size-8 items-center justify-center rounded-lg text-white shadow-[var(--shadow-soft)]"
style={{ background: "var(--gradient-honey)" }}
>
<Droplet className="size-4" />
</span>
<CardTitle className="font-mono">{honeyBatch.batchNumber}</CardTitle>
</div>
<div className="flex items-center gap-2">
<Badge>{HONEY_BATCH_STATUS_LABELS[honeyBatch.status]}</Badge>
<HoneyBatchEditDialog honeyBatch={honeyBatch} /> <HoneyBatchEditDialog honeyBatch={honeyBatch} />
<QrCodeDialog entityType="honey-batches" id={honeyBatch.id} /> <QrCodeDialog entityType="honey-batches" id={honeyBatch.id} />
</div> </div>
@@ -75,15 +95,6 @@ export default function HoneyBatchDetailPage() {
{honeyBatch.waterContent !== null ? ( {honeyBatch.waterContent !== null ? (
<p>Wassergehalt: {honeyBatch.waterContent}%</p> <p>Wassergehalt: {honeyBatch.waterContent}%</p>
) : null} ) : null}
<Button
variant="outline"
size="sm"
className="w-fit"
nativeButton={false}
render={<Link href={`/bottling/new?honeyBatchId=${honeyBatch.id}`} />}
>
Neue Abfüllung
</Button>
</CardContent> </CardContent>
</Card> </Card>
<HoneyBatchStorageCard honeyBatch={honeyBatch} /> <HoneyBatchStorageCard honeyBatch={honeyBatch} />
@@ -133,17 +144,19 @@ export default function HoneyBatchDetailPage() {
<Card> <Card>
<CardHeader className="flex flex-row items-center justify-between"> <CardHeader className="flex flex-row items-center justify-between">
<CardTitle>Rückverfolgbarkeit</CardTitle> <CardTitle>Rückverfolgbarkeit</CardTitle>
<Button
variant="outline"
size="sm"
disabled={exportPdfMutation.isPending}
onClick={() => exportPdfMutation.mutate()}
>
PDF-Bericht
</Button>
</CardHeader> </CardHeader>
<CardContent className="flex flex-col gap-3 text-sm"> <CardContent className="flex flex-col gap-3 text-sm">
{!traceability ? <p className="text-muted-foreground">Lädt…</p> : null} <p className="text-muted-foreground">
Woher der Honig dieser Charge stammt: Schleuderung, Ernten und Völker.
</p>
{traceabilityQuery.isLoading ? <LoadingState what="Herkunftsangaben" /> : null}
{traceabilityQuery.isError ? (
<ErrorState
what="Herkunftsangaben"
onRetry={() => void traceabilityQuery.refetch()}
isRetrying={traceabilityQuery.isFetching}
/>
) : null}
{traceability ? ( {traceability ? (
<> <>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
@@ -162,7 +175,7 @@ export default function HoneyBatchDetailPage() {
<Link <Link
key={harvest.id} key={harvest.id}
href={`/harvests/${harvest.id}`} href={`/harvests/${harvest.id}`}
className="rounded-md border p-3 hover:bg-muted" className="min-h-11 rounded-md border p-3 hover:bg-muted"
> >
<p className="font-medium"> <p className="font-medium">
Ernte am {new Date(harvest.date).toLocaleDateString("de-DE")} Ernte am {new Date(harvest.date).toLocaleDateString("de-DE")}