feat(tournament): implement class merging and pool management features

- Added mergeClassesIntoPool and resetPool functions in tournamentService to handle merging classes into a common pool and resetting pool assignments.
- Introduced new API routes for merging and resetting pools in tournamentRoutes.
- Enhanced TournamentGroupsTab component with UI for merging classes, including selection for source and target classes, strategy options, and out-of-competition settings.
- Updated localization files to include new strings related to class merging functionality.
- Modified TournamentTab to handle merge pool events and manage API interactions for merging classes.
This commit is contained in:
Torsten Schulz (local)
2026-01-07 12:10:33 +01:00
parent e94a12cd20
commit fea84e210a
10 changed files with 315 additions and 2 deletions

View File

@@ -57,6 +57,45 @@
<button @click="$emit('create-groups')">{{ $t('tournaments.createGroups') }}</button>
<button @click="$emit('randomize-groups')">{{ $t('tournaments.randomizeGroups') }}</button>
<button @click="$emit('reset-groups')">{{ $t('tournaments.resetGroups') }}</button>
<!-- Klassen zusammenlegen (Pools) -->
<div class="merge-pools-box">
<h4>{{ $t('tournaments.mergeClasses') || 'Klassen zusammenlegen (gemeinsame Gruppenphase)' }}</h4>
<div class="merge-pools-row">
<label>
{{ $t('tournaments.sourceClass') || 'Quelle' }}:
<select v-model="mergeSourceClassId">
<option :value="null"></option>
<option v-for="c in tournamentClasses" :key="c.id" :value="c.id">{{ c.name }}</option>
</select>
</label>
<label>
{{ $t('tournaments.targetClass') || 'Ziel' }}:
<select v-model="mergeTargetClassId">
<option :value="null"></option>
<option v-for="c in tournamentClasses" :key="c.id" :value="c.id">{{ c.name }}</option>
</select>
</label>
<template v-if="mergePoolsReady">
<label>
{{ $t('tournaments.strategy') || 'Strategie' }}:
<select v-model="mergeStrategy">
<option value="singleGroup">{{ $t('tournaments.mergeSingleGroup') || 'Alle Spieler aus A in eine Gruppe (bei B)' }}</option>
<option value="distribute">{{ $t('tournaments.mergeDistribute') || 'Spieler aus A auf alle Gruppen (von B) verteilen' }}</option>
</select>
</label>
<label>
<input type="checkbox" v-model="mergeSourceAsAK" />
{{ mergeOutOfCompetitionLabel }}
</label>
</template>
</div>
<div class="merge-pools-actions">
<button @click="requestMergePools" :disabled="!mergeSourceClassId || !mergeTargetClassId || String(mergeSourceClassId)===String(mergeTargetClassId)">
{{ $t('tournaments.apply') || 'Übernehmen' }}
</button>
</div>
</div>
</section>
<section v-if="groups.length" class="groups-overview">
<h3>{{ $t('tournaments.groupsOverview') }}</h3>
@@ -209,11 +248,40 @@ export default {
'reset-matches',
'create-matches',
'highlight-match',
'go-to-match'
'go-to-match',
'merge-pools'
],
data() {
return {
// Merge-UI (Pools)
mergeSourceClassId: null,
mergeTargetClassId: null,
mergeStrategy: 'distribute', // 'singleGroup' | 'distribute'
mergeSourceAsAK: false,
};
},
computed: {
filteredGroupMatches() {
return this.filterMatchesByClass(this.matches.filter(m => m.round === 'group'));
},
mergePoolsReady() {
return !!(
this.mergeSourceClassId &&
this.mergeTargetClassId &&
String(this.mergeSourceClassId) !== String(this.mergeTargetClassId)
);
},
mergeSourceClassName() {
const id = this.mergeSourceClassId;
if (!id) return '';
const c = (this.tournamentClasses || []).find(x => String(x.id) === String(id));
return c ? c.name : '';
},
mergeOutOfCompetitionLabel() {
const base = this.$t && this.$t('tournaments.outOfCompetition');
// Wenn Übersetzung vorhanden ist, ersetzen wir nur das "A" nicht zuverlässig -> lieber dynamisch bauen
const src = this.mergeSourceClassName || 'Quelle';
return `Spieler aus ${src} außer Konkurrenz`;
}
},
methods: {
@@ -399,8 +467,45 @@ export default {
if (match) {
this.$emit('go-to-match', match.id);
}
},
requestMergePools() {
if (!this.mergeSourceClassId || !this.mergeTargetClassId) return;
if (String(this.mergeSourceClassId) === String(this.mergeTargetClassId)) return;
this.$emit('merge-pools', {
sourceClassId: Number(this.mergeSourceClassId),
targetClassId: Number(this.mergeTargetClassId),
strategy: this.mergeStrategy,
outOfCompetitionForSource: !!this.mergeSourceAsAK,
});
}
}
};
</script>
<style scoped>
.merge-pools-box {
margin: 1rem 0 0 0;
padding: 0.75rem 1rem;
border: 1px solid #e0e0e0;
border-radius: 6px;
background: #fafafa;
}
.merge-pools-box h4 {
margin: 0 0 0.5rem 0;
font-size: 1rem;
color: #333;
}
.merge-pools-row {
display: flex;
gap: 0.75rem;
flex-wrap: wrap;
align-items: center;
}
.merge-pools-row select {
padding: 0.25rem 0.5rem;
}
.merge-pools-actions {
margin-top: 0.5rem;
}
</style>

View File

@@ -646,6 +646,7 @@
"encounter": "Begegnung",
"result": "Ergebnis",
"sets": "Sätze",
"setDiff": "Satzdifferenz",
"createMatches": "Spiele erstellen",
"startKORound": "K.o.-Runde starten",
"deleteKORound": "K.o.-Runde",
@@ -658,7 +659,19 @@
"errorCreatingTournament": "Fehler beim Erstellen des Turniers.",
"pleaseSelectParticipant": "Bitte wählen Sie einen Teilnehmer aus!",
"errorCreatingGroups": "Fehler beim Erstellen der Gruppen.",
"errorResettingKORound": "Fehler beim Zurücksetzen der K.o.-Runde."
"errorResettingKORound": "Fehler beim Zurücksetzen der K.o.-Runde.",
"mergeClasses": "Klassen zusammenlegen (gemeinsame Gruppenphase)",
"sourceClass": "Quelle",
"targetClass": "Ziel",
"strategy": "Strategie",
"mergeSingleGroup": "Alle Spieler aus A in eine Gruppe (bei B)",
"mergeDistribute": "Spieler aus A auf alle Gruppen (von B) verteilen",
"outOfCompetition": "Spieler aus A außer Konkurrenz",
"errorMergingClasses": "Fehler beim Zusammenlegen der Klassen.",
"apply": "Übernehmen"
},
"tournament": {
"apply": "Übernehmen"
},
"clubSettings": {
"title": "Vereins-Einstellungen",

View File

@@ -178,6 +178,7 @@
@create-matches="createMatches()"
@highlight-match="highlightMatch"
@go-to-match="goToMatch"
@merge-pools="mergePools"
/>
<!-- Tab: Ergebnisse -->
@@ -2252,6 +2253,24 @@ export default {
});
},
async mergePools({ sourceClassId, targetClassId, strategy, outOfCompetitionForSource }) {
try {
await apiClient.post('/tournament/pools/merge', {
clubId: this.currentClub,
tournamentId: this.selectedDate,
sourceClassId,
targetClassId,
strategy,
outOfCompetitionForSource: !!outOfCompetitionForSource,
});
await this.loadTournamentData();
} catch (error) {
console.error('[mergePools] Error:', error);
const msg = (this.$t && this.$t('tournaments.errorMergingClasses')) || 'Fehler beim Zusammenlegen der Klassen.';
await this.showInfo((this.$t && this.$t('messages.error')) || 'Fehler', msg, '', 'error');
}
},
async updateParticipantSeeded(participant, event) {
const seeded = event.target.checked;