1769 lines
67 KiB
JavaScript
1769 lines
67 KiB
JavaScript
let currentUser = null;
|
||
let currentDataType = null;
|
||
let currentData = [];
|
||
|
||
// Beim Laden der Seite
|
||
document.addEventListener('DOMContentLoaded', function () {
|
||
checkAuth();
|
||
loadStatistics();
|
||
|
||
// Add cookie settings button functionality
|
||
const cookieSettingsBtn = document.getElementById('cookie-settings-footer');
|
||
if (cookieSettingsBtn) {
|
||
cookieSettingsBtn.addEventListener('click', function () {
|
||
if (window.cookieConsent) {
|
||
window.cookieConsent.resetConsent();
|
||
}
|
||
});
|
||
}
|
||
loadPageStatistics();
|
||
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/v1/web/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/v1/public/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/v1/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);
|
||
}
|
||
}
|
||
|
||
async function loadPageStatistics() {
|
||
try {
|
||
const response = await fetch('/api/v1/admin/page-stats');
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
const data = result.data;
|
||
|
||
// Display page view statistics
|
||
displayPageStats('todayStats', data.today);
|
||
displayPageStats('weekStats', data.week);
|
||
displayPageStats('monthStats', data.month);
|
||
displayPageStats('totalStats', data.total);
|
||
|
||
// Display link statistics
|
||
if (data.linkStats) {
|
||
document.getElementById('totalPlayersCount').textContent = data.linkStats.total_players || 0;
|
||
document.getElementById('linkedPlayersCount').textContent = data.linkStats.linked_players || 0;
|
||
document.getElementById('linkPercentage').textContent = `${data.linkStats.link_percentage || 0}%`;
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('Failed to load page statistics:', error);
|
||
// Show error in all stat containers
|
||
['todayStats', 'weekStats', 'monthStats', 'totalStats'].forEach(id => {
|
||
document.getElementById(id).innerHTML = '<div style="color: #dc3545;">Fehler beim Laden</div>';
|
||
});
|
||
}
|
||
}
|
||
|
||
function displayPageStats(containerId, stats) {
|
||
const container = document.getElementById(containerId);
|
||
|
||
if (!stats || stats.length === 0) {
|
||
container.innerHTML = '<div style="color: #6c757d;">0</div>';
|
||
return;
|
||
}
|
||
|
||
// Find main page visits
|
||
const mainPageStat = stats.find(stat => stat.page === 'main_page_visit');
|
||
const count = mainPageStat ? mainPageStat.count : 0;
|
||
|
||
container.innerHTML = `<div style="font-size: 1.5em; font-weight: bold; color: #00d4ff;">${count}</div>`;
|
||
}
|
||
|
||
function getPageDisplayName(page) {
|
||
const pageNames = {
|
||
'main_page_visit': '🏠 Hauptseiten-Besuche',
|
||
'home': '🏠 Hauptseite',
|
||
'login': '🔐 Login',
|
||
'dashboard': '📊 Dashboard',
|
||
'admin_login': '🛡️ Admin Login',
|
||
'admin_dashboard': '🛡️ Admin Dashboard',
|
||
'license_generator': '🔧 Lizenzgenerator',
|
||
'reset_password': '🔑 Passwort Reset'
|
||
};
|
||
|
||
return pageNames[page] || page;
|
||
}
|
||
|
||
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/v1/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/v1/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/v1/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/v1/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-warning" onclick="editPlayer('${player.id}')">Bearbeiten</button>
|
||
<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-warning" onclick="editRun('${run.id}')">Bearbeiten</button>
|
||
<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>Mindestzeit (s)</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>${location.time_threshold ? (typeof location.time_threshold === 'object' ? location.time_threshold.seconds : location.time_threshold).toFixed(3) : '-'}</td>
|
||
<td>${new Date(location.created_at).toLocaleDateString('de-DE')}</td>
|
||
<td class="action-buttons">
|
||
<button class="btn btn-small btn-warning" onclick="editLocation('${location.id}')">Bearbeiten</button>
|
||
<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;
|
||
case 'achievements':
|
||
if (currentAchievementMode === 'achievements') {
|
||
currentAchievements = filteredData;
|
||
displayAchievements();
|
||
} else {
|
||
currentPlayers = filteredData;
|
||
displayPlayersWithAchievements();
|
||
}
|
||
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;
|
||
case 'achievements':
|
||
if (currentAchievementMode === 'achievements') {
|
||
loadAchievements();
|
||
} else {
|
||
loadPlayersWithAchievements();
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
|
||
function showAddModal() {
|
||
const modal = document.getElementById('addModal');
|
||
const modalTitle = document.getElementById('modalTitle');
|
||
const formFields = document.getElementById('formFields');
|
||
|
||
// Modal-Titel basierend auf aktuellem Datentyp setzen
|
||
switch (currentDataType) {
|
||
case 'players':
|
||
modalTitle.textContent = 'Neuen Spieler hinzufügen';
|
||
formFields.innerHTML = `
|
||
<div class="form-group">
|
||
<label for="playerName">Vollständiger Name:</label>
|
||
<input type="text" id="playerName" name="full_name" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="playerRfid">RFID UID:</label>
|
||
<input type="text" id="playerRfid" name="rfiduid" placeholder="z.B. 1234567890">
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="playerSupabaseId">Supabase User ID (optional):</label>
|
||
<input type="text" id="playerSupabaseId" name="supabase_user_id" placeholder="UUID">
|
||
</div>
|
||
`;
|
||
break;
|
||
|
||
case 'locations':
|
||
modalTitle.textContent = 'Neuen Standort hinzufügen';
|
||
formFields.innerHTML = `
|
||
<div class="form-group">
|
||
<label for="locationName">Standort-Name:</label>
|
||
<input type="text" id="locationName" name="name" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="locationLat">Breitengrad (Latitude):</label>
|
||
<input type="number" id="locationLat" name="latitude" step="any" required placeholder="z.B. 48.385337">
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="locationLng">Längengrad (Longitude):</label>
|
||
<input type="number" id="locationLng" name="longitude" step="any" required placeholder="z.B. 9.986960">
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="locationThreshold">Zeit-Schwellenwert (optional):</label>
|
||
<input type="number" id="locationThreshold" name="time_threshold" step="0.001" placeholder="z.B. 60.000">
|
||
</div>
|
||
`;
|
||
break;
|
||
|
||
case 'adminusers':
|
||
modalTitle.textContent = 'Neuen Admin-Benutzer hinzufügen';
|
||
formFields.innerHTML = `
|
||
<div class="form-group">
|
||
<label for="adminUsername">Benutzername:</label>
|
||
<input type="text" id="adminUsername" name="username" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="adminPassword">Passwort:</label>
|
||
<input type="password" id="adminPassword" name="password" required minlength="6">
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="adminAccessLevel">Zugriffslevel:</label>
|
||
<select id="adminAccessLevel" name="access_level" required>
|
||
<option value="1">Level 1 - Basis-Admin</option>
|
||
<option value="2">Level 2 - Vollzugriff</option>
|
||
</select>
|
||
</div>
|
||
`;
|
||
break;
|
||
|
||
case 'runs':
|
||
modalTitle.textContent = 'Neuen Lauf hinzufügen';
|
||
formFields.innerHTML = `
|
||
<div class="form-group">
|
||
<label for="runPlayerId">Spieler ID:</label>
|
||
<input type="number" id="runPlayerId" name="player_id" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="runLocationId">Standort ID:</label>
|
||
<input type="number" id="runLocationId" name="location_id" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="runTime">Zeit (Sekunden):</label>
|
||
<input type="number" id="runTime" name="time_seconds" step="0.001" required placeholder="z.B. 60.123">
|
||
</div>
|
||
`;
|
||
break;
|
||
|
||
default:
|
||
modalTitle.textContent = 'Element hinzufügen';
|
||
formFields.innerHTML = '<p>Keine Felder für diesen Datentyp verfügbar.</p>';
|
||
}
|
||
|
||
modal.style.display = 'block';
|
||
}
|
||
|
||
function closeModal() {
|
||
document.querySelectorAll('.modal').forEach(modal => {
|
||
modal.style.display = 'none';
|
||
});
|
||
// Formular zurücksetzen
|
||
document.getElementById('addForm').reset();
|
||
document.getElementById('modalMessage').style.display = 'none';
|
||
}
|
||
|
||
async function handleAddSubmit(e) {
|
||
e.preventDefault();
|
||
|
||
const formData = new FormData(e.target);
|
||
const data = Object.fromEntries(formData.entries());
|
||
|
||
// Prüfen ob es sich um eine Edit-Operation handelt
|
||
const isEdit = data.edit_id;
|
||
const editId = data.edit_id;
|
||
delete data.edit_id; // edit_id aus den Daten entfernen
|
||
|
||
try {
|
||
let endpoint = '';
|
||
let successMessage = '';
|
||
let method = 'POST';
|
||
|
||
switch (currentDataType) {
|
||
case 'players':
|
||
endpoint = isEdit ? `/api/v1/admin/players/${editId}` : '/api/v1/admin/players';
|
||
successMessage = isEdit ? 'Spieler erfolgreich aktualisiert' : 'Spieler erfolgreich hinzugefügt';
|
||
method = isEdit ? 'PUT' : 'POST';
|
||
break;
|
||
case 'locations':
|
||
endpoint = isEdit ? `/api/v1/admin/locations/${editId}` : '/api/v1/admin/locations';
|
||
successMessage = isEdit ? 'Standort erfolgreich aktualisiert' : 'Standort erfolgreich hinzugefügt';
|
||
method = isEdit ? 'PUT' : 'POST';
|
||
break;
|
||
case 'adminusers':
|
||
endpoint = isEdit ? `/api/v1/admin/adminusers/${editId}` : '/api/v1/admin/adminusers';
|
||
successMessage = isEdit ? 'Admin-Benutzer erfolgreich aktualisiert' : 'Admin-Benutzer erfolgreich hinzugefügt';
|
||
method = isEdit ? 'PUT' : 'POST';
|
||
break;
|
||
case 'runs':
|
||
endpoint = isEdit ? `/api/v1/admin/runs/${editId}` : '/api/v1/admin/runs';
|
||
successMessage = isEdit ? 'Lauf erfolgreich aktualisiert' : 'Lauf erfolgreich hinzugefügt';
|
||
method = isEdit ? 'PUT' : 'POST';
|
||
break;
|
||
case 'achievements':
|
||
// Handle achievement form submission
|
||
await handleAchievementSubmit(formData);
|
||
return;
|
||
default:
|
||
showError('Unbekannter Datentyp');
|
||
return;
|
||
}
|
||
|
||
const response = await fetch(endpoint, {
|
||
method: method,
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify(data)
|
||
});
|
||
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
showSuccess(successMessage);
|
||
closeModal();
|
||
refreshData(); // Daten neu laden
|
||
} else {
|
||
showError(result.message || `Fehler beim ${isEdit ? 'Aktualisieren' : 'Hinzufügen'}`);
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('Submit failed:', error);
|
||
showError(`Fehler beim ${isEdit ? 'Aktualisieren' : 'Hinzufügen'}`);
|
||
}
|
||
}
|
||
|
||
// Edit-Funktionen
|
||
async function editPlayer(id) {
|
||
const player = currentData.find(p => p.id == id);
|
||
if (!player) return;
|
||
|
||
const modal = document.getElementById('addModal');
|
||
const modalTitle = document.getElementById('modalTitle');
|
||
const formFields = document.getElementById('formFields');
|
||
|
||
modalTitle.textContent = 'Spieler bearbeiten';
|
||
formFields.innerHTML = `
|
||
<div class="form-group">
|
||
<label for="playerName">Vollständiger Name:</label>
|
||
<input type="text" id="playerName" name="full_name" value="${player.full_name || ''}" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="playerRfid">RFID UID:</label>
|
||
<input type="text" id="playerRfid" name="rfiduid" value="${player.rfiduid || ''}" placeholder="z.B. 1234567890">
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="playerSupabaseId">Supabase User ID (optional):</label>
|
||
<input type="text" id="playerSupabaseId" name="supabase_user_id" value="${player.supabase_user_id || ''}" placeholder="UUID">
|
||
</div>
|
||
<input type="hidden" name="edit_id" value="${player.id}">
|
||
`;
|
||
|
||
modal.style.display = 'block';
|
||
}
|
||
|
||
async function editLocation(id) {
|
||
const location = currentData.find(l => l.id == id);
|
||
if (!location) return;
|
||
|
||
const modal = document.getElementById('addModal');
|
||
const modalTitle = document.getElementById('modalTitle');
|
||
const formFields = document.getElementById('formFields');
|
||
|
||
modalTitle.textContent = 'Standort bearbeiten';
|
||
formFields.innerHTML = `
|
||
<div class="form-group">
|
||
<label for="locationName">Standort-Name:</label>
|
||
<input type="text" id="locationName" name="name" value="${location.name}" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="locationLat">Breitengrad (Latitude):</label>
|
||
<input type="number" id="locationLat" name="latitude" step="any" value="${location.latitude}" required placeholder="z.B. 48.385337">
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="locationLng">Längengrad (Longitude):</label>
|
||
<input type="number" id="locationLng" name="longitude" step="any" value="${location.longitude}" required placeholder="z.B. 9.986960">
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="locationThreshold">Zeit-Schwellenwert (optional):</label>
|
||
<input type="number" id="locationThreshold" name="time_threshold" step="0.001" value="${location.time_threshold || ''}" placeholder="z.B. 60.000">
|
||
</div>
|
||
<input type="hidden" name="edit_id" value="${location.id}">
|
||
`;
|
||
|
||
modal.style.display = 'block';
|
||
}
|
||
|
||
async function editRun(id) {
|
||
try {
|
||
// Lade die spezifischen Lauf-Daten direkt von der API
|
||
const response = await fetch(`/api/admin-runs/${id}`);
|
||
const result = await response.json();
|
||
|
||
if (!result.success) {
|
||
showError('Fehler beim Laden der Lauf-Daten: ' + (result.message || 'Unbekannter Fehler'));
|
||
return;
|
||
}
|
||
|
||
const run = result.data;
|
||
if (!run) {
|
||
showError('Lauf nicht gefunden');
|
||
return;
|
||
}
|
||
|
||
console.log('Editing run:', run); // Debug log
|
||
|
||
const modal = document.getElementById('addModal');
|
||
const modalTitle = document.getElementById('modalTitle');
|
||
const formFields = document.getElementById('formFields');
|
||
|
||
modalTitle.textContent = 'Lauf bearbeiten';
|
||
formFields.innerHTML = `
|
||
<div class="form-group">
|
||
<label for="runPlayerId">Spieler ID:</label>
|
||
<input type="text" id="runPlayerId" name="player_id" value="${run.player_id || ''}" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="runLocationId">Standort ID:</label>
|
||
<input type="text" id="runLocationId" name="location_id" value="${run.location_id || ''}" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="runTime">Zeit (Sekunden):</label>
|
||
<input type="number" id="runTime" name="time_seconds" step="0.001" value="${parseFloat(run.time_seconds || 0)}" required placeholder="z.B. 60.123">
|
||
</div>
|
||
<input type="hidden" name="edit_id" value="${run.id}">
|
||
`;
|
||
|
||
modal.style.display = 'block';
|
||
|
||
} catch (error) {
|
||
console.error('Error loading run data:', error);
|
||
showError('Fehler beim Laden der Lauf-Daten');
|
||
}
|
||
}
|
||
|
||
async function deletePlayer(id) {
|
||
if (await confirmDelete(`Spieler mit ID ${id} löschen?`)) {
|
||
try {
|
||
const response = await fetch(`/api/v1/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/v1/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/v1/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/v1/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);
|
||
}
|
||
|
||
// ============================================================================
|
||
// BLACKLIST MANAGEMENT FUNCTIONS
|
||
// ============================================================================
|
||
|
||
async function showBlacklistManagement() {
|
||
currentDataType = 'blacklist';
|
||
document.getElementById('blacklistModal').style.display = 'block';
|
||
await loadBlacklist();
|
||
await loadBlacklistStats();
|
||
}
|
||
|
||
async function loadBlacklist() {
|
||
try {
|
||
const response = await fetch('/api/v1/admin/blacklist');
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
displayBlacklist(result.data);
|
||
} else {
|
||
showBlacklistMessage('Fehler beim Laden der Blacklist: ' + result.message, 'error');
|
||
}
|
||
} catch (error) {
|
||
console.error('Error loading blacklist:', error);
|
||
showBlacklistMessage('Fehler beim Laden der Blacklist', 'error');
|
||
}
|
||
}
|
||
|
||
function displayBlacklist(blacklist) {
|
||
const content = document.getElementById('blacklistContent');
|
||
|
||
let html = '<h3 style="color: #ffffff; margin-bottom: 1rem;">📋 Blacklist-Inhalte</h3>';
|
||
|
||
Object.entries(blacklist).forEach(([category, terms]) => {
|
||
const categoryName = getCategoryDisplayName(category);
|
||
const categoryIcon = getCategoryIcon(category);
|
||
|
||
html += `
|
||
<div style="border: 1px solid #334155; padding: 1rem; margin-bottom: 1rem; border-radius: 8px; background: rgba(15, 23, 42, 0.3);">
|
||
<h4 style="color: #ffffff; margin-bottom: 0.75rem; display: flex; align-items: center; gap: 0.5rem;">
|
||
${categoryIcon} ${categoryName}
|
||
<span style="background: #1e293b; color: #8892b0; padding: 0.25rem 0.5rem; border-radius: 12px; font-size: 0.8rem;">
|
||
${terms.length} Einträge
|
||
</span>
|
||
</h4>
|
||
<div style="display: flex; flex-wrap: wrap; gap: 0.5rem;">
|
||
`;
|
||
|
||
if (terms.length === 0) {
|
||
html += `
|
||
<div style="color: #64748b; font-style: italic; padding: 1rem; text-align: center; width: 100%;">
|
||
Keine Einträge in dieser Kategorie
|
||
</div>
|
||
`;
|
||
} else {
|
||
terms.forEach(term => {
|
||
html += `
|
||
<span style="background: #1e293b; color: #e2e8f0; padding: 0.375rem 0.75rem; border-radius: 6px; display: flex; align-items: center; gap: 0.5rem; border: 1px solid #334155;">
|
||
<span style="font-family: monospace; font-size: 0.9rem;">${term}</span>
|
||
<button onclick="removeFromBlacklist('${term}', '${category}')"
|
||
style="background: #dc2626; color: white; border: none; border-radius: 50%; width: 20px; height: 20px; cursor: pointer; font-size: 12px; display: flex; align-items: center; justify-content: center; transition: background-color 0.2s;"
|
||
onmouseover="this.style.backgroundColor='#b91c1c'"
|
||
onmouseout="this.style.backgroundColor='#dc2626'"
|
||
title="Entfernen">
|
||
×
|
||
</button>
|
||
</span>
|
||
`;
|
||
});
|
||
}
|
||
|
||
html += `
|
||
</div>
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
content.innerHTML = html;
|
||
}
|
||
|
||
// Kategorie-Icons hinzufügen
|
||
function getCategoryIcon(category) {
|
||
const icons = {
|
||
historical: '🏛️',
|
||
offensive: '⚠️',
|
||
titles: '👑',
|
||
brands: '🏷️',
|
||
inappropriate: '🚫',
|
||
racial: '🌍',
|
||
religious: '⛪',
|
||
disability: '♿',
|
||
leetspeak: '🔤',
|
||
cyberbullying: '💻',
|
||
drugs: '💊',
|
||
violence: '⚔️'
|
||
};
|
||
return icons[category] || '📝';
|
||
}
|
||
|
||
function getCategoryDisplayName(category) {
|
||
const names = {
|
||
historical: 'Historisch belastet',
|
||
offensive: 'Beleidigend/anstößig',
|
||
titles: 'Titel/Berufsbezeichnung',
|
||
brands: 'Markenname',
|
||
inappropriate: 'Unpassend',
|
||
racial: 'Rassistisch/ethnisch',
|
||
religious: 'Religiös beleidigend',
|
||
disability: 'Behinderungsbezogen',
|
||
leetspeak: 'Verschleiert',
|
||
cyberbullying: 'Cyberbullying',
|
||
drugs: 'Drogenbezogen',
|
||
violence: 'Gewalt/Bedrohung'
|
||
};
|
||
return names[category] || category;
|
||
}
|
||
|
||
async function testNameAgainstBlacklist() {
|
||
const firstname = document.getElementById('testFirstname').value.trim();
|
||
const lastname = document.getElementById('testLastname').value.trim();
|
||
const resultDiv = document.getElementById('testResult');
|
||
|
||
if (!firstname || !lastname) {
|
||
showBlacklistMessage('Bitte gib Vorname und Nachname ein', 'error');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const response = await fetch('/api/v1/admin/blacklist/test', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({ firstname, lastname })
|
||
});
|
||
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
const testResult = result.data;
|
||
|
||
if (testResult.isBlocked) {
|
||
let matchTypeText = '';
|
||
let similarityText = '';
|
||
|
||
if (testResult.matchType === 'exact') {
|
||
matchTypeText = 'Exakte Übereinstimmung';
|
||
} else if (testResult.matchType === 'similar') {
|
||
matchTypeText = 'Ähnliche Übereinstimmung (Levenshtein)';
|
||
const similarityPercent = Math.round((1 - testResult.similarity) * 100);
|
||
similarityText = `<br>Ähnlichkeit: ${similarityPercent}% (Distanz: ${testResult.levenshteinDistance})`;
|
||
}
|
||
|
||
resultDiv.innerHTML = `
|
||
<div style="background: #ffebee; color: #c62828; padding: 0.5rem; border-radius: 3px;">
|
||
<strong>❌ Name ist blockiert</strong><br>
|
||
Grund: ${testResult.reason}<br>
|
||
Kategorie: ${getCategoryDisplayName(testResult.category)}<br>
|
||
Gefundener Begriff: "${testResult.matchedTerm}"<br>
|
||
Typ: ${matchTypeText}${similarityText}
|
||
</div>
|
||
`;
|
||
} else {
|
||
resultDiv.innerHTML = `
|
||
<div style="background: #e8f5e8; color: #2e7d32; padding: 0.5rem; border-radius: 3px;">
|
||
<strong>✅ Name ist erlaubt</strong><br>
|
||
Der Name "${firstname} ${lastname}" ist nicht in der Blacklist.
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
resultDiv.style.display = 'block';
|
||
} else {
|
||
showBlacklistMessage('Fehler beim Testen: ' + result.message, 'error');
|
||
}
|
||
} catch (error) {
|
||
console.error('Error testing name:', error);
|
||
showBlacklistMessage('Fehler beim Testen des Namens', 'error');
|
||
}
|
||
}
|
||
|
||
// Detailed Levenshtein test function
|
||
async function testLevenshteinDetailed() {
|
||
const firstname = document.getElementById('testFirstname').value.trim();
|
||
const lastname = document.getElementById('testLastname').value.trim();
|
||
const resultDiv = document.getElementById('testResult');
|
||
|
||
if (!firstname || !lastname) {
|
||
showBlacklistMessage('Bitte gib Vorname und Nachname ein', 'error');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const response = await fetch('/api/v1/admin/blacklist/levenshtein-test', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({ firstname, lastname, threshold: 0.3 })
|
||
});
|
||
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
const data = result.data;
|
||
let html = `
|
||
<div style="background: #f3e5f5; color: #4a148c; padding: 1rem; border-radius: 5px; margin-top: 1rem;">
|
||
<h4>🔍 Detaillierte Levenshtein-Analyse</h4>
|
||
<p><strong>Getesteter Name:</strong> ${data.input.firstname} ${data.input.lastname}</p>
|
||
<p><strong>Schwellenwert:</strong> ${data.threshold}</p>
|
||
<hr style="margin: 1rem 0;">
|
||
`;
|
||
|
||
// Show results for each category
|
||
for (const [category, categoryResult] of Object.entries(data.results)) {
|
||
if (categoryResult.hasSimilarTerms && categoryResult.similarTerms.length > 0) {
|
||
html += `
|
||
<div style="margin-bottom: 1rem; padding: 0.5rem; background: #fff3e0; border-left: 4px solid #ff9800; border-radius: 3px;">
|
||
<h5>📁 ${getCategoryDisplayName(category)} (Schwellenwert: ${categoryResult.categoryThreshold})</h5>
|
||
`;
|
||
|
||
categoryResult.similarTerms.forEach(term => {
|
||
const similarityPercent = Math.round((1 - term.distance) * 100);
|
||
html += `
|
||
<div style="margin: 0.5rem 0; padding: 0.5rem; background: white; border-radius: 3px;">
|
||
<strong>Begriff:</strong> "${term.term}"<br>
|
||
<strong>Levenshtein-Distanz:</strong> ${term.levenshteinDistance}<br>
|
||
<strong>Normalisierte Distanz:</strong> ${term.distance.toFixed(4)}<br>
|
||
<strong>Ähnlichkeit:</strong> ${similarityPercent}%<br>
|
||
<strong>Match-Typ:</strong> ${term.matchType || 'similar'}
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
html += `</div>`;
|
||
}
|
||
}
|
||
|
||
// If no matches found
|
||
if (!Object.values(data.results).some(r => r.hasSimilarTerms)) {
|
||
html += `
|
||
<div style="background: #e8f5e8; color: #2e7d32; padding: 1rem; border-radius: 3px;">
|
||
<strong>✅ Keine ähnlichen Begriffe gefunden</strong><br>
|
||
Der Name "${data.input.firstname} ${data.input.lastname}" hat keine ähnlichen Begriffe in der Blacklist.
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
html += `</div>`;
|
||
resultDiv.innerHTML = html;
|
||
resultDiv.style.display = 'block';
|
||
} else {
|
||
showBlacklistMessage(result.message || 'Fehler beim Testen', 'error');
|
||
}
|
||
} catch (error) {
|
||
console.error('Error testing Levenshtein:', error);
|
||
showBlacklistMessage('Fehler beim Testen der Levenshtein-Distanz', 'error');
|
||
}
|
||
}
|
||
|
||
async function addToBlacklist() {
|
||
const term = document.getElementById('newTerm').value.trim();
|
||
const category = document.getElementById('newCategory').value;
|
||
|
||
if (!term) {
|
||
showBlacklistMessage('Bitte gib einen Begriff ein', 'error');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const response = await fetch('/api/v1/admin/blacklist', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({ term, category })
|
||
});
|
||
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
showBlacklistMessage('Begriff erfolgreich hinzugefügt', 'success');
|
||
document.getElementById('newTerm').value = '';
|
||
await loadBlacklist();
|
||
await loadBlacklistStats();
|
||
} else {
|
||
showBlacklistMessage('Fehler beim Hinzufügen: ' + result.message, 'error');
|
||
}
|
||
} catch (error) {
|
||
console.error('Error adding to blacklist:', error);
|
||
showBlacklistMessage('Fehler beim Hinzufügen zur Blacklist', 'error');
|
||
}
|
||
}
|
||
|
||
async function removeFromBlacklist(term, category) {
|
||
if (!confirm(`Möchtest du "${term}" aus der Kategorie "${getCategoryDisplayName(category)}" entfernen?`)) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const response = await fetch('/api/v1/admin/blacklist', {
|
||
method: 'DELETE',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({ term, category })
|
||
});
|
||
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
showBlacklistMessage('Begriff erfolgreich entfernt', 'success');
|
||
await loadBlacklist();
|
||
await loadBlacklistStats();
|
||
} else {
|
||
showBlacklistMessage('Fehler beim Entfernen: ' + result.message, 'error');
|
||
}
|
||
} catch (error) {
|
||
console.error('Error removing from blacklist:', error);
|
||
showBlacklistMessage('Fehler beim Entfernen aus der Blacklist', 'error');
|
||
}
|
||
}
|
||
|
||
function showBlacklistMessage(message, type) {
|
||
const messageDiv = document.getElementById('blacklistMessage');
|
||
messageDiv.textContent = message;
|
||
messageDiv.className = `message ${type}`;
|
||
messageDiv.style.display = 'block';
|
||
setTimeout(() => {
|
||
messageDiv.style.display = 'none';
|
||
}, 3000);
|
||
}
|
||
|
||
// Blacklist-Statistiken laden
|
||
async function loadBlacklistStats() {
|
||
try {
|
||
const response = await fetch('/api/v1/admin/blacklist/stats');
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
displayBlacklistStats(result.data);
|
||
} else {
|
||
console.error('Error loading blacklist stats:', result.message);
|
||
}
|
||
} catch (error) {
|
||
console.error('Error loading blacklist stats:', error);
|
||
}
|
||
}
|
||
|
||
// Blacklist-Statistiken anzeigen
|
||
function displayBlacklistStats(stats) {
|
||
// Erstelle oder aktualisiere Statistiken-Bereich
|
||
let statsDiv = document.getElementById('blacklistStats');
|
||
if (!statsDiv) {
|
||
// Erstelle Statistiken-Bereich am Anfang des Modals
|
||
const modalContent = document.querySelector('#blacklistModal .modal-content');
|
||
const firstChild = modalContent.firstChild;
|
||
statsDiv = document.createElement('div');
|
||
statsDiv.id = 'blacklistStats';
|
||
statsDiv.style.cssText = 'border: 1px solid #4ade80; padding: 1rem; margin-bottom: 1rem; border-radius: 5px; background: rgba(34, 197, 94, 0.1);';
|
||
modalContent.insertBefore(statsDiv, firstChild);
|
||
}
|
||
|
||
let html = `
|
||
<h4 style="color: #4ade80; margin-bottom: 1rem;">📊 Blacklist-Statistiken</h4>
|
||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 1rem; margin-bottom: 1rem;">
|
||
`;
|
||
|
||
stats.categories.forEach(category => {
|
||
const categoryName = getCategoryDisplayName(category.category);
|
||
const lastAdded = new Date(category.last_added).toLocaleDateString('de-DE');
|
||
html += `
|
||
<div style="background: rgba(15, 23, 42, 0.5); padding: 0.75rem; border-radius: 5px; text-align: center;">
|
||
<div style="font-size: 1.5rem; font-weight: bold; color: #4ade80;">${category.count}</div>
|
||
<div style="font-size: 0.9rem; color: #8892b0;">${categoryName}</div>
|
||
<div style="font-size: 0.8rem; color: #64748b;">Letzte: ${lastAdded}</div>
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
html += `
|
||
</div>
|
||
<div style="text-align: center; padding: 0.5rem; background: rgba(15, 23, 42, 0.3); border-radius: 5px;">
|
||
<strong style="color: #4ade80;">Gesamt: ${stats.total} Begriffe</strong>
|
||
</div>
|
||
`;
|
||
|
||
statsDiv.innerHTML = html;
|
||
}
|
||
|
||
// ==================== ACHIEVEMENT MANAGEMENT ====================
|
||
|
||
let currentAchievementMode = 'achievements'; // 'achievements' or 'players'
|
||
let currentAchievements = [];
|
||
let currentPlayers = [];
|
||
|
||
// Show achievement management
|
||
async function showAchievementManagement() {
|
||
currentDataType = 'achievements';
|
||
currentAchievementMode = 'achievements';
|
||
|
||
document.getElementById('dataTitle').textContent = '🏆 Achievement-Verwaltung';
|
||
document.getElementById('dataSection').style.display = 'block';
|
||
|
||
// Update search placeholder
|
||
document.getElementById('searchInput').placeholder = 'Achievements durchsuchen...';
|
||
|
||
await loadAchievements();
|
||
}
|
||
|
||
// Load all achievements
|
||
async function loadAchievements() {
|
||
try {
|
||
const response = await fetch('/api/v1/admin/achievements');
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
currentAchievements = result.data;
|
||
currentData = result.data; // Set for filtering
|
||
displayAchievements();
|
||
} else {
|
||
showError('Fehler beim Laden der Achievements: ' + result.message);
|
||
}
|
||
} catch (error) {
|
||
console.error('Error loading achievements:', error);
|
||
showError('Fehler beim Laden der Achievements');
|
||
}
|
||
}
|
||
|
||
// Display achievements in table
|
||
function displayAchievements() {
|
||
const content = document.getElementById('dataContent');
|
||
|
||
if (currentAchievements.length === 0) {
|
||
content.innerHTML = '<div class="no-data">Keine Achievements gefunden</div>';
|
||
return;
|
||
}
|
||
|
||
let html = `
|
||
<div class="achievement-controls">
|
||
<button class="btn btn-success" onclick="showAddAchievementModal()">➕ Neues Achievement</button>
|
||
<button class="btn" onclick="toggleAchievementMode()">👥 Spieler-Ansicht</button>
|
||
</div>
|
||
<div class="table-container">
|
||
<table class="data-table">
|
||
<thead>
|
||
<tr>
|
||
<th>Status</th>
|
||
<th>Icon</th>
|
||
<th>Name</th>
|
||
<th>Kategorie</th>
|
||
<th>Bedingung</th>
|
||
<th>Punkte</th>
|
||
<th>Mehrmals</th>
|
||
<th>Aktionen</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
`;
|
||
|
||
currentAchievements.forEach(achievement => {
|
||
const statusClass = achievement.is_active ? 'active' : 'inactive';
|
||
const statusText = achievement.is_active ? 'Aktiv' : 'Inaktiv';
|
||
const multipleText = achievement.can_be_earned_multiple_times ? 'Ja' : 'Nein';
|
||
|
||
html += `
|
||
<tr>
|
||
<td><span class="status-badge ${statusClass}">${statusText}</span></td>
|
||
<td>${achievement.icon || '🏆'}</td>
|
||
<td>
|
||
<strong>${achievement.name}</strong>
|
||
${achievement.name_en ? `<br><small>${achievement.name_en}</small>` : ''}
|
||
</td>
|
||
<td>${achievement.category}</td>
|
||
<td>${achievement.condition_type}: ${achievement.condition_value}</td>
|
||
<td>${achievement.points}</td>
|
||
<td>${multipleText}</td>
|
||
<td>
|
||
<button class="btn btn-sm" onclick="editAchievement('${achievement.id}')">✏️</button>
|
||
<button class="btn btn-sm btn-danger" onclick="deleteAchievement('${achievement.id}', '${achievement.name}')">🗑️</button>
|
||
</td>
|
||
</tr>
|
||
`;
|
||
});
|
||
|
||
html += `
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
`;
|
||
|
||
content.innerHTML = html;
|
||
}
|
||
|
||
// Handle achievement form submission
|
||
async function handleAchievementSubmit(formData) {
|
||
const isEdit = formData.has('achievement_id');
|
||
const url = isEdit ?
|
||
`/api/v1/admin/achievements/${formData.get('achievement_id')}` :
|
||
'/api/v1/admin/achievements';
|
||
const method = isEdit ? 'PUT' : 'POST';
|
||
|
||
const data = {
|
||
name: formData.get('name'),
|
||
name_en: formData.get('name_en') || null,
|
||
description: formData.get('description'),
|
||
description_en: formData.get('description_en') || null,
|
||
category: formData.get('category'),
|
||
condition_type: formData.get('condition_type'),
|
||
condition_value: parseInt(formData.get('condition_value')),
|
||
icon: formData.get('icon') || '🏆',
|
||
points: parseInt(formData.get('points')) || 10,
|
||
is_active: formData.has('is_active'),
|
||
can_be_earned_multiple_times: formData.has('can_be_earned_multiple_times')
|
||
};
|
||
|
||
try {
|
||
const response = await fetch(url, {
|
||
method: method,
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify(data)
|
||
});
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
showSuccess(result.message);
|
||
closeModal();
|
||
await loadAchievements();
|
||
} else {
|
||
showError('Fehler beim Speichern: ' + result.message);
|
||
}
|
||
} catch (error) {
|
||
console.error('Error saving achievement:', error);
|
||
showError('Fehler beim Speichern des Achievements');
|
||
}
|
||
}
|
||
|
||
// Toggle between achievements and players view
|
||
async function toggleAchievementMode() {
|
||
if (currentAchievementMode === 'achievements') {
|
||
currentAchievementMode = 'players';
|
||
document.getElementById('dataTitle').textContent = '👥 Spieler-Achievements';
|
||
document.getElementById('searchInput').placeholder = 'Spieler durchsuchen...';
|
||
await loadPlayersWithAchievements();
|
||
} else {
|
||
currentAchievementMode = 'achievements';
|
||
document.getElementById('dataTitle').textContent = '🏆 Achievement-Verwaltung';
|
||
document.getElementById('searchInput').placeholder = 'Achievements durchsuchen...';
|
||
await loadAchievements();
|
||
}
|
||
}
|
||
|
||
// Load all players with achievement statistics
|
||
async function loadPlayersWithAchievements() {
|
||
try {
|
||
const response = await fetch('/api/v1/admin/achievements/players');
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
currentPlayers = result.data;
|
||
currentData = result.data; // Set for filtering
|
||
displayPlayersWithAchievements();
|
||
} else {
|
||
showError('Fehler beim Laden der Spieler: ' + result.message);
|
||
}
|
||
} catch (error) {
|
||
console.error('Error loading players:', error);
|
||
showError('Fehler beim Laden der Spieler');
|
||
}
|
||
}
|
||
|
||
// Display players in table
|
||
function displayPlayersWithAchievements() {
|
||
const content = document.getElementById('dataContent');
|
||
|
||
if (currentPlayers.length === 0) {
|
||
content.innerHTML = '<div class="no-data">Keine Spieler gefunden</div>';
|
||
return;
|
||
}
|
||
|
||
let html = `
|
||
<div class="achievement-controls">
|
||
<button class="btn" onclick="toggleAchievementMode()">🏆 Achievement-Ansicht</button>
|
||
</div>
|
||
<div class="table-container">
|
||
<table class="data-table">
|
||
<thead>
|
||
<tr>
|
||
<th>Spieler</th>
|
||
<th>Abgeschlossen</th>
|
||
<th>Gesamt</th>
|
||
<th>Fortschritt</th>
|
||
<th>Punkte</th>
|
||
<th>Aktionen</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
`;
|
||
|
||
currentPlayers.forEach(player => {
|
||
const progressPercentage = player.completion_percentage || 0;
|
||
const progressBar = `
|
||
<div class="progress-bar">
|
||
<div class="progress-fill" style="width: ${progressPercentage}%"></div>
|
||
<span class="progress-text">${progressPercentage}%</span>
|
||
</div>
|
||
`;
|
||
|
||
html += `
|
||
<tr>
|
||
<td><strong>${player.firstname} ${player.lastname}</strong></td>
|
||
<td>${player.completed_achievements}</td>
|
||
<td>${player.total_achievements}</td>
|
||
<td>${progressBar}</td>
|
||
<td>${player.total_points}</td>
|
||
<td>
|
||
<button class="btn btn-sm" onclick="viewPlayerAchievements('${player.id}', '${player.firstname} ${player.lastname}')">👁️</button>
|
||
</td>
|
||
</tr>
|
||
`;
|
||
});
|
||
|
||
html += `
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
`;
|
||
|
||
content.innerHTML = html;
|
||
}
|
||
|
||
// Show add achievement modal
|
||
function showAddAchievementModal() {
|
||
const modal = document.getElementById('addModal');
|
||
const modalTitle = document.getElementById('modalTitle');
|
||
const formFields = document.getElementById('formFields');
|
||
|
||
modalTitle.textContent = 'Neues Achievement erstellen';
|
||
|
||
formFields.innerHTML = `
|
||
<div class="form-group">
|
||
<label for="achievement_name">Name *</label>
|
||
<input type="text" id="achievement_name" name="name" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="achievement_name_en">Name (Englisch)</label>
|
||
<input type="text" id="achievement_name_en" name="name_en">
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="achievement_description">Beschreibung *</label>
|
||
<textarea id="achievement_description" name="description" required></textarea>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="achievement_description_en">Beschreibung (Englisch)</label>
|
||
<textarea id="achievement_description_en" name="description_en"></textarea>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="achievement_category">Kategorie *</label>
|
||
<select id="achievement_category" name="category" required>
|
||
<option value="">Kategorie wählen</option>
|
||
<option value="consistency">Konsistenz</option>
|
||
<option value="improvement">Verbesserung</option>
|
||
<option value="seasonal">Saisonal</option>
|
||
<option value="monthly">Monatlich</option>
|
||
<option value="best_time">Beste Zeit</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="achievement_condition_type">Bedingungstyp *</label>
|
||
<input type="text" id="achievement_condition_type" name="condition_type" required placeholder="z.B. first_time, attempts_per_day">
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="achievement_condition_value">Bedingungswert *</label>
|
||
<input type="number" id="achievement_condition_value" name="condition_value" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="achievement_icon">Icon</label>
|
||
<input type="text" id="achievement_icon" name="icon" value="🏆">
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="achievement_points">Punkte</label>
|
||
<input type="number" id="achievement_points" name="points" value="10">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>
|
||
<input type="checkbox" id="achievement_is_active" name="is_active" checked>
|
||
Aktiv
|
||
</label>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>
|
||
<input type="checkbox" id="achievement_multiple" name="can_be_earned_multiple_times">
|
||
Kann mehrmals erreicht werden
|
||
</label>
|
||
</div>
|
||
`;
|
||
|
||
modal.style.display = 'block';
|
||
}
|
||
|
||
// Edit achievement
|
||
async function editAchievement(achievementId) {
|
||
const achievement = currentAchievements.find(a => a.id === achievementId);
|
||
if (!achievement) return;
|
||
|
||
const modal = document.getElementById('addModal');
|
||
const modalTitle = document.getElementById('modalTitle');
|
||
const formFields = document.getElementById('formFields');
|
||
|
||
modalTitle.textContent = 'Achievement bearbeiten';
|
||
|
||
formFields.innerHTML = `
|
||
<input type="hidden" id="achievement_id" value="${achievement.id}">
|
||
<div class="form-group">
|
||
<label for="achievement_name">Name *</label>
|
||
<input type="text" id="achievement_name" name="name" value="${achievement.name}" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="achievement_name_en">Name (Englisch)</label>
|
||
<input type="text" id="achievement_name_en" name="name_en" value="${achievement.name_en || ''}">
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="achievement_description">Beschreibung *</label>
|
||
<textarea id="achievement_description" name="description" required>${achievement.description}</textarea>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="achievement_description_en">Beschreibung (Englisch)</label>
|
||
<textarea id="achievement_description_en" name="description_en">${achievement.description_en || ''}</textarea>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="achievement_category">Kategorie *</label>
|
||
<select id="achievement_category" name="category" required>
|
||
<option value="consistency" ${achievement.category === 'consistency' ? 'selected' : ''}>Konsistenz</option>
|
||
<option value="improvement" ${achievement.category === 'improvement' ? 'selected' : ''}>Verbesserung</option>
|
||
<option value="seasonal" ${achievement.category === 'seasonal' ? 'selected' : ''}>Saisonal</option>
|
||
<option value="monthly" ${achievement.category === 'monthly' ? 'selected' : ''}>Monatlich</option>
|
||
<option value="best_time" ${achievement.category === 'best_time' ? 'selected' : ''}>Beste Zeit</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="achievement_condition_type">Bedingungstyp *</label>
|
||
<input type="text" id="achievement_condition_type" name="condition_type" value="${achievement.condition_type}" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="achievement_condition_value">Bedingungswert *</label>
|
||
<input type="number" id="achievement_condition_value" name="condition_value" value="${achievement.condition_value}" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="achievement_icon">Icon</label>
|
||
<input type="text" id="achievement_icon" name="icon" value="${achievement.icon || '🏆'}">
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="achievement_points">Punkte</label>
|
||
<input type="number" id="achievement_points" name="points" value="${achievement.points}">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>
|
||
<input type="checkbox" id="achievement_is_active" name="is_active" ${achievement.is_active ? 'checked' : ''}>
|
||
Aktiv
|
||
</label>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>
|
||
<input type="checkbox" id="achievement_multiple" name="can_be_earned_multiple_times" ${achievement.can_be_earned_multiple_times ? 'checked' : ''}>
|
||
Kann mehrmals erreicht werden
|
||
</label>
|
||
</div>
|
||
`;
|
||
|
||
modal.style.display = 'block';
|
||
}
|
||
|
||
// Delete achievement
|
||
function deleteAchievement(achievementId, achievementName) {
|
||
document.getElementById('confirmMessage').textContent = `Möchten Sie das Achievement "${achievementName}" wirklich deaktivieren?`;
|
||
document.getElementById('confirmYes').onclick = () => confirmDeleteAchievement(achievementId);
|
||
document.getElementById('confirmModal').style.display = 'block';
|
||
}
|
||
|
||
// Confirm delete achievement
|
||
async function confirmDeleteAchievement(achievementId) {
|
||
try {
|
||
const response = await fetch(`/api/v1/admin/achievements/${achievementId}`, {
|
||
method: 'DELETE'
|
||
});
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
showSuccess(result.message);
|
||
closeModal();
|
||
await loadAchievements();
|
||
} else {
|
||
showError('Fehler beim Löschen: ' + result.message);
|
||
}
|
||
} catch (error) {
|
||
console.error('Error deleting achievement:', error);
|
||
showError('Fehler beim Löschen des Achievements');
|
||
}
|
||
}
|
||
|
||
// View player achievements
|
||
async function viewPlayerAchievements(playerId, playerName) {
|
||
try {
|
||
const response = await fetch(`/api/v1/admin/achievements/players/${playerId}`);
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
showPlayerAchievementsModal(result.player, result.data);
|
||
} else {
|
||
showError('Fehler beim Laden der Spieler-Achievements: ' + result.message);
|
||
}
|
||
} catch (error) {
|
||
console.error('Error loading player achievements:', error);
|
||
showError('Fehler beim Laden der Spieler-Achievements');
|
||
}
|
||
}
|
||
|
||
// Show player achievements modal
|
||
function showPlayerAchievementsModal(player, achievements) {
|
||
const modal = document.getElementById('addModal');
|
||
const modalTitle = document.getElementById('modalTitle');
|
||
const formFields = document.getElementById('formFields');
|
||
|
||
modalTitle.textContent = `Achievements von ${player.firstname} ${player.lastname}`;
|
||
|
||
let html = `
|
||
<div class="player-achievements">
|
||
<div class="achievement-stats">
|
||
<div class="stat-item">
|
||
<strong>Abgeschlossen:</strong> ${achievements.filter(a => a.is_completed).length} / ${achievements.length}
|
||
</div>
|
||
<div class="stat-item">
|
||
<strong>Gesamtpunkte:</strong> ${achievements.filter(a => a.is_completed).reduce((sum, a) => sum + a.points, 0)}
|
||
</div>
|
||
</div>
|
||
<div class="achievements-list">
|
||
`;
|
||
|
||
achievements.forEach(achievement => {
|
||
const statusClass = achievement.is_completed ? 'completed' : 'not-completed';
|
||
const statusIcon = achievement.is_completed ? '✅' : '❌';
|
||
const completionCount = achievement.completion_count || 0;
|
||
|
||
html += `
|
||
<div class="achievement-item ${statusClass}">
|
||
<div class="achievement-header">
|
||
<span class="achievement-icon">${achievement.icon}</span>
|
||
<span class="achievement-name">${achievement.name}</span>
|
||
<span class="achievement-status">${statusIcon}</span>
|
||
</div>
|
||
<div class="achievement-details">
|
||
<p>${achievement.description}</p>
|
||
<div class="achievement-meta">
|
||
<span>Kategorie: ${achievement.category}</span>
|
||
<span>Punkte: ${achievement.points}</span>
|
||
${achievement.is_completed ? `<span>Erreicht: ${new Date(achievement.earned_at).toLocaleDateString('de-DE')}</span>` : ''}
|
||
${completionCount > 1 ? `<span>Anzahl: ${completionCount}</span>` : ''}
|
||
</div>
|
||
<div class="achievement-actions">
|
||
${!achievement.is_completed ?
|
||
`<button class="btn btn-sm btn-success" onclick="awardAchievement('${player.id}', '${achievement.id}', '${achievement.name}')">Vergeben</button>` :
|
||
`<button class="btn btn-sm btn-danger" onclick="revokeAchievement('${player.id}', '${achievement.id}', '${achievement.name}')">Entfernen</button>`
|
||
}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
html += `
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
formFields.innerHTML = html;
|
||
modal.style.display = 'block';
|
||
}
|
||
|
||
// Award achievement to player
|
||
async function awardAchievement(playerId, achievementId, achievementName) {
|
||
try {
|
||
const response = await fetch(`/api/v1/admin/achievements/players/${playerId}/award`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({
|
||
achievement_id: achievementId,
|
||
progress: 1
|
||
})
|
||
});
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
showSuccess(result.message);
|
||
// Refresh player achievements
|
||
await viewPlayerAchievements(playerId, '');
|
||
} else {
|
||
showError('Fehler beim Vergeben: ' + result.message);
|
||
}
|
||
} catch (error) {
|
||
console.error('Error awarding achievement:', error);
|
||
showError('Fehler beim Vergeben des Achievements');
|
||
}
|
||
}
|
||
|
||
// Revoke achievement from player
|
||
async function revokeAchievement(playerId, achievementId, achievementName) {
|
||
if (!confirm(`Möchten Sie das Achievement "${achievementName}" wirklich entfernen?`)) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const response = await fetch(`/api/v1/admin/achievements/players/${playerId}/revoke`, {
|
||
method: 'DELETE',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({
|
||
achievement_id: achievementId
|
||
})
|
||
});
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
showSuccess(result.message);
|
||
// Refresh player achievements
|
||
await viewPlayerAchievements(playerId, '');
|
||
} else {
|
||
showError('Fehler beim Entfernen: ' + result.message);
|
||
}
|
||
} catch (error) {
|
||
console.error('Error revoking achievement:', error);
|
||
showError('Fehler beim Entfernen des Achievements');
|
||
}
|
||
}
|
||
|