367 lines
14 KiB
JavaScript
367 lines
14 KiB
JavaScript
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;
|