Compare commits
18 Commits
accident
...
tournament
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f5deb343a8 | ||
|
|
4122868ab0 | ||
|
|
eba160c43d | ||
|
|
39089a70d3 | ||
|
|
d0544da1ba | ||
|
|
b6dd39dda3 | ||
|
|
f3a4159536 | ||
|
|
69b4302e23 | ||
|
|
68725af630 | ||
|
|
f753d45e17 | ||
|
|
549147cfb3 | ||
|
|
81cf94cebc | ||
|
|
9f17f2399a | ||
|
|
9ba39f9f47 | ||
|
|
f935c72f56 | ||
|
|
f29185dd33 | ||
|
|
821f9d24f5 | ||
|
|
df41720b50 |
280
backend/controllers/tournamentController.js
Normal file
280
backend/controllers/tournamentController.js
Normal file
@@ -0,0 +1,280 @@
|
||||
// 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;
|
||||
try {
|
||||
const tournaments = await tournamentService.getTournaments(token, clubId);
|
||||
res.status(200).json(tournaments);
|
||||
} catch (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(201).json(tournament);
|
||||
} catch (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;
|
||||
try {
|
||||
await tournamentService.addParticipant(token, clubId, tournamentId, participantId);
|
||||
const participants = await tournamentService.getParticipants(token, clubId, tournamentId);
|
||||
res.status(200).json(participants);
|
||||
} catch (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;
|
||||
try {
|
||||
const participants = await tournamentService.getParticipants(token, clubId, tournamentId);
|
||||
res.status(200).json(participants);
|
||||
} catch (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, advancingPerGroup } = req.body;
|
||||
try {
|
||||
await tournamentService.setModus(token, clubId, tournamentId, type, numberOfGroups, advancingPerGroup);
|
||||
res.sendStatus(204);
|
||||
} catch (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.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;
|
||||
try {
|
||||
const updatedMembers = await tournamentService.fillGroups(token, clubId, tournamentId);
|
||||
res.status(200).json(updatedMembers);
|
||||
} catch (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;
|
||||
try {
|
||||
const groups = await tournamentService.getGroupsWithParticipants(token, clubId, tournamentId);
|
||||
res.status(200).json(groups);
|
||||
} catch (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;
|
||||
try {
|
||||
const tournament = await tournamentService.getTournament(token, clubId, tournamentId);
|
||||
res.status(200).json(tournament);
|
||||
} catch (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;
|
||||
try {
|
||||
const matches = await tournamentService.getTournamentMatches(token, clubId, tournamentId);
|
||||
res.status(200).json(matches);
|
||||
} catch (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" });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
// 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) {
|
||||
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) {
|
||||
const status = /Gruppenmodus|Zu wenige Qualifikanten/.test(error.message) ? 400 : 500;
|
||||
res.status(status).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
export const manualAssignGroups = async (req, res) => {
|
||||
const { authcode: token } = req.headers;
|
||||
const {
|
||||
clubId,
|
||||
tournamentId,
|
||||
assignments, // [{ participantId, groupNumber }]
|
||||
numberOfGroups, // optional
|
||||
maxGroupSize // optional
|
||||
} = req.body;
|
||||
|
||||
try {
|
||||
const groupsWithParts = await tournamentService.manualAssignGroups(
|
||||
token,
|
||||
clubId,
|
||||
tournamentId,
|
||||
assignments,
|
||||
numberOfGroups, // neu
|
||||
maxGroupSize // neu
|
||||
);
|
||||
res.status(200).json(groupsWithParts);
|
||||
} catch (error) {
|
||||
console.error('Error in manualAssignGroups:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
export const resetGroups = async (req, res) => {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubId, tournamentId } = req.body;
|
||||
try {
|
||||
await tournamentService.resetGroups(token, clubId, tournamentId);
|
||||
res.sendStatus(204);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
};
|
||||
|
||||
export const resetMatches = async (req, res) => {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubId, tournamentId } = req.body;
|
||||
try {
|
||||
await tournamentService.resetMatches(token, clubId, tournamentId);
|
||||
res.sendStatus(204);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
};
|
||||
|
||||
export const removeParticipant = async (req, res) => {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubId, tournamentId, participantId } = req.body;
|
||||
try {
|
||||
await tournamentService.removeParticipant(token, clubId, tournamentId, participantId);
|
||||
const participants = await tournamentService.getParticipants(token, clubId, tournamentId);
|
||||
res.status(200).json(participants);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteMatchResult = async (req, res) => {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubId, tournamentId, matchId, set } = req.body;
|
||||
try {
|
||||
await tournamentService.deleteMatchResult(
|
||||
token,
|
||||
clubId,
|
||||
tournamentId,
|
||||
matchId,
|
||||
set
|
||||
);
|
||||
res.status(200).json({ message: 'Einzelsatz gelöscht' });
|
||||
} catch (error) {
|
||||
console.error('Error in deleteMatchResult:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
export const reopenMatch = async (req, res) => {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubId, tournamentId, matchId } = req.body;
|
||||
try {
|
||||
await tournamentService.reopenMatch(token, clubId, tournamentId, matchId);
|
||||
// Gib optional das aktualisierte Match zurück
|
||||
res.status(200).json({ message: "Match reopened" });
|
||||
} catch (error) {
|
||||
console.error("Error in reopenMatch:", error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteKnockoutMatches = async (req, res) => {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubId, tournamentId } = req.body;
|
||||
try {
|
||||
await tournamentService.resetKnockout(token, clubId, tournamentId);
|
||||
res.status(200).json({ message: "K.o.-Runde gelöscht" });
|
||||
} catch (error) {
|
||||
console.error("Error in deleteKnockoutMatches:", error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
38
backend/models/Tournament.js
Normal file
38
backend/models/Tournament.js
Normal file
@@ -0,0 +1,38 @@
|
||||
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,
|
||||
},
|
||||
advancingPerGroup: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
},
|
||||
numberOfGroups: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
defaultValue: 0,
|
||||
},
|
||||
clubId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 1
|
||||
},
|
||||
advancingPerGroup: { type: DataTypes.INTEGER, allowNull: false, defaultValue: 1 },
|
||||
}, {
|
||||
underscored: true,
|
||||
tableName: 'tournament',
|
||||
timestamps: true,
|
||||
});
|
||||
|
||||
export default Tournament;
|
||||
21
backend/models/TournamentGroup.js
Normal file
21
backend/models/TournamentGroup.js
Normal 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;
|
||||
59
backend/models/TournamentMatch.js
Normal file
59
backend/models/TournamentMatch.js
Normal file
@@ -0,0 +1,59 @@
|
||||
// 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,
|
||||
references: {
|
||||
model: Tournament,
|
||||
key: 'id'
|
||||
},
|
||||
onDelete: 'CASCADE',
|
||||
onUpdate: 'CASCADE'
|
||||
},
|
||||
groupId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
references: {
|
||||
model: TournamentGroup,
|
||||
key: 'id'
|
||||
},
|
||||
onDelete: 'SET NULL',
|
||||
onUpdate: 'CASCADE'
|
||||
},
|
||||
groupRound: {
|
||||
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,
|
||||
},
|
||||
}, {
|
||||
underscored: true,
|
||||
tableName: 'tournament_match',
|
||||
timestamps: true,
|
||||
});
|
||||
|
||||
export default TournamentMatch;
|
||||
26
backend/models/TournamentMember.js
Normal file
26
backend/models/TournamentMember.js
Normal 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;
|
||||
24
backend/models/TournamentResult.js
Normal file
24
backend/models/TournamentResult.js
Normal 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;
|
||||
@@ -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';
|
||||
import Accident from './Accident.js';
|
||||
|
||||
User.hasMany(Log, { foreignKey: 'userId' });
|
||||
@@ -115,6 +120,54 @@ 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' });
|
||||
|
||||
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' });
|
||||
TournamentMember.hasMany(TournamentMatch, { foreignKey: 'player1Id', as: 'player1Matches' });
|
||||
TournamentMember.hasMany(TournamentMatch, { foreignKey: 'player2Id', as: 'player2Matches' });
|
||||
|
||||
Accident.belongsTo(Member, { foreignKey: 'memberId', as: 'members' });
|
||||
Member.hasMany(Accident, { foreignKey: 'memberId', as: 'accidents' });
|
||||
|
||||
@@ -144,5 +197,10 @@ export {
|
||||
Team,
|
||||
Group,
|
||||
GroupActivity,
|
||||
Tournament,
|
||||
TournamentGroup,
|
||||
TournamentMember,
|
||||
TournamentMatch,
|
||||
TournamentResult,
|
||||
Accident,
|
||||
};
|
||||
|
||||
49
backend/routes/tournamentRoutes.js
Normal file
49
backend/routes/tournamentRoutes.js
Normal file
@@ -0,0 +1,49 @@
|
||||
import express from 'express';
|
||||
import {
|
||||
getTournaments,
|
||||
addTournament,
|
||||
addParticipant,
|
||||
getParticipants,
|
||||
setModus,
|
||||
createGroups,
|
||||
fillGroups,
|
||||
getGroups,
|
||||
getTournament,
|
||||
getTournamentMatches,
|
||||
addMatchResult,
|
||||
finishMatch,
|
||||
startKnockout,
|
||||
manualAssignGroups,
|
||||
resetGroups,
|
||||
resetMatches,
|
||||
removeParticipant,
|
||||
deleteMatchResult,
|
||||
reopenMatch,
|
||||
deleteKnockoutMatches,
|
||||
} 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.delete('/participant', authenticate, removeParticipant);
|
||||
router.post('/modus', authenticate, setModus);
|
||||
router.post('/groups/reset', authenticate, resetGroups);
|
||||
router.post('/matches/reset', authenticate, resetMatches);
|
||||
router.put('/groups', authenticate, createGroups);
|
||||
router.post('/groups', authenticate, fillGroups);
|
||||
router.get('/groups', authenticate, getGroups);
|
||||
router.post('/match/result', authenticate, addMatchResult);
|
||||
router.delete('/match/result', authenticate, deleteMatchResult);
|
||||
router.post("/match/reopen", reopenMatch);
|
||||
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.delete("/matches/knockout", deleteKnockoutMatches);
|
||||
router.post('/groups/manual', authenticate, manualAssignGroups);
|
||||
router.post('/', authenticate, addTournament);
|
||||
|
||||
export default router;
|
||||
@@ -7,8 +7,8 @@ import {
|
||||
User, Log, Club, UserClub, Member, DiaryDate, Participant, Activity, MemberNote,
|
||||
DiaryNote, DiaryTag, MemberDiaryTag, DiaryDateTag, DiaryMemberNote, DiaryMemberTag,
|
||||
PredefinedActivity, DiaryDateActivity, Match, League, Team, Group,
|
||||
GroupActivity,
|
||||
Accident
|
||||
GroupActivity, Tournament, TournamentGroup, TournamentMatch, TournamentResult,
|
||||
TournamentMember, Accident
|
||||
} from './models/index.js';
|
||||
import authRoutes from './routes/authRoutes.js';
|
||||
import clubRoutes from './routes/clubRoutes.js';
|
||||
@@ -28,6 +28,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';
|
||||
import accidentRoutes from './routes/accidentRoutes.js';
|
||||
|
||||
const app = express();
|
||||
@@ -55,6 +56,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('/api/accident', accidentRoutes);
|
||||
|
||||
app.use(express.static(path.join(__dirname, '../frontend/dist')));
|
||||
@@ -91,6 +93,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 });
|
||||
await Accident.sync({ alter: true });
|
||||
|
||||
app.listen(port, () => {
|
||||
|
||||
@@ -8,8 +8,7 @@ class AccidentService {
|
||||
await checkAccess(userToken, clubId);
|
||||
const user = await getUserByToken(userToken);
|
||||
if (!user) {
|
||||
console.log('---------------');
|
||||
throw new Error('User not found');
|
||||
throw new Error('User not found');
|
||||
}
|
||||
const member = await Member.findByPk(memberId);
|
||||
if (!member || member.clubId != clubId) {
|
||||
|
||||
@@ -31,7 +31,7 @@ class MatchService {
|
||||
const matches = [];
|
||||
try {
|
||||
const fileStream = fs.createReadStream(filePath)
|
||||
.pipe(iconv.decodeStream('ISO-8859-15'))
|
||||
.pipe(iconv.decodeStream('utf8'))
|
||||
.pipe(csv({ separator: ';' }));
|
||||
for await (const row of fileStream) {
|
||||
const parsedDate = parse(row['Termin'], 'dd.MM.yyyy HH:mm', new Date());
|
||||
@@ -67,15 +67,11 @@ class MatchService {
|
||||
clubId: clubId,
|
||||
});
|
||||
}
|
||||
let season = null;
|
||||
if (seasonString) {
|
||||
const season = await Season.findOne({ where: { season: seasonString } });
|
||||
season = await Season.findOne({ where: { season: seasonString } });
|
||||
if (season) {
|
||||
await Match.destroy({
|
||||
where: {
|
||||
clubId: clubId,
|
||||
seasonId: season.id,
|
||||
}
|
||||
});
|
||||
await Match.destroy({ where: { clubId, seasonId: season.id } });
|
||||
}
|
||||
}
|
||||
const result = await Match.bulkCreate(matches);
|
||||
|
||||
580
backend/services/tournamentService.js
Normal file
580
backend/services/tournamentService.js
Normal file
@@ -0,0 +1,580 @@
|
||||
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 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) {
|
||||
await checkAccess(userToken, clubId);
|
||||
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 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,
|
||||
clubId: +clubId,
|
||||
bestOfEndroundSize: 0,
|
||||
type: ''
|
||||
});
|
||||
return JSON.parse(JSON.stringify(t));
|
||||
}
|
||||
|
||||
// 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('Turnier nicht gefunden');
|
||||
}
|
||||
const exists = await TournamentMember.findOne({
|
||||
where: { tournamentId, clubMemberId: participantId }
|
||||
});
|
||||
if (exists) {
|
||||
throw new Error('Teilnehmer bereits hinzugefügt');
|
||||
}
|
||||
await TournamentMember.create({
|
||||
tournamentId,
|
||||
clubMemberId: participantId,
|
||||
groupId: null
|
||||
});
|
||||
}
|
||||
|
||||
// 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('Turnier nicht gefunden');
|
||||
}
|
||||
return await TournamentMember.findAll({
|
||||
where: { tournamentId },
|
||||
include: [{
|
||||
model: Member,
|
||||
as: 'member',
|
||||
attributes: ['id', 'firstName', 'lastName'],
|
||||
}],
|
||||
order: [[{ model: Member, as: 'member' }, 'firstName', 'ASC']]
|
||||
});
|
||||
}
|
||||
|
||||
// 5. Modus setzen (Gruppen / KO‑Runde)
|
||||
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, advancingPerGroup });
|
||||
}
|
||||
|
||||
// 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('Turnier nicht gefunden');
|
||||
}
|
||||
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 });
|
||||
}
|
||||
}
|
||||
|
||||
// 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');
|
||||
}
|
||||
|
||||
// 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) {
|
||||
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.');
|
||||
}
|
||||
|
||||
// 2) Alte Matches löschen
|
||||
await TournamentMatch.destroy({ where: { tournamentId } });
|
||||
|
||||
// 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]];
|
||||
}
|
||||
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 } });
|
||||
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: p1Id,
|
||||
player2Id: p2Id,
|
||||
groupRound: roundIndex + 1
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 5) Teilnehmer mit Gruppen zurückgeben
|
||||
return await TournamentMember.findAll({ where: { tournamentId } });
|
||||
}
|
||||
|
||||
async getGroups(userToken, clubId, tournamentId) {
|
||||
await checkAccess(userToken, clubId);
|
||||
const tournament = await Tournament.findByPk(tournamentId);
|
||||
if (!tournament || tournament.clubId != clubId) {
|
||||
throw new Error('Turnier nicht gefunden');
|
||||
}
|
||||
return await TournamentGroup.findAll({ where: { tournamentId } });
|
||||
}
|
||||
|
||||
// 9. Gruppen mit ihren Teilnehmern
|
||||
// services/tournamentService.js
|
||||
|
||||
async getGroupsWithParticipants(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 },
|
||||
include: [{
|
||||
model: TournamentMember,
|
||||
as: 'tournamentGroupMembers',
|
||||
include: [{ model: Member, as: 'member', attributes: ['id', 'firstName', 'lastName'] }]
|
||||
}],
|
||||
order: [['id', 'ASC']]
|
||||
});
|
||||
|
||||
// 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}`
|
||||
}))
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// 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: [
|
||||
['group_id', 'ASC'],
|
||||
['group_round', 'ASC'],
|
||||
['id', 'ASC'],
|
||||
[{ model: TournamentResult, as: 'tournamentResults' }, 'set', 'ASC']
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
// 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 });
|
||||
}
|
||||
}
|
||||
|
||||
async finishMatch(userToken, clubId, tournamentId, matchId) {
|
||||
await checkAccess(userToken, clubId);
|
||||
const match = await TournamentMatch.findByPk(matchId, {
|
||||
include: [{ model: TournamentResult, as: "tournamentResults" }]
|
||||
});
|
||||
if (!match) throw new Error("Match nicht gefunden");
|
||||
|
||||
let win = 0, lose = 0;
|
||||
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]
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async _determineQualifiers(tournamentId, tournament) {
|
||||
const groups = await TournamentGroup.findAll({
|
||||
where: { tournamentId },
|
||||
include: [{ model: TournamentMember, as: "tournamentGroupMembers" }]
|
||||
});
|
||||
const groupMatches = await TournamentMatch.findAll({
|
||||
where: { tournamentId, round: "group", isFinished: true }
|
||||
});
|
||||
|
||||
const qualifiers = [];
|
||||
for (const g of groups) {
|
||||
const stats = {};
|
||||
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 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;
|
||||
}
|
||||
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;
|
||||
});
|
||||
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."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const qualifiers = await this._determineQualifiers(tournamentId, t);
|
||||
if (qualifiers.length < 2) throw new Error("Zu wenige Qualifikanten für K.O.-Runde");
|
||||
|
||||
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();
|
||||
@@ -22,6 +22,7 @@
|
||||
<a href="/diary">Tagebuch</a>
|
||||
<a href="/pending-approvals">Freigaben</a>
|
||||
<a href="/schedule">Spielpläne</a>
|
||||
<a href="/tournaments">Turniere</a>
|
||||
</div>
|
||||
|
||||
<div class="logout-btn">
|
||||
|
||||
@@ -6,7 +6,7 @@ class PDFGenerator {
|
||||
this.pdf = new jsPDF('p', 'mm', 'a4');
|
||||
this.margin = margin;
|
||||
this.columnGap = columnGap;
|
||||
this.pageHeight = 297 - margin * 2;
|
||||
this.pageHeight = 297 - margin * 2;
|
||||
this.columnWidth = (210 - margin * 2 - columnGap) / 2;
|
||||
this.position = margin;
|
||||
this.yPos = this.position;
|
||||
@@ -20,13 +20,29 @@ class PDFGenerator {
|
||||
}
|
||||
|
||||
async addSchedule(element) {
|
||||
const canvas = await html2canvas(element, { scale: 2 });
|
||||
const canvas = await html2canvas(element, {
|
||||
scale: 2,
|
||||
onclone: (clonedDoc) => {
|
||||
const clonedEl = clonedDoc.getElementById('schedule');
|
||||
if (clonedEl) clonedEl.style.fontSize = '12pt';
|
||||
// Klon des Wurzel-Elements
|
||||
const tbl = clonedDoc.getElementById(element.id);
|
||||
if (!tbl) return;
|
||||
|
||||
// Alle Zellen und Überschriften-Elemente auswählen
|
||||
const cells = tbl.querySelectorAll('td, th');
|
||||
cells.forEach(cell => {
|
||||
cell.style.fontSize = '12pt';
|
||||
});
|
||||
}
|
||||
});
|
||||
const imgData = canvas.toDataURL('image/png');
|
||||
const imgWidth = 210 - this.margin * 2;
|
||||
const imgHeight = (canvas.height * imgWidth) / canvas.width;
|
||||
let heightLeft = imgHeight;
|
||||
let position = this.margin;
|
||||
this.pdf.addImage(imgData, 'PNG', this.margin, position, imgWidth, imgHeight);
|
||||
this.pdf.setFontSize(12);
|
||||
heightLeft -= this.pageHeight;
|
||||
while (heightLeft >= 0) {
|
||||
position = heightLeft - imgHeight + this.margin;
|
||||
@@ -175,6 +191,25 @@ class PDFGenerator {
|
||||
this.pdf.text(phoneNumber, this.margin + 120, this.yPos);
|
||||
this.yPos += this.LINE_HEIGHT;
|
||||
}
|
||||
|
||||
addAddress(clubName, addressLines) {
|
||||
if (!this.addressY) {
|
||||
this.addressY = 30;
|
||||
}
|
||||
|
||||
this.pdf.setFontSize(14);
|
||||
this.pdf.setFont(undefined, 'bold');
|
||||
this.pdf.text(clubName, 20, this.addressY);
|
||||
|
||||
this.pdf.setFontSize(12);
|
||||
this.pdf.setFont(undefined, 'normal');
|
||||
addressLines.forEach(line => {
|
||||
this.addressY += 7;
|
||||
this.pdf.text(line, 20, this.addressY);
|
||||
});
|
||||
|
||||
this.addressY += 10; // Abstand zur nächsten Adresse
|
||||
}
|
||||
}
|
||||
|
||||
export default PDFGenerator;
|
||||
|
||||
@@ -9,6 +9,7 @@ import MembersView from './views/MembersView.vue';
|
||||
import DiaryView from './views/DiaryView.vue';
|
||||
import PendingApprovalsView from './views/PendingApprovalsView.vue';
|
||||
import ScheduleView from './views/ScheduleView.vue';
|
||||
import TournamentsView from './views/TournamentsView.vue';
|
||||
|
||||
const routes = [
|
||||
{ path: '/register', component: Register },
|
||||
@@ -21,6 +22,7 @@ const routes = [
|
||||
{ path: '/diary', component: DiaryView },
|
||||
{ path: '/pending-approvals', component: PendingApprovalsView},
|
||||
{ path: '/schedule', component: ScheduleView},
|
||||
{ path: '/tournaments', component: TournamentsView },
|
||||
];
|
||||
|
||||
const router = createRouter({
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
|
||||
<div v-if="!showForm && date !== null && date !== 'new'">
|
||||
<h3>Trainingszeiten bearbeiten <span @click="toggleShowGeneralData" class="clickable">{{ showGeneralData ?
|
||||
'-' : '+' }}</span></h3>
|
||||
'-' : '+' }}</span></h3>
|
||||
<form @submit.prevent="updateTrainingTimes" v-if="showGeneralData">
|
||||
<div>
|
||||
<label for="editTrainingStart">Trainingsbeginn:</label>
|
||||
@@ -54,7 +54,7 @@
|
||||
<ul>
|
||||
<li v-for="group in groups" :key="group.id">
|
||||
<span v-if="editingGroupId !== group.id" @click="editGroup(group.id)">{{ group.name
|
||||
}}</span>
|
||||
}}</span>
|
||||
<input v-else type="text" v-model="group.name" @blur="saveGroup(group)"
|
||||
@keyup.enter="saveGroup(group)" @keyup.esc="cancelEditGroup"
|
||||
style="display: inline;width:10em" />
|
||||
@@ -88,6 +88,7 @@
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th> <!-- Neue Spalte für Drag-Handle -->
|
||||
<th>Startzeit</th>
|
||||
<th>Aktivität / Zeitblock</th>
|
||||
<th>Gruppe</th>
|
||||
@@ -98,6 +99,7 @@
|
||||
<tbody ref="sortableList">
|
||||
<template v-for="(item, index) in trainingPlan" :key="item.id">
|
||||
<tr>
|
||||
<td class="drag-handle" style="cursor: move;">☰</td> <!-- Drag-Handle -->
|
||||
<td>{{ item.startTime }}</td>
|
||||
<td>
|
||||
<span v-if="item.isTimeblock"><i>Zeitblock</i></span>
|
||||
@@ -105,12 +107,16 @@
|
||||
item.activity }}</span>
|
||||
</td>
|
||||
<td>{{ item.groupActivity ? item.groupActivity.name : '' }}</td>
|
||||
<td><span v-if="item.durationText">{{ item.durationText }} / </span>{{
|
||||
item.duration }}</td>
|
||||
<td>
|
||||
{{ item.duration }}<span
|
||||
v-if="item.durationText && item.durationText.trim() !== ''"> ({{
|
||||
item.durationText }})</span>
|
||||
</td>
|
||||
<td><button @click="removePlanItem(item.id)">Entfernen</button></td>
|
||||
</tr>
|
||||
<template v-for="groupItem in item.groupActivities">
|
||||
<tr>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td>{{ groupItem.groupPredefinedActivity.name }}</td>
|
||||
<td>{{ groupItem.groupsGroupActivity.name }}</td>
|
||||
@@ -119,7 +125,9 @@
|
||||
</tr>
|
||||
</template>
|
||||
</template>
|
||||
<!-- Zeile zum Hinzufügen eines neuen Items -->
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>{{ calculateNextTime }}</td>
|
||||
<td colspan="4" v-if="!addNewItem && !addNewTimeblock && !addNewGroupActivity">
|
||||
<button @click="openNewPlanItem()">Gesamt-Aktivität</button>
|
||||
@@ -140,7 +148,7 @@
|
||||
</td>
|
||||
<td v-else-if="addNewItem || addNewTimeblock"></td>
|
||||
<td v-if="(addNewItem || addNewTimeblock) && !addNewGroupActivity">
|
||||
<input type="text" v-model="newPlanItem.durationInput"
|
||||
<input type="text" v-model="newPlanItem.durationText"
|
||||
@input="calculateDuration" placeholder="z.B. 2x7 oder 3*5"
|
||||
style="width:10em" />
|
||||
<input type="number" v-model="newPlanItem.duration" placeholder="Minuten" />
|
||||
@@ -154,7 +162,8 @@
|
||||
</tbody>
|
||||
</table>
|
||||
<button v-if="trainingPlan && trainingPlan.length && trainingPlan.length > 0"
|
||||
@click="generatePDF">Als PDF herunterladen</button>
|
||||
@click="generatePDF">Als PDF
|
||||
herunterladen</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column">
|
||||
@@ -181,9 +190,9 @@
|
||||
:checked="isParticipant(member.id)">
|
||||
<span class="clickable" @click="selectMember(member)"
|
||||
:class="{ highlighted: selectedMember && selectedMember.id === member.id }">{{
|
||||
member ? member.firstName : ''
|
||||
member ? member.firstName : ''
|
||||
}} {{
|
||||
member ? member.lastName : '' }}</span>
|
||||
member ? member.lastName : '' }}</span>
|
||||
<span v-if="false" @click="openNotesModal(member)" class="clickable">📝</span>
|
||||
<span @click="showPic(member)" class="img-icon" v-if="member.hasImage">🖼</span>
|
||||
<span class="pointer" @click="openTagInfos(member)">ℹ️</span>
|
||||
@@ -252,18 +261,21 @@
|
||||
<label for="memberId">Mitglied:</label>
|
||||
<select id="memberId" v-model="accident.memberId">
|
||||
<template v-for="member in members" :key="member.id" :value="member.id">
|
||||
<option v-if="participants.indexOf(member.id) >= 0" :value="member.id">{{ member.firstName + ' ' + member.lastName }}</option>
|
||||
<option v-if="participants.indexOf(member.id) >= 0" :value="member.id">{{ member.firstName + ' '
|
||||
+ member.lastName }}</option>
|
||||
</template>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="accident">Unfall:</label>
|
||||
<textarea id="accident" v-model="accident.accident" required ></textarea>
|
||||
<textarea id="accident" v-model="accident.accident" required></textarea>
|
||||
</div>
|
||||
<button type="button" @click="saveAccident">Eintragen</button>
|
||||
<button type="button" @click="closeAccidentForm">Schießen</button>
|
||||
<ul>
|
||||
<li v-for="accident in accidents" :key="accident.id">{{ accident.firstName + ' ' + accident.lastName + ': ' + accident.accident}}</li>
|
||||
<li v-for="accident in accidents" :key="accident.id">{{ accident.firstName + ' ' + accident.lastName +
|
||||
': '
|
||||
+ accident.accident}}</li>
|
||||
</ul>
|
||||
</form>
|
||||
</div>
|
||||
@@ -856,7 +868,7 @@ export default {
|
||||
return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}`;
|
||||
},
|
||||
calculateDuration() {
|
||||
const input = this.newPlanItem.durationInput;
|
||||
const input = this.newPlanItem.durationText;
|
||||
let calculatedDuration = 0;
|
||||
const multiplyPattern = /(\d+)\s*[x*]\s*(\d+)/i;
|
||||
const match = input.match(multiplyPattern);
|
||||
@@ -893,17 +905,17 @@ export default {
|
||||
alert('Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.');
|
||||
}
|
||||
},
|
||||
/* async loadMemberImage(member) {
|
||||
try {
|
||||
const response = await apiClient.get(`/clubmembers/image/${this.currentClub}/${member.id}`, {
|
||||
responseType: 'blob',
|
||||
});
|
||||
const imageUrl = URL.createObjectURL(response.data);
|
||||
member.imageUrl = imageUrl;
|
||||
} catch (error) {
|
||||
member.imageUrl = null;
|
||||
}
|
||||
},*/
|
||||
/* async loadMemberImage(member) {
|
||||
try {
|
||||
const response = await apiClient.get(`/clubmembers/image/${this.currentClub}/${member.id}`, {
|
||||
responseType: 'blob',
|
||||
});
|
||||
const imageUrl = URL.createObjectURL(response.data);
|
||||
member.imageUrl = imageUrl;
|
||||
} catch (error) {
|
||||
member.imageUrl = null;
|
||||
}
|
||||
},*/
|
||||
async generatePDF() {
|
||||
const pdf = new PDFGenerator();
|
||||
pdf.addTrainingPlan(this.currentClubName, this.date.date, this.trainingStart, this.trainingEnd, this.trainingPlan);
|
||||
@@ -985,7 +997,7 @@ export default {
|
||||
this.showGeneralData = !this.showGeneralData;
|
||||
},
|
||||
getFormattedDate(date) {
|
||||
return (new Date(date)).toLocaleDateString('de-DE', { year: 'numeric', month: '2-digit', day: '2-digit'});
|
||||
return (new Date(date)).toLocaleDateString('de-DE', { year: 'numeric', month: '2-digit', day: '2-digit' });
|
||||
},
|
||||
editGroup(groupId) {
|
||||
this.editingGroupId = groupId;
|
||||
@@ -998,14 +1010,14 @@ export default {
|
||||
clubid: this.currentClub,
|
||||
dateid: this.date.id,
|
||||
});
|
||||
this.editingGroupId = null;
|
||||
this.editingGroupId = null;
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Speichern der Gruppendaten:', error);
|
||||
alert('Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.');
|
||||
}
|
||||
},
|
||||
cancelEditGroup() {
|
||||
this.editingGroupId = null;
|
||||
this.editingGroupId = null;
|
||||
},
|
||||
async openTagInfos(member) {
|
||||
if (!member) {
|
||||
@@ -1029,7 +1041,7 @@ export default {
|
||||
},
|
||||
async addNewTagForDay(tag) {
|
||||
await apiClient.post(`/diarydatetags/${this.currentClub}`, {
|
||||
dateId:this.date.id,
|
||||
dateId: this.date.id,
|
||||
memberId: this.tagHistoryMember.id,
|
||||
tag: tag,
|
||||
});
|
||||
@@ -1047,7 +1059,7 @@ export default {
|
||||
if (this.timeChecker) clearInterval(this.timeChecker);
|
||||
this.timeChecker = setInterval(() => {
|
||||
const currentTime = new Date().toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
||||
if (!this.trainingStart || ! this.trainingEnd) {
|
||||
if (!this.trainingStart || !this.trainingEnd) {
|
||||
return;
|
||||
}
|
||||
let startCheckTime = this.trainingStart;
|
||||
@@ -1073,23 +1085,11 @@ export default {
|
||||
},
|
||||
|
||||
playBellSound() {
|
||||
this.bellSound.play()
|
||||
.then(() => {
|
||||
console.log("Bell sound played successfully");
|
||||
})
|
||||
.catch(error => {
|
||||
console.error("Error playing bell sound:", error);
|
||||
});
|
||||
this.bellSound.play();
|
||||
},
|
||||
|
||||
playThumbSound() {
|
||||
this.thumbSound.play()
|
||||
.then(() => {
|
||||
console.log("Thumb sound played successfully");
|
||||
})
|
||||
.catch(error => {
|
||||
console.error("Error playing thumb sound:", error);
|
||||
});
|
||||
this.thumbSound.play();
|
||||
},
|
||||
|
||||
calculateIntermediateTimes() {
|
||||
@@ -1100,12 +1100,12 @@ export default {
|
||||
let times = [];
|
||||
let currentTime = new Date("2025-01-01 " + this.trainingStart);
|
||||
this.trainingPlan.forEach(item => {
|
||||
const rawItem = JSON.parse(JSON.stringify(item));
|
||||
const rawItem = JSON.parse(JSON.stringify(item));
|
||||
currentTime.setMinutes(currentTime.getMinutes() + item.duration);
|
||||
times.push(currentTime.toTimeString({ hours: '2-digit', minutes: '2-digit', seconds: '2-digit' }).slice(0, 8));
|
||||
});
|
||||
times = [...new Set(times)].sort();
|
||||
this.intermediateTimes = times.filter(time =>
|
||||
this.intermediateTimes = times.filter(time =>
|
||||
time !== this.trainingStart && time !== this.trainingEnd
|
||||
);
|
||||
},
|
||||
@@ -1188,7 +1188,8 @@ h3 {
|
||||
.column:first-child {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
height: 100%;justify-self: start;
|
||||
height: 100%;
|
||||
justify-self: start;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
@@ -150,7 +150,6 @@ export default {
|
||||
const pdfGen = new PDFGenerator();
|
||||
await pdfGen.addSchedule(element);
|
||||
pdfGen.addNewPage();
|
||||
pdfGen.addHeader('Hallen-Adressen');
|
||||
const uniqueLocations = this.getUniqueLocations();
|
||||
uniqueLocations.forEach((addressLines, clubName) => {
|
||||
pdfGen.addAddress(clubName, addressLines);
|
||||
@@ -178,7 +177,7 @@ export default {
|
||||
});
|
||||
|
||||
return uniqueLocations;
|
||||
}
|
||||
},
|
||||
},
|
||||
async created() {
|
||||
await this.loadLeagues();
|
||||
|
||||
828
frontend/src/views/TournamentsView.vue
Normal file
828
frontend/src/views/TournamentsView.vue
Normal file
@@ -0,0 +1,828 @@
|
||||
<template>
|
||||
<div class="tournaments-view">
|
||||
<h2>Turnier</h2>
|
||||
<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>
|
||||
<div v-if="selectedDate !== 'new'" class="tournament-setup">
|
||||
<label>
|
||||
<input type="checkbox" v-model="isGroupTournament" @change="onModusChange" />
|
||||
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 }}
|
||||
<template v-if="isGroupTournament">
|
||||
<label class="inline-label">
|
||||
Gruppe:
|
||||
<select v-model.number="participant.groupNumber">
|
||||
<option :value="null">–</option>
|
||||
<option v-for="group in groups" :key="group.groupId" :value="group.groupNumber">
|
||||
Gruppe {{ group.groupNumber }}
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
</template>
|
||||
<button @click="removeParticipant(participant)" style="margin-left:0.5rem">
|
||||
Entfernen
|
||||
</button>
|
||||
</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" class="group-controls">
|
||||
<label>
|
||||
Anzahl Gruppen:
|
||||
<input type="number" v-model.number="numberOfGroups" min="1" @change="onGroupCountChange" />
|
||||
</label>
|
||||
<label style="margin-left:1em">
|
||||
Aufsteiger pro Gruppe:
|
||||
<input type="number" v-model.number="advancingPerGroup" min="1" @change="onModusChange" />
|
||||
</label>
|
||||
|
||||
<label style="margin-left:1em">
|
||||
Maximale Gruppengröße:
|
||||
<input type="number" v-model.number="maxGroupSize" 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.groupNumber }}</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>
|
||||
<div class="reset-controls" style="margin-top:1rem">
|
||||
<button @click="resetGroups">
|
||||
Gruppen zurücksetzen
|
||||
</button>
|
||||
<button @click="resetMatches" style="margin-left:0.5rem">
|
||||
Gruppenspiele löschen
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<section v-if="groupMatches.length" class="group-matches">
|
||||
<h4>Gruppenspiele</h4>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Runde</th>
|
||||
<th>Gruppe</th>
|
||||
<th>Begegnung</th>
|
||||
<th>Ergebnis</th>
|
||||
<th>Sätze</th>
|
||||
<th>Aktion</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="m in groupMatches" :key="m.id">
|
||||
<td>{{ m.groupRound }}</td>
|
||||
<td>{{ m.groupNumber }}</td>
|
||||
<td>
|
||||
<template v-if="m.isFinished">
|
||||
<span v-if="winnerIsPlayer1(m)">
|
||||
<strong>{{ getPlayerName(m.player1) }}</strong> – {{ getPlayerName(m.player2) }}
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ getPlayerName(m.player1) }} – <strong>{{ getPlayerName(m.player2) }}</strong>
|
||||
</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ getPlayerName(m.player1) }} – {{ getPlayerName(m.player2) }}
|
||||
</template>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<!-- 1. Fall: Match ist noch offen → Edit‑Mode -->
|
||||
<template v-if="!m.isFinished">
|
||||
<!-- existierende Sätze als klickbare Labels -->
|
||||
<template v-for="r in m.tournamentResults" :key="r.set">
|
||||
<span @click="startEditResult(m, r)" class="result-text">
|
||||
{{ r.pointsPlayer1 }}:{{ r.pointsPlayer2 }}
|
||||
</span>
|
||||
<span v-if="!isLastResult(m, r)">, </span>
|
||||
</template>
|
||||
|
||||
<!-- Eingabefeld für neue Sätze (immer sichtbar solange offen) -->
|
||||
<div class="new-set-line">
|
||||
<input v-model="m.resultInput" placeholder="Neuen Satz, z.B. 11:7"
|
||||
@keyup.enter="saveMatchResult(m, m.resultInput)"
|
||||
@blur="saveMatchResult(m, m.resultInput)" class="inline-input" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 2. Fall: Match ist abgeschlossen → Read‑only -->
|
||||
<template v-else>
|
||||
{{ formatResult(m) }}
|
||||
</template>
|
||||
</td>
|
||||
<td>
|
||||
{{ getSetsString(m) }}
|
||||
</td>
|
||||
<td>
|
||||
<!-- „Abschließen“-Button nur, wenn noch nicht fertig -->
|
||||
<button v-if="!m.isFinished" @click="finishMatch(m)">Abschließen</button>
|
||||
<!-- „Korrigieren“-Button nur, wenn fertig -->
|
||||
<button v-else @click="reopenMatch(m)">Korrigieren</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
<div v-if="participants.length > 1
|
||||
&& !groupMatches.length
|
||||
&& !knockoutMatches.length" class="start-matches" style="margin-top:1.5rem">
|
||||
<button @click="startMatches">
|
||||
Spiele erstellen
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="canStartKnockout && !showKnockout" class="ko-start">
|
||||
<button @click="startKnockout">
|
||||
K.o.-Runde starten
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="showKnockout && canResetKnockout" class="ko-reset" style="margin-top:1rem">
|
||||
<button @click="resetKnockout">
|
||||
K.o.-Runde löschen
|
||||
</button>
|
||||
</div>
|
||||
<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>Sätze</th>
|
||||
<th>Aktion</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="m in knockoutMatches" :key="m.id">
|
||||
<td>{{ m.round }}</td>
|
||||
<td>
|
||||
<template v-if="m.isFinished">
|
||||
<span v-if="winnerIsPlayer1(m)">
|
||||
<strong>{{ getPlayerName(m.player1) }}</strong> – {{ getPlayerName(m.player2) }}
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ getPlayerName(m.player1) }} – <strong>{{ getPlayerName(m.player2) }}</strong>
|
||||
</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ getPlayerName(m.player1) }} – {{ getPlayerName(m.player2) }}
|
||||
</template>
|
||||
</td>
|
||||
<td>
|
||||
<!-- 1. Fall: Match ist noch offen → Edit‑Mode -->
|
||||
<template v-if="!m.isFinished">
|
||||
<!-- existierende Sätze als klickbare Labels -->
|
||||
<template v-for="r in m.tournamentResults" :key="r.set">
|
||||
<span @click="startEditResult(m, r)" class="result-text">
|
||||
{{ r.pointsPlayer1 }}:{{ r.pointsPlayer2 }}
|
||||
</span>
|
||||
<span v-if="!isLastResult(m, r)">, </span>
|
||||
</template>
|
||||
|
||||
<!-- Eingabefeld für neue Sätze (immer sichtbar solange offen) -->
|
||||
<div class="new-set-line">
|
||||
<input v-model="m.resultInput" placeholder="Neuen Satz, z.B. 11:7"
|
||||
@keyup.enter="saveMatchResult(m, m.resultInput)"
|
||||
@blur="saveMatchResult(m, m.resultInput)" class="inline-input" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 2. Fall: Match ist abgeschlossen → Read‑only -->
|
||||
<template v-else>
|
||||
{{ formatResult(m) }}
|
||||
</template>
|
||||
</td>
|
||||
<td>
|
||||
{{ getSetsString(m) }}
|
||||
</td>
|
||||
<td>
|
||||
<button v-if="!m.isFinished" @click="finishMatch(m)">Fertig</button>
|
||||
<button v-else @click="reopenMatch(m)">Korrigieren</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
<section v-if="rankingList.length" class="ranking">
|
||||
<h4>Rangliste</h4>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Platz</th>
|
||||
<th>Spieler</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(entry, idx) in rankingList" :key="`${entry.member.id}-${idx}`">
|
||||
<td>{{ entry.position }}.</td>
|
||||
<td>
|
||||
{{ entry.member.firstName }}
|
||||
{{ entry.member.lastName }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import apiClient from '../apiClient';
|
||||
|
||||
export default {
|
||||
name: 'TournamentsView',
|
||||
data() {
|
||||
return {
|
||||
selectedDate: 'new',
|
||||
newDate: '',
|
||||
dates: [],
|
||||
participants: [],
|
||||
selectedMember: null,
|
||||
clubMembers: [],
|
||||
advancingPerGroup: 1,
|
||||
numberOfGroups: 1,
|
||||
maxGroupSize: null,
|
||||
isGroupTournament: false,
|
||||
groups: [],
|
||||
matches: [],
|
||||
showKnockout: false,
|
||||
editingResult: {
|
||||
matchId: null, // aktuell bearbeitetes Match
|
||||
set: null, // aktuell bearbeitete Satz‑Nummer
|
||||
value: '' // Eingabewert
|
||||
}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['isAuthenticated', 'currentClub']),
|
||||
|
||||
knockoutMatches() {
|
||||
return this.matches.filter(m => m.round !== 'group');
|
||||
},
|
||||
|
||||
groupMatches() {
|
||||
return this.matches
|
||||
.filter(m => m.round === 'group')
|
||||
.sort((a, b) => {
|
||||
// zuerst nach Runde
|
||||
if (a.groupRound !== b.groupRound) {
|
||||
return a.groupRound - b.groupRound;
|
||||
}
|
||||
// dann nach Gruppe
|
||||
return a.groupNumber - b.groupNumber;
|
||||
});
|
||||
},
|
||||
|
||||
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;
|
||||
},
|
||||
|
||||
rankingList() {
|
||||
const finalMatch = this.knockoutMatches.find(
|
||||
m => m.round.toLowerCase() === 'finale'
|
||||
);
|
||||
if (!finalMatch || !finalMatch.isFinished) return [];
|
||||
const list = [];
|
||||
const [s1, s2] = finalMatch.result.split(':').map(n => +n);
|
||||
const winner = s1 > s2 ? finalMatch.player1 : finalMatch.player2;
|
||||
const loser = s1 > s2 ? finalMatch.player2 : finalMatch.player1;
|
||||
list.push({ position: 1, member: winner.member });
|
||||
list.push({ position: 2, member: loser.member });
|
||||
const roundsMap = {};
|
||||
this.knockoutMatches.forEach(m => {
|
||||
if (m.round.toLowerCase() === 'finale') return;
|
||||
(roundsMap[m.round] ||= []).push(m);
|
||||
});
|
||||
Object.values(roundsMap).forEach(matches => {
|
||||
const M = matches.length;
|
||||
const pos = M + 1;
|
||||
matches.forEach(match => {
|
||||
const [a, b] = match.result.split(':').map(n => +n);
|
||||
const knockedOut = a > b ? match.player2 : match.player1;
|
||||
list.push({ position: pos, member: knockedOut.member });
|
||||
});
|
||||
});
|
||||
return list.sort((a, b) => a.position - b.position);
|
||||
},
|
||||
|
||||
canStartKnockout() {
|
||||
if (this.participants.length < 2) return false;
|
||||
if (!this.isGroupTournament) {
|
||||
// kein Gruppenmodus → immer starten
|
||||
return true;
|
||||
}
|
||||
// Gruppenmodus → nur, wenn es Gruppenspiele gibt und alle beendet sind
|
||||
return this.groupMatches.length > 0
|
||||
&& this.groupMatches.every(m => m.isFinished);
|
||||
},
|
||||
|
||||
canResetKnockout() {
|
||||
// KO‑Matches existieren und keiner ist beendet
|
||||
return this.knockoutMatches.length > 0
|
||||
&& this.knockoutMatches.every(m => !m.isFinished);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
selectedDate: {
|
||||
immediate: true,
|
||||
handler: async function (val) {
|
||||
if (val === 'new') return;
|
||||
await this.loadTournamentData();
|
||||
}
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
if (!this.isAuthenticated) {
|
||||
this.$router.push('/login');
|
||||
return;
|
||||
}
|
||||
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: {
|
||||
normalizeResultInput(raw) {
|
||||
const s = raw.trim();
|
||||
if (s.includes(':')) {
|
||||
const [xRaw, yRaw] = s.split(':');
|
||||
const x = Number(xRaw), y = Number(yRaw);
|
||||
if (
|
||||
Number.isInteger(x) && Number.isInteger(y) &&
|
||||
(x >= 11 || y >= 11) &&
|
||||
Math.abs(x - y) >= 2
|
||||
) {
|
||||
return `${x}:${y}`;
|
||||
}
|
||||
console.warn('Ungültiges Satz-Ergebnis:', s);
|
||||
return null;
|
||||
}
|
||||
|
||||
const num = Number(s);
|
||||
if (isNaN(num)) {
|
||||
console.warn('Ungültiges Ergebnisformat:', raw);
|
||||
return null;
|
||||
}
|
||||
|
||||
const losing = Math.abs(num);
|
||||
const winning = losing < 10 ? 11 : losing + 2;
|
||||
|
||||
if (num >= 0) {
|
||||
return `${winning}:${losing}`;
|
||||
} else {
|
||||
return `${losing}:${winning}`;
|
||||
}
|
||||
},
|
||||
|
||||
async loadTournamentData() {
|
||||
const tRes = await apiClient.get(
|
||||
`/tournament/${this.currentClub}/${this.selectedDate}`
|
||||
);
|
||||
const tournament = tRes.data;
|
||||
this.isGroupTournament = tournament.type === 'groups';
|
||||
this.numberOfGroups = tournament.numberOfGroups;
|
||||
this.advancingPerGroup = tournament.advancingPerGroup;
|
||||
const pRes = await apiClient.post('/tournament/participants', {
|
||||
clubId: this.currentClub,
|
||||
tournamentId: this.selectedDate
|
||||
});
|
||||
this.participants = pRes.data;
|
||||
const gRes = await apiClient.get('/tournament/groups', {
|
||||
params: {
|
||||
clubId: this.currentClub,
|
||||
tournamentId: this.selectedDate
|
||||
}
|
||||
});
|
||||
this.groups = gRes.data;
|
||||
const mRes = await apiClient.get(
|
||||
`/tournament/matches/${this.currentClub}/${this.selectedDate}`
|
||||
);
|
||||
const grpMap = this.groups.reduce((m, g) => {
|
||||
m[g.groupId] = g.groupNumber;
|
||||
return m;
|
||||
}, {});
|
||||
this.matches = mRes.data.map(m => ({
|
||||
...m,
|
||||
groupNumber: grpMap[m.groupId] || 0,
|
||||
resultInput: ''
|
||||
}));
|
||||
this.showKnockout = this.matches.some(m => m.round !== 'group');
|
||||
},
|
||||
|
||||
getPlayerName(p) {
|
||||
return p.member.firstName + ' ' + p.member.lastName;
|
||||
},
|
||||
|
||||
async createTournament() {
|
||||
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() {
|
||||
const oldMap = this.participants.reduce((map, p) => {
|
||||
map[p.id] = p.groupNumber
|
||||
return map
|
||||
}, {})
|
||||
const r = await apiClient.post('/tournament/participant', {
|
||||
clubId: this.currentClub,
|
||||
tournamentId: this.selectedDate,
|
||||
participant: this.selectedMember
|
||||
})
|
||||
this.participants = r.data.map(p => ({
|
||||
...p,
|
||||
groupNumber:
|
||||
oldMap[p.id] != null
|
||||
? oldMap[p.id]
|
||||
: (p.groupId || null)
|
||||
}))
|
||||
this.selectedMember = null
|
||||
},
|
||||
|
||||
async createGroups() {
|
||||
const assignments = this.participants.map(p => ({
|
||||
participantId: p.id,
|
||||
groupNumber: p.groupNumber
|
||||
}));
|
||||
const manual = assignments.some(a => a.groupNumber != null);
|
||||
if (manual) {
|
||||
await apiClient.post('/tournament/groups/manual', {
|
||||
clubId: this.currentClub,
|
||||
tournamentId: this.selectedDate,
|
||||
assignments,
|
||||
numberOfGroups: this.numberOfGroups,
|
||||
maxGroupSize: this.maxGroupSize
|
||||
});
|
||||
} else {
|
||||
await apiClient.put('/tournament/groups', {
|
||||
clubId: this.currentClub,
|
||||
tournamentId: this.selectedDate
|
||||
});
|
||||
}
|
||||
await this.loadTournamentData();
|
||||
},
|
||||
|
||||
async randomizeGroups() {
|
||||
try {
|
||||
const r = await apiClient.post('/tournament/groups', {
|
||||
clubId: this.currentClub,
|
||||
tournamentId: this.selectedDate
|
||||
});
|
||||
this.participants = r.data;
|
||||
} catch (err) {
|
||||
alert('Fehler beim Zufällig‑Verteilen:\n' +
|
||||
(err.response?.data?.error || err.message));
|
||||
}
|
||||
await this.loadTournamentData();
|
||||
},
|
||||
|
||||
async saveMatchResult(match, result) {
|
||||
if (!result || result.trim().length === 0) {
|
||||
return;
|
||||
}
|
||||
const normalized = this.normalizeResultInput(result);
|
||||
if (!normalized) return;
|
||||
result = normalized;
|
||||
await apiClient.post('/tournament/match/result', {
|
||||
clubId: this.currentClub,
|
||||
tournamentId: this.selectedDate,
|
||||
matchId: match.id,
|
||||
set: (match.tournamentResults?.length || 0) + 1,
|
||||
result
|
||||
});
|
||||
const allRes = await apiClient.get(
|
||||
`/tournament/matches/${this.currentClub}/${this.selectedDate}`
|
||||
);
|
||||
const updated = allRes.data.find(m2 => m2.id === match.id);
|
||||
if (!updated) {
|
||||
console.error('Konnte aktualisiertes Match nicht finden');
|
||||
return;
|
||||
}
|
||||
match.tournamentResults = updated.tournamentResults || [];
|
||||
const resultString = match.tournamentResults.length
|
||||
? match.tournamentResults
|
||||
.sort((a, b) => a.set - b.set)
|
||||
.map(r => `${Math.abs(r.pointsPlayer1)}:${Math.abs(r.pointsPlayer2)}`)
|
||||
.join(', ')
|
||||
: null;
|
||||
|
||||
match.result = resultString;
|
||||
match.resultInput = '';
|
||||
},
|
||||
|
||||
async finishMatch(match) {
|
||||
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();
|
||||
},
|
||||
|
||||
formatResult(match) {
|
||||
if (!match.tournamentResults?.length) return '-';
|
||||
return match.tournamentResults
|
||||
.sort((a, b) => a.set - b.set)
|
||||
.map(r => `${Math.abs(r.pointsPlayer1)}:${Math.abs(r.pointsPlayer2)}`)
|
||||
.join(', ');
|
||||
},
|
||||
|
||||
async startMatches() {
|
||||
if (this.isGroupTournament) {
|
||||
if (!this.groups.length) {
|
||||
await this.createGroups();
|
||||
}
|
||||
await this.randomizeGroups();
|
||||
} else {
|
||||
await this.startKnockout();
|
||||
}
|
||||
await this.loadTournamentData();
|
||||
},
|
||||
|
||||
async onModusChange() {
|
||||
const type = this.isGroupTournament ? 'groups' : 'knockout';
|
||||
await apiClient.post('/tournament/modus', {
|
||||
clubId: this.currentClub,
|
||||
tournamentId: this.selectedDate,
|
||||
type,
|
||||
numberOfGroups: this.numberOfGroups,
|
||||
advancingPerGroup: this.advancingPerGroup
|
||||
});
|
||||
await this.loadTournamentData();
|
||||
},
|
||||
|
||||
async resetGroups() {
|
||||
await apiClient.post('/tournament/groups/reset', {
|
||||
clubId: this.currentClub,
|
||||
tournamentId: this.selectedDate
|
||||
});
|
||||
await this.loadTournamentData();
|
||||
},
|
||||
|
||||
async resetMatches() {
|
||||
await apiClient.post('/tournament/matches/reset', {
|
||||
clubId: this.currentClub,
|
||||
tournamentId: this.selectedDate
|
||||
});
|
||||
await this.loadTournamentData();
|
||||
},
|
||||
|
||||
async removeParticipant(p) {
|
||||
await apiClient.delete('/tournament/participant', {
|
||||
data: {
|
||||
clubId: this.currentClub,
|
||||
tournamentId: this.selectedDate,
|
||||
participantId: p.id
|
||||
}
|
||||
});
|
||||
this.participants = this.participants.filter(x => x.id !== p.id);
|
||||
},
|
||||
|
||||
async onGroupCountChange() {
|
||||
await apiClient.post('/tournament/modus', {
|
||||
clubId: this.currentClub,
|
||||
tournamentId: this.selectedDate,
|
||||
type: this.isGroupTournament ? 'groups' : 'knockout',
|
||||
numberOfGroups: this.numberOfGroups
|
||||
});
|
||||
await this.loadTournamentData();
|
||||
},
|
||||
|
||||
async reopenMatch(match) {
|
||||
await apiClient.post('/tournament/match/reopen', {
|
||||
clubId: this.currentClub,
|
||||
tournamentId: this.selectedDate,
|
||||
matchId: match.id
|
||||
});
|
||||
await this.loadTournamentData();
|
||||
},
|
||||
|
||||
async deleteResult(match, set) {
|
||||
if (match.isFinished) {
|
||||
await this.reopenMatch(match);
|
||||
match.isFinished = false;
|
||||
}
|
||||
await apiClient.delete('/tournament/match/result', {
|
||||
data: {
|
||||
clubId: this.currentClub,
|
||||
tournamentId: this.selectedDate,
|
||||
matchId: match.id,
|
||||
set
|
||||
}
|
||||
});
|
||||
await this.loadTournamentData();
|
||||
},
|
||||
|
||||
startEditResult(match, result) {
|
||||
if (match.isFinished) {
|
||||
this.reopenMatch(match);
|
||||
match.isFinished = false;
|
||||
}
|
||||
this.editingResult.matchId = match.id;
|
||||
this.editingResult.set = result.set;
|
||||
this.editingResult.value = `${result.pointsPlayer1}:${result.pointsPlayer2}`;
|
||||
},
|
||||
|
||||
async saveEditedResult(match) {
|
||||
const { set, value } = this.editingResult;
|
||||
const normalized = this.normalizeResultInput(value);
|
||||
if (!normalized) return;
|
||||
let result = normalized;
|
||||
await apiClient.post('/tournament/match/result', {
|
||||
clubId: this.currentClub,
|
||||
tournamentId: this.selectedDate,
|
||||
matchId: match.id,
|
||||
set,
|
||||
result
|
||||
});
|
||||
this.editingResult.matchId = null;
|
||||
this.editingResult.set = null;
|
||||
this.editingResult.value = '';
|
||||
await this.loadTournamentData();
|
||||
},
|
||||
|
||||
isEditing(match, set) {
|
||||
return (
|
||||
this.editingResult.matchId === match.id &&
|
||||
this.editingResult.set === set
|
||||
);
|
||||
},
|
||||
|
||||
isLastResult(match, result) {
|
||||
const arr = match.tournamentResults || [];
|
||||
return arr.length > 0 && arr[arr.length - 1].set === result.set;
|
||||
},
|
||||
|
||||
getSetsString(match) {
|
||||
const results = match.tournamentResults || [];
|
||||
let win1 = 0, win2 = 0;
|
||||
for (const r of results) {
|
||||
if (r.pointsPlayer1 > r.pointsPlayer2) win1++;
|
||||
else if (r.pointsPlayer2 > r.pointsPlayer1) win2++;
|
||||
}
|
||||
return `${win1}:${win2}`;
|
||||
},
|
||||
|
||||
winnerIsPlayer1(match) {
|
||||
const [w1, w2] = this.getSetsString(match).split(':').map(Number);
|
||||
return w1 > w2;
|
||||
},
|
||||
|
||||
async resetKnockout() {
|
||||
try {
|
||||
await apiClient.delete('/tournament/matches/knockout', {
|
||||
data: {
|
||||
clubId: this.currentClub,
|
||||
tournamentId: this.selectedDate
|
||||
}
|
||||
});
|
||||
await this.loadTournamentData();
|
||||
} catch (err) {
|
||||
console.error('Reset KO failed:', err);
|
||||
alert(err.response?.data?.error || err.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tournaments-view {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.participants,
|
||||
.group-controls,
|
||||
.groups-overview,
|
||||
.ko-round,
|
||||
.ko-start {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.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>
|
||||
Reference in New Issue
Block a user