678 lines
29 KiB
Plaintext
678 lines
29 KiB
Plaintext
<!DOCTYPE html>
|
||
<html lang="de">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>PlusFit24 – Finanzübersicht</title>
|
||
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;700;800&display=swap" rel="stylesheet">
|
||
<link rel="stylesheet" href="/css/style.css">
|
||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
||
</head>
|
||
<body class="admin-body">
|
||
<div class="admin-layout">
|
||
|
||
<aside class="admin-sidebar">
|
||
<div class="logo admin-logo">Plusfit<span>24</span></div>
|
||
<nav class="admin-nav">
|
||
<a href="/admin" class="nav-link">📋 Tarife</a>
|
||
<a href="/admin#kategorien" class="nav-link">🏷️ Kategorien</a>
|
||
<a href="/admin#mitglieder" class="nav-link">👥 Mitglieder</a>
|
||
<a href="/admin/contracts" class="nav-link">📑 Verträge</a>
|
||
<a href="/admin/billing" class="nav-link">💶 Abrechnung</a>
|
||
<a href="/admin/finance" class="nav-link active">📊 Finanzen</a>
|
||
<a href="/admin/mailing" class="nav-link">📧 Mailing</a>
|
||
<a href="/dokumentation/handbuch.docx" class="nav-link" target="_blank">❓ Hilfe</a>
|
||
<a href="/admin#einstellungen" class="nav-link">⚙️ Einstellungen</a>
|
||
</nav>
|
||
<div class="sidebar-footer">
|
||
<span>👤 <%= admin %></span>
|
||
<a href="/admin/logout" class="logout-link">Abmelden</a>
|
||
</div>
|
||
</aside>
|
||
|
||
<main class="admin-main">
|
||
<h1 class="finance-title">📊 Finanzübersicht</h1>
|
||
|
||
<% if (success) { %><div class="alert alert-success"><%= success %></div><% } %>
|
||
<% if (error) { %><div class="alert alert-error"><%= error %></div><% } %>
|
||
|
||
<!-- ===== KPI KARTEN ===== -->
|
||
<div class="finance-kpi-grid">
|
||
<div class="kpi-card kpi-blue">
|
||
<div class="kpi-label">Gesamtumsatz (bezahlt)</div>
|
||
<div class="kpi-value"><%= Number(totalRevenue.paid_total).toFixed(2).replace('.', ',') %> €</div>
|
||
<div class="kpi-sub"><%= totalRevenue.invoice_count %> Rechnungen gesamt</div>
|
||
</div>
|
||
<div class="kpi-card kpi-red">
|
||
<div class="kpi-label">Offene Posten</div>
|
||
<div class="kpi-value"><%= Number(totalRevenue.open_total).toFixed(2).replace('.', ',') %> €</div>
|
||
<div class="kpi-sub"><%= openInvoices.length %> offene Rechnungen</div>
|
||
</div>
|
||
<div class="kpi-card kpi-orange">
|
||
<div class="kpi-label">Rückläufer (offen)</div>
|
||
<div class="kpi-value"><%= chargebackStats.open_count %></div>
|
||
<div class="kpi-sub"><%= Number(chargebackStats.total_amount).toFixed(2).replace('.', ',') %> € gesamt</div>
|
||
</div>
|
||
<div class="kpi-card kpi-purple">
|
||
<div class="kpi-label">Mahngebühren (offen)</div>
|
||
<div class="kpi-value"><%= Number(dunningStats.open_total).toFixed(2).replace('.', ',') %> €</div>
|
||
<div class="kpi-sub"><%= dunningStats.open_count %> offene Mahnungen</div>
|
||
</div>
|
||
<div class="kpi-card kpi-green">
|
||
<div class="kpi-label">Mahngebühren (bezahlt)</div>
|
||
<div class="kpi-value"><%= Number(dunningStats.paid_total).toFixed(2).replace('.', ',') %> €</div>
|
||
<div class="kpi-sub">Aktueller Satz: <%= Number(dunningFee).toFixed(2).replace('.', ',') %> €</div>
|
||
</div>
|
||
<div class="kpi-card kpi-yellow">
|
||
<div class="kpi-label">Auslaufende Verträge</div>
|
||
<div class="kpi-value"><%= expiringContracts.length %></div>
|
||
<div class="kpi-sub">In den nächsten 3 Monaten</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ===== TABS ===== -->
|
||
<div class="finance-tabs">
|
||
<button class="ftab active" onclick="showTab('chart', this)">📈 Umsatzverlauf</button>
|
||
<button class="ftab" onclick="showTab('open', this)">🔴 Offene Posten</button>
|
||
<button class="ftab" onclick="showTab('chargebacks', this)">↩️ Rückläufer</button>
|
||
<button class="ftab" onclick="showTab('dunning', this)">📬 Mahngebühren</button>
|
||
<button class="ftab" onclick="showTab('expiring', this)">⏳ Auslaufende Verträge</button>
|
||
<button class="ftab" onclick="showTab('cancelled', this)">🚫 Storniert</button>
|
||
<button class="ftab" onclick="showTab('settings', this)">⚙️ Einstellungen</button>
|
||
</div>
|
||
|
||
<!-- ===== TAB: UMSATZVERLAUF ===== -->
|
||
<div class="ftab-content active" id="tab-chart">
|
||
<div class="finance-card">
|
||
<h3>Monatlicher Umsatz (letzte 12 Monate)</h3>
|
||
<canvas id="revenueChart" height="80"></canvas>
|
||
</div>
|
||
<div class="table-wrap" style="margin-top:16px">
|
||
<table class="admin-table">
|
||
<thead><tr><th>Periode</th><th>Rechnungen</th><th>Bezahlt</th><th>Offen</th><th>Gesamt</th></tr></thead>
|
||
<tbody>
|
||
<% monthlyRevenue.slice().reverse().forEach(m => { %>
|
||
<tr>
|
||
<td><strong><%= m.period %></strong></td>
|
||
<td><%= m.count %></td>
|
||
<td style="color:var(--success)"><%= Number(m.paid).toFixed(2).replace('.', ',') %> €</td>
|
||
<td style="color:var(--error)"><%= Number(m.open_amount).toFixed(2).replace('.', ',') %> €</td>
|
||
<td><strong><%= Number(m.total).toFixed(2).replace('.', ',') %> €</strong></td>
|
||
</tr>
|
||
<% }) %>
|
||
<% if (monthlyRevenue.length === 0) { %>
|
||
<tr><td colspan="5" class="no-data">Noch keine Abrechnungsdaten.</td></tr>
|
||
<% } %>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ===== TAB: OFFENE POSTEN ===== -->
|
||
<div class="ftab-content" id="tab-open">
|
||
<div class="finance-card">
|
||
<div class="section-header">
|
||
<h3>Offene Posten (<%= openInvoices.length %>)</h3>
|
||
</div>
|
||
<% if (openInvoices.length === 0) { %>
|
||
<p class="karte-empty">✅ Keine offenen Posten!</p>
|
||
<% } else { %>
|
||
<div class="table-wrap">
|
||
<table class="admin-table">
|
||
<thead><tr><th>Mitglied</th><th>Tarif</th><th>Periode</th><th>Betrag</th><th>Rechnung Nr.</th><th>Aktion</th></tr></thead>
|
||
<tbody>
|
||
<% openInvoices.forEach(inv => { %>
|
||
<tr>
|
||
<td>
|
||
<strong><%= inv.last_name %>, <%= inv.first_name %></strong><br>
|
||
<small class="text-muted"><%= inv.email %></small>
|
||
</td>
|
||
<td><%= inv.tariff_name || '–' %></td>
|
||
<td><%= inv.period %></td>
|
||
<td style="color:var(--error)"><strong><%= Number(inv.amount).toFixed(2).replace('.', ',') %> €</strong></td>
|
||
<td class="invoice-nr">PF24-<%= String(inv.id).padStart(6,'0') %></td>
|
||
<td>
|
||
<a href="/admin/billing?period=<%= inv.period %>" class="btn btn-sm btn-outline">
|
||
Zur Abrechnung
|
||
</a>
|
||
</td>
|
||
</tr>
|
||
<% }) %>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<% } %>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ===== TAB: RÜCKLÄUFER ===== -->
|
||
<div class="ftab-content" id="tab-chargebacks">
|
||
<div class="finance-card">
|
||
<div class="section-header">
|
||
<h3>Rückläufer</h3>
|
||
<div style="display:flex;gap:8px;flex-wrap:wrap">
|
||
<button class="btn btn-warning btn-sm" onclick="toggleModal('allDunningModal')">📬 Alle mahnen</button>
|
||
<button class="btn btn-outline btn-sm" onclick="toggleModal('addChargebackModal')">+ Manuell eintragen</button>
|
||
<button class="btn btn-outline btn-sm" onclick="toggleModal('importChargebackModal')">📥 CSV Import</button>
|
||
<a href="/admin/finance/chargebacks/sepa-export" class="btn btn-primary btn-sm">📥 SEPA Nachforderung</a>
|
||
</div>
|
||
</div>
|
||
<% if (chargebacks.length === 0) { %>
|
||
<p class="karte-empty">Keine Rückläufer vorhanden.</p>
|
||
<% } else { %>
|
||
<div class="table-wrap">
|
||
<table class="admin-table">
|
||
<thead><tr><th>Datum</th><th>Mitglied</th><th>Periode</th><th>Betrag</th><th>Grund</th><th>Status</th><th>Aktion</th></tr></thead>
|
||
<tbody>
|
||
<% chargebacks.forEach(c => { %>
|
||
<tr>
|
||
<td><%= new Date(c.chargeback_date).toLocaleDateString('de-DE') %></td>
|
||
<td><strong><%= c.last_name %>, <%= c.first_name %></strong></td>
|
||
<td><%= c.period %></td>
|
||
<td style="color:var(--error)"><strong><%= Number(c.amount).toFixed(2).replace('.', ',') %> €</strong></td>
|
||
<td><%= c.reason || '–' %></td>
|
||
<td>
|
||
<span class="invoice-status <%= c.status === 'resolved' ? 'paid' : 'open' %>">
|
||
<%= c.status === 'resolved' ? '✅ Erledigt' : '🔴 Offen' %>
|
||
</span>
|
||
</td>
|
||
<td>
|
||
<div style="display:flex;gap:6px;flex-wrap:wrap">
|
||
<% if (c.status === 'open') { %>
|
||
<button type="button" class="btn btn-sm btn-warning"
|
||
onclick="openDunningModal(<%= c.id %>, '<%= c.last_name %>, <%= c.first_name %>', <%= c.amount %>, '<%= c.period %>');event.stopPropagation()">
|
||
📬 Mahnen
|
||
</button>
|
||
<form method="POST" action="/admin/finance/chargebacks/<%= c.id %>/resolve" style="display:inline">
|
||
<button type="submit" class="btn btn-sm btn-success">✅ Erledigt</button>
|
||
</form>
|
||
<% } %>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
<% }) %>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<% } %>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ===== TAB: MAHNGEBÜHREN ===== -->
|
||
<div class="ftab-content" id="tab-dunning">
|
||
<div class="finance-card">
|
||
<div class="section-header">
|
||
<h3>Mahngebühren</h3>
|
||
<button class="btn btn-primary btn-sm" onclick="toggleModal('addDunningModal')">+ Mahngebühr eintragen</button>
|
||
</div>
|
||
<% if (dunnings.length === 0) { %>
|
||
<p class="karte-empty">Keine Mahngebühren eingetragen.</p>
|
||
<% } else { %>
|
||
<div class="table-wrap">
|
||
<table class="admin-table">
|
||
<thead><tr><th>Datum</th><th>Mitglied</th><th>Betrag</th><th>Grund</th><th>Status</th><th>Aktion</th></tr></thead>
|
||
<tbody>
|
||
<% dunnings.forEach(d => { %>
|
||
<tr>
|
||
<td><%= new Date(d.issued_date).toLocaleDateString('de-DE') %></td>
|
||
<td><strong><%= d.last_name %>, <%= d.first_name %></strong></td>
|
||
<td><strong><%= Number(d.amount).toFixed(2).replace('.', ',') %> €</strong></td>
|
||
<td><%= d.reason %></td>
|
||
<td>
|
||
<span class="invoice-status <%= d.status === 'paid' ? 'paid' : d.status === 'cancelled' ? 'cancelled' : 'open' %>">
|
||
<%= d.status === 'paid' ? '✅ Bezahlt' : d.status === 'cancelled' ? '❌ Storniert' : '🔴 Offen' %>
|
||
</span>
|
||
<% if (d.paid_at) { %><br><small class="text-muted"><%= new Date(d.paid_at).toLocaleDateString('de-DE') %></small><% } %>
|
||
</td>
|
||
<td>
|
||
<div style="display:flex;gap:6px">
|
||
<% if (d.status === 'open') { %>
|
||
<form method="POST" action="/admin/finance/dunning/<%= d.id %>/paid" style="display:inline">
|
||
<button type="submit" class="btn btn-sm btn-success">✅</button>
|
||
</form>
|
||
<form method="POST" action="/admin/finance/dunning/<%= d.id %>/cancel" style="display:inline"
|
||
onsubmit="return confirm('Mahngebühr stornieren?')">
|
||
<button type="submit" class="btn btn-sm btn-danger">✕</button>
|
||
</form>
|
||
<% } %>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
<% }) %>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<% } %>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ===== TAB: AUSLAUFENDE VERTRÄGE ===== -->
|
||
<div class="ftab-content" id="tab-expiring">
|
||
<div class="finance-card">
|
||
<h3>Auslaufende Verträge – nächste 3 Monate (<%= expiringContracts.length %>)</h3>
|
||
<% if (expiringContracts.length === 0) { %>
|
||
<p class="karte-empty">Keine auslaufenden Verträge in den nächsten 3 Monaten.</p>
|
||
<% } else { %>
|
||
<div class="table-wrap">
|
||
<table class="admin-table">
|
||
<thead><tr><th>Mitglied</th><th>Tarif</th><th>Vertragsende</th><th>Restlaufzeit</th><th>Monatsbeitrag</th><th>Aktion</th></tr></thead>
|
||
<tbody>
|
||
<% expiringContracts.forEach(m => { %>
|
||
<%
|
||
const endDate = new Date(m.effective_end);
|
||
const today = new Date();
|
||
const diffDays = Math.ceil((endDate - today) / (1000*60*60*24));
|
||
const urgency = diffDays <= 30 ? 'urgent' : diffDays <= 60 ? 'warning' : 'normal';
|
||
%>
|
||
<tr>
|
||
<td>
|
||
<strong><%= m.last_name %>, <%= m.first_name %></strong><br>
|
||
<small class="text-muted"><%= m.email %></small>
|
||
</td>
|
||
<td><%= m.tariff_name %></td>
|
||
<td><strong><%= endDate.toLocaleDateString('de-DE') %></strong></td>
|
||
<td>
|
||
<span class="expiry-badge expiry-<%= urgency %>">
|
||
noch <%= diffDays %> Tage
|
||
</span>
|
||
</td>
|
||
<td><%= Number(m.price_monthly).toFixed(2).replace('.', ',') %> €</td>
|
||
<td>
|
||
<div style="display:flex;gap:6px">
|
||
<a href="/admin/members/<%= m.id %>" class="btn btn-sm btn-outline">Karteikarte</a>
|
||
<form method="POST" action="/admin/send-renewal/<%= m.id %>" style="display:inline">
|
||
<button type="submit" class="btn btn-sm btn-primary" title="Verlängerungs-E-Mail senden">📧 E-Mail</button>
|
||
</form>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
<% }) %>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<% } %>
|
||
</div>
|
||
</div>
|
||
|
||
|
||
<!-- ===== TAB: STORNIERT ===== -->
|
||
<div class="ftab-content" id="tab-cancelled">
|
||
<div class="finance-card">
|
||
<h3>Stornierte Rechnungen (<%= cancelledInvoices.length %>)</h3>
|
||
<% if (cancelledInvoices.length === 0) { %>
|
||
<p class="karte-empty">Keine stornierten Rechnungen vorhanden.</p>
|
||
<% } else { %>
|
||
<div class="table-wrap">
|
||
<table class="admin-table">
|
||
<thead>
|
||
<tr>
|
||
<th>Nr.</th>
|
||
<th>Mitglied</th>
|
||
<th>Tarif</th>
|
||
<th>Periode</th>
|
||
<th>Betrag</th>
|
||
<th>Storniert am</th>
|
||
<th>Aktion</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<% cancelledInvoices.forEach(inv => { %>
|
||
<tr>
|
||
<td class="invoice-nr">PF24-<%= String(inv.id).padStart(6,'0') %></td>
|
||
<td>
|
||
<strong><%= inv.last_name %>, <%= inv.first_name %></strong><br>
|
||
<small class="text-muted"><%= inv.email %></small>
|
||
</td>
|
||
<td><%= inv.tariff_name || '–' %></td>
|
||
<td><%= inv.period %></td>
|
||
<td><span style="text-decoration:line-through;color:var(--text-muted)">
|
||
<%= Number(inv.amount).toFixed(2).replace('.', ',') %> €
|
||
</span></td>
|
||
<td><%= new Date(inv.created_at).toLocaleDateString('de-DE') %></td>
|
||
<td>
|
||
<a href="/admin/billing?period=<%= inv.period %>" class="btn btn-sm btn-outline">
|
||
Zur Abrechnung
|
||
</a>
|
||
</td>
|
||
</tr>
|
||
<% }) %>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<% } %>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ===== TAB: EINSTELLUNGEN ===== -->
|
||
<div class="ftab-content" id="tab-settings">
|
||
<div class="finance-card" style="max-width:400px">
|
||
<h3>Finanz-Einstellungen</h3>
|
||
<form method="POST" action="/admin/finance/settings">
|
||
<div class="form-group" style="margin-top:16px">
|
||
<label>Mahngebühr (€)</label>
|
||
<div class="input-wrap">
|
||
<input type="number" name="dunning_fee" step="0.01" min="0"
|
||
value="<%= Number(dunningFee).toFixed(2) %>" class="form-control">
|
||
</div>
|
||
</div>
|
||
<button type="submit" class="btn btn-primary" style="margin-top:12px">💾 Speichern</button>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
</main>
|
||
</div>
|
||
|
||
<!-- Modal: Rückläufer manuell -->
|
||
<div class="modal-overlay hidden" id="addChargebackModal">
|
||
<div class="modal">
|
||
<div class="modal-header">
|
||
<h3>Rückläufer eintragen</h3>
|
||
<button onclick="toggleModal('addChargebackModal')" class="modal-close">✕</button>
|
||
</div>
|
||
<form method="POST" action="/admin/finance/chargebacks/add">
|
||
<div class="form-group">
|
||
<label>Mitglied *</label>
|
||
<select name="membership_id" class="form-control" required onchange="loadMemberInvoices(this.value)">
|
||
<option value="">– Mitglied wählen –</option>
|
||
<% members.forEach(m => { %>
|
||
<option value="<%= m.id %>"><%= m.last_name %>, <%= m.first_name %></option>
|
||
<% }) %>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Rechnung (optional)</label>
|
||
<select name="invoice_id" class="form-control" id="invoiceSelect">
|
||
<option value="">– Rechnung wählen –</option>
|
||
<% openInvoicesDropdown.forEach(i => { %>
|
||
<option value="<%= i.id %>" data-member="<%= i.membership_id %>">
|
||
<%= i.period %> – <%= Number(i.amount).toFixed(2).replace('.', ',') %> € (<%= i.last_name %>, <%= i.first_name %>)
|
||
</option>
|
||
<% }) %>
|
||
</select>
|
||
</div>
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label>Periode *</label>
|
||
<input type="month" name="period" class="form-control" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Betrag (€) *</label>
|
||
<input type="number" name="amount" step="0.01" min="0" class="form-control" required>
|
||
</div>
|
||
</div>
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label>Datum *</label>
|
||
<input type="date" name="chargeback_date" class="form-control" required
|
||
value="<%= new Date().toISOString().split('T')[0] %>">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Grund</label>
|
||
<input type="text" name="reason" class="form-control" value="SEPA Rücklastschrift">
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Notizen</label>
|
||
<textarea name="notes" class="form-control" rows="2"></textarea>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button type="button" onclick="toggleModal('addChargebackModal')" class="btn btn-outline">Abbrechen</button>
|
||
<button type="submit" class="btn btn-primary">Eintragen</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Modal: CSV Import Rückläufer -->
|
||
<div class="modal-overlay hidden" id="importChargebackModal">
|
||
<div class="modal">
|
||
<div class="modal-header">
|
||
<h3>Rückläufer CSV Import</h3>
|
||
<button onclick="toggleModal('importChargebackModal')" class="modal-close">✕</button>
|
||
</div>
|
||
<form method="POST" action="/admin/finance/chargebacks/import"
|
||
enctype="multipart/form-data">
|
||
<p style="font-size:0.8rem;color:var(--text-muted);margin-bottom:16px">
|
||
Format pro Zeile: <code>IBAN;Betrag;Datum;Grund</code><br>
|
||
Beispiel: <code>DE89370400440532013000;29,95;2026-04-05;Rücklastschrift</code>
|
||
</p>
|
||
|
||
<!-- Option 1: Datei -->
|
||
<div class="form-group">
|
||
<label>📁 CSV-Datei hochladen</label>
|
||
<div class="file-upload-wrap">
|
||
<input type="file" name="csv_file" id="csvFile" accept=".csv,.txt"
|
||
onchange="showFileName(this)">
|
||
<label for="csvFile" class="file-upload-label">
|
||
<span id="fileNameDisplay">Datei auswählen...</span>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="import-divider"><span>oder</span></div>
|
||
|
||
<!-- Option 2: Textfeld -->
|
||
<div class="form-group">
|
||
<label>📋 Daten einfügen</label>
|
||
<textarea name="csv_data" class="form-control" rows="5"
|
||
placeholder="IBAN;Betrag;Datum;Grund DE89370400440532013000;29,95;2026-04-05;Rücklastschrift"></textarea>
|
||
</div>
|
||
|
||
<div class="modal-footer">
|
||
<button type="button" onclick="toggleModal('importChargebackModal')" class="btn btn-outline">Abbrechen</button>
|
||
<button type="submit" class="btn btn-primary">📥 Importieren</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Modal: Mahngebühr eintragen -->
|
||
<div class="modal-overlay hidden" id="addDunningModal">
|
||
<div class="modal">
|
||
<div class="modal-header">
|
||
<h3>Mahngebühr eintragen</h3>
|
||
<button onclick="toggleModal('addDunningModal')" class="modal-close">✕</button>
|
||
</div>
|
||
<form method="POST" action="/admin/finance/dunning/add">
|
||
<div class="form-group">
|
||
<label>Mitglied *</label>
|
||
<select name="membership_id" class="form-control" required>
|
||
<option value="">– Mitglied wählen –</option>
|
||
<% members.forEach(m => { %>
|
||
<option value="<%= m.id %>"><%= m.last_name %>, <%= m.first_name %></option>
|
||
<% }) %>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Rechnung (optional)</label>
|
||
<select name="invoice_id" class="form-control">
|
||
<option value="">– Rechnung wählen –</option>
|
||
<% openInvoicesDropdown.forEach(i => { %>
|
||
<option value="<%= i.id %>">
|
||
<%= i.period %> – <%= Number(i.amount).toFixed(2).replace('.', ',') %> € (<%= i.last_name %>, <%= i.first_name %>)
|
||
</option>
|
||
<% }) %>
|
||
</select>
|
||
</div>
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label>Betrag (€) *</label>
|
||
<input type="number" name="amount" step="0.01" min="0"
|
||
value="<%= Number(dunningFee).toFixed(2) %>" class="form-control" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Datum *</label>
|
||
<input type="date" name="issued_date" class="form-control" required
|
||
value="<%= new Date().toISOString().split('T')[0] %>">
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Grund</label>
|
||
<input type="text" name="reason" class="form-control" value="Mahngebühr">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Notizen</label>
|
||
<textarea name="notes" class="form-control" rows="2"></textarea>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button type="button" onclick="toggleModal('addDunningModal')" class="btn btn-outline">Abbrechen</button>
|
||
<button type="submit" class="btn btn-primary">Eintragen</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Modal: Einzelne Mahngebühr für Rückläufer -->
|
||
<div class="modal-overlay hidden" id="singleDunningModal">
|
||
<div class="modal">
|
||
<div class="modal-header">
|
||
<h3>Mahngebühr zuweisen</h3>
|
||
<button onclick="toggleModal('singleDunningModal')" class="modal-close">✕</button>
|
||
</div>
|
||
<form method="POST" action="/admin/finance/dunning/add-from-chargeback">
|
||
<input type="hidden" name="chargeback_id" id="dunning_membership_id">
|
||
<input type="hidden" name="invoice_id" value="">
|
||
<div class="form-group">
|
||
<label>Mitglied</label>
|
||
<input type="text" id="dunning_member_name" class="form-control" disabled>
|
||
</div>
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label>Mahngebühr (€) *</label>
|
||
<input type="number" name="amount" id="dunning_amount" step="0.01" min="0"
|
||
class="form-control" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Datum *</label>
|
||
<input type="date" name="issued_date" class="form-control" required
|
||
value="<%= new Date().toISOString().split('T')[0] %>">
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Grund</label>
|
||
<input type="text" name="reason" class="form-control" value="Mahngebühr Rücklastschrift">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Notizen</label>
|
||
<textarea name="notes" class="form-control" rows="2"></textarea>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button type="button" onclick="toggleModal('singleDunningModal')" class="btn btn-outline">Abbrechen</button>
|
||
<button type="submit" class="btn btn-primary">📬 Mahngebühr eintragen</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Modal: Alle offenen Rückläufer mahnen -->
|
||
<div class="modal-overlay hidden" id="allDunningModal">
|
||
<div class="modal">
|
||
<div class="modal-header">
|
||
<h3>Alle offenen Rückläufer mahnen</h3>
|
||
<button onclick="toggleModal('allDunningModal')" class="modal-close">✕</button>
|
||
</div>
|
||
<form method="POST" action="/admin/finance/chargebacks/dunning-all">
|
||
<p style="font-size:0.9rem;margin-bottom:16px;color:var(--text-muted)">
|
||
Trägt für alle offenen Rückläufer automatisch eine Mahngebühr ein.
|
||
</p>
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label>Mahngebühr pro Rückläufer (€) *</label>
|
||
<input type="number" name="amount" step="0.01" min="0"
|
||
value="<%= Number(dunningFee).toFixed(2) %>" class="form-control" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Datum *</label>
|
||
<input type="date" name="issued_date" class="form-control" required
|
||
value="<%= new Date().toISOString().split('T')[0] %>">
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Grund</label>
|
||
<input type="text" name="reason" class="form-control" value="Mahngebühr Rücklastschrift">
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button type="button" onclick="toggleModal('allDunningModal')" class="btn btn-outline">Abbrechen</button>
|
||
<button type="submit" class="btn btn-warning"
|
||
onclick="return confirm('Für alle offenen Rückläufer eine Mahngebühr eintragen?')">
|
||
📬 Alle mahnen
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
|
||
<script>
|
||
const dunningDefaultFee = <%= Number(dunningFee).toFixed(2) %>;
|
||
// Chart
|
||
const ctx = document.getElementById('revenueChart').getContext('2d');
|
||
const chartData = <%- JSON.stringify(monthlyRevenue) %>;
|
||
new Chart(ctx, {
|
||
type: 'bar',
|
||
data: {
|
||
labels: chartData.map(m => m.period),
|
||
datasets: [
|
||
{
|
||
label: 'Bezahlt (€)',
|
||
data: chartData.map(m => parseFloat(m.paid) || 0),
|
||
backgroundColor: 'rgba(22, 163, 74, 0.7)',
|
||
borderRadius: 6
|
||
},
|
||
{
|
||
label: 'Offen (€)',
|
||
data: chartData.map(m => parseFloat(m.open_amount) || 0),
|
||
backgroundColor: 'rgba(220, 38, 38, 0.5)',
|
||
borderRadius: 6
|
||
}
|
||
]
|
||
},
|
||
options: {
|
||
responsive: true,
|
||
plugins: { legend: { position: 'top' } },
|
||
scales: { y: { beginAtZero: true, ticks: { callback: v => v + ' €' } } }
|
||
}
|
||
});
|
||
|
||
// Tabs
|
||
const tabMap = {
|
||
chart: 0, open: 1, chargebacks: 2, dunning: 3, expiring: 4, cancelled: 5, settings: 6
|
||
};
|
||
|
||
function showTab(name, el) {
|
||
document.querySelectorAll('.ftab-content').forEach(t => t.classList.remove('active'));
|
||
document.querySelectorAll('.ftab').forEach(t => t.classList.remove('active'));
|
||
document.getElementById('tab-' + name).classList.add('active');
|
||
const btn = el || document.querySelectorAll('.ftab')[tabMap[name]];
|
||
if (btn) btn.classList.add('active');
|
||
history.replaceState(null, '', '#' + name);
|
||
}
|
||
|
||
// Beim Laden Hash auswerten
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
const hash = window.location.hash.replace('#', '');
|
||
if (hash && document.getElementById('tab-' + hash)) {
|
||
showTab(hash);
|
||
}
|
||
});
|
||
|
||
function toggleModal(id) {
|
||
document.getElementById(id).classList.toggle('hidden');
|
||
}
|
||
|
||
function showFileName(input) {
|
||
const display = document.getElementById('fileNameDisplay');
|
||
display.textContent = input.files.length > 0 ? input.files[0].name : 'Datei auswählen...';
|
||
}
|
||
|
||
function openDunningModal(chargebackId, memberName, amount, period) {
|
||
document.getElementById('dunning_member_name').value = memberName;
|
||
document.getElementById('dunning_amount').value = dunningDefaultFee;
|
||
document.getElementById('dunning_membership_id').value = chargebackId;
|
||
toggleModal('singleDunningModal');
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|