feat(frontend): Ernten- und Schleuderungs-Ansichten (Modul 10)
/harvests/new (Bienenstand + Mehrfachauswahl Völker), /harvests/[id] und /extractions/new (Mehrfachauswahl Ernten). Bienenstand- und Ernte-Detailseiten verlinken direkt in die Kette.
This commit is contained in:
1 parent
340397b34d
commit
5c2c0608d1
9 files changed
+526
-2
No files matched your search
@@ -1,8 +1,10 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import Link from "next/link";
|
||||||
import { useParams } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { fetchApiary } from "@/lib/apiaries/api";
|
import { fetchApiary } from "@/lib/apiaries/api";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
import { ApiaryEditCard } from "./apiary-edit-card";
|
import { ApiaryEditCard } from "./apiary-edit-card";
|
||||||
|
|
||||||
export default function ApiaryDetailPage() {
|
export default function ApiaryDetailPage() {
|
||||||
@@ -24,8 +26,15 @@ export default function ApiaryDetailPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="p-4 sm:p-6">
|
<main className="flex flex-col gap-4 p-4 sm:p-6">
|
||||||
<ApiaryEditCard key={apiary.id} apiary={apiary} />
|
<ApiaryEditCard key={apiary.id} apiary={apiary} />
|
||||||
|
<Button
|
||||||
|
className="w-fit"
|
||||||
|
nativeButton={false}
|
||||||
|
render={<Link href={`/harvests/new?apiaryId=${apiary.id}`} />}
|
||||||
|
>
|
||||||
|
Neue Ernte
|
||||||
|
</Button>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { FormEvent, useState } from "react";
|
||||||
|
import { useRouter, useSearchParams } from "next/navigation";
|
||||||
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { fetchHarvests } from "@/lib/harvests/api";
|
||||||
|
import { createExtraction } from "@/lib/extractions/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 { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
|
||||||
|
function todayIsoDate(): string {
|
||||||
|
return new Date().toISOString().slice(0, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function NewExtractionPage() {
|
||||||
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const { data: harvests } = useQuery({ queryKey: ["harvests"], queryFn: fetchHarvests });
|
||||||
|
|
||||||
|
const preselectedHarvestId = searchParams.get("harvestId");
|
||||||
|
const [date, setDate] = useState(todayIsoDate());
|
||||||
|
const [harvestIds, setHarvestIds] = useState<string[]>(
|
||||||
|
preselectedHarvestId ? [preselectedHarvestId] : [],
|
||||||
|
);
|
||||||
|
const [amount, setAmount] = useState("");
|
||||||
|
const [room, setRoom] = useState("");
|
||||||
|
const [waterContent, setWaterContent] = useState("");
|
||||||
|
const [notes, setNotes] = useState("");
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const mutation = useMutation({
|
||||||
|
mutationFn: createExtraction,
|
||||||
|
onSuccess: () => {
|
||||||
|
void queryClient.invalidateQueries({ queryKey: ["extractions"] });
|
||||||
|
router.push("/colonies");
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
setError("Schleuderung konnte nicht gespeichert werden.");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function toggleHarvest(harvestId: string, checked: boolean) {
|
||||||
|
setHarvestIds((current) =>
|
||||||
|
checked ? [...current, harvestId] : current.filter((id) => id !== harvestId),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSubmit(event: FormEvent<HTMLFormElement>) {
|
||||||
|
event.preventDefault();
|
||||||
|
setError(null);
|
||||||
|
if (harvestIds.length === 0) {
|
||||||
|
setError("Bitte mindestens eine Ernte auswählen.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mutation.mutate({
|
||||||
|
date: new Date(date).toISOString(),
|
||||||
|
harvestIds,
|
||||||
|
amount: amount ? Number(amount) : undefined,
|
||||||
|
room: room || undefined,
|
||||||
|
waterContent: waterContent ? Number(waterContent) : undefined,
|
||||||
|
notes: notes || undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="p-4 sm:p-6">
|
||||||
|
<Card className="max-w-lg">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Neue Schleuderung</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<form className="flex flex-col gap-5" onSubmit={handleSubmit}>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Label htmlFor="extraction-date">Datum</Label>
|
||||||
|
<Input
|
||||||
|
id="extraction-date"
|
||||||
|
type="date"
|
||||||
|
required
|
||||||
|
className="h-11"
|
||||||
|
value={date}
|
||||||
|
onChange={(event) => setDate(event.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Label>Ernten</Label>
|
||||||
|
{harvests?.length === 0 ? (
|
||||||
|
<p className="text-sm text-muted-foreground">Noch keine Ernten vorhanden.</p>
|
||||||
|
) : null}
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
{harvests?.map((harvest) => (
|
||||||
|
<label
|
||||||
|
key={harvest.id}
|
||||||
|
className="flex h-11 items-center gap-3 text-sm"
|
||||||
|
htmlFor={`extraction-harvest-${harvest.id}`}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
id={`extraction-harvest-${harvest.id}`}
|
||||||
|
type="checkbox"
|
||||||
|
className="size-5"
|
||||||
|
checked={harvestIds.includes(harvest.id)}
|
||||||
|
onChange={(event) => toggleHarvest(harvest.id, event.target.checked)}
|
||||||
|
/>
|
||||||
|
{new Date(harvest.date).toLocaleDateString("de-DE")} ·{" "}
|
||||||
|
{harvest.colonies.map((entry) => entry.colony.code).join(", ")}
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Label htmlFor="extraction-amount">Menge (kg)</Label>
|
||||||
|
<Input
|
||||||
|
id="extraction-amount"
|
||||||
|
type="number"
|
||||||
|
inputMode="decimal"
|
||||||
|
className="h-11"
|
||||||
|
value={amount}
|
||||||
|
onChange={(event) => setAmount(event.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Label htmlFor="extraction-room">Schleuderraum</Label>
|
||||||
|
<Input
|
||||||
|
id="extraction-room"
|
||||||
|
className="h-11"
|
||||||
|
value={room}
|
||||||
|
onChange={(event) => setRoom(event.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Label htmlFor="extraction-water">Wassergehalt (%)</Label>
|
||||||
|
<Input
|
||||||
|
id="extraction-water"
|
||||||
|
type="number"
|
||||||
|
inputMode="decimal"
|
||||||
|
className="h-11"
|
||||||
|
value={waterContent}
|
||||||
|
onChange={(event) => setWaterContent(event.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Label htmlFor="extraction-notes">Notizen</Label>
|
||||||
|
<Textarea
|
||||||
|
id="extraction-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…" : "Schleuderung speichern"}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useParams } from "next/navigation";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { fetchHarvest } from "@/lib/harvests/api";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
|
||||||
|
export default function HarvestDetailPage() {
|
||||||
|
const params = useParams<{ id: string }>();
|
||||||
|
const { data: harvest, isLoading, isError } = useQuery({
|
||||||
|
queryKey: ["harvests", params.id],
|
||||||
|
queryFn: () => fetchHarvest(params.id),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <p className="p-4 text-sm text-muted-foreground sm:p-6">Lädt…</p>;
|
||||||
|
}
|
||||||
|
if (isError || !harvest) {
|
||||||
|
return (
|
||||||
|
<p role="alert" className="p-4 text-sm text-red-600 sm:p-6">
|
||||||
|
Ernte konnte nicht geladen werden.
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="flex flex-col gap-4 p-4 sm:p-6">
|
||||||
|
<Card className="max-w-lg">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Ernte vom {new Date(harvest.date).toLocaleDateString("de-DE")}</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="flex flex-col gap-3 text-sm">
|
||||||
|
{harvest.honeyType ? <p>Honigsorte: {harvest.honeyType}</p> : null}
|
||||||
|
{harvest.netWeight !== null ? <p>Nettogewicht: {harvest.netWeight} kg</p> : null}
|
||||||
|
{harvest.honeySuperCount !== null ? (
|
||||||
|
<p>Honigräume: {harvest.honeySuperCount}</p>
|
||||||
|
) : null}
|
||||||
|
{harvest.notes ? <p>Notizen: {harvest.notes}</p> : null}
|
||||||
|
<div>
|
||||||
|
<p className="font-medium">Beteiligte Völker</p>
|
||||||
|
<ul className="list-disc pl-5">
|
||||||
|
{harvest.colonies.map((entry) => (
|
||||||
|
<li key={entry.id}>{entry.colony.code}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
className="w-fit"
|
||||||
|
nativeButton={false}
|
||||||
|
render={<Link href={`/extractions/new?harvestId=${harvest.id}`} />}
|
||||||
|
>
|
||||||
|
Zur Schleuderung
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,191 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { FormEvent, useState } from "react";
|
||||||
|
import { useRouter, useSearchParams } from "next/navigation";
|
||||||
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { fetchApiaries } from "@/lib/apiaries/api";
|
||||||
|
import { fetchColonies } from "@/lib/colonies/api";
|
||||||
|
import { createHarvest } from "@/lib/harvests/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 NewHarvestPage() {
|
||||||
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const { data: apiaries } = useQuery({ queryKey: ["apiaries"], queryFn: fetchApiaries });
|
||||||
|
const { data: colonies } = useQuery({ queryKey: ["colonies"], queryFn: fetchColonies });
|
||||||
|
|
||||||
|
const [apiaryId, setApiaryId] = useState(searchParams.get("apiaryId") ?? "");
|
||||||
|
const [date, setDate] = useState(todayIsoDate());
|
||||||
|
const [colonyIds, setColonyIds] = useState<string[]>([]);
|
||||||
|
const [honeyType, setHoneyType] = useState("");
|
||||||
|
const [netWeight, setNetWeight] = useState("");
|
||||||
|
const [notes, setNotes] = useState("");
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const apiaryColonies = (colonies ?? []).filter(
|
||||||
|
(colony) => colony.apiaryId === apiaryId && colony.status === "ACTIVE",
|
||||||
|
);
|
||||||
|
|
||||||
|
const mutation = useMutation({
|
||||||
|
mutationFn: createHarvest,
|
||||||
|
onSuccess: (harvest) => {
|
||||||
|
void queryClient.invalidateQueries({ queryKey: ["harvests"] });
|
||||||
|
router.push(`/harvests/${harvest.id}`);
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
setError("Ernte konnte nicht gespeichert werden.");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function toggleColony(colonyId: string, checked: boolean) {
|
||||||
|
setColonyIds((current) =>
|
||||||
|
checked ? [...current, colonyId] : current.filter((id) => id !== colonyId),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSubmit(event: FormEvent<HTMLFormElement>) {
|
||||||
|
event.preventDefault();
|
||||||
|
setError(null);
|
||||||
|
if (!apiaryId) {
|
||||||
|
setError("Bitte einen Bienenstand auswählen.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (colonyIds.length === 0) {
|
||||||
|
setError("Bitte mindestens ein Volk auswählen.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mutation.mutate({
|
||||||
|
apiaryId,
|
||||||
|
date: new Date(date).toISOString(),
|
||||||
|
colonyIds,
|
||||||
|
honeyType: honeyType || undefined,
|
||||||
|
netWeight: netWeight ? Number(netWeight) : undefined,
|
||||||
|
notes: notes || undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="p-4 sm:p-6">
|
||||||
|
<Card className="max-w-lg">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Neue Ernte</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<form className="flex flex-col gap-5" onSubmit={handleSubmit}>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Label htmlFor="harvest-apiary">Bienenstand</Label>
|
||||||
|
<Select
|
||||||
|
value={apiaryId}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
setApiaryId(value ?? "");
|
||||||
|
setColonyIds([]);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger id="harvest-apiary" className="h-11 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 htmlFor="harvest-date">Datum</Label>
|
||||||
|
<Input
|
||||||
|
id="harvest-date"
|
||||||
|
type="date"
|
||||||
|
required
|
||||||
|
className="h-11"
|
||||||
|
value={date}
|
||||||
|
onChange={(event) => setDate(event.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Label>Beteiligte Völker</Label>
|
||||||
|
{apiaryId && apiaryColonies.length === 0 ? (
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Keine aktiven Völker an diesem Bienenstand.
|
||||||
|
</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>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Label htmlFor="harvest-honey-type">Honigsorte</Label>
|
||||||
|
<Input
|
||||||
|
id="harvest-honey-type"
|
||||||
|
className="h-11"
|
||||||
|
value={honeyType}
|
||||||
|
onChange={(event) => setHoneyType(event.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Label htmlFor="harvest-net-weight">Nettogewicht (kg)</Label>
|
||||||
|
<Input
|
||||||
|
id="harvest-net-weight"
|
||||||
|
type="number"
|
||||||
|
inputMode="decimal"
|
||||||
|
className="h-11"
|
||||||
|
value={netWeight}
|
||||||
|
onChange={(event) => setNetWeight(event.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Label htmlFor="harvest-notes">Notizen</Label>
|
||||||
|
<Textarea
|
||||||
|
id="harvest-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…" : "Ernte speichern"}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { apiFetch } from "@/lib/auth/api-client";
|
||||||
|
import { CreateExtractionInput, Extraction } from "./types";
|
||||||
|
|
||||||
|
export function fetchExtractions(): Promise<Extraction[]> {
|
||||||
|
return apiFetch<Extraction[]>("/extractions");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createExtraction(input: CreateExtractionInput): Promise<Extraction> {
|
||||||
|
return apiFetch<Extraction>("/extractions", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(input),
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
export interface ExtractionHarvestEntry {
|
||||||
|
id: string;
|
||||||
|
harvestId: string;
|
||||||
|
harvest: {
|
||||||
|
id: string;
|
||||||
|
date: string;
|
||||||
|
colonies: {
|
||||||
|
colonyId: string;
|
||||||
|
colony: { id: string; code: string };
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Extraction {
|
||||||
|
id: string;
|
||||||
|
date: string;
|
||||||
|
userId: string;
|
||||||
|
amount: number | null;
|
||||||
|
room: string | null;
|
||||||
|
waterContent: number | null;
|
||||||
|
temperature: number | null;
|
||||||
|
notes: string | null;
|
||||||
|
createdAt: string;
|
||||||
|
harvests: ExtractionHarvestEntry[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateExtractionInput {
|
||||||
|
date: string;
|
||||||
|
harvestIds: string[];
|
||||||
|
amount?: number;
|
||||||
|
room?: string;
|
||||||
|
waterContent?: number;
|
||||||
|
temperature?: number;
|
||||||
|
notes?: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { apiFetch } from "@/lib/auth/api-client";
|
||||||
|
import { CreateHarvestInput, Harvest } from "./types";
|
||||||
|
|
||||||
|
export function fetchHarvests(): Promise<Harvest[]> {
|
||||||
|
return apiFetch<Harvest[]>("/harvests");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchHarvest(id: string): Promise<Harvest> {
|
||||||
|
return apiFetch<Harvest>(`/harvests/${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createHarvest(input: CreateHarvestInput): Promise<Harvest> {
|
||||||
|
return apiFetch<Harvest>("/harvests", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(input),
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
export interface HarvestColonyEntry {
|
||||||
|
id: string;
|
||||||
|
colonyId: string;
|
||||||
|
colony: {
|
||||||
|
id: string;
|
||||||
|
code: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Harvest {
|
||||||
|
id: string;
|
||||||
|
apiaryId: string;
|
||||||
|
userId: string;
|
||||||
|
date: string;
|
||||||
|
honeyType: string | null;
|
||||||
|
honeySuperCount: number | null;
|
||||||
|
grossWeight: number | null;
|
||||||
|
netWeight: number | null;
|
||||||
|
notes: string | null;
|
||||||
|
createdAt: string;
|
||||||
|
colonies: HarvestColonyEntry[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateHarvestInput {
|
||||||
|
apiaryId: string;
|
||||||
|
date: string;
|
||||||
|
colonyIds: string[];
|
||||||
|
honeyType?: string;
|
||||||
|
honeySuperCount?: number;
|
||||||
|
grossWeight?: number;
|
||||||
|
netWeight?: number;
|
||||||
|
notes?: string;
|
||||||
|
}
|
||||||
@@ -91,7 +91,7 @@ Abgeleitet aus [`docs/lastenheft.md`](docs/lastenheft.md) Abschnitt 26. Reihenfo
|
|||||||
- Endpunkte: `GET/POST /api/v1/harvests`, `GET /api/v1/harvests/:id`, `GET/POST /api/v1/extractions`, `GET /api/v1/extractions/:id`
|
- Endpunkte: `GET/POST /api/v1/harvests`, `GET /api/v1/harvests/:id`, `GET/POST /api/v1/extractions`, `GET /api/v1/extractions/:id`
|
||||||
- Frontend: `/harvests/new`, `/harvests/[id]`, `/extractions/new`
|
- Frontend: `/harvests/new`, `/harvests/[id]`, `/extractions/new`
|
||||||
- [x] Backend-Modul fertig, getestet, grün, committet
|
- [x] Backend-Modul fertig, getestet, grün, committet
|
||||||
- [ ] Frontend-Views fertig, getestet, grün, committet
|
- [x] Frontend-Views fertig, getestet, grün, committet
|
||||||
|
|
||||||
### 11. Honigchargen
|
### 11. Honigchargen
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user