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:
1 parent
63ab0e842d
commit
68f459001a
58 files changed
+1909
-60
No files matched your search
@@ -0,0 +1,97 @@
|
||||
"use client";
|
||||
|
||||
import { FormEvent, useState } from "react";
|
||||
import { Pencil } from "lucide-react";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { updateBottlingBatch } from "@/lib/bottling/api";
|
||||
import { BottlingBatch } from "@/lib/bottling/types";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
|
||||
export function BottlingEditDialog({ bottlingBatch }: { bottlingBatch: BottlingBatch }) {
|
||||
const queryClient = useQueryClient();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [date, setDate] = useState(bottlingBatch.date.slice(0, 10));
|
||||
const [bestBeforeDate, setBestBeforeDate] = useState(
|
||||
bottlingBatch.bestBeforeDate.slice(0, 10),
|
||||
);
|
||||
const [lotNumber, setLotNumber] = useState(bottlingBatch.lotNumber);
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: () =>
|
||||
updateBottlingBatch(bottlingBatch.id, {
|
||||
date: new Date(date).toISOString(),
|
||||
bestBeforeDate: new Date(bestBeforeDate).toISOString(),
|
||||
lotNumber,
|
||||
}),
|
||||
onSuccess: () => {
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: ["bottling-batches", bottlingBatch.id],
|
||||
});
|
||||
setOpen(false);
|
||||
},
|
||||
});
|
||||
|
||||
function handleSubmit(event: FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
mutation.mutate();
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger
|
||||
render={
|
||||
<Button variant="outline" size="sm">
|
||||
<Pencil className="size-3.5" />
|
||||
Bearbeiten
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Abfüllung bearbeiten</DialogTitle>
|
||||
</DialogHeader>
|
||||
<form className="flex flex-col gap-4" onSubmit={handleSubmit}>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="bottling-edit-date">Datum</Label>
|
||||
<Input
|
||||
id="bottling-edit-date"
|
||||
type="date"
|
||||
value={date}
|
||||
onChange={(event) => setDate(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="bottling-edit-best-before">Mindesthaltbarkeitsdatum</Label>
|
||||
<Input
|
||||
id="bottling-edit-best-before"
|
||||
type="date"
|
||||
value={bestBeforeDate}
|
||||
onChange={(event) => setBestBeforeDate(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="bottling-edit-lot">Los-/Chargennummer</Label>
|
||||
<Input
|
||||
id="bottling-edit-lot"
|
||||
required
|
||||
value={lotNumber}
|
||||
onChange={(event) => setLotNumber(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Button type="submit" disabled={mutation.isPending}>
|
||||
{mutation.isPending ? "Speichert…" : "Speichern"}
|
||||
</Button>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import { useQuery } from "@tanstack/react-query";
|
||||
import { fetchBottlingBatch } from "@/lib/bottling/api";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { QrCodeDialog } from "@/components/qr-code-dialog";
|
||||
import { BottlingEditDialog } from "./bottling-edit-dialog";
|
||||
|
||||
export default function BottlingBatchDetailPage() {
|
||||
const params = useParams<{ id: string }>();
|
||||
@@ -39,11 +40,14 @@ export default function BottlingBatchDetailPage() {
|
||||
</span>
|
||||
<CardTitle className="font-mono">Abfüllung {bottlingBatch.lotNumber}</CardTitle>
|
||||
</div>
|
||||
<QrCodeDialog
|
||||
entityType="bottling-batches"
|
||||
id={bottlingBatch.id}
|
||||
label="QR-Code für Etikett"
|
||||
/>
|
||||
<div className="flex items-center gap-2">
|
||||
<BottlingEditDialog bottlingBatch={bottlingBatch} />
|
||||
<QrCodeDialog
|
||||
entityType="bottling-batches"
|
||||
id={bottlingBatch.id}
|
||||
label="QR-Code für Etikett"
|
||||
/>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-2 text-sm">
|
||||
<p>Honigcharge: {bottlingBatch.honeyBatch.batchNumber}</p>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
"use client";
|
||||
|
||||
import { FormEvent, useState } from "react";
|
||||
import { Pencil } from "lucide-react";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { updateHarvest } from "@/lib/harvests/api";
|
||||
import { Harvest } from "@/lib/harvests/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 HarvestEditDialog({ harvest }: { harvest: Harvest }) {
|
||||
const queryClient = useQueryClient();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [date, setDate] = useState(harvest.date.slice(0, 10));
|
||||
const [honeyType, setHoneyType] = useState(harvest.honeyType ?? "");
|
||||
const [netWeight, setNetWeight] = useState(harvest.netWeight?.toString() ?? "");
|
||||
const [honeySuperCount, setHoneySuperCount] = useState(
|
||||
harvest.honeySuperCount?.toString() ?? "",
|
||||
);
|
||||
const [notes, setNotes] = useState(harvest.notes ?? "");
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: () =>
|
||||
updateHarvest(harvest.id, {
|
||||
date: new Date(date).toISOString(),
|
||||
honeyType: honeyType || undefined,
|
||||
netWeight: netWeight ? Number(netWeight) : undefined,
|
||||
honeySuperCount: honeySuperCount ? Number(honeySuperCount) : undefined,
|
||||
notes: notes || undefined,
|
||||
}),
|
||||
onSuccess: () => {
|
||||
void queryClient.invalidateQueries({ queryKey: ["harvests", harvest.id] });
|
||||
setOpen(false);
|
||||
},
|
||||
});
|
||||
|
||||
function handleSubmit(event: FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
mutation.mutate();
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger
|
||||
render={
|
||||
<Button variant="outline" size="sm">
|
||||
<Pencil className="size-3.5" />
|
||||
Bearbeiten
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Ernte bearbeiten</DialogTitle>
|
||||
</DialogHeader>
|
||||
<form className="flex flex-col gap-4" onSubmit={handleSubmit}>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="harvest-edit-date">Datum</Label>
|
||||
<Input
|
||||
id="harvest-edit-date"
|
||||
type="date"
|
||||
value={date}
|
||||
onChange={(event) => setDate(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="harvest-edit-type">Honigsorte</Label>
|
||||
<Input
|
||||
id="harvest-edit-type"
|
||||
value={honeyType}
|
||||
onChange={(event) => setHoneyType(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<div className="flex flex-1 flex-col gap-2">
|
||||
<Label htmlFor="harvest-edit-weight">Nettogewicht (kg)</Label>
|
||||
<Input
|
||||
id="harvest-edit-weight"
|
||||
type="number"
|
||||
value={netWeight}
|
||||
onChange={(event) => setNetWeight(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-1 flex-col gap-2">
|
||||
<Label htmlFor="harvest-edit-supers">Honigräume</Label>
|
||||
<Input
|
||||
id="harvest-edit-supers"
|
||||
type="number"
|
||||
value={honeySuperCount}
|
||||
onChange={(event) => setHoneySuperCount(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="harvest-edit-notes">Notizen</Label>
|
||||
<Textarea
|
||||
id="harvest-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>
|
||||
);
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import { useQuery } from "@tanstack/react-query";
|
||||
import { fetchHarvest } from "@/lib/harvests/api";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { HarvestEditDialog } from "./harvest-edit-dialog";
|
||||
|
||||
export default function HarvestDetailPage() {
|
||||
const params = useParams<{ id: string }>();
|
||||
@@ -29,14 +30,17 @@ export default function HarvestDetailPage() {
|
||||
return (
|
||||
<main className="flex flex-col gap-4 p-4 sm:p-6">
|
||||
<Card className="max-w-lg">
|
||||
<CardHeader className="flex flex-row 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)" }}
|
||||
>
|
||||
<Boxes className="size-4" />
|
||||
</span>
|
||||
<CardTitle>Ernte vom {new Date(harvest.date).toLocaleDateString("de-DE")}</CardTitle>
|
||||
<CardHeader className="flex flex-row items-center justify-between gap-2.5">
|
||||
<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)" }}
|
||||
>
|
||||
<Boxes className="size-4" />
|
||||
</span>
|
||||
<CardTitle>Ernte vom {new Date(harvest.date).toLocaleDateString("de-DE")}</CardTitle>
|
||||
</div>
|
||||
<HarvestEditDialog harvest={harvest} />
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-3 text-sm">
|
||||
{harvest.honeyType ? <p>Honigsorte: {harvest.honeyType}</p> : null}
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
"use client";
|
||||
|
||||
import { FormEvent, useState } from "react";
|
||||
import { Pencil } from "lucide-react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { fetchExtraction, updateExtraction } from "@/lib/extractions/api";
|
||||
import { Extraction } from "@/lib/extractions/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";
|
||||
|
||||
function ExtractionEditForm({
|
||||
extraction,
|
||||
honeyBatchId,
|
||||
onSaved,
|
||||
}: {
|
||||
extraction: Extraction;
|
||||
honeyBatchId: string;
|
||||
onSaved: () => void;
|
||||
}) {
|
||||
const queryClient = useQueryClient();
|
||||
const [date, setDate] = useState(extraction.date.slice(0, 10));
|
||||
const [room, setRoom] = useState(extraction.room ?? "");
|
||||
const [waterContent, setWaterContent] = useState(
|
||||
extraction.waterContent?.toString() ?? "",
|
||||
);
|
||||
const [temperature, setTemperature] = useState(
|
||||
extraction.temperature?.toString() ?? "",
|
||||
);
|
||||
const [notes, setNotes] = useState(extraction.notes ?? "");
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: () =>
|
||||
updateExtraction(extraction.id, {
|
||||
date: new Date(date).toISOString(),
|
||||
room: room || undefined,
|
||||
waterContent: waterContent ? Number(waterContent) : undefined,
|
||||
temperature: temperature ? Number(temperature) : undefined,
|
||||
notes: notes || undefined,
|
||||
}),
|
||||
onSuccess: () => {
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: ["honey-batches", honeyBatchId, "traceability"],
|
||||
});
|
||||
onSaved();
|
||||
},
|
||||
});
|
||||
|
||||
function handleSubmit(event: FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
mutation.mutate();
|
||||
}
|
||||
|
||||
return (
|
||||
<form className="flex flex-col gap-4" onSubmit={handleSubmit}>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="extraction-edit-date">Datum</Label>
|
||||
<Input
|
||||
id="extraction-edit-date"
|
||||
type="date"
|
||||
value={date}
|
||||
onChange={(event) => setDate(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="extraction-edit-room">Raum</Label>
|
||||
<Input
|
||||
id="extraction-edit-room"
|
||||
value={room}
|
||||
onChange={(event) => setRoom(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<div className="flex flex-1 flex-col gap-2">
|
||||
<Label htmlFor="extraction-edit-water">Wassergehalt (%)</Label>
|
||||
<Input
|
||||
id="extraction-edit-water"
|
||||
type="number"
|
||||
step="0.1"
|
||||
value={waterContent}
|
||||
onChange={(event) => setWaterContent(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-1 flex-col gap-2">
|
||||
<Label htmlFor="extraction-edit-temp">Temperatur (°C)</Label>
|
||||
<Input
|
||||
id="extraction-edit-temp"
|
||||
type="number"
|
||||
value={temperature}
|
||||
onChange={(event) => setTemperature(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="extraction-edit-notes">Notizen</Label>
|
||||
<Textarea
|
||||
id="extraction-edit-notes"
|
||||
value={notes}
|
||||
onChange={(event) => setNotes(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Button type="submit" disabled={mutation.isPending}>
|
||||
{mutation.isPending ? "Speichert…" : "Speichern"}
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
export function ExtractionEditDialog({
|
||||
extractionId,
|
||||
honeyBatchId,
|
||||
}: {
|
||||
extractionId: string;
|
||||
honeyBatchId: string;
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const { data: extraction } = useQuery({
|
||||
queryKey: ["extractions", extractionId],
|
||||
queryFn: () => fetchExtraction(extractionId),
|
||||
enabled: open,
|
||||
});
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger
|
||||
render={
|
||||
<Button variant="ghost" size="sm">
|
||||
<Pencil className="size-3.5" />
|
||||
Bearbeiten
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Schleuderung bearbeiten</DialogTitle>
|
||||
</DialogHeader>
|
||||
{!extraction ? <p className="text-sm text-muted-foreground">Lädt…</p> : null}
|
||||
{extraction ? (
|
||||
<ExtractionEditForm
|
||||
key={extraction.id}
|
||||
extraction={extraction}
|
||||
honeyBatchId={honeyBatchId}
|
||||
onSaved={() => setOpen(false)}
|
||||
/>
|
||||
) : null}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -14,6 +14,7 @@ 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";
|
||||
import { ExtractionEditDialog } from "./extraction-edit-dialog";
|
||||
|
||||
export default function HoneyBatchDetailPage() {
|
||||
const params = useParams<{ id: string }>();
|
||||
@@ -115,21 +116,31 @@ export default function HoneyBatchDetailPage() {
|
||||
{!traceability ? <p className="text-muted-foreground">Lädt…</p> : null}
|
||||
{traceability ? (
|
||||
<>
|
||||
<p>
|
||||
Schleuderung am{" "}
|
||||
{new Date(traceability.extraction.date).toLocaleDateString("de-DE")}
|
||||
{traceability.extraction.room ? ` · ${traceability.extraction.room}` : ""}
|
||||
</p>
|
||||
<div className="flex items-center justify-between">
|
||||
<p>
|
||||
Schleuderung am{" "}
|
||||
{new Date(traceability.extraction.date).toLocaleDateString("de-DE")}
|
||||
{traceability.extraction.room ? ` · ${traceability.extraction.room}` : ""}
|
||||
</p>
|
||||
<ExtractionEditDialog
|
||||
extractionId={traceability.extraction.id}
|
||||
honeyBatchId={honeyBatch.id}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
{traceability.harvests.map((harvest) => (
|
||||
<div key={harvest.id} className="rounded-md border p-3">
|
||||
<Link
|
||||
key={harvest.id}
|
||||
href={`/harvests/${harvest.id}`}
|
||||
className="rounded-md border p-3 hover:bg-muted"
|
||||
>
|
||||
<p className="font-medium">
|
||||
Ernte am {new Date(harvest.date).toLocaleDateString("de-DE")}
|
||||
</p>
|
||||
<p className="text-muted-foreground">
|
||||
Völker: {harvest.colonies.map((colony) => colony.code).join(", ")}
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user