diff --git a/app/api/account/orders/[orderNumber]/invoice/route.ts b/app/api/account/orders/[orderNumber]/invoice/route.ts new file mode 100644 index 0000000..b170539 --- /dev/null +++ b/app/api/account/orders/[orderNumber]/invoice/route.ts @@ -0,0 +1,50 @@ +import { NextResponse } from "next/server"; +import { getSessionCustomer, getCustomerOrderDetail } from "../../../../../lib/customerAuth"; +import { generateInvoicePdf } from "../../../../../lib/invoiceData"; + +// On-demand download for "Rechnung herunterladen" on +// /konto/bestellungen/[orderNumber] — reuses the exact same render call as +// the checkout-time attachment (app/lib/orderEmail.ts), so a re-download +// always matches what was emailed; invoiceNumber itself never changes +// (assigned once, server-side, at order creation — see Orders.ts). +export async function GET(request: Request, { params }: { params: Promise<{ orderNumber: string }> }) { + const session = await getSessionCustomer(); + if (!session) return NextResponse.json({ ok: false, reason: "Bitte zuerst einloggen." }, { status: 401 }); + + const { orderNumber } = await params; + const order = await getCustomerOrderDetail(session.token, session.customer.id, decodeURIComponent(orderNumber)); + if (!order) return NextResponse.json({ ok: false, reason: "Bestellung nicht gefunden." }, { status: 404 }); + if (!order.invoiceNumber || !order.invoiceIssuedAt) { + return NextResponse.json({ ok: false, reason: "Für diese Bestellung liegt noch keine Rechnung vor." }, { status: 404 }); + } + + const pdf = await generateInvoicePdf({ + orderNumber: order.orderNumber, + invoiceNumber: order.invoiceNumber, + invoiceIssuedAt: order.invoiceIssuedAt, + customerFirstName: order.customerFirstName, + customerLastName: order.customerLastName, + deliveryMethod: order.deliveryMethod, + street: order.street, + packstationNumber: order.packstationNumber, + postNumber: order.postNumber, + zip: order.zip, + city: order.city, + country: order.country, + items: order.items, + subtotal: order.subtotal, + shippingCost: order.shippingCost, + discountAmount: order.discountAmount, + discountCode: order.discountCode, + total: order.total, + }); + if (!pdf) return NextResponse.json({ ok: false, reason: "Rechnung konnte nicht erzeugt werden." }, { status: 500 }); + + return new NextResponse(new Uint8Array(pdf), { + status: 200, + headers: { + "Content-Type": "application/pdf", + "Content-Disposition": `attachment; filename="Rechnung-${order.invoiceNumber}.pdf"`, + }, + }); +} diff --git a/app/api/checkout/route.ts b/app/api/checkout/route.ts index 15dbe5a..9b16a0f 100644 --- a/app/api/checkout/route.ts +++ b/app/api/checkout/route.ts @@ -88,11 +88,12 @@ export async function POST(request: Request) { // Re-price everything server-side — never trust client-submitted prices. const productsBySlug = await fetchProductsBySlug(); - const items: { productId: number; productName: string; quantity: number; unitPrice: number }[] = []; + const items: { productId: number; productName: string; quantity: number; unitPrice: number; imageUrl: string | null }[] = []; for (const line of body.cart) { const product = productsBySlug.get(line.id); if (!product) return NextResponse.json({ ok: false, reason: "Ein Artikel im Warenkorb ist nicht mehr verfügbar." }, { status: 400 }); - items.push({ productId: product.id, productName: product.name, quantity: line.qty, unitPrice: product.price }); + const imageUrl = typeof product.image === "object" && product.image ? product.image.url : null; + items.push({ productId: product.id, productName: product.name, quantity: line.qty, unitPrice: product.price, imageUrl }); } const subtotal = items.reduce((sum, i) => sum + i.quantity * i.unitPrice, 0); @@ -164,7 +165,18 @@ export async function POST(request: Request) { { orderNumber: order.orderNumber, createdAt: order.createdAt, - items: items.map((i) => ({ productName: i.productName, quantity: i.quantity, unitPrice: i.unitPrice })), + invoiceNumber: order.invoiceNumber, + invoiceIssuedAt: order.invoiceIssuedAt, + customerFirstName: body.firstName, + customerLastName: body.lastName, + deliveryMethod: body.deliveryMethod, + street: body.street, + packstationNumber: body.packstationNumber, + postNumber: body.postNumber, + zip: body.zip, + city: body.city, + country: body.country, + items: items.map((i) => ({ productName: i.productName, quantity: i.quantity, unitPrice: i.unitPrice, imageUrl: i.imageUrl })), subtotal, shippingCost, discountAmount, diff --git a/app/email-preview/[type]/components/LiveEmailPreviewClient.tsx b/app/email-preview/[type]/components/LiveEmailPreviewClient.tsx index b05834a..8e97982 100644 --- a/app/email-preview/[type]/components/LiveEmailPreviewClient.tsx +++ b/app/email-preview/[type]/components/LiveEmailPreviewClient.tsx @@ -4,6 +4,8 @@ import { useLivePreview } from "@payloadcms/live-preview-react"; import { renderOrderConfirmationHtml, renderPasswordResetHtml, + renderOrderStatusHtml, + ORDER_STATUS_EMAIL_ICON, SAMPLE_ORDER, type EmailTemplateContent, } from "../../../lib/emailTemplates"; @@ -35,7 +37,14 @@ export function LiveEmailPreviewClient({ const html = type === "order-confirmation" ? renderOrderConfirmationHtml(data, SAMPLE_ORDER) - : renderPasswordResetHtml(data, "https://einfach-produktiv.mk360.de/konto/passwort-zuruecksetzen?token=beispiel-token"); + : type === "password-reset" + ? renderPasswordResetHtml(data, "https://einfach-produktiv.mk360.de/konto/passwort-zuruecksetzen?token=beispiel-token") + : renderOrderStatusHtml( + data, + ORDER_STATUS_EMAIL_ICON[type] ?? "✓", + SAMPLE_ORDER.orderNumber, + `https://einfach-produktiv.mk360.de/konto/bestellungen/${SAMPLE_ORDER.orderNumber}`, + ); return (