Vertragsverwaltung_Plusfit24/routes/admin.js
2026-03-28 09:37:52 +00:00

367 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;