From 8ad087d007dc55ff4eebe0991aba103921057bf6 Mon Sep 17 00:00:00 2001 From: s4luorth Date: Mon, 28 Sep 2026 09:23:40 +0200 Subject: [PATCH] Batch-Uebermittlung (Phase 3): Board-Reiter + Buendeln-Knopf + Schreibvorlage Produktions-Board: neuer Reiter "Uebermittlungen" (getrennt vom Auftragsboard). Offene batch_submissions je Zugang gruppiert; Knopf "Jetzt buendeln & hochladen" pro Zugang loest die Backend-Buendelung (xlsx + Sammel-PDF + FTP) sofort aus. Text-Uebermittlungen bekommen einen "Schreibvorlage"-Knopf, der Text + 5-Zeilen-Adressblock (Firma / Vorname Nachname / Zeile1-3 / PLZ Stadt / Land, max 5) in den manuellen Druck uebernimmt. skrift-orders: Proxy-Route /batch-build -> Backend. Co-Authored-By: Claude Opus 4.8 --- .../dist/index.js | 21 +++++ .../dist/index.js | 94 ++++++++++++++++++- 2 files changed, 114 insertions(+), 1 deletion(-) diff --git a/extensions/directus-extension-skrift-orders/dist/index.js b/extensions/directus-extension-skrift-orders/dist/index.js index f1ebe24..911152b 100644 --- a/extensions/directus-extension-skrift-orders/dist/index.js +++ b/extensions/directus-extension-skrift-orders/dist/index.js @@ -1367,6 +1367,27 @@ const handler = (router, { services, getSchema, logger }) => { } }); + // ── Batch-Bündelung sofort auslösen (Board-Knopf → Backend) ──────────────── + // Proxyt an das Backend, das die offenen batch_submissions dieses Zugangs zu + // xlsx + Sammel-PDF bündelt und per FTP hochlädt. client_id = api_clients-ID. + router.post('/batch-build', async (req, res) => { + const acc = req.accountability; + if (!acc || !acc.user || !(acc.admin || acc.app)) return res.status(403).json({ error: 'Kein Zugriff.' }); + try { + const r = await fetch(`${BACKEND_URL}/api/batch/build`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'X-API-Token': BACKEND_TOKEN }, + body: JSON.stringify({ client_id: req.body?.client_id ?? null }), + }); + const txt = await r.text(); + let data = null; try { data = txt ? JSON.parse(txt) : null; } catch { /* egal */ } + if (!r.ok) return res.status(502).json({ error: 'Bündelung fehlgeschlagen.', detail: (data && data.message) || String(txt).slice(0, 300) }); + return res.json(data || { ok: true }); + } catch (e) { + return res.status(500).json({ error: 'Backend nicht erreichbar.', detail: String(e.message || e).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) => { diff --git a/extensions/directus-extension-skrift-produktion/dist/index.js b/extensions/directus-extension-skrift-produktion/dist/index.js index 7778754..9178792 100644 --- a/extensions/directus-extension-skrift-produktion/dist/index.js +++ b/extensions/directus-extension-skrift-produktion/dist/index.js @@ -60,6 +60,7 @@ const TABS = [ { key: 'kuvertierung', label: '✉ Kuvertierung' }, { key: 'versandfertig', label: '📦 Versandfertig' }, { key: 'versendet', label: '🚚 Versendet' }, + { key: 'batch', label: '📦 Übermittlungen' }, { key: 'archiv', label: '🗄 Archiv' }, ]; const STATUS_LABEL = { @@ -192,6 +193,10 @@ const Board = defineComponent({ const muster = ref({ text_du: '', text_sie: '' }); // Benannte Mustertexte (Collection muster_texts) – im Muster-Dialog wählbar. const musterListe = ref([]); + // Alternativer Übermittlungsweg (batch_submissions) – offene Datensätze, + // getrennt vom normalen Auftragsboard im Reiter „Übermittlungen". + const batch = ref([]); + const batchBusy = ref(''); // ── Modale ── const druck = reactive({ open: false, order: null, machine: null, mode: 'queue', busy: false, brief: null, umschlag: null, spaltenweise: false, msg: '', err: '', deNums: '', intNums: '', deN: 0, intN: 0 }); @@ -387,7 +392,7 @@ const Board = defineComponent({ // leise = Hintergrund-Aktualisierung: kein Spinner, kein Fehlerbanner. if (!leise) { loading.value = true; fehler.value = ''; } try { - const [o, r, j, m, t, ms, mtx] = await Promise.all([ + const [o, r, j, m, t, ms, mtx, bs] = await Promise.all([ ladeOrders(), api.get('/items/offer_requests', { params: { limit: -1, sort: '-date_created', @@ -399,6 +404,11 @@ const Board = defineComponent({ api.get('/items/format_templates', { params: { limit: -1, fields: 'id,key,name,type,width,height,xpos,ypos,scale,rotation,default_format,search_default' } }).catch(() => api.get('/items/format_templates', { params: { limit: -1, fields: 'id,key,name,type,width,height,xpos,ypos,scale,rotation,default_format' } })), api.get('/items/muster_settings', { params: { fields: 'text_du,text_sie' } }).catch(() => ({ data: { data: { text_du: '', text_sie: '' } } })), api.get('/items/muster_texts', { params: { limit: -1, filter: { active: { _eq: true } }, sort: 'sort', fields: 'id,label,text' } }).catch(() => ({ data: { data: [] } })), + api.get('/items/batch_submissions', { params: { + limit: -1, sort: 'date_created', + filter: { status: { _eq: 'pending' } }, + fields: 'id,api_client.id,api_client.name,firma,vorname,nachname,zeile1,zeile2,zeile3,plz,stadt,land,text,pdf_file,status,date_created', + } }).catch(() => ({ data: { data: [] } })), ]); orders.value = o.data?.data || []; requests.value = r.data?.data || []; @@ -407,6 +417,7 @@ const Board = defineComponent({ templates.value = t.data?.data || []; muster.value = ms.data?.data || { text_du: '', text_sie: '' }; musterListe.value = mtx.data?.data || []; + batch.value = bs.data?.data || []; // Empfänger-Index nur beim vollen Laden neu bauen (nicht bei der 15-s- // Hintergrund-Aktualisierung) – im Hintergrund, blockiert das Board nicht. if (!leise) { ladeEmpfIndex(); ladeSendIndex(); ladeChargeNotizen(); } @@ -531,6 +542,7 @@ const Board = defineComponent({ if (key === 'kuvertierung') return ordersByStatus('kuvertieren').length; if (key === 'versandfertig') return ordersByStatus('versandfertig').length; if (key === 'versendet') return versendetAktiv().length; + if (key === 'batch') return batch.value.length; if (key === 'archiv') return archivListe().length; return 0; }; @@ -910,6 +922,54 @@ const Board = defineComponent({ } catch (e) { flash('Übernahme fehlgeschlagen.'); } } + // ── Batch-Übermittlungen (alternativer Weg) ────────────────────────────── + // Offene Datensätze je Zugang gruppieren. + const batchGruppen = () => { + const m = new Map(); + for (const s of batch.value) { + const cid = (s.api_client && s.api_client.id != null) ? s.api_client.id : (s.api_client ?? '—'); + const key = String(cid); + if (!m.has(key)) m.set(key, { id: cid, name: (s.api_client && s.api_client.name) || `Zugang ${cid}`, subs: [] }); + m.get(key).subs.push(s); + } + return [...m.values()]; + }; + // Sofort bündeln (xlsx + Sammel-PDF + FTP) für EINEN Zugang. + async function batchBuild(clientId, clientName) { + if (batchBusy.value) return; + batchBusy.value = String(clientId); + fehler.value = ''; + try { + const res = await api.post('/skrift-orders/batch-build', { client_id: clientId }); + const bundles = res.data?.bundles || []; + const meins = bundles.find((x) => String(x.client_id) === String(clientId)) || bundles[0] || {}; + if (meins.error) fehler.value = `Bündelung: ${meins.error}`; + else flash(`✓ ${clientName || clientId}: ${meins.count || 0} Übermittlung(en) gebündelt${meins.file ? ` → ${meins.file}.{xlsx,pdf}` : ''}.`); + await laden(); + } catch (e) { + fehler.value = e?.response?.data?.detail || e?.response?.data?.error || e?.message || 'Bündelung fehlgeschlagen.'; + } finally { batchBusy.value = ''; } + } + // Text-Übermittlung als Schreibvorlage in den manuellen Druck übernehmen. + // 5-Zeilen-Adressblock: Firma / Vorname Nachname / Zeile1-3 / PLZ Stadt / Land (leere weg, max 5). + function batchSchreibvorlage(sub) { + const zeilen = [ + sub.firma, + [sub.vorname, sub.nachname].filter(Boolean).join(' '), + sub.zeile1, sub.zeile2, sub.zeile3, + [sub.plz, sub.stadt].filter(Boolean).join(' '), + sub.land, + ].map((z) => String(z || '').trim()).filter(Boolean).slice(0, 5); + try { + localStorage.setItem('skrift_neuauftrag_uebernahme', JSON.stringify({ + text: sub.text || '', adressModus: '5zeilen', + rows: [{ free_text: zeilen.join('\n'), placeholders: {} }], + })); + const base = window.location.pathname.split('/admin/')[0] + '/admin/'; + window.location.href = base + 'skrift-neuauftrag'; + } catch (e) { flash('Übernahme fehlgeschlagen.'); } + } + // ── Empfänger-Drawer (mit Dokument-Vorschau je Zeile) ── // Zerlegt einen Dateinamen in Brief-Nummer und Seite: // letter_001_p2.svg → { nummer:1, seite:2 }, letter_003.svg → { nummer:3, seite:1 }. @@ -1348,7 +1408,39 @@ const Board = defineComponent({ return rahmen(h('div', { style: 'display:flex;flex-direction:column;gap:2px;' }, treffer.map(zeile))); } + // ── Reiter „Übermittlungen" (Batch, getrennt vom Auftragsboard) ────────── + function batchInhalt() { + const gruppen = batchGruppen(); + if (!gruppen.length) return subdued('Keine offenen Übermittlungen.'); + return gruppen.map((g) => { + const kopf = h('div', { style: 'display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap;margin-bottom:12px;' }, [ + h('div', {}, [ + h('div', { style: 'font-weight:700;font-size:15px;' }, g.name), + subdued(`Zugang #${g.id} · ${g.subs.length} offene Übermittlung(en)`), + ]), + btn(batchBusy.value === String(g.id) ? 'Bündelt …' : '📤 Jetzt bündeln & hochladen', () => batchBuild(g.id, g.name), 'primary'), + ]); + const zeilen = g.subs.map((s) => { + const person = [s.firma, [s.vorname, s.nachname].filter(Boolean).join(' ')].filter(Boolean).join(' · ') || '—'; + const ort = [s.plz, s.stadt].filter(Boolean).join(' '); + const istText = !s.pdf_file; + return h('div', { key: 'bs' + s.id, style: CARD + 'margin-bottom:8px;display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap;' }, [ + h('div', { style: 'min-width:0;' }, [ + h('div', { style: 'font-weight:600;' }, person), + subdued([ort, s.land].filter(Boolean).join(' · ') || '—'), + ]), + h('div', { style: 'display:flex;gap:8px;align-items:center;flex-wrap:wrap;' }, [ + chip(istText ? '✍ Text' : '📄 Druck-PDF'), + istText ? btn('✍ Schreibvorlage', () => batchSchreibvorlage(s)) : null, + ]), + ]); + }); + return h('div', { style: SECTION }, [kopf, h('div', { style: 'display:flex;flex-direction:column;gap:2px;' }, zeilen)]); + }); + } + function tabInhalt() { + if (tab.value === 'batch') return batchInhalt(); if (tab.value === 'suche') return sucheInhalt(); if (tab.value === 'neu') { const items = [