Vertragsverwaltung_Plusfit24/views/admin/contracts.ejs
2026-03-28 10:36:08 +00:00

299 lines
12 KiB
Plaintext
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.

<!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>