Commit Graph

20 Commits

Author SHA1 Message Date
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
Marco c5500bcc97 Wire up product variants end-to-end, add tracking-number display
Completes the frontend half of the Payload backend's variant/inventory/
tracking work (see that repo's own commit):

- **Cart**: CartItem gained an optional `variant?: string` field — every
  function that used to match a line by `id` alone (addToCart/
  removeFromCart/setQuantity) now matches by `(id, variant)` together via
  a shared sameLine() helper, so two lines for the same product with
  different variants stay separate entries. `variant` undefined on both
  sides (the no-variants case) still matches by simple equality, so every
  pre-existing call site keeps working unchanged.
- **Selection UI**: AddToCartInlineButton renders a <select> above the
  button when given a non-empty `variants` prop (ProductGrid/
  RelatedProducts pass product.variants straight through); defaults to
  the first variant.
- **Pricing**: cartTotals.ts's new effectivePrice(entry, product) — a
  variant's priceOverride wins over the base product price. Every cart/
  checkout/order-confirmation total and per-line price display now goes
  through this instead of reading product.price directly (fixes both a
  wrong-price bug and a duplicate-React-key bug the old `key={product.id}`
  pattern would have had the moment two variants of one product were both
  in the cart).
- **Checkout**: re-validates the requested variant server-side (same
  "never trust the client" reasoning as price re-derivation) — a variant
  name that doesn't exist on that product fails the whole checkout.
  variantName snapshots onto orders.items, shown as a parenthetical next
  to the product name on the confirmation email, both invoice PDF types,
  and the order-detail page.
- **Cross-device cart**: Customers.cart[].variantName (synced via
  /api/account/cart) carries the selection through a login/logout cycle,
  not just the current session.

Also adds tracking-number display: /konto/bestellungen/[orderNumber]
shows a clickable link when orders.trackingNumber is set, built by a new
app/lib/tracking.ts that mirrors the Payload backend's own copy
byte-for-byte close (same carrier set/URL patterns) so what a customer
sees here matches exactly what the order-shipped email already links to.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-22 17:43:47 +00:00
Marco 91f6fef6ea Support partial returns — per-item quantity, item-only Gutschrift (no shipping refund, no discount reproration)
Customers can now select which items and how many units to return
instead of only the whole order. The Gutschrift reflects only the
returned quantities, excludes shipping (already delivered), and leaves
the original discount untouched — confirmed policy, not an engineering
default. Stornorechnung (pre-shipping cancellation) is unaffected and
stays a full reversal including shipping.
2026-07-22 11:30:46 +00:00
Marco 179b59d73d Redesign invoice PDFs, add correction-invoice downloads, return reasons, and per-product tax/bundle support
Invoice + Stornorechnung/Gutschrift PDFs get a modern header-band layout,
a "bereits beglichen" badge for immediately-paid orders, labelled bank
details, and a per-tax-rate summary breakdown. Correction invoices can
now be re-downloaded from the account (regenerated deterministically,
not stored as files, same approach as the original invoice). Return
requests capture a reason. Products can define bundles (bundleItems) and
a per-product VAT rate override, both snapshotted onto order items.
2026-07-22 10:31:22 +00:00
Marco 5232b14cdf Generate invoice PDFs attached to order confirmation, and send emails on order status changes
Invoice PDFs (§14 UStG line items, tenant-configurable VAT rate) are now
generated at checkout and attached to the confirmation email, plus
available on demand from the order-detail page. Payload-side, orders now
also email the customer on shipped/cancelled/return_requested/returned,
with Stornorechnung/Gutschrift correction PDFs attached for the latter two
so the original invoice's immutable number stays honest.
2026-07-22 09:49:56 +00:00
Marco f0df359db4 Add password reset, order confirmation email with editable templates, and fix missing account entry points
Password reset uses Payload's built-in forgot/reset-password flow,
customized to link to this app instead of the Payload admin. Order
confirmation email and the password-reset email's wording both come from
a new Payload email-templates collection, editable without a deploy and
previewable via Live Preview at /email-preview/[type] (same mechanism as
Posts/LegalPages/Testimonials, sample data instead of a real document).

Also: order numbers get a random suffix (prevents guessing, motivated by
a considered-and-deferred guest order-lookup feature); the discount code
field only shows in the cart when a code is actually active (codes now
apply via a ?code= link instead of manual entry); and three navigation
gaps found while testing — no reachable login link with an empty cart, no
logout link anywhere, no way back from profile to order history.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-22 08:14:08 +00:00
Marco df05ea5358 Add rate limiting, sliding sessions, email verification, GDPR self-service, order cancellation/returns, and critical-error alerting
Complements Payload's per-account login lockout with per-IP rate limiting
on auth routes; proxy.ts silently refreshes an active customer's session
via Payload's built-in refresh-token endpoint instead of a long-lived
token. Registration now sends a non-blocking email-verification link
(doesn't gate login, since checkout registers and immediately logs in
mid-purchase). /konto/profil gets GDPR export/delete; order detail pages
get self-service cancel/return-request, backed by a Payload hook that
closes a real gap (a customer's JWT could previously PATCH any field of
their own order, not just status). Checkout failures now email an alert
independent of Payload's own health, since Kuma's uptime checks can't see
an order silently failing to persist.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-22 07:28:01 +00:00
Marco 7f37f111e8 Add real order persistence, customer accounts, and cart sync
Checkout now persists orders server-side (Payload orders collection,
re-priced from live product data, discount codes redeemed exactly once)
instead of writing a client-only sessionStorage snapshot. Buying requires
an account (registration inline in checkout, no separate step) — accounts
get order history with delivery status, profile/address editing, password
change, and a cart that syncs across devices while logged in.

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