diff --git a/bootstrap/bootstrap.mjs b/bootstrap/bootstrap.mjs index 406c353..30c9e18 100644 --- a/bootstrap/bootstrap.mjs +++ b/bootstrap/bootstrap.mjs @@ -915,10 +915,16 @@ async function applySeed() { { key: 'kuvert_b_v1', name: 'Kuvert C6 – Variante 1', type: 'umschlag', width: 4.49, height: 6.38, xpos: 4.2, ypos: -0.2, scale: 1.0, rotation: 90 }, { key: 'kuvert_b_v2', name: 'Kuvert C6 – Variante 2', type: 'umschlag', width: 4.33, height: 6.38, xpos: 4.0, ypos: -0.2, scale: 1.0, rotation: 90 }, ]; - for (const t of formatTemplates) { - const vorhanden = await api('GET', `/items/format_templates?filter[key][_eq]=${encodeURIComponent(t.key)}&limit=1&fields=id`); - if (vorhanden && vorhanden.length) await api('PATCH', `/items/format_templates/${vorhanden[0].id}`, t); - else { await api('POST', '/items/format_templates', t); log('seed + format_templates.' + t.key); } + // Nur bei LEERER Collection anlegen (Erstinstallation). Der Betreiber pflegt die + // Templates danach selbst (löschen/umbenennen/eigene) – Bootstrap fügt sie NICHT + // wieder hinzu. Vorhandene werden bewusst nicht überschrieben. + const ftVorhanden = await api('GET', `/items/format_templates?limit=1&fields=id`); + if (!ftVorhanden || !ftVorhanden.length) { + for (const t of formatTemplates) { + await api('POST', '/items/format_templates', t); log('seed + format_templates.' + t.key); + } + } else { + log('format_templates vorhanden → Seed übersprungen (vom Betreiber gepflegt)'); } // Rendering-Formate (zentral fürs Backend). Werte = bisherige Defaults. diff --git a/extensions/directus-extension-skrift-neuauftrag/dist/index.js b/extensions/directus-extension-skrift-neuauftrag/dist/index.js index 0bb9d11..cd8088a 100644 --- a/extensions/directus-extension-skrift-neuauftrag/dist/index.js +++ b/extensions/directus-extension-skrift-neuauftrag/dist/index.js @@ -59,11 +59,6 @@ const NeuAuftrag = defineComponent({ modus: 'auftrag', // 'auftrag' (aus Auftrag laden) oder 'manuell' (direkt hochladen) orderNr: '', order: null, docs: [], docId: null, docName: '', signaturen: [], customerEmail: '', laden: false, pdf: '', msg: '', err: '', - // Kuverts zum Unterschriftenservice (wie manueller Druck): je Empfänger ein Umschlag. - kuverts: false, kuvertFormat: 'dinlang', empfBlocks: '', - absenderAuf: false, absender: '', absenderScale: 100, - // Verknüpfung mit vorhandenem Auftrag (dessen Kuverts genutzt werden). - linkNr: '', }); // Datei nach Directus hochladen (liefert die Datei-ID). async function sigUpload(file) { @@ -768,20 +763,10 @@ const NeuAuftrag = defineComponent({ async function sigAuftrag() { if (!sig.docId) { sig.err = 'Bitte ein Dokument wählen/hochladen.'; return; } if (!sigAktive().length) { sig.err = 'Mindestens eine Unterschrift aktivieren.'; return; } - if (sig.kuverts && !sig.empfBlocks.trim()) { sig.err = 'Kuverts gewählt, aber keine Empfänger angegeben.'; return; } sig.err = ''; sig.msg = 'Auftrag wird angelegt …'; try { - const res = await api.post('/skrift-orders/signature-order', { - ...sigPayload(), customer_email: sig.customerEmail || undefined, - kuverts: sig.kuverts, - recipients_text: sig.kuverts ? sig.empfBlocks : undefined, - envelope_format: sig.kuverts ? sig.kuvertFormat : undefined, - envelope_sender: sig.kuverts && sig.absenderAuf ? sig.absender.trim() : undefined, - envelope_sender_scale: (Number(sig.absenderScale) || 100) / 100, - link_order_number: sig.linkNr.trim() || undefined, - }); - const kt = res.data.kuverts ? `, ${res.data.kuverts} Kuvert/s` : ''; - sig.msg = `✔ Auftrag ${res.data.order_number} angelegt (${res.data.pages} Seite/n${kt}). Im Produktions-Board unter „Neu" → Drucken.`; + const res = await api.post('/skrift-orders/signature-order', { ...sigPayload(), customer_email: sig.customerEmail || undefined }); + sig.msg = `✔ Auftrag ${res.data.order_number} angelegt (${res.data.pages} Seite/n). Im Produktions-Board unter „Neu" → Drucken.`; } catch (e) { sig.err = e?.response?.data?.detail || e?.response?.data?.error || e?.message || 'Auftrag konnte nicht angelegt werden.'; sig.msg = ''; } } @@ -856,44 +841,11 @@ const NeuAuftrag = defineComponent({ btn('⤓ A4-Signaturen (ZIP)', sigDownload), ]), ]); - // ── Kuverts & Verknüpfung ────────────────────────────────────────────── - const b3 = h('div', { style: BEREICH }, [ - bereichKopf('✉ Kuverts & Verknüpfung'), - subdued('Optional: adressierte Kuverts zum Unterschriftenservice erzeugen (wie beim manuellen Druck) – oder den Auftrag mit einem vorhandenen Auftrag verknüpfen, dessen Kuverts genutzt werden.', 'margin-bottom:10px;'), - h('label', { style: 'display:flex;align-items:center;gap:8px;font-size:14px;cursor:pointer;margin-bottom:8px;' }, [ - h('input', { type: 'checkbox', checked: sig.kuverts, onChange: (e) => { sig.kuverts = e.target.checked; }, style: 'width:16px;height:16px;' }), - 'Kuverts mitdrucken (je Empfänger ein Umschlag)', - ]), - sig.kuverts ? h('div', { style: 'padding-left:24px;display:flex;flex-direction:column;gap:12px;margin-bottom:12px;' }, [ - h('div', { style: 'max-width:240px;' }, [label('Kuvertformat'), sel(sig.kuvertFormat, [{ v: 'dinlang', t: 'DIN Lang' }, { v: 'c6', t: 'C6' }], (e) => { sig.kuvertFormat = e.target.value; })]), - h('div', {}, [ - label('Empfänger (ein Block je Empfänger, durch Leerzeile getrennt)'), - h('textarea', { value: sig.empfBlocks, rows: 6, placeholder: 'Max Mustermann\nMusterstr. 1\n12345 Musterstadt\n\nErika Beispiel\nBeispielweg 2\n54321 Beispielort', - style: WFELD + 'width:100%;font-family:var(--family-monospace,monospace);', onInput: (e) => { sig.empfBlocks = e.target.value; } }), - ]), - h('label', { style: 'display:flex;align-items:center;gap:8px;font-size:14px;cursor:pointer;' }, [ - h('input', { type: 'checkbox', checked: sig.absenderAuf, onChange: (e) => { sig.absenderAuf = e.target.checked; }, style: 'width:16px;height:16px;' }), - 'Absender aufs Kuvert', - ]), - sig.absenderAuf ? h('div', { style: 'display:flex;gap:12px;flex-wrap:wrap;' }, [ - h('div', { style: 'flex:1;min-width:200px;' }, [label('Absender (max. 5 Zeilen)'), - h('textarea', { value: sig.absender, rows: 5, style: WFELD + 'width:100%;', - onInput: (e) => { sig.absender = e.target.value.split('\n').slice(0, 5).join('\n'); } })]), - h('div', { style: 'width:120px;' }, [label('Größe (%)'), - h('input', { type: 'number', step: 5, min: 50, max: 200, value: sig.absenderScale, style: WFELD + 'width:100%;', onInput: (e) => { sig.absenderScale = e.target.value; } })]), - ]) : null, - ]) : null, - h('div', { style: 'max-width:360px;' }, [ - label('Mit vorhandenem Auftrag verknüpfen (Nr., optional)'), - h('input', { value: sig.linkNr, placeholder: 'z. B. 07-09-26-003', style: WFELD + 'width:100%;', onInput: (e) => { sig.linkNr = e.target.value; } }), - subdued('Dessen Kuverts werden für diesen Auftrag genutzt. Änderbar im Produktions-Board.', 'margin-top:4px;'), - ]), - ]); const meldung = h('div', {}, [ sig.err ? h('div', { style: 'color:var(--danger,#c62828);font-size:13px;margin-top:8px;' }, sig.err) : null, sig.msg ? subdued(sig.msg, 'margin-top:8px;') : null, ]); - const links = h('div', { style: 'flex:1;min-width:0;display:flex;flex-direction:column;gap:16px;' }, [b1, b2, b3, meldung]); + const links = h('div', { style: 'flex:1;min-width:0;display:flex;flex-direction:column;gap:16px;' }, [b1, b2, meldung]); const rechts = h('div', { style: BEREICH + 'flex:0 0 420px;position:sticky;top:20px;align-self:flex-start;min-height:200px;' }, [ h('div', { style: 'display:flex;justify-content:space-between;align-items:center;gap:8px;' }, [ bereichKopf('👁 Vorschau'), diff --git a/extensions/directus-extension-skrift-orders/dist/index.js b/extensions/directus-extension-skrift-orders/dist/index.js index e482840..b7c501d 100644 --- a/extensions/directus-extension-skrift-orders/dist/index.js +++ b/extensions/directus-extension-skrift-orders/dist/index.js @@ -310,20 +310,6 @@ const handler = (router, { services, getSchema, logger }) => { const pages = (await r.json())?.pages || []; if (!pages.length) return res.status(422).json({ error: 'Keine Seiten erzeugt.' }); - // Optionale Kuverts (wie manueller Druck): je Empfänger ein Umschlag. Empfänger - // als Blöcke (durch Leerzeile getrennt) im Freitext. Absender optional. - const kuvertsAn = !!body.kuverts; - const empf = kuvertsAn - ? String(body.recipients_text || '') - .split(/\n\s*\n/).map((s) => s.trim()).filter(Boolean) - .map((free_text) => ({ free_text })) - : []; - if (kuvertsAn && !empf.length) return res.status(400).json({ error: 'Kuverts gewählt, aber keine Empfänger angegeben.' }); - const envFormat = ['c6', 'dinlang'].includes(body.envelope_format) ? body.envelope_format : 'dinlang'; - const absender = String(body.envelope_sender || '').trim(); - const absenderScale = Math.max(0.5, Math.min(2, Number(body.envelope_sender_scale) || 1)); - const countAusland = empf.filter((e) => !zielInland(e)).length; - 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 naechsteNummer = async () => { @@ -334,14 +320,9 @@ const handler = (router, { services, getSchema, logger }) => { customer: customerId, customer_email: customerEmail, product: product.id, person_type: 'privat', source: 'operator', status: 'in_queue', production_status: 'eingegangen', payment_status: 'bezahlt', payment_method: 'rechnung', shipping_type: 'sammel', - needs_envelope: kuvertsAn, envelope_labeling: kuvertsAn ? 'empfaenger' : 'keine', - envelope_format: kuvertsAn ? envFormat : null, - envelope_sender: kuvertsAn && absender ? absender : null, - envelope_sender_scale: absenderScale, + needs_envelope: false, envelope_labeling: 'keine', source_file: pdfId, source_files: [pdfId], - entries_count: kuvertsAn ? empf.length : pages.length, - count_inland: kuvertsAn ? (empf.length - countAusland) : pages.length, - count_ausland: kuvertsAn ? countAusland : 0, + entries_count: pages.length, count_inland: pages.length, count_ausland: 0, }; let orderId, orderNumber; for (let v = 0; v < 6; v += 1) { @@ -351,28 +332,11 @@ const handler = (router, { services, getSchema, logger }) => { } const entriesSvc = svc('order_entries'); - if (kuvertsAn) { - // Empfänger als Zeilen – daraus erzeugt die Pipeline je einen Umschlag. - for (let i = 0; i < empf.length; i += 1) { - await entriesSvc.createOne({ order: orderId, letter_number: i + 1, free_text: empf[i].free_text }); - } - } else { - for (let i = 0; i < pages.length; i += 1) { - await entriesSvc.createOne({ order: orderId, letter_number: i + 1, free_text: `Seite ${i + 1}` }); - } + for (let i = 0; i < pages.length; i += 1) { + await entriesSvc.createOne({ order: orderId, letter_number: i + 1, free_text: `Seite ${i + 1}` }); } await svc('status_history').createOne({ order: orderId, status: 'eingegangen', note: 'Unterschriftenservice – manuell angelegt.' }).catch(() => {}); - // Verknüpfung mit vorhandenem Auftrag (dessen Kuverts genutzt werden) – optional. - if (body.link_order_number) { - try { - const linked = (await svc('orders').readByQuery({ - filter: { order_number: { _eq: String(body.link_order_number).trim() } }, limit: 1, fields: ['id'], - }))?.[0]; - if (linked) await svc('orders').updateOne(orderId, { linked_order: linked.id }); - } catch (e) { logger.warn(`[skrift-orders] signature-order Verknüpfung: ${e.message}`); } - } - // Seiten-SVGs als Schriftstück in den Auftragsordner schreiben. const ordner = path.join(OUTPUT_DIR, orderNumber); fs.mkdirSync(ordner, { recursive: true }); @@ -380,16 +344,7 @@ const handler = (router, { services, getSchema, logger }) => { fs.writeFileSync(path.join(ordner, `letter_${String(i + 1).padStart(3, '0')}.svg`), p.svg, 'utf8'); }); - // Kuverts über die Standard-Pipeline erzeugen (Datei-Produkt → NUR Umschläge; - // die Seiten-SVGs bleiben erhalten, da runGeneration den Ordner nicht leert). - if (kuvertsAn) { - 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] signature-order Kuverts nicht gestartet: ${e.message}`)); - } - - return res.json({ ok: true, order_number: orderNumber, id: orderId, pages: pages.length, kuverts: empf.length }); + return res.json({ ok: true, order_number: orderNumber, id: orderId, pages: pages.length }); } catch (err) { logger.error(`[skrift-orders] signature-order: ${err.stack || err.message}`); return res.status(500).json({ error: 'Auftrag konnte nicht angelegt werden.', detail: String(err.message || err).slice(0, 400) }); diff --git a/extensions/directus-extension-skrift-produktion/dist/index.js b/extensions/directus-extension-skrift-produktion/dist/index.js index 71bbd44..656a83a 100644 --- a/extensions/directus-extension-skrift-produktion/dist/index.js +++ b/extensions/directus-extension-skrift-produktion/dist/index.js @@ -272,6 +272,9 @@ const Board = defineComponent({ // Notizen je Charge (charge_key → Notiz) + Speichern von Auftrags-/Charge-Notizen. const chargeNotizen = ref({}); + // Ausgeklappter „Weiteres"-Bereich je Auftrag (Verknüpfen/Kontakte/Neu gen./Übernehmen). + const weiteresOffen = ref({}); + const toggleWeiteres = (id) => { weiteresOffen.value = { ...weiteresOffen.value, [id]: !weiteresOffen.value[id] }; }; async function ladeChargeNotizen() { try { const r = await api.get('/items/charge_notes', { params: { limit: -1, fields: 'charge_key,note' } }); @@ -804,8 +807,8 @@ const Board = defineComponent({ // Auftrag in den manuellen Druck (neuauftrag-Modul) übernehmen: Text, Schrift, // Format, Kuvert-Option und ALLE Empfänger (free_text + Platzhalter) per // localStorage übergeben und dorthin navigieren – dort editierbar/neu druckbar. - async function uebernehmenManuell() { - const o = empf.orderObj; + async function uebernehmenManuell(order) { + const o = order || empf.orderObj; if (!o) return; try { const full = (await api.get('/items/orders/' + o.id, { params: { @@ -914,15 +917,32 @@ const Board = defineComponent({ // ── Render-Bausteine ────────────────────────────────────────────────── function actionButtons(order) { - // Drucken + Empfänger (Empfänger enthält die Vorschau je Dokument). + // Drucken + Empfänger immer sichtbar; Rest hinter „Weiteres". + const offen = !!weiteresOffen.value[order.id]; return h('div', { style: 'display:flex;gap:8px;flex-wrap:wrap;' }, [ btn('🖨 Drucken', () => openDruck(order), 'primary'), btn('👥 Empfänger', () => openEmpf(order)), orderDateiIds(order).length ? btn('📎 Dateien', () => openDateien(order)) : null, - btn(order.keep_contacts ? '📇 In Kontakte gespeichert ✓' : '📇 In Kontakte speichern', - () => toggleKeepContacts(order), order.keep_contacts ? 'primary' : 'default'), + btn(offen ? '⚙ Weiteres ▴' : '⚙ Weiteres ▾', () => toggleWeiteres(order.id)), ].filter(Boolean)); } + // „Weiteres": Verknüpfen · In Kontakte speichern · Neu generieren · In manuellen Druck. + function weiteresPanel(o) { + return h('div', { style: 'margin-top:10px;padding:12px;border:1px solid var(--border-subdued);border-radius:8px;background:var(--background-subdued);display:flex;flex-direction:column;gap:10px;' }, [ + h('div', { style: 'display:flex;align-items:center;gap:8px;flex-wrap:wrap;' }, [ + subdued('🔗 Verknüpfter Auftrag:', ''), + h('input', { placeholder: 'Auftragsnr. (leer = keine)', value: o.linked_order?.order_number || '', + style: 'flex:1;min-width:140px;max-width:220px;border:1px solid var(--border-subdued);border-radius:6px;padding:5px 8px;font:inherit;font-size:12px;background:var(--background-page);color:var(--foreground-normal);', + onChange: (e) => setOrderLink(o, e.target.value) }), + ]), + h('div', { style: 'display:flex;gap:8px;flex-wrap:wrap;' }, [ + btn(o.keep_contacts ? '📇 In Kontakte gespeichert ✓' : '📇 In Kontakte speichern', + () => toggleKeepContacts(o), o.keep_contacts ? 'primary' : 'default'), + btn('🔄 Neu generieren', () => regenerieren(o.id), 'danger'), + btn('✍ In manuellen Druck übernehmen', () => uebernehmenManuell(o), 'primary'), + ]), + ]); + } function statusSelect(order) { return h('select', { style: S_SELECT + 'font-size:12px;padding:4px 8px;', @@ -958,15 +978,6 @@ const Board = defineComponent({ o.payment_status === 'bezahlt' ? 'var(--success-25,rgba(46,125,50,.15))' : 'var(--warning-25,rgba(230,160,0,.15))'), o.linked_order?.order_number ? chip('🔗 Kuverts: #' + o.linked_order.order_number, 'var(--primary-25,rgba(30,80,165,.14))') : null, ]); - // Verknüpfter Auftrag (Kuverts zum Unterschriftenservice) – setzen/ändern/entfernen. - const linkFeld = h('div', { style: 'display:flex;align-items:center;gap:8px;margin:2px 0 6px;flex-wrap:wrap;' }, [ - subdued('🔗 Verknüpfter Auftrag:', ''), - h('input', { - placeholder: 'Auftragsnr. (leer = keine)', value: o.linked_order?.order_number || '', - style: 'flex:1;min-width:140px;max-width:220px;border:1px solid var(--border-subdued);border-radius:6px;padding:5px 8px;font:inherit;font-size:12px;background:var(--background-page);color:var(--foreground-normal);', - onChange: (e) => setOrderLink(o, e.target.value), - }), - ]); // „Auf einen Blick": welche Nummern wurden schon in den Druck gesendet // (aus den Jobs, inkl. bereits gedruckter). Kein Status, nur die Eingabe. const send = sendIndex.value[String(o.id)]; @@ -975,7 +986,12 @@ const Board = defineComponent({ send.brief.length ? chip('🖨 Brief gesendet: ' + send.brief.join(' · '), 'var(--primary-25,rgba(30,80,165,.14))') : null, send.umschlag.length ? chip('✉ Kuvert gesendet: ' + send.umschlag.join(' · '), 'var(--primary-25,rgba(30,80,165,.14))') : null, ]) : null; - return h('div', { key: 'o' + o.id, style: CARD + `margin-bottom:14px;border-left:4px solid ${ACCENT};` }, [kopf, meta, badges, linkFeld, notizFeld(o.internal_note, (v) => setOrderNotiz(o, v), 'Notiz zum Auftrag …'), actionButtons(o)]); + return h('div', { key: 'o' + o.id, style: CARD + `margin-bottom:14px;border-left:4px solid ${ACCENT};` }, [ + kopf, meta, badges, + notizFeld(o.internal_note, (v) => setOrderNotiz(o, v), 'Notiz zum Auftrag …'), + actionButtons(o), + weiteresOffen.value[o.id] ? weiteresPanel(o) : null, + ]); } function requestCard(req) { const istMuster = req.type === 'muster'; @@ -1330,7 +1346,7 @@ const Board = defineComponent({ const uebernahme = istOrder ? h('div', { style: 'padding:12px 20px;border-bottom:1px solid var(--border-subdued);display:flex;justify-content:flex-end;align-items:center;gap:10px;flex-wrap:wrap;' }, [ subdued('Neu generieren erzeugt Schriftstück + Kuvert frisch.'), btn('🔄 Neu generieren', () => regenerieren(empf.orderObj?.id), 'danger'), - btn('✍ In manuellen Druck übernehmen', uebernehmenManuell, 'primary'), + btn('✍ In manuellen Druck übernehmen', () => uebernehmenManuell(), 'primary'), ]) : null; const body = h('div', { style: 'padding:16px 20px;display:flex;gap:20px;flex-wrap:wrap;' }, [liste, vorschau]); return overlay([modalKopf(empf.titel, () => { empf.open = false; }), uebernahme, body].filter(Boolean), () => { empf.open = false; }, istOrder ? '980px' : '620px');