Fix broken email-preview rendering, add payment status to order list, second sample state

- LiveEmailPreviewClient.tsx rendered the email HTML (a full <body>...
  fragment) via dangerouslySetInnerHTML into a plain div, nesting it
  inside the page's own already-existing <body> — invalid HTML the
  browser silently mangled. Switched to an <iframe srcDoc>, giving the
  email its own real document context, exactly like an actual email
  client would render it.
- Added SAMPLE_ORDER_MANUAL + a toggle in the preview so both
  order-confirmation states (paid vs. Vorkasse/Überweisung, incl. the new
  "switch to Kreditkarte/PayPal" mention) are actually visible in the
  admin's Live Preview — SAMPLE_ORDER alone always had isManualPayment:
  false, so the Vorkasse branch was never previewable at all before this.
- /konto/bestellungen (the order list, not just the single-order detail
  page) now also shows the payment status badge next to the existing
  fulfillment status one — CustomerOrder was missing paymentStatus
  entirely.
This commit is contained in:
Marco
2026-07-30 09:56:06 +00:00
parent 0e995884a7
commit 6448c8736a
4 changed files with 83 additions and 3 deletions
@@ -1,5 +1,6 @@
"use client";
import { useState } from "react";
import { useLivePreview } from "@payloadcms/live-preview-react";
import {
renderOrderConfirmationHtml,
@@ -7,6 +8,7 @@ import {
renderOrderStatusHtml,
ORDER_STATUS_EMAIL_ICON,
SAMPLE_ORDER,
SAMPLE_ORDER_MANUAL,
type EmailTemplateContent,
} from "../../../lib/emailTemplates";
import type { EmailTemplateType } from "../../../lib/payload";
@@ -34,13 +36,19 @@ export function LiveEmailPreviewClient({
depth: 0,
});
// Only order-confirmation has two meaningfully different rendered
// states (isManualPayment true/false change which blocks show at all,
// not just text) — every other type has one sample and no toggle.
const [sampleVariant, setSampleVariant] = useState<"paid" | "manual">("paid");
const orderSample = sampleVariant === "paid" ? SAMPLE_ORDER : SAMPLE_ORDER_MANUAL;
// No real company-settings fetch in this preview context — passing null
// falls back to DEFAULT_LEGAL_FOOTER_LINES (placeholder Anbieterkennzeichnung)
// inside buildLegalFooterLines(), same shape as the real send just with
// placeholder business data.
const html =
type === "order-confirmation"
? renderOrderConfirmationHtml(data, SAMPLE_ORDER, null)
? renderOrderConfirmationHtml(data, orderSample, null)
: type === "password-reset"
? renderPasswordResetHtml(data, "https://einfach-produktiv.mk360.de/konto/passwort-zuruecksetzen?token=beispiel-token", null)
: renderOrderStatusHtml(
@@ -53,7 +61,54 @@ export function LiveEmailPreviewClient({
return (
<div style={{ background: "#f4f2ee", minHeight: "100vh", padding: "32px 0" }}>
<div dangerouslySetInnerHTML={{ __html: html }} />
{type === "order-confirmation" && (
<div style={{ display: "flex", justifyContent: "center", gap: 8, marginBottom: 16 }}>
<button
type="button"
onClick={() => setSampleVariant("paid")}
style={{
padding: "8px 16px",
borderRadius: 999,
border: "1px solid #d1cec4",
background: sampleVariant === "paid" ? "#f6a701" : "#fff",
fontWeight: 700,
fontSize: 13,
cursor: "pointer",
}}
>
Online bezahlt
</button>
<button
type="button"
onClick={() => setSampleVariant("manual")}
style={{
padding: "8px 16px",
borderRadius: 999,
border: "1px solid #d1cec4",
background: sampleVariant === "manual" ? "#f6a701" : "#fff",
fontWeight: 700,
fontSize: 13,
cursor: "pointer",
}}
>
Vorkasse (Überweisung)
</button>
</div>
)}
{/* An iframe, not dangerouslySetInnerHTML into a plain div — `html`
here is a full `<body>...</body>` fragment (see emailTemplates.ts's
emailShell()), meant to become an actual email document. Dropped
directly into this page's own already-existing <body> via
dangerouslySetInnerHTML, that's a nested <body> tag — invalid
HTML the browser "fixes" unpredictably, which is why this preview
used to render broken (wrong background/padding/font, inline
styles not applying). An iframe gives the email HTML its own
real document, exactly like an actual email client would. */}
<iframe
srcDoc={`<!DOCTYPE html><html>${html}</html>`}
title="E-Mail-Vorschau"
style={{ width: "100%", height: "100vh", border: "none", display: "block" }}
/>
</div>
);
}
+5
View File
@@ -6,6 +6,7 @@ import { Footer } from "../../components/Footer";
import { formatPrice, formatDate } from "../../lib/format";
import { getSessionCustomer, getCustomerOrders } from "../../lib/customerAuth";
import { OrderStatusBadge } from "../components/OrderStatusBadge";
import { PaymentStatusBadge } from "../components/PaymentStatusBadge";
import { LogoutButton } from "../components/LogoutButton";
// robots: noindex — account area, same reasoning as /checkout.
@@ -61,6 +62,10 @@ export default async function KontoBestellungenPage() {
<p className="text-label text-text-muted">Status</p>
<OrderStatusBadge status={order.status} />
</div>
<div className="flex flex-col gap-1">
<p className="text-label text-text-muted">Zahlungsstatus</p>
<PaymentStatusBadge paymentStatus={order.paymentStatus} />
</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>
+10 -1
View File
@@ -464,6 +464,7 @@ export type CustomerOrder = {
createdAt: string;
total: number;
status: string;
paymentStatus: "not_applicable" | "pending" | "paid" | "failed" | "refunded" | "partially_refunded";
itemCount: number;
/** Raw product relationship ids, in item order — depth=0 keeps them as
* plain numbers, not populated objects. Callers resolve these to image
@@ -509,13 +510,21 @@ export async function getCustomerOrders(token: string, customerId: number, exclu
});
if (!res.ok) return [];
const data: {
docs?: { orderNumber: string; createdAt: string; total: number; status: string; items: { product: number }[] }[];
docs?: {
orderNumber: string;
createdAt: string;
total: number;
status: string;
paymentStatus: CustomerOrder["paymentStatus"];
items: { product: number }[];
}[];
} = await res.json();
return (data.docs ?? []).map((doc) => ({
orderNumber: doc.orderNumber,
createdAt: doc.createdAt,
total: doc.total,
status: doc.status,
paymentStatus: doc.paymentStatus,
itemCount: doc.items.length,
productIds: doc.items.map((item) => item.product),
}));
+11
View File
@@ -246,6 +246,17 @@ export const SAMPLE_ORDER: OrderConfirmationData = {
isManualPayment: false,
};
// Second preview fixture — demonstrates the Vorkasse/Überweisung branch
// (vorkasseNotice(), incl. the "switch to Kreditkarte/PayPal" mention)
// that SAMPLE_ORDER's own isManualPayment: false never shows. Used by
// LiveEmailPreviewClient.tsx's toggle, not by any real send — a real
// order-confirmation email always computes both flags live per order.
export const SAMPLE_ORDER_MANUAL: OrderConfirmationData = {
...SAMPLE_ORDER,
isManualPayment: true,
hasOnlinePaymentOption: true,
};
export function renderOrderConfirmationHtml(template: EmailTemplateContent, order: OrderConfirmationData, seller: CompanySettings | null): string {
const rows = order.items
.map(