diff --git a/ApiCore-Frontend/src/app/(app)/apiaries/page.tsx b/ApiCore-Frontend/src/app/(app)/apiaries/page.tsx index 019a0c1..0a9fe9e 100644 --- a/ApiCore-Frontend/src/app/(app)/apiaries/page.tsx +++ b/ApiCore-Frontend/src/app/(app)/apiaries/page.tsx @@ -1,10 +1,20 @@ "use client"; +import { useState } from "react"; import Link from "next/link"; import { useQuery } from "@tanstack/react-query"; import { fetchApiaries } from "@/lib/apiaries/api"; +import { ApiaryStatus } from "@/lib/apiaries/types"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; +import { Input } from "@/components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; import { Table, TableBody, @@ -15,9 +25,12 @@ import { } from "@/components/ui/table"; export default function ApiariesPage() { + const [status, setStatus] = useState(""); + const [location, setLocation] = useState(""); + const { data: apiaries, isLoading, isError } = useQuery({ - queryKey: ["apiaries"], - queryFn: fetchApiaries, + queryKey: ["apiaries", status, location], + queryFn: () => fetchApiaries({ status: status || undefined, location: location || undefined }), }); return ( @@ -29,6 +42,28 @@ export default function ApiariesPage() { +
+ + setLocation(event.target.value)} + /> +
+ {isLoading ?

Lädt…

: null} {isError ? (

diff --git a/ApiCore-Frontend/src/app/(app)/colonies/[id]/colony-actions.tsx b/ApiCore-Frontend/src/app/(app)/colonies/[id]/colony-actions.tsx index 6e5b60e..7ebf7b9 100644 --- a/ApiCore-Frontend/src/app/(app)/colonies/[id]/colony-actions.tsx +++ b/ApiCore-Frontend/src/app/(app)/colonies/[id]/colony-actions.tsx @@ -47,7 +47,7 @@ function MoveDialog({ colony }: { colony: Colony }) { const queryClient = useQueryClient(); const [open, setOpen] = useState(false); const [apiaryId, setApiaryId] = useState(""); - const { data: apiaries } = useQuery({ queryKey: ["apiaries"], queryFn: fetchApiaries }); + const { data: apiaries } = useQuery({ queryKey: ["apiaries"], queryFn: () => fetchApiaries() }); const mutation = useMutation({ mutationFn: () => moveColony(colony.id, { apiaryId }), @@ -182,7 +182,7 @@ function MergeDialog({ colony }: { colony: Colony }) { const queryClient = useQueryClient(); const [open, setOpen] = useState(false); const [mergedColonyId, setMergedColonyId] = useState(""); - const { data: colonies } = useQuery({ queryKey: ["colonies"], queryFn: fetchColonies }); + const { data: colonies } = useQuery({ queryKey: ["colonies"], queryFn: () => fetchColonies() }); const mutation = useMutation({ mutationFn: () => mergeColony(colony.id, { mergedColonyIds: [mergedColonyId] }), diff --git a/ApiCore-Frontend/src/app/(app)/colonies/new/page.tsx b/ApiCore-Frontend/src/app/(app)/colonies/new/page.tsx index 8451713..735857c 100644 --- a/ApiCore-Frontend/src/app/(app)/colonies/new/page.tsx +++ b/ApiCore-Frontend/src/app/(app)/colonies/new/page.tsx @@ -35,7 +35,7 @@ const COLONY_TYPES: ColonyType[] = [ export default function NewColonyPage() { const router = useRouter(); const queryClient = useQueryClient(); - const { data: apiaries } = useQuery({ queryKey: ["apiaries"], queryFn: fetchApiaries }); + const { data: apiaries } = useQuery({ queryKey: ["apiaries"], queryFn: () => fetchApiaries() }); const [code, setCode] = useState(""); const [apiaryId, setApiaryId] = useState(""); diff --git a/ApiCore-Frontend/src/app/(app)/colonies/page.tsx b/ApiCore-Frontend/src/app/(app)/colonies/page.tsx index 73ea810..3cf8157 100644 --- a/ApiCore-Frontend/src/app/(app)/colonies/page.tsx +++ b/ApiCore-Frontend/src/app/(app)/colonies/page.tsx @@ -1,10 +1,19 @@ "use client"; +import { useState } from "react"; import Link from "next/link"; import { useQuery } from "@tanstack/react-query"; import { fetchColonies } from "@/lib/colonies/api"; +import { ColonyStatus } from "@/lib/colonies/types"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; import { Table, TableBody, @@ -14,10 +23,23 @@ import { TableRow, } from "@/components/ui/table"; +const STATUS_OPTIONS: ColonyStatus[] = [ + "ACTIVE", + "WEAK", + "CONSPICUOUS", + "QUEENLESS", + "DISSOLVED", + "SOLD", + "DECEASED", + "MERGED", +]; + export default function ColoniesPage() { + const [status, setStatus] = useState(""); + const { data: colonies, isLoading, isError } = useQuery({ - queryKey: ["colonies"], - queryFn: fetchColonies, + queryKey: ["colonies", status], + queryFn: () => fetchColonies({ status: status || undefined }), }); return ( @@ -29,6 +51,23 @@ export default function ColoniesPage() { + + {isLoading ?

Lädt…

: null} {isError ? (

diff --git a/ApiCore-Frontend/src/app/(app)/feedings/new/page.tsx b/ApiCore-Frontend/src/app/(app)/feedings/new/page.tsx index f030dc3..5931828 100644 --- a/ApiCore-Frontend/src/app/(app)/feedings/new/page.tsx +++ b/ApiCore-Frontend/src/app/(app)/feedings/new/page.tsx @@ -38,7 +38,7 @@ export default function NewFeedingPage() { const { data: colonies } = useQuery({ queryKey: ["colonies"], - queryFn: fetchColonies, + queryFn: () => fetchColonies(), }); const activeColonies = colonies?.filter((colony) => colony.status === "ACTIVE") ?? []; diff --git a/ApiCore-Frontend/src/app/(app)/harvests/new/page.tsx b/ApiCore-Frontend/src/app/(app)/harvests/new/page.tsx index 7a2f012..1b22be7 100644 --- a/ApiCore-Frontend/src/app/(app)/harvests/new/page.tsx +++ b/ApiCore-Frontend/src/app/(app)/harvests/new/page.tsx @@ -28,8 +28,8 @@ export default function NewHarvestPage() { const searchParams = useSearchParams(); const queryClient = useQueryClient(); - const { data: apiaries } = useQuery({ queryKey: ["apiaries"], queryFn: fetchApiaries }); - const { data: colonies } = useQuery({ queryKey: ["colonies"], queryFn: fetchColonies }); + 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()); diff --git a/ApiCore-Frontend/src/app/(app)/honey-batches/page.tsx b/ApiCore-Frontend/src/app/(app)/honey-batches/page.tsx index 63d445b..8619ae0 100644 --- a/ApiCore-Frontend/src/app/(app)/honey-batches/page.tsx +++ b/ApiCore-Frontend/src/app/(app)/honey-batches/page.tsx @@ -1,9 +1,18 @@ "use client"; +import { useState } from "react"; import Link from "next/link"; import { useQuery } from "@tanstack/react-query"; import { fetchHoneyBatches } from "@/lib/honey-batches/api"; +import { HoneyBatchStatus } from "@/lib/honey-batches/types"; import { Badge } from "@/components/ui/badge"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; import { Table, TableBody, @@ -14,10 +23,23 @@ import { } from "@/components/ui/table"; import { CreateHoneyBatchDialog } from "./create-honey-batch-dialog"; +const STATUS_OPTIONS: HoneyBatchStatus[] = [ + "RAW", + "EXTRACTED", + "CLARIFIED", + "CREAMED", + "STORED", + "BOTTLED", + "SOLD", + "CONSUMED", +]; + export default function HoneyBatchesPage() { + const [status, setStatus] = useState(""); + const { data: honeyBatches, isLoading, isError } = useQuery({ - queryKey: ["honey-batches"], - queryFn: fetchHoneyBatches, + queryKey: ["honey-batches", status], + queryFn: () => fetchHoneyBatches({ status: status || undefined }), }); return ( @@ -27,6 +49,25 @@ export default function HoneyBatchesPage() { + + {isLoading ?

Lädt…

: null} {isError ? (

diff --git a/ApiCore-Frontend/src/app/(app)/inspections/new/page.tsx b/ApiCore-Frontend/src/app/(app)/inspections/new/page.tsx index 14ecbb9..1a6bf09 100644 --- a/ApiCore-Frontend/src/app/(app)/inspections/new/page.tsx +++ b/ApiCore-Frontend/src/app/(app)/inspections/new/page.tsx @@ -29,7 +29,7 @@ export default function NewInspectionPage() { const { data: colonies } = useQuery({ queryKey: ["colonies"], - queryFn: fetchColonies, + queryFn: () => fetchColonies(), }); const activeColonies = colonies?.filter((colony) => colony.status === "ACTIVE") ?? []; diff --git a/ApiCore-Frontend/src/app/(app)/layout.tsx b/ApiCore-Frontend/src/app/(app)/layout.tsx index f9b998c..1afd84c 100644 --- a/ApiCore-Frontend/src/app/(app)/layout.tsx +++ b/ApiCore-Frontend/src/app/(app)/layout.tsx @@ -49,6 +49,9 @@ export default function AppLayout({ children }: { children: ReactNode }) { Lager + + Suche + {user.role === "ADMIN" ? ( fetchColonies() }); const mutation = useMutation({ mutationFn: () => assignQueenColony(queenId, { colonyId }), diff --git a/ApiCore-Frontend/src/app/(app)/queens/page.tsx b/ApiCore-Frontend/src/app/(app)/queens/page.tsx index a1f4d9b..eec223a 100644 --- a/ApiCore-Frontend/src/app/(app)/queens/page.tsx +++ b/ApiCore-Frontend/src/app/(app)/queens/page.tsx @@ -1,10 +1,19 @@ "use client"; +import { useState } from "react"; import Link from "next/link"; import { useQuery } from "@tanstack/react-query"; import { fetchQueens } from "@/lib/queens/api"; +import { QueenStatus } from "@/lib/queens/types"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; import { Table, TableBody, @@ -14,10 +23,23 @@ import { TableRow, } from "@/components/ui/table"; +const STATUS_OPTIONS: QueenStatus[] = [ + "ACTIVE", + "RESERVE", + "UNMATED", + "MATED", + "LOST", + "REPLACED", + "SOLD", + "DECEASED", +]; + export default function QueensPage() { + const [status, setStatus] = useState(""); + const { data: queens, isLoading, isError } = useQuery({ - queryKey: ["queens"], - queryFn: fetchQueens, + queryKey: ["queens", status], + queryFn: () => fetchQueens({ status: status || undefined }), }); return ( @@ -29,6 +51,23 @@ export default function QueensPage() { + + {isLoading ?

Lädt…

: null} {isError ? (

diff --git a/ApiCore-Frontend/src/app/(app)/search/page.tsx b/ApiCore-Frontend/src/app/(app)/search/page.tsx new file mode 100644 index 0000000..be39954 --- /dev/null +++ b/ApiCore-Frontend/src/app/(app)/search/page.tsx @@ -0,0 +1,81 @@ +"use client"; + +import { useState } from "react"; +import Link from "next/link"; +import { useQuery } from "@tanstack/react-query"; +import { search } from "@/lib/search/api"; +import { SearchResult, SearchResultType } from "@/lib/search/types"; +import { Badge } from "@/components/ui/badge"; +import { Input } from "@/components/ui/input"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; + +const TYPE_LABELS: Record = { + Apiary: "Bienenstand", + Colony: "Volk", + Queen: "Königin", + Task: "Aufgabe", + HoneyBatch: "Honigcharge", +}; + +function resultHref(result: SearchResult): string { + switch (result.type) { + case "Apiary": + return `/apiaries/${result.id}`; + case "Colony": + return `/colonies/${result.id}`; + case "Queen": + return `/queens/${result.id}`; + case "HoneyBatch": + return `/honey-batches/${result.id}`; + case "Task": + return "/tasks"; + } +} + +export default function SearchPage() { + const [query, setQuery] = useState(""); + + const { data: results, isLoading } = useQuery({ + queryKey: ["search", query], + queryFn: () => search(query), + enabled: query.trim().length > 0, + }); + + return ( +

+

Suche

+ setQuery(event.target.value)} + autoFocus + /> + + {isLoading ?

Lädt…

: null} + {query.trim().length > 0 && !isLoading && (!results || results.length === 0) ? ( +

Keine Treffer.

+ ) : null} + + {results && results.length > 0 ? ( + + + Ergebnisse + + + {results.map((result) => ( + + {result.label} + {TYPE_LABELS[result.type]} + + ))} + + + ) : null} +
+ ); +} diff --git a/ApiCore-Frontend/src/app/(app)/treatments/new/page.tsx b/ApiCore-Frontend/src/app/(app)/treatments/new/page.tsx index 6d4d113..059b425 100644 --- a/ApiCore-Frontend/src/app/(app)/treatments/new/page.tsx +++ b/ApiCore-Frontend/src/app/(app)/treatments/new/page.tsx @@ -29,7 +29,7 @@ export default function NewTreatmentPage() { const { data: colonies } = useQuery({ queryKey: ["colonies"], - queryFn: fetchColonies, + queryFn: () => fetchColonies(), }); const activeColonies = colonies?.filter((colony) => colony.status === "ACTIVE") ?? []; diff --git a/ApiCore-Frontend/src/app/(app)/varroa/new/page.tsx b/ApiCore-Frontend/src/app/(app)/varroa/new/page.tsx index 603ebbf..fb86236 100644 --- a/ApiCore-Frontend/src/app/(app)/varroa/new/page.tsx +++ b/ApiCore-Frontend/src/app/(app)/varroa/new/page.tsx @@ -37,7 +37,7 @@ export default function NewVarroaMeasurementPage() { const { data: colonies } = useQuery({ queryKey: ["colonies"], - queryFn: fetchColonies, + queryFn: () => fetchColonies(), }); const activeColonies = colonies?.filter((colony) => colony.status === "ACTIVE") ?? []; diff --git a/ApiCore-Frontend/src/lib/apiaries/api.ts b/ApiCore-Frontend/src/lib/apiaries/api.ts index d2bdba1..63f8bd4 100644 --- a/ApiCore-Frontend/src/lib/apiaries/api.ts +++ b/ApiCore-Frontend/src/lib/apiaries/api.ts @@ -1,8 +1,17 @@ import { apiFetch } from "@/lib/auth/api-client"; -import { Apiary, ApiaryInput } from "./types"; +import { Apiary, ApiaryInput, ApiaryStatus } from "./types"; -export function fetchApiaries(): Promise { - return apiFetch("/apiaries"); +export interface ApiaryFilter { + status?: ApiaryStatus; + location?: string; +} + +export function fetchApiaries(filter: ApiaryFilter = {}): Promise { + const params = new URLSearchParams(); + if (filter.status) params.set("status", filter.status); + if (filter.location) params.set("location", filter.location); + const query = params.toString(); + return apiFetch(`/apiaries${query ? `?${query}` : ""}`); } export function fetchApiary(id: string): Promise { diff --git a/ApiCore-Frontend/src/lib/colonies/api.ts b/ApiCore-Frontend/src/lib/colonies/api.ts index 0310779..4129007 100644 --- a/ApiCore-Frontend/src/lib/colonies/api.ts +++ b/ApiCore-Frontend/src/lib/colonies/api.ts @@ -2,6 +2,7 @@ import { apiFetch } from "@/lib/auth/api-client"; import { Colony, ColonyHistory, + ColonyStatus, CreateColonyInput, MergeColonyInput, MoveColonyInput, @@ -9,8 +10,17 @@ import { UpdateColonyInput, } from "./types"; -export function fetchColonies(): Promise { - return apiFetch("/colonies"); +export interface ColonyFilter { + status?: ColonyStatus; + apiaryId?: string; +} + +export function fetchColonies(filter: ColonyFilter = {}): Promise { + const params = new URLSearchParams(); + if (filter.status) params.set("status", filter.status); + if (filter.apiaryId) params.set("apiaryId", filter.apiaryId); + const query = params.toString(); + return apiFetch(`/colonies${query ? `?${query}` : ""}`); } export function fetchColony(id: string): Promise { diff --git a/ApiCore-Frontend/src/lib/honey-batches/api.ts b/ApiCore-Frontend/src/lib/honey-batches/api.ts index 5d104d0..7c64a6a 100644 --- a/ApiCore-Frontend/src/lib/honey-batches/api.ts +++ b/ApiCore-Frontend/src/lib/honey-batches/api.ts @@ -3,11 +3,19 @@ import { CreateHoneyBatchInput, CreateHoneyMeasurementInput, HoneyBatch, + HoneyBatchStatus, Traceability, } from "./types"; -export function fetchHoneyBatches(): Promise { - return apiFetch("/honey-batches"); +export interface HoneyBatchFilter { + status?: HoneyBatchStatus; +} + +export function fetchHoneyBatches(filter: HoneyBatchFilter = {}): Promise { + const params = new URLSearchParams(); + if (filter.status) params.set("status", filter.status); + const query = params.toString(); + return apiFetch(`/honey-batches${query ? `?${query}` : ""}`); } export function fetchHoneyBatch(id: string): Promise { diff --git a/ApiCore-Frontend/src/lib/queens/api.ts b/ApiCore-Frontend/src/lib/queens/api.ts index 1e8ad19..658aad4 100644 --- a/ApiCore-Frontend/src/lib/queens/api.ts +++ b/ApiCore-Frontend/src/lib/queens/api.ts @@ -4,11 +4,19 @@ import { CreateQueenInput, Queen, QueenColonyHistoryEntry, + QueenStatus, UpdateQueenInput, } from "./types"; -export function fetchQueens(): Promise { - return apiFetch("/queens"); +export interface QueenFilter { + status?: QueenStatus; +} + +export function fetchQueens(filter: QueenFilter = {}): Promise { + const params = new URLSearchParams(); + if (filter.status) params.set("status", filter.status); + const query = params.toString(); + return apiFetch(`/queens${query ? `?${query}` : ""}`); } export function fetchQueen(id: string): Promise { diff --git a/ApiCore-Frontend/src/lib/search/api.ts b/ApiCore-Frontend/src/lib/search/api.ts new file mode 100644 index 0000000..79315b2 --- /dev/null +++ b/ApiCore-Frontend/src/lib/search/api.ts @@ -0,0 +1,6 @@ +import { apiFetch } from "@/lib/auth/api-client"; +import { SearchResult } from "./types"; + +export function search(q: string): Promise { + return apiFetch(`/search?q=${encodeURIComponent(q)}`); +} diff --git a/ApiCore-Frontend/src/lib/search/types.ts b/ApiCore-Frontend/src/lib/search/types.ts new file mode 100644 index 0000000..981f9ee --- /dev/null +++ b/ApiCore-Frontend/src/lib/search/types.ts @@ -0,0 +1,7 @@ +export type SearchResultType = "Apiary" | "Colony" | "Queen" | "Task" | "HoneyBatch"; + +export interface SearchResult { + type: SearchResultType; + id: string; + label: string; +} diff --git a/TASKS.md b/TASKS.md index 827967a..bb50bf1 100644 --- a/TASKS.md +++ b/TASKS.md @@ -122,7 +122,7 @@ Abgeleitet aus [`docs/lastenheft.md`](docs/lastenheft.md) Abschnitt 26. Reihenfo - Backend: `SearchModule` (`GET /api/v1/search?q=`), Filter-Query-Parameter in bestehenden List-Endpunkten (Standort, Status, Zeitraum etc., kombinierbar) - Frontend: `/search`, Filterleisten in allen Listen-Views - [x] Backend-Modul fertig, getestet, grün, committet -- [ ] Frontend-Views fertig, getestet, grün, committet +- [x] Frontend-Views fertig, getestet, grün, committet ### 15. Responsive Smartphone-Oberfläche