Lade Turniere...
-
-
-
+
Lade Turniere...
+
-
- Live Spiele / Aktuelle Runde
+
+ ● Live Spiele / Aktuelle Runde
-
Live-Spiele werden hier angezeigt (Implementierung erforderlich).
+
Keine Live-Spiele gefunden oder Turnier nicht ausgewählt.
- Lade Live-Spiele...
+ Lade Live-Spiele...
+
-
+
Turnierbaum / Gruppenphase
-
Visualisierung des Turnierbaums oder der Gruppenphase wird hier angezeigt (Implementierung erforderlich).
+
Visualisierung des Turnierbaums oder der Gruppenphase ist für dieses Turnier nicht verfügbar oder noch nicht implementiert.
- Lade Turnierbaum/Gruppen...
+ Lade Turnierbaum/Gruppen...
+
-
+
Alle Spiele (Zeitplan)
-
Liste aller vergangenen und zukünftigen Spiele wird hier angezeigt (Implementierung erforderlich).
+
Keine Spiele gefunden oder Turnier nicht ausgewählt.
- Lade alle Spiele...
+ Lade alle Spiele...
+
-
- Spieler & Statistiken
+
+ Spieler
-
-
+
+
+ Hinweis: Zeigt aktuell alle Spieler im System, nicht nur die des ausgewählten Turniers.
-
-
Spielerliste und Profile werden hier angezeigt (Implementierung erforderlich).
+
+
Keine Spieler gefunden.
-
Lade Spieler...
+
Lade Spieler...
+
-
+
Hinweise & Benachrichtigungen
-
Wichtige Hinweise oder Push-Benachrichtigungen werden hier angezeigt (Implementierung erforderlich).
+
Keine aktuellen Hinweise für dieses Turnier vorhanden oder Funktion noch nicht implementiert.
+ Lade Hinweise...
+
-
-
-
diff --git a/server.js b/server.js
index 33ad493..3a0f17c 100644
--- a/server.js
+++ b/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) => {
diff --git a/src/controllers/matchController.js b/src/controllers/matchController.js
index c5be4da..b77e064 100644
--- a/src/controllers/matchController.js
+++ b/src/controllers/matchController.js
@@ -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.' });
diff --git a/src/controllers/playerController.js b/src/controllers/playerController.js
index c5a6f8f..b236975 100644
--- a/src/controllers/playerController.js
+++ b/src/controllers/playerController.js
@@ -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
};
diff --git a/src/controllers/tournamentController.js b/src/controllers/tournamentController.js
index 9aba60d..bca4fda 100644
--- a/src/controllers/tournamentController.js
+++ b/src/controllers/tournamentController.js
@@ -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) => {
diff --git a/src/routes/matchRoutes.js b/src/routes/matchRoutes.js
index 4b245c2..7c7ba2f 100644
--- a/src/routes/matchRoutes.js
+++ b/src/routes/matchRoutes.js
@@ -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;
diff --git a/src/routes/playerRoutes.js b/src/routes/playerRoutes.js
index 7afd86c..5e960cc 100644
--- a/src/routes/playerRoutes.js
+++ b/src/routes/playerRoutes.js
@@ -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;
diff --git a/src/routes/tournamentRoutes.js b/src/routes/tournamentRoutes.js
index 7d94a9a..db8ccda 100644
--- a/src/routes/tournamentRoutes.js
+++ b/src/routes/tournamentRoutes.js
@@ -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;