249 lines
9.6 KiB
JavaScript
249 lines
9.6 KiB
JavaScript
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 => `
|
|
<tr>
|
|
<td style="padding:8px 12px;border-bottom:1px solid #eee">${t.name}</td>
|
|
<td style="padding:8px 12px;border-bottom:1px solid #eee;text-align:right">
|
|
<strong>${Number(t.price_monthly).toFixed(2).replace('.', ',')} €/Monat</strong>
|
|
</td>
|
|
<td style="padding:8px 12px;border-bottom:1px solid #eee">${t.duration_months} Monate</td>
|
|
</tr>`).join('');
|
|
|
|
return `<!DOCTYPE html>
|
|
<html lang="de">
|
|
<head><meta charset="UTF-8"></head>
|
|
<body style="font-family:Outfit,Arial,sans-serif;background:#f8f9ff;margin:0;padding:20px">
|
|
<div style="max-width:600px;margin:0 auto;background:white;border-radius:16px;overflow:hidden;box-shadow:0 2px 16px rgba(0,0,0,0.08)">
|
|
<div style="background:#2d2dcc;padding:32px;text-align:center">
|
|
<h1 style="color:white;margin:0;font-size:1.8rem">Plusfit<span style="color:#a5b4fc">24</span></h1>
|
|
<p style="color:#c7d2fe;margin:8px 0 0">Deine Mitgliedschaft läuft bald ab</p>
|
|
</div>
|
|
<div style="padding:32px">
|
|
<p>Hallo ${member.first_name} ${member.last_name},</p>
|
|
<p>deine Mitgliedschaft bei PlusFit24 läuft am <strong>${new Date(expiryDate).toLocaleDateString('de-DE')}</strong> aus.</p>
|
|
<p>Wir würden uns freuen, dich weiterhin in unserem Studio begrüßen zu dürfen! Wähle jetzt deinen neuen Tarif:</p>
|
|
|
|
<table style="width:100%;border-collapse:collapse;margin:20px 0;border:1px solid #eee;border-radius:8px;overflow:hidden">
|
|
<thead>
|
|
<tr style="background:#f0f0ff">
|
|
<th style="padding:10px 12px;text-align:left;font-size:0.8rem;text-transform:uppercase;letter-spacing:0.5px">Tarif</th>
|
|
<th style="padding:10px 12px;text-align:right;font-size:0.8rem;text-transform:uppercase;letter-spacing:0.5px">Preis</th>
|
|
<th style="padding:10px 12px;font-size:0.8rem;text-transform:uppercase;letter-spacing:0.5px">Laufzeit</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>${tarifList}</tbody>
|
|
</table>
|
|
|
|
<div style="text-align:center;margin:28px 0">
|
|
<a href="${renewalLink}" style="display:inline-block;background:#2d2dcc;color:white;padding:14px 32px;border-radius:10px;text-decoration:none;font-weight:700;font-size:1rem">
|
|
Jetzt Mitgliedschaft verlängern →
|
|
</a>
|
|
</div>
|
|
|
|
<p style="color:#6b7280;font-size:0.85rem">
|
|
Dieser Link ist 30 Tage gültig. Falls du Fragen hast, melde dich gerne bei uns.<br>
|
|
<strong>PlusFit24 UG</strong> · Moosleiten 12 · 84089 Aiglsbach
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</body></html>`;
|
|
}
|
|
|
|
// ============================================
|
|
// Ö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;
|