feat: voucher crud and list

This commit is contained in:
opencode committed 2026-10-18 10:30:55 +02:00
1 parent 73295653ee
commit 660d6cff51
7 files changed
+358

No files matched your search

+95
View File
@@ -0,0 +1,95 @@
import Link from "next/link";
import { Suspense } from "react";
import { getDb } from "@/lib/db";
import { getVoucherStatus } from "@/lib/voucher-status";
import { formatEuro } from "@/lib/vouchers";
const FILTERS = [
{ key: "", label: "Alle" },
{ key: "aktiv", label: "Aktiv" },
{ key: "bald", label: "Bald ablaufend" },
{ key: "abgelaufen", label: "Abgelaufen" },
{ key: "eingeloest", label: "Eingelöst" },
] as const;
function Skeleton() {
return (
<ul aria-label="lädt">
{[0, 1, 2].map((i) => (
<li key={i} className="animate-pulse rounded border p-4">
<div className="h-4 w-1/3 bg-gray-200" />
<div className="mt-2 h-3 w-1/4 bg-gray-100" />
</li>
))}
</ul>
);
}
async function VoucherList({ filter }: { filter: string }) {
const db = getDb();
const all = await db.voucher.findMany({ orderBy: { createdAt: "desc" } });
const now = Date.now();
const in14d = now + 14 * 24 * 3600 * 1000;
const filtered = all.filter((v) => {
const s = getVoucherStatus(v);
if (filter === "eingeloest") return s === "eingeloest";
if (filter === "abgelaufen") return s === "abgelaufen";
if (filter === "bald")
return s === "aktiv" && v.ablaufdatum && +new Date(v.ablaufdatum) >= now && +new Date(v.ablaufdatum) <= in14d;
if (filter === "aktiv") return s === "aktiv";
return true;
});
if (filtered.length === 0) {
return (
<div className="rounded border p-8 text-center">
<p>Keine Gutscheine gefunden.</p>
<Link href="/vouchers/new" className="mt-4 inline-block rounded bg-black px-4 py-2 text-white">
Gutschein anlegen
</Link>
</div>
);
}
return (
<ul className="space-y-2">
{filtered.map((v) => (
<li key={v.id} className="rounded border p-4">
<Link href={`/vouchers/${v.id}`} className="font-medium underline">
{v.name}
</Link>
<p className="text-sm text-gray-600">
{v.anbieter} · {formatEuro(v.restCents)} / {formatEuro(v.wertCents)} · {getVoucherStatus(v)}
</p>
</li>
))}
</ul>
);
}
export default async function Home({ searchParams }: { searchParams: Promise<{ filter?: string }> }) {
const { filter = "" } = await searchParams;
return (
<main className="mx-auto max-w-2xl space-y-4 p-6">
<div className="flex items-center justify-between">
<h1 className="text-xl font-bold">Gutscheine</h1>
<Link href="/vouchers/new" className="rounded bg-black px-4 py-2 text-white">
Neu
</Link>
</div>
<nav className="flex gap-2" aria-label="Filter">
{FILTERS.map((f) => (
<Link
key={f.key}
href={f.key ? `/?filter=${f.key}` : "/"}
aria-current={filter === f.key ? "page" : undefined}
className={`rounded border px-3 py-1 text-sm ${filter === f.key ? "bg-black text-white" : ""}`}
>
{f.label}
</Link>
))}
</nav>
<Suspense fallback={<Skeleton />}>
<VoucherList filter={filter} />
</Suspense>
</main>
);
}