some enhancements for tournaments

This commit is contained in:
Torsten Schulz
2025-07-15 18:06:07 +02:00
parent f29185dd33
commit 69b4302e23
7 changed files with 646 additions and 492 deletions

View File

@@ -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 });
}
};

View File

@@ -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',

View File

@@ -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',

View File

@@ -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' });

View File

@@ -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;

View File

@@ -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 / KORunde)
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 roundrobinGruppen
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');
}
}

View File

@@ -1,107 +1,127 @@
<template>
<div>
<div class="tournaments-view">
<h2>Turnier</h2>
<div>
<div>
<h3>Datum</h3>
<div>
<select v-model="selectedDate">
<option value="new">Neues Turnier</option>
<option v-for="date in dates" :key="date.id" :value="date.id">
{{ new Date(date.date).toLocaleDateString('de-DE', {
year: 'numeric',
month: '2-digit',
day: '2-digit'
}) }}
</option>
</select>
</div>
<template v-if="selectedDate === 'new'">
<div>
<input type="date" v-model="newDate" />
<button @click="createTournament">Erstellen</button>
</div>
</template>
<template v-else>
<div>
<h3>Turnier</h3>
<div>
<div>
<label>
<input type="checkbox" v-model="isGroupTournament">
Spielen in Gruppen
</label>
</div>
<div>
<h4>Teilnehmer</h4>
<ul>
<li v-for="participant in participants" :key="participant.id">
{{ participant.member.firstName }} {{ participant.member.lastName }}
</li>
</ul>
<select v-model="selectedMember">
<option v-for="member in clubMembers" :key="member.id" :value="member.id">
{{ member.firstName }} {{ member.lastName }}
</option>
</select>
<button type="button" @click="addParticipant">Hinzufügen</button>
</div>
<div v-if="isGroupTournament && participants.length > 1">
<label>
Anzahl Gruppen:
<input type="number" v-model="numberOfGroups">
</label>
<button @click="createGroups">Gruppen erstellen</button>
<button @click="randomizeGroups">Zufällig verteilen</button>
</div>
<div v-if="groups && groups.length > 0">
<h4>Gruppen</h4>
<ul class="groupoverview">
<li v-for="group in groups" :key="group.groupId">
<h4>Gruppe {{ group.groupId }}</h4>
<table>
<thead>
<tr>
<th>Spielername</th>
<th>Bilanz</th>
</tr>
</thead>
<tbody>
<tr v-for="participant in group.participants" :key="participant.id">
<td>{{ participant.name }}</td>
<td></td>
</tr>
</tbody>
</table>
</li>
</ul>
<div>
<table>
<thead>
<tr>
<th>&nbsp;</th>
<th>Begegnung</th>
<th>Sätze</th>
</tr>
</thead>
<tbody>
<tr v-for="match in matches" :key="match.id">
<td>{{ match.groupId ? "Gr " + match.groupId : match.round }}</td>
<td>{{ getPlayerName(match.player1) }} - {{ getPlayerName(match.player2) }}</td>
<td v-for="result in match.tournamentResults">{{ result.pointsPlayer1 }}:{{ result.pointsPlayer2 }}</td>
<td><input size="5" type="text" v-model="match.result" @keyup.enter="saveMatchResult(match, match.tournamentResults.length + 1, match.result)" /></td>
<td v-if="match.isFinished">{{ match.result ?? '0:0' }}</td>
<td v-else><button @click="finishMatch(match)">Abschließen</button></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</template>
<!-- Datumsauswahl / Neues Turnier -->
<div class="tournament-config">
<h3>Datum</h3>
<select v-model="selectedDate">
<option value="new">Neues Turnier</option>
<option v-for="date in dates" :key="date.id" :value="date.id">
{{ new Date(date.date).toLocaleDateString('de-DE', {
year: 'numeric',
month: '2-digit',
day: '2-digit'
}) }}
</option>
</select>
<div v-if="selectedDate === 'new'" class="new-tournament">
<input type="date" v-model="newDate" />
<button @click="createTournament">Erstellen</button>
</div>
</div>
<!-- Konfiguration & Gruppenphase -->
<div v-if="selectedDate !== 'new'" class="tournament-setup">
<label>
<input type="checkbox" v-model="isGroupTournament" />
Spielen in Gruppen
</label>
<section class="participants">
<h4>Teilnehmer</h4>
<ul>
<li v-for="participant in participants" :key="participant.id">
{{ participant.member.firstName }}
{{ participant.member.lastName }}
</li>
</ul>
<select v-model="selectedMember">
<option v-for="member in clubMembers" :key="member.id" :value="member.id">
{{ member.firstName }}
{{ member.lastName }}
</option>
</select>
<button @click="addParticipant">Hinzufügen</button>
</section>
<section v-if="isGroupTournament && participants.length > 1" class="group-controls">
<label>
Anzahl Gruppen:
<input type="number" v-model.number="numberOfGroups" min="1" />
</label>
<button @click="createGroups">Gruppen erstellen</button>
<button @click="randomizeGroups">Zufällig verteilen</button>
</section>
<section v-if="groups.length" class="groups-overview">
<h3>Gruppenübersicht</h3>
<div v-for="group in groups" :key="group.groupId" class="group-table">
<h4>Gruppe {{ group.groupId }}</h4>
<table>
<thead>
<tr>
<th>Platz</th>
<th>Spieler</th>
<th>Punkte</th>
<th>Satz</th>
<th>Diff</th>
</tr>
</thead>
<tbody>
<tr v-for="pl in groupRankings[group.groupId]" :key="pl.id">
<td>{{ pl.position }}.</td>
<td>{{ pl.name }}</td>
<td>{{ pl.points }}</td>
<td>{{ pl.setsWon }}:{{ pl.setsLost }}</td>
<td>
{{ pl.setDiff >= 0 ? '+' + pl.setDiff : pl.setDiff }}
</td>
</tr>
</tbody>
</table>
</div>
</section>
</div>
<!-- K.o.-Runde starten -->
<div v-if="participants.length > 1 && !showKnockout" class="ko-start">
<button @click="startKnockout">
K.o.-Runde starten
</button>
</div>
<!-- K.o.-Runde anzeigen -->
<section v-if="showKnockout" class="ko-round">
<h4>K.-o.-Runde</h4>
<table>
<thead>
<tr>
<th>Runde</th>
<th>Begegnung</th>
<th>Ergebnis</th>
<th>Aktion</th>
</tr>
</thead>
<tbody>
<tr v-for="m in knockoutMatches" :key="m.id">
<td>{{ m.round }}</td>
<td>
{{ getPlayerName(m.player1) }}
{{ getPlayerName(m.player2) }}
</td>
<td>{{ m.result || '-' }}</td>
<td v-if="!m.isFinished">
<input v-model="m.resultInput" placeholder="z.B. 11:4, 4:11, 4, -4"
@keyup.enter="saveMatchResult(m, m.resultInput)" />
<button @click="finishMatch(m)">
Fertig
</button>
</td>
</tr>
</tbody>
</table>
</section>
</div>
</template>
@@ -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) TurnierMetadaten 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älligVerteilen:\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();
}
}
};
</script>
<style scoped>
.tournaments {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
.tournaments-view {
padding: 1rem;
}
.groupoverview {
display: flex;
flex-direction: row;
align-items: baseline;
justify-content: left;
padding: 0;
.participants,
.group-controls,
.groups-overview,
.ko-round,
.ko-start {
margin-top: 1.5rem;
}
.groupoverview li {
list-style-type: none;
margin: 0;
padding: 0;
.group-table {
margin-bottom: 2rem;
}
table {
width: 100%;
border-collapse: collapse;
}
th,
td {
padding: 0.5em;
border: 1px solid #ccc;
text-align: left;
}
button {
margin-left: 0.5em;
}
</style>