diff --git a/backend/controllers/tournamentController.js b/backend/controllers/tournamentController.js index d0c879b4..c3887268 100644 --- a/backend/controllers/tournamentController.js +++ b/backend/controllers/tournamentController.js @@ -62,9 +62,9 @@ export const getTournaments = async (req, res) => { // 2. Neues Turnier anlegen export const addTournament = async (req, res) => { const { authcode: token } = req.headers; - const { clubId, tournamentName, date, winningSets, allowsExternal } = req.body; + const { clubId, tournamentName, date, winningSets, allowsExternal, isDoublesTournament } = req.body; try { - const tournament = await tournamentService.addTournament(token, clubId, tournamentName, date, winningSets, allowsExternal); + const tournament = await tournamentService.addTournament(token, clubId, tournamentName, date, winningSets, allowsExternal, isDoublesTournament); if (clubId && tournament && tournament.id) { emitTournamentChanged(clubId, tournament.id); } @@ -271,9 +271,9 @@ export const getTournament = async (req, res) => { export const updateTournament = async (req, res) => { const { authcode: token } = req.headers; const { clubId, tournamentId } = req.params; - const { name, date, winningSets, numberOfTables } = req.body; + const { name, date, winningSets, numberOfTables, isDoublesTournament } = req.body; try { - const tournament = await tournamentService.updateTournament(token, clubId, tournamentId, name, date, winningSets, numberOfTables); + const tournament = await tournamentService.updateTournament(token, clubId, tournamentId, name, date, winningSets, numberOfTables, isDoublesTournament); // Emit Socket-Event emitTournamentChanged(clubId, tournamentId); res.status(200).json(tournament); @@ -748,4 +748,4 @@ export const deletePairing = async (req, res) => { res.status(500).json({ error: error.message }); } }; - \ No newline at end of file + diff --git a/backend/models/Tournament.js b/backend/models/Tournament.js index dddb2f7e..29dc7677 100644 --- a/backend/models/Tournament.js +++ b/backend/models/Tournament.js @@ -51,6 +51,13 @@ const Tournament = sequelize.define('Tournament', { defaultValue: null, comment: 'Anzahl der Tische, auf denen gespielt wird' }, + isDoublesTournament: { + type: DataTypes.BOOLEAN, + allowNull: false, + defaultValue: false, + field: 'is_doubles_tournament', + comment: 'Turnierweite Markierung fuer Doppel-Turniere' + }, }, { underscored: true, tableName: 'tournament', diff --git a/backend/services/tournamentService.js b/backend/services/tournamentService.js index a383f3e7..e8272619 100644 --- a/backend/services/tournamentService.js +++ b/backend/services/tournamentService.js @@ -832,13 +832,13 @@ class TournamentService { const tournaments = await Tournament.findAll({ where, order: [['date', 'DESC']], - attributes: ['id', 'name', 'date', 'allowsExternal', 'miniChampionshipYear'] + attributes: ['id', 'name', 'date', 'allowsExternal', 'miniChampionshipYear', 'isDoublesTournament'] }); return JSON.parse(JSON.stringify(tournaments)); } Ve // 2. Neues Turnier anlegen - async addTournament(userToken, clubId, tournamentName, date, winningSets, allowsExternal) { + async addTournament(userToken, clubId, tournamentName, date, winningSets, allowsExternal, isDoublesTournament = false) { await checkAccess(userToken, clubId); const t = await Tournament.create({ name: tournamentName, @@ -847,7 +847,8 @@ Ve // 2. Neues Turnier anlegen bestOfEndroundSize: 0, type: '', winningSets: winningSets || 3, // Default: 3 Sätze - allowsExternal: allowsExternal || false + allowsExternal: allowsExternal || false, + isDoublesTournament: Boolean(isDoublesTournament) }); return JSON.parse(JSON.stringify(t)); } @@ -2281,7 +2282,7 @@ Ve // 2. Neues Turnier anlegen } // Update Turnier (Name, Datum, Gewinnsätze und Tischanzahl) - async updateTournament(userToken, clubId, tournamentId, name, date, winningSets, numberOfTables) { + async updateTournament(userToken, clubId, tournamentId, name, date, winningSets, numberOfTables, isDoublesTournament) { await checkAccess(userToken, clubId); const tournament = await Tournament.findOne({ where: { id: tournamentId, clubId } }); if (!tournament) { @@ -2310,6 +2311,9 @@ Ve // 2. Neues Turnier anlegen } tournament.numberOfTables = numberOfTables; } + if (isDoublesTournament !== undefined) { + tournament.isDoublesTournament = Boolean(isDoublesTournament); + } await tournament.save(); return JSON.parse(JSON.stringify(tournament)); @@ -3945,6 +3949,22 @@ Ve // 2. Neues Turnier anlegen if (existingPairing) { throw new Error('Diese Paarung existiert bereits'); } + const playerAssignments = []; + if (player1Type === 'member') playerAssignments.push({ member1Id: player1Id }, { member2Id: player1Id }); + if (player1Type === 'external') playerAssignments.push({ external1Id: player1Id }, { external2Id: player1Id }); + if (player2Type === 'member') playerAssignments.push({ member1Id: player2Id }, { member2Id: player2Id }); + if (player2Type === 'external') playerAssignments.push({ external1Id: player2Id }, { external2Id: player2Id }); + + const participantAlreadyAssigned = await TournamentPairing.findOne({ + where: { + tournamentId, + classId, + [Op.or]: playerAssignments + } + }); + if (participantAlreadyAssigned) { + throw new Error('Mindestens ein Spieler ist bereits einer Doppelpaarung zugeordnet'); + } return await TournamentPairing.create({ tournamentId, classId, diff --git a/frontend/src/components/tournament/TournamentConfigTab.vue b/frontend/src/components/tournament/TournamentConfigTab.vue index 5fee8a3d..5dcda352 100644 --- a/frontend/src/components/tournament/TournamentConfigTab.vue +++ b/frontend/src/components/tournament/TournamentConfigTab.vue @@ -32,8 +32,15 @@ {{ $t('tournaments.playInGroups') }} + +
+ {{ $t('tournaments.doublesTournamentHint') }} +
@@ -633,6 +640,10 @@ export default { newClassMinBirthYear: { type: [Number, null], default: null + }, + tournamentWideIsDoubles: { + type: Boolean, + default: false } }, data() { @@ -1056,7 +1067,8 @@ export default { 'update:newClassName', 'update:newClassIsDoubles', 'update:newClassGender', - 'update:newClassMinBirthYear' + 'update:newClassMinBirthYear', + 'set-all-classes-doubles' ] , methods: { @@ -2045,6 +2057,13 @@ export default { gap: 0.75rem; } +.config-inline-hint { + margin-top: 0.65rem; + color: #6b7280; + font-size: 0.88rem; + line-height: 1.4; +} + .checkbox-item { display: inline-flex; align-items: center; diff --git a/frontend/src/components/tournament/TournamentParticipantsTab.vue b/frontend/src/components/tournament/TournamentParticipantsTab.vue index 536bdbdc..e6a369b8 100644 --- a/frontend/src/components/tournament/TournamentParticipantsTab.vue +++ b/frontend/src/components/tournament/TournamentParticipantsTab.vue @@ -194,122 +194,21 @@
{{ $t('tournaments.withoutClass') }} {{ getFilteredParticipantsForClass(null).length }}
- - - - - - - - - - - -
{{ $t('tournaments.name') }}{{ detailsLabel() }}{{ $t('tournaments.class') }}{{ $t('tournaments.group') }}{{ statusLabel() }}{{ $t('tournaments.action') }}
-
- - - - - - - - - - - -
- {{ participantDisplayName(participant) }} - -
- - {{ item }} - -
- -
- {{ participantAssignmentHint(participant) }} -
-
- - {{ warning }} - -
-
- {{ $t('tournaments.conflictSuggestionLabel') }} - -
-
- - - - -
- - -
-
- -
-
-
- -