feat(frontend): Behandlungen-Modul (Modul 8)

Mobile-optimiertes Erfassungsformular unter /treatments/new sowie
"Behandlungen"-Ansicht auf der Volk-Detailseite.
This commit is contained in:
Nick Szaunig committed 2026-08-22 00:10:46 +02:00
1 parent 61e0bbe794
commit 1f71ac9e3c
6 files changed
+261 -1

No files matched your search

@@ -0,0 +1,47 @@
"use client";
import Link from "next/link";
import { useQuery } from "@tanstack/react-query";
import { fetchColonyTreatments } from "@/lib/treatments/api";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
export function ColonyTreatmentsCard({ colonyId }: { colonyId: string }) {
const { data: treatments, isLoading } = useQuery({
queryKey: ["colonies", colonyId, "treatments"],
queryFn: () => fetchColonyTreatments(colonyId),
});
return (
<Card className="max-w-lg">
<CardHeader className="flex flex-row items-center justify-between">
<CardTitle>Behandlungen</CardTitle>
<Button
size="sm"
nativeButton={false}
render={<Link href={`/treatments/new?colonyId=${colonyId}`} />}
>
Neue Behandlung
</Button>
</CardHeader>
<CardContent className="flex flex-col gap-3 text-sm">
{isLoading ? <p className="text-muted-foreground">Lädt…</p> : null}
{!isLoading && (!treatments || treatments.length === 0) ? (
<p className="text-muted-foreground">Noch keine Behandlung dokumentiert.</p>
) : 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>
<p className="text-muted-foreground">
{treatment.amount ?? "—"}
{treatment.method ? ` · ${treatment.method}` : ""}
</p>
{treatment.reason ? <p>Grund: {treatment.reason}</p> : null}
</div>
))}
</CardContent>
</Card>
);
}
@@ -8,6 +8,7 @@ import { ColonyActions } from "./colony-actions";
import { ColonyHistoryCard } from "./colony-history";
import { ColonyInspectionsCard } from "./colony-inspections";
import { ColonyVarroaCard } from "./colony-varroa";
import { ColonyTreatmentsCard } from "./colony-treatments";
export default function ColonyDetailPage() {
const params = useParams<{ id: string }>();
@@ -33,6 +34,7 @@ export default function ColonyDetailPage() {
<ColonyActions colony={colony} />
<ColonyInspectionsCard colonyId={colony.id} />
<ColonyVarroaCard colonyId={colony.id} />
<ColonyTreatmentsCard colonyId={colony.id} />
<ColonyHistoryCard colonyId={colony.id} />
</main>
);
@@ -0,0 +1,168 @@
"use client";
import { FormEvent, useState } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { fetchColonies } from "@/lib/colonies/api";
import { createTreatment } from "@/lib/treatments/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 { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
function todayIsoDate(): string {
return new Date().toISOString().slice(0, 10);
}
export default function NewTreatmentPage() {
const router = useRouter();
const searchParams = useSearchParams();
const queryClient = useQueryClient();
const { data: colonies } = useQuery({
queryKey: ["colonies"],
queryFn: fetchColonies,
});
const activeColonies = colonies?.filter((colony) => colony.status === "ACTIVE") ?? [];
const [colonyId, setColonyId] = useState(searchParams.get("colonyId") ?? "");
const [date, setDate] = useState(todayIsoDate());
const [product, setProduct] = useState("");
const [amount, setAmount] = useState("");
const [method, setMethod] = useState("");
const [reason, setReason] = useState("");
const [notes, setNotes] = useState("");
const [error, setError] = useState<string | null>(null);
const mutation = useMutation({
mutationFn: createTreatment,
onSuccess: (treatment) => {
void queryClient.invalidateQueries({
queryKey: ["colonies", treatment.colonyId, "treatments"],
});
router.push(`/colonies/${treatment.colonyId}`);
},
onError: () => {
setError("Behandlung konnte nicht gespeichert werden.");
},
});
function handleSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
setError(null);
if (!colonyId) {
setError("Bitte ein Volk auswählen.");
return;
}
mutation.mutate({
colonyId,
date: new Date(date).toISOString(),
product,
amount: amount || undefined,
method: method || undefined,
reason: reason || undefined,
notes: notes || undefined,
});
}
return (
<main className="p-4 sm:p-6">
<Card className="max-w-lg">
<CardHeader>
<CardTitle>Neue Behandlung</CardTitle>
</CardHeader>
<CardContent>
<form className="flex flex-col gap-5" onSubmit={handleSubmit}>
<div className="flex flex-col gap-2">
<Label htmlFor="treatment-colony">Volk</Label>
<Select value={colonyId} onValueChange={(value) => setColonyId(value ?? "")}>
<SelectTrigger id="treatment-colony" className="h-11 w-full">
<SelectValue placeholder="Volk wählen" />
</SelectTrigger>
<SelectContent>
{activeColonies.map((colony) => (
<SelectItem key={colony.id} value={colony.id}>
{colony.code}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="treatment-date">Datum</Label>
<Input
id="treatment-date"
type="date"
required
className="h-11"
value={date}
onChange={(event) => setDate(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="treatment-product">Mittel</Label>
<Input
id="treatment-product"
required
className="h-11"
value={product}
onChange={(event) => setProduct(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="treatment-amount">Menge</Label>
<Input
id="treatment-amount"
className="h-11"
value={amount}
onChange={(event) => setAmount(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="treatment-method">Methode</Label>
<Input
id="treatment-method"
className="h-11"
value={method}
onChange={(event) => setMethod(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="treatment-reason">Grund</Label>
<Input
id="treatment-reason"
className="h-11"
value={reason}
onChange={(event) => setReason(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="treatment-notes">Notizen</Label>
<Textarea
id="treatment-notes"
value={notes}
onChange={(event) => setNotes(event.target.value)}
/>
</div>
{error ? (
<p role="alert" className="text-sm text-red-600">
{error}
</p>
) : null}
<Button type="submit" className="h-12 text-base" disabled={mutation.isPending}>
{mutation.isPending ? "Wird gespeichert…" : "Behandlung speichern"}
</Button>
</form>
</CardContent>
</Card>
</main>
);
}
@@ -0,0 +1,13 @@
import { apiFetch } from "@/lib/auth/api-client";
import { CreateTreatmentInput, Treatment } from "./types";
export function fetchColonyTreatments(colonyId: string): Promise<Treatment[]> {
return apiFetch<Treatment[]>(`/colonies/${colonyId}/treatments`);
}
export function createTreatment(input: CreateTreatmentInput): Promise<Treatment> {
return apiFetch<Treatment>("/treatments", {
method: "POST",
body: JSON.stringify(input),
});
}
@@ -0,0 +1,30 @@
export interface Treatment {
id: string;
colonyId: string;
userId: string;
date: string;
startDate: string | null;
endDate: string | null;
product: string;
activeIngredient: string | null;
amount: string | null;
method: string | null;
reason: string | null;
batchNumber: string | null;
notes: string | null;
createdAt: string;
}
export interface CreateTreatmentInput {
colonyId: string;
date: string;
startDate?: string;
endDate?: string;
product: string;
activeIngredient?: string;
amount?: string;
method?: string;
reason?: string;
batchNumber?: string;
notes?: string;
}
+1 -1
View File
@@ -75,7 +75,7 @@ Abgeleitet aus [`docs/lastenheft.md`](docs/lastenheft.md) Abschnitt 26. Reihenfo
- Endpunkte: `GET/POST /api/v1/treatments`, `GET /api/v1/colonies/:id/treatments`
- Frontend: `/treatments/new`, `/colonies/[id]` Tab "Behandlungen"
- [x] Backend-Modul fertig, getestet, grün, committet
- [ ] Frontend-Views fertig, getestet, grün, committet
- [x] Frontend-Views fertig, getestet, grün, committet
### 9. Fütterungen