some enhancements for tournaments
This commit is contained in:
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user