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:
Torsten Schulz (local)
2025-12-17 13:38:40 +01:00
parent 4b4c48a50f
commit dc084806ab
7 changed files with 676 additions and 186 deletions

View File

@@ -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,
});
}
// BYEMatches zuerst
let remaining = [...uniqueEntrants];
// BYEMatches 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) {