This commit is contained in:
MLH
2025-04-07 22:37:14 +02:00
parent 8bddc22b62
commit 57f8981446
9 changed files with 879 additions and 356 deletions

View File

@ -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>