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:
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 {
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { apiFetch } from "@/lib/auth/api-client";
|
||||
import { DashboardAlert, DashboardSummary } from "./types";
|
||||
|
||||
export function fetchDashboardSummary(): Promise<DashboardSummary> {
|
||||
return apiFetch<DashboardSummary>("/dashboard/summary");
|
||||
}
|
||||
|
||||
export function fetchDashboardAlerts(): Promise<DashboardAlert[]> {
|
||||
return apiFetch<DashboardAlert[]>("/dashboard/alerts");
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
export interface DashboardSummary {
|
||||
apiariesActive: number;
|
||||
coloniesActive: number;
|
||||
queensActive: number;
|
||||
openTasks: number;
|
||||
honeyBatchesTotal: number;
|
||||
stockItemsLowCount: number;
|
||||
}
|
||||
|
||||
export type DashboardAlertType = "OVERDUE_TASK" | "COLONY_ATTENTION" | "LOW_STOCK";
|
||||
|
||||
export interface DashboardAlert {
|
||||
type: DashboardAlertType;
|
||||
message: string;
|
||||
relatedType: string;
|
||||
relatedId: string;
|
||||
}
|
||||
@@ -115,7 +115,7 @@ Abgeleitet aus [`docs/lastenheft.md`](docs/lastenheft.md) Abschnitt 26. Reihenfo
|
||||
- Endpunkte: `GET /api/v1/dashboard/summary`, `GET /api/v1/dashboard/alerts`
|
||||
- Frontend: `/dashboard` (Startseite nach Login)
|
||||
- [x] Backend-Modul fertig, getestet, grün, committet
|
||||
- [ ] Frontend-Views fertig, getestet, grün, committet
|
||||
- [x] Frontend-Views fertig, getestet, grün, committet
|
||||
|
||||
### 14. Suche und Filter
|
||||
|
||||
|
||||
Reference in new issue
Block a user