Add participant assignment to groups functionality
Implement a new endpoint to assign participants to specific groups within tournaments. This includes the addition of the `assignParticipantToGroup` function in the tournament controller, which handles the assignment logic and emits relevant events. Update the tournament routes to include this new functionality. Enhance the tournament service to manage group assignments and ensure proper statistics are calculated for participants. Additionally, update the frontend to support adding participants, including external ones, and reflect changes in the UI for group assignments.
This commit is contained in:
@@ -610,6 +610,7 @@
|
||||
"points": "Punkte",
|
||||
"sets": "Satz",
|
||||
"diff": "Diff",
|
||||
"pointsRatio": "Spielpunkte",
|
||||
"livePosition": "Live-Platz",
|
||||
"resetGroupMatches": "Gruppenspiele",
|
||||
"groupMatches": "Gruppenspiele",
|
||||
|
||||
@@ -89,6 +89,47 @@
|
||||
<span class="collapse-icon" :class="{ 'expanded': showParticipants }">▼</span>
|
||||
</div>
|
||||
<div v-show="showParticipants" class="participants-content">
|
||||
<div class="add-participant">
|
||||
<div v-if="allowsExternal" class="add-participant-section">
|
||||
<h5>{{ $t('tournaments.addClubMember') }}</h5>
|
||||
<div class="add-participant-row">
|
||||
<select v-model="selectedMember" class="member-select">
|
||||
<option :value="null">{{ $t('tournaments.selectParticipant') }}</option>
|
||||
<option v-for="member in clubMembers" :key="member.id" :value="member.id">
|
||||
{{ member.firstName }}
|
||||
{{ member.lastName }}
|
||||
</option>
|
||||
</select>
|
||||
<button @click="addParticipant" class="btn-add">{{ $t('tournaments.add') }}</button>
|
||||
<button v-if="hasTrainingToday && !allowsExternal" @click="loadParticipantsFromTraining" class="training-btn">
|
||||
📅 {{ $t('tournaments.loadFromTraining') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="allowsExternal" class="add-participant-section">
|
||||
<h5>{{ $t('tournaments.addExternalParticipant') }}</h5>
|
||||
<div class="add-participant-row">
|
||||
<input type="text" v-model="newExternalParticipant.firstName" :placeholder="$t('tournaments.firstName')" class="external-input" />
|
||||
<input type="text" v-model="newExternalParticipant.lastName" :placeholder="$t('tournaments.lastName')" class="external-input" />
|
||||
<input type="text" v-model="newExternalParticipant.club" :placeholder="$t('tournaments.club') + ' (' + $t('tournaments.optional') + ')'" class="external-input" />
|
||||
<input type="date" v-model="newExternalParticipant.birthDate" :placeholder="$t('tournaments.birthdate') + ' (' + $t('tournaments.optional') + ')'" class="external-input" />
|
||||
<button @click="addExternalParticipant" class="btn-add">{{ $t('tournaments.add') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!allowsExternal" class="add-participant-row">
|
||||
<select v-model="selectedMember" class="member-select">
|
||||
<option :value="null">{{ $t('tournaments.selectParticipant') }}</option>
|
||||
<option v-for="member in clubMembers" :key="member.id" :value="member.id">
|
||||
{{ member.firstName }}
|
||||
{{ member.lastName }}
|
||||
</option>
|
||||
</select>
|
||||
<button @click="addParticipant" class="btn-add">Hinzufügen</button>
|
||||
<button v-if="hasTrainingToday" @click="loadParticipantsFromTraining" class="training-btn">
|
||||
📅 Aus Trainingstag laden
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="participants-table-container">
|
||||
<table class="participants-table participants-table-header">
|
||||
<thead>
|
||||
@@ -153,47 +194,6 @@
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="add-participant">
|
||||
<div v-if="allowsExternal" class="add-participant-section">
|
||||
<h5>{{ $t('tournaments.addClubMember') }}</h5>
|
||||
<div class="add-participant-row">
|
||||
<select v-model="selectedMember" class="member-select">
|
||||
<option :value="null">{{ $t('tournaments.selectParticipant') }}</option>
|
||||
<option v-for="member in clubMembers" :key="member.id" :value="member.id">
|
||||
{{ member.firstName }}
|
||||
{{ member.lastName }}
|
||||
</option>
|
||||
</select>
|
||||
<button @click="addParticipant" class="btn-add">{{ $t('tournaments.add') }}</button>
|
||||
<button v-if="hasTrainingToday && !allowsExternal" @click="loadParticipantsFromTraining" class="training-btn">
|
||||
📅 {{ $t('tournaments.loadFromTraining') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="allowsExternal" class="add-participant-section">
|
||||
<h5>{{ $t('tournaments.addExternalParticipant') }}</h5>
|
||||
<div class="add-participant-row">
|
||||
<input type="text" v-model="newExternalParticipant.firstName" :placeholder="$t('tournaments.firstName')" class="external-input" />
|
||||
<input type="text" v-model="newExternalParticipant.lastName" :placeholder="$t('tournaments.lastName')" class="external-input" />
|
||||
<input type="text" v-model="newExternalParticipant.club" :placeholder="$t('tournaments.club') + ' (' + $t('tournaments.optional') + ')'" class="external-input" />
|
||||
<input type="date" v-model="newExternalParticipant.birthDate" :placeholder="$t('tournaments.birthdate') + ' (' + $t('tournaments.optional') + ')'" class="external-input" />
|
||||
<button @click="addExternalParticipant" class="btn-add">{{ $t('tournaments.add') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!allowsExternal" class="add-participant-row">
|
||||
<select v-model="selectedMember" class="member-select">
|
||||
<option :value="null">{{ $t('tournaments.selectParticipant') }}</option>
|
||||
<option v-for="member in clubMembers" :key="member.id" :value="member.id">
|
||||
{{ member.firstName }}
|
||||
{{ member.lastName }}
|
||||
</option>
|
||||
</select>
|
||||
<button @click="addParticipant" class="btn-add">Hinzufügen</button>
|
||||
<button v-if="hasTrainingToday" @click="loadParticipantsFromTraining" class="training-btn">
|
||||
📅 Aus Trainingstag laden
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section v-if="isGroupTournament" class="group-controls">
|
||||
@@ -246,6 +246,7 @@
|
||||
<th>{{ $t('tournaments.points') }}</th>
|
||||
<th>{{ $t('tournaments.sets') }}</th>
|
||||
<th>{{ $t('tournaments.diff') }}</th>
|
||||
<th>{{ $t('tournaments.pointsRatio') }}</th>
|
||||
<th v-for="(opponent, idx) in groupRankings[group.groupId]" :key="`opp-${opponent.id}`">
|
||||
G{{ String.fromCharCode(96 + group.groupNumber) }}{{ idx + 1 }}
|
||||
</th>
|
||||
@@ -262,6 +263,9 @@
|
||||
<td>
|
||||
{{ pl.setDiff >= 0 ? '+' + pl.setDiff : pl.setDiff }}
|
||||
</td>
|
||||
<td>
|
||||
{{ pl.pointsWon }}:{{ pl.pointsLost }} ({{ (pl.pointsWon - pl.pointsLost) >= 0 ? '+' + (pl.pointsWon - pl.pointsLost) : (pl.pointsWon - pl.pointsLost) }})
|
||||
</td>
|
||||
<td v-for="(opponent, oppIdx) in groupRankings[group.groupId]"
|
||||
:key="`match-${pl.id}-${opponent.id}`"
|
||||
:class="['match-cell', { 'clickable': idx !== oppIdx, 'active-group-cell': activeGroupCells.includes(`match-${pl.id}-${opponent.id}`), 'diagonal-cell': idx === oppIdx }]"
|
||||
@@ -1044,63 +1048,23 @@ export default {
|
||||
},
|
||||
|
||||
groupRankings() {
|
||||
const byGroup = {};
|
||||
// Die Teilnehmer kommen bereits sortiert vom Backend mit allen benötigten Statistiken
|
||||
const rankings = {};
|
||||
this.groups.forEach(g => {
|
||||
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();
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user