feat: Bearbeiten für alle Log-Einträge (Aufgaben, Durchsichten, Behandlungen, Fütterungen, Varroa-Messungen, Ernten, Schleuderungen, Abfüllungen)

Bisher liessen sich diese acht Objekttypen nur anlegen, nie nachträglich
korrigieren – Aufgaben hatten zwar schon einen PATCH-Endpunkt, aber
keine Bearbeiten-UI, die übrigen sieben hatten weder Backend- noch
Frontend-Unterstützung. Jetzt bekommt jeder Typ einen PATCH :id
Endpunkt (UpdateXDto per PartialType(PickType(...)) nach demselben
Muster wie UpdateColonyDto/UpdateApiaryDto, mit Audit-Log-Eintrag
inkl. Vorher-/Nachher-Stand) sowie eine Bearbeiten-UI:

- Aufgaben: Dialog auf der Aufgabenliste
- Durchsichten/Behandlungen/Fütterungen/Varroa-Messungen: Dialog pro
  Listeneintrag auf der Volk-Detailseite
- Ernten: Dialog auf der eigenen Ernte-Detailseite
- Schleuderungen: Dialog direkt in der Rückverfolgbarkeits-Karte der
  Honigcharge (keine eigene Detailseite nötig); Erntezeilen dort
  verlinken jetzt zur Ernte-Detailseite statt sie zu duplizieren
- Abfüllungen: Dialog auf der Abfüllungs-Detailseite, bewusst nur
  Datum/MHD/Losnummer editierbar – Glasgröße/-anzahl bleiben
  unveränderlich, weil sie bereits unumkehrbar die Honigcharge-Menge
  reduziert haben

Relationsstiftende Fremdschlüssel (colonyId, apiaryId/colonyIds,
harvestIds, honeyBatchId) bleiben überall bewusst nicht editierbar,
konsistent mit dem bestehenden apiaryId-Ausschluss bei UpdateColonyDto.
This commit is contained in:
Nick Szaunig committed 2026-08-25 04:28:37 +02:00
1 parent 63ab0e842d
commit 68f459001a
58 files changed
+1909 -60

No files matched your search

@@ -7,6 +7,7 @@ import { fetchColonyFeedings, fetchColonyFeedingsSummary } from "@/lib/feedings/
import { Button } from "@/components/ui/button";
import { EmptyState } from "@/components/empty-state";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { FeedingEditDialog } from "./feeding-edit-dialog";
export function ColonyFeedingsCard({ colonyId }: { colonyId: string }) {
const { data: feedings, isLoading } = useQuery({
@@ -49,9 +50,12 @@ export function ColonyFeedingsCard({ colonyId }: { colonyId: string }) {
) : null}
{feedings?.map((feeding) => (
<div key={feeding.id} className="rounded-md border p-3">
<p className="font-medium">
{new Date(feeding.date).toLocaleDateString("de-DE")} · {feeding.feedType}
</p>
<div className="flex items-center justify-between">
<p className="font-medium">
{new Date(feeding.date).toLocaleDateString("de-DE")} · {feeding.feedType}
</p>
<FeedingEditDialog feeding={feeding} />
</div>
<p className="text-muted-foreground">
{feeding.amount} {feeding.unit}
{feeding.product ? ` · ${feeding.product}` : ""}
@@ -7,6 +7,7 @@ import { fetchColonyInspections } from "@/lib/inspections/api";
import { Button } from "@/components/ui/button";
import { EmptyState } from "@/components/empty-state";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { InspectionEditDialog } from "./inspection-edit-dialog";
export function ColonyInspectionsCard({ colonyId }: { colonyId: string }) {
const { data: inspections, isLoading } = useQuery({
@@ -37,9 +38,12 @@ export function ColonyInspectionsCard({ colonyId }: { colonyId: string }) {
) : null}
{inspections?.map((inspection) => (
<div key={inspection.id} className="rounded-md border p-3">
<p className="font-medium">
{new Date(inspection.date).toLocaleDateString("de-DE")}
</p>
<div className="flex items-center justify-between">
<p className="font-medium">
{new Date(inspection.date).toLocaleDateString("de-DE")}
</p>
<InspectionEditDialog inspection={inspection} />
</div>
<p className="text-muted-foreground">
{inspection.colonyStrength !== null
? `${inspection.colonyStrength} Waben`
@@ -7,6 +7,7 @@ import { fetchColonyTreatments } from "@/lib/treatments/api";
import { Button } from "@/components/ui/button";
import { EmptyState } from "@/components/empty-state";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { TreatmentEditDialog } from "./treatment-edit-dialog";
export function ColonyTreatmentsCard({ colonyId }: { colonyId: string }) {
const { data: treatments, isLoading } = useQuery({
@@ -37,9 +38,12 @@ export function ColonyTreatmentsCard({ colonyId }: { colonyId: string }) {
) : null}
{treatments?.map((treatment) => (
<div key={treatment.id} className="rounded-md border p-3">
<p className="font-medium">
{new Date(treatment.date).toLocaleDateString("de-DE")} · {treatment.product}
</p>
<div className="flex items-center justify-between">
<p className="font-medium">
{new Date(treatment.date).toLocaleDateString("de-DE")} · {treatment.product}
</p>
<TreatmentEditDialog treatment={treatment} />
</div>
<p className="text-muted-foreground">
{treatment.amount ?? "—"}
{treatment.method ? ` · ${treatment.method}` : ""}
@@ -7,6 +7,7 @@ import { fetchColonyVarroaMeasurements } from "@/lib/varroa/api";
import { Button } from "@/components/ui/button";
import { EmptyState } from "@/components/empty-state";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { VarroaEditDialog } from "./varroa-edit-dialog";
const CHART_WIDTH = 280;
const CHART_HEIGHT = 80;
@@ -75,9 +76,12 @@ export function ColonyVarroaCard({ colonyId }: { colonyId: string }) {
<VarroaTrendChart values={values} />
{measurements?.map((measurement) => (
<div key={measurement.id} className="rounded-md border p-3">
<p className="font-medium">
{new Date(measurement.date).toLocaleDateString("de-DE")} · {measurement.method}
</p>
<div className="flex items-center justify-between">
<p className="font-medium">
{new Date(measurement.date).toLocaleDateString("de-DE")} · {measurement.method}
</p>
<VarroaEditDialog measurement={measurement} />
</div>
<p className="text-muted-foreground">
{measurement.miteCount !== null ? `${measurement.miteCount} Milben` : "—"}
{measurement.calculatedValue !== null
@@ -0,0 +1,152 @@
"use client";
import { FormEvent, useState } from "react";
import { Pencil } from "lucide-react";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { updateFeeding } from "@/lib/feedings/api";
import { Feeding, FeedType } from "@/lib/feedings/types";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
const FEED_TYPE_OPTIONS: FeedType[] = [
"SUGAR_WATER",
"FEED_SYRUP",
"FEED_PASTE",
"OWN_HONEY",
"OTHER",
];
export function FeedingEditDialog({ feeding }: { feeding: Feeding }) {
const queryClient = useQueryClient();
const [open, setOpen] = useState(false);
const [date, setDate] = useState(feeding.date.slice(0, 10));
const [feedType, setFeedType] = useState<FeedType>(feeding.feedType);
const [product, setProduct] = useState(feeding.product ?? "");
const [amount, setAmount] = useState(feeding.amount.toString());
const [unit, setUnit] = useState(feeding.unit);
const [notes, setNotes] = useState(feeding.notes ?? "");
const mutation = useMutation({
mutationFn: () =>
updateFeeding(feeding.id, {
date: new Date(date).toISOString(),
feedType,
product: product || undefined,
amount: amount ? Number(amount) : undefined,
unit,
notes: notes || undefined,
}),
onSuccess: () => {
void queryClient.invalidateQueries({
queryKey: ["colonies", feeding.colonyId, "feedings"],
});
setOpen(false);
},
});
function handleSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
mutation.mutate();
}
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger
render={
<Button variant="ghost" size="sm">
<Pencil className="size-3.5" />
Bearbeiten
</Button>
}
/>
<DialogContent>
<DialogHeader>
<DialogTitle>Fütterung bearbeiten</DialogTitle>
</DialogHeader>
<form className="flex flex-col gap-4" onSubmit={handleSubmit}>
<div className="flex flex-col gap-2">
<Label htmlFor="feeding-edit-date">Datum</Label>
<Input
id="feeding-edit-date"
type="date"
value={date}
onChange={(event) => setDate(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="feeding-edit-type">Futterart</Label>
<Select
value={feedType}
onValueChange={(value) => setFeedType((value ?? feedType) as FeedType)}
>
<SelectTrigger id="feeding-edit-type" className="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
{FEED_TYPE_OPTIONS.map((option) => (
<SelectItem key={option} value={option}>
{option}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex gap-3">
<div className="flex flex-1 flex-col gap-2">
<Label htmlFor="feeding-edit-amount">Menge</Label>
<Input
id="feeding-edit-amount"
type="number"
value={amount}
onChange={(event) => setAmount(event.target.value)}
/>
</div>
<div className="flex flex-1 flex-col gap-2">
<Label htmlFor="feeding-edit-unit">Einheit</Label>
<Input
id="feeding-edit-unit"
value={unit}
onChange={(event) => setUnit(event.target.value)}
/>
</div>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="feeding-edit-product">Produkt</Label>
<Input
id="feeding-edit-product"
value={product}
onChange={(event) => setProduct(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="feeding-edit-notes">Notizen</Label>
<Textarea
id="feeding-edit-notes"
value={notes}
onChange={(event) => setNotes(event.target.value)}
/>
</div>
<Button type="submit" disabled={mutation.isPending}>
{mutation.isPending ? "Speichert…" : "Speichern"}
</Button>
</form>
</DialogContent>
</Dialog>
);
}
@@ -0,0 +1,176 @@
"use client";
import { FormEvent, useState } from "react";
import { Pencil } from "lucide-react";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { updateInspection } from "@/lib/inspections/api";
import { Inspection } from "@/lib/inspections/types";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
export function InspectionEditDialog({ inspection }: { inspection: Inspection }) {
const queryClient = useQueryClient();
const [open, setOpen] = useState(false);
const [date, setDate] = useState(inspection.date.slice(0, 10));
const [colonyStrength, setColonyStrength] = useState(
inspection.colonyStrength?.toString() ?? "",
);
const [queenSeen, setQueenSeen] = useState(inspection.queenSeen ?? false);
const [eggsPresent, setEggsPresent] = useState(inspection.eggsPresent ?? true);
const [feedStatus, setFeedStatus] = useState(inspection.feedStatus ?? "ausreichend");
const [healthIssues, setHealthIssues] = useState(inspection.healthIssues ?? "");
const [varroaIssues, setVarroaIssues] = useState(inspection.varroaIssues ?? "");
const [measures, setMeasures] = useState(inspection.measures ?? "");
const [notes, setNotes] = useState(inspection.notes ?? "");
const mutation = useMutation({
mutationFn: () =>
updateInspection(inspection.id, {
date: new Date(date).toISOString(),
colonyStrength: colonyStrength ? Number(colonyStrength) : undefined,
queenSeen,
eggsPresent,
feedStatus,
healthIssues: healthIssues || undefined,
varroaIssues: varroaIssues || undefined,
measures: measures || undefined,
notes: notes || undefined,
}),
onSuccess: () => {
void queryClient.invalidateQueries({
queryKey: ["colonies", inspection.colonyId, "inspections"],
});
setOpen(false);
},
});
function handleSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
mutation.mutate();
}
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger
render={
<Button variant="ghost" size="sm">
<Pencil className="size-3.5" />
Bearbeiten
</Button>
}
/>
<DialogContent>
<DialogHeader>
<DialogTitle>Durchsicht bearbeiten</DialogTitle>
</DialogHeader>
<form className="flex flex-col gap-4" onSubmit={handleSubmit}>
<div className="flex flex-col gap-2">
<Label htmlFor="inspection-edit-date">Datum</Label>
<Input
id="inspection-edit-date"
type="date"
value={date}
onChange={(event) => setDate(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="inspection-edit-strength">Volksstärke (Waben)</Label>
<Input
id="inspection-edit-strength"
type="number"
value={colonyStrength}
onChange={(event) => setColonyStrength(event.target.value)}
/>
</div>
<label className="flex items-center gap-3 text-sm" htmlFor="inspection-edit-queen">
<input
id="inspection-edit-queen"
type="checkbox"
className="size-5"
checked={queenSeen}
onChange={(event) => setQueenSeen(event.target.checked)}
/>
Königin gesehen
</label>
<label className="flex items-center gap-3 text-sm" htmlFor="inspection-edit-eggs">
<input
id="inspection-edit-eggs"
type="checkbox"
className="size-5"
checked={eggsPresent}
onChange={(event) => setEggsPresent(event.target.checked)}
/>
Stifte vorhanden
</label>
<div className="flex flex-col gap-2">
<Label htmlFor="inspection-edit-feed">Futterstatus</Label>
<Select
value={feedStatus}
onValueChange={(value) => setFeedStatus((value ?? "ausreichend") as string)}
>
<SelectTrigger id="inspection-edit-feed" className="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="ausreichend">Ausreichend</SelectItem>
<SelectItem value="niedrig">Niedrig</SelectItem>
<SelectItem value="kritisch">Kritisch</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="inspection-edit-varroa">Varroa-Befund</Label>
<Input
id="inspection-edit-varroa"
value={varroaIssues}
onChange={(event) => setVarroaIssues(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="inspection-edit-health">Gesundheitsauffälligkeiten</Label>
<Input
id="inspection-edit-health"
value={healthIssues}
onChange={(event) => setHealthIssues(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="inspection-edit-measures">Maßnahmen</Label>
<Textarea
id="inspection-edit-measures"
value={measures}
onChange={(event) => setMeasures(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="inspection-edit-notes">Notizen</Label>
<Textarea
id="inspection-edit-notes"
value={notes}
onChange={(event) => setNotes(event.target.value)}
/>
</div>
<Button type="submit" disabled={mutation.isPending}>
{mutation.isPending ? "Speichert…" : "Speichern"}
</Button>
</form>
</DialogContent>
</Dialog>
);
}
@@ -0,0 +1,125 @@
"use client";
import { FormEvent, useState } from "react";
import { Pencil } from "lucide-react";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { updateTreatment } from "@/lib/treatments/api";
import { Treatment } from "@/lib/treatments/types";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
export function TreatmentEditDialog({ treatment }: { treatment: Treatment }) {
const queryClient = useQueryClient();
const [open, setOpen] = useState(false);
const [date, setDate] = useState(treatment.date.slice(0, 10));
const [product, setProduct] = useState(treatment.product);
const [amount, setAmount] = useState(treatment.amount ?? "");
const [method, setMethod] = useState(treatment.method ?? "");
const [reason, setReason] = useState(treatment.reason ?? "");
const [notes, setNotes] = useState(treatment.notes ?? "");
const mutation = useMutation({
mutationFn: () =>
updateTreatment(treatment.id, {
date: new Date(date).toISOString(),
product,
amount: amount || undefined,
method: method || undefined,
reason: reason || undefined,
notes: notes || undefined,
}),
onSuccess: () => {
void queryClient.invalidateQueries({
queryKey: ["colonies", treatment.colonyId, "treatments"],
});
setOpen(false);
},
});
function handleSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
mutation.mutate();
}
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger
render={
<Button variant="ghost" size="sm">
<Pencil className="size-3.5" />
Bearbeiten
</Button>
}
/>
<DialogContent>
<DialogHeader>
<DialogTitle>Behandlung bearbeiten</DialogTitle>
</DialogHeader>
<form className="flex flex-col gap-4" onSubmit={handleSubmit}>
<div className="flex flex-col gap-2">
<Label htmlFor="treatment-edit-date">Datum</Label>
<Input
id="treatment-edit-date"
type="date"
value={date}
onChange={(event) => setDate(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="treatment-edit-product">Mittel</Label>
<Input
id="treatment-edit-product"
required
value={product}
onChange={(event) => setProduct(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="treatment-edit-amount">Menge</Label>
<Input
id="treatment-edit-amount"
value={amount}
onChange={(event) => setAmount(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="treatment-edit-method">Methode</Label>
<Input
id="treatment-edit-method"
value={method}
onChange={(event) => setMethod(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="treatment-edit-reason">Grund</Label>
<Input
id="treatment-edit-reason"
value={reason}
onChange={(event) => setReason(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="treatment-edit-notes">Notizen</Label>
<Textarea
id="treatment-edit-notes"
value={notes}
onChange={(event) => setNotes(event.target.value)}
/>
</div>
<Button type="submit" disabled={mutation.isPending}>
{mutation.isPending ? "Speichert…" : "Speichern"}
</Button>
</form>
</DialogContent>
</Dialog>
);
}
@@ -0,0 +1,153 @@
"use client";
import { FormEvent, useState } from "react";
import { Pencil } from "lucide-react";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { updateVarroaMeasurement } from "@/lib/varroa/api";
import { VarroaMeasurement, VarroaMethod } from "@/lib/varroa/types";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
const METHOD_OPTIONS: VarroaMethod[] = [
"NATURAL_MITE_FALL",
"POWDERED_SUGAR",
"WASHING_METHOD",
"DRONE_BROOD_CONTROL",
];
export function VarroaEditDialog({ measurement }: { measurement: VarroaMeasurement }) {
const queryClient = useQueryClient();
const [open, setOpen] = useState(false);
const [date, setDate] = useState(measurement.date.slice(0, 10));
const [method, setMethod] = useState<VarroaMethod>(measurement.method);
const [miteCount, setMiteCount] = useState(measurement.miteCount?.toString() ?? "");
const [calculatedValue, setCalculatedValue] = useState(
measurement.calculatedValue?.toString() ?? "",
);
const [assessment, setAssessment] = useState(measurement.assessment ?? "");
const [notes, setNotes] = useState(measurement.notes ?? "");
const mutation = useMutation({
mutationFn: () =>
updateVarroaMeasurement(measurement.id, {
date: new Date(date).toISOString(),
method,
miteCount: miteCount ? Number(miteCount) : undefined,
calculatedValue: calculatedValue ? Number(calculatedValue) : undefined,
assessment: assessment || undefined,
notes: notes || undefined,
}),
onSuccess: () => {
void queryClient.invalidateQueries({
queryKey: ["colonies", measurement.colonyId, "varroa-measurements"],
});
setOpen(false);
},
});
function handleSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
mutation.mutate();
}
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger
render={
<Button variant="ghost" size="sm">
<Pencil className="size-3.5" />
Bearbeiten
</Button>
}
/>
<DialogContent>
<DialogHeader>
<DialogTitle>Varroamessung bearbeiten</DialogTitle>
</DialogHeader>
<form className="flex flex-col gap-4" onSubmit={handleSubmit}>
<div className="flex flex-col gap-2">
<Label htmlFor="varroa-edit-date">Datum</Label>
<Input
id="varroa-edit-date"
type="date"
value={date}
onChange={(event) => setDate(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="varroa-edit-method">Methode</Label>
<Select
value={method}
onValueChange={(value) => setMethod((value ?? method) as VarroaMethod)}
>
<SelectTrigger id="varroa-edit-method" className="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
{METHOD_OPTIONS.map((option) => (
<SelectItem key={option} value={option}>
{option}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="varroa-edit-count">Milbenzahl</Label>
<Input
id="varroa-edit-count"
type="number"
value={miteCount}
onChange={(event) => setMiteCount(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="varroa-edit-calculated">Milben pro Tag</Label>
<Input
id="varroa-edit-calculated"
type="number"
step="0.1"
value={calculatedValue}
onChange={(event) => setCalculatedValue(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="varroa-edit-assessment">Einschätzung</Label>
<Input
id="varroa-edit-assessment"
value={assessment}
onChange={(event) => setAssessment(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="varroa-edit-notes">Notizen</Label>
<Textarea
id="varroa-edit-notes"
value={notes}
onChange={(event) => setNotes(event.target.value)}
/>
</div>
<Button type="submit" disabled={mutation.isPending}>
{mutation.isPending ? "Speichert…" : "Speichern"}
</Button>
</form>
</DialogContent>
</Dialog>
);
}