From ecd6da398c5275e8f40923973a00a23cfaedc4fd Mon Sep 17 00:00:00 2001 From: s4luorth Date: Thu, 1 Oct 2026 11:28:08 +0200 Subject: [PATCH] Manueller Auftrag: Neukundenzwang + Mails-Checkbox - Neue E-Mail im Neuauftrag -> Pruefung; unbekannt => Stammdaten (Vor-/Nachname oder Firma + Adresse) werden erzwungen und beim Anlegen am Kundenkonto gespeichert (statt leerem Auto-Konto). - Checkbox "Mails an Kunden senden" je manuellem Auftrag; orders.mails_senden (Default true). Mailer unterdrueckt Kunden-Mails bei mails_senden=false. Co-Authored-By: Claude Opus 4.8 --- bootstrap/bootstrap.mjs | 1 + .../dist/index.js | 4 +- .../dist/index.js | 49 ++++++++++++++++++- .../dist/index.js | 12 ++++- 4 files changed, 63 insertions(+), 3 deletions(-) diff --git a/bootstrap/bootstrap.mjs b/bootstrap/bootstrap.mjs index e2d4600..6db504e 100644 --- a/bootstrap/bootstrap.mjs +++ b/bootstrap/bootstrap.mjs @@ -253,6 +253,7 @@ const collections = [ { collection: 'orders', meta: { icon: 'receipt_long', note: 'Auftrag.' }, fields: [ f('order_number', 'string', { interface: 'input', unique: true, required: true }), f('order_name', 'string', { interface: 'input', note: 'Optionaler Auftragsname (z. B. „EinDollarBrille Dankeskarten"). Wird zusätzlich zur Nummer angezeigt.' }), + f('mails_senden', 'boolean', { interface: 'boolean', default: true, note: 'Kunden-Mails (Bestätigung/Status) senden? Bei manuellen Aufträgen abschaltbar.' }), m2o('customer', 'directus_users', { note: 'Kunde = Login-Konto (Zuordnung per E-Mail).' }), f('customer_email', 'string', { interface: 'input', required: true, note: 'Zuordnungsschlüssel.' }), m2o('product', 'products'), diff --git a/extensions/directus-extension-skrift-mailer/dist/index.js b/extensions/directus-extension-skrift-mailer/dist/index.js index 87027bc..4ad0ffb 100644 --- a/extensions/directus-extension-skrift-mailer/dist/index.js +++ b/extensions/directus-extension-skrift-mailer/dist/index.js @@ -170,7 +170,7 @@ export default (register, { services, getSchema, logger }) => { const order = await new ItemsService('orders', { schema, accountability: null }).readOne(orderId, { fields: [ - 'order_number', 'customer_email', 'entries_count', 'shipping_type', + 'order_number', 'customer_email', 'entries_count', 'shipping_type', 'mails_senden', 'needs_envelope', 'envelope_labeling', 'envelope_format', 'net_total', 'vat_amount', 'gross_total', 'billing', 'shipping_address', 'customer.first_name', 'customer.last_name', 'customer.company', @@ -178,6 +178,8 @@ export default (register, { services, getSchema, logger }) => { ], }); if (!order?.customer_email) return; + // Manuell deaktivierbar: „Mails senden = nein" unterdrückt Kunden-Mails. + if (order.mails_senden === false) return; // Datei-Produkte (Unterschriftenservice) erhalten KEINE Standard-Bestell- // bestätigung – dafür geht eine eigene Vorlage aus dem Bestell-Endpunkt raus. if (key === 'order_created' && order.product?.input_mode === 'datei') return; diff --git a/extensions/directus-extension-skrift-neuauftrag/dist/index.js b/extensions/directus-extension-skrift-neuauftrag/dist/index.js index 860c3d8..523bd29 100644 --- a/extensions/directus-extension-skrift-neuauftrag/dist/index.js +++ b/extensions/directus-extension-skrift-neuauftrag/dist/index.js @@ -73,6 +73,10 @@ const NeuAuftrag = defineComponent({ const leereZeile = () => ({ free_text: '', placeholders: {} }); const neu = reactive({ product: '', format: '', font: 'tilda', orderName: '', + // Mails an den Kunden senden (Bestätigung/Status). Pro manuellem Auftrag abschaltbar. + mailsSenden: true, + // Kunden-Check: '', 'pruefe', 'vorhanden', 'neu'. Bei 'neu' werden Stammdaten erzwungen. + kundeStatus: '', kundeVorname: '', kundeNachname: '', kundeFirma: '', kundeStr: '', kundePlz: '', kundeOrt: '', // Relative Schriftgröße in % (getrennt Brief/Kuvert); 100 = Standard. fontSizeLetter: 100, fontSizeEnvelope: 100, // Unterschrift auf dem Brief ([[signatur1]]): SVG-Inhalt + Feinjustierung. @@ -89,6 +93,20 @@ const NeuAuftrag = defineComponent({ docs: [], previewSession: null, pLaedt: false, msg: '', err: '', hinweis: '', lb: '', lbDoc: null, }); // Ist das gewählte Format eine Postkarte (Text links, Adresse rechts, ein SVG)? + // Kunden-Check: existiert die E-Mail? Bei „neu" werden Stammdaten erzwungen. + let kundeTimer = null; + function pruefeKunde() { + const mail = String(neu.customer_email || '').trim(); + clearTimeout(kundeTimer); + if (!mail || !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(mail)) { neu.kundeStatus = ''; return; } + neu.kundeStatus = 'pruefe'; + kundeTimer = setTimeout(async () => { + try { + const r = await api.get('/users', { params: { filter: { email: { _eq: mail } }, limit: 1, fields: 'id' } }); + neu.kundeStatus = (r.data?.data?.[0]) ? 'vorhanden' : 'neu'; + } catch { neu.kundeStatus = ''; } + }, 350); + } const istPostkarte = () => pkFormate.value.has(neu.format); // Kuverts nur generieren, wenn angehakt (Beschriftung aus der Empfängeradresse). // Postkarte: KEIN separater Umschlag – der Absender kommt über envelope_sender @@ -398,6 +416,10 @@ const NeuAuftrag = defineComponent({ neu.msg = `✓ Auftrag ${nr} angelegt – Dokumente werden noch generiert. Status im Produktions-Board prüfen.`; } async function absenden() { + // Neukunde: Stammdaten erzwingen, bevor der Auftrag angelegt wird. + if (neu.kundeStatus === 'neu' && !((neu.kundeVorname.trim() && neu.kundeNachname.trim()) || neu.kundeFirma.trim())) { + neu.err = 'Neuer Kunde: bitte Vor- und Nachnamen oder eine Firma angeben.'; return; + } // Ohne Kuvert & ohne eigene Platzhalter: N identische Schriftstücke (Blankozeilen). // Sonst: die erfassten Empfänger – ohne Empfänger genau ein Schriftstück. let payloadEntries; @@ -420,6 +442,11 @@ const NeuAuftrag = defineComponent({ envelope_format: kv.envelope_format, shipping_type: 'sammel', customer_email: neu.customer_email.trim() || undefined, + mails_senden: neu.mailsSenden, + customer_vorname: neu.kundeVorname.trim() || undefined, + customer_nachname: neu.kundeNachname.trim() || undefined, + customer_company: neu.kundeFirma.trim() || undefined, + customer_address: (neu.kundeStr || neu.kundePlz || neu.kundeOrt) ? { street: neu.kundeStr.trim(), zip: neu.kundePlz.trim(), city: neu.kundeOrt.trim(), country: 'Deutschland' } : undefined, text_template: neu.text, preview_session: neu.previewSession || undefined, entries: payloadEntries, }); @@ -435,6 +462,7 @@ const NeuAuftrag = defineComponent({ function zuruecksetzen() { Object.assign(neu, { product: produkte.value[0]?.key || 'briefe', format: neuFormate()[0]?.key || '', font: 'tilda', orderName: '', + mailsSenden: true, kundeStatus: '', kundeVorname: '', kundeNachname: '', kundeFirma: '', kundeStr: '', kundePlz: '', kundeOrt: '', fontSizeLetter: 100, fontSizeEnvelope: 100, sigSvg: '', sigLabel: '', sigX: 0, sigY: 0, sigScale: 100, sigSpacing: '', mehrseitig: false, seitenInfo: '', adressModus: '5zeilen', kuverts: true, absenderAufKuvert: false, envelopeSender: '', envelopeSenderScale: 100, envFormat: '', realistic: true, customer_email: '', text: '', rows: [leereZeile()], @@ -697,9 +725,28 @@ const NeuAuftrag = defineComponent({ ]), h('div', { style: 'flex:1 1 280px;max-width:420px;' }, [ label('Kunde verknüpfen (E-Mail, optional)'), - h('input', { type: 'email', value: neu.customer_email, placeholder: 'kunde@example.de', style: WFELD + 'width:100%;', onInput: (e) => { neu.customer_email = e.target.value; } }), + h('input', { type: 'email', value: neu.customer_email, placeholder: 'kunde@example.de', style: WFELD + 'width:100%;', onInput: (e) => { neu.customer_email = e.target.value; pruefeKunde(); } }), + neu.kundeStatus === 'vorhanden' ? h('div', { style: 'font-size:12px;color:var(--success,#2e7d32);margin-top:5px;font-weight:600;' }, '✓ Bestehender Kunde wird verknüpft') : null, + neu.kundeStatus === 'neu' ? h('div', { style: 'font-size:12px;color:var(--primary,#1e50a5);margin-top:5px;font-weight:600;' }, 'Neuer Kunde – bitte unten Stammdaten erfassen.') : null, ]), ]), + // Neukundenanlage erzwingen, sobald eine unbekannte E-Mail eingetragen ist. + neu.kundeStatus === 'neu' ? h('div', { style: 'margin-top:14px;border:1px solid var(--border-normal);border-radius:10px;padding:16px;background:var(--background-subdued,#f1f4f9);' }, [ + h('div', { style: 'font-weight:600;margin-bottom:10px;' }, 'Neuen Kunden anlegen'), + h('div', { style: 'display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px;' }, [ + h('div', {}, [label('Vorname'), h('input', { value: neu.kundeVorname, style: WFELD + 'width:100%;', onInput: (e) => { neu.kundeVorname = e.target.value; } })]), + h('div', {}, [label('Nachname'), h('input', { value: neu.kundeNachname, style: WFELD + 'width:100%;', onInput: (e) => { neu.kundeNachname = e.target.value; } })]), + h('div', {}, [label('Firma (optional)'), h('input', { value: neu.kundeFirma, style: WFELD + 'width:100%;', onInput: (e) => { neu.kundeFirma = e.target.value; } })]), + h('div', {}, [label('Straße + Nr.'), h('input', { value: neu.kundeStr, style: WFELD + 'width:100%;', onInput: (e) => { neu.kundeStr = e.target.value; } })]), + h('div', {}, [label('PLZ'), h('input', { value: neu.kundePlz, style: WFELD + 'width:100%;', onInput: (e) => { neu.kundePlz = e.target.value; } })]), + h('div', {}, [label('Ort'), h('input', { value: neu.kundeOrt, style: WFELD + 'width:100%;', onInput: (e) => { neu.kundeOrt = e.target.value; } })]), + ]), + ]) : null, + // Mails an Kunden senden (Bestätigung/Status) – pro Auftrag abschaltbar. + h('label', { style: 'display:flex;align-items:center;gap:8px;margin-top:14px;font-size:14px;cursor:pointer;' }, [ + h('input', { type: 'checkbox', checked: neu.mailsSenden, onChange: (e) => { neu.mailsSenden = e.target.checked; }, style: 'width:16px;height:16px;' }), + 'Mails an Kunden senden (Bestätigung/Status)', + ]), ]); const aktionen = h('div', { class: 'sk-btnrow', style: 'display:flex;gap:8px;flex-wrap:wrap;' }, [ diff --git a/extensions/directus-extension-skrift-orders/dist/index.js b/extensions/directus-extension-skrift-orders/dist/index.js index a91dbbd..8e1355a 100644 --- a/extensions/directus-extension-skrift-orders/dist/index.js +++ b/extensions/directus-extension-skrift-orders/dist/index.js @@ -2053,7 +2053,16 @@ const handler = (router, { services, getSchema, logger }) => { const role = (await new RolesService({ schema, accountability: null }).readByQuery({ filter: { name: { _eq: 'Kunde' } }, limit: 1, fields: ['id'], }))?.[0]; - customerId = await users.createOne({ email: kundeMail, password: zufallsPasswort(), role: role?.id ?? null, status: 'active' }); + // Neukunde mit den im Formular erfassten Stammdaten anlegen. + const neuKunde = { email: kundeMail, password: zufallsPasswort(), role: role?.id ?? null, status: 'active', + first_name: clean(body.customer_vorname) || null, + last_name: clean(body.customer_nachname) || null, + company: clean(body.customer_company) || null, + person_type: body.customer_company ? 'unternehmen' : 'privat', + }; + const adr = body.customer_address && typeof body.customer_address === 'object' ? body.customer_address : null; + if (adr && (adr.street || adr.zip || adr.city)) neuKunde.billing_address = adr; + customerId = await users.createOne(neuKunde); customerEmail = kundeMail; } catch (e) { logger.warn(`[skrift-orders] manual-order Kunde anlegen: ${e.message}`); customerEmail = kundeMail; } } @@ -2084,6 +2093,7 @@ const handler = (router, { services, getSchema, logger }) => { const orderData = { customer: customerId, customer_email: customerEmail, order_name: clean(body.order_name) || null, + mails_senden: body.mails_senden !== false, product: product.id, format: formatId, person_type: body.person_type === 'unternehmen' ? 'unternehmen' : 'privat', font: ['tilda', 'alva', 'ellie'].includes(body.font) ? body.font : 'tilda',