From de612b713f19570fb2ebfb69cae1343ed39becb4 Mon Sep 17 00:00:00 2001 From: s4luorth Date: Mon, 7 Sep 2026 07:22:19 +0200 Subject: [PATCH] Produktion+neuauftrag: Neu generieren, Einzelfelder, Formate je Produkt + getrennt aktivierbar - orders: /regenerate (Schriftstueck+Kuvert neu generieren). - produktion: 'Neu generieren'-Button im Empfaenger-Dialog + Massenauswahl. - neuauftrag: Adressmodus 'Einzelfelder' (Vorname..Land) inkl. Excel-Import; Formate nach gewaehltem Produkt gefiltert + nur show_manual. - bootstrap: formats.show_manual (getrennt von active=Konfigurator). Co-Authored-By: Claude Opus 4.8 --- bootstrap/bootstrap.mjs | 3 +- .../dist/index.js | 35 ++++++++++++++----- .../dist/index.js | 29 +++++++++++++++ .../dist/index.js | 14 +++++++- 4 files changed, 70 insertions(+), 11 deletions(-) diff --git a/bootstrap/bootstrap.mjs b/bootstrap/bootstrap.mjs index e37a590..16cb761 100644 --- a/bootstrap/bootstrap.mjs +++ b/bootstrap/bootstrap.mjs @@ -181,7 +181,8 @@ const collections = [ m2o('product', 'products', { onDelete: 'CASCADE' }), f('key', 'string', { interface: 'input', required: true }), f('name', 'string', { interface: 'input', required: true }), - f('active', 'boolean', { interface: 'boolean', default: true }), + f('active', 'boolean', { interface: 'boolean', default: true, note: 'Im Konfigurator (app.skrift.de) sichtbar.' }), + f('show_manual', 'boolean', { interface: 'boolean', default: true, note: 'Im manuellen Druck (Admin) sichtbar.' }), ] }, { collection: 'motifs', meta: { icon: 'image', note: 'Motiv-Katalog für Postkarten.' }, fields: [ diff --git a/extensions/directus-extension-skrift-neuauftrag/dist/index.js b/extensions/directus-extension-skrift-neuauftrag/dist/index.js index a447457..6ba6e5e 100644 --- a/extensions/directus-extension-skrift-neuauftrag/dist/index.js +++ b/extensions/directus-extension-skrift-neuauftrag/dist/index.js @@ -93,7 +93,7 @@ const NeuAuftrag = defineComponent({ try { const [p, f, ps, mt, mtx, sg] = await Promise.all([ api.get('/items/products', { params: { limit: -1, filter: { active: { _eq: true }, type: { _in: ['letter', 'postcard'] } }, fields: 'key,name,type' } }), - api.get('/items/formats', { params: { limit: -1, filter: { active: { _eq: true } }, fields: 'key,name,product.key' } }), + api.get('/items/formats', { params: { limit: -1, fields: '*,product.key' } }), api.get('/items/pricing_settings', { params: { fields: 'char_whitelist' } }).catch(() => ({ data: { data: {} } })), api.get('/items/muster_settings', { params: { fields: 'text_du,text_sie' } }).catch(() => ({ data: { data: {} } })), api.get('/items/muster_texts', { params: { limit: -1, filter: { active: { _eq: true } }, sort: 'sort', fields: 'id,label,text' } }).catch(() => ({ data: { data: [] } })), @@ -158,9 +158,10 @@ const NeuAuftrag = defineComponent({ setTimeout(() => { if (neu.hinweis.includes(einmalig)) neu.hinweis = ''; }, 4000); return String(v).replace(verbotenRe.value, ''); }; - // Alle aktiven Formate zur Auswahl (nicht aufs Produkt eingeschränkt) – - // so lassen sich auch freie/eigene Formate direkt wählen. - const neuFormate = () => formate.value; + // Nur im manuellen Druck sichtbare Formate (show_manual) und passend zum + // gewählten Produkt (Formate ohne Produktbindung gelten für alle). + const neuFormate = () => formate.value.filter((f) => + f.show_manual !== false && (!f.product?.key || f.product.key === neu.product)); // Wie viele Adresszeilen je Empfänger: 5 („5 Zeilen") bzw. 1 („Individueller Text"). const zeilenAnzahl = () => (neu.adressModus === 'individuell' ? 1 : 5); const freitext = (e, z) => (String(e.free_text || '').split('\n')[z] ?? ''); @@ -172,6 +173,12 @@ const NeuAuftrag = defineComponent({ neu.rows[i].free_text = zeilen.slice(0, max).join('\n').replace(/\n+$/, ''); neu.previewSession = null; }; + // Einzelfeld-Modus: strukturierte Adressfelder je Empfänger (statt freier Zeilen). + const FELDER = [['first_name', 'Vorname'], ['last_name', 'Nachname'], ['street', 'Straße'], ['house_no', 'Nr.'], ['zip', 'PLZ'], ['city', 'Stadt'], ['country', 'Land']]; + const setFeld = (i, key, v) => { + neu.rows[i] = { ...neu.rows[i], [key]: saeubere(String(v).replace(/[\r\n]/g, '')) }; + neu.previewSession = null; + }; // Individuelle Platzhalter je Empfänger (z. B. [[Gutschein]]). const setPlatzhalter = (i, name, v) => { neu.rows[i].placeholders = { ...(neu.rows[i].placeholders || {}), [name]: saeubere(v) }; @@ -201,8 +208,10 @@ const NeuAuftrag = defineComponent({ }; const addZeile = () => neu.rows.push(leereZeile()); const delZeile = (i) => { neu.rows.splice(i, 1); if (!neu.rows.length) neu.rows.push(leereZeile()); }; - // Empfänger zählt, wenn eine Adresszeile ODER ein Platzhalterwert gefüllt ist. + // Empfänger zählt, wenn eine Adresszeile/ein Feld ODER ein Platzhalterwert gefüllt ist. + const felderGefuellt = (e) => FELDER.some(([k]) => String(e[k] || '').trim()); const entries = () => neu.rows.filter((e) => String(e.free_text || '').trim() + || felderGefuellt(e) || (e.placeholders && Object.values(e.placeholders).some((v) => String(v || '').trim()))); // Aus Excel/CSV eingefügte Zeilen übernehmen. Erst die individuellen @@ -216,7 +225,12 @@ const NeuAuftrag = defineComponent({ const zellen = z.split(/\t|;|,/).map((c) => saeubere(c.trim())); const row = leereZeile(); ph.forEach((name, i) => { row.placeholders = { ...row.placeholders, [name]: zellen[i] || '' }; }); - row.free_text = zellen.slice(ph.length, ph.length + max).join('\n'); + if (neu.adressModus === 'felder') { + // Spalten nach den Platzhaltern → Vorname, Nachname, Straße, Nr., PLZ, Stadt, Land. + FELDER.forEach(([key], i) => { row[key] = zellen[ph.length + i] || ''; }); + } else { + row.free_text = zellen.slice(ph.length, ph.length + max).join('\n'); + } return row; }); neu.rows = rows; @@ -460,8 +474,11 @@ const NeuAuftrag = defineComponent({ const zeile = (e, i) => [ ...customPlaceholders().map((name) => feld(name, e.placeholders?.[name] || '', (ev) => setPlatzhalter(i, name, ev.target.value), '', (ev) => { if (spalteEinfuegen(i, (ri, v) => setPlatzhalter(ri, name, v), ev.clipboardData && ev.clipboardData.getData('text'))) ev.preventDefault(); })), - ...Array.from({ length: zeilenAnzahl() }, (_, z) => feld('Zeile ' + (z + 1), freitext(e, z), (ev) => setFreitext(i, z, ev.target.value), '', - (ev) => { if (spalteEinfuegen(i, (ri, v) => setFreitext(ri, z, v), ev.clipboardData && ev.clipboardData.getData('text'))) ev.preventDefault(); })), + ...(neu.adressModus === 'felder' + ? FELDER.map(([key, lbl]) => feld(lbl, e[key] || '', (ev) => setFeld(i, key, ev.target.value), '', + (ev) => { if (spalteEinfuegen(i, (ri, v) => setFeld(ri, key, v), ev.clipboardData && ev.clipboardData.getData('text'))) ev.preventDefault(); })) + : Array.from({ length: zeilenAnzahl() }, (_, z) => feld('Zeile ' + (z + 1), freitext(e, z), (ev) => setFreitext(i, z, ev.target.value), '', + (ev) => { if (spalteEinfuegen(i, (ri, v) => setFreitext(ri, z, v), ev.clipboardData && ev.clipboardData.getData('text'))) ev.preventDefault(); }))), ]; // Bereich 1 – Produkt @@ -497,7 +514,7 @@ const NeuAuftrag = defineComponent({ const b2 = h('div', { style: BEREICH }, [ h('div', { style: 'display:flex;justify-content:space-between;align-items:center;gap:10px;flex-wrap:wrap;margin-bottom:14px;' }, [ bereichKopf('✉ Kuvert'), - h('div', { style: 'width:300px;' }, [segToggle(neu.adressModus, [{ key: '5zeilen', label: '5 Zeilen' }, { key: 'individuell', label: 'Individueller Text' }], (k) => { neu.adressModus = k; neu.previewSession = null; })]), + h('div', { style: 'width:420px;max-width:100%;' }, [segToggle(neu.adressModus, [{ key: '5zeilen', label: '5 Zeilen' }, { key: 'felder', label: 'Einzelfelder' }, { key: 'individuell', label: 'Ind. Text' }], (k) => { neu.adressModus = k; neu.previewSession = null; })]), ]), h('div', { style: 'display:flex;flex-direction:column;gap:8px;max-height:34vh;overflow:auto;' }, neu.rows.map((e, i) => h('div', { style: 'display:flex;gap:8px;align-items:center;' }, [ diff --git a/extensions/directus-extension-skrift-orders/dist/index.js b/extensions/directus-extension-skrift-orders/dist/index.js index ca4e70b..8301ca3 100644 --- a/extensions/directus-extension-skrift-orders/dist/index.js +++ b/extensions/directus-extension-skrift-orders/dist/index.js @@ -1584,6 +1584,35 @@ const handler = (router, { services, getSchema, logger }) => { // Legt intern einen Auftrag an (keine Zahlung, keine Datei-Eingabe), erzeugt // die SVGs (Vorschau 1:1) und stellt ihn als „eingegangen" ins Board. Nutzt // dieselben Bausteine wie die öffentliche Anlage – keine Doppellogik. + // Schriftstück(e) + Kuvert(e) eines/mehrerer Aufträge neu generieren (Operator). + router.post('/regenerate', async (req, res) => { + const acc = req.accountability; + if (!acc || !acc.user || !(acc.admin || acc.app)) return res.status(403).json({ error: 'Kein Zugriff.' }); + const body = req.body || {}; + const ids = Array.isArray(body.orders) ? body.orders : (body.order != null ? [body.order] : []); + if (!ids.length) return res.status(400).json({ error: 'Keine Auftrags-ID.' }); + try { + const schema = await getSchema(); + const svc = (c) => new ItemsService(c, { schema, accountability: null }); + let ok = 0; + for (const id of ids) { + // Vorschau-Session lösen → frisch rendern (nicht 1:1 aus der alten Vorschau kopieren). + try { await svc('orders').updateOne(id, { preview_session: null }); } catch { /* egal */ } + try { + const r = await fetch(`${BACKEND_URL}/api/order/from-directus`, { + method: 'POST', headers: { 'Content-Type': 'application/json', 'X-API-Token': BACKEND_TOKEN }, + body: JSON.stringify({ orderId: id }), + }); + if (r.ok) ok += 1; + } catch (e) { logger.warn(`[skrift-orders] regenerate ${id}: ${e.message}`); } + } + return res.json({ ok: true, regeneriert: ok, gesamt: ids.length }); + } catch (err) { + logger.error(`[skrift-orders] regenerate: ${err.stack || err.message}`); + return res.status(500).json({ error: 'Neu-Generierung fehlgeschlagen.' }); + } + }); + router.post('/manual-order', async (req, res) => { const acc = req.accountability; if (!acc || !acc.user || !(acc.admin || acc.app)) return res.status(403).json({ error: 'Kein Zugriff.' }); diff --git a/extensions/directus-extension-skrift-produktion/dist/index.js b/extensions/directus-extension-skrift-produktion/dist/index.js index d22858d..db610f1 100644 --- a/extensions/directus-extension-skrift-produktion/dist/index.js +++ b/extensions/directus-extension-skrift-produktion/dist/index.js @@ -719,6 +719,16 @@ const Board = defineComponent({ } } catch (e) { empf.perr = 'Vorschau fehlgeschlagen.'; } finally { empf.pLoad = false; } } + // Schriftstück(e) + Kuvert(e) eines/mehrerer Aufträge neu generieren lassen. + async function regenerieren(ids) { + const liste = (Array.isArray(ids) ? ids : [ids]).filter(Boolean); + if (!liste.length) return; + try { + const r = await api.post('/skrift-orders/regenerate', { orders: liste }); + flash(`Neu generiert: ${r.data?.regeneriert ?? liste.length} Auftrag/Aufträge (läuft im Hintergrund).`); + setTimeout(() => laden(), 1500); + } catch (e) { fehler.value = e?.response?.data?.error || e?.message || 'Neu-Generierung fehlgeschlagen.'; } + } // 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. @@ -1203,7 +1213,8 @@ const Board = defineComponent({ vorschauBox('✉ Kuvert', empf.pe), ]) : null; 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('Text, Format & alle Empfänger in den manuellen Druck laden – dort anpassen und neu anlegen.'), + subdued('Neu generieren erzeugt Schriftstück + Kuvert frisch.'), + btn('🔄 Neu generieren', () => regenerieren(empf.orderObj?.id), 'danger'), 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]); @@ -1282,6 +1293,7 @@ const Board = defineComponent({ h('span', { style: 'font-weight:600;' }, `${auswahl.value.length} Auftrag/Aufträge ausgewählt`), h('button', { type: 'button', onClick: openBulk, style: `cursor:pointer;border:none;background:#fff;color:${ACCENT};border-radius:7px;padding:6px 14px;font:inherit;font-size:13px;font-weight:600;` }, '🖨 In Druck geben'), h('button', { type: 'button', onClick: chargePdf, style: `cursor:pointer;border:none;background:#fff;color:${ACCENT};border-radius:7px;padding:6px 14px;font:inherit;font-size:13px;font-weight:600;` }, '📄 PDFs drucken'), + h('button', { type: 'button', onClick: () => regenerieren(auswahlSortiert()), style: `cursor:pointer;border:none;background:#fff;color:${ACCENT};border-radius:7px;padding:6px 14px;font:inherit;font-size:13px;font-weight:600;` }, '🔄 Neu generieren'), h('select', { style: `cursor:pointer;border:none;background:#fff;color:${ACCENT};border-radius:7px;padding:7px 12px;font:inherit;font-size:13px;font-weight:600;`, value: '', onChange: (e) => { if (e.target.value) { bulkStatus(e.target.value); e.target.value = ''; } },