From 65de3294fbc299ad25ee4354c430d8848eb0a932 Mon Sep 17 00:00:00 2001 From: Lucas Orth Date: Mon, 5 Oct 2026 09:45:18 +0200 Subject: [PATCH] Board: "PDFs + Adressen" laedt Sammel-PDF + Adress-Excel in gleicher Reihenfolge - produktion: Button (vorher "PDFs drucken") laedt charge__.pdf und .xlsx mit gleichem Namen; Fehlertexte auch aus Blob-Antworten lesbar - skrift-orders: Proxy POST /skrift-orders/charge/xlsx -> Backend /api/charge/merge-xlsx - Reihenfolge von Sammel-PDF und Kuverts unveraendert; die Excel folgt exakt dem PDF Braucht Backend >= v1.0.32. Co-Authored-By: Claude Opus 5.5 --- .../dist/index.js | 27 ++++++++++++++ .../dist/index.js | 37 ++++++++++++++----- 2 files changed, 54 insertions(+), 10 deletions(-) diff --git a/extensions/directus-extension-skrift-orders/dist/index.js b/extensions/directus-extension-skrift-orders/dist/index.js index dba878a..c9fd774 100644 --- a/extensions/directus-extension-skrift-orders/dist/index.js +++ b/extensions/directus-extension-skrift-orders/dist/index.js @@ -1233,6 +1233,33 @@ const handler = (router, { services, getSchema, logger }) => { } }); + // Excel-Export der Adressen zum Sammel-PDF (gleiche Reihenfolge wie charge/pdf). + router.post('/charge/xlsx', async (req, res) => { + const acc = req.accountability; + if (!acc || !acc.user || !(acc.admin || acc.app)) return res.status(403).json({ error: 'Kein Zugriff.' }); + const orderIds = Array.isArray(req.body?.orderIds) ? req.body.orderIds.filter((v) => v != null) : []; + if (!orderIds.length) return res.status(400).json({ error: 'orderIds (Array) erforderlich.' }); + try { + const r = await fetch(`${BACKEND_URL}/api/charge/merge-xlsx`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'X-API-Token': BACKEND_TOKEN }, + body: JSON.stringify({ orderIds }), + }); + if (!r.ok) { + let msg = 'Excel-Export fehlgeschlagen.'; + try { msg = (await r.json())?.message || msg; } catch { /* ignore */ } + return res.status(r.status === 400 || r.status === 404 ? r.status : 502).json({ error: msg }); + } + const buf = Buffer.from(await r.arrayBuffer()); + res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); + res.setHeader('Content-Disposition', 'attachment; filename="charge.xlsx"'); + return res.send(buf); + } catch (e) { + logger.warn(`[skrift-orders] charge/xlsx: ${e.message}`); + return res.status(502).json({ error: 'Backend nicht erreichbar.' }); + } + }); + // ── Druckauftrag einreihen (manueller Trigger aus dem Admin) ─────────────── // Legt pro Typ (Schriftstück / Umschlag) einen Job mit aufgelöstem Template // für die gewählte Maschine an. Kein Statuswechsel am Auftrag (bewusst). diff --git a/extensions/directus-extension-skrift-produktion/dist/index.js b/extensions/directus-extension-skrift-produktion/dist/index.js index bf70dfb..b87b4f7 100644 --- a/extensions/directus-extension-skrift-produktion/dist/index.js +++ b/extensions/directus-extension-skrift-produktion/dist/index.js @@ -664,20 +664,37 @@ const Board = defineComponent({ .localeCompare(String(byId.get(String(b))?.order_number || ''), undefined, { numeric: true })); } - // Sammel-PDF der aktuell ausgewählten Aufträge (mehrere Chargen kombiniert). + // Sammel-PDF der aktuell ausgewählten Aufträge (mehrere Chargen kombiniert) + // plus Excel mit allen Adressen in derselben Reihenfolge (gleicher Dateiname). + const blobDownload = (blob, name) => { + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); a.href = url; a.download = name; + document.body.appendChild(a); a.click(); a.remove(); + setTimeout(() => URL.revokeObjectURL(url), 5000); + }; + // Fehlertext aus einer Blob-Antwort (responseType 'blob' liefert auch Fehler als Blob). + const blobFehler = async (e, fallback) => { + const d = e?.response?.data; + if (d instanceof Blob) { try { return JSON.parse(await d.text())?.error || fallback; } catch { return fallback; } } + return d?.error || e?.message || fallback; + }; async function chargePdf() { const ids = auswahlSortiert(); if (!ids.length) { fehler.value = 'Keine Aufträge ausgewählt.'; return; } - meldung.value = 'Sammel-PDF wird erstellt …'; + const jetzt = new Date(); + const p2 = (n) => String(n).padStart(2, '0'); + const basis = `charge_${jetzt.getFullYear()}-${p2(jetzt.getMonth() + 1)}-${p2(jetzt.getDate())}_${p2(jetzt.getHours())}${p2(jetzt.getMinutes())}`; + meldung.value = 'Sammel-PDF + Adress-Excel werden erstellt …'; try { - const r = await api.post('/skrift-orders/charge/pdf', { orderIds: ids }, { responseType: 'blob' }); - const url = URL.createObjectURL(r.data); - const a = document.createElement('a'); a.href = url; a.download = 'charge.pdf'; - document.body.appendChild(a); a.click(); a.remove(); - setTimeout(() => URL.revokeObjectURL(url), 5000); - flash(`Sammel-PDF erstellt (${ids.length} Auftrag/Aufträge).`); + const [rPdf, rXlsx] = await Promise.all([ + api.post('/skrift-orders/charge/pdf', { orderIds: ids }, { responseType: 'blob' }), + api.post('/skrift-orders/charge/xlsx', { orderIds: ids }, { responseType: 'blob' }), + ]); + blobDownload(rPdf.data, `${basis}.pdf`); + blobDownload(rXlsx.data, `${basis}.xlsx`); + flash(`Sammel-PDF + Adress-Excel erstellt (${ids.length} Auftrag/Aufträge).`); } catch (e) { - fehler.value = e?.response?.data?.error || e?.message || 'Sammel-PDF fehlgeschlagen.'; meldung.value = ''; + fehler.value = await blobFehler(e, 'Sammel-PDF / Excel fehlgeschlagen.'); meldung.value = ''; } } @@ -2223,7 +2240,7 @@ const Board = defineComponent({ const bulkBar = auswahl.value.length ? h('div', { style: `display:flex;align-items:center;gap:12px;flex-wrap:wrap;background:${ACCENT};color:#fff;padding:12px 16px;border-radius:12px;margin-bottom:12px;` }, [ h('span', { style: 'font-weight:500;font-size:15px;' }, `${auswahl.value.length} ausgewählt`), h('button', { type: 'button', onClick: openBulk, style: bulkWBtn }, [icon('print'), 'In Druck geben']), - h('button', { type: 'button', onClick: chargePdf, style: bulkWBtn }, [icon('picture_as_pdf'), 'PDFs drucken']), + h('button', { type: 'button', onClick: chargePdf, style: bulkWBtn }, [icon('picture_as_pdf'), 'PDFs + Adressen']), h('button', { type: 'button', onClick: () => regenerieren(auswahlSortiert()), style: bulkWBtn }, [icon('autorenew'), 'Neu generieren']), h('select', { style: `cursor:pointer;border:none;background:#fff;color:#1E50A5;border-radius:8px;padding:9px 12px;font:inherit;font-size:14px;font-weight:500;`,