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
+71 -13
View File
@@ -243,19 +243,32 @@ export function CheckoutContent({
const [zip, setZip] = useState(savedProfile?.zip ?? "");
const [city, setCity] = useState(savedProfile?.city ?? "");
const [country, setCountry] = useState(savedProfile?.country ?? "Deutschland");
// Optional package destination distinct from the billing address above
// no savedProfile fallback (a customer's saved profile has only ever had
// one address), just an empty draft-only section.
const [hasDifferentShippingAddress, setHasDifferentShippingAddress] = useState(false);
const [shippingFirstName, setShippingFirstName] = useState("");
const [shippingLastName, setShippingLastName] = useState("");
const [shippingDeliveryMethod, setShippingDeliveryMethod] = useState<"address" | "packstation">("address");
const [shippingStreet, setShippingStreet] = useState("");
const [shippingPackstationNumber, setShippingPackstationNumber] = useState("");
const [shippingPostNumber, setShippingPostNumber] = useState("");
const [shippingZip, setShippingZip] = useState("");
const [shippingCity, setShippingCity] = useState("");
const [shippingCountry, setShippingCountry] = useState("Deutschland");
// Optional package destination distinct from the billing address above.
// Seeded from savedProfile's own "Lieferadresse" tab (Customers.ts),
// same precedence as Card 1's billing fields above: draft (if any)
// overwrites this in the hydration effect below, savedProfile is only
// the pre-hydration/SSR-safe fallback. Unlike Card 1, savedProfile *can*
// supply these now (see Customers.ts's "Lieferadresse" tab) — this used
// to always start empty since the saved profile had only one address.
const [hasDifferentShippingAddress, setHasDifferentShippingAddress] = useState(savedProfile?.hasDifferentShippingAddress ?? false);
const [shippingFirstName, setShippingFirstName] = useState(savedProfile?.shippingFirstName ?? "");
const [shippingLastName, setShippingLastName] = useState(savedProfile?.shippingLastName ?? "");
const [shippingDeliveryMethod, setShippingDeliveryMethod] = useState<"address" | "packstation">(
savedProfile?.shippingDeliveryMethod ?? "address",
);
const [shippingStreet, setShippingStreet] = useState(savedProfile?.shippingStreet ?? "");
const [shippingPackstationNumber, setShippingPackstationNumber] = useState(savedProfile?.shippingPackstationNumber ?? "");
const [shippingPostNumber, setShippingPostNumber] = useState(savedProfile?.shippingPostNumber ?? "");
const [shippingZip, setShippingZip] = useState(savedProfile?.shippingZip ?? "");
const [shippingCity, setShippingCity] = useState(savedProfile?.shippingCity ?? "");
const [shippingCountry, setShippingCountry] = useState(savedProfile?.shippingCountry ?? "Deutschland");
// Optional, mirrors companyName above — no shipping-side vatId though,
// a VAT ID is a billing/invoice concept, not a shipping one. Contact
// email/phone have no billing-side equivalent at all: they're handed to
// the shipping carrier, not used for any customer communication.
const [shippingCompanyName, setShippingCompanyName] = useState(savedProfile?.shippingCompanyName ?? "");
const [shippingContactEmail, setShippingContactEmail] = useState(savedProfile?.shippingContactEmail ?? "");
const [shippingContactPhone, setShippingContactPhone] = useState(savedProfile?.shippingContactPhone ?? "");
const [newsletterOptIn, setNewsletterOptIn] = useState(false);
// Live VIES status for the USt-IdNr. field — only meaningful once the
// goods' destination (shipping override country when set, billing
@@ -301,6 +314,9 @@ export function CheckoutContent({
if (draft.shippingZip) setShippingZip(draft.shippingZip);
if (draft.shippingCity) setShippingCity(draft.shippingCity);
if (draft.shippingCountry) setShippingCountry(draft.shippingCountry);
if (draft.shippingCompanyName) setShippingCompanyName(draft.shippingCompanyName);
if (draft.shippingContactEmail) setShippingContactEmail(draft.shippingContactEmail);
if (draft.shippingContactPhone) setShippingContactPhone(draft.shippingContactPhone);
if (typeof draft.newsletterOptIn === "boolean") setNewsletterOptIn(draft.newsletterOptIn);
if (draft.shippingMethodId != null) setShippingMethodId(draft.shippingMethodId);
if (draft.paymentMethodId != null) setPaymentMethodId(draft.paymentMethodId);
@@ -331,6 +347,9 @@ export function CheckoutContent({
shippingZip,
shippingCity,
shippingCountry,
shippingCompanyName,
shippingContactEmail,
shippingContactPhone,
newsletterOptIn,
shippingMethodId,
paymentMethodId,
@@ -356,6 +375,9 @@ export function CheckoutContent({
shippingZip,
shippingCity,
shippingCountry,
shippingCompanyName,
shippingContactEmail,
shippingContactPhone,
newsletterOptIn,
shippingMethodId,
paymentMethodId,
@@ -558,6 +580,9 @@ export function CheckoutContent({
shippingZip: hasDifferentShippingAddress ? shippingZip : undefined,
shippingCity: hasDifferentShippingAddress ? shippingCity : undefined,
shippingCountry: hasDifferentShippingAddress ? shippingCountry : undefined,
shippingCompanyName: hasDifferentShippingAddress ? shippingCompanyName || undefined : undefined,
shippingContactEmail: hasDifferentShippingAddress ? shippingContactEmail || undefined : undefined,
shippingContactPhone: hasDifferentShippingAddress ? shippingContactPhone || undefined : undefined,
newsletterOptIn,
};
@@ -1048,6 +1073,15 @@ export function CheckoutContent({
required
/>
</div>
<FormField
label="Firma (optional)"
type="text"
value={shippingCompanyName}
onChange={(e) => setShippingCompanyName(e.target.value)}
placeholder="Muster GmbH"
autoComplete="off"
wrapperClassName="w-full"
/>
<div className="w-full sm:w-[calc(50%-0.5rem)] flex flex-col gap-2 items-start">
<span className="text-label text-text-muted">Lieferart</span>
<div className="flex w-full rounded-sm border border-border overflow-hidden">
@@ -1165,6 +1199,30 @@ export function CheckoutContent({
))}
</select>
</label>
{/* Both optional and independent — handed to the shipping
carrier, not used for any customer communication (that
stays the account email above). Useful when the
recipient at this address isn't reachable via the
account holder's own email/phone. */}
<div className="flex flex-col sm:flex-row gap-4 w-full">
<FormField
label="Kontakt-E-Mail (optional)"
type="email"
value={shippingContactEmail}
onChange={(e) => setShippingContactEmail(e.target.value)}
placeholder="empfang@beispiel.de"
autoComplete="off"
/>
<FormField
label="Telefonnummer (optional)"
type="tel"
value={shippingContactPhone}
onChange={(e) => setShippingContactPhone(e.target.value)}
placeholder="+49 30 123456"
autoComplete="off"
/>
</div>
<p className="text-label text-text-muted">Werden nur dem Versanddienstleister übergeben, z. B. für Lieferbenachrichtigungen.</p>
</div>
)}