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

+14 -10
View File
@@ -25,6 +25,7 @@ import {
TableHeader,
TableRow,
} from "@/components/ui/table";
import { TaskEditDialog } from "./task-edit-dialog";
const STATUS_OPTIONS: TaskStatus[] = ["OPEN", "IN_PROGRESS", "DONE", "CANCELLED"];
const PRIORITY_OPTIONS: TaskPriority[] = ["LOW", "NORMAL", "HIGH", "URGENT"];
@@ -160,16 +161,19 @@ export default function TasksPage() {
</Badge>
</TableCell>
<TableCell>
{task.status !== "DONE" && task.status !== "CANCELLED" ? (
<Button
size="sm"
variant="outline"
disabled={completeMutation.isPending}
onClick={() => completeMutation.mutate(task.id)}
>
Erledigen
</Button>
) : null}
<div className="flex items-center gap-2">
<TaskEditDialog task={task} />
{task.status !== "DONE" && task.status !== "CANCELLED" ? (
<Button
size="sm"
variant="outline"
disabled={completeMutation.isPending}
onClick={() => completeMutation.mutate(task.id)}
>
Erledigen
</Button>
) : null}
</div>
</TableCell>
</TableRow>
))}
@@ -0,0 +1,122 @@
"use client";
import { FormEvent, useState } from "react";
import { Pencil } from "lucide-react";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { updateTask } from "@/lib/tasks/api";
import { Task, TaskPriority } from "@/lib/tasks/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 PRIORITY_OPTIONS: TaskPriority[] = ["LOW", "NORMAL", "HIGH", "URGENT"];
export function TaskEditDialog({ task }: { task: Task }) {
const queryClient = useQueryClient();
const [open, setOpen] = useState(false);
const [title, setTitle] = useState(task.title);
const [description, setDescription] = useState(task.description ?? "");
const [priority, setPriority] = useState<TaskPriority>(task.priority);
const [dueDate, setDueDate] = useState(task.dueDate?.slice(0, 10) ?? "");
const mutation = useMutation({
mutationFn: () =>
updateTask(task.id, {
title,
description: description || undefined,
priority,
dueDate: dueDate ? new Date(dueDate).toISOString() : undefined,
}),
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ["tasks"] });
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>Aufgabe bearbeiten</DialogTitle>
</DialogHeader>
<form className="flex flex-col gap-4" onSubmit={handleSubmit}>
<div className="flex flex-col gap-2">
<Label htmlFor="task-edit-title">Titel</Label>
<Input
id="task-edit-title"
required
value={title}
onChange={(event) => setTitle(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="task-edit-priority">Priorität</Label>
<Select
value={priority}
onValueChange={(value) => setPriority((value ?? priority) as TaskPriority)}
>
<SelectTrigger id="task-edit-priority" className="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
{PRIORITY_OPTIONS.map((option) => (
<SelectItem key={option} value={option}>
{option}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="task-edit-due">Fällig am</Label>
<Input
id="task-edit-due"
type="date"
value={dueDate}
onChange={(event) => setDueDate(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="task-edit-description">Beschreibung</Label>
<Textarea
id="task-edit-description"
value={description}
onChange={(event) => setDescription(event.target.value)}
/>
</div>
<Button type="submit" disabled={mutation.isPending}>
{mutation.isPending ? "Speichert…" : "Speichern"}
</Button>
</form>
</DialogContent>
</Dialog>
);
}