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

130
public/admin.html Normal file
View File

@@ -0,0 +1,130 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Admin-Bereich - Turnierverwaltung</title>
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<nav>
<a href="/admin.html" class="active">Admin</a>
<a href="/referee.html">Schiedsrichter</a>
<a href="/spectator.html">Zuschauer</a>
<a href="#" id="logout-button" style="float: right;">Logout</a>
</nav>
<div class="container">
<h1>Admin-Bereich</h1>
<p id="welcome-message">Willkommen!</p>
<div id="login-section" class="auth-form">
<h2>Login</h2>
<form id="login-form">
<div>
<label for="login-username">Benutzername:</label>
<input type="text" id="login-username" required>
</div>
<div>
<label for="login-password">Passwort:</label>
<input type="password" id="login-password" required>
</div>
<button type="submit">Login</button>
<div id="login-error" class="error-message hidden"></div>
</form>
</div>
<div id="admin-content" class="hidden">
<section id="tournament-management" class="content-section">
<div class="section-header">
<h2>Turnierverwaltung</h2>
<button id="show-add-tournament-form">Neues Turnier hinzufügen</button>
</div>
<form id="tournament-form" class="hidden">
<h3 id="tournament-form-title">Neues Turnier hinzufügen</h3>
<input type="hidden" id="tournament-id"> <div>
<label for="tournament-name">Name:</label>
<input type="text" id="tournament-name" required>
</div>
<div>
<label for="tournament-date">Datum:</label>
<input type="date" id="tournament-date">
</div>
<div>
<label for="tournament-location">Ort:</label>
<input type="text" id="tournament-location">
</div>
<div>
<label for="tournament-type">Turnier-Typ:</label>
<select id="tournament-type" required>
<option value="knockout">KO-System</option>
<option value="group">Gruppenphase</option>
</select>
</div>
<div>
<label for="tournament-game-type">Spiel-Typ (Punkte pro Satz):</label>
<select id="tournament-game-type" required>
<option value="11_points">11 Punkte</option>
<option value="21_points">21 Punkte</option>
</select>
</div>
<div>
<label for="tournament-description">Beschreibung:</label>
<textarea id="tournament-description"></textarea>
</div>
<button type="submit" id="save-tournament-button">Speichern</button>
<button type="button" id="cancel-tournament-button" class="secondary">Abbrechen</button>
<div id="tournament-form-error" class="error-message hidden"></div>
</form>
<div id="tournament-list-message" class="hidden"></div> <table id="tournament-table">
<thead>
<tr>
<th>Name</th>
<th>Datum</th>
<th>Ort</th>
<th>Typ</th>
<th>Status</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody id="tournament-list">
</tbody>
</table>
<p id="loading-tournaments" class="hidden">Lade Turniere...</p>
</section>
<section id="player-management" class="content-section">
<div class="section-header">
<h2>Spielerverwaltung</h2>
<button disabled>Neuen Spieler hinzufügen</button> </div>
<p>Funktionalität für Spieler (hinzufügen, bearbeiten, löschen, importieren, exportieren) wird hier implementiert.</p>
</section>
<section id="user-management" class="content-section">
<div class="section-header">
<h2>Benutzerverwaltung</h2>
<button disabled>Neuen Benutzer hinzufügen</button> </div>
<p>Funktionalität für Benutzer (Admins, Schiedsrichter hinzufügen/verwalten) wird hier implementiert.</p>
</section>
<section id="match-management" class="content-section">
<h2>Spielverwaltung & Ergebnisse</h2>
<p>Funktionalität für Spiele (manuell hinzufügen, Ergebnisse eintragen, Turnierbaum generieren, exportieren) wird hier implementiert.</p>
</section>
<section id="statistics-section" class="content-section">
<h2>Statistiken</h2>
<p>Anzeige von Statistiken (Spielergebnisse, Quoten etc.) wird hier implementiert.</p>
</section>
<section id="settings-section" class="content-section">
<h2>Einstellungen</h2>
<p>Einstellungen (z.B. Backup-Intervall) werden hier implementiert.</p>
</section>
</div> </div> <script src="/js/admin.js"></script>
</body>
</html>

177
public/css/style.css Normal file
View File

@@ -0,0 +1,177 @@
/* public/css/style.css */
/* Very basic styling - focus on function over form as requested */
body {
font-family: sans-serif;
line-height: 1.6;
margin: 0;
padding: 20px;
background-color: #f4f4f4;
color: #333;
}
h1, h2, h3 {
color: #0056b3; /* A blue tone */
}
nav {
background-color: #333;
padding: 10px;
margin-bottom: 20px;
border-radius: 5px;
}
nav a {
color: white;
text-decoration: none;
margin-right: 15px;
padding: 5px 10px;
border-radius: 3px;
transition: background-color 0.3s ease;
}
nav a:hover, nav a.active {
background-color: #555;
}
.container {
max-width: 1200px;
margin: auto;
background: #fff;
padding: 25px;
border-radius: 8px;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}
.auth-form, .content-section {
margin-bottom: 30px;
padding: 20px;
border: 1px solid #ddd;
border-radius: 5px;
background-color: #fdfdfd;
}
label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
input[type="text"],
input[type="password"],
input[type="email"],
input[type="date"],
input[type="number"],
select,
textarea {
width: 100%;
padding: 10px;
margin-bottom: 15px;
border: 1px solid #ccc;
border-radius: 4px;
box-sizing: border-box; /* Include padding in width */
}
textarea {
min-height: 80px;
}
button {
background-color: #007bff; /* Standard blue */
color: white;
padding: 10px 18px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 1em;
transition: background-color 0.3s ease;
margin-right: 5px; /* Spacing between buttons */
}
button:hover {
background-color: #0056b3; /* Darker blue on hover */
}
button.secondary {
background-color: #6c757d; /* Grey */
}
button.secondary:hover {
background-color: #5a6268;
}
button.danger {
background-color: #dc3545; /* Red */
}
button.danger:hover {
background-color: #c82333;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
th, td {
border: 1px solid #ddd;
padding: 10px;
text-align: left;
}
th {
background-color: #e9ecef; /* Light grey header */
font-weight: bold;
}
tbody tr:nth-child(odd) {
background-color: #f8f9fa; /* Slightly off-white for odd rows */
}
tbody tr:hover {
background-color: #e2e6ea; /* Highlight on hover */
}
.error-message {
color: #dc3545; /* Red */
background-color: #f8d7da;
border: 1px solid #f5c6cb;
padding: 10px;
margin-top: 10px;
margin-bottom: 15px;
border-radius: 4px;
}
.success-message {
color: #155724; /* Green */
background-color: #d4edda;
border: 1px solid #c3e6cb;
padding: 10px;
margin-top: 10px;
margin-bottom: 15px;
border-radius: 4px;
}
.hidden {
display: none;
}
/* Basic layout for sections */
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.section-header h2 {
margin: 0;
}
/* Simple modal styling (if needed later) */
/* .modal { */
/* ... styles for modal background and container ... */
/* } */

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
}

311
public/referee.html Normal file
View File

@@ -0,0 +1,311 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Schiedsrichter-Bereich</title>
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<nav>
<a href="/admin.html">Admin</a>
<a href="/referee.html" class="active">Schiedsrichter</a>
<a href="/spectator.html">Zuschauer</a>
<a href="#" id="logout-button" style="float: right;" class="hidden">Logout</a>
</nav>
<div class="container">
<h1>Schiedsrichter-Bereich</h1>
<p id="welcome-message" class="hidden">Willkommen!</p>
<div id="login-section" class="auth-form">
<h2>Login (Schiedsrichter)</h2>
<form id="login-form">
<div>
<label for="login-username">Benutzername:</label>
<input type="text" id="login-username" required>
</div>
<div>
<label for="login-password">Passwort:</label>
<input type="password" id="login-password" required>
</div>
<button type="submit">Login</button>
<div id="login-error" class="error-message hidden"></div>
</form>
</div>
<div id="referee-content" class="hidden">
<section class="content-section">
<h2>Spiel auswählen & Ergebnisse eintragen</h2>
<p>Hier kann der Schiedsrichter ein ihm zugewiesenes Spiel auswählen und die Satzergebnisse eintragen.</p>
<div id="match-selection">
<label for="select-match">Spiel auswählen:</label>
<select id="select-match" disabled>
<option value="">-- Bitte Spiel wählen --</option>
</select>
<p id="loading-matches" class="hidden">Lade Spiele...</p>
</div>
<div id="score-entry" class="hidden">
<h3>Ergebnisse für Spiel: <span id="match-details"></span></h3>
<form id="score-form">
<div id="set-inputs"></div>
<button type="button" id="add-set-button">Satz hinzufügen</button>
<button type="submit">Ergebnis speichern</button>
<div id="score-error" class="error-message hidden"></div>
<div id="score-success" class="success-message hidden"></div>
</form>
</div>
<p><i>(Funktionalität zum Laden von Spielen und Speichern von Ergebnissen muss noch implementiert werden.)</i></p>
</section>
</div> </div> <script>
// Basic JS for Referee Login/Logout (similar to admin.js but checks for 'referee' or 'admin' role)
const loginSectionRef = document.getElementById('login-section');
const refereeContent = document.getElementById('referee-content');
const loginFormRef = document.getElementById('login-form');
const loginUsernameInputRef = document.getElementById('login-username');
const loginPasswordInputRef = document.getElementById('login-password');
const loginErrorRef = document.getElementById('login-error');
const logoutButtonRef = document.getElementById('logout-button');
const welcomeMessageRef = document.getElementById('welcome-message');
const selectMatch = document.getElementById('select-match');
const scoreEntry = document.getElementById('score-entry');
const matchDetails = document.getElementById('match-details');
const setInputs = document.getElementById('set-inputs');
const addSetButton = document.getElementById('add-set-button');
const scoreForm = document.getElementById('score-form');
const scoreError = document.getElementById('score-error');
const scoreSuccess = document.getElementById('score-success');
const loadingMatches = document.getElementById('loading-matches');
let authTokenRef = localStorage.getItem('authToken');
let currentUserRef = JSON.parse(localStorage.getItem('currentUser'));
const API_BASE_URL_REF = '/api'; // Consistent API base
const showMessageRef = (element, message, isError = true) => {
element.textContent = message;
element.className = isError ? 'error-message' : 'success-message';
element.classList.remove('hidden');
};
const hideMessageRef = (element) => {
element.classList.add('hidden');
element.textContent = '';
};
const fetchAPIRef = async (endpoint, options = {}) => {
// Reusing fetchAPI logic requires it to be in a shared file or duplicated.
// For simplicity here, we assume a similar function exists or reuse admin.js's if loaded globally (not recommended).
// Here's a simplified version for demonstration:
const headers = { 'Content-Type': 'application/json', ...options.headers };
if (authTokenRef) {
headers['Authorization'] = `Bearer ${authTokenRef}`;
}
try {
const response = await fetch(`${API_BASE_URL_REF}${endpoint}`, { ...options, headers });
if (response.status === 401 || response.status === 403) {
logoutRef();
throw new Error('Authentifizierung fehlgeschlagen.');
}
if (!response.ok) {
let errorData;
try { errorData = await response.json(); } catch { errorData = { message: response.statusText }; }
throw new Error(errorData.message || `HTTP Error: ${response.status}`);
}
if (response.status === 204) return null;
return await response.json();
} catch (error) {
console.error('API Fetch Error (Referee):', error);
throw error;
}
};
const handleLoginRef = async (event) => {
event.preventDefault();
hideMessageRef(loginErrorRef);
const username = loginUsernameInputRef.value.trim();
const password = loginPasswordInputRef.value.trim();
if (!username || !password) {
showMessageRef(loginErrorRef, 'Bitte Benutzername und Passwort eingeben.');
return;
}
try {
const data = await fetchAPIRef('/auth/login', {
method: 'POST',
body: JSON.stringify({ username, password }),
});
// Allow 'admin' or 'referee' role
if (data.token && data.user && (data.user.role === 'referee' || data.user.role === 'admin')) {
authTokenRef = data.token;
currentUserRef = data.user;
localStorage.setItem('authToken', authTokenRef);
localStorage.setItem('currentUser', JSON.stringify(currentUserRef));
updateUIRef();
loadRefereeMatches(); // Load matches for the referee
loginFormRef.reset();
} else if (data.user && data.user.role !== 'referee' && data.user.role !== 'admin') {
showMessageRef(loginErrorRef, 'Zugriff verweigert. Nur Schiedsrichter oder Admins können sich hier anmelden.');
} else {
showMessageRef(loginErrorRef, data.message || 'Login fehlgeschlagen.');
}
} catch (error) {
showMessageRef(loginErrorRef, `Login fehlgeschlagen: ${error.message}`);
}
};
const logoutRef = () => {
authTokenRef = null;
currentUserRef = null;
localStorage.removeItem('authToken');
localStorage.removeItem('currentUser');
updateUIRef();
// Clear referee specific data
selectMatch.innerHTML = '<option value="">-- Bitte Spiel wählen --</option>';
selectMatch.disabled = true;
scoreEntry.classList.add('hidden');
};
const updateUIRef = () => {
if (authTokenRef && currentUserRef && (currentUserRef.role === 'referee' || currentUserRef.role === 'admin')) {
loginSectionRef.classList.add('hidden');
refereeContent.classList.remove('hidden');
logoutButtonRef.classList.remove('hidden');
welcomeMessageRef.textContent = `Willkommen, ${currentUserRef.username}! (${currentUserRef.role})`;
welcomeMessageRef.classList.remove('hidden');
} else {
loginSectionRef.classList.remove('hidden');
refereeContent.classList.add('hidden');
logoutButtonRef.classList.add('hidden');
welcomeMessageRef.classList.add('hidden');
if (currentUserRef) { // Logged in but wrong role
showMessageRef(loginErrorRef, 'Sie sind angemeldet, haben aber keine Schiedsrichter-Berechtigung für diese Seite.');
}
}
};
// --- Referee Specific Functions (Placeholders) ---
const loadRefereeMatches = async () => {
// TODO: Implement API call to fetch matches assigned to this referee (or all ongoing matches if admin)
console.log("Lade Spiele für Schiedsrichter...");
loadingMatches.classList.remove('hidden');
selectMatch.disabled = true;
try {
// Example: Fetch matches (needs backend endpoint)
// const matches = await fetchAPIRef('/matches?status=ongoing&assignee=me'); // Fictional endpoint
const matches = []; // Placeholder
selectMatch.innerHTML = '<option value="">-- Bitte Spiel wählen --</option>'; // Reset
if (matches.length === 0) {
selectMatch.innerHTML += '<option value="" disabled>Keine aktiven Spiele gefunden</option>';
} else {
matches.forEach(match => {
const option = document.createElement('option');
option.value = match.match_id;
// Construct a readable match description
option.textContent = `Runde ${match.round || '?'} - Tisch ${match.table_number || '?'} (${match.player1_name || 'N/A'} vs ${match.player2_name || 'N/A'})`;
selectMatch.appendChild(option);
});
selectMatch.disabled = false;
}
showMessageRef(document.getElementById('match-selection'), 'TODO: Lade zugewiesene/aktuelle Spiele.', false);
} catch (error) {
showMessageRef(document.getElementById('match-selection'), `Fehler beim Laden der Spiele: ${error.message}`);
} finally {
loadingMatches.classList.add('hidden');
}
};
const displayScoreEntryForm = (matchId) => {
// TODO: Fetch current scores for the selected match if they exist
console.log("Zeige Formular für Match ID:", matchId);
hideMessageRef(scoreError);
hideMessageRef(scoreSuccess);
// Placeholder: Display match identifier
matchDetails.textContent = `ID ${matchId.substring(0, 8)}...`; // Show partial ID
// TODO: Dynamically create input fields based on game type (best of 3/5 sets) and current scores
setInputs.innerHTML = `
<div>Satz 1: <input type="number" min="0" placeholder="P1"> - <input type="number" min="0" placeholder="P2"></div>
<div>Satz 2: <input type="number" min="0" placeholder="P1"> - <input type="number" min="0" placeholder="P2"></div>
<div>Satz 3: <input type="number" min="0" placeholder="P1"> - <input type="number" min="0" placeholder="P2"></div>
`;
scoreEntry.classList.remove('hidden');
};
const handleAddSet = () => {
// TODO: Add another row of set score inputs
const setNumber = setInputs.children.length + 1;
const div = document.createElement('div');
div.innerHTML = `Satz ${setNumber}: <input type="number" min="0" placeholder="P1"> - <input type="number" min="0" placeholder="P2">`;
setInputs.appendChild(div);
};
const handleSaveScore = async (event) => {
event.preventDefault();
const matchId = selectMatch.value;
if (!matchId) return;
hideMessageRef(scoreError);
hideMessageRef(scoreSuccess);
// TODO: Collect scores from input fields
const scores = [];
const setDivs = setInputs.querySelectorAll('div');
setDivs.forEach((div, index) => {
const inputs = div.querySelectorAll('input[type="number"]');
const score1 = inputs[0].value;
const score2 = inputs[1].value;
// Basic validation: Add scores only if both are entered
if (score1 !== '' && score2 !== '') {
scores.push({ set_number: index + 1, player1_score: parseInt(score1), player2_score: parseInt(score2) });
}
});
console.log("Speichere Ergebnis für Match", matchId, scores);
try {
// TODO: Implement API call to save scores
// await fetchAPIRef(`/matches/${matchId}/score`, { method: 'POST', body: JSON.stringify({ sets: scores }) });
showMessageRef(scoreSuccess, 'Ergebnis erfolgreich gespeichert (TODO: Implement API Call).', false);
// Optionally: Reload matches or update status
} catch (error) {
showMessageRef(scoreError, `Fehler beim Speichern: ${error.message}`);
}
};
// --- Event Listeners ---
loginFormRef.addEventListener('submit', handleLoginRef);
logoutButtonRef.addEventListener('click', logoutRef);
selectMatch.addEventListener('change', (event) => {
const selectedMatchId = event.target.value;
if (selectedMatchId) {
displayScoreEntryForm(selectedMatchId);
} else {
scoreEntry.classList.add('hidden');
}
});
addSetButton.addEventListener('click', handleAddSet);
scoreForm.addEventListener('submit', handleSaveScore);
// --- Initial Load ---
updateUIRef();
if (authTokenRef && currentUserRef && (currentUserRef.role === 'referee' || currentUserRef.role === 'admin')) {
loadRefereeMatches();
}
</script>
</body>
</html>

235
public/spectator.html Normal file
View File

@@ -0,0 +1,235 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Turnierübersicht - Zuschauer</title>
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<nav>
<a href="/admin.html">Admin</a>
<a href="/referee.html">Schiedsrichter</a>
<a href="/spectator.html" class="active">Zuschauer</a>
</nav>
<div class="container">
<h1>Turnierübersicht</h1>
<section class="content-section">
<h2>Turnier auswählen</h2>
<label for="select-tournament">Aktuelles/Vergangenes Turnier:</label>
<select id="select-tournament" disabled>
<option value="">-- Bitte Turnier wählen --</option>
</select>
<p id="loading-tournaments-spectator" class="hidden">Lade Turniere...</p>
<div id="tournament-info" class="hidden">
<h3 id="selected-tournament-name"></h3>
<p id="selected-tournament-details"></p>
</div>
</section>
<div id="tournament-display" class="hidden">
<section class="content-section">
<h2>Live Spiele / Aktuelle Runde</h2>
<div id="live-matches-list">
<p><i>Live-Spiele werden hier angezeigt (Implementierung erforderlich).</i></p>
</div>
<p id="loading-live-matches" class="hidden">Lade Live-Spiele...</p>
</section>
<section class="content-section">
<h2>Turnierbaum / Gruppenphase</h2>
<div id="bracket-groups-visualization">
<p><i>Visualisierung des Turnierbaums oder der Gruppenphase wird hier angezeigt (Implementierung erforderlich).</i></p>
</div>
<p id="loading-bracket" class="hidden">Lade Turnierbaum/Gruppen...</p>
</section>
<section class="content-section">
<h2>Alle Spiele (Zeitplan)</h2>
<div id="all-matches-list">
<p><i>Liste aller vergangenen und zukünftigen Spiele wird hier angezeigt (Implementierung erforderlich).</i></p>
</div>
<p id="loading-all-matches" class="hidden">Lade alle Spiele...</p>
</section>
<section class="content-section">
<h2>Spieler & Statistiken</h2>
<div>
<label for="filter-player">Spieler suchen/filtern:</label>
<input type="text" id="filter-player" placeholder="Spielername eingeben...">
</div>
<div id="player-list">
<p><i>Spielerliste und Profile werden hier angezeigt (Implementierung erforderlich).</i></p>
</div>
<p id="loading-players" class="hidden">Lade Spieler...</p>
</section>
<section class="content-section">
<h2>Hinweise & Benachrichtigungen</h2>
<div id="notifications-area">
<p><i>Wichtige Hinweise oder Push-Benachrichtigungen werden hier angezeigt (Implementierung erforderlich).</i></p>
</div>
</section>
</div> <div id="spectator-error" class="error-message hidden"></div>
</div> <script>
// Basic JS for Spectator View
// --- DOM Elements ---
const selectTournament = document.getElementById('select-tournament');
const loadingTournamentsSpectator = document.getElementById('loading-tournaments-spectator');
const tournamentInfo = document.getElementById('tournament-info');
const selectedTournamentName = document.getElementById('selected-tournament-name');
const selectedTournamentDetails = document.getElementById('selected-tournament-details');
const tournamentDisplay = document.getElementById('tournament-display');
const spectatorError = document.getElementById('spectator-error');
// Add other display area elements (live matches list, bracket viz, etc.)
const API_BASE_URL_SPEC = '/api'; // Use relative path
const showMessageSpec = (element, message, isError = true) => {
element.textContent = message;
element.className = isError ? 'error-message' : 'success-message';
element.classList.remove('hidden');
};
const hideMessageSpec = (element) => {
element.classList.add('hidden');
element.textContent = '';
};
// --- Functions ---
// Fetch API (Simplified - assumes public endpoints or uses admin token if available for testing)
// IMPORTANT: Spectator view should ideally hit public, non-authenticated endpoints.
// For now, it might rely on the admin/referee being logged in in another tab,
// or backend needs specific public routes. Let's assume public routes exist or reuse token if present.
const fetchAPISpec = async (endpoint, options = {}) => {
const headers = { 'Content-Type': 'application/json', ...options.headers };
// Spectator view ideally shouldn't need a token, but we add it if present for now
const token = localStorage.getItem('authToken');
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
try {
const response = await fetch(`${API_BASE_URL_SPEC}${endpoint}`, { ...options, headers });
if (!response.ok) {
let errorData;
try { errorData = await response.json(); } catch { errorData = { message: response.statusText }; }
// Don't auto-logout spectator on auth errors
throw new Error(errorData.message || `HTTP Error: ${response.status}`);
}
if (response.status === 204) return null;
return await response.json();
} catch (error) {
console.error('API Fetch Error (Spectator):', error);
throw error;
}
};
// Load available tournaments into the dropdown
const loadAvailableTournaments = async () => {
loadingTournamentsSpectator.classList.remove('hidden');
selectTournament.disabled = true;
hideMessageSpec(spectatorError);
try {
// Assuming /api/tournaments is accessible without strict auth for spectators
// Or backend needs a specific public endpoint like /api/public/tournaments
const tournaments = await fetchAPISpec('/tournaments'); // Using the authenticated route for now
selectTournament.innerHTML = '<option value="">-- Bitte Turnier wählen --</option>'; // Reset
if (tournaments.length === 0) {
selectTournament.innerHTML += '<option value="" disabled>Keine Turniere verfügbar</option>';
} else {
tournaments.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') : 'Datum n.a.'})`;
selectTournament.appendChild(option);
});
selectTournament.disabled = false;
}
} catch (error) {
showMessageSpec(spectatorError, `Fehler beim Laden der Turniere: ${error.message}`);
selectTournament.innerHTML += '<option value="" disabled>Fehler beim Laden</option>';
} finally {
loadingTournamentsSpectator.classList.add('hidden');
}
};
// Load details for the selected tournament
const loadTournamentDetails = async (tournamentId) => {
console.log("Lade Details für Turnier:", tournamentId);
hideMessageSpec(spectatorError);
tournamentInfo.classList.add('hidden');
tournamentDisplay.classList.add('hidden'); // Hide match/player sections until loaded
// Show loading indicators for sections
document.getElementById('loading-live-matches').classList.remove('hidden');
document.getElementById('loading-bracket').classList.remove('hidden');
document.getElementById('loading-all-matches').classList.remove('hidden');
document.getElementById('loading-players').classList.remove('hidden');
try {
// 1. Fetch basic tournament info
const tournament = await fetchAPISpec(`/tournaments/${tournamentId}`);
selectedTournamentName.textContent = tournament.name;
const dateStr = tournament.date ? new Date(tournament.date).toLocaleDateString('de-DE') : 'N/A';
selectedTournamentDetails.textContent = `Ort: ${tournament.location || 'N/A'} | Datum: ${dateStr} | Typ: ${tournament.tournament_type}`;
// TODO: Display description, logo etc.
tournamentInfo.classList.remove('hidden');
// 2. Fetch related data (matches, players, etc.) - Implement these API calls
// const liveMatches = await fetchAPISpec(`/matches?tournament=${tournamentId}&status=ongoing`);
// const allMatches = await fetchAPISpec(`/matches?tournament=${tournamentId}`);
// const players = await fetchAPISpec(`/players?tournament=${tournamentId}`); // Needs backend support
// const bracketData = await fetchAPISpec(`/tournaments/${tournamentId}/bracket`); // Needs backend support
// TODO: Render the fetched data into the respective sections
document.getElementById('live-matches-list').innerHTML = `<p><i>Live-Spiele für ${tournament.name} laden... (TODO)</i></p>`;
document.getElementById('bracket-groups-visualization').innerHTML = `<p><i>Turnierbaum für ${tournament.name} laden... (TODO)</i></p>`;
document.getElementById('all-matches-list').innerHTML = `<p><i>Alle Spiele für ${tournament.name} laden... (TODO)</i></p>`;
document.getElementById('player-list').innerHTML = `<p><i>Spieler für ${tournament.name} laden... (TODO)</i></p>`;
tournamentDisplay.classList.remove('hidden'); // Show the details sections
} catch (error) {
showMessageSpec(spectatorError, `Fehler beim Laden der Turnierdetails: ${error.message}`);
tournamentDisplay.classList.add('hidden'); // Hide details on error
} finally {
// Hide loading indicators
document.getElementById('loading-live-matches').classList.add('hidden');
document.getElementById('loading-bracket').classList.add('hidden');
document.getElementById('loading-all-matches').classList.add('hidden');
document.getElementById('loading-players').classList.add('hidden');
}
};
// --- Event Listeners ---
selectTournament.addEventListener('change', (event) => {
const selectedId = event.target.value;
if (selectedId) {
loadTournamentDetails(selectedId);
} else {
tournamentInfo.classList.add('hidden');
tournamentDisplay.classList.add('hidden');
hideMessageSpec(spectatorError);
}
});
// TODO: Add event listener for player filter input
// --- Initial Load ---
loadAvailableTournaments();
</script>
</body>
</html>