diff --git a/app.js b/app.js
index c7c0d40..9cfb8fc 100644
--- a/app.js
+++ b/app.js
@@ -35,6 +35,7 @@ const adminRouter = require('./routes/admin');
const apiRouter = require('./routes/api');
const billingRouter = require('./routes/billing');
const financeRouter = require('./routes/finance');
+const renewalRouter = require('./routes/renewal');
const cron = require('node-cron');
app.use('/', indexRouter);
@@ -42,6 +43,7 @@ app.use('/admin', adminRouter);
app.use('/api', apiRouter);
app.use('/admin/billing', billingRouter);
app.use('/admin/finance', financeRouter);
+app.use('/', renewalRouter);
// 404 Handler
app.use((req, res) => {
@@ -71,6 +73,58 @@ async function initAdmin() {
}
}
+// Verlängerungs-E-Mails: täglich um 08:00 Uhr prüfen (60 Tage vorher)
+cron.schedule('0 8 * * *', async () => {
+ console.log('⏰ Prüfe auslaufende Verträge (60 Tage)...');
+ try {
+ const [expiring] = await db.query(`
+ SELECT m.* FROM memberships m
+ WHERE m.status = 'active'
+ AND m.effective_end BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL 61 DAY)
+ AND m.id NOT IN (
+ SELECT membership_id FROM renewal_requests
+ WHERE status IN ('pending','completed')
+ AND sent_at >= DATE_SUB(NOW(), INTERVAL 70 DAY)
+ )
+ `);
+ if (expiring.length > 0) {
+ const { renewalEmailHtml } = require('./routes/renewal');
+ const mailer = require('./config/mailer');
+ const crypto = require('crypto');
+ const [tariffs] = await db.query('SELECT * FROM tariffs WHERE active = 1 ORDER BY price_monthly ASC');
+ for (const member of expiring) {
+ try {
+ const token = crypto.randomBytes(32).toString('hex');
+ const expiresAt = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000);
+ await db.query(
+ 'INSERT INTO renewal_requests (membership_id, token, expires_at) VALUES (?,?,?)',
+ [member.id, token, expiresAt]
+ );
+ const baseUrl = process.env.APP_URL || 'https://plusfit24.software-joksch.com';
+ const link = baseUrl + '/renew/' + token;
+ await mailer.sendMail({
+ from: process.env.MAIL_FROM,
+ to: member.email,
+ subject: `Deine PlusFit24 Mitgliedschaft läuft bald ab`,
+ html: renewalEmailHtml(member, tariffs, link, member.effective_end || member.contract_end)
+ });
+ await db.query(
+ 'INSERT INTO email_log (membership_id, type, recipient, subject, status) VALUES (?,?,?,?,?)',
+ [member.id, 'renewal_auto', member.email, 'Mitgliedschaft läuft ab', 'sent']
+ );
+ console.log('📧 Verlängerungs-E-Mail gesendet an:', member.email);
+ } catch (err) {
+ console.error('❌ E-Mail Fehler für', member.email, ':', err.message);
+ }
+ }
+ } else {
+ console.log('✅ Keine auslaufenden Verträge heute.');
+ }
+ } catch (err) {
+ console.error('❌ Cron Fehler:', err.message);
+ }
+});
+
// Auto-Abrechnungslauf jeden 1. des Monats um 06:00 Uhr
cron.schedule('0 6 1 * *', async () => {
const { currentPeriod } = require('./routes/billing');
diff --git a/config/mailer.js b/config/mailer.js
new file mode 100644
index 0000000..c80e0c9
--- /dev/null
+++ b/config/mailer.js
@@ -0,0 +1,20 @@
+const nodemailer = require('nodemailer');
+
+const transporter = nodemailer.createTransport({
+ host: process.env.MAIL_HOST || 'smtp.ionos.de',
+ port: parseInt(process.env.MAIL_PORT) || 587,
+ secure: process.env.MAIL_SECURE === 'true',
+ auth: {
+ user: process.env.MAIL_USER,
+ pass: process.env.MAIL_PASSWORD
+ },
+ tls: { rejectUnauthorized: false }
+});
+
+// Verbindung testen beim Start
+transporter.verify((err) => {
+ if (err) console.error('❌ E-Mail Verbindung fehlgeschlagen:', err.message);
+ else console.log('✅ E-Mail Server verbunden:', process.env.MAIL_HOST);
+});
+
+module.exports = transporter;
diff --git a/package.json b/package.json
index b41b51d..7a8b083 100644
--- a/package.json
+++ b/package.json
@@ -17,7 +17,8 @@
"dns": "^0.2.2",
"pdfkit": "^0.14.0",
"node-cron": "^3.0.3",
- "multer": "^1.4.5-lts.1"
+ "multer": "^1.4.5-lts.1",
+ "nodemailer": "^6.9.7"
},
"devDependencies": {
"nodemon": "^3.0.1"
diff --git a/routes/renewal.js b/routes/renewal.js
new file mode 100644
index 0000000..a3e3f36
--- /dev/null
+++ b/routes/renewal.js
@@ -0,0 +1,248 @@
+const express = require('express');
+const router = express.Router();
+const crypto = require('crypto');
+const db = require('../config/database');
+const mailer = require('../config/mailer');
+const { requireAdmin } = require('../middleware/auth');
+
+// ============================================
+// E-Mail Templates
+// ============================================
+function renewalEmailHtml(member, tariffs, renewalLink, expiryDate) {
+ const tarifList = tariffs.map(t => `
+
+ | ${t.name} |
+
+ ${Number(t.price_monthly).toFixed(2).replace('.', ',')} €/Monat
+ |
+ ${t.duration_months} Monate |
+
`).join('');
+
+ return `
+
+
+
+
+
+
Plusfit24
+
Deine Mitgliedschaft läuft bald ab
+
+
+
Hallo ${member.first_name} ${member.last_name},
+
deine Mitgliedschaft bei PlusFit24 läuft am ${new Date(expiryDate).toLocaleDateString('de-DE')} aus.
+
Wir würden uns freuen, dich weiterhin in unserem Studio begrüßen zu dürfen! Wähle jetzt deinen neuen Tarif:
+
+
+
+
+ | Tarif |
+ Preis |
+ Laufzeit |
+
+
+ ${tarifList}
+
+
+
+
+
+ Dieser Link ist 30 Tage gültig. Falls du Fragen hast, melde dich gerne bei uns.
+ PlusFit24 UG · Moosleiten 12 · 84089 Aiglsbach
+
+
+
+`;
+}
+
+// ============================================
+// Öffentliche Verlängerungsseite (für Mitglieder)
+// ============================================
+router.get('/renew/:token', async (req, res) => {
+ try {
+ const [rows] = await db.query(`
+ SELECT r.*, m.first_name, m.last_name, m.email,
+ m.agreed_price, m.tariff_id
+ FROM renewal_requests r
+ JOIN memberships m ON r.membership_id = m.id
+ WHERE r.token = ? AND r.status = 'pending' AND r.expires_at > NOW()
+ `, [req.params.token]);
+
+ if (rows.length === 0) {
+ return res.render('renewal-expired');
+ }
+
+ const [tariffs] = await db.query(
+ 'SELECT * FROM tariffs WHERE active = 1 ORDER BY price_monthly ASC'
+ );
+
+ res.render('renewal', {
+ request: rows[0],
+ tariffs,
+ error: null,
+ success: null
+ });
+ } catch (err) {
+ console.error(err);
+ res.render('error', { message: 'Fehler beim Laden der Seite.' });
+ }
+});
+
+router.post('/renew/:token', async (req, res) => {
+ const { tariff_id } = req.body;
+ try {
+ const [rows] = await db.query(`
+ SELECT r.*, m.first_name, m.last_name, m.email,
+ m.contract_end, m.effective_end
+ FROM renewal_requests r
+ JOIN memberships m ON r.membership_id = m.id
+ WHERE r.token = ? AND r.status = 'pending' AND r.expires_at > NOW()
+ `, [req.params.token]);
+
+ if (rows.length === 0) return res.render('renewal-expired');
+ const request = rows[0];
+
+ const [tariffs] = await db.query('SELECT * FROM tariffs WHERE id = ? AND active = 1', [tariff_id]);
+ if (tariffs.length === 0) {
+ return res.render('renewal', { request, tariffs: [], error: 'Ungültiger Tarif.', success: null });
+ }
+ const tariff = tariffs[0];
+
+ // Neues Vertragsende berechnen
+ const startDate = new Date(request.effective_end || request.contract_end || new Date());
+ const newEnd = new Date(startDate);
+ newEnd.setMonth(newEnd.getMonth() + tariff.duration_months);
+
+ await db.query(`
+ UPDATE memberships SET
+ tariff_id = ?,
+ agreed_price = ?,
+ agreed_duration = ?,
+ contract_end = ?,
+ effective_end = ?,
+ status = 'active'
+ WHERE id = ?
+ `, [
+ tariff.id, tariff.price_monthly, tariff.duration_months,
+ newEnd, newEnd, request.membership_id
+ ]);
+
+ await db.query(
+ "UPDATE renewal_requests SET status='completed', completed_at=NOW() WHERE id=?",
+ [request.id]
+ );
+
+ res.render('renewal', {
+ request, tariffs: [tariff],
+ error: null,
+ success: `Deine Mitgliedschaft wurde erfolgreich verlängert! Neuer Tarif: ${tariff.name} bis ${newEnd.toLocaleDateString('de-DE')}`
+ });
+ } catch (err) {
+ console.error(err);
+ res.render('error', { message: 'Fehler bei der Verlängerung.' });
+ }
+});
+
+// ============================================
+// Admin: Verlängerungs-E-Mail manuell senden
+// ============================================
+router.post('/admin/send-renewal/:memberId', requireAdmin, async (req, res) => {
+ const memberId = req.params.memberId;
+ const backUrl = req.headers.referer || '/admin/finance';
+ try {
+ const [members] = await db.query(`
+ SELECT m.*, t.name as tariff_name
+ FROM memberships m
+ LEFT JOIN tariffs t ON m.tariff_id = t.id
+ WHERE m.id = ?
+ `, [memberId]);
+
+ if (members.length === 0) return res.redirect(backUrl + '?error=Mitglied+nicht+gefunden');
+ const member = members[0];
+
+ // Alten Pending-Request invalidieren
+ await db.query(
+ "UPDATE renewal_requests SET status='expired' WHERE membership_id=? AND status='pending'",
+ [memberId]
+ );
+
+ // Neuen Token generieren
+ const token = crypto.randomBytes(32).toString('hex');
+ const expiresAt = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000); // 30 Tage
+
+ await db.query(
+ 'INSERT INTO renewal_requests (membership_id, token, expires_at) VALUES (?, ?, ?)',
+ [memberId, token, expiresAt]
+ );
+
+ // Aktive Tarife laden
+ const [tariffs] = await db.query('SELECT * FROM tariffs WHERE active = 1 ORDER BY price_monthly ASC');
+ const baseUrl = `${req.protocol}://${req.get('host')}`;
+ const renewalLink = `${baseUrl}/renew/${token}`;
+ const expiryDate = member.effective_end || member.contract_end;
+
+ // E-Mail senden
+ await mailer.sendMail({
+ from: process.env.MAIL_FROM,
+ to: member.email,
+ subject: `Deine PlusFit24 Mitgliedschaft läuft am ${new Date(expiryDate).toLocaleDateString('de-DE')} ab`,
+ html: renewalEmailHtml(member, tariffs, renewalLink, expiryDate)
+ });
+
+ // Log
+ await db.query(
+ 'INSERT INTO email_log (membership_id, type, recipient, subject, status) VALUES (?,?,?,?,?)',
+ [memberId, 'renewal', member.email, 'Mitgliedschaft läuft ab', 'sent']
+ );
+
+ res.redirect(backUrl + '?success=Verlängerungs-E-Mail+an+' + encodeURIComponent(member.email) + '+gesendet');
+ } catch (err) {
+ console.error(err);
+ // Log failure
+ await db.query(
+ 'INSERT INTO email_log (membership_id, type, recipient, subject, status) VALUES (?,?,?,?,?)',
+ [memberId, 'renewal', '', 'Verlängerung', 'failed']
+ ).catch(() => {});
+ res.redirect(backUrl + '?error=E-Mail+Fehler:+' + encodeURIComponent(err.message));
+ }
+});
+
+// Admin: Verlängerung manuell durchführen
+router.post('/admin/renew-manual/:memberId', requireAdmin, async (req, res) => {
+ const { new_tariff_id } = req.body;
+ const memberId = req.params.memberId;
+ try {
+ const [tariffs] = await db.query('SELECT * FROM tariffs WHERE id = ?', [new_tariff_id]);
+ if (tariffs.length === 0) return res.redirect(`/admin/members/${memberId}?error=Tarif+nicht+gefunden`);
+ const tariff = tariffs[0];
+
+ const [members] = await db.query('SELECT * FROM memberships WHERE id = ?', [memberId]);
+ const member = members[0];
+
+ const startDate = new Date(member.effective_end || member.contract_end || new Date());
+ const newEnd = new Date(startDate);
+ newEnd.setMonth(newEnd.getMonth() + tariff.duration_months);
+
+ await db.query(`
+ UPDATE memberships SET
+ tariff_id = ?,
+ agreed_price = ?,
+ agreed_duration = ?,
+ contract_end = ?,
+ effective_end = ?,
+ status = 'active'
+ WHERE id = ?
+ `, [tariff.id, tariff.price_monthly, tariff.duration_months, newEnd, newEnd, memberId]);
+
+ res.redirect(`/admin/members/${memberId}?success=Vertrag+verlängert+bis+${newEnd.toLocaleDateString('de-DE')}`);
+ } catch (err) {
+ console.error(err);
+ res.redirect(`/admin/members/${memberId}?error=Fehler+bei+Verlängerung`);
+ }
+});
+
+module.exports = router;
+module.exports.renewalEmailHtml = renewalEmailHtml;
diff --git a/views/admin/finance.ejs b/views/admin/finance.ejs
index c58233d..cef21b4 100644
--- a/views/admin/finance.ejs
+++ b/views/admin/finance.ejs
@@ -275,7 +275,12 @@
<%= Number(m.price_monthly).toFixed(2).replace('.', ',') %> € |
- Zur Karteikarte
+
|
<% }) %>
diff --git a/views/admin/member-detail.ejs b/views/admin/member-detail.ejs
index e82e430..5e4772e 100644
--- a/views/admin/member-detail.ejs
+++ b/views/admin/member-detail.ejs
@@ -455,6 +455,41 @@
+
+
+
+
diff --git a/views/renewal-expired.ejs b/views/renewal-expired.ejs
new file mode 100644
index 0000000..4d456ec
--- /dev/null
+++ b/views/renewal-expired.ejs
@@ -0,0 +1,18 @@
+
+
+PlusFit24 – Link abgelaufen
+
+
+
+
+
+
+
+
⏰
+
Link abgelaufen
+
Dieser Verlängerungslink ist nicht mehr gültig.
+
Bitte kontaktiere uns direkt im Studio oder ruf uns an.
+
Zur Startseite
+
+
+
diff --git a/views/renewal.ejs b/views/renewal.ejs
new file mode 100644
index 0000000..991ea9f
--- /dev/null
+++ b/views/renewal.ejs
@@ -0,0 +1,70 @@
+
+
+
+
+
+ PlusFit24 – Mitgliedschaft verlängern
+
+
+
+
+
+
+
+
Mitgliedschaft verlängern
+
Hallo <%= request.first_name %>! Wähle deinen neuen Tarif.
+
+ <% if (success) { %>
+
<%= success %>
+
Zurück zur Startseite
+ <% } else { %>
+ <% if (error) { %>
<%= error %>
<% } %>
+
+
+ <% } %>
+
+
+
+
+
+