From e50d43ea444a6a9f6bd3457d102fd65d426f333f Mon Sep 17 00:00:00 2001 From: Marco Date: Wed, 22 Jul 2026 11:11:58 +0000 Subject: [PATCH] Rename invoice-settings to company-settings, add its own Live Preview, and refine invoice PDF layout Company data now has its own Payload admin group and a live in-browser PDF preview (react-pdf's PDFViewer) instead of just a plain settings form. Invoice header is a brand-colored rule instead of a filled band, and the footer is now pinned to the page bottom instead of following content flow. --- README.md | 104 ++++++++++++------ app/api/checkout/route.ts | 6 +- .../LiveCompanySettingsPreviewClient.tsx | 37 +++++++ app/company-settings-preview/page.tsx | 34 ++++++ app/lib/correctionInvoicePdf.tsx | 41 +++++-- app/lib/invoiceData.ts | 20 ++-- app/lib/invoicePdf.tsx | 80 ++++++++++++-- app/lib/payload.ts | 14 +-- 8 files changed, 261 insertions(+), 75 deletions(-) create mode 100644 app/company-settings-preview/components/LiveCompanySettingsPreviewClient.tsx create mode 100644 app/company-settings-preview/page.tsx diff --git a/README.md b/README.md index 7316dbe..d18cb4f 100644 --- a/README.md +++ b/README.md @@ -43,8 +43,8 @@ Preview components should ever point somewhere else). `DISCOUNT_SERVICE_SECRET` must match the value set on the Payload backend). `ORDER_SERVICE_SECRET` (no safe default — required for `/api/checkout` to persist an order in Payload at all, for `/api/account/verify-email` to look up a customer -by their verification token, and for `getInvoiceSettings()` to read the -`invoice-settings` collection (seller data for invoice PDFs); must match +by their verification token, and for `getCompanySettings()` to read the +`company-settings` collection (seller data for invoice PDFs); must match the value set on the Payload backend — also used there for the same header). `SMTP_USER`/`SMTP_PASSWORD` (no safe default — required for `app/lib/alertAdmin.ts`'s critical-failure @@ -92,7 +92,7 @@ API directly and reuse Next.js's fetch cache instead of an extra round trip. Most reads are public (`access.read: () => true`); writes are always admin-gated in the Payload admin UI at `payload.mk360.de/admin`. A growing subset below is **not** public-read at all (`discount-codes`, `orders`, -`customers`, `number-ranges`, `invoice-settings`) — each row says so and +`customers`, `number-ranges`, `company-settings`) — each row says so and explains what does have access instead. | Collection (slug) | Used for | Key fields | @@ -114,19 +114,20 @@ explains what does have access instead. | `customers` | Storefront accounts — register/login/order-history, a second `auth: true` collection separate from the Payload admin's own `users` login | `customerNumber`, `firstName`/`lastName`/`email`, one default address, `cart` (server-side mirror), `emailVerified` (non-blocking). **Not public-read** — see "Orders & customer accounts" below | | `number-ranges` | Admin-configurable prefix + running counter for customer/order/invoice numbers — one row per tenant | `customerPrefix`/`customerNext`/`customerPadding`, `orderPrefix`/`orderNext`/`orderPadding`, `invoicePrefix`/`invoiceNext`/`invoicePadding`. **Admin-only**, no frontend read at all — internal to the two `beforeChange` hooks that assign these numbers | | `email-templates` | Editable subject/heading/body/footer for all 6 transactional emails this shop sends (see "Email templates & Live Preview" and "Status-change emails" below) | `type` (`order-confirmation`/`password-reset`/`order-shipped`/`order-cancelled`/`order-return-requested`/`order-returned`), `subject`, `heading`, `bodyText`, `footerText`. Public-read, has a Live Preview button | -| `invoice-settings` | Structured seller data for invoice PDFs (see "Invoice PDFs" below) — one row per tenant | `sellerName`/`sellerStreet`/`sellerZip`/`sellerCity`/`sellerCountry`/`sellerEmail`, `vatId`, `taxRatePercent` (admin-editable, not hardcoded), `bankDetails`. **Not public-read** — admin or `ORDER_SERVICE_SECRET` | +| `company-settings` | Structured business data for invoice PDFs *and* every email footer's company line (see "Invoice PDFs" below) — one row per tenant, own **Company** admin group (not Commerce — this is business identity, not a storefront concern) | `sellerName`/`sellerStreet`/`sellerZip`/`sellerCity`/`sellerCountry`/`sellerEmail`, `vatId`, `taxRatePercent` (admin-editable, not hardcoded), `bankDetails`. **Not public-read** — admin or `ORDER_SERVICE_SECRET`. Has a Live Preview button — see "Company Settings & Live Preview" below | -All of the above (except `media`, `users`, `tenants`) are grouped in the -Payload admin sidebar under **Commerce** (`products`, `discount-codes`, -`orders`, `customers`, `number-ranges`, `email-templates`, -`invoice-settings`, `shipping-methods`, `shipping-settings`, +All of the above except `company-settings`, `media`, `users`, `tenants` +are grouped in the Payload admin sidebar under **Commerce** (`products`, +`discount-codes`, `orders`, `customers`, `number-ranges`, +`email-templates`, `shipping-methods`, `shipping-settings`, `payment-methods`, `trust-badges`, `cart-trust-badges`) or **Content** -(`posts`, `categories`, `legal-pages`, `werkzeuge-cards`, `testimonials`); -`media`/`users`/`tenants` sit under **Platform** — `users` and `tenants` -are hidden from non-super-admins' nav entirely, and every tenant-scoped -collection's own "assigned tenant" field is hidden from non-super-admins -in the edit view (though not yet the list view's column — see the infra -README's Payload CMS section for why that one's a harder fix). +(`posts`, `categories`, `legal-pages`, `werkzeuge-cards`, `testimonials`). +`company-settings` sits in its own **Company** group; `media`/`users`/`tenants` +sit under **Platform** — `users` and `tenants` are hidden from +non-super-admins' nav entirely, and every tenant-scoped collection's own +"assigned tenant" field is hidden from non-super-admins in the edit view +(though not yet the list view's column — see the infra README's Payload +CMS section for why that one's a harder fix). **What an admin can actually configure without a code deploy, at a glance:** product/shipping/payment catalog data and `active` toggles @@ -135,7 +136,7 @@ discount codes, all page content (blog/legal/testimonials/trust badges), delivery-time disclosure (`shipping-settings`), order/customer/invoice numbering schemes (`number-ranges`), all 6 email wordings (`email-templates`, with Live Preview), and invoice seller data, -bank details, and VAT rate (`invoice-settings` — also what every email's +bank details, and VAT rate (`company-settings` — also what every email's footer company line is sourced from). What still requires a code change: adding a new *field* to any collection (needs a migration), payment processing itself (not built), and anything structural in @@ -150,7 +151,10 @@ which does need a deploy of the Payload service. `posts`, `legal-pages`, and `testimonials` support Payload's Live Preview — opening a document in the Payload admin shows this app's real rendered page -in an iframe, updating as you type, no save required. +in an iframe, updating as you type, no save required. `email-templates` +also has Live Preview, but against a synthetic page + sample data rather +than one of these three's own real page — different enough to cover +separately, see "Email templates & Live Preview" further below. - **`app/api/preview/route.ts`** — validates `PAYLOAD_PREVIEW_SECRET` (matching value required on the Payload side too, or this route 401s) and @@ -272,10 +276,10 @@ Both resolved server-side in `/api/checkout/route.ts`, at the same point prices are already being re-derived from live Payload data (never trusted from the client): -- **Tax rate**: `product.taxRatePercent ?? invoiceSettings.taxRatePercent` +- **Tax rate**: `product.taxRatePercent ?? companySettings.taxRatePercent` — a product's own override if set, otherwise the tenant-wide default - from `invoice-settings` (fetched alongside the product catalog, - `Promise.all([fetchProductsBySlug(), getInvoiceSettings()])`). Snapshotted + from `company-settings` (fetched alongside the product catalog, + `Promise.all([fetchProductsBySlug(), getCompanySettings()])`). Snapshotted onto `orders.items[].taxRatePercent` at order creation — see the Payload README's "Per-product tax rates" section for why this has to be a snapshot, not a live lookup. @@ -305,13 +309,16 @@ inbox, not only in `/konto/bestellungen`. failure there would be one more way to silently lose the attachment for no real design benefit; brand color/spacing still carries the visual identity via `StyleSheet`. -- **Layout**: a brand-tinted header band (wordmark + "RECHNUNG" label, - not a plain title line), seller/buyer addresses, invoice - number/date/order-reference/USt-IdNr. shown as small bordered "meta - boxes" rather than a plain text row, a rounded/bordered item table with - alternating row shading, and a shaded summary card for the totals — - deliberately closer to the site's own card-based UI language than a - generic invoice template. +- **Layout**: a header separated by a bold brand-colored rule (not a + filled color band — a plain line reads cleaner than a solid block of + color across the top) with the wordmark + "RECHNUNG" label, seller/buyer + addresses, invoice number/date/order-reference/USt-IdNr. shown as small + bordered "meta boxes" rather than a plain text row, a rounded/bordered + item table with alternating row shading, and a shaded summary card for + the totals — deliberately closer to the site's own card-based UI + language than a generic invoice template. The footer is pinned to the + bottom of the page (`position: absolute` + react-pdf's `fixed` prop), + not just wherever the content flow happens to end. - **"Bereits beglichen" badge**: shown next to the meta boxes whenever `order.paymentMethodTitle` is anything other than `"Überweisung"` (bank transfer) — Kreditkarte and PayPal both settle at checkout, so the @@ -319,11 +326,11 @@ inbox, not only in `/konto/bestellungen`. "Überweisung" is the one method named explicitly as the exception, rather than hardcoding a list of "immediate" titles that would need updating every time a new payment method is added in Payload). -- **Bank details**: `invoice-settings.bankDetails`, when set, prints in +- **Bank details**: `company-settings.bankDetails`, when set, prints in the footer as "Bankverbindung (für Überweisung): …" — for the case a customer paid (or still needs to pay) by bank transfer and needs the account details to do so. Currently seeded with a placeholder IBAN/BIC, - same caveat as the rest of `invoice-settings`' seller data below. + same caveat as the rest of `company-settings`' seller data below. - **Per-tax-rate summary**: line items are grouped by their own snapshotted `taxRatePercent` (see the Payload README's "Per-product tax rates" section) and the summary prints one "Netto (X%)" / "zzgl. X% @@ -338,7 +345,7 @@ inbox, not only in `/konto/bestellungen`. the Payload README's "Product bundles" section). - **`app/lib/invoiceData.ts`** — `generateInvoicePdf(order, seller)` / `generateCorrectionInvoicePdf(kind, order, seller)`, the render - entrypoints every caller below goes through. `seller` (`invoice-settings` + entrypoints every caller below goes through. `seller` (`company-settings` data) is passed in rather than fetched inside these functions, so a caller that also needs it for something else in the same request (e.g. `orderEmail.ts`'s email footer, see "Configurable company data" below) @@ -372,9 +379,9 @@ inbox, not only in `/konto/bestellungen`. disk/S3/Media — the underlying data is already durable in Postgres, and deterministic regeneration needs no cleanup or storage cost, same reasoning already applied to the original invoice. -- **`invoice-settings`** (Payload collection, structured seller data — +- **`company-settings`** (Payload collection, structured seller data — name/address/`vatId`/`taxRatePercent`/`bankDetails`) is fetched via - `getInvoiceSettings()`/`getSellerForInvoice()`, authenticated the same + `getCompanySettings()`/`getSellerForInvoice()`, authenticated the same way as order creation (`x-order-service-secret` header, `ORDER_SERVICE_SECRET`) since it's not public-read (holds bank details) but does need to be reachable from this app's own server-side code, not @@ -391,6 +398,39 @@ inbox, not only in `/konto/bestellungen`. amount per rate, gross total — all on the PDF, not just the summary the confirmation email's HTML already shows. +### Company Settings & Live Preview + +`company-settings` has a Live Preview button too, like `email-templates` +— but instead of an HTML page, it's a **live, in-browser rendered PDF**: +opening the document in the Payload admin shows the actual invoice layout +updating as the admin edits `sellerName`/address/`taxRatePercent`/ +`bankDetails`, no save required. + +- **`app/company-settings-preview/page.tsx`** + **`components/LiveCompanySettingsPreviewClient.tsx`** + — same entrypoint pattern as `/email-preview/[type]` (Draft Mode via + `app/api/preview/route.ts`, never a real visitor destination), but no + `[type]` segment — there's only one kind of document here, unlike the 6 + email types. +- **`@react-pdf/renderer`'s ``** (not `renderToBuffer()`) is + what makes this a *live* preview rather than a static download — it's a + browser-only component that renders a `Document` straight into an + embedded PDF viewer `