From 54de08448abf29cfec4b93cdfa5c6b643ab8f7f1 Mon Sep 17 00:00:00 2001 From: s4luorth Date: Fri, 11 Sep 2026 21:11:08 +0200 Subject: [PATCH] =?UTF-8?q?Muster-Workflow:=20Mustertext=20w=C3=A4hlen,=20?= =?UTF-8?q?Adresse=20aufs=20Kuvert,=20Platzhalter=20f=C3=BCllen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Produktion-Muster-Dialog: benannten Mustertext (muster_texts) wählen; Platzhalter ([[Vorname]]/[[Nachname]]/[[Ort]]/[[Strasse]]/[[PLZ]]/[[Land]]) aus der Anfrage- Adresse automatisch füllen; Text vor dem Anlegen bearbeitbar; „Adresse aufs Kuvert" (Standard an) mit Format/Template-Wahl. - manual-print: erzeugt optional zusätzlich ein Kuvert (Umschlag-Job) mit der Adresse. Co-Authored-By: Claude Opus 4.8 --- .../dist/index.js | 24 ++++- .../dist/index.js | 102 +++++++++++++++--- 2 files changed, 106 insertions(+), 20 deletions(-) diff --git a/extensions/directus-extension-skrift-orders/dist/index.js b/extensions/directus-extension-skrift-orders/dist/index.js index 627c8ec..9920d5d 100644 --- a/extensions/directus-extension-skrift-orders/dist/index.js +++ b/extensions/directus-extension-skrift-orders/dist/index.js @@ -746,7 +746,7 @@ const handler = (router, { services, getSchema, logger }) => { letters.push({ index: nr, type: 'postcard', text: combined, format: briefFormat, font, realisticHandwriting: realistisch, placeholders: platzhalterVon(e, i), - fontScale: fsLetter, senderScale: Number(body.envelope_sender_scale) || 1, + fontScale: fsLetter, senderScale: Number(body.envelope_sender_scale) || 1, recipientScale: fsEnv, }); return; } @@ -1241,12 +1241,23 @@ const handler = (router, { services, getSchema, logger }) => { const font = ['tilda', 'alva', 'ellie'].includes(body.font) ? body.font : 'tilda'; const format = TEST_FORMAT[body.format] || 'a4'; + // Kuvert (Adresse aus der Anfrage) optional mitdrucken. + const ENV_FMT = { dinlang: 'din_lang', c6: 'c6' }; + const envAddr = clean(String(body.envelope || '')).trim(); + const wantsEnv = !!(envAddr && body.envelope_template); + const envFormat = ENV_FMT[body.envelope_format] || 'din_lang'; + + const letters = [{ index: 1, type: 'letter', text: clean(String(body.text)), format, font, realisticHandwriting: body.realistic !== false }]; + if (wantsEnv) { + letters.push({ index: 1, type: 'envelope', envelopeType: 'recipient', text: envAddr, format: envFormat, font, realisticHandwriting: body.realistic !== false }); + } + const gen = await fetch(`${BACKEND_URL}/api/order/generate`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-API-Token': BACKEND_TOKEN }, body: JSON.stringify({ orderNumber: ref, - letters: [{ index: 1, type: 'letter', text: clean(String(body.text)), format, font, realisticHandwriting: body.realistic !== false }], + letters, metadata: { rawText: String(body.text) }, }), }); @@ -1265,10 +1276,17 @@ const handler = (router, { services, getSchema, logger }) => { machine: body.machine, template: body.template, type: 'brief', status: 'queued', output_ref: ref, priority: prio, format: body.format || 'a4', format_label: LETTER_LABEL[body.format] || body.format || 'A4', }); + // Zweiter Job für das Kuvert (gleicher Ausgabeordner, Typ umschlag). + if (wantsEnv) { + await svc('jobs').createOne({ + machine: body.machine, template: body.envelope_template, type: 'umschlag', status: 'queued', output_ref: ref, priority: prio, + format: body.envelope_format || 'dinlang', format_label: ENV_LABEL[body.envelope_format] || 'Kuvert', + }); + } if (body.offer_request) { try { await svc('offer_requests').updateOne(body.offer_request, { status: 'in_bearbeitung' }); } catch { /* nicht kritisch */ } } - return res.json({ ok: true, ref, job, mode: prio ? 'direct' : 'queue', busy }); + return res.json({ ok: true, ref, job, mode: prio ? 'direct' : 'queue', busy, kuvert: wantsEnv }); } catch (err) { logger.error(`[skrift-orders] manual-print: ${err.stack || err.message}`); return res.status(500).json({ error: 'Manueller Druck fehlgeschlagen.' }); diff --git a/extensions/directus-extension-skrift-produktion/dist/index.js b/extensions/directus-extension-skrift-produktion/dist/index.js index 032f499..1c477c7 100644 --- a/extensions/directus-extension-skrift-produktion/dist/index.js +++ b/extensions/directus-extension-skrift-produktion/dist/index.js @@ -179,10 +179,14 @@ const Board = defineComponent({ const machines = ref([]); const templates = ref([]); const muster = ref({ text_du: '', text_sie: '' }); + // Benannte Mustertexte (Collection muster_texts) – im Muster-Dialog wählbar. + const musterListe = ref([]); // ── Modale ── const druck = reactive({ open: false, order: null, machine: null, mode: 'queue', busy: false, brief: null, umschlag: null, msg: '', err: '', deNums: '', intNums: '', deN: 0, intN: 0 }); - const manuell = reactive({ open: false, req: null, form: 'sie', font: 'tilda', format: 'a4', template: null, machine: null, mode: 'queue', text: '', msg: '', err: '' }); + const manuell = reactive({ open: false, req: null, form: 'sie', vorlageId: '', roh: '', font: 'tilda', format: 'a4', template: null, machine: null, mode: 'queue', text: '', + // Adresse automatisch aufs Kuvert (Standard an) + eigenes Kuvert-Template/-Format. + kuvert: true, envFormat: 'dinlang', envTemplate: null, msg: '', err: '' }); const empf = reactive({ open: false, mode: 'order', titel: '', order: null, suche: '', rows: [], files: [], info: '', laden: false, selected: null, pl: '', pe: '', pLoad: false, perr: '', lb: '', lbTitel: '' }); // Massenauswahl (Auftrags-IDs) + Sammel-Druck-Popup (pro Format Template+Maschine). @@ -337,7 +341,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] = await Promise.all([ + const [o, r, j, m, t, ms, mtx] = await Promise.all([ ladeOrders(), api.get('/items/offer_requests', { params: { limit: -1, sort: '-date_created', @@ -348,6 +352,7 @@ const Board = defineComponent({ ladeMachines(), 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: [] } })), ]); orders.value = o.data?.data || []; requests.value = r.data?.data || []; @@ -355,6 +360,7 @@ const Board = defineComponent({ machines.value = (m.data?.data || []); templates.value = t.data?.data || []; muster.value = ms.data?.data || { text_du: '', text_sie: '' }; + musterListe.value = mtx.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(); } @@ -685,38 +691,83 @@ const Board = defineComponent({ } // ── Muster: manueller Druck ── - function musterAufloesen() { - const roh = manuell.form === 'du' ? muster.value.text_du : muster.value.text_sie; + // Namensteile der Anfrage (Adresse hat Vorrang vor dem freien Namensfeld). + function musterNamen() { const a = manuell.req?.address && typeof manuell.req.address === 'object' ? manuell.req.address : {}; const name = manuell.req?.name || ''; const teile = name.split(' '); - const vor = teile[0] || ''; const nach = teile.slice(1).join(' '); - manuell.text = String(roh || '') - .replace(/\[\[Vorname\]\]/gi, a.first_name || a.vorname || vor) - .replace(/\[\[Nachname\]\]/gi, a.last_name || a.nachname || nach) - .replace(/\[\[Anrede\]\]/gi, a.salutation || a.anrede || '') - .replace(/\[\[Name\]\]/gi, name); + return { + anrede: a.salutation || a.anrede || '', + vor: a.first_name || a.vorname || teile[0] || '', + nach: a.last_name || a.nachname || teile.slice(1).join(' '), + strasse: a.street || a.strasse || '', + plz: a.zip || a.plz || '', + ort: a.city || a.stadt || a.ort || '', + land: a.country || a.land || '', + firma: a.company || a.firma || '', + name, + }; + } + // Adressblock fürs Kuvert (Empfänger). Land nur bei Ausland. + function musterAdresse() { + const n = musterNamen(); + const person = [n.anrede, n.vor, n.nach].filter(Boolean).join(' ') || n.name; + const ort = [n.plz, n.ort].filter(Boolean).join(' '); + const land = (n.land && !/^(de|deu|d|deutschland|germany)$/i.test(String(n.land).trim())) ? n.land : ''; + return [n.firma, person, n.strasse, ort, land].filter(Boolean).join('\n'); + } + // Platzhalter im gewählten Rohtext füllen → manuell.text. + function musterAufloesen() { + const n = musterNamen(); + manuell.text = String(manuell.roh || '') + .replace(/\[\[Vorname\]\]/gi, n.vor) + .replace(/\[\[Nachname\]\]/gi, n.nach) + .replace(/\[\[Anrede\]\]/gi, n.anrede) + .replace(/\[\[Strasse\]\]/gi, n.strasse) + .replace(/\[\[PLZ\]\]/gi, n.plz) + .replace(/\[\[Ort\]\]/gi, n.ort) + .replace(/\[\[Stadt\]\]/gi, n.ort) + .replace(/\[\[Land\]\]/gi, n.land) + .replace(/\[\[Name\]\]/gi, n.name); + } + // Vorlage wählen: 'du'/'sie' (aus muster_settings) oder eine muster_texts-ID. + function musterVorlage(wert) { + manuell.vorlageId = wert; + if (wert === 'du') manuell.roh = muster.value.text_du || ''; + else if (wert === 'sie') manuell.roh = muster.value.text_sie || ''; + else manuell.roh = (musterListe.value.find((m) => String(m.id) === String(wert)) || {}).text || ''; + musterAufloesen(); } function openMuster(req) { - manuell.req = req; manuell.form = 'sie'; manuell.font = 'tilda'; manuell.format = 'a4'; + manuell.req = req; manuell.font = 'tilda'; manuell.format = 'a4'; manuell.template = tplByKey('a4_single', 'brief')?.id ?? templates.value.find((t) => t.type === 'brief')?.id ?? null; manuell.machine = aktiveMaschinen.value[0]?.id ?? null; manuell.mode = 'queue'; + // Kuvert automatisch: A4 → DIN Lang, sonst C6; passendes Umschlag-Template. + manuell.kuvert = true; + manuell.envFormat = 'dinlang'; + manuell.envTemplate = tplForFormat('dinlang', 'umschlag')?.id ?? templates.value.find((t) => t.type === 'umschlag')?.id ?? null; manuell.msg = ''; manuell.err = ''; - musterAufloesen(); + // Erste benannte Vorlage bevorzugen, sonst Sie-Form. + musterVorlage(musterListe.value[0] ? musterListe.value[0].id : 'sie'); manuell.open = true; } async function submitMuster() { if (!manuell.text.trim()) { manuell.err = 'Text fehlt.'; return; } if (!manuell.template || !manuell.machine) { manuell.err = 'Template und Maschine wählen.'; return; } + if (manuell.kuvert && !manuell.envTemplate) { manuell.err = 'Kuvert-Template wählen (oder Kuvert deaktivieren).'; return; } manuell.err = ''; manuell.msg = 'Wird erzeugt …'; try { const res = await api.post('/skrift-orders/manual-print', { text: manuell.text, font: manuell.font, format: manuell.format, template: manuell.template, machine: manuell.machine, mode: manuell.mode, offer_request: manuell.req?.id || null, + // Adresse automatisch aufs Kuvert. + envelope: manuell.kuvert ? musterAdresse() : '', + envelope_format: manuell.kuvert ? manuell.envFormat : '', + envelope_template: manuell.kuvert ? manuell.envTemplate : null, }); const modus = res.data?.mode === 'direct' ? 'Direktdruck' : 'Warteschlange'; - flash(`Muster erzeugt & eingereiht (${modus}).`); + flash(`Muster erzeugt & eingereiht (${modus})${manuell.kuvert ? ' inkl. Kuvert' : ''}.`); manuell.open = false; await laden(); } catch (e) { manuell.err = e?.response?.data?.detail || e?.response?.data?.error || e?.message || 'Fehlgeschlagen.'; manuell.msg = ''; } @@ -1273,13 +1324,30 @@ const Board = defineComponent({ function musterModal() { if (!manuell.open) return null; const briefTpls = templates.value.filter((t) => t.type === 'brief'); + const umschlagTpls = templates.value.filter((t) => t.type === 'umschlag'); const body = h('div', { style: 'padding:18px 20px;display:flex;flex-direction:column;gap:12px;' }, [ - h('div', { style: 'display:flex;gap:16px;align-items:center;' }, [ - h('label', { style: 'display:flex;align-items:center;gap:6px;' }, [h('input', { type: 'radio', name: 'anrede', checked: manuell.form === 'sie', onChange: () => { manuell.form = 'sie'; musterAufloesen(); } }), 'Sie-Form']), - h('label', { style: 'display:flex;align-items:center;gap:6px;' }, [h('input', { type: 'radio', name: 'anrede', checked: manuell.form === 'du', onChange: () => { manuell.form = 'du'; musterAufloesen(); } }), 'Du-Form']), - subdued('Adresse aus der Anfrage übernommen · [[abschnitt]] bitte füllen'), + // Vorlage wählen (benannte Mustertexte + Du/Sie). Platzhalter werden gefüllt. + h('div', { style: 'display:flex;gap:10px;align-items:center;flex-wrap:wrap;' }, [ + h('label', { style: 'font-size:12px;color:var(--foreground-subdued);font-weight:600;' }, 'Mustertext:'), + h('select', { style: S_SELECT + 'flex:1;min-width:220px;', value: String(manuell.vorlageId), onChange: (e) => musterVorlage(e.target.value) }, [ + ...musterListe.value.map((m) => h('option', { value: String(m.id), selected: String(manuell.vorlageId) === String(m.id) }, m.label || `Text ${m.id}`)), + h('option', { value: 'sie', selected: manuell.vorlageId === 'sie' }, 'Standard Sie-Form'), + h('option', { value: 'du', selected: manuell.vorlageId === 'du' }, 'Standard Du-Form'), + ]), + subdued('Platzhalter aus der Anfrage-Adresse automatisch gefüllt · vor dem Anlegen bearbeitbar'), ]), h('textarea', { style: S_INPUT + 'width:100%;min-height:200px;font-family:var(--family-monospace,monospace);', value: manuell.text, onInput: (e) => { manuell.text = e.target.value; } }), + // Kuvert automatisch mit der Adresse aus der Anfrage. + h('div', { style: 'display:flex;gap:12px;align-items:center;flex-wrap:wrap;background:var(--background-subdued,#f1f4f9);border-radius:8px;padding:10px 12px;' }, [ + h('label', { style: 'display:flex;align-items:center;gap:8px;font-size:14px;cursor:pointer;font-weight:600;' }, [ + h('input', { type: 'checkbox', checked: manuell.kuvert, onChange: (e) => { manuell.kuvert = e.target.checked; }, style: 'width:16px;height:16px;' }), + '✉ Adresse aufs Kuvert', + ]), + manuell.kuvert ? h('select', { style: S_SELECT + 'width:auto;', value: manuell.envFormat, onChange: (e) => { manuell.envFormat = e.target.value; manuell.envTemplate = tplForFormat(e.target.value, 'umschlag')?.id ?? manuell.envTemplate; } }, + [['dinlang', 'DIN Lang'], ['c6', 'C6']].map(([v, l]) => h('option', { value: v, selected: manuell.envFormat === v }, l))) : null, + manuell.kuvert ? h('select', { style: S_SELECT + 'flex:1;min-width:160px;', value: manuell.envTemplate, onChange: (e) => { manuell.envTemplate = e.target.value; } }, umschlagTpls.map((t) => h('option', { value: t.id, selected: String(manuell.envTemplate) === String(t.id) }, t.name))) : null, + manuell.kuvert ? subdued('→ ' + (musterAdresse().replace(/\n/g, ', ') || 'keine Adresse')) : null, + ]), h('div', { style: 'display:grid;grid-template-columns:repeat(4,1fr);gap:8px;' }, [ h('div', {}, [h('label', { style: 'font-size:11px;color:var(--foreground-subdued);' }, 'Schrift'), h('select', { style: S_SELECT + 'width:100%;', value: manuell.font, onChange: (e) => { manuell.font = e.target.value; } }, ['tilda', 'alva', 'ellie'].map((f) => h('option', { value: f, selected: manuell.font === f }, f)))]),