feat: warn against harvesting honey during a treatment's Wartezeit
Treatments (Ameisensäure, Oxalsäure, etc.) can require a legally mandated withdrawal period before honey from that colony may be sold. Nothing in the app tracked this before, so a beekeeper had no safeguard against harvesting into an active withdrawal window. Treatments now optionally record withdrawalPeriodDays (from the product's Packungsbeilage). The new harvest form fetches each candidate colony's active withdrawal window and shows an inline warning naming the product and the date it clears, computed as max(endDate ?? date) + withdrawalPeriodDays across a colony's treatments, restricted to windows still in the future. Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
1 parent
0881a38800
commit
d42f8331f9
13 files changed
+270
-56
No files matched your search
@@ -49,6 +49,9 @@ export function ColonyTreatmentsCard({ colonyId }: { colonyId: string }) {
|
||||
{treatment.method ? ` · ${treatment.method}` : ""}
|
||||
</p>
|
||||
{treatment.reason ? <p>Grund: {treatment.reason}</p> : null}
|
||||
{treatment.withdrawalPeriodDays !== null ? (
|
||||
<p>Wartezeit: {treatment.withdrawalPeriodDays} Tag(e)</p>
|
||||
) : null}
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
|
||||
@@ -26,6 +26,9 @@ export function TreatmentEditDialog({ treatment }: { treatment: Treatment }) {
|
||||
const [method, setMethod] = useState(treatment.method ?? "");
|
||||
const [reason, setReason] = useState(treatment.reason ?? "");
|
||||
const [notes, setNotes] = useState(treatment.notes ?? "");
|
||||
const [withdrawalPeriodDays, setWithdrawalPeriodDays] = useState(
|
||||
treatment.withdrawalPeriodDays?.toString() ?? "",
|
||||
);
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: () =>
|
||||
@@ -36,6 +39,7 @@ export function TreatmentEditDialog({ treatment }: { treatment: Treatment }) {
|
||||
method: method || undefined,
|
||||
reason: reason || undefined,
|
||||
notes: notes || undefined,
|
||||
withdrawalPeriodDays: withdrawalPeriodDays ? Number(withdrawalPeriodDays) : undefined,
|
||||
}),
|
||||
onSuccess: () => {
|
||||
void queryClient.invalidateQueries({
|
||||
@@ -107,6 +111,17 @@ export function TreatmentEditDialog({ treatment }: { treatment: Treatment }) {
|
||||
onChange={(event) => setReason(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="treatment-edit-withdrawal">Wartezeit (Tage)</Label>
|
||||
<Input
|
||||
id="treatment-edit-withdrawal"
|
||||
type="number"
|
||||
inputMode="numeric"
|
||||
min={0}
|
||||
value={withdrawalPeriodDays}
|
||||
onChange={(event) => setWithdrawalPeriodDays(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="treatment-edit-notes">Notizen</Label>
|
||||
<Textarea
|
||||
|
||||
@@ -4,10 +4,11 @@ import { FormEvent, useState } from "react";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { toast } from "sonner";
|
||||
import { Boxes } from "lucide-react";
|
||||
import { Boxes, AlertTriangle } from "lucide-react";
|
||||
import { fetchApiaries } from "@/lib/apiaries/api";
|
||||
import { fetchColonies } from "@/lib/colonies/api";
|
||||
import { createHarvest } from "@/lib/harvests/api";
|
||||
import { fetchWithdrawalStatus } from "@/lib/treatments/api";
|
||||
import { ApiError } from "@/lib/auth/api-client";
|
||||
import { queueHarvest } from "@/lib/offline/db";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -47,6 +48,15 @@ export default function NewHarvestPage() {
|
||||
const apiaryColonies = (colonies ?? []).filter(
|
||||
(colony) => colony.apiaryId === apiaryId && colony.status === "ACTIVE",
|
||||
);
|
||||
const apiaryColonyIds = apiaryColonies.map((colony) => colony.id);
|
||||
const { data: withdrawalStatuses } = useQuery({
|
||||
queryKey: ["treatments", "withdrawal-status", apiaryColonyIds],
|
||||
queryFn: () => fetchWithdrawalStatus(apiaryColonyIds),
|
||||
enabled: apiaryColonyIds.length > 0,
|
||||
});
|
||||
const withdrawalByColony = new Map(
|
||||
(withdrawalStatuses ?? []).map((status) => [status.colonyId, status]),
|
||||
);
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: createHarvest,
|
||||
@@ -144,22 +154,33 @@ export default function NewHarvestPage() {
|
||||
</p>
|
||||
) : null}
|
||||
<div className="flex flex-col gap-2">
|
||||
{apiaryColonies.map((colony) => (
|
||||
<label
|
||||
key={colony.id}
|
||||
className="flex h-11 items-center gap-3 text-sm"
|
||||
htmlFor={`harvest-colony-${colony.id}`}
|
||||
>
|
||||
<input
|
||||
id={`harvest-colony-${colony.id}`}
|
||||
type="checkbox"
|
||||
className="size-5"
|
||||
checked={colonyIds.includes(colony.id)}
|
||||
onChange={(event) => toggleColony(colony.id, event.target.checked)}
|
||||
/>
|
||||
{colony.code}
|
||||
</label>
|
||||
))}
|
||||
{apiaryColonies.map((colony) => {
|
||||
const withdrawal = withdrawalByColony.get(colony.id);
|
||||
return (
|
||||
<div key={colony.id} className="flex flex-col gap-1">
|
||||
<label
|
||||
className="flex h-11 items-center gap-3 text-sm"
|
||||
htmlFor={`harvest-colony-${colony.id}`}
|
||||
>
|
||||
<input
|
||||
id={`harvest-colony-${colony.id}`}
|
||||
type="checkbox"
|
||||
className="size-5"
|
||||
checked={colonyIds.includes(colony.id)}
|
||||
onChange={(event) => toggleColony(colony.id, event.target.checked)}
|
||||
/>
|
||||
{colony.code}
|
||||
</label>
|
||||
{withdrawal ? (
|
||||
<p className="flex items-center gap-1.5 pl-8 text-xs text-destructive">
|
||||
<AlertTriangle className="size-3.5 shrink-0" />
|
||||
Wartezeit ({withdrawal.product}) bis{" "}
|
||||
{new Date(withdrawal.activeUntil).toLocaleDateString("de-DE")}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
|
||||
@@ -45,6 +45,7 @@ export default function NewTreatmentPage() {
|
||||
const [method, setMethod] = useState("");
|
||||
const [reason, setReason] = useState("");
|
||||
const [notes, setNotes] = useState("");
|
||||
const [withdrawalPeriodDays, setWithdrawalPeriodDays] = useState("");
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const mutation = useMutation({
|
||||
@@ -83,6 +84,7 @@ export default function NewTreatmentPage() {
|
||||
method: method || undefined,
|
||||
reason: reason || undefined,
|
||||
notes: notes || undefined,
|
||||
withdrawalPeriodDays: withdrawalPeriodDays ? Number(withdrawalPeriodDays) : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -160,6 +162,22 @@ export default function NewTreatmentPage() {
|
||||
onChange={(event) => setReason(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="treatment-withdrawal">Wartezeit (Tage)</Label>
|
||||
<Input
|
||||
id="treatment-withdrawal"
|
||||
type="number"
|
||||
inputMode="numeric"
|
||||
min={0}
|
||||
className="h-11"
|
||||
value={withdrawalPeriodDays}
|
||||
onChange={(event) => setWithdrawalPeriodDays(event.target.value)}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Laut Packungsbeilage des Mittels – solange darf aus diesem Volk kein Honig
|
||||
geerntet werden.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="treatment-notes">Notizen</Label>
|
||||
<Textarea
|
||||
|
||||
@@ -1,10 +1,26 @@
|
||||
import { apiFetch } from "@/lib/auth/api-client";
|
||||
import { CreateTreatmentInput, Treatment, UpdateTreatmentInput } from "./types";
|
||||
import {
|
||||
CreateTreatmentInput,
|
||||
Treatment,
|
||||
UpdateTreatmentInput,
|
||||
WithdrawalStatus,
|
||||
} from "./types";
|
||||
|
||||
export function fetchColonyTreatments(colonyId: string): Promise<Treatment[]> {
|
||||
return apiFetch<Treatment[]>(`/colonies/${colonyId}/treatments`);
|
||||
}
|
||||
|
||||
export function fetchWithdrawalStatus(
|
||||
colonyIds: string[],
|
||||
): Promise<WithdrawalStatus[]> {
|
||||
if (colonyIds.length === 0) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
return apiFetch<WithdrawalStatus[]>(
|
||||
`/treatments/withdrawal-status?colonyIds=${colonyIds.join(",")}`,
|
||||
);
|
||||
}
|
||||
|
||||
export function createTreatment(input: CreateTreatmentInput): Promise<Treatment> {
|
||||
return apiFetch<Treatment>("/treatments", {
|
||||
method: "POST",
|
||||
|
||||
@@ -12,6 +12,7 @@ export interface Treatment {
|
||||
reason: string | null;
|
||||
batchNumber: string | null;
|
||||
notes: string | null;
|
||||
withdrawalPeriodDays: number | null;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
@@ -27,6 +28,7 @@ export interface CreateTreatmentInput {
|
||||
reason?: string;
|
||||
batchNumber?: string;
|
||||
notes?: string;
|
||||
withdrawalPeriodDays?: number;
|
||||
}
|
||||
|
||||
export interface UpdateTreatmentInput {
|
||||
@@ -40,4 +42,11 @@ export interface UpdateTreatmentInput {
|
||||
reason?: string;
|
||||
batchNumber?: string;
|
||||
notes?: string;
|
||||
withdrawalPeriodDays?: number;
|
||||
}
|
||||
|
||||
export interface WithdrawalStatus {
|
||||
colonyId: string;
|
||||
activeUntil: string;
|
||||
product: string;
|
||||
}
|
||||
Reference in new issue
Block a user