fixes
This commit is contained in:
@@ -5,6 +5,31 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Schiedsrichter-Bereich</title>
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
<style>
|
||||
/* Zusätzliche Stile für die Ergebniseingabe */
|
||||
.set-score-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
gap: 10px; /* Abstand zwischen Elementen */
|
||||
}
|
||||
.set-score-row label {
|
||||
width: 50px; /* Feste Breite für "Satz X:" */
|
||||
margin-bottom: 0; /* Label neben Inputs */
|
||||
}
|
||||
.set-score-row input[type="number"] {
|
||||
width: 60px; /* Schmalere Input-Felder für Punkte */
|
||||
margin-bottom: 0;
|
||||
text-align: center;
|
||||
}
|
||||
.set-score-row span {
|
||||
margin: 0 5px;
|
||||
}
|
||||
#match-details-players {
|
||||
font-weight: bold;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav>
|
||||
@@ -37,31 +62,33 @@
|
||||
<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>
|
||||
<label for="select-match">Aktives/Geplantes 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>
|
||||
<p id="loading-matches" class="loading-indicator hidden">Lade verfügbare Spiele...</p>
|
||||
<div id="match-load-error" class="message-area error-message hidden"></div>
|
||||
</div>
|
||||
|
||||
<div id="score-entry" class="hidden">
|
||||
<h3>Ergebnisse für Spiel: <span id="match-details"></span></h3>
|
||||
<div id="score-entry" class="hidden" style="margin-top: 20px; padding-top: 20px; border-top: 1px solid #eee;">
|
||||
<h3>Ergebnisse eintragen</h3>
|
||||
<div id="match-details-players">Spieler: Lädt...</div>
|
||||
<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>
|
||||
<div id="set-inputs">
|
||||
</div>
|
||||
<div style="margin-top: 15px;">
|
||||
<button type="button" id="add-set-button">Weiteren Satz hinzufügen</button>
|
||||
<button type="submit">Ergebnis speichern</button>
|
||||
</div>
|
||||
<div id="score-error" class="message-area error-message hidden" style="margin-top: 15px;"></div>
|
||||
<div id="score-success" class="message-area success-message hidden" style="margin-top: 15px;"></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)
|
||||
|
||||
// --- DOM Elements ---
|
||||
const loginSectionRef = document.getElementById('login-section');
|
||||
const refereeContent = document.getElementById('referee-content');
|
||||
const loginFormRef = document.getElementById('login-form');
|
||||
@@ -71,36 +98,50 @@
|
||||
const logoutButtonRef = document.getElementById('logout-button');
|
||||
const welcomeMessageRef = document.getElementById('welcome-message');
|
||||
const selectMatch = document.getElementById('select-match');
|
||||
const loadingMatches = document.getElementById('loading-matches');
|
||||
const matchLoadError = document.getElementById('match-load-error');
|
||||
const scoreEntry = document.getElementById('score-entry');
|
||||
const matchDetails = document.getElementById('match-details');
|
||||
const matchDetailsPlayers = document.getElementById('match-details-players');
|
||||
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');
|
||||
|
||||
|
||||
// --- State ---
|
||||
let authTokenRef = localStorage.getItem('authToken');
|
||||
let currentUserRef = JSON.parse(localStorage.getItem('currentUser'));
|
||||
const API_BASE_URL_REF = '/api'; // Consistent API base
|
||||
let currentSelectedMatch = null; // Store details of the selected match
|
||||
|
||||
const showMessageRef = (element, message, isError = true) => {
|
||||
// --- API Base URL ---
|
||||
const API_BASE_URL_REF = '/api';
|
||||
|
||||
// --- Helper Functions ---
|
||||
const showMessageRef = (element, message, isError = true, autohide = false) => {
|
||||
if (!element) return;
|
||||
element.textContent = message;
|
||||
element.className = isError ? 'error-message' : 'success-message';
|
||||
element.className = `message-area ${isError ? 'error-message' : 'success-message'}`;
|
||||
element.classList.remove('hidden');
|
||||
if (autohide) {
|
||||
setTimeout(() => hideMessageRef(element), 5000);
|
||||
}
|
||||
};
|
||||
|
||||
const hideMessageRef = (element) => {
|
||||
element.classList.add('hidden');
|
||||
element.textContent = '';
|
||||
if (element) {
|
||||
element.classList.add('hidden');
|
||||
element.textContent = '';
|
||||
}
|
||||
};
|
||||
|
||||
const setLoadingRef = (element, isLoading) => {
|
||||
if (element) {
|
||||
element.classList.toggle('hidden', !isLoading);
|
||||
}
|
||||
}
|
||||
|
||||
// --- API Fetch Function ---
|
||||
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}`;
|
||||
@@ -108,23 +149,26 @@
|
||||
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.');
|
||||
logoutRef(); // Logout on auth error
|
||||
throw new Error('Authentifizierung fehlgeschlagen oder Token abgelaufen.');
|
||||
}
|
||||
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;
|
||||
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) { /* Ignore parsing error */ }
|
||||
throw new Error(errorData.message);
|
||||
}
|
||||
if (response.status === 204) return null; // Handle No Content
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('API Fetch Error (Referee):', error);
|
||||
throw error;
|
||||
throw error; // Re-throw for handling in calling function
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// --- Authentication ---
|
||||
const handleLoginRef = async (event) => {
|
||||
event.preventDefault();
|
||||
hideMessageRef(loginErrorRef);
|
||||
@@ -153,6 +197,7 @@
|
||||
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.');
|
||||
logoutRef(); // Log out if wrong role but valid login somehow occurred
|
||||
} else {
|
||||
showMessageRef(loginErrorRef, data.message || 'Login fehlgeschlagen.');
|
||||
}
|
||||
@@ -171,114 +216,275 @@
|
||||
selectMatch.innerHTML = '<option value="">-- Bitte Spiel wählen --</option>';
|
||||
selectMatch.disabled = true;
|
||||
scoreEntry.classList.add('hidden');
|
||||
currentSelectedMatch = null;
|
||||
};
|
||||
|
||||
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.');
|
||||
}
|
||||
}
|
||||
const isAllowedUser = authTokenRef && currentUserRef && (currentUserRef.role === 'referee' || currentUserRef.role === 'admin');
|
||||
loginSectionRef.classList.toggle('hidden', isAllowedUser);
|
||||
refereeContent.classList.toggle('hidden', !isAllowedUser);
|
||||
logoutButtonRef.classList.toggle('hidden', !isAllowedUser);
|
||||
welcomeMessageRef.classList.toggle('hidden', !isAllowedUser);
|
||||
if (isAllowedUser) {
|
||||
welcomeMessageRef.textContent = `Willkommen, ${currentUserRef.username}! (${currentUserRef.role})`;
|
||||
} else {
|
||||
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) ---
|
||||
// --- Referee Specific Functions ---
|
||||
|
||||
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');
|
||||
setLoadingRef(loadingMatches, true);
|
||||
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="">Lade Spiele...</option>';
|
||||
hideMessageRef(matchLoadError);
|
||||
scoreEntry.classList.add('hidden'); // Hide score entry while loading matches
|
||||
currentSelectedMatch = null;
|
||||
|
||||
let allRelevantMatches = [];
|
||||
|
||||
try {
|
||||
// Inefficient approach: Fetch all tournaments, then fetch relevant matches for each.
|
||||
// A dedicated backend endpoint would be much better.
|
||||
console.warn("Fetching all tournaments to find matches - consider optimizing backend.");
|
||||
const tournaments = await fetchAPIRef('/tournaments'); // Assumes this endpoint is available
|
||||
|
||||
// Array to hold promises for fetching matches for each tournament
|
||||
const matchFetchPromises = tournaments.map(tournament =>
|
||||
fetchAPIRef(`/matches?tournamentId=${tournament.tournament_id}&status=scheduled`)
|
||||
.catch(e => { console.error(`Error fetching scheduled matches for ${tournament.name}:`, e); return []; }) // Ignore errors for single tournament fetch
|
||||
);
|
||||
const matchFetchPromisesOngoing = tournaments.map(tournament =>
|
||||
fetchAPIRef(`/matches?tournamentId=${tournament.tournament_id}&status=ongoing`)
|
||||
.catch(e => { console.error(`Error fetching ongoing matches for ${tournament.name}:`, e); return []; }) // Ignore errors for single tournament fetch
|
||||
);
|
||||
|
||||
|
||||
// Wait for all fetches to complete
|
||||
const resultsScheduled = await Promise.all(matchFetchPromises);
|
||||
const resultsOngoing = await Promise.all(matchFetchPromisesOngoing);
|
||||
|
||||
// Flatten the results and add tournament name for display
|
||||
resultsScheduled.forEach((matches, index) => {
|
||||
matches.forEach(match => {
|
||||
match.tournament_name = tournaments[index].name; // Add tournament name
|
||||
allRelevantMatches.push(match);
|
||||
});
|
||||
});
|
||||
resultsOngoing.forEach((matches, index) => {
|
||||
matches.forEach(match => {
|
||||
match.tournament_name = tournaments[index].name; // Add tournament name
|
||||
// Avoid duplicates if a match was somehow fetched twice
|
||||
if (!allRelevantMatches.some(m => m.match_id === match.match_id)) {
|
||||
allRelevantMatches.push(match);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// Sort matches (e.g., by tournament, then round, then time)
|
||||
allRelevantMatches.sort((a, b) => {
|
||||
if (a.tournament_name < b.tournament_name) return -1;
|
||||
if (a.tournament_name > b.tournament_name) return 1;
|
||||
if ((a.round || 999) < (b.round || 999)) return -1; // Treat null rounds as last
|
||||
if ((a.round || 999) > (b.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;
|
||||
});
|
||||
|
||||
// Populate dropdown
|
||||
selectMatch.innerHTML = '<option value="">-- Bitte Spiel wählen --</option>'; // Reset
|
||||
if (matches.length === 0) {
|
||||
selectMatch.innerHTML += '<option value="" disabled>Keine aktiven Spiele gefunden</option>';
|
||||
if (allRelevantMatches.length === 0) {
|
||||
selectMatch.innerHTML += '<option value="" disabled>Keine aktiven/geplanten Spiele gefunden</option>';
|
||||
} else {
|
||||
matches.forEach(match => {
|
||||
allRelevantMatches.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'})`;
|
||||
const timeStr = match.scheduled_time ? `(${new Date(match.scheduled_time).toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })})` : '';
|
||||
option.textContent = `[${match.tournament_name}] Rd ${match.round || '?'} Tisch ${match.table_number || '?'} - ${match.player1_name || 'N/A'} vs ${match.player2_name || 'N/A'} ${timeStr}`;
|
||||
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}`);
|
||||
showMessageRef(matchLoadError, `Fehler beim Laden der Spiele: ${error.message}`);
|
||||
selectMatch.innerHTML = '<option value="" disabled>Fehler beim Laden</option>';
|
||||
} finally {
|
||||
loadingMatches.classList.add('hidden');
|
||||
setLoadingRef(loadingMatches, false);
|
||||
}
|
||||
};
|
||||
|
||||
const displayScoreEntryForm = (matchId) => {
|
||||
// TODO: Fetch current scores for the selected match if they exist
|
||||
// Display the score entry form for the selected match
|
||||
const displayScoreEntryForm = async (matchId) => {
|
||||
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>
|
||||
`;
|
||||
setInputs.innerHTML = '<div>Lade Spieldetails...</div>'; // Placeholder
|
||||
scoreEntry.classList.remove('hidden');
|
||||
|
||||
try {
|
||||
// Fetch full match details, including existing sets and player names
|
||||
currentSelectedMatch = await fetchAPIRef(`/matches/${matchId}`);
|
||||
if (!currentSelectedMatch) {
|
||||
throw new Error("Spieldetails konnten nicht geladen werden.");
|
||||
}
|
||||
|
||||
matchDetailsPlayers.textContent = `Spieler: ${currentSelectedMatch.player1_name || 'Spieler 1'} vs ${currentSelectedMatch.player2_name || 'Spieler 2'}`;
|
||||
|
||||
// Dynamically create input fields based on game type (best of 5 default) and existing scores
|
||||
setInputs.innerHTML = ''; // Clear placeholder/previous inputs
|
||||
const maxSets = 5; // Assume best of 5 for now
|
||||
const existingSets = currentSelectedMatch.sets || [];
|
||||
|
||||
for (let i = 1; i <= maxSets; i++) {
|
||||
const existingSet = existingSets.find(s => s.set_number === i);
|
||||
createSetInputRow(i, existingSet?.player1_score, existingSet?.player2_score);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
showMessageRef(scoreError, `Fehler beim Laden der Spieldetails: ${error.message}`);
|
||||
matchDetailsPlayers.textContent = 'Fehler';
|
||||
setInputs.innerHTML = ''; // Clear on error
|
||||
currentSelectedMatch = null; // Reset selected match on error
|
||||
}
|
||||
};
|
||||
|
||||
// Helper to create a row of set score inputs
|
||||
const createSetInputRow = (setNumber, score1 = '', score2 = '') => {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'set-score-row';
|
||||
div.dataset.setNumber = setNumber; // Store set number
|
||||
|
||||
const label = document.createElement('label');
|
||||
label.htmlFor = `set-${setNumber}-p1`;
|
||||
label.textContent = `Satz ${setNumber}:`;
|
||||
|
||||
const input1 = document.createElement('input');
|
||||
input1.type = 'number';
|
||||
input1.id = `set-${setNumber}-p1`;
|
||||
input1.min = "0";
|
||||
input1.placeholder = "P1";
|
||||
input1.value = score1; // Pre-fill if score exists
|
||||
input1.dataset.player = "1";
|
||||
|
||||
const separator = document.createElement('span');
|
||||
separator.textContent = "-";
|
||||
|
||||
const input2 = document.createElement('input');
|
||||
input2.type = 'number';
|
||||
input2.id = `set-${setNumber}-p2`;
|
||||
input2.min = "0";
|
||||
input2.placeholder = "P2";
|
||||
input2.value = score2; // Pre-fill if score exists
|
||||
input2.dataset.player = "2";
|
||||
|
||||
div.appendChild(label);
|
||||
div.appendChild(input1);
|
||||
div.appendChild(separator);
|
||||
div.appendChild(input2);
|
||||
setInputs.appendChild(div);
|
||||
};
|
||||
|
||||
|
||||
// Handle adding another set input row
|
||||
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 currentSetCount = setInputs.children.length;
|
||||
createSetInputRow(currentSetCount + 1);
|
||||
};
|
||||
|
||||
// Handle saving the scores
|
||||
const handleSaveScore = async (event) => {
|
||||
event.preventDefault();
|
||||
const matchId = selectMatch.value;
|
||||
if (!matchId) return;
|
||||
if (!matchId || !currentSelectedMatch) {
|
||||
showMessageRef(scoreError, "Kein gültiges Spiel ausgewählt.");
|
||||
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
|
||||
// Collect scores from input fields
|
||||
const setsPayload = [];
|
||||
const setRows = setInputs.querySelectorAll('.set-score-row');
|
||||
let formIsValid = true;
|
||||
|
||||
setRows.forEach((row) => {
|
||||
const setNumber = parseInt(row.dataset.setNumber);
|
||||
const inputs = row.querySelectorAll('input[type="number"]');
|
||||
const score1Input = inputs[0];
|
||||
const score2Input = inputs[1];
|
||||
const score1 = score1Input.value.trim();
|
||||
const score2 = score2Input.value.trim();
|
||||
|
||||
// Add scores only if both are entered and valid numbers
|
||||
if (score1 !== '' && score2 !== '') {
|
||||
scores.push({ set_number: index + 1, player1_score: parseInt(score1), player2_score: parseInt(score2) });
|
||||
const p1Score = parseInt(score1);
|
||||
const p2Score = parseInt(score2);
|
||||
if (!isNaN(p1Score) && !isNaN(p2Score) && p1Score >= 0 && p2Score >= 0) {
|
||||
setsPayload.push({ set_number: setNumber, player1_score: p1Score, player2_score: p2Score });
|
||||
} else {
|
||||
showMessageRef(scoreError, `Ungültige Eingabe in Satz ${setNumber}. Bitte nur positive Zahlen eingeben.`);
|
||||
formIsValid = false;
|
||||
// Highlight invalid inputs (optional)
|
||||
score1Input.style.borderColor = 'red';
|
||||
score2Input.style.borderColor = 'red';
|
||||
}
|
||||
} else if (score1 !== '' || score2 !== '') {
|
||||
// Only one score entered for the set
|
||||
showMessageRef(scoreError, `Bitte beide Punktzahlen für Satz ${setNumber} eingeben oder beide leer lassen.`);
|
||||
formIsValid = false;
|
||||
score1Input.style.borderColor = 'red';
|
||||
score2Input.style.borderColor = 'red';
|
||||
} else {
|
||||
// Both empty, reset border (optional)
|
||||
score1Input.style.borderColor = '';
|
||||
score2Input.style.borderColor = '';
|
||||
}
|
||||
});
|
||||
|
||||
console.log("Speichere Ergebnis für Match", matchId, scores);
|
||||
if (!formIsValid) {
|
||||
return; // Stop if validation failed
|
||||
}
|
||||
|
||||
if (setsPayload.length === 0) {
|
||||
showMessageRef(scoreError, "Keine gültigen Satzergebnisse zum Speichern eingegeben.");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("Speichere Ergebnis für Match", matchId, setsPayload);
|
||||
|
||||
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
|
||||
// Call the API endpoint to update scores
|
||||
const result = await fetchAPIRef(`/matches/${matchId}/score`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ sets: setsPayload })
|
||||
});
|
||||
|
||||
showMessageRef(scoreSuccess, 'Ergebnis erfolgreich gespeichert.', false, true); // Autohide success message
|
||||
// Update the displayed form with potentially recalculated winner/status from response
|
||||
if (result && result.match) {
|
||||
currentSelectedMatch = result.match; // Update local match data
|
||||
// Re-display the form with updated data (especially if winner/status changed)
|
||||
matchDetailsPlayers.textContent = `Spieler: ${currentSelectedMatch.player1_name || 'Spieler 1'} vs ${currentSelectedMatch.player2_name || 'Spieler 2'}`;
|
||||
setInputs.innerHTML = ''; // Clear old inputs
|
||||
const maxSets = 5;
|
||||
const existingSets = currentSelectedMatch.sets || [];
|
||||
for (let i = 1; i <= maxSets; i++) {
|
||||
const existingSet = existingSets.find(s => s.set_number === i);
|
||||
createSetInputRow(i, existingSet?.player1_score, existingSet?.player2_score);
|
||||
}
|
||||
// Optionally update the match list dropdown if status changed
|
||||
// loadRefereeMatches(); // Could reload the whole list, maybe too much?
|
||||
}
|
||||
|
||||
|
||||
} catch (error) {
|
||||
showMessageRef(scoreError, `Fehler beim Speichern: ${error.message}`);
|
||||
}
|
||||
@@ -288,23 +494,28 @@
|
||||
// --- 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');
|
||||
scoreEntry.classList.add('hidden'); // Hide form if default option selected
|
||||
currentSelectedMatch = null;
|
||||
}
|
||||
});
|
||||
|
||||
addSetButton.addEventListener('click', handleAddSet);
|
||||
scoreForm.addEventListener('submit', handleSaveScore);
|
||||
|
||||
|
||||
// --- Initial Load ---
|
||||
updateUIRef();
|
||||
if (authTokenRef && currentUserRef && (currentUserRef.role === 'referee' || currentUserRef.role === 'admin')) {
|
||||
loadRefereeMatches();
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
updateUIRef();
|
||||
if (authTokenRef && currentUserRef && (currentUserRef.role === 'referee' || currentUserRef.role === 'admin')) {
|
||||
loadRefereeMatches();
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
</body>
|
||||
|
@@ -5,6 +5,39 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Turnierübersicht - Zuschauer</title>
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
<style>
|
||||
/* Optional: Zusätzliche Stile für die Zuschaueransicht */
|
||||
.spectator-section h2 {
|
||||
border-bottom: 2px solid #007bff;
|
||||
padding-bottom: 5px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.match-card { /* Beispiel für Live-Spiel-Anzeige */
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
padding: 15px;
|
||||
margin-bottom: 15px;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
.match-card .players {
|
||||
font-size: 1.1em;
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.match-card .details {
|
||||
font-size: 0.9em;
|
||||
color: #555;
|
||||
}
|
||||
.match-card .score { /* Für Live-Score später */
|
||||
font-size: 1.2em;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
margin: 10px 0;
|
||||
}
|
||||
#player-list table th, #player-list table td {
|
||||
padding: 8px; /* Etwas kompakter für Spielerliste */
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav>
|
||||
@@ -16,114 +49,147 @@
|
||||
<div class="container">
|
||||
<h1>Turnierübersicht</h1>
|
||||
|
||||
<section class="content-section">
|
||||
<section class="content-section spectator-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>
|
||||
<p id="loading-tournaments-spectator" class="loading-indicator hidden">Lade Turniere...</p>
|
||||
<div id="tournament-info" class="hidden" style="margin-top: 15px; padding: 10px; background-color: #e9ecef; border-radius: 5px;">
|
||||
<h3 id="selected-tournament-name" style="margin: 0 0 5px 0;"></h3>
|
||||
<p id="selected-tournament-details" style="margin: 0; font-size: 0.9em;"></p>
|
||||
<p id="selected-tournament-description" style="margin: 5px 0 0 0; font-size: 0.9em;"></p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div id="tournament-display" class="hidden">
|
||||
|
||||
<section class="content-section">
|
||||
<h2>Live Spiele / Aktuelle Runde</h2>
|
||||
<section class="content-section spectator-section">
|
||||
<h2><span class="live-indicator" style="color: red;">●</span> Live Spiele / Aktuelle Runde</h2>
|
||||
<div id="live-matches-list">
|
||||
<p><i>Live-Spiele werden hier angezeigt (Implementierung erforderlich).</i></p>
|
||||
<p><i>Keine Live-Spiele gefunden oder Turnier nicht ausgewählt.</i></p>
|
||||
</div>
|
||||
<p id="loading-live-matches" class="hidden">Lade Live-Spiele...</p>
|
||||
<p id="loading-live-matches" class="loading-indicator hidden">Lade Live-Spiele...</p>
|
||||
<div id="live-matches-error" class="message-area error-message hidden"></div>
|
||||
</section>
|
||||
|
||||
<section class="content-section">
|
||||
<section class="content-section spectator-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>
|
||||
<p><i>Visualisierung des Turnierbaums oder der Gruppenphase ist für dieses Turnier nicht verfügbar oder noch nicht implementiert.</i></p>
|
||||
</div>
|
||||
<p id="loading-bracket" class="hidden">Lade Turnierbaum/Gruppen...</p>
|
||||
<p id="loading-bracket" class="loading-indicator hidden">Lade Turnierbaum/Gruppen...</p>
|
||||
<div id="bracket-error" class="message-area error-message hidden"></div>
|
||||
</section>
|
||||
|
||||
<section class="content-section">
|
||||
<section class="content-section spectator-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>
|
||||
<p><i>Keine Spiele gefunden oder Turnier nicht ausgewählt.</i></p>
|
||||
</div>
|
||||
<p id="loading-all-matches" class="hidden">Lade alle Spiele...</p>
|
||||
<p id="loading-all-matches" class="loading-indicator hidden">Lade alle Spiele...</p>
|
||||
<div id="all-matches-error" class="message-area error-message hidden"></div>
|
||||
</section>
|
||||
|
||||
<section class="content-section">
|
||||
<h2>Spieler & Statistiken</h2>
|
||||
<section class="content-section spectator-section">
|
||||
<h2>Spieler</h2>
|
||||
<div>
|
||||
<label for="filter-player">Spieler suchen/filtern:</label>
|
||||
<input type="text" id="filter-player" placeholder="Spielername eingeben...">
|
||||
<label for="filter-player">Spieler suchen:</label>
|
||||
<input type="text" id="filter-player" placeholder="Namen oder Verein eingeben...">
|
||||
<small style="display: block; margin-top: 5px;"><i>Hinweis: Zeigt aktuell alle Spieler im System, nicht nur die des ausgewählten Turniers.</i></small>
|
||||
</div>
|
||||
<div id="player-list">
|
||||
<p><i>Spielerliste und Profile werden hier angezeigt (Implementierung erforderlich).</i></p>
|
||||
<div id="player-list" style="margin-top: 15px;">
|
||||
<p><i>Keine Spieler gefunden.</i></p>
|
||||
</div>
|
||||
<p id="loading-players" class="hidden">Lade Spieler...</p>
|
||||
<p id="loading-players" class="loading-indicator hidden">Lade Spieler...</p>
|
||||
<div id="player-list-error" class="message-area error-message hidden"></div>
|
||||
</section>
|
||||
|
||||
<section class="content-section">
|
||||
<section class="content-section spectator-section">
|
||||
<h2>Hinweise & Benachrichtigungen</h2>
|
||||
<div id="notifications-area">
|
||||
<p><i>Wichtige Hinweise oder Push-Benachrichtigungen werden hier angezeigt (Implementierung erforderlich).</i></p>
|
||||
<p><i>Keine aktuellen Hinweise für dieses Turnier vorhanden oder Funktion noch nicht implementiert.</i></p>
|
||||
</div>
|
||||
<p id="loading-notifications" class="loading-indicator hidden">Lade Hinweise...</p>
|
||||
<div id="notifications-error" class="message-area error-message hidden"></div>
|
||||
</section>
|
||||
|
||||
</div> <div id="spectator-error" class="error-message hidden"></div>
|
||||
|
||||
</div> <script>
|
||||
// Basic JS for Spectator View
|
||||
|
||||
</div> <div id="spectator-error" class="message-area error-message hidden"></div> </div> <script>
|
||||
// --- 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 selectedTournamentDescription = document.getElementById('selected-tournament-description');
|
||||
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 liveMatchesList = document.getElementById('live-matches-list');
|
||||
const loadingLiveMatches = document.getElementById('loading-live-matches');
|
||||
const liveMatchesError = document.getElementById('live-matches-error');
|
||||
|
||||
const showMessageSpec = (element, message, isError = true) => {
|
||||
const bracketViz = document.getElementById('bracket-groups-visualization');
|
||||
const loadingBracket = document.getElementById('loading-bracket');
|
||||
const bracketError = document.getElementById('bracket-error');
|
||||
|
||||
const allMatchesList = document.getElementById('all-matches-list');
|
||||
const loadingAllMatches = document.getElementById('loading-all-matches');
|
||||
const allMatchesError = document.getElementById('all-matches-error');
|
||||
|
||||
const filterPlayerInput = document.getElementById('filter-player');
|
||||
const playerListDiv = document.getElementById('player-list');
|
||||
const loadingPlayers = document.getElementById('loading-players');
|
||||
const playerListError = document.getElementById('player-list-error');
|
||||
|
||||
const notificationsArea = document.getElementById('notifications-area');
|
||||
const loadingNotifications = document.getElementById('loading-notifications');
|
||||
const notificationsError = document.getElementById('notifications-error');
|
||||
|
||||
// --- State ---
|
||||
let allPlayersCache = []; // Cache all players fetched once
|
||||
let currentTournamentId = null;
|
||||
|
||||
// --- API Base URL ---
|
||||
const API_BASE_URL_SPEC = '/api';
|
||||
|
||||
// --- Helper Functions ---
|
||||
const showMessageSpec = (element, message, isError = true) => {
|
||||
if (!element) return;
|
||||
element.textContent = message;
|
||||
element.className = isError ? 'error-message' : 'success-message';
|
||||
element.className = `message-area ${isError ? 'error-message' : 'success-message'}`;
|
||||
element.classList.remove('hidden');
|
||||
};
|
||||
|
||||
const hideMessageSpec = (element) => {
|
||||
element.classList.add('hidden');
|
||||
element.textContent = '';
|
||||
if (element) {
|
||||
element.classList.add('hidden');
|
||||
element.textContent = '';
|
||||
}
|
||||
};
|
||||
|
||||
const setLoadingSpec = (element, isLoading) => {
|
||||
if (element) {
|
||||
element.classList.toggle('hidden', !isLoading);
|
||||
}
|
||||
}
|
||||
|
||||
// --- 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.
|
||||
// --- API Fetch Function (Spectator - No Auth Header) ---
|
||||
// Spectator endpoints should ideally be public.
|
||||
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}`;
|
||||
}
|
||||
// No Authorization header for public spectator view
|
||||
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}`);
|
||||
}
|
||||
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) { /* Ignore */ }
|
||||
throw new Error(errorData.message);
|
||||
}
|
||||
if (response.status === 204) return null;
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
@@ -133,20 +199,23 @@
|
||||
};
|
||||
|
||||
|
||||
// --- Core Functions ---
|
||||
|
||||
// Load available tournaments into the dropdown
|
||||
const loadAvailableTournaments = async () => {
|
||||
loadingTournamentsSpectator.classList.remove('hidden');
|
||||
setLoadingSpec(loadingTournamentsSpectator, true);
|
||||
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
|
||||
// Assuming /api/tournaments is accessible without auth
|
||||
const tournaments = await fetchAPISpec('/tournaments');
|
||||
|
||||
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 {
|
||||
// Sort tournaments, e.g., by date descending
|
||||
tournaments.sort((a, b) => (b.date || '').localeCompare(a.date || ''));
|
||||
tournaments.forEach(t => {
|
||||
const option = document.createElement('option');
|
||||
option.value = t.tournament_id;
|
||||
@@ -159,76 +228,266 @@
|
||||
showMessageSpec(spectatorError, `Fehler beim Laden der Turniere: ${error.message}`);
|
||||
selectTournament.innerHTML += '<option value="" disabled>Fehler beim Laden</option>';
|
||||
} finally {
|
||||
loadingTournamentsSpectator.classList.add('hidden');
|
||||
setLoadingSpec(loadingTournamentsSpectator, false);
|
||||
}
|
||||
};
|
||||
|
||||
// Load details for the selected tournament
|
||||
// Load details and related data for the selected tournament
|
||||
const loadTournamentDetails = async (tournamentId) => {
|
||||
console.log("Lade Details für Turnier:", tournamentId);
|
||||
currentTournamentId = tournamentId; // Store current ID
|
||||
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');
|
||||
tournamentDisplay.classList.add('hidden'); // Hide sections until loaded
|
||||
|
||||
setLoadingSpec(loadingLiveMatches, true);
|
||||
setLoadingSpec(loadingBracket, true); // Show loading even if not implemented
|
||||
setLoadingSpec(loadingAllMatches, true);
|
||||
setLoadingSpec(loadingNotifications, true); // Show loading even if not implemented
|
||||
|
||||
try {
|
||||
// 1. Fetch basic tournament info
|
||||
const tournament = await fetchAPISpec(`/tournaments/${tournamentId}`);
|
||||
selectedTournamentName.textContent = tournament.name;
|
||||
selectedTournamentName.textContent = tournament.name || 'Unbekanntes Turnier';
|
||||
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.
|
||||
const typeStr = tournament.tournament_type === 'knockout' ? 'KO-System' : 'Gruppenphase';
|
||||
selectedTournamentDetails.textContent = `Ort: ${tournament.location || 'N/A'} | Datum: ${dateStr} | Typ: ${typeStr}`;
|
||||
selectedTournamentDescription.textContent = tournament.description || '';
|
||||
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>`;
|
||||
|
||||
// 2. Trigger loading of related data concurrently
|
||||
await Promise.all([
|
||||
loadLiveMatches(tournamentId),
|
||||
loadAllMatches(tournamentId),
|
||||
loadBracketInfo(tournamentId), // Placeholder call
|
||||
loadNotifications(tournamentId) // Placeholder call
|
||||
]);
|
||||
|
||||
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');
|
||||
}
|
||||
};
|
||||
|
||||
// Load and display live matches
|
||||
const loadLiveMatches = async (tournamentId) => {
|
||||
setLoadingSpec(loadingLiveMatches, true);
|
||||
liveMatchesList.innerHTML = '';
|
||||
hideMessageSpec(liveMatchesError);
|
||||
try {
|
||||
const matches = await fetchAPISpec(`/matches?tournamentId=${tournamentId}&status=ongoing`);
|
||||
if (matches.length === 0) {
|
||||
liveMatchesList.innerHTML = '<p><i>Aktuell keine laufenden Spiele.</i></p>';
|
||||
} else {
|
||||
matches.forEach(match => {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'match-card';
|
||||
// TODO: Display live score if available (requires backend changes)
|
||||
card.innerHTML = `
|
||||
<div class="players">${match.player1_name || 'N/A'} vs ${match.player2_name || 'N/A'}</div>
|
||||
<div class="details">
|
||||
Runde: ${match.round || '?'} | Tisch: ${match.table_number || '?'}
|
||||
</div>
|
||||
`;
|
||||
liveMatchesList.appendChild(card);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
showMessageSpec(liveMatchesError, `Fehler beim Laden der Live-Spiele: ${error.message}`);
|
||||
liveMatchesList.innerHTML = '<p><i>Fehler beim Laden der Live-Spiele.</i></p>';
|
||||
} finally {
|
||||
setLoadingSpec(loadingLiveMatches, false);
|
||||
}
|
||||
};
|
||||
|
||||
// Load and display all matches (schedule)
|
||||
const loadAllMatches = async (tournamentId) => {
|
||||
setLoadingSpec(loadingAllMatches, true);
|
||||
allMatchesList.innerHTML = '';
|
||||
hideMessageSpec(allMatchesError);
|
||||
try {
|
||||
const matches = await fetchAPISpec(`/matches?tournamentId=${tournamentId}`);
|
||||
// Sort matches: by round, then match number, then scheduled time
|
||||
matches.sort((a, b) => {
|
||||
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;
|
||||
});
|
||||
|
||||
|
||||
if (matches.length === 0) {
|
||||
allMatchesList.innerHTML = '<p><i>Keine Spiele für dieses Turnier gefunden.</i></p>';
|
||||
} else {
|
||||
const table = document.createElement('table');
|
||||
table.innerHTML = `
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Runde</th>
|
||||
<th>Spiel Nr.</th>
|
||||
<th>Spieler 1</th>
|
||||
<th>Spieler 2</th>
|
||||
<th>Ergebnis</th>
|
||||
<th>Status</th>
|
||||
<th>Geplant</th>
|
||||
<th>Tisch</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
`;
|
||||
const tbody = table.querySelector('tbody');
|
||||
matches.forEach(match => {
|
||||
const row = tbody.insertRow();
|
||||
const scheduled = match.scheduled_time ? new Date(match.scheduled_time).toLocaleString('de-DE', { dateStyle: 'short', timeStyle: 'short'}) : '-';
|
||||
// TODO: Fetch and display actual scores/result string more accurately
|
||||
let resultPlaceholder = '-';
|
||||
if (match.status === 'finished') {
|
||||
resultPlaceholder = match.winner_name ? `${match.winner_name} gewinnt` : 'Beendet';
|
||||
// Ideally fetch sets: const sets = await fetchAPISpec(`/matches/${match.match_id}/sets`) ... display score string
|
||||
}
|
||||
|
||||
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>
|
||||
`;
|
||||
});
|
||||
allMatchesList.appendChild(table);
|
||||
}
|
||||
} catch (error) {
|
||||
showMessageSpec(allMatchesError, `Fehler beim Laden des Spielplans: ${error.message}`);
|
||||
allMatchesList.innerHTML = '<p><i>Fehler beim Laden des Spielplans.</i></p>';
|
||||
} finally {
|
||||
setLoadingSpec(loadingAllMatches, false);
|
||||
}
|
||||
};
|
||||
|
||||
// Load and display players (currently ALL players)
|
||||
const loadAndDisplayPlayers = async () => {
|
||||
// Only fetch all players once if not already cached
|
||||
if (allPlayersCache.length === 0) {
|
||||
setLoadingSpec(loadingPlayers, true);
|
||||
hideMessageSpec(playerListError);
|
||||
try {
|
||||
// Fetch ALL players - backend endpoint for tournament-specific players is needed
|
||||
console.warn("Fetching ALL players for spectator view. Backend should provide tournament-specific player list.");
|
||||
allPlayersCache = await fetchAPISpec('/players');
|
||||
allPlayersCache.sort((a,b) => (a.last_name || '').localeCompare(b.last_name || '')); // Sort by last name
|
||||
} catch (error) {
|
||||
showMessageSpec(playerListError, `Fehler beim Laden der Spielerliste: ${error.message}`);
|
||||
allPlayersCache = []; // Ensure cache is empty on error
|
||||
} finally {
|
||||
setLoadingSpec(loadingPlayers, false);
|
||||
}
|
||||
}
|
||||
// Filter and display based on the current cache and search term
|
||||
filterAndRenderPlayers();
|
||||
};
|
||||
|
||||
// Filter and render the player list based on search term
|
||||
const filterAndRenderPlayers = () => {
|
||||
const searchTerm = filterPlayerInput.value.trim().toLowerCase();
|
||||
const filteredPlayers = allPlayersCache.filter(p => {
|
||||
return !searchTerm ||
|
||||
(p.first_name && p.first_name.toLowerCase().includes(searchTerm)) ||
|
||||
(p.last_name && p.last_name.toLowerCase().includes(searchTerm)) ||
|
||||
(p.club && p.club.toLowerCase().includes(searchTerm));
|
||||
});
|
||||
|
||||
playerListDiv.innerHTML = ''; // Clear previous list
|
||||
if (filteredPlayers.length === 0) {
|
||||
playerListDiv.innerHTML = '<p><i>Keine Spieler entsprechen dem Suchbegriff.</i></p>';
|
||||
} else {
|
||||
const table = document.createElement('table');
|
||||
table.innerHTML = `
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nachname</th>
|
||||
<th>Vorname</th>
|
||||
<th>Verein</th>
|
||||
<th>QTTR</th>
|
||||
<th>Altersklasse</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>`;
|
||||
const tbody = table.querySelector('tbody');
|
||||
filteredPlayers.forEach(player => {
|
||||
const row = tbody.insertRow();
|
||||
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>
|
||||
`;
|
||||
});
|
||||
playerListDiv.appendChild(table);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Placeholder for loading bracket info
|
||||
const loadBracketInfo = async (tournamentId) => {
|
||||
setLoadingSpec(loadingBracket, true);
|
||||
bracketViz.innerHTML = '<p><i>Turnierbaum-Visualisierung wird geladen... (Funktion noch nicht implementiert)</i></p>';
|
||||
hideMessageSpec(bracketError);
|
||||
// TODO: Implement API call to fetch bracket data
|
||||
// Example: const bracketData = await fetchAPISpec(`/tournaments/${tournamentId}/bracket`);
|
||||
// TODO: Use a library (e.g., jquery-bracket, d3) or custom rendering to display the bracket
|
||||
await new Promise(resolve => setTimeout(resolve, 500)); // Simulate loading
|
||||
setLoadingSpec(loadingBracket, false);
|
||||
};
|
||||
|
||||
// Placeholder for loading notifications
|
||||
const loadNotifications = async (tournamentId) => {
|
||||
setLoadingSpec(loadingNotifications, true);
|
||||
notificationsArea.innerHTML = '<p><i>Hinweise werden geladen... (Funktion noch nicht implementiert)</i></p>';
|
||||
hideMessageSpec(notificationsError);
|
||||
// TODO: Implement API call to fetch notifications
|
||||
// Example: const notes = await fetchAPISpec(`/notifications?tournamentId=${tournamentId}&target=spectators`);
|
||||
// TODO: Display notifications
|
||||
await new Promise(resolve => setTimeout(resolve, 500)); // Simulate loading
|
||||
setLoadingSpec(loadingNotifications, false);
|
||||
};
|
||||
|
||||
|
||||
// --- Event Listeners ---
|
||||
selectTournament.addEventListener('change', (event) => {
|
||||
const selectedId = event.target.value;
|
||||
if (selectedId) {
|
||||
loadTournamentDetails(selectedId);
|
||||
} else {
|
||||
currentTournamentId = null;
|
||||
tournamentInfo.classList.add('hidden');
|
||||
tournamentDisplay.classList.add('hidden');
|
||||
hideMessageSpec(spectatorError);
|
||||
}
|
||||
});
|
||||
|
||||
// TODO: Add event listener for player filter input
|
||||
// Player filter input (debounced)
|
||||
let playerFilterTimeout;
|
||||
filterPlayerInput.addEventListener('input', () => {
|
||||
clearTimeout(playerFilterTimeout);
|
||||
playerFilterTimeout = setTimeout(() => {
|
||||
filterAndRenderPlayers(); // Filter based on cached players
|
||||
}, 300);
|
||||
});
|
||||
|
||||
// --- Initial Load ---
|
||||
loadAvailableTournaments();
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
loadAvailableTournaments();
|
||||
loadAndDisplayPlayers(); // Load global player list initially
|
||||
});
|
||||
|
||||
</script>
|
||||
</body>
|
||||
|
Reference in New Issue
Block a user