started tournament implementation

This commit is contained in:
Torsten Schulz
2025-02-24 16:21:43 +01:00
parent 9442e3683b
commit df41720b50
14 changed files with 906 additions and 15 deletions

View File

@@ -0,0 +1,133 @@
import tournamentService from "../services/tournamentService.js";
export const getTournaments = async (req, res) => {
const { authcode: token } = req.headers;
const clubId = req.params.clubId;
try {
const tournaments = await tournamentService.getTournaments(token, clubId);
res.status(200).json(tournaments);
} catch (error) {
console.log(error);
res.status(500).json({ error: error.message });
}
};
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);
} catch (error) {
console.log(error);
res.status(500).json({ error: error.message });
}
}
export const addParticipant = async (req, res) => {
const { authcode: token } = req.headers;
const { clubId, tournamentId, participant: participantId } = req.body;
try {
await tournamentService.addParticipant(token, clubId, tournamentId, participantId);
const participants = await tournamentService.getParticipants(token, clubId, tournamentId);
res.status(200).json(participants);
} catch (error) {
console.log(error);
res.status(500).json({ error: error.message });
}
}
export const getParticipants = async (req, res) => {
const { authcode: token } = req.headers;
const { clubId, tournamentId } = req.body;
try {
const participants = await tournamentService.getParticipants(token, clubId, tournamentId);
res.status(200).json(participants);
} catch (error) {
console.log(error);
res.status(500).json({ error: error.message });
}
}
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);
} catch (error) {
console.log(error);
res.status(500).json({ error: error.message });
}
}
export const createGroups = async (req, res) => {
const { authcode: token } = req.headers;
const { clubId, tournamentId } = req.body;
try {
await tournamentService.createGroups(token, clubId, tournamentId);
} catch (error) {
console.log(error);
res.status(500).json({ error: error.message });
}
}
export const fillGroups = async (req, res) => {
const { authcode: token } = req.headers;
const { clubId, tournamentId } = req.body;
try {
const updatedMembers = await tournamentService.fillGroups(token, clubId, tournamentId);
res.status(200).json(updatedMembers);
} catch (error) {
console.log(error);
res.status(500).json({ error: error.message });
}
}
export const getGroups = async (req, res) => {
const { authcode: token } = req.headers;
const { clubId, tournamentId } = req.query;
try {
const groups = await tournamentService.getGroupsWithParticipants(token, clubId, tournamentId);
res.status(200).json(groups);
} catch (error) {
console.log(error);
res.status(500).json({ error: error.message });
}
};
export const getTournament = async (req, res) => {
const { authcode: token } = req.headers;
const { clubId, tournamentId } = req.params;
try {
const tournament = await tournamentService.getTournament(token, clubId, tournamentId);
res.status(200).json(tournament);
} catch (error) {
console.log(error);
res.status(500).json({ error: error.message });
}
}
export const getTournamentMatches = async (req, res) => {
const { authcode: token } = req.headers;
const { clubId, tournamentId } = req.params;
try {
const matches = await tournamentService.getTournamentMatches(token, clubId, tournamentId);
res.status(200).json(matches);
} catch (error) {
console.log(error);
res.status(500).json({ error: error.message });
}
}
export const addMatchResult = async (req, res) => {
const { authcode: token } = req.headers;
const { clubId, tournamentId, matchId, result } = req.body;
try {
await tournamentService.addMatchResult(token, clubId, tournamentId, matchId, result);
res.status(200).json({ message: "Result added successfully" });
} catch (error) {
console.error(error);
res.status(500).json({ error: error.message });
}
};

View File

@@ -0,0 +1,37 @@
import { DataTypes } from 'sequelize';
import sequelize from '../database.js';
const Tournament = sequelize.define('Tournament', {
name: {
type: DataTypes.STRING,
allowNull: false,
},
date: {
type: DataTypes.DATEONLY,
allowNull: false,
},
type: {
type: DataTypes.STRING,
allowNull: false,
},
bestOfEndroundSize: {
type: DataTypes.INTEGER,
allowNull: false,
},
numberOfGroups: {
type: DataTypes.INTEGER,
allowNull: true,
defaultValue: 0,
},
clubId: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 1
}
}, {
underscored: true,
tableName: 'tournament',
timestamps: true,
});
export default Tournament;

View File

@@ -0,0 +1,21 @@
import { DataTypes } from 'sequelize';
import sequelize from '../database.js';
const TournamentGroup = sequelize.define('TournamentGroup', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
allowNull: false
},
tournamentId : {
type: DataTypes.INTEGER,
allowNull: false
},
}, {
underscored: true,
tableName: 'tournament_group',
timestamps: true,
});
export default TournamentGroup;

View File

@@ -0,0 +1,31 @@
import { DataTypes } from 'sequelize';
import sequelize from '../database.js';
const TournamentMatch = sequelize.define('TournamentMatch', {
tournamentId: {
type: DataTypes.INTEGER,
allowNull: false,
},
groupId: {
type: DataTypes.INTEGER,
allowNull: true,
},
round: {
type: DataTypes.STRING,
allowNull: false,
},
player1Id: {
type: DataTypes.INTEGER,
allowNull: false,
},
player2Id: {
type: DataTypes.INTEGER,
allowNull: false,
},
}, {
underscored: true,
tableName: 'tournament_match',
timestamps: true,
});
export default TournamentMatch;

View File

@@ -0,0 +1,26 @@
import { DataTypes } from 'sequelize';
import sequelize from '../database.js';
const TournamentMember = sequelize.define('TournamentMember', {
tournamentId: {
type: DataTypes.INTEGER,
autoIncrement: false,
allowNull: true
},
groupId : {
type: DataTypes.INTEGER,
autoIncrement: false,
allowNull: true
},
clubMemberId: {
type: DataTypes.INTEGER,
autoIncrement: false,
allowNull: false
}
}, {
underscored: true,
tableName: 'tournament_member',
timestamps: true,
});
export default TournamentMember;

View File

@@ -0,0 +1,24 @@
import { DataTypes } from 'sequelize';
import sequelize from '../database.js';
const TournamentResult = sequelize.define('TournamentResult', {
matchId: {
type: DataTypes.INTEGER,
allowNull: false,
},
set: {
type: DataTypes.INTEGER,
},
pointsPlayer1: {
type: DataTypes.INTEGER,
},
pointsPlayer2: {
type: DataTypes.INTEGER,
},
}, {
underscored: true,
tableName: 'tournament_result',
timestamps: true,
});
export default TournamentResult;

View File

@@ -21,6 +21,11 @@ import Season from './Season.js';
import Location from './Location.js';
import Group from './Group.js';
import GroupActivity from './GroupActivity.js';
import Tournament from './Tournament.js';
import TournamentGroup from './TournamentGroup.js';
import TournamentMember from './TournamentMember.js';
import TournamentMatch from './TournamentMatch.js';
import TournamentResult from './TournamentResult.js';
User.hasMany(Log, { foreignKey: 'userId' });
Log.belongsTo(User, { foreignKey: 'userId' });
@@ -114,6 +119,43 @@ DiaryDateTag.belongsTo(DiaryTag, { foreignKey: 'tagId', as: 'tag' });
DiaryMemberTag.belongsTo(DiaryDate, { foreignKey: 'diaryDateId', as: 'diaryDates' });
DiaryDate.hasMany(DiaryMemberTag, { foreignKey: 'diaryDateId', as: 'diaryMemberTags' });
Tournament.belongsTo(Club, { foreignKey: 'clubId', as: 'tournamentclub' });
Club.hasMany(Tournament, { foreignKey: 'clubId', as: 'tournaments' });
TournamentGroup.belongsTo(Tournament, { foreignKey: 'tournamentId', as: 'tournaments' });
Tournament.hasMany(TournamentGroup, { foreignKey: 'tournamentId', as: 'tournamentGroups' });
TournamentMember.belongsTo(TournamentGroup, {
foreignKey: 'groupId',
targetKey: 'id',
as: 'group',
constraints: false
});
TournamentGroup.hasMany(TournamentMember, {
foreignKey: 'groupId',
as: 'tournamentGroupMembers'
});
TournamentMember.belongsTo(Member, { foreignKey: 'clubMemberId', as: 'member' });
Member.hasMany(TournamentMember, { foreignKey: 'clubMemberId', as: 'tournamentGroupMembers' });
TournamentMember.belongsTo(Tournament, { foreignKey: 'tournamentId', as: 'tournament' });
Tournament.hasMany(TournamentMember, { foreignKey: 'tournamentId', as: 'tournamentMembers' });
TournamentMatch.belongsTo(Tournament, { foreignKey: 'tournamentId', as: 'tournament' });
Tournament.hasMany(TournamentMatch, { foreignKey: 'tournamentId', as: 'tournamentMatches' });
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.belongsTo(TournamentMember, { foreignKey: 'player1Id', as: 'player1' });
TournamentMatch.belongsTo(TournamentMember, { foreignKey: 'player2Id', as: 'player2' });
TournamentMember.hasMany(TournamentMatch, { foreignKey: 'player1Id', as: 'player1Matches' });
TournamentMember.hasMany(TournamentMatch, { foreignKey: 'player2Id', as: 'player2Matches' });
export {
User,
Log,
@@ -137,4 +179,9 @@ export {
Team,
Group,
GroupActivity,
Tournament,
TournamentGroup,
TournamentMember,
TournamentMatch,
TournamentResult,
};

View File

@@ -0,0 +1,31 @@
import express from 'express';
import {
getTournaments,
addTournament,
addParticipant,
getParticipants,
setModus,
createGroups,
fillGroups,
getGroups,
getTournament,
getTournamentMatches,
addMatchResult,
} from '../controllers/tournamentController.js';
import { authenticate } from '../middleware/authMiddleware.js';
const router = express.Router();
router.post('/participant', authenticate, addParticipant);
router.post('/participants', authenticate, getParticipants);
router.post('/modus', authenticate, setModus);
router.put('/groups', authenticate, createGroups);
router.post('/groups', authenticate, fillGroups);
router.get('/groups', authenticate, getGroups);
router.post('/match/result', authenticate, addMatchResult);
router.get('/matches/:clubId/:tournamentId', authenticate, getTournamentMatches);
router.get('/:clubId/:tournamentId', authenticate, getTournament);
router.get('/:clubId', authenticate, getTournaments);
router.post('/', authenticate, addTournament);
export default router;

View File

@@ -7,7 +7,12 @@ import {
User, Log, Club, UserClub, Member, DiaryDate, Participant, Activity, MemberNote,
DiaryNote, DiaryTag, MemberDiaryTag, DiaryDateTag, DiaryMemberNote, DiaryMemberTag,
PredefinedActivity, DiaryDateActivity, Match, League, Team, Group,
GroupActivity
GroupActivity,
Tournament,
TournamentGroup,
TournamentMatch,
TournamentResult,
TournamentMember
} from './models/index.js';
import authRoutes from './routes/authRoutes.js';
import clubRoutes from './routes/clubRoutes.js';
@@ -27,6 +32,7 @@ import Location from './models/Location.js';
import groupRoutes from './routes/groupRoutes.js';
import diaryDateTagRoutes from './routes/diaryDateTagRoutes.js';
import sessionRoutes from './routes/sessionRoutes.js';
import tournamentRoutes from './routes/tournamentRoutes.js';
const app = express();
const port = process.env.PORT || 3000;
@@ -53,6 +59,7 @@ app.use('/api/matches', matchRoutes);
app.use('/api/group', groupRoutes);
app.use('/api/diarydatetags', diaryDateTagRoutes);
app.use('/api/session', sessionRoutes);
app.use('/api/tournament', tournamentRoutes);
app.use(express.static(path.join(__dirname, '../frontend/dist')));
@@ -88,6 +95,11 @@ app.get('*', (req, res) => {
await Match.sync({ alter: true });
await Group.sync({ alter: true });
await GroupActivity.sync({ alter: true });
await Tournament.sync({ alter: true });
await TournamentGroup.sync({ alter: true });
await TournamentMember.sync({ alter: true });
await TournamentMatch.sync({ alter: true });
await TournamentResult.sync({ alter: true });
app.listen(port, () => {
console.log(`Server is running on http://localhost:${port}`);

View File

@@ -0,0 +1,249 @@
import Club from "../models/Club.js";
import Member from "../models/Member.js";
import Tournament from "../models/Tournament.js";
import TournamentGroup from "../models/TournamentGroup.js";
import TournamentMatch from "../models/TournamentMatch.js";
import TournamentMember from "../models/TournamentMember.js";
import { checkAccess } from '../utils/userUtils.js';
class TournamentService {
async getTournaments(userToken, clubId) {
await checkAccess(userToken, clubId);
const tournaments = await Tournament.findAll(
{
where: { clubId },
order: [['date', 'DESC']],
attributes: ['id', 'name', 'date']
}
);
return JSON.parse(JSON.stringify(tournaments));
}
async addTournament(userToken, clubId, tournamentName, date) {
await checkAccess(userToken, clubId);
const club = await Club.findByPk(clubId);
await Tournament.create({
name: tournamentName,
date: date,
clubId: club.id,
bestOfEndroundSize: 0,
type: '',
name: '',
});
return await this.getTournaments(userToken, clubId);
}
async addParticipant(token, clubId, tournamentId, participantId) {
await checkAccess(token, clubId);
const tournament = await Tournament.findByPk(tournamentId);
if (!tournament || tournament.clubId != clubId) {
throw new Error('Tournament not found');
}
const participant = TournamentMember.findAll({
where: { tournamentId: tournamentId, groupId: participantId, clubMemberId: participantId },
});
if (participant) {
throw new Error('Participant already exists');
}
await TournamentMember.create({
tournamentId: tournamentId,
groupId: participantId,
clubMemberId: participantId,
});
}
async getParticipants(token, clubId, tournamentId) {
await checkAccess(token, clubId);
const tournament = await Tournament.findByPk(tournamentId);
if (!tournament || tournament.clubId != clubId) {
throw new Error('Tournament not found');
}
return await TournamentMember.findAll({
where: {
tournamentId: tournamentId,
},
include: [
{
model: Member,
as: 'member',
attributes: ['id', 'lastName', 'firstName'],
order: [['firstName', 'ASC'], ['lastName', 'ASC']],
}
]
});
}
async setModus(token, clubId, tournamentId, type, numberOfGroups) {
await checkAccess(token, clubId);
const tournament = await Tournament.findByPk(tournamentId);
if (!tournament || tournament.clubId != clubId) {
throw new Error('Tournament not found');
}
await tournament.update({ type, numberOfGroups });
}
async createGroups(token, clubId, tournamentId) {
await checkAccess(token, clubId);
const tournament = await Tournament.findByPk(tournamentId);
if (!tournament || tournament.clubId != clubId) {
throw new Error('Tournament not found');
}
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();
}
}
}
async fillGroups(token, clubId, tournamentId) {
await checkAccess(token, clubId);
const tournament = await Tournament.findByPk(tournamentId);
if (!tournament || tournament.clubId != clubId) {
throw new Error('Tournament not found');
}
const groups = await TournamentGroup.findAll({ where: { tournamentId } });
if (!groups || groups.length === 0) {
throw new Error('No groups available. Please create groups first.');
}
const members = await TournamentMember.findAll({ where: { tournamentId } });
if (!members || members.length === 0) {
throw new Error('No tournament members found.');
}
await TournamentMatch.destroy({ where: { tournamentId } });
const shuffledMembers = [...members];
for (let i = shuffledMembers.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[shuffledMembers[i], shuffledMembers[j]] = [shuffledMembers[j], shuffledMembers[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 });
}
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++) {
await TournamentMatch.create({
tournamentId: tournamentId,
groupId: group.id,
round: 'group',
player1Id: groupMembers[i].id,
player2Id: groupMembers[j].id,
});
}
}
}
return await TournamentMember.findAll({ where: { tournamentId } });
}
async getGroups(token, clubId, tournamentId) {
await checkAccess(token, clubId);
const tournament = await Tournament.findByPk(tournamentId);
if (!tournament || tournament.clubId != clubId) {
throw new Error('Tournament not found');
}
const groups = await TournamentGroup.findAll({ where: { tournamentId } });
return groups;
}
async getGroupsWithParticipants(token, clubId, tournamentId) {
await checkAccess(token, clubId);
const tournament = await Tournament.findByPk(tournamentId);
if (!tournament || tournament.clubId != clubId) {
throw new Error('Tournament not found');
}
const groups = await TournamentGroup.findAll({
where: { tournamentId },
include: [
{
model: TournamentMember,
as: 'tournamentGroupMembers',
required: false,
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}`
}))
}));
}
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;
}
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({
where: { tournamentId },
include: [
{
model: TournamentMember,
as: 'player1',
include: {
model: Member,
as: 'member',
}
},
{
model: TournamentMember,
as: 'player2',
include: {
model: Member,
as: 'member',
}
}
],
order: [['id', 'ASC']]
});
return matches;
}
async addMatchResult(token, clubId, tournamentId, matchId, result) {
await checkAccess(token, clubId);
const match = await TournamentMatch.findByPk(matchId);
if (!match || match.tournamentId != tournamentId) {
throw new Error("Match not found or doesn't belong to this tournament");
}
match.result = result;
await match.save();
}
}
export default new TournamentService();