Vertragsverwaltung_Plusfit24/views/admin/dashboard.ejs
2026-03-28 10:38:42 +00:00

461 lines
20 KiB
Plaintext
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 Admin Dashboard</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="#" class="nav-link active" onclick="showSection('tarife', this)">📋 Tarife</a>
<a href="#" class="nav-link" onclick="showSection('kategorien', this)">🏷️ Kategorien</a>
<a href="#" class="nav-link" onclick="showSection('mitglieder', this)">
👥 Mitglieder
<% if ((stats.pending_count || 0) + (stats.new_count || 0) > 0) { %>
<span class="nav-badge"><%= (stats.pending_count || 0) + (stats.new_count || 0) %></span>
<% } %>
</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">📊 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="#" class="nav-link" onclick="showSection('einstellungen', this)">⚙️ 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">
<div class="admin-header">
<h1 id="sectionTitle">Tarife</h1>
<div class="stats-row">
<div class="stat-card">
<div class="stat-number"><%= stats.total || 0 %></div>
<div class="stat-label">Gesamt Mitglieder</div>
</div>
<div class="stat-card">
<div class="stat-number"><%= stats.active_count || 0 %></div>
<div class="stat-label">Aktive Mitglieder</div>
</div>
<div class="stat-card">
<div class="stat-number"><%= stats.last_30_days || 0 %></div>
<div class="stat-label">Letzte 30 Tage</div>
</div>
<% if (stats.pending_count > 0) { %>
<div class="stat-card stat-card-pending" onclick="showSection('mitglieder', document.querySelector('[onclick*=mitglieder]'))" style="cursor:pointer" title="Ausstehende Bestätigungen">
<div class="stat-number" style="color:var(--error)"><%= stats.pending_count %></div>
<div class="stat-label">⏳ Ausstehend</div>
</div>
<% } %>
<% if (stats.new_count > 0) { %>
<div class="stat-card stat-card-new" onclick="showSection('mitglieder', document.querySelector('[onclick*=mitglieder]'))" style="cursor:pointer" title="Neue Mitglieder die noch nicht bearbeitet wurden">
<div class="stat-number" style="color:#0891b2"><%= stats.new_count %></div>
<div class="stat-label">🆕 Neu</div>
</div>
<% } %>
<div class="stat-card">
<div class="stat-number"><%= stats.minors || 0 %></div>
<div class="stat-label">Minderjährige</div>
</div>
</div>
</div>
<% if (success) { %><div class="alert alert-success"><%= success %></div><% } %>
<% if (error) { %><div class="alert alert-error"><%= error %></div><% } %>
<!-- ===== TARIFE ===== -->
<section id="section-tarife" class="admin-section">
<div class="section-header">
<h2>Tarife verwalten</h2>
<button class="btn btn-primary" onclick="toggleModal('createTariffModal')">+ Neuer Tarif</button>
</div>
<div class="tariff-admin-grid">
<% tariffs.forEach(tariff => { %>
<div class="tariff-admin-card <%= tariff.active ? '' : 'inactive' %>">
<div class="tariff-admin-header">
<span class="tariff-status-badge <%= tariff.active ? 'active' : 'inactive' %>">
<%= tariff.active ? '✅ Aktiv' : '❌ Inaktiv' %>
</span>
<% if (tariff.category_name) { %>
<span class="category-pill"><%= tariff.category_name %></span>
<% } %>
</div>
<h3><%= tariff.name %></h3>
<div class="tariff-admin-details">
<span>💰 <%= Number(tariff.price_monthly).toFixed(2) %>€/Monat</span>
<span>📅 <%= tariff.duration_months %> Monate</span>
<span>📦 Startpaket: <%= Number(tariff.start_package_price).toFixed(2) %>€</span>
</div>
<div class="tariff-admin-actions">
<button class="btn btn-sm btn-outline" onclick="editTariff(<%= JSON.stringify(tariff) %>)">✏️ Bearbeiten</button>
<form method="POST" action="/admin/tariffs/<%= tariff.id %>/toggle" style="display:inline">
<button type="submit" class="btn btn-sm <%= tariff.active ? 'btn-warning' : 'btn-success' %>">
<%= tariff.active ? '⏸ Deaktivieren' : '▶ Aktivieren' %>
</button>
</form>
</div>
</div>
<% }) %>
<% if (tariffs.length === 0) { %>
<div class="no-data-card">Noch keine Tarife angelegt.</div>
<% } %>
</div>
</section>
<!-- ===== KATEGORIEN ===== -->
<section id="section-kategorien" class="admin-section hidden">
<div class="section-header">
<h2>Kategorien verwalten</h2>
<button class="btn btn-primary" onclick="toggleModal('createCategoryModal')">+ Neue Kategorie</button>
</div>
<div class="category-list">
<% if (categories.length === 0) { %>
<div class="no-data-card">Noch keine Kategorien angelegt.</div>
<% } %>
<% categories.forEach(cat => { %>
<div class="category-row">
<div class="category-info">
<span class="category-name">🏷️ <%= cat.name %></span>
<span class="category-meta">
<%= tariffs.filter(t => t.category_id === cat.id).length %> Tarif(e)
</span>
</div>
<div class="category-actions">
<button class="btn btn-sm btn-outline"
onclick="editCategory(<%= cat.id %>, '<%= cat.name.replace(/'/g, `\\'`) %>')">
✏️ Umbenennen
</button>
<form method="POST" action="/admin/categories/<%= cat.id %>/delete" style="display:inline"
onsubmit="return confirm('Kategorie \'<%= cat.name %>\' wirklich löschen?')">
<button type="submit" class="btn btn-sm btn-danger">🗑 Löschen</button>
</form>
</div>
</div>
<% }) %>
</div>
<div class="info-box">
<strong> Hinweis:</strong> Kategorien können nur gelöscht werden wenn keine Tarife zugeordnet sind.
Weise die Tarife zuerst einer anderen Kategorie zu.
</div>
</section>
<!-- ===== MITGLIEDER ===== -->
<section id="section-mitglieder" class="admin-section hidden">
<div class="section-header">
<h2>Mitglieder</h2>
<input type="text" id="memberSearch" placeholder="Suche nach Name, E-Mail..."
class="search-input" onkeyup="filterMembers()">
</div>
<div class="table-wrap">
<table class="admin-table" id="memberTable">
<thead>
<tr>
<th>Name</th>
<th>E-Mail</th>
<th>Tarif</th>
<th>Geburtsdatum</th>
<th>Min.</th>
<th>IBAN</th>
<th>Angemeldet am</th>
</tr>
</thead>
<tbody>
<% memberships.forEach(m => { %>
<tr class="member-row <%= m.status === 'pending' ? 'member-row-pending' : (!m.reviewed ? 'member-row-new' : '') %>" onclick="openMember(<%= m.id %>, <%= m.reviewed ? 1 : 0 %>)" style="cursor:pointer">
<td><strong><%= m.salutation %> <%= m.first_name %> <%= m.last_name %></strong></td>
<td><%= m.email %></td>
<td><%= m.tariff_name || '' %></td>
<td><%= m.birth_date ? new Date(m.birth_date).toLocaleDateString('de-DE') : '' %></td>
<td><%= m.is_minor ? '⚠️ Ja' : 'Nein' %></td>
<td class="iban-cell"><%= m.iban ? m.iban.replace(/(.{4})/g, '$1 ').trim() : '' %></td>
<td><%= new Date(m.created_at).toLocaleDateString('de-DE') %></td>
</tr>
<% }) %>
<% if (memberships.length === 0) { %>
<tr><td colspan="7" class="no-data">Noch keine Mitglieder registriert.</td></tr>
<% } %>
</tbody>
</table>
</div>
</section>
<!-- ===== EINSTELLUNGEN ===== -->
<section id="section-einstellungen" class="admin-section hidden">
<h2>Passwort ändern</h2>
<div class="settings-card">
<form method="POST" action="/admin/change-password">
<div class="form-group">
<label>Aktuelles Passwort</label>
<div class="input-wrap"><input type="password" name="current_password" required></div>
</div>
<div class="form-group">
<label>Neues Passwort</label>
<div class="input-wrap"><input type="password" name="new_password" required minlength="8"></div>
</div>
<div class="form-group">
<label>Neues Passwort bestätigen</label>
<div class="input-wrap"><input type="password" name="confirm_password" required minlength="8"></div>
</div>
<button type="submit" class="btn btn-primary">Passwort ändern</button>
</form>
</div>
</section>
</main>
</div>
<!-- Modal: Neuer Tarif -->
<div class="modal-overlay hidden" id="createTariffModal">
<div class="modal">
<div class="modal-header">
<h3>Neuer Tarif</h3>
<button onclick="toggleModal('createTariffModal')" class="modal-close">✕</button>
</div>
<form method="POST" action="/admin/tariffs">
<div class="form-group">
<label>Name *</label>
<input type="text" name="name" required class="form-control" placeholder="z.B. Single 12 Monate">
</div>
<div class="form-group">
<label>Kategorie</label>
<select name="category_id" class="form-control">
<option value=""> Keine Kategorie </option>
<% categories.forEach(cat => { %>
<option value="<%= cat.id %>"><%= cat.name %></option>
<% }) %>
</select>
</div>
<div class="form-row">
<div class="form-group">
<label>Laufzeit (Monate) *</label>
<input type="number" name="duration_months" required class="form-control" min="1" value="12">
</div>
<div class="form-group">
<label>Preis/Monat (€) *</label>
<input type="number" name="price_monthly" required class="form-control" step="0.01" min="0">
</div>
</div>
<div class="form-group">
<label>Startpaket Preis (€)</label>
<input type="number" name="start_package_price" class="form-control" step="0.01" value="35.00">
</div>
<div class="form-group">
<label>Beschreibung</label>
<textarea name="description" class="form-control" rows="2"></textarea>
</div>
<div class="modal-footer">
<button type="button" onclick="toggleModal('createTariffModal')" class="btn btn-outline">Abbrechen</button>
<button type="submit" class="btn btn-primary">Tarif erstellen</button>
</div>
</form>
</div>
</div>
<!-- Modal: Tarif bearbeiten -->
<div class="modal-overlay hidden" id="editTariffModal">
<div class="modal">
<div class="modal-header">
<h3>Tarif bearbeiten</h3>
<button onclick="toggleModal('editTariffModal')" class="modal-close">✕</button>
</div>
<form method="POST" id="editTariffForm">
<div class="form-group">
<label>Name *</label>
<input type="text" name="name" id="edit_name" required class="form-control">
</div>
<div class="form-group">
<label>Kategorie</label>
<select name="category_id" id="edit_category" class="form-control">
<option value=""> Keine Kategorie </option>
<% categories.forEach(cat => { %>
<option value="<%= cat.id %>"><%= cat.name %></option>
<% }) %>
</select>
</div>
<div class="form-row">
<div class="form-group">
<label>Laufzeit (Monate) *</label>
<input type="number" name="duration_months" id="edit_duration" required class="form-control" min="1">
</div>
<div class="form-group">
<label>Preis/Monat (€) *</label>
<input type="number" name="price_monthly" id="edit_price" required class="form-control" step="0.01" min="0">
</div>
</div>
<div class="form-group">
<label>Startpaket Preis (€)</label>
<input type="number" name="start_package_price" id="edit_start_package" class="form-control" step="0.01">
</div>
<div class="form-group">
<label>Beschreibung</label>
<textarea name="description" id="edit_description" class="form-control" rows="2"></textarea>
</div>
<div class="modal-footer">
<button type="button" onclick="toggleModal('editTariffModal')" class="btn btn-outline">Abbrechen</button>
<button type="submit" class="btn btn-primary">Speichern</button>
</div>
</form>
</div>
</div>
<!-- Modal: Neue Kategorie -->
<div class="modal-overlay hidden" id="createCategoryModal">
<div class="modal">
<div class="modal-header">
<h3>Neue Kategorie</h3>
<button onclick="toggleModal('createCategoryModal')" class="modal-close">✕</button>
</div>
<form method="POST" action="/admin/categories">
<div class="form-group">
<label>Kategoriename *</label>
<input type="text" name="name" required class="form-control" placeholder="z.B. Senioren">
</div>
<div class="modal-footer">
<button type="button" onclick="toggleModal('createCategoryModal')" class="btn btn-outline">Abbrechen</button>
<button type="submit" class="btn btn-primary">Kategorie erstellen</button>
</div>
</form>
</div>
</div>
<!-- Modal: Kategorie bearbeiten -->
<div class="modal-overlay hidden" id="editCategoryModal">
<div class="modal">
<div class="modal-header">
<h3>Kategorie umbenennen</h3>
<button onclick="toggleModal('editCategoryModal')" class="modal-close">✕</button>
</div>
<form method="POST" id="editCategoryForm">
<div class="form-group">
<label>Neuer Name *</label>
<input type="text" name="name" id="edit_cat_name" required class="form-control">
</div>
<div class="modal-footer">
<button type="button" onclick="toggleModal('editCategoryModal')" class="btn btn-outline">Abbrechen</button>
<button type="submit" class="btn btn-primary">Speichern</button>
</div>
</form>
</div>
</div>
<script>
const sectionTitles = {
tarife: 'Tarife', kategorien: 'Kategorien',
mitglieder: 'Mitglieder', einstellungen: 'Einstellungen'
};
function showSection(name, el) {
document.querySelectorAll('.admin-section').forEach(s => s.classList.add('hidden'));
document.getElementById('section-' + name).classList.remove('hidden');
document.querySelectorAll('.nav-link').forEach(l => l.classList.remove('active'));
if (el) el.classList.add('active');
document.getElementById('sectionTitle').textContent = sectionTitles[name] || name;
event && event.preventDefault();
}
function toggleModal(id) {
document.getElementById(id).classList.toggle('hidden');
}
function editTariff(tariff) {
document.getElementById('edit_name').value = tariff.name;
document.getElementById('edit_category').value = tariff.category_id || '';
document.getElementById('edit_duration').value = tariff.duration_months;
document.getElementById('edit_price').value = tariff.price_monthly;
document.getElementById('edit_start_package').value = tariff.start_package_price;
document.getElementById('edit_description').value = tariff.description || '';
document.getElementById('editTariffForm').action = '/admin/tariffs/' + tariff.id + '/update';
toggleModal('editTariffModal');
}
function editCategory(id, name) {
document.getElementById('edit_cat_name').value = name;
document.getElementById('editCategoryForm').action = '/admin/categories/' + id + '/update';
toggleModal('editCategoryModal');
}
function openMember(id, reviewed) {
if (!reviewed) {
fetch('/admin/members/' + id + '/reviewed', { method: 'POST' });
}
window.location = '/admin/members/' + id;
}
// Live Badge Update alle 30 Sekunden
function updateBadge() {
fetch('/admin/api/badge-count')
.then(r => r.json())
.then(data => {
const badge = document.querySelector('.nav-badge');
if (data.total > 0) {
if (badge) {
badge.textContent = data.total;
} else {
// Badge neu erstellen
const link = document.querySelector('[onclick*="mitglieder"]');
if (link) {
const span = document.createElement('span');
span.className = 'nav-badge';
span.textContent = data.total;
link.appendChild(span);
}
}
// Stat-Karten aktualisieren
if (data.new > 0) {
let newCard = document.getElementById('stat-new');
if (!newCard) {
const statsRow = document.querySelector('.stats-row');
newCard = document.createElement('div');
newCard.id = 'stat-new';
newCard.className = 'stat-card stat-card-new';
newCard.style.cursor = 'pointer';
newCard.onclick = () => showSection('mitglieder', document.querySelector('[onclick*="mitglieder"]'));
newCard.innerHTML = '<div class="stat-number" style="color:#0891b2">' + data.new + '</div><div class="stat-label">🆕 Neu</div>';
statsRow.appendChild(newCard);
} else {
newCard.querySelector('.stat-number').textContent = data.new;
}
} else {
const newCard = document.getElementById('stat-new');
if (newCard) newCard.remove();
}
} else {
if (badge) badge.remove();
const newCard = document.getElementById('stat-new');
if (newCard) newCard.remove();
}
}).catch(() => {});
}
// Sofort + alle 30 Sekunden
updateBadge();
setInterval(updateBadge, 30000);
function filterMembers() {
const q = document.getElementById('memberSearch').value.toLowerCase();
document.querySelectorAll('.member-row').forEach(row => {
row.style.display = row.textContent.toLowerCase().includes(q) ? '' : 'none';
});
}
// URL-Hash auswerten (nach Redirect mit #kategorien etc.)
const hash = window.location.hash.replace('#', '');
if (hash && document.getElementById('section-' + hash)) {
const navLink = document.querySelector(`[onclick*="'${hash}'"]`);
showSection(hash, navLink);
}
</script>
</body>
</html>