feat: card-grid boxes with Aktiv/Historie split on main list pages

Replace the list-row layout with a responsive box grid (EntityCard):
icon avatar, title, badge, and 1-2 key fields visible at a glance per
box, matching what the user asked for ("Kaestchen" over rows).

Add an Aktiv/Historie tab split (ActiveHistoryTabs) so deactivated or
terminal-status entries move out of the default view instead of mixing
in with active ones:
- Apiaries, Colonies, Queens: split on deactivatedAt (the field the app
  already uses everywhere for "Historie statt Loeschen").
- Task templates: split on the existing active boolean.
- Tasks: OPEN/IN_PROGRESS vs DONE/CANCELLED.
- Honey batches: everything vs SOLD/CONSUMED.
- Products: ACTIVE vs DISCONTINUED.
- Storage containers: everything vs OUT_OF_SERVICE.
Equipment and stock items have no lifecycle field in the schema, so
they keep the plain grid without a Historie tab.

Verified live: deactivating a Bienenstand moves it out of Aktiv into
Historie immediately, both counts update, reactivating moves it back.
This commit is contained in:
Nick Szaunig committed 2026-08-26 00:25:23 +02:00
1 parent c586bff18f
commit d31aa79f50
12 files changed
+496 -320

No files matched your search

@@ -5,30 +5,30 @@ import Link from "next/link";
import { MapPin } from "lucide-react";
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 { PageHeader } from "@/components/page-header";
import { EmptyState } from "@/components/empty-state";
import { ListItemCard } from "@/components/list-item-card";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { EntityCard } from "@/components/entity-card";
import { ActiveHistoryTabs, type EntityLifecycleTab } from "@/components/active-history-tabs";
export default function ApiariesPage() {
const [status, setStatus] = useState<ApiaryStatus | "">("");
const [tab, setTab] = useState<EntityLifecycleTab>("active");
const [location, setLocation] = useState("");
const { data: apiaries, isLoading, isError } = useQuery({
queryKey: ["apiaries", status, location],
queryFn: () => fetchApiaries({ status: status || undefined, location: location || undefined }),
queryKey: ["apiaries"],
queryFn: () => fetchApiaries({}),
});
const byLocation = (apiaries ?? []).filter((apiary) =>
location ? (apiary.location ?? "").toLowerCase().includes(location.toLowerCase()) : true,
);
const activeApiaries = byLocation.filter((apiary) => apiary.status === "ACTIVE");
const historyApiaries = byLocation.filter((apiary) => apiary.status !== "ACTIVE");
const visibleApiaries = tab === "active" ? activeApiaries : historyApiaries;
return (
<main className="flex flex-col gap-4 p-4 sm:p-6">
<PageHeader
@@ -48,19 +48,12 @@ export default function ApiariesPage() {
/>
<div className="flex flex-wrap items-center gap-3">
<Select
value={status || "ALL"}
onValueChange={(value) => setStatus(value === "ALL" ? "" : ((value ?? "") as ApiaryStatus))}
>
<SelectTrigger className="w-40">
<SelectValue placeholder="Status">{status || "Alle Status"}</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectItem value="ALL">Alle Status</SelectItem>
<SelectItem value="ACTIVE">Aktiv</SelectItem>
<SelectItem value="INACTIVE">Inaktiv</SelectItem>
</SelectContent>
</Select>
<ActiveHistoryTabs
value={tab}
onChange={setTab}
activeCount={activeApiaries.length}
historyCount={historyApiaries.length}
/>
<Input
className="w-48"
placeholder="Standort"
@@ -76,28 +69,33 @@ export default function ApiariesPage() {
</p>
) : null}
{apiaries && apiaries.length === 0 ? (
{apiaries && visibleApiaries.length === 0 ? (
<EmptyState
icon={MapPin}
title="Noch keine Bienenstände"
description="Lege deinen ersten Bienenstand an, um Völker zuzuordnen."
title={tab === "active" ? "Noch keine Bienenstände" : "Keine Historie"}
description={
tab === "active"
? "Lege deinen ersten Bienenstand an, um Völker zuzuordnen."
: "Deaktivierte Bienenstände erscheinen hier."
}
/>
) : null}
{apiaries && apiaries.length > 0 ? (
<div className="flex flex-col gap-2">
{apiaries.map((apiary) => (
<ListItemCard
{visibleApiaries.length > 0 ? (
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
{visibleApiaries.map((apiary) => (
<EntityCard
key={apiary.id}
href={`/apiaries/${apiary.id}`}
icon={MapPin}
title={apiary.name}
subtitle={apiary.location ?? "—"}
trailing={
badge={
<Badge variant={apiary.status === "ACTIVE" ? "default" : "secondary"}>
{apiary.status === "ACTIVE" ? "Aktiv" : "Inaktiv"}
</Badge>
}
fields={[{ label: "Adresse", value: apiary.address ?? "—" }]}
/>
))}
</div>