finished tournaments
This commit is contained in:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user