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

1203 lines
45 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>Erstellt</th><th>Aktionen</th></tr></thead><tbody>';
locations.forEach(location => {
html += `<tr>
<td>${location.id}</td>
<td>${location.name}</td>
<td>${location.latitude}</td>
<td>${location.longitude}</td>
<td>${new Date(location.created_at).toLocaleDateString('de-DE')}</td>
<td class="action-buttons">
<button class="btn btn-small btn-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;
}
}
function refreshData() {
switch (currentDataType) {
case 'players':
loadPlayers();
break;
case 'runs':
loadRuns();
break;
case 'locations':
loadLocations();
break;
case 'adminusers':
loadAdminUsers();
break;
case 'system':
loadSystemInfo();
break;
}
}
function showAddModal() {
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;
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;
}