feat(frontend): Dashboard-Ansicht (Modul 13)

/dashboard als Startseite nach Login mit Kennzahlen-Kacheln und
Hinweisen (überfällige Aufgaben, auffällige Völker, Lagerartikel
unter Mindestbestand).
This commit is contained in:
Nick Szaunig committed 2026-08-22 05:22:50 +02:00
1 parent 09407d4378
commit 55b9bc2587
7 files changed
+123 -3

No files matched your search

@@ -0,0 +1,90 @@
"use client";
import Link from "next/link";
import { useQuery } from "@tanstack/react-query";
import { fetchDashboardAlerts, fetchDashboardSummary } from "@/lib/dashboard/api";
import { DashboardAlert } from "@/lib/dashboard/types";
import { Badge } from "@/components/ui/badge";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
function alertHref(alert: DashboardAlert): string | null {
if (alert.relatedType === "Colony") return `/colonies/${alert.relatedId}`;
if (alert.relatedType === "Task") return "/tasks";
if (alert.relatedType === "StockItem") return "/storage";
return null;
}
export default function DashboardPage() {
const { data: summary, isLoading: summaryLoading } = useQuery({
queryKey: ["dashboard", "summary"],
queryFn: fetchDashboardSummary,
});
const { data: alerts, isLoading: alertsLoading } = useQuery({
queryKey: ["dashboard", "alerts"],
queryFn: fetchDashboardAlerts,
});
const tiles = summary
? [
{ label: "Aktive Bienenstände", value: summary.apiariesActive },
{ label: "Aktive Völker", value: summary.coloniesActive },
{ label: "Aktive Königinnen", value: summary.queensActive },
{ label: "Offene Aufgaben", value: summary.openTasks },
{ label: "Honigchargen", value: summary.honeyBatchesTotal },
{ label: "Artikel unter Mindestbestand", value: summary.stockItemsLowCount },
]
: [];
return (
<main className="flex flex-col gap-6 p-4 sm:p-6">
<h1 className="text-xl font-semibold">Dashboard</h1>
{summaryLoading ? <p className="text-sm text-muted-foreground">Lädt…</p> : null}
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3">
{tiles.map((tile) => (
<Card key={tile.label}>
<CardHeader className="pb-1">
<CardTitle className="text-sm font-normal text-muted-foreground">
{tile.label}
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-2xl font-semibold">{tile.value}</p>
</CardContent>
</Card>
))}
</div>
<Card>
<CardHeader>
<CardTitle>Hinweise</CardTitle>
</CardHeader>
<CardContent className="flex flex-col gap-2 text-sm">
{alertsLoading ? <p className="text-muted-foreground">Lädt…</p> : null}
{!alertsLoading && (!alerts || alerts.length === 0) ? (
<p className="text-muted-foreground">Keine offenen Hinweise.</p>
) : null}
{alerts?.map((alert) => {
const href = alertHref(alert);
return (
<div
key={`${alert.type}-${alert.relatedId}`}
className="flex items-center justify-between rounded-md border p-3"
>
<span>{alert.message}</span>
<div className="flex items-center gap-2">
<Badge variant="secondary">{alert.type}</Badge>
{href ? (
<Link className="underline" href={href}>
Ansehen
</Link>
) : null}
</div>
</div>
);
})}
</CardContent>
</Card>
</main>
);
}
@@ -25,6 +25,9 @@ export default function AppLayout({ children }: { children: ReactNode }) {
<header className="flex items-center justify-between border-b px-4 py-3">
<nav className="flex items-center gap-4">
<span className="font-semibold">ApiCore</span>
<Link className="text-sm text-muted-foreground hover:text-foreground" href="/dashboard">
Dashboard
</Link>
<Link className="text-sm text-muted-foreground hover:text-foreground" href="/apiaries">
Bienenstände
</Link>
@@ -28,7 +28,7 @@ describe("LoginPage", () => {
await userEvent.click(screen.getByRole("button", { name: /anmelden/i }));
expect(loginMock).toHaveBeenCalledWith("[email protected]", "ChangeMe123!");
expect(pushMock).toHaveBeenCalledWith("/settings/users");
expect(pushMock).toHaveBeenCalledWith("/dashboard");
});
it("shows an error message when login fails", async () => {
@@ -28,7 +28,7 @@ export default function LoginPage() {
setIsSubmitting(true);
try {
await login(email, password);
router.push("/settings/users");
router.push("/dashboard");
} catch {
setError("Login fehlgeschlagen. Bitte E-Mail und Passwort prüfen.");
} finally {