diff --git a/.react-router/types/+routes.ts b/.react-router/types/+routes.ts index ff92dfc..4e1b4e8 100644 --- a/.react-router/types/+routes.ts +++ b/.react-router/types/+routes.ts @@ -140,6 +140,11 @@ type Pages = { "id": string; }; }; + "/api/invoices/:id/xml": { + params: { + "id": string; + }; + }; "/api/reports": { params: {}; }; @@ -148,7 +153,7 @@ type Pages = { type RouteFiles = { "root.tsx": { id: "root"; - page: "/" | "/login" | "/logout" | "/companies" | "/companies/new" | "/companies/:id" | "/companies/:id/edit" | "/companies/:id/customers" | "/companies/:id/leistungen" | "/companies/:id/invoices" | "/companies/:id/invoices/new" | "/companies/:id/invoices/:invoiceId" | "/companies/:id/invoices/:invoiceId/edit" | "/companies/:id/reports" | "/archiv" | "/settings/password" | "/admin/users" | "/admin/users/new" | "/admin/users/:id" | "/admin/logs" | "/api/companies" | "/api/companies/:id" | "/api/companies/:id/customers" | "/api/companies/:id/invoices" | "/api/customers" | "/api/customers/:id" | "/api/services" | "/api/services/:id" | "/api/invoices" | "/api/invoices/:id" | "/api/invoices/:id/pdf" | "/api/reports"; + page: "/" | "/login" | "/logout" | "/companies" | "/companies/new" | "/companies/:id" | "/companies/:id/edit" | "/companies/:id/customers" | "/companies/:id/leistungen" | "/companies/:id/invoices" | "/companies/:id/invoices/new" | "/companies/:id/invoices/:invoiceId" | "/companies/:id/invoices/:invoiceId/edit" | "/companies/:id/reports" | "/archiv" | "/settings/password" | "/admin/users" | "/admin/users/new" | "/admin/users/:id" | "/admin/logs" | "/api/companies" | "/api/companies/:id" | "/api/companies/:id/customers" | "/api/companies/:id/invoices" | "/api/customers" | "/api/customers/:id" | "/api/services" | "/api/services/:id" | "/api/invoices" | "/api/invoices/:id" | "/api/invoices/:id/pdf" | "/api/invoices/:id/xml" | "/api/reports"; }; "routes/login.tsx": { id: "routes/login"; @@ -282,6 +287,10 @@ type RouteFiles = { id: "routes/api.invoices.$id.pdf"; page: "/api/invoices/:id/pdf"; }; + "routes/api.invoices.$id.xml.ts": { + id: "routes/api.invoices.$id.xml"; + page: "/api/invoices/:id/xml"; + }; "routes/api.reports.ts": { id: "routes/api.reports"; page: "/api/reports"; @@ -323,5 +332,6 @@ type RouteModules = { "routes/api.invoices": typeof import("./app/routes/api.invoices.ts"); "routes/api.invoices.$id": typeof import("./app/routes/api.invoices.$id.ts"); "routes/api.invoices.$id.pdf": typeof import("./app/routes/api.invoices.$id.pdf.ts"); + "routes/api.invoices.$id.xml": typeof import("./app/routes/api.invoices.$id.xml.ts"); "routes/api.reports": typeof import("./app/routes/api.reports.ts"); }; \ No newline at end of file diff --git a/.react-router/types/app/routes/+types/api.invoices.$id.xml.ts b/.react-router/types/app/routes/+types/api.invoices.$id.xml.ts new file mode 100644 index 0000000..ae97615 --- /dev/null +++ b/.react-router/types/app/routes/+types/api.invoices.$id.xml.ts @@ -0,0 +1,62 @@ +// Generated by React Router + +import type { GetInfo, GetAnnotations } from "react-router/internal"; + +type Module = typeof import("../api.invoices.$id.xml.js") + +type Info = GetInfo<{ + file: "routes/api.invoices.$id.xml.ts", + module: Module +}> + +type Matches = [{ + id: "root"; + module: typeof import("../../root.js"); +}, { + id: "routes/api.invoices.$id.xml"; + module: typeof import("../api.invoices.$id.xml.js"); +}]; + +type Annotations = GetAnnotations; + +export namespace Route { + // links + export type LinkDescriptors = Annotations["LinkDescriptors"]; + export type LinksFunction = Annotations["LinksFunction"]; + + // meta + export type MetaArgs = Annotations["MetaArgs"]; + export type MetaDescriptors = Annotations["MetaDescriptors"]; + export type MetaFunction = Annotations["MetaFunction"]; + + // headers + export type HeadersArgs = Annotations["HeadersArgs"]; + export type HeadersFunction = Annotations["HeadersFunction"]; + + // middleware + export type MiddlewareFunction = Annotations["MiddlewareFunction"]; + + // clientMiddleware + export type ClientMiddlewareFunction = Annotations["ClientMiddlewareFunction"]; + + // loader + export type LoaderArgs = Annotations["LoaderArgs"]; + + // clientLoader + export type ClientLoaderArgs = Annotations["ClientLoaderArgs"]; + + // action + export type ActionArgs = Annotations["ActionArgs"]; + + // clientAction + export type ClientActionArgs = Annotations["ClientActionArgs"]; + + // HydrateFallback + export type HydrateFallbackProps = Annotations["HydrateFallbackProps"]; + + // Component + export type ComponentProps = Annotations["ComponentProps"]; + + // ErrorBoundary + export type ErrorBoundaryProps = Annotations["ErrorBoundaryProps"]; +} \ No newline at end of file diff --git a/app/components/invoice/invoice-form.tsx b/app/components/invoice/invoice-form.tsx index 38f870e..55bfb25 100644 --- a/app/components/invoice/invoice-form.tsx +++ b/app/components/invoice/invoice-form.tsx @@ -276,10 +276,9 @@ export function InvoiceForm({ customers, companyId, onSubmit, defaultValues, def
-
+
Beschreibung
Menge
-
Einh.
{kleinunternehmer ? "Einzelpreis (brutto)" : "Einzelpreis"}
{!kleinunternehmer &&
MwSt.
}
Gesamt (brutto)
@@ -287,7 +286,7 @@ export function InvoiceForm({ customers, companyId, onSubmit, defaultValues, def
{fields.map((field, index) => ( -
+
{(() => { const descValue = watchedItems[index]?.description ?? ""; @@ -341,13 +340,6 @@ export function InvoiceForm({ customers, companyId, onSubmit, defaultValues, def onBlur={() => recalcItem(index)} />
-
- -
# Beschreibung Menge - Einh. {invoice.kleinunternehmer ? "EP (brutto)" : "EP (netto)"} @@ -339,7 +337,6 @@ export function InvoicePDFDocument({ invoice }: InvoicePDFProps) { {item.position} {item.description} {n(item.quantity)} - {item.unit ?? ""} {formatMoney(n(item.unitPrice))} {!invoice.kleinunternehmer && ( {n(item.taxRate)}% diff --git a/app/routes.ts b/app/routes.ts index 1b1a3c2..3f89ac8 100644 --- a/app/routes.ts +++ b/app/routes.ts @@ -41,5 +41,6 @@ export default [ route("api/invoices", "routes/api.invoices.ts"), route("api/invoices/:id", "routes/api.invoices.$id.ts"), route("api/invoices/:id/pdf", "routes/api.invoices.$id.pdf.ts"), + route("api/invoices/:id/xml", "routes/api.invoices.$id.xml.ts"), route("api/reports", "routes/api.reports.ts"), ] satisfies RouteConfig; diff --git a/app/routes/api.invoices.$id.xml.ts b/app/routes/api.invoices.$id.xml.ts new file mode 100644 index 0000000..9740bc3 --- /dev/null +++ b/app/routes/api.invoices.$id.xml.ts @@ -0,0 +1,156 @@ +import { getApiUser } from "@/session.server"; +import prisma from "@/lib/prisma.server"; + +const UNIT_C62 = "C62" as const; +const UNIT_HUR = "HUR" as const; +const UNIT_DAY = "DAY" as const; +const UNIT_MON = "MON" as const; +const UNIT_ANN = "ANN" as const; +const UNIT_KMT = "KMT" as const; +type UnitCode = typeof UNIT_C62 | typeof UNIT_HUR | typeof UNIT_DAY | typeof UNIT_MON | typeof UNIT_ANN | typeof UNIT_KMT; + +function toUnitCode(unit: string | null | undefined): UnitCode { + if (!unit) return UNIT_C62; + const u = unit.toLowerCase().trim(); + if (u === "stunde" || u === "stunden" || u === "h" || u === "std" || u === "hour") return UNIT_HUR; + if (u === "tag" || u === "tage" || u === "day" || u === "days") return UNIT_DAY; + if (u === "monat" || u === "monate" || u === "month") return UNIT_MON; + if (u === "jahr" || u === "jahre" || u === "year") return UNIT_ANN; + if (u === "km") return UNIT_KMT; + return UNIT_C62; +} + +export async function loader({ request, params }: { request: Request; params: { id: string } }) { + const user = await getApiUser(request); + if (!user) return Response.json({ error: "Unauthorized" }, { status: 401 }); + + const invoice = await prisma.invoice.findFirst({ + where: { id: params.id, company: { userId: user.id } }, + include: { + items: { orderBy: { position: "asc" } }, + customer: true, + company: true, + }, + }); + + if (!invoice) return Response.json({ error: "Not found" }, { status: 404 }); + + const { zugferd } = await import("node-zugferd"); + const { EN16931 } = await import("node-zugferd/profile"); + + const z = zugferd({ profile: EN16931, strict: false }); + + const isKleinunternehmer = invoice.kleinunternehmer; + const netTotal = Number(invoice.netTotal); + const taxTotal = Number(invoice.taxTotal); + const grossTotal = Number(invoice.grossTotal); + + // Group taxes by rate for vatBreakdown + const taxGroups: Record = {}; + for (const item of invoice.items) { + const rate = Number(item.taxRate); + if (!taxGroups[rate]) taxGroups[rate] = { basis: 0, tax: 0 }; + taxGroups[rate].basis += Number(item.netAmount); + taxGroups[rate].tax += Number(item.taxAmount); + } + + const vatBreakdown = isKleinunternehmer + ? [ + { + calculatedAmount: 0, + typeCode: "VAT", + basisAmount: netTotal, + categoryCode: "E" as const, + rateApplicablePercent: 0, + exemptionReasonText: "Steuerbefreiung gemäß §19 Abs. 1 UStG", + }, + ] + : Object.entries(taxGroups).map(([rate, { basis, tax }]) => ({ + calculatedAmount: tax, + typeCode: "VAT", + basisAmount: basis, + categoryCode: "S" as const, + rateApplicablePercent: Number(rate), + })); + + const lines = invoice.items.map((item) => ({ + tradeProduct: { name: item.description }, + tradeDelivery: { + billedQuantity: { + amount: Number(item.quantity), + unitMeasureCode: toUnitCode(item.unit), + }, + }, + tradeAgreement: { + netTradePrice: { chargeAmount: Number(item.unitPrice) }, + }, + tradeSettlement: { + tradeTax: { + typeCode: "VAT", + categoryCode: (isKleinunternehmer ? "E" : "S") as "E" | "S", + rateApplicablePercent: isKleinunternehmer ? 0 : Number(item.taxRate), + }, + monetarySummation: { lineTotalAmount: Number(item.netAmount) }, + }, + })); + + const doc = z.create({ + number: invoice.number ?? invoice.id, + typeCode: "380", + issueDate: invoice.issueDate, + transaction: { + tradeAgreement: { + seller: { + name: invoice.company.name, + postalAddress: { + ...(invoice.company.zip ? { postCode: invoice.company.zip } : {}), + ...(invoice.company.address ? { line1: invoice.company.address } : {}), + ...(invoice.company.city ? { city: invoice.company.city } : {}), + countryCode: "DE", + }, + taxRegistration: { + ...(invoice.company.vatId ? { vatIdentifier: invoice.company.vatId } : {}), + ...(invoice.company.taxId ? { localIdentifier: invoice.company.taxId } : {}), + }, + }, + buyer: { + name: invoice.customer.name, + postalAddress: { + ...(invoice.customer.zip ? { postCode: invoice.customer.zip } : {}), + ...(invoice.customer.address ? { line1: invoice.customer.address } : {}), + ...(invoice.customer.city ? { city: invoice.customer.city } : {}), + countryCode: "DE", + }, + }, + }, + tradeDelivery: { + ...(invoice.deliveryDate + ? { information: { deliveryDate: invoice.deliveryDate } } + : {}), + }, + tradeSettlement: { + currencyCode: "EUR", + paymentTerms: { dueDate: invoice.dueDate }, + vatBreakdown, + monetarySummation: { + lineTotalAmount: netTotal, + taxBasisTotalAmount: netTotal, + taxTotal: { amount: taxTotal, currencyCode: "EUR" as const }, + grandTotalAmount: grossTotal, + duePayableAmount: grossTotal, + }, + }, + line: lines, + }, + }); + + const xml = await doc.toXML(); + + return new Response(xml as string, { + status: 200, + headers: { + "Content-Type": "application/xml; charset=utf-8", + "Content-Disposition": `attachment; filename="rechnung-${invoice.number ?? invoice.id}.xml"`, + }, + }); +} diff --git a/app/routes/companies.$id.invoices.$invoiceId.tsx b/app/routes/companies.$id.invoices.$invoiceId.tsx index 27e2a61..8b5c67a 100644 --- a/app/routes/companies.$id.invoices.$invoiceId.tsx +++ b/app/routes/companies.$id.invoices.$invoiceId.tsx @@ -168,16 +168,24 @@ export default function InvoiceDetailPage() { * It then creates a blob URL from the response and creates a new anchor element with the blob URL and a download attribute with the filename. * It then simulates a click event on the anchor element, so the user is prompted to download the PDF file. */ - async function downloadPdf() { - const res = await fetch(`/api/invoices/${invoice.id}/pdf`); + async function downloadFile(url: string, filename: string) { + const res = await fetch(url); if (!res.ok) return; const blob = await res.blob(); - const url = URL.createObjectURL(blob); + const objectUrl = URL.createObjectURL(blob); const a = document.createElement("a"); - a.href = url; - a.download = `rechnung-${invoice.number ?? invoice.id}.pdf`; + a.href = objectUrl; + a.download = filename; a.click(); - URL.revokeObjectURL(url); + URL.revokeObjectURL(objectUrl); + } + + function downloadPdf() { + return downloadFile(`/api/invoices/${invoice.id}/pdf`, `rechnung-${invoice.number ?? invoice.id}.pdf`); + } + + function downloadXml() { + return downloadFile(`/api/invoices/${invoice.id}/xml`, `rechnung-${invoice.number ?? invoice.id}.xml`); } return ( @@ -203,9 +211,14 @@ export default function InvoiceDetailPage() { {/* Invoice Actions */}
{invoice.status !== "DELETED" && ( - + <> + + + )} {invoice.status === "DRAFT" && (
-
-
- - -
-
- - - {errors.unitPrice &&

{errors.unitPrice.message}

} -
+
+ + + {errors.unitPrice &&

{errors.unitPrice.message}

}
@@ -127,7 +119,7 @@ function ServiceForm({ ); } -type SortKey = "name" | "description" | "unit" | "unitPrice" | "taxRate"; +type SortKey = "name" | "description" | "unitPrice" | "taxRate"; type SortDir = "asc" | "desc"; export default function LeistungenPage() { @@ -220,7 +212,6 @@ export default function LeistungenPage() { defaultValues={{ name: editService.name, description: editService.description ?? undefined, - unit: editService.unit ?? undefined, unitPrice: editService.unitPrice, taxRate: editService.taxRate, }} @@ -244,8 +235,8 @@ export default function LeistungenPage() { - {(["name", "description", "unit", "unitPrice", "taxRate"] as SortKey[]).map((key, i) => { - const labels: Record = { name: "Bezeichnung", description: "Beschreibung", unit: "Einheit", unitPrice: "Preis", taxRate: "MwSt." }; + {(["name", "description", "unitPrice", "taxRate"] as SortKey[]).map((key, i) => { + const labels: Record = { name: "Bezeichnung", description: "Beschreibung", unitPrice: "Preis", taxRate: "MwSt." }; const isNum = key === "unitPrice" || key === "taxRate"; const active = sortKey === key; const Icon = active ? (sortDir === "asc" ? ChevronUp : ChevronDown) : ChevronsUpDown; @@ -270,7 +261,6 @@ export default function LeistungenPage() { -
{service.name} {service.description ?? "-"}{service.unit ?? "-"} {formatCurrency(service.unitPrice)} {service.taxRate}% diff --git a/package-lock.json b/package-lock.json index e217ec5..30f4886 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,7 @@ "isbot": "^5", "lucide-react": "^0.577.0", "node-cron": "^4.2.1", + "node-zugferd": "^0.1.1-beta.1", "prisma": "^5.22.0", "react": "^19", "react-dom": "^19", @@ -1288,6 +1289,22 @@ "@tybys/wasm-util": "^0.10.0" } }, + "node_modules/@pdf-lib/standard-fonts": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@pdf-lib/standard-fonts/-/standard-fonts-1.0.0.tgz", + "integrity": "sha512-hU30BK9IUN/su0Mn9VdlVKsWBS6GyhVfqjwl1FjZN4TxP6cCw0jP2w7V3Hf5uX7M0AZJ16vey9yE0ny7Sa59ZA==", + "dependencies": { + "pako": "^1.0.6" + } + }, + "node_modules/@pdf-lib/upng": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@pdf-lib/upng/-/upng-1.0.1.tgz", + "integrity": "sha512-dQK2FUMQtowVP00mtIksrlZhdFXQZPC+taih1q4CvPZ5vqdxR/LKBaFg0oAfzd1GlHZXXSPdQfzQnt+ViGvEIQ==", + "dependencies": { + "pako": "^1.0.10" + } + }, "node_modules/@prisma/client": { "version": "5.22.0", "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.22.0.tgz", @@ -3954,6 +3971,11 @@ "dev": true, "license": "MIT" }, + "node_modules/defu": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==" + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -4419,6 +4441,23 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-xml-parser": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.4.tgz", + "integrity": "sha512-jE8ugADnYOBsu1uaoayVl1tVKAMNOXyjwvv2U6udEA2ORBhDooJDWoGxTkhd4Qn4yh59JVVt/pKXtjPwx9OguQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "dependencies": { + "strnum": "^1.0.5" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -5503,6 +5542,28 @@ "dev": true, "license": "MIT" }, + "node_modules/node-zugferd": { + "version": "0.1.1-beta.1", + "resolved": "https://registry.npmjs.org/node-zugferd/-/node-zugferd-0.1.1-beta.1.tgz", + "integrity": "sha512-FTX2SMSl2qT0C0iwfggdYcwoPBndTyqP7SiQyW9ClkLFwTPTm4ZUSpn4eqwBcrc2wFGLrEtr+w+s7602JCcG6Q==", + "dependencies": { + "defu": "^6.1.4", + "fast-xml-parser": "^4.5.1", + "pdf-lib": "^1.17.1", + "zod": "^3.24.1" + }, + "optionalDependencies": { + "xsd-schema-validator": "^0.10.0" + } + }, + "node_modules/node-zugferd/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/normalize-svg-path": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/normalize-svg-path/-/normalize-svg-path-1.1.0.tgz", @@ -5678,6 +5739,22 @@ "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", "dev": true }, + "node_modules/pdf-lib": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/pdf-lib/-/pdf-lib-1.17.1.tgz", + "integrity": "sha512-V/mpyJAoTsN4cnP31vc0wfNA1+p20evqqnap0KLoRUN0Yk/p3wN52DOEsL4oBFcLdb76hlpKPtzJIgo67j/XLw==", + "dependencies": { + "@pdf-lib/standard-fonts": "^1.0.0", + "@pdf-lib/upng": "^1.0.1", + "pako": "^1.0.11", + "tslib": "^1.11.1" + } + }, + "node_modules/pdf-lib/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -6357,6 +6434,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strnum": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz", + "integrity": "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ] + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -6885,6 +6973,43 @@ "node": ">=0.10.0" } }, + "node_modules/xsd-schema-validator": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/xsd-schema-validator/-/xsd-schema-validator-0.10.0.tgz", + "integrity": "sha512-G1GtYp9Smww5D9U3QJy/uMeoaDlEYg5BR4qZYSBZWa/5TG5az2j3Np27uLKaRcg6ajwe3Ew6SJrAo3B/QFrgdg==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "which": "^5.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/xsd-schema-validator/node_modules/isexe": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", + "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/xsd-schema-validator/node_modules/which": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "optional": true, + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/package.json b/package.json index aad0c5b..0d435a0 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "isbot": "^5", "lucide-react": "^0.577.0", "node-cron": "^4.2.1", + "node-zugferd": "^0.1.1-beta.1", "prisma": "^5.22.0", "react": "^19", "react-dom": "^19",