const express = require('express'); const router = express.Router(); const db = require('../config/database'); const { requireAdmin } = require('../middleware/auth'); // ============================================ // GET /admin/finance – Übersicht // ============================================ router.get('/', requireAdmin, async (req, res) => { try { // Mahngebühr aus Einstellungen const [settingRows] = await db.query("SELECT value FROM settings WHERE key_name='dunning_fee'"); const dunningFee = settingRows.length ? parseFloat(settingRows[0].value) : 7.50; // Gesamtumsatz const [totalRevenue] = await db.query(` SELECT COALESCE(SUM(CASE WHEN status='paid' THEN amount ELSE 0 END), 0) as paid_total, COALESCE(SUM(CASE WHEN status='open' THEN amount ELSE 0 END), 0) as open_total, COALESCE(SUM(amount), 0) as gross_total, COUNT(*) as invoice_count FROM invoices `); // Monatlicher Umsatz (letzte 12 Monate) const [monthlyRevenue] = await db.query(` SELECT CONVERT(period USING utf8mb4) as period, SUM(CASE WHEN status='paid' THEN amount ELSE 0 END) as paid, SUM(CASE WHEN status='open' THEN amount ELSE 0 END) as open_amount, SUM(amount) as total, COUNT(*) as count FROM invoices GROUP BY CONVERT(period USING utf8mb4) ORDER BY period DESC LIMIT 12 `); // Offene Posten const [openInvoices] = await db.query(` SELECT i.*, m.first_name, m.last_name, m.email, m.phone, t.name as tariff_name FROM invoices i JOIN memberships m ON i.membership_id = m.id LEFT JOIN tariffs t ON m.tariff_id = t.id WHERE i.status = 'open' ORDER BY i.period ASC, m.last_name ASC `); // Rückläufer const [chargebacks] = await db.query(` SELECT c.*, m.first_name, m.last_name, m.email FROM chargebacks c JOIN memberships m ON c.membership_id = m.id ORDER BY c.chargeback_date DESC `); // Mahngebühren const [dunnings] = await db.query(` SELECT d.*, m.first_name, m.last_name, m.email FROM dunning_fees d JOIN memberships m ON d.membership_id = m.id ORDER BY d.issued_date DESC `); // Mahngebühren Summen const [dunningStats] = await db.query(` SELECT COALESCE(SUM(CASE WHEN status='open' THEN amount ELSE 0 END), 0) as open_total, COALESCE(SUM(CASE WHEN status='paid' THEN amount ELSE 0 END), 0) as paid_total, COUNT(CASE WHEN status='open' THEN 1 END) as open_count FROM dunning_fees `); // Rückläufer Summen const [chargebackStats] = await db.query(` SELECT COUNT(*) as total, COUNT(CASE WHEN status='open' THEN 1 END) as open_count, COALESCE(SUM(amount), 0) as total_amount FROM chargebacks `); // Auslaufende Verträge (nächste 3 Monate) const [expiringContracts] = await db.query(` SELECT m.*, t.name as tariff_name, t.price_monthly 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 Mitglieder für Dropdowns const [members] = await db.query(` SELECT m.id, m.first_name, m.last_name FROM memberships m WHERE m.status IN ('active','paused','inactive') ORDER BY m.last_name ASC `); // Offene Rechnungen für Dropdown const [openInvoicesDropdown] = await db.query(` SELECT i.id, i.period, i.amount, m.first_name, m.last_name FROM invoices i JOIN memberships m ON i.membership_id = m.id WHERE i.status = 'open' ORDER BY i.period DESC, m.last_name ASC `); res.render('admin/finance', { admin: req.session.adminUser, dunningFee, totalRevenue: totalRevenue[0], monthlyRevenue: monthlyRevenue.reverse(), // aufsteigend für Chart openInvoices, chargebacks, dunnings, dunningStats: dunningStats[0], chargebackStats: chargebackStats[0], expiringContracts, members, openInvoicesDropdown, success: req.query.success || null, error: req.query.error || null }); } catch (err) { console.error(err); res.redirect('/admin?error=Fehler+in+der+Finanzübersicht:+' + encodeURIComponent(err.message)); } }); // ============================================ // Einstellungen speichern // ============================================ router.post('/settings', requireAdmin, async (req, res) => { const { dunning_fee } = req.body; try { await db.query( "INSERT INTO settings (key_name, value, label) VALUES ('dunning_fee', ?, 'Mahngebühr (€)') ON DUPLICATE KEY UPDATE value = ?", [dunning_fee, dunning_fee] ); res.redirect('/admin/finance?success=Einstellungen+gespeichert'); } catch (err) { res.redirect('/admin/finance?error=Fehler+beim+Speichern'); } }); // ============================================ // Rückläufer eintragen // ============================================ router.post('/chargebacks/add', requireAdmin, async (req, res) => { const { membership_id, invoice_id, period, amount, reason, chargeback_date, notes } = req.body; try { await db.query( 'INSERT INTO chargebacks (membership_id, invoice_id, period, amount, reason, chargeback_date, notes) VALUES (?,?,?,?,?,?,?)', [membership_id, invoice_id || null, period, amount, reason || 'SEPA Rücklastschrift', chargeback_date, notes || null] ); // Rechnung wieder auf offen setzen if (invoice_id) { await db.query("UPDATE invoices SET status='open', paid_at=NULL WHERE id=?", [invoice_id]); } res.redirect('/admin/finance?success=Rückläufer+eingetragen'); } catch (err) { console.error(err); res.redirect('/admin/finance?error=Fehler+beim+Eintragen'); } }); router.post('/chargebacks/:id/resolve', requireAdmin, async (req, res) => { try { await db.query("UPDATE chargebacks SET status='resolved' WHERE id=?", [req.params.id]); res.redirect('/admin/finance?success=Rückläufer+als+erledigt+markiert'); } catch (err) { res.redirect('/admin/finance?error=Fehler'); } }); // CSV Import Rückläufer router.post('/chargebacks/import', requireAdmin, async (req, res) => { const { csv_data } = req.body; if (!csv_data || !csv_data.trim()) return res.redirect('/admin/finance?error=Keine+Daten+eingegeben'); try { const lines = csv_data.trim().split('\n').filter(l => l.trim()); let imported = 0; for (const line of lines) { const cols = line.split(';').map(c => c.trim().replace(/"/g, '')); if (cols.length < 3) continue; const [iban, amount, date, reason] = cols; const cleanIban = iban.replace(/\s/g, ''); // Mitglied anhand IBAN suchen const [members] = await db.query( "SELECT id FROM memberships WHERE REPLACE(iban,' ','') = ?", [cleanIban] ); if (members.length === 0) continue; const now = new Date(); const period = `${now.getFullYear()}-${String(now.getMonth()+1).padStart(2,'0')}`; await db.query( 'INSERT INTO chargebacks (membership_id, period, amount, reason, chargeback_date) VALUES (?,?,?,?,?)', [members[0].id, period, Math.abs(parseFloat(amount.replace(',','.'))), reason || 'SEPA Rücklastschrift', date || new Date().toISOString().split('T')[0]] ); imported++; } res.redirect(`/admin/finance?success=${imported}+Rückläufer+importiert`); } catch (err) { console.error(err); res.redirect('/admin/finance?error=Import+Fehler:+' + encodeURIComponent(err.message)); } }); // ============================================ // Mahngebühren // ============================================ router.post('/dunning/add', requireAdmin, async (req, res) => { const { membership_id, invoice_id, amount, reason, issued_date, notes } = req.body; try { await db.query( 'INSERT INTO dunning_fees (membership_id, invoice_id, amount, reason, issued_date, notes) VALUES (?,?,?,?,?,?)', [membership_id, invoice_id || null, amount, reason || 'Mahngebühr', issued_date, notes || null] ); res.redirect('/admin/finance?success=Mahngebühr+eingetragen'); } catch (err) { res.redirect('/admin/finance?error=Fehler+beim+Eintragen'); } }); router.post('/dunning/:id/paid', requireAdmin, async (req, res) => { try { await db.query("UPDATE dunning_fees SET status='paid', paid_at=NOW() WHERE id=?", [req.params.id]); res.redirect('/admin/finance?success=Mahngebühr+als+bezahlt+markiert'); } catch (err) { res.redirect('/admin/finance?error=Fehler'); } }); router.post('/dunning/:id/cancel', requireAdmin, async (req, res) => { try { await db.query("UPDATE dunning_fees SET status='cancelled' WHERE id=?", [req.params.id]); res.redirect('/admin/finance?success=Mahngebühr+storniert'); } catch (err) { res.redirect('/admin/finance?error=Fehler'); } }); module.exports = router;