Board: "PDFs + Adressen" laedt Sammel-PDF + Adress-Excel in gleicher Reihenfolge

- produktion: Button (vorher "PDFs drucken") laedt charge_<Datum>_<HHMM>.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 <noreply@anthropic.com>
This commit is contained in:
2026-10-05 09:45:18 +02:00
parent 5196e30343
commit 65de3294fb
2 changed files with 54 additions and 10 deletions

View File

@@ -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) ─────────────── // ── Druckauftrag einreihen (manueller Trigger aus dem Admin) ───────────────
// Legt pro Typ (Schriftstück / Umschlag) einen Job mit aufgelöstem Template // Legt pro Typ (Schriftstück / Umschlag) einen Job mit aufgelöstem Template
// für die gewählte Maschine an. Kein Statuswechsel am Auftrag (bewusst). // für die gewählte Maschine an. Kein Statuswechsel am Auftrag (bewusst).

View File

@@ -664,20 +664,37 @@ const Board = defineComponent({
.localeCompare(String(byId.get(String(b))?.order_number || ''), undefined, { numeric: true })); .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() { async function chargePdf() {
const ids = auswahlSortiert(); const ids = auswahlSortiert();
if (!ids.length) { fehler.value = 'Keine Aufträge ausgewählt.'; return; } 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 { try {
const r = await api.post('/skrift-orders/charge/pdf', { orderIds: ids }, { responseType: 'blob' }); const [rPdf, rXlsx] = await Promise.all([
const url = URL.createObjectURL(r.data); api.post('/skrift-orders/charge/pdf', { orderIds: ids }, { responseType: 'blob' }),
const a = document.createElement('a'); a.href = url; a.download = 'charge.pdf'; api.post('/skrift-orders/charge/xlsx', { orderIds: ids }, { responseType: 'blob' }),
document.body.appendChild(a); a.click(); a.remove(); ]);
setTimeout(() => URL.revokeObjectURL(url), 5000); blobDownload(rPdf.data, `${basis}.pdf`);
flash(`Sammel-PDF erstellt (${ids.length} Auftrag/Aufträge).`); blobDownload(rXlsx.data, `${basis}.xlsx`);
flash(`Sammel-PDF + Adress-Excel erstellt (${ids.length} Auftrag/Aufträge).`);
} catch (e) { } 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;` }, [ 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('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: 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('button', { type: 'button', onClick: () => regenerieren(auswahlSortiert()), style: bulkWBtn }, [icon('autorenew'), 'Neu generieren']),
h('select', { 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;`, style: `cursor:pointer;border:none;background:#fff;color:#1E50A5;border-radius:8px;padding:9px 12px;font:inherit;font-size:14px;font-weight:500;`,