From 4122868ab0bef2033ec166c16948fa057799da34 Mon Sep 17 00:00:00 2001 From: Torsten Schulz Date: Wed, 16 Jul 2025 14:29:34 +0200 Subject: [PATCH] finished tournaments --- backend/controllers/tournamentController.js | 116 +++- backend/models/Tournament.js | 4 +- backend/models/TournamentMatch.js | 4 + backend/routes/tournamentRoutes.js | 15 +- backend/services/tournamentService.js | 404 +++++++++++--- frontend/src/views/TournamentsView.vue | 557 ++++++++++++++++++-- 6 files changed, 963 insertions(+), 137 deletions(-) diff --git a/backend/controllers/tournamentController.js b/backend/controllers/tournamentController.js index 3932947..dd197f1 100644 --- a/backend/controllers/tournamentController.js +++ b/backend/controllers/tournamentController.js @@ -57,9 +57,9 @@ export const getParticipants = async (req, res) => { // 5. Turniermodus (Gruppen/K.O.) setzen export const setModus = async (req, res) => { const { authcode: token } = req.headers; - const { clubId, tournamentId, type, numberOfGroups } = req.body; + const { clubId, tournamentId, type, numberOfGroups, advancingPerGroup } = req.body; try { - await tournamentService.setModus(token, clubId, tournamentId, type, numberOfGroups); + await tournamentService.setModus(token, clubId, tournamentId, type, numberOfGroups, advancingPerGroup); res.sendStatus(204); } catch (error) { console.error(error); @@ -164,9 +164,117 @@ export const startKnockout = async (req, res) => { try { await tournamentService.startKnockout(token, clubId, tournamentId); - res.status(200).json({ message: 'K.O.-Runde erfolgreich gestartet' }); + res.status(200).json({ message: "K.o.-Runde erfolgreich gestartet" }); } catch (error) { - console.error('Error in startKnockout:', error); + const status = /Gruppenmodus|Zu wenige Qualifikanten/.test(error.message) ? 400 : 500; + res.status(status).json({ error: error.message }); + } +}; + +export const manualAssignGroups = async (req, res) => { + const { authcode: token } = req.headers; + const { + clubId, + tournamentId, + assignments, // [{ participantId, groupNumber }] + numberOfGroups, // optional + maxGroupSize // optional + } = req.body; + + try { + const groupsWithParts = await tournamentService.manualAssignGroups( + token, + clubId, + tournamentId, + assignments, + numberOfGroups, // neu + maxGroupSize // neu + ); + res.status(200).json(groupsWithParts); + } catch (error) { + console.error('Error in manualAssignGroups:', error); res.status(500).json({ error: error.message }); } }; + +export const resetGroups = async (req, res) => { + const { authcode: token } = req.headers; + const { clubId, tournamentId } = req.body; + try { + await tournamentService.resetGroups(token, clubId, tournamentId); + res.sendStatus(204); + } catch (err) { + console.error(err); + res.status(500).json({ error: err.message }); + } +}; + +export const resetMatches = async (req, res) => { + const { authcode: token } = req.headers; + const { clubId, tournamentId } = req.body; + try { + await tournamentService.resetMatches(token, clubId, tournamentId); + res.sendStatus(204); + } catch (err) { + console.error(err); + res.status(500).json({ error: err.message }); + } +}; + +export const removeParticipant = async (req, res) => { + const { authcode: token } = req.headers; + const { clubId, tournamentId, participantId } = req.body; + try { + await tournamentService.removeParticipant(token, clubId, tournamentId, participantId); + const participants = await tournamentService.getParticipants(token, clubId, tournamentId); + res.status(200).json(participants); + } catch (err) { + console.error(err); + res.status(500).json({ error: err.message }); + } +}; + +export const deleteMatchResult = async (req, res) => { + const { authcode: token } = req.headers; + const { clubId, tournamentId, matchId, set } = req.body; + try { + await tournamentService.deleteMatchResult( + token, + clubId, + tournamentId, + matchId, + set + ); + res.status(200).json({ message: 'Einzelsatz gelöscht' }); + } catch (error) { + console.error('Error in deleteMatchResult:', error); + res.status(500).json({ error: error.message }); + } +}; + + +export const reopenMatch = async (req, res) => { + const { authcode: token } = req.headers; + const { clubId, tournamentId, matchId } = req.body; + try { + await tournamentService.reopenMatch(token, clubId, tournamentId, matchId); + // Gib optional das aktualisierte Match zurück + res.status(200).json({ message: "Match reopened" }); + } catch (error) { + console.error("Error in reopenMatch:", error); + res.status(500).json({ error: error.message }); + } +}; + +export const deleteKnockoutMatches = async (req, res) => { + const { authcode: token } = req.headers; + const { clubId, tournamentId } = req.body; + try { + await tournamentService.resetKnockout(token, clubId, tournamentId); + res.status(200).json({ message: "K.o.-Runde gelöscht" }); + } catch (error) { + console.error("Error in deleteKnockoutMatches:", error); + 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 a03d5c4..277c468 100644 --- a/backend/models/Tournament.js +++ b/backend/models/Tournament.js @@ -14,14 +14,14 @@ const Tournament = sequelize.define('Tournament', { type: DataTypes.STRING, allowNull: false, }, - bestOfEndroundSize: { + advancingPerGroup: { type: DataTypes.INTEGER, allowNull: false, }, numberOfGroups: { type: DataTypes.INTEGER, allowNull: true, - defaultValue: 0, + defaultValue: 0, }, clubId: { type: DataTypes.INTEGER, diff --git a/backend/models/TournamentMatch.js b/backend/models/TournamentMatch.js index 940c5a7..4b69075 100644 --- a/backend/models/TournamentMatch.js +++ b/backend/models/TournamentMatch.js @@ -25,6 +25,10 @@ const TournamentMatch = sequelize.define('TournamentMatch', { onDelete: 'SET NULL', onUpdate: 'CASCADE' }, + groupRound: { + type: DataTypes.INTEGER, + allowNull: true, + }, round: { type: DataTypes.STRING, allowNull: false, diff --git a/backend/routes/tournamentRoutes.js b/backend/routes/tournamentRoutes.js index 824e0f1..8e81dc3 100644 --- a/backend/routes/tournamentRoutes.js +++ b/backend/routes/tournamentRoutes.js @@ -13,6 +13,13 @@ import { addMatchResult, finishMatch, startKnockout, + manualAssignGroups, + resetGroups, + resetMatches, + removeParticipant, + deleteMatchResult, + reopenMatch, + deleteKnockoutMatches, } from '../controllers/tournamentController.js'; import { authenticate } from '../middleware/authMiddleware.js'; @@ -20,17 +27,23 @@ const router = express.Router(); router.post('/participant', authenticate, addParticipant); router.post('/participants', authenticate, getParticipants); +router.delete('/participant', authenticate, removeParticipant); router.post('/modus', authenticate, setModus); +router.post('/groups/reset', authenticate, resetGroups); +router.post('/matches/reset', authenticate, resetMatches); router.put('/groups', authenticate, createGroups); router.post('/groups', authenticate, fillGroups); router.get('/groups', authenticate, getGroups); router.post('/match/result', authenticate, addMatchResult); +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.get('/:clubId/:tournamentId', authenticate, getTournament); router.get('/:clubId', authenticate, getTournaments); router.post('/knockout', authenticate, startKnockout); +router.delete("/matches/knockout", deleteKnockoutMatches); +router.post('/groups/manual', authenticate, manualAssignGroups); router.post('/', authenticate, addTournament); - export default router; diff --git a/backend/services/tournamentService.js b/backend/services/tournamentService.js index b8a5938..ec043f7 100644 --- a/backend/services/tournamentService.js +++ b/backend/services/tournamentService.js @@ -8,6 +8,25 @@ import TournamentResult from "../models/TournamentResult.js"; import { checkAccess } from '../utils/userUtils.js'; import { Op, literal } from 'sequelize'; + +function 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}`; + } +} + +function nextRoundName(currentName) { + switch (currentName) { + case "Achtelfinale": return "Viertelfinale"; + case "Viertelfinale": return "Halbfinale"; + case "Halbfinale": return "Finale"; + default: return null; + } +} class TournamentService { // 1. Turniere listen async getTournaments(userToken, clubId) { @@ -76,13 +95,13 @@ class TournamentService { } // 5. Modus setzen (Gruppen / KO‑Runde) - async setModus(userToken, clubId, tournamentId, type, numberOfGroups) { + async setModus(userToken, clubId, tournamentId, type, numberOfGroups, advancingPerGroup) { await checkAccess(userToken, clubId); const tournament = await Tournament.findByPk(tournamentId); if (!tournament || tournament.clubId != clubId) { throw new Error('Turnier nicht gefunden'); } - await tournament.update({ type, numberOfGroups }); + await tournament.update({ type, numberOfGroups, advancingPerGroup }); } // 6. Leere Gruppen anlegen @@ -106,53 +125,95 @@ class TournamentService { } // 7. Gruppen zufällig füllen & Spiele anlegen + generateRoundRobinSchedule(players) { + const list = [...players]; + const n = list.length; + const hasBye = n % 2 === 1; + if (hasBye) list.push(null); // füge Bye hinzu + const total = list.length; // jetzt gerade Zahl + const rounds = []; + + for (let round = 0; round < total - 1; round++) { + const pairs = []; + for (let i = 0; i < total / 2; i++) { + const p1 = list[i]; + const p2 = list[total - 1 - i]; + if (p1 && p2) { + pairs.push([p1.id, p2.id]); + } + } + rounds.push(pairs); + // Rotation (Fixpunkt list[0]): + list.splice(1, 0, list.pop()); + } + + return rounds; + } + + + // services/tournamentService.js async fillGroups(userToken, clubId, tournamentId) { await checkAccess(userToken, clubId); + const tournament = await Tournament.findByPk(tournamentId); if (!tournament || tournament.clubId != clubId) { throw new Error('Turnier nicht gefunden'); } - const groups = await TournamentGroup.findAll({ where: { tournamentId } }); + + // 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) { - throw new Error('Keine Gruppen vorhanden. Erst erstellen.'); + 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 } }); } + const members = await TournamentMember.findAll({ where: { tournamentId } }); if (!members.length) { throw new Error('Keine Teilnehmer vorhanden.'); } - // alte Matches löschen + + // 2) Alte Matches löschen await TournamentMatch.destroy({ where: { tournamentId } }); - // mische Teilnehmer + // 3) Shuffle + 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]]; } - // 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 }); - } - // lege alle Paarungen in jeder Gruppe an + groups.forEach((g, idx) => { + shuffled + .filter((_, i) => i % groups.length === idx) + .forEach(m => m.update({ groupId: g.id })); + }); + + // 4) Round‑Robin anlegen wie gehabt 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++) { + const rounds = this.generateRoundRobinSchedule(gm); + for (let roundIndex = 0; roundIndex < rounds.length; roundIndex++) { + for (const [p1Id, p2Id] of rounds[roundIndex]) { await TournamentMatch.create({ tournamentId, groupId: g.id, round: 'group', - player1Id: gm[i].id, - player2Id: gm[j].id + player1Id: p1Id, + player2Id: p2Id, + groupRound: roundIndex + 1 }); } } } + + // 5) Teilnehmer mit Gruppen zurückgeben return await TournamentMember.findAll({ where: { tournamentId } }); } - // 8. Nur Gruppen (ohne Teilnehmer) async getGroups(userToken, clubId, tournamentId) { await checkAccess(userToken, clubId); const tournament = await Tournament.findByPk(tournamentId); @@ -163,6 +224,8 @@ class TournamentService { } // 9. Gruppen mit ihren Teilnehmern + // services/tournamentService.js + async getGroupsWithParticipants(userToken, clubId, tournamentId) { await checkAccess(userToken, clubId); const tournament = await Tournament.findByPk(tournamentId); @@ -178,8 +241,11 @@ class TournamentService { }], order: [['id', 'ASC']] }); - return groups.map(g => ({ + + // hier den Index mit aufnehmen: + return groups.map((g, idx) => ({ groupId: g.id, + groupNumber: idx + 1, // jetzt definiert participants: g.tournamentGroupMembers.map(m => ({ id: m.id, name: `${m.member.firstName} ${m.member.lastName}` @@ -187,6 +253,7 @@ class TournamentService { })); } + // 10. Einzelnes Turnier async getTournament(userToken, clubId, tournamentId) { await checkAccess(userToken, clubId); @@ -208,6 +275,8 @@ class TournamentService { { model: TournamentResult, as: 'tournamentResults' } ], order: [ + ['group_id', 'ASC'], + ['group_round', 'ASC'], ['id', 'ASC'], [{ model: TournamentResult, as: 'tournamentResults' }, 'set', 'ASC'] ] @@ -230,71 +299,74 @@ class TournamentService { } } - // 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' }], - order: [[{ model: TournamentResult, as: 'tournamentResults' }, 'set', 'ASC']] + const match = await TournamentMatch.findByPk(matchId, { + include: [{ model: TournamentResult, as: "tournamentResults" }] }); - const match = matches[0]; - if (!match) throw new Error('Match nicht gefunden'); + if (!match) throw new Error("Match nicht gefunden"); + let win = 0, lose = 0; - match.tournamentResults.forEach(r => { + for (const r of match.tournamentResults) { if (r.pointsPlayer1 > r.pointsPlayer2) win++; else lose++; - }); + } match.isFinished = true; match.result = `${win}:${lose}`; await match.save(); + + const allFinished = await TournamentMatch.count({ + where: { tournamentId, round: match.round, isFinished: false } + }) === 0; + + if (allFinished) { + const sameRound = await TournamentMatch.findAll({ + where: { tournamentId, round: match.round } + }); + const winners = sameRound.map(m => { + const [w1, w2] = m.result.split(":").map(n => +n); + return w1 > w2 ? m.player1Id : m.player2Id; + }); + + const nextName = nextRoundName(match.round); + if (nextName) { + for (let i = 0; i < winners.length / 2; i++) { + await TournamentMatch.create({ + tournamentId, + round: nextName, + player1Id: winners[i], + player2Id: winners[winners.length - 1 - i] + }); + } + } + } } - /** - * 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 + async _determineQualifiers(tournamentId, tournament) { const groups = await TournamentGroup.findAll({ where: { tournamentId }, - include: [{ model: TournamentMember, as: 'tournamentGroupMembers' }] + include: [{ model: TournamentMember, as: "tournamentGroupMembers" }] }); - // lade alle Gruppenspiele und Ergebnisse const groupMatches = await TournamentMatch.findAll({ - where: { tournamentId, round: 'group' }, - include: [{ model: TournamentResult, as: 'tournamentResults' }] + where: { tournamentId, round: "group", isFinished: true } }); 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)); + for (const tm of g.tournamentGroupMembers) { + stats[tm.id] = { member: tm, points: 0, setsWon: 0, setsLost: 0 }; + } + for (const m of groupMatches.filter(m => m.groupId === g.id)) { + if (!stats[m.player1Id] || !stats[m.player2Id]) continue; + 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; + else 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; @@ -303,34 +375,206 @@ class TournamentService { 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)); + qualifiers.push(...ranked.slice(0, tournament.advancingPerGroup).map(r => r.member)); + } + return qualifiers; + } + + async startKnockout(userToken, clubId, tournamentId) { + await checkAccess(userToken, clubId); + const t = await Tournament.findByPk(tournamentId); + if (!t || t.clubId != clubId) throw new Error("Tournament not found"); + + if (t.type === "groups") { + const unfinished = await TournamentMatch.count({ + where: { tournamentId, round: "group", isFinished: false } + }); + if (unfinished > 0) { + throw new Error( + "Turnier ist im Gruppenmodus, K.o.-Runde kann erst nach Abschluss aller Gruppenspiele gestartet werden." + ); + } } - // 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}`; - } - }; + const qualifiers = await this._determineQualifiers(tournamentId, t); + if (qualifiers.length < 2) throw new Error("Zu wenige Qualifikanten für K.O.-Runde"); - 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; + await TournamentMatch.destroy({ + where: { tournamentId, round: { [Op.ne]: "group" } } + }); + + const roundSize = qualifiers.length; + const rn = getRoundName(roundSize); + for (let i = 0; i < roundSize / 2; i++) { + await TournamentMatch.create({ + tournamentId, + round: rn, + player1Id: qualifiers[i].id, + player2Id: qualifiers[roundSize - 1 - i].id + }); } } + async manualAssignGroups( + userToken, + clubId, + tournamentId, + assignments, + numberOfGroups, + maxGroupSize + ) { + await checkAccess(userToken, clubId); + + // 1) Turnier und Teilnehmerzahl validieren + const tournament = await Tournament.findByPk(tournamentId); + if (!tournament || tournament.clubId != clubId) { + throw new Error('Turnier nicht gefunden'); + } + const totalMembers = assignments.length; + if (totalMembers === 0) { + throw new Error('Keine Teilnehmer zum Verteilen'); + } + + // 2) Bestimme, wie viele Gruppen wir anlegen + let groupCount; + if (numberOfGroups != null) { + groupCount = Number(numberOfGroups); + if (isNaN(groupCount) || groupCount < 1) { + throw new Error('Ungültige Anzahl Gruppen'); + } + } else if (maxGroupSize != null) { + const sz = Number(maxGroupSize); + if (isNaN(sz) || sz < 1) { + throw new Error('Ungültige maximale Gruppengröße'); + } + groupCount = Math.ceil(totalMembers / sz); + } else { + // Fallback auf im Turnier gespeicherte Anzahl + groupCount = tournament.numberOfGroups; + if (!groupCount || groupCount < 1) { + throw new Error('Anzahl Gruppen nicht definiert'); + } + } + + // 3) Alte Gruppen löschen und neue anlegen + await TournamentGroup.destroy({ where: { tournamentId } }); + const createdGroups = []; + for (let i = 0; i < groupCount; i++) { + const grp = await TournamentGroup.create({ tournamentId }); + createdGroups.push(grp); + } + + // 4) Mapping von UI‑Nummer (1…groupCount) auf reale DB‑ID + const groupMap = {}; + createdGroups.forEach((grp, idx) => { + groupMap[idx + 1] = grp.id; + }); + + // 5) Teilnehmer updaten + await Promise.all( + assignments.map(({ participantId, groupNumber }) => { + const dbGroupId = groupMap[groupNumber]; + if (!dbGroupId) { + throw new Error(`Ungültige Gruppen‑Nummer: ${groupNumber}`); + } + return TournamentMember.update( + { groupId: dbGroupId }, + { where: { id: participantId } } + ); + }) + ); + + // 6) Ergebnis zurückliefern wie getGroupsWithParticipants + const groups = await TournamentGroup.findAll({ + where: { tournamentId }, + include: [{ + model: TournamentMember, + as: 'tournamentGroupMembers', + include: [{ model: Member, as: 'member', attributes: ['id', 'firstName', 'lastName'] }] + }], + order: [['id', 'ASC']] + }); + + return groups.map(g => ({ + groupId: g.id, + participants: g.tournamentGroupMembers.map(m => ({ + id: m.id, + name: `${m.member.firstName} ${m.member.lastName}` + })) + })); + } + + // services/tournamentService.js + async resetGroups(userToken, clubId, tournamentId) { + await checkAccess(userToken, clubId); + // löscht alle Gruppen … (inkl. CASCADE oder manuell TournamentMatch.destroy) + await TournamentMatch.destroy({ where: { tournamentId } }); + await TournamentGroup.destroy({ where: { tournamentId } }); + } + async resetMatches(userToken, clubId, tournamentId) { + await checkAccess(userToken, clubId); + await TournamentMatch.destroy({ where: { tournamentId } }); + } + + async removeParticipant(userToken, clubId, tournamentId, participantId) { + await checkAccess(userToken, clubId); + await TournamentMember.destroy({ + where: { id: participantId, tournamentId } + }); + } + + // services/tournamentService.js + async deleteMatchResult(userToken, clubId, tournamentId, matchId, setToDelete) { + await checkAccess(userToken, clubId); + + // Match existiert? + const match = await TournamentMatch.findOne({ where: { id: matchId, tournamentId } }); + if (!match) throw new Error('Match nicht gefunden'); + + // Satz löschen + await TournamentResult.destroy({ where: { matchId, set: setToDelete } }); + + // verbleibende Sätze neu durchnummerieren + const remaining = await TournamentResult.findAll({ + where: { matchId }, + order: [['set', 'ASC']] + }); + for (let i = 0; i < remaining.length; i++) { + const r = remaining[i]; + const newSet = i + 1; + if (r.set !== newSet) { + r.set = newSet; + await r.save(); + } + } + } + + async reopenMatch(userToken, clubId, tournamentId, matchId) { + await checkAccess(userToken, clubId); + + const match = await TournamentMatch.findOne({ + where: { id: matchId, tournamentId } + }); + if (!match) { + throw new Error("Match nicht gefunden"); + } + + // Nur den Abschluss‑Status zurücksetzen, nicht die Einzelsätze + match.isFinished = false; + match.result = null; // optional: entfernt die zusammengefasste Ergebnis‑Spalte + await match.save(); + } + + async resetKnockout(userToken, clubId, tournamentId) { + await checkAccess(userToken, clubId); + // lösche alle Matches außer Gruppenphase + await TournamentMatch.destroy({ + where: { + tournamentId, + round: { [Op.ne]: "group" } + } + }); + } + } export default new TournamentService(); diff --git a/frontend/src/views/TournamentsView.vue b/frontend/src/views/TournamentsView.vue index 3bcf8c6..22088b2 100644 --- a/frontend/src/views/TournamentsView.vue +++ b/frontend/src/views/TournamentsView.vue @@ -1,8 +1,6 @@