feat(tournament): add group match creation and enhance match handling
- Implemented createGroupMatches function to generate matches for existing groups without altering group assignments. - Updated resetMatches function to support optional class filtering when resetting group matches. - Enhanced frontend components to filter and display group matches based on selected class, improving user experience. - Adjusted tournament results display to reflect accurate match statistics, including wins and losses.
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -7,4 +7,5 @@ backend/.env
|
||||
|
||||
backend/images/*
|
||||
backend/backend-debug.log
|
||||
backend/*.log
|
||||
backend/*.log
|
||||
backend/.env.local
|
||||
|
||||
@@ -174,6 +174,21 @@ export const fillGroups = async (req, res) => {
|
||||
}
|
||||
};
|
||||
|
||||
// 7b. Gruppenspiele erstellen ohne Gruppenzuordnungen zu ändern
|
||||
export const createGroupMatches = async (req, res) => {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubId, tournamentId, classId } = req.body;
|
||||
try {
|
||||
await tournamentService.createGroupMatches(token, clubId, tournamentId, classId);
|
||||
// Emit Socket-Event
|
||||
emitTournamentChanged(clubId, tournamentId);
|
||||
res.sendStatus(204);
|
||||
} 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;
|
||||
@@ -341,9 +356,9 @@ export const resetGroups = async (req, res) => {
|
||||
|
||||
export const resetMatches = async (req, res) => {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubId, tournamentId } = req.body;
|
||||
const { clubId, tournamentId, classId } = req.body;
|
||||
try {
|
||||
await tournamentService.resetMatches(token, clubId, tournamentId);
|
||||
await tournamentService.resetMatches(token, clubId, tournamentId, classId || null);
|
||||
// Emit Socket-Event
|
||||
emitTournamentChanged(clubId, tournamentId);
|
||||
res.sendStatus(204);
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
setModus,
|
||||
createGroups,
|
||||
fillGroups,
|
||||
createGroupMatches,
|
||||
getGroups,
|
||||
getTournament,
|
||||
getTournamentMatches,
|
||||
@@ -58,6 +59,7 @@ router.post('/matches/reset', authenticate, resetMatches);
|
||||
router.put('/groups', authenticate, createGroups);
|
||||
router.post('/groups/create', authenticate, createGroupsPerClass);
|
||||
router.post('/groups', authenticate, fillGroups);
|
||||
router.post('/matches/create', authenticate, createGroupMatches);
|
||||
router.get('/groups', authenticate, getGroups);
|
||||
router.post('/match/result', authenticate, addMatchResult);
|
||||
router.delete('/match/result', authenticate, deleteMatchResult);
|
||||
|
||||
@@ -274,12 +274,22 @@ class TournamentService {
|
||||
const classIdsInStage1 = [...new Set(perGroupRanked.map(g => (g.classId ?? null)))];
|
||||
|
||||
for (const classId of classIdsInStage1) {
|
||||
// Zähle die Gruppen für diese Klasse
|
||||
const groupsForClass = perGroupRanked.filter(g => (g.classId ?? null) === classId);
|
||||
const numberOfGroupsForClass = groupsForClass.length;
|
||||
|
||||
// Nur Klassen mit mehr als einer Gruppe dürfen weitere Runden haben
|
||||
if (numberOfGroupsForClass <= 1) {
|
||||
// Überspringe diese Klasse - keine Zwischen-/Endrunde für Klassen mit nur einer Gruppe
|
||||
continue;
|
||||
}
|
||||
|
||||
const items = [];
|
||||
for (const grp of perGroupRanked) {
|
||||
if ((grp.classId ?? null) !== classId) continue;
|
||||
for (const place of fromPlaces) {
|
||||
const p = getByPlace(grp, Number(place));
|
||||
if (p) items.push({ ...p, classId });
|
||||
if (p) items.push({ ...p, classId, place: Number(place) });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -295,11 +305,20 @@ class TournamentService {
|
||||
const singleFieldKoItems = [];
|
||||
let wantsThirdPlace = false;
|
||||
for (const pool of poolItems) {
|
||||
const target = pool.target || {};
|
||||
let target = pool.target || {};
|
||||
const items = pool.items || [];
|
||||
const poolClassId = pool.classId ?? null;
|
||||
if (items.length === 0) continue;
|
||||
|
||||
// Wenn Endrunde K.O. sein soll, aber target.type === 'groups' ist,
|
||||
// dann sollte es K.O. sein statt Gruppen.
|
||||
// Die Stage-Konfiguration (toStage.type) ist die Quelle der Wahrheit
|
||||
if (toStage.type === 'knockout' && target.type === 'groups') {
|
||||
// Erzwinge K.O. wenn die Stage-Konfiguration K.O. ist
|
||||
// singleField sollte true sein für Endrunden (K.O. als ein einziges Feld)
|
||||
target = { ...target, type: 'knockout', singleField: true };
|
||||
}
|
||||
|
||||
if (target.type === 'groups') {
|
||||
const groupCount = Math.max(1, Number(target.groupCount || toStage.numberOfGroups || 1));
|
||||
const poolGroups = [];
|
||||
@@ -436,24 +455,42 @@ class TournamentService {
|
||||
const entrants = classItems.map(p => ({
|
||||
id: Number(p.id),
|
||||
isExternal: !!p.isExternal,
|
||||
place: p.place || 999, // Platz-Information behalten
|
||||
}));
|
||||
|
||||
// Dedupliziere (falls jemand in mehreren Regeln landet)
|
||||
const seen = new Set();
|
||||
const uniqueEntrants = [];
|
||||
// Wenn jemand mehrfach vorkommt, nehme den besten Platz
|
||||
const seen = new Map();
|
||||
for (const e of entrants) {
|
||||
const key = `${e.isExternal ? 'E' : 'M'}:${e.id}`;
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
uniqueEntrants.push(e);
|
||||
const existing = seen.get(key);
|
||||
if (!existing || (e.place < existing.place)) {
|
||||
seen.set(key, e);
|
||||
}
|
||||
}
|
||||
const uniqueEntrants = Array.from(seen.values());
|
||||
|
||||
const thirdPlace = wantsThirdPlace;
|
||||
if (uniqueEntrants.length >= 2) {
|
||||
shuffleInPlace(uniqueEntrants);
|
||||
// Sortiere nach Platz: beste Plätze zuerst, dann schlechtere
|
||||
// Wenn mehrere Teilnehmer den gleichen Platz haben, behalte die ursprüngliche Reihenfolge
|
||||
uniqueEntrants.sort((a, b) => {
|
||||
const placeA = a.place || 999;
|
||||
const placeB = b.place || 999;
|
||||
return placeA - placeB;
|
||||
});
|
||||
|
||||
// Paare: Bester gegen Schlechtesten, Zweiter gegen Vorletzten, etc.
|
||||
// Reverse die zweite Hälfte, um das gewünschte Pairing zu erreichen
|
||||
const bracketSize = nextPowerOfTwo(uniqueEntrants.length);
|
||||
const byes = bracketSize - uniqueEntrants.length;
|
||||
|
||||
const playersNeeded = bracketSize - byes;
|
||||
|
||||
// Teile in zwei Hälften für das Pairing
|
||||
const firstHalf = uniqueEntrants.slice(0, Math.ceil(playersNeeded / 2));
|
||||
const secondHalf = uniqueEntrants.slice(Math.ceil(playersNeeded / 2), playersNeeded);
|
||||
const reversedSecondHalf = [...secondHalf].reverse(); // Umgekehrte Reihenfolge für Pairing
|
||||
|
||||
const roundName = getRoundName(bracketSize);
|
||||
if (thirdPlace && bracketSize >= 4) {
|
||||
await TournamentMatch.create({
|
||||
@@ -470,12 +507,12 @@ class TournamentService {
|
||||
result: null,
|
||||
});
|
||||
}
|
||||
// BYE‑Matches zuerst
|
||||
let remaining = [...uniqueEntrants];
|
||||
|
||||
// BYE‑Matches zuerst (für die ersten Teilnehmer, wenn nötig)
|
||||
if (byes > 0) {
|
||||
const byePlayers = remaining.slice(0, Math.min(byes, remaining.length));
|
||||
remaining = remaining.slice(byePlayers.length);
|
||||
for (const p of byePlayers) {
|
||||
// Die besten Spieler bekommen BYEs
|
||||
for (let i = 0; i < byes && i < uniqueEntrants.length; i++) {
|
||||
const p = uniqueEntrants[i];
|
||||
if (!p) continue;
|
||||
await TournamentMatch.create({
|
||||
tournamentId,
|
||||
@@ -492,10 +529,12 @@ class TournamentService {
|
||||
});
|
||||
}
|
||||
}
|
||||
// Verbleibende normal paaren
|
||||
for (let i = 0; i < remaining.length; i += 2) {
|
||||
const a = remaining[i];
|
||||
const b = remaining[i + 1];
|
||||
|
||||
// Paare: Erster gegen Letzten, Zweiter gegen Vorletzten, etc.
|
||||
const maxPairs = Math.min(firstHalf.length, reversedSecondHalf.length);
|
||||
for (let i = 0; i < maxPairs; i++) {
|
||||
const a = firstHalf[i];
|
||||
const b = reversedSecondHalf[i];
|
||||
if (!a || !b) continue;
|
||||
await TournamentMatch.create({
|
||||
tournamentId,
|
||||
@@ -1278,6 +1317,142 @@ class TournamentService {
|
||||
return allParticipants;
|
||||
}
|
||||
|
||||
// Erstellt nur die Matches für bestehende Gruppen, ohne Gruppenzuordnungen zu ändern
|
||||
async createGroupMatches(userToken, clubId, tournamentId, classId = null) {
|
||||
await checkAccess(userToken, clubId);
|
||||
|
||||
const tournament = await Tournament.findByPk(tournamentId);
|
||||
if (!tournament || tournament.clubId != clubId) {
|
||||
throw new Error('Turnier nicht gefunden');
|
||||
}
|
||||
|
||||
// Lade alle Gruppen, optional gefiltert nach classId
|
||||
const whereClause = { tournamentId };
|
||||
if (classId !== null && classId !== undefined) {
|
||||
whereClause.classId = classId;
|
||||
}
|
||||
|
||||
const allGroups = await TournamentGroup.findAll({
|
||||
where: whereClause,
|
||||
order: [['id', 'ASC']]
|
||||
});
|
||||
|
||||
if (allGroups.length === 0) {
|
||||
throw new Error('Keine Gruppen vorhanden.');
|
||||
}
|
||||
|
||||
// Lösche nur Matches für die betroffenen Gruppen (optional gefiltert nach classId)
|
||||
const matchWhereClause = {
|
||||
tournamentId,
|
||||
round: 'group'
|
||||
};
|
||||
if (classId !== null && classId !== undefined) {
|
||||
matchWhereClause.classId = classId;
|
||||
}
|
||||
await TournamentMatch.destroy({ where: matchWhereClause });
|
||||
|
||||
// Lade alle Klassen, um zu prüfen, ob es sich um Doppel-Klassen handelt
|
||||
const tournamentClasses = await TournamentClass.findAll({ where: { tournamentId } });
|
||||
const classIsDoublesMap = tournamentClasses.reduce((map, cls) => {
|
||||
map[cls.id] = cls.isDoubles;
|
||||
return map;
|
||||
}, {});
|
||||
|
||||
for (const g of allGroups) {
|
||||
const groupClassId = g.classId;
|
||||
const isDoubles = groupClassId ? (classIsDoublesMap[groupClassId] || false) : false;
|
||||
|
||||
if (isDoubles) {
|
||||
// Bei Doppel: Lade Paarungen für diese Gruppe
|
||||
const pairings = await TournamentPairing.findAll({
|
||||
where: { tournamentId, classId: groupClassId, groupId: g.id },
|
||||
include: [
|
||||
{ model: TournamentMember, as: 'member1', include: [{ model: Member, as: 'member' }] },
|
||||
{ model: TournamentMember, as: 'member2', include: [{ model: Member, as: 'member' }] },
|
||||
{ model: ExternalTournamentParticipant, as: 'external1' },
|
||||
{ model: ExternalTournamentParticipant, as: 'external2' }
|
||||
]
|
||||
});
|
||||
|
||||
if (pairings.length < 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Erstelle Round-Robin zwischen Paarungen
|
||||
const pairingItems = pairings.map(p => ({
|
||||
pairingId: p.id,
|
||||
player1Id: p.member1Id || p.external1Id,
|
||||
player2Id: p.member2Id || p.external2Id,
|
||||
key: `pairing-${p.id}`
|
||||
}));
|
||||
|
||||
const rounds = this.generateRoundRobinSchedule(pairingItems.map(p => ({ id: p.key })));
|
||||
|
||||
for (let roundIndex = 0; roundIndex < rounds.length; roundIndex++) {
|
||||
for (const [p1Key, p2Key] of rounds[roundIndex]) {
|
||||
if (p1Key && p2Key) {
|
||||
const pairing1 = pairingItems.find(p => p.key === p1Key);
|
||||
const pairing2 = pairingItems.find(p => p.key === p2Key);
|
||||
|
||||
if (pairing1 && pairing2) {
|
||||
await TournamentMatch.create({
|
||||
tournamentId,
|
||||
groupId: g.id,
|
||||
round: 'group',
|
||||
player1Id: pairing1.player1Id,
|
||||
player2Id: pairing2.player1Id,
|
||||
groupRound: roundIndex + 1,
|
||||
classId: g.classId
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Bei Einzel: Normale Logik mit einzelnen Spielern
|
||||
const internalMembers = await TournamentMember.findAll({ where: { groupId: g.id } });
|
||||
const externalMembers = await ExternalTournamentParticipant.findAll({ where: { groupId: g.id } });
|
||||
|
||||
const allGroupMembers = [
|
||||
...internalMembers.map(m => ({ id: m.id, isExternal: false, key: `internal-${m.id}` })),
|
||||
...externalMembers.map(m => ({ id: m.id, isExternal: true, key: `external-${m.id}` }))
|
||||
];
|
||||
|
||||
const memberMap = new Map();
|
||||
allGroupMembers.forEach(m => {
|
||||
memberMap.set(m.key, m);
|
||||
});
|
||||
|
||||
if (allGroupMembers.length < 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const rounds = this.generateRoundRobinSchedule(allGroupMembers.map(m => ({ id: m.key })));
|
||||
|
||||
for (let roundIndex = 0; roundIndex < rounds.length; roundIndex++) {
|
||||
for (const [p1Key, p2Key] of rounds[roundIndex]) {
|
||||
if (p1Key && p2Key) {
|
||||
const p1 = memberMap.get(p1Key);
|
||||
const p2 = memberMap.get(p2Key);
|
||||
|
||||
if (p1 && p2) {
|
||||
await TournamentMatch.create({
|
||||
tournamentId,
|
||||
groupId: g.id,
|
||||
round: 'group',
|
||||
player1Id: p1.id,
|
||||
player2Id: p2.id,
|
||||
groupRound: roundIndex + 1,
|
||||
classId: g.classId
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async getGroups(userToken, clubId, tournamentId) {
|
||||
await checkAccess(userToken, clubId);
|
||||
const tournament = await Tournament.findByPk(tournamentId);
|
||||
@@ -1400,7 +1575,9 @@ class TournamentService {
|
||||
setsLost: 0,
|
||||
pointsWon: 0,
|
||||
pointsLost: 0,
|
||||
pointRatio: 0
|
||||
pointRatio: 0,
|
||||
matchesWon: 0,
|
||||
matchesLost: 0
|
||||
};
|
||||
}
|
||||
} else {
|
||||
@@ -1417,7 +1594,9 @@ class TournamentService {
|
||||
setsLost: 0,
|
||||
pointsWon: 0,
|
||||
pointsLost: 0,
|
||||
pointRatio: 0
|
||||
pointRatio: 0,
|
||||
matchesWon: 0,
|
||||
matchesLost: 0
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1433,7 +1612,9 @@ class TournamentService {
|
||||
setsLost: 0,
|
||||
pointsWon: 0,
|
||||
pointsLost: 0,
|
||||
pointRatio: 0
|
||||
pointRatio: 0,
|
||||
matchesWon: 0,
|
||||
matchesLost: 0
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1458,10 +1639,14 @@ class TournamentService {
|
||||
|
||||
if (s1 > s2) {
|
||||
stats[pairing1Key].points += 1;
|
||||
stats[pairing1Key].matchesWon += 1;
|
||||
stats[pairing2Key].points -= 1;
|
||||
stats[pairing2Key].matchesLost += 1;
|
||||
} else if (s2 > s1) {
|
||||
stats[pairing2Key].points += 1;
|
||||
stats[pairing2Key].matchesWon += 1;
|
||||
stats[pairing1Key].points -= 1;
|
||||
stats[pairing1Key].matchesLost += 1;
|
||||
}
|
||||
|
||||
stats[pairing1Key].setsWon += s1;
|
||||
@@ -1473,8 +1658,9 @@ class TournamentService {
|
||||
if (m.tournamentResults && m.tournamentResults.length > 0) {
|
||||
let p1Points = 0, p2Points = 0;
|
||||
for (const r of m.tournamentResults) {
|
||||
p1Points += r.pointsPlayer1 || 0;
|
||||
p2Points += r.pointsPlayer2 || 0;
|
||||
// Verwende absolute Werte, falls negative Werte gespeichert wurden
|
||||
p1Points += Math.abs(r.pointsPlayer1 || 0);
|
||||
p2Points += Math.abs(r.pointsPlayer2 || 0);
|
||||
}
|
||||
stats[pairing1Key].pointsWon += p1Points;
|
||||
stats[pairing1Key].pointsLost += p2Points;
|
||||
@@ -1490,10 +1676,14 @@ class TournamentService {
|
||||
|
||||
if (s1 > s2) {
|
||||
stats[m.player1Id].points += 1;
|
||||
stats[m.player1Id].matchesWon += 1;
|
||||
stats[m.player2Id].points -= 1;
|
||||
stats[m.player2Id].matchesLost += 1;
|
||||
} else if (s2 > s1) {
|
||||
stats[m.player2Id].points += 1;
|
||||
stats[m.player2Id].matchesWon += 1;
|
||||
stats[m.player1Id].points -= 1;
|
||||
stats[m.player1Id].matchesLost += 1;
|
||||
}
|
||||
|
||||
stats[m.player1Id].setsWon += s1;
|
||||
@@ -1505,8 +1695,9 @@ class TournamentService {
|
||||
if (m.tournamentResults && m.tournamentResults.length > 0) {
|
||||
let p1Points = 0, p2Points = 0;
|
||||
for (const r of m.tournamentResults) {
|
||||
p1Points += r.pointsPlayer1 || 0;
|
||||
p2Points += r.pointsPlayer2 || 0;
|
||||
// Verwende absolute Werte, falls negative Werte gespeichert wurden
|
||||
p1Points += Math.abs(r.pointsPlayer1 || 0);
|
||||
p2Points += Math.abs(r.pointsPlayer2 || 0);
|
||||
}
|
||||
stats[m.player1Id].pointsWon += p1Points;
|
||||
stats[m.player1Id].pointsLost += p2Points;
|
||||
@@ -1692,11 +1883,10 @@ class TournamentService {
|
||||
await checkAccess(userToken, clubId);
|
||||
const t = await Tournament.findOne({ where: { id: tournamentId, clubId } });
|
||||
if (!t) throw new Error('Turnier nicht gefunden');
|
||||
// 1) Matches ohne Spieler laden (damit Sequelize nicht automatisch falsche TournamentMember matched)
|
||||
let matches = await TournamentMatch.findAll({
|
||||
where: { tournamentId },
|
||||
include: [
|
||||
{ model: TournamentMember, as: 'player1', required: false, include: [{ model: Member, as: 'member' }] },
|
||||
{ model: TournamentMember, as: 'player2', required: false, include: [{ model: Member, as: 'member' }] },
|
||||
{ model: TournamentResult, as: 'tournamentResults' }
|
||||
],
|
||||
order: [
|
||||
@@ -1725,93 +1915,85 @@ class TournamentService {
|
||||
if (rA !== rB) return rA - rB;
|
||||
return (a.id ?? 0) - (b.id ?? 0);
|
||||
});
|
||||
|
||||
// WICHTIG: Lade alle gültigen TournamentMember-IDs und ExternalTournamentParticipant-IDs für dieses Turnier
|
||||
// um zu prüfen, ob ein geladenes TournamentMember tatsächlich zu diesem Turnier gehört
|
||||
const validTournamentMemberIds = new Set();
|
||||
const allTournamentMembers = await TournamentMember.findAll({
|
||||
where: { tournamentId },
|
||||
attributes: ['id']
|
||||
});
|
||||
allTournamentMembers.forEach(tm => validTournamentMemberIds.add(tm.id));
|
||||
|
||||
// Lade auch alle ExternalTournamentParticipant-IDs, um zu prüfen, ob eine ID zu einem externen Teilnehmer gehört
|
||||
const validExternalParticipantIds = new Set();
|
||||
const allExternalParticipants = await ExternalTournamentParticipant.findAll({
|
||||
where: { tournamentId },
|
||||
attributes: ['id']
|
||||
});
|
||||
allExternalParticipants.forEach(ep => validExternalParticipantIds.add(ep.id));
|
||||
|
||||
// Prüfe für jedes Match, ob die player1Id/player2Id zu einem externen Teilnehmer gehört
|
||||
// Wenn ja, setze das geladene TournamentMember auf null (auch wenn es zufällig geladen wurde)
|
||||
matches.forEach(match => {
|
||||
if (match.player1Id && validExternalParticipantIds.has(match.player1Id)) {
|
||||
// Diese ID gehört zu einem externen Teilnehmer, also sollte player1 null sein
|
||||
match.player1 = null;
|
||||
} else if (match.player1 && !validTournamentMemberIds.has(match.player1.id)) {
|
||||
// Das geladene TournamentMember gehört nicht zu diesem Turnier
|
||||
match.player1 = null;
|
||||
}
|
||||
|
||||
if (match.player2Id && validExternalParticipantIds.has(match.player2Id)) {
|
||||
// Diese ID gehört zu einem externen Teilnehmer, also sollte player2 null sein
|
||||
match.player2 = null;
|
||||
} else if (match.player2 && !validTournamentMemberIds.has(match.player2.id)) {
|
||||
// Das geladene TournamentMember gehört nicht zu diesem Turnier
|
||||
match.player2 = null;
|
||||
}
|
||||
});
|
||||
|
||||
// Lade externe Teilnehmer für Matches, bei denen player1 oder player2 null ist
|
||||
const player1Ids = matches.filter(m => !m.player1 && m.player1Id).map(m => m.player1Id);
|
||||
const player2Ids = matches.filter(m => !m.player2 && m.player2Id).map(m => m.player2Id);
|
||||
const externalPlayerIds = [...new Set([...player1Ids, ...player2Ids])];
|
||||
|
||||
if (externalPlayerIds.length > 0) {
|
||||
const externalPlayers = await ExternalTournamentParticipant.findAll({
|
||||
where: {
|
||||
id: { [Op.in]: externalPlayerIds },
|
||||
tournamentId
|
||||
}
|
||||
});
|
||||
|
||||
const externalPlayerMap = new Map();
|
||||
externalPlayers.forEach(ep => {
|
||||
externalPlayerMap.set(ep.id, ep);
|
||||
});
|
||||
|
||||
// Ersetze null player1/player2 mit externen Teilnehmern
|
||||
// WICHTIG: Stelle sicher, dass externe Teilnehmer KEIN member-Feld haben
|
||||
matches.forEach(match => {
|
||||
if (!match.player1 && externalPlayerMap.has(match.player1Id)) {
|
||||
const externalPlayer = externalPlayerMap.get(match.player1Id);
|
||||
// Erstelle ein sauberes Objekt ohne member-Feld
|
||||
match.player1 = {
|
||||
id: externalPlayer.id,
|
||||
firstName: externalPlayer.firstName,
|
||||
lastName: externalPlayer.lastName,
|
||||
club: externalPlayer.club,
|
||||
gender: externalPlayer.gender,
|
||||
birthDate: externalPlayer.birthDate
|
||||
};
|
||||
}
|
||||
if (!match.player2 && externalPlayerMap.has(match.player2Id)) {
|
||||
const externalPlayer = externalPlayerMap.get(match.player2Id);
|
||||
// Erstelle ein sauberes Objekt ohne member-Feld
|
||||
match.player2 = {
|
||||
id: externalPlayer.id,
|
||||
firstName: externalPlayer.firstName,
|
||||
lastName: externalPlayer.lastName,
|
||||
club: externalPlayer.club,
|
||||
gender: externalPlayer.gender,
|
||||
birthDate: externalPlayer.birthDate
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// 2) Alle im Turnier verwendeten Spieler-IDs einsammeln
|
||||
const allPlayerIds = new Set();
|
||||
for (const m of matches) {
|
||||
if (m.player1Id) allPlayerIds.add(m.player1Id);
|
||||
if (m.player2Id) allPlayerIds.add(m.player2Id);
|
||||
}
|
||||
|
||||
return matches;
|
||||
|
||||
const idsArray = Array.from(allPlayerIds);
|
||||
|
||||
// Wenn gar keine Spieler-IDs vorhanden sind, können wir direkt zurückgeben
|
||||
if (idsArray.length === 0) {
|
||||
const plain = matches.map(m => m.toJSON());
|
||||
return plain;
|
||||
}
|
||||
|
||||
// 3) Passende interne Turnierteilnehmer + Member laden
|
||||
const internalMembers = await TournamentMember.findAll({
|
||||
where: {
|
||||
tournamentId,
|
||||
id: { [Op.in]: idsArray }
|
||||
},
|
||||
include: [{ model: Member, as: 'member' }]
|
||||
});
|
||||
const internalById = new Map(internalMembers.map(m => [m.id, m]));
|
||||
|
||||
// 4) Passende externe Teilnehmer laden
|
||||
const externalParticipants = await ExternalTournamentParticipant.findAll({
|
||||
where: {
|
||||
tournamentId,
|
||||
id: { [Op.in]: idsArray }
|
||||
}
|
||||
});
|
||||
const externalById = new Map(externalParticipants.map(e => [e.id, e]));
|
||||
|
||||
// 5) Finale, saubere Struktur erzeugen
|
||||
const finalMatches = matches.map(m => {
|
||||
const plain = m.toJSON();
|
||||
|
||||
// player1
|
||||
const p1Id = plain.player1Id;
|
||||
if (p1Id && internalById.has(p1Id)) {
|
||||
plain.player1 = internalById.get(p1Id).toJSON();
|
||||
} else if (p1Id && externalById.has(p1Id)) {
|
||||
const ep = externalById.get(p1Id);
|
||||
plain.player1 = {
|
||||
id: ep.id,
|
||||
firstName: ep.firstName,
|
||||
lastName: ep.lastName,
|
||||
club: ep.club,
|
||||
gender: ep.gender,
|
||||
birthDate: ep.birthDate
|
||||
};
|
||||
} else {
|
||||
plain.player1 = null;
|
||||
}
|
||||
|
||||
// player2
|
||||
const p2Id = plain.player2Id;
|
||||
if (p2Id && internalById.has(p2Id)) {
|
||||
plain.player2 = internalById.get(p2Id).toJSON();
|
||||
} else if (p2Id && externalById.has(p2Id)) {
|
||||
const ep = externalById.get(p2Id);
|
||||
plain.player2 = {
|
||||
id: ep.id,
|
||||
firstName: ep.firstName,
|
||||
lastName: ep.lastName,
|
||||
club: ep.club,
|
||||
gender: ep.gender,
|
||||
birthDate: ep.birthDate
|
||||
};
|
||||
} else {
|
||||
plain.player2 = null;
|
||||
}
|
||||
|
||||
return plain;
|
||||
});
|
||||
|
||||
return finalMatches;
|
||||
}
|
||||
|
||||
// 12. Satz-Ergebnis hinzufügen/überschreiben
|
||||
@@ -2059,21 +2241,25 @@ class TournamentService {
|
||||
const stats = {};
|
||||
// Interne Teilnehmer
|
||||
for (const tm of g.tournamentGroupMembers || []) {
|
||||
stats[tm.id] = { member: tm, points: 0, setsWon: 0, setsLost: 0, pointsWon: 0, pointsLost: 0, isExternal: false };
|
||||
stats[tm.id] = { member: tm, points: 0, setsWon: 0, setsLost: 0, pointsWon: 0, pointsLost: 0, isExternal: false, matchesWon: 0, matchesLost: 0 };
|
||||
}
|
||||
// Externe Teilnehmer
|
||||
for (const ext of g.externalGroupMembers || []) {
|
||||
stats[ext.id] = { member: ext, points: 0, setsWon: 0, setsLost: 0, pointsWon: 0, pointsLost: 0, isExternal: true };
|
||||
stats[ext.id] = { member: ext, points: 0, setsWon: 0, setsLost: 0, pointsWon: 0, pointsLost: 0, isExternal: true, matchesWon: 0, matchesLost: 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 += 1; // Sieger bekommt +1
|
||||
stats[m.player1Id].matchesWon += 1;
|
||||
stats[m.player2Id].points -= 1; // Verlierer bekommt -1
|
||||
stats[m.player2Id].matchesLost += 1;
|
||||
} else {
|
||||
stats[m.player2Id].points += 1; // Sieger bekommt +1
|
||||
stats[m.player2Id].matchesWon += 1;
|
||||
stats[m.player1Id].points -= 1; // Verlierer bekommt -1
|
||||
stats[m.player1Id].matchesLost += 1;
|
||||
}
|
||||
stats[m.player1Id].setsWon += p1;
|
||||
stats[m.player1Id].setsLost += p2;
|
||||
@@ -2084,8 +2270,9 @@ class TournamentService {
|
||||
if (m.tournamentResults && m.tournamentResults.length > 0) {
|
||||
let p1Points = 0, p2Points = 0;
|
||||
for (const r of m.tournamentResults) {
|
||||
p1Points += r.pointsPlayer1 || 0;
|
||||
p2Points += r.pointsPlayer2 || 0;
|
||||
// Verwende absolute Werte, falls negative Werte gespeichert wurden
|
||||
p1Points += Math.abs(r.pointsPlayer1 || 0);
|
||||
p2Points += Math.abs(r.pointsPlayer2 || 0);
|
||||
}
|
||||
stats[m.player1Id].pointsWon += p1Points;
|
||||
stats[m.player1Id].pointsLost += p2Points;
|
||||
@@ -2503,55 +2690,77 @@ class TournamentService {
|
||||
throw new Error('Turnier nicht gefunden');
|
||||
}
|
||||
|
||||
// Hole bestehende Gruppen
|
||||
const existingGroups = await TournamentGroup.findAll({
|
||||
where: { tournamentId },
|
||||
order: [['id', 'ASC']]
|
||||
});
|
||||
console.log(`[assignParticipantToGroup] Found ${existingGroups.length} existing groups`);
|
||||
|
||||
// Berechne dbGroupId
|
||||
let dbGroupId = null;
|
||||
if (groupNumber != null) {
|
||||
// Stelle sicher, dass genug Gruppen existieren
|
||||
if (groupNumber > existingGroups.length) {
|
||||
console.log(`[assignParticipantToGroup] Creating ${groupNumber - existingGroups.length} new groups`);
|
||||
// Erstelle fehlende Gruppen
|
||||
for (let i = existingGroups.length; i < groupNumber; i++) {
|
||||
const grp = await TournamentGroup.create({ tournamentId });
|
||||
existingGroups.push(grp);
|
||||
}
|
||||
}
|
||||
// Mapping von groupNumber (1-based) zu groupId
|
||||
dbGroupId = existingGroups[groupNumber - 1].id;
|
||||
console.log(`[assignParticipantToGroup] Mapped groupNumber ${groupNumber} to dbGroupId ${dbGroupId}`);
|
||||
}
|
||||
|
||||
// Aktualisiere den Teilnehmer
|
||||
// Hole zunächst den Teilnehmer, um seine classId zu erhalten
|
||||
let participantClassId = null;
|
||||
let internalMember = null;
|
||||
let externalMember = null;
|
||||
|
||||
if (isExternal) {
|
||||
const externalMember = await ExternalTournamentParticipant.findOne({
|
||||
externalMember = await ExternalTournamentParticipant.findOne({
|
||||
where: { id: participantId, tournamentId }
|
||||
});
|
||||
|
||||
if (!externalMember) {
|
||||
throw new Error(`Externer Teilnehmer mit ID ${participantId} nicht gefunden`);
|
||||
}
|
||||
|
||||
console.log(`[assignParticipantToGroup] Updating external member ${participantId} to groupId ${dbGroupId}`);
|
||||
await ExternalTournamentParticipant.update(
|
||||
{ groupId: dbGroupId },
|
||||
{ where: { id: participantId, tournamentId } }
|
||||
);
|
||||
participantClassId = externalMember.classId;
|
||||
} else {
|
||||
const internalMember = await TournamentMember.findOne({
|
||||
internalMember = await TournamentMember.findOne({
|
||||
where: { id: participantId, tournamentId }
|
||||
});
|
||||
|
||||
if (!internalMember) {
|
||||
throw new Error(`Interner Teilnehmer mit ID ${participantId} nicht gefunden`);
|
||||
}
|
||||
|
||||
console.log(`[assignParticipantToGroup] Updating internal member ${participantId} (memberId=${internalMember.memberId}) to groupId ${dbGroupId}`);
|
||||
participantClassId = internalMember.classId;
|
||||
}
|
||||
|
||||
// Hole bestehende Gruppen für diese Klasse (oder ohne Klasse, wenn classId null)
|
||||
const whereClause = { tournamentId };
|
||||
if (participantClassId === null) {
|
||||
whereClause.classId = { [Op.is]: null };
|
||||
} else {
|
||||
whereClause.classId = participantClassId;
|
||||
}
|
||||
const existingGroups = await TournamentGroup.findAll({
|
||||
where: whereClause,
|
||||
order: [['id', 'ASC']]
|
||||
});
|
||||
console.log(`[assignParticipantToGroup] Found ${existingGroups.length} existing groups for classId ${participantClassId}`);
|
||||
|
||||
// Berechne dbGroupId
|
||||
let dbGroupId = null;
|
||||
if (groupNumber != null && groupNumber > 0) {
|
||||
// Stelle sicher, dass genug Gruppen existieren
|
||||
if (groupNumber > existingGroups.length) {
|
||||
console.log(`[assignParticipantToGroup] Creating ${groupNumber - existingGroups.length} new groups for classId ${participantClassId}`);
|
||||
// Erstelle fehlende Gruppen
|
||||
for (let i = existingGroups.length; i < groupNumber; i++) {
|
||||
const grp = await TournamentGroup.create({
|
||||
tournamentId,
|
||||
classId: participantClassId
|
||||
});
|
||||
existingGroups.push(grp);
|
||||
}
|
||||
}
|
||||
// Mapping von groupNumber (1-based) zu groupId innerhalb dieser Klasse
|
||||
if (existingGroups.length >= groupNumber) {
|
||||
dbGroupId = existingGroups[groupNumber - 1].id;
|
||||
console.log(`[assignParticipantToGroup] Mapped groupNumber ${groupNumber} to dbGroupId ${dbGroupId} for classId ${participantClassId}`);
|
||||
} else {
|
||||
throw new Error(`Ungültiges groupNumber ${groupNumber} - es gibt nur ${existingGroups.length} Gruppen für classId ${participantClassId}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Aktualisiere den Teilnehmer
|
||||
if (isExternal) {
|
||||
console.log(`[assignParticipantToGroup] Updating external member ${participantId} (classId=${participantClassId}) to groupId ${dbGroupId}`);
|
||||
await ExternalTournamentParticipant.update(
|
||||
{ groupId: dbGroupId },
|
||||
{ where: { id: participantId, tournamentId } }
|
||||
);
|
||||
} else {
|
||||
console.log(`[assignParticipantToGroup] Updating internal member ${participantId} (memberId=${internalMember.clubMemberId || 'N/A'}, classId=${participantClassId}) to groupId ${dbGroupId}`);
|
||||
await TournamentMember.update(
|
||||
{ groupId: dbGroupId },
|
||||
{ where: { id: participantId, tournamentId } }
|
||||
@@ -2570,9 +2779,16 @@ class TournamentService {
|
||||
await TournamentMatch.destroy({ where: { tournamentId } });
|
||||
await TournamentGroup.destroy({ where: { tournamentId } });
|
||||
}
|
||||
async resetMatches(userToken, clubId, tournamentId) {
|
||||
async resetMatches(userToken, clubId, tournamentId, classId = null) {
|
||||
await checkAccess(userToken, clubId);
|
||||
await TournamentMatch.destroy({ where: { tournamentId } });
|
||||
const where = {
|
||||
tournamentId,
|
||||
round: 'group'
|
||||
};
|
||||
if (classId != null) {
|
||||
where.classId = Number(classId);
|
||||
}
|
||||
await TournamentMatch.destroy({ where });
|
||||
}
|
||||
|
||||
async removeParticipant(userToken, clubId, tournamentId, participantId) {
|
||||
|
||||
@@ -90,13 +90,16 @@
|
||||
<td><strong>G{{ String.fromCharCode(96 + group.groupNumber) }}{{ idx + 1 }}</strong></td>
|
||||
<td>{{ pl.position }}.</td>
|
||||
<td><span v-if="pl.seeded" class="seeded-star">★</span>{{ pl.name }}</td>
|
||||
<td>{{ pl.points }}</td>
|
||||
<td>{{ (pl.matchesWon || 0) * 2 }}:{{ (pl.matchesLost || 0) * 2 }}</td>
|
||||
<td>{{ pl.setsWon }}:{{ pl.setsLost }}</td>
|
||||
<td>
|
||||
{{ pl.setDiff >= 0 ? '+' + pl.setDiff : pl.setDiff }}
|
||||
</td>
|
||||
<td>
|
||||
{{ pl.pointsWon }}:{{ pl.pointsLost }} ({{ (pl.pointsWon - pl.pointsLost) >= 0 ? '+' + (pl.pointsWon - pl.pointsLost) : (pl.pointsWon - pl.pointsLost) }})
|
||||
{{ Math.abs(pl.pointsWon || 0) }}:{{ Math.abs(pl.pointsLost || 0) }}
|
||||
<span v-if="(Math.abs(pl.pointsWon || 0) - Math.abs(pl.pointsLost || 0)) !== 0" class="points-diff">
|
||||
({{ (Math.abs(pl.pointsWon || 0) - Math.abs(pl.pointsLost || 0)) >= 0 ? '+' : '' }}{{ Math.abs(pl.pointsWon || 0) - Math.abs(pl.pointsLost || 0) }})
|
||||
</span>
|
||||
</td>
|
||||
<td v-for="(opponent, oppIdx) in groupRankings[group.groupId]"
|
||||
:key="`match-${pl.id}-${opponent.id}`"
|
||||
@@ -116,12 +119,12 @@
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
<div v-if="!matches.some(m => m.round === 'group')" class="reset-controls" style="margin-top:1rem">
|
||||
<div v-if="filteredGroupMatches.length === 0" class="reset-controls" style="margin-top:1rem">
|
||||
<button @click="$emit('create-matches')" class="btn-primary">
|
||||
▶️ Gruppenspiele berechnen
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="matches.some(m => m.round === 'group')" class="reset-controls" style="margin-top:1rem">
|
||||
<div v-if="filteredGroupMatches.length > 0" class="reset-controls" style="margin-top:1rem">
|
||||
<button @click="$emit('reset-matches')" class="trash-btn">
|
||||
🗑️ {{ $t('tournaments.resetGroupMatches') }}
|
||||
</button>
|
||||
@@ -207,7 +210,34 @@ export default {
|
||||
'create-matches',
|
||||
'highlight-match'
|
||||
],
|
||||
computed: {
|
||||
filteredGroupMatches() {
|
||||
return this.filterMatchesByClass(this.matches.filter(m => m.round === 'group'));
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
filterMatchesByClass(matches) {
|
||||
// Wenn keine Klasse ausgewählt ist (null), zeige alle
|
||||
if (this.selectedViewClass === null || this.selectedViewClass === undefined) {
|
||||
return matches;
|
||||
}
|
||||
// Wenn "Ohne Klasse" ausgewählt ist
|
||||
if (this.selectedViewClass === '__none__' || this.selectedViewClass === 'null') {
|
||||
return matches.filter(m => m.classId === null || m.classId === undefined);
|
||||
}
|
||||
// Filtere nach der ausgewählten Klasse
|
||||
const selectedId = Number(this.selectedViewClass);
|
||||
if (Number.isNaN(selectedId)) {
|
||||
return matches;
|
||||
}
|
||||
return matches.filter(m => {
|
||||
const matchClassId = m.classId;
|
||||
if (matchClassId === null || matchClassId === undefined) {
|
||||
return false;
|
||||
}
|
||||
return Number(matchClassId) === selectedId;
|
||||
});
|
||||
},
|
||||
shouldShowClass(classId) {
|
||||
// Wenn keine Klasse ausgewählt ist (null), zeige alle
|
||||
if (this.selectedViewClass === null || this.selectedViewClass === undefined) {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
:selected-date="selectedDate"
|
||||
@update:modelValue="$emit('update:selectedViewClass', $event)"
|
||||
/>
|
||||
<section v-if="groupMatches.length" class="group-matches">
|
||||
<section v-if="filteredGroupMatches.length" class="group-matches">
|
||||
<h4>{{ $t('tournaments.groupMatches') }}</h4>
|
||||
<table>
|
||||
<thead>
|
||||
@@ -21,7 +21,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="m in groupMatches" :key="m.id" :data-match-id="m.id" :class="{ 'active-match': activeMatchId === m.id, 'match-finished': m.isFinished, 'match-live': m.isActive }" @click="$emit('update:activeMatchId', m.id)">
|
||||
<tr v-for="m in filteredGroupMatches" :key="m.id" :data-match-id="m.id" :class="{ 'active-match': activeMatchId === m.id, 'match-finished': m.isFinished, 'match-live': m.isActive }" @click="$emit('update:activeMatchId', m.id)">
|
||||
<td>{{ m.groupRound }}</td>
|
||||
<td>
|
||||
<template v-if="getGroupClassName(m.groupId)">
|
||||
@@ -95,22 +95,22 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
<div v-if="participants.length > 1 && !groupMatches.length && !knockoutMatches.length" class="start-matches" style="margin-top:1.5rem">
|
||||
<div v-if="participants.length > 1 && !filteredGroupMatches.length && !filteredKnockoutMatches.length" class="start-matches" style="margin-top:1.5rem">
|
||||
<button @click="$emit('start-matches')">
|
||||
{{ $t('tournaments.createMatches') }}
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="canStartKnockout && !showKnockout && getTotalNumberOfGroups > 1" class="ko-start">
|
||||
<div v-if="canStartKnockout && !showKnockout && numberOfGroupsForSelectedClass > 1" class="ko-start">
|
||||
<button @click="$emit('start-knockout')">
|
||||
{{ $t('tournaments.startKORound') }}
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="showKnockout && canResetKnockout && getTotalNumberOfGroups > 1" class="ko-reset" style="margin-top:1rem">
|
||||
<div v-if="showKnockout && canResetKnockout && numberOfGroupsForSelectedClass > 1" class="ko-reset" style="margin-top:1rem">
|
||||
<button @click="$emit('reset-knockout')" class="trash-btn">
|
||||
🗑️ {{ $t('tournaments.deleteKORound') }}
|
||||
</button>
|
||||
</div>
|
||||
<section v-if="showKnockout && getTotalNumberOfGroups > 1" class="ko-round">
|
||||
<section v-if="showKnockout && numberOfGroupsForSelectedClass > 1 && filteredKnockoutMatches.length" class="ko-round">
|
||||
<h4>{{ $t('tournaments.koRound') }}</h4>
|
||||
<table>
|
||||
<thead>
|
||||
@@ -124,7 +124,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="m in knockoutMatches" :key="m.id" :class="{ 'active-match': activeMatchId === m.id, 'match-finished': m.isFinished, 'match-live': m.isActive }" @click="$emit('update:activeMatchId', m.id)">
|
||||
<tr v-for="m in filteredKnockoutMatches" :key="m.id" :class="{ 'active-match': activeMatchId === m.id, 'match-finished': m.isFinished, 'match-live': m.isActive }" @click="$emit('update:activeMatchId', m.id)">
|
||||
<td>{{ getKnockoutMatchClassName(m) }}</td>
|
||||
<td>{{ m.round }}</td>
|
||||
<td>
|
||||
@@ -188,9 +188,9 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
<section v-if="Object.keys(groupedRankingList).length > 0" class="ranking">
|
||||
<section v-if="Object.keys(filteredGroupedRankingList).length > 0" class="ranking">
|
||||
<h4>Rangliste</h4>
|
||||
<template v-for="(classKey, idx) in Object.keys(groupedRankingList).sort((a, b) => {
|
||||
<template v-for="(classKey, idx) in Object.keys(filteredGroupedRankingList).sort((a, b) => {
|
||||
const aNum = a === 'null' ? 999999 : parseInt(a);
|
||||
const bNum = b === 'null' ? 999999 : parseInt(b);
|
||||
return aNum - bNum;
|
||||
@@ -205,7 +205,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(entry, entryIdx) in groupedRankingList[classKey].filter(e => Number(e.position) <= 3)" :key="`${entry.member.id}-${entryIdx}`">
|
||||
<tr v-for="(entry, entryIdx) in filteredGroupedRankingList[classKey].filter(e => Number(e.position) <= 3)" :key="`${entry.member.id}-${entryIdx}`">
|
||||
<td>{{ entry.position }}.</td>
|
||||
<td>
|
||||
{{ entry.member.firstName }}
|
||||
@@ -289,6 +289,71 @@ export default {
|
||||
required: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
filteredGroupMatches() {
|
||||
return this.filterMatchesByClass(this.groupMatches);
|
||||
},
|
||||
filteredKnockoutMatches() {
|
||||
return this.filterMatchesByClass(this.knockoutMatches);
|
||||
},
|
||||
filteredGroupedRankingList() {
|
||||
// Wenn keine Klasse ausgewählt ist (null), zeige alle
|
||||
if (this.selectedViewClass === null || this.selectedViewClass === undefined) {
|
||||
return this.groupedRankingList;
|
||||
}
|
||||
// Wenn "Ohne Klasse" ausgewählt ist
|
||||
if (this.selectedViewClass === '__none__' || this.selectedViewClass === 'null') {
|
||||
const result = {};
|
||||
if (this.groupedRankingList['null']) {
|
||||
result['null'] = this.groupedRankingList['null'];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
// Filtere nach der ausgewählten Klasse
|
||||
const selectedId = Number(this.selectedViewClass);
|
||||
if (Number.isNaN(selectedId)) {
|
||||
return this.groupedRankingList;
|
||||
}
|
||||
const result = {};
|
||||
const classKey = String(selectedId);
|
||||
if (this.groupedRankingList[classKey]) {
|
||||
result[classKey] = this.groupedRankingList[classKey];
|
||||
}
|
||||
return result;
|
||||
},
|
||||
numberOfGroupsForSelectedClass() {
|
||||
// Zähle direkt die Gruppen für die ausgewählte Klasse
|
||||
// Nur Stage 1 Gruppen (stageId null/undefined) zählen
|
||||
// Und nur Gruppen mit mindestens einem Teilnehmer
|
||||
let groupsToCount = this.groups.filter(g =>
|
||||
(!g.stageId || g.stageId === null || g.stageId === undefined) &&
|
||||
g.participants && Array.isArray(g.participants) && g.participants.length > 0
|
||||
);
|
||||
|
||||
// Wenn keine Klasse ausgewählt ist, zähle alle Stage 1 Gruppen
|
||||
if (this.selectedViewClass === null || this.selectedViewClass === undefined) {
|
||||
return groupsToCount.length;
|
||||
}
|
||||
|
||||
// Wenn "Ohne Klasse" ausgewählt ist
|
||||
if (this.selectedViewClass === '__none__' || this.selectedViewClass === 'null') {
|
||||
return groupsToCount.filter(g => g.classId === null || g.classId === undefined).length;
|
||||
}
|
||||
|
||||
// Filtere nach der ausgewählten Klasse
|
||||
const selectedId = Number(this.selectedViewClass);
|
||||
if (Number.isNaN(selectedId)) {
|
||||
return groupsToCount.length;
|
||||
}
|
||||
|
||||
return groupsToCount.filter(g => {
|
||||
if (g.classId === null || g.classId === undefined) {
|
||||
return false;
|
||||
}
|
||||
return Number(g.classId) === selectedId;
|
||||
}).length;
|
||||
}
|
||||
},
|
||||
emits: [
|
||||
'update:selectedViewClass',
|
||||
'update:activeMatchId',
|
||||
@@ -305,6 +370,28 @@ export default {
|
||||
'reset-knockout'
|
||||
],
|
||||
methods: {
|
||||
filterMatchesByClass(matches) {
|
||||
// Wenn keine Klasse ausgewählt ist (null), zeige alle
|
||||
if (this.selectedViewClass === null || this.selectedViewClass === undefined) {
|
||||
return matches;
|
||||
}
|
||||
// Wenn "Ohne Klasse" ausgewählt ist
|
||||
if (this.selectedViewClass === '__none__' || this.selectedViewClass === 'null') {
|
||||
return matches.filter(m => m.classId === null || m.classId === undefined);
|
||||
}
|
||||
// Filtere nach der ausgewählten Klasse
|
||||
const selectedId = Number(this.selectedViewClass);
|
||||
if (Number.isNaN(selectedId)) {
|
||||
return matches;
|
||||
}
|
||||
return matches.filter(m => {
|
||||
const matchClassId = m.classId;
|
||||
if (matchClassId === null || matchClassId === undefined) {
|
||||
return false;
|
||||
}
|
||||
return Number(matchClassId) === selectedId;
|
||||
});
|
||||
},
|
||||
getGroupClassName(groupId) {
|
||||
if (!groupId) return '';
|
||||
const group = this.groups.find(g => g.groupId === groupId);
|
||||
|
||||
@@ -175,7 +175,7 @@
|
||||
@randomize-groups="randomizeGroups()"
|
||||
@reset-groups="resetGroups()"
|
||||
@reset-matches="resetMatches()"
|
||||
@create-matches="startMatches()"
|
||||
@create-matches="createMatches()"
|
||||
@highlight-match="highlightMatch"
|
||||
/>
|
||||
|
||||
@@ -487,9 +487,11 @@ export default {
|
||||
setsWon: p.setsWon || 0,
|
||||
setsLost: p.setsLost || 0,
|
||||
setDiff: p.setDiff || 0,
|
||||
pointsWon: p.pointsWon || 0,
|
||||
pointsLost: p.pointsLost || 0,
|
||||
pointRatio: p.pointRatio || 0
|
||||
pointsWon: Math.abs(p.pointsWon || 0),
|
||||
pointsLost: Math.abs(p.pointsLost || 0),
|
||||
pointRatio: p.pointRatio || 0,
|
||||
matchesWon: p.matchesWon || 0,
|
||||
matchesLost: p.matchesLost || 0
|
||||
}));
|
||||
});
|
||||
return rankings;
|
||||
@@ -1314,6 +1316,41 @@ export default {
|
||||
|
||||
this.showKnockout = this.matches.some(m => m.round !== 'group');
|
||||
},
|
||||
|
||||
async loadMatches() {
|
||||
// Lade nur die Matches, ohne die Teilnehmer-Daten zu überschreiben
|
||||
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 => {
|
||||
// Verwende groupId aus dem Backend, falls vorhanden, sonst aus den Spielern
|
||||
const matchGroupId = m.groupId || m.player1?.groupId || m.player2?.groupId;
|
||||
|
||||
// Stelle sicher, dass groupRound vorhanden ist (kann als group_round vom Backend kommen)
|
||||
const groupRound = m.groupRound || m.group_round || 0;
|
||||
|
||||
const groupNumber = grpMap[matchGroupId] || 0;
|
||||
|
||||
return {
|
||||
...m,
|
||||
groupId: matchGroupId,
|
||||
groupNumber: groupNumber,
|
||||
groupRound: groupRound,
|
||||
resultInput: '',
|
||||
isActive: m.isActive || false
|
||||
};
|
||||
});
|
||||
|
||||
// Setze Kollaps-Status: ausgeklappt wenn keine Spiele, eingeklappt wenn Spiele vorhanden
|
||||
this.showParticipants = this.matches.length === 0;
|
||||
|
||||
this.showKnockout = this.matches.some(m => m.round !== 'group');
|
||||
},
|
||||
|
||||
async handleTournamentChanged(data) {
|
||||
if (!data || !data.tournamentId) {
|
||||
@@ -1855,6 +1892,25 @@ export default {
|
||||
await this.loadTournamentData();
|
||||
},
|
||||
|
||||
async createMatches() {
|
||||
if (!this.isGroupTournament) {
|
||||
return;
|
||||
}
|
||||
if (!this.groups.length) {
|
||||
await this.createGroups();
|
||||
}
|
||||
// Übergebe classId, wenn eine Klasse ausgewählt ist (nicht '__none__' oder null)
|
||||
const classId = (this.selectedViewClass && this.selectedViewClass !== '__none__' && this.selectedViewClass !== null)
|
||||
? this.selectedViewClass
|
||||
: null;
|
||||
await apiClient.post('/tournament/matches/create', {
|
||||
clubId: this.currentClub,
|
||||
tournamentId: this.selectedDate,
|
||||
classId: classId
|
||||
});
|
||||
await this.loadTournamentData();
|
||||
},
|
||||
|
||||
async onModusChange() {
|
||||
const type = this.isGroupTournament ? 'groups' : 'knockout';
|
||||
const desired = Math.max(1, parseInt(String(this.numberOfGroups), 10) || 1);
|
||||
@@ -1877,9 +1933,14 @@ export default {
|
||||
},
|
||||
|
||||
async resetMatches() {
|
||||
// Übergebe classId, wenn eine Klasse ausgewählt ist (nicht '__none__' oder null)
|
||||
const classId = (this.selectedViewClass && this.selectedViewClass !== '__none__' && this.selectedViewClass !== null)
|
||||
? this.selectedViewClass
|
||||
: null;
|
||||
await apiClient.post('/tournament/matches/reset', {
|
||||
clubId: this.currentClub,
|
||||
tournamentId: this.selectedDate
|
||||
tournamentId: this.selectedDate,
|
||||
classId: classId
|
||||
});
|
||||
await this.loadTournamentData();
|
||||
},
|
||||
@@ -2201,8 +2262,17 @@ export default {
|
||||
|
||||
console.log('[updateParticipantGroup] Updating participant:', participant.id, 'to groupNumber:', groupNumber, 'isExternal:', participant.isExternal);
|
||||
|
||||
// Aktualisiere lokal
|
||||
participant.groupNumber = groupNumber;
|
||||
// Speichere die alte groupNumber für den Fall eines Fehlers
|
||||
const oldGroupNumber = participant.groupNumber;
|
||||
|
||||
// Aktualisiere lokal sofort für responsive UI (mit Vue-Reaktivität)
|
||||
if (this.$set) {
|
||||
// Vue 2
|
||||
this.$set(participant, 'groupNumber', groupNumber);
|
||||
} else {
|
||||
// Vue 3
|
||||
participant.groupNumber = groupNumber;
|
||||
}
|
||||
|
||||
// Sende nur diesen einen Teilnehmer an Backend
|
||||
try {
|
||||
@@ -2221,17 +2291,86 @@ export default {
|
||||
this.groups = [...response.data];
|
||||
console.log('[updateParticipantGroup] Updated groups:', this.groups);
|
||||
} else {
|
||||
// Fallback: Lade Daten neu
|
||||
await this.loadTournamentData();
|
||||
// Fallback: Lade Gruppen neu
|
||||
const gRes = await apiClient.get('/tournament/groups', {
|
||||
params: {
|
||||
clubId: this.currentClub,
|
||||
tournamentId: this.selectedDate
|
||||
}
|
||||
});
|
||||
const groupsData = Array.isArray(gRes.data)
|
||||
? gRes.data
|
||||
: (Array.isArray(gRes.data?.groups) ? gRes.data.groups : []);
|
||||
this.groups = [...groupsData];
|
||||
}
|
||||
// Force Vue update, um sicherzustellen, dass die Gruppenübersicht aktualisiert wird
|
||||
this.$forceUpdate();
|
||||
|
||||
// Aktualisiere auch die groupId des Teilnehmers basierend auf der neuen groupNumber
|
||||
if (groupNumber !== null) {
|
||||
const newGroup = this.groups.find(g => g.groupNumber === groupNumber);
|
||||
if (newGroup) {
|
||||
if (this.$set) {
|
||||
this.$set(participant, 'groupId', newGroup.groupId);
|
||||
} else {
|
||||
participant.groupId = newGroup.groupId;
|
||||
}
|
||||
} else {
|
||||
// Gruppe nicht gefunden, setze groupId auf null
|
||||
if (this.$set) {
|
||||
this.$set(participant, 'groupId', null);
|
||||
} else {
|
||||
participant.groupId = null;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// groupNumber ist null, also auch groupId auf null
|
||||
if (this.$set) {
|
||||
this.$set(participant, 'groupId', null);
|
||||
} else {
|
||||
participant.groupId = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Lade Matches neu, da sich die Gruppenzuordnung geändert hat
|
||||
await this.loadMatches();
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Aktualisieren der Gruppe:', error);
|
||||
// Bei Fehler: Lade Daten neu und setze groupNumber zurück
|
||||
participant.groupNumber = participant.groupId ? this.groups.find(g => g.groupId === participant.groupId)?.groupNumber || null : null;
|
||||
await this.loadTournamentData();
|
||||
this.$forceUpdate();
|
||||
// Bei Fehler: Setze groupNumber zurück auf den alten Wert
|
||||
if (this.$set) {
|
||||
this.$set(participant, 'groupNumber', oldGroupNumber);
|
||||
} else {
|
||||
participant.groupNumber = oldGroupNumber;
|
||||
}
|
||||
// Lade Gruppen neu, um sicherzustellen, dass groupId korrekt ist
|
||||
try {
|
||||
const gRes = await apiClient.get('/tournament/groups', {
|
||||
params: {
|
||||
clubId: this.currentClub,
|
||||
tournamentId: this.selectedDate
|
||||
}
|
||||
});
|
||||
const groupsData = Array.isArray(gRes.data)
|
||||
? gRes.data
|
||||
: (Array.isArray(gRes.data?.groups) ? gRes.data.groups : []);
|
||||
this.groups = [...groupsData];
|
||||
// Setze groupId basierend auf dem alten groupNumber
|
||||
if (oldGroupNumber !== null) {
|
||||
const oldGroup = this.groups.find(g => g.groupNumber === oldGroupNumber);
|
||||
const oldGroupId = oldGroup ? oldGroup.groupId : null;
|
||||
if (this.$set) {
|
||||
this.$set(participant, 'groupId', oldGroupId);
|
||||
} else {
|
||||
participant.groupId = oldGroupId;
|
||||
}
|
||||
} else {
|
||||
if (this.$set) {
|
||||
this.$set(participant, 'groupId', null);
|
||||
} else {
|
||||
participant.groupId = null;
|
||||
}
|
||||
}
|
||||
} catch (loadError) {
|
||||
console.error('Fehler beim Neuladen der Gruppen:', loadError);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user