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

+ + + + + + + + + + + + + <% byCategory.forEach(row => { %> + + + + + + + + + <% }) %> + +
KategorieGesamtAktivPausiertInaktivMonatl. Umsatz
<%= 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

+
+ + + + + + + + + + + + + + <% byTariff.forEach(row => { %> + + + + + + + + + + <% }) %> + +
TarifLaufzeitAktueller PreisMitgliederDavon aktivMonatl. UmsatzStatus
<%= 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' %> + +
+
+
+
+ + +
+
+
+

Auslaufende Verträge – nächste 3 Monate (<%= expiring.length %>)

+
+ <% if (expiring.length === 0) { %> +

✅ Keine auslaufenden Verträge in den nächsten 3 Monaten.

+ <% } else { %> +
+ + + + + + + + + + + + + <% 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'; + %> + + + + + + + + + <% }) %> + +
MitgliedTarifVereinbarter PreisVertragsendeRestlaufzeitAktionen
+ <%= 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 + + +
+ 👤 Karteikarte +
+ + +
+ +
+
+
+ <% } %> +
+
+ +
+
+ + + + + + + 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 @@ - -
-
- 🔄 -

Vertragsverlängerung

-
-
-
- -
-
- - -
- -
- -
- -
-
-
-
-