299 lines
12 KiB
Plaintext
299 lines
12 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 – Verträge</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 active">📑 Verträge</a>
|
||
<a href="/admin/billing" class="nav-link">💶 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">
|
||
<h1 class="finance-title">📑 Verträge</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">Gesamt Verträge</div>
|
||
<div class="kpi-value"><%= totals.total || 0 %></div>
|
||
</div>
|
||
<div class="kpi-card kpi-green">
|
||
<div class="kpi-label">Aktiv</div>
|
||
<div class="kpi-value"><%= totals.active || 0 %></div>
|
||
</div>
|
||
<div class="kpi-card kpi-orange">
|
||
<div class="kpi-label">Pausiert</div>
|
||
<div class="kpi-value"><%= totals.paused || 0 %></div>
|
||
</div>
|
||
<div class="kpi-card kpi-red">
|
||
<div class="kpi-label">Inaktiv</div>
|
||
<div class="kpi-value"><%= totals.inactive || 0 %></div>
|
||
</div>
|
||
<div class="kpi-card kpi-purple">
|
||
<div class="kpi-label">Minderjährige</div>
|
||
<div class="kpi-value"><%= totals.minors || 0 %></div>
|
||
</div>
|
||
<div class="kpi-card kpi-yellow">
|
||
<div class="kpi-label">Monatl. Umsatz (aktiv)</div>
|
||
<div class="kpi-value" style="font-size:1.2rem"><%= Number(totals.total_monthly||0).toFixed(2).replace('.',',') %> €</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Tabs -->
|
||
<div class="finance-tabs">
|
||
<button class="ftab active" onclick="showTab('categories', this)">🏷️ Nach Kategorie</button>
|
||
<button class="ftab" onclick="showTab('tariffs', this)">📋 Nach Tarif</button>
|
||
<button class="ftab" onclick="showTab('expiring', this)">
|
||
⏳ Auslaufend
|
||
<% if (expiring.length > 0) { %>
|
||
<span class="expiry-badge expiry-<%= expiring.some(e => { const d=Math.ceil((new Date(e.effective_end)-new Date())/(864e5)); return d<=30; }) ? 'urgent' : 'warning' %>" style="margin-left:6px"><%= expiring.length %></span>
|
||
<% } %>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Tab: Nach Kategorie -->
|
||
<div class="ftab-content active" id="tab-categories">
|
||
<div class="contracts-grid">
|
||
|
||
<!-- Donut Chart -->
|
||
<div class="finance-card" style="display:flex;flex-direction:column;align-items:center">
|
||
<h3>Verteilung nach Kategorie</h3>
|
||
<canvas id="categoryChart" style="max-width:260px;max-height:260px;margin-top:12px"></canvas>
|
||
</div>
|
||
|
||
<!-- Tabelle -->
|
||
<div class="finance-card" style="flex:1">
|
||
<h3>Übersicht</h3>
|
||
<table class="admin-table" style="margin-top:12px">
|
||
<thead>
|
||
<tr>
|
||
<th>Kategorie</th>
|
||
<th>Gesamt</th>
|
||
<th>Aktiv</th>
|
||
<th>Pausiert</th>
|
||
<th>Inaktiv</th>
|
||
<th>Monatl. Umsatz</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<% byCategory.forEach(row => { %>
|
||
<tr>
|
||
<td><strong><%= row.category_name || '– Keine Kategorie –' %></strong></td>
|
||
<td><%= row.total %></td>
|
||
<td style="color:var(--success)"><strong><%= row.active %></strong></td>
|
||
<td style="color:var(--warning)"><%= row.paused %></td>
|
||
<td style="color:var(--error)"><%= row.inactive %></td>
|
||
<td><strong><%= Number(row.monthly_revenue||0).toFixed(2).replace('.',',') %> €</strong></td>
|
||
</tr>
|
||
<% }) %>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Tab: Nach Tarif -->
|
||
<div class="ftab-content" id="tab-tariffs">
|
||
<div class="finance-card">
|
||
<h3>Verträge nach Tarif</h3>
|
||
<div class="table-wrap" style="margin-top:12px">
|
||
<table class="admin-table">
|
||
<thead>
|
||
<tr>
|
||
<th>Tarif</th>
|
||
<th>Laufzeit</th>
|
||
<th>Aktueller Preis</th>
|
||
<th>Mitglieder</th>
|
||
<th>Davon aktiv</th>
|
||
<th>Monatl. Umsatz</th>
|
||
<th>Status</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<% byTariff.forEach(row => { %>
|
||
<tr>
|
||
<td><strong><%= row.tariff_name %></strong></td>
|
||
<td><%= row.duration_months %> Monate</td>
|
||
<td><%= Number(row.price_monthly).toFixed(2).replace('.',',') %> €/Monat</td>
|
||
<td><%= row.total %></td>
|
||
<td style="color:var(--success)"><strong><%= row.active %></strong></td>
|
||
<td><strong><%= Number(row.monthly_revenue||0).toFixed(2).replace('.',',') %> €</strong></td>
|
||
<td>
|
||
<span class="invoice-status <%= row.tariff_active ? 'paid' : 'cancelled' %>">
|
||
<%= row.tariff_active ? '✅ Aktiv' : '❌ Inaktiv' %>
|
||
</span>
|
||
</td>
|
||
</tr>
|
||
<% }) %>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Tab: Auslaufende Verträge -->
|
||
<div class="ftab-content" id="tab-expiring">
|
||
<div class="finance-card">
|
||
<div class="section-header">
|
||
<h3>Auslaufende Verträge – nächste 3 Monate (<%= expiring.length %>)</h3>
|
||
</div>
|
||
<% if (expiring.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>Vereinbarter Preis</th>
|
||
<th>Vertragsende</th>
|
||
<th>Restlaufzeit</th>
|
||
<th>Aktionen</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<% expiring.forEach(m => { %>
|
||
<%
|
||
const endDate = new Date(m.effective_end);
|
||
const diffDays = Math.ceil((endDate - new Date()) / (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><%= Number(m.agreed_price||m.price_monthly).toFixed(2).replace('.',',') %> €</strong></td>
|
||
<td><strong><%= endDate.toLocaleDateString('de-DE') %></strong></td>
|
||
<td>
|
||
<span class="expiry-badge expiry-<%= urgency %>">
|
||
noch <%= diffDays %> Tage
|
||
</span>
|
||
</td>
|
||
<td>
|
||
<div style="display:flex;gap:6px;flex-wrap:wrap">
|
||
<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">
|
||
<input type="hidden" name="_redirect" value="/admin/contracts">
|
||
<button type="submit" class="btn btn-sm btn-primary">📧 E-Mail</button>
|
||
</form>
|
||
<button type="button" class="btn btn-sm btn-success"
|
||
onclick="openRenewModal(<%= m.id %>, '<%= m.last_name %>, <%= m.first_name %>')">
|
||
🔄 Verlängern
|
||
</button>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
<% }) %>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<% } %>
|
||
</div>
|
||
</div>
|
||
|
||
</main>
|
||
</div>
|
||
|
||
<!-- Modal: Manuell verlängern -->
|
||
<div class="modal-overlay hidden" id="renewModal">
|
||
<div class="modal">
|
||
<div class="modal-header">
|
||
<h3>Vertrag manuell verlängern</h3>
|
||
<button onclick="toggleModal('renewModal')" class="modal-close">✕</button>
|
||
</div>
|
||
<form method="POST" id="renewForm">
|
||
<div class="form-group">
|
||
<label>Mitglied</label>
|
||
<input type="text" id="renewMemberName" class="form-control" disabled>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Neuer Tarif *</label>
|
||
<select name="new_tariff_id" class="form-control" required>
|
||
<option value="">– Tarif wählen –</option>
|
||
<% tariffs.forEach(t => { %>
|
||
<option value="<%= t.id %>"><%= t.name %> – <%= Number(t.price_monthly).toFixed(2).replace('.',',') %>€/Monat (<%= t.duration_months %> Monate)</option>
|
||
<% }) %>
|
||
</select>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button type="button" onclick="toggleModal('renewModal')" class="btn btn-outline">Abbrechen</button>
|
||
<button type="submit" class="btn btn-success"
|
||
onclick="return confirm('Vertrag verlängern?')">🔄 Verlängern</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
// Chart
|
||
const catData = <%- JSON.stringify(byCategory) %>;
|
||
new Chart(document.getElementById('categoryChart'), {
|
||
type: 'doughnut',
|
||
data: {
|
||
labels: catData.map(c => c.category_name || 'Sonstige'),
|
||
datasets: [{
|
||
data: catData.map(c => c.active),
|
||
backgroundColor: ['#2d2dcc','#16a34a','#d97706','#7c3aed','#dc2626','#0891b2'],
|
||
borderWidth: 2, borderColor: '#fff'
|
||
}]
|
||
},
|
||
options: {
|
||
responsive: true,
|
||
plugins: {
|
||
legend: { position: 'bottom', labels: { font: { family: 'Outfit' } } }
|
||
}
|
||
}
|
||
});
|
||
|
||
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');
|
||
if (el) el.classList.add('active');
|
||
}
|
||
|
||
function toggleModal(id) {
|
||
document.getElementById(id).classList.toggle('hidden');
|
||
}
|
||
|
||
function openRenewModal(memberId, memberName) {
|
||
document.getElementById('renewMemberName').value = memberName;
|
||
document.getElementById('renewForm').action = '/admin/renew-manual/' + memberId;
|
||
toggleModal('renewModal');
|
||
}
|
||
|
||
// Hash on load
|
||
const hash = window.location.hash.replace('#','');
|
||
if (hash && document.getElementById('tab-' + hash)) showTab(hash);
|
||
</script>
|
||
</body>
|
||
</html>
|