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:
co-authored by
Claude Sonnet 5
parent
e523d6f0e5
commit
d7ba1b9bff
+4
-4
@@ -233,7 +233,7 @@ export const ui = {
|
||||
invoiceTitle: "Rechnung", invoiceNumberLabel: "Rechnungsnummer", invoiceDateLabel: "Rechnungsdatum",
|
||||
invoiceSellerLabel: "Verkäufer", invoiceBillToLabel: "Rechnungsempfänger",
|
||||
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",
|
||||
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",
|
||||
@@ -471,7 +471,7 @@ export const ui = {
|
||||
invoiceTitle: "Invoice", invoiceNumberLabel: "Invoice number", invoiceDateLabel: "Invoice date",
|
||||
invoiceSellerLabel: "Seller", invoiceBillToLabel: "Billed to",
|
||||
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",
|
||||
wishlistTitle: "🩵 My wishlist", wishlistEmpty: "Your wishlist is still empty — browse the shop and save your favourites with the heart icon.",
|
||||
recentTitle: "🕘 Recently viewed",
|
||||
@@ -709,7 +709,7 @@ export const ui = {
|
||||
invoiceTitle: "Rächnig", invoiceNumberLabel: "Rächnigsnummere", invoiceDateLabel: "Rächnigsdatum",
|
||||
invoiceSellerLabel: "Verchöifer", invoiceBillToLabel: "Rächnigsempfänger",
|
||||
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",
|
||||
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",
|
||||
@@ -947,7 +947,7 @@ export const ui = {
|
||||
invoiceTitle: "Facture", invoiceNumberLabel: "Numéro de facture", invoiceDateLabel: "Date de facture",
|
||||
invoiceSellerLabel: "Vendeur", invoiceBillToLabel: "Facturé à",
|
||||
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",
|
||||
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",
|
||||
|
||||
@@ -149,6 +149,16 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
|
||||
<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>}
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -282,8 +292,8 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
|
||||
</section>
|
||||
</Layout>
|
||||
|
||||
<script define:vars={{ loginPath: "/ch/konto/", fallbackName: "Kundin", greetingTemplate: t.accountDash.greeting("{name}"), reviewedBadgeText: t.accountDash.reviewedBadge }}>
|
||||
window.__accountDashVars = { loginPath, fallbackName, greetingTemplate, reviewedBadgeText };
|
||||
<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, invoiceLang, invoiceLabels };
|
||||
</script>
|
||||
<script>
|
||||
// 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>
|
||||
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
|
||||
// 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();
|
||||
});
|
||||
});
|
||||
|
||||
// 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) => {
|
||||
dialog.querySelectorAll<HTMLButtonElement>("[data-close-invoice]").forEach((btn) => {
|
||||
btn.addEventListener("click", () => dialog.close());
|
||||
|
||||
@@ -149,6 +149,16 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
|
||||
<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>}
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -282,8 +292,8 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
|
||||
</section>
|
||||
</Layout>
|
||||
|
||||
<script define:vars={{ loginPath: "/en/konto/", fallbackName: "Customer", greetingTemplate: t.accountDash.greeting("{name}"), reviewedBadgeText: t.accountDash.reviewedBadge }}>
|
||||
window.__accountDashVars = { loginPath, fallbackName, greetingTemplate, reviewedBadgeText };
|
||||
<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, invoiceLang, invoiceLabels };
|
||||
</script>
|
||||
<script>
|
||||
// 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>
|
||||
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
|
||||
// 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();
|
||||
});
|
||||
});
|
||||
|
||||
// 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) => {
|
||||
dialog.querySelectorAll<HTMLButtonElement>("[data-close-invoice]").forEach((btn) => {
|
||||
btn.addEventListener("click", () => dialog.close());
|
||||
|
||||
@@ -149,6 +149,16 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
|
||||
<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>}
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -282,8 +292,8 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
|
||||
</section>
|
||||
</Layout>
|
||||
|
||||
<script define:vars={{ loginPath: "/fr/konto/", fallbackName: "Cliente", greetingTemplate: t.accountDash.greeting("{name}"), reviewedBadgeText: t.accountDash.reviewedBadge }}>
|
||||
window.__accountDashVars = { loginPath, fallbackName, greetingTemplate, reviewedBadgeText };
|
||||
<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, invoiceLang, invoiceLabels };
|
||||
</script>
|
||||
<script>
|
||||
// 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>
|
||||
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
|
||||
// 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();
|
||||
});
|
||||
});
|
||||
|
||||
// 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) => {
|
||||
dialog.querySelectorAll<HTMLButtonElement>("[data-close-invoice]").forEach((btn) => {
|
||||
btn.addEventListener("click", () => dialog.close());
|
||||
|
||||
@@ -149,6 +149,16 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
|
||||
<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>}
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -282,8 +292,8 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
|
||||
</section>
|
||||
</Layout>
|
||||
|
||||
<script define:vars={{ loginPath: "/konto/", fallbackName: "Kundin", greetingTemplate: t.accountDash.greeting("{name}"), reviewedBadgeText: t.accountDash.reviewedBadge }}>
|
||||
window.__accountDashVars = { loginPath, fallbackName, greetingTemplate, reviewedBadgeText };
|
||||
<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, invoiceLang, invoiceLabels };
|
||||
</script>
|
||||
<script>
|
||||
// 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>
|
||||
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
|
||||
// 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();
|
||||
});
|
||||
});
|
||||
|
||||
// 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) => {
|
||||
dialog.querySelectorAll<HTMLButtonElement>("[data-close-invoice]").forEach((btn) => {
|
||||
btn.addEventListener("click", () => dialog.close());
|
||||
|
||||
@@ -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`);
|
||||
}
|
||||
Reference in New Issue
Block a user