feat: make the parent relation of every record correctable after entry
An inspection, treatment, feeding, varroa measurement or swarm control check filed against the wrong colony could not be repaired: the colony was frozen at creation time, and with no delete endpoint either, the only remedy was leaving a wrong record in the colony's history. The same held for a harvest booked against the wrong apiary or with a colony forgotten, for an extraction linked to the wrong harvests, and for a honey batch whose number or source extraction was mistyped. Backend: - The five colony-scoped update DTOs now cover the full create DTO, colonyId included. - Harvest update accepts apiaryId and colonyIds, extraction update accepts harvestIds. Both validate the ids first and replace the whole join-row set inside the existing transaction; the join rows carry no data of their own, so replacing beats diffing. - Honey batch update accepts batchNumber, extractionId and status. Status was previously only ever set to BOTTLED by the bottling module, so SOLD and CONSUMED were unreachable through the API. - Colony and storage container codes, and a task's relatedType/relatedId, are editable. - apiaryId on a colony and storageContainerId on a honey batch stay out of PATCH on purpose: both have dedicated endpoints that also write the history entry (ColonyApiaryHistory, HoneyBatchRelocation) the traceability chain depends on. - AllExceptionsFilter now also translates Prisma P2003 to a 400 and P2025 to a 404. Services write straight against the foreign keys with no application level existence check, so before this a stale or mistyped relation id came back as a bare 500 and filled the log with fake server errors. Frontend: - New shared fields: ColonySelectField (used by all five colony-scoped edit dialogs), HarvestSelectField (both extraction dialogs) and TaskRelatedField. - Re-filing a record invalidates the lists of both the colony it left and the one it joined, otherwise one of the two stays stale. - The harvest dialog gained an apiary select and a colony checklist that keeps already-linked colonies visible even when they moved or were dissolved; the honey batch dialog gained batch number, source extraction and status. - Extraction and harvest forms refuse to submit an empty relation set instead of letting the backend reject it. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
1 parent
2b15795a60
commit
bee2199647
44 files changed
+958
-156
No files matched your search
@@ -76,6 +76,7 @@ function ConditionSelect({
|
||||
|
||||
export function ColonyEditCard({ colony }: { colony: Colony }) {
|
||||
const queryClient = useQueryClient();
|
||||
const [code, setCode] = useState(colony.code);
|
||||
const [status, setStatus] = useState<ColonyStatus>(colony.status);
|
||||
const [colonyType, setColonyType] = useState<ColonyType>(colony.colonyType);
|
||||
const [hiveType, setHiveType] = useState(colony.hiveType ?? "");
|
||||
@@ -95,6 +96,7 @@ export function ColonyEditCard({ colony }: { colony: Colony }) {
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: () =>
|
||||
updateColony(colony.id, {
|
||||
code,
|
||||
status,
|
||||
colonyType,
|
||||
hiveType: hiveType || undefined,
|
||||
@@ -133,6 +135,15 @@ export function ColonyEditCard({ colony }: { colony: Colony }) {
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form className="flex flex-col gap-4" onSubmit={handleSubmit}>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="colony-code">Volksnummer</Label>
|
||||
<Input
|
||||
id="colony-code"
|
||||
required
|
||||
value={code}
|
||||
onChange={(event) => setCode(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="colony-status">Status</Label>
|
||||
<Select value={status} onValueChange={(value) => setStatus(value as ColonyStatus)}>
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { ColonySelectField } from "@/components/colony-select-field";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -29,6 +30,7 @@ import {
|
||||
export function FeedingEditDialog({ feeding }: { feeding: Feeding }) {
|
||||
const queryClient = useQueryClient();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [colonyId, setColonyId] = useState(feeding.colonyId);
|
||||
const [date, setDate] = useState(feeding.date.slice(0, 10));
|
||||
const [feedType, setFeedType] = useState<FeedType>(feeding.feedType);
|
||||
const [product, setProduct] = useState(feeding.product ?? "");
|
||||
@@ -40,6 +42,7 @@ export function FeedingEditDialog({ feeding }: { feeding: Feeding }) {
|
||||
const mutation = useMutation({
|
||||
mutationFn: () =>
|
||||
updateFeeding(feeding.id, {
|
||||
colonyId,
|
||||
date: new Date(date).toISOString(),
|
||||
feedType,
|
||||
product: product || undefined,
|
||||
@@ -49,9 +52,13 @@ export function FeedingEditDialog({ feeding }: { feeding: Feeding }) {
|
||||
notes: notes || undefined,
|
||||
}),
|
||||
onSuccess: () => {
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: ["colonies", feeding.colonyId, "feedings"],
|
||||
});
|
||||
// After re-filing, both the colony it left and the one it joined
|
||||
// show a stale list until each is invalidated.
|
||||
for (const affectedColonyId of new Set([feeding.colonyId, colonyId])) {
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: ["colonies", affectedColonyId, "feedings"],
|
||||
});
|
||||
}
|
||||
setOpen(false);
|
||||
},
|
||||
onError: () => toast.error("Fütterung konnte nicht gespeichert werden"),
|
||||
@@ -77,6 +84,11 @@ export function FeedingEditDialog({ feeding }: { feeding: Feeding }) {
|
||||
<DialogTitle>Fütterung bearbeiten</DialogTitle>
|
||||
</DialogHeader>
|
||||
<form className="flex flex-col gap-4" onSubmit={handleSubmit}>
|
||||
<ColonySelectField
|
||||
id="feeding-edit-colony"
|
||||
value={colonyId}
|
||||
onChange={setColonyId}
|
||||
/>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="feeding-edit-date">Datum</Label>
|
||||
<Input
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { ColonySelectField } from "@/components/colony-select-field";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -34,6 +35,7 @@ import {
|
||||
export function InspectionEditDialog({ inspection }: { inspection: Inspection }) {
|
||||
const queryClient = useQueryClient();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [colonyId, setColonyId] = useState(inspection.colonyId);
|
||||
const [date, setDate] = useState(inspection.date.slice(0, 10));
|
||||
const [weather, setWeather] = useState(inspection.weather ?? "");
|
||||
const [temperature, setTemperature] = useState(inspection.temperature?.toString() ?? "");
|
||||
@@ -64,6 +66,7 @@ export function InspectionEditDialog({ inspection }: { inspection: Inspection })
|
||||
const mutation = useMutation({
|
||||
mutationFn: () =>
|
||||
updateInspection(inspection.id, {
|
||||
colonyId,
|
||||
date: new Date(date).toISOString(),
|
||||
weather: weather || undefined,
|
||||
temperature: temperature ? Number(temperature) : undefined,
|
||||
@@ -86,9 +89,13 @@ export function InspectionEditDialog({ inspection }: { inspection: Inspection })
|
||||
notes: notes || undefined,
|
||||
}),
|
||||
onSuccess: () => {
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: ["colonies", inspection.colonyId, "inspections"],
|
||||
});
|
||||
// After re-filing, both the colony it left and the one it joined
|
||||
// show a stale list until each is invalidated.
|
||||
for (const affectedColonyId of new Set([inspection.colonyId, colonyId])) {
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: ["colonies", affectedColonyId, "inspections"],
|
||||
});
|
||||
}
|
||||
setOpen(false);
|
||||
},
|
||||
onError: () => toast.error("Durchsicht konnte nicht gespeichert werden"),
|
||||
@@ -114,6 +121,11 @@ export function InspectionEditDialog({ inspection }: { inspection: Inspection })
|
||||
<DialogTitle>Durchsicht bearbeiten</DialogTitle>
|
||||
</DialogHeader>
|
||||
<form className="flex flex-col gap-4" onSubmit={handleSubmit}>
|
||||
<ColonySelectField
|
||||
id="inspection-edit-colony"
|
||||
value={colonyId}
|
||||
onChange={setColonyId}
|
||||
/>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="inspection-edit-date">Datum</Label>
|
||||
<Input
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { ColonySelectField } from "@/components/colony-select-field";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -29,6 +30,7 @@ import {
|
||||
export function SwarmControlEditDialog({ check }: { check: SwarmControlCheck }) {
|
||||
const queryClient = useQueryClient();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [colonyId, setColonyId] = useState(check.colonyId);
|
||||
const [date, setDate] = useState(check.date.slice(0, 10));
|
||||
const [queenCellsFound, setQueenCellsFound] = useState(check.queenCellsFound?.toString() ?? "");
|
||||
const [queenCellsCapped, setQueenCellsCapped] = useState(check.queenCellsCapped);
|
||||
@@ -40,6 +42,7 @@ export function SwarmControlEditDialog({ check }: { check: SwarmControlCheck })
|
||||
const mutation = useMutation({
|
||||
mutationFn: () =>
|
||||
updateSwarmControlCheck(check.id, {
|
||||
colonyId,
|
||||
date: new Date(date).toISOString(),
|
||||
queenCellsFound: queenCellsFound ? Number(queenCellsFound) : undefined,
|
||||
queenCellsCapped,
|
||||
@@ -49,9 +52,13 @@ export function SwarmControlEditDialog({ check }: { check: SwarmControlCheck })
|
||||
notes: notes || undefined,
|
||||
}),
|
||||
onSuccess: () => {
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: ["colonies", check.colonyId, "swarm-control-checks"],
|
||||
});
|
||||
// After re-filing, both the colony it left and the one it joined
|
||||
// show a stale list until each is invalidated.
|
||||
for (const affectedColonyId of new Set([check.colonyId, colonyId])) {
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: ["colonies", affectedColonyId, "swarm-control-checks"],
|
||||
});
|
||||
}
|
||||
setOpen(false);
|
||||
},
|
||||
onError: () => toast.error("Schwarmkontrolle konnte nicht gespeichert werden"),
|
||||
@@ -77,6 +84,11 @@ export function SwarmControlEditDialog({ check }: { check: SwarmControlCheck })
|
||||
<DialogTitle>Schwarmkontrolle bearbeiten</DialogTitle>
|
||||
</DialogHeader>
|
||||
<form className="flex flex-col gap-4" onSubmit={handleSubmit}>
|
||||
<ColonySelectField
|
||||
id="swarm-edit-colony"
|
||||
value={colonyId}
|
||||
onChange={setColonyId}
|
||||
/>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="swarm-edit-date">Datum</Label>
|
||||
<Input
|
||||
|
||||
@@ -10,6 +10,7 @@ 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 { ColonySelectField } from "@/components/colony-select-field";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -21,6 +22,7 @@ import {
|
||||
export function TreatmentEditDialog({ treatment }: { treatment: Treatment }) {
|
||||
const queryClient = useQueryClient();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [colonyId, setColonyId] = useState(treatment.colonyId);
|
||||
const [date, setDate] = useState(treatment.date.slice(0, 10));
|
||||
const [startDate, setStartDate] = useState(treatment.startDate?.slice(0, 10) ?? "");
|
||||
const [endDate, setEndDate] = useState(treatment.endDate?.slice(0, 10) ?? "");
|
||||
@@ -38,6 +40,7 @@ export function TreatmentEditDialog({ treatment }: { treatment: Treatment }) {
|
||||
const mutation = useMutation({
|
||||
mutationFn: () =>
|
||||
updateTreatment(treatment.id, {
|
||||
colonyId,
|
||||
date: new Date(date).toISOString(),
|
||||
startDate: startDate ? new Date(startDate).toISOString() : undefined,
|
||||
endDate: endDate ? new Date(endDate).toISOString() : undefined,
|
||||
@@ -51,9 +54,13 @@ export function TreatmentEditDialog({ treatment }: { treatment: Treatment }) {
|
||||
withdrawalPeriodDays: withdrawalPeriodDays ? Number(withdrawalPeriodDays) : undefined,
|
||||
}),
|
||||
onSuccess: () => {
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: ["colonies", treatment.colonyId, "treatments"],
|
||||
});
|
||||
// After re-filing, both the colony it left and the one it joined
|
||||
// show a stale list until each is invalidated.
|
||||
for (const affectedColonyId of new Set([treatment.colonyId, colonyId])) {
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: ["colonies", affectedColonyId, "treatments"],
|
||||
});
|
||||
}
|
||||
setOpen(false);
|
||||
},
|
||||
onError: () => toast.error("Behandlung konnte nicht gespeichert werden"),
|
||||
@@ -79,6 +86,11 @@ export function TreatmentEditDialog({ treatment }: { treatment: Treatment }) {
|
||||
<DialogTitle>Behandlung bearbeiten</DialogTitle>
|
||||
</DialogHeader>
|
||||
<form className="flex flex-col gap-4" onSubmit={handleSubmit}>
|
||||
<ColonySelectField
|
||||
id="treatment-edit-colony"
|
||||
value={colonyId}
|
||||
onChange={setColonyId}
|
||||
/>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="treatment-edit-date">Datum</Label>
|
||||
<Input
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { ColonySelectField } from "@/components/colony-select-field";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -29,6 +30,7 @@ import {
|
||||
export function VarroaEditDialog({ measurement }: { measurement: VarroaMeasurement }) {
|
||||
const queryClient = useQueryClient();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [colonyId, setColonyId] = useState(measurement.colonyId);
|
||||
const [date, setDate] = useState(measurement.date.slice(0, 10));
|
||||
const [method, setMethod] = useState<VarroaMethod>(measurement.method);
|
||||
const [miteCount, setMiteCount] = useState(measurement.miteCount?.toString() ?? "");
|
||||
@@ -44,6 +46,7 @@ export function VarroaEditDialog({ measurement }: { measurement: VarroaMeasureme
|
||||
const mutation = useMutation({
|
||||
mutationFn: () =>
|
||||
updateVarroaMeasurement(measurement.id, {
|
||||
colonyId,
|
||||
date: new Date(date).toISOString(),
|
||||
method,
|
||||
miteCount: miteCount ? Number(miteCount) : undefined,
|
||||
@@ -53,9 +56,13 @@ export function VarroaEditDialog({ measurement }: { measurement: VarroaMeasureme
|
||||
notes: notes || undefined,
|
||||
}),
|
||||
onSuccess: () => {
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: ["colonies", measurement.colonyId, "varroa-measurements"],
|
||||
});
|
||||
// After re-filing, both the colony it left and the one it joined
|
||||
// show a stale list until each is invalidated.
|
||||
for (const affectedColonyId of new Set([measurement.colonyId, colonyId])) {
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: ["colonies", affectedColonyId, "varroa-measurements"],
|
||||
});
|
||||
}
|
||||
setOpen(false);
|
||||
},
|
||||
onError: () => toast.error("Varroamessung konnte nicht gespeichert werden"),
|
||||
@@ -81,6 +88,11 @@ export function VarroaEditDialog({ measurement }: { measurement: VarroaMeasureme
|
||||
<DialogTitle>Varroamessung bearbeiten</DialogTitle>
|
||||
</DialogHeader>
|
||||
<form className="flex flex-col gap-4" onSubmit={handleSubmit}>
|
||||
<ColonySelectField
|
||||
id="varroa-edit-colony"
|
||||
value={colonyId}
|
||||
onChange={setColonyId}
|
||||
/>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="varroa-edit-date">Datum</Label>
|
||||
<Input
|
||||
|
||||
@@ -10,6 +10,7 @@ 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 { HarvestSelectField } from "@/components/harvest-select-field";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -21,6 +22,9 @@ import {
|
||||
export function ExtractionEditDialog({ extraction }: { extraction: Extraction }) {
|
||||
const queryClient = useQueryClient();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [harvestIds, setHarvestIds] = useState<string[]>(
|
||||
extraction.harvests.map((entry) => entry.harvestId),
|
||||
);
|
||||
const [date, setDate] = useState(extraction.date.slice(0, 10));
|
||||
const [amount, setAmount] = useState(extraction.amount?.toString() ?? "");
|
||||
const [room, setRoom] = useState(extraction.room ?? "");
|
||||
@@ -31,6 +35,7 @@ export function ExtractionEditDialog({ extraction }: { extraction: Extraction })
|
||||
const mutation = useMutation({
|
||||
mutationFn: () =>
|
||||
updateExtraction(extraction.id, {
|
||||
harvestIds,
|
||||
date: new Date(date).toISOString(),
|
||||
amount: amount ? Number(amount) : undefined,
|
||||
room: room || undefined,
|
||||
@@ -42,11 +47,16 @@ export function ExtractionEditDialog({ extraction }: { extraction: Extraction })
|
||||
void queryClient.invalidateQueries({ queryKey: ["extractions", extraction.id] });
|
||||
setOpen(false);
|
||||
},
|
||||
onError: () => toast.error("Schleuderung konnte nicht gespeichert werden"),
|
||||
onError: (error: Error) =>
|
||||
toast.error(error.message || "Schleuderung konnte nicht gespeichert werden"),
|
||||
});
|
||||
|
||||
function handleSubmit(event: FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
if (harvestIds.length === 0) {
|
||||
toast.error("Bitte mindestens eine Ernte auswählen.");
|
||||
return;
|
||||
}
|
||||
mutation.mutate();
|
||||
}
|
||||
|
||||
@@ -65,6 +75,11 @@ export function ExtractionEditDialog({ extraction }: { extraction: Extraction })
|
||||
<DialogTitle>Schleuderung bearbeiten</DialogTitle>
|
||||
</DialogHeader>
|
||||
<form className="flex flex-col gap-4" onSubmit={handleSubmit}>
|
||||
<HarvestSelectField
|
||||
idPrefix="extraction-edit-harvest"
|
||||
value={harvestIds}
|
||||
onChange={setHarvestIds}
|
||||
/>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="extraction-edit-date">Datum</Label>
|
||||
<Input
|
||||
|
||||
@@ -2,14 +2,23 @@
|
||||
|
||||
import { FormEvent, useState } from "react";
|
||||
import { Pencil } from "lucide-react";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { toast } from "sonner";
|
||||
import { updateHarvest } from "@/lib/harvests/api";
|
||||
import { Harvest } from "@/lib/harvests/types";
|
||||
import { fetchApiaries } from "@/lib/apiaries/api";
|
||||
import { fetchColonies } from "@/lib/colonies/api";
|
||||
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,
|
||||
@@ -21,6 +30,10 @@ import {
|
||||
export function HarvestEditDialog({ harvest }: { harvest: Harvest }) {
|
||||
const queryClient = useQueryClient();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [apiaryId, setApiaryId] = useState(harvest.apiaryId);
|
||||
const [colonyIds, setColonyIds] = useState<string[]>(
|
||||
harvest.colonies.map((entry) => entry.colonyId),
|
||||
);
|
||||
const [date, setDate] = useState(harvest.date.slice(0, 10));
|
||||
const [honeyType, setHoneyType] = useState(harvest.honeyType ?? "");
|
||||
const [grossWeight, setGrossWeight] = useState(harvest.grossWeight?.toString() ?? "");
|
||||
@@ -30,9 +43,35 @@ export function HarvestEditDialog({ harvest }: { harvest: Harvest }) {
|
||||
);
|
||||
const [notes, setNotes] = useState(harvest.notes ?? "");
|
||||
|
||||
const { data: apiaries } = useQuery({
|
||||
queryKey: ["apiaries"],
|
||||
queryFn: () => fetchApiaries(),
|
||||
});
|
||||
const { data: colonies } = useQuery({
|
||||
queryKey: ["colonies"],
|
||||
queryFn: () => fetchColonies(),
|
||||
});
|
||||
|
||||
// Colonies of the chosen apiary, plus any colony already on this harvest -
|
||||
// one that was meanwhile moved or dissolved must stay visible, otherwise
|
||||
// saving would silently drop it from the harvest.
|
||||
const selectableColonies = (colonies ?? []).filter(
|
||||
(colony) => colony.apiaryId === apiaryId || colonyIds.includes(colony.id),
|
||||
);
|
||||
|
||||
function toggleColony(colonyId: string, checked: boolean) {
|
||||
setColonyIds((current) =>
|
||||
checked
|
||||
? [...current, colonyId]
|
||||
: current.filter((id) => id !== colonyId),
|
||||
);
|
||||
}
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: () =>
|
||||
updateHarvest(harvest.id, {
|
||||
apiaryId,
|
||||
colonyIds,
|
||||
date: new Date(date).toISOString(),
|
||||
honeyType: honeyType || undefined,
|
||||
grossWeight: grossWeight ? Number(grossWeight) : undefined,
|
||||
@@ -41,14 +80,19 @@ export function HarvestEditDialog({ harvest }: { harvest: Harvest }) {
|
||||
notes: notes || undefined,
|
||||
}),
|
||||
onSuccess: () => {
|
||||
void queryClient.invalidateQueries({ queryKey: ["harvests", harvest.id] });
|
||||
void queryClient.invalidateQueries({ queryKey: ["harvests"] });
|
||||
setOpen(false);
|
||||
},
|
||||
onError: () => toast.error("Ernte konnte nicht gespeichert werden"),
|
||||
onError: (error: Error) =>
|
||||
toast.error(error.message || "Ernte konnte nicht gespeichert werden"),
|
||||
});
|
||||
|
||||
function handleSubmit(event: FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
if (colonyIds.length === 0) {
|
||||
toast.error("Bitte mindestens ein Volk auswählen.");
|
||||
return;
|
||||
}
|
||||
mutation.mutate();
|
||||
}
|
||||
|
||||
@@ -67,6 +111,48 @@ export function HarvestEditDialog({ harvest }: { harvest: Harvest }) {
|
||||
<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-apiary">Bienenstand</Label>
|
||||
<Select
|
||||
value={apiaryId}
|
||||
onValueChange={(value) => setApiaryId(value ?? apiaryId)}
|
||||
>
|
||||
<SelectTrigger id="harvest-edit-apiary" className="w-full">
|
||||
<SelectValue placeholder="Bienenstand wählen" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(apiaries ?? []).map((apiary) => (
|
||||
<SelectItem key={apiary.id} value={apiary.id}>
|
||||
{apiary.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>Beteiligte Völker</Label>
|
||||
{selectableColonies.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Keine Völker an diesem Bienenstand.
|
||||
</p>
|
||||
) : null}
|
||||
{selectableColonies.map((colony) => (
|
||||
<label
|
||||
key={colony.id}
|
||||
className="flex h-11 items-center gap-3 text-sm"
|
||||
htmlFor={`harvest-edit-colony-${colony.id}`}
|
||||
>
|
||||
<input
|
||||
id={`harvest-edit-colony-${colony.id}`}
|
||||
type="checkbox"
|
||||
className="size-5"
|
||||
checked={colonyIds.includes(colony.id)}
|
||||
onChange={(event) => toggleColony(colony.id, event.target.checked)}
|
||||
/>
|
||||
{colony.code}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="harvest-edit-date">Datum</Label>
|
||||
<Input
|
||||
|
||||
@@ -10,6 +10,7 @@ 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 { HarvestSelectField } from "@/components/harvest-select-field";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -28,6 +29,9 @@ function ExtractionEditForm({
|
||||
onSaved: () => void;
|
||||
}) {
|
||||
const queryClient = useQueryClient();
|
||||
const [harvestIds, setHarvestIds] = useState<string[]>(
|
||||
extraction.harvests.map((entry) => entry.harvestId),
|
||||
);
|
||||
const [date, setDate] = useState(extraction.date.slice(0, 10));
|
||||
const [amount, setAmount] = useState(extraction.amount?.toString() ?? "");
|
||||
const [room, setRoom] = useState(extraction.room ?? "");
|
||||
@@ -42,6 +46,7 @@ function ExtractionEditForm({
|
||||
const mutation = useMutation({
|
||||
mutationFn: () =>
|
||||
updateExtraction(extraction.id, {
|
||||
harvestIds,
|
||||
date: new Date(date).toISOString(),
|
||||
amount: amount ? Number(amount) : undefined,
|
||||
room: room || undefined,
|
||||
@@ -55,16 +60,26 @@ function ExtractionEditForm({
|
||||
});
|
||||
onSaved();
|
||||
},
|
||||
onError: () => toast.error("Schleuderung konnte nicht gespeichert werden"),
|
||||
onError: (error: Error) =>
|
||||
toast.error(error.message || "Schleuderung konnte nicht gespeichert werden"),
|
||||
});
|
||||
|
||||
function handleSubmit(event: FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
if (harvestIds.length === 0) {
|
||||
toast.error("Bitte mindestens eine Ernte auswählen.");
|
||||
return;
|
||||
}
|
||||
mutation.mutate();
|
||||
}
|
||||
|
||||
return (
|
||||
<form className="flex flex-col gap-4" onSubmit={handleSubmit}>
|
||||
<HarvestSelectField
|
||||
idPrefix="batch-extraction-edit-harvest"
|
||||
value={harvestIds}
|
||||
onChange={setHarvestIds}
|
||||
/>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="extraction-edit-date">Datum</Label>
|
||||
<Input
|
||||
|
||||
@@ -2,13 +2,22 @@
|
||||
|
||||
import { FormEvent, useState } from "react";
|
||||
import { Pencil } from "lucide-react";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { toast } from "sonner";
|
||||
import { updateHoneyBatch } from "@/lib/honey-batches/api";
|
||||
import { HoneyBatch } from "@/lib/honey-batches/types";
|
||||
import { HoneyBatch, HoneyBatchStatus } from "@/lib/honey-batches/types";
|
||||
import { HONEY_BATCH_STATUS_LABELS } from "@/lib/honey-batches/labels";
|
||||
import { fetchExtractions } from "@/lib/extractions/api";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -21,13 +30,24 @@ import {
|
||||
export function HoneyBatchEditDialog({ honeyBatch }: { honeyBatch: HoneyBatch }) {
|
||||
const queryClient = useQueryClient();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [batchNumber, setBatchNumber] = useState(honeyBatch.batchNumber);
|
||||
const [extractionId, setExtractionId] = useState(honeyBatch.extractionId);
|
||||
const [status, setStatus] = useState<HoneyBatchStatus>(honeyBatch.status);
|
||||
const [honeyType, setHoneyType] = useState(honeyBatch.honeyType ?? "");
|
||||
const [amount, setAmount] = useState(honeyBatch.amount?.toString() ?? "");
|
||||
const [waterContent, setWaterContent] = useState(honeyBatch.waterContent?.toString() ?? "");
|
||||
|
||||
const { data: extractions } = useQuery({
|
||||
queryKey: ["extractions"],
|
||||
queryFn: () => fetchExtractions(),
|
||||
});
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: () =>
|
||||
updateHoneyBatch(honeyBatch.id, {
|
||||
batchNumber,
|
||||
extractionId,
|
||||
status,
|
||||
honeyType: honeyType || undefined,
|
||||
amount: amount ? Number(amount) : undefined,
|
||||
waterContent: waterContent ? Number(waterContent) : undefined,
|
||||
@@ -60,6 +80,54 @@ export function HoneyBatchEditDialog({ honeyBatch }: { honeyBatch: HoneyBatch })
|
||||
<DialogTitle>Honigcharge bearbeiten</DialogTitle>
|
||||
</DialogHeader>
|
||||
<form className="flex flex-col gap-4" onSubmit={handleSubmit}>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="batch-edit-number">Chargennummer</Label>
|
||||
<Input
|
||||
id="batch-edit-number"
|
||||
required
|
||||
value={batchNumber}
|
||||
onChange={(event) => setBatchNumber(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="batch-edit-extraction">Schleuderung</Label>
|
||||
<Select
|
||||
value={extractionId}
|
||||
onValueChange={(value) => setExtractionId(value ?? extractionId)}
|
||||
>
|
||||
<SelectTrigger id="batch-edit-extraction" className="w-full">
|
||||
<SelectValue placeholder="Schleuderung wählen" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(extractions ?? []).map((extraction) => (
|
||||
<SelectItem key={extraction.id} value={extraction.id}>
|
||||
{new Date(extraction.date).toLocaleDateString("de-DE")}
|
||||
{extraction.amount !== null ? ` · ${extraction.amount} kg` : ""}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="batch-edit-status">Status</Label>
|
||||
<Select
|
||||
value={status}
|
||||
onValueChange={(value) => setStatus((value ?? status) as HoneyBatchStatus)}
|
||||
>
|
||||
<SelectTrigger id="batch-edit-status" className="w-full">
|
||||
<SelectValue>{HONEY_BATCH_STATUS_LABELS[status]}</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(
|
||||
Object.keys(HONEY_BATCH_STATUS_LABELS) as HoneyBatchStatus[]
|
||||
).map((option) => (
|
||||
<SelectItem key={option} value={option}>
|
||||
{HONEY_BATCH_STATUS_LABELS[option]}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="batch-edit-honey-type">Sorte</Label>
|
||||
<Input
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
export function EditStorageContainerDialog({ container }: { container: StorageContainer }) {
|
||||
const queryClient = useQueryClient();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [code, setCode] = useState(container.code);
|
||||
const [containerType, setContainerType] = useState(container.containerType ?? "");
|
||||
const [capacity, setCapacity] = useState(container.capacity?.toString() ?? "");
|
||||
const [location, setLocation] = useState(container.location ?? "");
|
||||
@@ -28,6 +29,7 @@ export function EditStorageContainerDialog({ container }: { container: StorageCo
|
||||
const mutation = useMutation({
|
||||
mutationFn: () =>
|
||||
updateStorageContainer(container.id, {
|
||||
code,
|
||||
containerType: containerType || undefined,
|
||||
capacity: capacity ? Number(capacity) : undefined,
|
||||
location: location || undefined,
|
||||
@@ -59,6 +61,15 @@ export function EditStorageContainerDialog({ container }: { container: StorageCo
|
||||
<DialogTitle>Lagerbehälter bearbeiten</DialogTitle>
|
||||
</DialogHeader>
|
||||
<form className="flex flex-col gap-4" onSubmit={handleSubmit}>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="container-edit-code">Code</Label>
|
||||
<Input
|
||||
id="container-edit-code"
|
||||
required
|
||||
value={code}
|
||||
onChange={(event) => setCode(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="container-edit-type">Typ</Label>
|
||||
<Input
|
||||
|
||||
@@ -5,13 +5,14 @@ import { Pencil } from "lucide-react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { toast } from "sonner";
|
||||
import { updateTask } from "@/lib/tasks/api";
|
||||
import { Task, TaskPriority } from "@/lib/tasks/types";
|
||||
import { Task, TaskPriority, TaskRelatedType } from "@/lib/tasks/types";
|
||||
import { TASK_PRIORITIES, TASK_PRIORITY_LABELS } from "@/lib/tasks/labels";
|
||||
import { fetchUsers } from "@/lib/users/api";
|
||||
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 { TaskRelatedField } from "@/components/task-related-field";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -35,6 +36,8 @@ export function TaskEditDialog({ task }: { task: Task }) {
|
||||
const [priority, setPriority] = useState<TaskPriority>(task.priority);
|
||||
const [dueDate, setDueDate] = useState(task.dueDate?.slice(0, 10) ?? "");
|
||||
const [assignedUserId, setAssignedUserId] = useState(task.assignedUserId ?? "");
|
||||
const [relatedType, setRelatedType] = useState<TaskRelatedType>(task.relatedType);
|
||||
const [relatedId, setRelatedId] = useState(task.relatedId ?? "");
|
||||
|
||||
// Assigning a task requires users:read, which not every role has - if the
|
||||
// request fails, the assignee field below is simply omitted rather than
|
||||
@@ -53,6 +56,8 @@ export function TaskEditDialog({ task }: { task: Task }) {
|
||||
priority,
|
||||
dueDate: dueDate ? new Date(dueDate).toISOString() : undefined,
|
||||
assignedUserId: assignedUserId || undefined,
|
||||
relatedType,
|
||||
relatedId: relatedType === "GENERAL" ? undefined : relatedId || undefined,
|
||||
}),
|
||||
onSuccess: () => {
|
||||
void queryClient.invalidateQueries({ queryKey: ["tasks"] });
|
||||
@@ -133,6 +138,13 @@ export function TaskEditDialog({ task }: { task: Task }) {
|
||||
</Select>
|
||||
</div>
|
||||
) : null}
|
||||
<TaskRelatedField
|
||||
idPrefix="task-edit-related"
|
||||
relatedType={relatedType}
|
||||
relatedId={relatedId}
|
||||
onRelatedTypeChange={setRelatedType}
|
||||
onRelatedIdChange={setRelatedId}
|
||||
/>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="task-edit-due">Fällig am</Label>
|
||||
<Input
|
||||
|
||||
Reference in new issue
Block a user