Konto: echte "Rechnung downloaden"-Funktion neben "Rechnung ansehen"

Neuer Button lädt die Rechnung als echte PDF-Datei herunter — komplett
clientseitig mit jsPDF + jspdf-autotable (beide MIT-lizenziert, per
dynamic import erst beim Klick nachgeladen, kein Einfluss auf die normale
Seitenladezeit). Zeigt exakt dieselben Daten wie der "Rechnung ansehen"-
Dialog (Rechnungsnummer/-datum, Verkäufer, Empfänger, Artikeltabelle,
Gesamtbetrag, §19-UStG-Hinweis), sauber formatiert mit Kopfzeile, zwei
Spalten für Verkäufer/Empfänger und einer echten Tabelle. Datei heißt
"Rechnung-<Bestellnummer>.pdf". Mit Node-Testskript verifiziert, dass die
PDF-Erzeugung fehlerfrei ein valides PDF mit korrekten Umlauten liefert.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
qcigano
2026-08-03 14:34:21 +02:00
co-authored by Claude Sonnet 5
parent e523d6f0e5
commit d7ba1b9bff
8 changed files with 521 additions and 18 deletions
+247 -1
View File
@@ -9,7 +9,9 @@
"version": "0.0.1", "version": "0.0.1",
"dependencies": { "dependencies": {
"@astrojs/sitemap": "^3.7.3", "@astrojs/sitemap": "^3.7.3",
"astro": "^7.1.6" "astro": "^7.1.6",
"jspdf": "^4.2.1",
"jspdf-autotable": "^5.0.8"
}, },
"engines": { "engines": {
"node": ">=22.12.0" "node": ">=22.12.0"
@@ -303,6 +305,15 @@
"node": ">=6.0.0" "node": ">=6.0.0"
} }
}, },
"node_modules/@babel/runtime": {
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz",
"integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/types": { "node_modules/@babel/types": {
"version": "7.29.8", "version": "7.29.8",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz",
@@ -2015,6 +2026,19 @@
"undici-types": "~7.18.0" "undici-types": "~7.18.0"
} }
}, },
"node_modules/@types/pako": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.4.tgz",
"integrity": "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==",
"license": "MIT"
},
"node_modules/@types/raf": {
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz",
"integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==",
"license": "MIT",
"optional": true
},
"node_modules/@types/sax": { "node_modules/@types/sax": {
"version": "1.2.7", "version": "1.2.7",
"resolved": "https://registry.npmjs.org/@types/sax/-/sax-1.2.7.tgz", "resolved": "https://registry.npmjs.org/@types/sax/-/sax-1.2.7.tgz",
@@ -2024,6 +2048,13 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"node_modules/@types/trusted-types": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"license": "MIT",
"optional": true
},
"node_modules/@types/unist": { "node_modules/@types/unist": {
"version": "3.0.3", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
@@ -2197,12 +2228,42 @@
"url": "https://github.com/sponsors/wooorm" "url": "https://github.com/sponsors/wooorm"
} }
}, },
"node_modules/base64-arraybuffer": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">= 0.6.0"
}
},
"node_modules/boolbase": { "node_modules/boolbase": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
"license": "ISC" "license": "ISC"
}, },
"node_modules/canvg": {
"version": "3.0.11",
"resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.11.tgz",
"integrity": "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==",
"license": "MIT",
"optional": true,
"dependencies": {
"@babel/runtime": "^7.12.5",
"@types/raf": "^3.4.0",
"core-js": "^3.8.3",
"raf": "^3.4.1",
"regenerator-runtime": "^0.13.7",
"rgbcolor": "^1.0.1",
"stackblur-canvas": "^2.0.0",
"svg-pathdata": "^6.0.3"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/ccount": { "node_modules/ccount": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz",
@@ -2319,6 +2380,18 @@
"integrity": "sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw==", "integrity": "sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/core-js": {
"version": "3.49.0",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.49.0.tgz",
"integrity": "sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/core-js"
}
},
"node_modules/crossws": { "node_modules/crossws": {
"version": "0.3.5", "version": "0.3.5",
"resolved": "https://registry.npmjs.org/crossws/-/crossws-0.3.5.tgz", "resolved": "https://registry.npmjs.org/crossws/-/crossws-0.3.5.tgz",
@@ -2328,6 +2401,16 @@
"uncrypto": "^0.1.3" "uncrypto": "^0.1.3"
} }
}, },
"node_modules/css-line-break": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
"license": "MIT",
"optional": true,
"dependencies": {
"utrie": "^1.0.2"
}
},
"node_modules/css-select": { "node_modules/css-select": {
"version": "5.2.2", "version": "5.2.2",
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz",
@@ -2513,6 +2596,16 @@
"url": "https://github.com/fb55/domhandler?sponsor=1" "url": "https://github.com/fb55/domhandler?sponsor=1"
} }
}, },
"node_modules/dompurify": {
"version": "3.4.12",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.12.tgz",
"integrity": "sha512-zQvGet8Z2sWbQhCmfFz/T5QWH2oBmjnqK3qvOjaqaNLrLEF912WamU+ohnTp0TCep/MFVHpdJuCZEdFOdTnEFg==",
"license": "(MPL-2.0 OR Apache-2.0)",
"optional": true,
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
}
},
"node_modules/domutils": { "node_modules/domutils": {
"version": "3.2.2", "version": "3.2.2",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
@@ -2613,6 +2706,17 @@
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/fast-png": {
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/fast-png/-/fast-png-6.4.0.tgz",
"integrity": "sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q==",
"license": "MIT",
"dependencies": {
"@types/pako": "^2.0.3",
"iobuffer": "^5.3.2",
"pako": "^2.1.0"
}
},
"node_modules/fast-string-truncated-width": { "node_modules/fast-string-truncated-width": {
"version": "3.0.3", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz", "resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz",
@@ -2654,6 +2758,12 @@
} }
} }
}, },
"node_modules/fflate": {
"version": "0.8.3",
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz",
"integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==",
"license": "MIT"
},
"node_modules/flattie": { "node_modules/flattie": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/flattie/-/flattie-1.1.1.tgz", "resolved": "https://registry.npmjs.org/flattie/-/flattie-1.1.1.tgz",
@@ -2856,12 +2966,32 @@
"url": "https://github.com/sponsors/wooorm" "url": "https://github.com/sponsors/wooorm"
} }
}, },
"node_modules/html2canvas": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
"license": "MIT",
"optional": true,
"dependencies": {
"css-line-break": "^2.1.0",
"text-segmentation": "^1.0.3"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/http-cache-semantics": { "node_modules/http-cache-semantics": {
"version": "4.2.0", "version": "4.2.0",
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz",
"integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==",
"license": "BSD-2-Clause" "license": "BSD-2-Clause"
}, },
"node_modules/iobuffer": {
"version": "5.4.0",
"resolved": "https://registry.npmjs.org/iobuffer/-/iobuffer-5.4.0.tgz",
"integrity": "sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==",
"license": "MIT"
},
"node_modules/iron-webcrypto": { "node_modules/iron-webcrypto": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/iron-webcrypto/-/iron-webcrypto-1.2.1.tgz", "resolved": "https://registry.npmjs.org/iron-webcrypto/-/iron-webcrypto-1.2.1.tgz",
@@ -2926,6 +3056,32 @@
"integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/jspdf": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/jspdf/-/jspdf-4.2.1.tgz",
"integrity": "sha512-YyAXyvnmjTbR4bHQRLzex3CuINCDlQnBqoSYyjJwTP2x9jDLuKDzy7aKUl0hgx3uhcl7xzg32agn5vlie6HIlQ==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.28.6",
"fast-png": "^6.2.0",
"fflate": "^0.8.1"
},
"optionalDependencies": {
"canvg": "^3.0.11",
"core-js": "^3.6.0",
"dompurify": "^3.3.1",
"html2canvas": "^1.0.0-rc.5"
}
},
"node_modules/jspdf-autotable": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/jspdf-autotable/-/jspdf-autotable-5.0.8.tgz",
"integrity": "sha512-Hy05N86yBO7CXBrnSLOge7i1ZYpKH2DjQ94iybaP7vBhSInjvRBgDc99ngKzSbSO8Jc98ZCally8I6n0tj2RJQ==",
"license": "MIT",
"peerDependencies": {
"jspdf": "^2 || ^3 || ^4"
}
},
"node_modules/lightningcss": { "node_modules/lightningcss": {
"version": "1.33.0", "version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz",
@@ -3510,6 +3666,22 @@
"integrity": "sha512-yQA4H19AmPEoMUeavPMDIe1higySl/gH/yaQrkT/s07Qp+7pp2hYz30N3z2l5BkjVkF9Ow6o0wjJamm2y7Sn0A==", "integrity": "sha512-yQA4H19AmPEoMUeavPMDIe1higySl/gH/yaQrkT/s07Qp+7pp2hYz30N3z2l5BkjVkF9Ow6o0wjJamm2y7Sn0A==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/pako": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/pako/-/pako-2.2.0.tgz",
"integrity": "sha512-zJq6RP/5q+TO2OpFV3FHzlPnFjmkb7Nc99a5SNjJE+uu/PkpChs+NIZSSzbBoD+6kjiISXjfYdwj1ZRQ81dz/w==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/puzrin"
},
{
"type": "github",
"url": "https://github.com/sponsors/nodeca"
}
],
"license": "(MIT AND Zlib)"
},
"node_modules/parse5": { "node_modules/parse5": {
"version": "7.3.0", "version": "7.3.0",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
@@ -3522,6 +3694,13 @@
"url": "https://github.com/inikulin/parse5?sponsor=1" "url": "https://github.com/inikulin/parse5?sponsor=1"
} }
}, },
"node_modules/performance-now": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
"integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==",
"license": "MIT",
"optional": true
},
"node_modules/piccolore": { "node_modules/piccolore": {
"version": "0.1.3", "version": "0.1.3",
"resolved": "https://registry.npmjs.org/piccolore/-/piccolore-0.1.3.tgz", "resolved": "https://registry.npmjs.org/piccolore/-/piccolore-0.1.3.tgz",
@@ -3608,6 +3787,16 @@
"integrity": "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==", "integrity": "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/raf": {
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz",
"integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==",
"license": "MIT",
"optional": true,
"dependencies": {
"performance-now": "^2.1.0"
}
},
"node_modules/readdirp": { "node_modules/readdirp": {
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz",
@@ -3621,6 +3810,13 @@
"url": "https://paulmillr.com/funding/" "url": "https://paulmillr.com/funding/"
} }
}, },
"node_modules/regenerator-runtime": {
"version": "0.13.11",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
"license": "MIT",
"optional": true
},
"node_modules/regex": { "node_modules/regex": {
"version": "6.1.0", "version": "6.1.0",
"resolved": "https://registry.npmjs.org/regex/-/regex-6.1.0.tgz", "resolved": "https://registry.npmjs.org/regex/-/regex-6.1.0.tgz",
@@ -3669,6 +3865,16 @@
"url": "https://opencollective.com/unified" "url": "https://opencollective.com/unified"
} }
}, },
"node_modules/rgbcolor": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz",
"integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==",
"license": "MIT OR SEE LICENSE IN FEEL-FREE.md",
"optional": true,
"engines": {
"node": ">= 0.8.15"
}
},
"node_modules/rolldown": { "node_modules/rolldown": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.1.tgz", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.1.tgz",
@@ -3871,6 +4077,16 @@
"url": "https://github.com/sponsors/wooorm" "url": "https://github.com/sponsors/wooorm"
} }
}, },
"node_modules/stackblur-canvas": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz",
"integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=0.1.14"
}
},
"node_modules/stream-replace-string": { "node_modules/stream-replace-string": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/stream-replace-string/-/stream-replace-string-2.0.0.tgz", "resolved": "https://registry.npmjs.org/stream-replace-string/-/stream-replace-string-2.0.0.tgz",
@@ -3891,6 +4107,16 @@
"url": "https://github.com/sponsors/wooorm" "url": "https://github.com/sponsors/wooorm"
} }
}, },
"node_modules/svg-pathdata": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz",
"integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/svgo": { "node_modules/svgo": {
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/svgo/-/svgo-4.0.2.tgz", "resolved": "https://registry.npmjs.org/svgo/-/svgo-4.0.2.tgz",
@@ -3916,6 +4142,16 @@
"url": "https://opencollective.com/svgo" "url": "https://opencollective.com/svgo"
} }
}, },
"node_modules/text-segmentation": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
"license": "MIT",
"optional": true,
"dependencies": {
"utrie": "^1.0.2"
}
},
"node_modules/tiny-inflate": { "node_modules/tiny-inflate": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz",
@@ -4201,6 +4437,16 @@
} }
} }
}, },
"node_modules/utrie": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
"license": "MIT",
"optional": true,
"dependencies": {
"base64-arraybuffer": "^1.0.2"
}
},
"node_modules/vfile": { "node_modules/vfile": {
"version": "6.0.3", "version": "6.0.3",
"resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz",
+3 -1
View File
@@ -13,6 +13,8 @@
}, },
"dependencies": { "dependencies": {
"@astrojs/sitemap": "^3.7.3", "@astrojs/sitemap": "^3.7.3",
"astro": "^7.1.6" "astro": "^7.1.6",
"jspdf": "^4.2.1",
"jspdf-autotable": "^5.0.8"
} }
} }
+4 -4
View File
@@ -233,7 +233,7 @@ export const ui = {
invoiceTitle: "Rechnung", invoiceNumberLabel: "Rechnungsnummer", invoiceDateLabel: "Rechnungsdatum", invoiceTitle: "Rechnung", invoiceNumberLabel: "Rechnungsnummer", invoiceDateLabel: "Rechnungsdatum",
invoiceSellerLabel: "Verkäufer", invoiceBillToLabel: "Rechnungsempfänger", invoiceSellerLabel: "Verkäufer", invoiceBillToLabel: "Rechnungsempfänger",
invoiceItemLabel: "Artikel", invoiceQtyLabel: "Menge", invoiceUnitPriceLabel: "Einzelpreis", invoiceSumLabel: "Gesamtbetrag", invoiceItemLabel: "Artikel", invoiceQtyLabel: "Menge", invoiceUnitPriceLabel: "Einzelpreis", invoiceSumLabel: "Gesamtbetrag",
invoiceCloseBtn: "Schließen", invoiceCloseBtn: "Schließen", invoiceDownloadBtn: "Rechnung downloaden", invoiceDownloading: "Wird erstellt …",
statusBezahlt: "Bezahlt", statusBearbeitung: "In Bearbeitung", statusVersandVorbereitet: "Versand vorbereitet", statusVersendet: "Versendet", statusAbgeschlossen: "Abgeschlossen", statusBezahlt: "Bezahlt", statusBearbeitung: "In Bearbeitung", statusVersandVorbereitet: "Versand vorbereitet", statusVersendet: "Versendet", statusAbgeschlossen: "Abgeschlossen",
wishlistTitle: "🩵 Meine Wunschliste", wishlistEmpty: "Deine Wunschliste ist noch leer — stöbere im Shop und speichere deine Lieblingsstücke mit dem Herz-Symbol.", wishlistTitle: "🩵 Meine Wunschliste", wishlistEmpty: "Deine Wunschliste ist noch leer — stöbere im Shop und speichere deine Lieblingsstücke mit dem Herz-Symbol.",
recentTitle: "🕘 Zuletzt angesehen", recentTitle: "🕘 Zuletzt angesehen",
@@ -471,7 +471,7 @@ export const ui = {
invoiceTitle: "Invoice", invoiceNumberLabel: "Invoice number", invoiceDateLabel: "Invoice date", invoiceTitle: "Invoice", invoiceNumberLabel: "Invoice number", invoiceDateLabel: "Invoice date",
invoiceSellerLabel: "Seller", invoiceBillToLabel: "Billed to", invoiceSellerLabel: "Seller", invoiceBillToLabel: "Billed to",
invoiceItemLabel: "Item", invoiceQtyLabel: "Qty", invoiceUnitPriceLabel: "Unit price", invoiceSumLabel: "Total amount", invoiceItemLabel: "Item", invoiceQtyLabel: "Qty", invoiceUnitPriceLabel: "Unit price", invoiceSumLabel: "Total amount",
invoiceCloseBtn: "Close", invoiceCloseBtn: "Close", invoiceDownloadBtn: "Download invoice", invoiceDownloading: "Preparing …",
statusBezahlt: "Paid", statusBearbeitung: "In progress", statusVersandVorbereitet: "Preparing shipment", statusVersendet: "Shipped", statusAbgeschlossen: "Completed", statusBezahlt: "Paid", statusBearbeitung: "In progress", statusVersandVorbereitet: "Preparing shipment", statusVersendet: "Shipped", statusAbgeschlossen: "Completed",
wishlistTitle: "🩵 My wishlist", wishlistEmpty: "Your wishlist is still empty — browse the shop and save your favourites with the heart icon.", wishlistTitle: "🩵 My wishlist", wishlistEmpty: "Your wishlist is still empty — browse the shop and save your favourites with the heart icon.",
recentTitle: "🕘 Recently viewed", recentTitle: "🕘 Recently viewed",
@@ -709,7 +709,7 @@ export const ui = {
invoiceTitle: "Rächnig", invoiceNumberLabel: "Rächnigsnummere", invoiceDateLabel: "Rächnigsdatum", invoiceTitle: "Rächnig", invoiceNumberLabel: "Rächnigsnummere", invoiceDateLabel: "Rächnigsdatum",
invoiceSellerLabel: "Verchöifer", invoiceBillToLabel: "Rächnigsempfänger", invoiceSellerLabel: "Verchöifer", invoiceBillToLabel: "Rächnigsempfänger",
invoiceItemLabel: "Artikel", invoiceQtyLabel: "Mengi", invoiceUnitPriceLabel: "Ystückspreis", invoiceSumLabel: "Gsamtbetrag", invoiceItemLabel: "Artikel", invoiceQtyLabel: "Mengi", invoiceUnitPriceLabel: "Ystückspreis", invoiceSumLabel: "Gsamtbetrag",
invoiceCloseBtn: "Schlüsse", invoiceCloseBtn: "Schlüsse", invoiceDownloadBtn: "Rächnig downloade", invoiceDownloading: "Wird erstellt …",
statusBezahlt: "Zahlt", statusBearbeitung: "I dr Bearbeitig", statusVersandVorbereitet: "Versand vorbereitet", statusVersendet: "Verschickt", statusAbgeschlossen: "Abgschlosse", statusBezahlt: "Zahlt", statusBearbeitung: "I dr Bearbeitig", statusVersandVorbereitet: "Versand vorbereitet", statusVersendet: "Verschickt", statusAbgeschlossen: "Abgschlosse",
wishlistTitle: "🩵 Mini Wunschlischte", wishlistEmpty: "Dini Wunschlischte isch no leer — lueg im Shop verbi und spicher dini Lieblingssächeli mit em Härz-Symbol.", wishlistTitle: "🩵 Mini Wunschlischte", wishlistEmpty: "Dini Wunschlischte isch no leer — lueg im Shop verbi und spicher dini Lieblingssächeli mit em Härz-Symbol.",
recentTitle: "🕘 Zletscht aagluegt", recentTitle: "🕘 Zletscht aagluegt",
@@ -947,7 +947,7 @@ export const ui = {
invoiceTitle: "Facture", invoiceNumberLabel: "Numéro de facture", invoiceDateLabel: "Date de facture", invoiceTitle: "Facture", invoiceNumberLabel: "Numéro de facture", invoiceDateLabel: "Date de facture",
invoiceSellerLabel: "Vendeur", invoiceBillToLabel: "Facturé à", invoiceSellerLabel: "Vendeur", invoiceBillToLabel: "Facturé à",
invoiceItemLabel: "Article", invoiceQtyLabel: "Qté", invoiceUnitPriceLabel: "Prix unitaire", invoiceSumLabel: "Montant total", invoiceItemLabel: "Article", invoiceQtyLabel: "Qté", invoiceUnitPriceLabel: "Prix unitaire", invoiceSumLabel: "Montant total",
invoiceCloseBtn: "Fermer", invoiceCloseBtn: "Fermer", invoiceDownloadBtn: "Télécharger la facture", invoiceDownloading: "Préparation …",
statusBezahlt: "Payée", statusBearbeitung: "En préparation", statusVersandVorbereitet: "Envoi en préparation", statusVersendet: "Expédiée", statusAbgeschlossen: "Terminée", statusBezahlt: "Payée", statusBearbeitung: "En préparation", statusVersandVorbereitet: "Envoi en préparation", statusVersendet: "Expédiée", statusAbgeschlossen: "Terminée",
wishlistTitle: "🩵 Ma liste de souhaits", wishlistEmpty: "Votre liste de souhaits est encore vide — parcourez la boutique et enregistrez vos coups de cœur avec l'icône en forme de cœur.", wishlistTitle: "🩵 Ma liste de souhaits", wishlistEmpty: "Votre liste de souhaits est encore vide — parcourez la boutique et enregistrez vos coups de cœur avec l'icône en forme de cœur.",
recentTitle: "🕘 Vus récemment", recentTitle: "🕘 Vus récemment",
+38 -3
View File
@@ -149,6 +149,16 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
<div class="order-actions"> <div class="order-actions">
{b.tracking && <a class="btn btn-outline btn-sm" href={`https://www.dhl.de/de/privatkunden/dhl-sendungsverfolgung.html?piececode=${b.tracking}`} target="_blank" rel="noopener">{t.accountDash.trackingBtn}</a>} {b.tracking && <a class="btn btn-outline btn-sm" href={`https://www.dhl.de/de/privatkunden/dhl-sendungsverfolgung.html?piececode=${b.tracking}`} target="_blank" rel="noopener">{t.accountDash.trackingBtn}</a>}
<button type="button" class="btn btn-outline btn-sm" data-open-invoice={`invoice-${b.nummer}`}>{t.accountDash.invoiceBtn}</button> <button type="button" class="btn btn-outline btn-sm" data-open-invoice={`invoice-${b.nummer}`}>{t.accountDash.invoiceBtn}</button>
<button
type="button"
class="btn btn-outline btn-sm"
data-download-invoice={b.nummer}
data-invoice-datum={b.datum}
data-invoice-summe={b.summe}
data-invoice-artikel={JSON.stringify(b.artikel.map((a) => ({ name: a.name, menge: a.menge, preis: a.preis })))}
data-label-default={t.accountDash.invoiceDownloadBtn}
data-label-loading={t.accountDash.invoiceDownloading}
>{t.accountDash.invoiceDownloadBtn}</button>
<a class="btn btn-outline btn-sm" href="/ch/shop/">{t.accountDash.reorderBtn}</a> <a class="btn btn-outline btn-sm" href="/ch/shop/">{t.accountDash.reorderBtn}</a>
</div> </div>
</div> </div>
@@ -282,8 +292,8 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
</section> </section>
</Layout> </Layout>
<script define:vars={{ loginPath: "/ch/konto/", fallbackName: "Kundin", greetingTemplate: t.accountDash.greeting("{name}"), reviewedBadgeText: t.accountDash.reviewedBadge }}> <script define:vars={{ loginPath: "/ch/konto/", fallbackName: "Kundin", greetingTemplate: t.accountDash.greeting("{name}"), reviewedBadgeText: t.accountDash.reviewedBadge , invoiceLang: lang, invoiceLabels: { title: t.accountDash.invoiceTitle, numberLabel: t.accountDash.invoiceNumberLabel, dateLabel: t.accountDash.invoiceDateLabel, sellerLabel: t.accountDash.invoiceSellerLabel, billToLabel: t.accountDash.invoiceBillToLabel, itemLabel: t.accountDash.invoiceItemLabel, qtyLabel: t.accountDash.invoiceQtyLabel, unitPriceLabel: t.accountDash.invoiceUnitPriceLabel, sumLabel: t.accountDash.invoiceSumLabel, totalLabel: t.accountDash.orderTotal, vatNote: t.common.vatNote }}}>
window.__accountDashVars = { loginPath, fallbackName, greetingTemplate, reviewedBadgeText }; window.__accountDashVars = { loginPath, fallbackName, greetingTemplate, reviewedBadgeText, invoiceLang, invoiceLabels };
</script> </script>
<script> <script>
// Schnellnavigation als echte Filter-Tabs: Klick zeigt NUR den passenden Bereich, blendet den // Schnellnavigation als echte Filter-Tabs: Klick zeigt NUR den passenden Bereich, blendet den
@@ -350,7 +360,8 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
</script> </script>
<script> <script>
import { getAccount, isLoggedIn, logout, setUsername, setPhoto, hatBestellungBewertet, anzeigeName } from "../../../scripts/account"; import { getAccount, isLoggedIn, logout, setUsername, setPhoto, hatBestellungBewertet, anzeigeName } from "../../../scripts/account";
const { loginPath, fallbackName, greetingTemplate, reviewedBadgeText } = (window as any).__accountDashVars; import { rechnungAlsPdfHerunterladen } from "../../../scripts/invoice-pdf";
const { loginPath, fallbackName, greetingTemplate, reviewedBadgeText, invoiceLang, invoiceLabels } = (window as any).__accountDashVars;
// Zugriffsschutz: diese Seite ist nur für angemeldete Kund:innen gedacht. Es gibt noch kein // Zugriffsschutz: diese Seite ist nur für angemeldete Kund:innen gedacht. Es gibt noch kein
// echtes Backend, das den Zugriff serverseitig verweigern könnte (Phase 2) — deshalb hier per // echtes Backend, das den Zugriff serverseitig verweigern könnte (Phase 2) — deshalb hier per
@@ -432,6 +443,30 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
dialog?.showModal(); dialog?.showModal();
}); });
}); });
// Rechnung als echte PDF-Datei herunterladen (siehe scripts/invoice-pdf.ts) — dieselben
// Daten wie im "Rechnung ansehen"-Dialog, per dynamic import erst beim Klick nachgeladen.
document.querySelectorAll<HTMLButtonElement>("[data-download-invoice]").forEach((btn) => {
btn.addEventListener("click", async () => {
const artikel = JSON.parse(btn.dataset.invoiceArtikel || "[]");
btn.disabled = true;
btn.textContent = btn.dataset.labelLoading || "…";
try {
await rechnungAlsPdfHerunterladen({
nummer: btn.dataset.downloadInvoice || "",
datum: btn.dataset.invoiceDatum || "",
summe: Number(btn.dataset.invoiceSumme || 0),
artikel,
empfaenger: anzeigeName(getAccount()) || fallbackName,
lang: invoiceLang,
labels: invoiceLabels,
});
} finally {
btn.disabled = false;
btn.textContent = btn.dataset.labelDefault || "";
}
});
});
document.querySelectorAll<HTMLDialogElement>(".invoice-dialog").forEach((dialog) => { document.querySelectorAll<HTMLDialogElement>(".invoice-dialog").forEach((dialog) => {
dialog.querySelectorAll<HTMLButtonElement>("[data-close-invoice]").forEach((btn) => { dialog.querySelectorAll<HTMLButtonElement>("[data-close-invoice]").forEach((btn) => {
btn.addEventListener("click", () => dialog.close()); btn.addEventListener("click", () => dialog.close());
+38 -3
View File
@@ -149,6 +149,16 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
<div class="order-actions"> <div class="order-actions">
{b.tracking && <a class="btn btn-outline btn-sm" href={`https://www.dhl.de/de/privatkunden/dhl-sendungsverfolgung.html?piececode=${b.tracking}`} target="_blank" rel="noopener">{t.accountDash.trackingBtn}</a>} {b.tracking && <a class="btn btn-outline btn-sm" href={`https://www.dhl.de/de/privatkunden/dhl-sendungsverfolgung.html?piececode=${b.tracking}`} target="_blank" rel="noopener">{t.accountDash.trackingBtn}</a>}
<button type="button" class="btn btn-outline btn-sm" data-open-invoice={`invoice-${b.nummer}`}>{t.accountDash.invoiceBtn}</button> <button type="button" class="btn btn-outline btn-sm" data-open-invoice={`invoice-${b.nummer}`}>{t.accountDash.invoiceBtn}</button>
<button
type="button"
class="btn btn-outline btn-sm"
data-download-invoice={b.nummer}
data-invoice-datum={b.datum}
data-invoice-summe={b.summe}
data-invoice-artikel={JSON.stringify(b.artikel.map((a) => ({ name: a.name, menge: a.menge, preis: a.preis })))}
data-label-default={t.accountDash.invoiceDownloadBtn}
data-label-loading={t.accountDash.invoiceDownloading}
>{t.accountDash.invoiceDownloadBtn}</button>
<a class="btn btn-outline btn-sm" href="/en/shop/">{t.accountDash.reorderBtn}</a> <a class="btn btn-outline btn-sm" href="/en/shop/">{t.accountDash.reorderBtn}</a>
</div> </div>
</div> </div>
@@ -282,8 +292,8 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
</section> </section>
</Layout> </Layout>
<script define:vars={{ loginPath: "/en/konto/", fallbackName: "Customer", greetingTemplate: t.accountDash.greeting("{name}"), reviewedBadgeText: t.accountDash.reviewedBadge }}> <script define:vars={{ loginPath: "/en/konto/", fallbackName: "Customer", greetingTemplate: t.accountDash.greeting("{name}"), reviewedBadgeText: t.accountDash.reviewedBadge , invoiceLang: lang, invoiceLabels: { title: t.accountDash.invoiceTitle, numberLabel: t.accountDash.invoiceNumberLabel, dateLabel: t.accountDash.invoiceDateLabel, sellerLabel: t.accountDash.invoiceSellerLabel, billToLabel: t.accountDash.invoiceBillToLabel, itemLabel: t.accountDash.invoiceItemLabel, qtyLabel: t.accountDash.invoiceQtyLabel, unitPriceLabel: t.accountDash.invoiceUnitPriceLabel, sumLabel: t.accountDash.invoiceSumLabel, totalLabel: t.accountDash.orderTotal, vatNote: t.common.vatNote }}}>
window.__accountDashVars = { loginPath, fallbackName, greetingTemplate, reviewedBadgeText }; window.__accountDashVars = { loginPath, fallbackName, greetingTemplate, reviewedBadgeText, invoiceLang, invoiceLabels };
</script> </script>
<script> <script>
// Schnellnavigation als echte Filter-Tabs: Klick zeigt NUR den passenden Bereich, blendet den // Schnellnavigation als echte Filter-Tabs: Klick zeigt NUR den passenden Bereich, blendet den
@@ -350,7 +360,8 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
</script> </script>
<script> <script>
import { getAccount, isLoggedIn, logout, setUsername, setPhoto, hatBestellungBewertet, anzeigeName } from "../../../scripts/account"; import { getAccount, isLoggedIn, logout, setUsername, setPhoto, hatBestellungBewertet, anzeigeName } from "../../../scripts/account";
const { loginPath, fallbackName, greetingTemplate, reviewedBadgeText } = (window as any).__accountDashVars; import { rechnungAlsPdfHerunterladen } from "../../../scripts/invoice-pdf";
const { loginPath, fallbackName, greetingTemplate, reviewedBadgeText, invoiceLang, invoiceLabels } = (window as any).__accountDashVars;
// Zugriffsschutz: diese Seite ist nur für angemeldete Kund:innen gedacht. Es gibt noch kein // Zugriffsschutz: diese Seite ist nur für angemeldete Kund:innen gedacht. Es gibt noch kein
// echtes Backend, das den Zugriff serverseitig verweigern könnte (Phase 2) — deshalb hier per // echtes Backend, das den Zugriff serverseitig verweigern könnte (Phase 2) — deshalb hier per
@@ -432,6 +443,30 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
dialog?.showModal(); dialog?.showModal();
}); });
}); });
// Rechnung als echte PDF-Datei herunterladen (siehe scripts/invoice-pdf.ts) — dieselben
// Daten wie im "Rechnung ansehen"-Dialog, per dynamic import erst beim Klick nachgeladen.
document.querySelectorAll<HTMLButtonElement>("[data-download-invoice]").forEach((btn) => {
btn.addEventListener("click", async () => {
const artikel = JSON.parse(btn.dataset.invoiceArtikel || "[]");
btn.disabled = true;
btn.textContent = btn.dataset.labelLoading || "…";
try {
await rechnungAlsPdfHerunterladen({
nummer: btn.dataset.downloadInvoice || "",
datum: btn.dataset.invoiceDatum || "",
summe: Number(btn.dataset.invoiceSumme || 0),
artikel,
empfaenger: anzeigeName(getAccount()) || fallbackName,
lang: invoiceLang,
labels: invoiceLabels,
});
} finally {
btn.disabled = false;
btn.textContent = btn.dataset.labelDefault || "";
}
});
});
document.querySelectorAll<HTMLDialogElement>(".invoice-dialog").forEach((dialog) => { document.querySelectorAll<HTMLDialogElement>(".invoice-dialog").forEach((dialog) => {
dialog.querySelectorAll<HTMLButtonElement>("[data-close-invoice]").forEach((btn) => { dialog.querySelectorAll<HTMLButtonElement>("[data-close-invoice]").forEach((btn) => {
btn.addEventListener("click", () => dialog.close()); btn.addEventListener("click", () => dialog.close());
+38 -3
View File
@@ -149,6 +149,16 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
<div class="order-actions"> <div class="order-actions">
{b.tracking && <a class="btn btn-outline btn-sm" href={`https://www.dhl.de/de/privatkunden/dhl-sendungsverfolgung.html?piececode=${b.tracking}`} target="_blank" rel="noopener">{t.accountDash.trackingBtn}</a>} {b.tracking && <a class="btn btn-outline btn-sm" href={`https://www.dhl.de/de/privatkunden/dhl-sendungsverfolgung.html?piececode=${b.tracking}`} target="_blank" rel="noopener">{t.accountDash.trackingBtn}</a>}
<button type="button" class="btn btn-outline btn-sm" data-open-invoice={`invoice-${b.nummer}`}>{t.accountDash.invoiceBtn}</button> <button type="button" class="btn btn-outline btn-sm" data-open-invoice={`invoice-${b.nummer}`}>{t.accountDash.invoiceBtn}</button>
<button
type="button"
class="btn btn-outline btn-sm"
data-download-invoice={b.nummer}
data-invoice-datum={b.datum}
data-invoice-summe={b.summe}
data-invoice-artikel={JSON.stringify(b.artikel.map((a) => ({ name: a.name, menge: a.menge, preis: a.preis })))}
data-label-default={t.accountDash.invoiceDownloadBtn}
data-label-loading={t.accountDash.invoiceDownloading}
>{t.accountDash.invoiceDownloadBtn}</button>
<a class="btn btn-outline btn-sm" href="/fr/shop/">{t.accountDash.reorderBtn}</a> <a class="btn btn-outline btn-sm" href="/fr/shop/">{t.accountDash.reorderBtn}</a>
</div> </div>
</div> </div>
@@ -282,8 +292,8 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
</section> </section>
</Layout> </Layout>
<script define:vars={{ loginPath: "/fr/konto/", fallbackName: "Cliente", greetingTemplate: t.accountDash.greeting("{name}"), reviewedBadgeText: t.accountDash.reviewedBadge }}> <script define:vars={{ loginPath: "/fr/konto/", fallbackName: "Cliente", greetingTemplate: t.accountDash.greeting("{name}"), reviewedBadgeText: t.accountDash.reviewedBadge , invoiceLang: lang, invoiceLabels: { title: t.accountDash.invoiceTitle, numberLabel: t.accountDash.invoiceNumberLabel, dateLabel: t.accountDash.invoiceDateLabel, sellerLabel: t.accountDash.invoiceSellerLabel, billToLabel: t.accountDash.invoiceBillToLabel, itemLabel: t.accountDash.invoiceItemLabel, qtyLabel: t.accountDash.invoiceQtyLabel, unitPriceLabel: t.accountDash.invoiceUnitPriceLabel, sumLabel: t.accountDash.invoiceSumLabel, totalLabel: t.accountDash.orderTotal, vatNote: t.common.vatNote }}}>
window.__accountDashVars = { loginPath, fallbackName, greetingTemplate, reviewedBadgeText }; window.__accountDashVars = { loginPath, fallbackName, greetingTemplate, reviewedBadgeText, invoiceLang, invoiceLabels };
</script> </script>
<script> <script>
// Schnellnavigation als echte Filter-Tabs: Klick zeigt NUR den passenden Bereich, blendet den // Schnellnavigation als echte Filter-Tabs: Klick zeigt NUR den passenden Bereich, blendet den
@@ -350,7 +360,8 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
</script> </script>
<script> <script>
import { getAccount, isLoggedIn, logout, setUsername, setPhoto, hatBestellungBewertet, anzeigeName } from "../../../scripts/account"; import { getAccount, isLoggedIn, logout, setUsername, setPhoto, hatBestellungBewertet, anzeigeName } from "../../../scripts/account";
const { loginPath, fallbackName, greetingTemplate, reviewedBadgeText } = (window as any).__accountDashVars; import { rechnungAlsPdfHerunterladen } from "../../../scripts/invoice-pdf";
const { loginPath, fallbackName, greetingTemplate, reviewedBadgeText, invoiceLang, invoiceLabels } = (window as any).__accountDashVars;
// Zugriffsschutz: diese Seite ist nur für angemeldete Kund:innen gedacht. Es gibt noch kein // Zugriffsschutz: diese Seite ist nur für angemeldete Kund:innen gedacht. Es gibt noch kein
// echtes Backend, das den Zugriff serverseitig verweigern könnte (Phase 2) — deshalb hier per // echtes Backend, das den Zugriff serverseitig verweigern könnte (Phase 2) — deshalb hier per
@@ -432,6 +443,30 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
dialog?.showModal(); dialog?.showModal();
}); });
}); });
// Rechnung als echte PDF-Datei herunterladen (siehe scripts/invoice-pdf.ts) — dieselben
// Daten wie im "Rechnung ansehen"-Dialog, per dynamic import erst beim Klick nachgeladen.
document.querySelectorAll<HTMLButtonElement>("[data-download-invoice]").forEach((btn) => {
btn.addEventListener("click", async () => {
const artikel = JSON.parse(btn.dataset.invoiceArtikel || "[]");
btn.disabled = true;
btn.textContent = btn.dataset.labelLoading || "…";
try {
await rechnungAlsPdfHerunterladen({
nummer: btn.dataset.downloadInvoice || "",
datum: btn.dataset.invoiceDatum || "",
summe: Number(btn.dataset.invoiceSumme || 0),
artikel,
empfaenger: anzeigeName(getAccount()) || fallbackName,
lang: invoiceLang,
labels: invoiceLabels,
});
} finally {
btn.disabled = false;
btn.textContent = btn.dataset.labelDefault || "";
}
});
});
document.querySelectorAll<HTMLDialogElement>(".invoice-dialog").forEach((dialog) => { document.querySelectorAll<HTMLDialogElement>(".invoice-dialog").forEach((dialog) => {
dialog.querySelectorAll<HTMLButtonElement>("[data-close-invoice]").forEach((btn) => { dialog.querySelectorAll<HTMLButtonElement>("[data-close-invoice]").forEach((btn) => {
btn.addEventListener("click", () => dialog.close()); btn.addEventListener("click", () => dialog.close());
+38 -3
View File
@@ -149,6 +149,16 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
<div class="order-actions"> <div class="order-actions">
{b.tracking && <a class="btn btn-outline btn-sm" href={`https://www.dhl.de/de/privatkunden/dhl-sendungsverfolgung.html?piececode=${b.tracking}`} target="_blank" rel="noopener">{t.accountDash.trackingBtn}</a>} {b.tracking && <a class="btn btn-outline btn-sm" href={`https://www.dhl.de/de/privatkunden/dhl-sendungsverfolgung.html?piececode=${b.tracking}`} target="_blank" rel="noopener">{t.accountDash.trackingBtn}</a>}
<button type="button" class="btn btn-outline btn-sm" data-open-invoice={`invoice-${b.nummer}`}>{t.accountDash.invoiceBtn}</button> <button type="button" class="btn btn-outline btn-sm" data-open-invoice={`invoice-${b.nummer}`}>{t.accountDash.invoiceBtn}</button>
<button
type="button"
class="btn btn-outline btn-sm"
data-download-invoice={b.nummer}
data-invoice-datum={b.datum}
data-invoice-summe={b.summe}
data-invoice-artikel={JSON.stringify(b.artikel.map((a) => ({ name: a.name, menge: a.menge, preis: a.preis })))}
data-label-default={t.accountDash.invoiceDownloadBtn}
data-label-loading={t.accountDash.invoiceDownloading}
>{t.accountDash.invoiceDownloadBtn}</button>
<a class="btn btn-outline btn-sm" href="/shop/">{t.accountDash.reorderBtn}</a> <a class="btn btn-outline btn-sm" href="/shop/">{t.accountDash.reorderBtn}</a>
</div> </div>
</div> </div>
@@ -282,8 +292,8 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
</section> </section>
</Layout> </Layout>
<script define:vars={{ loginPath: "/konto/", fallbackName: "Kundin", greetingTemplate: t.accountDash.greeting("{name}"), reviewedBadgeText: t.accountDash.reviewedBadge }}> <script define:vars={{ loginPath: "/konto/", fallbackName: "Kundin", greetingTemplate: t.accountDash.greeting("{name}"), reviewedBadgeText: t.accountDash.reviewedBadge , invoiceLang: lang, invoiceLabels: { title: t.accountDash.invoiceTitle, numberLabel: t.accountDash.invoiceNumberLabel, dateLabel: t.accountDash.invoiceDateLabel, sellerLabel: t.accountDash.invoiceSellerLabel, billToLabel: t.accountDash.invoiceBillToLabel, itemLabel: t.accountDash.invoiceItemLabel, qtyLabel: t.accountDash.invoiceQtyLabel, unitPriceLabel: t.accountDash.invoiceUnitPriceLabel, sumLabel: t.accountDash.invoiceSumLabel, totalLabel: t.accountDash.orderTotal, vatNote: t.common.vatNote }}}>
window.__accountDashVars = { loginPath, fallbackName, greetingTemplate, reviewedBadgeText }; window.__accountDashVars = { loginPath, fallbackName, greetingTemplate, reviewedBadgeText, invoiceLang, invoiceLabels };
</script> </script>
<script> <script>
// Schnellnavigation als echte Filter-Tabs: Klick zeigt NUR den passenden Bereich, blendet den // Schnellnavigation als echte Filter-Tabs: Klick zeigt NUR den passenden Bereich, blendet den
@@ -350,7 +360,8 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
</script> </script>
<script> <script>
import { getAccount, isLoggedIn, logout, setUsername, setPhoto, hatBestellungBewertet, anzeigeName } from "../../scripts/account"; import { getAccount, isLoggedIn, logout, setUsername, setPhoto, hatBestellungBewertet, anzeigeName } from "../../scripts/account";
const { loginPath, fallbackName, greetingTemplate, reviewedBadgeText } = (window as any).__accountDashVars; import { rechnungAlsPdfHerunterladen } from "../../scripts/invoice-pdf";
const { loginPath, fallbackName, greetingTemplate, reviewedBadgeText, invoiceLang, invoiceLabels } = (window as any).__accountDashVars;
// Zugriffsschutz: diese Seite ist nur für angemeldete Kund:innen gedacht. Es gibt noch kein // Zugriffsschutz: diese Seite ist nur für angemeldete Kund:innen gedacht. Es gibt noch kein
// echtes Backend, das den Zugriff serverseitig verweigern könnte (Phase 2) — deshalb hier per // echtes Backend, das den Zugriff serverseitig verweigern könnte (Phase 2) — deshalb hier per
@@ -432,6 +443,30 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
dialog?.showModal(); dialog?.showModal();
}); });
}); });
// Rechnung als echte PDF-Datei herunterladen (siehe scripts/invoice-pdf.ts) — dieselben
// Daten wie im "Rechnung ansehen"-Dialog, per dynamic import erst beim Klick nachgeladen.
document.querySelectorAll<HTMLButtonElement>("[data-download-invoice]").forEach((btn) => {
btn.addEventListener("click", async () => {
const artikel = JSON.parse(btn.dataset.invoiceArtikel || "[]");
btn.disabled = true;
btn.textContent = btn.dataset.labelLoading || "…";
try {
await rechnungAlsPdfHerunterladen({
nummer: btn.dataset.downloadInvoice || "",
datum: btn.dataset.invoiceDatum || "",
summe: Number(btn.dataset.invoiceSumme || 0),
artikel,
empfaenger: anzeigeName(getAccount()) || fallbackName,
lang: invoiceLang,
labels: invoiceLabels,
});
} finally {
btn.disabled = false;
btn.textContent = btn.dataset.labelDefault || "";
}
});
});
document.querySelectorAll<HTMLDialogElement>(".invoice-dialog").forEach((dialog) => { document.querySelectorAll<HTMLDialogElement>(".invoice-dialog").forEach((dialog) => {
dialog.querySelectorAll<HTMLButtonElement>("[data-close-invoice]").forEach((btn) => { dialog.querySelectorAll<HTMLButtonElement>("[data-close-invoice]").forEach((btn) => {
btn.addEventListener("click", () => dialog.close()); btn.addEventListener("click", () => dialog.close());
+115
View File
@@ -0,0 +1,115 @@
// Rechnung als echte PDF-Datei herunterladen — komplett clientseitig mit jsPDF + jspdf-autotable
// (beide MIT-lizenziert, siehe package.json), kein Backend nötig. Beide Bibliotheken werden per
// dynamic import() erst beim tatsächlichen Klick nachgeladen (Code-Splitting) — auf den ersten
// Seitenaufruf hat das also keinen Einfluss, nur wer wirklich herunterlädt lädt die ~230 KB nach.
// Spiegelt bewusst 1:1 dieselben Daten wie die Ansicht im <dialog> (siehe konto/angemeldet.astro),
// damit "Rechnung ansehen" und "Rechnung downloaden" nie auseinanderlaufen.
import { formatPrice } from "../i18n/format";
import type { Locale } from "../i18n/config";
export interface RechnungArtikel {
name: string;
menge: number;
preis: number;
}
export interface RechnungLabels {
title: string;
numberLabel: string;
dateLabel: string;
sellerLabel: string;
billToLabel: string;
itemLabel: string;
qtyLabel: string;
unitPriceLabel: string;
sumLabel: string;
totalLabel: string;
vatNote: string;
}
export interface RechnungDaten {
nummer: string;
datum: string;
artikel: RechnungArtikel[];
summe: number;
empfaenger: string;
lang: Locale;
labels: RechnungLabels;
}
export async function rechnungAlsPdfHerunterladen(daten: RechnungDaten): Promise<void> {
const [{ default: JsPDF }, autoTableModule] = await Promise.all([
import("jspdf"),
import("jspdf-autotable"),
]);
const autoTable = autoTableModule.default;
const doc = new JsPDF({ unit: "mm", format: "a4" });
const marginX = 18;
let y = 22;
// Kopf: Shop-Name groß, Rechnungstitel + Metadaten rechtsbündig daneben.
doc.setFont("helvetica", "bold");
doc.setFontSize(18);
doc.text("Van's DIY & Bastelbedarf", marginX, y);
doc.setFontSize(20);
doc.text(daten.labels.title, 210 - marginX, y, { align: "right" });
y += 8;
doc.setFont("helvetica", "normal");
doc.setFontSize(10);
doc.setTextColor(90, 90, 90);
doc.text(`${daten.labels.numberLabel}: ${daten.nummer}`, 210 - marginX, y, { align: "right" });
y += 5;
doc.text(`${daten.labels.dateLabel}: ${daten.datum}`, 210 - marginX, y, { align: "right" });
// Verkäufer / Rechnungsempfänger nebeneinander.
y += 14;
doc.setTextColor(20, 20, 20);
doc.setFont("helvetica", "bold");
doc.setFontSize(9);
doc.text(daten.labels.sellerLabel.toUpperCase(), marginX, y);
doc.text(daten.labels.billToLabel.toUpperCase(), marginX + 95, y);
y += 5;
doc.setFont("helvetica", "normal");
doc.setFontSize(10.5);
doc.text("Van's DIY & Bastelbedarf", marginX, y);
const empfaengerZeilen = [daten.empfaenger, "Musterstraße 1", "10115 Berlin", "Deutschland"];
empfaengerZeilen.forEach((zeile, i) => doc.text(zeile, marginX + 95, y + i * 5));
// Artikeltabelle.
y += empfaengerZeilen.length * 5 + 10;
autoTable(doc, {
startY: y,
head: [[daten.labels.itemLabel, daten.labels.qtyLabel, daten.labels.unitPriceLabel, daten.labels.sumLabel]],
body: daten.artikel.map((a) => [
a.name,
String(a.menge),
formatPrice(a.preis, daten.lang),
formatPrice(a.preis * a.menge, daten.lang),
]),
foot: [["", "", daten.labels.totalLabel, formatPrice(daten.summe, daten.lang)]],
theme: "plain",
margin: { left: marginX, right: marginX },
styles: { font: "helvetica", fontSize: 10, cellPadding: { top: 2.5, bottom: 2.5, left: 2, right: 2 } },
headStyles: { fontStyle: "bold", textColor: [110, 110, 110], fontSize: 8.5, lineWidth: { bottom: 0.4 }, lineColor: [200, 200, 200] },
footStyles: { fontStyle: "bold", textColor: [20, 20, 20], fontSize: 11.5, lineWidth: { top: 0.4 }, lineColor: [200, 200, 200] },
columnStyles: {
1: { halign: "right" },
2: { halign: "right" },
3: { halign: "right" },
},
});
// §19-UStG-Hinweis unter der Tabelle.
// @ts-expect-error — lastAutoTable wird vom autoTable-Plugin zur Laufzeit an doc angehängt.
const endY = doc.lastAutoTable.finalY as number;
doc.setFontSize(9);
doc.setTextColor(120, 120, 120);
doc.text(daten.labels.vatNote, marginX, endY + 10);
doc.save(`Rechnung-${daten.nummer}.pdf`);
}