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.
This commit is contained in:
@@ -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 `<PDFViewer>`** (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 `<iframe>`, re-rendering whenever its props change.
|
||||
Paired with `useLivePreview()`'s live `data` (same postMessage mechanism
|
||||
as the email templates preview), editing a field in the admin re-renders
|
||||
the actual PDF in real time — no server round-trip per keystroke.
|
||||
Dynamically imported with `{ ssr: false }` (`next/dynamic`) since it
|
||||
touches the DOM directly; the HTML-string email previews elsewhere don't
|
||||
need that since they're just `dangerouslySetInnerHTML`.
|
||||
- Renders `InvoiceDocument` (exported from `app/lib/invoicePdf.tsx`
|
||||
specifically for this — everywhere else only the async
|
||||
`renderInvoicePdf()` buffer-generator is used) against a fixed
|
||||
`SAMPLE_INVOICE_ORDER` — same "no real document to preview against
|
||||
generically" reasoning as `email-templates`' own `SAMPLE_ORDER`.
|
||||
- No draft/published distinction here, unlike `email-templates`:
|
||||
`company-settings` has no content-versioning concept, it's just the
|
||||
current row — the page's initial (pre-postMessage) fetch is the same
|
||||
live data `getCompanySettings()` always returns.
|
||||
|
||||
## Orders & customer accounts
|
||||
|
||||
An account is required to buy — there is no guest checkout. Registration
|
||||
@@ -600,7 +640,7 @@ mechanism as Posts/LegalPages/Testimonials (`useLivePreview()` from
|
||||
`<table>`s, no flexbox) — just matched by eye.
|
||||
- **Footer company line is configurable, not hardcoded.** `emailShell()`
|
||||
takes a required `companyLine` parameter ("<sellerName> · <sellerEmail>")
|
||||
— `orderEmail.ts` fetches `invoice-settings` once (`getSellerForInvoice()`)
|
||||
— `orderEmail.ts` fetches `company-settings` once (`getSellerForInvoice()`)
|
||||
and derives it from there, same admin-editable business data the invoice
|
||||
PDFs already use, rather than a literal `"einfach produktiv ·
|
||||
admin@mk360.de"` string. The Payload-side sends (password-reset, the 4
|
||||
|
||||
Reference in New Issue
Block a user