From adefb120c0ca4b4105d942d5fa4cb9eaf39afa1c Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Sat, 28 Mar 2026 10:57:29 +0100 Subject: [PATCH] feat(TournamentResultsTab, TournamentTab): add knockout operation handling - Introduced knockoutOperationInProgress state to manage button states during knockout operations in TournamentResultsTab.vue. - Updated button elements to disable during ongoing operations, enhancing user experience and preventing multiple submissions. - Integrated knockoutOperationInProgress state in TournamentTab.vue to control the flow of knockout-related actions. --- .../tournament/TournamentResultsTab.vue | 8 +- frontend/src/views/TournamentTab.vue | 129 ++++++++++-------- 2 files changed, 76 insertions(+), 61 deletions(-) diff --git a/frontend/src/components/tournament/TournamentResultsTab.vue b/frontend/src/components/tournament/TournamentResultsTab.vue index 15268a26..e8df15a6 100644 --- a/frontend/src/components/tournament/TournamentResultsTab.vue +++ b/frontend/src/components/tournament/TournamentResultsTab.vue @@ -145,12 +145,12 @@
-
-
@@ -335,6 +335,10 @@ export default { type: Boolean, required: true }, + knockoutOperationInProgress: { + type: Boolean, + default: false + }, getTotalNumberOfGroups: { type: Number, required: true diff --git a/frontend/src/views/TournamentTab.vue b/frontend/src/views/TournamentTab.vue index 08951c7e..b6de56e1 100644 --- a/frontend/src/views/TournamentTab.vue +++ b/frontend/src/views/TournamentTab.vue @@ -236,6 +236,7 @@ :can-start-knockout="canStartKnockout" :show-knockout="showKnockout" :can-reset-knockout="canResetKnockout" + :knockout-operation-in-progress="knockoutOperationInProgress" :get-total-number-of-groups="getTotalNumberOfGroups" :grouped-ranking-list="groupedRankingList" :participants="participants" @@ -392,6 +393,7 @@ export default { groups: [], matches: [], showKnockout: false, + knockoutOperationInProgress: false, showParticipants: false, // Kollaps-Status für Teilnehmerliste hasTrainingToday: false, // Gibt es einen Trainingstag heute? editingResult: { @@ -1936,6 +1938,7 @@ export default { return; } if (quickAction.kind === 'start-knockout') { + if (this.knockoutOperationInProgress) return; this.setActiveTab('results'); this.resultsSubTab = 'matches'; await this.startKnockout(); @@ -2622,80 +2625,88 @@ export default { }, async startKnockout() { + if (this.knockoutOperationInProgress) return; + if (!this.currentClub || !this.selectedDate || this.selectedDate === 'new') { + await this.showInfo(this.$t('messages.error'), this.$t('tournaments.selectTournamentFirst'), '', 'error'); + return; + } + + this.knockoutOperationInProgress = true; // Wenn eine Stage-Konfiguration existiert, ist /tournament/stages/advance der // korrekte Weg, weil nur dort die Pool-Regeln (z.B. Plätze 1,2) berücksichtigt werden. // Fallback ist die Legacy-Route /tournament/knockout. try { - const stagesRes = await apiClient.get('/tournament/stages', { - params: { - clubId: this.currentClub, - tournamentId: this.selectedDate - } - }); + try { + const stagesRes = await apiClient.get('/tournament/stages', { + params: { + clubId: this.currentClub, + tournamentId: this.selectedDate + } + }); - const stages = stagesRes?.data?.stages; - if (Array.isArray(stages) && stages.length > 0) { - // Backend arbeitet mit expliziten Stage-Indizes (z.B. 1 und 3), die nicht - // zwingend 1..N sind. Daher müssen wir die Indizes aus der Antwort ableiten. - const normalizedStages = stages - .map(s => ({ - ...s, - // Prefer explicit index field; fall back to id for ordering if needed - stageIndex: Number(s.stageIndex ?? s.index ?? s.id), - stageId: Number(s.id ?? s.stageId ?? s.stageIndex) - })) - .filter(s => Number.isFinite(s.stageIndex)); + const stages = stagesRes?.data?.stages; + if (Array.isArray(stages) && stages.length > 0) { + const normalizedStages = stages + .map(s => ({ + ...s, + stageIndex: Number(s.stageIndex ?? s.index ?? s.id), + stageId: Number(s.id ?? s.stageId ?? s.stageIndex) + })) + .filter(s => Number.isFinite(s.stageIndex)); - // Ermittle die Reihenfolge der Stages - const ordered = normalizedStages.sort((a, b) => a.stageIndex - b.stageIndex); - const groupStage = ordered.find(s => (s.type || s.targetType || s.target) === 'groups'); - const knockoutStage = ordered.find(s => (s.type || s.targetType || s.target) === 'knockout'); + const ordered = normalizedStages.sort((a, b) => a.stageIndex - b.stageIndex); + const groupStage = ordered.find(s => (s.type || s.targetType || s.target) === 'groups'); + const knockoutStage = ordered.find(s => (s.type || s.targetType || s.target) === 'knockout'); - if (groupStage && knockoutStage) { - // Falls es Zwischenstufen vom Typ 'groups' gibt, iteriere bis zur KO‑Stufe - let fromIdx = groupStage.stageIndex; - let fromId = groupStage.stageId; - for (const stage of ordered) { - if (stage.stageIndex <= fromIdx) continue; - // Advance Schrittweise zur nächsten Stage; prefer IDs if backend expects them - const payload = { - clubId: this.currentClub, - tournamentId: this.selectedDate - }; - if (Number.isFinite(fromId) && Number.isFinite(stage.stageId)) { - payload.fromStageId = fromId; - payload.toStageId = stage.stageId; - } else { - payload.fromStageIndex = fromIdx; - payload.toStageIndex = stage.stageIndex; - } - await apiClient.post('/tournament/stages/advance', payload); + if (groupStage && knockoutStage) { + let fromIdx = groupStage.stageIndex; + let fromId = groupStage.stageId; + for (const stage of ordered) { + if (stage.stageIndex <= fromIdx) continue; + const payload = { + clubId: this.currentClub, + tournamentId: this.selectedDate + }; + if (Number.isFinite(fromId) && Number.isFinite(stage.stageId)) { + payload.fromStageId = fromId; + payload.toStageId = stage.stageId; + } else { + payload.fromStageIndex = fromIdx; + payload.toStageIndex = stage.stageIndex; + } + await apiClient.post('/tournament/stages/advance', payload); + fromIdx = stage.stageIndex; + fromId = stage.stageId; - // Update trackers - fromIdx = stage.stageIndex; - fromId = stage.stageId; + if ((stage.type || stage.targetType || stage.target) === 'knockout') { + await this.loadTournamentData(); + return; + } - // Wenn KO erreicht, beende - if ((stage.type || stage.targetType || stage.target) === 'knockout') { await this.loadTournamentData(); - return; } - - // Nach jedem Schritt neu laden, damit Folgeschritt korrekte Daten hat - await this.loadTournamentData(); } } + } catch (e) { + console.warn('Stage-basierter Start der K.o.-Runde nicht möglich, verwende Legacy-Fallback.', e); } - } catch (e) { - // Ignorieren und Legacy-Fallback nutzen. - // (z.B. wenn Endpoint nicht verfügbar oder Stages nicht konfiguriert) - } - await apiClient.post('/tournament/knockout', { - clubId: this.currentClub, - tournamentId: this.selectedDate - }); - await this.loadTournamentData(); + await apiClient.post('/tournament/knockout', { + clubId: this.currentClub, + tournamentId: this.selectedDate + }); + await this.loadTournamentData(); + } catch (error) { + console.error('Fehler beim Starten der K.o.-Runde:', error); + await this.showInfo( + this.$t('messages.error'), + this.$t('tournaments.errorCreatingGroups'), + error?.response?.data?.error || error?.response?.data?.message || error?.message || '', + 'error' + ); + } finally { + this.knockoutOperationInProgress = false; + } }, formatResult(match) {