Batch-Reiter Umbau: Uebernahme in Produktion, Tag-Gruppen, Auswahl, Versendet, Loeschen

Uebermittlungen-Reiter neu: nach Tag gruppiert, Mehrfachauswahl (Strg/
Shift, board-weit auch fuer Auftragskarten). Aktionen auf Auswahl:
- "In Produktion uebernehmen" -> neuer /skrift-orders/batch-uebernehmen:
  PDF -> Datei-Auftrag (PDF = Druckdatei), Text -> Handschrift-Brief
  (Text + 5-Zeilen-Adresse), Kuvert mit Empfaenger; Status uebernommen;
  Generierung angestossen. Danach in "Neu" (dort wie bisher buendeln).
- "Versendet" / "Nicht versendet" (neues Feld batch_submissions.versendet).
- "Loeschen" (mit Rueckfrage).
FTP-"Jetzt buendeln"-Knopf entfernt (FTP-Weg entfaellt). Bootstrap:
Status 'uebernommen' + Feld versendet.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
s4luorth
2026-09-28 10:15:18 +02:00
parent 2aa6f008bf
commit 208d398034
3 changed files with 198 additions and 50 deletions

View File

@@ -1388,6 +1388,80 @@ const handler = (router, { services, getSchema, logger }) => {
}
});
// ── Batch-Übermittlungen in die Produktion übernehmen (Reiter „Neu") ───────
// Ausgewählte batch_submissions werden Aufträge: PDF → Datei-Auftrag (die PDF
// ist die Druckdatei, source_file), Text → Handschrift-Brief (Text + 5-Zeilen-
// Adresse). Kuvert mit Empfängeradresse. Danach Generierung + Status uebernommen.
router.post('/batch-uebernehmen', async (req, res) => {
const acc = req.accountability;
if (!acc || !acc.user || !(acc.admin || acc.app)) return res.status(403).json({ error: 'Kein Zugriff.' });
const ids = Array.isArray(req.body?.ids) ? req.body.ids : [];
if (!ids.length) return res.status(400).json({ error: 'Keine Übermittlungen gewählt.' });
try {
const schema = await getSchema();
const svc = (c) => new ItemsService(c, { schema, accountability: null });
const { settings } = await loadPricingContext(schema);
const clean = makeSanitizer(settings.char_whitelist || '\\x20-\\x7E');
const dateiProd = (await svc('products').readByQuery({ filter: { input_mode: { _eq: 'datei' }, active: { _eq: true } }, limit: 1, fields: ['id'] }))?.[0];
let briefProd = (await svc('products').readByQuery({ filter: { type: { _eq: 'letter' }, active: { _eq: true } }, limit: 1, sort: ['sort'], fields: ['id'] }))?.[0];
if (!briefProd) briefProd = (await svc('products').readByQuery({ filter: { input_mode: { _neq: 'datei' }, active: { _eq: true } }, limit: 1, sort: ['sort'], fields: ['id'] }))?.[0];
const fmtA4 = (await svc('formats').readByQuery({ filter: { key: { _eq: 'a4' } }, limit: 1, fields: ['id'] }))?.[0]?.id ?? null;
const subs = await svc('batch_submissions').readMany(ids, {
fields: ['id', 'firma', 'vorname', 'nachname', 'zeile1', 'zeile2', 'zeile3', 'plz', 'stadt', 'land', 'text', 'pdf_file', 'status', 'customer'],
});
const naechsteNummer = async () => {
const jt = new Date();
const praefix = `${String(jt.getDate()).padStart(2, '0')}-${String(jt.getMonth() + 1).padStart(2, '0')}-${String(jt.getFullYear()).slice(2)}`;
const heutige = await svc('orders').readByQuery({ filter: { order_number: { _starts_with: `${praefix}-` } }, limit: -1, fields: ['id'] });
return `${praefix}-${String((heutige?.length ?? 0) + 1).padStart(3, '0')}`;
};
const angelegt = [];
const uebersprungen = [];
for (const s of subs || []) {
if (!s || s.status !== 'pending') { uebersprungen.push({ id: s?.id, grund: 'nicht offen' }); continue; }
const istPdf = !!s.pdf_file;
const product = istPdf ? dateiProd : briefProd;
if (!product) { uebersprungen.push({ id: s.id, grund: istPdf ? 'kein Datei-Produkt' : 'kein Brief-Produkt' }); continue; }
const adr = [s.firma, [s.vorname, s.nachname].filter(Boolean).join(' '), s.zeile1, s.zeile2, s.zeile3, [s.plz, s.stadt].filter(Boolean).join(' '), s.land]
.map((x) => String(x || '').trim()).filter(Boolean).slice(0, 5);
const orderData = {
customer: s.customer || null,
product: product.id, format: fmtA4,
person_type: 'unternehmen', font: 'tilda',
source: 'operator', status: 'in_queue', production_status: 'eingegangen',
payment_status: 'bezahlt', payment_method: 'rechnung', shipping_type: 'sammel',
needs_envelope: true, envelope_labeling: 'empfaenger', envelope_format: 'dinlang',
text_template: istPdf ? null : (clean(s.text) || null),
source_file: istPdf ? s.pdf_file : null,
source_files: istPdf ? [s.pdf_file] : null,
entries_count: 1, count_inland: 1, count_ausland: 0,
multi_page: !istPdf,
};
let orderId, orderNumber;
for (let v = 0; v < 6; v += 1) {
orderNumber = await naechsteNummer();
try { orderId = await svc('orders').createOne({ order_number: orderNumber, ...orderData }); break; }
catch (e) { if (v === 5 || !/unique|duplicate|bereits|exists/i.test(String(e?.message || ''))) throw e; }
}
await svc('order_entries').createOne({ order: orderId, letter_number: 1, free_text: adr.join('\n') || null, country: String(s.land || '').trim() || null });
await svc('status_history').createOne({ order: orderId, status: 'eingegangen', note: 'Aus Batch-Übermittlung übernommen.' }).catch(() => {});
await svc('batch_submissions').updateOne(s.id, { status: 'uebernommen', bundled_at: new Date().toISOString(), bundle_name: orderNumber });
fetch(`${BACKEND_URL}/api/order/from-directus`, {
method: 'POST', headers: { 'Content-Type': 'application/json', 'X-API-Token': BACKEND_TOKEN }, body: JSON.stringify({ orderId }),
}).catch((e) => logger.warn(`[skrift-orders] batch-uebernehmen Generierung: ${e.message}`));
angelegt.push({ id: s.id, order_number: orderNumber });
}
return res.json({ ok: true, created: angelegt.length, orders: angelegt, skipped: uebersprungen });
} catch (err) {
logger.error(`[skrift-orders] batch-uebernehmen: ${err.stack || err.message}`);
return res.status(500).json({ error: 'Übernahme fehlgeschlagen.', detail: String(err.message || err).slice(0, 300) });
}
});
// ── Testdruck: beliebiges Template + Text generieren und einreihen ─────────
const TEST_FORMAT = { a4: 'a4', a6_hoch: 'a6p', a6_quer: 'a6l' };
router.post('/test-print', async (req, res) => {