267 lines
11 KiB
Plaintext
267 lines
11 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 – Abrechnung</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">
|
||
</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 active">💶 Abrechnung</a>
|
||
<a href="/admin/finance" class="nav-link">📊 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">
|
||
|
||
<!-- Kopfzeile mit Perioden-Auswahl -->
|
||
<div class="billing-header">
|
||
<h1>💶 Abrechnung</h1>
|
||
<form method="GET" action="/admin/billing" class="period-form">
|
||
<input type="month" name="period" value="<%= period %>" class="form-control period-input">
|
||
<button type="submit" class="btn btn-outline">Anzeigen</button>
|
||
</form>
|
||
</div>
|
||
|
||
<% if (success) { %><div class="alert alert-success"><%= success %></div><% } %>
|
||
<% if (error) { %><div class="alert alert-error"><%= error %></div><% } %>
|
||
|
||
<!-- Monatsüberschrift -->
|
||
<h2 class="billing-period-title"><%= periodLabel %></h2>
|
||
|
||
<!-- Stats -->
|
||
<div class="stats-row">
|
||
<div class="stat-card">
|
||
<div class="stat-number"><%= summary.total || 0 %></div>
|
||
<div class="stat-label">Rechnungen gesamt</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-number" style="color:var(--error)"><%= summary.open_count || 0 %></div>
|
||
<div class="stat-label">Offen</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-number" style="color:var(--success)"><%= summary.paid_count || 0 %></div>
|
||
<div class="stat-label">Bezahlt</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-number"><%= summary.total_amount ? Number(summary.total_amount).toFixed(2).replace('.', ',') + ' €' : '0,00 €' %></div>
|
||
<div class="stat-label">Gesamtbetrag</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-number" style="color:var(--error)"><%= summary.open_amount ? Number(summary.open_amount).toFixed(2).replace('.', ',') + ' €' : '0,00 €' %></div>
|
||
<div class="stat-label">Noch offen</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-number" style="color:var(--success)"><%= summary.paid_amount ? Number(summary.paid_amount).toFixed(2).replace('.', ',') + ' €' : '0,00 €' %></div>
|
||
<div class="stat-label">Bereits bezahlt</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Aktionen -->
|
||
<div class="billing-actions">
|
||
<% if (invoices.length === 0) { %>
|
||
<!-- Noch kein Lauf → Vorschau + Lauf starten -->
|
||
<div class="billing-preview-box">
|
||
<div class="preview-info">
|
||
<strong>Bereit für Abrechnungslauf <%= periodLabel %></strong>
|
||
<span><%= eligible.length %> Mitglieder · Voraussichtlich <%= Number(preview_total).toFixed(2).replace('.', ',') %> €</span>
|
||
</div>
|
||
<form method="POST" action="/admin/billing/run"
|
||
onsubmit="return confirm('Abrechnungslauf für <%= periodLabel %> starten? Dieser Vorgang kann nicht rückgängig gemacht werden.')">
|
||
<input type="hidden" name="period" value="<%= period %>">
|
||
<button type="submit" class="btn btn-primary">▶ Abrechnungslauf starten</button>
|
||
</form>
|
||
</div>
|
||
<% } else { %>
|
||
<!-- Lauf bereits durchgeführt → Export & Aktionen -->
|
||
<div class="billing-run-done">
|
||
<div class="billing-action-btns">
|
||
<a href="/admin/billing/export/csv?period=<%= period %>" class="btn btn-outline">
|
||
📥 SEPA CSV exportieren
|
||
</a>
|
||
<% if (summary.open_count > 0) { %>
|
||
<form method="POST" action="/admin/billing/mark-all-paid"
|
||
onsubmit="return confirm('Alle offenen Rechnungen als bezahlt markieren?')">
|
||
<input type="hidden" name="period" value="<%= period %>">
|
||
<button type="submit" class="btn btn-success">✅ Alle als bezahlt markieren</button>
|
||
</form>
|
||
<% } %>
|
||
</div>
|
||
</div>
|
||
<% } %>
|
||
</div>
|
||
|
||
<!-- Rechnungsliste -->
|
||
<% if (invoices.length > 0) { %>
|
||
<div class="table-wrap">
|
||
<table class="admin-table">
|
||
<thead>
|
||
<tr>
|
||
<th>Nr.</th>
|
||
<th>Mitglied</th>
|
||
<th>Tarif</th>
|
||
<th>Betrag</th>
|
||
<th>IBAN</th>
|
||
<th>Status</th>
|
||
<th>Aktionen</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<% invoices.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 class="amount-cell">
|
||
<strong><%= Number(inv.amount).toFixed(2).replace('.', ',') %> €</strong>
|
||
</td>
|
||
<td class="iban-cell">
|
||
<%= inv.iban ? inv.iban.replace(/(.{4})/g, '$1 ').trim() : '–' %>
|
||
</td>
|
||
<td>
|
||
<span class="invoice-status <%= inv.status %>">
|
||
<%= inv.status === 'paid' ? '✅ Bezahlt' : inv.status === 'open' ? '🔴 Offen' : '❌ Storniert' %>
|
||
</span>
|
||
<% if (inv.paid_at) { %>
|
||
<br><small class="text-muted"><%= new Date(inv.paid_at).toLocaleDateString('de-DE') %></small>
|
||
<% } %>
|
||
</td>
|
||
<td>
|
||
<div class="invoice-actions">
|
||
<% if (inv.status !== 'cancelled') { %>
|
||
<!-- Normale Rechnung PDF -->
|
||
<a href="/admin/billing/export/pdf/<%= inv.id %>"
|
||
class="btn btn-sm btn-outline" target="_blank">
|
||
📄 Rechnung
|
||
</a>
|
||
<% } else { %>
|
||
<!-- Storno PDF -->
|
||
<a href="/admin/billing/export/storno-pdf/<%= inv.id %>"
|
||
class="btn btn-sm btn-storno" target="_blank">
|
||
🚫 Storno-PDF
|
||
</a>
|
||
<!-- Neue Rechnung ausstellen -->
|
||
<form method="POST" action="/admin/billing/invoices/<%= inv.id %>/reissue" style="display:inline"
|
||
onsubmit="return confirm('Neue Rechnung für diesen Posten ausstellen?')">
|
||
<button type="submit" class="btn btn-sm btn-primary">🔄 Neue Rechnung</button>
|
||
</form>
|
||
<% } %>
|
||
<% if (inv.status === 'open') { %>
|
||
<form method="POST" action="/admin/billing/invoices/<%= inv.id %>/paid" style="display:inline">
|
||
<input type="hidden" name="period" value="<%= period %>">
|
||
<button type="submit" class="btn btn-sm btn-success">✅ Bezahlt</button>
|
||
</form>
|
||
<% } %>
|
||
<% if (inv.status !== 'cancelled') { %>
|
||
<form method="POST" action="/admin/billing/invoices/<%= inv.id %>/cancel" style="display:inline"
|
||
onsubmit="return confirm('Rechnung PF24-<%= String(inv.id).padStart(6,'0') %> wirklich stornieren?')">
|
||
<input type="hidden" name="period" value="<%= period %>">
|
||
<button type="submit" class="btn btn-sm btn-danger">🚫 Stornieren</button>
|
||
</form>
|
||
<% } %>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
<% }) %>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<% } else if (eligible.length > 0) { %>
|
||
<!-- Vorschau der Mitglieder -->
|
||
<div class="preview-table-wrap">
|
||
<h3 class="preview-title">Vorschau – wird abgerechnet</h3>
|
||
<table class="admin-table">
|
||
<thead>
|
||
<tr>
|
||
<th>Mitglied</th>
|
||
<th>Tarif</th>
|
||
<th>Voraussichtlicher Betrag</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<% eligible.forEach(m => { %>
|
||
<tr>
|
||
<td><%= m.last_name %>, <%= m.first_name %></td>
|
||
<td><%= m.tariff_name %></td>
|
||
<td>
|
||
<strong><%= Number(m.agreed_price || m.price_monthly).toFixed(2).replace('.', ',') %> €</strong>
|
||
<% if (m.agreed_price && Number(m.agreed_price) !== Number(m.price_monthly)) { %>
|
||
<br><small class="text-muted" title="Aktueller Tarif-Preis">Tarif: <%= Number(m.price_monthly).toFixed(2).replace('.', ',') %> €</small>
|
||
<% } %>
|
||
</td>
|
||
</tr>
|
||
<% }) %>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<% } else { %>
|
||
<div class="no-data-card">Keine Mitglieder für diesen Zeitraum.</div>
|
||
<% } %>
|
||
|
||
<!-- Letzte Abrechnungsläufe -->
|
||
<% if (runs.length > 0) { %>
|
||
<div class="runs-section">
|
||
<h3>Letzte Abrechnungsläufe</h3>
|
||
<div class="runs-scroll-wrap">
|
||
<table class="admin-table">
|
||
<thead>
|
||
<tr>
|
||
<th>Periode</th>
|
||
<th>Datum</th>
|
||
<th>Rechnungen</th>
|
||
<th>Gesamtbetrag</th>
|
||
<th>Erstellt von</th>
|
||
<th>Aktion</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<% runs.forEach(run => { %>
|
||
<tr>
|
||
<td><strong><%= run.period %></strong></td>
|
||
<td><%= new Date(run.created_at).toLocaleDateString('de-DE') %></td>
|
||
<td><%= run.invoice_count %></td>
|
||
<td><%= Number(run.total_amount).toFixed(2).replace('.', ',') %> €</td>
|
||
<td><%= run.created_by || '–' %></td>
|
||
<td>
|
||
<div style="display:flex;gap:6px">
|
||
<a href="/admin/billing?period=<%= run.period %>" class="btn btn-sm btn-outline">
|
||
Anzeigen
|
||
</a>
|
||
<a href="/admin/billing/export/csv?period=<%= run.period %>" class="btn btn-sm btn-outline" title="SEPA CSV">
|
||
📥 CSV
|
||
</a>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
<% }) %>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
<% } %>
|
||
|
||
</main>
|
||
</div>
|
||
</body>
|
||
</html>
|