feat: Wabenhain Honig-Shop Anwendung

Vollstaendige Next.js/Prisma-E-Commerce-Anwendung fuer eine Imkerei:
Shop, Warenkorb, Stripe-Checkout, Kundenkonto mit 2FA, Admin-Bereich
(Produkte, Ernten, Chargen, Bestellungen, Gutscheine, admin-editierbare
Website-Inhalte), i18n (DE/EN), Rate-Limiting/CSP/Audit-Log-Haertung,
PostgreSQL- und MySQL-Unterstuetzung ueber austauschbare Prisma-
Treiber-Adapter, Unit-/Komponenten-/E2E-Tests.
This commit is contained in:
Nick Szaunig committed 2026-08-17 14:30:28 +02:00
1 parent 5c76fcc266
commit c7927f2883
240 files changed
+26096 -137

No files matched your search

+147
View File
@@ -0,0 +1,147 @@
import Link from "next/link";
import { prisma } from "@/lib/prisma";
import { BackLink } from "@/components/back-link";
import { formatPrice, formatDate } from "@/lib/format";
import { OrderStatusBadge } from "@/components/account-ui";
import { updateOrderStatusAction } from "../actions";
import type { OrderStatus } from "@/generated/prisma/client";
import { insensitiveContains } from "@/lib/prisma-search";
import { getLocale } from "@/i18n/locale";
import { getDictionary } from "@/i18n/get-dictionary";
export default async function AdminOrdersPage(props: PageProps<"/admin/bestellungen">) {
const t = getDictionary(await getLocale());
const statusLabels = t.orderStatusBadge;
const { q, status } = await props.searchParams;
const query = typeof q === "string" ? q.trim() : "";
const statusFilter = typeof status === "string" && status in statusLabels ? (status as OrderStatus) : undefined;
const orders = await prisma.order.findMany({
where: {
...(statusFilter ? { status: statusFilter } : {}),
...(query
? {
OR: [
{ orderNumber: insensitiveContains(query) },
{ email: insensitiveContains(query) },
{ lastName: insensitiveContains(query) },
],
}
: {}),
},
orderBy: { createdAt: "desc" },
take: 100,
include: { items: true },
});
return (
<div>
<BackLink href="/admin" label={t.adminOrdersPage.backToDashboard} />
<h1 className="text-2xl text-forest mb-8">{t.adminOrdersPage.title}</h1>
<form method="GET" className="flex flex-wrap items-end gap-3 mb-6">
<label className="text-sm">
<span className="block text-ink/65 text-xs mb-1">{t.adminOrdersPage.searchLabel}</span>
<input
type="text"
name="q"
defaultValue={query}
placeholder={t.adminOrdersPage.searchPlaceholder}
className="border border-ink/20 bg-background px-3 py-2 text-sm w-64"
/>
</label>
<label className="text-sm">
<span className="block text-ink/65 text-xs mb-1">{t.adminOrdersPage.statusLabel}</span>
<select
name="status"
defaultValue={statusFilter ?? ""}
className="border border-ink/20 bg-background px-3 py-2 text-sm"
>
<option value="">{t.adminOrdersPage.statusAll}</option>
{Object.entries(statusLabels).map(([value, label]) => (
<option key={value} value={value}>
{label}
</option>
))}
</select>
</label>
<button type="submit" className="bg-forest text-cream px-4 py-2 text-sm">
{t.adminOrdersPage.filterButton}
</button>
{query || statusFilter ? (
<Link href="/admin/bestellungen" className="text-sm text-ink/65 underline">
{t.adminOrdersPage.resetFilter}
</Link>
) : null}
</form>
<div className="overflow-x-auto bg-background border border-ink/10">
<table className="w-full text-sm">
<thead className="text-left text-ink/65 border-b border-ink/10">
<tr>
<th className="p-4">{t.adminOrdersPage.colNumber}</th>
<th className="p-4">{t.adminOrdersPage.colCustomer}</th>
<th className="p-4">{t.adminOrdersPage.colDate}</th>
<th className="p-4">{t.adminOrdersPage.colItems}</th>
<th className="p-4">{t.adminOrdersPage.colTotal}</th>
<th className="p-4">{t.adminOrdersPage.colStatus}</th>
<th className="p-4">{t.adminOrdersPage.colFulfillment}</th>
</tr>
</thead>
<tbody className="divide-y divide-ink/10">
{orders.map((order) => (
<tr key={order.id} className="transition-colors hover:bg-ink/[0.03]">
<td className="p-4 text-forest">
<Link href={`/admin/bestellungen/${order.id}`} className="underline">
{order.orderNumber}
</Link>
{order.giftMessage ? (
<span className="ml-1.5" title={t.adminOrdersPage.giftOrderTitle}>
🎁
</span>
) : null}
</td>
<td className="p-4">
{order.firstName} {order.lastName}
<br />
<span className="text-ink/65 text-xs">{order.email}</span>
</td>
<td className="p-4">{formatDate(order.createdAt)}</td>
<td className="p-4">{order.items.length}</td>
<td className="p-4">{formatPrice(order.totalCents, order.currency)}</td>
<td className="p-4">
<OrderStatusBadge status={order.status} label={statusLabels[order.status] ?? order.status} />
<form action={updateOrderStatusAction} className="mt-2 flex items-center gap-2">
<input type="hidden" name="orderId" value={order.id} />
<select
name="status"
defaultValue={order.status}
className="border border-ink/20 bg-background px-2 py-1 text-xs"
>
{Object.entries(statusLabels).map(([value, label]) => (
<option key={value} value={value}>
{label}
</option>
))}
</select>
<button type="submit" className="text-xs text-forest underline">
{t.adminOrdersPage.save}
</button>
</form>
</td>
<td className="p-4">
{order.fulfillmentMethod === "VERSAND" ? t.adminOrdersPage.shipping : t.adminOrdersPage.pickup}
</td>
</tr>
))}
</tbody>
</table>
{orders.length === 0 ? (
<p className="p-6 text-ink/65">
{query || statusFilter ? t.adminOrdersPage.noResultsFiltered : t.adminOrdersPage.noOrdersYet}
</p>
) : null}
</div>
</div>
);
}