Files
Ninjaserver/public/js/admin-dashboard.js

1769 lines
67 KiB
JavaScript
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.
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');
}
}