feat(frontend): Leerer-Zustand-Komponente für alle Listenseiten
Neue gemeinsame EmptyState-Komponente (Icon in Kreis, Titel, Beschreibung,
gestrichelter Rahmen) und Einbindung auf allen 9 Listenseiten mit Tabellen
(Völker, Bienenstände, Königinnen, Aufgaben, Honigchargen, Benutzer,
Inventar, Produkte, Lager mit Behältern und Artikeln).
Bug behoben: Bei leerem Datensatz wurde bisher nur eine Tabelle mit
Kopfzeile und keinerlei Inhalt oder Erklärung angezeigt – wirkte wie ein
Ladefehler statt eines normalen "noch keine Daten"-Zustands. Besonders
für neurodivergente Nutzer wichtig, da Unklarheit über den Systemzustand
("ist das kaputt oder einfach leer?") unnötige kognitive Last erzeugt.
This commit is contained in:
1 parent
fc88c272ae
commit
ebfbd2ef6d
10 files changed
+121
-11
No files matched your search
@@ -10,6 +10,7 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { PageHeader } from "@/components/page-header";
|
import { PageHeader } from "@/components/page-header";
|
||||||
|
import { EmptyState } from "@/components/empty-state";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@@ -77,7 +78,15 @@ export default function ApiariesPage() {
|
|||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{apiaries ? (
|
{apiaries && apiaries.length === 0 ? (
|
||||||
|
<EmptyState
|
||||||
|
icon={MapPin}
|
||||||
|
title="Noch keine Bienenstände"
|
||||||
|
description="Lege deinen ersten Bienenstand an, um Völker zuzuordnen."
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{apiaries && apiaries.length > 0 ? (
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { statusTone } from "@/lib/status-tone";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { PageHeader } from "@/components/page-header";
|
import { PageHeader } from "@/components/page-header";
|
||||||
|
import { EmptyState } from "@/components/empty-state";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@@ -94,7 +95,15 @@ export default function ColoniesPage() {
|
|||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{colonies ? (
|
{colonies && colonies.length === 0 ? (
|
||||||
|
<EmptyState
|
||||||
|
icon={Layers}
|
||||||
|
title="Noch keine Völker"
|
||||||
|
description="Lege dein erstes Volk an, um den Überblick zu behalten."
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{colonies && colonies.length > 0 ? (
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { statusTone } from "@/lib/status-tone";
|
|||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { PageHeader } from "@/components/page-header";
|
import { PageHeader } from "@/components/page-header";
|
||||||
|
import { EmptyState } from "@/components/empty-state";
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
@@ -44,7 +45,15 @@ export default function EquipmentPage() {
|
|||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{equipmentList ? (
|
{equipmentList && equipmentList.length === 0 ? (
|
||||||
|
<EmptyState
|
||||||
|
icon={Wrench}
|
||||||
|
title="Noch kein Inventar"
|
||||||
|
description="Erfasse deinen ersten Gegenstand, um den Zustand im Blick zu behalten."
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{equipmentList && equipmentList.length > 0 ? (
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { statusTone } from "@/lib/status-tone";
|
|||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { PageHeader } from "@/components/page-header";
|
import { PageHeader } from "@/components/page-header";
|
||||||
|
import { EmptyState } from "@/components/empty-state";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@@ -95,7 +96,15 @@ export default function HoneyBatchesPage() {
|
|||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{honeyBatches ? (
|
{honeyBatches && honeyBatches.length === 0 ? (
|
||||||
|
<EmptyState
|
||||||
|
icon={Droplet}
|
||||||
|
title="Noch keine Honigchargen"
|
||||||
|
description="Lege eine Charge an, sobald du geschleudert hast."
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{honeyBatches && honeyBatches.length > 0 ? (
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { fetchProducts } from "@/lib/products/api";
|
|||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { PageHeader } from "@/components/page-header";
|
import { PageHeader } from "@/components/page-header";
|
||||||
|
import { EmptyState } from "@/components/empty-state";
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
@@ -42,7 +43,15 @@ export default function ProductsPage() {
|
|||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{products ? (
|
{products && products.length === 0 ? (
|
||||||
|
<EmptyState
|
||||||
|
icon={ShoppingBag}
|
||||||
|
title="Noch keine Produkte"
|
||||||
|
description="Lege dein erstes verkaufsfertiges Produkt an."
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{products && products.length > 0 ? (
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { QueenStatus } from "@/lib/queens/types";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { PageHeader } from "@/components/page-header";
|
import { PageHeader } from "@/components/page-header";
|
||||||
|
import { EmptyState } from "@/components/empty-state";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@@ -82,7 +83,15 @@ export default function QueensPage() {
|
|||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{queens ? (
|
{queens && queens.length === 0 ? (
|
||||||
|
<EmptyState
|
||||||
|
icon={Crown}
|
||||||
|
title="Noch keine Königinnen"
|
||||||
|
description="Lege deine erste Königin an, um Zuchtlinien zu verfolgen."
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{queens && queens.length > 0 ? (
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { ConfirmDialog } from "@/components/confirm-dialog";
|
import { ConfirmDialog } from "@/components/confirm-dialog";
|
||||||
import { PageHeader } from "@/components/page-header";
|
import { PageHeader } from "@/components/page-header";
|
||||||
|
import { EmptyState } from "@/components/empty-state";
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
@@ -62,7 +63,11 @@ export default function UsersPage() {
|
|||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{users ? (
|
{users && users.length === 0 ? (
|
||||||
|
<EmptyState icon={Users} title="Noch keine Benutzer" description="Lege den ersten Zugang an." />
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{users && users.length > 0 ? (
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Warehouse } from "lucide-react";
|
import { Package, Warehouse } from "lucide-react";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { fetchStockItems, fetchStorageContainers } from "@/lib/storage/api";
|
import { fetchStockItems, fetchStorageContainers } from "@/lib/storage/api";
|
||||||
import { QrCodeDialog } from "@/components/qr-code-dialog";
|
import { QrCodeDialog } from "@/components/qr-code-dialog";
|
||||||
import { PageHeader } from "@/components/page-header";
|
import { PageHeader } from "@/components/page-header";
|
||||||
|
import { EmptyState } from "@/components/empty-state";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
@@ -40,7 +41,15 @@ export default function StoragePage() {
|
|||||||
/>
|
/>
|
||||||
<h2 className="text-lg font-semibold tracking-tight">Lagerbehälter</h2>
|
<h2 className="text-lg font-semibold tracking-tight">Lagerbehälter</h2>
|
||||||
{containersLoading ? <p className="text-sm text-muted-foreground">Lädt…</p> : null}
|
{containersLoading ? <p className="text-sm text-muted-foreground">Lädt…</p> : null}
|
||||||
{containers ? (
|
{containers && containers.length === 0 ? (
|
||||||
|
<EmptyState
|
||||||
|
icon={Warehouse}
|
||||||
|
title="Noch keine Lagerbehälter"
|
||||||
|
description="Lege deinen ersten Behälter an."
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{containers && containers.length > 0 ? (
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
@@ -78,7 +87,15 @@ export default function StoragePage() {
|
|||||||
<CreateStockItemDialog />
|
<CreateStockItemDialog />
|
||||||
</div>
|
</div>
|
||||||
{itemsLoading ? <p className="text-sm text-muted-foreground">Lädt…</p> : null}
|
{itemsLoading ? <p className="text-sm text-muted-foreground">Lädt…</p> : null}
|
||||||
{stockItems ? (
|
{stockItems && stockItems.length === 0 ? (
|
||||||
|
<EmptyState
|
||||||
|
icon={Package}
|
||||||
|
title="Noch keine Lagerartikel"
|
||||||
|
description="Lege deinen ersten Artikel an, um den Bestand zu verfolgen."
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{stockItems && stockItems.length > 0 ? (
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { TaskPriority, TaskStatus } from "@/lib/tasks/types";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { PageHeader } from "@/components/page-header";
|
import { PageHeader } from "@/components/page-header";
|
||||||
|
import { EmptyState } from "@/components/empty-state";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@@ -122,7 +123,19 @@ export default function TasksPage() {
|
|||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{tasks ? (
|
{tasks && visibleTasks.length === 0 ? (
|
||||||
|
<EmptyState
|
||||||
|
icon={ListTodo}
|
||||||
|
title="Keine Aufgaben"
|
||||||
|
description={
|
||||||
|
onlyOverdue
|
||||||
|
? "Keine überfälligen Aufgaben – gut gemacht!"
|
||||||
|
: "Lege deine erste Aufgabe an, um den Überblick zu behalten."
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{tasks && visibleTasks.length > 0 ? (
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { LucideIcon } from "lucide-react";
|
||||||
|
|
||||||
|
interface EmptyStateProps {
|
||||||
|
icon: LucideIcon;
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EmptyState({ icon: Icon, title, description }: EmptyStateProps) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center gap-3 rounded-2xl border border-dashed border-border py-12 text-center">
|
||||||
|
<span className="flex size-12 items-center justify-center rounded-full bg-muted text-muted-foreground">
|
||||||
|
<Icon className="size-6" />
|
||||||
|
</span>
|
||||||
|
<div>
|
||||||
|
<p className="font-medium">{title}</p>
|
||||||
|
{description ? <p className="text-sm text-muted-foreground">{description}</p> : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in new issue
Block a user