Add real order persistence, customer accounts, and cart sync

Checkout now persists orders server-side (Payload orders collection,
re-priced from live product data, discount codes redeemed exactly once)
instead of writing a client-only sessionStorage snapshot. Buying requires
an account (registration inline in checkout, no separate step) — accounts
get order history with delivery status, profile/address editing, password
change, and a cart that syncs across devices while logged in.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Marco
2026-07-22 06:45:42 +00:00
parent 516945fc8c
commit 7f37f111e8
27 changed files with 1697 additions and 89 deletions
@@ -0,0 +1,112 @@
import type { Metadata } from "next";
import { redirect, notFound } from "next/navigation";
import Link from "next/link";
import { Reveal } from "../../../components/Reveal";
import { Footer } from "../../../components/Footer";
import { formatPrice, formatDate } from "../../../lib/format";
import { getSessionCustomer, getCustomerOrderDetail, ORDER_STATUS_LABEL } from "../../../lib/customerAuth";
export const metadata: Metadata = {
title: "Bestelldetails",
robots: { index: false, follow: true },
};
export default async function KontoBestellungDetailPage({ params }: { params: Promise<{ orderNumber: string }> }) {
const { orderNumber } = await params;
const session = await getSessionCustomer();
if (!session) redirect("/konto/login");
const order = await getCustomerOrderDetail(session.token, session.customer.id, decodeURIComponent(orderNumber));
if (!order) notFound();
const address =
order.deliveryMethod === "address"
? order.street
: `Packstation ${order.packstationNumber} · Postnummer ${order.postNumber}`;
return (
<>
<main className="flex flex-col flex-1 bg-bg-base">
<Reveal className="flex flex-col gap-6 items-start pt-10 pb-16 px-[var(--layout-padding-x)] w-full max-w-[48rem] mx-auto">
<Link href="/konto/bestellungen" className="text-body-sm text-text-muted hover:text-brand transition-colors">
Zurück zur Bestellhistorie
</Link>
<p className="font-semibold text-h-feature text-text-primary" style={{ fontFamily: "var(--font-lora)" }}>
{order.orderNumber}
</p>
<div className="flex flex-wrap gap-8 w-full">
<div className="flex flex-col gap-1">
<p className="text-label text-text-muted">Datum</p>
<p className="text-body-sm text-text-primary">{formatDate(order.createdAt)}</p>
</div>
<div className="flex flex-col gap-1">
<p className="text-label text-text-muted">Status</p>
<p className="text-body-sm text-text-primary">{ORDER_STATUS_LABEL[order.status] ?? order.status}</p>
</div>
<div className="flex flex-col gap-1">
<p className="text-label text-text-muted">Zahlungsart</p>
<p className="text-body-sm text-text-primary">{order.paymentMethodTitle}</p>
</div>
</div>
<div className="flex flex-col gap-1 w-full">
<p className="text-label text-text-muted">Lieferadresse</p>
<p className="text-body-sm text-text-primary">
{order.customerFirstName} {order.customerLastName}
</p>
<p className="text-body-sm text-text-primary">{address}</p>
<p className="text-body-sm text-text-primary">
{order.zip} {order.city}, {order.country}
</p>
</div>
<div className="w-full bg-bg-base border border-border rounded-md p-6 flex flex-col gap-3">
{order.items.map((item, i) => (
<div key={i} className="flex items-center gap-4 w-full">
<p className="flex-1 text-body-sm text-text-primary">
{item.quantity} × {item.productName}
</p>
<p className="text-body-sm text-text-primary">{formatPrice(item.quantity * item.unitPrice)}</p>
</div>
))}
<div className="h-px bg-border w-full" />
<div className="flex items-center w-full">
<span className="text-body-sm text-text-primary">Zwischensumme</span>
<span className="flex-1" />
<span className="text-body-sm text-text-primary">{formatPrice(order.subtotal)}</span>
</div>
{order.discountCode && (
<div className="flex items-center w-full">
<span className="text-body-sm text-success">Rabattcode ({order.discountCode})</span>
<span className="flex-1" />
<span className="font-bold text-body-sm text-success">-{formatPrice(order.discountAmount)}</span>
</div>
)}
<div className="flex items-center w-full">
<span className="text-body-sm text-text-primary">Versand ({order.shippingMethodTitle})</span>
<span className="flex-1" />
<span className="text-body-sm text-text-primary">
{order.shippingCost === 0 ? "Kostenlos" : formatPrice(order.shippingCost)}
</span>
</div>
<div className="h-px bg-border w-full" />
<div className="flex items-center w-full">
<span className="font-semibold text-h4 text-text-primary" style={{ fontFamily: "var(--font-lora)" }}>
Gesamtsumme
</span>
<span className="flex-1" />
<span className="font-bold text-h-small text-text-primary">{formatPrice(order.total)}</span>
</div>
</div>
</Reveal>
</main>
<Footer />
</>
);
}
+84
View File
@@ -0,0 +1,84 @@
import type { Metadata } from "next";
import { redirect } from "next/navigation";
import Link from "next/link";
import { Reveal } from "../../components/Reveal";
import { Footer } from "../../components/Footer";
import { formatPrice, formatDate } from "../../lib/format";
import { getSessionCustomer, getCustomerOrders, ORDER_STATUS_LABEL } from "../../lib/customerAuth";
// robots: noindex — account area, same reasoning as /checkout.
export const metadata: Metadata = {
title: "Meine Bestellungen",
description: "Deine Bestellhistorie bei einfach produktiv.",
robots: {
index: false,
follow: true,
},
};
export default async function KontoBestellungenPage() {
const session = await getSessionCustomer();
if (!session) redirect("/konto/login");
const orders = await getCustomerOrders(session.token, session.customer.id);
return (
<>
<main className="flex flex-col flex-1 bg-bg-base">
<Reveal className="flex flex-col gap-6 items-start pt-10 pb-16 px-[var(--layout-padding-x)] w-full max-w-[56rem] mx-auto">
<p className="font-semibold text-h-feature text-text-primary" style={{ fontFamily: "var(--font-lora)" }}>
Meine Bestellungen
</p>
<p className="text-body text-text-muted">
Eingeloggt als {session.customer.email} (Kundennummer {session.customer.customerNumber})
</p>
{orders.length === 0 ? (
<p className="text-body text-text-muted">Du hast noch keine Bestellung aufgegeben.</p>
) : (
<div className="flex flex-col gap-4 w-full">
{orders.map((order) => (
<Link
key={order.orderNumber}
href={`/konto/bestellungen/${encodeURIComponent(order.orderNumber)}`}
className="flex flex-wrap items-center gap-4 w-full bg-bg-base border border-border rounded-md p-6 hover:border-brand transition-colors"
>
<div className="flex flex-col gap-1">
<p className="text-label text-text-muted">Bestellnummer</p>
<p className="font-bold text-body-sm text-text-primary">{order.orderNumber}</p>
</div>
<div className="flex flex-col gap-1">
<p className="text-label text-text-muted">Datum</p>
<p className="text-body-sm text-text-primary">{formatDate(order.createdAt)}</p>
</div>
<div className="flex flex-col gap-1">
<p className="text-label text-text-muted">Artikel</p>
<p className="text-body-sm text-text-primary">{order.itemCount}</p>
</div>
<div className="flex flex-col gap-1">
<p className="text-label text-text-muted">Status</p>
<p className="text-body-sm text-text-primary">{ORDER_STATUS_LABEL[order.status] ?? order.status}</p>
</div>
<div className="flex flex-col gap-1 ml-auto">
<p className="text-label text-text-muted">Gesamtbetrag</p>
<p className="font-bold text-body-sm text-text-primary">{formatPrice(order.total)}</p>
</div>
</Link>
))}
</div>
)}
<div className="flex gap-6">
<Link href="/konto/profil" className="underline text-body-sm text-text-primary hover:text-brand transition-colors">
Profil &amp; Adresse
</Link>
<Link href="/shop" className="underline text-body-sm text-text-primary hover:text-brand transition-colors">
Weiter einkaufen
</Link>
</div>
</Reveal>
</main>
<Footer />
</>
);
}