Files
TTurnier/public/js/admin.js
2025-04-08 21:29:54 +02:00

661 lines
38 KiB
JavaScript

// public/js/admin.js (Tournament-Centric)
// --- Helper Functions ---
const showMessage = (element, message, isError = true, autohide = false) => {
if (!element) { console.warn("Message target element not found:", message); return; }
element.textContent = message;
element.className = `message-area ${isError ? 'error-message' : 'success-message'}`;
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 & General
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 Management (Top Level)
const tournamentManagementSection = document.getElementById('tournament-management');
const showAddTournamentFormButton = document.getElementById('show-add-tournament-form');
const tournamentForm = document.getElementById('tournament-form');
const tournamentFormTitle = document.getElementById('tournament-form-title');
const tournamentIdInput = document.getElementById('tournament-id'); // Hidden input in the form
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');
const tournamentList = document.getElementById('tournament-list'); // tbody of the table
const tournamentTable = document.getElementById('tournament-table');
const loadingTournaments = document.getElementById('loading-tournaments');
const tournamentListMessage = document.getElementById('tournament-list-message');
// Selected Tournament Context Area
const selectedTournamentContext = document.getElementById('selected-tournament-context');
const selectedTournamentHeader = document.getElementById('selected-tournament-header');
const selectedTournamentContextName = document.getElementById('selected-tournament-context-name');
const tabContainer = document.querySelector('.tab-container');
const tabButtons = document.querySelectorAll('.tab-button');
const tabContents = document.querySelectorAll('.tab-content');
// Tab: Info
const infoTabStatus = document.getElementById('info-tab-status');
const infoTabType = document.getElementById('info-tab-type');
const infoTabLocation = document.getElementById('info-tab-location');
const infoTabDescription = document.getElementById('info-tab-description');
const editSelectedTournamentButton = document.getElementById('edit-selected-tournament-button');
// Tab: Players (Participants)
const showAddPlayerFormTabButton = document.getElementById('show-add-player-form-tab');
const linkExistingPlayerButton = document.getElementById('link-existing-player-button'); // Placeholder button
const playerFormInTab = document.getElementById('player-form'); // Reusing the form, now inside tab
const playerFormTitleInTab = document.getElementById('player-form-title'); // Reusing title
const playerIdInputInTab = document.getElementById('player-id'); // Reusing hidden input
const playerFirstNameInputInTab = document.getElementById('player-first-name');
const playerLastNameInputInTab = document.getElementById('player-last-name');
const playerGenderInput = document.getElementById('player-gender'); // New gender field
const playerClubInputInTab = document.getElementById('player-club');
const playerQttrInputInTab = document.getElementById('player-qttr');
const playerAgeClassInputInTab = document.getElementById('player-age-class');
const savePlayerButtonInTab = document.getElementById('save-player-button');
const cancelPlayerButtonInTab = document.getElementById('cancel-player-button');
const playerFormErrorInTab = document.getElementById('player-form-error');
const tournamentPlayerList = document.getElementById('tournament-player-list'); // tbody
const loadingTournamentPlayers = document.getElementById('loading-tournament-players');
const tournamentPlayerListMessage = document.getElementById('tournament-player-list-message');
// Tab: Matches
const showAddMatchFormButtonInTab = document.getElementById('show-add-match-form');
const matchFormInTab = document.getElementById('match-form'); // Reusing form
const matchFormTitleInTab = document.getElementById('match-form-title');
const matchIdInputInTab = document.getElementById('match-id');
const matchFormTournamentIdInputInTab = document.getElementById('match-form-tournament-id');
const matchRoundInputInTab = document.getElementById('match-round');
const matchNumberInRoundInputInTab = document.getElementById('match-number-in-round');
const matchPlayer1SelectInTab = document.getElementById('match-player1');
const matchPlayer2SelectInTab = document.getElementById('match-player2');
const matchScheduledTimeInputInTab = document.getElementById('match-scheduled-time');
const matchTableNumberInputInTab = document.getElementById('match-table-number');
const matchStatusInputInTab = document.getElementById('match-status');
const saveMatchButtonInTab = document.getElementById('save-match-button');
const cancelMatchButtonInTab = document.getElementById('cancel-match-button');
const matchFormErrorInTab = document.getElementById('match-form-error');
const matchListInTab = document.getElementById('match-list'); // tbody
const loadingMatchesInTab = document.getElementById('loading-matches');
const matchListMessageInTab = document.getElementById('match-list-message');
// Tab: Users (Global)
const showAddUserFormButtonInTab = document.getElementById('show-add-user-form');
const userFormInTab = document.getElementById('user-form'); // Reusing form
const userFormTitleInTab = document.getElementById('user-form-title');
const userIdInputInTab = document.getElementById('user-id');
const userUsernameInputInTab = document.getElementById('user-username');
const userPasswordInputInTab = document.getElementById('user-password');
const userRoleInputInTab = document.getElementById('user-role');
const saveUserButtonInTab = document.getElementById('save-user-button');
const cancelUserButtonInTab = document.getElementById('cancel-user-button');
const userFormErrorInTab = document.getElementById('user-form-error');
const userListInTab = document.getElementById('user-list'); // tbody
const loadingUsersInTab = document.getElementById('loading-users');
const userListMessageInTab = document.getElementById('user-list-message');
// --- State ---
let authToken = localStorage.getItem('authToken');
let currentUser = JSON.parse(localStorage.getItem('currentUser'));
let currentTournaments = []; // Cache all tournaments
let selectedTournament = null; // Store the currently selected tournament object
let tournamentPlayersCache = []; // Cache players for the selected tournament
let globalUsersCache = []; // Cache global users
// --- API Base URL ---
const API_BASE_URL = '/api';
// --- API Fetch Function (from v2) ---
const fetchAPI = async (endpoint, options = {}) => {
const headers = { 'Content-Type': 'application/json', ...options.headers };
if (authToken) { headers['Authorization'] = `Bearer ${authToken}`; }
try {
const response = await fetch(`${API_BASE_URL}${endpoint}`, { ...options, headers });
if (response.status === 401 || response.status === 403) { logout(); throw new Error('Authentifizierung fehlgeschlagen.'); }
if (!response.ok) {
let errorData = { message: `HTTP-Fehler: ${response.status} ${response.statusText}` };
try { const parsedError = await response.json(); if (parsedError && parsedError.message) errorData.message = parsedError.message; } catch (e) {}
console.error('API Error:', response.status, errorData); throw new Error(errorData.message);
}
if (response.status === 204) return null;
return await response.json();
} catch (error) { console.error('Fetch API Error:', error); throw error; }
};
// --- Authentication ---
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 && data.user.role === 'admin') {
authToken = data.token; currentUser = data.user;
localStorage.setItem('authToken', authToken); localStorage.setItem('currentUser', JSON.stringify(currentUser));
updateUIBasedOnAuthState(); loadInitialData(); loginForm.reset();
} else if (data.user && data.user.role !== 'admin') {
showMessage(loginError, 'Zugriff verweigert. Nur Administratoren können sich hier anmelden.'); logout();
} else { showMessage(loginError, data.message || 'Login fehlgeschlagen.'); }
} catch (error) { showMessage(loginError, `Login fehlgeschlagen: ${error.message}`); }
};
const logout = () => {
authToken = null; currentUser = null; selectedTournament = null;
localStorage.removeItem('authToken'); localStorage.removeItem('currentUser');
updateUIBasedOnAuthState();
// Clear content and hide context section
tournamentList.innerHTML = '';
selectedTournamentContext.classList.add('hidden');
// Reset forms maybe?
};
// --- UI Update & Initial Load ---
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);
selectedTournamentContext.classList.add('hidden'); // Hide context on logout/initial load
if (isAdminLoggedIn) { welcomeMessage.textContent = `Willkommen, ${currentUser.username}! (Admin)`; }
};
const loadInitialData = async () => {
if (!authToken || !currentUser || currentUser.role !== 'admin') return;
await loadTournaments(); // Load tournaments first
// Other data (global users) can be loaded when the respective tab is clicked or here if needed often
// await loadUsers(); // Load global users initially? Or wait for tab click. Let's wait.
};
// --- Tournament Management (Top Level) ---
const resetAndHideTournamentForm = () => {
tournamentForm.reset(); tournamentIdInput.value = '';
tournamentFormTitle.textContent = 'Neues Turnier erstellen'; 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 () => {
setLoading(loadingTournaments, true); tournamentList.innerHTML = ''; hideMessage(tournamentListMessage);
try {
currentTournaments = await fetchAPI('/tournaments');
if (currentTournaments.length === 0) { tournamentList.innerHTML = '<tr><td colspan="4">Keine Turniere gefunden.</td></tr>'; }
else {
currentTournaments.forEach(t => {
const row = tournamentList.insertRow(); row.dataset.id = t.tournament_id;
const displayDate = t.date ? new Date(t.date).toLocaleDateString('de-DE') : '-';
row.innerHTML = `
<td>${t.name || '-'}</td> <td>${displayDate}</td> <td>${t.status || 'Geplant'}</td>
<td>
<button class="manage-tournament" data-id="${t.tournament_id}">Verwalten</button>
<button class="edit-tournament" data-id="${t.tournament_id}">Bearbeiten</button>
<button class="delete-tournament danger" data-id="${t.tournament_id}" data-name="${t.name}">Löschen</button>
</td>`;
});
}
} catch (error) { showMessage(tournamentListMessage, `Fehler beim Laden der Turniere: ${error.message}`); tournamentList.innerHTML = `<tr><td colspan="4">Fehler beim Laden.</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) || tournamentData.max_players < 0)) { showMessage(tournamentFormError, 'Max. Spieler muss eine positive Zahl sein.'); return; }
const method = isEditing ? 'PUT' : 'POST'; const endpoint = isEditing ? `/tournaments/${tournamentId}` : '/tournaments';
try {
const savedTournament = await fetchAPI(endpoint, { method, body: JSON.stringify(tournamentData) });
showMessage(tournamentListMessage, `Turnier erfolgreich ${isEditing ? 'aktualisiert' : 'gespeichert'}.`, false, true);
resetAndHideTournamentForm();
loadTournaments(); // Refresh list
// If editing the currently selected tournament, update the context header/info tab
if (isEditing && selectedTournament && selectedTournament.tournament_id === tournamentId) {
// Refetch the single tournament to get potentially updated data returned by PUT
await selectTournamentForManagement(tournamentId); // Reselect to reload context
}
} 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
// If the deleted tournament was selected, hide the context section
if (selectedTournament && selectedTournament.tournament_id === tournamentId) {
selectedTournament = null;
selectedTournamentContext.classList.add('hidden');
}
} catch (error) { showMessage(tournamentListMessage, `Fehler beim Löschen: ${error.message}`); }
};
// --- Tournament Context Selection & Tab Handling ---
const selectTournamentForManagement = async (tournamentId) => {
selectedTournament = currentTournaments.find(t => t.tournament_id === tournamentId);
if (!selectedTournament) {
console.error("Selected tournament not found in cache");
// Optionally fetch the tournament details if not in cache
try {
selectedTournament = await fetchAPI(`/tournaments/${tournamentId}`);
} catch (error) {
showMessage(tournamentListMessage, `Fehler: Turnier ${tournamentId} konnte nicht geladen werden.`, true);
return;
}
}
selectedTournamentContextName.textContent = `Turnier: ${selectedTournament.name}`;
selectedTournamentContext.classList.remove('hidden');
// Reset forms within tabs
resetAndHidePlayerForm();
resetAndHideMatchForm();
resetAndHideUserForm(); // User form is global but might be open
// Activate the default tab ('info') and load its data
switchTab('info');
};
const switchTab = async (tabId) => {
tabButtons.forEach(button => {
button.classList.toggle('active', button.dataset.tab === tabId);
});
tabContents.forEach(content => {
content.classList.toggle('hidden', content.id !== `tab-content-${tabId}`);
});
// Load data for the activated tab if needed (and a tournament is selected)
if (!selectedTournament) return;
switch (tabId) {
case 'info':
displayTournamentInfo();
break;
case 'players':
await loadTournamentPlayers(selectedTournament.tournament_id);
break;
case 'matches':
await loadMatches(selectedTournament.tournament_id);
break;
case 'users':
await loadUsers(); // Load global users
break;
case 'settings':
// Load settings data if implemented
break;
}
};
const displayTournamentInfo = () => {
if (!selectedTournament) return;
infoTabStatus.textContent = selectedTournament.status || 'N/A';
infoTabType.textContent = selectedTournament.tournament_type === 'knockout' ? 'KO-System' : 'Gruppenphase';
infoTabLocation.textContent = selectedTournament.location || 'N/A';
infoTabDescription.textContent = selectedTournament.description || 'Keine Beschreibung vorhanden.';
};
// --- Tab: Info Functions ---
editSelectedTournamentButton.addEventListener('click', () => {
if (selectedTournament) {
showTournamentForm(selectedTournament); // Show the main tournament form for editing
// Optionally scroll to the form
tournamentManagementSection.scrollIntoView({ behavior: 'smooth' });
}
});
// --- Tab: Players (Tournament Participants) ---
const resetAndHidePlayerForm = () => {
playerFormInTab.reset(); playerIdInputInTab.value = '';
playerFormTitleInTab.textContent = 'Neuen Spieler erstellen'; savePlayerButtonInTab.textContent = 'Spieler erstellen';
hideMessage(playerFormErrorInTab); playerFormInTab.classList.add('hidden');
};
const showPlayerFormInTab = () => { // Only for adding new global players now
resetAndHidePlayerForm();
playerFormInTab.classList.remove('hidden');
playerFirstNameInputInTab.focus();
};
// Loads players *for the specific tournament*
const loadTournamentPlayers = async (tournamentId) => {
if (!tournamentId) return;
setLoading(loadingTournamentPlayers, true); tournamentPlayerList.innerHTML = ''; hideMessage(tournamentPlayerListMessage);
try {
// Use the new endpoint to get only players linked to this tournament
tournamentPlayersCache = await fetchAPI(`/tournaments/${tournamentId}/players`);
if (tournamentPlayersCache.length === 0) { tournamentPlayerList.innerHTML = '<tr><td colspan="7">Noch keine Teilnehmer für dieses Turnier registriert.</td></tr>'; }
else {
tournamentPlayersCache.forEach(player => {
const row = tournamentPlayerList.insertRow(); row.dataset.id = player.player_id;
let genderText = '-';
if (player.gender === 'm') genderText = 'Männlich';
else if (player.gender === 'w') genderText = 'Weiblich';
else if (player.gender === 'd') genderText = 'Divers';
row.innerHTML = `
<td>${player.last_name || '-'}</td> <td>${player.first_name || '-'}</td>
<td>${genderText}</td> {/* Display Gender */}
<td>${player.club || '-'}</td> <td>${player.qttr_points || '-'}</td>
<td>${player.participation_status || 'Registriert'}</td>
<td>
{/* TODO: Add actions like 'Remove from Tournament' - requires backend */}
<button class="remove-player-from-tournament" data-player-id="${player.player_id}" disabled title="Entfernen noch nicht implementiert">Entfernen</button>
{/* Maybe link to edit global player details? */}
</td>`;
});
// Update player dropdowns for the match form within this tournament's context
populateMatchPlayerSelects(tournamentPlayersCache);
}
} catch (error) { showMessage(tournamentPlayerListMessage, `Fehler beim Laden der Teilnehmer: ${error.message}`); tournamentPlayerList.innerHTML = `<tr><td colspan="7">Fehler beim Laden.</td></tr>`; }
finally { setLoading(loadingTournamentPlayers, false); }
};
// Handles saving a *new global* player (linking to tournament needs backend)
const handleSavePlayer = async (event) => {
event.preventDefault(); hideMessage(playerFormErrorInTab);
const playerData = {
first_name: playerFirstNameInputInTab.value.trim(), last_name: playerLastNameInputInTab.value.trim(),
gender: playerGenderInput.value, // Add gender
club: playerClubInputInTab.value.trim(),
qttr_points: playerQttrInputInTab.value ? parseInt(playerQttrInputInTab.value, 10) : null,
age_class: playerAgeClassInputInTab.value.trim(),
};
if (!playerData.first_name || !playerData.last_name) { showMessage(playerFormErrorInTab, 'Vor- und Nachname sind erforderlich.'); return; }
if (!playerData.gender) { showMessage(playerFormErrorInTab, 'Geschlecht ist erforderlich.'); return; } // Validate gender
if (playerQttrInputInTab.value && (isNaN(playerData.qttr_points) || playerData.qttr_points === null || playerData.qttr_points < 0)) { showMessage(playerFormErrorInTab, 'QTTR-Punkte müssen eine gültige positive Zahl oder 0 sein.'); return; }
try {
// This only creates the global player
const newPlayer = await fetchAPI('/players', { method: 'POST', body: JSON.stringify(playerData) });
showMessage(tournamentPlayerListMessage, `Globaler Spieler "${newPlayer.first_name} ${newPlayer.last_name}" erstellt. Hinzufügen zum Turnier muss separat erfolgen.`, false, true);
resetAndHidePlayerForm();
// Reloading tournament players won't show the new player yet unless backend link exists
// loadTournamentPlayers(selectedTournament.tournament_id);
} catch (error) { showMessage(playerFormErrorInTab, `Fehler beim Erstellen des Spielers: ${error.message}`); }
};
// --- Tab: Matches ---
const populateMatchPlayerSelects = (players) => {
const optionsHtml = '<option value="">-- Teilnehmer wählen --</option>' +
players.map(p => `<option value="${p.player_id}">${p.first_name} ${p.last_name}</option>`).join('');
matchPlayer1SelectInTab.innerHTML = optionsHtml;
matchPlayer2SelectInTab.innerHTML = optionsHtml;
};
const resetAndHideMatchForm = () => {
matchFormInTab.reset(); matchIdInputInTab.value = ''; matchFormTournamentIdInputInTab.value = '';
matchFormTitleInTab.textContent = 'Neues Spiel hinzufügen'; saveMatchButtonInTab.textContent = 'Speichern';
hideMessage(matchFormErrorInTab); matchFormInTab.classList.add('hidden');
};
const showMatchFormInTab = (match = null) => {
resetAndHideMatchForm();
if (!selectedTournament) { showMessage(matchListMessageInTab, "Fehler: Kein Turnier ausgewählt.", true); return; }
matchFormTournamentIdInputInTab.value = selectedTournament.tournament_id; // Set hidden field
// Ensure player dropdowns are populated with *tournament* players
populateMatchPlayerSelects(tournamentPlayersCache);
if (match) {
matchFormTitleInTab.textContent = 'Spiel bearbeiten'; saveMatchButtonInTab.textContent = 'Änderungen speichern';
matchIdInputInTab.value = match.match_id; matchRoundInputInTab.value = match.round || '';
matchNumberInRoundInputInTab.value = match.match_number_in_round || '';
matchPlayer1SelectInTab.value = match.player1_id || ''; matchPlayer2SelectInTab.value = match.player2_id || '';
matchScheduledTimeInputInTab.value = match.scheduled_time ? match.scheduled_time.substring(0, 16) : '';
matchTableNumberInputInTab.value = match.table_number || ''; matchStatusInputInTab.value = match.status || 'scheduled';
}
matchFormInTab.classList.remove('hidden'); matchRoundInputInTab.focus();
};
const loadMatches = async (tournamentId) => {
if (!tournamentId) return;
setLoading(loadingMatchesInTab, true); matchListInTab.innerHTML = ''; hideMessage(matchListMessageInTab);
try {
const matches = await fetchAPI(`/matches?tournamentId=${tournamentId}`);
if (matches.length === 0) { matchListInTab.innerHTML = '<tr><td colspan="9">Keine Spiele für dieses Turnier gefunden.</td></tr>'; }
else {
matches.sort((a, b) => { /* Sorting logic from spectator view */
if ((a.round || 999) < (b.round || 999)) return -1; if ((a.round || 999) > (b.round || 999)) return 1;
if ((a.match_number_in_round || 999) < (b.match_number_in_round || 999)) return -1; if ((a.match_number_in_round || 999) > (b.match_number_in_round || 999)) return 1;
const timeA = a.scheduled_time ? new Date(a.scheduled_time).getTime() : Infinity; const timeB = b.scheduled_time ? new Date(b.scheduled_time).getTime() : Infinity;
return timeA - timeB;
});
matches.forEach(match => {
const row = matchListInTab.insertRow(); row.dataset.id = match.match_id;
const scheduled = match.scheduled_time ? new Date(match.scheduled_time).toLocaleString('de-DE', { dateStyle: 'short', timeStyle: 'short'}) : '-';
let resultPlaceholder = '-';
if (match.status === 'finished') { resultPlaceholder = match.winner_name ? `${match.winner_name} gewinnt` : 'Beendet'; /* TODO: Show score */ }
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>
{/* TODO: Button to enter scores? Link to referee view? */}
</td>`;
});
}
} catch (error) { showMessage(matchListMessageInTab, `Fehler beim Laden der Spiele: ${error.message}`); matchListInTab.innerHTML = `<tr><td colspan="9">Fehler beim Laden.</td></tr>`; }
finally { setLoading(loadingMatchesInTab, false); }
};
const handleSaveMatch = async (event) => {
event.preventDefault(); hideMessage(matchFormErrorInTab);
const matchId = matchIdInputInTab.value; const isEditing = !!matchId;
const tournamentId = matchFormTournamentIdInputInTab.value;
if (!tournamentId) { showMessage(matchFormErrorInTab, "Fehler: Turnier-ID fehlt."); return; }
const matchData = {
tournament_id: tournamentId, round: matchRoundInputInTab.value ? parseInt(matchRoundInputInTab.value, 10) : null,
match_number_in_round: matchNumberInRoundInputInTab.value ? parseInt(matchNumberInRoundInputInTab.value, 10) : null,
player1_id: matchPlayer1SelectInTab.value || null, player2_id: matchPlayer2SelectInTab.value || null,
scheduled_time: matchScheduledTimeInputInTab.value || null, table_number: matchTableNumberInputInTab.value.trim() || null,
status: matchStatusInputInTab.value,
};
if (matchData.player1_id && matchData.player1_id === matchData.player2_id) { showMessage(matchFormErrorInTab, "Spieler 1/2 dürfen nicht identisch sein."); return; }
if (matchData.round !== null && isNaN(matchData.round)) { showMessage(matchFormErrorInTab, "Runde muss eine Zahl sein."); return; }
if (matchData.match_number_in_round !== null && isNaN(matchData.match_number_in_round)) { showMessage(matchFormErrorInTab, "Spiel Nr. muss eine Zahl sein."); return; }
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(matchListMessageInTab, `Spiel erfolgreich ${isEditing ? 'aktualisiert' : 'gespeichert'}.`, false, true);
resetAndHideMatchForm(); loadMatches(tournamentId);
} catch (error) { showMessage(matchFormErrorInTab, `Fehler beim Speichern: ${error.message}`); }
};
const handleDeleteMatch = async (matchId) => {
if (!matchId || !confirm(`Möchten Sie dieses Spiel wirklich löschen?`)) return;
hideMessage(matchListMessageInTab);
const currentTournamentId = selectedTournament?.tournament_id;
try {
await fetchAPI(`/matches/${matchId}`, { method: 'DELETE' });
showMessage(matchListMessageInTab, `Spiel erfolgreich gelöscht.`, false, true);
if (currentTournamentId) { loadMatches(currentTournamentId); }
} catch (error) { showMessage(matchListMessageInTab, `Fehler beim Löschen: ${error.message}`); }
};
// --- Tab: Users (Global) ---
const resetAndHideUserForm = () => {
userFormInTab.reset(); userIdInputInTab.value = '';
userFormTitleInTab.textContent = 'Neuen Benutzer hinzufügen'; saveUserButtonInTab.textContent = 'Speichern';
userPasswordInputInTab.required = true; userPasswordInputInTab.placeholder = "";
hideMessage(userFormErrorInTab); userFormInTab.classList.add('hidden');
};
const showUserFormInTab = (user = null) => {
resetAndHideUserForm();
if (user) {
userFormTitleInTab.textContent = 'Benutzer bearbeiten'; saveUserButtonInTab.textContent = 'Änderungen speichern';
userIdInputInTab.value = user.user_id; userUsernameInputInTab.value = user.username || '';
userRoleInputInTab.value = user.role || 'spectator'; userPasswordInputInTab.required = false;
userPasswordInputInTab.placeholder = "Leer lassen, um nicht zu ändern";
} else { userPasswordInputInTab.required = true; userPasswordInputInTab.placeholder = ""; }
userFormInTab.classList.remove('hidden'); userUsernameInputInTab.focus();
};
const loadUsers = async () => {
if (!authToken) return;
setLoading(loadingUsersInTab, true); userListInTab.innerHTML = ''; hideMessage(userListMessageInTab);
try {
globalUsersCache = await fetchAPI('/users');
if (globalUsersCache.length === 0) { userListInTab.innerHTML = '<tr><td colspan="4">Keine Benutzer gefunden.</td></tr>'; }
else {
globalUsersCache.forEach(user => {
const row = userListInTab.insertRow(); row.dataset.id = user.user_id;
const createdAt = user.created_at ? new Date(user.created_at).toLocaleString('de-DE') : '-';
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="Akt. Admin kann nicht gelöscht werden"' : ''}>Löschen</button>
</td>`;
});
}
} catch (error) { showMessage(userListMessageInTab, `Fehler beim Laden der Benutzer: ${error.message}`); userListInTab.innerHTML = `<tr><td colspan="4">Fehler beim Laden.</td></tr>`; }
finally { setLoading(loadingUsersInTab, false); }
};
const handleSaveUser = async (event) => {
event.preventDefault(); hideMessage(userFormErrorInTab);
const userId = userIdInputInTab.value; const isEditing = !!userId;
const userData = { username: userUsernameInputInTab.value.trim(), password: userPasswordInputInTab.value, role: userRoleInputInTab.value };
if (!userData.username) { showMessage(userFormErrorInTab, 'Benutzername ist erforderlich.'); return; }
if (!isEditing && !userData.password) { showMessage(userFormErrorInTab, 'Passwort ist beim Erstellen erforderlich.'); return; }
if (userData.password && userData.password.length < 6) { showMessage(userFormErrorInTab, 'Passwort muss mind. 6 Zeichen haben.'); return; }
const method = isEditing ? 'PUT' : 'POST'; const endpoint = isEditing ? `/users/${userId}` : '/users';
if (isEditing && !userData.password) { delete userData.password; }
try {
await fetchAPI(endpoint, { method, body: JSON.stringify(userData) });
showMessage(userListMessageInTab, `Benutzer erfolgreich ${isEditing ? 'aktualisiert' : 'gespeichert'}.`, false, true);
resetAndHideUserForm(); loadUsers();
} catch (error) { showMessage(userFormErrorInTab, `Fehler beim Speichern: ${error.message}`); }
};
const handleDeleteUser = async (userId, username) => {
if (!userId || (currentUser && currentUser.userId === userId) || !confirm(`Benutzer "${username}" wirklich löschen?`)) return;
hideMessage(userListMessageInTab);
try {
await fetchAPI(`/users/${userId}`, { method: 'DELETE' });
showMessage(userListMessageInTab, `Benutzer "${username}" gelöscht.`, false, true); loadUsers();
} catch (error) { showMessage(userListMessageInTab, `Fehler beim Löschen: ${error.message}`); }
};
// --- Event Listeners ---
document.addEventListener('DOMContentLoaded', () => {
updateUIBasedOnAuthState();
if (authToken && currentUser && currentUser.role === 'admin') { loadInitialData(); }
});
loginForm.addEventListener('submit', handleLogin);
logoutButton.addEventListener('click', logout);
// Tournament List Actions
showAddTournamentFormButton.addEventListener('click', () => showTournamentForm());
cancelTournamentButton.addEventListener('click', resetAndHideTournamentForm);
tournamentForm.addEventListener('submit', handleSaveTournament);
tournamentList.addEventListener('click', (event) => {
const target = event.target;
const tournamentId = target.dataset.id;
if (!tournamentId) return;
if (target.classList.contains('manage-tournament')) {
selectTournamentForManagement(tournamentId);
} else if (target.classList.contains('edit-tournament')) {
const tournamentToEdit = currentTournaments.find(t => t.tournament_id === tournamentId);
if (tournamentToEdit) showTournamentForm(tournamentToEdit);
} else if (target.classList.contains('delete-tournament')) {
handleDeleteTournament(tournamentId, target.dataset.name);
}
});
// Tab Navigation
tabContainer.addEventListener('click', (event) => {
if (event.target.classList.contains('tab-button')) {
switchTab(event.target.dataset.tab);
}
});
// Player Tab Actions
showAddPlayerFormTabButton.addEventListener('click', showPlayerFormInTab);
cancelPlayerButtonInTab.addEventListener('click', resetAndHidePlayerForm);
playerFormInTab.addEventListener('submit', handleSavePlayer);
// TODO: Add listener for removing player from tournament on tournamentPlayerList
// Match Tab Actions
showAddMatchFormButtonInTab.addEventListener('click', () => showMatchFormInTab());
cancelMatchButtonInTab.addEventListener('click', resetAndHideMatchForm);
matchFormInTab.addEventListener('submit', handleSaveMatch);
matchListInTab.addEventListener('click', async (event) => {
const target = event.target;
const matchId = target.dataset.id;
if (!matchId) return;
if (target.classList.contains('edit-match')) {
hideMessage(matchListMessageInTab);
try { const matchToEdit = await fetchAPI(`/matches/${matchId}`); showMatchFormInTab(matchToEdit); }
catch (error) { showMessage(matchListMessageInTab, `Fehler beim Laden der Spieldaten: ${error.message}`); }
} else if (target.classList.contains('delete-match')) { handleDeleteMatch(matchId); }
});
// User Tab Actions
showAddUserFormButtonInTab.addEventListener('click', () => showUserFormInTab());
cancelUserButtonInTab.addEventListener('click', resetAndHideUserForm);
userFormInTab.addEventListener('submit', handleSaveUser);
userListInTab.addEventListener('click', async (event) => {
const target = event.target;
const userId = target.dataset.id;
if (!userId) return;
if (target.classList.contains('edit-user')) {
hideMessage(userListMessageInTab);
try { const userToEdit = await fetchAPI(`/users/${userId}`); showUserFormInTab(userToEdit); }
catch(error) { showMessage(userListMessageInTab, `Fehler beim Laden der Benutzerdaten: ${error.message}`); }
} else if (target.classList.contains('delete-user')) { handleDeleteUser(userId, target.dataset.name); }
});