Files
Ninjaserver/public/admin-dashboard.html
2025-09-03 17:13:18 +02:00

905 lines
30 KiB
HTML
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>Admin Dashboard - NinjaCross</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);
min-height: 100vh;
color: #333;
}
.header {
background: rgba(255, 255, 255, 0.95);
padding: 20px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
display: flex;
justify-content: space-between;
align-items: center;
}
.header h1 {
color: #1e3c72;
font-size: 2em;
}
.user-info {
display: flex;
align-items: center;
gap: 15px;
}
.access-badge {
background: #4CAF50;
color: white;
padding: 5px 10px;
border-radius: 15px;
font-size: 0.8em;
font-weight: bold;
}
.access-badge.level-1 {
background: #ff9800;
}
.access-badge.level-2 {
background: #4CAF50;
}
.btn {
background: #2196F3;
color: white;
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
text-decoration: none;
display: inline-block;
font-size: 0.9em;
transition: all 0.3s ease;
}
.btn:hover {
background: #1976D2;
transform: translateY(-2px);
}
.btn-danger {
background: #f44336;
}
.btn-danger:hover {
background: #d32f2f;
}
.btn-warning {
background: #ff9800;
}
.btn-warning:hover {
background: #f57c00;
}
.btn-success {
background: #4CAF50;
}
.btn-success:hover {
background: #388E3C;
}
.container {
max-width: 1200px;
margin: 30px auto;
padding: 0 20px;
}
.dashboard-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.card {
background: white;
border-radius: 10px;
padding: 20px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
transition: transform 0.3s ease;
}
.card:hover {
transform: translateY(-5px);
}
.card h3 {
color: #1e3c72;
margin-bottom: 15px;
display: flex;
align-items: center;
gap: 10px;
}
.icon {
font-size: 1.5em;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin-bottom: 30px;
}
.stat-card {
background: white;
padding: 20px;
border-radius: 10px;
text-align: center;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
}
.stat-number {
font-size: 2.5em;
font-weight: bold;
color: #1e3c72;
margin-bottom: 5px;
}
.stat-label {
color: #666;
font-size: 0.9em;
}
.data-table {
width: 100%;
border-collapse: collapse;
background: white;
border-radius: 10px;
overflow: hidden;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
}
.data-table th,
.data-table td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #eee;
}
.data-table th {
background: #f5f5f5;
font-weight: bold;
color: #333;
}
.data-table tr:hover {
background: #f9f9f9;
}
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
}
.modal-content {
background-color: white;
margin: 10% auto;
padding: 20px;
border-radius: 10px;
width: 90%;
max-width: 500px;
position: relative;
}
.close {
color: #aaa;
float: right;
font-size: 28px;
font-weight: bold;
cursor: pointer;
position: absolute;
right: 15px;
top: 10px;
}
.close:hover {
color: #000;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: bold;
color: #333;
}
.form-group input,
.form-group select {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
font-size: 1em;
}
.message {
padding: 10px;
border-radius: 5px;
margin-bottom: 15px;
display: none;
}
.message.success {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.message.error {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.loading {
text-align: center;
padding: 20px;
color: #666;
}
.no-data {
text-align: center;
padding: 40px;
color: #999;
font-style: italic;
}
.action-buttons {
display: flex;
gap: 5px;
}
.btn-small {
padding: 5px 10px;
font-size: 0.8em;
}
.table-container {
max-height: 400px;
overflow-y: auto;
margin-top: 15px;
}
.search-container {
margin-bottom: 20px;
display: flex;
gap: 10px;
align-items: center;
}
.search-input {
flex: 1;
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
}
</style>
</head>
<body>
<div class="header">
<h1>🛡️ Admin Dashboard</h1>
<div class="user-info">
<span id="username">Loading...</span>
<span id="accessBadge" class="access-badge">Level ?</span>
<button id="generatorBtn" class="btn btn-success" style="display: none;">
🔧 Lizenzgenerator
</button>
<button id="logoutBtn" class="btn btn-danger">Logout</button>
</div>
</div>
<div class="container">
<!-- Statistiken -->
<div class="stats-grid">
<div class="stat-card">
<div class="stat-number" id="totalPlayers">-</div>
<div class="stat-label">Spieler</div>
</div>
<div class="stat-card">
<div class="stat-number" id="totalRuns">-</div>
<div class="stat-label">Läufe</div>
</div>
<div class="stat-card">
<div class="stat-number" id="totalLocations">-</div>
<div class="stat-label">Standorte</div>
</div>
<div class="stat-card">
<div class="stat-number" id="totalAdminUsers">-</div>
<div class="stat-label">Admin-Benutzer</div>
</div>
</div>
<!-- Dashboard Cards -->
<div class="dashboard-grid">
<!-- Benutzer-Verwaltung -->
<div class="card">
<h3><span class="icon">👥</span> Benutzer-Verwaltung</h3>
<p>Verwalte Supabase-Benutzer und deren RFID-Verknüpfungen</p>
<button class="btn" onclick="showUserManagement()">Benutzer anzeigen</button>
</div>
<!-- Spieler-Verwaltung -->
<div class="card">
<h3><span class="icon">🏃</span> Spieler-Verwaltung</h3>
<p>Verwalte Spieler und deren RFID-UIDs</p>
<button class="btn" onclick="showPlayerManagement()">Spieler anzeigen</button>
</div>
<!-- Läufe-Verwaltung -->
<div class="card">
<h3><span class="icon">⏱️</span> Läufe-Verwaltung</h3>
<p>Zeige und lösche Läufe</p>
<button class="btn" onclick="showRunManagement()">Läufe anzeigen</button>
</div>
<!-- Standort-Verwaltung -->
<div class="card">
<h3><span class="icon">📍</span> Standort-Verwaltung</h3>
<p>Verwalte Standorte und deren Koordinaten</p>
<button class="btn" onclick="showLocationManagement()">Standorte anzeigen</button>
</div>
<!-- Admin-Benutzer -->
<div class="card">
<h3><span class="icon">🔐</span> Admin-Benutzer</h3>
<p>Verwalte Admin-Benutzer und Zugriffsrechte</p>
<button class="btn" onclick="showAdminUserManagement()">Admins anzeigen</button>
</div>
<!-- System-Info -->
<div class="card">
<h3><span class="icon">📊</span> System-Informationen</h3>
<p>Server-Status und Systemdaten</p>
<button class="btn" onclick="showSystemInfo()">Info anzeigen</button>
</div>
</div>
<!-- Daten-Anzeige Bereich -->
<div id="dataSection" style="display: none;">
<div class="card">
<h3 id="dataTitle">Daten</h3>
<div class="search-container">
<input type="text" id="searchInput" class="search-input" placeholder="Suchen...">
<button class="btn" onclick="refreshData()">🔄 Aktualisieren</button>
<button class="btn btn-success" onclick="showAddModal()"> Hinzufügen</button>
</div>
<div id="dataContent">
<div class="loading">Lade Daten...</div>
</div>
</div>
</div>
</div>
<!-- Modals -->
<div id="addModal" class="modal">
<div class="modal-content">
<span class="close">&times;</span>
<h3 id="modalTitle">Element hinzufügen</h3>
<div class="message" id="modalMessage"></div>
<form id="addForm">
<div id="formFields"></div>
<button type="submit" class="btn">Speichern</button>
<button type="button" class="btn btn-danger" onclick="closeModal()">Abbrechen</button>
</form>
</div>
</div>
<div id="confirmModal" class="modal">
<div class="modal-content">
<span class="close">&times;</span>
<h3>Bestätigung</h3>
<p id="confirmMessage">Sind Sie sicher?</p>
<button id="confirmYes" class="btn btn-danger">Ja, löschen</button>
<button id="confirmNo" class="btn">Abbrechen</button>
</div>
</div>
<script>
let currentUser = null;
let currentDataType = null;
let currentData = [];
// Beim Laden der Seite
document.addEventListener('DOMContentLoaded', function() {
checkAuth();
loadStatistics();
setupEventListeners();
});
function setupEventListeners() {
// Logout Button
document.getElementById('logoutBtn').addEventListener('click', logout);
// Generator Button
document.getElementById('generatorBtn').addEventListener('click', function() {
window.location.href = '/generator';
});
// Modal Close Buttons
document.querySelectorAll('.close').forEach(closeBtn => {
closeBtn.addEventListener('click', closeModal);
});
// Confirm Modal Buttons
document.getElementById('confirmNo').addEventListener('click', closeModal);
// Search Input
document.getElementById('searchInput').addEventListener('input', filterData);
// Add Form
document.getElementById('addForm').addEventListener('submit', handleAddSubmit);
}
async function checkAuth() {
try {
const response = await fetch('/api/check-session');
const result = await response.json();
if (!result.success) {
window.location.href = '/adminlogin.html';
return;
}
currentUser = result.user;
document.getElementById('username').textContent = currentUser.username;
const accessBadge = document.getElementById('accessBadge');
accessBadge.textContent = `Level ${currentUser.access_level}`;
accessBadge.className = `access-badge level-${currentUser.access_level}`;
// Generator-Button nur für Level 2
if (currentUser.access_level >= 2) {
document.getElementById('generatorBtn').style.display = 'inline-block';
}
} catch (error) {
console.error('Auth check failed:', error);
window.location.href = '/adminlogin.html';
}
}
async function logout() {
try {
await fetch('/api/logout', { method: 'POST' });
window.location.href = '/adminlogin.html';
} catch (error) {
console.error('Logout failed:', error);
window.location.href = '/adminlogin.html';
}
}
async function loadStatistics() {
try {
const response = await fetch('/api/admin-stats');
const stats = await response.json();
if (stats.success) {
document.getElementById('totalPlayers').textContent = stats.data.players || 0;
document.getElementById('totalRuns').textContent = stats.data.runs || 0;
document.getElementById('totalLocations').textContent = stats.data.locations || 0;
document.getElementById('totalAdminUsers').textContent = stats.data.adminUsers || 0;
}
} catch (error) {
console.error('Failed to load statistics:', error);
}
}
function showUserManagement() {
showDataSection('users', 'Benutzer-Verwaltung');
loadUsers();
}
function showPlayerManagement() {
showDataSection('players', 'Spieler-Verwaltung');
loadPlayers();
}
function showRunManagement() {
showDataSection('runs', 'Läufe-Verwaltung');
loadRuns();
}
function showLocationManagement() {
showDataSection('locations', 'Standort-Verwaltung');
loadLocations();
}
function showAdminUserManagement() {
showDataSection('adminusers', 'Admin-Benutzer');
loadAdminUsers();
}
function showSystemInfo() {
showDataSection('system', 'System-Informationen');
loadSystemInfo();
}
function showDataSection(type, title) {
currentDataType = type;
document.getElementById('dataTitle').textContent = title;
document.getElementById('dataSection').style.display = 'block';
document.getElementById('searchInput').value = '';
}
async function loadUsers() {
// Implementation für Supabase-Benutzer
document.getElementById('dataContent').innerHTML = '<div class="no-data">Supabase-Benutzer werden über die Supabase-Console verwaltet</div>';
}
async function loadPlayers() {
try {
const response = await fetch('/api/admin-players');
const result = await response.json();
if (result.success) {
currentData = result.data;
displayPlayersTable(result.data);
} else {
showError('Fehler beim Laden der Spieler');
}
} catch (error) {
console.error('Failed to load players:', error);
showError('Fehler beim Laden der Spieler');
}
}
async function loadRuns() {
try {
const response = await fetch('/api/admin-runs');
const result = await response.json();
if (result.success) {
currentData = result.data;
displayRunsTable(result.data);
} else {
showError('Fehler beim Laden der Läufe');
}
} catch (error) {
console.error('Failed to load runs:', error);
showError('Fehler beim Laden der Läufe');
}
}
async function loadLocations() {
try {
const response = await fetch('/api/admin-locations');
const result = await response.json();
if (result.success) {
currentData = result.data;
displayLocationsTable(result.data);
} else {
showError('Fehler beim Laden der Standorte');
}
} catch (error) {
console.error('Failed to load locations:', error);
showError('Fehler beim Laden der Standorte');
}
}
async function loadAdminUsers() {
try {
const response = await fetch('/api/admin-adminusers');
const result = await response.json();
if (result.success) {
currentData = result.data;
displayAdminUsersTable(result.data);
} else {
showError('Fehler beim Laden der Admin-Benutzer');
}
} catch (error) {
console.error('Failed to load admin users:', error);
showError('Fehler beim Laden der Admin-Benutzer');
}
}
function displayPlayersTable(players) {
let html = '<div class="table-container"><table class="data-table">';
html += '<thead><tr><th>ID</th><th>Name</th><th>RFID UID</th><th>Supabase User</th><th>Registriert</th><th>Aktionen</th></tr></thead><tbody>';
players.forEach(player => {
html += `<tr>
<td>${player.id}</td>
<td>${player.full_name || '-'}</td>
<td>${player.rfiduid || '-'}</td>
<td>${player.supabase_user_id ? '✅' : '❌'}</td>
<td>${new Date(player.created_at).toLocaleDateString('de-DE')}</td>
<td class="action-buttons">
<button class="btn btn-small btn-danger" onclick="deletePlayer('${player.id}')">Löschen</button>
</td>
</tr>`;
});
html += '</tbody></table></div>';
document.getElementById('dataContent').innerHTML = html;
}
function displayRunsTable(runs) {
let html = '<div class="table-container"><table class="data-table">';
html += '<thead><tr><th>ID</th><th>Spieler</th><th>Standort</th><th>Zeit</th><th>Datum</th><th>Aktionen</th></tr></thead><tbody>';
runs.forEach(run => {
// Use the time_seconds value from the backend
const timeInSeconds = parseFloat(run.time_seconds) || 0;
html += `<tr>
<td>${run.id}</td>
<td>${run.player_name || `Player ${run.player_id}`}</td>
<td>${run.location_name || `Location ${run.location_id}`}</td>
<td>${timeInSeconds.toFixed(3)}s</td>
<td>${new Date(run.created_at).toLocaleDateString('de-DE')} ${new Date(run.created_at).toLocaleTimeString('de-DE')}</td>
<td class="action-buttons">
<button class="btn btn-small btn-danger" onclick="deleteRun('${run.id}')">Löschen</button>
</td>
</tr>`;
});
html += '</tbody></table></div>';
document.getElementById('dataContent').innerHTML = html;
}
function displayLocationsTable(locations) {
let html = '<div class="table-container"><table class="data-table">';
html += '<thead><tr><th>ID</th><th>Name</th><th>Latitude</th><th>Longitude</th><th>Erstellt</th><th>Aktionen</th></tr></thead><tbody>';
locations.forEach(location => {
html += `<tr>
<td>${location.id}</td>
<td>${location.name}</td>
<td>${location.latitude}</td>
<td>${location.longitude}</td>
<td>${new Date(location.created_at).toLocaleDateString('de-DE')}</td>
<td class="action-buttons">
<button class="btn btn-small btn-danger" onclick="deleteLocation('${location.id}')">Löschen</button>
</td>
</tr>`;
});
html += '</tbody></table></div>';
document.getElementById('dataContent').innerHTML = html;
}
function displayAdminUsersTable(users) {
let html = '<div class="table-container"><table class="data-table">';
html += '<thead><tr><th>ID</th><th>Benutzername</th><th>Access Level</th><th>Aktiv</th><th>Letzter Login</th><th>Aktionen</th></tr></thead><tbody>';
users.forEach(user => {
const isCurrentUser = user.id === currentUser.id;
html += `<tr>
<td>${user.id}</td>
<td>${user.username} ${isCurrentUser ? '(Du)' : ''}</td>
<td>Level ${user.access_level}</td>
<td>${user.is_active ? '✅' : '❌'}</td>
<td>${user.last_login ? new Date(user.last_login).toLocaleDateString('de-DE') : 'Nie'}</td>
<td class="action-buttons">
${!isCurrentUser ? `<button class="btn btn-small btn-danger" onclick="deleteAdminUser(${user.id})">Löschen</button>` : ''}
</td>
</tr>`;
});
html += '</tbody></table></div>';
document.getElementById('dataContent').innerHTML = html;
}
function loadSystemInfo() {
document.getElementById('dataContent').innerHTML = `
<div style="padding: 20px;">
<h4>Server-Informationen</h4>
<p><strong>Node.js Version:</strong> ${navigator.userAgent}</p>
<p><strong>Aktuelle Zeit:</strong> ${new Date().toLocaleString('de-DE')}</p>
<p><strong>Angemeldeter Benutzer:</strong> ${currentUser.username} (Level ${currentUser.access_level})</p>
</div>
`;
}
function filterData() {
const searchTerm = document.getElementById('searchInput').value.toLowerCase();
if (!currentData) return;
let filteredData = currentData.filter(item => {
return Object.values(item).some(value =>
value && value.toString().toLowerCase().includes(searchTerm)
);
});
switch(currentDataType) {
case 'players':
displayPlayersTable(filteredData);
break;
case 'runs':
displayRunsTable(filteredData);
break;
case 'locations':
displayLocationsTable(filteredData);
break;
case 'adminusers':
displayAdminUsersTable(filteredData);
break;
}
}
function refreshData() {
switch(currentDataType) {
case 'players':
loadPlayers();
break;
case 'runs':
loadRuns();
break;
case 'locations':
loadLocations();
break;
case 'adminusers':
loadAdminUsers();
break;
case 'system':
loadSystemInfo();
break;
}
}
function showAddModal() {
// Implementierung für Add Modal je nach Datentyp
document.getElementById('addModal').style.display = 'block';
}
function closeModal() {
document.querySelectorAll('.modal').forEach(modal => {
modal.style.display = 'none';
});
}
function handleAddSubmit(e) {
e.preventDefault();
// Implementierung für das Hinzufügen von Daten
closeModal();
}
async function deletePlayer(id) {
if (await confirmDelete(`Spieler mit ID ${id} löschen?`)) {
try {
const response = await fetch(`/api/admin-players/${id}`, { method: 'DELETE' });
const result = await response.json();
if (result.success) {
showSuccess('Spieler erfolgreich gelöscht');
loadPlayers();
} else {
showError('Fehler beim Löschen des Spielers');
}
} catch (error) {
console.error('Delete failed:', error);
showError('Fehler beim Löschen des Spielers');
}
}
}
async function deleteRun(id) {
if (await confirmDelete(`Lauf mit ID ${id} löschen?`)) {
try {
const response = await fetch(`/api/admin-runs/${id}`, { method: 'DELETE' });
const result = await response.json();
if (result.success) {
showSuccess('Lauf erfolgreich gelöscht');
loadRuns();
} else {
showError('Fehler beim Löschen des Laufs');
}
} catch (error) {
console.error('Delete failed:', error);
showError('Fehler beim Löschen des Laufs');
}
}
}
async function deleteLocation(id) {
if (await confirmDelete(`Standort mit ID ${id} löschen?`)) {
try {
const response = await fetch(`/api/admin-locations/${id}`, { method: 'DELETE' });
const result = await response.json();
if (result.success) {
showSuccess('Standort erfolgreich gelöscht');
loadLocations();
} else {
showError('Fehler beim Löschen des Standorts');
}
} catch (error) {
console.error('Delete failed:', error);
showError('Fehler beim Löschen des Standorts');
}
}
}
async function deleteAdminUser(id) {
if (await confirmDelete(`Admin-Benutzer mit ID ${id} löschen?`)) {
try {
const response = await fetch(`/api/admin-adminusers/${id}`, { method: 'DELETE' });
const result = await response.json();
if (result.success) {
showSuccess('Admin-Benutzer erfolgreich gelöscht');
loadAdminUsers();
} else {
showError('Fehler beim Löschen des Admin-Benutzers');
}
} catch (error) {
console.error('Delete failed:', error);
showError('Fehler beim Löschen des Admin-Benutzers');
}
}
}
function confirmDelete(message) {
return new Promise((resolve) => {
document.getElementById('confirmMessage').textContent = message;
document.getElementById('confirmModal').style.display = 'block';
document.getElementById('confirmYes').onclick = () => {
closeModal();
resolve(true);
};
document.getElementById('confirmNo').onclick = () => {
closeModal();
resolve(false);
};
});
}
function showSuccess(message) {
const messageDiv = document.getElementById('modalMessage');
messageDiv.textContent = message;
messageDiv.className = 'message success';
messageDiv.style.display = 'block';
setTimeout(() => {
messageDiv.style.display = 'none';
}, 3000);
}
function showError(message) {
const messageDiv = document.getElementById('modalMessage');
messageDiv.textContent = message;
messageDiv.className = 'message error';
messageDiv.style.display = 'block';
setTimeout(() => {
messageDiv.style.display = 'none';
}, 3000);
}
</script>
</body>
</html>