diff --git a/backend/controllers/tournamentController.js b/backend/controllers/tournamentController.js index dccfff9..e735b83 100644 --- a/backend/controllers/tournamentController.js +++ b/backend/controllers/tournamentController.js @@ -253,6 +253,28 @@ export const manualAssignGroups = async (req, res) => { } }; +export const assignParticipantToGroup = async (req, res) => { + const { authcode: token } = req.headers; + const { clubId, tournamentId, participantId, groupNumber, isExternal } = req.body; + + try { + const groups = await tournamentService.assignParticipantToGroup( + token, + clubId, + tournamentId, + participantId, + groupNumber, + isExternal || false + ); + // Emit Socket-Event + emitTournamentChanged(clubId, tournamentId); + res.status(200).json(groups); + } catch (error) { + console.error('Error in assignParticipantToGroup:', error); + res.status(500).json({ error: error.message }); + } +}; + export const resetGroups = async (req, res) => { const { authcode: token } = req.headers; const { clubId, tournamentId } = req.body; diff --git a/backend/routes/tournamentRoutes.js b/backend/routes/tournamentRoutes.js index 8ac35fb..d2324aa 100644 --- a/backend/routes/tournamentRoutes.js +++ b/backend/routes/tournamentRoutes.js @@ -33,6 +33,7 @@ import { deleteTournamentClass, updateParticipantClass, createGroupsPerClass, + assignParticipantToGroup, } from '../controllers/tournamentController.js'; import { authenticate } from '../middleware/authMiddleware.js'; @@ -55,12 +56,13 @@ router.post("/match/reopen", authenticate, reopenMatch); router.post('/match/finish', authenticate, finishMatch); router.put('/match/:clubId/:tournamentId/:matchId/active', authenticate, setMatchActive); router.get('/matches/:clubId/:tournamentId', authenticate, getTournamentMatches); -router.put('/:clubId/:tournamentId', authenticate, updateTournament); -router.get('/:clubId/:tournamentId', authenticate, getTournament); -router.get('/:clubId', authenticate, getTournaments); router.post('/knockout', authenticate, startKnockout); router.delete("/matches/knockout", authenticate, deleteKnockoutMatches); router.post('/groups/manual', authenticate, manualAssignGroups); +router.put('/participant/group', authenticate, assignParticipantToGroup); // Muss VOR /:clubId/:tournamentId stehen! +router.put('/:clubId/:tournamentId', authenticate, updateTournament); +router.get('/:clubId/:tournamentId', authenticate, getTournament); +router.get('/:clubId', authenticate, getTournaments); router.post('/', authenticate, addTournament); // Externe Teilnehmer diff --git a/backend/services/tournamentService.js b/backend/services/tournamentService.js index 32cfa2a..58b4d47 100644 --- a/backend/services/tournamentService.js +++ b/backend/services/tournamentService.js @@ -497,6 +497,12 @@ class TournamentService { ] }); + // Lade alle Gruppen-Matches mit Results für Rankings + const groupMatches = await TournamentMatch.findAll({ + where: { tournamentId, round: 'group', isFinished: true }, + include: [{ model: TournamentResult, as: 'tournamentResults' }] + }); + // Gruppiere nach Klassen und nummeriere Gruppen pro Klasse const groupsByClass = {}; groups.forEach(g => { @@ -510,25 +516,156 @@ class TournamentService { const result = []; for (const [classKey, classGroups] of Object.entries(groupsByClass)) { classGroups.forEach((g, idx) => { - const internalParticipants = (g.tournamentGroupMembers || []).map(m => ({ - id: m.id, - name: `${m.member.firstName} ${m.member.lastName}`, - seeded: m.seeded || false, - isExternal: false - })); + // Berechne Rankings für diese Gruppe + const stats = {}; - const externalParticipants = (g.externalGroupMembers || []).map(m => ({ - id: m.id, - name: `${m.firstName} ${m.lastName}`, - seeded: m.seeded || false, - isExternal: true - })); + // Interne Teilnehmer + for (const tm of g.tournamentGroupMembers || []) { + stats[tm.id] = { + id: tm.id, + name: `${tm.member.firstName} ${tm.member.lastName}`, + seeded: tm.seeded || false, + isExternal: false, + points: 0, + setsWon: 0, + setsLost: 0, + pointsWon: 0, + pointsLost: 0, + pointRatio: 0 + }; + } + + // Externe Teilnehmer + for (const ext of g.externalGroupMembers || []) { + stats[ext.id] = { + id: ext.id, + name: `${ext.firstName} ${ext.lastName}`, + seeded: ext.seeded || false, + isExternal: true, + points: 0, + setsWon: 0, + setsLost: 0, + pointsWon: 0, + pointsLost: 0, + pointRatio: 0 + }; + } + + // Berechne Statistiken aus Matches + for (const m of groupMatches.filter(m => m.groupId === g.id)) { + if (!stats[m.player1Id] || !stats[m.player2Id]) continue; + const [s1, s2] = m.result.split(':').map(n => parseInt(n, 10)); + + if (s1 > s2) { + stats[m.player1Id].points += 1; // Sieger bekommt +1 + stats[m.player2Id].points -= 1; // Verlierer bekommt -1 + } else if (s2 > s1) { + stats[m.player2Id].points += 1; // Sieger bekommt +1 + stats[m.player1Id].points -= 1; // Verlierer bekommt -1 + } + + stats[m.player1Id].setsWon += s1; + stats[m.player1Id].setsLost += s2; + stats[m.player2Id].setsWon += s2; + stats[m.player2Id].setsLost += s1; + + // Berechne gespielte Punkte aus tournamentResults + 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; + } + stats[m.player1Id].pointsWon += p1Points; + stats[m.player1Id].pointsLost += p2Points; + stats[m.player2Id].pointsWon += p2Points; + stats[m.player2Id].pointsLost += p1Points; + } + } + + // Berechne Punktverhältnis und absolute Differenz für jeden Spieler + Object.values(stats).forEach(s => { + const totalPoints = s.pointsWon + s.pointsLost; + s.pointRatio = totalPoints > 0 ? s.pointsWon / totalPoints : 0; + s.setDiff = s.setsWon - s.setsLost; + s.pointsDiff = s.pointsWon - s.pointsLost; // Absolute Differenz der gespielten Punkte + }); + + // Sortiere nach: Punkte -> Satzverhältnis -> mehr gewonnene Sätze -> absolute Punktdifferenz -> mehr erzielte Spielpunkte -> direkter Vergleich + const ranked = Object.values(stats).sort((a, b) => { + // 1. Beste Punkte + if (b.points !== a.points) return b.points - a.points; + // 2. Besseres Satzverhältnis + if (b.setDiff !== a.setDiff) return b.setDiff - a.setDiff; + // 3. Bei Satzgleichheit: Wer mehr Sätze gewonnen hat + if (b.setsWon !== a.setsWon) return b.setsWon - a.setsWon; + // 4. Bessere absolute Differenz der gespielten Punkte (höhere Differenz zuerst) + if (b.pointsDiff !== a.pointsDiff) return b.pointsDiff - a.pointsDiff; + // 5. Bei Spielpunktgleichheit: Wer mehr Spielpunkte erzielt hat + if (b.pointsWon !== a.pointsWon) return b.pointsWon - a.pointsWon; + // 6. Direkter Vergleich (Sieger weiter oben) + const directMatch = groupMatches.find(m => + m.groupId === g.id && + ((m.player1Id === a.id && m.player2Id === b.id) || + (m.player1Id === b.id && m.player2Id === a.id)) + ); + if (directMatch) { + const [s1, s2] = directMatch.result.split(':').map(n => parseInt(n, 10)); + const aWon = (directMatch.player1Id === a.id && s1 > s2) || + (directMatch.player2Id === a.id && s2 > s1); + if (aWon) return -1; // a hat gewonnen -> a kommt weiter oben + return 1; // b hat gewonnen -> b kommt weiter oben + } + // Fallback: Alphabetisch nach Name + return a.name.localeCompare(b.name); + }); + + + // Weise Positionen zu, wobei Spieler mit identischen Werten den gleichen Platz bekommen + let currentPosition = 1; + const participantsWithPosition = ranked.map((p, i) => { + if (i > 0) { + const prev = ranked[i - 1]; + // Prüfe, ob alle Sortierkriterien identisch sind + const pointsEqual = prev.points === p.points; + const setDiffEqual = prev.setDiff === p.setDiff; + const setsWonEqual = prev.setsWon === p.setsWon; + const pointsDiffEqual = prev.pointsDiff === p.pointsDiff; + const pointsWonEqual = prev.pointsWon === p.pointsWon; + + if (pointsEqual && setDiffEqual && setsWonEqual && pointsDiffEqual && pointsWonEqual) { + // Prüfe direkten Vergleich + const directMatch = groupMatches.find(m => + m.groupId === g.id && + ((m.player1Id === prev.id && m.player2Id === p.id) || + (m.player1Id === p.id && m.player2Id === prev.id)) + ); + if (!directMatch || directMatch.result.split(':').map(n => +n)[0] === directMatch.result.split(':').map(n => +n)[1]) { + // Gleicher Platz wie Vorgänger (unentschieden oder kein direktes Match) + return { + ...p, + position: currentPosition, + setDiff: p.setDiff, + pointsDiff: p.pointsDiff + }; + } + } + } + // Neuer Platz + currentPosition = i + 1; + return { + ...p, + position: currentPosition, + setDiff: p.setDiff, + pointsDiff: p.pointsDiff + }; + }); result.push({ groupId: g.id, classId: g.classId, groupNumber: idx + 1, // Nummer innerhalb der Klasse - participants: [...internalParticipants, ...externalParticipants] + participants: participantsWithPosition }); }); } @@ -752,7 +889,8 @@ class TournamentService { ] }); const groupMatches = await TournamentMatch.findAll({ - where: { tournamentId, round: "group", isFinished: true } + where: { tournamentId, round: "group", isFinished: true }, + include: [{ model: TournamentResult, as: "tournamentResults" }] }); const qualifiers = []; @@ -760,11 +898,11 @@ class TournamentService { const stats = {}; // Interne Teilnehmer for (const tm of g.tournamentGroupMembers || []) { - stats[tm.id] = { member: tm, points: 0, setsWon: 0, setsLost: 0, isExternal: false }; + stats[tm.id] = { member: tm, points: 0, setsWon: 0, setsLost: 0, pointsWon: 0, pointsLost: 0, isExternal: false }; } // Externe Teilnehmer for (const ext of g.externalGroupMembers || []) { - stats[ext.id] = { member: ext, points: 0, setsWon: 0, setsLost: 0, isExternal: true }; + stats[ext.id] = { member: ext, points: 0, setsWon: 0, setsLost: 0, pointsWon: 0, pointsLost: 0, isExternal: true }; } for (const m of groupMatches.filter(m => m.groupId === g.id)) { if (!stats[m.player1Id] || !stats[m.player2Id]) continue; @@ -780,13 +918,55 @@ class TournamentService { stats[m.player1Id].setsLost += p2; stats[m.player2Id].setsWon += p2; stats[m.player2Id].setsLost += p1; + + // Berechne gespielte Punkte aus tournamentResults + 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; + } + stats[m.player1Id].pointsWon += p1Points; + stats[m.player1Id].pointsLost += p2Points; + stats[m.player2Id].pointsWon += p2Points; + stats[m.player2Id].pointsLost += p1Points; + } } + + // Berechne Punktverhältnis und absolute Differenz für jeden Spieler + Object.values(stats).forEach(s => { + const totalPoints = s.pointsWon + s.pointsLost; + s.pointRatio = totalPoints > 0 ? s.pointsWon / totalPoints : 0; + s.pointsDiff = s.pointsWon - s.pointsLost; // Absolute Differenz der gespielten Punkte + }); + const ranked = Object.values(stats).sort((a, b) => { const diffA = a.setsWon - a.setsLost; const diffB = b.setsWon - b.setsLost; + // 1. Beste Punkte if (b.points !== a.points) return b.points - a.points; + // 2. Besseres Satzverhältnis if (diffB !== diffA) return diffB - diffA; + // 3. Bei Satzgleichheit: Wer mehr Sätze gewonnen hat if (b.setsWon !== a.setsWon) return b.setsWon - a.setsWon; + // 4. Bessere absolute Differenz der gespielten Punkte (höhere Differenz zuerst) + if (b.pointsDiff !== a.pointsDiff) return b.pointsDiff - a.pointsDiff; + // 5. Bei Spielpunktgleichheit: Wer mehr Spielpunkte erzielt hat + if (b.pointsWon !== a.pointsWon) return b.pointsWon - a.pointsWon; + // 6. Direkter Vergleich (Sieger weiter oben) + const directMatch = groupMatches.find(m => + m.groupId === g.id && + ((m.player1Id === a.member.id && m.player2Id === b.member.id) || + (m.player1Id === b.member.id && m.player2Id === a.member.id)) + ); + if (directMatch) { + const [s1, s2] = directMatch.result.split(":").map(n => parseInt(n, 10)); + const aWon = (directMatch.player1Id === a.member.id && s1 > s2) || + (directMatch.player2Id === a.member.id && s2 > s1); + if (aWon) return -1; // a hat gewonnen -> a kommt weiter oben + return 1; // b hat gewonnen -> b kommt weiter oben + } + // Fallback: Nach ID return a.member.id - b.member.id; }); // Füge classId zur Gruppe hinzu @@ -903,8 +1083,25 @@ class TournamentService { throw new Error('Keine Teilnehmer zum Verteilen'); } - // 2) Bestimme, wie viele Gruppen wir anlegen - let groupCount; + // 2) Hole bestehende Gruppen und erstelle nur neue, wenn nötig + const existingGroups = await TournamentGroup.findAll({ + where: { tournamentId }, + order: [['id', 'ASC']] + }); + + // Berechne die maximale groupNumber aus den assignments + const maxGroupNumber = assignments.reduce((max, a) => { + if (a.groupNumber != null && a.groupNumber > max) { + return a.groupNumber; + } + return max; + }, 0); + + // Erstelle nur neue Gruppen, wenn die maximale groupNumber größer ist als die Anzahl der bestehenden Gruppen + const existingGroupCount = existingGroups.length; + let groupCount = Math.max(existingGroupCount, maxGroupNumber); + + // Wenn numberOfGroups oder maxGroupSize gesetzt sind, verwende diese if (numberOfGroups != null) { groupCount = Number(numberOfGroups); if (isNaN(groupCount) || groupCount < 1) { @@ -915,21 +1112,17 @@ class TournamentService { if (isNaN(sz) || sz < 1) { throw new Error('Ungültige maximale Gruppengröße'); } - groupCount = Math.ceil(totalMembers / sz); - } else { - // Fallback auf im Turnier gespeicherte Anzahl - groupCount = tournament.numberOfGroups; - if (!groupCount || groupCount < 1) { - throw new Error('Anzahl Gruppen nicht definiert'); - } + groupCount = Math.max(groupCount, Math.ceil(totalMembers / sz)); } + + console.log(`[manualAssignGroups] Existing groups: ${existingGroupCount}, maxGroupNumber: ${maxGroupNumber}, target groupCount: ${groupCount}`); - // 3) Alte Gruppen löschen und neue anlegen - await TournamentGroup.destroy({ where: { tournamentId } }); - const createdGroups = []; - for (let i = 0; i < groupCount; i++) { + // 3) Erstelle nur fehlende Gruppen + const createdGroups = [...existingGroups]; + for (let i = existingGroupCount; i < groupCount; i++) { const grp = await TournamentGroup.create({ tournamentId }); createdGroups.push(grp); + console.log(`[manualAssignGroups] Created new group ${i + 1} with id ${grp.id}`); } // 4) Mapping von UI‑Nummer (1…groupCount) auf reale DB‑ID @@ -937,77 +1130,142 @@ class TournamentService { createdGroups.forEach((grp, idx) => { groupMap[idx + 1] = grp.id; }); + + console.log(`[manualAssignGroups] Group mapping:`, groupMap); // 5) Teilnehmer updaten (sowohl interne als auch externe) await Promise.all( - assignments.map(async ({ participantId, groupNumber }) => { - const dbGroupId = groupMap[groupNumber]; - if (!dbGroupId) { + assignments.map(async ({ participantId, groupNumber, isExternal }) => { + console.log(`[manualAssignGroups] Processing assignment: participantId=${participantId}, groupNumber=${groupNumber}, isExternal=${isExternal}`); + console.log(`[manualAssignGroups] Group map:`, JSON.stringify(groupMap)); + + // Wenn groupNumber null ist, entferne den Teilnehmer aus der Gruppe (groupId = null) + const dbGroupId = groupNumber != null ? groupMap[groupNumber] : null; + console.log(`[manualAssignGroups] Calculated dbGroupId: ${dbGroupId} (from groupNumber ${groupNumber})`); + + if (groupNumber != null && !dbGroupId) { throw new Error(`Ungültige Gruppen‑Nummer: ${groupNumber}`); } - // Prüfe zuerst, ob es ein interner Teilnehmer ist - const internalMember = await TournamentMember.findOne({ + console.log(`[manualAssignGroups] Updating participant ${participantId}, isExternal: ${isExternal}, groupNumber: ${groupNumber}, dbGroupId: ${dbGroupId}`); + + // Prüfe zuerst, ob es ein interner Teilnehmer ist (wenn isExternal nicht explizit true ist) + if (isExternal !== true) { + const internalMember = await TournamentMember.findOne({ + where: { id: participantId, tournamentId } + }); + + if (internalMember) { + // Interner Teilnehmer + console.log(`[manualAssignGroups] Updating internal member id=${participantId} (memberId=${internalMember.memberId}) to groupId ${dbGroupId}`); + await TournamentMember.update( + { groupId: dbGroupId }, + { where: { id: participantId, tournamentId } } + ); + return; + } + } + + // Versuche externen Teilnehmer + const externalMember = await ExternalTournamentParticipant.findOne({ where: { id: participantId, tournamentId } }); - if (internalMember) { - // Interner Teilnehmer - return TournamentMember.update( + if (externalMember) { + // Externer Teilnehmer + console.log(`[manualAssignGroups] Updating external member id=${participantId} to groupId ${dbGroupId}`); + await ExternalTournamentParticipant.update( { groupId: dbGroupId }, { where: { id: participantId, tournamentId } } ); } else { - // Versuche externen Teilnehmer - const externalMember = await ExternalTournamentParticipant.findOne({ - where: { id: participantId, tournamentId } - }); - - if (externalMember) { - // Externer Teilnehmer - return ExternalTournamentParticipant.update( - { groupId: dbGroupId }, - { where: { id: participantId, tournamentId } } - ); - } else { - throw new Error(`Teilnehmer mit ID ${participantId} nicht gefunden`); - } + throw new Error(`Teilnehmer mit ID ${participantId} nicht gefunden`); } }) ); - // 6) Ergebnis zurückliefern wie getGroupsWithParticipants - const groups = await TournamentGroup.findAll({ + // 6) Ergebnis zurückliefern wie getGroupsWithParticipants (mit vollständigen Rankings) + // Verwende die gleiche Methode wie getGroupsWithParticipants, um konsistente Daten zu liefern + return await this.getGroupsWithParticipants(userToken, clubId, tournamentId); + } + + /** + * Ordne einen einzelnen Teilnehmer einer Gruppe zu + * @param {string} userToken - User authentication token + * @param {number} clubId - Club ID + * @param {number} tournamentId - Tournament ID + * @param {number} participantId - Participant ID + * @param {number|null} groupNumber - Group number (1-based) or null to remove from group + * @param {boolean} isExternal - Whether the participant is external + * @returns {Promise} Updated groups with participants + */ + async assignParticipantToGroup(userToken, clubId, tournamentId, participantId, groupNumber, isExternal = false) { + console.log(`[assignParticipantToGroup] Called with: participantId=${participantId}, groupNumber=${groupNumber}, isExternal=${isExternal}, tournamentId=${tournamentId}, clubId=${clubId}`); + await checkAccess(userToken, clubId); + + const tournament = await Tournament.findByPk(tournamentId); + if (!tournament || tournament.clubId != clubId) { + throw new Error('Turnier nicht gefunden'); + } + + // Hole bestehende Gruppen + const existingGroups = await TournamentGroup.findAll({ where: { tournamentId }, - include: [{ - model: TournamentMember, - as: 'tournamentGroupMembers', - include: [{ model: Member, as: 'member', attributes: ['id', 'firstName', 'lastName'] }] - }, { - model: ExternalTournamentParticipant, - as: 'externalGroupMembers' - }], order: [['id', 'ASC']] }); + console.log(`[assignParticipantToGroup] Found ${existingGroups.length} existing groups`); - return groups.map(g => { - const internalParticipants = (g.tournamentGroupMembers || []).map(m => ({ - id: m.id, - name: `${m.member.firstName} ${m.member.lastName}`, - isExternal: false - })); + // 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 + if (isExternal) { + const externalMember = await ExternalTournamentParticipant.findOne({ + where: { id: participantId, tournamentId } + }); - const externalParticipants = (g.externalGroupMembers || []).map(m => ({ - id: m.id, - name: `${m.firstName} ${m.lastName}`, - isExternal: true - })); + if (!externalMember) { + throw new Error(`Externer Teilnehmer mit ID ${participantId} nicht gefunden`); + } - return { - groupId: g.id, - participants: [...internalParticipants, ...externalParticipants] - }; - }); + console.log(`[assignParticipantToGroup] Updating external member ${participantId} to groupId ${dbGroupId}`); + await ExternalTournamentParticipant.update( + { groupId: dbGroupId }, + { where: { id: participantId, tournamentId } } + ); + } else { + const 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}`); + await TournamentMember.update( + { groupId: dbGroupId }, + { where: { id: participantId, tournamentId } } + ); + } + + console.log(`[assignParticipantToGroup] Successfully updated participant, loading groups with participants...`); + // Lade aktualisierte Gruppen mit Teilnehmern zurück + return await this.getGroupsWithParticipants(userToken, clubId, tournamentId); } // services/tournamentService.js diff --git a/frontend/src/i18n/locales/de.json b/frontend/src/i18n/locales/de.json index 34c0311..0549567 100644 --- a/frontend/src/i18n/locales/de.json +++ b/frontend/src/i18n/locales/de.json @@ -610,6 +610,7 @@ "points": "Punkte", "sets": "Satz", "diff": "Diff", + "pointsRatio": "Spielpunkte", "livePosition": "Live-Platz", "resetGroupMatches": "Gruppenspiele", "groupMatches": "Gruppenspiele", diff --git a/frontend/src/views/TournamentTab.vue b/frontend/src/views/TournamentTab.vue index 4818319..2290b28 100644 --- a/frontend/src/views/TournamentTab.vue +++ b/frontend/src/views/TournamentTab.vue @@ -89,6 +89,47 @@
+
+
+
{{ $t('tournaments.addClubMember') }}
+
+ + + +
+
+
+
{{ $t('tournaments.addExternalParticipant') }}
+
+ + + + + +
+
+
+ + + +
+
@@ -153,47 +194,6 @@
-
-
-
{{ $t('tournaments.addClubMember') }}
-
- - - -
-
-
-
{{ $t('tournaments.addExternalParticipant') }}
-
- - - - - -
-
-
- - - -
-
@@ -246,6 +246,7 @@ {{ $t('tournaments.points') }} {{ $t('tournaments.sets') }} {{ $t('tournaments.diff') }} + {{ $t('tournaments.pointsRatio') }} G{{ String.fromCharCode(96 + group.groupNumber) }}{{ idx + 1 }} @@ -262,6 +263,9 @@ {{ pl.setDiff >= 0 ? '+' + pl.setDiff : pl.setDiff }} + + {{ pl.pointsWon }}:{{ pl.pointsLost }} ({{ (pl.pointsWon - pl.pointsLost) >= 0 ? '+' + (pl.pointsWon - pl.pointsLost) : (pl.pointsWon - pl.pointsLost) }}) + { - byGroup[g.groupId] = g.participants.map(p => ({ + rankings[g.groupId] = g.participants.map(p => ({ id: p.id, name: p.name, seeded: p.seeded || false, - points: 0, - setsWon: 0, - setsLost: 0, - setDiff: 0, + position: p.position || 0, + points: p.points || 0, + setsWon: p.setsWon || 0, + setsLost: p.setsLost || 0, + setDiff: p.setDiff || 0, + pointsWon: p.pointsWon || 0, + pointsLost: p.pointsLost || 0, + pointRatio: p.pointRatio || 0 })); }); - this.matches.forEach(m => { - if (!m.isFinished || m.round !== 'group') return; - const [s1, s2] = m.result.split(':').map(n => +n); - const arr = byGroup[m.groupId]; - if (!arr) return; - const e1 = arr.find(x => x.id === m.player1.id); - const e2 = arr.find(x => x.id === m.player2.id); - if (!e1 || !e2) return; - if (s1 > s2) { - e1.points += 1; // Sieger bekommt +1 - e2.points -= 1; // Verlierer bekommt -1 - } else if (s2 > s1) { - e2.points += 1; // Sieger bekommt +1 - e1.points -= 1; // Verlierer bekommt -1 - } - e1.setsWon += s1; e1.setsLost += s2; - e2.setsWon += s2; e2.setsLost += s1; - }); - const rankings = {}; - Object.entries(byGroup).forEach(([gid, arr]) => { - arr.forEach(p => p.setDiff = p.setsWon - p.setsLost); - arr.sort((a, b) => { - if (b.points !== a.points) return b.points - a.points; - if (b.setDiff !== a.setDiff) return b.setDiff - a.setDiff; - if (b.setsWon !== a.setsWon) return b.setsWon - a.setsWon; - return a.name.localeCompare(b.name); - }); - // Weise Positionen zu, wobei Spieler mit identischen Werten den gleichen Platz bekommen - let currentPosition = 1; - rankings[gid] = arr.map((p, i) => { - // Wenn nicht der erste Spieler und die Werte identisch sind, verwende die gleiche Position - if (i > 0) { - const prev = arr[i - 1]; - if (prev.points === p.points && - prev.setDiff === p.setDiff && - prev.setsWon === p.setsWon) { - // Gleicher Platz wie Vorgänger - return { ...p, position: currentPosition }; - } - } - // Neuer Platz - currentPosition = i + 1; - return { ...p, position: currentPosition }; - }); - }); return rankings; }, @@ -1335,7 +1299,14 @@ export default { selectedDate: { immediate: true, handler: async function (val) { - if (val === 'new') return; + if (val === 'new') { + // Leere die Matches-Liste, wenn "neues Turnier" ausgewählt wird + this.matches = []; + this.groups = []; + this.showKnockout = false; + this.showParticipants = true; + return; + } await this.loadTournamentData(); } } @@ -1507,10 +1478,28 @@ export default { clubId: this.currentClub, tournamentId: this.selectedDate }); + // Lade Gruppen zuerst, damit wir groupNumber initialisieren können + const gRes = await apiClient.get('/tournament/groups', { + params: { + clubId: this.currentClub, + tournamentId: this.selectedDate + } + }); + // Setze Gruppen neu, um Vue-Reaktivität sicherzustellen + this.groups = [...gRes.data]; + + // Erstelle Mapping von groupId zu groupNumber + const groupIdToNumberMap = this.groups.reduce((m, g) => { + m[g.groupId] = g.groupNumber; + return m; + }, {}); + // Stelle sicher, dass seeded-Feld vorhanden ist (für alte Einträge) + // Initialisiere auch groupNumber basierend auf groupId this.participants = pRes.data.map(p => ({ ...p, - seeded: p.seeded || false + seeded: p.seeded || false, + groupNumber: p.groupId ? (groupIdToNumberMap[p.groupId] || null) : null })); // Lade externe Teilnehmer (nur bei allowsExternal = true) @@ -1523,7 +1512,8 @@ export default { this.externalParticipants = extRes.data.map(p => ({ ...p, seeded: p.seeded || false, - isExternal: true + isExternal: true, + groupNumber: p.groupId ? (groupIdToNumberMap[p.groupId] || null) : null })); } catch (error) { console.error('Fehler beim Laden der externen Teilnehmer:', error); @@ -1532,13 +1522,6 @@ export default { } else { this.externalParticipants = []; } - const gRes = await apiClient.get('/tournament/groups', { - params: { - clubId: this.currentClub, - tournamentId: this.selectedDate - } - }); - this.groups = gRes.data; const mRes = await apiClient.get( `/tournament/matches/${this.currentClub}/${this.selectedDate}` ); @@ -2288,46 +2271,42 @@ export default { }, async updateParticipantGroup(participant, event) { - const groupNumber = parseInt(event.target.value); + const value = event.target.value; + const groupNumber = value === '' || value === 'null' ? null : parseInt(value); + + console.log('[updateParticipantGroup] Updating participant:', participant.id, 'to groupNumber:', groupNumber, 'isExternal:', participant.isExternal); // Aktualisiere lokal participant.groupNumber = groupNumber; - // Bereite alle Teilnehmer-Zuordnungen vor (interne und externe, wenn allowsExternal = true) - const assignments = this.allowsExternal - ? [ - ...this.participants.map(p => ({ - participantId: p.id, - groupNumber: p.groupNumber || null, - isExternal: false - })), - ...this.externalParticipants.map(p => ({ - participantId: p.id, - groupNumber: p.groupNumber || null, - isExternal: true - })) - ] - : this.participants.map(p => ({ - participantId: p.id, - groupNumber: p.groupNumber || null - })); - - // Sende an Backend + // Sende nur diesen einen Teilnehmer an Backend try { - await apiClient.post('/tournament/groups/manual', { + const response = await apiClient.put('/tournament/participant/group', { clubId: this.currentClub, tournamentId: this.selectedDate, - assignments: assignments, - numberOfGroups: this.numberOfGroups, - maxGroupSize: this.maxGroupSize + participantId: participant.id, + groupNumber: groupNumber, + isExternal: participant.isExternal || false }); + console.log('[updateParticipantGroup] Response:', response.data); - // Lade Daten neu, um die aktualisierten Gruppen zu erhalten - await this.loadTournamentData(); + // Verwende die Response-Daten direkt, um die Gruppen zu aktualisieren + if (response.data && Array.isArray(response.data)) { + // Setze Gruppen neu, um Vue-Reaktivität sicherzustellen + this.groups = [...response.data]; + console.log('[updateParticipantGroup] Updated groups:', this.groups); + } else { + // Fallback: Lade Daten neu + await this.loadTournamentData(); + } + // Force Vue update, um sicherzustellen, dass die Gruppenübersicht aktualisiert wird + this.$forceUpdate(); } catch (error) { console.error('Fehler beim Aktualisieren der Gruppe:', error); - // Bei Fehler: Lade Daten neu + // 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(); } },