461 lines
20 KiB
Plaintext
461 lines
20 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 – 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>
|