fixes
This commit is contained in:
@@ -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