diff --git a/app.js b/app.js
index 9cfb8fc..3b5505b 100644
--- a/app.js
+++ b/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) => {
diff --git a/public/css/style.css b/public/css/style.css
index 904773d..9c8e9e1 100644
--- a/public/css/style.css
+++ b/public/css/style.css
@@ -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; } }
diff --git a/routes/contracts.js b/routes/contracts.js
new file mode 100644
index 0000000..4169c4f
--- /dev/null
+++ b/routes/contracts.js
@@ -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;
diff --git a/views/admin/billing.ejs b/views/admin/billing.ejs
index 0e37840..caf928c 100644
--- a/views/admin/billing.ejs
+++ b/views/admin/billing.ejs
@@ -16,7 +16,8 @@
📋 Tarife
🏷️ Kategorien
👥 Mitglieder
- 💶 Abrechnung
+ 📑 Verträge
+ 💶 Abrechnung
📊 Finanzen
⚙️ Einstellungen
diff --git a/views/admin/contracts.ejs b/views/admin/contracts.ejs
new file mode 100644
index 0000000..36c9dfa
--- /dev/null
+++ b/views/admin/contracts.ejs
@@ -0,0 +1,296 @@
+
+
+
+
+
+ PlusFit24 – Verträge
+
+
+
+
+
+
+
+
+
+
+ 📑 Verträge
+
+ <% if (success) { %><%= success %>
<% } %>
+ <% if (error) { %><%= error %>
<% } %>
+
+
+
+
+
Gesamt Verträge
+
<%= totals.total || 0 %>
+
+
+
Aktiv
+
<%= totals.active || 0 %>
+
+
+
Pausiert
+
<%= totals.paused || 0 %>
+
+
+
Inaktiv
+
<%= totals.inactive || 0 %>
+
+
+
Minderjährige
+
<%= totals.minors || 0 %>
+
+
+
Monatl. Umsatz (aktiv)
+
<%= Number(totals.total_monthly||0).toFixed(2).replace('.',',') %> €
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Verteilung nach Kategorie
+
+
+
+
+
+
Übersicht
+
+
+
+ | Kategorie |
+ Gesamt |
+ Aktiv |
+ Pausiert |
+ Inaktiv |
+ Monatl. Umsatz |
+
+
+
+ <% byCategory.forEach(row => { %>
+
+ | <%= row.category_name || '– Keine Kategorie –' %> |
+ <%= row.total %> |
+ <%= row.active %> |
+ <%= row.paused %> |
+ <%= row.inactive %> |
+ <%= Number(row.monthly_revenue||0).toFixed(2).replace('.',',') %> € |
+
+ <% }) %>
+
+
+
+
+
+
+
+
+
+
Verträge nach Tarif
+
+
+
+
+ | Tarif |
+ Laufzeit |
+ Aktueller Preis |
+ Mitglieder |
+ Davon aktiv |
+ Monatl. Umsatz |
+ Status |
+
+
+
+ <% byTariff.forEach(row => { %>
+
+ | <%= row.tariff_name %> |
+ <%= row.duration_months %> Monate |
+ <%= Number(row.price_monthly).toFixed(2).replace('.',',') %> €/Monat |
+ <%= row.total %> |
+ <%= row.active %> |
+ <%= Number(row.monthly_revenue||0).toFixed(2).replace('.',',') %> € |
+
+
+ <%= row.tariff_active ? '✅ Aktiv' : '❌ Inaktiv' %>
+
+ |
+
+ <% }) %>
+
+
+
+
+
+
+
+
+
+
+ <% if (expiring.length === 0) { %>
+
✅ Keine auslaufenden Verträge in den nächsten 3 Monaten.
+ <% } else { %>
+
+
+
+
+ | Mitglied |
+ Tarif |
+ Vereinbarter Preis |
+ Vertragsende |
+ Restlaufzeit |
+ Aktionen |
+
+
+
+ <% 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';
+ %>
+
+
+ <%= m.last_name %>, <%= m.first_name %>
+ <%= m.email %>
+ |
+ <%= m.tariff_name %> |
+ <%= Number(m.agreed_price||m.price_monthly).toFixed(2).replace('.',',') %> € |
+ <%= endDate.toLocaleDateString('de-DE') %> |
+
+
+ noch <%= diffDays %> Tage
+
+ |
+
+
+ |
+
+ <% }) %>
+
+
+
+ <% } %>
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/views/admin/dashboard.ejs b/views/admin/dashboard.ejs
index f7a2300..80e5cec 100644
--- a/views/admin/dashboard.ejs
+++ b/views/admin/dashboard.ejs
@@ -16,6 +16,7 @@
📋 Tarife
🏷️ Kategorien
👥 Mitglieder
+ 📑 Verträge
💶 Abrechnung
📊 Finanzen
⚙️ Einstellungen
diff --git a/views/admin/finance.ejs b/views/admin/finance.ejs
index cef21b4..b6c3f00 100644
--- a/views/admin/finance.ejs
+++ b/views/admin/finance.ejs
@@ -17,7 +17,8 @@
📋 Tarife
🏷️ Kategorien
👥 Mitglieder
- 💶 Abrechnung
+ 📑 Verträge
+ 💶 Abrechnung
📊 Finanzen
⚙️ Einstellungen
diff --git a/views/admin/member-detail.ejs b/views/admin/member-detail.ejs
index 5e4772e..04dcebd 100644
--- a/views/admin/member-detail.ejs
+++ b/views/admin/member-detail.ejs
@@ -16,7 +16,8 @@
📋 Tarife
🏷️ Kategorien
👥 Mitglieder
- 💶 Abrechnung
+ 📑 Verträge
+ 💶 Abrechnung
📊 Finanzen
⚙️ Einstellungen
@@ -456,40 +457,6 @@
-
-
-