diff --git a/ApiCore-Frontend/proxy.ts b/ApiCore-Frontend/proxy.ts
index f9ffd47..8f2b2cb 100644
--- a/ApiCore-Frontend/proxy.ts
+++ b/ApiCore-Frontend/proxy.ts
@@ -22,5 +22,11 @@ export function proxy(request: NextRequest) {
}
export const config = {
- matcher: ["/login", "/settings/:path*", "/apiaries/:path*", "/colonies/:path*"],
+ matcher: [
+ "/login",
+ "/settings/:path*",
+ "/apiaries/:path*",
+ "/colonies/:path*",
+ "/queens/:path*",
+ ],
};
diff --git a/ApiCore-Frontend/src/app/(app)/layout.tsx b/ApiCore-Frontend/src/app/(app)/layout.tsx
index 070792e..9fbbdbd 100644
--- a/ApiCore-Frontend/src/app/(app)/layout.tsx
+++ b/ApiCore-Frontend/src/app/(app)/layout.tsx
@@ -31,6 +31,9 @@ export default function AppLayout({ children }: { children: ReactNode }) {
Völker
+
+ Königinnen
+
{user.role === "ADMIN" ? (
assignQueenColony(queenId, { colonyId }),
+ onSuccess: () => {
+ void queryClient.invalidateQueries({ queryKey: ["queens", queenId] });
+ void queryClient.invalidateQueries({ queryKey: ["queens", queenId, "history"] });
+ setOpen(false);
+ },
+ });
+
+ return (
+
+ );
+}
diff --git a/ApiCore-Frontend/src/app/(app)/queens/[id]/page.tsx b/ApiCore-Frontend/src/app/(app)/queens/[id]/page.tsx
new file mode 100644
index 0000000..24334cd
--- /dev/null
+++ b/ApiCore-Frontend/src/app/(app)/queens/[id]/page.tsx
@@ -0,0 +1,35 @@
+"use client";
+
+import { useParams } from "next/navigation";
+import { useQuery } from "@tanstack/react-query";
+import { fetchQueen } from "@/lib/queens/api";
+import { QueenEditCard } from "./queen-edit-card";
+import { AssignColonyDialog } from "./assign-colony-dialog";
+import { QueenHistoryCard } from "./queen-history";
+
+export default function QueenDetailPage() {
+ const params = useParams<{ id: string }>();
+ const { data: queen, isLoading, isError } = useQuery({
+ queryKey: ["queens", params.id],
+ queryFn: () => fetchQueen(params.id),
+ });
+
+ if (isLoading) {
+ return
Lädt…
;
+ }
+ if (isError || !queen) {
+ return (
+
+ Königin konnte nicht geladen werden.
+
+ );
+ }
+
+ return (
+
+
+
+
+
+ );
+}
diff --git a/ApiCore-Frontend/src/app/(app)/queens/[id]/queen-edit-card.tsx b/ApiCore-Frontend/src/app/(app)/queens/[id]/queen-edit-card.tsx
new file mode 100644
index 0000000..25627ba
--- /dev/null
+++ b/ApiCore-Frontend/src/app/(app)/queens/[id]/queen-edit-card.tsx
@@ -0,0 +1,119 @@
+"use client";
+
+import { FormEvent, useState } from "react";
+import { useMutation, useQueryClient } from "@tanstack/react-query";
+import { updateQueen } from "@/lib/queens/api";
+import { Queen, QueenStatus } from "@/lib/queens/types";
+import { Button } from "@/components/ui/button";
+import { Badge } from "@/components/ui/badge";
+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";
+
+const QUEEN_STATUSES: QueenStatus[] = [
+ "ACTIVE",
+ "RESERVE",
+ "UNMATED",
+ "MATED",
+ "LOST",
+ "REPLACED",
+ "SOLD",
+ "DECEASED",
+];
+
+export function QueenEditCard({ queen }: { queen: Queen }) {
+ const queryClient = useQueryClient();
+ const [breed, setBreed] = useState(queen.breed ?? "");
+ const [origin, setOrigin] = useState(queen.origin ?? "");
+ const [notes, setNotes] = useState(queen.notes ?? "");
+ const [status, setStatus] = useState(queen.status);
+
+ const mutation = useMutation({
+ mutationFn: () =>
+ updateQueen(queen.id, {
+ breed: breed || undefined,
+ origin: origin || undefined,
+ notes: notes || undefined,
+ status,
+ }),
+ onSuccess: () => {
+ void queryClient.invalidateQueries({ queryKey: ["queens"] });
+ void queryClient.invalidateQueries({ queryKey: ["queens", queen.id] });
+ },
+ });
+
+ function handleSubmit(event: FormEvent) {
+ event.preventDefault();
+ mutation.mutate();
+ }
+
+ return (
+
+
+ {queen.code}
+
+ {queen.status}
+
+
+
+
+
+
+ );
+}
diff --git a/ApiCore-Frontend/src/app/(app)/queens/[id]/queen-history.tsx b/ApiCore-Frontend/src/app/(app)/queens/[id]/queen-history.tsx
new file mode 100644
index 0000000..82e29b9
--- /dev/null
+++ b/ApiCore-Frontend/src/app/(app)/queens/[id]/queen-history.tsx
@@ -0,0 +1,42 @@
+"use client";
+
+import { useQuery } from "@tanstack/react-query";
+import { fetchQueenHistory } from "@/lib/queens/api";
+import {
+ Card,
+ CardContent,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+
+export function QueenHistoryCard({ queenId }: { queenId: string }) {
+ const { data: history, isLoading } = useQuery({
+ queryKey: ["queens", queenId, "history"],
+ queryFn: () => fetchQueenHistory(queenId),
+ });
+
+ if (isLoading || !history) {
+ return Historie lädt…
;
+ }
+
+ return (
+
+
+ Volk-Historie
+
+
+ {history.length === 0 ? (
+ Noch keine Historie vorhanden.
+ ) : null}
+ {history.map((entry) => (
+
+ Seit {new Date(entry.startDate).toLocaleDateString("de-DE")}
+ {entry.endDate
+ ? ` bis ${new Date(entry.endDate).toLocaleDateString("de-DE")}`
+ : " (aktuell)"}
+
+ ))}
+
+
+ );
+}
diff --git a/ApiCore-Frontend/src/app/(app)/queens/new/page.tsx b/ApiCore-Frontend/src/app/(app)/queens/new/page.tsx
new file mode 100644
index 0000000..e9f195c
--- /dev/null
+++ b/ApiCore-Frontend/src/app/(app)/queens/new/page.tsx
@@ -0,0 +1,88 @@
+"use client";
+
+import { FormEvent, useState } from "react";
+import { useRouter } from "next/navigation";
+import { useMutation, useQueryClient } from "@tanstack/react-query";
+import { createQueen } from "@/lib/queens/api";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import {
+ Card,
+ CardContent,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+
+export default function NewQueenPage() {
+ const router = useRouter();
+ const queryClient = useQueryClient();
+ const [code, setCode] = useState("");
+ const [breed, setBreed] = useState("");
+ const [origin, setOrigin] = useState("");
+ const [error, setError] = useState(null);
+
+ const mutation = useMutation({
+ mutationFn: createQueen,
+ onSuccess: (queen) => {
+ void queryClient.invalidateQueries({ queryKey: ["queens"] });
+ router.push(`/queens/${queen.id}`);
+ },
+ onError: () => {
+ setError("Königin konnte nicht angelegt werden.");
+ },
+ });
+
+ function handleSubmit(event: FormEvent) {
+ event.preventDefault();
+ setError(null);
+ mutation.mutate({ code, breed: breed || undefined, origin: origin || undefined });
+ }
+
+ return (
+
+
+
+ Neue Königin
+
+
+
+
+
+
+ );
+}
diff --git a/ApiCore-Frontend/src/app/(app)/queens/page.spec.tsx b/ApiCore-Frontend/src/app/(app)/queens/page.spec.tsx
new file mode 100644
index 0000000..44347af
--- /dev/null
+++ b/ApiCore-Frontend/src/app/(app)/queens/page.spec.tsx
@@ -0,0 +1,51 @@
+import { beforeEach, describe, expect, it, vi } from "vitest";
+import { render, screen } from "@testing-library/react";
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import type { ReactElement } from "react";
+import QueensPage from "./page";
+import { fetchQueens } from "@/lib/queens/api";
+
+vi.mock("@/lib/queens/api", () => ({
+ fetchQueens: vi.fn(),
+}));
+
+function renderWithClient(ui: ReactElement) {
+ const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
+ return render({ui});
+}
+
+describe("QueensPage", () => {
+ beforeEach(() => {
+ vi.mocked(fetchQueens).mockReset();
+ });
+
+ it("renders the fetched queens", async () => {
+ vi.mocked(fetchQueens).mockResolvedValueOnce([
+ {
+ id: "1",
+ code: "Q-001",
+ hatchYear: null,
+ breed: "Carnica",
+ origin: null,
+ breeder: null,
+ markingColor: null,
+ markingNumber: null,
+ matingType: null,
+ matingStatus: null,
+ inseminationData: null,
+ motherLine: null,
+ fatherLine: null,
+ status: "ACTIVE",
+ notes: null,
+ createdAt: "",
+ updatedAt: "",
+ deactivatedAt: null,
+ },
+ ]);
+
+ renderWithClient();
+
+ expect(await screen.findByText("Q-001")).toBeInTheDocument();
+ expect(screen.getByText("Carnica")).toBeInTheDocument();
+ });
+});
diff --git a/ApiCore-Frontend/src/app/(app)/queens/page.tsx b/ApiCore-Frontend/src/app/(app)/queens/page.tsx
new file mode 100644
index 0000000..a1f4d9b
--- /dev/null
+++ b/ApiCore-Frontend/src/app/(app)/queens/page.tsx
@@ -0,0 +1,69 @@
+"use client";
+
+import Link from "next/link";
+import { useQuery } from "@tanstack/react-query";
+import { fetchQueens } from "@/lib/queens/api";
+import { Button } from "@/components/ui/button";
+import { Badge } from "@/components/ui/badge";
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/components/ui/table";
+
+export default function QueensPage() {
+ const { data: queens, isLoading, isError } = useQuery({
+ queryKey: ["queens"],
+ queryFn: fetchQueens,
+ });
+
+ return (
+
+
+
Königinnen
+ }>
+ + Neue Königin
+
+
+
+ {isLoading ? Lädt…
: null}
+ {isError ? (
+
+ Königinnen konnten nicht geladen werden.
+
+ ) : null}
+
+ {queens ? (
+
+
+
+ Code
+ Zucht
+ Status
+
+
+
+ {queens.map((queen) => (
+
+
+
+ {queen.code}
+
+
+ {queen.breed ?? "—"}
+
+
+ {queen.status}
+
+
+
+ ))}
+
+
+ ) : null}
+
+ );
+}
diff --git a/ApiCore-Frontend/src/lib/queens/api.ts b/ApiCore-Frontend/src/lib/queens/api.ts
new file mode 100644
index 0000000..1e8ad19
--- /dev/null
+++ b/ApiCore-Frontend/src/lib/queens/api.ts
@@ -0,0 +1,41 @@
+import { apiFetch } from "@/lib/auth/api-client";
+import {
+ AssignColonyInput,
+ CreateQueenInput,
+ Queen,
+ QueenColonyHistoryEntry,
+ UpdateQueenInput,
+} from "./types";
+
+export function fetchQueens(): Promise {
+ return apiFetch("/queens");
+}
+
+export function fetchQueen(id: string): Promise {
+ return apiFetch(`/queens/${id}`);
+}
+
+export function fetchQueenHistory(id: string): Promise {
+ return apiFetch(`/queens/${id}/history`);
+}
+
+export function createQueen(input: CreateQueenInput): Promise {
+ return apiFetch("/queens", {
+ method: "POST",
+ body: JSON.stringify(input),
+ });
+}
+
+export function updateQueen(id: string, input: UpdateQueenInput): Promise {
+ return apiFetch(`/queens/${id}`, {
+ method: "PATCH",
+ body: JSON.stringify(input),
+ });
+}
+
+export function assignQueenColony(id: string, input: AssignColonyInput): Promise {
+ return apiFetch(`/queens/${id}/assign-colony`, {
+ method: "POST",
+ body: JSON.stringify(input),
+ });
+}
diff --git a/ApiCore-Frontend/src/lib/queens/types.ts b/ApiCore-Frontend/src/lib/queens/types.ts
new file mode 100644
index 0000000..f079578
--- /dev/null
+++ b/ApiCore-Frontend/src/lib/queens/types.ts
@@ -0,0 +1,57 @@
+export type QueenStatus =
+ | "ACTIVE"
+ | "RESERVE"
+ | "UNMATED"
+ | "MATED"
+ | "LOST"
+ | "REPLACED"
+ | "SOLD"
+ | "DECEASED";
+
+export interface Queen {
+ id: string;
+ code: string;
+ hatchYear: number | null;
+ breed: string | null;
+ origin: string | null;
+ breeder: string | null;
+ markingColor: string | null;
+ markingNumber: string | null;
+ matingType: string | null;
+ matingStatus: string | null;
+ inseminationData: string | null;
+ motherLine: string | null;
+ fatherLine: string | null;
+ status: QueenStatus;
+ notes: string | null;
+ createdAt: string;
+ updatedAt: string;
+ deactivatedAt: string | null;
+}
+
+export interface CreateQueenInput {
+ code: string;
+ breed?: string;
+ origin?: string;
+ notes?: string;
+}
+
+export interface UpdateQueenInput {
+ breed?: string;
+ origin?: string;
+ notes?: string;
+ status?: QueenStatus;
+}
+
+export interface AssignColonyInput {
+ colonyId: string;
+ notes?: string;
+}
+
+export interface QueenColonyHistoryEntry {
+ id: string;
+ colonyId: string;
+ startDate: string;
+ endDate: string | null;
+ notes: string | null;
+}
diff --git a/TASKS.md b/TASKS.md
index 47508a1..da12413 100644
--- a/TASKS.md
+++ b/TASKS.md
@@ -43,7 +43,7 @@ Abgeleitet aus [`docs/lastenheft.md`](docs/lastenheft.md) Abschnitt 26. Reihenfo
- Endpunkte: `GET/POST /api/v1/queens`, `GET/PATCH /api/v1/queens/:id`, `POST /api/v1/queens/:id/assign-colony`, `GET /api/v1/queens/:id/history`
- Frontend: `/queens`, `/queens/[id]`, `/queens/new`
- [x] Backend-Modul fertig, getestet, grün, committet
-- [ ] Frontend-Views fertig, getestet, grün, committet
+- [x] Frontend-Views fertig, getestet, grün, committet
### 5. Durchsichten