feat(frontend): Durchsichten-Modul (Modul 5)

Mobile-optimiertes Schnellformular unter /inspections/new sowie
"Durchsichten"-Ansicht auf der Volk-Detailseite.
This commit is contained in:
Nick Szaunig committed 2026-08-21 18:51:47 +02:00
1 parent 9b59e4bb35
commit 829753ec90
6 files changed
+347 -1

No files matched your search

@@ -0,0 +1,53 @@
"use client";
import Link from "next/link";
import { useQuery } from "@tanstack/react-query";
import { fetchColonyInspections } from "@/lib/inspections/api";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
export function ColonyInspectionsCard({ colonyId }: { colonyId: string }) {
const { data: inspections, isLoading } = useQuery({
queryKey: ["colonies", colonyId, "inspections"],
queryFn: () => fetchColonyInspections(colonyId),
});
return (
<Card className="max-w-lg">
<CardHeader className="flex flex-row items-center justify-between">
<CardTitle>Durchsichten</CardTitle>
<Button
size="sm"
nativeButton={false}
render={<Link href={`/inspections/new?colonyId=${colonyId}`} />}
>
Neue Durchsicht
</Button>
</CardHeader>
<CardContent className="flex flex-col gap-3 text-sm">
{isLoading ? <p className="text-muted-foreground">Lädt…</p> : null}
{!isLoading && (!inspections || inspections.length === 0) ? (
<p className="text-muted-foreground">Noch keine Durchsicht dokumentiert.</p>
) : null}
{inspections?.map((inspection) => (
<div key={inspection.id} className="rounded-md border p-3">
<p className="font-medium">
{new Date(inspection.date).toLocaleDateString("de-DE")}
</p>
<p className="text-muted-foreground">
{inspection.colonyStrength !== null
? `${inspection.colonyStrength} Waben`
: "Stärke unbekannt"}
{inspection.queenSeen !== null
? ` · Königin ${inspection.queenSeen ? "gesehen" : "nicht gesehen"}`
: ""}
</p>
{inspection.healthIssues ? <p>Gesundheit: {inspection.healthIssues}</p> : null}
{inspection.varroaIssues ? <p>Varroa: {inspection.varroaIssues}</p> : null}
{inspection.measures ? <p>Maßnahmen: {inspection.measures}</p> : null}
</div>
))}
</CardContent>
</Card>
);
}
@@ -6,6 +6,7 @@ import { fetchColony } from "@/lib/colonies/api";
import { ColonyEditCard } from "./colony-edit-card";
import { ColonyActions } from "./colony-actions";
import { ColonyHistoryCard } from "./colony-history";
import { ColonyInspectionsCard } from "./colony-inspections";
export default function ColonyDetailPage() {
const params = useParams<{ id: string }>();
@@ -29,6 +30,7 @@ export default function ColonyDetailPage() {
<main className="flex flex-col gap-4 p-4 sm:p-6">
<ColonyEditCard key={colony.id} colony={colony} />
<ColonyActions colony={colony} />
<ColonyInspectionsCard colonyId={colony.id} />
<ColonyHistoryCard colonyId={colony.id} />
</main>
);
@@ -0,0 +1,209 @@
"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 { createInspection } from "@/lib/inspections/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 NewInspectionPage() {
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 [colonyStrength, setColonyStrength] = useState("");
const [queenSeen, setQueenSeen] = useState(false);
const [queenCellsPresent, setQueenCellsPresent] = useState(false);
const [feedStatusOk, setFeedStatusOk] = useState(true);
const [healthIssues, setHealthIssues] = useState("");
const [varroaIssues, setVarroaIssues] = useState("");
const [measures, setMeasures] = useState("");
const [notes, setNotes] = useState("");
const [error, setError] = useState<string | null>(null);
const mutation = useMutation({
mutationFn: createInspection,
onSuccess: (inspection) => {
void queryClient.invalidateQueries({
queryKey: ["colonies", inspection.colonyId, "inspections"],
});
router.push(`/colonies/${inspection.colonyId}`);
},
onError: () => {
setError("Durchsicht 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(),
colonyStrength: colonyStrength ? Number(colonyStrength) : undefined,
queenSeen,
queenCellsPresent,
feedStatus: feedStatusOk ? "ausreichend" : "niedrig",
healthIssues: healthIssues || undefined,
varroaIssues: varroaIssues || undefined,
measures: measures || undefined,
notes: notes || undefined,
});
}
return (
<main className="p-4 sm:p-6">
<Card className="max-w-lg">
<CardHeader>
<CardTitle>Neue Durchsicht</CardTitle>
</CardHeader>
<CardContent>
<form className="flex flex-col gap-5" onSubmit={handleSubmit}>
<div className="flex flex-col gap-2">
<Label htmlFor="inspection-colony">Volk</Label>
<Select value={colonyId} onValueChange={(value) => setColonyId(value ?? "")}>
<SelectTrigger id="inspection-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="inspection-date">Datum</Label>
<Input
id="inspection-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="inspection-strength">Volksstärke (Waben)</Label>
<Input
id="inspection-strength"
type="number"
inputMode="numeric"
className="h-11"
value={colonyStrength}
onChange={(event) => setColonyStrength(event.target.value)}
/>
</div>
<div className="flex flex-col gap-3">
<label className="flex h-11 items-center gap-3 text-sm" htmlFor="inspection-queen-seen">
<input
id="inspection-queen-seen"
type="checkbox"
className="size-5"
checked={queenSeen}
onChange={(event) => setQueenSeen(event.target.checked)}
/>
Königin gesehen
</label>
<label
className="flex h-11 items-center gap-3 text-sm"
htmlFor="inspection-queen-cells"
>
<input
id="inspection-queen-cells"
type="checkbox"
className="size-5"
checked={queenCellsPresent}
onChange={(event) => setQueenCellsPresent(event.target.checked)}
/>
Weiselzellen vorhanden
</label>
<label className="flex h-11 items-center gap-3 text-sm" htmlFor="inspection-feed-ok">
<input
id="inspection-feed-ok"
type="checkbox"
className="size-5"
checked={feedStatusOk}
onChange={(event) => setFeedStatusOk(event.target.checked)}
/>
Futtervorrat ausreichend
</label>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="inspection-varroa">Varroa-Befund</Label>
<Input
id="inspection-varroa"
className="h-11"
value={varroaIssues}
onChange={(event) => setVarroaIssues(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="inspection-health">Gesundheitsauffälligkeiten</Label>
<Input
id="inspection-health"
className="h-11"
value={healthIssues}
onChange={(event) => setHealthIssues(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="inspection-measures">Maßnahmen</Label>
<Textarea
id="inspection-measures"
value={measures}
onChange={(event) => setMeasures(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="inspection-notes">Notizen</Label>
<Textarea
id="inspection-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…" : "Durchsicht speichern"}
</Button>
</form>
</CardContent>
</Card>
</main>
);
}
@@ -0,0 +1,28 @@
import { apiFetch } from "@/lib/auth/api-client";
import {
CreateInspectionInput,
CreateTaskFromInspectionInput,
Inspection,
InspectionTask,
} from "./types";
export function fetchColonyInspections(colonyId: string): Promise<Inspection[]> {
return apiFetch<Inspection[]>(`/colonies/${colonyId}/inspections`);
}
export function createInspection(input: CreateInspectionInput): Promise<Inspection> {
return apiFetch<Inspection>("/inspections", {
method: "POST",
body: JSON.stringify(input),
});
}
export function createTaskFromInspection(
id: string,
input: CreateTaskFromInspectionInput,
): Promise<InspectionTask> {
return apiFetch<InspectionTask>(`/inspections/${id}/create-task`, {
method: "POST",
body: JSON.stringify(input),
});
}
@@ -0,0 +1,54 @@
export interface Inspection {
id: string;
colonyId: string;
userId: string;
date: string;
weather: string | null;
temperature: number | null;
colonyStrength: number | null;
broodAmount: string | null;
broodPattern: number | null;
openBroodPresent: boolean | null;
cappedBroodPresent: boolean | null;
eggsPresent: boolean | null;
queenSeen: boolean | null;
queenCellsPresent: boolean | null;
swarmMood: number | null;
gentleness: number | null;
combBehavior: number | null;
feedStatus: string | null;
honeySuperStatus: string | null;
healthIssues: string | null;
varroaIssues: string | null;
measures: string | null;
notes: string | null;
createdAt: string;
}
export interface CreateInspectionInput {
colonyId: string;
date: string;
weather?: string;
temperature?: number;
colonyStrength?: number;
queenSeen?: boolean;
queenCellsPresent?: boolean;
feedStatus?: string;
healthIssues?: string;
varroaIssues?: string;
measures?: string;
notes?: string;
}
export interface CreateTaskFromInspectionInput {
title: string;
description?: string;
priority?: string;
dueDate?: string;
assignedUserId?: string;
}
export interface InspectionTask {
id: string;
sourceInspectionId: string | null;
}