verlängerung
This commit is contained in:
parent
a6151c6da8
commit
7cee8ac281
54
app.js
54
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');
|
||||
|
||||
20
config/mailer.js
Normal file
20
config/mailer.js
Normal file
@ -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;
|
||||
@ -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"
|
||||
|
||||
248
routes/renewal.js
Normal file
248
routes/renewal.js
Normal file
@ -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 => `
|
||||
<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;
|
||||
@ -275,7 +275,12 @@
|
||||
</td>
|
||||
<td><%= Number(m.price_monthly).toFixed(2).replace('.', ',') %> €</td>
|
||||
<td>
|
||||
<a href="/admin/members/<%= m.id %>" class="btn btn-sm btn-outline">Zur Karteikarte</a>
|
||||
<div style="display:flex;gap:6px">
|
||||
<a href="/admin/members/<%= m.id %>" class="btn btn-sm btn-outline">Karteikarte</a>
|
||||
<form method="POST" action="/admin/send-renewal/<%= m.id %>" style="display:inline">
|
||||
<button type="submit" class="btn btn-sm btn-primary" title="Verlängerungs-E-Mail senden">📧 E-Mail</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<% }) %>
|
||||
|
||||
@ -455,6 +455,41 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- ===== KARTE: Vertragsverlängerung ===== -->
|
||||
<div class="karte karte-full">
|
||||
<div class="karte-header">
|
||||
<span class="karte-icon">🔄</span>
|
||||
<h3>Vertragsverlängerung</h3>
|
||||
</div>
|
||||
<div class="karte-body">
|
||||
<div style="display:flex;gap:16px;flex-wrap:wrap;align-items:flex-end">
|
||||
<!-- Manuell verlängern -->
|
||||
<form method="POST" action="/admin/renew-manual/<%= member.id %>"
|
||||
style="display:flex;gap:10px;align-items:flex-end;flex:1;min-width:300px"
|
||||
onsubmit="return confirm('Vertrag manuell verlängern?')">
|
||||
<div class="karte-field" style="flex:1">
|
||||
<label>Neuer Tarif</label>
|
||||
<select name="new_tariff_id" class="karte-input" style="border:1.5px solid var(--border);background:white">
|
||||
<% tariffs.forEach(t => { %>
|
||||
<option value="<%= t.id %>"><%= t.name %> – <%= Number(t.price_monthly).toFixed(2) %>€/Monat</option>
|
||||
<% }) %>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-sm" style="white-space:nowrap">
|
||||
🔄 Manuell verlängern
|
||||
</button>
|
||||
</form>
|
||||
<!-- E-Mail senden -->
|
||||
<form method="POST" action="/admin/send-renewal/<%= member.id %>">
|
||||
<button type="submit" class="btn btn-outline btn-sm" style="white-space:nowrap">
|
||||
📧 Verlängerungs-E-Mail senden
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div><!-- end karteikarte-grid -->
|
||||
</form>
|
||||
|
||||
|
||||
18
views/renewal-expired.ejs
Normal file
18
views/renewal-expired.ejs
Normal file
@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de"><head><meta charset="UTF-8">
|
||||
<title>PlusFit24 – Link abgelaufen</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;700;800&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<header class="site-header"><div class="header-inner"><div class="logo">Plusfit<span>24</span></div></div></header>
|
||||
<main class="success-main">
|
||||
<div class="success-card">
|
||||
<div class="success-icon">⏰</div>
|
||||
<h1>Link abgelaufen</h1>
|
||||
<p>Dieser Verlängerungslink ist nicht mehr gültig.</p>
|
||||
<p class="success-sub">Bitte kontaktiere uns direkt im Studio oder ruf uns an.</p>
|
||||
<a href="/" class="btn btn-primary">Zur Startseite</a>
|
||||
</div>
|
||||
</main>
|
||||
</body></html>
|
||||
70
views/renewal.ejs
Normal file
70
views/renewal.ejs
Normal file
@ -0,0 +1,70 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>PlusFit24 – Mitgliedschaft verlängern</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;700;800&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<header class="site-header">
|
||||
<div class="header-inner">
|
||||
<div class="logo">Plusfit<span>24</span></div>
|
||||
</div>
|
||||
</header>
|
||||
<main class="signup-main">
|
||||
<div class="signup-container" style="max-width:680px">
|
||||
<h2 class="step-title bold" style="margin-bottom:8px">Mitgliedschaft verlängern</h2>
|
||||
<p class="step-subtitle">Hallo <%= request.first_name %>! Wähle deinen neuen Tarif.</p>
|
||||
|
||||
<% if (success) { %>
|
||||
<div class="alert alert-success" style="margin-bottom:24px"><%= success %></div>
|
||||
<a href="/" class="btn btn-primary">Zurück zur Startseite</a>
|
||||
<% } else { %>
|
||||
<% if (error) { %><div class="alert alert-error"><%= error %></div><% } %>
|
||||
|
||||
<form method="POST" action="/renew/<%= request.token %>">
|
||||
<div class="tarif-grid" style="margin-bottom:24px">
|
||||
<% tariffs.forEach(tariff => { %>
|
||||
<label class="tarif-card renewal-tarif-card">
|
||||
<input type="radio" name="tariff_id" value="<%= tariff.id %>" required
|
||||
style="display:none" class="tarif-radio">
|
||||
<div class="tarif-card-inner">
|
||||
<div class="tarif-badge">
|
||||
<span class="tarif-icon">◑</span>
|
||||
<span><%= tariff.name %></span>
|
||||
</div>
|
||||
<h2 class="tarif-name"><%= tariff.name %></h2>
|
||||
<div class="tarif-feature">
|
||||
<span class="feature-icon">📦</span>
|
||||
<span>Startpaket <%= Number(tariff.start_package_price).toFixed(2).replace('.', ',') %>€ einmalig</span>
|
||||
</div>
|
||||
<div class="tarif-price">
|
||||
<span class="price-amount"><%= Number(tariff.price_monthly).toFixed(2).replace('.', ',') %>€</span>
|
||||
<span class="price-period">/Monat</span>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
<% }) %>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-full">Mitgliedschaft verlängern →</button>
|
||||
</form>
|
||||
<% } %>
|
||||
</div>
|
||||
</main>
|
||||
<footer class="site-footer">
|
||||
<p>© 2024 PlusFit24 UG</p>
|
||||
</footer>
|
||||
<script>
|
||||
// Tarif-Karte anklicken → Radio aktivieren
|
||||
document.querySelectorAll('.renewal-tarif-card').forEach(card => {
|
||||
card.addEventListener('click', () => {
|
||||
document.querySelectorAll('.renewal-tarif-card').forEach(c => c.style.borderColor = '');
|
||||
card.style.borderColor = '#2d2dcc';
|
||||
card.querySelector('input').checked = true;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Reference in New Issue
Block a user