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

@@ -12,7 +12,8 @@ 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 { ListItemCard } from "@/components/list-item-card";
import { EntityCard } from "@/components/entity-card";
import { ActiveHistoryTabs, type EntityLifecycleTab } from "@/components/active-history-tabs";
import {
Select,
SelectContent,
@@ -21,27 +22,26 @@ import {
SelectValue,
} from "@/components/ui/select";
const STATUS_OPTIONS: ColonyStatus[] = [
"ACTIVE",
"WEAK",
"CONSPICUOUS",
"QUEENLESS",
"DISSOLVED",
"SOLD",
"DECEASED",
"MERGED",
];
const ACTIVE_STATUS_OPTIONS: ColonyStatus[] = ["ACTIVE", "WEAK", "CONSPICUOUS", "QUEENLESS"];
const HISTORY_STATUS_OPTIONS: ColonyStatus[] = ["DISSOLVED", "SOLD", "DECEASED", "MERGED"];
export default function ColoniesPage() {
const [tab, setTab] = useState<EntityLifecycleTab>("active");
const [status, setStatus] = useState<ColonyStatus | "">("");
const { data: colonies, isLoading, isError } = useQuery({
queryKey: ["colonies", status],
queryFn: () => fetchColonies({ status: status || undefined }),
queryKey: ["colonies"],
queryFn: () => fetchColonies({}),
});
const exportMutation = useMutation({ mutationFn: exportColoniesCsv });
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;
return (
<main className="flex flex-col gap-4 p-4 sm:p-6">
<PageHeader
@@ -64,22 +64,33 @@ export default function ColoniesPage() {
}
/>
<Select
value={status || "ALL"}
onValueChange={(value) => setStatus(value === "ALL" ? "" : ((value ?? "") as ColonyStatus))}
>
<SelectTrigger className="w-40">
<SelectValue placeholder="Status">{status || "Alle Status"}</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectItem value="ALL">Alle Status</SelectItem>
{STATUS_OPTIONS.map((option) => (
<SelectItem key={option} value={option}>
{option}
</SelectItem>
))}
</SelectContent>
</Select>
<div className="flex flex-wrap items-center gap-3">
<ActiveHistoryTabs
value={tab}
onChange={(next) => {
setTab(next);
setStatus("");
}}
activeCount={activeColonies.length}
historyCount={historyColonies.length}
/>
<Select
value={status || "ALL"}
onValueChange={(value) => setStatus(value === "ALL" ? "" : ((value ?? "") as ColonyStatus))}
>
<SelectTrigger className="w-40">
<SelectValue placeholder="Status">{status || "Alle Status"}</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectItem value="ALL">Alle Status</SelectItem>
{statusOptions.map((option) => (
<SelectItem key={option} value={option}>
{option}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{isLoading ? <p className="text-sm text-muted-foreground">Lädt…</p> : null}
{isError ? (
@@ -88,24 +99,32 @@ export default function ColoniesPage() {
</p>
) : null}
{colonies && colonies.length === 0 ? (
{colonies && visibleColonies.length === 0 ? (
<EmptyState
icon={Layers}
title="Noch keine Völker"
description="Lege dein erstes Volk an, um den Überblick zu behalten."
title={tab === "active" ? "Noch keine Völker" : "Keine Historie"}
description={
tab === "active"
? "Lege dein erstes Volk an, um den Überblick zu behalten."
: "Aufgelöste, verkaufte oder verstorbene Völker erscheinen hier."
}
/>
) : null}
{colonies && colonies.length > 0 ? (
<div className="flex flex-col gap-2">
{colonies.map((colony) => (
<ListItemCard
{visibleColonies.length > 0 ? (
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
{visibleColonies.map((colony) => (
<EntityCard
key={colony.id}
href={`/colonies/${colony.id}`}
icon={Layers}
title={<span className="font-mono">{colony.code}</span>}
subtitle={colony.colonyType}
trailing={<Badge variant={statusTone(colony.status)}>{colony.status}</Badge>}
badge={<Badge variant={statusTone(colony.status)}>{colony.status}</Badge>}
fields={[
{ label: "Stärke", value: colony.strength ?? "—" },
{ label: "Gesundheit", value: colony.healthStatus ?? "—" },
]}
/>
))}
</div>