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

View File

@@ -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.' });

View File

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

View File

@@ -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) => {

View File

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

View File

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

View File

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