feat(tournament): implement multi-stage tournament support with intermediate and final stages

- Added backend controller for tournament stages with endpoints to get, upsert, and advance stages.
- Created database migration for new tables: tournament_stage and tournament_stage_advancement.
- Updated models for TournamentStage and TournamentStageAdvancement.
- Enhanced frontend components to manage tournament stages, including configuration for intermediate and final rounds.
- Implemented logic for saving and advancing tournament stages, including handling of pool rules and third place matches.
- Added error handling and loading states in the frontend for better user experience.
This commit is contained in:
Torsten Schulz (local)
2025-12-14 06:46:00 +01:00
parent e83bc250a8
commit 945ec0d48c
23 changed files with 1688 additions and 50 deletions

View File

@@ -8,6 +8,10 @@ const TournamentGroup = sequelize.define('TournamentGroup', {
autoIncrement: true,
allowNull: false
},
stageId: {
type: DataTypes.INTEGER,
allowNull: true,
},
tournamentId : {
type: DataTypes.INTEGER,
allowNull: false

View File

@@ -5,6 +5,10 @@ import Tournament from './Tournament.js';
import TournamentGroup from './TournamentGroup.js';
const TournamentMatch = sequelize.define('TournamentMatch', {
stageId: {
type: DataTypes.INTEGER,
allowNull: true,
},
tournamentId: {
type: DataTypes.INTEGER,
allowNull: false,
@@ -39,11 +43,11 @@ const TournamentMatch = sequelize.define('TournamentMatch', {
},
player1Id: {
type: DataTypes.INTEGER,
allowNull: false,
allowNull: true,
},
player2Id: {
type: DataTypes.INTEGER,
allowNull: false,
allowNull: true,
},
isFinished: {
type: DataTypes.BOOLEAN,

View File

@@ -0,0 +1,46 @@
import { DataTypes } from 'sequelize';
import sequelize from '../database.js';
const TournamentStage = sequelize.define('TournamentStage', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
allowNull: false,
},
tournamentId: {
type: DataTypes.INTEGER,
allowNull: false,
},
index: {
type: DataTypes.INTEGER,
allowNull: false,
field: 'stage_index',
},
name: {
type: DataTypes.STRING,
allowNull: true,
},
type: {
type: DataTypes.STRING,
allowNull: false, // 'groups' | 'knockout'
},
numberOfGroups: {
type: DataTypes.INTEGER,
allowNull: true,
},
advancingPerGroup: {
type: DataTypes.INTEGER,
allowNull: true,
},
maxGroupSize: {
type: DataTypes.INTEGER,
allowNull: true,
},
}, {
underscored: true,
tableName: 'tournament_stage',
timestamps: true,
});
export default TournamentStage;

View File

@@ -0,0 +1,40 @@
import { DataTypes } from 'sequelize';
import sequelize from '../database.js';
const TournamentStageAdvancement = sequelize.define('TournamentStageAdvancement', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
allowNull: false,
},
tournamentId: {
type: DataTypes.INTEGER,
allowNull: false,
},
fromStageId: {
type: DataTypes.INTEGER,
allowNull: false,
},
toStageId: {
type: DataTypes.INTEGER,
allowNull: false,
},
mode: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: 'pools',
},
config: {
// JSON: { pools: [{ fromPlaces:[1,2], target:{ type:'groups', groupCount:2 }}, ...] }
type: DataTypes.JSON,
allowNull: false,
defaultValue: {},
},
}, {
underscored: true,
tableName: 'tournament_stage_advancement',
timestamps: true,
});
export default TournamentStageAdvancement;

View File

@@ -33,6 +33,8 @@ import TournamentMatch from './TournamentMatch.js';
import TournamentResult from './TournamentResult.js';
import ExternalTournamentParticipant from './ExternalTournamentParticipant.js';
import TournamentPairing from './TournamentPairing.js';
import TournamentStage from './TournamentStage.js';
import TournamentStageAdvancement from './TournamentStageAdvancement.js';
import Accident from './Accident.js';
import UserToken from './UserToken.js';
import OfficialTournament from './OfficialTournament.js';
@@ -192,6 +194,13 @@ Club.hasMany(Tournament, { foreignKey: 'clubId', as: 'tournaments' });
TournamentGroup.belongsTo(Tournament, { foreignKey: 'tournamentId', as: 'tournaments' });
Tournament.hasMany(TournamentGroup, { foreignKey: 'tournamentId', as: 'tournamentGroups' });
// Tournament Stages
TournamentStage.belongsTo(Tournament, { foreignKey: 'tournamentId', as: 'tournament' });
Tournament.hasMany(TournamentStage, { foreignKey: 'tournamentId', as: 'stages' });
TournamentStageAdvancement.belongsTo(Tournament, { foreignKey: 'tournamentId', as: 'tournament' });
Tournament.hasMany(TournamentStageAdvancement, { foreignKey: 'tournamentId', as: 'stageAdvancements' });
TournamentMember.belongsTo(TournamentGroup, {
foreignKey: 'groupId',
targetKey: 'id',