This commit is contained in:
MLH
2025-04-07 01:13:36 +02:00
parent 3bf2aae1a3
commit cdd3e180a0
24 changed files with 5156 additions and 0 deletions

396
public/js/admin.js Normal file
View File

@@ -0,0 +1,396 @@
// public/js/admin.js
// --- DOM Elements ---
const loginSection = document.getElementById('login-section');
const adminContent = document.getElementById('admin-content');
const loginForm = document.getElementById('login-form');
const loginUsernameInput = document.getElementById('login-username');
const loginPasswordInput = document.getElementById('login-password');
const loginError = document.getElementById('login-error');
const logoutButton = document.getElementById('logout-button');
const welcomeMessage = document.getElementById('welcome-message');
// Tournament elements
const tournamentForm = document.getElementById('tournament-form');
const tournamentFormTitle = document.getElementById('tournament-form-title');
const tournamentIdInput = document.getElementById('tournament-id');
const tournamentNameInput = document.getElementById('tournament-name');
const tournamentDateInput = document.getElementById('tournament-date');
const tournamentLocationInput = document.getElementById('tournament-location');
const tournamentTypeInput = document.getElementById('tournament-type');
const tournamentGameTypeInput = document.getElementById('tournament-game-type');
const tournamentDescriptionInput = document.getElementById('tournament-description');
const saveTournamentButton = document.getElementById('save-tournament-button');
const cancelTournamentButton = document.getElementById('cancel-tournament-button');
const tournamentFormError = document.getElementById('tournament-form-error');
const showAddTournamentFormButton = document.getElementById('show-add-tournament-form');
const tournamentList = document.getElementById('tournament-list');
const tournamentTable = document.getElementById('tournament-table');
const loadingTournaments = document.getElementById('loading-tournaments');
const tournamentListMessage = document.getElementById('tournament-list-message');
// --- State ---
let authToken = localStorage.getItem('authToken'); // Store token in local storage
let currentUser = JSON.parse(localStorage.getItem('currentUser')); // Store user info
// --- API Base URL ---
// Use relative URL for API calls, assuming frontend and backend are on the same origin
const API_BASE_URL = '/api';
// --- Utility Functions ---
// Function to display messages (error or success)
const showMessage = (element, message, isError = true) => {
element.textContent = message;
element.className = isError ? 'error-message' : 'success-message'; // Use CSS classes
element.classList.remove('hidden');
// Optional: Auto-hide after a few seconds
// setTimeout(() => element.classList.add('hidden'), 5000);
};
// Function to hide messages
const hideMessage = (element) => {
element.classList.add('hidden');
element.textContent = '';
};
// Function to make authenticated API requests
const fetchAPI = async (endpoint, options = {}) => {
const headers = {
'Content-Type': 'application/json',
...options.headers, // Allow overriding headers
};
// Add Authorization header if token exists
if (authToken) {
headers['Authorization'] = `Bearer ${authToken}`;
}
try {
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
...options,
headers,
});
// Handle token expiration or invalid token
if (response.status === 401 || response.status === 403) {
console.warn('Authentication error:', response.status);
logout(); // Log out the user if token is invalid/expired
throw new Error('Authentifizierung fehlgeschlagen oder Token abgelaufen.');
}
// Check if response is OK (status code 200-299)
if (!response.ok) {
// Try to parse error message from response body
let errorData;
try {
errorData = await response.json();
} catch (parseError) {
// If parsing fails, use status text
errorData = { message: response.statusText };
}
console.error('API Error:', response.status, errorData);
throw new Error(errorData.message || `HTTP-Fehler: ${response.status}`);
}
// Handle responses with no content (e.g., DELETE 204)
if (response.status === 204) {
return null; // Or return a success indicator if needed
}
// Parse JSON response body for other successful responses
return await response.json();
} catch (error) {
console.error('Fetch API Error:', error);
// Re-throw the error so it can be caught by the calling function
throw error;
}
};
// --- Authentication Functions ---
const handleLogin = async (event) => {
event.preventDefault(); // Prevent default form submission
hideMessage(loginError); // Hide previous errors
const username = loginUsernameInput.value.trim();
const password = loginPasswordInput.value.trim();
if (!username || !password) {
showMessage(loginError, 'Bitte Benutzername und Passwort eingeben.');
return;
}
try {
const data = await fetchAPI('/auth/login', {
method: 'POST',
body: JSON.stringify({ username, password }),
});
if (data.token && data.user) {
// Check if the logged-in user has the 'admin' role
if (data.user.role !== 'admin') {
showMessage(loginError, 'Zugriff verweigert. Nur Administratoren können sich hier anmelden.');
return; // Prevent non-admins from logging in to the admin panel
}
// Store token and user info
authToken = data.token;
currentUser = data.user;
localStorage.setItem('authToken', authToken);
localStorage.setItem('currentUser', JSON.stringify(currentUser));
// Update UI
updateUIBasedOnAuthState();
loadTournaments(); // Load initial data for admin
// Clear form
loginForm.reset();
} else {
// Should be caught by fetchAPI, but as a fallback
showMessage(loginError, data.message || 'Login fehlgeschlagen.');
}
} catch (error) {
showMessage(loginError, `Login fehlgeschlagen: ${error.message}`);
}
};
const logout = () => {
authToken = null;
currentUser = null;
localStorage.removeItem('authToken');
localStorage.removeItem('currentUser');
updateUIBasedOnAuthState();
// Clear any sensitive data displayed on the page
tournamentList.innerHTML = ''; // Clear tournament list
// Add similar clearing for other sections if needed
};
// --- UI Update Functions ---
const updateUIBasedOnAuthState = () => {
if (authToken && currentUser && currentUser.role === 'admin') {
// Logged in as Admin
loginSection.classList.add('hidden');
adminContent.classList.remove('hidden');
logoutButton.classList.remove('hidden');
welcomeMessage.textContent = `Willkommen, ${currentUser.username}! (Admin)`;
welcomeMessage.classList.remove('hidden');
} else {
// Logged out or not an Admin
loginSection.classList.remove('hidden');
adminContent.classList.add('hidden');
logoutButton.classList.add('hidden');
welcomeMessage.classList.add('hidden');
// If logged in but not admin, show appropriate message
if (currentUser && currentUser.role !== 'admin') {
showMessage(loginError, 'Sie sind angemeldet, haben aber keine Admin-Berechtigung für diese Seite.');
}
}
};
// --- Tournament Functions ---
// Reset and hide the tournament form
const resetAndHideTournamentForm = () => {
tournamentForm.reset(); // Clear form fields
tournamentIdInput.value = ''; // Clear hidden ID field
tournamentFormTitle.textContent = 'Neues Turnier hinzufügen'; // Reset title
saveTournamentButton.textContent = 'Speichern'; // Reset button text
hideMessage(tournamentFormError); // Hide any previous form errors
tournamentForm.classList.add('hidden'); // Hide the form
};
// Show the tournament form for adding or editing
const showTournamentForm = (tournament = null) => {
resetAndHideTournamentForm(); // Start clean
if (tournament) {
// Editing existing tournament
tournamentFormTitle.textContent = 'Turnier bearbeiten';
saveTournamentButton.textContent = 'Änderungen speichern';
tournamentIdInput.value = tournament.tournament_id;
tournamentNameInput.value = tournament.name || '';
// Format date correctly for input type="date" (YYYY-MM-DD)
tournamentDateInput.value = tournament.date ? tournament.date.split('T')[0] : '';
tournamentLocationInput.value = tournament.location || '';
tournamentTypeInput.value = tournament.tournament_type || 'knockout';
tournamentGameTypeInput.value = tournament.game_type || '11_points';
tournamentDescriptionInput.value = tournament.description || '';
// Populate other fields as needed (max_players, status, etc.)
} else {
// Adding new tournament - title and button text are already set by reset
}
tournamentForm.classList.remove('hidden'); // Show the form
tournamentNameInput.focus(); // Focus the first field
};
// Load tournaments from the API and display them
const loadTournaments = async () => {
if (!authToken) return; // Don't load if not logged in
loadingTournaments.classList.remove('hidden');
tournamentList.innerHTML = ''; // Clear existing list
hideMessage(tournamentListMessage);
try {
const tournaments = await fetchAPI('/tournaments');
if (tournaments.length === 0) {
tournamentList.innerHTML = '<tr><td colspan="6">Keine Turniere gefunden.</td></tr>';
} else {
tournaments.forEach(tournament => {
const row = tournamentList.insertRow();
row.dataset.id = tournament.tournament_id; // Store ID for easier access
// Format date for display (DD.MM.YYYY or leave empty)
const displayDate = tournament.date ? new Date(tournament.date).toLocaleDateString('de-DE') : '-';
row.innerHTML = `
<td>${tournament.name || '-'}</td>
<td>${displayDate}</td>
<td>${tournament.location || '-'}</td>
<td>${tournament.tournament_type === 'knockout' ? 'KO-System' : 'Gruppenphase'}</td>
<td>${tournament.status || 'Geplant'}</td>
<td>
<button class="edit-tournament">Bearbeiten</button>
<button class="delete-tournament danger">Löschen</button>
</td>
`;
});
}
} catch (error) {
showMessage(tournamentListMessage, `Fehler beim Laden der Turniere: ${error.message}`);
tournamentList.innerHTML = `<tr><td colspan="6">Fehler beim Laden der Daten.</td></tr>`; // Indicate error in table
} finally {
loadingTournaments.classList.add('hidden');
}
};
// Handle saving (creating or updating) a tournament
const handleSaveTournament = async (event) => {
event.preventDefault();
hideMessage(tournamentFormError);
const tournamentId = tournamentIdInput.value;
const isEditing = !!tournamentId; // Check if we are editing
// Gather data from form
const tournamentData = {
name: tournamentNameInput.value.trim(),
date: tournamentDateInput.value || null, // Send null if empty
location: tournamentLocationInput.value.trim() || null,
tournament_type: tournamentTypeInput.value,
game_type: tournamentGameTypeInput.value,
description: tournamentDescriptionInput.value.trim() || null,
// Add other fields (max_players, status) here if they are in the form
};
// Basic validation
if (!tournamentData.name) {
showMessage(tournamentFormError, 'Turniername ist erforderlich.');
return;
}
const method = isEditing ? 'PUT' : 'POST';
const endpoint = isEditing ? `/tournaments/${tournamentId}` : '/tournaments';
try {
const savedTournament = await fetchAPI(endpoint, {
method: method,
body: JSON.stringify(tournamentData),
});
showMessage(tournamentListMessage, `Turnier erfolgreich ${isEditing ? 'aktualisiert' : 'gespeichert'}.`, false); // Success message
resetAndHideTournamentForm();
loadTournaments(); // Refresh the list
} catch (error) {
showMessage(tournamentFormError, `Fehler beim Speichern des Turniers: ${error.message}`);
}
};
// Handle deleting a tournament
const handleDeleteTournament = async (tournamentId, tournamentName) => {
if (!tournamentId) return;
// Confirmation dialog
if (!confirm(`Möchten Sie das Turnier "${tournamentName}" wirklich löschen? Alle zugehörigen Spiele und Daten gehen verloren!`)) {
return; // User cancelled
}
hideMessage(tournamentListMessage);
try {
await fetchAPI(`/tournaments/${tournamentId}`, {
method: 'DELETE',
});
showMessage(tournamentListMessage, `Turnier "${tournamentName}" erfolgreich gelöscht.`, false); // Success message
loadTournaments(); // Refresh the list
} catch (error) {
showMessage(tournamentListMessage, `Fehler beim Löschen des Turniers: ${error.message}`);
}
};
// --- Event Listeners ---
// Login form submission
loginForm.addEventListener('submit', handleLogin);
// Logout button click
logoutButton.addEventListener('click', logout);
// Show "Add Tournament" form button
showAddTournamentFormButton.addEventListener('click', () => showTournamentForm());
// Cancel button in tournament form
cancelTournamentButton.addEventListener('click', resetAndHideTournamentForm);
// Tournament form submission (Save/Update)
tournamentForm.addEventListener('submit', handleSaveTournament);
// Event delegation for Edit and Delete buttons in the tournament list
tournamentList.addEventListener('click', async (event) => {
const target = event.target;
const row = target.closest('tr'); // Find the table row
if (!row) return; // Click wasn't inside a row
const tournamentId = row.dataset.id;
if (!tournamentId) return; // Row doesn't have an ID
if (target.classList.contains('edit-tournament')) {
// Edit button clicked
hideMessage(tournamentListMessage); // Hide list messages when editing
try {
// Fetch the specific tournament data to pre-fill the form accurately
const tournamentToEdit = await fetchAPI(`/tournaments/${tournamentId}`);
showTournamentForm(tournamentToEdit);
} catch (error) {
showMessage(tournamentListMessage, `Fehler beim Laden der Turnierdaten zum Bearbeiten: ${error.message}`);
}
} else if (target.classList.contains('delete-tournament')) {
// Delete button clicked
const tournamentName = row.cells[0].textContent; // Get name from the first cell
handleDeleteTournament(tournamentId, tournamentName);
}
});
// --- Initial Load ---
// Check authentication state and update UI on page load
updateUIBasedOnAuthState();
// Load initial data if logged in as admin
if (authToken && currentUser && currentUser.role === 'admin') {
loadTournaments();
// Load other data (players, users) here as well
}