diff --git a/public/css/style.css b/public/css/style.css index ff01d5d..d56b740 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -817,3 +817,192 @@ body { font-size: 0.88rem; color: #1e40af; } + +/* ================================================ + MITGLIED DETAIL – KARTEIKARTE + ================================================ */ + +.detail-header { + display: flex; + align-items: center; + gap: 16px; + margin-bottom: 24px; + flex-wrap: wrap; +} +.detail-header-title { + flex: 1; + display: flex; + align-items: center; + gap: 12px; + flex-wrap: wrap; +} +.detail-header-title h1 { + font-size: 1.6rem; + font-weight: 800; + margin: 0; +} +.member-id-badge { + font-size: 0.78rem; + font-weight: 700; + background: var(--bg); + border: 1.5px solid var(--border); + color: var(--text-muted); + padding: 3px 10px; + border-radius: 20px; +} +.status-badge { + font-size: 0.78rem; + font-weight: 700; + padding: 4px 12px; + border-radius: 20px; +} +.status-badge.active { background: #dcfce7; color: var(--success); } +.status-badge.inactive { background: #fee2e2; color: var(--error); } +.status-badge.warning { background: #fffbeb; color: var(--warning); } +.detail-header-actions { + display: flex; + gap: 10px; +} + +/* Grid der Karten */ +.karteikarte-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 20px; +} +@media (max-width: 900px) { + .karteikarte-grid { grid-template-columns: 1fr; } +} + +/* Einzelne Karte */ +.karte { + background: white; + border: 1.5px solid var(--border); + border-radius: 16px; + overflow: hidden; + transition: box-shadow 0.2s; +} +.karte.edit-mode { + border-color: var(--primary); + box-shadow: 0 0 0 3px rgba(45,45,204,0.08); +} +.karte-full { + grid-column: 1 / -1; +} +.karte-header { + display: flex; + align-items: center; + gap: 10px; + padding: 14px 20px; + background: var(--bg); + border-bottom: 1.5px solid var(--border); +} +.karte-header h3 { + font-size: 0.95rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.5px; + color: var(--text-muted); + margin: 0; +} +.karte-icon { font-size: 1.1rem; } +.karte-body { padding: 20px; } + +/* Zeilen & Felder */ +.karte-row { + display: flex; + gap: 16px; + margin-bottom: 14px; +} +.karte-row:last-child { margin-bottom: 0; } +.karte-field { + flex: 1; + display: flex; + flex-direction: column; + gap: 5px; +} +.karte-field-full { flex: 1 1 100%; } +.karte-field label { + font-size: 0.75rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.5px; + color: var(--text-muted); +} +.karte-field label small { + font-size: 0.7rem; + font-weight: 400; + text-transform: none; + letter-spacing: 0; +} + +/* Input Felder im Readonly-Modus */ +.karte-input { + padding: 9px 12px; + border: 1.5px solid transparent; + border-radius: 8px; + background: var(--bg); + font-family: 'Outfit', sans-serif; + font-size: 0.92rem; + color: var(--text); + width: 100%; + transition: all 0.2s; + outline: none; +} +.karte-input:disabled { + background: var(--bg); + color: var(--text); + cursor: default; + -webkit-text-fill-color: var(--text); + opacity: 1; +} +/* Im Edit-Modus */ +.karte-input:not(:disabled) { + background: white; + border-color: var(--border); +} +.karte-input:not(:disabled):focus { + border-color: var(--primary); +} +/* Immer readonly (nie editierbar) */ +.karte-readonly { + background: #f8f8ff !important; + color: var(--text-muted) !important; + font-style: italic; +} +.karte-iban { + font-family: monospace; + letter-spacing: 1px; +} +.karte-empty { + color: var(--text-muted); + font-style: italic; + text-align: center; + padding: 20px 0; +} + +/* Auszeit Tabelle */ +.pause-table { + width: 100%; + border-collapse: collapse; + font-size: 0.88rem; +} +.pause-table th { + text-align: left; + padding: 8px 12px; + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.5px; + color: var(--text-muted); + border-bottom: 1.5px solid var(--border); + background: var(--bg); +} +.pause-table td { + padding: 10px 12px; + border-bottom: 1px solid var(--border); +} +.pause-table tr:last-child td { border-bottom: none; } +.pause-table tr:hover td { background: var(--bg); } + +/* Hover auf Mitglieder-Tabellenzeilen */ +.member-row:hover td { background: #f0f0ff !important; } diff --git a/routes/admin.js b/routes/admin.js index 418497a..66eae08 100644 --- a/routes/admin.js +++ b/routes/admin.js @@ -156,4 +156,66 @@ router.post('/change-password', requireAdmin, async (req, res) => { } }); +// ===== MITGLIED DETAIL ===== +router.get('/members/:id', requireAdmin, async (req, res) => { + try { + const [rows] = await db.query(` + SELECT m.*, + t.name as tariff_name, t.price_monthly, t.duration_months, + c.name as category_name + FROM memberships m + LEFT JOIN tariffs t ON m.tariff_id = t.id + LEFT JOIN categories c ON t.category_id = c.id + WHERE m.id = ? + `, [req.params.id]); + + if (rows.length === 0) return res.redirect('/admin?error=Mitglied+nicht+gefunden'); + + const [pauses] = await db.query( + 'SELECT * FROM membership_pauses WHERE membership_id = ? ORDER BY pause_start DESC', + [req.params.id] + ); + const [tariffs] = await db.query( + 'SELECT * FROM tariffs WHERE active = 1 ORDER BY name ASC' + ); + + res.render('admin/member-detail', { + member: rows[0], + pauses, + tariffs, + admin: req.session.adminUser, + success: req.query.success || null, + error: req.query.error || null + }); + } catch (err) { + console.error(err); + res.redirect('/admin?error=Fehler+beim+Laden+des+Mitglieds'); + } +}); + +router.post('/members/:id/update', requireAdmin, async (req, res) => { + const { + salutation, title, first_name, last_name, birth_date, + email, phone, street, address_addition, zip, city, + bank_name, account_holder, iban, tariff_id, status + } = req.body; + try { + await db.query(` + UPDATE memberships SET + salutation=?, title=?, first_name=?, last_name=?, birth_date=?, + email=?, phone=?, street=?, address_addition=?, zip=?, city=?, + bank_name=?, account_holder=?, iban=?, tariff_id=?, status=? + WHERE id=? + `, [ + salutation, title || '', first_name, last_name, birth_date, + email, phone || '', street, address_addition || '', zip, city, + bank_name || '', account_holder || '', iban || '', + tariff_id, status, req.params.id + ]); + res.redirect(`/admin/members/${req.params.id}?success=Daten+gespeichert`); + } catch (err) { + console.error(err); + res.redirect(`/admin/members/${req.params.id}?error=Fehler+beim+Speichern`); + } +}); module.exports = router; diff --git a/views/admin/dashboard.ejs b/views/admin/dashboard.ejs index 7b30a6b..dfcc00c 100644 --- a/views/admin/dashboard.ejs +++ b/views/admin/dashboard.ejs @@ -150,7 +150,7 @@ <% memberships.forEach(m => { %> - + <%= m.salutation %> <%= m.first_name %> <%= m.last_name %> <%= m.email %> <%= m.tariff_name || '–' %> diff --git a/views/admin/member-detail.ejs b/views/admin/member-detail.ejs new file mode 100644 index 0000000..f8627bc --- /dev/null +++ b/views/admin/member-detail.ejs @@ -0,0 +1,311 @@ + + + + + + PlusFit24 – Mitglied #<%= member.id %> + + + + +
+ + + + + +
+ + +
+ ← Zurück +
+

<%= member.first_name %> <%= member.last_name %>

+ Mitglied #<%= member.id %> + + <%= member.status === 'active' ? '✅ Aktiv' : '❌ Inaktiv' %> + + <% if (member.is_minor) { %> + ⚠️ Minderjährig + <% } %> +
+
+ + + +
+
+ + <% if (success) { %>
<%= success %>
<% } %> + <% if (error) { %>
<%= error %>
<% } %> + +
+ +
+ + +
+
+ 👤 +

Persönliche Daten

+
+
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+ 📍 +

Adresse

+
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ 📄 +

Vertrag

+
+
+
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ 🏦 +

Bankdaten / SEPA

+
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+ + +
+
+ <% if (member.is_minor) { %> +
+
+ + +
+
+ <% } %> +
+
+ + +
+
+ +

Auszeiten

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

Keine Auszeiten eingetragen.

+ <% } else { %> + + + + + + + + + + + + <% pauses.forEach(p => { %> + + + + + + + + <% }) %> + +
VonBisMonateGrundEingetragen am
<%= new Date(p.pause_start).toLocaleDateString('de-DE') %><%= new Date(p.pause_end).toLocaleDateString('de-DE') %><%= p.pause_months %><%= p.reason || '–' %><%= new Date(p.created_at).toLocaleDateString('de-DE') %>
+ <% } %> +
+
+ +
+
+ +
+
+ + + +