diff --git a/backend/controllers/tournamentController.js b/backend/controllers/tournamentController.js index 76692f0..3932947 100644 --- a/backend/controllers/tournamentController.js +++ b/backend/controllers/tournamentController.js @@ -1,29 +1,33 @@ +// controllers/tournamentController.js import tournamentService from "../services/tournamentService.js"; +// 1. Alle Turniere eines Vereins export const getTournaments = async (req, res) => { const { authcode: token } = req.headers; - const clubId = req.params.clubId; + const { clubId } = req.params; try { const tournaments = await tournamentService.getTournaments(token, clubId); res.status(200).json(tournaments); } catch (error) { - console.log(error); + console.error(error); res.status(500).json({ error: error.message }); } }; +// 2. Neues Turnier anlegen export const addTournament = async (req, res) => { const { authcode: token } = req.headers; const { clubId, tournamentName, date } = req.body; try { const tournament = await tournamentService.addTournament(token, clubId, tournamentName, date); - res.status(200).json(tournament); + res.status(201).json(tournament); } catch (error) { - console.log(error); + console.error(error); res.status(500).json({ error: error.message }); } -} +}; +// 3. Teilnehmer hinzufügen export const addParticipant = async (req, res) => { const { authcode: token } = req.headers; const { clubId, tournamentId, participant: participantId } = req.body; @@ -32,11 +36,12 @@ export const addParticipant = async (req, res) => { const participants = await tournamentService.getParticipants(token, clubId, tournamentId); res.status(200).json(participants); } catch (error) { - console.log(error); + console.error(error); res.status(500).json({ error: error.message }); } -} +}; +// 4. Teilnehmerliste abrufen export const getParticipants = async (req, res) => { const { authcode: token } = req.headers; const { clubId, tournamentId } = req.body; @@ -44,33 +49,38 @@ export const getParticipants = async (req, res) => { const participants = await tournamentService.getParticipants(token, clubId, tournamentId); res.status(200).json(participants); } catch (error) { - console.log(error); + console.error(error); res.status(500).json({ error: error.message }); } -} +}; +// 5. Turniermodus (Gruppen/K.O.) setzen export const setModus = async (req, res) => { const { authcode: token } = req.headers; const { clubId, tournamentId, type, numberOfGroups } = req.body; try { await tournamentService.setModus(token, clubId, tournamentId, type, numberOfGroups); + res.sendStatus(204); } catch (error) { - console.log(error); + console.error(error); res.status(500).json({ error: error.message }); } -} +}; +// 6. Gruppen-Strukturen anlegen (leere Gruppen) export const createGroups = async (req, res) => { const { authcode: token } = req.headers; const { clubId, tournamentId } = req.body; try { await tournamentService.createGroups(token, clubId, tournamentId); + res.sendStatus(204); } catch (error) { - console.log(error); + console.error(error); res.status(500).json({ error: error.message }); } -} +}; +// 7. Teilnehmer zufällig auf Gruppen verteilen & Gruppenspiele anlegen export const fillGroups = async (req, res) => { const { authcode: token } = req.headers; const { clubId, tournamentId } = req.body; @@ -78,11 +88,12 @@ export const fillGroups = async (req, res) => { const updatedMembers = await tournamentService.fillGroups(token, clubId, tournamentId); res.status(200).json(updatedMembers); } catch (error) { - console.log(error); + console.error(error); res.status(500).json({ error: error.message }); } -} +}; +// 8. Gruppen mit ihren Teilnehmern abfragen export const getGroups = async (req, res) => { const { authcode: token } = req.headers; const { clubId, tournamentId } = req.query; @@ -90,11 +101,12 @@ export const getGroups = async (req, res) => { const groups = await tournamentService.getGroupsWithParticipants(token, clubId, tournamentId); res.status(200).json(groups); } catch (error) { - console.log(error); + console.error(error); res.status(500).json({ error: error.message }); } }; +// 9. Einzelnes Turnier abrufen export const getTournament = async (req, res) => { const { authcode: token } = req.headers; const { clubId, tournamentId } = req.params; @@ -102,11 +114,12 @@ export const getTournament = async (req, res) => { const tournament = await tournamentService.getTournament(token, clubId, tournamentId); res.status(200).json(tournament); } catch (error) { - console.log(error); + console.error(error); res.status(500).json({ error: error.message }); } -} +}; +// 10. Alle Spiele eines Turniers abfragen export const getTournamentMatches = async (req, res) => { const { authcode: token } = req.headers; const { clubId, tournamentId } = req.params; @@ -114,15 +127,15 @@ export const getTournamentMatches = async (req, res) => { const matches = await tournamentService.getTournamentMatches(token, clubId, tournamentId); res.status(200).json(matches); } catch (error) { - console.log(error); + console.error(error); res.status(500).json({ error: error.message }); } -} +}; +// 11. Satz-Ergebnis speichern export const addMatchResult = async (req, res) => { const { authcode: token } = req.headers; const { clubId, tournamentId, matchId, set, result } = req.body; - try { await tournamentService.addMatchResult(token, clubId, tournamentId, matchId, set, result); res.status(200).json({ message: "Result added successfully" }); @@ -132,15 +145,28 @@ export const addMatchResult = async (req, res) => { } }; +// 12. Spiel abschließen (Endergebnis ermitteln) export const finishMatch = async (req, res) => { const { authcode: token } = req.headers; const { clubId, tournamentId, matchId } = req.body; - try { await tournamentService.finishMatch(token, clubId, tournamentId, matchId); res.status(200).json({ message: "Match finished successfully" }); - } catch (error) { + } catch (error) { console.error(error); res.status(500).json({ error: error.message }); } -} +}; + +export const startKnockout = async (req, res) => { + const { authcode: token } = req.headers; + const { clubId, tournamentId } = req.body; + + try { + await tournamentService.startKnockout(token, clubId, tournamentId); + res.status(200).json({ message: 'K.O.-Runde erfolgreich gestartet' }); + } catch (error) { + console.error('Error in startKnockout:', error); + res.status(500).json({ error: error.message }); + } +}; diff --git a/backend/models/Tournament.js b/backend/models/Tournament.js index f93c286..a03d5c4 100644 --- a/backend/models/Tournament.js +++ b/backend/models/Tournament.js @@ -27,7 +27,8 @@ const Tournament = sequelize.define('Tournament', { type: DataTypes.INTEGER, allowNull: false, defaultValue: 1 - } + }, + advancingPerGroup: { type: DataTypes.INTEGER, allowNull: false, defaultValue: 1 }, }, { underscored: true, tableName: 'tournament', diff --git a/backend/models/TournamentMatch.js b/backend/models/TournamentMatch.js index b025e97..940c5a7 100644 --- a/backend/models/TournamentMatch.js +++ b/backend/models/TournamentMatch.js @@ -1,36 +1,51 @@ +// models/TournamentMatch.js import { DataTypes } from 'sequelize'; import sequelize from '../database.js'; +import Tournament from './Tournament.js'; +import TournamentGroup from './TournamentGroup.js'; const TournamentMatch = sequelize.define('TournamentMatch', { - tournamentId: { - type: DataTypes.INTEGER, - allowNull: false, + tournamentId: { + type: DataTypes.INTEGER, + allowNull: false, + references: { + model: Tournament, + key: 'id' }, - groupId: { - type: DataTypes.INTEGER, - allowNull: true, - }, - round: { - type: DataTypes.STRING, - allowNull: false, - }, - player1Id: { - type: DataTypes.INTEGER, - allowNull: false, - }, - player2Id: { - type: DataTypes.INTEGER, - allowNull: false, - }, - isFinished: { - type: DataTypes.BOOLEAN, - allowNull: false, - defaultValue: false, - }, - result: { - type: DataTypes.STRING, - allowNull: true, + onDelete: 'CASCADE', + onUpdate: 'CASCADE' + }, + groupId: { + type: DataTypes.INTEGER, + allowNull: true, + references: { + model: TournamentGroup, + key: 'id' }, + onDelete: 'SET NULL', + onUpdate: 'CASCADE' + }, + round: { + type: DataTypes.STRING, + allowNull: false, + }, + player1Id: { + type: DataTypes.INTEGER, + allowNull: false, + }, + player2Id: { + type: DataTypes.INTEGER, + allowNull: false, + }, + isFinished: { + type: DataTypes.BOOLEAN, + allowNull: false, + defaultValue: false, + }, + result: { + type: DataTypes.STRING, + allowNull: true, + }, }, { underscored: true, tableName: 'tournament_match', diff --git a/backend/models/index.js b/backend/models/index.js index 086af88..7a00d96 100644 --- a/backend/models/index.js +++ b/backend/models/index.js @@ -149,8 +149,19 @@ Tournament.hasMany(TournamentMatch, { foreignKey: 'tournamentId', as: 'tournamen TournamentMatch.belongsTo(TournamentGroup, { foreignKey: 'groupId', as: 'group' }); TournamentGroup.hasMany(TournamentMatch, { foreignKey: 'groupId', as: 'tournamentMatches' }); -TournamentResult.belongsTo(TournamentMatch, { foreignKey: 'matchId', as: 'match' }); -TournamentMatch.hasMany(TournamentResult, { foreignKey: 'matchId', as: 'tournamentResults' }); +TournamentMatch.hasMany(TournamentResult, { + foreignKey: 'matchId', + as: 'tournamentResults', + onDelete: 'CASCADE', + onUpdate: 'CASCADE' +}); + +TournamentResult.belongsTo(TournamentMatch, { + foreignKey: 'matchId', + as: 'match', + onDelete: 'CASCADE', + onUpdate: 'CASCADE' +}); TournamentMatch.belongsTo(TournamentMember, { foreignKey: 'player1Id', as: 'player1' }); TournamentMatch.belongsTo(TournamentMember, { foreignKey: 'player2Id', as: 'player2' }); diff --git a/backend/routes/tournamentRoutes.js b/backend/routes/tournamentRoutes.js index 2427064..824e0f1 100644 --- a/backend/routes/tournamentRoutes.js +++ b/backend/routes/tournamentRoutes.js @@ -12,6 +12,7 @@ import { getTournamentMatches, addMatchResult, finishMatch, + startKnockout, } from '../controllers/tournamentController.js'; import { authenticate } from '../middleware/authMiddleware.js'; @@ -28,6 +29,8 @@ router.post('/match/finish', authenticate, finishMatch); router.get('/matches/:clubId/:tournamentId', authenticate, getTournamentMatches); router.get('/:clubId/:tournamentId', authenticate, getTournament); router.get('/:clubId', authenticate, getTournaments); +router.post('/knockout', authenticate, startKnockout); router.post('/', authenticate, addTournament); + export default router; diff --git a/backend/services/tournamentService.js b/backend/services/tournamentService.js index 357b34c..b8a5938 100644 --- a/backend/services/tournamentService.js +++ b/backend/services/tournamentService.js @@ -6,300 +6,330 @@ import TournamentMatch from "../models/TournamentMatch.js"; import TournamentMember from "../models/TournamentMember.js"; import TournamentResult from "../models/TournamentResult.js"; import { checkAccess } from '../utils/userUtils.js'; +import { Op, literal } from 'sequelize'; class TournamentService { + // 1. Turniere listen async getTournaments(userToken, clubId) { await checkAccess(userToken, clubId); - const tournaments = await Tournament.findAll( - { - where: { clubId }, - order: [['date', 'DESC']], - attributes: ['id', 'name', 'date'] - } - ); + const tournaments = await Tournament.findAll({ + where: { clubId }, + order: [['date', 'DESC']], + attributes: ['id', 'name', 'date'] + }); return JSON.parse(JSON.stringify(tournaments)); } + // 2. Neues Turnier anlegen (prüft Duplikat) async addTournament(userToken, clubId, tournamentName, date) { await checkAccess(userToken, clubId); - const club = await Club.findByPk(clubId); - await Tournament.create({ + const existing = await Tournament.findOne({ where: { clubId, date } }); + if (existing) { + throw new Error('Ein Turnier mit diesem Datum existiert bereits'); + } + const t = await Tournament.create({ name: tournamentName, - date: date, - clubId: club.id, + date, + clubId: +clubId, bestOfEndroundSize: 0, - type: '', - name: '', + type: '' }); - return await this.getTournaments(userToken, clubId); - } + return JSON.parse(JSON.stringify(t)); + } - async addParticipant(token, clubId, tournamentId, participantId) { - await checkAccess(token, clubId); + // 3. Teilnehmer hinzufügen (kein Duplikat) + async addParticipant(userToken, clubId, tournamentId, participantId) { + await checkAccess(userToken, clubId); const tournament = await Tournament.findByPk(tournamentId); if (!tournament || tournament.clubId != clubId) { - throw new Error('Tournament not found'); + throw new Error('Turnier nicht gefunden'); } - const participant = TournamentMember.findAll({ - where: { tournamentId: tournamentId, groupId: participantId, clubMemberId: participantId }, + const exists = await TournamentMember.findOne({ + where: { tournamentId, clubMemberId: participantId } }); - if (participant) { - throw new Error('Participant already exists'); + if (exists) { + throw new Error('Teilnehmer bereits hinzugefügt'); } await TournamentMember.create({ - tournamentId: tournamentId, - groupId: participantId, + tournamentId, clubMemberId: participantId, + groupId: null }); } - async getParticipants(token, clubId, tournamentId) { - await checkAccess(token, clubId); + // 4. Teilnehmerliste + async getParticipants(userToken, clubId, tournamentId) { + await checkAccess(userToken, clubId); const tournament = await Tournament.findByPk(tournamentId); if (!tournament || tournament.clubId != clubId) { - throw new Error('Tournament not found'); + throw new Error('Turnier nicht gefunden'); } return await TournamentMember.findAll({ - where: { - tournamentId: tournamentId, - }, - include: [ - { - model: Member, - as: 'member', - attributes: ['id', 'lastName', 'firstName'], - order: [['firstName', 'ASC'], ['lastName', 'ASC']], - } - ] + where: { tournamentId }, + include: [{ + model: Member, + as: 'member', + attributes: ['id', 'firstName', 'lastName'], + }], + order: [[{ model: Member, as: 'member' }, 'firstName', 'ASC']] }); } - async setModus(token, clubId, tournamentId, type, numberOfGroups) { - await checkAccess(token, clubId); + // 5. Modus setzen (Gruppen / KO‑Runde) + async setModus(userToken, clubId, tournamentId, type, numberOfGroups) { + await checkAccess(userToken, clubId); const tournament = await Tournament.findByPk(tournamentId); if (!tournament || tournament.clubId != clubId) { - throw new Error('Tournament not found'); + throw new Error('Turnier nicht gefunden'); } await tournament.update({ type, numberOfGroups }); } - - async createGroups(token, clubId, tournamentId) { - await checkAccess(token, clubId); + + // 6. Leere Gruppen anlegen + async createGroups(userToken, clubId, tournamentId) { + await checkAccess(userToken, clubId); const tournament = await Tournament.findByPk(tournamentId); if (!tournament || tournament.clubId != clubId) { - throw new Error('Tournament not found'); + throw new Error('Turnier nicht gefunden'); } - const existingGroups = await TournamentGroup.findAll({ where: { tournamentId } }); - const desiredGroupCount = tournament.numberOfGroups; - if (existingGroups.length < desiredGroupCount) { - const missingGroups = desiredGroupCount - existingGroups.length; - for (let i = 0; i < missingGroups; i++) { - await TournamentGroup.create({ tournamentId }); - } - } else if (existingGroups.length > desiredGroupCount) { - existingGroups.sort((a, b) => a.id - b.id); - const groupsToRemove = existingGroups.slice(desiredGroupCount); - for (const group of groupsToRemove) { - await group.destroy(); - } + const existing = await TournamentGroup.findAll({ where: { tournamentId } }); + const desired = tournament.numberOfGroups; + // zu viele Gruppen löschen + if (existing.length > desired) { + const toRemove = existing.slice(desired); + await Promise.all(toRemove.map(g => g.destroy())); + } + // fehlende Gruppen anlegen + for (let i = existing.length; i < desired; i++) { + await TournamentGroup.create({ tournamentId }); } } - async fillGroups(token, clubId, tournamentId) { - await checkAccess(token, clubId); + // 7. Gruppen zufällig füllen & Spiele anlegen + async fillGroups(userToken, clubId, tournamentId) { + await checkAccess(userToken, clubId); const tournament = await Tournament.findByPk(tournamentId); if (!tournament || tournament.clubId != clubId) { - throw new Error('Tournament not found'); + throw new Error('Turnier nicht gefunden'); } const groups = await TournamentGroup.findAll({ where: { tournamentId } }); - if (!groups || groups.length === 0) { - throw new Error('No groups available. Please create groups first.'); + if (!groups.length) { + throw new Error('Keine Gruppen vorhanden. Erst erstellen.'); } const members = await TournamentMember.findAll({ where: { tournamentId } }); - if (!members || members.length === 0) { - throw new Error('No tournament members found.'); + if (!members.length) { + throw new Error('Keine Teilnehmer vorhanden.'); } + // alte Matches löschen await TournamentMatch.destroy({ where: { tournamentId } }); - const shuffledMembers = [...members]; - for (let i = shuffledMembers.length - 1; i > 0; i--) { + + // mische Teilnehmer + const shuffled = members.slice(); + for (let i = shuffled.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); - [shuffledMembers[i], shuffledMembers[j]] = [shuffledMembers[j], shuffledMembers[i]]; + [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]; } - const numberOfGroups = groups.length; - for (let i = 0; i < shuffledMembers.length; i++) { - const groupAssignment = groups[i % numberOfGroups].id; - await shuffledMembers[i].update({ groupId: groupAssignment }); + // verteile in round‑robin‑Gruppen + for (let idx = 0; idx < shuffled.length; idx++) { + const grpId = groups[idx % groups.length].id; + await shuffled[idx].update({ groupId: grpId }); } - for (const group of groups) { - const groupMembers = await TournamentMember.findAll({ where: { groupId: group.id } }); - for (let i = 0; i < groupMembers.length; i++) { - for (let j = i + 1; j < groupMembers.length; j++) { + // lege alle Paarungen in jeder Gruppe an + for (const g of groups) { + const gm = await TournamentMember.findAll({ where: { groupId: g.id } }); + for (let i = 0; i < gm.length; i++) { + for (let j = i + 1; j < gm.length; j++) { await TournamentMatch.create({ - tournamentId: tournamentId, - groupId: group.id, + tournamentId, + groupId: g.id, round: 'group', - player1Id: groupMembers[i].id, - player2Id: groupMembers[j].id, + player1Id: gm[i].id, + player2Id: gm[j].id }); } } } return await TournamentMember.findAll({ where: { tournamentId } }); } - - async getGroups(token, clubId, tournamentId) { - await checkAccess(token, clubId); + + // 8. Nur Gruppen (ohne Teilnehmer) + async getGroups(userToken, clubId, tournamentId) { + await checkAccess(userToken, clubId); const tournament = await Tournament.findByPk(tournamentId); if (!tournament || tournament.clubId != clubId) { - throw new Error('Tournament not found'); + throw new Error('Turnier nicht gefunden'); } - const groups = await TournamentGroup.findAll({ where: { tournamentId } }); - return groups; + return await TournamentGroup.findAll({ where: { tournamentId } }); } - async getGroupsWithParticipants(token, clubId, tournamentId) { - await checkAccess(token, clubId); + // 9. Gruppen mit ihren Teilnehmern + async getGroupsWithParticipants(userToken, clubId, tournamentId) { + await checkAccess(userToken, clubId); const tournament = await Tournament.findByPk(tournamentId); if (!tournament || tournament.clubId != clubId) { - throw new Error('Tournament not found'); + throw new Error('Turnier nicht gefunden'); } const groups = await TournamentGroup.findAll({ where: { tournamentId }, - include: [ - { - model: TournamentMember, - as: 'tournamentGroupMembers', - required: false, - include: [ - { - model: Member, - as: 'member', - attributes: ['id', 'firstName', 'lastName'] - } - ] - } - ], + include: [{ + model: TournamentMember, + as: 'tournamentGroupMembers', + include: [{ model: Member, as: 'member', attributes: ['id', 'firstName', 'lastName'] }] + }], order: [['id', 'ASC']] }); - return groups.map(group => ({ - groupId: group.id, - participants: group.tournamentGroupMembers.map(p => ({ - id: p.id, - name: `${p.member.firstName} ${p.member.lastName}` + return groups.map(g => ({ + groupId: g.id, + participants: g.tournamentGroupMembers.map(m => ({ + id: m.id, + name: `${m.member.firstName} ${m.member.lastName}` })) })); } - async getTournament(token, clubId, tournamentId) { - await checkAccess(token, clubId); - const tournament = await Tournament.findOne({ - where: { id: tournamentId, clubId }, - }); - if (!tournament) { - throw new Error('Tournament not found'); - } - return tournament; + // 10. Einzelnes Turnier + async getTournament(userToken, clubId, tournamentId) { + await checkAccess(userToken, clubId); + const t = await Tournament.findOne({ where: { id: tournamentId, clubId } }); + if (!t) throw new Error('Turnier nicht gefunden'); + return t; } - async getTournamentMatches(token, clubId, tournamentId) { - await checkAccess(token, clubId); - const tournament = await Tournament.findOne({ - where: { id: tournamentId, clubId }, - }); - if (!tournament) { - throw new Error('Tournament not found'); - } - const matches = await TournamentMatch.findAll({ + // 11. Spiele eines Turniers + async getTournamentMatches(userToken, clubId, tournamentId) { + await checkAccess(userToken, clubId); + const t = await Tournament.findOne({ where: { id: tournamentId, clubId } }); + if (!t) throw new Error('Turnier nicht gefunden'); + return await TournamentMatch.findAll({ where: { tournamentId }, include: [ - { - model: TournamentMember, - as: 'player1', - include: { - model: Member, - as: 'member', - } - }, - { - model: TournamentMember, - as: 'player2', - include: { - model: Member, - as: 'member', - } - }, - { - model: TournamentResult, - as: 'tournamentResults', - order: [['set', 'ASC']] - } + { model: TournamentMember, as: 'player1', include: [{ model: Member, as: 'member' }] }, + { model: TournamentMember, as: 'player2', include: [{ model: Member, as: 'member' }] }, + { model: TournamentResult, as: 'tournamentResults' } ], - order: [['id', 'ASC']] + order: [ + ['id', 'ASC'], + [{ model: TournamentResult, as: 'tournamentResults' }, 'set', 'ASC'] + ] }); - return matches; } - async addMatchResult(token, clubId, tournamentId, matchId, set, result) { - await checkAccess(token, clubId); - - const matches = await TournamentMatch.findAll({ - where: { id: matchId, tournamentId }, - }); - - if (matches.length > 0) { - const match = matches[0]; - const tournamentResult = await TournamentResult.findOne({where: { - matchId: match.id, - set: set, - } - }); - if (tournamentResult && tournamentResult.set == set) { - tournamentResult.result = result; - await tournamentResult.save(); - } else { - const points = result.split(':'); - await TournamentResult.create({ - matchId, - set, - pointsPlayer1: points[0], - pointsPlayer2: points[1], - }); - } - return; + // 12. Satz-Ergebnis hinzufügen/überschreiben + async addMatchResult(userToken, clubId, tournamentId, matchId, set, result) { + await checkAccess(userToken, clubId); + const [match] = await TournamentMatch.findAll({ where: { id: matchId, tournamentId } }); + if (!match) throw new Error('Match nicht gefunden'); + const existing = await TournamentResult.findOne({ where: { matchId, set } }); + if (existing) { + existing.pointsPlayer1 = +result.split(':')[0]; + existing.pointsPlayer2 = +result.split(':')[1]; + await existing.save(); + } else { + const [p1, p2] = result.split(':').map(Number); + await TournamentResult.create({ matchId, set, pointsPlayer1: p1, pointsPlayer2: p2 }); } - throw new Error('Match not found'); } - async finishMatch(token, clubId, tournamentId, matchId) { - await checkAccess(token, clubId); + // 13. Match abschließen (Endergebnis setzen) + async finishMatch(userToken, clubId, tournamentId, matchId) { + await checkAccess(userToken, clubId); const matches = await TournamentMatch.findAll({ where: { id: matchId, tournamentId }, - include: [ - { - model: TournamentResult, - as: 'tournamentResults' - } - ], + include: [{ model: TournamentResult, as: 'tournamentResults' }], order: [[{ model: TournamentResult, as: 'tournamentResults' }, 'set', 'ASC']] }); - let win = 0; - let lose = 0; - for (const results of matches[0].tournamentResults) { - if (results.pointsPlayer1 > results.pointsPlayer2) { - win++; - } else { - lose++; + const match = matches[0]; + if (!match) throw new Error('Match nicht gefunden'); + let win = 0, lose = 0; + match.tournamentResults.forEach(r => { + if (r.pointsPlayer1 > r.pointsPlayer2) win++; + else lose++; + }); + match.isFinished = true; + match.result = `${win}:${lose}`; + await match.save(); + } + + /** + * Ermittelt aus jeder Gruppe den Gruppensieger und legt + * für die K.O.-Runde die ersten Matches an. + */ + // services/tournamentService.js + async startKnockout(token, clubId, tournamentId) { + await checkAccess(token, clubId); + const t = await Tournament.findByPk(tournamentId); + if (!t || t.clubId != clubId) throw new Error('Tournament not found'); + + const totalQualifiers = t.numberOfGroups * t.advancingPerGroup; + if (totalQualifiers < 2) throw new Error('Zu wenige Qualifikanten für K.O.-Runde'); + + // lösche frühere KO-Matches + await TournamentMatch.destroy({ where: { tournamentId, round: { [Op.ne]: 'group' } } }); + + // lade alle Gruppenteilnehmer + const groups = await TournamentGroup.findAll({ + where: { tournamentId }, + include: [{ model: TournamentMember, as: 'tournamentGroupMembers' }] + }); + // lade alle Gruppenspiele und Ergebnisse + const groupMatches = await TournamentMatch.findAll({ + where: { tournamentId, round: 'group' }, + include: [{ model: TournamentResult, as: 'tournamentResults' }] + }); + + const qualifiers = []; + for (const g of groups) { + // init stats + const stats = {}; + g.tournamentGroupMembers.forEach(m => { + stats[m.id] = { member: m, points: 0, setsWon: 0, setsLost: 0 }; + }); + // auswerten + for (const m of groupMatches.filter(m => m.groupId === g.id && m.isFinished)) { + const [p1, p2] = m.result.split(':').map(n => parseInt(n, 10)); + if (p1 > p2) stats[m.player1Id].points += 2; + else if (p2 > p1) stats[m.player2Id].points += 2; + stats[m.player1Id].setsWon += p1; + stats[m.player1Id].setsLost += p2; + stats[m.player2Id].setsWon += p2; + stats[m.player2Id].setsLost += p1; } + // sortieren + const ranked = Object.values(stats).sort((a, b) => { + const diffA = a.setsWon - a.setsLost; + const diffB = b.setsWon - b.setsLost; + if (b.points !== a.points) return b.points - a.points; + if (diffB !== diffA) return diffB - diffA; + if (b.setsWon !== a.setsWon) return b.setsWon - a.setsWon; + return a.member.id - b.member.id; + }); + // take top N + qualifiers.push(...ranked.slice(0, t.advancingPerGroup).map(r => r.member)); } - const result = win.toString() + ':' + lose.toString(); - if (matches.length == 1) { - const match = matches[0]; - match.isFinished = true; - match.result = result; - await match.save(); - return; + + // bracket aufbauen + let roundSize = qualifiers.length; + const getRoundName = size => { + switch (size) { + case 2: return 'Finale'; + case 4: return 'Halbfinale'; + case 8: return 'Viertelfinale'; + case 16: return 'Achtelfinale'; + default: return `Runde der ${size}`; + } + }; + + while (roundSize >= 2) { + const rn = getRoundName(roundSize); + for (let i = 0; i < roundSize / 2; i++) { + const p1 = qualifiers[i].id; + const p2 = qualifiers[roundSize - 1 - i].id; + await TournamentMatch.create({ tournamentId, round: rn, player1Id: p1, player2Id: p2 }); + } + // Platzhalter für nächste Runde + qualifiers.splice(roundSize / 2); + roundSize = roundSize / 2; } - throw new Error('Match not found'); } } diff --git a/frontend/src/views/TournamentsView.vue b/frontend/src/views/TournamentsView.vue index 0508c11..3bcf8c6 100644 --- a/frontend/src/views/TournamentsView.vue +++ b/frontend/src/views/TournamentsView.vue @@ -1,107 +1,127 @@ @@ -111,60 +131,77 @@ import apiClient from '../apiClient'; export default { name: 'TournamentsView', - computed: { - ...mapGetters(['isAuthenticated', 'currentClub', 'clubs']), - }, data() { return { selectedDate: 'new', newDate: '', dates: [], + participants: [], selectedMember: null, clubMembers: [], + numberOfGroups: 1, isGroupTournament: false, groups: [], + matches: [], + showKnockout: false, }; }, + computed: { + ...mapGetters(['isAuthenticated', 'currentClub']), + knockoutMatches() { + return this.matches.filter(m => m.round !== 'group'); + }, + groupRankings() { + const byGroup = {}; + this.groups.forEach(g => { + byGroup[g.groupId] = g.participants.map(p => ({ + id: p.id, + name: p.name, + points: 0, + setsWon: 0, + setsLost: 0, + setDiff: 0, + })); + }); + this.matches.forEach(m => { + if (!m.isFinished || m.round !== 'group') return; + const [s1, s2] = m.result.split(':').map(n => +n); + const arr = byGroup[m.groupId]; + if (!arr) return; + const e1 = arr.find(x => x.id === m.player1.id); + const e2 = arr.find(x => x.id === m.player2.id); + if (!e1 || !e2) return; + if (s1 > s2) e1.points += 2; + else if (s2 > s1) e2.points += 2; + e1.setsWon += s1; e1.setsLost += s2; + e2.setsWon += s2; e2.setsLost += s1; + }); + const rankings = {}; + Object.entries(byGroup).forEach(([gid, arr]) => { + arr.forEach(p => p.setDiff = p.setsWon - p.setsLost); + arr.sort((a, b) => { + if (b.points !== a.points) return b.points - a.points; + if (b.setDiff !== a.setDiff) return b.setDiff - a.setDiff; + if (b.setsWon !== a.setsWon) return b.setsWon - a.setsWon; + return a.name.localeCompare(b.name); + }); + rankings[gid] = arr.map((p, i) => ({ + ...p, position: i + 1 + })); + }); + return rankings; + } + }, watch: { - selectedDate: async function (newVal) { - if (newVal !== 'new') { - try { - const groupResponse = await apiClient.get(`/tournament/${this.currentClub}/${newVal}`); - this.isGroupTournament = groupResponse.data.type === 'groups'; - const participantsResponse = await apiClient.post('/tournament/participants', { - clubId: this.currentClub, - tournamentId: newVal, - }); - this.participants = participantsResponse.data; - } catch (error) { - console.error(error); - } - await this.fetchGroups(); + selectedDate: { + immediate: true, + handler: async function (val) { + if (val === 'new') return; + await this.loadTournamentData(); } - }, - isGroupTournament: async function (newVal) { - if (newVal) { - this.numberOfGroups = 2; - } else { - this.numberOfGroups = 1; - } - await apiClient.post('/tournament/modus', { - clubId: this.currentClub, - tournamentId: this.selectedDate, - type: newVal ? 'groups' : 'bestOf', - numberOfGroups: this.numberOfGroups, - }); - }, - numberOfGroups: async function (newVal) { - await apiClient.post('/tournament/modus', { - clubId: this.currentClub, - tournamentId: this.selectedDate, - type: this.isGroupTournament ? 'groups' : 'bestOf', - numberOfGroups: newVal, - }); } }, async created() { @@ -172,132 +209,163 @@ export default { this.$router.push('/login'); return; } - - try { - const responseDates = await apiClient.get(`/tournament/${this.currentClub}`); - this.dates = responseDates.data; - } catch (error) { - console.error('Error fetching tournaments:', error); - } - - try { - const responseMembers = await apiClient.get(`/clubmembers/get/${this.currentClub}/false`); - this.clubMembers = responseMembers.data; - } catch (error) { - console.error('Error fetching club members:', error); - } + // Turniere und Mitglieder laden + const d = await apiClient.get(`/tournament/${this.currentClub}`); + this.dates = d.data; + const m = await apiClient.get( + `/clubmembers/get/${this.currentClub}/false` + ); + this.clubMembers = m.data; }, methods: { + async loadTournamentData() { + // 1) Turnier‐Metadaten holen (Typ + Anzahl Gruppen) + const tRes = await apiClient.get( + `/tournament/${this.currentClub}/${this.selectedDate}` + ); + const tournament = tRes.data; + this.isGroupTournament = tournament.type === 'groups'; + this.numberOfGroups = tournament.numberOfGroups; + + // 2) Teilnehmer + const pRes = await apiClient.post('/tournament/participants', { + clubId: this.currentClub, + tournamentId: this.selectedDate + }); + this.participants = pRes.data; + + // 3) Gruppen (mit Teilnehmern) + const gRes = await apiClient.get('/tournament/groups', { + params: { + clubId: this.currentClub, + tournamentId: this.selectedDate + } + }); + this.groups = gRes.data; + + // 4) Alle Matches + const mRes = await apiClient.get( + `/tournament/matches/${this.currentClub}/${this.selectedDate}` + ); + this.matches = mRes.data; + + // 5) Steuere K.o.-Anzeige + this.showKnockout = this.matches.some(m => m.round !== 'group'); + }, + + getPlayerName(p) { + return p.member.firstName + ' ' + p.member.lastName; + }, + async createTournament() { - try { - const response = await apiClient.post('/tournament', { - clubId: this.currentClub, - name: this.newDate, - date: this.newDate, - }); - this.dates = response.data; - this.newDate = ''; - } catch (error) { - console.error('Error creating tournament:', error); - } + const r = await apiClient.post('/tournament', { + clubId: this.currentClub, + tournamentName: this.newDate, + date: this.newDate + }); + this.dates = r.data; + this.selectedDate = this.dates[this.dates.length - 1].id; + this.newDate = ''; }, + async addParticipant() { - try { - const response = await apiClient.post('/tournament/participant', { - clubId: this.currentClub, - tournamentId: this.selectedDate, - participant: this.selectedMember, - }); - this.participants = response.data; - } catch (error) { - console.error('Error adding participant:', error); - } + const r = await apiClient.post('/tournament/participant', { + clubId: this.currentClub, + tournamentId: this.selectedDate, + participant: this.selectedMember + }); + this.participants = r.data; }, + async createGroups() { await apiClient.put('/tournament/groups', { clubId: this.currentClub, - tournamentId: this.selectedDate, + tournamentId: this.selectedDate }); - await this.fetchGroups(); + await this.loadTournamentData(); }, + async randomizeGroups() { try { - const response = await apiClient.post('/tournament/groups', { + const r = await apiClient.post('/tournament/groups', { clubId: this.currentClub, - tournamentId: this.selectedDate, + tournamentId: this.selectedDate }); - } catch (error) { - console.error('Error randomizing groups:', error); + this.participants = r.data; + } catch (err) { + alert('Fehler beim Zufällig‑Verteilen:\n' + + (err.response?.data?.error || err.message)); } - await this.fetchGroups(); + await this.loadTournamentData(); }, - async fetchGroups() { - try { - const response = await apiClient.get('/tournament/groups', { - params: { - clubId: this.currentClub, - tournamentId: this.selectedDate - } - }); - this.groups = response.data; - const matchesResponse = await apiClient.get(`/tournament/matches/${this.currentClub}/${this.selectedDate}`); - this.matches = matchesResponse.data; - console.log(this.matches); - } catch (error) { - console.error('Error fetching groups:', error); - } - }, - getPlayerName(player) { - return player.member.firstName + ' ' + player.member.lastName; - }, - async saveMatchResult(match, set, result) { - try { - await apiClient.post('/tournament/match/result', { - clubId: this.currentClub, - tournamentId: this.selectedDate, - matchId: match.id, - set, - result: result, - }); - this.fetchGroups(); - } catch (error) { - console.error('Error saving match result:', error); + + async saveMatchResult(match, result) { + // wenn kein ':' dabei, ergänzen + if (result.indexOf(':') === -1) { + result = result.indexOf('-') > -1 + ? '11:' + result + : (result * -1) + ':11'; } + await apiClient.post('/tournament/match/result', { + clubId: this.currentClub, + tournamentId: this.selectedDate, + matchId: match.id, + set: (match.tournamentResults?.length || 0) + 1, + result + }); + await this.loadTournamentData(); }, + async finishMatch(match) { - try { - await apiClient.post('/tournament/match/finish', { - clubId: this.currentClub, - tournamentId: this.selectedDate, - matchId: match.id, - }); - this.fetchGroups(); - } catch (error) { - console.error('Error finishing match:', error); - } + await apiClient.post('/tournament/match/finish', { + clubId: this.currentClub, + tournamentId: this.selectedDate, + matchId: match.id + }); + await this.loadTournamentData(); }, - }, + + async startKnockout() { + await apiClient.post('/tournament/knockout', { + clubId: this.currentClub, + tournamentId: this.selectedDate + }); + await this.loadTournamentData(); + } + } }; \ No newline at end of file