const express = require('express'); const crypto = require('crypto'); const router = express.Router(); const bcrypt = require('bcryptjs'); const db = require('../config/database'); const { requireAdmin } = require('../middleware/auth'); // Login router.get('/login', (req, res) => { if (req.session.adminId) return res.redirect('/admin'); res.render('admin/login', { error: null }); }); router.post('/login', async (req, res) => { const { username, password } = req.body; try { const [admins] = await db.query('SELECT * FROM admins WHERE username = ?', [username]); if (admins.length === 0) return res.render('admin/login', { error: 'Ungültige Anmeldedaten.' }); const valid = await bcrypt.compare(password, admins[0].password_hash); if (!valid) return res.render('admin/login', { error: 'Ungültige Anmeldedaten.' }); req.session.adminId = admins[0].id; req.session.adminUser = admins[0].username; res.redirect('/admin'); } catch (err) { res.render('admin/login', { error: 'Serverfehler.' }); } }); router.get('/logout', (req, res) => { req.session.destroy(); res.redirect('/admin/login'); }); // Dashboard router.get('/', requireAdmin, async (req, res) => { try { const [tariffs] = await db.query(` SELECT t.*, c.name as category_name FROM tariffs t LEFT JOIN categories c ON t.category_id = c.id ORDER BY t.active DESC, t.created_at DESC `); const [categories] = await db.query('SELECT * FROM categories ORDER BY name ASC'); const [memberships] = await db.query(` SELECT m.*, t.name as tariff_name, t.price_monthly FROM memberships m LEFT JOIN tariffs t ON m.tariff_id = t.id ORDER BY m.created_at DESC `); const [stats] = await db.query(` SELECT COUNT(*) as total, SUM(CASE WHEN status = 'active' THEN 1 ELSE 0 END) as active_count, SUM(CASE WHEN is_minor = 1 THEN 1 ELSE 0 END) as minors, SUM(CASE WHEN created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY) THEN 1 ELSE 0 END) as last_30_days, SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) as pending_count, SUM(CASE WHEN reviewed = 0 AND status = 'active' THEN 1 ELSE 0 END) as new_count FROM memberships `); res.render('admin/dashboard', { tariffs, categories, memberships, stats: stats[0], admin: req.session.adminUser, success: req.query.success || null, error: req.query.error || null }); } catch (err) { console.error(err); res.render('admin/dashboard', { tariffs: [], categories: [], memberships: [], stats: {}, admin: req.session.adminUser, success: null, error: 'Datenbankfehler: ' + err.message }); } }); // ===== KATEGORIEN ===== router.post('/categories', requireAdmin, async (req, res) => { const { name } = req.body; if (!name || !name.trim()) return res.redirect('/admin?error=Kategoriename+fehlt'); try { await db.query('INSERT INTO categories (name) VALUES (?)', [name.trim()]); res.redirect('/admin?success=Kategorie+erstellt#kategorien'); } catch (err) { res.redirect('/admin?error=Fehler+beim+Erstellen'); } }); router.post('/categories/:id/update', requireAdmin, async (req, res) => { const { name } = req.body; if (!name || !name.trim()) return res.redirect('/admin?error=Kategoriename+fehlt'); try { await db.query('UPDATE categories SET name = ? WHERE id = ?', [name.trim(), req.params.id]); res.redirect('/admin?success=Kategorie+aktualisiert#kategorien'); } catch (err) { res.redirect('/admin?error=Fehler+beim+Aktualisieren'); } }); router.post('/categories/:id/delete', requireAdmin, async (req, res) => { try { const [used] = await db.query('SELECT COUNT(*) as c FROM tariffs WHERE category_id = ?', [req.params.id]); if (used[0].c > 0) { return res.redirect('/admin?error=Kategorie+wird+von+' + used[0].c + '+Tarifen+verwendet+–+bitte+erst+Tarife+umziehen#kategorien'); } await db.query('DELETE FROM categories WHERE id = ?', [req.params.id]); res.redirect('/admin?success=Kategorie+gelöscht#kategorien'); } catch (err) { res.redirect('/admin?error=Fehler+beim+Löschen'); } }); // ===== TARIFE ===== router.post('/tariffs', requireAdmin, async (req, res) => { const { name, category_id, duration_months, price_monthly, start_package_price, description } = req.body; try { await db.query( 'INSERT INTO tariffs (name, category_id, duration_months, price_monthly, start_package_price, description) VALUES (?, ?, ?, ?, ?, ?)', [name, category_id || null, duration_months, price_monthly, start_package_price || 35.00, description || ''] ); res.redirect('/admin?success=Tarif+erstellt'); } catch (err) { res.redirect('/admin?error=Fehler+beim+Erstellen+des+Tarifs'); } }); router.post('/tariffs/:id/toggle', requireAdmin, async (req, res) => { try { await db.query('UPDATE tariffs SET active = NOT active WHERE id = ?', [req.params.id]); res.redirect('/admin?success=Tarif+aktualisiert'); } catch (err) { res.redirect('/admin?error=Fehler+beim+Aktualisieren'); } }); router.post('/tariffs/:id/update', requireAdmin, async (req, res) => { const { name, category_id, duration_months, price_monthly, start_package_price, description } = req.body; try { await db.query( 'UPDATE tariffs SET name=?, category_id=?, duration_months=?, price_monthly=?, start_package_price=?, description=? WHERE id=?', [name, category_id || null, duration_months, price_monthly, start_package_price, description, req.params.id] ); res.redirect('/admin?success=Tarif+aktualisiert'); } catch (err) { res.redirect('/admin?error=Fehler+beim+Aktualisieren'); } }); // Passwort ändern router.post('/change-password', requireAdmin, async (req, res) => { const { current_password, new_password, confirm_password } = req.body; if (new_password !== confirm_password) return res.redirect('/admin?error=Passwörter+stimmen+nicht+überein'); try { const [admins] = await db.query('SELECT * FROM admins WHERE id = ?', [req.session.adminId]); const valid = await bcrypt.compare(current_password, admins[0].password_hash); if (!valid) return res.redirect('/admin?error=Aktuelles+Passwort+falsch'); const hash = await bcrypt.hash(new_password, 12); await db.query('UPDATE admins SET password_hash = ? WHERE id = ?', [hash, req.session.adminId]); res.redirect('/admin?success=Passwort+geändert'); } catch (err) { res.redirect('/admin?error=Fehler+beim+Ändern+des+Passworts'); } }); // ===== 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 [invoices] = await db.query(` SELECT * FROM invoices WHERE membership_id = ? ORDER BY period DESC, created_at 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, invoices, 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, agreed_price, agreed_duration, start_package_price } = 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`); } }); // ===== AUSZEITEN ===== router.post('/members/:id/pauses/add', requireAdmin, async (req, res) => { const { pause_start, pause_end, reason } = req.body; const memberId = req.params.id; try { if (!pause_start || !pause_end) { return res.redirect(`/admin/members/${memberId}?error=Bitte+Von+und+Bis+ausfüllen`); } const start = new Date(pause_start); const end = new Date(pause_end); if (end <= start) { return res.redirect(`/admin/members/${memberId}?error=Enddatum+muss+nach+Startdatum+liegen`); } // Monate automatisch berechnen (aufgerundet) const pause_months = Math.ceil( (end.getFullYear() - start.getFullYear()) * 12 + (end.getMonth() - start.getMonth()) + (end.getDate() > start.getDate() ? 1 : 0) ); // Auszeit eintragen await db.query( 'INSERT INTO membership_pauses (membership_id, pause_start, pause_end, pause_months, reason) VALUES (?, ?, ?, ?, ?)', [memberId, pause_start, pause_end, pause_months, reason || null] ); // pause_months_total und effective_end aktualisieren await db.query(` UPDATE memberships SET pause_months_total = ( SELECT COALESCE(SUM(pause_months), 0) FROM membership_pauses WHERE membership_id = ? ), effective_end = DATE_ADD(contract_end, INTERVAL ( SELECT COALESCE(SUM(pause_months), 0) FROM membership_pauses WHERE membership_id = ? ) MONTH) WHERE id = ? `, [memberId, memberId, memberId]); res.redirect(`/admin/members/${memberId}?success=Auszeit+eingetragen`); } catch (err) { console.error(err); res.redirect(`/admin/members/${memberId}?error=Fehler+beim+Eintragen+der+Auszeit`); } }); router.post('/members/:id/pauses/:pauseId/delete', requireAdmin, async (req, res) => { const { id: memberId, pauseId } = req.params; try { await db.query('DELETE FROM membership_pauses WHERE id = ? AND membership_id = ?', [pauseId, memberId]); // Summe neu berechnen await db.query(` UPDATE memberships SET pause_months_total = ( SELECT COALESCE(SUM(pause_months), 0) FROM membership_pauses WHERE membership_id = ? ), effective_end = DATE_ADD(contract_end, INTERVAL ( SELECT COALESCE(SUM(pause_months), 0) FROM membership_pauses WHERE membership_id = ? ) MONTH) WHERE id = ? `, [memberId, memberId, memberId]); res.redirect(`/admin/members/${memberId}?success=Auszeit+gelöscht`); } catch (err) { console.error(err); res.redirect(`/admin/members/${memberId}?error=Fehler+beim+Löschen`); } }); // ===== NFC / ZUGANGSKARTE ===== router.post('/members/:id/regenerate-token', requireAdmin, async (req, res) => { try { const token = crypto.randomBytes(16).toString('hex'); await db.query( 'UPDATE memberships SET access_token = ? WHERE id = ?', [token, req.params.id] ); res.redirect(`/admin/members/${req.params.id}?success=Neuer+Token+generiert`); } catch (err) { res.redirect(`/admin/members/${req.params.id}?error=Fehler+beim+Generieren`); } }); router.post('/members/:id/update-nfc', requireAdmin, async (req, res) => { const { nfc_uid } = req.body; try { await db.query( 'UPDATE memberships SET nfc_uid = ?, card_issued = 1, card_issued_at = NOW() WHERE id = ?', [nfc_uid ? nfc_uid.trim().toUpperCase() : null, req.params.id] ); res.redirect(`/admin/members/${req.params.id}?success=NFC+UID+gespeichert`); } catch (err) { res.redirect(`/admin/members/${req.params.id}?error=Fehler+beim+Speichern`); } }); // Mitglied manuell bestätigen router.post('/members/:id/confirm', requireAdmin, async (req, res) => { try { await db.query( "UPDATE memberships SET status='active', confirmed_at=NOW() WHERE id=?", [req.params.id] ); res.redirect(`/admin/members/${req.params.id}?success=Mitglied+manuell+bestätigt`); } catch (err) { res.redirect(`/admin/members/${req.params.id}?error=Fehler+bei+Bestätigung`); } }); // Mitglied als gesehen markieren router.post('/members/:id/reviewed', requireAdmin, async (req, res) => { try { await db.query('UPDATE memberships SET reviewed = 1 WHERE id = ?', [req.params.id]); res.json({ success: true }); } catch (err) { res.json({ success: false }); } }); // GET /admin/api/badge-count – für Live-Update router.get('/api/badge-count', requireAdmin, async (req, res) => { try { const [rows] = await db.query(` SELECT SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) as pending_count, SUM(CASE WHEN reviewed = 0 AND status = 'active' THEN 1 ELSE 0 END) as new_count FROM memberships `); const total = (rows[0].pending_count || 0) + (rows[0].new_count || 0); res.json({ total, pending: rows[0].pending_count || 0, new: rows[0].new_count || 0 }); } catch (err) { res.json({ total: 0, pending: 0, new: 0 }); } }); module.exports = router;