frtjrd
This commit is contained in:
parent
7cee8ac281
commit
6574b5f6d9
4
app.js
4
app.js
@ -35,7 +35,8 @@ 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 renewalRouter = require('./routes/renewal');
|
||||
const contractsRouter = require('./routes/contracts');
|
||||
const cron = require('node-cron');
|
||||
|
||||
app.use('/', indexRouter);
|
||||
@ -44,6 +45,7 @@ app.use('/api', apiRouter);
|
||||
app.use('/admin/billing', billingRouter);
|
||||
app.use('/admin/finance', financeRouter);
|
||||
app.use('/', renewalRouter);
|
||||
app.use('/admin/contracts', contractsRouter);
|
||||
|
||||
// 404 Handler
|
||||
app.use((req, res) => {
|
||||
|
||||
@ -1407,3 +1407,12 @@ body:not(.admin-body) > * {
|
||||
.member-detail-page .btn-sm { padding: 4px 9px; font-size: 0.76rem; }
|
||||
.member-detail-page .neue-auszeit { padding: 10px 12px; }
|
||||
.member-detail-page .neue-auszeit-title { font-size: 0.8rem; margin-bottom: 8px; }
|
||||
|
||||
/* Verträge Grid */
|
||||
.contracts-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 300px 1fr;
|
||||
gap: 16px;
|
||||
align-items: start;
|
||||
}
|
||||
@media (max-width: 900px) { .contracts-grid { grid-template-columns: 1fr; } }
|
||||
|
||||
78
routes/contracts.js
Normal file
78
routes/contracts.js
Normal file
@ -0,0 +1,78 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const db = require('../config/database');
|
||||
const { requireAdmin } = require('../middleware/auth');
|
||||
|
||||
// GET /admin/contracts
|
||||
router.get('/', requireAdmin, async (req, res) => {
|
||||
try {
|
||||
// Verträge nach Kategorie
|
||||
const [byCategory] = await db.query(`
|
||||
SELECT
|
||||
c.name as category_name,
|
||||
COUNT(m.id) as total,
|
||||
SUM(CASE WHEN m.status='active' THEN 1 ELSE 0 END) as active,
|
||||
SUM(CASE WHEN m.status='paused' THEN 1 ELSE 0 END) as paused,
|
||||
SUM(CASE WHEN m.status='inactive' THEN 1 ELSE 0 END) as inactive,
|
||||
SUM(COALESCE(m.agreed_price, t.price_monthly)) as monthly_revenue
|
||||
FROM memberships m
|
||||
JOIN tariffs t ON m.tariff_id = t.id
|
||||
LEFT JOIN categories c ON t.category_id = c.id
|
||||
GROUP BY c.id, c.name
|
||||
ORDER BY total DESC
|
||||
`);
|
||||
|
||||
// Verträge nach Tarif
|
||||
const [byTariff] = await db.query(`
|
||||
SELECT
|
||||
t.name as tariff_name, t.price_monthly, t.duration_months, t.active as tariff_active,
|
||||
COUNT(m.id) as total,
|
||||
SUM(CASE WHEN m.status='active' THEN 1 ELSE 0 END) as active,
|
||||
SUM(COALESCE(m.agreed_price, t.price_monthly)) as monthly_revenue
|
||||
FROM memberships m
|
||||
JOIN tariffs t ON m.tariff_id = t.id
|
||||
GROUP BY t.id, t.name, t.price_monthly, t.duration_months, t.active
|
||||
ORDER BY active DESC, total DESC
|
||||
`);
|
||||
|
||||
// Gesamtübersicht
|
||||
const [totals] = await db.query(`
|
||||
SELECT
|
||||
COUNT(*) as total,
|
||||
SUM(CASE WHEN status='active' THEN 1 ELSE 0 END) as active,
|
||||
SUM(CASE WHEN status='paused' THEN 1 ELSE 0 END) as paused,
|
||||
SUM(CASE WHEN status='inactive' THEN 1 ELSE 0 END) as inactive,
|
||||
SUM(CASE WHEN is_minor=1 THEN 1 ELSE 0 END) as minors,
|
||||
SUM(COALESCE(agreed_price, 0)) as total_monthly
|
||||
FROM memberships
|
||||
`);
|
||||
|
||||
// Auslaufende Verträge – 3 Monate
|
||||
const [expiring] = await db.query(`
|
||||
SELECT m.*, t.name as tariff_name, t.price_monthly,
|
||||
COALESCE(m.agreed_price, t.price_monthly) as agreed_price
|
||||
FROM memberships m
|
||||
JOIN tariffs t ON m.tariff_id = t.id
|
||||
WHERE m.status = 'active'
|
||||
AND m.effective_end BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL 3 MONTH)
|
||||
ORDER BY m.effective_end ASC
|
||||
`);
|
||||
|
||||
// Alle aktiven Tarife für Dropdown
|
||||
const [tariffs] = await db.query('SELECT * FROM tariffs WHERE active=1 ORDER BY name ASC');
|
||||
|
||||
res.render('admin/contracts', {
|
||||
admin: req.session.adminUser,
|
||||
byCategory, byTariff,
|
||||
totals: totals[0],
|
||||
expiring, tariffs,
|
||||
success: req.query.success || null,
|
||||
error: req.query.error || null
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.redirect('/admin?error=Fehler+in+Vertragsübersicht:+' + encodeURIComponent(err.message));
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@ -16,7 +16,8 @@
|
||||
<a href="/admin" class="nav-link">📋 Tarife</a>
|
||||
<a href="/admin#kategorien" class="nav-link">🏷️ Kategorien</a>
|
||||
<a href="/admin#mitglieder" class="nav-link">👥 Mitglieder</a>
|
||||
<a href="/admin/billing" class="nav-link active">💶 Abrechnung</a>
|
||||
<a href="/admin/contracts" class="nav-link">📑 Verträge</a>
|
||||
<a href="/admin/billing" class="nav-link active">💶 Abrechnung</a>
|
||||
<a href="/admin/finance" class="nav-link">📊 Finanzen</a>
|
||||
<a href="/admin#einstellungen" class="nav-link">⚙️ Einstellungen</a>
|
||||
</nav>
|
||||
|
||||
296
views/admin/contracts.ejs
Normal file
296
views/admin/contracts.ejs
Normal file
@ -0,0 +1,296 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>PlusFit24 – Verträge</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">
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
||||
</head>
|
||||
<body class="admin-body">
|
||||
<div class="admin-layout">
|
||||
|
||||
<aside class="admin-sidebar">
|
||||
<div class="logo admin-logo">Plusfit<span>24</span></div>
|
||||
<nav class="admin-nav">
|
||||
<a href="/admin" class="nav-link">📋 Tarife</a>
|
||||
<a href="/admin#kategorien" class="nav-link">🏷️ Kategorien</a>
|
||||
<a href="/admin#mitglieder" class="nav-link">👥 Mitglieder</a>
|
||||
<a href="/admin/contracts" class="nav-link active">📑 Verträge</a>
|
||||
<a href="/admin/billing" class="nav-link">💶 Abrechnung</a>
|
||||
<a href="/admin/finance" class="nav-link">📊 Finanzen</a>
|
||||
<a href="/admin#einstellungen" class="nav-link">⚙️ Einstellungen</a>
|
||||
</nav>
|
||||
<div class="sidebar-footer">
|
||||
<span>👤 <%= admin %></span>
|
||||
<a href="/admin/logout" class="logout-link">Abmelden</a>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main class="admin-main">
|
||||
<h1 class="finance-title">📑 Verträge</h1>
|
||||
|
||||
<% if (success) { %><div class="alert alert-success"><%= success %></div><% } %>
|
||||
<% if (error) { %><div class="alert alert-error"><%= error %></div><% } %>
|
||||
|
||||
<!-- KPI Karten -->
|
||||
<div class="finance-kpi-grid">
|
||||
<div class="kpi-card kpi-blue">
|
||||
<div class="kpi-label">Gesamt Verträge</div>
|
||||
<div class="kpi-value"><%= totals.total || 0 %></div>
|
||||
</div>
|
||||
<div class="kpi-card kpi-green">
|
||||
<div class="kpi-label">Aktiv</div>
|
||||
<div class="kpi-value"><%= totals.active || 0 %></div>
|
||||
</div>
|
||||
<div class="kpi-card kpi-orange">
|
||||
<div class="kpi-label">Pausiert</div>
|
||||
<div class="kpi-value"><%= totals.paused || 0 %></div>
|
||||
</div>
|
||||
<div class="kpi-card kpi-red">
|
||||
<div class="kpi-label">Inaktiv</div>
|
||||
<div class="kpi-value"><%= totals.inactive || 0 %></div>
|
||||
</div>
|
||||
<div class="kpi-card kpi-purple">
|
||||
<div class="kpi-label">Minderjährige</div>
|
||||
<div class="kpi-value"><%= totals.minors || 0 %></div>
|
||||
</div>
|
||||
<div class="kpi-card kpi-yellow">
|
||||
<div class="kpi-label">Monatl. Umsatz (aktiv)</div>
|
||||
<div class="kpi-value" style="font-size:1.2rem"><%= Number(totals.total_monthly||0).toFixed(2).replace('.',',') %> €</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tabs -->
|
||||
<div class="finance-tabs">
|
||||
<button class="ftab active" onclick="showTab('categories', this)">🏷️ Nach Kategorie</button>
|
||||
<button class="ftab" onclick="showTab('tariffs', this)">📋 Nach Tarif</button>
|
||||
<button class="ftab" onclick="showTab('expiring', this)">
|
||||
⏳ Auslaufend
|
||||
<% if (expiring.length > 0) { %>
|
||||
<span class="expiry-badge expiry-<%= expiring.some(e => { const d=Math.ceil((new Date(e.effective_end)-new Date())/(864e5)); return d<=30; }) ? 'urgent' : 'warning' %>" style="margin-left:6px"><%= expiring.length %></span>
|
||||
<% } %>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Tab: Nach Kategorie -->
|
||||
<div class="ftab-content active" id="tab-categories">
|
||||
<div class="contracts-grid">
|
||||
|
||||
<!-- Donut Chart -->
|
||||
<div class="finance-card" style="display:flex;flex-direction:column;align-items:center">
|
||||
<h3>Verteilung nach Kategorie</h3>
|
||||
<canvas id="categoryChart" style="max-width:260px;max-height:260px;margin-top:12px"></canvas>
|
||||
</div>
|
||||
|
||||
<!-- Tabelle -->
|
||||
<div class="finance-card" style="flex:1">
|
||||
<h3>Übersicht</h3>
|
||||
<table class="admin-table" style="margin-top:12px">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Kategorie</th>
|
||||
<th>Gesamt</th>
|
||||
<th>Aktiv</th>
|
||||
<th>Pausiert</th>
|
||||
<th>Inaktiv</th>
|
||||
<th>Monatl. Umsatz</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% byCategory.forEach(row => { %>
|
||||
<tr>
|
||||
<td><strong><%= row.category_name || '– Keine Kategorie –' %></strong></td>
|
||||
<td><%= row.total %></td>
|
||||
<td style="color:var(--success)"><strong><%= row.active %></strong></td>
|
||||
<td style="color:var(--warning)"><%= row.paused %></td>
|
||||
<td style="color:var(--error)"><%= row.inactive %></td>
|
||||
<td><strong><%= Number(row.monthly_revenue||0).toFixed(2).replace('.',',') %> €</strong></td>
|
||||
</tr>
|
||||
<% }) %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab: Nach Tarif -->
|
||||
<div class="ftab-content" id="tab-tariffs">
|
||||
<div class="finance-card">
|
||||
<h3>Verträge nach Tarif</h3>
|
||||
<div class="table-wrap" style="margin-top:12px">
|
||||
<table class="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Tarif</th>
|
||||
<th>Laufzeit</th>
|
||||
<th>Aktueller Preis</th>
|
||||
<th>Mitglieder</th>
|
||||
<th>Davon aktiv</th>
|
||||
<th>Monatl. Umsatz</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% byTariff.forEach(row => { %>
|
||||
<tr>
|
||||
<td><strong><%= row.tariff_name %></strong></td>
|
||||
<td><%= row.duration_months %> Monate</td>
|
||||
<td><%= Number(row.price_monthly).toFixed(2).replace('.',',') %> €/Monat</td>
|
||||
<td><%= row.total %></td>
|
||||
<td style="color:var(--success)"><strong><%= row.active %></strong></td>
|
||||
<td><strong><%= Number(row.monthly_revenue||0).toFixed(2).replace('.',',') %> €</strong></td>
|
||||
<td>
|
||||
<span class="invoice-status <%= row.tariff_active ? 'paid' : 'cancelled' %>">
|
||||
<%= row.tariff_active ? '✅ Aktiv' : '❌ Inaktiv' %>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<% }) %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab: Auslaufende Verträge -->
|
||||
<div class="ftab-content" id="tab-expiring">
|
||||
<div class="finance-card">
|
||||
<div class="section-header">
|
||||
<h3>Auslaufende Verträge – nächste 3 Monate (<%= expiring.length %>)</h3>
|
||||
</div>
|
||||
<% if (expiring.length === 0) { %>
|
||||
<p class="karte-empty">✅ Keine auslaufenden Verträge in den nächsten 3 Monaten.</p>
|
||||
<% } else { %>
|
||||
<div class="table-wrap">
|
||||
<table class="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Mitglied</th>
|
||||
<th>Tarif</th>
|
||||
<th>Vereinbarter Preis</th>
|
||||
<th>Vertragsende</th>
|
||||
<th>Restlaufzeit</th>
|
||||
<th>Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% expiring.forEach(m => { %>
|
||||
<%
|
||||
const endDate = new Date(m.effective_end);
|
||||
const diffDays = Math.ceil((endDate - new Date()) / (1000*60*60*24));
|
||||
const urgency = diffDays <= 30 ? 'urgent' : diffDays <= 60 ? 'warning' : 'normal';
|
||||
%>
|
||||
<tr>
|
||||
<td>
|
||||
<strong><%= m.last_name %>, <%= m.first_name %></strong><br>
|
||||
<small class="text-muted"><%= m.email %></small>
|
||||
</td>
|
||||
<td><%= m.tariff_name %></td>
|
||||
<td><strong><%= Number(m.agreed_price||m.price_monthly).toFixed(2).replace('.',',') %> €</strong></td>
|
||||
<td><strong><%= endDate.toLocaleDateString('de-DE') %></strong></td>
|
||||
<td>
|
||||
<span class="expiry-badge expiry-<%= urgency %>">
|
||||
noch <%= diffDays %> Tage
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<div style="display:flex;gap:6px;flex-wrap:wrap">
|
||||
<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">
|
||||
<input type="hidden" name="_redirect" value="/admin/contracts">
|
||||
<button type="submit" class="btn btn-sm btn-primary">📧 E-Mail</button>
|
||||
</form>
|
||||
<button type="button" class="btn btn-sm btn-success"
|
||||
onclick="openRenewModal(<%= m.id %>, '<%= m.last_name %>, <%= m.first_name %>')">
|
||||
🔄 Verlängern
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<% }) %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- Modal: Manuell verlängern -->
|
||||
<div class="modal-overlay hidden" id="renewModal">
|
||||
<div class="modal">
|
||||
<div class="modal-header">
|
||||
<h3>Vertrag manuell verlängern</h3>
|
||||
<button onclick="toggleModal('renewModal')" class="modal-close">✕</button>
|
||||
</div>
|
||||
<form method="POST" id="renewForm">
|
||||
<div class="form-group">
|
||||
<label>Mitglied</label>
|
||||
<input type="text" id="renewMemberName" class="form-control" disabled>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Neuer Tarif *</label>
|
||||
<select name="new_tariff_id" class="form-control" required>
|
||||
<option value="">– Tarif wählen –</option>
|
||||
<% tariffs.forEach(t => { %>
|
||||
<option value="<%= t.id %>"><%= t.name %> – <%= Number(t.price_monthly).toFixed(2).replace('.',',') %>€/Monat (<%= t.duration_months %> Monate)</option>
|
||||
<% }) %>
|
||||
</select>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" onclick="toggleModal('renewModal')" class="btn btn-outline">Abbrechen</button>
|
||||
<button type="submit" class="btn btn-success"
|
||||
onclick="return confirm('Vertrag verlängern?')">🔄 Verlängern</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Chart
|
||||
const catData = <%- JSON.stringify(byCategory) %>;
|
||||
new Chart(document.getElementById('categoryChart'), {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: catData.map(c => c.category_name || 'Sonstige'),
|
||||
datasets: [{
|
||||
data: catData.map(c => c.active),
|
||||
backgroundColor: ['#2d2dcc','#16a34a','#d97706','#7c3aed','#dc2626','#0891b2'],
|
||||
borderWidth: 2, borderColor: '#fff'
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: { position: 'bottom', labels: { font: { family: 'Outfit' } } }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function showTab(name, el) {
|
||||
document.querySelectorAll('.ftab-content').forEach(t => t.classList.remove('active'));
|
||||
document.querySelectorAll('.ftab').forEach(t => t.classList.remove('active'));
|
||||
document.getElementById('tab-' + name).classList.add('active');
|
||||
if (el) el.classList.add('active');
|
||||
}
|
||||
|
||||
function toggleModal(id) {
|
||||
document.getElementById(id).classList.toggle('hidden');
|
||||
}
|
||||
|
||||
function openRenewModal(memberId, memberName) {
|
||||
document.getElementById('renewMemberName').value = memberName;
|
||||
document.getElementById('renewForm').action = '/admin/renew-manual/' + memberId;
|
||||
toggleModal('renewModal');
|
||||
}
|
||||
|
||||
// Hash on load
|
||||
const hash = window.location.hash.replace('#','');
|
||||
if (hash && document.getElementById('tab-' + hash)) showTab(hash);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -16,6 +16,7 @@
|
||||
<a href="#" class="nav-link active" onclick="showSection('tarife', this)">📋 Tarife</a>
|
||||
<a href="#" class="nav-link" onclick="showSection('kategorien', this)">🏷️ Kategorien</a>
|
||||
<a href="#" class="nav-link" onclick="showSection('mitglieder', this)">👥 Mitglieder</a>
|
||||
<a href="/admin/contracts" class="nav-link">📑 Verträge</a>
|
||||
<a href="/admin/billing" class="nav-link">💶 Abrechnung</a>
|
||||
<a href="/admin/finance" class="nav-link">📊 Finanzen</a>
|
||||
<a href="#" class="nav-link" onclick="showSection('einstellungen', this)">⚙️ Einstellungen</a>
|
||||
|
||||
@ -17,7 +17,8 @@
|
||||
<a href="/admin" class="nav-link">📋 Tarife</a>
|
||||
<a href="/admin#kategorien" class="nav-link">🏷️ Kategorien</a>
|
||||
<a href="/admin#mitglieder" class="nav-link">👥 Mitglieder</a>
|
||||
<a href="/admin/billing" class="nav-link">💶 Abrechnung</a>
|
||||
<a href="/admin/contracts" class="nav-link">📑 Verträge</a>
|
||||
<a href="/admin/billing" class="nav-link">💶 Abrechnung</a>
|
||||
<a href="/admin/finance" class="nav-link active">📊 Finanzen</a>
|
||||
<a href="/admin#einstellungen" class="nav-link">⚙️ Einstellungen</a>
|
||||
</nav>
|
||||
|
||||
@ -16,7 +16,8 @@
|
||||
<a href="/admin" class="nav-link">📋 Tarife</a>
|
||||
<a href="/admin#kategorien" class="nav-link">🏷️ Kategorien</a>
|
||||
<a href="/admin#mitglieder" class="nav-link active">👥 Mitglieder</a>
|
||||
<a href="/admin/billing" class="nav-link">💶 Abrechnung</a>
|
||||
<a href="/admin/contracts" class="nav-link">📑 Verträge</a>
|
||||
<a href="/admin/billing" class="nav-link">💶 Abrechnung</a>
|
||||
<a href="/admin/finance" class="nav-link">📊 Finanzen</a>
|
||||
<a href="/admin#einstellungen" class="nav-link">⚙️ Einstellungen</a>
|
||||
</nav>
|
||||
@ -456,40 +457,6 @@
|
||||
</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>
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user