From d48cc4385f59c86bb056dd5174a98c54de0a2060 Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Fri, 14 Nov 2025 10:44:18 +0100 Subject: [PATCH] Add tournament update functionality and enhance UI for tournament management This commit introduces the ability to update tournament details, including name and date, in the backend and frontend. The new `updateTournament` method is added to the `tournamentController` and `tournamentService`, allowing for validation and error handling. The frontend `TournamentsView` is updated to include input fields for editing tournament details, with real-time updates reflected in the UI. Additionally, new CSS styles are introduced for improved layout and user interaction, enhancing the overall experience in tournament management. --- backend/controllers/tournamentController.js | 17 + backend/migrations/add_name_to_tournament.sql | 29 ++ backend/routes/tournamentRoutes.js | 2 + backend/services/tournamentService.js | 104 +++-- frontend/src/views/TournamentsView.vue | 381 ++++++++++++++---- 5 files changed, 414 insertions(+), 119 deletions(-) create mode 100644 backend/migrations/add_name_to_tournament.sql diff --git a/backend/controllers/tournamentController.js b/backend/controllers/tournamentController.js index dd5e36f..b216376 100644 --- a/backend/controllers/tournamentController.js +++ b/backend/controllers/tournamentController.js @@ -135,6 +135,23 @@ export const getTournament = async (req, res) => { } }; +// Update Turnier +export const updateTournament = async (req, res) => { + const { authcode: token } = req.headers; + const { clubId, tournamentId } = req.params; + const { name, date } = req.body; + try { + const tournament = await tournamentService.updateTournament(token, clubId, tournamentId, name, date); + // Emit Socket-Event + emitTournamentChanged(clubId, tournamentId); + res.status(200).json(tournament); + } catch (error) { + console.error('[updateTournament] Error:', error); + const status = error.message.includes('existiert bereits') ? 400 : 500; + res.status(status).json({ error: error.message }); + } +}; + // 10. Alle Spiele eines Turniers abfragen export const getTournamentMatches = async (req, res) => { const { authcode: token } = req.headers; diff --git a/backend/migrations/add_name_to_tournament.sql b/backend/migrations/add_name_to_tournament.sql new file mode 100644 index 0000000..9373bfe --- /dev/null +++ b/backend/migrations/add_name_to_tournament.sql @@ -0,0 +1,29 @@ +-- Migration: Add name column to tournament table +-- Date: 2025-01-13 +-- For MariaDB/MySQL + +-- Add name column if it doesn't exist +-- Check if column exists and add it if not +SET @dbname = DATABASE(); +SET @tablename = 'tournament'; +SET @columnname = 'name'; +SET @preparedStatement = (SELECT IF( + ( + SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS + WHERE + (TABLE_SCHEMA = @dbname) + AND (TABLE_NAME = @tablename) + AND (COLUMN_NAME = @columnname) + ) > 0, + 'SELECT 1', + CONCAT('ALTER TABLE `', @tablename, '` ADD COLUMN `', @columnname, '` VARCHAR(255) NOT NULL DEFAULT "" AFTER `id`') +)); +PREPARE alterIfNotExists FROM @preparedStatement; +EXECUTE alterIfNotExists; +DEALLOCATE PREPARE alterIfNotExists; + +-- Update existing tournaments: set name to formatted date if name is empty +UPDATE `tournament` +SET `name` = DATE_FORMAT(`date`, '%d.%m.%Y') +WHERE `name` = '' OR `name` IS NULL; + diff --git a/backend/routes/tournamentRoutes.js b/backend/routes/tournamentRoutes.js index 8e81dc3..4fe493f 100644 --- a/backend/routes/tournamentRoutes.js +++ b/backend/routes/tournamentRoutes.js @@ -2,6 +2,7 @@ import express from 'express'; import { getTournaments, addTournament, + updateTournament, addParticipant, getParticipants, setModus, @@ -39,6 +40,7 @@ router.delete('/match/result', authenticate, deleteMatchResult); router.post("/match/reopen", reopenMatch); router.post('/match/finish', authenticate, finishMatch); router.get('/matches/:clubId/:tournamentId', authenticate, getTournamentMatches); +router.put('/:clubId/:tournamentId', authenticate, updateTournament); router.get('/:clubId/:tournamentId', authenticate, getTournament); router.get('/:clubId', authenticate, getTournaments); router.post('/knockout', authenticate, startKnockout); diff --git a/backend/services/tournamentService.js b/backend/services/tournamentService.js index 6f95629..793fb0f 100644 --- a/backend/services/tournamentService.js +++ b/backend/services/tournamentService.js @@ -161,17 +161,12 @@ class TournamentService { throw new Error('Turnier nicht gefunden'); } - // 1) Hole vorhandene Gruppen - let groups = await TournamentGroup.findAll({ where: { tournamentId } }); - - // **Neu**: Falls noch keine Gruppen existieren, lege sie nach numberOfGroups an - if (!groups.length) { - const desired = tournament.numberOfGroups || 1; // Fallback auf 1, wenn undefiniert - for (let i = 0; i < desired; i++) { - await TournamentGroup.create({ tournamentId }); - } - groups = await TournamentGroup.findAll({ where: { tournamentId } }); - } + // 1) Stelle sicher, dass die richtige Anzahl von Gruppen existiert + await this.createGroups(userToken, clubId, tournamentId); + let groups = await TournamentGroup.findAll({ + where: { tournamentId }, + order: [['id', 'ASC']] // Stelle sicher, dass Gruppen sortiert sind + }); const members = await TournamentMember.findAll({ where: { tournamentId } }); if (!members.length) { @@ -181,32 +176,39 @@ class TournamentService { // 2) Alte Matches löschen await TournamentMatch.destroy({ where: { tournamentId } }); - // 3) Prüfe, ob Spieler bereits manuell zugeordnet wurden - const alreadyAssigned = members.filter(m => m.groupId !== null); - const unassigned = members.filter(m => m.groupId === null); - - if (alreadyAssigned.length > 0) { - // Spieler sind bereits manuell zugeordnet - nicht neu verteilen - devLog(`${alreadyAssigned.length} Spieler bereits zugeordnet, ${unassigned.length} noch nicht zugeordnet`); - } else { - // Keine manuellen Zuordnungen - zufällig verteilen - const shuffled = members.slice(); - for (let i = shuffled.length - 1; i > 0; i--) { - const j = Math.floor(Math.random() * (i + 1)); - [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]; - } - groups.forEach((g, idx) => { - shuffled - .filter((_, i) => i % groups.length === idx) - .forEach(m => m.update({ groupId: g.id })); - }); - } + // 3) Alle Zuordnungen löschen und zufällig neu verteilen + // (Bei "Zufällig verteilen" sollen alle alten Zuordnungen gelöscht werden) + await TournamentMember.update( + { groupId: null }, + { where: { tournamentId } } + ); - // 4) Round‑Robin anlegen wie gehabt - NUR innerhalb jeder Gruppe - for (const g of groups) { + // 4) Zufällig verteilen + const shuffled = members.slice(); + for (let i = shuffled.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]; + } + + // Warte auf alle Updates, damit die Zuordnungen korrekt sind + const updatePromises = []; + groups.forEach((g, idx) => { + const groupMembers = shuffled.filter((_, i) => i % groups.length === idx); + groupMembers.forEach(m => { + updatePromises.push(m.update({ groupId: g.id })); + }); + }); + await Promise.all(updatePromises); + + // 5) Round‑Robin anlegen wie gehabt - NUR innerhalb jeder Gruppe + // Stelle sicher, dass Gruppen sortiert sind + const sortedGroups = groups.sort((a, b) => a.id - b.id); + + for (const g of sortedGroups) { const gm = await TournamentMember.findAll({ where: { groupId: g.id } }); if (gm.length < 2) { + console.log(`⚠️ Gruppe ${g.id} hat weniger als 2 Spieler (${gm.length}), überspringe Spiele-Erstellung`); continue; } @@ -214,11 +216,10 @@ class TournamentService { for (let roundIndex = 0; roundIndex < rounds.length; roundIndex++) { for (const [p1Id, p2Id] of rounds[roundIndex]) { - // Prüfe, ob beide Spieler zur gleichen Gruppe gehören - const p1 = gm.find(p => p.id === p1Id); - const p2 = gm.find(p => p.id === p2Id); - if (p1 && p2 && p1.groupId === p2.groupId && p1.groupId === g.id) { - const match = await TournamentMatch.create({ + // p1Id und p2Id sind bereits aus gm, also müssen sie zur Gruppe g gehören + // Prüfe nur, ob beide IDs vorhanden sind (nicht null, falls Bye) + if (p1Id && p2Id) { + await TournamentMatch.create({ tournamentId, groupId: g.id, round: 'group', @@ -283,6 +284,29 @@ class TournamentService { return t; } + // Update Turnier (Name und Datum) + async updateTournament(userToken, clubId, tournamentId, name, date) { + await checkAccess(userToken, clubId); + const tournament = await Tournament.findOne({ where: { id: tournamentId, clubId } }); + if (!tournament) { + throw new Error('Turnier nicht gefunden'); + } + + // Prüfe auf Duplikat, wenn Datum geändert wird + if (date && date !== tournament.date) { + const existing = await Tournament.findOne({ where: { clubId, date, id: { [Op.ne]: tournamentId } } }); + if (existing) { + throw new Error('Ein Turnier mit diesem Datum existiert bereits'); + } + } + + if (name !== undefined) tournament.name = name; + if (date !== undefined) tournament.date = date; + + await tournament.save(); + return JSON.parse(JSON.stringify(tournament)); + } + // 11. Spiele eines Turniers async getTournamentMatches(userToken, clubId, tournamentId) { await checkAccess(userToken, clubId); @@ -296,8 +320,8 @@ class TournamentService { { model: TournamentResult, as: 'tournamentResults' } ], order: [ - ['group_id', 'ASC'], - ['group_round', 'ASC'], + ['group_round', 'ASC'], // Zuerst nach Runde sortieren + // group_id wird nicht sortiert, da die logische groupNumber im Frontend verwendet wird ['id', 'ASC'], [{ model: TournamentResult, as: 'tournamentResults' }, 'set', 'ASC'] ] diff --git a/frontend/src/views/TournamentsView.vue b/frontend/src/views/TournamentsView.vue index 254691a..5946382 100644 --- a/frontend/src/views/TournamentsView.vue +++ b/frontend/src/views/TournamentsView.vue @@ -6,19 +6,45 @@
- + +
+
+ + +
-