feat: modernize main list pages from tables to card-based rows
New shared ListItemCard component: icon avatar, title/subtitle, trailing badges/actions, chevron affordance when linked to a detail page. Applied to the 9 primary data-list pages (Bienenstaende, Voelker, Koeniginnen, Honigchargen, Aufgaben, Lager, Produkte, Inventar, Wiederkehrende Aufgaben), replacing the plain HTML tables with the same rounded-card list aesthetic already used elsewhere in the app (inspections, treatments, varroa, etc.), for a consistent look across the whole product. Admin/settings tables (Benutzer, Webhooks, Audit-Log, Import) intentionally left as tables per user's scoping choice.
This commit is contained in:
1 parent
faefd47e4b
commit
c586bff18f
10 files changed
+185
-305
No files matched your search
@@ -11,6 +11,7 @@ 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 { EmptyState } from "@/components/empty-state";
|
||||||
|
import { ListItemCard } from "@/components/list-item-card";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@@ -18,14 +19,6 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import {
|
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableCell,
|
|
||||||
TableHead,
|
|
||||||
TableHeader,
|
|
||||||
TableRow,
|
|
||||||
} from "@/components/ui/table";
|
|
||||||
|
|
||||||
export default function ApiariesPage() {
|
export default function ApiariesPage() {
|
||||||
const [status, setStatus] = useState<ApiaryStatus | "">("");
|
const [status, setStatus] = useState<ApiaryStatus | "">("");
|
||||||
@@ -92,32 +85,22 @@ export default function ApiariesPage() {
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{apiaries && apiaries.length > 0 ? (
|
{apiaries && apiaries.length > 0 ? (
|
||||||
<Table>
|
<div className="flex flex-col gap-2">
|
||||||
<TableHeader>
|
|
||||||
<TableRow>
|
|
||||||
<TableHead>Name</TableHead>
|
|
||||||
<TableHead>Standort</TableHead>
|
|
||||||
<TableHead>Status</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{apiaries.map((apiary) => (
|
{apiaries.map((apiary) => (
|
||||||
<TableRow key={apiary.id}>
|
<ListItemCard
|
||||||
<TableCell>
|
key={apiary.id}
|
||||||
<Link className="underline" href={`/apiaries/${apiary.id}`}>
|
href={`/apiaries/${apiary.id}`}
|
||||||
{apiary.name}
|
icon={MapPin}
|
||||||
</Link>
|
title={apiary.name}
|
||||||
</TableCell>
|
subtitle={apiary.location ?? "—"}
|
||||||
<TableCell>{apiary.location ?? "—"}</TableCell>
|
trailing={
|
||||||
<TableCell>
|
|
||||||
<Badge variant={apiary.status === "ACTIVE" ? "default" : "secondary"}>
|
<Badge variant={apiary.status === "ACTIVE" ? "default" : "secondary"}>
|
||||||
{apiary.status === "ACTIVE" ? "Aktiv" : "Inaktiv"}
|
{apiary.status === "ACTIVE" ? "Aktiv" : "Inaktiv"}
|
||||||
</Badge>
|
</Badge>
|
||||||
</TableCell>
|
}
|
||||||
</TableRow>
|
/>
|
||||||
))}
|
))}
|
||||||
</TableBody>
|
</div>
|
||||||
</Table>
|
|
||||||
) : null}
|
) : null}
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ 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 { EmptyState } from "@/components/empty-state";
|
||||||
|
import { ListItemCard } from "@/components/list-item-card";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@@ -19,14 +20,6 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import {
|
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableCell,
|
|
||||||
TableHead,
|
|
||||||
TableHeader,
|
|
||||||
TableRow,
|
|
||||||
} from "@/components/ui/table";
|
|
||||||
|
|
||||||
const STATUS_OPTIONS: ColonyStatus[] = [
|
const STATUS_OPTIONS: ColonyStatus[] = [
|
||||||
"ACTIVE",
|
"ACTIVE",
|
||||||
@@ -104,33 +97,18 @@ export default function ColoniesPage() {
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{colonies && colonies.length > 0 ? (
|
{colonies && colonies.length > 0 ? (
|
||||||
<Table>
|
<div className="flex flex-col gap-2">
|
||||||
<TableHeader>
|
|
||||||
<TableRow>
|
|
||||||
<TableHead>Code</TableHead>
|
|
||||||
<TableHead>Typ</TableHead>
|
|
||||||
<TableHead>Status</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{colonies.map((colony) => (
|
{colonies.map((colony) => (
|
||||||
<TableRow key={colony.id}>
|
<ListItemCard
|
||||||
<TableCell>
|
key={colony.id}
|
||||||
<Link
|
|
||||||
className="font-mono font-medium text-primary underline-offset-4 hover:underline"
|
|
||||||
href={`/colonies/${colony.id}`}
|
href={`/colonies/${colony.id}`}
|
||||||
>
|
icon={Layers}
|
||||||
{colony.code}
|
title={<span className="font-mono">{colony.code}</span>}
|
||||||
</Link>
|
subtitle={colony.colonyType}
|
||||||
</TableCell>
|
trailing={<Badge variant={statusTone(colony.status)}>{colony.status}</Badge>}
|
||||||
<TableCell>{colony.colonyType}</TableCell>
|
/>
|
||||||
<TableCell>
|
|
||||||
<Badge variant={statusTone(colony.status)}>{colony.status}</Badge>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))}
|
))}
|
||||||
</TableBody>
|
</div>
|
||||||
</Table>
|
|
||||||
) : null}
|
) : null}
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -9,14 +9,7 @@ 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 { EmptyState } from "@/components/empty-state";
|
||||||
import {
|
import { ListItemCard } from "@/components/list-item-card";
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableCell,
|
|
||||||
TableHead,
|
|
||||||
TableHeader,
|
|
||||||
TableRow,
|
|
||||||
} from "@/components/ui/table";
|
|
||||||
|
|
||||||
export default function EquipmentPage() {
|
export default function EquipmentPage() {
|
||||||
const { data: equipmentList, isLoading, isError } = useQuery({
|
const { data: equipmentList, isLoading, isError } = useQuery({
|
||||||
@@ -54,35 +47,19 @@ export default function EquipmentPage() {
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{equipmentList && equipmentList.length > 0 ? (
|
{equipmentList && equipmentList.length > 0 ? (
|
||||||
<Table>
|
<div className="flex flex-col gap-2">
|
||||||
<TableHeader>
|
|
||||||
<TableRow>
|
|
||||||
<TableHead>Inventarnummer</TableHead>
|
|
||||||
<TableHead>Bezeichnung</TableHead>
|
|
||||||
<TableHead>Standort</TableHead>
|
|
||||||
<TableHead>Zustand</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{equipmentList.map((item) => (
|
{equipmentList.map((item) => (
|
||||||
<TableRow key={item.id}>
|
<ListItemCard
|
||||||
<TableCell>
|
key={item.id}
|
||||||
<Link
|
|
||||||
className="font-mono font-medium text-primary underline-offset-4 hover:underline"
|
|
||||||
href={`/equipment/${item.id}`}
|
href={`/equipment/${item.id}`}
|
||||||
>
|
icon={Wrench}
|
||||||
{item.inventoryNumber}
|
gradient="var(--gradient-sage)"
|
||||||
</Link>
|
title={item.name}
|
||||||
</TableCell>
|
subtitle={`${item.inventoryNumber} · ${item.location ?? "—"}`}
|
||||||
<TableCell>{item.name}</TableCell>
|
trailing={<Badge variant={statusTone(item.condition)}>{item.condition}</Badge>}
|
||||||
<TableCell>{item.location ?? "—"}</TableCell>
|
/>
|
||||||
<TableCell>
|
|
||||||
<Badge variant={statusTone(item.condition)}>{item.condition}</Badge>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))}
|
))}
|
||||||
</TableBody>
|
</div>
|
||||||
</Table>
|
|
||||||
) : null}
|
) : null}
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import Link from "next/link";
|
|
||||||
import { Droplet } from "lucide-react";
|
import { Droplet } from "lucide-react";
|
||||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||||
import { fetchHoneyBatches } from "@/lib/honey-batches/api";
|
import { fetchHoneyBatches } from "@/lib/honey-batches/api";
|
||||||
@@ -12,6 +11,7 @@ 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 { EmptyState } from "@/components/empty-state";
|
||||||
|
import { ListItemCard } from "@/components/list-item-card";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@@ -19,14 +19,6 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import {
|
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableCell,
|
|
||||||
TableHead,
|
|
||||||
TableHeader,
|
|
||||||
TableRow,
|
|
||||||
} from "@/components/ui/table";
|
|
||||||
import { CreateHoneyBatchDialog } from "./create-honey-batch-dialog";
|
import { CreateHoneyBatchDialog } from "./create-honey-batch-dialog";
|
||||||
|
|
||||||
const STATUS_OPTIONS: HoneyBatchStatus[] = [
|
const STATUS_OPTIONS: HoneyBatchStatus[] = [
|
||||||
@@ -105,37 +97,22 @@ export default function HoneyBatchesPage() {
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{honeyBatches && honeyBatches.length > 0 ? (
|
{honeyBatches && honeyBatches.length > 0 ? (
|
||||||
<Table>
|
<div className="flex flex-col gap-2">
|
||||||
<TableHeader>
|
|
||||||
<TableRow>
|
|
||||||
<TableHead>Chargennummer</TableHead>
|
|
||||||
<TableHead>Sorte</TableHead>
|
|
||||||
<TableHead>Menge</TableHead>
|
|
||||||
<TableHead>Status</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{honeyBatches.map((honeyBatch) => (
|
{honeyBatches.map((honeyBatch) => (
|
||||||
<TableRow key={honeyBatch.id}>
|
<ListItemCard
|
||||||
<TableCell>
|
key={honeyBatch.id}
|
||||||
<Link
|
|
||||||
className="font-mono font-medium text-primary underline-offset-4 hover:underline"
|
|
||||||
href={`/honey-batches/${honeyBatch.id}`}
|
href={`/honey-batches/${honeyBatch.id}`}
|
||||||
>
|
icon={Droplet}
|
||||||
{honeyBatch.batchNumber}
|
title={<span className="font-mono">{honeyBatch.batchNumber}</span>}
|
||||||
</Link>
|
subtitle={`${honeyBatch.honeyType ?? "—"} · ${
|
||||||
</TableCell>
|
honeyBatch.amount !== null ? `${honeyBatch.amount} kg` : "—"
|
||||||
<TableCell>{honeyBatch.honeyType ?? "—"}</TableCell>
|
}`}
|
||||||
<TableCell>
|
trailing={
|
||||||
{honeyBatch.amount !== null ? `${honeyBatch.amount} kg` : "—"}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<Badge variant={statusTone(honeyBatch.status)}>{honeyBatch.status}</Badge>
|
<Badge variant={statusTone(honeyBatch.status)}>{honeyBatch.status}</Badge>
|
||||||
</TableCell>
|
}
|
||||||
</TableRow>
|
/>
|
||||||
))}
|
))}
|
||||||
</TableBody>
|
</div>
|
||||||
</Table>
|
|
||||||
) : null}
|
) : null}
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,14 +8,7 @@ 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 { EmptyState } from "@/components/empty-state";
|
||||||
import {
|
import { ListItemCard } from "@/components/list-item-card";
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableCell,
|
|
||||||
TableHead,
|
|
||||||
TableHeader,
|
|
||||||
TableRow,
|
|
||||||
} from "@/components/ui/table";
|
|
||||||
|
|
||||||
export default function ProductsPage() {
|
export default function ProductsPage() {
|
||||||
const { data: products, isLoading, isError } = useQuery({
|
const { data: products, isLoading, isError } = useQuery({
|
||||||
@@ -52,30 +45,21 @@ export default function ProductsPage() {
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{products && products.length > 0 ? (
|
{products && products.length > 0 ? (
|
||||||
<Table>
|
<div className="flex flex-col gap-2">
|
||||||
<TableHeader>
|
|
||||||
<TableRow>
|
|
||||||
<TableHead>Name</TableHead>
|
|
||||||
<TableHead>SKU</TableHead>
|
|
||||||
<TableHead>Verkaufseinheit</TableHead>
|
|
||||||
<TableHead>Status</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{products.map((product) => (
|
{products.map((product) => (
|
||||||
<TableRow key={product.id}>
|
<ListItemCard
|
||||||
<TableCell className="font-medium">{product.name}</TableCell>
|
key={product.id}
|
||||||
<TableCell className="font-mono">{product.sku ?? "—"}</TableCell>
|
icon={ShoppingBag}
|
||||||
<TableCell>{product.salesUnit ?? "—"}</TableCell>
|
title={product.name}
|
||||||
<TableCell>
|
subtitle={`${product.sku ?? "—"} · ${product.salesUnit ?? "—"}`}
|
||||||
|
trailing={
|
||||||
<Badge variant={product.status === "ACTIVE" ? "default" : "secondary"}>
|
<Badge variant={product.status === "ACTIVE" ? "default" : "secondary"}>
|
||||||
{product.status}
|
{product.status}
|
||||||
</Badge>
|
</Badge>
|
||||||
</TableCell>
|
}
|
||||||
</TableRow>
|
/>
|
||||||
))}
|
))}
|
||||||
</TableBody>
|
</div>
|
||||||
</Table>
|
|
||||||
) : null}
|
) : null}
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ 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 { EmptyState } from "@/components/empty-state";
|
||||||
|
import { ListItemCard } from "@/components/list-item-card";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@@ -17,14 +18,6 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import {
|
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableCell,
|
|
||||||
TableHead,
|
|
||||||
TableHeader,
|
|
||||||
TableRow,
|
|
||||||
} from "@/components/ui/table";
|
|
||||||
|
|
||||||
const STATUS_OPTIONS: QueenStatus[] = [
|
const STATUS_OPTIONS: QueenStatus[] = [
|
||||||
"ACTIVE",
|
"ACTIVE",
|
||||||
@@ -92,32 +85,23 @@ export default function QueensPage() {
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{queens && queens.length > 0 ? (
|
{queens && queens.length > 0 ? (
|
||||||
<Table>
|
<div className="flex flex-col gap-2">
|
||||||
<TableHeader>
|
|
||||||
<TableRow>
|
|
||||||
<TableHead>Code</TableHead>
|
|
||||||
<TableHead>Zucht</TableHead>
|
|
||||||
<TableHead>Status</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{queens.map((queen) => (
|
{queens.map((queen) => (
|
||||||
<TableRow key={queen.id}>
|
<ListItemCard
|
||||||
<TableCell>
|
key={queen.id}
|
||||||
<Link className="underline" href={`/queens/${queen.id}`}>
|
href={`/queens/${queen.id}`}
|
||||||
{queen.code}
|
icon={Crown}
|
||||||
</Link>
|
gradient="var(--gradient-sage)"
|
||||||
</TableCell>
|
title={queen.code}
|
||||||
<TableCell>{queen.breed ?? "—"}</TableCell>
|
subtitle={queen.breed ?? "—"}
|
||||||
<TableCell>
|
trailing={
|
||||||
<Badge variant={queen.status === "ACTIVE" ? "default" : "secondary"}>
|
<Badge variant={queen.status === "ACTIVE" ? "default" : "secondary"}>
|
||||||
{queen.status}
|
{queen.status}
|
||||||
</Badge>
|
</Badge>
|
||||||
</TableCell>
|
}
|
||||||
</TableRow>
|
/>
|
||||||
))}
|
))}
|
||||||
</TableBody>
|
</div>
|
||||||
</Table>
|
|
||||||
) : null}
|
) : null}
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,14 +8,7 @@ import { TaskTemplate, TaskTemplateRecurrenceType } from "@/lib/task-templates/t
|
|||||||
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 {
|
import { ListItemCard } from "@/components/list-item-card";
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableCell,
|
|
||||||
TableHead,
|
|
||||||
TableHeader,
|
|
||||||
TableRow,
|
|
||||||
} from "@/components/ui/table";
|
|
||||||
import { CreateTaskTemplateDialog } from "./create-task-template-dialog";
|
import { CreateTaskTemplateDialog } from "./create-task-template-dialog";
|
||||||
|
|
||||||
const RECURRENCE_LABELS: Record<TaskTemplateRecurrenceType, string> = {
|
const RECURRENCE_LABELS: Record<TaskTemplateRecurrenceType, string> = {
|
||||||
@@ -76,29 +69,19 @@ export default function TaskTemplatesPage() {
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{templates && templates.length > 0 ? (
|
{templates && templates.length > 0 ? (
|
||||||
<div className="overflow-x-auto">
|
<div className="flex flex-col gap-2">
|
||||||
<Table>
|
|
||||||
<TableHeader>
|
|
||||||
<TableRow>
|
|
||||||
<TableHead>Titel</TableHead>
|
|
||||||
<TableHead>Wiederholung</TableHead>
|
|
||||||
<TableHead>Nächste Fälligkeit</TableHead>
|
|
||||||
<TableHead>Status</TableHead>
|
|
||||||
<TableHead className="text-right">Aktion</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{templates.map((template) => (
|
{templates.map((template) => (
|
||||||
<TableRow key={template.id}>
|
<ListItemCard
|
||||||
<TableCell className="font-medium">{template.title}</TableCell>
|
key={template.id}
|
||||||
<TableCell>{describeRecurrence(template)}</TableCell>
|
icon={Repeat}
|
||||||
<TableCell>{formatDate(template.nextDueDate)}</TableCell>
|
gradient="var(--gradient-sky)"
|
||||||
<TableCell>
|
title={template.title}
|
||||||
|
subtitle={`${describeRecurrence(template)} · Nächste Fälligkeit: ${formatDate(template.nextDueDate)}`}
|
||||||
|
trailing={
|
||||||
|
<>
|
||||||
<Badge variant={template.active ? "default" : "outline"}>
|
<Badge variant={template.active ? "default" : "outline"}>
|
||||||
{template.active ? "Aktiv" : "Inaktiv"}
|
{template.active ? "Aktiv" : "Inaktiv"}
|
||||||
</Badge>
|
</Badge>
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-right">
|
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@@ -108,11 +91,10 @@ export default function TaskTemplatesPage() {
|
|||||||
>
|
>
|
||||||
Deaktivieren
|
Deaktivieren
|
||||||
</Button>
|
</Button>
|
||||||
</TableCell>
|
</>
|
||||||
</TableRow>
|
}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -6,15 +6,8 @@ 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 { EmptyState } from "@/components/empty-state";
|
||||||
|
import { ListItemCard } from "@/components/list-item-card";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import {
|
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableCell,
|
|
||||||
TableHead,
|
|
||||||
TableHeader,
|
|
||||||
TableRow,
|
|
||||||
} from "@/components/ui/table";
|
|
||||||
import { CreateStorageContainerDialog } from "./create-storage-container-dialog";
|
import { CreateStorageContainerDialog } from "./create-storage-container-dialog";
|
||||||
import { CreateStockItemDialog } from "./create-stock-item-dialog";
|
import { CreateStockItemDialog } from "./create-stock-item-dialog";
|
||||||
import { RecordMovementDialog } from "./record-movement-dialog";
|
import { RecordMovementDialog } from "./record-movement-dialog";
|
||||||
@@ -50,34 +43,25 @@ export default function StoragePage() {
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{containers && containers.length > 0 ? (
|
{containers && containers.length > 0 ? (
|
||||||
<Table>
|
<div className="flex flex-col gap-2">
|
||||||
<TableHeader>
|
|
||||||
<TableRow>
|
|
||||||
<TableHead>Code</TableHead>
|
|
||||||
<TableHead>Typ</TableHead>
|
|
||||||
<TableHead>Standort</TableHead>
|
|
||||||
<TableHead>Status</TableHead>
|
|
||||||
<TableHead className="text-right">QR-Code</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{containers.map((container) => (
|
{containers.map((container) => (
|
||||||
<TableRow key={container.id}>
|
<ListItemCard
|
||||||
<TableCell className="font-mono font-medium">{container.code}</TableCell>
|
key={container.id}
|
||||||
<TableCell>{container.containerType ?? "—"}</TableCell>
|
icon={Warehouse}
|
||||||
<TableCell>{container.location ?? "—"}</TableCell>
|
gradient="var(--gradient-sky)"
|
||||||
<TableCell>
|
title={<span className="font-mono">{container.code}</span>}
|
||||||
|
subtitle={`${container.containerType ?? "—"} · ${container.location ?? "—"}`}
|
||||||
|
trailing={
|
||||||
|
<>
|
||||||
<Badge variant={container.status === "FILLED" ? "default" : "secondary"}>
|
<Badge variant={container.status === "FILLED" ? "default" : "secondary"}>
|
||||||
{container.status}
|
{container.status}
|
||||||
</Badge>
|
</Badge>
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-right">
|
|
||||||
<QrCodeDialog entityType="storage-containers" id={container.id} label="QR" />
|
<QrCodeDialog entityType="storage-containers" id={container.id} label="QR" />
|
||||||
</TableCell>
|
</>
|
||||||
</TableRow>
|
}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</TableBody>
|
</div>
|
||||||
</Table>
|
|
||||||
) : null}
|
) : null}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -96,35 +80,28 @@ export default function StoragePage() {
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{stockItems && stockItems.length > 0 ? (
|
{stockItems && stockItems.length > 0 ? (
|
||||||
<Table>
|
<div className="flex flex-col gap-2">
|
||||||
<TableHeader>
|
|
||||||
<TableRow>
|
|
||||||
<TableHead>Name</TableHead>
|
|
||||||
<TableHead>Kategorie</TableHead>
|
|
||||||
<TableHead>Bestand</TableHead>
|
|
||||||
<TableHead>Aktion</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{stockItems.map((item) => (
|
{stockItems.map((item) => (
|
||||||
<TableRow key={item.id}>
|
<ListItemCard
|
||||||
<TableCell>{item.name}</TableCell>
|
key={item.id}
|
||||||
<TableCell>{item.category}</TableCell>
|
icon={Package}
|
||||||
<TableCell>
|
gradient="var(--gradient-sky)"
|
||||||
|
title={item.name}
|
||||||
|
subtitle={item.category}
|
||||||
|
trailing={
|
||||||
|
<>
|
||||||
|
<span className="text-sm tabular-nums">
|
||||||
{item.quantity} {item.unit}
|
{item.quantity} {item.unit}
|
||||||
|
</span>
|
||||||
{item.minThreshold !== null && item.quantity < item.minThreshold ? (
|
{item.minThreshold !== null && item.quantity < item.minThreshold ? (
|
||||||
<Badge variant="destructive" className="ml-2">
|
<Badge variant="destructive">Niedrig</Badge>
|
||||||
Niedrig
|
|
||||||
</Badge>
|
|
||||||
) : null}
|
) : null}
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<RecordMovementDialog stockItemId={item.id} />
|
<RecordMovementDialog stockItemId={item.id} />
|
||||||
</TableCell>
|
</>
|
||||||
</TableRow>
|
}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</TableBody>
|
</div>
|
||||||
</Table>
|
|
||||||
) : null}
|
) : null}
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ 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 { EmptyState } from "@/components/empty-state";
|
||||||
|
import { ListItemCard } from "@/components/list-item-card";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@@ -17,14 +18,6 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import {
|
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableCell,
|
|
||||||
TableHead,
|
|
||||||
TableHeader,
|
|
||||||
TableRow,
|
|
||||||
} from "@/components/ui/table";
|
|
||||||
import { TaskEditDialog } from "./task-edit-dialog";
|
import { TaskEditDialog } from "./task-edit-dialog";
|
||||||
|
|
||||||
const STATUS_OPTIONS: TaskStatus[] = ["OPEN", "IN_PROGRESS", "DONE", "CANCELLED"];
|
const STATUS_OPTIONS: TaskStatus[] = ["OPEN", "IN_PROGRESS", "DONE", "CANCELLED"];
|
||||||
@@ -137,31 +130,21 @@ export default function TasksPage() {
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{tasks && visibleTasks.length > 0 ? (
|
{tasks && visibleTasks.length > 0 ? (
|
||||||
<Table>
|
<div className="flex flex-col gap-2">
|
||||||
<TableHeader>
|
|
||||||
<TableRow>
|
|
||||||
<TableHead>Titel</TableHead>
|
|
||||||
<TableHead>Priorität</TableHead>
|
|
||||||
<TableHead>Fällig</TableHead>
|
|
||||||
<TableHead>Status</TableHead>
|
|
||||||
<TableHead>Aktion</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{visibleTasks.map((task) => (
|
{visibleTasks.map((task) => (
|
||||||
<TableRow key={task.id}>
|
<ListItemCard
|
||||||
<TableCell>{task.title}</TableCell>
|
key={task.id}
|
||||||
<TableCell>{task.priority}</TableCell>
|
icon={ListTodo}
|
||||||
<TableCell>
|
gradient="var(--gradient-sky)"
|
||||||
{task.dueDate ? new Date(task.dueDate).toLocaleDateString("de-DE") : "—"}
|
title={task.title}
|
||||||
</TableCell>
|
subtitle={`${task.priority} · Fällig: ${
|
||||||
<TableCell>
|
task.dueDate ? new Date(task.dueDate).toLocaleDateString("de-DE") : "—"
|
||||||
|
}`}
|
||||||
|
trailing={
|
||||||
|
<>
|
||||||
<Badge variant={task.status === "DONE" ? "secondary" : "default"}>
|
<Badge variant={task.status === "DONE" ? "secondary" : "default"}>
|
||||||
{task.status}
|
{task.status}
|
||||||
</Badge>
|
</Badge>
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<TaskEditDialog task={task} />
|
<TaskEditDialog task={task} />
|
||||||
{task.status !== "DONE" && task.status !== "CANCELLED" ? (
|
{task.status !== "DONE" && task.status !== "CANCELLED" ? (
|
||||||
<Button
|
<Button
|
||||||
@@ -173,12 +156,11 @@ export default function TasksPage() {
|
|||||||
Erledigen
|
Erledigen
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</>
|
||||||
</TableCell>
|
}
|
||||||
</TableRow>
|
/>
|
||||||
))}
|
))}
|
||||||
</TableBody>
|
</div>
|
||||||
</Table>
|
|
||||||
) : null}
|
) : null}
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import { ReactNode } from "react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { ChevronRight, type LucideIcon } from "lucide-react";
|
||||||
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
|
|
||||||
|
interface ListItemCardProps {
|
||||||
|
href?: string;
|
||||||
|
icon: LucideIcon;
|
||||||
|
gradient?: string;
|
||||||
|
title: ReactNode;
|
||||||
|
subtitle?: ReactNode;
|
||||||
|
trailing?: ReactNode;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ListItemCard({
|
||||||
|
href,
|
||||||
|
icon: Icon,
|
||||||
|
gradient = "var(--gradient-honey)",
|
||||||
|
title,
|
||||||
|
subtitle,
|
||||||
|
trailing,
|
||||||
|
className,
|
||||||
|
}: ListItemCardProps) {
|
||||||
|
const body = (
|
||||||
|
<Card interactive={Boolean(href)} className={className}>
|
||||||
|
<CardContent className="flex items-center gap-3">
|
||||||
|
<div
|
||||||
|
className="flex size-10 shrink-0 items-center justify-center rounded-xl text-white shadow-[var(--shadow-soft)]"
|
||||||
|
style={{ background: gradient }}
|
||||||
|
>
|
||||||
|
<Icon className="size-5" />
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<p className="truncate font-medium">{title}</p>
|
||||||
|
{subtitle ? (
|
||||||
|
<p className="truncate text-sm text-muted-foreground">{subtitle}</p>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
{trailing ? <div className="flex shrink-0 items-center gap-2">{trailing}</div> : null}
|
||||||
|
{href ? (
|
||||||
|
<ChevronRight className="size-4 shrink-0 text-muted-foreground/60" />
|
||||||
|
) : null}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (href) {
|
||||||
|
return (
|
||||||
|
<Link href={href} className="block">
|
||||||
|
{body}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return body;
|
||||||
|
}
|
||||||
Reference in new issue
Block a user