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:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user