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:
+68
-12
@@ -14,7 +14,6 @@ import { formatDate } from "./format";
|
||||
// (src/lib/correctionInvoicePdf.tsx in the backend repo, kept visually in
|
||||
// sync with this file by eye — not shared code, two separate deployments).
|
||||
const BRAND = "#f6a701";
|
||||
const BRAND_TINT = "#fdf1d9";
|
||||
const TEXT_MUTED = "#6b6b69";
|
||||
const BORDER = "#e5e0d8";
|
||||
const BG_MUTED = "#f8f5f1";
|
||||
@@ -23,10 +22,21 @@ const SUCCESS_TINT = "#e7f5eb";
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
page: { padding: 0, fontSize: 10, fontFamily: "Helvetica", color: "#1a1a18" },
|
||||
headerBand: { backgroundColor: BRAND_TINT, padding: 32, flexDirection: "row", justifyContent: "space-between", alignItems: "center" },
|
||||
// A rule, not a filled band — a bold brand-colored line with a thin
|
||||
// muted second line underneath, rather than a plain 1pt gray divider.
|
||||
headerBand: {
|
||||
padding: 32,
|
||||
paddingBottom: 24,
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
borderBottomWidth: 3,
|
||||
borderBottomColor: BRAND,
|
||||
},
|
||||
headerRuleThin: { height: 1, backgroundColor: BORDER, marginHorizontal: 32 },
|
||||
wordmark: { fontFamily: "Helvetica-Bold", fontSize: 14 },
|
||||
kindLabel: { fontFamily: "Helvetica-Bold", fontSize: 22, color: BRAND, letterSpacing: 1 },
|
||||
body: { padding: 32 },
|
||||
body: { padding: 32, paddingBottom: 90 },
|
||||
addressRow: { flexDirection: "row", justifyContent: "space-between", marginBottom: 24 },
|
||||
addressBlock: { width: "45%" },
|
||||
addressLabel: { fontSize: 8, color: TEXT_MUTED, marginBottom: 4, textTransform: "uppercase" },
|
||||
@@ -55,7 +65,20 @@ const styles = StyleSheet.create({
|
||||
grandTotalRow: { flexDirection: "row", justifyContent: "space-between", paddingTop: 8, marginTop: 6, borderTopWidth: 1, borderTopColor: BORDER },
|
||||
grandTotalLabel: { fontSize: 12, fontFamily: "Helvetica-Bold" },
|
||||
grandTotalValue: { fontSize: 12, fontFamily: "Helvetica-Bold" },
|
||||
footer: { borderTopWidth: 1, borderTopColor: BORDER, paddingTop: 12, fontSize: 8, color: TEXT_MUTED },
|
||||
// `fixed` (below, on the element) + absolute positioning — always pinned
|
||||
// to the bottom of the page regardless of how much content is above it,
|
||||
// rather than just following wherever the content flow happens to end.
|
||||
footer: {
|
||||
position: "absolute",
|
||||
bottom: 32,
|
||||
left: 32,
|
||||
right: 32,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: BORDER,
|
||||
paddingTop: 12,
|
||||
fontSize: 8,
|
||||
color: TEXT_MUTED,
|
||||
},
|
||||
});
|
||||
|
||||
function formatPrice(amount: number): string {
|
||||
@@ -98,6 +121,32 @@ export type InvoiceSeller = {
|
||||
bankDetails?: string | null;
|
||||
};
|
||||
|
||||
// Used only by /company-settings-preview's Live Preview — a fixed sample
|
||||
// order so the admin sees a realistic-looking invoice while editing
|
||||
// company-settings fields, without depending on any real order existing.
|
||||
export const SAMPLE_INVOICE_ORDER: InvoiceOrder = {
|
||||
orderNumber: "#EP-0001-A7K2",
|
||||
invoiceNumber: "RE-0001",
|
||||
invoiceIssuedAt: new Date().toISOString(),
|
||||
customerFirstName: "Max",
|
||||
customerLastName: "Mustermann",
|
||||
deliveryMethod: "address",
|
||||
street: "Musterweg 5",
|
||||
zip: "10115",
|
||||
city: "Berlin",
|
||||
country: "Deutschland",
|
||||
paymentMethodTitle: "Kreditkarte",
|
||||
items: [
|
||||
{ productName: "ToDo-Karten – Set", quantity: 1, unitPrice: 12.9, taxRatePercent: 19, bundleContents: null },
|
||||
{ productName: "Wochenplaner – Überblick", quantity: 2, unitPrice: 14.9, taxRatePercent: 19, bundleContents: null },
|
||||
],
|
||||
subtotal: 42.7,
|
||||
shippingCost: 0,
|
||||
discountAmount: 5,
|
||||
discountCode: "WILLKOMMEN10",
|
||||
total: 37.7,
|
||||
};
|
||||
|
||||
// "Überweisung" (bank transfer) is the only payment method on this shop
|
||||
// that ISN'T settled immediately — Kreditkarte/PayPal both capture at
|
||||
// checkout. Rather than hardcode a list of "immediate" method titles
|
||||
@@ -131,7 +180,13 @@ function groupByTaxRate(order: InvoiceOrder, defaultRate: number): { rate: numbe
|
||||
.sort((a, b) => b.rate - a.rate);
|
||||
}
|
||||
|
||||
function InvoiceDocument({ order, seller }: { order: InvoiceOrder; seller: InvoiceSeller }) {
|
||||
// Exported (not just used internally by renderInvoicePdf below) so
|
||||
// /company-settings-preview's client component can mount it directly with
|
||||
// @react-pdf/renderer's browser-side <PDFViewer> — a live, in-browser
|
||||
// rendered PDF that re-renders as the admin edits company-settings fields
|
||||
// via Payload's postMessage-based useLivePreview(), no server round-trip
|
||||
// needed for each keystroke the way an HTML preview would.
|
||||
export function InvoiceDocument({ order, seller }: { order: InvoiceOrder; seller: InvoiceSeller }) {
|
||||
const rateGroups = groupByTaxRate(order, seller.taxRatePercent);
|
||||
const paid = isPaidImmediately(order.paymentMethodTitle);
|
||||
const deliveryLine =
|
||||
@@ -244,13 +299,14 @@ function InvoiceDocument({ order, seller }: { order: InvoiceOrder; seller: Invoi
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.footer}>
|
||||
<Text>
|
||||
{seller.sellerName} · {seller.sellerStreet}, {seller.sellerZip} {seller.sellerCity} · {seller.sellerEmail} · USt-IdNr.{" "}
|
||||
{seller.vatId}
|
||||
</Text>
|
||||
{seller.bankDetails ? <Text style={{ marginTop: 4 }}>Bankverbindung (für Überweisung): {seller.bankDetails}</Text> : null}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.footer} fixed>
|
||||
<Text>
|
||||
{seller.sellerName} · {seller.sellerStreet}, {seller.sellerZip} {seller.sellerCity} · {seller.sellerEmail} · USt-IdNr.{" "}
|
||||
{seller.vatId}
|
||||
</Text>
|
||||
{seller.bankDetails ? <Text style={{ marginTop: 4 }}>Bankverbindung (für Überweisung): {seller.bankDetails}</Text> : null}
|
||||
</View>
|
||||
</Page>
|
||||
</Document>
|
||||
|
||||
Reference in New Issue
Block a user