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:
@@ -13,7 +13,32 @@ export async function PATCH(request: Request) {
|
||||
if (!session) return NextResponse.json({ ok: false, reason: "Bitte zuerst einloggen." }, { status: 401 });
|
||||
|
||||
const body = await request.json().catch(() => null);
|
||||
const { firstName, lastName, deliveryMethod, street, packstationNumber, postNumber, zip, city, country, companyName, vatId } = body ?? {};
|
||||
const {
|
||||
firstName,
|
||||
lastName,
|
||||
deliveryMethod,
|
||||
street,
|
||||
packstationNumber,
|
||||
postNumber,
|
||||
zip,
|
||||
city,
|
||||
country,
|
||||
companyName,
|
||||
vatId,
|
||||
hasDifferentShippingAddress,
|
||||
shippingFirstName,
|
||||
shippingLastName,
|
||||
shippingCompanyName,
|
||||
shippingDeliveryMethod,
|
||||
shippingStreet,
|
||||
shippingPackstationNumber,
|
||||
shippingPostNumber,
|
||||
shippingZip,
|
||||
shippingCity,
|
||||
shippingCountry,
|
||||
shippingContactEmail,
|
||||
shippingContactPhone,
|
||||
} = body ?? {};
|
||||
if (
|
||||
typeof firstName !== "string" ||
|
||||
!firstName ||
|
||||
@@ -42,6 +67,33 @@ export async function PATCH(request: Request) {
|
||||
return NextResponse.json({ ok: false, reason: "Ungültiges USt-IdNr.-Format (z. B. DE123456789)." }, { status: 400 });
|
||||
}
|
||||
|
||||
// Same shape as the checkout's own shipping-address-override validation
|
||||
// (api/checkout/route.ts) — required fields only apply when the toggle
|
||||
// is actually on, since this whole block is optional otherwise.
|
||||
if (hasDifferentShippingAddress) {
|
||||
if (
|
||||
typeof shippingFirstName !== "string" ||
|
||||
!shippingFirstName ||
|
||||
typeof shippingLastName !== "string" ||
|
||||
!shippingLastName ||
|
||||
(shippingDeliveryMethod !== "address" && shippingDeliveryMethod !== "packstation") ||
|
||||
typeof shippingZip !== "string" ||
|
||||
!shippingZip ||
|
||||
typeof shippingCity !== "string" ||
|
||||
!shippingCity ||
|
||||
typeof shippingCountry !== "string" ||
|
||||
!shippingCountry
|
||||
) {
|
||||
return NextResponse.json({ ok: false, reason: "Bitte alle Pflichtfelder der Lieferadresse ausfüllen." }, { status: 400 });
|
||||
}
|
||||
if (shippingDeliveryMethod === "address" && !shippingStreet) {
|
||||
return NextResponse.json({ ok: false, reason: "Bitte Straße und Hausnummer der Lieferadresse angeben." }, { status: 400 });
|
||||
}
|
||||
if (shippingDeliveryMethod === "packstation" && (!shippingPackstationNumber || !shippingPostNumber)) {
|
||||
return NextResponse.json({ ok: false, reason: "Bitte Packstation- und Postnummer der Lieferadresse angeben." }, { status: 400 });
|
||||
}
|
||||
}
|
||||
|
||||
const result = await updateCustomerProfile(session.token, session.customer.id, {
|
||||
firstName,
|
||||
lastName,
|
||||
@@ -54,6 +106,19 @@ export async function PATCH(request: Request) {
|
||||
country,
|
||||
companyName: typeof companyName === "string" && companyName ? companyName : undefined,
|
||||
vatId: normalizedVatId,
|
||||
hasDifferentShippingAddress: Boolean(hasDifferentShippingAddress),
|
||||
shippingFirstName: hasDifferentShippingAddress ? shippingFirstName : undefined,
|
||||
shippingLastName: hasDifferentShippingAddress ? shippingLastName : undefined,
|
||||
shippingCompanyName: hasDifferentShippingAddress && shippingCompanyName ? shippingCompanyName : undefined,
|
||||
shippingDeliveryMethod: hasDifferentShippingAddress ? shippingDeliveryMethod : undefined,
|
||||
shippingStreet: hasDifferentShippingAddress ? shippingStreet : undefined,
|
||||
shippingPackstationNumber: hasDifferentShippingAddress ? shippingPackstationNumber : undefined,
|
||||
shippingPostNumber: hasDifferentShippingAddress ? shippingPostNumber : undefined,
|
||||
shippingZip: hasDifferentShippingAddress ? shippingZip : undefined,
|
||||
shippingCity: hasDifferentShippingAddress ? shippingCity : undefined,
|
||||
shippingCountry: hasDifferentShippingAddress ? shippingCountry : undefined,
|
||||
shippingContactEmail: hasDifferentShippingAddress && shippingContactEmail ? shippingContactEmail : undefined,
|
||||
shippingContactPhone: hasDifferentShippingAddress && shippingContactPhone ? shippingContactPhone : undefined,
|
||||
});
|
||||
return NextResponse.json(result, { status: result.ok ? 200 : 400 });
|
||||
}
|
||||
|
||||
@@ -54,6 +54,9 @@ type CheckoutBody = {
|
||||
shippingZip?: string;
|
||||
shippingCity?: string;
|
||||
shippingCountry?: string;
|
||||
shippingCompanyName?: string;
|
||||
shippingContactEmail?: string;
|
||||
shippingContactPhone?: string;
|
||||
newsletterOptIn: boolean;
|
||||
};
|
||||
|
||||
@@ -160,6 +163,7 @@ export async function POST(request: Request) {
|
||||
taxRatePercent: number;
|
||||
bundleContents: string | null;
|
||||
variantName: string | null;
|
||||
sku: string | null;
|
||||
}[] = [];
|
||||
for (const line of body.cart) {
|
||||
const product = productsBySlug.get(line.id);
|
||||
@@ -168,7 +172,7 @@ export async function POST(request: Request) {
|
||||
// requested variant that no longer exists on this product (removed,
|
||||
// or never existed — a tampered request) fails the whole checkout
|
||||
// rather than silently falling back to the base product/price.
|
||||
let variant: { name: string; priceOverride: number | null } | null = null;
|
||||
let variant: { name: string; priceOverride: number | null; sku: string | null } | null = null;
|
||||
if (line.variant) {
|
||||
variant = product.variants?.find((v) => v.name === line.variant) ?? null;
|
||||
if (!variant) return NextResponse.json({ ok: false, reason: "Eine gewählte Variante ist nicht mehr verfügbar." }, { status: 400 });
|
||||
@@ -195,6 +199,9 @@ export async function POST(request: Request) {
|
||||
taxRatePercent: kleinunternehmer ? 0 : (product.taxRatePercent ?? defaultTaxRate),
|
||||
bundleContents: describeBundleContents(product),
|
||||
variantName: variant?.name ?? null,
|
||||
// Variant sku takes precedence over the product's own — same
|
||||
// "variant overrides product" precedence unitPrice already uses.
|
||||
sku: variant?.sku ?? product.sku ?? null,
|
||||
});
|
||||
}
|
||||
const subtotal = roundMoney(items.reduce((sum, i) => sum + i.quantity * i.unitPrice, 0));
|
||||
@@ -345,11 +352,15 @@ export async function POST(request: Request) {
|
||||
shippingZip: body.shippingZip,
|
||||
shippingCity: body.shippingCity,
|
||||
shippingCountry: body.shippingCountry,
|
||||
shippingCompanyName: Boolean(body.hasDifferentShippingAddress) ? body.shippingCompanyName || undefined : undefined,
|
||||
shippingContactEmail: Boolean(body.hasDifferentShippingAddress) ? body.shippingContactEmail || undefined : undefined,
|
||||
shippingContactPhone: Boolean(body.hasDifferentShippingAddress) ? body.shippingContactPhone || undefined : undefined,
|
||||
newsletterOptIn: Boolean(body.newsletterOptIn),
|
||||
items,
|
||||
subtotal: finalSubtotal,
|
||||
shippingCost: finalShippingCost,
|
||||
shippingMethodTitle: shippingMethod.title,
|
||||
shippingMethod: shippingMethod.id,
|
||||
// The checkout UI collapses Kreditkarte/PayPal into one "Online-
|
||||
// Zahlung" pre-selection (see groupPaymentMethodsForCheckout) — the
|
||||
// customer hasn't actually chosen an instrument yet at this point,
|
||||
@@ -429,6 +440,7 @@ export async function POST(request: Request) {
|
||||
hasDifferentShippingAddress: Boolean(body.hasDifferentShippingAddress),
|
||||
shippingFirstName: body.shippingFirstName,
|
||||
shippingLastName: body.shippingLastName,
|
||||
shippingCompanyName: body.hasDifferentShippingAddress ? body.shippingCompanyName : undefined,
|
||||
shippingDeliveryMethod: body.shippingDeliveryMethod,
|
||||
shippingStreet: body.shippingStreet,
|
||||
shippingPackstationNumber: body.shippingPackstationNumber,
|
||||
@@ -436,6 +448,8 @@ export async function POST(request: Request) {
|
||||
shippingZip: body.shippingZip,
|
||||
shippingCity: body.shippingCity,
|
||||
shippingCountry: body.shippingCountry,
|
||||
shippingContactEmail: body.hasDifferentShippingAddress ? body.shippingContactEmail : undefined,
|
||||
shippingContactPhone: body.hasDifferentShippingAddress ? body.shippingContactPhone : undefined,
|
||||
paymentMethodTitle: paymentMethod.title,
|
||||
items: items.map((i) => ({
|
||||
productName: i.productName,
|
||||
@@ -445,6 +459,7 @@ export async function POST(request: Request) {
|
||||
taxRatePercent: i.taxRatePercent,
|
||||
bundleContents: i.bundleContents,
|
||||
variantName: i.variantName,
|
||||
sku: i.sku,
|
||||
})),
|
||||
subtotal: finalSubtotal,
|
||||
shippingCost: finalShippingCost,
|
||||
|
||||
@@ -9,7 +9,7 @@ import { mapPayloadPost, type PayloadPostDetail, type PostDetail } from "../../.
|
||||
|
||||
const PAYLOAD_URL = process.env.NEXT_PUBLIC_PAYLOAD_URL || "https://payload.mk360.de";
|
||||
|
||||
// Live-previewable subset of the blog detail page: title/category/readTime/
|
||||
// Live-previewable subset of the blog detail page: title/categories/readTime/
|
||||
// excerpt/byline, the thumbnail, and the RichText body — the fields an
|
||||
// editor actually watches update while typing. The author bio card,
|
||||
// "Weiterlesen" card, and Footer stay static in page.tsx: they either
|
||||
@@ -27,7 +27,7 @@ export function LivePostContent({ initialPost }: { initialPost: PostDetail }) {
|
||||
<>
|
||||
<Reveal className="flex flex-col gap-4 items-start pt-10 pb-8 px-[var(--layout-padding-x)] w-full max-w-[48rem] mx-auto">
|
||||
<div className="flex items-center gap-2 font-semibold text-text-muted text-body-sm uppercase tracking-wide">
|
||||
<span>{post.category}</span>
|
||||
<span>{post.categories.join(", ")}</span>
|
||||
<span>•</span>
|
||||
<span>{post.readTime} Min</span>
|
||||
</div>
|
||||
|
||||
@@ -75,7 +75,7 @@ export default async function BlogDetailPage({
|
||||
<>
|
||||
<Reveal className="flex flex-col gap-4 items-start pt-10 pb-8 px-[var(--layout-padding-x)] w-full max-w-[48rem] mx-auto">
|
||||
<div className="flex items-center gap-2 font-semibold text-text-muted text-body-sm uppercase tracking-wide">
|
||||
<span>{post.category}</span>
|
||||
<span>{post.categories.join(", ")}</span>
|
||||
<span>•</span>
|
||||
<span>{post.readTime} Min</span>
|
||||
</div>
|
||||
@@ -221,7 +221,7 @@ export default async function BlogDetailPage({
|
||||
</div>
|
||||
<div className="flex flex-col justify-center gap-2 p-6 min-w-0">
|
||||
<div className="flex items-center gap-2 font-semibold text-text-muted text-body-sm uppercase tracking-wide">
|
||||
<span>{nextPost.category}</span>
|
||||
<span>{nextPost.categories.join(", ")}</span>
|
||||
<span>•</span>
|
||||
<span>{nextPost.readTime} Min</span>
|
||||
</div>
|
||||
|
||||
+2
-2
@@ -65,7 +65,7 @@ export default async function BlogOverviewPage() {
|
||||
</div>
|
||||
<div className="flex flex-col justify-center gap-3 p-8 sm:p-12 min-w-0">
|
||||
<div className="flex items-center gap-2 font-semibold text-text-muted text-body-sm uppercase tracking-wide">
|
||||
<span>{featured.category}</span>
|
||||
<span>{featured.categories.join(", ")}</span>
|
||||
<span>•</span>
|
||||
<span>{featured.readTime} Min</span>
|
||||
<span>•</span>
|
||||
@@ -106,7 +106,7 @@ export default async function BlogOverviewPage() {
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2 font-semibold text-text-muted text-body-sm uppercase tracking-wide">
|
||||
<span>{post.category}</span>
|
||||
<span>{post.categories.join(", ")}</span>
|
||||
<span>•</span>
|
||||
<span>{post.readTime} Min</span>
|
||||
<span>•</span>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ export async function Blog() {
|
||||
<div className="flex flex-col justify-between gap-4 py-4 px-4 sm:pr-4 sm:pl-4 sm:col-span-5 min-w-0">
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex gap-2 items-center font-semibold text-text-muted text-body uppercase whitespace-nowrap">
|
||||
<span>{featured.category}</span>
|
||||
<span>{featured.categories.join(", ")}</span>
|
||||
<span>•</span>
|
||||
<span>{featured.readTime} Min</span>
|
||||
</div>
|
||||
@@ -96,7 +96,7 @@ export async function Blog() {
|
||||
<div className="flex flex-col justify-between gap-4 py-3 px-4 sm:col-span-3 min-w-0 text-text-primary">
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex gap-2 items-center font-semibold text-text-muted text-body uppercase whitespace-nowrap">
|
||||
<span>{post.category}</span>
|
||||
<span>{post.categories.join(", ")}</span>
|
||||
<span>•</span>
|
||||
<span>{post.readTime} Min</span>
|
||||
</div>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ const product = (overrides: Partial<RawProduct> = {}): RawProduct => ({
|
||||
id: 1,
|
||||
slug: "starter-set",
|
||||
name: "Starter-Set",
|
||||
sku: null,
|
||||
price: 29.9,
|
||||
active: true,
|
||||
image: null,
|
||||
|
||||
@@ -36,6 +36,9 @@ export type CheckoutDraft = {
|
||||
shippingZip: string;
|
||||
shippingCity: string;
|
||||
shippingCountry: string;
|
||||
shippingCompanyName: string;
|
||||
shippingContactEmail: string;
|
||||
shippingContactPhone: string;
|
||||
newsletterOptIn: boolean;
|
||||
shippingMethodId: number | null;
|
||||
paymentMethodId: number | null;
|
||||
|
||||
@@ -203,6 +203,23 @@ export type CustomerAddress = {
|
||||
// /checkout's Firma/USt-IdNr. fields for a returning customer.
|
||||
companyName: string | null;
|
||||
vatId: string | null;
|
||||
// Optional second/shipping address — see Customers.ts's "Lieferadresse"
|
||||
// tab. Prefills /checkout's "Abweichende Lieferadresse" section once
|
||||
// hasDifferentShippingAddress is set here; still fully overwritable per
|
||||
// order (Orders keeps its own shipping* snapshot regardless).
|
||||
hasDifferentShippingAddress: boolean;
|
||||
shippingFirstName: string | null;
|
||||
shippingLastName: string | null;
|
||||
shippingCompanyName: string | null;
|
||||
shippingDeliveryMethod: "address" | "packstation" | null;
|
||||
shippingStreet: string | null;
|
||||
shippingPackstationNumber: string | null;
|
||||
shippingPostNumber: string | null;
|
||||
shippingZip: string | null;
|
||||
shippingCity: string | null;
|
||||
shippingCountry: string | null;
|
||||
shippingContactEmail: string | null;
|
||||
shippingContactPhone: string | null;
|
||||
};
|
||||
|
||||
export type CustomerProfile = CustomerSummary & CustomerAddress;
|
||||
@@ -223,6 +240,19 @@ type PayloadCustomerMe = {
|
||||
country: string | null;
|
||||
companyName: string | null;
|
||||
vatId: string | null;
|
||||
hasDifferentShippingAddress: boolean | null;
|
||||
shippingFirstName: string | null;
|
||||
shippingLastName: string | null;
|
||||
shippingCompanyName: string | null;
|
||||
shippingDeliveryMethod: "address" | "packstation" | null;
|
||||
shippingStreet: string | null;
|
||||
shippingPackstationNumber: string | null;
|
||||
shippingPostNumber: string | null;
|
||||
shippingZip: string | null;
|
||||
shippingCity: string | null;
|
||||
shippingCountry: string | null;
|
||||
shippingContactEmail: string | null;
|
||||
shippingContactPhone: string | null;
|
||||
cart: { product: number; productSlug: string; quantity: number; variantName: string | null }[] | null;
|
||||
};
|
||||
|
||||
@@ -251,6 +281,19 @@ export async function getCustomerProfile(token: string): Promise<CustomerProfile
|
||||
country: u.country,
|
||||
companyName: u.companyName,
|
||||
vatId: u.vatId,
|
||||
hasDifferentShippingAddress: Boolean(u.hasDifferentShippingAddress),
|
||||
shippingFirstName: u.shippingFirstName,
|
||||
shippingLastName: u.shippingLastName,
|
||||
shippingCompanyName: u.shippingCompanyName,
|
||||
shippingDeliveryMethod: u.shippingDeliveryMethod,
|
||||
shippingStreet: u.shippingStreet,
|
||||
shippingPackstationNumber: u.shippingPackstationNumber,
|
||||
shippingPostNumber: u.shippingPostNumber,
|
||||
shippingZip: u.shippingZip,
|
||||
shippingCity: u.shippingCity,
|
||||
shippingCountry: u.shippingCountry,
|
||||
shippingContactEmail: u.shippingContactEmail,
|
||||
shippingContactPhone: u.shippingContactPhone,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -269,6 +312,19 @@ export async function updateCustomerProfile(
|
||||
country: string;
|
||||
companyName?: string;
|
||||
vatId?: string;
|
||||
hasDifferentShippingAddress?: boolean;
|
||||
shippingFirstName?: string;
|
||||
shippingLastName?: string;
|
||||
shippingCompanyName?: string;
|
||||
shippingDeliveryMethod?: "address" | "packstation";
|
||||
shippingStreet?: string;
|
||||
shippingPackstationNumber?: string;
|
||||
shippingPostNumber?: string;
|
||||
shippingZip?: string;
|
||||
shippingCity?: string;
|
||||
shippingCountry?: string;
|
||||
shippingContactEmail?: string;
|
||||
shippingContactPhone?: string;
|
||||
},
|
||||
): Promise<{ ok: true } | { ok: false; reason: string }> {
|
||||
const res = await fetch(`${PAYLOAD_URL}/api/customers/${customerId}`, {
|
||||
@@ -495,6 +551,7 @@ export type CustomerOrderDetail = CustomerOrder & {
|
||||
hasDifferentShippingAddress: boolean;
|
||||
shippingFirstName: string | null;
|
||||
shippingLastName: string | null;
|
||||
shippingCompanyName: string | null;
|
||||
shippingDeliveryMethod: "address" | "packstation" | null;
|
||||
shippingStreet: string | null;
|
||||
shippingPackstationNumber: string | null;
|
||||
@@ -502,6 +559,8 @@ export type CustomerOrderDetail = CustomerOrder & {
|
||||
shippingZip: string | null;
|
||||
shippingCity: string | null;
|
||||
shippingCountry: string | null;
|
||||
shippingContactEmail: string | null;
|
||||
shippingContactPhone: string | null;
|
||||
subtotal: number;
|
||||
shippingCost: number;
|
||||
shippingMethodTitle: string;
|
||||
@@ -521,6 +580,7 @@ export type CustomerOrderItem = {
|
||||
bundleContents: string | null;
|
||||
variantName: string | null;
|
||||
returnQuantity: number;
|
||||
sku: string | null;
|
||||
};
|
||||
|
||||
// Access control (Orders.ts) already scopes a customer's own JWT to only
|
||||
|
||||
@@ -196,6 +196,7 @@ export type OrderConfirmationItem = {
|
||||
bundleContents?: string | null;
|
||||
variantName?: string | null;
|
||||
taxRatePercent: number;
|
||||
sku?: string | null;
|
||||
};
|
||||
export type OrderConfirmationData = {
|
||||
orderNumber: string;
|
||||
@@ -249,7 +250,7 @@ export function renderOrderConfirmationHtml(template: EmailTemplateContent, orde
|
||||
: `<div style="width:44px;height:44px;border-radius:6px;background:${BG_MUTED};"></div>`
|
||||
}
|
||||
</td>
|
||||
<td style="padding:10px 0 10px 12px;border-bottom:1px solid ${BORDER};font-size:14px;color:${TEXT_PRIMARY};">${escapeHtml(item.productName)}${item.variantName ? ` (${escapeHtml(item.variantName)})` : ""} <span style="color:${TEXT_MUTED};">× ${item.quantity}</span>${item.bundleContents ? `<br/><span style="font-size:12px;color:${TEXT_MUTED};">${escapeHtml(item.bundleContents)}</span>` : ""}</td>
|
||||
<td style="padding:10px 0 10px 12px;border-bottom:1px solid ${BORDER};font-size:14px;color:${TEXT_PRIMARY};">${escapeHtml(item.productName)}${item.variantName ? ` (${escapeHtml(item.variantName)})` : ""} <span style="color:${TEXT_MUTED};">× ${item.quantity}</span>${item.bundleContents ? `<br/><span style="font-size:12px;color:${TEXT_MUTED};">${escapeHtml(item.bundleContents)}</span>` : ""}${item.sku ? `<br/><span style="font-size:12px;color:${TEXT_MUTED};">Art.-Nr. ${escapeHtml(item.sku)}</span>` : ""}</td>
|
||||
<td style="padding:10px 0;border-bottom:1px solid ${BORDER};text-align:right;white-space:nowrap;font-size:14px;color:${TEXT_PRIMARY};">${formatPrice(item.quantity * item.unitPrice)}</td>
|
||||
</tr>`,
|
||||
)
|
||||
|
||||
@@ -27,6 +27,7 @@ export type OrderConfirmationEmailData = OrderConfirmationData & {
|
||||
hasDifferentShippingAddress?: boolean;
|
||||
shippingFirstName?: string | null;
|
||||
shippingLastName?: string | null;
|
||||
shippingCompanyName?: string | null;
|
||||
shippingDeliveryMethod?: "address" | "packstation" | null;
|
||||
shippingStreet?: string | null;
|
||||
shippingPackstationNumber?: string | null;
|
||||
@@ -34,6 +35,8 @@ export type OrderConfirmationEmailData = OrderConfirmationData & {
|
||||
shippingZip?: string | null;
|
||||
shippingCity?: string | null;
|
||||
shippingCountry?: string | null;
|
||||
shippingContactEmail?: string | null;
|
||||
shippingContactPhone?: string | null;
|
||||
paymentMethodTitle: string;
|
||||
};
|
||||
|
||||
@@ -93,6 +96,7 @@ export async function sendOrderConfirmationEmail(order: OrderConfirmationEmailDa
|
||||
hasDifferentShippingAddress: order.hasDifferentShippingAddress ?? false,
|
||||
shippingFirstName: order.shippingFirstName,
|
||||
shippingLastName: order.shippingLastName,
|
||||
shippingCompanyName: order.shippingCompanyName,
|
||||
shippingDeliveryMethod: order.shippingDeliveryMethod,
|
||||
shippingStreet: order.shippingStreet,
|
||||
shippingPackstationNumber: order.shippingPackstationNumber,
|
||||
@@ -100,6 +104,8 @@ export async function sendOrderConfirmationEmail(order: OrderConfirmationEmailDa
|
||||
shippingZip: order.shippingZip,
|
||||
shippingCity: order.shippingCity,
|
||||
shippingCountry: order.shippingCountry,
|
||||
shippingContactEmail: order.shippingContactEmail,
|
||||
shippingContactPhone: order.shippingContactPhone,
|
||||
paymentMethodTitle: order.paymentMethodTitle,
|
||||
items: order.items.map((i) => ({
|
||||
productName: i.productName,
|
||||
@@ -109,6 +115,7 @@ export async function sendOrderConfirmationEmail(order: OrderConfirmationEmailDa
|
||||
bundleContents: i.bundleContents ?? null,
|
||||
variantName: i.variantName ?? null,
|
||||
imageUrl: i.imageUrl ?? null,
|
||||
sku: i.sku ?? null,
|
||||
})),
|
||||
subtotal: order.subtotal,
|
||||
shippingCost: order.shippingCost,
|
||||
|
||||
@@ -27,6 +27,11 @@ export type OrderItemInput = {
|
||||
taxRatePercent: number;
|
||||
bundleContents: string | null;
|
||||
variantName: string | null;
|
||||
// Resolved by the caller (api/checkout/route.ts): the variant's own sku
|
||||
// if one was selected, else the product's sku, else null — same
|
||||
// "variant overrides product" precedence as unitPrice/taxRatePercent
|
||||
// elsewhere in this checkout flow.
|
||||
sku: string | null;
|
||||
};
|
||||
|
||||
export type CreateOrderInput = {
|
||||
@@ -67,11 +72,23 @@ export type CreateOrderInput = {
|
||||
shippingZip?: string;
|
||||
shippingCity?: string;
|
||||
shippingCountry?: string;
|
||||
// Optional, mirrors companyName above — no shipping-side vatId (billing-
|
||||
// only concept). Contact email/phone have no billing-side equivalent:
|
||||
// they're handed to the shipping carrier, never used for customer
|
||||
// communication.
|
||||
shippingCompanyName?: string;
|
||||
shippingContactEmail?: string;
|
||||
shippingContactPhone?: string;
|
||||
newsletterOptIn: boolean;
|
||||
items: OrderItemInput[];
|
||||
subtotal: number;
|
||||
shippingCost: number;
|
||||
shippingMethodTitle: string;
|
||||
// Numeric ShippingMethod id, not the same as shippingMethodTitle's frozen
|
||||
// text snapshot — lets pollCarrierTracking.ts resolve order → shipping
|
||||
// method → carrier without a fragile title-text match. See Orders.ts's
|
||||
// own comment on why both fields exist side by side.
|
||||
shippingMethod: number;
|
||||
paymentMethodTitle: string;
|
||||
discountCode: string | null;
|
||||
discountAmount: number;
|
||||
@@ -140,6 +157,9 @@ export async function createOrder(input: CreateOrderInput): Promise<CreatedOrder
|
||||
shippingZip: input.shippingZip,
|
||||
shippingCity: input.shippingCity,
|
||||
shippingCountry: input.shippingCountry,
|
||||
shippingCompanyName: input.shippingCompanyName,
|
||||
shippingContactEmail: input.shippingContactEmail,
|
||||
shippingContactPhone: input.shippingContactPhone,
|
||||
newsletterOptIn: input.newsletterOptIn,
|
||||
items: input.items.map((i) => ({
|
||||
product: i.productId,
|
||||
@@ -149,10 +169,12 @@ export async function createOrder(input: CreateOrderInput): Promise<CreatedOrder
|
||||
taxRatePercent: i.taxRatePercent,
|
||||
bundleContents: i.bundleContents,
|
||||
variantName: i.variantName,
|
||||
sku: i.sku,
|
||||
})),
|
||||
subtotal: input.subtotal,
|
||||
shippingCost: input.shippingCost,
|
||||
shippingMethodTitle: input.shippingMethodTitle,
|
||||
shippingMethod: input.shippingMethod,
|
||||
paymentMethodTitle: input.paymentMethodTitle,
|
||||
discountCode: input.discountCode,
|
||||
discountAmount: input.discountAmount,
|
||||
|
||||
+8
-8
@@ -21,7 +21,7 @@ export type BlogPost = {
|
||||
id: number;
|
||||
title: string;
|
||||
slug: string;
|
||||
category: string;
|
||||
categories: string[];
|
||||
readTime: number;
|
||||
excerpt: string;
|
||||
thumbnail: string | null;
|
||||
@@ -33,7 +33,7 @@ type PayloadPost = {
|
||||
id: number;
|
||||
title: string;
|
||||
slug: string;
|
||||
category: { name: string } | number | null;
|
||||
categories: ({ name: string } | number)[];
|
||||
readTime: number;
|
||||
excerpt: string;
|
||||
thumbnail: { url: string } | number | null;
|
||||
@@ -73,10 +73,9 @@ export async function getBlogPosts(limit = 3): Promise<BlogPost[]> {
|
||||
id: post.id,
|
||||
title: post.title,
|
||||
slug: post.slug,
|
||||
category:
|
||||
typeof post.category === "object" && post.category
|
||||
? post.category.name
|
||||
: "",
|
||||
categories: (post.categories ?? [])
|
||||
.map((c) => (typeof c === "object" && c ? c.name : null))
|
||||
.filter((name): name is string => Boolean(name)),
|
||||
readTime: post.readTime,
|
||||
excerpt: post.excerpt,
|
||||
thumbnail:
|
||||
@@ -125,8 +124,9 @@ export function mapPayloadPost(doc: PayloadPostDetail): PostDetail {
|
||||
id: doc.id,
|
||||
title: doc.title,
|
||||
slug: doc.slug,
|
||||
category:
|
||||
typeof doc.category === "object" && doc.category ? doc.category.name : "",
|
||||
categories: (doc.categories ?? [])
|
||||
.map((c) => (typeof c === "object" && c ? c.name : null))
|
||||
.filter((name): name is string => Boolean(name)),
|
||||
readTime: doc.readTime,
|
||||
excerpt: doc.excerpt,
|
||||
thumbnail:
|
||||
|
||||
@@ -20,6 +20,7 @@ export type RawProduct = {
|
||||
id: number;
|
||||
slug: string;
|
||||
name: string;
|
||||
sku: string | null;
|
||||
price: number;
|
||||
active: boolean;
|
||||
image: { url: string } | number | null;
|
||||
|
||||
Generated
+2
-2
@@ -455,8 +455,8 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@einfach-produktiv/invoicing": {
|
||||
"version": "0.2.6",
|
||||
"resolved": "git+https://git.mk360.de/Marco/einfach-produktiv-invoicing.git#d6e83db5cf6343e65e2fa25f9ead49fce9a5a308",
|
||||
"version": "0.2.7",
|
||||
"resolved": "git+https://git.mk360.de/Marco/einfach-produktiv-invoicing.git#e66007cc4aacccf3b1f18892f3ae9a5e6adf685e",
|
||||
"dependencies": {
|
||||
"@e-invoice-eu/core": "^3.1.1"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user