Commit Graph

57 Commits

Author SHA1 Message Date
Marco 557f6a1abc Add product categories + shop sidebar filters, fix CTA button alignment
Products now carry an optional categories relationship (backend:
new product-categories collection mirroring the blog's categories
pattern, migration applied and deployed). The shop page gains a left
sidebar (styled like AccountNav) with a dual-handle price slider,
category checkboxes, and an availability toggle — all instant-apply
via searchParams, same union-filter semantics as the blog's category
chips. Uncategorized products always match every category filter
rather than disappearing.

Also fixes CTA buttons sitting at different heights across sibling
cards when one product's title wraps to two lines — MerklisteGrid.tsx
and RelatedProducts.tsx get the same h-full/flex-1 spacer pattern
ProductGrid.tsx already used.
2026-08-01 08:15:58 +00:00
Marco bf6c5d079a Fix order-filter dropdowns, restructure Konto identity line, drop blog reset link
- CustomSelect.tsx: mousedown on an option blurred the trigger button
  before the click landed, unmounting the list before anything could be
  selected — add preventDefault on the list's mousedown to stop that.
- KontoShell no longer renders "Eingeloggt als ..." above each page's
  content; each page renders its own title then AccountIdentity right
  below it, and ProfileForm's now-duplicate email/Kundennummer line is
  removed.
- Blog category filter: drop the separate "Zurücksetzen" link — clicking
  an active category badge again already deactivates it.
- Werkzeuge cards: more vertical gap between stacked cards below sm:.
- Legal pages' "Stand: ..." line is now derived from the LegalPages doc's
  own updatedAt instead of a hand-typed string.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018unaXmuzVA8ct1b6WoyP1U
2026-07-31 22:29:36 +00:00
Marco d018d87e25 Fix account-nav feedback, blog filter position, and small icon polish
- Remove Merkliste from AccountNav — the Navbar's own wishlist icon already covers it, a second nav entry was redundant.
- Fix duplicate "Mein Profil" heading (ProfileForm already renders its own; KontoShell no longer adds a second one).
- "Eingeloggt als ..." now renders once in KontoShell, consistently on every /konto/* page, instead of only on the orders page.
- Blog category filter chips moved into the hero's own text column (was a separate bar below the entire hero including the photo) — visible immediately on every width instead of requiring a scroll past the hero image first.
- ArrowRightIcon's arrowhead wings shortened (looked like a generic oversized chevron at full length).
- Search overlay's "Esc" text button replaced with an X icon; the actual Escape keyboard shortcut is unchanged.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-31 22:13:08 +00:00
Marco 6f033d52b5 Add persistent account navigation (sidebar/tabs); fix CTA arrow on Android
Konto-Aktionen links (profile, wishlist, logout) used to live at the bottom of the orders page's own content — unreachable once the order list got long enough. New KontoShell + AccountNav give every /konto/* page a persistent sidebar (sm+) / tab bar (mobile) instead, always reachable regardless of list length. The order filters also collapse behind a "Filter" toggle on mobile now, since the tab bar above them left little room for 3 full-width dropdowns.

Also: replaced the Unicode "→" arrow in CTA links (Tools.tsx, Blog.tsx, ProductGrid.tsx) with an SVG icon — the glyph isn't covered by the site's custom fonts, so browsers fall back to a system font per-platform; confirmed sitting visibly low relative to the label text on Android/Chrome (Galaxy S22), not reproducible in desktop Chromium. An SVG has no font-fallback path, so it renders identically everywhere.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-31 21:58:04 +00:00
Marco 92727ff22a Keep icon touch targets full-size; shrink logo instead; fluid nav gap
Revert the 40px icon-shrink below 375px (hard to tap accurately) and reclaim that space from the logo instead (76-130px depending on width, vs the always-181px controls). Re-verified overflow-free at 320px via Playwright with all 4 icons at full 44px.

Also: wishlist heart's touch-friendly smaller/subtler size now applies to already-wishlisted (filled) hearts too, not just not-yet-wishlisted ones. The wishlist page's login redirect now returns to the wishlist instead of the generic account/orders overview. Desktop nav-link gap now scales fluidly between 1024-1300px instead of a fixed 48px that felt too wide right where nav labels have the least room.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-31 21:20:01 +00:00
Marco b6a826dc46 Replace hand-duplicated VIES/VAT-ID/PLZ/tracking logic with shared package
Deletes app/lib/vies.ts, vatId.ts, tracking.ts in favor of the newly unified @einfach-produktiv/invoicing modules — fixes the actual PLZ inconsistency (this repo already validated per-country digit counts; the backend hardcoded German-only) rather than just deduplicating code.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-31 15:41:01 +00:00
Marco 230b8ebaad Mark already-purchased items on the wishlist instead of auto-removing
A customer often wishlists something specifically to buy it again (gifts, repurchases) — silently removing it after purchase would defeat that. /konto/merkliste now shows a dimmed image + "Gekauft am [date]" badge instead, derived read-only from the customer's own orders (cancelled/returned orders excluded). Removal stays manual.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-31 13:51:10 +00:00
Marco 8a4170a1e6 Add DHL checkout integrations (autocomplete, postnummer, return label)
Wires the new backend DHL endpoints into checkout: an address-autocomplete
dropdown on the street fields, live Postnummer validation for Packstation
delivery, and a return-label download link on the order-detail page.
Proxied through Next.js API routes since DHL credentials are tenant-
specific and CheckoutContent is a Client Component.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-31 13:01:05 +00:00
Marco ee39a5d7dc Use CustomSelect for checkout's country pickers too
Promoted CustomSelect from konto/bestellungen/components/ to a shared
app/components/ location. New includeAllOption (checkout doesn't want
a "clear" pseudo-option — a country is always genuinely selected) and
fullWidth (matches the other w-full form fields, no sm: shrink) props.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-30 23:15:08 +00:00
Marco 2b2ee4d23c Gate order-list filters behind CompanySettings.orderFilterEnabled
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-30 23:12:54 +00:00
Marco fc8a1d4201 Add standalone registration page; fix 2 merkliste bugs; honor login redirect
New /konto/registrieren (+ RegisterForm.tsx) — until now an account
could only be created inline during checkout, which stopped making
sense the moment the Wishlist gave accounts a non-purchase use case.
LoginForm now also honors ?redirect= (previously ignored, always
landing on /konto/bestellungen regardless of where the login was
triggered from, e.g. WishlistButton's login-gate) and links to the
new registration page instead of implying "only via checkout".

Fixed: removing an item from the wishlist didn't disappear from
/konto/merkliste — that page was a Server Component with a fixed
render, not reactive to the client-side toggle. New MerklisteGrid.tsx
(Client Component) uses useWishlist()'s live state as the actual
source of truth for which products are still shown.

Fixed: AddToCartInlineButton looked broken on that same page —
`className="w-full"` was passed to it, but its `className` prop
*replaces* the whole default styling (`?? default`, not a merge),
throwing away all the button's actual styling. Its default is
already `w-full`; no override needed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-30 22:59:23 +00:00
Marco 11aa9689c1 Add wishlist feature (frontend), gated by CompanySettings.wishlistEnabled
New: useWishlist.ts (fetch+optimistic-toggle hook, server-backed since
a wishlist needs a logged-in customer, unlike the guest-friendly cart),
WishlistButton.tsx (heart toggle, login-redirects on 401), Navbar's
wishlist icon+badge (hidden below sm: — Account+Cart are the only
always-visible icons on true mobile, a 3rd icon there risks the same
computed nav-overflow class of bug documented in the figma-to-nextjs
skill), and /konto/merkliste (list page, 404s if the feature gets
disabled after a customer already has rows).

Product gained numericId (the raw Payload id) alongside its existing
slug id — WishlistItems.product is a real numeric relationship field,
unlike cart/checkout's slug-keyed "commerce id".

Backend counterpart (WishlistItems collection, CompanySettings toggle,
migration) already deployed separately.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-30 22:28:30 +00:00
Marco 1d91a3f21c Replace native <select> filters with a fully custom-styled dropdown
A native <select>'s open options popup is rendered by the browser/OS
and can't be styled at all — wrong font size, wrong colors, no brand
styling whatsoever, reported as looking completely off-brand. New
CustomSelect.tsx renders both the trigger and the options list as
plain styled HTML (button + role="listbox" panel, keyboard nav,
click-outside-to-close) instead.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-30 22:13:51 +00:00
Marco 475ee3fa16 Replace order-filter chip wall with 3 compact selects; subgrid-align trust badges
OrderFilters.tsx: filter chips (7 status + 5 payment-status + N year
options as pills) read as cluttered and ate a lot of vertical space.
Replaced with 3 native <select>s in a row (a small Client Component
just for the onChange→router.push navigation) — same URL-search-param
filtering underneath, just a much more compact control.

TrustRow.tsx: replaced the hardcoded min-h-[3.2rem] title reservation
(a guess, and it visibly over-reserved space for single-line titles)
with a real CSS Grid + `grid-template-rows: subgrid` — every badge's
icon+title box shares the same row-track height (auto-sized to
whichever title actually needs 2 lines), so descriptions align
without any hardcoded value. The two structurally different layouts
(icon-above-title below lg vs. icon-beside-a-title/description-stack
at lg) now render as two separate blocks instead of fighting one
shared shape.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-30 22:11:12 +00:00
Marco 2f8c184dc6 Add status/paymentStatus/year filters to the order overview
Server-side, URL-search-param-driven filtering (?status=&paymentStatus=&year=)
so the page stays a Server Component — each filter chip is a plain Link
to the same route with one param changed, shareable/bookmarkable/back-
button-safe for free. getCustomerOrders() gained an optional filters
param translated into additional Payload where[] clauses; a new
getCustomerOrderYears() derives the year filter's option list from the
customer's actual orders instead of a hardcoded range. paymentStatus's
"Offen" filter option mirrors PaymentStatusBadge's own grouping
(pending + not_applicable as one option, not two).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-30 22:03:30 +00:00
Marco e7d5dd8b1e Revert Bestellnummer/Datum centering — back to left-aligned columns 1-2
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-30 21:58:51 +00:00
Marco 79d3f836a2 Fix order-list card: explicitly pin every cell's row+column
Root cause of the "still left-packed" report: mixing `order` with only
some items explicitly positioned (col-start on Bestellnummer/Artikel)
is a footgun — Grid's auto-placement cursor for the un-pinned items
(Datum, Status, Zahlungsstatus, Gesamtbetrag) starts scanning from
column 1 again regardless of what's already explicitly placed
elsewhere, so Datum landed back in column 1 instead of column 3.
Every cell now gets an explicit row-start+col-start, removing the
ambiguity entirely.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-30 21:55:24 +00:00
Marco 113d25eef9 Order list: center Bestellnummer/Datum row + center footer links on mobile
Bestellnummer now forces sm:col-start-2 so the top row's 2 items
(only spanning 2 of 4 tracks) center within the row instead of
packing flush left with all the leftover space on the right.

The Profil/Weiter einkaufen/Abmelden links row now centers itself
(self-center) below sm, matching the rest of the page's alignment
from sm up — shrink-to-fit width preserved, doesn't stretch full width.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-30 21:51:22 +00:00
Marco 9024112d88 Order-list card: move breakpoint from 500px to sm (640px)
Consistency with the site's structural sm: breakpoint elsewhere.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-30 21:48:07 +00:00
Marco 0f50ab334c Move Artikel to the front of the 4-column row
From 500px up, the second row is now Artikel/Status/Zahlungsstatus/
Gesamtbetrag (was Status/Zahlungsstatus/Artikel/Gesamtbetrag). Done via
min-[500px]:order-* so the below-500px pairing (badges together,
quantities together) stays on plain DOM order, untouched.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-30 19:55:13 +00:00
Marco 54725f7c5f Give Bestellnummer/Datum their own row, group the other 4 fields below
From 500px up: 4 columns, with Status forced to col-start-1 so
Status/Zahlungsstatus/Artikel/Gesamtbetrag land together in one row
below Bestellnummer/Datum, instead of Grid filling the empty cells
next to Bestellnummer/Datum first.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-30 19:52:48 +00:00
Marco 7c9bfc3897 Keep the order-list card at 2 columns always, drop the 4-column breakpoint
With the badges-together/quantities-together field order, 4 columns at
wider viewports put Status/Zahlungsstatus in the same row as
Bestellnummer/Datum instead of their own row. Plain 2 columns at every
width keeps the intended pairing regardless of available space.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-30 19:51:22 +00:00
Marco 014f1edcdc Reorder order-list card fields: badges together, quantities together
Status/Zahlungsstatus now land in one row (calmer than being split
diagonally across two rows), and Artikel/Gesamtbetrag pair up as the
two "quantity" fields. Also drops the now-unnecessary col-start-1 on
Artikel — Bestellnummer/Datum/Status/Zahlungsstatus already fill all
4 tracks of row 1 exactly with this order, so Grid wraps row 2 on
its own.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-30 19:48:29 +00:00
Marco c6fe65ad99 Fix cart quantities doubling on every logout/login and a crash on /cart
Root cause 1 (count doubling): LogoutButton never cleared the local
cart, and mergeServerCartIntoLocal() (called on login) adds server
quantities into the existing local ones rather than replacing — since
CartSync mirrors local to the server continuously, local and server
already held the same quantities at logout time, so every login added
them together, doubling the count each cycle. Fix: clear the local
cart on logout, so the next login's merge starts from empty (or only
genuine guest-session additions) instead of re-adding already-synced
quantities.

Root cause 2 (page couldn't load): readCart()/getCart() only guarded
against JSON.parse syntax errors, not against the parsed value being a
non-array — once localStorage held a corrupted (inflated/malformed)
value, CartContent.tsx's cart.map() threw uncaught during render.
Fix: validate Array.isArray() after parsing, falling back to [].

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-30 17:13:54 +00:00
Marco 73b4b06eae Keep Bestellnummer/Datum side by side below 500px too
Reverts the col-span-2 full-width-row experiment for very narrow
phones — side by side reads better even with the occasional wrap.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-30 13:59:20 +00:00
Marco 53c452583d Move the order-list 2-to-4-column breakpoint from 400px to 500px
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-30 13:56:33 +00:00
Marco 80843cc08b Give Bestellnummer/Datum full-width rows on very narrow phones
Below 400px each now spans both columns of the 2-column layout (own
row), so the long order number doesn't wrap; from 400px up they drop
back to col-span-1 side by side in the 4-column layout.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-30 13:54:58 +00:00
Marco c6051442df Fix order-list mobile layout: 2 columns under 400px, badges no longer stretch
The forced 4-column layout overflowed on narrow phones — badge labels
like "In Bearbeitung" and the order number wrapped/overlapped into
neighboring columns. Drop to 2 columns below 400px (the field order
already tiles cleanly into 3 rows of 2). Also stop OrderStatusBadge/
PaymentStatusBadge from stretching to their flex-col parent's full
width — self-start keeps the badge background only as wide as its label.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-30 13:53:41 +00:00
Marco 1fe1a260e4 Keep the order-list card at 4 columns on all screen sizes
Drop the responsive grid-cols-2/4 switch entirely — stay in the
4-column layout even on the narrowest mobile widths, with a tighter
column gap below sm to help it fit.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-30 13:49:16 +00:00
Marco 5d588c55b4 Push the order-list card's 4-column breakpoint out to lg
md (768px) still counts as mobile/tablet range — keep the 2-column
layout through that whole range and only switch to 4 columns at real
desktop widths (lg, 1024px).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-30 13:47:50 +00:00
Marco 44f98a3d64 Delay the order-list card's grid breakpoint from sm to md
The card sits inside a max-w-[56rem] container with padding, so at
the sm breakpoint (640px viewport) the card itself is still too
narrow for 4 columns — the wrap to 4 columns was happening too early.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-30 13:45:24 +00:00
Marco 8ddde89143 Force Artikel to start a new grid row on the order list
sm:grid-cols-4 left 2 empty cells in row 1 after Bestellnummer/Datum
became col-span-1, so Grid auto-placement flowed Artikel into the
same row instead of wrapping.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-30 13:40:52 +00:00
Marco 84e21589c3 Fix order-list grid: Datum should start in column 2, not 3 2026-07-30 13:34:34 +00:00
Marco 9502cf81b9 Unify order-list card into a single grid for consistent column alignment
One grid for the whole card (Bestellnummer/Datum spanning 2 of 4 tracks
from sm: up, everything else spanning 1) instead of two separate flex/grid
groups. Plain flex-wrap sized every field to its own content width, so a
field could start at a different x position from one order to the next
depending on how wide that particular badge label happened to be — a
grid's column tracks are fixed by the container width, identical on every
card regardless of content, which is what actually guarantees consistent
alignment across different orders.
2026-07-30 13:29:19 +00:00
Marco 2e537bc78d Restrict Rücksendung anfragen to delivered orders, fix Impressum email link
- customerOrderAction() only offers "Rücksendung anfragen" once an order
  is delivered, not already at shipped — a return before the package
  arrived doesn't make sense yet. UI-only change (a stricter subset of
  what the backend's CUSTOMER_ALLOWED_TRANSITIONS already permits).
- AnbieterAngaben.tsx's seller email is now a real mailto: link — it was
  plain text, the only non-clickable email on the site.
2026-07-30 10:39:08 +00:00
Marco 4adb844f8d Move Zahlungsart-ändern button next to Bestellung-stornieren 2026-07-30 10:12:11 +00:00
Marco beea592706 Reflect Orders.paymentStatus's new meaning for Überweisung orders
- switch-to-stripe eligibility gained paymentStatus !== "paid" — an
  order an admin already marked paid by hand must never also be
  switchable to Stripe.
- PaymentStatusBadge: "Offen" now renders in the same red/subtle
  style as a failed status (was neutral grey) — worth visually flagging,
  now that it's a real tracked state rather than a permanent placeholder.
2026-07-30 10:08:37 +00:00
Marco 6448c8736a 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.
2026-07-30 09:56:06 +00:00
Marco 0e995884a7 Allow switching an unpaid Überweisung order to Stripe payment
New "Zahlungsart ändern" button on the account order detail page,
shown for a still-'received', still-manual (Überweisung) order when an
active Stripe payment method exists. Reuses PaymentStep (the same Stripe
Payment Element checkout uses) and /checkout/verarbeitung's polling logic
(both now take a returnContext prop/param to land back on the order page
instead of clearing the cart and redirecting to /bestellbestaetigung).

Also:
- Payment status badge (Offen/Bezahlt/...) next to the existing Zahlungsart
  display on the order detail page.
- A one-line mention of the switch option in the Vorkasse unpaid notice
  in the order-confirmation email, shown only when a Stripe option is
  actually active (hasOnlinePaymentOption).
- CustomerOrderDetail gained paymentProvider (was missing from the type
  entirely, even though the field already existed on the order).

Backend counterpart: docker/payload's switchPaymentToStripeEndpoint.
2026-07-30 09:48:44 +00:00
Marco 8c843c0ac1 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.
2026-07-30 08:48:04 +00:00
Marco e3352d7e32 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.
2026-07-30 08:41:39 +00:00
Marco bae23775f2 Hide failed-payment order attempts from the customer's own order history
A cancelled order with no invoiceNumber is a Stripe payment that never
succeeded (failed or timed out before ever reaching received/invoiced),
not a real cancellation of something that actually happened — from the
customer's point of view it was never really an order. Filtered out of
getCustomerOrders/getCustomerOrderDetail by default; the row stays in
Payload for admin/audit purposes (shown there as "Zahlung
fehlgeschlagen", see backend).

getCustomerOrderDetail's filter is opt-in via a new optional parameter,
not the default — /api/checkout/status/route.ts's post-payment polling
needs to keep seeing exactly this order to show the "Zahlung
fehlgeschlagen, bitte erneut versuchen" retry state. The GDPR export
route also opts out for the same reason a legal completeness export
can't silently drop rows.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-25 16:05:26 +00:00
Marco 797d9d42fe Custom post content blocks (images/gallery/video/quote) + backend-driven SEO settings
RichText.tsx switched to Payload's official React renderer + custom
JSXConverters (same call signature, LiveRichText/LivePostContent
untouched) — needed to render the new Lexical Blocks the Payload repo's
Posts.content just gained. Converters follow the existing CMS-image
convention (relative + aspect-[...] + fill + object-cover); the video
block resolves YouTube/Vimeo links to an iframe embed.

New getSeoSettings() fetcher (same pattern as getKleinunternehmer()),
app/layout.tsx now generateMetadata() reading it with the same fallback
values it used to hardcode. Per-post SEO overrides (seoTitle/
seoDescription/seoImage) wired into the blog detail page's metadata,
falling back to title/excerpt/thumbnail when empty.

Also fixed while auditing every page's metadata: missing descriptions on
3 konto pages, a static title on the dynamic order-detail route, and
missing OG images on /shop and /blog.
2026-07-24 21:10:33 +00:00
Marco f0aec851d3 Fix checkout login-prompt placement and unify profile country select
Login prompt (email already has an account) now renders inline under
Card 1's own email field instead of a separate block above the whole
form — no scrolling needed in the common case, and no more re-typing
the email into a second field. The submit-time fallback still scrolls
it into view via a useEffect, now that the target is conditionally
rendered.

/konto/profil's "Land" select was still hardcoded to Deutschland/
Österreich/Schweiz independently of /checkout's own Payload-configurable
shipping-countries list.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-24 17:44:04 +00:00
Marco ba830947d2 Add Kleinunternehmerregelung (§19 UStG) support end-to-end
Checkout forces 0% VAT without de-grossing prices when the tenant is a
Kleinunternehmer (a business decision, not just an engineering default —
unlike the existing intra-community VAT exemption, which does de-gross).
Snapshotted onto the order at checkout time so a later toggle of the
company-settings checkbox never rewrites an already-issued invoice's tax
treatment — same reasoning as the existing vatExempt field.

Threaded through: checkout route, order creation/confirmation email,
on-demand invoice/Storno/Gutschrift downloads, the Bestellbestätigung
page, and the account order-detail page. The four storefront "inkl. X%
MwSt." price hints (shop grid, cart upsell, ToDo-Karten landing page,
homepage spotlight) drop that clause live when the setting is on. The
company-settings Live Preview reflects the checkbox in real time too.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-24 15:08:13 +00:00
Marco c60e936b7e Show company name + VAT ID on the account order-detail page; pick up Netto-row invoicing update
Order detail (/konto/bestellungen/[orderNumber]) already carried
companyName/vatId/vatExempt on CustomerOrderDetail but never rendered
them — a B2B customer looking at their own order couldn't see the
company/VAT info that's already on their invoice. Now shown in the
billing-address block, with a note when the order was VAT-exempt.

Also bumps @einfach-produktiv/invoicing to pick up the Netto-row fix
(now shown on every invoice, not just non-exempt ones).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 20:59:23 +00:00
Marco e48107470a Add optional Firma/USt-IdNr. fields to checkout and profile
B2B checkout fields, split out from the e-invoicing migration and
picked back up now that it's shipped. Both fields are independently
optional, format-validated (shared regex in lib/vatId.ts, mirrored
server-side in api/checkout and api/account/profile), persisted in
the checkout draft, and saved as a customer profile default that
pre-fills future checkouts. Order/customer snapshot fields land in a
companion Payload backend commit.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 17:53:21 +00:00
Marco 97833ab2bb Remove product thumbnails from the order list, keep them on the detail page
Bestellübersicht rows now show only text (Bestellnummer, Datum, Artikel-Anzahl, Status, Gesamtbetrag) — no product images. The order detail page (app/konto/bestellungen/[orderNumber]/page.tsx) is untouched and still shows a thumbnail per item, which is the only place they should appear.
2026-07-23 14:46:44 +00:00
Marco 66ac184a6f Move invoice/tax-breakdown PDF generation into @einfach-produktiv/invoicing
Phase 0 of the e-invoicing migration plan (see the E-Rechnung planning
session) — moves invoicePdf.tsx, correctionInvoicePdf.tsx, and
taxBreakdown.ts into a new shared package, consumed as a git dependency
by both this repo and the payload backend, instead of hand-duplicating
the correction-invoice logic between them (see that package's own README
for the three real drifts the duplication had already caused).

Consumed as raw TS/TSX source via next.config.ts's transpilePackages, not
a pre-built package. Needs `git` in the Docker deps stage and a
project .npmrc (allow-git=root) to let npm ci fetch a git-URL dependency
at all — npm 12+ disables that by default.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 09:18:20 +00:00
Marco 43944d8cc8 Fix navbar/discount/invoice bugs from manual QA, add VAT breakdown, shipping-address override, checkout persistence, redesigned mobile menu
Bug fixes:
- Navbar login/logout state now updates immediately (custom ep-auth-changed
  event) instead of requiring a hard reload
- Status-change email links were broken by an un-encoded "#" in the order
  number; fixed for all 4 status emails
- Cart discount code: manual input field restored (was removed entirely)
- Quote-label underline now scales with the label's actual text width
- Number Ranges admin list now shows the invoice prefix/counter columns

Pricing & VAT:
- Prices show the real per-product VAT rate ("inkl. X% MwSt.") instead of
  a generic disclosure
- Cart/checkout/confirmation totals show the actual € amount of VAT
  included, broken down per rate when a cart spans more than one
  (new lib/taxBreakdown.ts, shared with the invoice PDF's own math)
- Account order pages gained product thumbnails and the same VAT breakdown

Low-stock warning: a "Nur noch wenige verfügbar" badge/hint across the
shop grid, spotlight, and add-to-cart variant pickers, driven by the
existing lowStockThreshold field (still never exposes raw stock counts).

Invoice PDFs: product thumbnails on every line item, a plain "Netto"
label (rate was redundant, already stated on the MwSt. line below), no
more duplicate USt-IdNr. in the header, and — for a Stornorechnung
specifically — an explicit "Versand" line that was previously only
folded silently into the tax totals.

Checkout:
- Optional deviating shipping address (separate from the billing address
  used for the invoice), with its own toggle + address form
- Full checkout draft persistence (name/address/shipping/payment
  selections) survives navigating away and back, via localStorage
- Invoice PDF shows a third "Lieferadresse" block when the shipping
  address differs from billing

Mobile navigation: fullscreen panel with a circular reveal animation from
the hamburger's corner, replacing the old in-flow accordion drawer; no
login CTA inside it (redundant with the always-visible header icon).

Admin-facing (Payload backend, mirrored where the frontend has a ported
copy of the same renderer): dashboard rebuilt as individual cards, split
into 3 task queues (received/processing/returns) instead of 2, revenue
and order counts now exclude cancelled/returned orders immediately, and
the low-stock alert links to the specific affected product(s) instead of
the unfiltered list. A new immediate email notifies the shop owner the
moment an order comes in, instead of only via the daily digest.

Testimonials admin list now groups by page instead of interleaving all
three grids' entries. ~45 English admin field descriptions translated to
German for consistency.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-22 22:52:15 +00:00