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 <noreply@anthropic.com>
This commit is contained in:
s4luorth
2026-10-01 11:28:08 +02:00
parent 6a6b09d49d
commit ecd6da398c
4 changed files with 63 additions and 3 deletions

View File

@@ -253,6 +253,7 @@ const collections = [
{ collection: 'orders', meta: { icon: 'receipt_long', note: 'Auftrag.' }, fields: [ { collection: 'orders', meta: { icon: 'receipt_long', note: 'Auftrag.' }, fields: [
f('order_number', 'string', { interface: 'input', unique: true, required: true }), 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('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).' }), m2o('customer', 'directus_users', { note: 'Kunde = Login-Konto (Zuordnung per E-Mail).' }),
f('customer_email', 'string', { interface: 'input', required: true, note: 'Zuordnungsschlüssel.' }), f('customer_email', 'string', { interface: 'input', required: true, note: 'Zuordnungsschlüssel.' }),
m2o('product', 'products'), m2o('product', 'products'),

View File

@@ -170,7 +170,7 @@ export default (register, { services, getSchema, logger }) => {
const order = await new ItemsService('orders', { schema, accountability: null }).readOne(orderId, { const order = await new ItemsService('orders', { schema, accountability: null }).readOne(orderId, {
fields: [ 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', 'needs_envelope', 'envelope_labeling', 'envelope_format',
'net_total', 'vat_amount', 'gross_total', 'billing', 'shipping_address', 'net_total', 'vat_amount', 'gross_total', 'billing', 'shipping_address',
'customer.first_name', 'customer.last_name', 'customer.company', 'customer.first_name', 'customer.last_name', 'customer.company',
@@ -178,6 +178,8 @@ export default (register, { services, getSchema, logger }) => {
], ],
}); });
if (!order?.customer_email) return; 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- // Datei-Produkte (Unterschriftenservice) erhalten KEINE Standard-Bestell-
// bestätigung – dafür geht eine eigene Vorlage aus dem Bestell-Endpunkt raus. // bestätigung – dafür geht eine eigene Vorlage aus dem Bestell-Endpunkt raus.
if (key === 'order_created' && order.product?.input_mode === 'datei') return; if (key === 'order_created' && order.product?.input_mode === 'datei') return;

View File

@@ -73,6 +73,10 @@ const NeuAuftrag = defineComponent({
const leereZeile = () => ({ free_text: '', placeholders: {} }); const leereZeile = () => ({ free_text: '', placeholders: {} });
const neu = reactive({ const neu = reactive({
product: '', format: '', font: 'tilda', orderName: '', 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. // Relative Schriftgröße in % (getrennt Brief/Kuvert); 100 = Standard.
fontSizeLetter: 100, fontSizeEnvelope: 100, fontSizeLetter: 100, fontSizeEnvelope: 100,
// Unterschrift auf dem Brief ([[signatur1]]): SVG-Inhalt + Feinjustierung. // 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, docs: [], previewSession: null, pLaedt: false, msg: '', err: '', hinweis: '', lb: '', lbDoc: null,
}); });
// Ist das gewählte Format eine Postkarte (Text links, Adresse rechts, ein SVG)? // 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); const istPostkarte = () => pkFormate.value.has(neu.format);
// Kuverts nur generieren, wenn angehakt (Beschriftung aus der Empfängeradresse). // Kuverts nur generieren, wenn angehakt (Beschriftung aus der Empfängeradresse).
// Postkarte: KEIN separater Umschlag – der Absender kommt über envelope_sender // 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.`; neu.msg = `✓ Auftrag ${nr} angelegt – Dokumente werden noch generiert. Status im Produktions-Board prüfen.`;
} }
async function absenden() { 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). // Ohne Kuvert & ohne eigene Platzhalter: N identische Schriftstücke (Blankozeilen).
// Sonst: die erfassten Empfänger – ohne Empfänger genau ein Schriftstück. // Sonst: die erfassten Empfänger – ohne Empfänger genau ein Schriftstück.
let payloadEntries; let payloadEntries;
@@ -420,6 +442,11 @@ const NeuAuftrag = defineComponent({
envelope_format: kv.envelope_format, envelope_format: kv.envelope_format,
shipping_type: 'sammel', shipping_type: 'sammel',
customer_email: neu.customer_email.trim() || undefined, 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, text_template: neu.text, preview_session: neu.previewSession || undefined,
entries: payloadEntries, entries: payloadEntries,
}); });
@@ -435,6 +462,7 @@ const NeuAuftrag = defineComponent({
function zuruecksetzen() { function zuruecksetzen() {
Object.assign(neu, { Object.assign(neu, {
product: produkte.value[0]?.key || 'briefe', format: neuFormate()[0]?.key || '', font: 'tilda', orderName: '', 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, fontSizeLetter: 100, fontSizeEnvelope: 100,
sigSvg: '', sigLabel: '', sigX: 0, sigY: 0, sigScale: 100, sigSpacing: '', mehrseitig: false, seitenInfo: '', 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()], 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;' }, [ h('div', { style: 'flex:1 1 280px;max-width:420px;' }, [
label('Kunde verknüpfen (E-Mail, optional)'), 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;' }, [ const aktionen = h('div', { class: 'sk-btnrow', style: 'display:flex;gap:8px;flex-wrap:wrap;' }, [

View File

@@ -2053,7 +2053,16 @@ const handler = (router, { services, getSchema, logger }) => {
const role = (await new RolesService({ schema, accountability: null }).readByQuery({ const role = (await new RolesService({ schema, accountability: null }).readByQuery({
filter: { name: { _eq: 'Kunde' } }, limit: 1, fields: ['id'], filter: { name: { _eq: 'Kunde' } }, limit: 1, fields: ['id'],
}))?.[0]; }))?.[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; customerEmail = kundeMail;
} catch (e) { logger.warn(`[skrift-orders] manual-order Kunde anlegen: ${e.message}`); 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 = { const orderData = {
customer: customerId, customer_email: customerEmail, customer: customerId, customer_email: customerEmail,
order_name: clean(body.order_name) || null, order_name: clean(body.order_name) || null,
mails_senden: body.mails_senden !== false,
product: product.id, format: formatId, product: product.id, format: formatId,
person_type: body.person_type === 'unternehmen' ? 'unternehmen' : 'privat', person_type: body.person_type === 'unternehmen' ? 'unternehmen' : 'privat',
font: ['tilda', 'alva', 'ellie'].includes(body.font) ? body.font : 'tilda', font: ['tilda', 'alva', 'ellie'].includes(body.font) ? body.font : 'tilda',