diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..a8bfbd4 --- /dev/null +++ b/.npmrc @@ -0,0 +1,5 @@ +# npm 12+ disables fetching git-protocol dependencies by default +# (allow-git=none). @einfach-produktiv/invoicing is declared directly in +# this file's own package.json (not a transitive dependency), so "root" is +# the narrowest setting that still allows it. +allow-git=root diff --git a/Dockerfile b/Dockerfile index debf6c9..a79e5b3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,11 @@ FROM node:20-alpine AS base FROM base AS deps -RUN apk add --no-cache libc6-compat +# git — needed for `npm ci` to fetch @einfach-produktiv/invoicing, a git-URL +# dependency (see package.json); Alpine's base image doesn't ship it. +RUN apk add --no-cache libc6-compat git WORKDIR /app -COPY package*.json ./ +COPY package*.json .npmrc ./ RUN npm ci FROM base AS builder diff --git a/app/bestellbestaetigung/components/BestellbestaetigungContent.tsx b/app/bestellbestaetigung/components/BestellbestaetigungContent.tsx index e6b6d14..73a6268 100644 --- a/app/bestellbestaetigung/components/BestellbestaetigungContent.tsx +++ b/app/bestellbestaetigung/components/BestellbestaetigungContent.tsx @@ -6,7 +6,7 @@ import Image from "next/image"; import type { CartItem } from "../../lib/cart"; import { useProducts } from "../../lib/products"; import { computeCartTotals, effectivePrice, effectiveTaxRate } from "../../lib/cartTotals"; -import { computeTaxBreakdown } from "../../lib/taxBreakdown"; +import { computeTaxBreakdown } from "@einfach-produktiv/invoicing"; import { formatPrice, formatDate } from "../../lib/format"; import { Reveal } from "../../components/Reveal"; import { CheckoutSteps } from "../../components/CheckoutSteps"; diff --git a/app/cart/components/CartContent.tsx b/app/cart/components/CartContent.tsx index 2460a62..f080b56 100644 --- a/app/cart/components/CartContent.tsx +++ b/app/cart/components/CartContent.tsx @@ -8,7 +8,7 @@ import { useCart, removeFromCart, setQuantity } from "../../lib/cart"; import { useProducts } from "../../lib/products"; import { useDiscount, applyDiscount, clearDiscount } from "../../lib/discount"; import { computeSubtotal, computeCartTotals, effectivePrice, effectiveTaxRate } from "../../lib/cartTotals"; -import { computeTaxBreakdown } from "../../lib/taxBreakdown"; +import { computeTaxBreakdown } from "@einfach-produktiv/invoicing"; import { formatPrice, discountPercent } from "../../lib/format"; import { Reveal } from "../../components/Reveal"; import { VersandModal } from "../../components/VersandModal"; diff --git a/app/checkout/components/CheckoutContent.tsx b/app/checkout/components/CheckoutContent.tsx index eea6baa..5044414 100644 --- a/app/checkout/components/CheckoutContent.tsx +++ b/app/checkout/components/CheckoutContent.tsx @@ -8,7 +8,7 @@ import { useCart, clearCart, mergeServerCartIntoLocal } from "../../lib/cart"; import { useProducts } from "../../lib/products"; import { useDiscount, clearDiscount } from "../../lib/discount"; import { computeSubtotal, computeCartTotals, effectivePrice, effectiveTaxRate } from "../../lib/cartTotals"; -import { computeTaxBreakdown } from "../../lib/taxBreakdown"; +import { computeTaxBreakdown } from "@einfach-produktiv/invoicing"; import { formatPrice } from "../../lib/format"; import { Reveal } from "../../components/Reveal"; import { VersandModal } from "../../components/VersandModal"; diff --git a/app/company-settings-preview/components/LiveCompanySettingsPreviewClient.tsx b/app/company-settings-preview/components/LiveCompanySettingsPreviewClient.tsx index 37f9980..b25cf09 100644 --- a/app/company-settings-preview/components/LiveCompanySettingsPreviewClient.tsx +++ b/app/company-settings-preview/components/LiveCompanySettingsPreviewClient.tsx @@ -2,7 +2,7 @@ import dynamic from "next/dynamic"; import { useLivePreview } from "@payloadcms/live-preview-react"; -import { InvoiceDocument, SAMPLE_INVOICE_ORDER } from "../../lib/invoicePdf"; +import { InvoiceDocument, SAMPLE_INVOICE_ORDER } from "@einfach-produktiv/invoicing"; import type { CompanySettings } from "../../lib/payload"; const PAYLOAD_URL = process.env.NEXT_PUBLIC_PAYLOAD_URL || "https://payload.mk360.de"; diff --git a/app/components/VatBreakdown.tsx b/app/components/VatBreakdown.tsx index 010f052..a64046b 100644 --- a/app/components/VatBreakdown.tsx +++ b/app/components/VatBreakdown.tsx @@ -1,5 +1,5 @@ import { formatPrice } from "../lib/format"; -import type { TaxBreakdownGroup } from "../lib/taxBreakdown"; +import type { TaxBreakdownGroup } from "@einfach-produktiv/invoicing"; // The actual amount of VAT included in a total — not just a disclosure // that VAT is included (see cartTotals.ts's effectiveTaxRate() for the diff --git a/app/konto/bestellungen/[orderNumber]/page.tsx b/app/konto/bestellungen/[orderNumber]/page.tsx index fbaf782..e801242 100644 --- a/app/konto/bestellungen/[orderNumber]/page.tsx +++ b/app/konto/bestellungen/[orderNumber]/page.tsx @@ -8,7 +8,7 @@ import { VatBreakdown } from "../../../components/VatBreakdown"; import { formatPrice, formatDate } from "../../../lib/format"; import { getSessionCustomer, getCustomerOrderDetail, customerOrderAction } from "../../../lib/customerAuth"; import { getProductImagesByIds } from "../../../lib/payload"; -import { computeTaxBreakdown } from "../../../lib/taxBreakdown"; +import { computeTaxBreakdown } from "@einfach-produktiv/invoicing"; import { buildTrackingUrl, CARRIER_LABELS } from "../../../lib/tracking"; import { OrderActionButton } from "./components/OrderActionButton"; import { OrderStatusBadge } from "../../components/OrderStatusBadge"; diff --git a/app/lib/__tests__/invoicePdf.test.ts b/app/lib/__tests__/invoicePdf.test.ts deleted file mode 100644 index 5c58a66..0000000 --- a/app/lib/__tests__/invoicePdf.test.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { __testables, type InvoiceItem, type InvoiceOrder } from "../invoicePdf"; - -const { isPaidImmediately, groupByTaxRate } = __testables; - -const item = (overrides: Partial = {}): InvoiceItem => ({ - productName: "ToDo-Karten", - quantity: 2, - unitPrice: 12.9, - taxRatePercent: 19, - bundleContents: null, - ...overrides, -}); - -const order = (overrides: Partial = {}): InvoiceOrder => ({ - orderNumber: "#EP-0001", - invoiceNumber: "RE-0001", - invoiceIssuedAt: new Date().toISOString(), - customerFirstName: "Max", - customerLastName: "Mustermann", - deliveryMethod: "address", - street: "Musterweg 1", - zip: "10115", - city: "Berlin", - country: "Deutschland", - paymentMethodTitle: "Kreditkarte", - items: [item()], - subtotal: 25.8, - shippingCost: 2.9, - discountAmount: 0, - discountCode: null, - total: 28.7, - ...overrides, -}); - -describe("isPaidImmediately", () => { - it("is true for Kreditkarte", () => { - expect(isPaidImmediately("Kreditkarte")).toBe(true); - }); - - it("is true for PayPal", () => { - expect(isPaidImmediately("PayPal")).toBe(true); - }); - - it("is false only for Überweisung", () => { - expect(isPaidImmediately("Überweisung")).toBe(false); - }); - - it("defaults to true for any future/unknown payment method (only Überweisung is the named exception)", () => { - expect(isPaidImmediately("Sofortüberweisung")).toBe(true); - expect(isPaidImmediately("Klarna")).toBe(true); - }); -}); - -describe("groupByTaxRate (original invoice)", () => { - it("reconciles net+tax to gross, and gross to the order total for a single rate", () => { - const o = order({ items: [item({ quantity: 2, unitPrice: 12.9 })], subtotal: 25.8, shippingCost: 2.9, discountAmount: 0, total: 28.7 }); - const groups = groupByTaxRate(o, 19); - expect(groups).toHaveLength(1); - const [g] = groups; - expect(g.net + g.tax).toBeCloseTo(g.gross, 6); - expect(g.gross).toBeCloseTo(28.7, 2); - }); - - it("distributes a discount proportionally, still reconciling to the discounted total", () => { - const o = order({ - items: [item({ quantity: 1, unitPrice: 50 })], - subtotal: 50, - shippingCost: 0, - discountAmount: 10, - total: 40, - }); - const groups = groupByTaxRate(o, 19); - const total = groups.reduce((sum, g) => sum + g.gross, 0); - expect(total).toBeCloseTo(40, 2); - }); - - it("splits multiple tax rates into separate groups that each reconcile", () => { - const o = order({ - items: [item({ quantity: 1, unitPrice: 20, taxRatePercent: 19 }), item({ quantity: 1, unitPrice: 10, taxRatePercent: 7 })], - subtotal: 30, - shippingCost: 0, - discountAmount: 0, - total: 30, - }); - const groups = groupByTaxRate(o, 19); - expect(groups).toHaveLength(2); - for (const g of groups) expect(g.net + g.tax).toBeCloseTo(g.gross, 6); - expect(groups.reduce((sum, g) => sum + g.gross, 0)).toBeCloseTo(30, 2); - }); - - it("falls back to the tenant default rate when an item has no taxRatePercent", () => { - const o = order({ items: [item({ taxRatePercent: undefined as unknown as number })] }); - const groups = groupByTaxRate(o, 7); - expect(groups[0].rate).toBe(7); - }); -}); diff --git a/app/lib/correctionInvoicePdf.tsx b/app/lib/correctionInvoicePdf.tsx deleted file mode 100644 index 84b17ce..0000000 --- a/app/lib/correctionInvoicePdf.tsx +++ /dev/null @@ -1,311 +0,0 @@ -import React from "react"; -import { Document, Page, View, Text, Image, StyleSheet, renderToBuffer } from "@react-pdf/renderer"; -import { formatDate } from "./format"; -import { computeTaxBreakdown } from "./taxBreakdown"; - -// Frontend port of the Payload backend's src/lib/correctionInvoicePdf.tsx -// — the *real* Stornorechnung/Gutschrift is generated and emailed from -// Payload's own Orders.ts afterChange hook (that's where the status -// transition and the correction invoice NUMBER are actually assigned). -// This copy exists only so a customer can re-download the same document -// later from /konto/bestellungen/[orderNumber] without it having been -// stored as a file anywhere — same "deterministic regeneration, not file -// storage" approach already used for the original invoice (see -// invoicePdf.tsx): correctionInvoiceNumber/correctionInvoiceIssuedAt are -// immutable once set, so re-rendering from the order's own stored data -// always reproduces the identical document. -const BRAND = "#f6a701"; -const TEXT_MUTED = "#6b6b69"; -const BORDER = "#e5e0d8"; -const BG_MUTED = "#f8f5f1"; - -const styles = StyleSheet.create({ - page: { padding: 0, fontSize: 10, fontFamily: "Helvetica", color: "#1a1a18" }, - // A rule, not a filled band — a bold brand-colored line rather than a - // plain 1pt gray divider. - headerBand: { - padding: 32, - paddingBottom: 24, - flexDirection: "row", - justifyContent: "space-between", - alignItems: "center", - borderBottomWidth: 3, - borderBottomColor: BRAND, - }, - wordmark: { fontFamily: "Helvetica-Bold", fontSize: 14 }, - kindLabel: { fontFamily: "Helvetica-Bold", fontSize: 22, color: BRAND, letterSpacing: 1 }, - body: { padding: 32, paddingBottom: 90 }, - refLine: { fontSize: 10, color: TEXT_MUTED, marginBottom: 24 }, - addressRow: { flexDirection: "row", justifyContent: "space-between", marginBottom: 24 }, - addressBlock: { width: "45%" }, - addressLabel: { fontSize: 8, color: TEXT_MUTED, marginBottom: 4, textTransform: "uppercase" }, - addressLine: { fontSize: 10, lineHeight: 1.5 }, - metaRow: { flexDirection: "row", gap: 10, marginBottom: 24 }, - metaBox: { borderWidth: 1, borderColor: BORDER, borderRadius: 6, paddingVertical: 8, paddingHorizontal: 12 }, - metaLabel: { fontSize: 7, color: TEXT_MUTED, textTransform: "uppercase", marginBottom: 2 }, - metaValue: { fontSize: 10, fontFamily: "Helvetica-Bold" }, - table: { borderRadius: 6, overflow: "hidden", borderWidth: 1, borderColor: BORDER, marginBottom: 16 }, - tableHeader: { flexDirection: "row", backgroundColor: BG_MUTED, paddingVertical: 8, paddingHorizontal: 10 }, - tableRow: { flexDirection: "row", paddingVertical: 8, paddingHorizontal: 10, borderTopWidth: 1, borderTopColor: BORDER }, - tableRowAlt: { backgroundColor: BG_MUTED }, - colImage: { width: 28 }, - itemImage: { width: 28, height: 28, borderRadius: 3 }, - colName: { flex: 3 }, - colQty: { flex: 1, textAlign: "right" }, - colPrice: { flex: 1, textAlign: "right" }, - colTotal: { flex: 1, textAlign: "right" }, - headerCell: { fontSize: 8, color: TEXT_MUTED, textTransform: "uppercase" }, - bundleLine: { fontSize: 8, color: TEXT_MUTED, marginTop: 2 }, - summary: { alignItems: "flex-end", marginBottom: 24 }, - summaryBox: { width: 240, backgroundColor: BG_MUTED, borderRadius: 6, padding: 14 }, - summaryRow: { flexDirection: "row", justifyContent: "space-between", paddingVertical: 2 }, - summaryLabel: { fontSize: 10, color: TEXT_MUTED }, - summaryValue: { fontSize: 10 }, - 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: { - position: "absolute", - bottom: 32, - left: 32, - right: 32, - borderTopWidth: 1, - borderTopColor: BORDER, - paddingTop: 12, - fontSize: 8, - color: TEXT_MUTED, - }, -}); - -function formatPrice(amount: number): string { - return new Intl.NumberFormat("de-DE", { style: "currency", currency: "EUR" }).format(amount); -} - -export type CorrectionInvoiceKind = "storno" | "gutschrift"; - -export type CorrectionInvoiceItem = { - productName: string; - quantity: number; - unitPrice: number; - taxRatePercent: number; - bundleContents?: string | null; - variantName?: string | null; - returnQuantity?: number; - imageUrl?: string | null; -}; - -export type CorrectionInvoiceOrder = { - orderNumber: string; - invoiceNumber: string; - invoiceIssuedAt: string; - correctionInvoiceNumber: string; - correctionInvoiceIssuedAt: string; - customerFirstName: string; - customerLastName: string; - deliveryMethod: "address" | "packstation"; - street?: string | null; - packstationNumber?: string | null; - postNumber?: string | null; - zip: string; - city: string; - country: string; - items: CorrectionInvoiceItem[]; - subtotal: number; - shippingCost: number; - discountAmount: number; - total: number; -}; - -export type InvoiceSeller = { - sellerName: string; - sellerStreet: string; - sellerZip: string; - sellerCity: string; - sellerCountry: string; - sellerEmail: string; - vatId: string; - taxRatePercent: number; - bankDetails?: string | null; - // Pflichtangaben in Geschäftsbriefen for registered legal forms (§37a - // HGB / §35a GmbHG) — optional because a sole proprietorship (the - // default legalForm in company-settings) has neither. Mirrors - // invoicePdf.tsx's own InvoiceSeller — see that file's comment. - registerCourt?: string | null; - registerNumber?: string | null; - managingDirector?: string | null; -}; - -// Stornorechnung: every item at its full ordered quantity (nothing -// shipped, undo everything). Gutschrift: only items with a nonzero -// returnQuantity, at that returned quantity — see this file's port source -// (backend src/lib/correctionInvoicePdf.tsx) for the full policy -// reasoning (no shipping refund, no discount reproration on a Gutschrift). -function resolveLineItems(kind: CorrectionInvoiceKind, items: CorrectionInvoiceItem[]): { item: CorrectionInvoiceItem; effectiveQuantity: number }[] { - if (kind === "storno") return items.map((item) => ({ item, effectiveQuantity: item.quantity })); - return items - .filter((item) => (item.returnQuantity ?? 0) > 0) - .map((item) => ({ item, effectiveQuantity: item.returnQuantity as number })); -} - -function groupByTaxRate( - kind: CorrectionInvoiceKind, - lines: { item: CorrectionInvoiceItem; effectiveQuantity: number }[], - order: CorrectionInvoiceOrder, - defaultRate: number, -): { rate: number; net: number; tax: number; gross: number }[] { - // Gutschrift excludes shipping/discount entirely — passing 0 for both - // collapses computeTaxBreakdown's scale factor to 1, same as the old - // kind==='storno' ? ... : 1 branch did explicitly. - return computeTaxBreakdown( - lines.map(({ item, effectiveQuantity }) => ({ - quantity: effectiveQuantity, - unitPrice: item.unitPrice, - taxRatePercent: item.taxRatePercent ?? defaultRate, - })), - order.subtotal, - kind === "storno" ? order.discountAmount : 0, - kind === "storno" ? order.shippingCost : 0, - ); -} - -function CorrectionInvoiceDocument({ kind, order, seller }: { kind: CorrectionInvoiceKind; order: CorrectionInvoiceOrder; seller: InvoiceSeller }) { - const kindLabel = kind === "storno" ? "Stornorechnung" : "Gutschrift"; - const lines = resolveLineItems(kind, order.items); - const rateGroups = groupByTaxRate(kind, lines, order, seller.taxRatePercent); - const grandTotal = rateGroups.reduce((sum, g) => sum + g.gross, 0); - const refNote = - kind === "storno" - ? "vollständige Stornierung des ursprünglichen Rechnungsbetrags (inkl. Versand)." - : "Gutschrift für die zurückgesendeten Artikel — ohne Versandkosten, der ursprüngliche Rabatt bleibt unverändert bei den behaltenen Artikeln."; - const deliveryLine = - order.deliveryMethod === "address" ? order.street : `Packstation ${order.packstationNumber} · Postnummer ${order.postNumber}`; - - return ( - - - - einfach produktiv. - {kindLabel.toUpperCase()} - - - - - {kindLabel} zu Rechnung Nr. {order.invoiceNumber} vom {formatDate(order.invoiceIssuedAt)} (Bestellung {order.orderNumber}) — {refNote} - - - - - Von - {seller.sellerName} - {seller.sellerStreet} - - {seller.sellerZip} {seller.sellerCity} - - {seller.sellerCountry} - - - An - - {order.customerFirstName} {order.customerLastName} - - {deliveryLine} - - {order.zip} {order.city} - - {order.country} - - - - - - {kindLabel === "Gutschrift" ? "Gutschrift-Nr." : "Storno-Nr."} - {order.correctionInvoiceNumber} - - - Datum - {formatDate(order.correctionInvoiceIssuedAt)} - - - - - - - Artikel - Menge - Einzelpreis - Betrag - - {lines.map(({ item, effectiveQuantity }, i) => ( - - - {item.imageUrl && } - - - - {item.productName} - {item.variantName ? ` (${item.variantName})` : ""} - - {item.bundleContents ? {item.bundleContents} : null} - - {effectiveQuantity} - {formatPrice(item.unitPrice)} - -{formatPrice(effectiveQuantity * item.unitPrice)} - - ))} - - - - - {/* Storno reverses the full original invoice, shipping - included (see this file's top-of-file comment on why the - two kinds differ) — shown as its own line instead of - silently folded into the tax-rate groups below, same as - the original invoice's own Versand row. Gutschrift never - reverses shipping, so this never renders for it. */} - {kind === "storno" && order.shippingCost > 0 && ( - - Versand - -{formatPrice(order.shippingCost)} - - )} - {rateGroups.map((g) => ( - - - Netto - -{formatPrice(g.net)} - - - zzgl. {g.rate}% MwSt. - -{formatPrice(g.tax)} - - - ))} - - Gesamt - -{formatPrice(grandTotal)} - - - - - - - - {seller.sellerName} · {seller.sellerStreet}, {seller.sellerZip} {seller.sellerCity} · {seller.sellerEmail} · USt-IdNr.{" "} - {seller.vatId} - {seller.registerCourt && seller.registerNumber ? ` · ${seller.registerCourt} · ${seller.registerNumber}` : ""} - {seller.managingDirector ? ` · Geschäftsführung: ${seller.managingDirector}` : ""} - - {seller.bankDetails ? Bankverbindung (für Überweisung): {seller.bankDetails} : null} - - - - ); -} - -export async function renderCorrectionInvoicePdf(kind: CorrectionInvoiceKind, order: CorrectionInvoiceOrder, seller: InvoiceSeller): Promise { - return renderToBuffer(); -} - -// Exported for unit testing (see app/lib/__tests__/correctionInvoicePdf.test.ts) -// — the actual money math, independent of the PDF rendering. -export const __testables = { resolveLineItems, groupByTaxRate }; diff --git a/app/lib/emailTemplates.ts b/app/lib/emailTemplates.ts index c0403ca..3c9ecb3 100644 --- a/app/lib/emailTemplates.ts +++ b/app/lib/emailTemplates.ts @@ -1,5 +1,5 @@ import { formatPrice, formatDate } from "./format"; -import { computeTaxBreakdown } from "./taxBreakdown"; +import { computeTaxBreakdown } from "@einfach-produktiv/invoicing"; import type { CompanySettings } from "./payload"; // Pure string-building functions, no server-only or client-only imports — diff --git a/app/lib/invoiceData.ts b/app/lib/invoiceData.ts index afe6e30..0ab8631 100644 --- a/app/lib/invoiceData.ts +++ b/app/lib/invoiceData.ts @@ -1,6 +1,11 @@ import { getCompanySettings, type CompanySettings } from "./payload"; -import { renderInvoicePdf, type InvoiceOrder } from "./invoicePdf"; -import { renderCorrectionInvoicePdf, type CorrectionInvoiceKind, type CorrectionInvoiceOrder } from "./correctionInvoicePdf"; +import { + renderInvoicePdf, + renderCorrectionInvoicePdf, + type InvoiceOrder, + type CorrectionInvoiceKind, + type CorrectionInvoiceOrder, +} from "@einfach-produktiv/invoicing"; export type { CompanySettings }; diff --git a/app/lib/invoicePdf.tsx b/app/lib/invoicePdf.tsx deleted file mode 100644 index 878d4f5..0000000 --- a/app/lib/invoicePdf.tsx +++ /dev/null @@ -1,375 +0,0 @@ -import React from "react"; -import { Document, Page, View, Text, Image, StyleSheet, renderToBuffer } from "@react-pdf/renderer"; -import { formatDate } from "./format"; -import { computeTaxBreakdown } from "./taxBreakdown"; - -// Generated synchronously in the checkout request (see app/lib/orderEmail.ts) -// and attached to the order-confirmation email, plus available on-demand via -// app/api/account/orders/[orderNumber]/invoice/route.ts — same render -// function both times, so a re-download always matches what was emailed. -// -// Built-in Helvetica, not a registered web font — this renders inside the -// checkout request's own fire-and-forget email step; a font-fetch failure -// there is one more way to lose the invoice attachment for no real design -// benefit. Same choice as the Payload-side correction-invoice PDF -// (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 TEXT_MUTED = "#6b6b69"; -const BORDER = "#e5e0d8"; -const BG_MUTED = "#f8f5f1"; -const SUCCESS = "#2f8f4e"; -const SUCCESS_TINT = "#e7f5eb"; - -const styles = StyleSheet.create({ - page: { padding: 0, fontSize: 10, fontFamily: "Helvetica", color: "#1a1a18" }, - // 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, paddingBottom: 90 }, - addressRow: { flexDirection: "row", flexWrap: "wrap", justifyContent: "space-between", rowGap: 12, marginBottom: 24 }, - addressBlock: { width: "45%" }, - // Narrower variant for when a 3rd (shipping) block joins Von/An — three - // of these plus the row's own space-between still fit a Page's width - // without any block cramping its text. - addressBlockThird: { width: "30%" }, - addressLabel: { fontSize: 8, color: TEXT_MUTED, marginBottom: 4, textTransform: "uppercase" }, - addressLine: { fontSize: 10, lineHeight: 1.5 }, - metaRow: { flexDirection: "row", gap: 10, marginBottom: 16, flexWrap: "wrap" }, - metaBox: { borderWidth: 1, borderColor: BORDER, borderRadius: 6, paddingVertical: 8, paddingHorizontal: 12 }, - metaLabel: { fontSize: 7, color: TEXT_MUTED, textTransform: "uppercase", marginBottom: 2 }, - metaValue: { fontSize: 10, fontFamily: "Helvetica-Bold" }, - paidBadge: { backgroundColor: SUCCESS_TINT, borderRadius: 6, paddingVertical: 8, paddingHorizontal: 12, justifyContent: "center" }, - paidBadgeText: { fontSize: 10, fontFamily: "Helvetica-Bold", color: SUCCESS }, - table: { borderRadius: 6, overflow: "hidden", borderWidth: 1, borderColor: BORDER, marginTop: 8, marginBottom: 16 }, - tableHeader: { flexDirection: "row", backgroundColor: BG_MUTED, paddingVertical: 8, paddingHorizontal: 10 }, - tableRow: { flexDirection: "row", paddingVertical: 8, paddingHorizontal: 10, borderTopWidth: 1, borderTopColor: BORDER }, - tableRowAlt: { backgroundColor: BG_MUTED }, - colImage: { width: 28 }, - itemImage: { width: 28, height: 28, borderRadius: 3 }, - colName: { flex: 3 }, - colQty: { flex: 1, textAlign: "right" }, - colPrice: { flex: 1, textAlign: "right" }, - colTotal: { flex: 1, textAlign: "right" }, - headerCell: { fontSize: 8, color: TEXT_MUTED, textTransform: "uppercase" }, - bundleLine: { fontSize: 8, color: TEXT_MUTED, marginTop: 2 }, - summary: { alignItems: "flex-end", marginBottom: 24 }, - summaryBox: { width: 240, backgroundColor: BG_MUTED, borderRadius: 6, padding: 14 }, - summaryRow: { flexDirection: "row", justifyContent: "space-between", paddingVertical: 2 }, - summaryLabel: { fontSize: 10, color: TEXT_MUTED }, - summaryValue: { fontSize: 10 }, - 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" }, - // `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 { - return new Intl.NumberFormat("de-DE", { style: "currency", currency: "EUR" }).format(amount); -} - -export type InvoiceItem = { - productName: string; - quantity: number; - unitPrice: number; - taxRatePercent: number; - bundleContents?: string | null; - variantName?: string | null; - imageUrl?: string | null; -}; - -export type InvoiceOrder = { - orderNumber: string; - invoiceNumber: string; - invoiceIssuedAt: string; - customerFirstName: string; - customerLastName: string; - deliveryMethod: "address" | "packstation"; - street?: string | null; - packstationNumber?: string | null; - postNumber?: string | null; - zip: string; - city: string; - country: string; - // Optional package destination distinct from the "An" recipient above — - // when set, the invoice shows both addresses (billing stays "An", this - // becomes its own "Lieferadresse" block) instead of implying the order - // shipped to the billing address, which is only true when this is unset. - hasDifferentShippingAddress?: boolean; - shippingFirstName?: string | null; - shippingLastName?: string | null; - shippingDeliveryMethod?: "address" | "packstation" | null; - shippingStreet?: string | null; - shippingPackstationNumber?: string | null; - shippingPostNumber?: string | null; - shippingZip?: string | null; - shippingCity?: string | null; - shippingCountry?: string | null; - paymentMethodTitle: string; - items: InvoiceItem[]; - subtotal: number; - shippingCost: number; - discountAmount: number; - discountCode: string | null; - total: number; -}; - -export type InvoiceSeller = { - sellerName: string; - sellerStreet: string; - sellerZip: string; - sellerCity: string; - sellerCountry: string; - sellerEmail: string; - vatId: string; - taxRatePercent: number; - bankDetails?: string | null; - // Pflichtangaben in Geschäftsbriefen for registered legal forms (§37a - // HGB / §35a GmbHG) — optional because a sole proprietorship (the - // default legalForm in company-settings) has neither. See - // buildLegalFooterLines() in emailTemplates.ts for the same fields' - // equivalent treatment in the email footer. - registerCourt?: string | null; - registerNumber?: string | null; - managingDirector?: 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 -// (fragile the moment a new one is added in Payload's payment-methods -// collection), the only method that's ever NOT immediate is named -// explicitly — everything else defaults to "paid already". -function isPaidImmediately(paymentMethodTitle: string): boolean { - return paymentMethodTitle !== "Überweisung"; -} - -// Distributes the order-level discount/shipping proportionally across each -// item's gross line total before computing that line's net/tax — so the -// per-rate summary still reconciles exactly to `order.total` even when a -// discount or shipping cost is present alongside items taxed at different -// rates. Falls back to the seller's default rate for any line that -// predates this field (older orders had no per-item snapshot). -function groupByTaxRate(order: InvoiceOrder, defaultRate: number): { rate: number; net: number; tax: number; gross: number }[] { - return computeTaxBreakdown( - order.items.map((item) => ({ quantity: item.quantity, unitPrice: item.unitPrice, taxRatePercent: item.taxRatePercent ?? defaultRate })), - order.subtotal, - order.discountAmount, - order.shippingCost, - ); -} - -// 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 — 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 = - order.deliveryMethod === "address" ? order.street : `Packstation ${order.packstationNumber} · Postnummer ${order.postNumber}`; - const shippingLine = - order.shippingDeliveryMethod === "packstation" - ? `Packstation ${order.shippingPackstationNumber} · Postnummer ${order.shippingPostNumber}` - : order.shippingStreet; - const addressBlockStyle = order.hasDifferentShippingAddress ? styles.addressBlockThird : styles.addressBlock; - - return ( - - - - einfach produktiv. - RECHNUNG - - - - - - Von - {seller.sellerName} - {seller.sellerStreet} - - {seller.sellerZip} {seller.sellerCity} - - {seller.sellerCountry} - - - An - - {order.customerFirstName} {order.customerLastName} - - {deliveryLine} - - {order.zip} {order.city} - - {order.country} - - {order.hasDifferentShippingAddress && ( - - Lieferadresse - - {order.shippingFirstName} {order.shippingLastName} - - {shippingLine} - - {order.shippingZip} {order.shippingCity} - - {order.shippingCountry} - - )} - - - - - Rechnungs-Nr. - {order.invoiceNumber} - - - Datum - {formatDate(order.invoiceIssuedAt)} - - - Bestellnummer - {order.orderNumber} - - {paid && ( - - ✓ Bereits beglichen ({order.paymentMethodTitle}) - - )} - - - - - - Artikel - Menge - Einzelpreis - Betrag - - {order.items.map((item, i) => ( - - - {item.imageUrl && } - - - - {item.productName} - {item.variantName ? ` (${item.variantName})` : ""} - - {item.bundleContents ? {item.bundleContents} : null} - - {item.quantity} - {formatPrice(item.unitPrice)} - {formatPrice(item.quantity * item.unitPrice)} - - ))} - - - - - {order.discountAmount > 0 && ( - - Rabatt{order.discountCode ? ` (${order.discountCode})` : ""} - -{formatPrice(order.discountAmount)} - - )} - - Versand - {order.shippingCost === 0 ? "Kostenlos" : formatPrice(order.shippingCost)} - - {rateGroups.map((g) => ( - - - Netto - {formatPrice(g.net)} - - - zzgl. {g.rate}% MwSt. - {formatPrice(g.tax)} - - - ))} - - Gesamt - {formatPrice(order.total)} - - - - - - - - - {seller.sellerName} · {seller.sellerStreet}, {seller.sellerZip} {seller.sellerCity} · {seller.sellerEmail} · USt-IdNr.{" "} - {seller.vatId} - {seller.registerCourt && seller.registerNumber ? ` · ${seller.registerCourt} · ${seller.registerNumber}` : ""} - {seller.managingDirector ? ` · Geschäftsführung: ${seller.managingDirector}` : ""} - - {seller.bankDetails ? Bankverbindung (für Überweisung): {seller.bankDetails} : null} - - - - ); -} - -export async function renderInvoicePdf(order: InvoiceOrder, seller: InvoiceSeller): Promise { - return renderToBuffer(); -} - -// Exported for unit testing (see app/lib/__tests__/invoicePdf.test.ts) — -// the actual money math and payment-status logic, independent of PDF -// rendering. -export const __testables = { isPaidImmediately, groupByTaxRate }; diff --git a/app/lib/taxBreakdown.ts b/app/lib/taxBreakdown.ts deleted file mode 100644 index b659b9a..0000000 --- a/app/lib/taxBreakdown.ts +++ /dev/null @@ -1,36 +0,0 @@ -// Previously duplicated as groupByTaxRate() independently inside -// invoicePdf.tsx and correctionInvoicePdf.tsx (and, on the Payload backend, -// their own copies) — pulled out so the storefront's own MwSt. breakdowns -// (checkout summary, order confirmation page/email, account order pages) -// can share the exact same math instead of a fourth hand-rolled version -// drifting out of sync with what the actual invoices say. - -export type TaxBreakdownLine = { quantity: number; unitPrice: number; taxRatePercent: number }; -export type TaxBreakdownGroup = { rate: number; net: number; tax: number; gross: number }; - -// Groups line items by their effective VAT rate, then scales each group's -// gross total by however much shipping/discount moved the grand total away -// from the raw item subtotal — proportional to that group's own share of -// the subtotal, not a flat split. Passing discountAmount=0 and -// shippingCost=0 (e.g. a Gutschrift, which excludes both) collapses `scale` -// to 1, i.e. no adjustment at all. -export function computeTaxBreakdown( - items: TaxBreakdownLine[], - subtotal: number, - discountAmount: number, - shippingCost: number, -): TaxBreakdownGroup[] { - const groups = new Map(); - for (const item of items) { - const lineGross = item.quantity * item.unitPrice; - groups.set(item.taxRatePercent, (groups.get(item.taxRatePercent) ?? 0) + lineGross); - } - const scale = subtotal > 0 ? (subtotal - discountAmount + shippingCost) / subtotal : 1; - return Array.from(groups.entries()) - .map(([rate, lineGross]) => { - const gross = lineGross * scale; - const net = gross / (1 + rate / 100); - return { rate, net, tax: gross - net, gross }; - }) - .sort((a, b) => b.rate - a.rate); -} diff --git a/next.config.ts b/next.config.ts index 9a5061a..d52b075 100644 --- a/next.config.ts +++ b/next.config.ts @@ -2,6 +2,10 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { output: "standalone", + // Ships as raw TS/TSX source (no build step of its own) — this app's own + // bundler needs to transpile it, same as first-party app code, rather + // than assuming it's pre-built JS like a normal npm package. + transpilePackages: ["@einfach-produktiv/invoicing"], images: { remotePatterns: [ { diff --git a/package-lock.json b/package-lock.json index 53c1065..321c1ea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "einfach-produktiv", "version": "0.1.0", "dependencies": { + "@einfach-produktiv/invoicing": "git+https://git.mk360.de/Marco/einfach-produktiv-invoicing.git#main", "@payloadcms/live-preview-react": "^3.85.2", "@react-pdf/renderer": "^4.5.1", "motion": "^12.42.2", @@ -291,6 +292,14 @@ "node": ">=6.9.0" } }, + "node_modules/@einfach-produktiv/invoicing": { + "version": "0.1.0", + "resolved": "git+https://git.mk360.de/Marco/einfach-produktiv-invoicing.git#9336cfd0ba7faf1148fd56e7254e52fc016b9130", + "peerDependencies": { + "@react-pdf/renderer": "^4.0.0", + "react": "^19.0.0" + } + }, "node_modules/@emnapi/core": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", @@ -304,9 +313,9 @@ } }, "node_modules/@emnapi/runtime": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", - "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.2.tgz", + "integrity": "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==", "license": "MIT", "optional": true, "dependencies": { @@ -325,9 +334,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", - "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", "dev": true, "license": "MIT", "dependencies": { @@ -408,9 +417,9 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", - "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.6.tgz", + "integrity": "sha512-l2Ul9PrHsPCKcEY/ac7VgFj9D80C7S68sOKc618SyHDPK36s1XcFebXY0iTzUVn4Yq+YbwvSnDmCz9yxjX+QrA==", "dev": true, "license": "MIT", "dependencies": { @@ -420,7 +429,7 @@ "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", - "js-yaml": "^4.1.1", + "js-yaml": "^4.3.0", "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" }, @@ -432,9 +441,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.39.4", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", - "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.5.tgz", + "integrity": "sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==", "dev": true, "license": "MIT", "engines": { @@ -1356,18 +1365,18 @@ } }, "node_modules/@payloadcms/live-preview": { - "version": "3.85.2", - "resolved": "https://registry.npmjs.org/@payloadcms/live-preview/-/live-preview-3.85.2.tgz", - "integrity": "sha512-nWOQhBv4ei3uEjJGGCo1m541fx4D8wONLv74/yjzsawDHRq6tOcOMUTQuQPIZt5xNlomYFPH9XlZfgIs3BSWdA==", + "version": "3.86.0", + "resolved": "https://registry.npmjs.org/@payloadcms/live-preview/-/live-preview-3.86.0.tgz", + "integrity": "sha512-JjLFRCRKgKmDD+lUoXUaaovwpsoeTlwFTXOnCJfV9PMKeZodet1oVlezKNlg1U+Ygq+tI5NOAzEVRtXykfVmtg==", "license": "MIT" }, "node_modules/@payloadcms/live-preview-react": { - "version": "3.85.2", - "resolved": "https://registry.npmjs.org/@payloadcms/live-preview-react/-/live-preview-react-3.85.2.tgz", - "integrity": "sha512-HMulvQRXAQp2OaqmXgZa5Sg1htN+5FC36g0vFnoYooG5XtIuRriFNPOPbokygg67AOQPo+0G0r6jPM9JTjYkXA==", + "version": "3.86.0", + "resolved": "https://registry.npmjs.org/@payloadcms/live-preview-react/-/live-preview-react-3.86.0.tgz", + "integrity": "sha512-xsepFDuyw+0yYKUWFxAPl1xaUA6JXDU4rsPItMJJWYOYuwVy/5ZSkZAz5m/itoXbLkczDsq7gJbvnr2BnysrEw==", "license": "MIT", "dependencies": { - "@payloadcms/live-preview": "3.85.2" + "@payloadcms/live-preview": "3.86.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.1 || ^19.1.2 || ^19.2.1", @@ -1457,12 +1466,6 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, - "node_modules/@react-pdf/reconciler/node_modules/scheduler": { - "version": "0.25.0-rc-603e6108-20241029", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0-rc-603e6108-20241029.tgz", - "integrity": "sha512-pFwF6H1XrSdYYNLfOcGlM28/j8CGLu8IvdrxqhjWULe2bPcKiKW4CV+OWqR/9fT52mywx65l7ysNkjLKBda7eA==", - "license": "MIT" - }, "node_modules/@react-pdf/render": { "version": "4.5.1", "resolved": "https://registry.npmjs.org/@react-pdf/render/-/render-4.5.1.tgz", @@ -1804,6 +1807,17 @@ "tslib": "^2.4.0" } }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/wasi-threads": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", @@ -1871,58 +1885,58 @@ "license": "MIT" }, "node_modules/@swc/helpers": { - "version": "0.5.15", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", - "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "version": "0.5.23", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.23.tgz", + "integrity": "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.8.0" } }, "node_modules/@tailwindcss/node": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.2.tgz", - "integrity": "sha512-yWP/sqEcBLaD8JuA6zNwxoYKr75qxTioYwlRwekj5Jr/I5GXnoJfjetH/psLUIv74cYTH2lBUEzBkinthoYcBg==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz", + "integrity": "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==", "dev": true, "license": "MIT", "dependencies": { "@jridgewell/remapping": "^2.3.5", - "enhanced-resolve": "5.21.6", + "enhanced-resolve": "^5.24.1", "jiti": "^2.7.0", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", - "tailwindcss": "4.3.2" + "tailwindcss": "4.3.3" } }, "node_modules/@tailwindcss/oxide": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.2.tgz", - "integrity": "sha512-z8ZgnzX8gdNoWLBLqBPoh/sjnxkwvf9ZuWjnO0l0yIzbLa5/9S+eC5QxGZKRobVHIC3/1BoMWjHblqWjcgFgag==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.3.tgz", + "integrity": "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==", "dev": true, "license": "MIT", "engines": { "node": ">= 20" }, "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.3.2", - "@tailwindcss/oxide-darwin-arm64": "4.3.2", - "@tailwindcss/oxide-darwin-x64": "4.3.2", - "@tailwindcss/oxide-freebsd-x64": "4.3.2", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.2", - "@tailwindcss/oxide-linux-arm64-gnu": "4.3.2", - "@tailwindcss/oxide-linux-arm64-musl": "4.3.2", - "@tailwindcss/oxide-linux-x64-gnu": "4.3.2", - "@tailwindcss/oxide-linux-x64-musl": "4.3.2", - "@tailwindcss/oxide-wasm32-wasi": "4.3.2", - "@tailwindcss/oxide-win32-arm64-msvc": "4.3.2", - "@tailwindcss/oxide-win32-x64-msvc": "4.3.2" + "@tailwindcss/oxide-android-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-x64": "4.3.3", + "@tailwindcss/oxide-freebsd-x64": "4.3.3", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-x64-musl": "4.3.3", + "@tailwindcss/oxide-wasm32-wasi": "4.3.3", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" } }, "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.2.tgz", - "integrity": "sha512-WHxqIuHpvZ5VtdX6GTl1Ik/Vp2YuN42Et+0CdeaVd/frQ9jAvGmvR8vLT+jk3e8/Q3x8kECB9+R17pgpp2BulA==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.3.tgz", + "integrity": "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==", "cpu": [ "arm64" ], @@ -1937,9 +1951,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.2.tgz", - "integrity": "sha512-GZypeUY/IDJW3877KeM+O67vbXr3MBnbtEL4aYhNErv/JWZhye2vGSWWG9tB6iiqR2MqRNkY8IOUy4NdSZV26w==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.3.tgz", + "integrity": "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==", "cpu": [ "arm64" ], @@ -1954,9 +1968,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.2.tgz", - "integrity": "sha512-UIIzmefR6KO1sDU7MzRqAxC8iBpft/VhkGjTjnhoS6k7Z3rQ9wEgA1ODSiyH/tcSYssulNm4Ci3hOeK1jH7ccQ==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.3.tgz", + "integrity": "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==", "cpu": [ "x64" ], @@ -1971,9 +1985,9 @@ } }, "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.2.tgz", - "integrity": "sha512-GN+uAmcI6DNspnCDwtOAZrTz6oukJnp337qZvxqCGLd3BHBzJpO0ZbTLRvJNdztOeAmTzewewGIMPb0tk2R4WA==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.3.tgz", + "integrity": "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==", "cpu": [ "x64" ], @@ -1988,9 +2002,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.2.tgz", - "integrity": "sha512-4ABn7qSbdHRwTiDiuWNegCyb5+2FJ4vKIKc3DmKrvAFw7MU1Lm11dIkTPwUaFdTzc7IsOpDbqBrlh0x6y36U/w==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.3.tgz", + "integrity": "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==", "cpu": [ "arm" ], @@ -2005,9 +2019,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.2.tgz", - "integrity": "sha512-wDgEIGwoM8w8pufh9LVt1PahDgNdKXrLC2qfAnV3vAmococ9RWbxeAw4pxPttd/TsJfwjyLf90Dg1y9y8I6Emw==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.3.tgz", + "integrity": "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==", "cpu": [ "arm64" ], @@ -2025,9 +2039,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.2.tgz", - "integrity": "sha512-J5Nuk0uZQIiMTJj3LEx4sAA9tMFUoXQZFv1J6An+QGYe53HKRJuFDi0rpq/tuouCZeAbOBY3kQ6g8qeD4TUjtA==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.3.tgz", + "integrity": "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==", "cpu": [ "arm64" ], @@ -2045,9 +2059,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.2.tgz", - "integrity": "sha512-kqCZpSKOBEJO4mz7OqWoofBZeXTAwaVGPj0ErAj7CojmhKpWVWVOnrt9dE8odoIraZq4oj3ausM37kXi+Tow8w==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.3.tgz", + "integrity": "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==", "cpu": [ "x64" ], @@ -2065,9 +2079,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.2.tgz", - "integrity": "sha512-cixpqbh2toJDmkuCRI68nXA8ZxNmdK9Y+9v5h3MC3ZQKy/0BO8AWzlkWyRM7JAFSGBlfig4YVTPsK6MVgqz1uw==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.3.tgz", + "integrity": "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==", "cpu": [ "x64" ], @@ -2085,9 +2099,9 @@ } }, "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.2.tgz", - "integrity": "sha512-4ec2Z/LOmRsAgU23CS4xeJfcJlmRg94A/XrbGRCF1gyU/zdDfRLYDVsS+ynSZCmGNxQ1jQriQOKMQeQxBA3Isw==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.3.tgz", + "integrity": "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==", "bundleDependencies": [ "@napi-rs/wasm-runtime", "@emnapi/core", @@ -2115,9 +2129,9 @@ } }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.2.tgz", - "integrity": "sha512-Zyr/M0+XcYZu3bZrUytc7TXvrk0ftWfl8gN2MwekNDzhqhKRUucMPSeOzM0o0wH5AWOU49BsKRrfKxI2atCPMQ==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz", + "integrity": "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==", "cpu": [ "arm64" ], @@ -2132,9 +2146,9 @@ } }, "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.2.tgz", - "integrity": "sha512-QI9BO7KlNZsp2GuO0jwAAj5jCDABOKXRkCk2XuKTSaNEFSdfzqswYVTtCHBNKHLsqyjFyFkqlDiwkNbTYSssMQ==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.3.tgz", + "integrity": "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==", "cpu": [ "x64" ], @@ -2149,17 +2163,17 @@ } }, "node_modules/@tailwindcss/postcss": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.3.2.tgz", - "integrity": "sha512-rjVWYCa7Ngbi5AarT6k8TkxUG3Wl1QKzHdIZVsjZSzf36Jmo2IKZt/NHRAwly8oDkbBOH0YTu+CHuf9jPxMc+g==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.3.3.tgz", + "integrity": "sha512-JTSZZGQi1AyKirbLN3azmjVzef92tcX7h+iSqPdaeStyFpGpDlKvvpxeOE8njhbUanbRwr3z8DyzhICWnMtQeg==", "dev": true, "license": "MIT", "dependencies": { "@alloc/quick-lru": "^5.2.0", - "@tailwindcss/node": "4.3.2", - "@tailwindcss/oxide": "4.3.2", - "postcss": "^8.5.15", - "tailwindcss": "4.3.2" + "@tailwindcss/node": "4.3.3", + "@tailwindcss/oxide": "4.3.3", + "postcss": "^8.5.16", + "tailwindcss": "4.3.3" } }, "node_modules/@tybys/wasm-util": { @@ -2253,17 +2267,17 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.62.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.62.1.tgz", - "integrity": "sha512-4EQM77WgVNxj7OkL/5b/D/xZsw00G577+UriYTC7JF5opcF3T2AuoeY7ueLaZgSVjSgCS6yOAJB5bRGLPSJUzA==", + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.65.0.tgz", + "integrity": "sha512-IEgob78X12rHpUmtcwFsXhZdVGJtwTVP8FiCLZkR6GlYVrl2PcuB+KhCE5BlVC/eQpQnu8WXRtkHZuPar+gCRA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.62.1", - "@typescript-eslint/type-utils": "8.62.1", - "@typescript-eslint/utils": "8.62.1", - "@typescript-eslint/visitor-keys": "8.62.1", + "@typescript-eslint/scope-manager": "8.65.0", + "@typescript-eslint/type-utils": "8.65.0", + "@typescript-eslint/utils": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" @@ -2276,15 +2290,15 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.62.1", + "@typescript-eslint/parser": "^8.65.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz", + "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==", "dev": true, "license": "MIT", "engines": { @@ -2292,16 +2306,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.62.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.62.1.tgz", - "integrity": "sha512-sPhE4iHuJDSvoAiec+Ro8JyXw8f0ql13HFR82P99nCm9GwTEKG0KYLvDe6REk8BCXuit6vJAv/Yxg5ABaNS2rA==", + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.65.0.tgz", + "integrity": "sha512-CZ4nMxWwgu1HEEFNkeaCptra9QCtkmKdgf3sWh1rl1trIhmxLilgTV4cwcbQ4wemnT4sWQN8CaKOmdYx+g2gMA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.62.1", - "@typescript-eslint/types": "8.62.1", - "@typescript-eslint/typescript-estree": "8.62.1", - "@typescript-eslint/visitor-keys": "8.62.1", + "@typescript-eslint/scope-manager": "8.65.0", + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/typescript-estree": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.0", "debug": "^4.4.3" }, "engines": { @@ -2317,14 +2331,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.62.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.62.1.tgz", - "integrity": "sha512-yQ3RgY5RkSBpsNS1Bx/JQEcA24FOSdfGktoyprAr5u18390UQdtVcfnEv4nIrIshNnavlVyZBKxQwT1fIAE6cg==", + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.65.0.tgz", + "integrity": "sha512-SxnPhbTsGahizDgbu7oqFH/xVtzIqMd/s+WtnSxNxJZJpLbdT5IPdzg8EZxO3+PoKahXmwJLeNQOpKJb3/bi7Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.62.1", - "@typescript-eslint/types": "^8.62.1", + "@typescript-eslint/tsconfig-utils": "^8.65.0", + "@typescript-eslint/types": "^8.65.0", "debug": "^4.4.3" }, "engines": { @@ -2339,14 +2353,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.62.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.62.1.tgz", - "integrity": "sha512-r4d249KbQ1SFdpeStvob8Ih6aPPIzfqllPVOtvhve6ZcpuVcYo5/7zUWckKpHE7StASX4kTKZTLf0WQm/wPkcg==", + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.65.0.tgz", + "integrity": "sha512-Esbl8OSYiVxBokYgWPf7VVWg/BE798wXhimnn9ML9Pt5qoDf8bfQlgjlKXR/k98+AcNzlLKYrpCcrcuZ9DZLgg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.62.1", - "@typescript-eslint/visitor-keys": "8.62.1" + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2357,9 +2371,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.62.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.62.1.tgz", - "integrity": "sha512-xadytJqX9vJVQ2fdQjkcIVigwaOJNWkpjdLt6cEQ+xPnrI1fkp+/jZE/I97k9KUjqtpd25i0HeyZf3T6dutv2g==", + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.65.0.tgz", + "integrity": "sha512-j6GzGqCiRdA7Qhur2VVmKZAkBLfnHFQfx4TaJGL9RMveZqCo48jSHHO0DTgizEnGhtWnqmbtCUSrqSkdiY/0Hg==", "dev": true, "license": "MIT", "engines": { @@ -2374,15 +2388,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.62.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.62.1.tgz", - "integrity": "sha512-aXM5xlqXiTxPibXB93cLAURfT3rlizf7uMXISCXy66Isr/9hISJx3yDsKl0L7lKa51b8JpFuNKby0/O0pEm9jg==", + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.65.0.tgz", + "integrity": "sha512-YjaZ7PRI5qY7ax2L3PbvX0rRyGtipAReCWs0mhhDBHjH/vl0g0BonaGXrKdKpMbIIsMIwDgbk/xzkBTyAltS5g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.62.1", - "@typescript-eslint/typescript-estree": "8.62.1", - "@typescript-eslint/utils": "8.62.1", + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/typescript-estree": "8.65.0", + "@typescript-eslint/utils": "8.65.0", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, @@ -2399,9 +2413,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.62.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.62.1.tgz", - "integrity": "sha512-ooCzJFaf+Hg+uG6fA3NRFGuFjlfNlDhBthbv4ZPU/0elCAFUfnyXUvf/WOpHz/jYwSmvU2GkR2LtyUfy1AxZ1Q==", + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.65.0.tgz", + "integrity": "sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==", "dev": true, "license": "MIT", "engines": { @@ -2413,16 +2427,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.62.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.62.1.tgz", - "integrity": "sha512-xMcW9oP9u7fAMXYs9A65CVmtLQe2r//oXINHfi8HV+oiqhih17sbLdhXr4540YWlgpDKQdY854OL5ZrdCiQsAA==", + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.65.0.tgz", + "integrity": "sha512-JboAE2swaYt4tb1fHhHTABE2K+OLy09XfcTbhnk4Pw96f9dd2e9iYsJ28gBggHlo5z5x1rkyWvcPoTuNTd4oGg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.62.1", - "@typescript-eslint/tsconfig-utils": "8.62.1", - "@typescript-eslint/types": "8.62.1", - "@typescript-eslint/visitor-keys": "8.62.1", + "@typescript-eslint/project-service": "8.65.0", + "@typescript-eslint/tsconfig-utils": "8.65.0", + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", @@ -2493,16 +2507,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.62.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.62.1.tgz", - "integrity": "sha512-sHtbPfuKNZCG+ih8SyjjucqRntSVmp8XgL5u6o9mAhiSn8ds5o/M/XdM0abweme2Tln3szOstOrZ9OXitvPh0g==", + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.65.0.tgz", + "integrity": "sha512-gXiwIHsYreboxeJucHKPvgwl7dXt50mF8s1/c00cP/WoVTyWKFdtfhRWwZiXYFU5H2O8vVoSLNrexFZjYS/SGA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.62.1", - "@typescript-eslint/types": "8.62.1", - "@typescript-eslint/typescript-estree": "8.62.1" + "@typescript-eslint/scope-manager": "8.65.0", + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/typescript-estree": "8.65.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2517,13 +2531,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.62.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.62.1.tgz", - "integrity": "sha512-4g3BLxfdTMy8iZG0MaBkadnlRrCJ74cQiFbyEVMrkwIoqdyaXXQM22cotDvrl4x28wgIZ9rEJRoM+mmhSJpJ1g==", + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.65.0.tgz", + "integrity": "sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.62.1", + "@typescript-eslint/types": "8.65.0", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -3344,9 +3358,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.10.40", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.40.tgz", - "integrity": "sha512-BSSLZ9/Cjjv7Gtj5B68ZzXcXUg8iOf3fme+FCuh8rC/Go+Kmh8cox7M3A8dolou16s64QjLPOSdngh7GxXvkSw==", + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.1.tgz", + "integrity": "sha512-HYXq73DDpCtNzOmrFsm9eSwCvWCql0RzqjpDzXN9EadiLJ4DNat0nsZ/Bzmy+Ud12mb4/zKDY0cQ805ZzN+i0A==", "license": "Apache-2.0", "bin": { "baseline-browser-mapping": "dist/cli.cjs" @@ -3365,9 +3379,9 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", - "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", "dev": true, "license": "MIT", "dependencies": { @@ -3407,9 +3421,9 @@ } }, "node_modules/browserslist": { - "version": "4.28.4", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.4.tgz", - "integrity": "sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw==", + "version": "4.28.7", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.7.tgz", + "integrity": "sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==", "dev": true, "funding": [ { @@ -3427,10 +3441,10 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.10.38", - "caniuse-lite": "^1.0.30001799", - "electron-to-chromium": "^1.5.376", - "node-releases": "^2.0.48", + "baseline-browser-mapping": "^2.10.44", + "caniuse-lite": "^1.0.30001806", + "electron-to-chromium": "^1.5.393", + "node-releases": "^2.0.51", "update-browserslist-db": "^1.2.3" }, "bin": { @@ -3501,9 +3515,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001800", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001800.tgz", - "integrity": "sha512-MMHtuAz9Ys840zAY5F4k6fV5GaivZ9sPk+nz0mY+GYVzRBnYkN0mpqkSR92oWRQ19yQWo4HvBV/FnC16AJX8MA==", + "version": "1.0.30001806", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz", + "integrity": "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==", "funding": [ { "type": "opencollective", @@ -3575,13 +3589,22 @@ "node": ">=7.0.0" } }, - "node_modules/color-name": { + "node_modules/color-convert/node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true, "license": "MIT" }, + "node_modules/color-name": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.1.1.tgz", + "integrity": "sha512-p2FdgwVx1a9yWBHP2wI0VgShkDpgN4kZISkxdNipGBJWpa5G6b04OINlVWCyJj0JmfvcPrgqt95E9k8yvaOJFg==", + "license": "MIT", + "engines": { + "node": ">=12.20" + } + }, "node_modules/color-string": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/color-string/-/color-string-2.1.4.tgz", @@ -3594,15 +3617,6 @@ "node": ">=18" } }, - "node_modules/color-string/node_modules/color-name": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.1.0.tgz", - "integrity": "sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==", - "license": "MIT", - "engines": { - "node": ">=12.20" - } - }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -3806,9 +3820,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.382", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.382.tgz", - "integrity": "sha512-8ETaWbV6SZOrno+G93Ffd9ENsMtetqdnqj4nlfxFW90Sm5GgnuV28Kf62hqQVD6VUgzm7qFQKsTsAPmeUiU3Ug==", + "version": "1.5.395", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.395.tgz", + "integrity": "sha512-7zt9Aw+SrmxLWLN0zhaTWZQiCdryLVrYTq5R7iZakLvi2UQPYMMsROYV/2qVCzMeCiSXHwKOU+sZ4zOVVlrtKA==", "dev": true, "license": "ISC" }, @@ -3826,9 +3840,9 @@ "license": "MIT" }, "node_modules/enhanced-resolve": { - "version": "5.21.6", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.6.tgz", - "integrity": "sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==", + "version": "5.24.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.3.tgz", + "integrity": "sha512-PwKooW9JUzh5chmYfHM3IQl5OkK2u2Nm011MgeZrss3JmFraUx/fqrf78kk8GUMYoibx/14MdwTl/1WKkG7TpQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3948,9 +3962,9 @@ } }, "node_modules/es-iterator-helpers": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.3.3.tgz", - "integrity": "sha512-0PuBxFi+4uPanB97iDxCLWuHeYud2FALrw5HFZGtAF38UpJDbDC8frwp2cnDyae692CQ0dou60UwWfhgsa4U/g==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.4.0.tgz", + "integrity": "sha512-c/A0P0oxkACDc+cKWw8evLXK83oBKgn0qPOqCYT4x9uolpCIJAcYvJC9QYKNDRPsTeGyCrQ326jrvgZWdCdK5Q==", "dev": true, "license": "MIT", "dependencies": { @@ -4069,9 +4083,9 @@ } }, "node_modules/eslint": { - "version": "9.39.4", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", - "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.5.tgz", + "integrity": "sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==", "dev": true, "license": "MIT", "dependencies": { @@ -4080,8 +4094,8 @@ "@eslint/config-array": "^0.21.2", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", - "@eslint/eslintrc": "^3.3.5", - "@eslint/js": "9.39.4", + "@eslint/eslintrc": "^3.3.6", + "@eslint/js": "9.39.5", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", @@ -4226,9 +4240,9 @@ } }, "node_modules/eslint-module-utils": { - "version": "2.13.0", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.13.0.tgz", - "integrity": "sha512-bLohSkT6469rRs8czj0tLTD8vaeIS/whvPRJVjDr7IuoTT1k5DYDERlNycjDj/HkOlvQdYurmfZ/g3fG5bgeLQ==", + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.14.0.tgz", + "integrity": "sha512-W2WCRZ9Dqntd+2u8jJcVMV2PKulc6RdLgUUoh/yQr3uB6lo/ZOeGx11sv60/8S4QFFKNslAlWhr9u0Ef7ZW6Ig==", "dev": true, "license": "MIT", "dependencies": { @@ -4627,9 +4641,9 @@ } }, "node_modules/flatted": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", - "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.3.tgz", + "integrity": "sha512-/zipXxyO6rGvuNGDiULY9MvEGSkb2gaG4GGH4ygMi0ZZzyMHdUZBmntJmx5x1G2VuPytCwGN4xsJP6cw+sK+vQ==", "dev": true, "license": "ISC" }, @@ -6277,6 +6291,15 @@ } } }, + "node_modules/next/node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, "node_modules/next/node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", @@ -6325,9 +6348,9 @@ } }, "node_modules/node-releases": { - "version": "2.0.50", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.50.tgz", - "integrity": "sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg==", + "version": "2.0.51", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz", + "integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==", "dev": true, "license": "MIT", "engines": { @@ -6507,13 +6530,14 @@ } }, "node_modules/own-keys": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", - "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.2.tgz", + "integrity": "sha512-19YVAg7T+WTrxggPukVq7DjTv6+PJ867TmhCvBsYwmbFCsZd344rq2Ld1p0wo8f8Qrrhgp82c6FJRqdXWtSEhg==", "dev": true, "license": "MIT", "dependencies": { - "get-intrinsic": "^1.2.6", + "call-bound": "^1.0.4", + "get-intrinsic": "^1.3.0", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" }, @@ -6769,6 +6793,12 @@ "react": "^19.2.4" } }, + "node_modules/react-dom/node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -7023,9 +7053,9 @@ } }, "node_modules/scheduler": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", - "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "version": "0.25.0-rc-603e6108-20241029", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0-rc-603e6108-20241029.tgz", + "integrity": "sha512-pFwF6H1XrSdYYNLfOcGlM28/j8CGLu8IvdrxqhjWULe2bPcKiKW4CV+OWqR/9fT52mywx65l7ysNkjLKBda7eA==", "license": "MIT" }, "node_modules/semver": { @@ -7497,9 +7527,9 @@ "license": "ISC" }, "node_modules/tailwindcss": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.2.tgz", - "integrity": "sha512-WtctNNSH8A9jlMIqxzuYumOHU5uGZyRv0Q5svQl+oEPy5w84YpBxdb7MdqyiSPQge5jTJ6zFQLq0PFygdccSBA==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz", + "integrity": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==", "dev": true, "license": "MIT" }, @@ -7576,9 +7606,9 @@ } }, "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", "dev": true, "license": "MIT", "engines": { @@ -7762,16 +7792,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.62.1", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.62.1.tgz", - "integrity": "sha512-vymnnM5g0AKQDSAyfP12nMIBvgwgA42syg74kkuZ4x1VuTzwQKwc5h9rGxeShCjny5o+zWAb6OEoz7XLgrIkIw==", + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.65.0.tgz", + "integrity": "sha512-/ggrHAwyjENDusvyxbuqxAC2dTnZg/Z8F+fgQtYIz+L6n/9HfSlEZcFGV/NsMNa6CkGk0xUjUAFwC0vHOflvIA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.62.1", - "@typescript-eslint/parser": "8.62.1", - "@typescript-eslint/typescript-estree": "8.62.1", - "@typescript-eslint/utils": "8.62.1" + "@typescript-eslint/eslint-plugin": "8.65.0", + "@typescript-eslint/parser": "8.65.0", + "@typescript-eslint/typescript-estree": "8.65.0", + "@typescript-eslint/utils": "8.65.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" diff --git a/package.json b/package.json index 92c8d73..2a672a3 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "test:unit": "vitest run" }, "dependencies": { + "@einfach-produktiv/invoicing": "git+https://git.mk360.de/Marco/einfach-produktiv-invoicing.git#main", "@payloadcms/live-preview-react": "^3.85.2", "@react-pdf/renderer": "^4.5.1", "motion": "^12.42.2",