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>
|
||||
|
13
server.js
13
server.js
@ -45,13 +45,14 @@ app.use(express.urlencoded({ extended: true })); // Parse URL-encoded request bo
|
||||
app.use(express.static(path.join(__dirname, 'public')));
|
||||
|
||||
// --- API Routes ---
|
||||
// Public Routes (or routes where auth is handled internally/conditionally)
|
||||
app.use('/api/auth', authRoutes);
|
||||
// Apply authentication middleware to protected routes
|
||||
app.use('/api/tournaments', authenticateToken, tournamentRoutes);
|
||||
app.use('/api/players', authenticateToken, playerRoutes);
|
||||
app.use('/api/matches', authenticateToken, matchRoutes);
|
||||
app.use('/api/users', authenticateToken, userRoutes);
|
||||
// Add other protected API routes here
|
||||
app.use('/api/tournaments', tournamentRoutes); // GET is public, POST/PUT/DELETE require auth (handled in router)
|
||||
app.use('/api/players', playerRoutes); // GET is public, POST/PUT/DELETE require auth (handled in router)
|
||||
app.use('/api/matches', matchRoutes); // GET is public, POST/PUT/DELETE require auth (handled in router)
|
||||
|
||||
// Protected Routes (require authentication for all methods)
|
||||
app.use('/api/users', authenticateToken, userRoutes); // All user management requires login
|
||||
|
||||
// --- Root Route (Optional Redirect or Info) ---
|
||||
app.get('/', (req, res) => {
|
||||
|
@ -1,7 +1,7 @@
|
||||
// src/controllers/matchController.js
|
||||
const db = require('../db/db');
|
||||
|
||||
// Helper function to get player names for matches
|
||||
// Helper function to get player names for matches (no changes needed here)
|
||||
const enrichMatchesWithPlayerNames = async (matches) => {
|
||||
if (!matches || matches.length === 0) {
|
||||
return [];
|
||||
@ -30,27 +30,44 @@ const enrichMatchesWithPlayerNames = async (matches) => {
|
||||
};
|
||||
|
||||
|
||||
// Get all matches, filtered by tournament_id (required) and optionally status
|
||||
// Get all matches, filtered by tournament_id (optional) and optionally status
|
||||
// If no tournamentId, fetches only 'scheduled' or 'ongoing' matches (for referee view)
|
||||
exports.getAllMatches = async (req, res, next) => {
|
||||
const { tournamentId, status } = req.query;
|
||||
const { tournamentId, status, refereeId } = req.query; // Added refereeId possibility
|
||||
|
||||
if (!tournamentId) {
|
||||
return res.status(400).json({ message: 'tournamentId query parameter ist erforderlich.' });
|
||||
let query = 'SELECT m.* FROM matches m'; // Base query
|
||||
const params = [];
|
||||
const conditions = [];
|
||||
let paramIndex = 1;
|
||||
|
||||
if (tournamentId) {
|
||||
conditions.push(`m.tournament_id = $${paramIndex++}`);
|
||||
params.push(tournamentId);
|
||||
} else {
|
||||
// If no tournament ID, default to fetching only active matches for referee view
|
||||
conditions.push(`(m.status = 'scheduled' OR m.status = 'ongoing')`);
|
||||
}
|
||||
|
||||
let query = `
|
||||
SELECT m.*
|
||||
FROM matches m
|
||||
WHERE m.tournament_id = $1`;
|
||||
const params = [tournamentId];
|
||||
let paramIndex = 2;
|
||||
|
||||
if (status) {
|
||||
query += ` AND m.status = $${paramIndex++}`;
|
||||
conditions.push(`m.status = $${paramIndex++}`);
|
||||
params.push(status);
|
||||
}
|
||||
|
||||
query += ' ORDER BY m.round, m.match_number_in_round, m.scheduled_time NULLS LAST, m.created_at';
|
||||
// TODO: Implement refereeId filtering if needed
|
||||
// if (refereeId === 'me' && req.user) { // Check if filtering by logged-in referee
|
||||
// conditions.push(`m.referee_id = $${paramIndex++}`);
|
||||
// params.push(req.user.userId);
|
||||
// } else if (refereeId) {
|
||||
// conditions.push(`m.referee_id = $${paramIndex++}`);
|
||||
// params.push(refereeId);
|
||||
// }
|
||||
|
||||
|
||||
if (conditions.length > 0) {
|
||||
query += ' WHERE ' + conditions.join(' AND ');
|
||||
}
|
||||
|
||||
query += ' ORDER BY m.round NULLS LAST, m.match_number_in_round NULLS LAST, m.scheduled_time NULLS LAST, m.created_at';
|
||||
|
||||
try {
|
||||
const result = await db.query(query, params);
|
||||
@ -64,6 +81,9 @@ exports.getAllMatches = async (req, res, next) => {
|
||||
// Get a single match by ID
|
||||
exports.getMatchById = async (req, res, next) => {
|
||||
const { id } = req.params;
|
||||
if (!id || id.toLowerCase() === 'export' || id.toLowerCase() === 'generate-bracket') { // Avoid conflict with potential routes
|
||||
return next();
|
||||
}
|
||||
try {
|
||||
// Fetch match and associated sets
|
||||
const matchResult = await db.query('SELECT * FROM matches WHERE match_id = $1', [id]);
|
||||
@ -85,7 +105,7 @@ exports.getMatchById = async (req, res, next) => {
|
||||
}
|
||||
};
|
||||
|
||||
// Create a new match manually (Admin only)
|
||||
// Create a new match manually (Admin only - protected in router)
|
||||
exports.createMatch = async (req, res, next) => {
|
||||
const {
|
||||
tournament_id,
|
||||
@ -132,7 +152,7 @@ exports.createMatch = async (req, res, next) => {
|
||||
}
|
||||
};
|
||||
|
||||
// Update match details (Admin only)
|
||||
// Update match details (Admin only - protected in router)
|
||||
exports.updateMatch = async (req, res, next) => {
|
||||
const { id } = req.params;
|
||||
const {
|
||||
@ -144,8 +164,7 @@ exports.updateMatch = async (req, res, next) => {
|
||||
status,
|
||||
table_number,
|
||||
referee_id,
|
||||
// winner_id should generally be set via score update, but allow manual override if needed
|
||||
winner_id
|
||||
winner_id // Allow manual override
|
||||
} = req.body;
|
||||
|
||||
const fieldsToUpdate = [];
|
||||
@ -153,36 +172,34 @@ exports.updateMatch = async (req, res, next) => {
|
||||
let queryIndex = 1;
|
||||
|
||||
const addUpdate = (field, value) => {
|
||||
// Allow explicitly setting fields to null
|
||||
if (value !== undefined) {
|
||||
fieldsToUpdate.push(`${field} = $${queryIndex++}`);
|
||||
values.push(value === '' ? null : value); // Treat empty string as null
|
||||
values.push(value === '' ? null : value);
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
// Prevent setting player1 and player2 to the same ID during update
|
||||
// Fetch current match to validate player IDs if they are being changed
|
||||
const currentMatchRes = await db.query('SELECT player1_id, player2_id FROM matches WHERE match_id = $1', [id]);
|
||||
if (currentMatchRes.rows.length === 0) {
|
||||
return res.status(404).json({ message: 'Spiel nicht gefunden.' });
|
||||
}
|
||||
const currentMatch = currentMatchRes.rows[0];
|
||||
const p1 = player1_id !== undefined ? player1_id : currentMatch.player1_id;
|
||||
const p2 = player2_id !== undefined ? player2_id : currentMatch.player2_id;
|
||||
const p1 = player1_id !== undefined ? (player1_id || null) : currentMatch.player1_id; // Handle setting to null
|
||||
const p2 = player2_id !== undefined ? (player2_id || null) : currentMatch.player2_id;
|
||||
if (p1 && p2 && p1 === p2) {
|
||||
return res.status(400).json({ message: 'Spieler 1 und Spieler 2 dürfen nicht identisch sein.' });
|
||||
}
|
||||
|
||||
|
||||
addUpdate('round', round);
|
||||
addUpdate('match_number_in_round', match_number_in_round);
|
||||
addUpdate('player1_id', player1_id);
|
||||
addUpdate('player2_id', player2_id);
|
||||
addUpdate('player1_id', player1_id); // Allow null
|
||||
addUpdate('player2_id', player2_id); // Allow null
|
||||
addUpdate('scheduled_time', scheduled_time);
|
||||
addUpdate('status', status);
|
||||
addUpdate('table_number', table_number);
|
||||
addUpdate('referee_id', referee_id);
|
||||
addUpdate('winner_id', winner_id);
|
||||
addUpdate('referee_id', referee_id); // Allow null
|
||||
addUpdate('winner_id', winner_id); // Allow null
|
||||
|
||||
|
||||
if (fieldsToUpdate.length === 0) {
|
||||
@ -196,7 +213,6 @@ exports.updateMatch = async (req, res, next) => {
|
||||
const result = await db.query(updateQuery, values);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
// Should have been caught earlier, but as a safeguard
|
||||
return res.status(404).json({ message: 'Spiel nicht gefunden.' });
|
||||
}
|
||||
const [enrichedMatch] = await enrichMatchesWithPlayerNames(result.rows);
|
||||
@ -211,7 +227,7 @@ exports.updateMatch = async (req, res, next) => {
|
||||
}
|
||||
};
|
||||
|
||||
// Delete a match (Admin only)
|
||||
// Delete a match (Admin only - protected in router)
|
||||
exports.deleteMatch = async (req, res, next) => {
|
||||
const { id } = req.params;
|
||||
try {
|
||||
@ -226,23 +242,26 @@ exports.deleteMatch = async (req, res, next) => {
|
||||
}
|
||||
};
|
||||
|
||||
// Enter/Update scores for a match (Admin or Referee)
|
||||
// Enter/Update scores for a match (Admin or Referee - protected in router)
|
||||
exports.updateMatchScore = async (req, res, next) => {
|
||||
const { id: matchId } = req.params;
|
||||
const { sets } = req.body; // Expecting an array: [{ set_number: 1, player1_score: 11, player2_score: 9 }, ...]
|
||||
|
||||
if (!Array.isArray(sets) || sets.length === 0) {
|
||||
if (!Array.isArray(sets)) { // Allow empty array to clear scores? No, require scores to update.
|
||||
return res.status(400).json({ message: 'Ein Array von Satzergebnissen ist erforderlich.' });
|
||||
}
|
||||
|
||||
// --- Validation ---
|
||||
for (const set of sets) {
|
||||
const setNum = parseInt(set.set_number);
|
||||
const p1Score = parseInt(set.player1_score);
|
||||
const p2Score = parseInt(set.player2_score);
|
||||
if (
|
||||
set.set_number == null || set.player1_score == null || set.player2_score == null ||
|
||||
isNaN(parseInt(set.set_number)) || isNaN(parseInt(set.player1_score)) || isNaN(parseInt(set.player2_score)) ||
|
||||
parseInt(set.set_number) <= 0 || parseInt(set.player1_score) < 0 || parseInt(set.player2_score) < 0
|
||||
isNaN(setNum) || isNaN(p1Score) || isNaN(p2Score) ||
|
||||
setNum <= 0 || p1Score < 0 || p2Score < 0
|
||||
) {
|
||||
return res.status(400).json({ message: 'Ungültige Satzdaten. Jeder Satz benötigt set_number (>0), player1_score (>=0), player2_score (>=0).' });
|
||||
return res.status(400).json({ message: `Ungültige Satzdaten: Satz ${set.set_number} (${set.player1_score}-${set.player2_score}). Jeder Satz benötigt set_number (>0), player1_score (>=0), player2_score (>=0).` });
|
||||
}
|
||||
}
|
||||
|
||||
@ -257,6 +276,7 @@ exports.updateMatchScore = async (req, res, next) => {
|
||||
FROM matches m
|
||||
JOIN tournaments t ON m.tournament_id = t.tournament_id
|
||||
WHERE m.match_id = $1
|
||||
FOR UPDATE -- Lock the row during transaction
|
||||
`, [matchId]);
|
||||
|
||||
if (matchRes.rows.length === 0) {
|
||||
@ -269,9 +289,15 @@ exports.updateMatchScore = async (req, res, next) => {
|
||||
await client.query('ROLLBACK');
|
||||
return res.status(400).json({ message: 'Spiel kann nicht bewertet werden, da nicht beide Spieler zugewiesen sind.' });
|
||||
}
|
||||
// Optional: Prevent updating scores if match is already 'finished'?
|
||||
// if (match.status === 'finished') {
|
||||
// await client.query('ROLLBACK');
|
||||
// return res.status(400).json({ message: 'Ergebnisse für bereits beendete Spiele können nicht geändert werden.' });
|
||||
// }
|
||||
|
||||
// 2. Insert or Update Set Scores
|
||||
// Use ON CONFLICT to handle cases where scores for a set are updated
|
||||
|
||||
// 2. Insert or Update Set Scores provided in the request
|
||||
// We only update the sets passed in the request body.
|
||||
for (const set of sets) {
|
||||
await client.query(
|
||||
`INSERT INTO sets (match_id, set_number, player1_score, player2_score)
|
||||
@ -283,32 +309,31 @@ exports.updateMatchScore = async (req, res, next) => {
|
||||
);
|
||||
}
|
||||
|
||||
// 3. Determine Winner (Simplified Logic - needs refinement based on actual rules)
|
||||
// Fetch all sets for this match again after potential updates
|
||||
// 3. Determine Winner based on ALL sets in the DB for this match after update
|
||||
const allSetsRes = await client.query('SELECT set_number, player1_score, player2_score FROM sets WHERE match_id = $1 ORDER BY set_number', [matchId]);
|
||||
const allSets = allSetsRes.rows;
|
||||
|
||||
let player1SetsWon = 0;
|
||||
let player2SetsWon = 0;
|
||||
const pointsToWinSet = match.game_type === '21_points' ? 21 : 11; // Determine points needed per set
|
||||
const pointsToWinSet = match.game_type === '21_points' ? 21 : 11;
|
||||
|
||||
for (const s of allSets) {
|
||||
// Basic win condition (must win by 2 unless score is high, e.g. 10-10 -> 12-10)
|
||||
// This simplified logic doesn't handle deuce perfectly.
|
||||
const p1Win = s.player1_score >= pointsToWinSet && s.player1_score >= s.player2_score + 2;
|
||||
const p2Win = s.player2_score >= pointsToWinSet && s.player2_score >= s.player1_score + 2;
|
||||
// Handle edge case like 11-10 vs 10-11 -> requires deuce logic
|
||||
// TODO: Refine set win logic for edge cases and deuce
|
||||
const p1 = s.player1_score;
|
||||
const p2 = s.player2_score;
|
||||
// Check win condition: must reach score limit AND win by 2 points
|
||||
const p1WinsSet = p1 >= pointsToWinSet && p1 >= p2 + 2;
|
||||
const p2WinsSet = p2 >= pointsToWinSet && p2 >= p1 + 2;
|
||||
|
||||
if (p1Win) {
|
||||
if (p1WinsSet) {
|
||||
player1SetsWon++;
|
||||
} else if (p2Win) {
|
||||
} else if (p2WinsSet) {
|
||||
player2SetsWon++;
|
||||
}
|
||||
// If neither condition met (e.g., 10-10, 11-10, 20-20, 21-20), the set is not counted as won yet.
|
||||
}
|
||||
|
||||
// Determine match winner based on sets won (e.g., first to 3 sets in best-of-5)
|
||||
// TODO: Make "sets needed to win match" configurable per tournament type (KO/Group) or globally
|
||||
// Determine match winner based on sets won
|
||||
// TODO: Make "sets needed to win match" configurable per tournament
|
||||
const setsNeededToWinMatch = 3; // Assuming best of 5 for now
|
||||
let winnerId = null;
|
||||
let finalStatus = 'ongoing'; // Default status unless match is finished
|
||||
@ -320,7 +345,8 @@ exports.updateMatchScore = async (req, res, next) => {
|
||||
winnerId = match.player2_id;
|
||||
finalStatus = 'finished';
|
||||
}
|
||||
// Optional: If all possible sets are played (e.g., 5 sets) and no one reached setsNeededToWinMatch (unlikely with correct logic), mark as finished?
|
||||
// Check if max sets played (e.g., 5 sets) - if so, match is finished even if score is weird?
|
||||
// Or rely only on reaching setsNeededToWin. Let's stick to setsNeededToWin.
|
||||
|
||||
// 4. Update Match Status and Winner
|
||||
await client.query(
|
||||
@ -352,23 +378,12 @@ exports.updateMatchScore = async (req, res, next) => {
|
||||
// --- Placeholder functions for other features ---
|
||||
|
||||
exports.exportMatchesPdf = async (req, res, next) => {
|
||||
// TODO: Implement PDF export logic
|
||||
// 1. Fetch match data (filtered by tournament, status, date range etc.)
|
||||
// 2. Use 'pdfkit' library
|
||||
// 3. Define PDF structure (header, table layout)
|
||||
// 4. Add match data to the PDF table
|
||||
// 5. Set response headers for PDF download
|
||||
// 6. Pipe PDF stream to response
|
||||
// TODO: Implement PDF export logic using pdfkit
|
||||
res.status(501).json({ message: 'Funktion PDF-Export noch nicht implementiert.' });
|
||||
};
|
||||
|
||||
exports.generateBracket = async (req, res, next) => {
|
||||
// TODO: Implement bracket generation logic (very complex)
|
||||
// Requires: Tournament ID, list of registered/seeded players
|
||||
// Logic depends heavily on tournament_type ('knockout' or 'group')
|
||||
// - Knockout: Seeding, pairing logic, generating match records for each round.
|
||||
// - Group: Group creation, round-robin match generation within groups.
|
||||
// Needs to create match records in the 'matches' table.
|
||||
// TODO: Implement bracket generation logic
|
||||
const { tournamentId } = req.body;
|
||||
if (!tournamentId) return res.status(400).json({ message: "tournamentId ist erforderlich." });
|
||||
res.status(501).json({ message: 'Funktion Turnierbaum-Generierung noch nicht implementiert.' });
|
||||
|
@ -1,15 +1,19 @@
|
||||
// src/controllers/playerController.js
|
||||
const db = require('../db/db');
|
||||
|
||||
// Get all players - potentially with filtering/sorting later
|
||||
// Get all players - now includes search functionality
|
||||
exports.getAllPlayers = async (req, res, next) => {
|
||||
// Basic filtering example (add more as needed)
|
||||
const { search } = req.query;
|
||||
let query = 'SELECT player_id, first_name, last_name, club, qttr_points, age_class FROM players';
|
||||
const params = [];
|
||||
|
||||
if (search) {
|
||||
query += ' WHERE first_name ILIKE $1 OR last_name ILIKE $1 OR club ILIKE $1';
|
||||
// Search across multiple fields using ILIKE for case-insensitivity
|
||||
// Concatenate names for searching full name
|
||||
query += ` WHERE (first_name || ' ' || last_name) ILIKE $1
|
||||
OR (last_name || ' ' || first_name) ILIKE $1
|
||||
OR club ILIKE $1
|
||||
OR age_class ILIKE $1`;
|
||||
params.push(`%${search}%`);
|
||||
}
|
||||
|
||||
@ -26,6 +30,9 @@ exports.getAllPlayers = async (req, res, next) => {
|
||||
// Get a single player by ID
|
||||
exports.getPlayerById = async (req, res, next) => {
|
||||
const { id } = req.params;
|
||||
if (!id || id.toLowerCase() === 'export' || id.toLowerCase() === 'import') { // Avoid conflict with potential routes
|
||||
return next();
|
||||
}
|
||||
try {
|
||||
const result = await db.query('SELECT player_id, first_name, last_name, club, qttr_points, age_class, created_at, updated_at FROM players WHERE player_id = $1', [id]);
|
||||
if (result.rows.length === 0) {
|
||||
@ -37,7 +44,7 @@ exports.getPlayerById = async (req, res, next) => {
|
||||
}
|
||||
};
|
||||
|
||||
// Create a new player (Admin only)
|
||||
// Create a new player (Admin only - protected in router)
|
||||
exports.createPlayer = async (req, res, next) => {
|
||||
const { first_name, last_name, club, qttr_points, age_class } = req.body;
|
||||
|
||||
@ -48,8 +55,8 @@ exports.createPlayer = async (req, res, next) => {
|
||||
|
||||
// Ensure qttr_points is a number or null
|
||||
const qtt_numeric = qttr_points ? parseInt(qttr_points, 10) : null;
|
||||
if (qttr_points && isNaN(qtt_numeric)) {
|
||||
return res.status(400).json({ message: 'QTTR-Punkte müssen eine Zahl sein.' });
|
||||
if (qttr_points && (isNaN(qtt_numeric) || qtt_numeric < 0)) { // Allow 0 points, but not negative
|
||||
return res.status(400).json({ message: 'QTTR-Punkte müssen eine positive Zahl oder 0 sein.' });
|
||||
}
|
||||
|
||||
|
||||
@ -66,7 +73,7 @@ exports.createPlayer = async (req, res, next) => {
|
||||
}
|
||||
};
|
||||
|
||||
// Update an existing player (Admin only)
|
||||
// Update an existing player (Admin only - protected in router)
|
||||
exports.updatePlayer = async (req, res, next) => {
|
||||
const { id } = req.params;
|
||||
const { first_name, last_name, club, qttr_points, age_class } = req.body;
|
||||
@ -82,12 +89,12 @@ exports.updatePlayer = async (req, res, next) => {
|
||||
let processedValue = value;
|
||||
if (field === 'qttr_points') {
|
||||
processedValue = value ? parseInt(value, 10) : null;
|
||||
if (value && isNaN(processedValue)) {
|
||||
throw new Error('QTTR-Punkte müssen eine Zahl sein.');
|
||||
if (value && (isNaN(processedValue) || processedValue < 0)) {
|
||||
throw new Error('QTTR-Punkte müssen eine positive Zahl oder 0 sein.');
|
||||
}
|
||||
} else {
|
||||
// Treat empty strings as null for nullable text fields
|
||||
processedValue = value === '' ? null : value;
|
||||
// Treat empty strings as null for nullable text fields like club, age_class
|
||||
processedValue = (value === '' && (field === 'club' || field === 'age_class')) ? null : value;
|
||||
}
|
||||
fieldsToUpdate.push(`${field} = $${queryIndex++}`);
|
||||
values.push(processedValue);
|
||||
@ -129,13 +136,19 @@ exports.updatePlayer = async (req, res, next) => {
|
||||
}
|
||||
};
|
||||
|
||||
// Delete a player (Admin only)
|
||||
// Delete a player (Admin only - protected in router)
|
||||
exports.deletePlayer = async (req, res, next) => {
|
||||
const { id } = req.params;
|
||||
try {
|
||||
// The database schema uses ON DELETE CASCADE/SET NULL for relationships,
|
||||
// so deleting a player might automatically remove them from tournaments
|
||||
// or set player references in matches to NULL. Review init.sql for details.
|
||||
// Check if player is linked in tournament_players (more restrictive than just matches)
|
||||
const checkResult = await db.query('SELECT 1 FROM tournament_players WHERE player_id = $1 LIMIT 1', [id]);
|
||||
if (checkResult.rows.length > 0) {
|
||||
return res.status(409).json({ message: 'Spieler kann nicht gelöscht werden, da er einem oder mehreren Turnieren zugeordnet ist.' });
|
||||
}
|
||||
// Check if player is linked in matches (as player1, player2, or winner) where status is not 'finished' or 'cancelled'?
|
||||
// This might be too complex or restrictive. Relying on tournament_players check might be sufficient.
|
||||
// ON DELETE SET NULL in matches table handles completed matches okay.
|
||||
|
||||
const result = await db.query('DELETE FROM players WHERE player_id = $1 RETURNING player_id', [id]);
|
||||
if (result.rowCount === 0) {
|
||||
return res.status(404).json({ message: 'Spieler nicht gefunden.' });
|
||||
@ -145,7 +158,7 @@ exports.deletePlayer = async (req, res, next) => {
|
||||
// Handle potential foreign key issues if not handled by CASCADE/SET NULL
|
||||
if (error.code === '23503') {
|
||||
console.warn(`Attempted to delete player ${id} which might still be referenced elsewhere unexpectedly.`);
|
||||
return res.status(409).json({ message: 'Spieler konnte nicht gelöscht werden, da er noch in Verwendung ist (z.B. in einem aktiven Spiel). Überprüfen Sie die Datenbank-Constraints.' });
|
||||
return res.status(409).json({ message: 'Spieler konnte nicht gelöscht werden, da er noch in Verwendung ist. Überprüfen Sie die Datenbank-Constraints.' });
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
@ -156,45 +169,22 @@ exports.deletePlayer = async (req, res, next) => {
|
||||
|
||||
exports.importPlayers = async (req, res, next) => {
|
||||
// TODO: Implement CSV import logic
|
||||
// 1. Use 'multer' middleware in the route to handle file upload (e.g., req.file)
|
||||
// 2. Read the file stream (req.file.buffer or req.file.path)
|
||||
// 3. Use 'csv-parser' to parse the CSV data row by row
|
||||
// 4. Validate each row's data
|
||||
// 5. Insert valid player data into the database (consider bulk insert for performance)
|
||||
// 6. Report success or errors (e.g., which rows failed)
|
||||
console.log("Received file for import (implement logic):", req.file); // Example if using multer
|
||||
res.status(501).json({ message: 'Funktion Spieler-Import noch nicht implementiert.' });
|
||||
};
|
||||
|
||||
exports.exportPlayers = async (req, res, next) => {
|
||||
// TODO: Implement CSV export logic
|
||||
// 1. Fetch player data from the database (potentially filtered)
|
||||
// 2. Use 'fast-csv' or similar to format data as CSV
|
||||
// 3. Set response headers for CSV download:
|
||||
// res.setHeader('Content-Type', 'text/csv');
|
||||
// res.setHeader('Content-Disposition', 'attachment; filename="spieler_export.csv"');
|
||||
// 4. Pipe the CSV stream to the response object (res)
|
||||
// TODO: Implement CSV export logic using fast-csv
|
||||
try {
|
||||
const players = await db.query('SELECT first_name, last_name, club, qttr_points, age_class FROM players ORDER BY last_name');
|
||||
// Placeholder - just send JSON for now until CSV lib is added
|
||||
if (players.rows.length === 0) {
|
||||
return res.status(404).json({ message: "Keine Spieler zum Exportieren gefunden." });
|
||||
}
|
||||
// Actual implementation would use fast-csv here
|
||||
res.setHeader('Content-Type', 'application/json'); // Change to text/csv for actual export
|
||||
res.setHeader('Content-Disposition', 'attachment; filename="spieler_export.json"'); // Change filename
|
||||
res.json(players.rows); // Send JSON as placeholder
|
||||
|
||||
// Example with fast-csv (requires installation and import):
|
||||
// const csvStream = format({ headers: true });
|
||||
// res.setHeader('Content-Type', 'text/csv');
|
||||
// res.setHeader('Content-Disposition', 'attachment; filename="spieler_export.csv"');
|
||||
// csvStream.pipe(res);
|
||||
// players.rows.forEach(player => csvStream.write(player));
|
||||
// csvStream.end();
|
||||
|
||||
// Placeholder: Send JSON
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.setHeader('Content-Disposition', 'attachment; filename="spieler_export.json"');
|
||||
res.json(players.rows);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
// res.status(501).json({ message: 'Funktion Spieler-Export noch nicht implementiert.' }); // Remove this line when implemented
|
||||
};
|
||||
|
@ -5,7 +5,7 @@ const db = require('../db/db');
|
||||
exports.getAllTournaments = async (req, res, next) => {
|
||||
try {
|
||||
// Add filtering or pagination later if needed
|
||||
const result = await db.query('SELECT * FROM tournaments ORDER BY date DESC, created_at DESC');
|
||||
const result = await db.query('SELECT * FROM tournaments ORDER BY date DESC NULLS LAST, created_at DESC'); // Handle null dates
|
||||
res.json(result.rows);
|
||||
} catch (error) {
|
||||
next(error); // Pass error to the error handler
|
||||
@ -15,6 +15,9 @@ exports.getAllTournaments = async (req, res, next) => {
|
||||
// Get a single tournament by ID
|
||||
exports.getTournamentById = async (req, res, next) => {
|
||||
const { id } = req.params;
|
||||
if (!id || id.toLowerCase() === 'export') { // Avoid conflict with potential export route if not handled carefully
|
||||
return next(); // Skip if ID is 'export' or similar keyword
|
||||
}
|
||||
try {
|
||||
const result = await db.query('SELECT * FROM tournaments WHERE tournament_id = $1', [id]);
|
||||
if (result.rows.length === 0) {
|
||||
@ -26,7 +29,7 @@ exports.getTournamentById = async (req, res, next) => {
|
||||
}
|
||||
};
|
||||
|
||||
// Create a new tournament (Admin only)
|
||||
// Create a new tournament (Admin only - protected in router)
|
||||
exports.createTournament = async (req, res, next) => {
|
||||
// Destructure expected fields from request body
|
||||
const {
|
||||
@ -49,22 +52,29 @@ exports.createTournament = async (req, res, next) => {
|
||||
// Validate enum types
|
||||
const validTournamentTypes = ['knockout', 'group'];
|
||||
const validGameTypes = ['11_points', '21_points'];
|
||||
const validStatuses = ['planned', 'running', 'finished', 'cancelled']; // Add valid statuses
|
||||
if (!validTournamentTypes.includes(tournament_type)) {
|
||||
return res.status(400).json({ message: `Ungültiger Turnier-Typ: ${tournament_type}` });
|
||||
}
|
||||
if (!validGameTypes.includes(game_type)) {
|
||||
return res.status(400).json({ message: `Ungültiger Spiel-Typ: ${game_type}` });
|
||||
}
|
||||
const finalStatus = status && validStatuses.includes(status) ? status : 'planned'; // Default to 'planned' if invalid or missing
|
||||
const finalMaxPlayers = max_players ? parseInt(max_players, 10) : null;
|
||||
if (max_players && (isNaN(finalMaxPlayers) || finalMaxPlayers < 0)) {
|
||||
return res.status(400).json({ message: 'Max. Spieler muss eine positive Zahl sein.' });
|
||||
}
|
||||
|
||||
|
||||
// Get user ID from the authenticated request (set by authMiddleware)
|
||||
const created_by = req.user.userId;
|
||||
const created_by = req.user.userId; // req.user is available because route is protected
|
||||
|
||||
try {
|
||||
const result = await db.query(
|
||||
`INSERT INTO tournaments (name, date, location, description, logo_url, tournament_type, game_type, max_players, status, created_by)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
||||
RETURNING *`, // Return the newly created tournament
|
||||
[name, date || null, location, description, logo_url, tournament_type, game_type, max_players || null, status || 'planned', created_by]
|
||||
[name, date || null, location, description, logo_url, tournament_type, game_type, finalMaxPlayers, finalStatus, created_by]
|
||||
);
|
||||
res.status(201).json(result.rows[0]);
|
||||
} catch (error) {
|
||||
@ -72,7 +82,7 @@ exports.createTournament = async (req, res, next) => {
|
||||
}
|
||||
};
|
||||
|
||||
// Update an existing tournament (Admin only)
|
||||
// Update an existing tournament (Admin only - protected in router)
|
||||
exports.updateTournament = async (req, res, next) => {
|
||||
const { id } = req.params;
|
||||
// Destructure fields that can be updated
|
||||
@ -96,16 +106,27 @@ exports.updateTournament = async (req, res, next) => {
|
||||
// Helper function to add field and value if value is provided
|
||||
const addUpdate = (field, value) => {
|
||||
if (value !== undefined) {
|
||||
// Validate enums if they are being updated
|
||||
let processedValue = value;
|
||||
// Validate enums/types if they are being updated
|
||||
if (field === 'tournament_type' && !['knockout', 'group'].includes(value)) {
|
||||
throw new Error(`Ungültiger Turnier-Typ: ${value}`);
|
||||
}
|
||||
if (field === 'game_type' && !['11_points', '21_points'].includes(value)) {
|
||||
throw new Error(`Ungültiger Spiel-Typ: ${value}`);
|
||||
}
|
||||
if (field === 'status' && !['planned', 'running', 'finished', 'cancelled'].includes(value)) {
|
||||
throw new Error(`Ungültiger Status: ${value}`);
|
||||
}
|
||||
if (field === 'max_players') {
|
||||
processedValue = value ? parseInt(value, 10) : null;
|
||||
if (value && (isNaN(processedValue) || processedValue < 0)) {
|
||||
throw new Error('Max. Spieler muss eine positive Zahl sein.');
|
||||
}
|
||||
}
|
||||
|
||||
fieldsToUpdate.push(`${field} = $${queryIndex++}`);
|
||||
// Handle empty strings potentially being passed for nullable fields
|
||||
values.push(value === '' ? null : value);
|
||||
values.push(processedValue === '' ? null : processedValue);
|
||||
}
|
||||
};
|
||||
|
||||
@ -138,14 +159,14 @@ exports.updateTournament = async (req, res, next) => {
|
||||
|
||||
} catch (error) {
|
||||
// Handle potential validation errors from the helper
|
||||
if (error.message.startsWith('Ungültiger')) {
|
||||
if (error.message.startsWith('Ungültiger') || error.message.includes('Spieler')) {
|
||||
return res.status(400).json({ message: error.message });
|
||||
}
|
||||
next(error); // Pass other errors to the handler
|
||||
}
|
||||
};
|
||||
|
||||
// Delete a tournament (Admin only)
|
||||
// Delete a tournament (Admin only - protected in router)
|
||||
exports.deleteTournament = async (req, res, next) => {
|
||||
const { id } = req.params;
|
||||
try {
|
||||
@ -160,6 +181,25 @@ exports.deleteTournament = async (req, res, next) => {
|
||||
}
|
||||
};
|
||||
|
||||
// NEW: Get players participating in a specific tournament
|
||||
exports.getTournamentPlayers = async (req, res, next) => {
|
||||
const { id } = req.params; // Tournament ID
|
||||
try {
|
||||
const result = await db.query(
|
||||
`SELECT p.player_id, p.first_name, p.last_name, p.club, p.qttr_points, p.age_class, tp.seed, tp.group_name, tp.status AS participation_status
|
||||
FROM players p
|
||||
JOIN tournament_players tp ON p.player_id = tp.player_id
|
||||
WHERE tp.tournament_id = $1
|
||||
ORDER BY p.last_name, p.first_name`,
|
||||
[id]
|
||||
);
|
||||
res.json(result.rows);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// --- Placeholder functions for other features ---
|
||||
|
||||
exports.importTournaments = async (req, res, next) => {
|
||||
|
@ -1,41 +1,42 @@
|
||||
// src/routes/matchRoutes.js
|
||||
const express = require('express');
|
||||
const { authorizeRole, authorizeRoles } = require('../middleware/authMiddleware');
|
||||
const matchController = require('../controllers/matchController'); // Import actual controller
|
||||
const { authenticateToken, authorizeRole, authorizeRoles } = require('../middleware/authMiddleware');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// --- Placeholder Controller Functions (replace with actual controller) ---
|
||||
const placeholderController = (feature) => (req, res) => {
|
||||
res.status(501).json({ message: `Funktion '${feature}' noch nicht implementiert.` });
|
||||
};
|
||||
// --- Publicly Accessible GET Routes ---
|
||||
|
||||
// --- Routes ---
|
||||
|
||||
// GET /api/matches - Get all matches (e.g., for a specific tournament via query param)
|
||||
// Accessible by all logged-in users
|
||||
router.get('/', placeholderController('getAllMatches')); // Add query param filtering later
|
||||
// GET /api/matches - Get all matches (filtered by tournamentId or gets active ones)
|
||||
router.get('/', matchController.getAllMatches);
|
||||
|
||||
// GET /api/matches/:id - Get details of a specific match
|
||||
// Accessible by all logged-in users
|
||||
router.get('/:id', placeholderController('getMatchById'));
|
||||
router.get('/:id', matchController.getMatchById);
|
||||
|
||||
|
||||
// --- Admin Only Routes ---
|
||||
|
||||
// POST /api/matches - Manually add a match (Admin only)
|
||||
router.post('/', authorizeRole('admin'), placeholderController('createMatch'));
|
||||
router.post('/', authenticateToken, authorizeRole('admin'), matchController.createMatch);
|
||||
|
||||
// PUT /api/matches/:id - Update match details (e.g., schedule, table) (Admin only)
|
||||
router.put('/:id', authorizeRole('admin'), placeholderController('updateMatch'));
|
||||
router.put('/:id', authenticateToken, authorizeRole('admin'), matchController.updateMatch);
|
||||
|
||||
// DELETE /api/matches/:id - Delete a match (Admin only)
|
||||
router.delete('/:id', authorizeRole('admin'), placeholderController('deleteMatch'));
|
||||
|
||||
// POST /api/matches/:id/score - Enter/Update scores for a match (Admin or Referee)
|
||||
router.post('/:id/score', authorizeRoles('admin', 'referee'), placeholderController('updateMatchScore'));
|
||||
router.delete('/:id', authenticateToken, authorizeRole('admin'), matchController.deleteMatch);
|
||||
|
||||
// GET /api/matches/export/pdf - Export matches (current, future, past) as PDF (Admin only)
|
||||
router.get('/export/pdf', authorizeRole('admin'), placeholderController('exportMatchesPdf'));
|
||||
// Note: Might conflict with GET /:id if not handled carefully
|
||||
router.get('/export/pdf', authenticateToken, authorizeRole('admin'), matchController.exportMatchesPdf);
|
||||
|
||||
// POST /api/matches/generate-bracket - Generate tournament bracket (Admin only)
|
||||
// Requires tournament ID and potentially other parameters in the body
|
||||
router.post('/generate-bracket', authorizeRole('admin'), placeholderController('generateBracket'));
|
||||
router.post('/generate-bracket', authenticateToken, authorizeRole('admin'), matchController.generateBracket);
|
||||
|
||||
|
||||
// --- Admin or Referee Routes ---
|
||||
|
||||
// POST /api/matches/:id/score - Enter/Update scores for a match (Admin or Referee)
|
||||
router.post('/:id/score', authenticateToken, authorizeRoles('admin', 'referee'), matchController.updateMatchScore);
|
||||
|
||||
|
||||
module.exports = router;
|
||||
|
@ -1,36 +1,37 @@
|
||||
// src/routes/playerRoutes.js
|
||||
const express = require('express');
|
||||
const { authorizeRole, authorizeRoles } = require('../middleware/authMiddleware');
|
||||
const playerController = require('../controllers/playerController'); // Import the actual controller
|
||||
const { authenticateToken, authorizeRole, authorizeRoles } = require('../middleware/authMiddleware');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// --- Placeholder Controller Functions (replace with actual controller) ---
|
||||
const placeholderController = (feature) => (req, res) => {
|
||||
res.status(501).json({ message: `Funktion '${feature}' noch nicht implementiert.` });
|
||||
};
|
||||
// --- Publicly Accessible GET Routes ---
|
||||
|
||||
// --- Routes ---
|
||||
// GET /api/players - Get all players (with optional search)
|
||||
router.get('/', playerController.getAllPlayers);
|
||||
|
||||
// GET /api/players - Get all players (accessible by admin, maybe referee?)
|
||||
router.get('/', authorizeRoles('admin', 'referee'), placeholderController('getAllPlayers'));
|
||||
// GET /api/players/:id - Get a specific player
|
||||
router.get('/:id', playerController.getPlayerById);
|
||||
|
||||
// GET /api/players/:id - Get a specific player (accessible by admin, maybe referee?)
|
||||
router.get('/:id', authorizeRoles('admin', 'referee'), placeholderController('getPlayerById'));
|
||||
|
||||
// --- Admin only routes ---
|
||||
|
||||
// POST /api/players - Add a new player (Admin only)
|
||||
router.post('/', authorizeRole('admin'), placeholderController('createPlayer'));
|
||||
router.post('/', authenticateToken, authorizeRole('admin'), playerController.createPlayer);
|
||||
|
||||
// PUT /api/players/:id - Update a player (Admin only)
|
||||
router.put('/:id', authorizeRole('admin'), placeholderController('updatePlayer'));
|
||||
router.put('/:id', authenticateToken, authorizeRole('admin'), playerController.updatePlayer);
|
||||
|
||||
// DELETE /api/players/:id - Remove a player (Admin only)
|
||||
router.delete('/:id', authorizeRole('admin'), placeholderController('deletePlayer'));
|
||||
router.delete('/:id', authenticateToken, authorizeRole('admin'), playerController.deletePlayer);
|
||||
|
||||
// POST /api/players/import - Import players from CSV (Admin only)
|
||||
router.post('/import', authorizeRole('admin'), placeholderController('importPlayers'));
|
||||
// TODO: Add multer middleware here for file upload when implemented
|
||||
router.post('/import', authenticateToken, authorizeRole('admin'), playerController.importPlayers);
|
||||
|
||||
// GET /api/players/export - Export players to CSV (Admin only)
|
||||
router.get('/export', authorizeRole('admin'), placeholderController('exportPlayers'));
|
||||
// Note: Might conflict with GET /:id if not handled carefully in controller or route order
|
||||
router.get('/export', authenticateToken, authorizeRole('admin'), playerController.exportPlayers);
|
||||
|
||||
|
||||
module.exports = router;
|
||||
|
@ -1,38 +1,43 @@
|
||||
// src/routes/tournamentRoutes.js
|
||||
const express = require('express');
|
||||
const tournamentController = require('../controllers/tournamentController');
|
||||
const { authorizeRole } = require('../middleware/authMiddleware'); // Import role authorization
|
||||
// Import authentication middleware
|
||||
const { authenticateToken, authorizeRole } = require('../middleware/authMiddleware');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// --- Public routes (accessible by anyone logged in) ---
|
||||
// --- Publicly Accessible GET Routes ---
|
||||
// GET /api/tournaments - Get all tournaments
|
||||
router.get('/', tournamentController.getAllTournaments);
|
||||
|
||||
// GET /api/tournaments/:id - Get a specific tournament
|
||||
router.get('/:id', tournamentController.getTournamentById);
|
||||
|
||||
// GET /api/tournaments/:id/players - Get players for a specific tournament (NEW)
|
||||
router.get('/:id/players', tournamentController.getTournamentPlayers);
|
||||
|
||||
// --- Admin only routes ---
|
||||
|
||||
// --- Admin only routes (require authentication and 'admin' role) ---
|
||||
// POST /api/tournaments - Create a new tournament
|
||||
router.post('/', authorizeRole('admin'), tournamentController.createTournament);
|
||||
router.post('/', authenticateToken, authorizeRole('admin'), tournamentController.createTournament);
|
||||
|
||||
// PUT /api/tournaments/:id - Update a tournament
|
||||
router.put('/:id', authorizeRole('admin'), tournamentController.updateTournament);
|
||||
router.put('/:id', authenticateToken, authorizeRole('admin'), tournamentController.updateTournament);
|
||||
|
||||
// DELETE /api/tournaments/:id - Delete a tournament
|
||||
router.delete('/:id', authorizeRole('admin'), tournamentController.deleteTournament);
|
||||
router.delete('/:id', authenticateToken, authorizeRole('admin'), tournamentController.deleteTournament);
|
||||
|
||||
// --- Placeholder routes for future implementation (Admin only) ---
|
||||
|
||||
// POST /api/tournaments/import - Import tournaments from CSV
|
||||
router.post('/import', authorizeRole('admin'), tournamentController.importTournaments);
|
||||
router.post('/import', authenticateToken, authorizeRole('admin'), tournamentController.importTournaments);
|
||||
|
||||
// GET /api/tournaments/export - Export tournaments to CSV
|
||||
router.get('/export', authorizeRole('admin'), tournamentController.exportTournaments);
|
||||
// Note: Might conflict with GET /:id if not handled carefully in controller or route order
|
||||
router.get('/export', authenticateToken, authorizeRole('admin'), tournamentController.exportTournaments);
|
||||
|
||||
// POST /api/tournaments/:id/logo - Add/Update logo for a tournament
|
||||
router.post('/:id/logo', authorizeRole('admin'), tournamentController.addLogo);
|
||||
router.post('/:id/logo', authenticateToken, authorizeRole('admin'), tournamentController.addLogo);
|
||||
|
||||
|
||||
module.exports = router;
|
||||
|
Reference in New Issue
Block a user