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:
1 parent
86f9291ee3
commit
c3dc596170
2 files changed
+69
-56
No files matched your search
+11
-11
@@ -1,7 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { FormEvent, useState } from "react";
|
||||
import { Trash2 } from "lucide-react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
@@ -15,6 +14,7 @@ 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 { ConfirmDialog } from "@/components/confirm-dialog";
|
||||
import { Boxes } from "lucide-react";
|
||||
import {
|
||||
Select,
|
||||
@@ -132,16 +132,16 @@ export function HoneyBatchContainerAllocationsCard({ honeyBatch }: { honeyBatch:
|
||||
</p>
|
||||
<p className="text-muted-foreground">{allocation.amount} kg</p>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
disabled={removeMutation.isPending}
|
||||
onClick={() => removeMutation.mutate(allocation.storageContainerId)}
|
||||
aria-label="Zuweisung entfernen"
|
||||
>
|
||||
<Trash2 className="size-4" />
|
||||
</Button>
|
||||
<ConfirmDialog
|
||||
triggerLabel="Entfernen"
|
||||
triggerVariant="ghost"
|
||||
triggerDisabled={removeMutation.isPending}
|
||||
title="Zuweisung entfernen?"
|
||||
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.`}
|
||||
confirmLabel="Entfernen"
|
||||
isPending={removeMutation.isPending}
|
||||
onConfirm={() => removeMutation.mutate(allocation.storageContainerId)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import Link from "next/link";
|
||||
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 { toast } from "sonner";
|
||||
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 { DocumentUploadCard } from "@/components/document-upload-card";
|
||||
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 { HoneyBatchContainerAllocationsCard } from "./honey-batch-container-allocations-card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { AddMeasurementDialog } from "./add-measurement-dialog";
|
||||
@@ -21,50 +23,68 @@ import { EditMeasurementDialog } from "./edit-measurement-dialog";
|
||||
import { ExtractionEditDialog } from "./extraction-edit-dialog";
|
||||
import { HoneyBatchEditDialog } from "./honey-batch-edit-dialog";
|
||||
|
||||
const BACK = { backHref: "/honey-batches", backLabel: "Alle Honigchargen" };
|
||||
|
||||
export default function HoneyBatchDetailPage() {
|
||||
const params = useParams<{ id: string }>();
|
||||
const { data: honeyBatch, isLoading, isError } = useQuery({
|
||||
const honeyBatchQuery = useQuery({
|
||||
queryKey: ["honey-batches", params.id],
|
||||
queryFn: () => fetchHoneyBatch(params.id),
|
||||
});
|
||||
const { data: traceability } = useQuery({
|
||||
const honeyBatch = honeyBatchQuery.data;
|
||||
const traceabilityQuery = useQuery({
|
||||
queryKey: ["honey-batches", params.id, "traceability"],
|
||||
queryFn: () => fetchHoneyBatchTraceability(params.id),
|
||||
});
|
||||
const traceability = traceabilityQuery.data;
|
||||
|
||||
const exportPdfMutation = useMutation({
|
||||
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) {
|
||||
return <p className="p-4 text-sm text-muted-foreground sm:p-6">Lädt…</p>;
|
||||
}
|
||||
if (isError || !honeyBatch) {
|
||||
return (
|
||||
<p role="alert" className="p-4 text-sm text-destructive sm:p-6">
|
||||
Honigcharge konnte nicht geladen werden.
|
||||
</p>
|
||||
);
|
||||
if (honeyBatchQuery.isLoading || honeyBatchQuery.isError || !honeyBatch) {
|
||||
return <DetailQueryFallback query={honeyBatchQuery} what="Daten der Honigcharge" {...BACK} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<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="flex flex-col gap-4 lg:col-span-1">
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<span
|
||||
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>
|
||||
<CardHeader className="flex flex-row flex-wrap items-center justify-between gap-2">
|
||||
<CardTitle>Angaben</CardTitle>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<HoneyBatchEditDialog honeyBatch={honeyBatch} />
|
||||
<QrCodeDialog entityType="honey-batches" id={honeyBatch.id} />
|
||||
</div>
|
||||
@@ -75,15 +95,6 @@ export default function HoneyBatchDetailPage() {
|
||||
{honeyBatch.waterContent !== null ? (
|
||||
<p>Wassergehalt: {honeyBatch.waterContent}%</p>
|
||||
) : null}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-fit"
|
||||
nativeButton={false}
|
||||
render={<Link href={`/bottling/new?honeyBatchId=${honeyBatch.id}`} />}
|
||||
>
|
||||
Neue Abfüllung
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<HoneyBatchStorageCard honeyBatch={honeyBatch} />
|
||||
@@ -133,17 +144,19 @@ export default function HoneyBatchDetailPage() {
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between">
|
||||
<CardTitle>Rückverfolgbarkeit</CardTitle>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={exportPdfMutation.isPending}
|
||||
onClick={() => exportPdfMutation.mutate()}
|
||||
>
|
||||
PDF-Bericht
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<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 ? (
|
||||
<>
|
||||
<div className="flex items-center justify-between">
|
||||
@@ -162,7 +175,7 @@ export default function HoneyBatchDetailPage() {
|
||||
<Link
|
||||
key={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">
|
||||
Ernte am {new Date(harvest.date).toLocaleDateString("de-DE")}
|
||||
|
||||
Reference in new issue
Block a user