915 lines
38 KiB
JavaScript
915 lines
38 KiB
JavaScript
// public/js/admin.js
|
|
|
|
// --- Helper Functions (Keep from previous version) ---
|
|
const showMessage = (element, message, isError = true, autohide = false) => {
|
|
if (!element) {
|
|
console.warn("Attempted to show message on non-existent element:", message);
|
|
return;
|
|
}
|
|
element.textContent = message;
|
|
element.className = `message-area ${isError ? 'error-message' : 'success-message'}`; // Use CSS classes
|
|
element.classList.remove('hidden');
|
|
if (autohide) {
|
|
setTimeout(() => hideMessage(element), 5000);
|
|
}
|
|
};
|
|
|
|
const hideMessage = (element) => {
|
|
if (element) {
|
|
element.classList.add('hidden');
|
|
element.textContent = '';
|
|
}
|
|
};
|
|
|
|
const setLoading = (element, isLoading) => {
|
|
if (element) {
|
|
element.classList.toggle('hidden', !isLoading);
|
|
}
|
|
}
|
|
|
|
// --- DOM Elements ---
|
|
// Auth
|
|
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'); // Specific error element for login form
|
|
const logoutButton = document.getElementById('logout-button');
|
|
const welcomeMessage = document.getElementById('welcome-message');
|
|
|
|
// Tournaments
|
|
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 tournamentMaxPlayersInput = document.getElementById('tournament-max-players');
|
|
const tournamentStatusInput = document.getElementById('tournament-status');
|
|
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'); // Specific error element
|
|
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'); // General message area for list
|
|
|
|
// Players
|
|
const playerManagementSection = document.getElementById('player-management');
|
|
const playerSearchInput = document.getElementById('player-search');
|
|
const showAddPlayerFormButton = document.getElementById('show-add-player-form');
|
|
const playerForm = document.getElementById('player-form');
|
|
const playerFormTitle = document.getElementById('player-form-title');
|
|
const playerIdInput = document.getElementById('player-id');
|
|
const playerFirstNameInput = document.getElementById('player-first-name');
|
|
const playerLastNameInput = document.getElementById('player-last-name');
|
|
const playerClubInput = document.getElementById('player-club');
|
|
const playerQttrInput = document.getElementById('player-qttr');
|
|
const playerAgeClassInput = document.getElementById('player-age-class');
|
|
const savePlayerButton = document.getElementById('save-player-button');
|
|
const cancelPlayerButton = document.getElementById('cancel-player-button');
|
|
const playerFormError = document.getElementById('player-form-error');
|
|
const playerList = document.getElementById('player-list');
|
|
const playerTable = document.getElementById('player-table');
|
|
const loadingPlayers = document.getElementById('loading-players');
|
|
const playerListMessage = document.getElementById('player-list-message');
|
|
|
|
// Users
|
|
const userManagementSection = document.getElementById('user-management');
|
|
const showAddUserFormButton = document.getElementById('show-add-user-form');
|
|
const userForm = document.getElementById('user-form');
|
|
const userFormTitle = document.getElementById('user-form-title');
|
|
const userIdInput = document.getElementById('user-id');
|
|
const userUsernameInput = document.getElementById('user-username');
|
|
const userPasswordInput = document.getElementById('user-password');
|
|
const userRoleInput = document.getElementById('user-role');
|
|
const saveUserButton = document.getElementById('save-user-button');
|
|
const cancelUserButton = document.getElementById('cancel-user-button');
|
|
const userFormError = document.getElementById('user-form-error');
|
|
const userList = document.getElementById('user-list');
|
|
const userTable = document.getElementById('user-table');
|
|
const loadingUsers = document.getElementById('loading-users');
|
|
const userListMessage = document.getElementById('user-list-message');
|
|
|
|
// Matches
|
|
const matchManagementSection = document.getElementById('match-management');
|
|
const selectTournamentForMatches = document.getElementById('select-tournament-for-matches');
|
|
const showAddMatchFormButton = document.getElementById('show-add-match-form');
|
|
const matchForm = document.getElementById('match-form');
|
|
const matchFormTitle = document.getElementById('match-form-title');
|
|
const matchIdInput = document.getElementById('match-id');
|
|
const matchFormTournamentIdInput = document.getElementById('match-form-tournament-id');
|
|
const matchRoundInput = document.getElementById('match-round');
|
|
const matchNumberInRoundInput = document.getElementById('match-number-in-round');
|
|
const matchPlayer1Select = document.getElementById('match-player1');
|
|
const matchPlayer2Select = document.getElementById('match-player2');
|
|
const matchScheduledTimeInput = document.getElementById('match-scheduled-time');
|
|
const matchTableNumberInput = document.getElementById('match-table-number');
|
|
const matchStatusInput = document.getElementById('match-status');
|
|
const saveMatchButton = document.getElementById('save-match-button');
|
|
const cancelMatchButton = document.getElementById('cancel-match-button');
|
|
const matchFormError = document.getElementById('match-form-error');
|
|
const matchList = document.getElementById('match-list');
|
|
const matchTable = document.getElementById('match-table');
|
|
const loadingMatches = document.getElementById('loading-matches');
|
|
const matchListMessage = document.getElementById('match-list-message');
|
|
const selectTournamentPrompt = document.getElementById('select-tournament-prompt');
|
|
|
|
|
|
// --- State ---
|
|
let authToken = localStorage.getItem('authToken'); // Store token in local storage
|
|
let currentUser = JSON.parse(localStorage.getItem('currentUser')); // Store user info
|
|
let currentPlayers = []; // Cache players for dropdowns
|
|
let currentTournaments = []; // Cache tournaments for dropdowns
|
|
|
|
// --- API Base URL ---
|
|
const API_BASE_URL = '/api';
|
|
|
|
// --- Utility Functions --- (showMessage, hideMessage, setLoading defined above)
|
|
|
|
// 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.');
|
|
}
|
|
|
|
// Try to parse error message from response body for non-OK responses
|
|
if (!response.ok) {
|
|
let errorData = { message: `HTTP-Fehler: ${response.status} ${response.statusText}` }; // Default error
|
|
try {
|
|
// Attempt to parse JSON error response from backend
|
|
const parsedError = await response.json();
|
|
if (parsedError && parsedError.message) {
|
|
errorData.message = parsedError.message;
|
|
}
|
|
} catch (parseError) {
|
|
// Ignore if response is not JSON or empty
|
|
}
|
|
console.error('API Error:', response.status, errorData);
|
|
throw new Error(errorData.message);
|
|
}
|
|
|
|
// Handle responses with no content (e.g., DELETE 204)
|
|
if (response.status === 204) {
|
|
return null; // Indicate success with no content
|
|
}
|
|
|
|
// 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; // Propagate the specific error message
|
|
}
|
|
};
|
|
|
|
|
|
// --- Authentication Functions ---
|
|
|
|
const handleLogin = async (event) => {
|
|
event.preventDefault();
|
|
hideMessage(loginError);
|
|
|
|
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) {
|
|
if (data.user.role !== 'admin') {
|
|
showMessage(loginError, 'Zugriff verweigert. Nur Administratoren können sich hier anmelden.');
|
|
logout(); // Clear any potentially stored invalid token
|
|
return;
|
|
}
|
|
|
|
authToken = data.token;
|
|
currentUser = data.user;
|
|
localStorage.setItem('authToken', authToken);
|
|
localStorage.setItem('currentUser', JSON.stringify(currentUser));
|
|
updateUIBasedOnAuthState();
|
|
loadInitialData(); // Load data needed for admin view
|
|
loginForm.reset();
|
|
} else {
|
|
showMessage(loginError, data.message || 'Login fehlgeschlagen.');
|
|
}
|
|
} catch (error) {
|
|
// Use the error message thrown by fetchAPI
|
|
showMessage(loginError, `Login fehlgeschlagen: ${error.message}`);
|
|
}
|
|
};
|
|
|
|
const logout = () => {
|
|
authToken = null;
|
|
currentUser = null;
|
|
localStorage.removeItem('authToken');
|
|
localStorage.removeItem('currentUser');
|
|
updateUIBasedOnAuthState();
|
|
// Clear dynamic content
|
|
tournamentList.innerHTML = '';
|
|
playerList.innerHTML = '';
|
|
userList.innerHTML = '';
|
|
matchList.innerHTML = '';
|
|
selectTournamentForMatches.innerHTML = '<option value="">-- Turnier wählen --</option>';
|
|
// Hide forms
|
|
resetAndHideTournamentForm();
|
|
resetAndHidePlayerForm();
|
|
resetAndHideUserForm();
|
|
resetAndHideMatchForm();
|
|
};
|
|
|
|
// --- UI Update Functions ---
|
|
|
|
const updateUIBasedOnAuthState = () => {
|
|
const isAdminLoggedIn = authToken && currentUser && currentUser.role === 'admin';
|
|
loginSection.classList.toggle('hidden', isAdminLoggedIn);
|
|
adminContent.classList.toggle('hidden', !isAdminLoggedIn);
|
|
logoutButton.classList.toggle('hidden', !isAdminLoggedIn);
|
|
welcomeMessage.classList.toggle('hidden', !isAdminLoggedIn);
|
|
if (isAdminLoggedIn) {
|
|
welcomeMessage.textContent = `Willkommen, ${currentUser.username}! (Admin)`;
|
|
} else {
|
|
// If not logged in or wrong role, ensure login error message is cleared unless login just failed
|
|
// hideMessage(loginError); // Might hide a recent login error, handle carefully
|
|
}
|
|
};
|
|
|
|
// Load all necessary initial data after login
|
|
const loadInitialData = async () => {
|
|
if (!authToken || !currentUser || currentUser.role !== 'admin') return;
|
|
// Load concurrently for better performance
|
|
await Promise.all([
|
|
loadTournaments(),
|
|
loadPlayers(),
|
|
loadUsers()
|
|
// Matches are loaded when a tournament is selected
|
|
]);
|
|
populateTournamentSelects(); // Populate dropdowns after tournaments are loaded
|
|
};
|
|
|
|
// --- Tournament Functions ---
|
|
|
|
const resetAndHideTournamentForm = () => {
|
|
tournamentForm.reset();
|
|
tournamentIdInput.value = '';
|
|
tournamentFormTitle.textContent = 'Neues Turnier hinzufügen';
|
|
saveTournamentButton.textContent = 'Speichern';
|
|
hideMessage(tournamentFormError);
|
|
tournamentForm.classList.add('hidden');
|
|
};
|
|
|
|
const showTournamentForm = (tournament = null) => {
|
|
resetAndHideTournamentForm();
|
|
if (tournament) {
|
|
tournamentFormTitle.textContent = 'Turnier bearbeiten';
|
|
saveTournamentButton.textContent = 'Änderungen speichern';
|
|
tournamentIdInput.value = tournament.tournament_id;
|
|
tournamentNameInput.value = tournament.name || '';
|
|
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';
|
|
tournamentMaxPlayersInput.value = tournament.max_players || '';
|
|
tournamentStatusInput.value = tournament.status || 'planned';
|
|
tournamentDescriptionInput.value = tournament.description || '';
|
|
}
|
|
tournamentForm.classList.remove('hidden');
|
|
tournamentNameInput.focus();
|
|
};
|
|
|
|
const loadTournaments = async () => {
|
|
if (!authToken) return;
|
|
setLoading(loadingTournaments, true);
|
|
tournamentList.innerHTML = '';
|
|
hideMessage(tournamentListMessage);
|
|
|
|
try {
|
|
currentTournaments = await fetchAPI('/tournaments'); // Store fetched tournaments
|
|
if (currentTournaments.length === 0) {
|
|
tournamentList.innerHTML = '<tr><td colspan="6">Keine Turniere gefunden.</td></tr>';
|
|
} else {
|
|
currentTournaments.forEach(tournament => {
|
|
const row = tournamentList.insertRow();
|
|
row.dataset.id = tournament.tournament_id;
|
|
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" data-id="${tournament.tournament_id}">Bearbeiten</button>
|
|
<button class="delete-tournament danger" data-id="${tournament.tournament_id}" data-name="${tournament.name}">Löschen</button>
|
|
</td>
|
|
`;
|
|
});
|
|
populateTournamentSelects(); // Update dropdowns whenever tournaments are loaded/reloaded
|
|
}
|
|
} catch (error) {
|
|
showMessage(tournamentListMessage, `Fehler beim Laden der Turniere: ${error.message}`);
|
|
tournamentList.innerHTML = `<tr><td colspan="6">Fehler beim Laden der Daten.</td></tr>`;
|
|
} finally {
|
|
setLoading(loadingTournaments, false);
|
|
}
|
|
};
|
|
|
|
const handleSaveTournament = async (event) => {
|
|
event.preventDefault();
|
|
hideMessage(tournamentFormError);
|
|
|
|
const tournamentId = tournamentIdInput.value;
|
|
const isEditing = !!tournamentId;
|
|
|
|
const tournamentData = {
|
|
name: tournamentNameInput.value.trim(),
|
|
date: tournamentDateInput.value || null,
|
|
location: tournamentLocationInput.value.trim() || null,
|
|
tournament_type: tournamentTypeInput.value,
|
|
game_type: tournamentGameTypeInput.value,
|
|
max_players: tournamentMaxPlayersInput.value ? parseInt(tournamentMaxPlayersInput.value, 10) : null,
|
|
status: tournamentStatusInput.value,
|
|
description: tournamentDescriptionInput.value.trim() || null,
|
|
};
|
|
|
|
if (!tournamentData.name) {
|
|
showMessage(tournamentFormError, 'Turniername ist erforderlich.');
|
|
return;
|
|
}
|
|
if (tournamentData.max_players !== null && isNaN(tournamentData.max_players)) {
|
|
showMessage(tournamentFormError, 'Max. Spieler muss eine Zahl sein.');
|
|
return;
|
|
}
|
|
|
|
|
|
const method = isEditing ? 'PUT' : 'POST';
|
|
const endpoint = isEditing ? `/tournaments/${tournamentId}` : '/tournaments';
|
|
|
|
try {
|
|
await fetchAPI(endpoint, { method, body: JSON.stringify(tournamentData) });
|
|
showMessage(tournamentListMessage, `Turnier erfolgreich ${isEditing ? 'aktualisiert' : 'gespeichert'}.`, false, true);
|
|
resetAndHideTournamentForm();
|
|
loadTournaments(); // Refresh list and potentially dropdowns
|
|
} catch (error) {
|
|
showMessage(tournamentFormError, `Fehler beim Speichern: ${error.message}`);
|
|
}
|
|
};
|
|
|
|
const handleDeleteTournament = async (tournamentId, tournamentName) => {
|
|
if (!tournamentId || !confirm(`Möchten Sie das Turnier "${tournamentName}" wirklich löschen?`)) return;
|
|
hideMessage(tournamentListMessage);
|
|
try {
|
|
await fetchAPI(`/tournaments/${tournamentId}`, { method: 'DELETE' });
|
|
showMessage(tournamentListMessage, `Turnier "${tournamentName}" erfolgreich gelöscht.`, false, true);
|
|
loadTournaments(); // Refresh list and dropdowns
|
|
} catch (error) {
|
|
showMessage(tournamentListMessage, `Fehler beim Löschen: ${error.message}`);
|
|
}
|
|
};
|
|
|
|
// --- Player Functions ---
|
|
|
|
const resetAndHidePlayerForm = () => {
|
|
playerForm.reset();
|
|
playerIdInput.value = '';
|
|
playerFormTitle.textContent = 'Neuen Spieler hinzufügen';
|
|
savePlayerButton.textContent = 'Speichern';
|
|
hideMessage(playerFormError);
|
|
playerForm.classList.add('hidden');
|
|
};
|
|
|
|
const showPlayerForm = (player = null) => {
|
|
resetAndHidePlayerForm();
|
|
if (player) {
|
|
playerFormTitle.textContent = 'Spieler bearbeiten';
|
|
savePlayerButton.textContent = 'Änderungen speichern';
|
|
playerIdInput.value = player.player_id;
|
|
playerFirstNameInput.value = player.first_name || '';
|
|
playerLastNameInput.value = player.last_name || '';
|
|
playerClubInput.value = player.club || '';
|
|
playerQttrInput.value = player.qttr_points || '';
|
|
playerAgeClassInput.value = player.age_class || '';
|
|
}
|
|
playerForm.classList.remove('hidden');
|
|
playerFirstNameInput.focus();
|
|
};
|
|
|
|
const loadPlayers = async (searchTerm = '') => {
|
|
if (!authToken) return;
|
|
setLoading(loadingPlayers, true);
|
|
playerList.innerHTML = '';
|
|
hideMessage(playerListMessage);
|
|
const endpoint = searchTerm ? `/players?search=${encodeURIComponent(searchTerm)}` : '/players';
|
|
|
|
try {
|
|
currentPlayers = await fetchAPI(endpoint); // Cache players
|
|
if (currentPlayers.length === 0) {
|
|
playerList.innerHTML = '<tr><td colspan="6">Keine Spieler gefunden.</td></tr>';
|
|
} else {
|
|
currentPlayers.forEach(player => {
|
|
const row = playerList.insertRow();
|
|
row.dataset.id = player.player_id;
|
|
row.innerHTML = `
|
|
<td>${player.last_name || '-'}</td>
|
|
<td>${player.first_name || '-'}</td>
|
|
<td>${player.club || '-'}</td>
|
|
<td>${player.qttr_points || '-'}</td>
|
|
<td>${player.age_class || '-'}</td>
|
|
<td>
|
|
<button class="edit-player" data-id="${player.player_id}">Bearbeiten</button>
|
|
<button class="delete-player danger" data-id="${player.player_id}" data-name="${player.first_name} ${player.last_name}">Löschen</button>
|
|
</td>
|
|
`;
|
|
});
|
|
populatePlayerSelects(); // Update player dropdowns in match form
|
|
}
|
|
} catch (error) {
|
|
showMessage(playerListMessage, `Fehler beim Laden der Spieler: ${error.message}`);
|
|
playerList.innerHTML = `<tr><td colspan="6">Fehler beim Laden der Daten.</td></tr>`;
|
|
} finally {
|
|
setLoading(loadingPlayers, false);
|
|
}
|
|
};
|
|
|
|
const handleSavePlayer = async (event) => {
|
|
event.preventDefault();
|
|
hideMessage(playerFormError);
|
|
const playerId = playerIdInput.value;
|
|
const isEditing = !!playerId;
|
|
|
|
const playerData = {
|
|
first_name: playerFirstNameInput.value.trim(),
|
|
last_name: playerLastNameInput.value.trim(),
|
|
club: playerClubInput.value.trim(),
|
|
qttr_points: playerQttrInput.value ? parseInt(playerQttrInput.value, 10) : null,
|
|
age_class: playerAgeClassInput.value.trim(),
|
|
};
|
|
|
|
if (!playerData.first_name || !playerData.last_name) {
|
|
showMessage(playerFormError, 'Vor- und Nachname sind erforderlich.');
|
|
return;
|
|
}
|
|
if (playerQttrInput.value && (isNaN(playerData.qttr_points) || playerData.qttr_points === null)) {
|
|
showMessage(playerFormError, 'QTTR-Punkte müssen eine gültige Zahl sein.');
|
|
return;
|
|
}
|
|
|
|
const method = isEditing ? 'PUT' : 'POST';
|
|
const endpoint = isEditing ? `/players/${playerId}` : '/players';
|
|
|
|
try {
|
|
await fetchAPI(endpoint, { method, body: JSON.stringify(playerData) });
|
|
showMessage(playerListMessage, `Spieler erfolgreich ${isEditing ? 'aktualisiert' : 'gespeichert'}.`, false, true);
|
|
resetAndHidePlayerForm();
|
|
loadPlayers(playerSearchInput.value.trim()); // Refresh list with current search term
|
|
} catch (error) {
|
|
showMessage(playerFormError, `Fehler beim Speichern: ${error.message}`);
|
|
}
|
|
};
|
|
|
|
const handleDeletePlayer = async (playerId, playerName) => {
|
|
if (!playerId || !confirm(`Möchten Sie den Spieler "${playerName}" wirklich löschen?`)) return;
|
|
hideMessage(playerListMessage);
|
|
try {
|
|
await fetchAPI(`/players/${playerId}`, { method: 'DELETE' });
|
|
showMessage(playerListMessage, `Spieler "${playerName}" erfolgreich gelöscht.`, false, true);
|
|
loadPlayers(playerSearchInput.value.trim()); // Refresh list
|
|
} catch (error) {
|
|
showMessage(playerListMessage, `Fehler beim Löschen: ${error.message}`);
|
|
}
|
|
};
|
|
|
|
// --- User Functions ---
|
|
|
|
const resetAndHideUserForm = () => {
|
|
userForm.reset();
|
|
userIdInput.value = '';
|
|
userFormTitle.textContent = 'Neuen Benutzer hinzufügen';
|
|
saveUserButton.textContent = 'Speichern';
|
|
userPasswordInput.required = true; // Required when adding
|
|
hideMessage(userFormError);
|
|
userForm.classList.add('hidden');
|
|
};
|
|
|
|
const showUserForm = (user = null) => {
|
|
resetAndHideUserForm();
|
|
if (user) {
|
|
userFormTitle.textContent = 'Benutzer bearbeiten';
|
|
saveUserButton.textContent = 'Änderungen speichern';
|
|
userIdInput.value = user.user_id;
|
|
userUsernameInput.value = user.username || '';
|
|
userRoleInput.value = user.role || 'spectator';
|
|
userPasswordInput.required = false; // Not required when editing (unless changing)
|
|
userPasswordInput.placeholder = "Leer lassen, um nicht zu ändern";
|
|
} else {
|
|
userPasswordInput.required = true;
|
|
userPasswordInput.placeholder = "";
|
|
}
|
|
userForm.classList.remove('hidden');
|
|
userUsernameInput.focus();
|
|
};
|
|
|
|
const loadUsers = async () => {
|
|
if (!authToken) return;
|
|
setLoading(loadingUsers, true);
|
|
userList.innerHTML = '';
|
|
hideMessage(userListMessage);
|
|
|
|
try {
|
|
const users = await fetchAPI('/users');
|
|
if (users.length === 0) {
|
|
userList.innerHTML = '<tr><td colspan="4">Keine Benutzer gefunden.</td></tr>';
|
|
} else {
|
|
users.forEach(user => {
|
|
const row = userList.insertRow();
|
|
row.dataset.id = user.user_id;
|
|
const createdAt = user.created_at ? new Date(user.created_at).toLocaleString('de-DE') : '-';
|
|
// Prevent deleting the currently logged-in admin
|
|
const disableDelete = currentUser && currentUser.userId === user.user_id;
|
|
row.innerHTML = `
|
|
<td>${user.username || '-'}</td>
|
|
<td>${user.role || '-'}</td>
|
|
<td>${createdAt}</td>
|
|
<td>
|
|
<button class="edit-user" data-id="${user.user_id}">Bearbeiten</button>
|
|
<button class="delete-user danger" data-id="${user.user_id}" data-name="${user.username}" ${disableDelete ? 'disabled title="Aktueller Admin kann nicht gelöscht werden"' : ''}>Löschen</button>
|
|
</td>
|
|
`;
|
|
});
|
|
}
|
|
} catch (error) {
|
|
showMessage(userListMessage, `Fehler beim Laden der Benutzer: ${error.message}`);
|
|
userList.innerHTML = `<tr><td colspan="4">Fehler beim Laden der Daten.</td></tr>`;
|
|
} finally {
|
|
setLoading(loadingUsers, false);
|
|
}
|
|
};
|
|
|
|
const handleSaveUser = async (event) => {
|
|
event.preventDefault();
|
|
hideMessage(userFormError);
|
|
const userId = userIdInput.value;
|
|
const isEditing = !!userId;
|
|
|
|
const userData = {
|
|
username: userUsernameInput.value.trim(),
|
|
password: userPasswordInput.value, // Send password field (empty if not changed)
|
|
role: userRoleInput.value,
|
|
};
|
|
|
|
if (!userData.username) {
|
|
showMessage(userFormError, 'Benutzername ist erforderlich.');
|
|
return;
|
|
}
|
|
if (!isEditing && !userData.password) {
|
|
showMessage(userFormError, 'Passwort ist beim Erstellen erforderlich.');
|
|
return;
|
|
}
|
|
// Basic password validation (example)
|
|
if (userData.password && userData.password.length < 6) {
|
|
showMessage(userFormError, 'Passwort muss mindestens 6 Zeichen lang sein.');
|
|
return;
|
|
}
|
|
|
|
|
|
const method = isEditing ? 'PUT' : 'POST';
|
|
const endpoint = isEditing ? `/users/${userId}` : '/users';
|
|
|
|
// Don't send empty password field if editing and not changing password
|
|
if (isEditing && !userData.password) {
|
|
delete userData.password;
|
|
}
|
|
|
|
|
|
try {
|
|
await fetchAPI(endpoint, { method, body: JSON.stringify(userData) });
|
|
showMessage(userListMessage, `Benutzer erfolgreich ${isEditing ? 'aktualisiert' : 'gespeichert'}.`, false, true);
|
|
resetAndHideUserForm();
|
|
loadUsers(); // Refresh list
|
|
} catch (error) {
|
|
showMessage(userFormError, `Fehler beim Speichern: ${error.message}`);
|
|
}
|
|
};
|
|
|
|
const handleDeleteUser = async (userId, username) => {
|
|
if (!userId || (currentUser && currentUser.userId === userId) || !confirm(`Möchten Sie den Benutzer "${username}" wirklich löschen?`)) return;
|
|
hideMessage(userListMessage);
|
|
try {
|
|
await fetchAPI(`/users/${userId}`, { method: 'DELETE' });
|
|
showMessage(userListMessage, `Benutzer "${username}" erfolgreich gelöscht.`, false, true);
|
|
loadUsers(); // Refresh list
|
|
} catch (error) {
|
|
showMessage(userListMessage, `Fehler beim Löschen: ${error.message}`);
|
|
}
|
|
};
|
|
|
|
|
|
// --- Match Functions (Structure and Placeholders) ---
|
|
|
|
// Populate dropdown selects with current data
|
|
const populateTournamentSelects = () => {
|
|
selectTournamentForMatches.innerHTML = '<option value="">-- Turnier wählen --</option>';
|
|
currentTournaments.forEach(t => {
|
|
const option = document.createElement('option');
|
|
option.value = t.tournament_id;
|
|
option.textContent = `${t.name} (${t.date ? new Date(t.date).toLocaleDateString('de-DE') : 'N/A'})`;
|
|
selectTournamentForMatches.appendChild(option);
|
|
});
|
|
};
|
|
|
|
const populatePlayerSelects = () => {
|
|
const optionsHtml = '<option value="">-- Spieler wählen --</option>' +
|
|
currentPlayers.map(p => `<option value="${p.player_id}">${p.first_name} ${p.last_name} (${p.club || 'N/A'})</option>`).join('');
|
|
matchPlayer1Select.innerHTML = optionsHtml;
|
|
matchPlayer2Select.innerHTML = optionsHtml;
|
|
};
|
|
|
|
const resetAndHideMatchForm = () => {
|
|
matchForm.reset();
|
|
matchIdInput.value = '';
|
|
matchFormTournamentIdInput.value = '';
|
|
matchFormTitle.textContent = 'Neues Spiel hinzufügen';
|
|
saveMatchButton.textContent = 'Speichern';
|
|
hideMessage(matchFormError);
|
|
matchForm.classList.add('hidden');
|
|
};
|
|
|
|
const showMatchForm = (match = null) => {
|
|
resetAndHideMatchForm();
|
|
const selectedTournamentId = selectTournamentForMatches.value;
|
|
if (!selectedTournamentId) {
|
|
showMessage(matchListMessage, "Bitte zuerst ein Turnier auswählen, um ein Spiel hinzuzufügen.", true);
|
|
return;
|
|
}
|
|
matchFormTournamentIdInput.value = selectedTournamentId; // Set hidden field
|
|
|
|
// Ensure player dropdowns are up-to-date
|
|
populatePlayerSelects();
|
|
|
|
if (match) {
|
|
// TODO: Populate form for editing a match
|
|
matchFormTitle.textContent = 'Spiel bearbeiten';
|
|
saveMatchButton.textContent = 'Änderungen speichern';
|
|
matchIdInput.value = match.match_id;
|
|
matchRoundInput.value = match.round || '';
|
|
matchNumberInRoundInput.value = match.match_number_in_round || '';
|
|
matchPlayer1Select.value = match.player1_id || '';
|
|
matchPlayer2Select.value = match.player2_id || '';
|
|
// Format datetime-local (YYYY-MM-DDTHH:mm)
|
|
matchScheduledTimeInput.value = match.scheduled_time ? match.scheduled_time.substring(0, 16) : '';
|
|
matchTableNumberInput.value = match.table_number || '';
|
|
matchStatusInput.value = match.status || 'scheduled';
|
|
}
|
|
|
|
matchForm.classList.remove('hidden');
|
|
matchRoundInput.focus();
|
|
};
|
|
|
|
const loadMatches = async (tournamentId) => {
|
|
if (!authToken || !tournamentId) {
|
|
matchList.innerHTML = '';
|
|
selectTournamentPrompt.classList.remove('hidden');
|
|
showAddMatchFormButton.disabled = true;
|
|
return;
|
|
}
|
|
selectTournamentPrompt.classList.add('hidden');
|
|
showAddMatchFormButton.disabled = false; // Enable add button
|
|
setLoading(loadingMatches, true);
|
|
matchList.innerHTML = '';
|
|
hideMessage(matchListMessage);
|
|
|
|
try {
|
|
const matches = await fetchAPI(`/matches?tournamentId=${tournamentId}`);
|
|
if (matches.length === 0) {
|
|
matchList.innerHTML = '<tr><td colspan="9">Keine Spiele für dieses Turnier gefunden.</td></tr>';
|
|
} else {
|
|
matches.forEach(match => {
|
|
const row = matchList.insertRow();
|
|
row.dataset.id = match.match_id;
|
|
const scheduled = match.scheduled_time ? new Date(match.scheduled_time).toLocaleString('de-DE') : '-';
|
|
// TODO: Fetch and display actual scores/result string
|
|
const resultPlaceholder = match.status === 'finished' ? (match.winner_name ? `${match.winner_name} gewinnt` : 'Beendet') : '-';
|
|
|
|
row.innerHTML = `
|
|
<td>${match.round || '-'}</td>
|
|
<td>${match.match_number_in_round || '-'}</td>
|
|
<td>${match.player1_name || '<i>N/A</i>'}</td>
|
|
<td>${match.player2_name || '<i>N/A</i>'}</td>
|
|
<td>${resultPlaceholder}</td> <td>${match.status || '-'}</td>
|
|
<td>${scheduled}</td>
|
|
<td>${match.table_number || '-'}</td>
|
|
<td>
|
|
<button class="edit-match" data-id="${match.match_id}">Bearbeiten</button>
|
|
<button class="delete-match danger" data-id="${match.match_id}">Löschen</button>
|
|
</td>
|
|
`;
|
|
});
|
|
}
|
|
} catch (error) {
|
|
showMessage(matchListMessage, `Fehler beim Laden der Spiele: ${error.message}`);
|
|
matchList.innerHTML = `<tr><td colspan="9">Fehler beim Laden der Daten.</td></tr>`;
|
|
} finally {
|
|
setLoading(loadingMatches, false);
|
|
}
|
|
};
|
|
|
|
const handleSaveMatch = async (event) => {
|
|
event.preventDefault();
|
|
hideMessage(matchFormError);
|
|
const matchId = matchIdInput.value;
|
|
const isEditing = !!matchId;
|
|
const tournamentId = matchFormTournamentIdInput.value;
|
|
|
|
if (!tournamentId) {
|
|
showMessage(matchFormError, "Fehler: Turnier-ID fehlt im Formular.");
|
|
return;
|
|
}
|
|
|
|
const matchData = {
|
|
tournament_id: tournamentId, // Crucial for creation
|
|
round: matchRoundInput.value ? parseInt(matchRoundInput.value, 10) : null,
|
|
match_number_in_round: matchNumberInRoundInput.value ? parseInt(matchNumberInRoundInput.value, 10) : null,
|
|
player1_id: matchPlayer1Select.value || null,
|
|
player2_id: matchPlayer2Select.value || null,
|
|
scheduled_time: matchScheduledTimeInput.value || null,
|
|
table_number: matchTableNumberInput.value.trim() || null,
|
|
status: matchStatusInput.value,
|
|
};
|
|
|
|
// Basic validation
|
|
if (matchData.player1_id && matchData.player1_id === matchData.player2_id) {
|
|
showMessage(matchFormError, "Spieler 1 und Spieler 2 dürfen nicht identisch sein.");
|
|
return;
|
|
}
|
|
if (matchData.round !== null && isNaN(matchData.round)) {
|
|
showMessage(matchFormError, "Runde muss eine Zahl sein."); return;
|
|
}
|
|
if (matchData.match_number_in_round !== null && isNaN(matchData.match_number_in_round)) {
|
|
showMessage(matchFormError, "Spiel Nr. muss eine Zahl sein."); return;
|
|
}
|
|
|
|
// Remove tournament_id if editing (it's part of the URL, not body)
|
|
if (isEditing) {
|
|
delete matchData.tournament_id;
|
|
}
|
|
|
|
const method = isEditing ? 'PUT' : 'POST';
|
|
const endpoint = isEditing ? `/matches/${matchId}` : '/matches';
|
|
|
|
try {
|
|
await fetchAPI(endpoint, { method, body: JSON.stringify(matchData) });
|
|
showMessage(matchListMessage, `Spiel erfolgreich ${isEditing ? 'aktualisiert' : 'gespeichert'}.`, false, true);
|
|
resetAndHideMatchForm();
|
|
loadMatches(tournamentId); // Refresh list for the current tournament
|
|
} catch (error) {
|
|
showMessage(matchFormError, `Fehler beim Speichern des Spiels: ${error.message}`);
|
|
}
|
|
};
|
|
|
|
const handleDeleteMatch = async (matchId) => {
|
|
if (!matchId || !confirm(`Möchten Sie dieses Spiel wirklich löschen?`)) return;
|
|
hideMessage(matchListMessage);
|
|
const currentTournamentId = selectTournamentForMatches.value; // Get current tournament to refresh list
|
|
try {
|
|
await fetchAPI(`/matches/${matchId}`, { method: 'DELETE' });
|
|
showMessage(matchListMessage, `Spiel erfolgreich gelöscht.`, false, true);
|
|
if (currentTournamentId) {
|
|
loadMatches(currentTournamentId); // Refresh list
|
|
}
|
|
} catch (error) {
|
|
showMessage(matchListMessage, `Fehler beim Löschen des Spiels: ${error.message}`);
|
|
}
|
|
};
|
|
|
|
|
|
// --- Event Listeners ---
|
|
|
|
// General Setup
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
updateUIBasedOnAuthState();
|
|
if (authToken && currentUser && currentUser.role === 'admin') {
|
|
loadInitialData();
|
|
}
|
|
});
|
|
|
|
// Auth
|
|
loginForm.addEventListener('submit', handleLogin);
|
|
logoutButton.addEventListener('click', logout);
|
|
|
|
// Tournaments
|
|
showAddTournamentFormButton.addEventListener('click', () => showTournamentForm());
|
|
cancelTournamentButton.addEventListener('click', resetAndHideTournamentForm);
|
|
tournamentForm.addEventListener('submit', handleSaveTournament);
|
|
tournamentList.addEventListener('click', (event) => {
|
|
const target = event.target;
|
|
if (target.classList.contains('edit-tournament')) {
|
|
const id = target.dataset.id;
|
|
const tournamentToEdit = currentTournaments.find(t => t.tournament_id === id);
|
|
if (tournamentToEdit) showTournamentForm(tournamentToEdit);
|
|
} else if (target.classList.contains('delete-tournament')) {
|
|
handleDeleteTournament(target.dataset.id, target.dataset.name);
|
|
}
|
|
});
|
|
|
|
// Players
|
|
showAddPlayerFormButton.addEventListener('click', () => showPlayerForm());
|
|
cancelPlayerButton.addEventListener('click', resetAndHidePlayerForm);
|
|
playerForm.addEventListener('submit', handleSavePlayer);
|
|
playerList.addEventListener('click', (event) => {
|
|
const target = event.target;
|
|
if (target.classList.contains('edit-player')) {
|
|
const id = target.dataset.id;
|
|
const playerToEdit = currentPlayers.find(p => p.player_id === id);
|
|
if (playerToEdit) showPlayerForm(playerToEdit);
|
|
} else if (target.classList.contains('delete-player')) {
|
|
handleDeletePlayer(target.dataset.id, target.dataset.name);
|
|
}
|
|
});
|
|
// Debounce search input
|
|
let playerSearchTimeout;
|
|
playerSearchInput.addEventListener('input', () => {
|
|
clearTimeout(playerSearchTimeout);
|
|
playerSearchTimeout = setTimeout(() => {
|
|
loadPlayers(playerSearchInput.value.trim());
|
|
}, 300); // Wait 300ms after user stops typing
|
|
});
|
|
|
|
|
|
// Users
|
|
showAddUserFormButton.addEventListener('click', () => showUserForm());
|
|
cancelUserButton.addEventListener('click', resetAndHideUserForm);
|
|
userForm.addEventListener('submit', handleSaveUser);
|
|
userList.addEventListener('click', async (event) => { // Mark async if fetching user data for edit
|
|
const target = event.target;
|
|
if (target.classList.contains('edit-user')) {
|
|
const id = target.dataset.id;
|
|
// Fetch the specific user data to ensure we have the latest, including role
|
|
hideMessage(userListMessage);
|
|
try {
|
|
const userToEdit = await fetchAPI(`/users/${id}`); // Fetch single user
|
|
showUserForm(userToEdit);
|
|
} catch(error) {
|
|
showMessage(userListMessage, `Fehler beim Laden der Benutzerdaten: ${error.message}`);
|
|
}
|
|
} else if (target.classList.contains('delete-user')) {
|
|
handleDeleteUser(target.dataset.id, target.dataset.name);
|
|
}
|
|
});
|
|
|
|
// Matches
|
|
selectTournamentForMatches.addEventListener('change', (event) => {
|
|
loadMatches(event.target.value);
|
|
});
|
|
showAddMatchFormButton.addEventListener('click', () => showMatchForm());
|
|
cancelMatchButton.addEventListener('click', resetAndHideMatchForm);
|
|
matchForm.addEventListener('submit', handleSaveMatch);
|
|
matchList.addEventListener('click', async (event) => {
|
|
const target = event.target;
|
|
if (target.classList.contains('edit-match')) {
|
|
const id = target.dataset.id;
|
|
hideMessage(matchListMessage);
|
|
try {
|
|
// Fetch the specific match data to pre-fill the form accurately
|
|
const matchToEdit = await fetchAPI(`/matches/${id}`); // Assumes getMatchById returns necessary fields
|
|
showMatchForm(matchToEdit);
|
|
} catch (error) {
|
|
showMessage(matchListMessage, `Fehler beim Laden der Spieldaten zum Bearbeiten: ${error.message}`);
|
|
}
|
|
} else if (target.classList.contains('delete-match')) {
|
|
handleDeleteMatch(target.dataset.id);
|
|
}
|
|
});
|