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";
|
"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")}
|
||||||
|
|||||||
Reference in new issue
Block a user