fix: expose every backend-supported field in all edit forms

Audit requested by the user after finding queens couldn't set
hatchYear: many edit dialogs (and some create forms) were missing
fields the backend DTOs already accept, so that data could never be
entered or corrected anywhere in the UI. Fixes per entity:

- Colony: status (WEAK/QUEENLESS/CONSPICUOUS) was never settable
  anywhere, so the COLONY_ATTENTION dashboard alert could never fire.
  Added as a dropdown restricted to the non-terminal statuses;
  DISSOLVED/SOLD/DECEASED/MERGED stay on their dedicated confirm
  actions, which also set deactivatedAt.
- Apiary: description was missing from the edit form.
- Inspection: 10 fields (weather, temperature, broodAmount,
  broodPattern, openBroodPresent, cappedBroodPresent,
  queenCellsPresent, swarmMood, gentleness, combBehavior,
  honeySuperStatus) existed in the schema/DTOs but nowhere in the UI -
  added to both the create and edit forms.
- Varroa measurement: durationMinutes was on the create form but
  missing from edit.
- Feeding: batchNumber missing from both create and edit.
- Treatment: startDate, endDate, activeIngredient, batchNumber
  missing from both create and edit.
- Task: assignedUserId had no UI anywhere; added to create/edit,
  degrading gracefully if the current user's role lacks users:read.
- Extraction (honey-batch traceability variant): amount was missing,
  while the other edit dialog for the same entity already had it.
- Bottling: productId missing from edit.
- Harvest: grossWeight missing from edit; grossWeight and
  honeySuperCount missing from create.
- Queen: breeder, matingType, matingStatus, inseminationData,
  motherLine, fatherLine added to the edit form (hatchYear/
  markingColor/markingNumber were already fixed in a prior commit).

Left alone deliberately: fields the backend DTO itself excludes via
PickType (e.g. HoneyBatch's jarSize/jarCount/totalAmount, Bottling's
quantities) since mutating those post-hoc would desync inventory
deduction, and Colony's terminal statuses/code which have dedicated
lifecycle actions.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
Nick SzaunigandClaude Sonnet 5 committed 2026-09-07 12:20:18 +02:00
1 parent 792806b967
commit 1dddc387ba
20 files changed
+682 -4

No files matched your search

@@ -22,6 +22,7 @@ import {
export function ApiaryEditCard({ apiary }: { apiary: Apiary }) {
const queryClient = useQueryClient();
const [name, setName] = useState(apiary.name);
const [description, setDescription] = useState(apiary.description ?? "");
const [location, setLocation] = useState(apiary.location ?? "");
const [latitude, setLatitude] = useState(apiary.latitude?.toString() ?? "");
const [longitude, setLongitude] = useState(apiary.longitude?.toString() ?? "");
@@ -32,6 +33,7 @@ export function ApiaryEditCard({ apiary }: { apiary: Apiary }) {
mutationFn: () =>
updateApiary(apiary.id, {
name,
description: description || undefined,
location: location || undefined,
latitude: latitude ? Number(latitude) : undefined,
longitude: longitude ? Number(longitude) : undefined,
@@ -95,6 +97,14 @@ export function ApiaryEditCard({ apiary }: { apiary: Apiary }) {
onChange={(event) => setName(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="apiary-description">Beschreibung</Label>
<Textarea
id="apiary-description"
value={description}
onChange={(event) => setDescription(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="apiary-location">Standort</Label>
<Input
@@ -2,12 +2,20 @@
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 { updateBottlingBatch } from "@/lib/bottling/api";
import { BottlingBatch } from "@/lib/bottling/types";
import { fetchProducts } from "@/lib/products/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,
@@ -24,6 +32,12 @@ export function BottlingEditDialog({ bottlingBatch }: { bottlingBatch: BottlingB
bottlingBatch.bestBeforeDate.slice(0, 10),
);
const [lotNumber, setLotNumber] = useState(bottlingBatch.lotNumber);
const [productId, setProductId] = useState(bottlingBatch.productId ?? "");
const { data: products } = useQuery({
queryKey: ["products", { status: "ACTIVE" }],
queryFn: () => fetchProducts({ status: "ACTIVE" }),
});
const mutation = useMutation({
mutationFn: () =>
@@ -31,6 +45,7 @@ export function BottlingEditDialog({ bottlingBatch }: { bottlingBatch: BottlingB
date: new Date(date).toISOString(),
bestBeforeDate: new Date(bestBeforeDate).toISOString(),
lotNumber,
productId: productId || undefined,
}),
onSuccess: () => {
void queryClient.invalidateQueries({
@@ -78,6 +93,21 @@ export function BottlingEditDialog({ bottlingBatch }: { bottlingBatch: BottlingB
onChange={(event) => setBestBeforeDate(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="bottling-edit-product">Produkt</Label>
<Select value={productId} onValueChange={(value) => setProductId(value ?? "")}>
<SelectTrigger id="bottling-edit-product">
<SelectValue placeholder="Produkt wählen" />
</SelectTrigger>
<SelectContent>
{products?.map((product) => (
<SelectItem key={product.id} value={product.id}>
{product.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="bottling-edit-lot">Los-/Chargennummer</Label>
<Input
@@ -4,11 +4,12 @@ import { FormEvent, useState } from "react";
import { Layers } from "lucide-react";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { updateColony } from "@/lib/colonies/api";
import { Colony, ColonyCondition, ColonyType } from "@/lib/colonies/types";
import { Colony, ColonyCondition, ColonyStatus, ColonyType } from "@/lib/colonies/types";
import {
COLONY_CONDITIONS,
COLONY_CONDITION_LABELS,
COLONY_CONDITION_TONE_CLASS,
COLONY_EDITABLE_STATUSES,
COLONY_STATUS_LABELS,
COLONY_STRENGTH_OPTIONS,
COLONY_TYPES,
@@ -74,6 +75,7 @@ function ConditionSelect({
export function ColonyEditCard({ colony }: { colony: Colony }) {
const queryClient = useQueryClient();
const [status, setStatus] = useState<ColonyStatus>(colony.status);
const [colonyType, setColonyType] = useState<ColonyType>(colony.colonyType);
const [hiveType, setHiveType] = useState(colony.hiveType ?? "");
const [originText, setOriginText] = useState(colony.originText ?? "");
@@ -92,6 +94,7 @@ export function ColonyEditCard({ colony }: { colony: Colony }) {
const updateMutation = useMutation({
mutationFn: () =>
updateColony(colony.id, {
status,
colonyType,
hiveType: hiveType || undefined,
originText: originText || undefined,
@@ -128,6 +131,21 @@ 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-status">Status</Label>
<Select value={status} onValueChange={(value) => setStatus(value as ColonyStatus)}>
<SelectTrigger id="colony-status">
<SelectValue>{COLONY_STATUS_LABELS[status]}</SelectValue>
</SelectTrigger>
<SelectContent>
{COLONY_EDITABLE_STATUSES.map((option) => (
<SelectItem key={option} value={option}>
{COLONY_STATUS_LABELS[option]}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="colony-type">Volktyp</Label>
<Select
@@ -33,6 +33,7 @@ export function FeedingEditDialog({ feeding }: { feeding: Feeding }) {
const [product, setProduct] = useState(feeding.product ?? "");
const [amount, setAmount] = useState(feeding.amount.toString());
const [unit, setUnit] = useState(feeding.unit);
const [batchNumber, setBatchNumber] = useState(feeding.batchNumber ?? "");
const [notes, setNotes] = useState(feeding.notes ?? "");
const mutation = useMutation({
@@ -43,6 +44,7 @@ export function FeedingEditDialog({ feeding }: { feeding: Feeding }) {
product: product || undefined,
amount: amount ? Number(amount) : undefined,
unit,
batchNumber: batchNumber || undefined,
notes: notes || undefined,
}),
onSuccess: () => {
@@ -127,6 +129,14 @@ export function FeedingEditDialog({ feeding }: { feeding: Feeding }) {
onChange={(event) => setProduct(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="feeding-edit-batch">Chargennummer</Label>
<Input
id="feeding-edit-batch"
value={batchNumber}
onChange={(event) => setBatchNumber(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="feeding-edit-notes">Notizen</Label>
<Textarea
@@ -34,12 +34,27 @@ export function InspectionEditDialog({ inspection }: { inspection: Inspection })
const queryClient = useQueryClient();
const [open, setOpen] = useState(false);
const [date, setDate] = useState(inspection.date.slice(0, 10));
const [weather, setWeather] = useState(inspection.weather ?? "");
const [temperature, setTemperature] = useState(inspection.temperature?.toString() ?? "");
const [colonyStrength, setColonyStrength] = useState(
inspection.colonyStrength?.toString() ?? "",
);
const [broodAmount, setBroodAmount] = useState(inspection.broodAmount ?? "");
const [broodPattern, setBroodPattern] = useState(inspection.broodPattern?.toString() ?? "");
const [openBroodPresent, setOpenBroodPresent] = useState(inspection.openBroodPresent ?? false);
const [cappedBroodPresent, setCappedBroodPresent] = useState(
inspection.cappedBroodPresent ?? false,
);
const [queenSeen, setQueenSeen] = useState(inspection.queenSeen ?? false);
const [eggsPresent, setEggsPresent] = useState(inspection.eggsPresent ?? true);
const [queenCellsPresent, setQueenCellsPresent] = useState(
inspection.queenCellsPresent ?? false,
);
const [swarmMood, setSwarmMood] = useState(inspection.swarmMood?.toString() ?? "");
const [gentleness, setGentleness] = useState(inspection.gentleness?.toString() ?? "");
const [combBehavior, setCombBehavior] = useState(inspection.combBehavior?.toString() ?? "");
const [feedStatus, setFeedStatus] = useState(inspection.feedStatus ?? "ausreichend");
const [honeySuperStatus, setHoneySuperStatus] = useState(inspection.honeySuperStatus ?? "");
const [healthIssues, setHealthIssues] = useState(inspection.healthIssues ?? "");
const [varroaIssues, setVarroaIssues] = useState(inspection.varroaIssues ?? "");
const [measures, setMeasures] = useState(inspection.measures ?? "");
@@ -49,10 +64,21 @@ export function InspectionEditDialog({ inspection }: { inspection: Inspection })
mutationFn: () =>
updateInspection(inspection.id, {
date: new Date(date).toISOString(),
weather: weather || undefined,
temperature: temperature ? Number(temperature) : undefined,
colonyStrength: colonyStrength ? Number(colonyStrength) : undefined,
broodAmount: broodAmount || undefined,
broodPattern: broodPattern ? Number(broodPattern) : undefined,
openBroodPresent,
cappedBroodPresent,
queenSeen,
eggsPresent,
queenCellsPresent,
swarmMood: swarmMood ? Number(swarmMood) : undefined,
gentleness: gentleness ? Number(gentleness) : undefined,
combBehavior: combBehavior ? Number(combBehavior) : undefined,
feedStatus,
honeySuperStatus: honeySuperStatus || undefined,
healthIssues: healthIssues || undefined,
varroaIssues: varroaIssues || undefined,
measures: measures || undefined,
@@ -95,6 +121,24 @@ export function InspectionEditDialog({ inspection }: { inspection: Inspection })
onChange={(event) => setDate(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="inspection-edit-weather">Wetter</Label>
<Input
id="inspection-edit-weather"
value={weather}
onChange={(event) => setWeather(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="inspection-edit-temperature">Temperatur (°C)</Label>
<Input
id="inspection-edit-temperature"
type="number"
inputMode="decimal"
value={temperature}
onChange={(event) => setTemperature(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="inspection-edit-strength">Volksstärke (Waben)</Label>
<Input
@@ -104,6 +148,48 @@ export function InspectionEditDialog({ inspection }: { inspection: Inspection })
onChange={(event) => setColonyStrength(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="inspection-edit-brood-amount">Brutmenge</Label>
<Input
id="inspection-edit-brood-amount"
value={broodAmount}
onChange={(event) => setBroodAmount(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="inspection-edit-brood-pattern">Brutbild (1–5)</Label>
<Input
id="inspection-edit-brood-pattern"
type="number"
min={1}
max={5}
value={broodPattern}
onChange={(event) => setBroodPattern(event.target.value)}
/>
</div>
<label className="flex items-center gap-3 text-sm" htmlFor="inspection-edit-open-brood">
<input
id="inspection-edit-open-brood"
type="checkbox"
className="size-5"
checked={openBroodPresent}
onChange={(event) => setOpenBroodPresent(event.target.checked)}
/>
Offene Brut vorhanden
</label>
<label
className="flex items-center gap-3 text-sm"
htmlFor="inspection-edit-capped-brood"
>
<input
id="inspection-edit-capped-brood"
type="checkbox"
className="size-5"
checked={cappedBroodPresent}
onChange={(event) => setCappedBroodPresent(event.target.checked)}
/>
Verdeckelte Brut vorhanden
</label>
<label className="flex items-center gap-3 text-sm" htmlFor="inspection-edit-queen">
<input
id="inspection-edit-queen"
@@ -124,6 +210,52 @@ export function InspectionEditDialog({ inspection }: { inspection: Inspection })
/>
Stifte vorhanden
</label>
<label
className="flex items-center gap-3 text-sm"
htmlFor="inspection-edit-queen-cells"
>
<input
id="inspection-edit-queen-cells"
type="checkbox"
className="size-5"
checked={queenCellsPresent}
onChange={(event) => setQueenCellsPresent(event.target.checked)}
/>
Weiselzellen vorhanden
</label>
<div className="flex flex-col gap-2">
<Label htmlFor="inspection-edit-swarm-mood">Schwarmstimmung (1–5)</Label>
<Input
id="inspection-edit-swarm-mood"
type="number"
min={1}
max={5}
value={swarmMood}
onChange={(event) => setSwarmMood(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="inspection-edit-gentleness">Sanftmut (1–5)</Label>
<Input
id="inspection-edit-gentleness"
type="number"
min={1}
max={5}
value={gentleness}
onChange={(event) => setGentleness(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="inspection-edit-comb-behavior">Wabensitz (1–5)</Label>
<Input
id="inspection-edit-comb-behavior"
type="number"
min={1}
max={5}
value={combBehavior}
onChange={(event) => setCombBehavior(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="inspection-edit-feed">Futterstatus</Label>
<Select
@@ -140,6 +272,14 @@ export function InspectionEditDialog({ inspection }: { inspection: Inspection })
</SelectContent>
</Select>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="inspection-edit-honey-super">Honigraumstatus</Label>
<Input
id="inspection-edit-honey-super"
value={honeySuperStatus}
onChange={(event) => setHoneySuperStatus(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="inspection-edit-varroa">Varroa-Befund</Label>
<Input
@@ -21,10 +21,14 @@ export function TreatmentEditDialog({ treatment }: { treatment: Treatment }) {
const queryClient = useQueryClient();
const [open, setOpen] = useState(false);
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) ?? "");
const [product, setProduct] = useState(treatment.product);
const [activeIngredient, setActiveIngredient] = useState(treatment.activeIngredient ?? "");
const [amount, setAmount] = useState(treatment.amount ?? "");
const [method, setMethod] = useState(treatment.method ?? "");
const [reason, setReason] = useState(treatment.reason ?? "");
const [batchNumber, setBatchNumber] = useState(treatment.batchNumber ?? "");
const [notes, setNotes] = useState(treatment.notes ?? "");
const [withdrawalPeriodDays, setWithdrawalPeriodDays] = useState(
treatment.withdrawalPeriodDays?.toString() ?? "",
@@ -34,10 +38,14 @@ export function TreatmentEditDialog({ treatment }: { treatment: Treatment }) {
mutationFn: () =>
updateTreatment(treatment.id, {
date: new Date(date).toISOString(),
startDate: startDate ? new Date(startDate).toISOString() : undefined,
endDate: endDate ? new Date(endDate).toISOString() : undefined,
product,
activeIngredient: activeIngredient || undefined,
amount: amount || undefined,
method: method || undefined,
reason: reason || undefined,
batchNumber: batchNumber || undefined,
notes: notes || undefined,
withdrawalPeriodDays: withdrawalPeriodDays ? Number(withdrawalPeriodDays) : undefined,
}),
@@ -78,6 +86,24 @@ export function TreatmentEditDialog({ treatment }: { treatment: Treatment }) {
onChange={(event) => setDate(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="treatment-edit-start-date">Beginn</Label>
<Input
id="treatment-edit-start-date"
type="date"
value={startDate}
onChange={(event) => setStartDate(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="treatment-edit-end-date">Ende</Label>
<Input
id="treatment-edit-end-date"
type="date"
value={endDate}
onChange={(event) => setEndDate(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="treatment-edit-product">Mittel</Label>
<Input
@@ -87,6 +113,14 @@ export function TreatmentEditDialog({ treatment }: { treatment: Treatment }) {
onChange={(event) => setProduct(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="treatment-edit-active-ingredient">Wirkstoff</Label>
<Input
id="treatment-edit-active-ingredient"
value={activeIngredient}
onChange={(event) => setActiveIngredient(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="treatment-edit-amount">Menge</Label>
<Input
@@ -111,6 +145,14 @@ export function TreatmentEditDialog({ treatment }: { treatment: Treatment }) {
onChange={(event) => setReason(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="treatment-edit-batch">Chargennummer</Label>
<Input
id="treatment-edit-batch"
value={batchNumber}
onChange={(event) => setBatchNumber(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="treatment-edit-withdrawal">Wartezeit (Tage)</Label>
<Input
@@ -31,6 +31,9 @@ export function VarroaEditDialog({ measurement }: { measurement: VarroaMeasureme
const [date, setDate] = useState(measurement.date.slice(0, 10));
const [method, setMethod] = useState<VarroaMethod>(measurement.method);
const [miteCount, setMiteCount] = useState(measurement.miteCount?.toString() ?? "");
const [durationMinutes, setDurationMinutes] = useState(
measurement.durationMinutes?.toString() ?? "",
);
const [calculatedValue, setCalculatedValue] = useState(
measurement.calculatedValue?.toString() ?? "",
);
@@ -43,6 +46,7 @@ export function VarroaEditDialog({ measurement }: { measurement: VarroaMeasureme
date: new Date(date).toISOString(),
method,
miteCount: miteCount ? Number(miteCount) : undefined,
durationMinutes: durationMinutes ? Number(durationMinutes) : undefined,
calculatedValue: calculatedValue ? Number(calculatedValue) : undefined,
assessment: assessment || undefined,
notes: notes || undefined,
@@ -111,6 +115,15 @@ export function VarroaEditDialog({ measurement }: { measurement: VarroaMeasureme
onChange={(event) => setMiteCount(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="varroa-edit-duration">Zeitraum (Minuten)</Label>
<Input
id="varroa-edit-duration"
type="number"
value={durationMinutes}
onChange={(event) => setDurationMinutes(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="varroa-edit-calculated">Milben pro Tag</Label>
<Input
@@ -46,6 +46,7 @@ export default function NewFeedingPage() {
const [amount, setAmount] = useState("");
const [unit, setUnit] = useState("l");
const [product, setProduct] = useState("");
const [batchNumber, setBatchNumber] = useState("");
const [notes, setNotes] = useState("");
const [error, setError] = useState<string | null>(null);
@@ -91,6 +92,7 @@ export default function NewFeedingPage() {
amount: Number(amount),
unit,
product: product || undefined,
batchNumber: batchNumber || undefined,
notes: notes || undefined,
});
}
@@ -182,6 +184,15 @@ export default function NewFeedingPage() {
onChange={(event) => setProduct(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="feeding-batch">Chargennummer</Label>
<Input
id="feeding-batch"
className="h-11"
value={batchNumber}
onChange={(event) => setBatchNumber(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="feeding-notes">Notizen</Label>
<Textarea
@@ -22,6 +22,7 @@ export function HarvestEditDialog({ harvest }: { harvest: Harvest }) {
const [open, setOpen] = useState(false);
const [date, setDate] = useState(harvest.date.slice(0, 10));
const [honeyType, setHoneyType] = useState(harvest.honeyType ?? "");
const [grossWeight, setGrossWeight] = useState(harvest.grossWeight?.toString() ?? "");
const [netWeight, setNetWeight] = useState(harvest.netWeight?.toString() ?? "");
const [honeySuperCount, setHoneySuperCount] = useState(
harvest.honeySuperCount?.toString() ?? "",
@@ -33,6 +34,7 @@ export function HarvestEditDialog({ harvest }: { harvest: Harvest }) {
updateHarvest(harvest.id, {
date: new Date(date).toISOString(),
honeyType: honeyType || undefined,
grossWeight: grossWeight ? Number(grossWeight) : undefined,
netWeight: netWeight ? Number(netWeight) : undefined,
honeySuperCount: honeySuperCount ? Number(honeySuperCount) : undefined,
notes: notes || undefined,
@@ -81,6 +83,15 @@ export function HarvestEditDialog({ harvest }: { harvest: Harvest }) {
/>
</div>
<div className="flex gap-3">
<div className="flex flex-1 flex-col gap-2">
<Label htmlFor="harvest-edit-gross-weight">Bruttogewicht (kg)</Label>
<Input
id="harvest-edit-gross-weight"
type="number"
value={grossWeight}
onChange={(event) => setGrossWeight(event.target.value)}
/>
</div>
<div className="flex flex-1 flex-col gap-2">
<Label htmlFor="harvest-edit-weight">Nettogewicht (kg)</Label>
<Input
@@ -41,6 +41,8 @@ export default function NewHarvestPage() {
const [date, setDate] = useState(todayIsoDate());
const [colonyIds, setColonyIds] = useState<string[]>([]);
const [honeyType, setHoneyType] = useState("");
const [honeySuperCount, setHoneySuperCount] = useState("");
const [grossWeight, setGrossWeight] = useState("");
const [netWeight, setNetWeight] = useState("");
const [notes, setNotes] = useState("");
const [error, setError] = useState<string | null>(null);
@@ -99,6 +101,8 @@ export default function NewHarvestPage() {
date: new Date(date).toISOString(),
colonyIds,
honeyType: honeyType || undefined,
honeySuperCount: honeySuperCount ? Number(honeySuperCount) : undefined,
grossWeight: grossWeight ? Number(grossWeight) : undefined,
netWeight: netWeight ? Number(netWeight) : undefined,
notes: notes || undefined,
});
@@ -192,6 +196,28 @@ export default function NewHarvestPage() {
onChange={(event) => setHoneyType(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="harvest-super-count">Honigräume</Label>
<Input
id="harvest-super-count"
type="number"
inputMode="numeric"
className="h-11"
value={honeySuperCount}
onChange={(event) => setHoneySuperCount(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="harvest-gross-weight">Bruttogewicht (kg)</Label>
<Input
id="harvest-gross-weight"
type="number"
inputMode="decimal"
className="h-11"
value={grossWeight}
onChange={(event) => setGrossWeight(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="harvest-net-weight">Nettogewicht (kg)</Label>
<Input
@@ -28,6 +28,7 @@ function ExtractionEditForm({
}) {
const queryClient = useQueryClient();
const [date, setDate] = useState(extraction.date.slice(0, 10));
const [amount, setAmount] = useState(extraction.amount?.toString() ?? "");
const [room, setRoom] = useState(extraction.room ?? "");
const [waterContent, setWaterContent] = useState(
extraction.waterContent?.toString() ?? "",
@@ -41,6 +42,7 @@ function ExtractionEditForm({
mutationFn: () =>
updateExtraction(extraction.id, {
date: new Date(date).toISOString(),
amount: amount ? Number(amount) : undefined,
room: room || undefined,
waterContent: waterContent ? Number(waterContent) : undefined,
temperature: temperature ? Number(temperature) : undefined,
@@ -70,6 +72,15 @@ function ExtractionEditForm({
onChange={(event) => setDate(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="extraction-edit-amount">Menge (kg)</Label>
<Input
id="extraction-edit-amount"
type="number"
value={amount}
onChange={(event) => setAmount(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="extraction-edit-room">Raum</Label>
<Input
@@ -46,13 +46,23 @@ export default function NewInspectionPage() {
const [colonyId, setColonyId] = useState(searchParams.get("colonyId") ?? "");
const [date, setDate] = useState(todayIsoDate());
const [weather, setWeather] = useState("");
const [temperature, setTemperature] = useState("");
const [colonyStrength, setColonyStrength] = useState("");
const [broodAmount, setBroodAmount] = useState("");
const [broodPattern, setBroodPattern] = useState("");
const [openBroodPresent, setOpenBroodPresent] = useState(false);
const [cappedBroodPresent, setCappedBroodPresent] = useState(false);
const [queenSeen, setQueenSeen] = useState(false);
const [eggsPresent, setEggsPresent] = useState(true);
const [queenCellsPresent, setQueenCellsPresent] = useState(false);
const [swarmMood, setSwarmMood] = useState("");
const [gentleness, setGentleness] = useState("");
const [combBehavior, setCombBehavior] = useState("");
const [feedStatus, setFeedStatus] = useState<"ausreichend" | "niedrig" | "kritisch">(
"ausreichend",
);
const [honeySuperStatus, setHoneySuperStatus] = useState("");
const [healthIssues, setHealthIssues] = useState("");
const [varroaIssues, setVarroaIssues] = useState("");
const [measures, setMeasures] = useState("");
@@ -90,11 +100,21 @@ export default function NewInspectionPage() {
mutation.mutate({
colonyId,
date: new Date(date).toISOString(),
weather: weather || undefined,
temperature: temperature ? Number(temperature) : undefined,
colonyStrength: colonyStrength ? Number(colonyStrength) : undefined,
broodAmount: broodAmount || undefined,
broodPattern: broodPattern ? Number(broodPattern) : undefined,
openBroodPresent,
cappedBroodPresent,
queenSeen,
eggsPresent,
queenCellsPresent,
swarmMood: swarmMood ? Number(swarmMood) : undefined,
gentleness: gentleness ? Number(gentleness) : undefined,
combBehavior: combBehavior ? Number(combBehavior) : undefined,
feedStatus,
honeySuperStatus: honeySuperStatus || undefined,
healthIssues: healthIssues || undefined,
varroaIssues: varroaIssues || undefined,
measures: measures || undefined,
@@ -139,6 +159,26 @@ export default function NewInspectionPage() {
onChange={(event) => setDate(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="inspection-weather">Wetter</Label>
<Input
id="inspection-weather"
className="h-11"
value={weather}
onChange={(event) => setWeather(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="inspection-temperature">Temperatur (°C)</Label>
<Input
id="inspection-temperature"
type="number"
inputMode="decimal"
className="h-11"
value={temperature}
onChange={(event) => setTemperature(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="inspection-strength">Volksstärke (Waben)</Label>
<Input
@@ -150,7 +190,54 @@ export default function NewInspectionPage() {
onChange={(event) => setColonyStrength(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="inspection-brood-amount">Brutmenge</Label>
<Input
id="inspection-brood-amount"
className="h-11"
value={broodAmount}
onChange={(event) => setBroodAmount(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="inspection-brood-pattern">Brutbild (1–5)</Label>
<Input
id="inspection-brood-pattern"
type="number"
min={1}
max={5}
className="h-11"
value={broodPattern}
onChange={(event) => setBroodPattern(event.target.value)}
/>
</div>
<div className="flex flex-col gap-3">
<label
className="flex h-11 items-center gap-3 text-sm"
htmlFor="inspection-open-brood"
>
<input
id="inspection-open-brood"
type="checkbox"
className="size-5"
checked={openBroodPresent}
onChange={(event) => setOpenBroodPresent(event.target.checked)}
/>
Offene Brut vorhanden
</label>
<label
className="flex h-11 items-center gap-3 text-sm"
htmlFor="inspection-capped-brood"
>
<input
id="inspection-capped-brood"
type="checkbox"
className="size-5"
checked={cappedBroodPresent}
onChange={(event) => setCappedBroodPresent(event.target.checked)}
/>
Verdeckelte Brut vorhanden
</label>
<label className="flex h-11 items-center gap-3 text-sm" htmlFor="inspection-queen-seen">
<input
id="inspection-queen-seen"
@@ -188,6 +275,42 @@ export default function NewInspectionPage() {
Weiselzellen vorhanden
</label>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="inspection-swarm-mood">Schwarmstimmung (1–5)</Label>
<Input
id="inspection-swarm-mood"
type="number"
min={1}
max={5}
className="h-11"
value={swarmMood}
onChange={(event) => setSwarmMood(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="inspection-gentleness">Sanftmut (1–5)</Label>
<Input
id="inspection-gentleness"
type="number"
min={1}
max={5}
className="h-11"
value={gentleness}
onChange={(event) => setGentleness(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="inspection-comb-behavior">Wabensitz (1–5)</Label>
<Input
id="inspection-comb-behavior"
type="number"
min={1}
max={5}
className="h-11"
value={combBehavior}
onChange={(event) => setCombBehavior(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="inspection-feed-status">Futterstatus</Label>
<Select
@@ -206,6 +329,15 @@ export default function NewInspectionPage() {
</SelectContent>
</Select>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="inspection-honey-super">Honigraumstatus</Label>
<Input
id="inspection-honey-super"
className="h-11"
value={honeySuperStatus}
onChange={(event) => setHoneySuperStatus(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="inspection-varroa">Varroa-Befund</Label>
<Input
@@ -32,8 +32,14 @@ export function QueenEditCard({ queen }: { queen: Queen }) {
const [hatchYear, setHatchYear] = useState(queen.hatchYear?.toString() ?? "");
const [breed, setBreed] = useState(queen.breed ?? "");
const [origin, setOrigin] = useState(queen.origin ?? "");
const [breeder, setBreeder] = useState(queen.breeder ?? "");
const [markingColor, setMarkingColor] = useState(queen.markingColor ?? "");
const [markingNumber, setMarkingNumber] = useState(queen.markingNumber ?? "");
const [matingType, setMatingType] = useState(queen.matingType ?? "");
const [matingStatus, setMatingStatus] = useState(queen.matingStatus ?? "");
const [inseminationData, setInseminationData] = useState(queen.inseminationData ?? "");
const [motherLine, setMotherLine] = useState(queen.motherLine ?? "");
const [fatherLine, setFatherLine] = useState(queen.fatherLine ?? "");
const [motherQueenId, setMotherQueenId] = useState(queen.motherQueenId ?? "");
const [notes, setNotes] = useState(queen.notes ?? "");
const [status, setStatus] = useState<QueenStatus>(queen.status);
@@ -58,8 +64,14 @@ export function QueenEditCard({ queen }: { queen: Queen }) {
hatchYear: hatchYear ? Number(hatchYear) : undefined,
breed: breed || undefined,
origin: origin || undefined,
breeder: breeder || undefined,
markingColor: markingColor || undefined,
markingNumber: markingNumber || undefined,
matingType: matingType || undefined,
matingStatus: matingStatus || undefined,
inseminationData: inseminationData || undefined,
motherLine: motherLine || undefined,
fatherLine: fatherLine || undefined,
motherQueenId: motherQueenId || undefined,
notes: notes || undefined,
status,
@@ -117,6 +129,14 @@ export function QueenEditCard({ queen }: { queen: Queen }) {
onChange={(event) => setOrigin(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="queen-breeder">Züchter</Label>
<Input
id="queen-breeder"
value={breeder}
onChange={(event) => setBreeder(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="queen-marking-color">Zeichenfarbe</Label>
<Input
@@ -133,6 +153,46 @@ export function QueenEditCard({ queen }: { queen: Queen }) {
onChange={(event) => setMarkingNumber(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="queen-mating-type">Begattungsart</Label>
<Input
id="queen-mating-type"
value={matingType}
onChange={(event) => setMatingType(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="queen-mating-status">Begattungsstatus</Label>
<Input
id="queen-mating-status"
value={matingStatus}
onChange={(event) => setMatingStatus(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="queen-insemination-data">Besamungsdaten</Label>
<Input
id="queen-insemination-data"
value={inseminationData}
onChange={(event) => setInseminationData(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="queen-mother-line">Mutterlinie</Label>
<Input
id="queen-mother-line"
value={motherLine}
onChange={(event) => setMotherLine(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="queen-father-line">Vaterlinie</Label>
<Input
id="queen-father-line"
value={fatherLine}
onChange={(event) => setFatherLine(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="queen-mother">Mutter</Label>
<Select
@@ -2,12 +2,13 @@
import { FormEvent, useState } from "react";
import { useRouter } from "next/navigation";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { toast } from "sonner";
import { ListTodo } from "lucide-react";
import { createTask } from "@/lib/tasks/api";
import { TaskPriority } 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";
@@ -29,8 +30,17 @@ export default function NewTaskPage() {
const [description, setDescription] = useState("");
const [priority, setPriority] = useState<TaskPriority>("NORMAL");
const [dueDate, setDueDate] = useState("");
const [assignedUserId, setAssignedUserId] = useState("");
const [error, setError] = useState<string | null>(null);
// Assigning a task requires users:read, which not every role has - if the
// request fails, the assignee field below is simply omitted.
const { data: users } = useQuery({
queryKey: ["users"],
queryFn: () => fetchUsers(),
retry: false,
});
const mutation = useMutation({
mutationFn: createTask,
onSuccess: () => {
@@ -51,6 +61,7 @@ export default function NewTaskPage() {
description: description || undefined,
priority,
dueDate: dueDate ? new Date(dueDate).toISOString() : undefined,
assignedUserId: assignedUserId || undefined,
});
}
@@ -92,6 +103,34 @@ export default function NewTaskPage() {
</SelectContent>
</Select>
</div>
{users ? (
<div className="flex flex-col gap-2">
<Label htmlFor="task-assignee">Zugewiesen an</Label>
<Select
value={assignedUserId || "NONE"}
onValueChange={(value) =>
setAssignedUserId(value === "NONE" ? "" : (value ?? ""))
}
>
<SelectTrigger id="task-assignee">
<SelectValue placeholder="Niemand zugewiesen">
{assignedUserId
? (users.find((user) => user.id === assignedUserId)?.name ??
"Niemand zugewiesen")
: "Niemand zugewiesen"}
</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectItem value="NONE">Niemand zugewiesen</SelectItem>
{users.map((user) => (
<SelectItem key={user.id} value={user.id}>
{user.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
) : null}
<div className="flex flex-col gap-2">
<Label htmlFor="task-due-date">Fällig am</Label>
<Input
@@ -2,10 +2,11 @@
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 { updateTask } from "@/lib/tasks/api";
import { Task, TaskPriority } 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";
@@ -32,6 +33,16 @@ export function TaskEditDialog({ task }: { task: Task }) {
const [description, setDescription] = useState(task.description ?? "");
const [priority, setPriority] = useState<TaskPriority>(task.priority);
const [dueDate, setDueDate] = useState(task.dueDate?.slice(0, 10) ?? "");
const [assignedUserId, setAssignedUserId] = useState(task.assignedUserId ?? "");
// Assigning a task requires users:read, which not every role has - if the
// request fails, the assignee field below is simply omitted rather than
// breaking the rest of the edit dialog.
const { data: users } = useQuery({
queryKey: ["users"],
queryFn: () => fetchUsers(),
retry: false,
});
const mutation = useMutation({
mutationFn: () =>
@@ -40,6 +51,7 @@ export function TaskEditDialog({ task }: { task: Task }) {
description: description || undefined,
priority,
dueDate: dueDate ? new Date(dueDate).toISOString() : undefined,
assignedUserId: assignedUserId || undefined,
}),
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ["tasks"] });
@@ -94,6 +106,31 @@ export function TaskEditDialog({ task }: { task: Task }) {
</SelectContent>
</Select>
</div>
{users ? (
<div className="flex flex-col gap-2">
<Label htmlFor="task-edit-assignee">Zugewiesen an</Label>
<Select
value={assignedUserId || "NONE"}
onValueChange={(value) => setAssignedUserId(value === "NONE" ? "" : (value ?? ""))}
>
<SelectTrigger id="task-edit-assignee" className="w-full">
<SelectValue placeholder="Niemand zugewiesen">
{assignedUserId
? (users.find((user) => user.id === assignedUserId)?.name ?? "Niemand zugewiesen")
: "Niemand zugewiesen"}
</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectItem value="NONE">Niemand zugewiesen</SelectItem>
{users.map((user) => (
<SelectItem key={user.id} value={user.id}>
{user.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
) : null}
<div className="flex flex-col gap-2">
<Label htmlFor="task-edit-due">Fällig am</Label>
<Input
@@ -40,10 +40,14 @@ export default function NewTreatmentPage() {
const [colonyId, setColonyId] = useState(searchParams.get("colonyId") ?? "");
const [date, setDate] = useState(todayIsoDate());
const [startDate, setStartDate] = useState("");
const [endDate, setEndDate] = useState("");
const [product, setProduct] = useState("");
const [activeIngredient, setActiveIngredient] = useState("");
const [amount, setAmount] = useState("");
const [method, setMethod] = useState("");
const [reason, setReason] = useState("");
const [batchNumber, setBatchNumber] = useState("");
const [notes, setNotes] = useState("");
const [withdrawalPeriodDays, setWithdrawalPeriodDays] = useState("");
const [error, setError] = useState<string | null>(null);
@@ -79,10 +83,14 @@ export default function NewTreatmentPage() {
mutation.mutate({
colonyId,
date: new Date(date).toISOString(),
startDate: startDate ? new Date(startDate).toISOString() : undefined,
endDate: endDate ? new Date(endDate).toISOString() : undefined,
product,
activeIngredient: activeIngredient || undefined,
amount: amount || undefined,
method: method || undefined,
reason: reason || undefined,
batchNumber: batchNumber || undefined,
notes: notes || undefined,
withdrawalPeriodDays: withdrawalPeriodDays ? Number(withdrawalPeriodDays) : undefined,
});
@@ -125,6 +133,26 @@ export default function NewTreatmentPage() {
onChange={(event) => setDate(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="treatment-start-date">Beginn</Label>
<Input
id="treatment-start-date"
type="date"
className="h-11"
value={startDate}
onChange={(event) => setStartDate(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="treatment-end-date">Ende</Label>
<Input
id="treatment-end-date"
type="date"
className="h-11"
value={endDate}
onChange={(event) => setEndDate(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="treatment-product">Mittel</Label>
<Input
@@ -135,6 +163,15 @@ export default function NewTreatmentPage() {
onChange={(event) => setProduct(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="treatment-active-ingredient">Wirkstoff</Label>
<Input
id="treatment-active-ingredient"
className="h-11"
value={activeIngredient}
onChange={(event) => setActiveIngredient(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="treatment-amount">Menge</Label>
<Input
@@ -162,6 +199,15 @@ export default function NewTreatmentPage() {
onChange={(event) => setReason(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="treatment-batch">Chargennummer</Label>
<Input
id="treatment-batch"
className="h-11"
value={batchNumber}
onChange={(event) => setBatchNumber(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="treatment-withdrawal">Wartezeit (Tage)</Label>
<Input