933 lines
42 KiB
TypeScript
933 lines
42 KiB
TypeScript
import nodemailer from "nodemailer";
|
||
import { queueEmail } from "./email-queue";
|
||
|
||
// Port 587 = STARTTLS (bestätigt erreichbar vom Server + Docker-Container)
|
||
// Port 465 = SSL/TLS (auf diesem Server geblockt – nicht verwenden)
|
||
const transporter = nodemailer.createTransport({
|
||
host: process.env.SMTP_HOST,
|
||
port: Number(process.env.SMTP_PORT ?? 587),
|
||
secure: false, // STARTTLS auf Port 587
|
||
auth: {
|
||
user: process.env.SMTP_USER,
|
||
pass: process.env.SMTP_PASS,
|
||
},
|
||
connectionTimeout: 15000, // 15 s (war default 2 min – schlägt jetzt schneller fehl)
|
||
greetingTimeout: 10000,
|
||
socketTimeout: 20000,
|
||
tls: {
|
||
rejectUnauthorized: false,
|
||
ciphers: "SSLv3", // Kompatibilitätsmodus für ältere SMTP-Server
|
||
},
|
||
});
|
||
|
||
export interface AnfrageEmailData {
|
||
anfrageId: string;
|
||
firma: string;
|
||
telefon: string;
|
||
email: string;
|
||
positionen: {
|
||
maschineName: string;
|
||
mietbeginn: string;
|
||
mietende: string;
|
||
gesamtTage: number;
|
||
lieferung: boolean;
|
||
lieferadresse: string;
|
||
anmerkung: string;
|
||
tagessatz: number | null;
|
||
preisStufe?: "tag" | "woche" | "monat" | null;
|
||
zubehoer?: {
|
||
id: string;
|
||
name: string;
|
||
preisTag: number | null;
|
||
preisWoche?: number | null;
|
||
preisMonat?: number | null;
|
||
}[];
|
||
}[];
|
||
}
|
||
|
||
// ─── Preisberechnung ──────────────────────────────────────────────────────
|
||
const VERSICHERUNG_PROZENT = 7.5;
|
||
const MWST_PROZENT = 19;
|
||
|
||
function fmt(n: number) {
|
||
return n.toLocaleString("de-DE", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
||
}
|
||
|
||
function formatDatum(iso: string) {
|
||
return new Date(iso).toLocaleDateString("de-DE", {
|
||
day: "2-digit",
|
||
month: "2-digit",
|
||
year: "numeric",
|
||
});
|
||
}
|
||
|
||
/** Alle Sonntage zwischen von und bis (inkl.) als formatierte Strings */
|
||
function getSonntage(von: string, bis: string): string[] {
|
||
const result: string[] = [];
|
||
const end = new Date(bis);
|
||
for (let d = new Date(von); d <= end; d.setDate(d.getDate() + 1)) {
|
||
if (d.getDay() === 0) {
|
||
result.push(
|
||
new Date(d).toLocaleDateString("de-DE", { day: "2-digit", month: "2-digit" })
|
||
);
|
||
}
|
||
}
|
||
return result;
|
||
}
|
||
|
||
/** Kalendertage (inkl. Sonntage) zwischen von und bis */
|
||
export function getKalenderTage(von: string, bis: string): number {
|
||
const start = new Date(von);
|
||
const end = new Date(bis);
|
||
return Math.ceil((end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24)) + 1;
|
||
}
|
||
|
||
function zubehoerTagessatz(
|
||
z: { preisTag: number | null; preisWoche?: number | null; preisMonat?: number | null },
|
||
stufe: string | null | undefined
|
||
): number | null {
|
||
if (stufe === "monat" && z.preisMonat != null) return z.preisMonat;
|
||
if (stufe === "woche" && z.preisWoche != null) return z.preisWoche;
|
||
return z.preisTag;
|
||
}
|
||
|
||
function positionNetto(p: AnfrageEmailData["positionen"][number]): number {
|
||
const maschine = p.tagessatz != null ? p.tagessatz * p.gesamtTage : 0;
|
||
const zubehoer = (p.zubehoer ?? []).reduce((sum, z) => {
|
||
const rate = zubehoerTagessatz(z, p.preisStufe);
|
||
return sum + (rate != null ? rate * p.gesamtTage : 0);
|
||
}, 0);
|
||
return maschine + zubehoer;
|
||
}
|
||
|
||
function buildPreisBlock(positionen: AnfrageEmailData["positionen"]) {
|
||
const gesamtNetto = positionen.reduce((s, p) => s + positionNetto(p), 0);
|
||
const versicherungBetrag = gesamtNetto * (VERSICHERUNG_PROZENT / 100);
|
||
const mwstBetrag = (gesamtNetto + versicherungBetrag) * (MWST_PROZENT / 100);
|
||
const gesamtBrutto = gesamtNetto + versicherungBetrag + mwstBetrag;
|
||
|
||
const posRows = positionen.map((p, i) => {
|
||
const hatPreis = p.tagessatz != null;
|
||
const netto = hatPreis ? positionNetto(p) : null;
|
||
const zubehoerMitPreis = (p.zubehoer ?? []).filter(
|
||
(z) => zubehoerTagessatz(z, p.preisStufe) != null
|
||
);
|
||
|
||
// Sonntage berechnen
|
||
const kalenderTage = getKalenderTage(p.mietbeginn, p.mietende);
|
||
const sonntage = getSonntage(p.mietbeginn, p.mietende);
|
||
const hatSonntage = sonntage.length > 0 && kalenderTage !== p.gesamtTage;
|
||
|
||
const zubehoerRows = zubehoerMitPreis
|
||
.map((z) => {
|
||
const rate = zubehoerTagessatz(z, p.preisStufe)!;
|
||
return `<tr>
|
||
<td style="padding:2px 12px 2px 28px;color:#64748b;font-size:12px" colspan="2">↳ ${z.name}</td>
|
||
<td style="padding:2px 12px;font-family:monospace;font-size:12px;color:#475569;text-align:right">${fmt(rate * p.gesamtTage)} €</td>
|
||
</tr>`;
|
||
})
|
||
.join("");
|
||
|
||
const sonntageHtml = hatSonntage
|
||
? `<span style="font-size:11px;color:#94a3b8;display:block">
|
||
${kalenderTage} Kalendertage – ${sonntage.length} Sonntag${sonntage.length > 1 ? "e" : ""} nicht berechnet
|
||
(${sonntage.join(", ")})
|
||
</span>`
|
||
: "";
|
||
|
||
const details = [
|
||
p.tagessatz ? `${p.tagessatz} €/Tag · ${p.gesamtTage} berechnete Tag${p.gesamtTage !== 1 ? "e" : ""}` : "",
|
||
p.lieferung ? `Lieferung: ${p.lieferadresse}` : "",
|
||
p.anmerkung ? `Anmerkung: ${p.anmerkung}` : "",
|
||
]
|
||
.filter(Boolean)
|
||
.map((d) => `<span style="font-size:11px;color:#94a3b8;display:block">${d}</span>`)
|
||
.join("");
|
||
|
||
return `<tr style="background:${i % 2 === 0 ? "#f8fafc" : "#fff"}">
|
||
<td style="padding:8px 12px;font-weight:600;color:#1c1917;border-bottom:1px solid #e2e8f0;font-size:13px">
|
||
${i + 1}. ${p.maschineName}
|
||
</td>
|
||
<td style="padding:8px 12px;color:#475569;font-size:13px;border-bottom:1px solid #e2e8f0">
|
||
${formatDatum(p.mietbeginn)} – ${formatDatum(p.mietende)}
|
||
${details}
|
||
${sonntageHtml}
|
||
</td>
|
||
<td style="padding:8px 12px;font-family:monospace;font-weight:600;color:#1c1917;border-bottom:1px solid #e2e8f0;text-align:right;white-space:nowrap;font-size:13px">
|
||
${netto != null ? `${fmt(netto)} €` : "Auf Anfrage"}
|
||
</td>
|
||
</tr>${zubehoerRows}`;
|
||
}).join("");
|
||
|
||
const html = `
|
||
<table style="width:100%;border-collapse:collapse">
|
||
<thead>
|
||
<tr style="background:#1c1917">
|
||
<th style="padding:8px 12px;text-align:left;color:#f7d334;font-weight:600;font-size:13px">Maschine / Gerät</th>
|
||
<th style="padding:8px 12px;text-align:left;color:rgba(255,255,255,0.75);font-weight:500;font-size:12px">Zeitraum & Details</th>
|
||
<th style="padding:8px 12px;text-align:right;color:rgba(255,255,255,0.75);font-weight:500;font-size:12px">Mietpreis</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>${posRows}</tbody>
|
||
</table>
|
||
<table style="width:100%;border-collapse:collapse;margin-top:2px">
|
||
<tr>
|
||
<td style="padding:6px 12px;color:#64748b;font-size:13px">Zwischensumme (netto)</td>
|
||
<td style="padding:6px 12px;font-family:monospace;text-align:right;font-size:13px">${fmt(gesamtNetto)} €</td>
|
||
</tr>
|
||
<tr>
|
||
<td style="padding:6px 12px;color:#64748b;font-size:13px">+ Versicherung (${VERSICHERUNG_PROZENT} %)</td>
|
||
<td style="padding:6px 12px;font-family:monospace;text-align:right;font-size:13px">${fmt(versicherungBetrag)} €</td>
|
||
</tr>
|
||
<tr style="border-top:1px solid #e2e8f0">
|
||
<td style="padding:6px 12px;color:#475569;font-size:13px">Summe netto inkl. Versicherung</td>
|
||
<td style="padding:6px 12px;font-family:monospace;text-align:right;font-size:13px">${fmt(gesamtNetto + versicherungBetrag)} €</td>
|
||
</tr>
|
||
<tr>
|
||
<td style="padding:6px 12px;color:#64748b;font-size:13px">+ MwSt. (${MWST_PROZENT} %)</td>
|
||
<td style="padding:6px 12px;font-family:monospace;text-align:right;font-size:13px">${fmt(mwstBetrag)} €</td>
|
||
</tr>
|
||
<tr style="border-top:2px solid #f7d334">
|
||
<td style="padding:10px 12px;font-weight:700;font-size:15px;color:#1c1917">Gesamtbetrag (brutto)</td>
|
||
<td style="padding:10px 12px;font-family:monospace;font-weight:700;font-size:15px;color:#1c1917;text-align:right">${fmt(gesamtBrutto)} €</td>
|
||
</tr>
|
||
</table>`;
|
||
|
||
const text =
|
||
positionen
|
||
.map((p, i) => {
|
||
const netto = positionNetto(p);
|
||
const kalenderTage = getKalenderTage(p.mietbeginn, p.mietende);
|
||
const sonntage = getSonntage(p.mietbeginn, p.mietende);
|
||
const hatSonntage = sonntage.length > 0 && kalenderTage !== p.gesamtTage;
|
||
const lines = [
|
||
`${i + 1}. ${p.maschineName}`,
|
||
` ${formatDatum(p.mietbeginn)} – ${formatDatum(p.mietende)}`,
|
||
hatSonntage
|
||
? ` ${kalenderTage} Kalendertage – ${sonntage.length} Sonntag${sonntage.length > 1 ? "e" : ""} nicht berechnet (${sonntage.join(", ")})`
|
||
: null,
|
||
p.tagessatz
|
||
? ` ${p.tagessatz} €/Tag × ${p.gesamtTage} Tage = ${fmt(p.tagessatz * p.gesamtTage)} € Maschinenmiete`
|
||
: " Preis auf Anfrage",
|
||
...(p.zubehoer ?? [])
|
||
.filter((z) => zubehoerTagessatz(z, p.preisStufe) != null)
|
||
.map((z) => {
|
||
const rate = zubehoerTagessatz(z, p.preisStufe)!;
|
||
return ` + ${z.name}: ${fmt(rate * p.gesamtTage)} €`;
|
||
}),
|
||
p.lieferung ? ` Lieferung: ${p.lieferadresse}` : null,
|
||
p.anmerkung ? ` Anmerkung: ${p.anmerkung}` : null,
|
||
` Positionssumme: ${fmt(netto)} €`,
|
||
].filter((l): l is string => l != null && l !== "");
|
||
return lines.join("\n");
|
||
})
|
||
.join("\n\n") +
|
||
`\n\n${"─".repeat(40)}\nZwischensumme (netto): ${fmt(gesamtNetto)} €\n+ Versicherung ${VERSICHERUNG_PROZENT} %: ${fmt(versicherungBetrag)} €\n+ MwSt. ${MWST_PROZENT} %: ${fmt(mwstBetrag)} €\n${"─".repeat(40)}\nGesamtbetrag (brutto): ${fmt(gesamtBrutto)} €`;
|
||
|
||
return { html, text, gesamtNetto, versicherungBetrag, mwstBetrag, gesamtBrutto };
|
||
}
|
||
|
||
// ─── Robuster Send mit Queue-Fallback ────────────────────────────────────
|
||
interface MailOptions {
|
||
from: string;
|
||
to: string | undefined;
|
||
replyTo?: string;
|
||
subject: string;
|
||
text: string;
|
||
html: string;
|
||
}
|
||
|
||
async function sendWithFallback(options: MailOptions, label: string) {
|
||
if (!options.to) {
|
||
console.error(`[Mailer] Kein Empfänger für "${label}" – Mail übersprungen`);
|
||
return;
|
||
}
|
||
try {
|
||
await transporter.sendMail(options);
|
||
console.log(`[Mailer] ✓ Mail "${label}" an "${options.to}" gesendet`);
|
||
} catch (err) {
|
||
const msg = err instanceof Error ? err.message : String(err);
|
||
console.error(`[Mailer] ✗ Mail "${label}" fehlgeschlagen (${msg}) – in Queue gespeichert`);
|
||
await queueEmail({
|
||
mail_from: options.from,
|
||
mail_to: options.to,
|
||
reply_to: options.replyTo,
|
||
subject: options.subject,
|
||
html: options.html,
|
||
body_text: options.text,
|
||
});
|
||
}
|
||
}
|
||
|
||
// ─── Kontaktformular ──────────────────────────────────────────────────────
|
||
export interface KontaktEmailData {
|
||
name: string;
|
||
anrede?: string;
|
||
telefon: string;
|
||
email: string;
|
||
betreff: string;
|
||
nachricht?: string;
|
||
}
|
||
|
||
export async function sendeKontaktEmail(data: KontaktEmailData) {
|
||
const anrede = data.anrede
|
||
? `${data.anrede.charAt(0).toUpperCase() + data.anrede.slice(1)} `
|
||
: "";
|
||
const html = `
|
||
<!DOCTYPE html>
|
||
<html lang="de">
|
||
<head><meta charset="UTF-8"></head>
|
||
<body style="font-family:system-ui,sans-serif;color:#1c1917;max-width:600px;margin:0 auto;padding:0">
|
||
<div style="background:#1c1917;padding:20px 24px">
|
||
<h1 style="color:#f7d334;margin:0;font-size:20px;font-weight:700">Mietpark Hahn</h1>
|
||
<p style="color:rgba(255,255,255,0.7);margin:4px 0 0;font-size:13px">Neue Kontaktanfrage</p>
|
||
</div>
|
||
<div style="padding:24px;background:#fff">
|
||
<h2 style="margin:0 0 16px;font-size:18px">Kontaktanfrage von ${anrede}${data.name}</h2>
|
||
<table style="width:100%;border-collapse:collapse;margin-bottom:24px">
|
||
<tr><td style="padding:6px 0;color:#64748b;width:120px">Name</td><td style="padding:6px 0;font-weight:600">${anrede}${data.name}</td></tr>
|
||
<tr><td style="padding:6px 0;color:#64748b">Telefon</td><td style="padding:6px 0"><a href="tel:${data.telefon}" style="color:#1c1917">${data.telefon}</a></td></tr>
|
||
<tr><td style="padding:6px 0;color:#64748b">E-Mail</td><td style="padding:6px 0"><a href="mailto:${data.email}" style="color:#1c1917">${data.email}</a></td></tr>
|
||
<tr><td style="padding:6px 0;color:#64748b">Betreff</td><td style="padding:6px 0">${data.betreff}</td></tr>
|
||
</table>
|
||
${data.nachricht ? `<div style="padding:12px 16px;background:#f8fafc;border-left:3px solid #f7d334"><p style="margin:0;font-size:14px;white-space:pre-wrap">${data.nachricht}</p></div>` : ""}
|
||
</div>
|
||
</body>
|
||
</html>`;
|
||
|
||
await sendWithFallback({
|
||
from: `"Mietpark Hahn" <${process.env.SMTP_FROM}>`,
|
||
to: process.env.SMTP_TO,
|
||
replyTo: data.email,
|
||
subject: `Kontaktanfrage: ${anrede}${data.name} – ${data.betreff} – Mietpark Hahn`,
|
||
text: `Neue Kontaktanfrage\n\nName: ${anrede}${data.name}\nTelefon: ${data.telefon}\nE-Mail: ${data.email}\nBetreff: ${data.betreff}${data.nachricht ? `\n\nNachricht:\n${data.nachricht}` : ""}`,
|
||
html,
|
||
}, `Kontaktanfrage ${data.name}`);
|
||
}
|
||
|
||
// ─── Kunden-Eingangsbestätigung ───────────────────────────────────────────
|
||
export async function sendeKundenEingangsbestaetigung(data: AnfrageEmailData) {
|
||
const { html: preisHtml, text: preisText } = buildPreisBlock(data.positionen);
|
||
|
||
// Build a simple equipment list
|
||
const equipmentHtml = `
|
||
<div style="margin-bottom:24px">
|
||
<h3 style="margin:0 0 12px;font-size:13px;text-transform:uppercase;letter-spacing:0.05em;color:#64748b">
|
||
Ihre gemieteten Geräte
|
||
</h3>
|
||
<ul style="margin:0;padding-left:20px;list-style:none">
|
||
${data.positionen
|
||
.map((p) => `
|
||
<li style="margin:0 0 6px;padding:0;font-size:14px;color:#1c1917">
|
||
<span style="display:inline-block;width:6px;height:6px;background:#f7d334;border-radius:50%;margin-right:8px;vertical-align:middle"></span>
|
||
<strong>${p.maschineName}</strong>
|
||
<span style="color:#64748b"> · ${formatDatum(p.mietbeginn)} bis ${formatDatum(p.mietende)}</span>
|
||
</li>`)
|
||
.join("")}
|
||
</ul>
|
||
</div>
|
||
`;
|
||
|
||
const html = `
|
||
<!DOCTYPE html>
|
||
<html lang="de">
|
||
<head><meta charset="UTF-8"></head>
|
||
<body style="font-family:system-ui,sans-serif;color:#1c1917;max-width:600px;margin:0 auto;padding:0">
|
||
<div style="background:#1c1917;padding:20px 24px">
|
||
<h1 style="color:#f7d334;margin:0;font-size:20px;font-weight:700">Mietpark Hahn</h1>
|
||
<p style="color:rgba(255,255,255,0.7);margin:4px 0 0;font-size:13px">Ihre Mietanfrage ist eingegangen</p>
|
||
</div>
|
||
<div style="padding:24px;background:#fff">
|
||
<h2 style="margin:0 0 8px;font-size:18px">Vielen Dank für Ihre Anfrage!</h2>
|
||
<p style="color:#475569;margin:0 0 24px;font-size:14px">
|
||
Guten Tag ${data.firma},<br>
|
||
wir haben Ihre Mietanfrage erhalten und werden uns schnellstmöglich bei Ihnen melden.
|
||
</p>
|
||
|
||
${equipmentHtml}
|
||
|
||
<h3 style="margin:0 0 12px;font-size:15px;border-top:2px solid #f7d334;padding-top:16px">
|
||
Detaillierte Preisübersicht
|
||
</h3>
|
||
${preisHtml}
|
||
|
||
<div style="margin-top:20px;padding:14px 16px;background:#fef3c7;border-left:3px solid #f7d334">
|
||
<p style="margin:0;font-size:13px;color:#92400e">
|
||
<strong>Hinweis:</strong> Die angezeigten Preise sind Mietpreise inkl. ${VERSICHERUNG_PROZENT} % Versicherung und ${MWST_PROZENT} % MwSt.
|
||
Die Bestätigung erfolgt nach Prüfung durch den Verleih.
|
||
</p>
|
||
</div>
|
||
|
||
<p style="margin-top:20px;font-size:13px;color:#64748b">
|
||
Bei Fragen erreichen Sie uns unter
|
||
<a href="tel:${process.env.COMPANY_PHONE ?? ""}" style="color:#1c1917">${process.env.COMPANY_PHONE ?? ""}</a>
|
||
oder per E-Mail an
|
||
<a href="mailto:${process.env.SMTP_FROM ?? ""}" style="color:#1c1917">${process.env.SMTP_FROM ?? ""}</a>
|
||
</p>
|
||
</div>
|
||
<div style="padding:12px 24px;background:#f8fafc;border-top:1px solid #e2e8f0">
|
||
<p style="margin:0 0 8px;font-size:11px;color:#94a3b8">
|
||
Mit der Nutzung unserer Dienste akzeptieren Sie unsere
|
||
<a href="${process.env.APP_URL ?? "https://www.mietparkhahn.de"}/agb" style="color:#64748b">Allgemeinen Geschäftsbedingungen</a>.
|
||
</p>
|
||
<p style="margin:0;font-size:11px;color:#94a3b8">
|
||
Mietpark Hahn · Anfrage-ID: ${data.anfrageId}
|
||
</p>
|
||
</div>
|
||
</body>
|
||
</html>`;
|
||
|
||
const equipmentList = data.positionen
|
||
.map((p) => `• ${p.maschineName} (${formatDatum(p.mietbeginn)} bis ${formatDatum(p.mietende)})`)
|
||
.join("\n");
|
||
|
||
await sendWithFallback({
|
||
from: `"Mietpark Hahn" <${process.env.SMTP_FROM}>`,
|
||
to: data.email,
|
||
subject: `Ihre Mietanfrage ist eingegangen – Mietpark Hahn`,
|
||
text: `Guten Tag ${data.firma},\n\nvielen Dank für Ihre Mietanfrage. Wir werden uns schnellstmöglich bei Ihnen melden.\n\nIhre gemieteten Geräte:\n${equipmentList}\n\nDetaillierte Preisübersicht:\n\n${preisText}\n\nHinweis: Die angezeigten Preise sind Mietpreise inkl. ${VERSICHERUNG_PROZENT} % Versicherung und ${MWST_PROZENT} % MwSt. Die Bestätigung erfolgt nach Prüfung durch den Verleih.\n\nMit freundlichen Grüßen\nMietpark Hahn\nAnfrage-ID: ${data.anfrageId}\n\nUnsere AGB finden Sie unter: ${process.env.APP_URL ?? "https://www.mietparkhahn.de"}/agb`,
|
||
html,
|
||
}, `Kundeneingang ${data.firma}`);
|
||
}
|
||
|
||
// ─── Admin-Benachrichtigung (Vermieter) ───────────────────────────────────
|
||
export async function sendeAnfrageEmail(data: AnfrageEmailData) {
|
||
const { html: preisHtml, text: preisText } = buildPreisBlock(data.positionen);
|
||
|
||
// Action-Tokens für direkte Email-Links generieren
|
||
const { createActionToken } = await import("@/lib/admin-auth");
|
||
const [tokenBestaetigt, tokenAbgelehnt, tokenAbgeschlossen] = await Promise.all([
|
||
createActionToken(data.anfrageId, "bestaetigt"),
|
||
createActionToken(data.anfrageId, "abgelehnt"),
|
||
createActionToken(data.anfrageId, "abgeschlossen"),
|
||
]);
|
||
|
||
const baseUrl = process.env.APP_URL ?? "https://www.mietparkhahn.de";
|
||
const urlBestaetigt = `${baseUrl}/api/admin/anfragen-action?token=${tokenBestaetigt}`;
|
||
const urlAbgelehnt = `${baseUrl}/api/admin/anfragen-action?token=${tokenAbgelehnt}`;
|
||
const urlAbgeschlossen = `${baseUrl}/api/admin/anfragen-action?token=${tokenAbgeschlossen}`;
|
||
|
||
const html = `
|
||
<!DOCTYPE html>
|
||
<html lang="de">
|
||
<head><meta charset="UTF-8"></head>
|
||
<body style="font-family:system-ui,sans-serif;color:#1c1917;max-width:600px;margin:0 auto;padding:0">
|
||
<div style="background:#1c1917;padding:20px 24px">
|
||
<h1 style="color:#f7d334;margin:0;font-size:20px;font-weight:700">Mietpark Hahn</h1>
|
||
<p style="color:rgba(255,255,255,0.7);margin:4px 0 0;font-size:13px">Neue Mietanfrage eingegangen</p>
|
||
</div>
|
||
<div style="padding:24px;background:#fff">
|
||
|
||
<h2 style="margin:0 0 16px;font-size:18px">Neue Mietanfrage</h2>
|
||
|
||
<h3 style="margin:0 0 10px;font-size:13px;text-transform:uppercase;letter-spacing:0.05em;color:#64748b">
|
||
Kontaktdaten Kunde
|
||
</h3>
|
||
<table style="width:100%;border-collapse:collapse;margin-bottom:24px;font-size:13px;background:#f8fafc;border:1px solid #e2e8f0">
|
||
<tr>
|
||
<td style="padding:8px 12px;color:#64748b;width:130px;border-bottom:1px solid #e2e8f0">Firma / Name</td>
|
||
<td style="padding:8px 12px;font-weight:600;border-bottom:1px solid #e2e8f0">${data.firma}</td>
|
||
</tr>
|
||
<tr>
|
||
<td style="padding:8px 12px;color:#64748b;border-bottom:1px solid #e2e8f0">Telefon</td>
|
||
<td style="padding:8px 12px;border-bottom:1px solid #e2e8f0">
|
||
<a href="tel:${data.telefon}" style="color:#1c1917;font-weight:600">${data.telefon}</a>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<td style="padding:8px 12px;color:#64748b">E-Mail</td>
|
||
<td style="padding:8px 12px">
|
||
<a href="mailto:${data.email}" style="color:#1c1917">${data.email}</a>
|
||
</td>
|
||
</tr>
|
||
</table>
|
||
|
||
<h3 style="margin:0 0 12px;font-size:13px;text-transform:uppercase;letter-spacing:0.05em;color:#64748b;border-top:1px solid #e2e8f0;padding-top:16px">
|
||
Angefragte Maschinen & Preisübersicht
|
||
</h3>
|
||
${preisHtml}
|
||
|
||
<div style="margin:24px 0 0;display:flex;gap:12px;flex-wrap:wrap">
|
||
<a href="${urlBestaetigt}"
|
||
style="background:#16a34a;color:#fff;padding:10px 20px;border-radius:4px;font-weight:600;font-size:14px;text-decoration:none;display:inline-block">
|
||
✓ Bestätigen
|
||
</a>
|
||
<a href="${urlAbgelehnt}"
|
||
style="background:#dc2626;color:#fff;padding:10px 20px;border-radius:4px;font-weight:600;font-size:14px;text-decoration:none;display:inline-block">
|
||
✕ Ablehnen
|
||
</a>
|
||
<a href="${urlAbgeschlossen}"
|
||
style="background:#64748b;color:#fff;padding:10px 20px;border-radius:4px;font-weight:600;font-size:14px;text-decoration:none;display:inline-block">
|
||
✓ Abschließen
|
||
</a>
|
||
</div>
|
||
|
||
<div style="margin-top:20px;padding:12px 16px;background:#f8fafc;border-left:3px solid #f7d334">
|
||
<p style="margin:0;font-size:12px;color:#64748b">
|
||
Anfrage-ID: <code style="background:#e2e8f0;padding:1px 4px;border-radius:2px">${data.anfrageId}</code><br>
|
||
<a href="${baseUrl}/admin/anfragen/${data.anfrageId}"
|
||
style="color:#1c1917;font-weight:600">→ Anfrage im Admin öffnen</a>
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</body>
|
||
</html>`;
|
||
|
||
const posText = `Kontakt:\nFirma/Name: ${data.firma}\nTelefon: ${data.telefon}\nE-Mail: ${data.email}\n\nMaschinen & Preise:\n\n${preisText}`;
|
||
|
||
const actionLinks = `\nQuickaktion aus Email:\n✓ Bestätigen: ${urlBestaetigt}\n✕ Ablehnen: ${urlAbgelehnt}\n✓ Abschließen: ${urlAbgeschlossen}`;
|
||
|
||
await sendWithFallback({
|
||
from: `"Mietpark Hahn" <${process.env.SMTP_FROM}>`,
|
||
to: process.env.SMTP_TO,
|
||
subject: `Neue Mietanfrage: ${data.firma} – ${data.positionen.length} Gerät${data.positionen.length !== 1 ? "e" : ""} – Mietpark Hahn`,
|
||
text: `Neue Mietanfrage\n\n${posText}\n\nAdmin: ${baseUrl}/admin/anfragen/${data.anfrageId}${actionLinks}`,
|
||
html,
|
||
}, `Admin-Anfrage ${data.firma}`);
|
||
}
|
||
|
||
// ─── Kunden-Statusbenachrichtigung ────────────────────────────────────────
|
||
export interface StatusEmailData {
|
||
anfrageId: string;
|
||
firma: string;
|
||
email: string;
|
||
neuerStatus: "bestaetigt" | "abgelehnt" | "abgeschlossen";
|
||
notizen?: string;
|
||
positionen?: {
|
||
maschineName: string;
|
||
mietbeginn: string;
|
||
mietende: string;
|
||
gesamtTage: number;
|
||
lieferung: boolean;
|
||
lieferadresse: string;
|
||
anmerkung: string;
|
||
tagessatz: number | null;
|
||
preisStufe?: "tag" | "woche" | "monat" | null;
|
||
zubehoer?: {
|
||
id: string;
|
||
name: string;
|
||
preisTag: number | null;
|
||
preisWoche?: number | null;
|
||
preisMonat?: number | null;
|
||
}[];
|
||
}[];
|
||
}
|
||
|
||
const STATUS_TEXTE: Record<
|
||
StatusEmailData["neuerStatus"],
|
||
{ betreff: string; headline: string; text: string; farbe: string }
|
||
> = {
|
||
bestaetigt: {
|
||
betreff: "Ihre Mietanfrage wurde bestätigt – Mietpark Hahn",
|
||
headline: "Ihre Anfrage ist bestätigt!",
|
||
text: "Wir freuen uns, Ihnen mitteilen zu können, dass Ihre Mietanfrage bestätigt wurde. Wir setzen uns zur Vorbereitung mit Ihnen in Verbindung.",
|
||
farbe: "#16a34a",
|
||
},
|
||
abgelehnt: {
|
||
betreff: "Zu Ihrer Mietanfrage – Mietpark Hahn",
|
||
headline: "Zu Ihrer Mietanfrage",
|
||
text: "Leider können wir Ihre Mietanfrage im angefragten Zeitraum nicht erfüllen. Bitte kontaktieren Sie uns für alternative Termine.",
|
||
farbe: "#dc2626",
|
||
},
|
||
abgeschlossen: {
|
||
betreff: "Ihre Miete wurde abgeschlossen – Mietpark Hahn",
|
||
headline: "Vielen Dank!",
|
||
text: "Ihre Miete wurde erfolgreich abgeschlossen. Wir hoffen, Sie waren mit unserem Service zufrieden und freuen uns auf Ihre nächste Anfrage.",
|
||
farbe: "#475569",
|
||
},
|
||
};
|
||
|
||
export async function sendeKundenStatusEmail(data: StatusEmailData) {
|
||
const info = STATUS_TEXTE[data.neuerStatus];
|
||
|
||
// Geräte-Übersicht (falls vorhanden)
|
||
const equipmentHtml = data.positionen ? `
|
||
<div style="margin-bottom:24px">
|
||
<h3 style="margin:0 0 12px;font-size:13px;text-transform:uppercase;letter-spacing:0.05em;color:#64748b">
|
||
Ihre gemieteten Geräte
|
||
</h3>
|
||
<ul style="margin:0;padding-left:20px;list-style:none">
|
||
${data.positionen
|
||
.map((p) => `
|
||
<li style="margin:0 0 6px;padding:0;font-size:14px;color:#1c1917">
|
||
<span style="display:inline-block;width:6px;height:6px;background:#f7d334;border-radius:50%;margin-right:8px;vertical-align:middle"></span>
|
||
<strong>${p.maschineName}</strong>
|
||
<span style="color:#64748b"> · ${formatDatum(p.mietbeginn)} bis ${formatDatum(p.mietende)}</span>
|
||
</li>`)
|
||
.join("")}
|
||
</ul>
|
||
</div>
|
||
` : "";
|
||
|
||
// Preisblock (falls vorhanden)
|
||
const preisBlock = data.positionen ? buildPreisBlock(data.positionen) : null;
|
||
const preisHtml = preisBlock?.html ?? "";
|
||
|
||
const html = `
|
||
<!DOCTYPE html>
|
||
<html lang="de">
|
||
<head><meta charset="UTF-8"></head>
|
||
<body style="font-family:system-ui,sans-serif;color:#1c1917;max-width:600px;margin:0 auto;padding:0">
|
||
<div style="background:#1c1917;padding:20px 24px">
|
||
<h1 style="color:#f7d334;margin:0;font-size:20px;font-weight:700">Mietpark Hahn</h1>
|
||
<p style="color:rgba(255,255,255,0.7);margin:4px 0 0;font-size:13px">Update zu Ihrer Mietanfrage</p>
|
||
</div>
|
||
<div style="padding:24px;background:#fff">
|
||
<div style="border-left:4px solid ${info.farbe};padding-left:16px;margin-bottom:20px">
|
||
<h2 style="margin:0 0 8px;font-size:18px">${info.headline}</h2>
|
||
<p style="margin:0;color:#475569;font-size:14px">Guten Tag ${data.firma},<br>${info.text}</p>
|
||
</div>
|
||
|
||
${equipmentHtml}
|
||
|
||
${data.positionen && preisHtml ? `
|
||
<h3 style="margin:0 0 12px;font-size:15px;border-top:2px solid #f7d334;padding-top:16px">
|
||
Detaillierte Preisübersicht
|
||
</h3>
|
||
${preisHtml}
|
||
` : ""}
|
||
|
||
${
|
||
data.notizen
|
||
? `<div style="padding:12px 16px;background:#f8fafc;border:1px solid #e2e8f0;margin-bottom:20px;margin-top:20px">
|
||
<p style="margin:0 0 4px;font-size:12px;color:#64748b;font-weight:600">NACHRICHT VOM VERLEIH</p>
|
||
<p style="margin:0;font-size:14px;color:#1c1917;white-space:pre-wrap">${data.notizen}</p>
|
||
</div>`
|
||
: ""
|
||
}
|
||
<p style="margin-top:20px;font-size:13px;color:#64748b">
|
||
Bei Fragen erreichen Sie uns unter
|
||
<a href="mailto:${process.env.SMTP_FROM}" style="color:#1c1917">${process.env.SMTP_FROM ?? ""}</a>
|
||
</p>
|
||
</div>
|
||
<div style="padding:12px 24px;background:#f8fafc;border-top:1px solid #e2e8f0">
|
||
<p style="margin:0;font-size:11px;color:#94a3b8">
|
||
Mit der Nutzung unserer Dienste akzeptieren Sie unsere
|
||
<a href="${process.env.APP_URL ?? "https://www.mietparkhahn.de"}/agb" style="color:#64748b">Allgemeinen Geschäftsbedingungen</a>.
|
||
</p>
|
||
</div>
|
||
</body>
|
||
</html>`;
|
||
|
||
const equipmentList = data.positionen
|
||
? data.positionen
|
||
.map((p) => `• ${p.maschineName} (${formatDatum(p.mietbeginn)} bis ${formatDatum(p.mietende)})`)
|
||
.join("\n")
|
||
: "";
|
||
|
||
await sendWithFallback({
|
||
from: `"Mietpark Hahn" <${process.env.SMTP_FROM}>`,
|
||
to: data.email,
|
||
subject: info.betreff,
|
||
text: `Guten Tag ${data.firma},\n\n${info.text}${equipmentList ? `\n\nIhre gemieteten Geräte:\n${equipmentList}` : ""}${data.notizen ? `\n\nNachricht vom Verleih:\n${data.notizen}` : ""}\n\nMit freundlichen Grüßen\nMietpark Hahn\n\nUnsere AGB finden Sie unter: ${process.env.APP_URL ?? "https://www.mietparkhahn.de"}/agb`,
|
||
html,
|
||
}, `Status-${data.neuerStatus} ${data.firma}`);
|
||
}
|
||
|
||
// ─── Registrierungsbestätigung ─────────────────────────────────────────────
|
||
export async function sendeRegistrierungsBestaetigung(data: {
|
||
email: string;
|
||
firma?: string;
|
||
bestaetigungsLink: string;
|
||
}) {
|
||
const name = data.firma || data.email;
|
||
const html = `
|
||
<!DOCTYPE html>
|
||
<html lang="de">
|
||
<head><meta charset="UTF-8"></head>
|
||
<body style="font-family:system-ui,sans-serif;color:#1c1917;max-width:600px;margin:0 auto;padding:0">
|
||
<div style="background:#1c1917;padding:20px 24px">
|
||
<h1 style="color:#f7d334;margin:0;font-size:20px;font-weight:700">Mietpark Hahn</h1>
|
||
<p style="color:rgba(255,255,255,0.7);margin:4px 0 0;font-size:13px">E-Mail-Adresse bestätigen</p>
|
||
</div>
|
||
<div style="padding:24px;background:#fff">
|
||
<p style="margin:0 0 16px">Guten Tag ${name},</p>
|
||
<p style="margin:0 0 24px;color:#475569">
|
||
vielen Dank für Ihre Registrierung bei Mietpark Hahn. Bitte bestätigen Sie Ihre
|
||
E-Mail-Adresse, um Zugang zu Ihrem Kundenbereich zu erhalten.
|
||
</p>
|
||
<a href="${data.bestaetigungsLink}"
|
||
style="display:inline-block;background:#f7d334;color:#1c1917;font-weight:700;padding:12px 28px;text-decoration:none;border-radius:4px;font-size:15px">
|
||
E-Mail-Adresse bestätigen →
|
||
</a>
|
||
<p style="margin:24px 0 0;font-size:12px;color:#94a3b8">
|
||
Dieser Link ist 24 Stunden gültig. Falls Sie sich nicht registriert haben, können Sie
|
||
diese E-Mail ignorieren.
|
||
</p>
|
||
</div>
|
||
<div style="padding:12px 24px;background:#f8fafc;border-top:1px solid #e2e8f0">
|
||
<p style="margin:0;font-size:11px;color:#94a3b8">
|
||
Mit der Nutzung unserer Dienste akzeptieren Sie unsere
|
||
<a href="${process.env.APP_URL ?? "https://www.mietparkhahn.de"}/agb" style="color:#64748b">Allgemeinen Geschäftsbedingungen</a>.
|
||
</p>
|
||
</div>
|
||
</body>
|
||
</html>`;
|
||
|
||
await sendWithFallback({
|
||
from: `"Mietpark Hahn" <${process.env.SMTP_FROM}>`,
|
||
to: data.email,
|
||
subject: "Bitte bestätigen Sie Ihre E-Mail-Adresse – Mietpark Hahn",
|
||
text: `Guten Tag ${name},\n\nbitte bestätigen Sie Ihre E-Mail-Adresse:\n\n${data.bestaetigungsLink}\n\nDieser Link ist 24 Stunden gültig.\n\nMit freundlichen Grüßen\nMietpark Hahn\n\nUnsere AGB finden Sie unter: ${process.env.APP_URL ?? "https://www.mietparkhahn.de"}/agb`,
|
||
html,
|
||
}, `Registrierung ${name}`);
|
||
}
|
||
|
||
// ─── Maschinen-Bedarfscheck ────────────────────────────────────────────────
|
||
export async function sendeBedarfscheckAnKunde(data: {
|
||
name: string;
|
||
email: string;
|
||
}) {
|
||
const html = `
|
||
<!DOCTYPE html>
|
||
<html lang="de">
|
||
<head><meta charset="UTF-8"></head>
|
||
<body style="font-family:system-ui,sans-serif;color:#1c1917;max-width:600px;margin:0 auto;padding:0">
|
||
<div style="background:#1c1917;padding:20px 24px">
|
||
<h1 style="color:#f7d334;margin:0;font-size:20px;font-weight:700">Mietpark Hahn</h1>
|
||
<p style="color:rgba(255,255,255,0.7);margin:4px 0 0;font-size:13px">Ihr kostenloser Maschinen-Bedarfscheck</p>
|
||
</div>
|
||
<div style="padding:24px;background:#fff">
|
||
<p style="margin:0 0 20px">Guten Tag ${data.name},</p>
|
||
<p style="margin:0 0 24px;color:#475569;font-size:14px">
|
||
vielen Dank für Ihre Anfrage! Hier sind die Antworten zu den 7 wichtigsten Fragen vor der Maschinen-Miete:
|
||
</p>
|
||
|
||
<ol style="margin:0 0 24px;padding-left:20px;color:#1c1917">
|
||
<li style="margin-bottom:16px;font-size:14px;line-height:1.5">
|
||
<strong>Welche Maschinenklasse passt zu meinem Projekt?</strong><br>
|
||
<span style="color:#475569">Minibagger (1,5–3t) für Garten- und enge Baustellenarbeiten, Kettenbagger (5–16t) für Baugruben und Schachtarbeiten, Radlader für Materialtransport und Verdichtung.</span>
|
||
</li>
|
||
<li style="margin-bottom:16px;font-size:14px;line-height:1.5">
|
||
<strong>Welches Zubehör brauche ich?</strong><br>
|
||
<span style="color:#475569">Tieflöffel für Erde, Grabenräumlöffel für Leitungsgräben, Abbruchhammer für Beton/Asphalt, Greifer für Schüttgut. Die genaue Auswahl beraten wir gerne persönlich.</span>
|
||
</li>
|
||
<li style="margin-bottom:16px;font-size:14px;line-height:1.5">
|
||
<strong>Wie plane ich Mietdauer & Lieferung richtig?</strong><br>
|
||
<span style="color:#475569">Immer 1 Puffertag einkalkulieren. Lieferung mindestens 1 Werktag vorab anfragen. In Frühjahr/Herbst frühzeitig reservieren, da die Nachfrage hoch ist.</span>
|
||
</li>
|
||
<li style="margin-bottom:16px;font-size:14px;line-height:1.5">
|
||
<strong>Was muss ich bei der Übergabe prüfen?</strong><br>
|
||
<span style="color:#475569">Betriebsstunden und Ölstände notieren, Schäden fotografieren, Bedienungsanleitung mitnehmen. Dies schützt Sie vor Haftungsansprüchen.</span>
|
||
</li>
|
||
<li style="margin-bottom:16px;font-size:14px;line-height:1.5">
|
||
<strong>Welche Versicherung ist sinnvoll?</strong><br>
|
||
<span style="color:#475569">Privatpersonen sollten ihre Haftpflichtversicherung prüfen. Firmen benötigen Baugeräteversicherung. Eine Maschinenbruchversicherung über uns ist optional, aber empfohlen.</span>
|
||
</li>
|
||
<li style="margin-bottom:16px;font-size:14px;line-height:1.5">
|
||
<strong>Wie spare ich durch Wochenmiete?</strong><br>
|
||
<span style="color:#475569">Wochentarife sind ca. 30–40% günstiger als 5× Tagessatz. Schon ab 4 Tagen Einsatz lohnt sich die Wochenmiete gegenüber Tagesmietung.</span>
|
||
</li>
|
||
<li style="margin-bottom:0;font-size:14px;line-height:1.5">
|
||
<strong>Was tun, wenn die Maschine ausfällt?</strong><br>
|
||
<span style="color:#475569">Sofort den Verleih anrufen. Wenn kein Verschulden vorliegt, entstehen keine Kosten während des Ausfalls. Ersatz wird schnellstmöglich bereitgestellt.</span>
|
||
</li>
|
||
</ol>
|
||
|
||
<div style="padding:16px;background:#fef3c7;border-left:3px solid #f7d334;margin:24px 0">
|
||
<p style="margin:0;font-size:13px;color:#92400e">
|
||
<strong>Nächste Schritte:</strong> Rufen Sie uns an oder stellen Sie eine Anfrage mit den gewünschten Maschinen. Unser Team prüft sofort die Verfügbarkeit und meldet sich bei Ihnen!
|
||
</p>
|
||
</div>
|
||
|
||
<p style="margin:20px 0 0;font-size:13px;color:#64748b">
|
||
Bei Fragen erreichen Sie uns unter
|
||
<a href="tel:${process.env.COMPANY_PHONE ?? ""}" style="color:#1c1917">${process.env.COMPANY_PHONE ?? ""}</a>
|
||
oder per E-Mail an
|
||
<a href="mailto:${process.env.SMTP_FROM ?? ""}" style="color:#1c1917">${process.env.SMTP_FROM ?? ""}</a>
|
||
</p>
|
||
</div>
|
||
<div style="padding:12px 24px;background:#f8fafc;border-top:1px solid #e2e8f0">
|
||
<p style="margin:0;font-size:11px;color:#94a3b8">
|
||
Mit der Nutzung unserer Dienste akzeptieren Sie unsere
|
||
<a href="${process.env.APP_URL ?? "https://www.mietparkhahn.de"}/agb" style="color:#64748b">Allgemeinen Geschäftsbedingungen</a>.
|
||
</p>
|
||
</div>
|
||
</body>
|
||
</html>`;
|
||
|
||
const text = `Guten Tag ${data.name},
|
||
|
||
vielen Dank für Ihre Anfrage! Hier sind die Antworten zu den 7 wichtigsten Fragen vor der Maschinen-Miete:
|
||
|
||
1. Welche Maschinenklasse passt zu meinem Projekt?
|
||
Minibagger (1,5–3t) für Garten- und enge Baustellenarbeiten, Kettenbagger (5–16t) für Baugruben und Schachtarbeiten, Radlader für Materialtransport und Verdichtung.
|
||
|
||
2. Welches Zubehör brauche ich?
|
||
Tieflöffel für Erde, Grabenräumlöffel für Leitungsgräben, Abbruchhammer für Beton/Asphalt, Greifer für Schüttgut. Die genaue Auswahl beraten wir gerne persönlich.
|
||
|
||
3. Wie plane ich Mietdauer & Lieferung richtig?
|
||
Immer 1 Puffertag einkalkulieren. Lieferung mindestens 1 Werktag vorab anfragen. In Frühjahr/Herbst frühzeitig reservieren, da die Nachfrage hoch ist.
|
||
|
||
4. Was muss ich bei der Übergabe prüfen?
|
||
Betriebsstunden und Ölstände notieren, Schäden fotografieren, Bedienungsanleitung mitnehmen. Dies schützt Sie vor Haftungsansprüchen.
|
||
|
||
5. Welche Versicherung ist sinnvoll?
|
||
Privatpersonen sollten ihre Haftpflichtversicherung prüfen. Firmen benötigen Baugeräteversicherung. Eine Maschinenbruchversicherung über uns ist optional, aber empfohlen.
|
||
|
||
6. Wie spare ich durch Wochenmiete?
|
||
Wochentarife sind ca. 30–40% günstiger als 5× Tagessatz. Schon ab 4 Tagen Einsatz lohnt sich die Wochenmiete gegenüber Tagesmietung.
|
||
|
||
7. Was tun, wenn die Maschine ausfällt?
|
||
Sofort den Verleih anrufen. Wenn kein Verschulden vorliegt, entstehen keine Kosten während des Ausfalls. Ersatz wird schnellstmöglich bereitgestellt.
|
||
|
||
Nächste Schritte: Rufen Sie uns an oder stellen Sie eine Anfrage mit den gewünschten Maschinen.
|
||
|
||
Mit freundlichen Grüßen
|
||
Mietpark Hahn
|
||
|
||
Unsere AGB finden Sie unter: ${process.env.APP_URL ?? "https://www.mietparkhahn.de"}/agb`;
|
||
|
||
await sendWithFallback({
|
||
from: `"Mietpark Hahn" <${process.env.SMTP_FROM}>`,
|
||
to: data.email,
|
||
subject: "Ihr kostenloser Maschinen-Bedarfscheck – Mietpark Hahn",
|
||
text,
|
||
html,
|
||
}, `Bedarfscheck ${data.name}`);
|
||
}
|
||
|
||
export async function sendeBedarfscheckAnVermieter(data: {
|
||
name: string;
|
||
email: string;
|
||
telefon: string;
|
||
adresse: string | null;
|
||
}) {
|
||
const html = `
|
||
<!DOCTYPE html>
|
||
<html lang="de">
|
||
<head><meta charset="UTF-8"></head>
|
||
<body style="font-family:system-ui,sans-serif;color:#1c1917;max-width:600px;margin:0 auto;padding:0">
|
||
<div style="background:#1c1917;padding:20px 24px">
|
||
<h1 style="color:#f7d334;margin:0;font-size:20px;font-weight:700">Mietpark Hahn</h1>
|
||
<p style="color:rgba(255,255,255,0.7);margin:4px 0 0;font-size:13px">Neuer Lead aus Bedarfscheck</p>
|
||
</div>
|
||
<div style="padding:24px;background:#fff">
|
||
<h2 style="margin:0 0 16px;font-size:18px">Neuer Lead aus Maschinen-Bedarfscheck</h2>
|
||
|
||
<h3 style="margin:0 0 10px;font-size:13px;text-transform:uppercase;letter-spacing:0.05em;color:#64748b">
|
||
Kontaktdaten
|
||
</h3>
|
||
<table style="width:100%;border-collapse:collapse;margin-bottom:24px;font-size:13px;background:#f8fafc;border:1px solid #e2e8f0">
|
||
<tr>
|
||
<td style="padding:8px 12px;color:#64748b;width:130px;border-bottom:1px solid #e2e8f0">Name / Firma</td>
|
||
<td style="padding:8px 12px;font-weight:600;border-bottom:1px solid #e2e8f0">${data.name}</td>
|
||
</tr>
|
||
<tr>
|
||
<td style="padding:8px 12px;color:#64748b;border-bottom:1px solid #e2e8f0">E-Mail</td>
|
||
<td style="padding:8px 12px;border-bottom:1px solid #e2e8f0">
|
||
<a href="mailto:${data.email}" style="color:#1c1917">${data.email}</a>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<td style="padding:8px 12px;color:#64748b;border-bottom:1px solid #e2e8f0">Telefon</td>
|
||
<td style="padding:8px 12px;border-bottom:1px solid #e2e8f0">
|
||
<a href="tel:${data.telefon}" style="color:#1c1917;font-weight:600">${data.telefon}</a>
|
||
</td>
|
||
</tr>
|
||
${
|
||
data.adresse
|
||
? `<tr>
|
||
<td style="padding:8px 12px;color:#64748b">Adresse</td>
|
||
<td style="padding:8px 12px">${data.adresse}</td>
|
||
</tr>`
|
||
: ""
|
||
}
|
||
</table>
|
||
|
||
<div style="padding:12px 16px;background:#f8fafc;border-left:3px solid #f7d334">
|
||
<p style="margin:0;font-size:12px;color:#64748b">
|
||
Quelle: Maschinen-Bedarfscheck (Lead Magnet)
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</body>
|
||
</html>`;
|
||
|
||
const text = `Neuer Lead aus Maschinen-Bedarfscheck
|
||
|
||
Kontaktdaten:
|
||
Name/Firma: ${data.name}
|
||
E-Mail: ${data.email}
|
||
Telefon: ${data.telefon}
|
||
${data.adresse ? `Adresse: ${data.adresse}` : ""}
|
||
|
||
Quelle: Maschinen-Bedarfscheck`;
|
||
|
||
await sendWithFallback({
|
||
from: `"Mietpark Hahn" <${process.env.SMTP_FROM}>`,
|
||
to: process.env.SMTP_TO,
|
||
subject: `Neuer Lead: ${data.name} – Maschinen-Bedarfscheck`,
|
||
text,
|
||
html,
|
||
}, `Lead ${data.name}`);
|
||
}
|
||
|
||
// ─── Email-Änderungsbestätigung (Admin-Profil) ────────────────────────────────
|
||
export async function sendeEmailAenderungsBestaetigung(data: {
|
||
adminEmail: string;
|
||
adminName: string;
|
||
bestaetigungsLink: string;
|
||
}) {
|
||
const html = `
|
||
<!DOCTYPE html>
|
||
<html lang="de">
|
||
<head><meta charset="UTF-8"></head>
|
||
<body style="font-family:system-ui,sans-serif;color:#1c1917;max-width:600px;margin:0 auto;padding:0">
|
||
<div style="background:#1c1917;padding:20px 24px">
|
||
<h1 style="color:#f7d334;margin:0;font-size:20px;font-weight:700">Mietpark Hahn</h1>
|
||
<p style="color:rgba(255,255,255,0.7);margin:4px 0 0;font-size:13px">Admin-Bereich: E-Mail-Änderung</p>
|
||
</div>
|
||
<div style="padding:24px;background:#fff">
|
||
<p style="margin:0 0 16px">Guten Tag ${data.adminName},</p>
|
||
<p style="margin:0 0 24px;color:#475569">
|
||
Sie haben eine neue E-Mail-Adresse für Ihr Admin-Konto hinterlegt.
|
||
Bitte bestätigen Sie diese Änderung, um die neue E-Mail-Adresse zu aktivieren.
|
||
</p>
|
||
<a href="${data.bestaetigungsLink}"
|
||
style="display:inline-block;background:#f7d334;color:#1c1917;font-weight:700;padding:12px 28px;text-decoration:none;border-radius:4px;font-size:15px">
|
||
E-Mail-Adresse bestätigen →
|
||
</a>
|
||
<p style="margin:24px 0 0;font-size:12px;color:#94a3b8">
|
||
<strong>Wichtig:</strong> Ihre bisherige E-Mail-Adresse bleibt aktiv, bis Sie diesen Link klicken.
|
||
Dieser Link ist 24 Stunden gültig.
|
||
</p>
|
||
<p style="margin:16px 0 0;font-size:12px;color:#94a3b8">
|
||
Falls Sie diese E-Mail nicht angefordert haben, können Sie sie ignorieren.
|
||
Ihre E-Mail-Adresse wird nicht geändert.
|
||
</p>
|
||
</div>
|
||
<div style="padding:12px 24px;background:#f8fafc;border-top:1px solid #e2e8f0">
|
||
<p style="margin:0;font-size:11px;color:#94a3b8">
|
||
Mit der Nutzung unserer Dienste akzeptieren Sie unsere
|
||
<a href="${process.env.APP_URL ?? "https://www.mietparkhahn.de"}/agb" style="color:#64748b">Allgemeinen Geschäftsbedingungen</a>.
|
||
</p>
|
||
</div>
|
||
</body>
|
||
</html>`;
|
||
|
||
const text = `Guten Tag ${data.adminName},
|
||
|
||
Sie haben eine neue E-Mail-Adresse für Ihr Admin-Konto hinterlegt.
|
||
Bitte bestätigen Sie diese Änderung unter folgendem Link:
|
||
|
||
${data.bestaetigungsLink}
|
||
|
||
Dieser Link ist 24 Stunden gültig.
|
||
|
||
WICHTIG: Ihre bisherige E-Mail-Adresse bleibt aktiv, bis Sie den Link oben bestätigen.
|
||
|
||
Mit freundlichen Grüßen
|
||
Mietpark Hahn
|
||
|
||
Unsere AGB finden Sie unter: ${process.env.APP_URL ?? "https://www.mietparkhahn.de"}/agb`;
|
||
|
||
await sendWithFallback({
|
||
from: `"Mietpark Hahn" <${process.env.SMTP_FROM}>`,
|
||
to: data.adminEmail,
|
||
subject: "Neue E-Mail-Adresse bestätigen – Mietpark Hahn",
|
||
text,
|
||
html,
|
||
}, `Email-Änderung ${data.adminName}`);
|
||
}
|