Blog categories (hasMany), shipping-address contact fields, product SKU on invoices

- Posts.categories is now hasMany — blog list/detail/live-preview render
  a comma-joined list instead of a single category.
- Checkout's "Abweichende Lieferadresse" gains optional Firma + Kontakt-
  E-Mail/Telefon fields (handed to the shipping carrier, not used for
  customer communication).
- Customer profile can now store its own shipping address (mirroring
  Customers.ts's new "Lieferadresse" tab), prefilling the checkout
  override instead of always starting blank.
- Product-level optional SKU (previously only on variants) snapshots onto
  each order item and shows up on invoices (visual + EN16931 XML),
  the confirmation email, and the account order detail page.
This commit is contained in:
Marco
2026-07-30 08:41:39 +00:00
parent 1467750db6
commit e3352d7e32
18 changed files with 403 additions and 34 deletions
@@ -120,6 +120,7 @@ export default async function KontoBestellungDetailPage({ params }: { params: Pr
{order.hasDifferentShippingAddress && (
<div className="flex flex-col gap-1 w-full">
<p className="text-label text-text-muted">Lieferadresse</p>
{order.shippingCompanyName && <p className="text-body-sm text-text-primary">{order.shippingCompanyName}</p>}
<p className="text-body-sm text-text-primary">
{order.shippingFirstName} {order.shippingLastName}
</p>
@@ -127,6 +128,11 @@ export default async function KontoBestellungDetailPage({ params }: { params: Pr
<p className="text-body-sm text-text-primary">
{order.shippingZip} {order.shippingCity}, {order.shippingCountry}
</p>
{(order.shippingContactEmail || order.shippingContactPhone) && (
<p className="text-body-sm text-text-muted">
{[order.shippingContactEmail, order.shippingContactPhone].filter(Boolean).join(" · ")}
</p>
)}
</div>
)}
@@ -145,6 +151,7 @@ export default async function KontoBestellungDetailPage({ params }: { params: Pr
</p>
{!order.kleinunternehmer && <p className="text-label text-text-muted">inkl. {item.taxRatePercent}% MwSt.</p>}
{item.bundleContents && <p className="text-label text-text-muted">{item.bundleContents}</p>}
{item.sku && <p className="text-label text-text-muted">Art.-Nr. {item.sku}</p>}
{item.returnQuantity > 0 && (
<p className="text-label text-text-muted">davon {item.returnQuantity} zurückgesendet</p>
)}
+129
View File
@@ -35,6 +35,10 @@ export function ProfileForm({
}) {
const router = useRouter();
const [deliveryMethod, setDeliveryMethod] = useState<"address" | "packstation">(profile.deliveryMethod ?? "address");
const [hasDifferentShippingAddress, setHasDifferentShippingAddress] = useState(profile.hasDifferentShippingAddress);
const [shippingDeliveryMethod, setShippingDeliveryMethod] = useState<"address" | "packstation">(
profile.shippingDeliveryMethod ?? "address",
);
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState(false);
const [saving, setSaving] = useState(false);
@@ -58,6 +62,21 @@ export function ProfileForm({
country: String(form.get("country") ?? ""),
companyName: String(form.get("companyName") ?? "") || undefined,
vatId: String(form.get("vatId") ?? "") || undefined,
hasDifferentShippingAddress,
shippingFirstName: hasDifferentShippingAddress ? String(form.get("shippingFirstName") ?? "") : undefined,
shippingLastName: hasDifferentShippingAddress ? String(form.get("shippingLastName") ?? "") : undefined,
shippingCompanyName: hasDifferentShippingAddress ? String(form.get("shippingCompanyName") ?? "") || undefined : undefined,
shippingDeliveryMethod: hasDifferentShippingAddress ? shippingDeliveryMethod : undefined,
shippingStreet: hasDifferentShippingAddress ? String(form.get("shippingStreet") ?? "") || undefined : undefined,
shippingPackstationNumber: hasDifferentShippingAddress
? String(form.get("shippingPackstationNumber") ?? "") || undefined
: undefined,
shippingPostNumber: hasDifferentShippingAddress ? String(form.get("shippingPostNumber") ?? "") || undefined : undefined,
shippingZip: hasDifferentShippingAddress ? String(form.get("shippingZip") ?? "") : undefined,
shippingCity: hasDifferentShippingAddress ? String(form.get("shippingCity") ?? "") : undefined,
shippingCountry: hasDifferentShippingAddress ? String(form.get("shippingCountry") ?? "") : undefined,
shippingContactEmail: hasDifferentShippingAddress ? String(form.get("shippingContactEmail") ?? "") || undefined : undefined,
shippingContactPhone: hasDifferentShippingAddress ? String(form.get("shippingContactPhone") ?? "") || undefined : undefined,
};
try {
@@ -91,6 +110,11 @@ export function ProfileForm({
</p>
<form onSubmit={handleSubmit} className="flex flex-col gap-4 items-start w-full">
{/* Explicit heading, matching /checkout's own "1. Rechnungsadresse"
card title — without it, this section and the "Lieferadresse"
one further down read as one undifferentiated form instead of
two distinct addresses. */}
<p className="font-semibold text-body-sm text-text-primary">Rechnungsadresse</p>
<div className="flex flex-col sm:flex-row gap-4 w-full">
<Field label="Vorname" name="firstName" type="text" defaultValue={profile.firstName} required />
<Field label="Nachname" name="lastName" type="text" defaultValue={profile.lastName} required />
@@ -164,6 +188,111 @@ export function ProfileForm({
</select>
</label>
<div className="h-px bg-border w-full" />
<label className="flex gap-3 items-start w-full cursor-pointer">
<input
type="checkbox"
checked={hasDifferentShippingAddress}
onChange={(e) => setHasDifferentShippingAddress(e.target.checked)}
className="size-5 shrink-0 mt-0.5 rounded-xs border border-border accent-brand"
/>
<span className="text-body-sm text-text-primary">
Abweichende Lieferadresse hinterlegen wird beim Checkout vorgeschlagen, sobald dort "Abweichende Lieferadresse" aktiviert wird
</span>
</label>
{hasDifferentShippingAddress && (
<div className="flex flex-col gap-4 items-start w-full">
<p className="font-semibold text-body-sm text-text-primary">Lieferadresse</p>
<div className="flex flex-col sm:flex-row gap-4 w-full">
<Field label="Vorname" name="shippingFirstName" type="text" defaultValue={profile.shippingFirstName ?? ""} required />
<Field label="Nachname" name="shippingLastName" type="text" defaultValue={profile.shippingLastName ?? ""} required />
</div>
<Field
label="Firma (optional)"
name="shippingCompanyName"
type="text"
defaultValue={profile.shippingCompanyName ?? ""}
wrapperClassName="w-full"
/>
<div className="w-full flex flex-col gap-2 items-start">
<span className="text-label text-text-muted">Lieferart</span>
<div className="flex w-full max-w-sm rounded-sm border border-border overflow-hidden">
<button
type="button"
onClick={() => setShippingDeliveryMethod("address")}
aria-pressed={shippingDeliveryMethod === "address"}
className={`flex-1 py-3 text-body-sm font-bold transition-colors ${shippingDeliveryMethod === "address" ? "bg-brand text-text-primary" : "text-text-muted hover:text-text-primary"}`}
>
Lieferadresse
</button>
<button
type="button"
onClick={() => setShippingDeliveryMethod("packstation")}
aria-pressed={shippingDeliveryMethod === "packstation"}
className={`flex-1 py-3 text-body-sm font-bold border-l border-border transition-colors ${shippingDeliveryMethod === "packstation" ? "bg-brand text-text-primary" : "text-text-muted hover:text-text-primary"}`}
>
Packstation
</button>
</div>
</div>
{shippingDeliveryMethod === "address" ? (
<Field
label="Straße und Hausnummer"
name="shippingStreet"
type="text"
defaultValue={profile.shippingStreet ?? ""}
required
wrapperClassName="w-full"
/>
) : (
<div className="flex flex-col sm:flex-row gap-4 w-full">
<Field
label="Packstationnummer"
name="shippingPackstationNumber"
type="text"
defaultValue={profile.shippingPackstationNumber ?? ""}
required
/>
<Field label="Postnummer" name="shippingPostNumber" type="text" defaultValue={profile.shippingPostNumber ?? ""} required />
</div>
)}
<div className="flex flex-col sm:flex-row gap-4 w-full">
<Field label="PLZ" name="shippingZip" type="text" defaultValue={profile.shippingZip ?? ""} required />
<Field label="Ort" name="shippingCity" type="text" defaultValue={profile.shippingCity ?? ""} required />
</div>
<label className="flex flex-col gap-2 items-start w-full sm:w-1/2">
<span className="text-label text-text-muted">Land</span>
<select name="shippingCountry" defaultValue={profile.shippingCountry ?? "Deutschland"} required className={`${inputClass} bg-bg-base`}>
{shippingCountries.map((c) => (
<option key={c.name}>{c.name}</option>
))}
</select>
</label>
<div className="flex flex-col sm:flex-row gap-4 w-full">
<Field
label="Kontakt-E-Mail (optional)"
name="shippingContactEmail"
type="email"
defaultValue={profile.shippingContactEmail ?? ""}
/>
<Field
label="Telefonnummer (optional)"
name="shippingContactPhone"
type="tel"
defaultValue={profile.shippingContactPhone ?? ""}
/>
</div>
<p className="text-label text-text-muted">Werden nur dem Versanddienstleister übergeben, z. B. für Lieferbenachrichtigungen.</p>
</div>
)}
{error && <p className="text-label text-red-600">{error}</p>}
{success && <p className="text-label text-success">Gespeichert.</p>}