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