From 9944aabbc388b0ddf5dee4c3220a8d5626540438 Mon Sep 17 00:00:00 2001 From: Nick Szaunig Date: Thu, 1 Oct 2026 12:14:07 +0200 Subject: [PATCH] feat(ui): list pages with retry on errors, a next step when empty, one main action MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lists load through QueryState: a failed load says so and offers "Erneut versuchen" instead of looking empty; empty lists link to the create page; exports move into a "Mehr" menu so "Neu …" is the one primary action. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../src/app/(app)/apiaries/page.tsx | 150 ++++++++++----- .../src/app/(app)/bottling/page.tsx | 79 ++++---- .../src/app/(app)/colonies/page.tsx | 173 ++++++++++++------ .../src/app/(app)/equipment/page.tsx | 79 ++++---- .../src/app/(app)/extractions/page.tsx | 97 +++++----- .../src/app/(app)/harvests/page.tsx | 91 +++++---- .../src/app/(app)/honey-batches/page.tsx | 138 ++++++++------ .../src/app/(app)/products/page.tsx | 103 ++++++----- .../src/app/(app)/queens/page.tsx | 141 +++++++++----- 9 files changed, 623 insertions(+), 428 deletions(-) diff --git a/ApiCore-Frontend/src/app/(app)/apiaries/page.tsx b/ApiCore-Frontend/src/app/(app)/apiaries/page.tsx index 1062827..64b7ae2 100644 --- a/ApiCore-Frontend/src/app/(app)/apiaries/page.tsx +++ b/ApiCore-Frontend/src/app/(app)/apiaries/page.tsx @@ -2,27 +2,35 @@ import { useState } from "react"; import Link from "next/link"; -import { MapPin } from "lucide-react"; +import { ChevronDown, MapPin } from "lucide-react"; import { useQuery } from "@tanstack/react-query"; import { fetchApiaries } from "@/lib/apiaries/api"; +import { Apiary } from "@/lib/apiaries/types"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Input } from "@/components/ui/input"; import { PageHeader } from "@/components/page-header"; import { EmptyState } from "@/components/empty-state"; import { EntityCard } from "@/components/entity-card"; +import { QueryState } from "@/components/query-state"; import { ActiveHistoryTabs, type EntityLifecycleTab } from "@/components/active-history-tabs"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; export default function ApiariesPage() { const [tab, setTab] = useState("active"); const [location, setLocation] = useState(""); - const { data: apiaries, isLoading, isError } = useQuery({ + const apiariesQuery = useQuery({ queryKey: ["apiaries"], queryFn: () => fetchApiaries({}), }); - const byLocation = (apiaries ?? []).filter((apiary) => + const byLocation = (apiariesQuery.data ?? []).filter((apiary) => location ? (apiary.location ?? "").toLowerCase().includes(location.toLowerCase()) : true, ); const activeApiaries = byLocation.filter((apiary) => apiary.status === "ACTIVE"); @@ -34,13 +42,21 @@ export default function ApiariesPage() { - - @@ -57,49 +73,93 @@ export default function ApiariesPage() { setLocation(event.target.value)} /> - {isLoading ?

Lädt…

: null} - {isError ? ( -

- Bienenstände konnten nicht geladen werden. -

- ) : null} - - {apiaries && visibleApiaries.length === 0 ? ( - - ) : null} - - {visibleApiaries.length > 0 ? ( -
- {visibleApiaries.map((apiary) => ( - - {apiary.status === "ACTIVE" ? "Aktiv" : "Inaktiv"} - - } - fields={[{ label: "Adresse", value: apiary.address ?? "—" }]} + + {() => + visibleApiaries.length === 0 ? ( + setLocation("")} /> - ))} -
- ) : null} + ) : ( + + ) + } + ); } + +function ApiariesEmpty({ + tab, + isFiltered, + onResetFilter, +}: { + tab: EntityLifecycleTab; + isFiltered: boolean; + onResetFilter: () => void; +}) { + if (isFiltered) { + return ( + + Filter zurücksetzen + + } + /> + ); + } + if (tab === "history") { + return ( + + ); + } + return ( + }> + Ersten Bienenstand anlegen + + } + /> + ); +} + +function ApiaryGrid({ apiaries }: { apiaries: Apiary[] }) { + return ( +
+ {apiaries.map((apiary) => ( + + {apiary.status === "ACTIVE" ? "Aktiv" : "Inaktiv"} + + } + fields={[{ label: "Adresse", value: apiary.address ?? "—" }]} + /> + ))} +
+ ); +} diff --git a/ApiCore-Frontend/src/app/(app)/bottling/page.tsx b/ApiCore-Frontend/src/app/(app)/bottling/page.tsx index 11e9285..ad9a778 100644 --- a/ApiCore-Frontend/src/app/(app)/bottling/page.tsx +++ b/ApiCore-Frontend/src/app/(app)/bottling/page.tsx @@ -8,13 +8,14 @@ import { Button } from "@/components/ui/button"; import { PageHeader } from "@/components/page-header"; import { EmptyState } from "@/components/empty-state"; import { EntityCard } from "@/components/entity-card"; +import { QueryState } from "@/components/query-state"; function formatDate(iso: string): string { return new Date(iso).toLocaleDateString("de-DE"); } export default function BottlingBatchesPage() { - const { data: bottlingBatches, isLoading, isError } = useQuery({ + const bottlingQuery = useQuery({ queryKey: ["bottling-batches"], queryFn: () => fetchBottlingBatches(), }); @@ -24,49 +25,51 @@ export default function BottlingBatchesPage() { }> + } /> - {isLoading ?

Lädt…

: null} - {isError ? ( -

- Abfüllungen konnten nicht geladen werden. -

- ) : null} - - {bottlingBatches && bottlingBatches.length === 0 ? ( - - ) : null} - - {bottlingBatches && bottlingBatches.length > 0 ? ( -
- {bottlingBatches.map((bottlingBatch) => ( - {bottlingBatch.lotNumber}} - subtitle={bottlingBatch.product?.name ?? bottlingBatch.honeyBatch.batchNumber} - fields={[ - { - label: "Menge", - value: `${bottlingBatch.jarCount} × ${bottlingBatch.jarSize} g`, - }, - { label: "Datum", value: formatDate(bottlingBatch.date) }, - ]} - /> - ))} -
- ) : null} + }> + Erste Abfüllung anlegen + + } + /> + } + > + {(bottlingBatches) => ( +
+ {bottlingBatches.map((bottlingBatch) => ( + {bottlingBatch.lotNumber}} + subtitle={bottlingBatch.product?.name ?? bottlingBatch.honeyBatch.batchNumber} + fields={[ + { + label: "Menge", + value: `${bottlingBatch.jarCount} × ${bottlingBatch.jarSize} g`, + }, + { label: "Datum", value: formatDate(bottlingBatch.date) }, + ]} + /> + ))} +
+ )} +
); } diff --git a/ApiCore-Frontend/src/app/(app)/colonies/page.tsx b/ApiCore-Frontend/src/app/(app)/colonies/page.tsx index 278c6c4..cda028b 100644 --- a/ApiCore-Frontend/src/app/(app)/colonies/page.tsx +++ b/ApiCore-Frontend/src/app/(app)/colonies/page.tsx @@ -2,11 +2,11 @@ import { useState } from "react"; import Link from "next/link"; -import { Layers } from "lucide-react"; +import { ChevronDown, Layers } from "lucide-react"; import { useMutation, useQuery } from "@tanstack/react-query"; import { toast } from "sonner"; import { fetchColonies } from "@/lib/colonies/api"; -import { ColonyStatus } from "@/lib/colonies/types"; +import { Colony, ColonyStatus } from "@/lib/colonies/types"; import { COLONY_CONDITION_LABELS, COLONY_STATUS_LABELS, COLONY_TYPE_LABELS } from "@/lib/colonies/labels"; import { exportColoniesCsv } from "@/lib/export/api"; import { statusTone } from "@/lib/status-tone"; @@ -15,7 +15,14 @@ import { Badge } from "@/components/ui/badge"; import { PageHeader } from "@/components/page-header"; import { EmptyState } from "@/components/empty-state"; import { EntityCard } from "@/components/entity-card"; +import { QueryState } from "@/components/query-state"; import { ActiveHistoryTabs, type EntityLifecycleTab } from "@/components/active-history-tabs"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; import { Select, SelectContent, @@ -31,7 +38,7 @@ export default function ColoniesPage() { const [tab, setTab] = useState("active"); const [status, setStatus] = useState(""); - const { data: colonies, isLoading, isError } = useQuery({ + const coloniesQuery = useQuery({ queryKey: ["colonies"], queryFn: () => fetchColonies({}), }); @@ -41,8 +48,9 @@ export default function ColoniesPage() { onError: () => toast.error("CSV-Export konnte nicht heruntergeladen werden"), }); - const activeColonies = (colonies ?? []).filter((colony) => colony.deactivatedAt === null); - const historyColonies = (colonies ?? []).filter((colony) => colony.deactivatedAt !== null); + const colonies = coloniesQuery.data ?? []; + const activeColonies = colonies.filter((colony) => colony.deactivatedAt === null); + const historyColonies = colonies.filter((colony) => colony.deactivatedAt !== null); const bucketColonies = tab === "active" ? activeColonies : historyColonies; const visibleColonies = status ? bucketColonies.filter((colony) => colony.status === status) : bucketColonies; const statusOptions = tab === "active" ? ACTIVE_STATUS_OPTIONS : HISTORY_STATUS_OPTIONS; @@ -52,17 +60,25 @@ export default function ColoniesPage() { - - @@ -83,7 +99,7 @@ export default function ColoniesPage() { value={status || "ALL"} onValueChange={(value) => setStatus(value === "ALL" ? "" : ((value ?? "") as ColonyStatus))} > - + {status ? COLONY_STATUS_LABELS[status] : "Alle Status"} @@ -99,50 +115,91 @@ export default function ColoniesPage() { - {isLoading ?

Lädt…

: null} - {isError ? ( -

- Völker konnten nicht geladen werden. -

- ) : null} - - {colonies && visibleColonies.length === 0 ? ( - - ) : null} - - {visibleColonies.length > 0 ? ( -
- {visibleColonies.map((colony) => ( - {colony.code}} - subtitle={COLONY_TYPE_LABELS[colony.colonyType]} - badge={ - - {COLONY_STATUS_LABELS[colony.status]} - - } - fields={[ - { label: "Stärke", value: colony.strength ?? "—" }, - { - label: "Gesundheit", - value: colony.healthStatus ? COLONY_CONDITION_LABELS[colony.healthStatus] : "—", - }, - ]} + + {() => + visibleColonies.length === 0 ? ( + 0} + onResetFilter={() => setStatus("")} /> - ))} -
- ) : null} + ) : ( + + ) + } + ); } + +function ColoniesEmpty({ + tab, + isFiltered, + onResetFilter, +}: { + tab: EntityLifecycleTab; + isFiltered: boolean; + onResetFilter: () => void; +}) { + if (isFiltered) { + return ( + + Filter zurücksetzen + + } + /> + ); + } + if (tab === "history") { + return ( + + ); + } + return ( + }> + Erstes Volk anlegen + + } + /> + ); +} + +function ColonyGrid({ colonies }: { colonies: Colony[] }) { + return ( +
+ {colonies.map((colony) => ( + {colony.code}} + subtitle={COLONY_TYPE_LABELS[colony.colonyType]} + badge={ + {COLONY_STATUS_LABELS[colony.status]} + } + fields={[ + { label: "Stärke", value: colony.strength ?? "—" }, + { + label: "Gesundheit", + value: colony.healthStatus ? COLONY_CONDITION_LABELS[colony.healthStatus] : "—", + }, + ]} + /> + ))} +
+ ); +} diff --git a/ApiCore-Frontend/src/app/(app)/equipment/page.tsx b/ApiCore-Frontend/src/app/(app)/equipment/page.tsx index f8d78b7..15ebe5e 100644 --- a/ApiCore-Frontend/src/app/(app)/equipment/page.tsx +++ b/ApiCore-Frontend/src/app/(app)/equipment/page.tsx @@ -11,9 +11,10 @@ import { Button } from "@/components/ui/button"; import { PageHeader } from "@/components/page-header"; import { EmptyState } from "@/components/empty-state"; import { EntityCard } from "@/components/entity-card"; +import { QueryState } from "@/components/query-state"; export default function EquipmentPage() { - const { data: equipmentList, isLoading, isError } = useQuery({ + const equipmentQuery = useQuery({ queryKey: ["equipment"], queryFn: () => fetchEquipmentList(), }); @@ -23,50 +24,52 @@ export default function EquipmentPage() { }> + } /> - {isLoading ?

Lädt…

: null} - {isError ? ( -

- Inventar konnte nicht geladen werden. -

- ) : null} - - {equipmentList && equipmentList.length === 0 ? ( - - ) : null} - - {equipmentList && equipmentList.length > 0 ? ( -
- {equipmentList.map((item) => ( - - {EQUIPMENT_CONDITION_LABELS[item.condition]} - - } - fields={[{ label: "Standort", value: item.location ?? "—" }]} - /> - ))} -
- ) : null} + }> + Ersten Gegenstand erfassen + + } + /> + } + > + {(equipmentList) => ( +
+ {equipmentList.map((item) => ( + + {EQUIPMENT_CONDITION_LABELS[item.condition]} + + } + fields={[{ label: "Standort", value: item.location ?? "—" }]} + /> + ))} +
+ )} +
); } diff --git a/ApiCore-Frontend/src/app/(app)/extractions/page.tsx b/ApiCore-Frontend/src/app/(app)/extractions/page.tsx index 5bdc3d7..8e839db 100644 --- a/ApiCore-Frontend/src/app/(app)/extractions/page.tsx +++ b/ApiCore-Frontend/src/app/(app)/extractions/page.tsx @@ -8,70 +8,67 @@ import { Button } from "@/components/ui/button"; import { PageHeader } from "@/components/page-header"; import { EmptyState } from "@/components/empty-state"; import { EntityCard } from "@/components/entity-card"; +import { QueryState } from "@/components/query-state"; export default function ExtractionsPage() { - const { - data: extractions, - isLoading, - isError, - } = useQuery({ queryKey: ["extractions"], queryFn: fetchExtractions }); - - const sortedExtractions = [...(extractions ?? [])].sort( - (a, b) => new Date(b.date).getTime() - new Date(a.date).getTime(), - ); + const extractionsQuery = useQuery({ queryKey: ["extractions"], queryFn: fetchExtractions }); return (
}> + } /> - {isLoading ?

Lädt…

: null} - {isError ? ( -

- Schleuderungen konnten nicht geladen werden. -

- ) : null} - - {extractions && sortedExtractions.length === 0 ? ( - - ) : null} - - {sortedExtractions.length > 0 ? ( -
- {sortedExtractions.map((extraction) => ( - - ))} -
- ) : null} + }> + Erste Schleuderung erfassen + + } + /> + } + > + {(extractions) => ( +
+ {[...extractions] + .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()) + .map((extraction) => ( + + ))} +
+ )} +
); } diff --git a/ApiCore-Frontend/src/app/(app)/harvests/page.tsx b/ApiCore-Frontend/src/app/(app)/harvests/page.tsx index 0e5cf8c..e103d3e 100644 --- a/ApiCore-Frontend/src/app/(app)/harvests/page.tsx +++ b/ApiCore-Frontend/src/app/(app)/harvests/page.tsx @@ -9,69 +9,68 @@ import { Button } from "@/components/ui/button"; import { PageHeader } from "@/components/page-header"; import { EmptyState } from "@/components/empty-state"; import { EntityCard } from "@/components/entity-card"; +import { QueryState } from "@/components/query-state"; export default function HarvestsPage() { - const { - data: harvests, - isLoading, - isError, - } = useQuery({ queryKey: ["harvests"], queryFn: fetchHarvests }); + const harvestsQuery = useQuery({ queryKey: ["harvests"], queryFn: fetchHarvests }); + // Only used for apiary names; without it the cards fall back to "—". const { data: apiaries } = useQuery({ queryKey: ["apiaries"], queryFn: () => fetchApiaries() }); const apiaryNameById = new Map((apiaries ?? []).map((apiary) => [apiary.id, apiary.name])); - const sortedHarvests = [...(harvests ?? [])].sort( - (a, b) => new Date(b.date).getTime() - new Date(a.date).getTime(), - ); return (
}> + } /> - {isLoading ?

Lädt…

: null} - {isError ? ( -

- Ernten konnten nicht geladen werden. -

- ) : null} - - {harvests && sortedHarvests.length === 0 ? ( - - ) : null} - - {sortedHarvests.length > 0 ? ( -
- {sortedHarvests.map((harvest) => ( - - ))} -
- ) : null} + }> + Erste Ernte erfassen + + } + /> + } + > + {(harvests) => ( +
+ {[...harvests] + .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()) + .map((harvest) => ( + + ))} +
+ )} +
); } diff --git a/ApiCore-Frontend/src/app/(app)/honey-batches/page.tsx b/ApiCore-Frontend/src/app/(app)/honey-batches/page.tsx index 99277c9..84e943f 100644 --- a/ApiCore-Frontend/src/app/(app)/honey-batches/page.tsx +++ b/ApiCore-Frontend/src/app/(app)/honey-batches/page.tsx @@ -1,11 +1,11 @@ "use client"; import { useState } from "react"; -import { Droplet } from "lucide-react"; +import { ChevronDown, Droplet } from "lucide-react"; import { useMutation, useQuery } from "@tanstack/react-query"; import { toast } from "sonner"; import { fetchHoneyBatches } from "@/lib/honey-batches/api"; -import { HoneyBatchStatus } from "@/lib/honey-batches/types"; +import { HoneyBatch, HoneyBatchStatus } from "@/lib/honey-batches/types"; import { HONEY_BATCH_STATUS_LABELS } from "@/lib/honey-batches/labels"; import { exportHoneyBatchesCsv } from "@/lib/export/api"; import { statusTone } from "@/lib/status-tone"; @@ -14,7 +14,14 @@ import { Button } from "@/components/ui/button"; import { PageHeader } from "@/components/page-header"; import { EmptyState } from "@/components/empty-state"; import { EntityCard } from "@/components/entity-card"; +import { QueryState } from "@/components/query-state"; import { ActiveHistoryTabs, type EntityLifecycleTab } from "@/components/active-history-tabs"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; import { CreateHoneyBatchDialog } from "./create-honey-batch-dialog"; const HISTORY_STATUSES: HoneyBatchStatus[] = ["SOLD", "CONSUMED"]; @@ -22,7 +29,7 @@ const HISTORY_STATUSES: HoneyBatchStatus[] = ["SOLD", "CONSUMED"]; export default function HoneyBatchesPage() { const [tab, setTab] = useState("active"); - const { data: honeyBatches, isLoading, isError } = useQuery({ + const honeyBatchesQuery = useQuery({ queryKey: ["honey-batches"], queryFn: () => fetchHoneyBatches({}), }); @@ -32,12 +39,9 @@ export default function HoneyBatchesPage() { onError: () => toast.error("CSV-Export konnte nicht heruntergeladen werden"), }); - const activeBatches = (honeyBatches ?? []).filter( - (batch) => !HISTORY_STATUSES.includes(batch.status), - ); - const historyBatches = (honeyBatches ?? []).filter((batch) => - HISTORY_STATUSES.includes(batch.status), - ); + const honeyBatches = honeyBatchesQuery.data ?? []; + const activeBatches = honeyBatches.filter((batch) => !HISTORY_STATUSES.includes(batch.status)); + const historyBatches = honeyBatches.filter((batch) => HISTORY_STATUSES.includes(batch.status)); const visibleBatches = tab === "active" ? activeBatches : historyBatches; return ( @@ -45,16 +49,24 @@ export default function HoneyBatchesPage() { - + + }> + Mehr + + + + exportMutation.mutate()} + > + CSV exportieren + + + } @@ -67,49 +79,55 @@ export default function HoneyBatchesPage() { historyCount={historyBatches.length} /> - {isLoading ?

Lädt…

: null} - {isError ? ( -

- Honigchargen konnten nicht geladen werden. -

- ) : null} - - {honeyBatches && visibleBatches.length === 0 ? ( - - ) : null} - - {visibleBatches.length > 0 ? ( -
- {visibleBatches.map((honeyBatch) => ( - {honeyBatch.batchNumber}} - subtitle={honeyBatch.honeyType ?? "—"} - badge={ - - {HONEY_BATCH_STATUS_LABELS[honeyBatch.status]} - - } - fields={[ - { - label: "Menge", - value: honeyBatch.amount !== null ? `${honeyBatch.amount} kg` : "—", - }, - ]} - /> - ))} -
- ) : null} + + {() => + visibleBatches.length === 0 ? ( + tab === "active" ? ( + } + /> + ) : ( + + ) + ) : ( + + ) + } + ); } + +function HoneyBatchGrid({ honeyBatches }: { honeyBatches: HoneyBatch[] }) { + return ( +
+ {honeyBatches.map((honeyBatch) => ( + {honeyBatch.batchNumber}} + subtitle={honeyBatch.honeyType ?? "—"} + badge={ + + {HONEY_BATCH_STATUS_LABELS[honeyBatch.status]} + + } + fields={[ + { + label: "Menge", + value: honeyBatch.amount !== null ? `${honeyBatch.amount} kg` : "—", + }, + ]} + /> + ))} +
+ ); +} diff --git a/ApiCore-Frontend/src/app/(app)/products/page.tsx b/ApiCore-Frontend/src/app/(app)/products/page.tsx index 1b29136..e1bb7b4 100644 --- a/ApiCore-Frontend/src/app/(app)/products/page.tsx +++ b/ApiCore-Frontend/src/app/(app)/products/page.tsx @@ -5,6 +5,7 @@ import Link from "next/link"; import { ShoppingBag } from "lucide-react"; import { useQuery } from "@tanstack/react-query"; import { fetchProducts } from "@/lib/products/api"; +import { Product } from "@/lib/products/types"; import { PRODUCT_STATUS_LABELS } from "@/lib/products/labels"; import { EditProductDialog } from "./edit-product-dialog"; import { Badge } from "@/components/ui/badge"; @@ -12,18 +13,20 @@ import { Button } from "@/components/ui/button"; import { PageHeader } from "@/components/page-header"; import { EmptyState } from "@/components/empty-state"; import { EntityCard } from "@/components/entity-card"; +import { QueryState } from "@/components/query-state"; import { ActiveHistoryTabs, type EntityLifecycleTab } from "@/components/active-history-tabs"; export default function ProductsPage() { const [tab, setTab] = useState("active"); - const { data: products, isLoading, isError } = useQuery({ + const productsQuery = useQuery({ queryKey: ["products"], queryFn: () => fetchProducts(), }); - const activeProducts = (products ?? []).filter((product) => product.status === "ACTIVE"); - const historyProducts = (products ?? []).filter((product) => product.status !== "ACTIVE"); + const products = productsQuery.data ?? []; + const activeProducts = products.filter((product) => product.status === "ACTIVE"); + const historyProducts = products.filter((product) => product.status !== "ACTIVE"); const visibleProducts = tab === "active" ? activeProducts : historyProducts; return ( @@ -31,9 +34,9 @@ export default function ProductsPage() { }> + } @@ -46,46 +49,56 @@ export default function ProductsPage() { historyCount={historyProducts.length} /> - {isLoading ?

Lädt…

: null} - {isError ? ( -

- Produkte konnten nicht geladen werden. -

- ) : null} - - {products && visibleProducts.length === 0 ? ( - - ) : null} - - {visibleProducts.length > 0 ? ( -
- {visibleProducts.map((product) => ( - - {PRODUCT_STATUS_LABELS[product.status]} - - } - fields={[ - { label: "SKU", value: product.sku ?? "—" }, - { label: "Einheit", value: product.salesUnit ?? "—" }, - ]} - actions={} - /> - ))} -
- ) : null} + + {() => + visibleProducts.length === 0 ? ( + tab === "active" ? ( + }> + Erstes Produkt anlegen + + } + /> + ) : ( + + ) + ) : ( + + ) + } + ); } + +function ProductGrid({ products }: { products: Product[] }) { + return ( +
+ {products.map((product) => ( + + {PRODUCT_STATUS_LABELS[product.status]} + + } + fields={[ + { label: "SKU", value: product.sku ?? "—" }, + { label: "Einheit", value: product.salesUnit ?? "—" }, + ]} + actions={} + /> + ))} +
+ ); +} diff --git a/ApiCore-Frontend/src/app/(app)/queens/page.tsx b/ApiCore-Frontend/src/app/(app)/queens/page.tsx index ab4fa58..0e43d37 100644 --- a/ApiCore-Frontend/src/app/(app)/queens/page.tsx +++ b/ApiCore-Frontend/src/app/(app)/queens/page.tsx @@ -5,13 +5,14 @@ import Link from "next/link"; import { Crown } from "lucide-react"; import { useQuery } from "@tanstack/react-query"; import { fetchQueens } from "@/lib/queens/api"; -import { QueenStatus } from "@/lib/queens/types"; +import { Queen, QueenStatus } from "@/lib/queens/types"; import { QUEEN_STATUSES, QUEEN_STATUS_LABELS } from "@/lib/queens/labels"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { PageHeader } from "@/components/page-header"; import { EmptyState } from "@/components/empty-state"; import { EntityCard } from "@/components/entity-card"; +import { QueryState } from "@/components/query-state"; import { ActiveHistoryTabs, type EntityLifecycleTab } from "@/components/active-history-tabs"; import { Select, @@ -25,13 +26,14 @@ export default function QueensPage() { const [tab, setTab] = useState("active"); const [status, setStatus] = useState(""); - const { data: queens, isLoading, isError } = useQuery({ + const queensQuery = useQuery({ queryKey: ["queens"], queryFn: () => fetchQueens({}), }); - const activeQueens = (queens ?? []).filter((queen) => queen.deactivatedAt === null); - const historyQueens = (queens ?? []).filter((queen) => queen.deactivatedAt !== null); + const queens = queensQuery.data ?? []; + const activeQueens = queens.filter((queen) => queen.deactivatedAt === null); + const historyQueens = queens.filter((queen) => queen.deactivatedAt !== null); const bucketQueens = tab === "active" ? activeQueens : historyQueens; const visibleQueens = status ? bucketQueens.filter((queen) => queen.status === status) : bucketQueens; @@ -40,10 +42,10 @@ export default function QueensPage() { }> + } @@ -63,7 +65,7 @@ export default function QueensPage() { value={status || "ALL"} onValueChange={(value) => setStatus(value === "ALL" ? "" : ((value ?? "") as QueenStatus))} > - + {status ? QUEEN_STATUS_LABELS[status] : "Alle Status"} @@ -79,48 +81,91 @@ export default function QueensPage() { - {isLoading ?

Lädt…

: null} - {isError ? ( -

- Königinnen konnten nicht geladen werden. -

- ) : null} - - {queens && visibleQueens.length === 0 ? ( - - ) : null} - - {visibleQueens.length > 0 ? ( -
- {visibleQueens.map((queen) => ( - - {QUEEN_STATUS_LABELS[queen.status]} - - } - fields={[ - { label: "Schlupfjahr", value: queen.hatchYear ?? "—" }, - { label: "Zeichenfarbe", value: queen.markingColor ?? "—" }, - ]} + + {() => + visibleQueens.length === 0 ? ( + 0} + onResetFilter={() => setStatus("")} /> - ))} -
- ) : null} + ) : ( + + ) + } + ); } + +function QueensEmpty({ + tab, + isFiltered, + onResetFilter, +}: { + tab: EntityLifecycleTab; + isFiltered: boolean; + onResetFilter: () => void; +}) { + if (isFiltered) { + return ( + + Filter zurücksetzen + + } + /> + ); + } + if (tab === "history") { + return ( + + ); + } + return ( + }> + Erste Königin anlegen + + } + /> + ); +} + +function QueenGrid({ queens }: { queens: Queen[] }) { + return ( +
+ {queens.map((queen) => ( + + {QUEEN_STATUS_LABELS[queen.status]} + + } + fields={[ + { label: "Schlupfjahr", value: queen.hatchYear ?? "—" }, + { label: "Zeichenfarbe", value: queen.markingColor ?? "—" }, + ]} + /> + ))} +
+ ); +}