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.
This commit is contained in:
Torsten Schulz (local)
2025-11-14 10:44:18 +01:00
parent 9b8dcd8561
commit d48cc4385f
5 changed files with 414 additions and 119 deletions

View File

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

View File

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

View File

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

View File

@@ -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) RoundRobin 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) RoundRobin 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']
]