From c6c682d48f11095ecb9df9356f69b448022bcf48 Mon Sep 17 00:00:00 2001 From: s4luorth Date: Mon, 28 Sep 2026 10:24:10 +0200 Subject: [PATCH] =?UTF-8?q?Batch-Reiter:=20FTP-Upload=20(prim=C3=A4r)=20zu?= =?UTF-8?q?r=C3=BCck,=20Tag+Kunde-Gruppen=20aufklappbar?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - "Per FTP hochladen" als Primär-Aktion (wirkt auf Auswahl, ids an /skrift-orders/batch-build -> Backend), "In Produktion übernehmen" sekundär; dazu Versendet/Nicht versendet/Löschen. - Gruppierung Tag → Kunde, beide aufklappbar, Gruppen-Kopf mit Haken (wählt alle in der Gruppe). Co-Authored-By: Claude Opus 4.8 --- .../dist/index.js | 2 +- .../dist/index.js | 106 +++++++++++++++--- 2 files changed, 90 insertions(+), 18 deletions(-) diff --git a/extensions/directus-extension-skrift-orders/dist/index.js b/extensions/directus-extension-skrift-orders/dist/index.js index 23bd008..502d45b 100644 --- a/extensions/directus-extension-skrift-orders/dist/index.js +++ b/extensions/directus-extension-skrift-orders/dist/index.js @@ -1377,7 +1377,7 @@ const handler = (router, { services, getSchema, logger }) => { 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 }), + body: JSON.stringify({ client_id: req.body?.client_id ?? null, ids: Array.isArray(req.body?.ids) ? req.body.ids : null }), }); const txt = await r.text(); let data = null; try { data = txt ? JSON.parse(txt) : null; } catch { /* egal */ } diff --git a/extensions/directus-extension-skrift-produktion/dist/index.js b/extensions/directus-extension-skrift-produktion/dist/index.js index b763b60..0c35f99 100644 --- a/extensions/directus-extension-skrift-produktion/dist/index.js +++ b/extensions/directus-extension-skrift-produktion/dist/index.js @@ -198,6 +198,7 @@ const Board = defineComponent({ const batch = ref([]); const batchBusy = ref(''); const batchAuswahl = ref([]); + const batchOffen = ref({}); // Gruppen-Aufklappstatus (Default: aufgeklappt) // ── 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 }); @@ -985,6 +986,53 @@ const Board = defineComponent({ fehler.value = e?.response?.data?.detail || e?.response?.data?.error || e?.message || 'Übernahme fehlgeschlagen.'; } finally { batchBusy.value = ''; } } + // Ausgewählte per FTP hochladen (xlsx + Sammel-Druck-PDF). Nur PDF-Übermittlungen. + async function batchFtp() { + const ids = [...batchAuswahl.value]; + if (!ids.length || batchBusy.value) return; + batchBusy.value = 'ftp'; fehler.value = ''; + try { + const res = await api.post('/skrift-orders/batch-build', { ids }); + const bundles = res.data?.bundles || []; + const errs = bundles.filter((b) => b.error); + const total = bundles.reduce((s, b) => s + (b.count || 0), 0); + const files = bundles.filter((b) => b.file).map((b) => b.file).join(', '); + if (errs.length) fehler.value = `FTP: ${errs.map((e) => e.error).join('; ')}`; + else { flash(`✓ ${total} Übermittlung(en) per FTP hochgeladen${files ? ` → ${files}.{xlsx,pdf}` : ''}.`); batchAuswahlLeeren(); } + await laden(); + } catch (e) { + fehler.value = e?.response?.data?.detail || e?.response?.data?.error || e?.message || 'FTP-Upload fehlgeschlagen.'; + } finally { batchBusy.value = ''; } + } + // Gruppen (Tag/Kunde): Aufklappen + Kopf-Auswahl. + const istOffen = (k) => batchOffen.value[k] !== false; + const toggleOffen = (k) => { batchOffen.value = { ...batchOffen.value, [k]: !istOffen(k) }; }; + const batchGruppeGewaehlt = (subs) => subs.length > 0 && subs.every((s) => batchGewaehlt(s.id)); + function batchGruppeToggle(subs) { + const alle = batchGruppeGewaehlt(subs); + const set = new Set(batchAuswahl.value); + subs.forEach((s) => (alle ? set.delete(String(s.id)) : set.add(String(s.id)))); + batchAuswahl.value = [...set]; + } + // Baum: Tag → Kunde → Übermittlungen (neueste Tage zuerst). + const batchBaum = () => { + const tage = new Map(); + for (const s of batch.value) { + const tag = String(s.date_created || '').slice(0, 10) || '—'; + const cid = (s.api_client && s.api_client.id != null) ? s.api_client.id : (s.api_client ?? '—'); + const cname = (s.api_client && s.api_client.name) || `Zugang ${cid}`; + if (!tage.has(tag)) tage.set(tag, new Map()); + const kunden = tage.get(tag); + const ck = String(cid); + if (!kunden.has(ck)) kunden.set(ck, { id: cid, name: cname, subs: [] }); + kunden.get(ck).subs.push(s); + } + return [...tage.entries()].sort((a, b) => String(b[0]).localeCompare(String(a[0]))) + .map(([tag, kunden]) => { + const ks = [...kunden.values()]; + return { tag, subsAll: ks.flatMap((k) => k.subs), kunden: ks }; + }); + }; // Ausgewählte als versendet / nicht versendet markieren. async function batchVersendet(val) { const ids = [...batchAuswahl.value]; @@ -1469,46 +1517,70 @@ const Board = defineComponent({ return rahmen(h('div', { style: 'display:flex;flex-direction:column;gap:2px;' }, treffer.map(zeile))); } - // ── Reiter „Übermittlungen" (Batch, nach Tag gruppiert, mit Auswahl) ────── + // ── Reiter „Übermittlungen" (Batch, Tag → Kunde, aufklappbar, Auswahl) ──── function batchInhalt() { - const gruppen = batchGruppenTag(); + const baum = batchBaum(); const auswahlN = batchAuswahl.value.length; - // Aktionsleiste (nur bei Auswahl) – Übernehmen / Versendet / Löschen. + // Aktionsleiste (nur bei Auswahl): FTP primär, Übernehmen sekundär. + const wbtn = (label, on, primary) => h('button', { type: 'button', onClick: on, disabled: !!batchBusy.value, + style: 'cursor:pointer;border-radius:7px;padding:6px 14px;font:inherit;font-size:13px;font-weight:600;' + + (primary ? `border:none;background:#fff;color:${ACCENT};` : 'border:1px solid rgba(255,255,255,.6);background:transparent;color:#fff;') }, label); const leiste = auswahlN ? h('div', { style: `display:flex;align-items:center;gap:10px;flex-wrap:wrap;background:${ACCENT};color:#fff;padding:10px 16px;border-radius:8px;margin-bottom:14px;` }, [ h('span', { style: 'font-weight:600;' }, `${auswahlN} ausgewählt`), - h('button', { type: 'button', onClick: batchUebernehmen, disabled: !!batchBusy.value, style: `cursor:pointer;border:none;background:#fff;color:${ACCENT};border-radius:7px;padding:6px 14px;font:inherit;font-size:13px;font-weight:600;` }, batchBusy.value === 'uebernehmen' ? 'Übernimmt …' : '➡ In Produktion übernehmen'), - h('button', { type: 'button', onClick: () => batchVersendet(true), disabled: !!batchBusy.value, style: 'cursor:pointer;border:1px solid rgba(255,255,255,.6);background:transparent;color:#fff;border-radius:7px;padding:6px 12px;font:inherit;font-size:13px;' }, '✓ Versendet'), - h('button', { type: 'button', onClick: () => batchVersendet(false), disabled: !!batchBusy.value, style: 'cursor:pointer;border:1px solid rgba(255,255,255,.6);background:transparent;color:#fff;border-radius:7px;padding:6px 12px;font:inherit;font-size:13px;' }, '✗ Nicht versendet'), - h('button', { type: 'button', onClick: batchLoeschen, disabled: !!batchBusy.value, style: 'cursor:pointer;border:1px solid rgba(255,255,255,.6);background:transparent;color:#fff;border-radius:7px;padding:6px 12px;font:inherit;font-size:13px;' }, '🗑 Löschen'), - h('button', { type: 'button', onClick: batchAuswahlLeeren, style: 'cursor:pointer;border:none;background:transparent;color:#fff;border-radius:7px;padding:6px 8px;font:inherit;font-size:13px;text-decoration:underline;' }, 'Auswahl aufheben'), - ]) : subdued('Übermittlungen auswählen (Strg/Shift für mehrere), dann übernehmen/markieren/löschen.', 'margin-bottom:12px;'); + wbtn(batchBusy.value === 'ftp' ? 'Lädt hoch …' : '📤 Per FTP hochladen', batchFtp, true), + wbtn(batchBusy.value === 'uebernehmen' ? 'Übernimmt …' : '➡ In Produktion übernehmen', batchUebernehmen, false), + wbtn('✓ Versendet', () => batchVersendet(true), false), + wbtn('✗ Nicht versendet', () => batchVersendet(false), false), + wbtn('🗑 Löschen', batchLoeschen, false), + h('button', { type: 'button', onClick: batchAuswahlLeeren, style: 'cursor:pointer;border:none;background:transparent;color:#fff;font:inherit;font-size:13px;text-decoration:underline;' }, 'Auswahl aufheben'), + ]) : subdued('Übermittlungen auswählen (Strg/Shift für mehrere, oder Gruppen-Haken), dann per FTP hochladen / übernehmen / markieren / löschen.', 'margin-bottom:12px;'); - if (!gruppen.length) return [leiste, subdued('Keine offenen Übermittlungen.')]; + if (!baum.length) return [leiste, subdued('Keine offenen Übermittlungen.')]; + + const pfeil = (offen) => h('span', { style: 'display:inline-block;width:14px;font-size:12px;color:var(--foreground-subdued);' }, offen ? '▾' : '▸'); + const gruppenHaken = (subs) => h('input', { type: 'checkbox', checked: batchGruppeGewaehlt(subs), onClick: (e) => { e.stopPropagation(); batchGruppeToggle(subs); }, style: 'width:16px;height:16px;flex-shrink:0;' }); const zeile = (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; const gew = batchGewaehlt(s.id); - const zugang = (s.api_client && s.api_client.name) || (s.api_client ? `Zugang ${s.api_client.id ?? s.api_client}` : ''); - return h('div', { key: 'bs' + s.id, style: CARD + 'margin-bottom:8px;display:flex;align-items:center;gap:12px;flex-wrap:wrap;' + (gew ? `border-color:${ACCENT};` : '') }, [ + return h('div', { key: 'bs' + s.id, style: CARD + 'margin:6px 0 6px 26px;display:flex;align-items:center;gap:12px;flex-wrap:wrap;' + (gew ? `border-color:${ACCENT};` : '') }, [ h('input', { type: 'checkbox', checked: gew, onClick: (e) => batchToggle(s.id, e), style: 'width:16px;height:16px;flex-shrink:0;' }), h('div', { style: 'min-width:0;flex:1;' }, [ 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;' }, [ - zugang ? subdued(zugang) : null, chip(istText ? '✍ Text' : '📄 Druck-PDF'), s.versendet ? chip('✓ versendet', 'var(--success-25,rgba(46,125,50,.15))') : null, istText ? btn('✍ Schreibvorlage', () => batchSchreibvorlage(s)) : null, ]), ]); }; - const bloecke = gruppen.map(([tag, subs]) => h('div', { style: SECTION }, [ - h('div', { style: 'font-weight:700;font-size:14px;color:' + ACCENT + ';margin-bottom:10px;' }, `📅 ${tag} · ${subs.length} Übermittlung(en)`), - h('div', { style: 'display:flex;flex-direction:column;gap:2px;' }, subs.map(zeile)), - ])); + + const bloecke = baum.map((tg) => { + const tagKey = 'd:' + tg.tag; + const tagOffen = istOffen(tagKey); + const tagKopf = h('div', { style: 'display:flex;align-items:center;gap:10px;margin-bottom:8px;' }, [ + gruppenHaken(tg.subsAll), + h('div', { style: 'display:flex;align-items:center;gap:6px;cursor:pointer;font-weight:700;font-size:15px;color:' + ACCENT + ';', onClick: () => toggleOffen(tagKey) }, [ + pfeil(tagOffen), h('span', {}, `📅 ${tg.tag}`), subdued(`· ${tg.subsAll.length} Übermittlung(en)`), + ]), + ]); + const kundenBloecke = tagOffen ? tg.kunden.map((k) => { + const kKey = `c:${tg.tag}:${k.id}`; + const kOffen = istOffen(kKey); + const kKopf = h('div', { style: 'display:flex;align-items:center;gap:10px;margin:10px 0 4px 20px;' }, [ + gruppenHaken(k.subs), + h('div', { style: 'display:flex;align-items:center;gap:6px;cursor:pointer;font-weight:600;', onClick: () => toggleOffen(kKey) }, [ + pfeil(kOffen), h('span', {}, `👤 ${k.name}`), subdued(`· ${k.subs.length}`), + ]), + ]); + return h('div', {}, [kKopf, kOffen ? h('div', {}, k.subs.map(zeile)) : null]); + }) : null; + return h('div', { style: SECTION }, [tagKopf, kundenBloecke]); + }); return [leiste, ...bloecke]; }