diff --git a/backend/controllers/tournamentController.js b/backend/controllers/tournamentController.js index 4bfb580..a517233 100644 --- a/backend/controllers/tournamentController.js +++ b/backend/controllers/tournamentController.js @@ -420,6 +420,19 @@ export const resetMatches = async (req, res) => { } }; +export const cleanupOrphanedMatches = async (req, res) => { + const { authcode: token } = req.headers; + const { clubId, tournamentId } = req.body; + try { + const result = await tournamentService.cleanupOrphanedMatches(token, clubId, tournamentId); + emitTournamentChanged(clubId, tournamentId); + res.status(200).json(result); + } catch (err) { + console.error(err); + res.status(500).json({ error: err.message }); + } +}; + export const removeParticipant = async (req, res) => { const { authcode: token } = req.headers; const { clubId, tournamentId, participantId } = req.body; diff --git a/backend/routes/tournamentRoutes.js b/backend/routes/tournamentRoutes.js index 3afdd37..31eb2c8 100644 --- a/backend/routes/tournamentRoutes.js +++ b/backend/routes/tournamentRoutes.js @@ -19,6 +19,7 @@ import { manualAssignGroups, resetGroups, resetMatches, + cleanupOrphanedMatches, removeParticipant, updateParticipantSeeded, setParticipantGaveUp, @@ -61,6 +62,7 @@ router.put('/participant/:clubId/:tournamentId/:participantId/gave-up', authenti router.post('/modus', authenticate, setModus); router.post('/groups/reset', authenticate, resetGroups); router.post('/matches/reset', authenticate, resetMatches); +router.post('/matches/cleanup-orphaned', authenticate, cleanupOrphanedMatches); router.put('/groups', authenticate, createGroups); router.post('/groups/create', authenticate, createGroupsPerClass); router.post('/groups', authenticate, fillGroups); diff --git a/backend/services/tournamentService.js b/backend/services/tournamentService.js index a44dea3..2822efd 100644 --- a/backend/services/tournamentService.js +++ b/backend/services/tournamentService.js @@ -3201,6 +3201,38 @@ Ve // 2. Neues Turnier anlegen await TournamentMatch.destroy({ where }); } + /** + * Entfernt Matches, bei denen mindestens ein Spieler nicht mehr existiert + * (z.B. gelöscht bevor die Aufräum-Logik beim Teilnehmerlöschen eingeführt wurde). + */ + async cleanupOrphanedMatches(userToken, clubId, tournamentId) { + await checkAccess(userToken, clubId); + const tournament = await Tournament.findByPk(tournamentId); + if (!tournament || tournament.clubId != clubId) { + throw new Error('Turnier nicht gefunden'); + } + + const members = await TournamentMember.findAll({ where: { tournamentId }, attributes: ['id'] }); + const externals = await ExternalTournamentParticipant.findAll({ where: { tournamentId }, attributes: ['id'] }); + const validIds = new Set([ + ...members.map(m => m.id), + ...externals.map(e => e.id) + ]); + + const matches = await TournamentMatch.findAll({ where: { tournamentId } }); + let deletedCount = 0; + for (const m of matches) { + const p1Exists = !m.player1Id || validIds.has(m.player1Id); + const p2Exists = !m.player2Id || validIds.has(m.player2Id); + if (!p1Exists || !p2Exists) { + await TournamentResult.destroy({ where: { matchId: m.id } }); + await m.destroy(); + deletedCount++; + } + } + return { deletedCount }; + } + async removeParticipant(userToken, clubId, tournamentId, participantId) { await checkAccess(userToken, clubId); diff --git a/frontend/src/components/tournament/TournamentGroupsTab.vue b/frontend/src/components/tournament/TournamentGroupsTab.vue index 72042d5..83062d4 100644 --- a/frontend/src/components/tournament/TournamentGroupsTab.vue +++ b/frontend/src/components/tournament/TournamentGroupsTab.vue @@ -162,8 +162,14 @@ +
+ @@ -246,6 +252,7 @@ export default { 'randomize-groups', 'reset-groups', 'reset-matches', + 'cleanup-orphaned-matches', 'create-matches', 'highlight-match', 'go-to-match', diff --git a/frontend/src/i18n/locales/de.json b/frontend/src/i18n/locales/de.json index 132bee4..ddb2151 100644 --- a/frontend/src/i18n/locales/de.json +++ b/frontend/src/i18n/locales/de.json @@ -648,6 +648,7 @@ "errorMoreSeededThanUnseeded": "Es gibt mehr gesetzte als nicht gesetzte Spieler. Zufällige Paarungen können nicht erstellt werden.", "randomPairingsCreated": "Zufällige Paarungen wurden erstellt.", "resetGroupMatches": "Gruppenspiele", + "cleanupOrphanedMatches": "Verwaiste Spiele aufräumen", "groupMatches": "Gruppenspiele", "round": "Runde", "encounter": "Begegnung", diff --git a/frontend/src/views/TournamentTab.vue b/frontend/src/views/TournamentTab.vue index 563b8d9..ad3da0b 100644 --- a/frontend/src/views/TournamentTab.vue +++ b/frontend/src/views/TournamentTab.vue @@ -198,6 +198,7 @@ @randomize-groups="randomizeGroups()" @reset-groups="resetGroups()" @reset-matches="resetMatches()" + @cleanup-orphaned-matches="cleanupOrphanedMatches()" @create-matches="createMatches()" @highlight-match="highlightMatch" @go-to-match="goToMatch" @@ -2084,6 +2085,26 @@ export default { await this.loadTournamentData(); }, + async cleanupOrphanedMatches() { + try { + const res = await apiClient.post('/tournament/matches/cleanup-orphaned', { + clubId: this.currentClub, + tournamentId: this.selectedDate + }); + const count = res.data?.deletedCount ?? 0; + await this.loadTournamentData(); + if (count > 0) { + await this.showInfo(this.$t('messages.success'), `${count} ${count === 1 ? 'Spiel' : 'Spiele'} entfernt.`, ''); + } else { + await this.showInfo(this.$t('messages.info'), 'Keine verwaisten Spiele gefunden.', ''); + } + } catch (error) { + console.error('Fehler beim Aufräumen:', error); + const message = safeErrorMessage(error, 'Fehler beim Aufräumen verwaister Spiele.'); + await this.showInfo(this.$t('messages.error'), message, '', 'error'); + } + }, + async removeParticipant(p) { try { if (this.allowsExternal && p.isExternal) {