Wird geladen...
"); } const result = await Email.send({ SecureToken: server.secureToken, To: to, From: server.fromEmail || branding.senderEmail, Subject: subject, Body: htmlBody, }); if (result !== "OK") throw new Error(result); return { success: true, provider: "smtpjs" }; } else if (server.provider === "api") { // Custom API endpoint (POST JSON) const resp = await fetch(server.endpoint, { method: "POST", headers: { "Content-Type": "application/json", ...(server.apiKey ? { "Authorization": `Bearer ${server.apiKey}` } : {}) }, body: JSON.stringify({ to, from: server.fromEmail || branding.senderEmail, fromName: branding.senderName, subject, html: htmlBody, text: body }) }); if (!resp.ok) throw new Error(`API error: ${resp.status} ${resp.statusText}`); return { success: true, provider: "api" }; } else if (server.provider === "mailto") { // Fallback: open mailto link const mailtoUrl = `mailto:${to}?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`; window.open(mailtoUrl, "_blank"); return { success: true, provider: "mailto", note: "Opened in email client" }; } else { throw new Error("Unknown provider: " + server.provider); } } async function sendTemplateEmail(templateId, recipientData, onToast) { const templates = (await S.get("emailTemplates")) || DEFAULT_EMAIL_TEMPLATES; const branding = (await S.get("emailBranding")) || DEFAULT_BRANDING; const servers = await loadSmtpServers(); const tpl = templates.find(t => t.id === templateId); if (!tpl) { onToast({ type: "error", msg: "Template nicht gefunden" }); return false; } if (!tpl.a) { onToast({ type: "error", msg: `Template "${tpl.n}" ist deaktiviert` }); return false; } // Fill variables let subject = tpl.subject; let body = tpl.body; const vars = { firma: branding.senderName || "ConsultPro", ...recipientData }; Object.keys(vars).forEach(k => { subject = subject.replaceAll(`{{${k}}}`, vars[k] || ""); body = body.replaceAll(`{{${k}}}`, vars[k] || ""); }); if (!recipientData.email) { onToast({ type: "error", msg: "Keine E-Mail-Adresse" }); return false; } // Find active server const activeServer = servers.find(s => s.active); if (!activeServer) { // Fallback to mailto const mailtoUrl = `mailto:${recipientData.email}?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`; window.open(mailtoUrl, "_blank"); onToast({ type: "success", msg: `E-Mail in Mail-App geöffnet (kein SMTP konfiguriert)` }); return true; } try { await sendEmailViaSmtp(activeServer, recipientData.email, subject, body, branding); // Log sent email const logId = `emaillog_${Date.now()}`; const log = { id: logId, templateId, templateName: tpl.n, to: recipientData.email, toName: recipientData.name, subject, sentAt: new Date().toISOString(), server: activeServer.name, status: "sent" }; await S.set(`emaillog:${logId}`, log); const idx = (await S.get("emaillog:index")) || []; idx.unshift(logId); if (idx.length > 100) idx.length = 100; await S.set("emaillog:index", idx); onToast({ type: "success", msg: `✓ E-Mail an ${recipientData.email} gesendet via ${activeServer.name}` }); return true; } catch (err) { onToast({ type: "error", msg: `E-Mail-Fehler: ${err.message}` }); return false; } } function Settings({ onToast }) { const [settings, setSettings] = useState({ firma: "ConsultPro GmbH", primary: "#3b82f6", telegram: "", whatsapp: "", apiKey: "sk-••••••••••" }); const [smtpServers, setSmtpServers] = useState([]); const [editSmtp, setEditSmtp] = useState(null); const [newSmtp, setNewSmtp] = useState(null); const [emailLogs, setEmailLogs] = useState([]); const [showLogs, setShowLogs] = useState(false); const [testEmail, setTestEmail] = useState(""); const [sending, setSending] = useState(false); useEffect(() => { (async () => { const s = await S.get("settings:global"); if (s) setSettings(s); setSmtpServers(await loadSmtpServers()); const logIdx = (await S.get("emaillog:index")) || []; const logs = []; for (const id of logIdx.slice(0, 20)) { const l = await S.get(`emaillog:${id}`); if (l) logs.push(l); } setEmailLogs(logs); })(); }, []); const save = async () => { await S.set("settings:global", settings); onToast({ type: "success", msg: "Einstellungen gespeichert" }); }; const addSmtp = async () => { if (!newSmtp || !newSmtp.name) { onToast({ type: "error", msg: "Name ist Pflicht" }); return; } const id = `smtp_${Date.now()}`; const server = { id, ...newSmtp, active: smtpServers.length === 0 }; const updated = [...smtpServers, server]; setSmtpServers(updated); await saveSmtpServers(updated); setNewSmtp(null); onToast({ type: "success", msg: `SMTP "${server.name}" hinzugefügt` }); }; const updateSmtp = async () => { if (!editSmtp) return; const updated = smtpServers.map(s => s.id === editSmtp.id ? editSmtp : s); setSmtpServers(updated); await saveSmtpServers(updated); setEditSmtp(null); onToast({ type: "success", msg: "SMTP aktualisiert" }); }; const deleteSmtp = async (id) => { if (!confirm("SMTP-Server löschen?")) return; const updated = smtpServers.filter(s => s.id !== id); setSmtpServers(updated); await saveSmtpServers(updated); onToast({ type: "success", msg: "SMTP gelöscht" }); }; const toggleSmtpActive = async (id) => { const updated = smtpServers.map(s => ({ ...s, active: s.id === id })); setSmtpServers(updated); await saveSmtpServers(updated); onToast({ type: "success", msg: "Aktiver SMTP geändert" }); }; const sendTest = async () => { if (!testEmail || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(testEmail)) { onToast({ type: "error", msg: "Gültige E-Mail eingeben" }); return; } setSending(true); await sendTemplateEmail("welcome", { name: "Test User", email: testEmail, password: "TestPass123", loginUrl: "https://consultpro.de/login" }, onToast); setSending(false); // Refresh logs const logIdx = (await S.get("emaillog:index")) || []; const logs = []; for (const id of logIdx.slice(0, 20)) { const l = await S.get(`emaillog:${id}`); if (l) logs.push(l); } setEmailLogs(logs); }; const emptySmtp = { name: "", provider: "api", endpoint: "", apiKey: "", fromEmail: "", secureToken: "" }; const SmtpForm = ({ data, onChange, onSave, onCancel, title }) => ( React.createElement(Modal, { onClose: onCancel, title: title, wide: true,} , React.createElement(Field, { label: "Server-Name *" , value: data.name, onChange: v => onChange({ ...data, name: v }), ph: "z.B. Postmark, SendGrid, Brevo..." ,} ) , React.createElement(Select, { label: "Provider-Typ", value: data.provider, onChange: v => onChange({ ...data, provider: v }), options: [ { value: "api", label: "API Endpoint (Postmark, SendGrid, Brevo, etc.)" }, { value: "smtpjs", label: "SmtpJS.com (Browser-SMTP via Secure Token)" }, { value: "mailto", label: "Mailto-Fallback (öffnet Mail-App)" } ],} ) , data.provider === "api" && React.createElement(React.Fragment, null , React.createElement(Field, { label: "API Endpoint URL *" , value: data.endpoint, onChange: v => onChange({ ...data, endpoint: v }), ph: "https://api.postmarkapp.com/email",} ) , React.createElement(Field, { label: "API Key / Token" , value: data.apiKey, onChange: v => onChange({ ...data, apiKey: v }), ph: "Dein API-Key..." ,} ) , React.createElement('div', { style: { padding: 12, background: C.primaryL, borderRadius: 10, fontSize: 12, color: "#1e40af", marginBottom: 14 },}, "💡 " , React.createElement('strong', null, "API-Format:"), " Der Request geht als POST mit JSON: " , React.createElement('code', null, `{to, from, fromName, subject, html, text}`), ". Header: " , React.createElement('code', null, "Authorization: Bearer [API-Key]" ), ".", React.createElement('br', null), React.createElement('br', null) , React.createElement('strong', null, "Postmark:"), " Endpoint: " , React.createElement('code', null, "https://api.postmarkapp.com/email"), " — Token als API-Key" , React.createElement('br', null) , React.createElement('strong', null, "SendGrid:"), " Endpoint: " , React.createElement('code', null, "https://api.sendgrid.com/v3/mail/send"), " — API-Key" , React.createElement('br', null) , React.createElement('strong', null, "Brevo:"), " Endpoint: " , React.createElement('code', null, "https://api.brevo.com/v3/smtp/email"), " — API-Key" , React.createElement('br', null) , React.createElement('strong', null, "Eigener Server:" ), " Jeder Endpoint, der JSON akzeptiert" ) ) , data.provider === "smtpjs" && React.createElement(React.Fragment, null , React.createElement(Field, { label: "Secure Token *" , value: data.secureToken, onChange: v => onChange({ ...data, secureToken: v }), ph: "Dein SmtpJS Secure Token" ,} ) , React.createElement('div', { style: { padding: 12, background: C.warningBg, borderRadius: 10, fontSize: 12, color: "#92400e", marginBottom: 14 },}, "⚠️ " , React.createElement('strong', null, "SmtpJS"), " nutzt einen Relay-Service. Erstelle deinen Secure Token auf " , React.createElement('a', { href: "https://smtpjs.com", target: "_blank", style: { color: "#92400e" },}, "smtpjs.com"), " mit deinen SMTP-Daten (Host, Port, User, Password). Der Token verbirgt deine Zugangsdaten." ) ) , data.provider === "mailto" && React.createElement('div', { style: { padding: 12, background: C.successBg, borderRadius: 10, fontSize: 12, color: "#065f46", marginBottom: 14 },}, "ℹ️ " , React.createElement('strong', null, "Mailto-Fallback:"), " Öffnet die E-Mail im lokalen Mail-Programm des Benutzers (Outlook, Thunderbird, Apple Mail). Kein Server nötig, aber nicht automatisiert." ) , React.createElement(Field, { label: "Absender-E-Mail", value: data.fromEmail, onChange: v => onChange({ ...data, fromEmail: v }), ph: "noreply@deinedomain.de (überschreibt Branding)" ,} ) , React.createElement('div', { style: { display: "flex", gap: 8, marginTop: 16 },} , React.createElement('button', { onClick: onSave, style: btn("primary"),}, "Speichern") , React.createElement('button', { onClick: onCancel, style: btn("ghost"),}, "Abbrechen") ) ) ); return ( React.createElement('div', null , React.createElement(PageHeader, { title: "Einstellungen", sub: "System-Konfiguration",} ) , React.createElement('div', { style: { display: "grid", gridTemplateColumns: "1fr 1fr", gap: 20, marginBottom: 20 },} , React.createElement(Card, null, React.createElement('h3', { style: { fontSize: 15, fontWeight: 700, color: C.text, marginBottom: 16, display: "flex", alignItems: "center", gap: 8 },}, React.createElement(Ic, { n: "settings", s: 18, c: C.primary,} ), " Branding" ) , React.createElement(Field, { label: "Firmenname", value: settings.firma, onChange: v => setSettings({...settings, firma: v}),} ) , React.createElement(Field, { label: "Primärfarbe", type: "color", value: settings.primary, onChange: v => setSettings({...settings, primary: v}),} ) , React.createElement('button', { onClick: save, style: btn("primary", "sm"),}, "Speichern") ) , React.createElement(Card, null, React.createElement('h3', { style: { fontSize: 15, fontWeight: 700, color: C.text, marginBottom: 16, display: "flex", alignItems: "center", gap: 8 },}, React.createElement(Ic, { n: "chat", s: 18, c: C.accent,} ), " Integrationen" ) , React.createElement(Field, { label: "Telegram Bot Token" , value: settings.telegram, onChange: v => setSettings({...settings, telegram: v}), ph: "Bot-Token...",} ) , React.createElement(Field, { label: "WhatsApp API Key" , value: settings.whatsapp, onChange: v => setSettings({...settings, whatsapp: v}), ph: "API-Key...",} ) , React.createElement('button', { onClick: save, style: btn("primary", "sm"),}, "Speichern") ) ) , React.createElement(Card, null , React.createElement('div', { style: { display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 16 },} , React.createElement('h3', { style: { fontSize: 15, fontWeight: 700, color: C.text, display: "flex", alignItems: "center", gap: 8, margin: 0 },}, React.createElement(Ic, { n: "email", s: 18, c: C.primary,} ), " SMTP / E-Mail-Server" ) , React.createElement('button', { onClick: () => setNewSmtp({ ...emptySmtp }), style: btn("primary", "sm"),}, React.createElement(Ic, { n: "plus", s: 13,} ), " Server hinzufügen" ) ) , smtpServers.length === 0 ? ( React.createElement('div', { style: { padding: 32, textAlign: "center", background: C.subtle, borderRadius: 12 },} , React.createElement(Ic, { n: "email", s: 32, c: C.textDim,} ) , React.createElement('div', { style: { fontSize: 14, fontWeight: 600, color: C.textMid, marginTop: 12 },}, "Kein E-Mail-Server konfiguriert" ) , React.createElement('div', { style: { fontSize: 12, color: C.textDim, marginTop: 4, marginBottom: 16 },}, "E-Mails werden über die Mail-App geöffnet (mailto). Für automatischen Versand einen Server hinzufügen." ) , React.createElement('button', { onClick: () => setNewSmtp({ ...emptySmtp }), style: btn("primary"),}, React.createElement(Ic, { n: "plus", s: 14,} ), " Ersten Server hinzufügen" ) ) ) : ( React.createElement('div', { style: { display: "flex", flexDirection: "column", gap: 10 },} , smtpServers.map(s => ( React.createElement('div', { key: s.id, style: { display: "flex", alignItems: "center", gap: 14, padding: 16, background: s.active ? "#f0fdf4" : C.subtle, border: `1.5px solid ${s.active ? "#86efac" : C.border}`, borderRadius: 12 },} , React.createElement('div', { onClick: () => toggleSmtpActive(s.id), style: { width: 20, height: 20, borderRadius: "50%", border: `2px solid ${s.active ? C.success : C.textDim}`, background: s.active ? C.success : "transparent", cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0 },} , s.active && React.createElement(Ic, { n: "check", s: 12, c: "#fff",} ) ) , React.createElement('div', { style: { flex: 1 },} , React.createElement('div', { style: { fontSize: 14, fontWeight: 700, color: C.text },}, s.name, " " , s.active && React.createElement('span', { style: { fontSize: 11, color: C.success, fontWeight: 600 },}, " — Aktiv" )) , React.createElement('div', { style: { fontSize: 12, color: C.textDim },}, s.provider === "api" ? s.endpoint : s.provider === "smtpjs" ? "SmtpJS Relay" : "Mailto Fallback", " " , s.fromEmail ? `· ${s.fromEmail}` : "") ) , React.createElement('div', { style: { display: "flex", gap: 6 },} , React.createElement('button', { onClick: () => setEditSmtp({ ...s }), style: btn("ghost", "sm"),}, React.createElement(Ic, { n: "edit", s: 12,} )) , React.createElement('button', { onClick: () => deleteSmtp(s.id), style: btn("danger", "sm"),}, React.createElement(Ic, { n: "trash", s: 12,} )) ) ) )) ) ) , React.createElement('div', { style: { marginTop: 20, padding: 16, background: C.subtle, borderRadius: 12 },} , React.createElement('div', { style: { fontSize: 12, fontWeight: 700, color: C.textMid, textTransform: "uppercase", letterSpacing: 1, marginBottom: 10 },}, "Test-E-Mail senden" ) , React.createElement('div', { style: { display: "flex", gap: 8 },} , React.createElement('input', { type: "email", value: testEmail, onChange: e => setTestEmail(e.target.value), placeholder: "test@beispiel.de", style: { flex: 1, padding: "10px 14px", background: "#fff", border: `1.5px solid ${C.border}`, borderRadius: 10, fontSize: 13, outline: "none", fontFamily: "inherit", boxSizing: "border-box" },} ) , React.createElement('button', { onClick: sendTest, disabled: sending, style: btn("primary"),}, sending ? "Sende..." : "Test senden") ) , React.createElement('div', { style: { fontSize: 11, color: C.textDim, marginTop: 6 },}, "Sendet die Willkommens-Vorlage an die angegebene Adresse" , smtpServers.length === 0 ? " (via Mail-App)" : ` via ${(smtpServers.find(s => s.active) || {}).name || "Mail-App"}`) ) ) , React.createElement('div', { style: { display: "grid", gridTemplateColumns: "1fr 1fr", gap: 20, marginTop: 20 },} , React.createElement(Card, null, React.createElement('h3', { style: { fontSize: 15, fontWeight: 700, color: C.text, marginBottom: 16, display: "flex", alignItems: "center", gap: 8 },}, React.createElement(Ic, { n: "dl", s: 18, c: C.warning,} ), " Datenexport" ) , React.createElement('div', { style: { display: "flex", flexDirection: "column", gap: 8 },} , React.createElement('button', { onClick: () => onToast({ type: "success", msg: "CSV export gestartet" }), style: btn("ghost"),}, React.createElement(Ic, { n: "dl", s: 14,} ), " Mitarbeiter (CSV)" ) , React.createElement('button', { onClick: () => onToast({ type: "success", msg: "JSON export" }), style: btn("ghost"),}, React.createElement(Ic, { n: "dl", s: 14,} ), " Bewerbungen (JSON)" ) , React.createElement('button', { onClick: () => onToast({ type: "success", msg: "ZIP erstellt" }), style: btn("ghost"),}, React.createElement(Ic, { n: "dl", s: 14,} ), " Alle Ausweise (ZIP)" ) ) ) , React.createElement(Card, null , React.createElement('div', { style: { display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 16 },} , React.createElement('h3', { style: { fontSize: 15, fontWeight: 700, color: C.text, display: "flex", alignItems: "center", gap: 8, margin: 0 },}, React.createElement(Ic, { n: "email", s: 18, c: C.accent,} ), " E-Mail-Log" ) , React.createElement('span', { style: { fontSize: 11, color: C.textDim },}, emailLogs.length, " Einträge" ) ) , emailLogs.length === 0 ? React.createElement('div', { style: { padding: 24, textAlign: "center", color: C.textDim, fontSize: 13 },}, "Noch keine E-Mails versendet" ) : React.createElement('div', { style: { maxHeight: 240, overflow: "auto" },}, emailLogs.map(l => ( React.createElement('div', { key: l.id, style: { display: "flex", alignItems: "center", gap: 10, padding: "10px 0", borderBottom: `1px solid ${C.borderL}` },} , React.createElement('div', { style: { width: 28, height: 28, borderRadius: 8, background: l.status === "sent" ? C.successBg : C.dangerBg, display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0 },}, React.createElement(Ic, { n: l.status === "sent" ? "check" : "x", s: 12, c: l.status === "sent" ? C.success : C.danger,} )) , React.createElement('div', { style: { flex: 1, minWidth: 0 },} , React.createElement('div', { style: { fontSize: 12, fontWeight: 600, color: C.text },}, l.templateName) , React.createElement('div', { style: { fontSize: 11, color: C.textDim, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" },}, l.to, " · " , l.server, " · " , new Date(l.sentAt).toLocaleString("de-DE")) ) ) ))) ) ) , newSmtp && React.createElement(SmtpForm, { data: newSmtp, onChange: setNewSmtp, onSave: addSmtp, onCancel: () => setNewSmtp(null), title: "Neuen E-Mail-Server hinzufügen" ,} ) , editSmtp && React.createElement(SmtpForm, { data: editSmtp, onChange: setEditSmtp, onSave: updateSmtp, onCancel: () => setEditSmtp(null), title: `Server bearbeiten: ${editSmtp.name}`,} ) ) ); } // ══════ MAIN APP ══════ function App() { const [toast, setToast] = useState(null); const [ready, setReady] = useState(false); useEffect(function(){ seedData().then(function(){ setReady(true); }); }, []); if (!ready) return React.createElement("div", {style:{height:"100vh",display:"flex",alignItems:"center",justifyContent:"center",background:C.subtle,fontFamily:"sans-serif",fontSize:14,color:C.textSoft}}, "Wird geladen..."); return React.createElement(React.Fragment, null, React.createElement(Website, {onToast:setToast}), React.createElement(Toast, {t:toast,onClose:function(){setToast(null)}})); } var root=ReactDOM.createRoot(document.getElementById("root")); root.render(React.createElement(App,null));