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:
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user