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.
This commit is contained in:
Torsten Schulz (local)
2026-03-28 10:57:29 +01:00
parent 9d023b534d
commit adefb120c0
2 changed files with 76 additions and 61 deletions

View File

@@ -145,12 +145,12 @@
</button>
</div>
<div v-if="canStartKnockout && !showKnockout && numberOfGroupsForSelectedClass > 1" class="results-next-step">
<button @click="$emit('start-knockout')" class="btn-primary">
<button @click="$emit('start-knockout')" class="btn-primary" :disabled="knockoutOperationInProgress">
{{ $t('tournaments.startKORound') }}
</button>
</div>
<div v-if="showKnockout && canResetKnockout && numberOfGroupsForSelectedClass > 1" class="results-next-step results-next-step-muted">
<button @click="$emit('reset-knockout')" class="trash-btn">
<button @click="$emit('reset-knockout')" class="trash-btn" :disabled="knockoutOperationInProgress">
🗑 {{ $t('tournaments.deleteKORound') }}
</button>
</div>
@@ -335,6 +335,10 @@ export default {
type: Boolean,
required: true
},
knockoutOperationInProgress: {
type: Boolean,
default: false
},
getTotalNumberOfGroups: {
type: Number,
required: true

View File

@@ -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 KOStufe
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) {