diff --git a/app.js b/app.js index 3b5505b..c55890e 100644 --- a/app.js +++ b/app.js @@ -37,6 +37,7 @@ const billingRouter = require('./routes/billing'); const financeRouter = require('./routes/finance'); const renewalRouter = require('./routes/renewal'); const contractsRouter = require('./routes/contracts'); +const mailingRouter = require('./routes/mailing'); const cron = require('node-cron'); app.use('/', indexRouter); @@ -46,6 +47,7 @@ app.use('/admin/billing', billingRouter); app.use('/admin/finance', financeRouter); app.use('/', renewalRouter); app.use('/admin/contracts', contractsRouter); +app.use('/admin/mailing', mailingRouter); // 404 Handler app.use((req, res) => { diff --git a/public/css/style.css b/public/css/style.css index 0b92e22..63a304d 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -1450,3 +1450,51 @@ body:not(.admin-body) > * { background: #f0f0ff; border-color: var(--primary); } + +/* ================================================ + MAILING + ================================================ */ +.mailing-layout { + display: grid; + grid-template-columns: 1fr 300px; + gap: 16px; + align-items: start; +} +@media (max-width: 900px) { .mailing-layout { grid-template-columns: 1fr; } } + +.mail-textarea { + width: 100%; + font-family: 'Outfit', sans-serif; + font-size: 0.92rem; + resize: vertical; + min-height: 200px; +} +.mail-preview-hint { + font-size: 0.8rem; + color: var(--text-muted); + margin-top: 6px; +} +.mail-recipients { max-height: 500px; overflow-y: auto; } +.recipients-list { margin-top: 12px; } +.recipient-row { + display: flex; + flex-direction: column; + padding: 8px 0; + border-bottom: 1px solid var(--border); + font-size: 0.85rem; +} +.recipient-row:last-child { border-bottom: none; } +.recipient-name { font-weight: 600; } +.recipient-email { color: var(--text-muted); font-size: 0.8rem; } + +.mail-type-badge { + font-size: 0.78rem; + font-weight: 600; + padding: 2px 8px; + border-radius: 10px; + background: var(--bg); +} +.mail-type-bulk { background: #ede9fe; color: #5b21b6; } +.mail-type-direct { background: #dbeafe; color: #1e40af; } +.mail-type-renewal { background: #dcfce7; color: var(--success); } +.mail-type-renewal_auto { background: #fef3c7; color: var(--warning); } diff --git a/routes/mailing.js b/routes/mailing.js new file mode 100644 index 0000000..ff08e51 --- /dev/null +++ b/routes/mailing.js @@ -0,0 +1,164 @@ +const express = require('express'); +const router = express.Router(); +const db = require('../config/database'); +const mailer = require('../config/mailer'); +const { requireAdmin } = require('../middleware/auth'); + +// ============================================ +// GET /admin/mailing – Übersicht +// ============================================ +router.get('/', requireAdmin, async (req, res) => { + try { + const [members] = await db.query(` + SELECT id, first_name, last_name, email, status, + t.name as tariff_name + FROM memberships m + LEFT JOIN tariffs t ON m.tariff_id = t.id + WHERE m.status = 'active' + ORDER BY m.last_name ASC + `); + + const [log] = await db.query(` + SELECT e.*, m.first_name, m.last_name + FROM email_log e + LEFT JOIN memberships m ON e.membership_id = m.id + ORDER BY e.sent_at DESC + LIMIT 50 + `); + + res.render('admin/mailing', { + admin: req.session.adminUser, + members, + log, + success: req.query.success || null, + error: req.query.error || null + }); + } catch (err) { + console.error(err); + res.redirect('/admin?error=' + encodeURIComponent('Fehler im Mailing: ' + err.message)); + } +}); + +// ============================================ +// POST /admin/mailing/send-all – An alle aktiven +// ============================================ +router.post('/send-all', requireAdmin, async (req, res) => { + const { subject, body, include_name } = req.body; + if (!subject || !body) return res.redirect('/admin/mailing?error=Betreff+und+Text+erforderlich'); + + try { + const [members] = await db.query( + "SELECT * FROM memberships WHERE status = 'active'" + ); + + let sent = 0, failed = 0; + + for (const member of members) { + const personalBody = include_name + ? `Hallo ${member.first_name} ${member.last_name},\n\n${body}` + : body; + + const html = bodyToHtml(personalBody, subject); + + try { + await mailer.sendMail({ + from: process.env.MAIL_FROM, + to: member.email, + subject: subject, + html: html + }); + await db.query( + 'INSERT INTO email_log (membership_id, type, recipient, subject, status) VALUES (?,?,?,?,?)', + [member.id, 'bulk', member.email, subject, 'sent'] + ); + sent++; + } catch (err) { + await db.query( + 'INSERT INTO email_log (membership_id, type, recipient, subject, status) VALUES (?,?,?,?,?)', + [member.id, 'bulk', member.email, subject, 'failed'] + ); + failed++; + } + } + + res.redirect(`/admin/mailing?success=${sent}+E-Mails+gesendet${failed > 0 ? '+(' + failed + '+Fehler)' : ''}`); + } catch (err) { + console.error(err); + res.redirect('/admin/mailing?error=' + encodeURIComponent(err.message)); + } +}); + +// ============================================ +// POST /admin/mailing/send-one – An einzelnes Mitglied +// ============================================ +router.post('/send-one', requireAdmin, async (req, res) => { + const { membership_id, subject, body } = req.body; + const backUrl = req.headers.referer || '/admin/mailing'; + + if (!membership_id || !subject || !body) { + return res.redirect(backUrl + '?error=Alle+Felder+erforderlich'); + } + + try { + const [rows] = await db.query( + 'SELECT * FROM memberships WHERE id = ?', [membership_id] + ); + if (rows.length === 0) return res.redirect(backUrl + '?error=Mitglied+nicht+gefunden'); + const member = rows[0]; + + const html = bodyToHtml(body, subject); + + await mailer.sendMail({ + from: process.env.MAIL_FROM, + to: member.email, + subject: subject, + html: html + }); + + await db.query( + 'INSERT INTO email_log (membership_id, type, recipient, subject, status) VALUES (?,?,?,?,?)', + [member.id, 'direct', member.email, subject, 'sent'] + ); + + res.redirect(backUrl + '?success=E-Mail+an+' + encodeURIComponent(member.email) + '+gesendet'); + } catch (err) { + console.error(err); + await db.query( + 'INSERT INTO email_log (membership_id, type, recipient, subject, status) VALUES (?,?,?,?,?)', + [membership_id, 'direct', '', subject, 'failed'] + ).catch(() => {}); + res.redirect(backUrl + '?error=E-Mail+Fehler:+' + encodeURIComponent(err.message)); + } +}); + +// ============================================ +// HTML Template +// ============================================ +function bodyToHtml(text, subject) { + const paragraphs = text.split('\n') + .map(l => l.trim()) + .map(l => l ? `

${l}

` : '
') + .join(''); + + return ` + + + +
+
+

Plusfit24

+
+
+

${subject}

+ ${paragraphs} +
+

+ PlusFit24 UG · Moosleiten 12 · 84089 Aiglsbach
+ Diese E-Mail wurde über das PlusFit24 Verwaltungssystem gesendet. +

+
+
+`; +} + +module.exports = router; diff --git a/views/admin/billing.ejs b/views/admin/billing.ejs index caf928c..864dc39 100644 --- a/views/admin/billing.ejs +++ b/views/admin/billing.ejs @@ -19,7 +19,8 @@ 📑 Verträge 💶 Abrechnung 📊 Finanzen - ⚙️ Einstellungen + 📧 Mailing + ⚙️ Einstellungen