Anfragen-Team-Mails: vollständiger Inhalt (alle Felder)

Muster- und Angebotsanfragen (App sample/offer-request + WordPress inbound) senden
jetzt den kompletten Inhalt als Feld-Tabelle (Name, Kontakt, Adresse, Produkt,
Menge, Nachricht/Vorhaben) – bei WordPress zusätzlich alle weiteren übermittelten
Formularfelder.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
s4luorth
2026-09-18 12:02:45 +02:00
parent f4ce602b7a
commit 2c075f9fb5

View File

@@ -163,6 +163,17 @@ async function paypalToken() {
// Auslandsländer als GANZE Wörter (\b) – sonst matchte „schweiz" in „Schweizer
// Straße", „usa" in „Neusass", „polen" in „Polenz" usw. und flaggte fälschlich Ausland.
const AUSLAND_RE = /\b(österreich|schweiz|austria|switzerland|frankreich|france|italien|italy|niederlande|netherlands|belgien|belgium|luxemburg|luxembourg|spanien|spain|portugal|usa|vereinigte staaten|united states|kanada|canada|england|großbritannien|grossbritannien|united kingdom|dänemark|denmark|schweden|sweden|norwegen|norway|finnland|finland|polen|poland|tschechien|czech|ungarn|hungary|griechenland|greece|irland|ireland|australien|australia|liechtenstein)\b/i;
// HTML-Escaping + Feld-Tabelle für vollständige Anfrage-Team-Mails.
const escH = (s) => String(s == null ? '' : s).replace(/[<>&]/g, (c) => ({ '<': '&lt;', '>': '&gt;', '&': '&amp;' }[c]));
function felderHtml(pairs) {
const rows = pairs
.filter(([, v]) => v != null && String(v).trim())
.map(([k, v]) => `<tr><td style="padding:3px 14px 3px 0;color:#666;vertical-align:top;white-space:nowrap;">${escH(k)}</td>`
+ `<td style="padding:3px 0;"><strong>${escH(v).replace(/\n/g, '<br>')}</strong></td></tr>`)
.join('');
return `<table style="border-collapse:collapse;font-size:14px;line-height:1.5;">${rows}</table>`;
}
// Erstes nicht-leeres Feld aus mehreren möglichen Namen (App + WordPress-Varianten).
function feldWert(body, ...keys) {
for (const k of keys) {
@@ -1577,11 +1588,16 @@ const handler = (router, { services, getSchema, logger }) => {
customer: customer?.id ?? null,
});
// Schlanke interne Benachrichtigung.
const produkt = product ? clean(product) : null;
// Vollständige interne Benachrichtigung.
await teamMail(schema, `Neue Angebotsanfrage: ${name}`,
`<p>Neue Angebotsanfrage von <strong>${name}</strong> (${email}${body.phone ? `, ${clean(String(body.phone))}` : ''})`
+ `${produkt ? ` · ${produkt}` : ''} – im Board unter „Neu".</p>`);
`<p>Neue Angebotsanfrage – im Board unter „Neu".</p>`
+ felderHtml([
['Vorname', clean(vorname)], ['Nachname', clean(nachname)],
['E-Mail', email], ['Telefon', clean(phone)],
['Produkt', product ? clean(product) : ''],
['Menge', Number.isFinite(quantity) && quantity > 0 ? quantity : ''],
['Vorhaben', clean(message)],
]));
return res.json({ ok: true });
} catch (err) {
@@ -1646,11 +1662,15 @@ const handler = (router, { services, getSchema, logger }) => {
customer: customer?.id ?? null,
});
const musterNachricht = clean(String(body.message || '').trim().slice(0, 1000));
const musterNachricht = clean(String(body.message || '').trim().slice(0, 2000));
await teamMail(schema, `Neue Musteranfrage: ${clean(vollName)}`,
`<p>Neue Musteranfrage von <strong>${clean(vollName)}</strong> (${email}${phone ? `, ${clean(phone)}` : ''})`
+ ` · Produkt: ${clean(product)} – im Board unter „Neu".</p>`
+ (musterNachricht ? `<p><strong>Nachricht:</strong><br>${musterNachricht.replace(/\n/g, '<br>')}</p>` : ''));
`<p>Neue Musteranfrage – im Board unter „Neu".</p>`
+ felderHtml([
['Vorname', clean(first)], ['Nachname', clean(last)],
['E-Mail', email], ['Telefon', clean(phone)], ['Produkt', clean(product)],
['Straße', clean(street)], ['PLZ', clean(zip)], ['Ort', clean(city)], ['Land', clean(country)],
['Nachricht', musterNachricht],
]));
return res.json({ ok: true });
} catch (err) {
@@ -1750,10 +1770,11 @@ const handler = (router, { services, getSchema, logger }) => {
status: 'neu', customer: customer?.id ?? null,
};
subject = `Neue Musteranfrage: ${name}`;
mailHtml =
`<p><strong>${clean(name)}</strong> (${email}${phone ? `, ${phone}` : ''})</p>` +
`<p>Interesse: <strong>${produkt ? clean(produkt) : 'k. A.'}</strong></p>` +
`<p>${clean(strasse)}, ${clean(plz)} ${clean(ort)}, ${clean(land)}</p>`;
mailHtml = felderHtml([
['Vorname', clean(vorname)], ['Nachname', clean(nachname)],
['E-Mail', email], ['Telefon', phone], ['Produkt', produkt ? clean(produkt) : ''],
['Straße', clean(strasse)], ['PLZ', clean(plz)], ['Ort', clean(ort)], ['Land', clean(land)],
]);
} else {
const unternehmen = clean(String(body.unternehmen || '').trim());
const nachricht = clean(String(body.nachricht || body.message || '').trim());
@@ -1764,11 +1785,18 @@ const handler = (router, { services, getSchema, logger }) => {
status: 'neu', customer: customer?.id ?? null,
};
subject = `Neue Angebotsanfrage: ${name}`;
mailHtml =
`<p><strong>${clean(name)}</strong>${unternehmen ? ` · ${unternehmen}` : ''} (${email}${phone ? `, ${phone}` : ''})</p>` +
`<p style="white-space:pre-wrap">${nachricht || '—'}</p>`;
mailHtml = felderHtml([
['Vorname', clean(vorname)], ['Nachname', clean(nachname)], ['Unternehmen', unternehmen],
['E-Mail', email], ['Telefon', phone], ['Nachricht', nachricht],
]);
}
// „Gesamter Inhalt": alle übermittelten Felder (außer technische) mit anhängen,
// damit auch nicht standardisierte Formularfelder in der Mail landen.
const TECH = new Set(['token', 'dataprotection', '__form_id', '__refer', '__is_ajax', '__is_ajax_submit']);
const rest = Object.entries(body).filter(([k, v]) => !TECH.has(k.toLowerCase()) && String(v ?? '').trim());
if (rest.length) mailHtml += `<p style="margin:14px 0 4px;color:#888;font-size:12px;">Alle übermittelten Felder:</p>` + felderHtml(rest.map(([k, v]) => [k, clean(String(v))]));
await svc('offer_requests').createOne(record);
logger.info(`[skrift-orders] inbound/${kind}: Anfrage von ${email} gespeichert.`);