Profile: Rechnungsadresse is always a street address, no Packstation toggle
Matches /checkout's own billing card exactly (an invoice needs a real postal address) — the profile form previously offered a Lieferart/ Packstation choice for what's actually always used as the billing address, inconsistent now that a separate "Lieferadresse" section exists.
This commit is contained in:
@@ -1999,6 +1999,43 @@ field-lock security logic is still tested in the **Payload backend's**
|
||||
own `test:unit` — see that repo's README's own "Tests" section, since
|
||||
that's where that logic actually lives.
|
||||
|
||||
## 2026-07-30 changes
|
||||
|
||||
**Blog posts can have multiple categories.** `BlogPost.categories` is now
|
||||
`string[]` (was a single `category: string`), joined with `", "` wherever
|
||||
a single category used to render (`/blog`, `/blog/[slug]`,
|
||||
`LivePostContent.tsx`, `components/Blog.tsx`). Backend field is
|
||||
`Posts.categories`, a `hasMany` relationship — see the Payload backend's
|
||||
own README.
|
||||
|
||||
**Checkout's "Abweichende Lieferadresse" gained Firma + Kontakt-E-Mail/
|
||||
Telefon** (all optional) — handed to the shipping carrier, not used for
|
||||
any customer communication (that stays the account email above). Backed
|
||||
by new `Orders.shipping*` fields with no shipping-side `vatId` (billing-
|
||||
only concept, deliberately not mirrored).
|
||||
|
||||
**The customer profile (`/konto/profil`) can now store its own shipping
|
||||
address**, mirroring the checkout's own "Abweichende Lieferadresse"
|
||||
section field-for-field (same checkbox pattern, same fields incl. the new
|
||||
Firma/Kontakt fields above) — prefills the checkout section for a
|
||||
returning customer instead of always starting blank. Backed by
|
||||
`Customers`' new "Lieferadresse" tab in the Payload backend.
|
||||
|
||||
**Products can have an optional SKU** even without variants
|
||||
(`Products.sku` — variants already had their own). Snapshotted onto each
|
||||
order item at checkout (`app/api/checkout/route.ts` — variant SKU takes
|
||||
precedence over the product's own) and shown as "Art.-Nr." on the invoice
|
||||
PDF, the order-confirmation email, and `/konto/bestellungen/[orderNumber]`.
|
||||
|
||||
**`@einfach-produktiv/invoicing` bumped to 0.2.7** for the SKU +
|
||||
shipping-contact rendering above — re-run `npm install
|
||||
@einfach-produktiv/invoicing` after pulling if your local `node_modules`
|
||||
predates this.
|
||||
|
||||
Full plan for an n8n-driven blog-post content-automation pipeline (not yet
|
||||
built) lives in the Payload backend repo's `docs/blog-automation-plan.md`,
|
||||
not duplicated here since it changes independently of this frontend.
|
||||
|
||||
## Deployment
|
||||
|
||||
- **Dockerfile**: 3-stage build (`deps` → `builder` → `runner`) with
|
||||
|
||||
@@ -16,10 +16,7 @@ export async function PATCH(request: Request) {
|
||||
const {
|
||||
firstName,
|
||||
lastName,
|
||||
deliveryMethod,
|
||||
street,
|
||||
packstationNumber,
|
||||
postNumber,
|
||||
zip,
|
||||
city,
|
||||
country,
|
||||
@@ -44,7 +41,8 @@ export async function PATCH(request: Request) {
|
||||
!firstName ||
|
||||
typeof lastName !== "string" ||
|
||||
!lastName ||
|
||||
(deliveryMethod !== "address" && deliveryMethod !== "packstation") ||
|
||||
typeof street !== "string" ||
|
||||
!street ||
|
||||
typeof zip !== "string" ||
|
||||
!zip ||
|
||||
typeof city !== "string" ||
|
||||
@@ -54,12 +52,6 @@ export async function PATCH(request: Request) {
|
||||
) {
|
||||
return NextResponse.json({ ok: false, reason: "Bitte alle Pflichtfelder ausfüllen." }, { status: 400 });
|
||||
}
|
||||
if (deliveryMethod === "address" && !street) {
|
||||
return NextResponse.json({ ok: false, reason: "Bitte Straße und Hausnummer angeben." }, { status: 400 });
|
||||
}
|
||||
if (deliveryMethod === "packstation" && (!packstationNumber || !postNumber)) {
|
||||
return NextResponse.json({ ok: false, reason: "Bitte Packstation- und Postnummer angeben." }, { status: 400 });
|
||||
}
|
||||
// Both independently optional (see Customers.ts's own comment) — only
|
||||
// format-checked when actually provided, same as the backend field itself.
|
||||
const normalizedVatId = typeof vatId === "string" && vatId ? normalizeVatId(vatId) : undefined;
|
||||
@@ -97,10 +89,8 @@ export async function PATCH(request: Request) {
|
||||
const result = await updateCustomerProfile(session.token, session.customer.id, {
|
||||
firstName,
|
||||
lastName,
|
||||
deliveryMethod,
|
||||
deliveryMethod: "address",
|
||||
street,
|
||||
packstationNumber,
|
||||
postNumber,
|
||||
zip,
|
||||
city,
|
||||
country,
|
||||
|
||||
@@ -34,7 +34,6 @@ export function ProfileForm({
|
||||
shippingCountries: ShippingCountry[];
|
||||
}) {
|
||||
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",
|
||||
@@ -53,10 +52,12 @@ export function ProfileForm({
|
||||
const body = {
|
||||
firstName: String(form.get("firstName") ?? ""),
|
||||
lastName: String(form.get("lastName") ?? ""),
|
||||
deliveryMethod,
|
||||
// Always a plain street address — same "Rechnungsadresse is never a
|
||||
// Packstation" rule as /checkout's own billing card (an invoice
|
||||
// needs a real postal address). Packstation is only ever offered
|
||||
// below, in the optional "Lieferadresse" section.
|
||||
deliveryMethod: "address" as const,
|
||||
street: String(form.get("street") ?? "") || undefined,
|
||||
packstationNumber: String(form.get("packstationNumber") ?? "") || undefined,
|
||||
postNumber: String(form.get("postNumber") ?? "") || undefined,
|
||||
zip: String(form.get("zip") ?? ""),
|
||||
city: String(form.get("city") ?? ""),
|
||||
country: String(form.get("country") ?? ""),
|
||||
@@ -136,43 +137,18 @@ export function ProfileForm({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<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={() => setDeliveryMethod("address")}
|
||||
aria-pressed={deliveryMethod === "address"}
|
||||
className={`flex-1 py-3 text-body-sm font-bold transition-colors ${deliveryMethod === "address" ? "bg-brand text-text-primary" : "text-text-muted hover:text-text-primary"}`}
|
||||
>
|
||||
Lieferadresse
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setDeliveryMethod("packstation")}
|
||||
aria-pressed={deliveryMethod === "packstation"}
|
||||
className={`flex-1 py-3 text-body-sm font-bold border-l border-border transition-colors ${deliveryMethod === "packstation" ? "bg-brand text-text-primary" : "text-text-muted hover:text-text-primary"}`}
|
||||
>
|
||||
Packstation
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{deliveryMethod === "address" ? (
|
||||
<Field
|
||||
label="Straße und Hausnummer"
|
||||
name="street"
|
||||
type="text"
|
||||
defaultValue={profile.street ?? ""}
|
||||
required
|
||||
wrapperClassName="w-full"
|
||||
/>
|
||||
) : (
|
||||
<div className="flex flex-col sm:flex-row gap-4 w-full">
|
||||
<Field label="Packstationnummer" name="packstationNumber" type="text" defaultValue={profile.packstationNumber ?? ""} required />
|
||||
<Field label="Postnummer" name="postNumber" type="text" defaultValue={profile.postNumber ?? ""} required />
|
||||
</div>
|
||||
)}
|
||||
{/* Always a plain street address, no Lieferart/Packstation toggle —
|
||||
matches /checkout's own Rechnungsadresse card exactly (an
|
||||
invoice needs a real postal address). Packstation is only ever
|
||||
offered below, in the optional "Lieferadresse" section. */}
|
||||
<Field
|
||||
label="Straße und Hausnummer"
|
||||
name="street"
|
||||
type="text"
|
||||
defaultValue={profile.street ?? ""}
|
||||
required
|
||||
wrapperClassName="w-full"
|
||||
/>
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-4 w-full">
|
||||
<Field label="PLZ" name="zip" type="text" defaultValue={profile.zip ?? ""} required />
|
||||
|
||||
Reference in New Issue
Block a user