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:
@@ -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).
|
||||||
|
|||||||
@@ -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;`,
|
||||||
|
|||||||
Reference in New Issue
Block a user