Redesign invoice PDFs, add correction-invoice downloads, return reasons, and per-product tax/bundle support

Invoice + Stornorechnung/Gutschrift PDFs get a modern header-band layout,
a "bereits beglichen" badge for immediately-paid orders, labelled bank
details, and a per-tax-rate summary breakdown. Correction invoices can
now be re-downloaded from the account (regenerated deterministically,
not stored as files, same approach as the original invoice). Return
requests capture a reason. Products can define bundles (bundleItems) and
a per-product VAT rate override, both snapshotted onto order items.
This commit is contained in:
Marco
2026-07-22 10:31:22 +00:00
parent 04cc69f98b
commit 179b59d73d
15 changed files with 723 additions and 222 deletions
@@ -6,7 +6,6 @@ import { useRouter } from "next/navigation";
const LABEL = { cancel: "Bestellung stornieren", "request-return": "Rücksendung anfragen" } as const;
const CONFIRM = {
cancel: "Bestellung wirklich stornieren?",
"request-return": "Rücksendung wirklich anfragen? Wir melden uns mit den nächsten Schritten.",
} as const;
export function OrderActionButton({ orderNumber, action }: { orderNumber: string; action: "cancel" | "request-return" }) {
@@ -15,14 +14,29 @@ export function OrderActionButton({ orderNumber, action }: { orderNumber: string
const [error, setError] = useState<string | null>(null);
async function handleClick() {
if (!window.confirm(CONFIRM[action])) return;
let returnReason: string | undefined;
if (action === "cancel") {
if (!window.confirm(CONFIRM.cancel)) return;
} else {
// A short reason helps quality/assortment decisions later (see the
// Payload README's Orders.ts section) — prompt() rather than a
// custom form, same "plain browser dialog" pattern already used for
// cancel's confirm() above.
const input = window.prompt("Kurz gesagt, warum möchtest du die Bestellung zurücksenden?");
if (input === null) return;
returnReason = input.trim();
if (!returnReason) {
setError("Bitte kurz einen Grund angeben.");
return;
}
}
setLoading(true);
setError(null);
try {
const res = await fetch(`/api/account/orders/${encodeURIComponent(orderNumber)}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ action }),
body: JSON.stringify({ action, returnReason }),
});
const data = await res.json();
if (!data.ok) {
+31 -11
View File
@@ -67,10 +67,13 @@ export default async function KontoBestellungDetailPage({ params }: { params: Pr
<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>
<div key={i} className="flex items-start gap-4 w-full">
<div className="flex-1 flex flex-col gap-0.5">
<p className="text-body-sm text-text-primary">
{item.quantity} × {item.productName}
</p>
{item.bundleContents && <p className="text-label text-text-muted">{item.bundleContents}</p>}
</div>
<p className="text-body-sm text-text-primary">{formatPrice(item.quantity * item.unitPrice)}</p>
</div>
))}
@@ -108,15 +111,32 @@ export default async function KontoBestellungDetailPage({ params }: { params: Pr
</div>
</div>
{order.invoiceNumber && (
<a
href={`/api/account/orders/${encodeURIComponent(order.orderNumber)}/invoice`}
className="text-body-sm text-brand hover:underline"
>
Rechnung herunterladen ({order.invoiceNumber})
</a>
{order.returnReason && (
<div className="flex flex-col gap-1 w-full">
<p className="text-label text-text-muted">Grund der Rücksendung</p>
<p className="text-body-sm text-text-primary">{order.returnReason}</p>
</div>
)}
<div className="flex flex-col gap-2 items-start">
{order.invoiceNumber && (
<a
href={`/api/account/orders/${encodeURIComponent(order.orderNumber)}/invoice`}
className="text-body-sm text-brand hover:underline"
>
Rechnung herunterladen ({order.invoiceNumber})
</a>
)}
{order.correctionInvoiceNumber && (
<a
href={`/api/account/orders/${encodeURIComponent(order.orderNumber)}/correction-invoice`}
className="text-body-sm text-brand hover:underline"
>
{order.status === "returned" ? "Gutschrift" : "Stornorechnung"} herunterladen ({order.correctionInvoiceNumber})
</a>
)}
</div>
{action && <OrderActionButton orderNumber={order.orderNumber} action={action} />}
</Reveal>
</main>