Add placements tab and localization support in TournamentTab
- Introduced a new tab for displaying tournament placements in the TournamentTab component. - Added localization entries for placements in the German language JSON file, enhancing the user interface for German-speaking users. - Updated the component structure to include the new TournamentPlacementsTab and ensure proper rendering based on the active tab.
This commit is contained in:
458
frontend/src/components/tournament/TournamentPlacementsTab.vue
Normal file
458
frontend/src/components/tournament/TournamentPlacementsTab.vue
Normal file
@@ -0,0 +1,458 @@
|
||||
<template>
|
||||
<div class="tab-content">
|
||||
<TournamentClassSelector
|
||||
v-if="selectedDate && selectedDate !== 'new'"
|
||||
:model-value="selectedViewClass"
|
||||
:tournament-classes="tournamentClasses"
|
||||
:selected-date="selectedDate"
|
||||
@update:modelValue="$emit('update:selectedViewClass', $event)"
|
||||
/>
|
||||
|
||||
<!-- Endplatzierungen (K.O.-Runde) -->
|
||||
<section v-if="finalPlacements.length > 0" class="final-placements">
|
||||
<h3>{{ $t('tournaments.finalPlacements') }}</h3>
|
||||
<template v-for="(classPlacements, classId) in finalPlacementsByClass" :key="`final-${classId}`">
|
||||
<div v-if="shouldShowClass(classId === 'null' ? null : parseInt(classId))" class="class-section">
|
||||
<h4 v-if="classId !== 'null' && classId !== 'undefined'" class="class-header">
|
||||
{{ getClassName(classId) }}
|
||||
</h4>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ $t('tournaments.position') }}</th>
|
||||
<th>{{ $t('tournaments.player') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="placement in classPlacements" :key="placement.id">
|
||||
<td><strong>{{ placement.position }}.</strong></td>
|
||||
<td>{{ placement.name }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
</section>
|
||||
|
||||
<!-- Gruppenplatzierungen -->
|
||||
<section v-if="groupPlacements.length > 0" class="group-placements">
|
||||
<h3>{{ $t('tournaments.groupPlacements') }}</h3>
|
||||
<template v-for="(classGroups, classId) in groupPlacementsByClass" :key="`group-${classId}`">
|
||||
<div v-if="shouldShowClass(classId === 'null' ? null : parseInt(classId))" class="class-section">
|
||||
<h4 v-if="classId !== 'null' && classId !== 'undefined'" class="class-header">
|
||||
{{ getClassName(classId) }}
|
||||
</h4>
|
||||
<div v-for="group in classGroups" :key="group.groupId" class="group-table">
|
||||
<h5>{{ $t('tournaments.groupNumber') }} {{ group.groupNumber }}</h5>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ $t('tournaments.position') }}</th>
|
||||
<th>{{ $t('tournaments.player') }}</th>
|
||||
<th>{{ $t('tournaments.points') }}</th>
|
||||
<th>{{ $t('tournaments.sets') }}</th>
|
||||
<th>{{ $t('tournaments.diff') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(pl, idx) in group.rankings" :key="pl.id">
|
||||
<td><strong>{{ pl.position }}.</strong></td>
|
||||
<td><span v-if="pl.seeded" class="seeded-star">★</span>{{ pl.name }}</td>
|
||||
<td>{{ pl.points }}</td>
|
||||
<td>{{ pl.setsWon }}:{{ pl.setsLost }}</td>
|
||||
<td>{{ pl.setDiff >= 0 ? '+' + pl.setDiff : pl.setDiff }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</section>
|
||||
|
||||
<div v-if="finalPlacements.length === 0 && groupPlacements.length === 0" class="no-placements">
|
||||
<p>{{ $t('tournaments.noPlacementsYet') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import TournamentClassSelector from './TournamentClassSelector.vue';
|
||||
|
||||
export default {
|
||||
name: 'TournamentPlacementsTab',
|
||||
components: {
|
||||
TournamentClassSelector
|
||||
},
|
||||
props: {
|
||||
selectedDate: {
|
||||
type: [String, Number],
|
||||
default: null
|
||||
},
|
||||
selectedViewClass: {
|
||||
type: [Number, String, null],
|
||||
default: null
|
||||
},
|
||||
tournamentClasses: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
knockoutMatches: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
groups: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
groupRankings: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
participants: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
externalParticipants: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
pairings: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
emits: [
|
||||
'update:selectedViewClass'
|
||||
],
|
||||
computed: {
|
||||
finalPlacements() {
|
||||
// Extrahiere Endplatzierungen aus den K.O.-Matches
|
||||
const placements = [];
|
||||
const finishedMatches = this.knockoutMatches.filter(m => m.isFinished);
|
||||
|
||||
// Finde die Finale-Matches (letzte Runde)
|
||||
const rounds = [...new Set(finishedMatches.map(m => m.round))];
|
||||
if (rounds.length === 0) return [];
|
||||
|
||||
// Sortiere Runden nach Reihenfolge (Finale ist die letzte)
|
||||
const roundOrder = ['Finale', 'Halbfinale', 'Viertelfinale', 'Achtelfinale'];
|
||||
rounds.sort((a, b) => {
|
||||
const aIdx = roundOrder.indexOf(a);
|
||||
const bIdx = roundOrder.indexOf(b);
|
||||
if (aIdx === -1 && bIdx === -1) return a.localeCompare(b);
|
||||
if (aIdx === -1) return 1;
|
||||
if (bIdx === -1) return -1;
|
||||
return aIdx - bIdx;
|
||||
});
|
||||
|
||||
const finalRound = rounds[rounds.length - 1];
|
||||
const finalMatches = finishedMatches.filter(m => m.round === finalRound);
|
||||
|
||||
// Für jedes Finale-Match: Gewinner = Platz 1, Verlierer = Platz 2
|
||||
finalMatches.forEach(match => {
|
||||
const winner = this.getMatchWinner(match);
|
||||
const loser = this.getMatchLoser(match);
|
||||
|
||||
if (winner) {
|
||||
placements.push({
|
||||
id: `final-${match.id}-winner`,
|
||||
classId: match.classId,
|
||||
position: 1,
|
||||
name: winner,
|
||||
matchId: match.id
|
||||
});
|
||||
}
|
||||
if (loser) {
|
||||
placements.push({
|
||||
id: `final-${match.id}-loser`,
|
||||
classId: match.classId,
|
||||
position: 2,
|
||||
name: loser,
|
||||
matchId: match.id
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Wenn es Halbfinale gibt, finde die Verlierer (Platz 3)
|
||||
const semiFinalRound = rounds.find(r => r.includes('Halbfinale'));
|
||||
if (semiFinalRound) {
|
||||
const semiFinalMatches = finishedMatches.filter(m => m.round === semiFinalRound);
|
||||
semiFinalMatches.forEach(match => {
|
||||
const loser = this.getMatchLoser(match);
|
||||
if (loser) {
|
||||
placements.push({
|
||||
id: `semi-${match.id}-loser`,
|
||||
classId: match.classId,
|
||||
position: 3,
|
||||
name: loser,
|
||||
matchId: match.id
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return placements.sort((a, b) => {
|
||||
if (a.classId !== b.classId) {
|
||||
const aNum = a.classId || 999999;
|
||||
const bNum = b.classId || 999999;
|
||||
return aNum - bNum;
|
||||
}
|
||||
return a.position - b.position;
|
||||
});
|
||||
},
|
||||
finalPlacementsByClass() {
|
||||
const grouped = {};
|
||||
this.finalPlacements.forEach(p => {
|
||||
const key = p.classId || 'null';
|
||||
if (!grouped[key]) {
|
||||
grouped[key] = [];
|
||||
}
|
||||
grouped[key].push(p);
|
||||
});
|
||||
return grouped;
|
||||
},
|
||||
groupPlacements() {
|
||||
// Extrahiere Gruppenplatzierungen
|
||||
const placements = [];
|
||||
|
||||
this.groups.forEach(group => {
|
||||
const rankings = this.groupRankings[group.groupId] || [];
|
||||
if (rankings.length > 0) {
|
||||
placements.push({
|
||||
groupId: group.groupId,
|
||||
groupNumber: group.groupNumber,
|
||||
classId: group.classId,
|
||||
rankings: rankings.map(r => ({
|
||||
id: r.id,
|
||||
position: r.position,
|
||||
name: r.name,
|
||||
seeded: r.seeded,
|
||||
points: r.points,
|
||||
setsWon: r.setsWon,
|
||||
setsLost: r.setsLost,
|
||||
setDiff: r.setDiff
|
||||
}))
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return placements.sort((a, b) => {
|
||||
if (a.classId !== b.classId) {
|
||||
const aNum = a.classId || 999999;
|
||||
const bNum = b.classId || 999999;
|
||||
return aNum - bNum;
|
||||
}
|
||||
return a.groupNumber - b.groupNumber;
|
||||
});
|
||||
},
|
||||
groupPlacementsByClass() {
|
||||
const grouped = {};
|
||||
this.groupPlacements.forEach(p => {
|
||||
const key = p.classId || 'null';
|
||||
if (!grouped[key]) {
|
||||
grouped[key] = [];
|
||||
}
|
||||
grouped[key].push(p);
|
||||
});
|
||||
return grouped;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
shouldShowClass(classId) {
|
||||
if (this.selectedViewClass === null || this.selectedViewClass === undefined || this.selectedViewClass === '__none__') {
|
||||
return true;
|
||||
}
|
||||
const selectedId = typeof this.selectedViewClass === 'string' ? parseInt(this.selectedViewClass) : this.selectedViewClass;
|
||||
return classId === selectedId;
|
||||
},
|
||||
getClassName(classId) {
|
||||
if (classId === null || classId === '__none__' || classId === 'null' || classId === 'undefined' || classId === undefined) {
|
||||
return this.$t('tournaments.withoutClass');
|
||||
}
|
||||
try {
|
||||
const classIdNum = typeof classId === 'string' ? parseInt(classId) : classId;
|
||||
const classItem = this.tournamentClasses.find(c => c.id === classIdNum);
|
||||
return classItem ? classItem.name : '';
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
},
|
||||
getMatchWinner(match) {
|
||||
if (!match.isFinished || !match.tournamentResults || match.tournamentResults.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const results = match.tournamentResults || [];
|
||||
let win1 = 0, win2 = 0;
|
||||
results.forEach(r => {
|
||||
if (r.pointsPlayer1 > r.pointsPlayer2) win1++;
|
||||
else if (r.pointsPlayer2 > r.pointsPlayer1) win2++;
|
||||
});
|
||||
|
||||
if (win1 > win2) {
|
||||
return this.getPlayerName(match.player1, match);
|
||||
} else if (win2 > win1) {
|
||||
return this.getPlayerName(match.player2, match);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
getMatchLoser(match) {
|
||||
if (!match.isFinished || !match.tournamentResults || match.tournamentResults.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const results = match.tournamentResults || [];
|
||||
let win1 = 0, win2 = 0;
|
||||
results.forEach(r => {
|
||||
if (r.pointsPlayer1 > r.pointsPlayer2) win1++;
|
||||
else if (r.pointsPlayer2 > r.pointsPlayer1) win2++;
|
||||
});
|
||||
|
||||
if (win1 > win2) {
|
||||
const names = this.getMatchPlayerNames(match);
|
||||
return names ? names.name2 : null;
|
||||
} else if (win2 > win1) {
|
||||
const names = this.getMatchPlayerNames(match);
|
||||
return names ? names.name1 : null;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
getPlayerName(player, match) {
|
||||
if (!player && !match) return this.$t('tournaments.unknown');
|
||||
|
||||
// Prüfe ob es ein Doppel ist
|
||||
if (match && match.classId) {
|
||||
const tournamentClass = this.tournamentClasses.find(c => c.id === match.classId);
|
||||
if (tournamentClass && tournamentClass.isDoubles) {
|
||||
// Finde die Paarung basierend auf player1Id oder player2Id
|
||||
const playerId = player?.id || (match.player1Id === player?.id ? match.player1Id : match.player2Id);
|
||||
const pairing = this.pairings.find(p =>
|
||||
p.classId === match.classId &&
|
||||
(p.member1Id === playerId || p.member2Id === playerId ||
|
||||
p.external1Id === playerId || p.external2Id === playerId)
|
||||
);
|
||||
|
||||
if (pairing) {
|
||||
const name1 = this.getPairingPlayerName(pairing, 1);
|
||||
const name2 = this.getPairingPlayerName(pairing, 2);
|
||||
return `${name1} / ${name2}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Normale Spieler
|
||||
if (player) {
|
||||
if (player.member) {
|
||||
return `${player.member.firstName} ${player.member.lastName}`;
|
||||
} else if (player.firstName && player.lastName) {
|
||||
return `${player.firstName} ${player.lastName}`;
|
||||
}
|
||||
}
|
||||
return this.$t('tournaments.unknown');
|
||||
},
|
||||
getPairingPlayerName(pairing, playerNumber) {
|
||||
if (playerNumber === 1) {
|
||||
if (pairing.member1 && pairing.member1.member) {
|
||||
return `${pairing.member1.member.firstName} ${pairing.member1.member.lastName}`;
|
||||
} else if (pairing.external1) {
|
||||
return `${pairing.external1.firstName} ${pairing.external1.lastName}`;
|
||||
}
|
||||
} else if (playerNumber === 2) {
|
||||
if (pairing.member2 && pairing.member2.member) {
|
||||
return `${pairing.member2.member.firstName} ${pairing.member2.member.lastName}`;
|
||||
} else if (pairing.external2) {
|
||||
return `${pairing.external2.firstName} ${pairing.external2.lastName}`;
|
||||
}
|
||||
}
|
||||
return this.$t('tournaments.unknown');
|
||||
},
|
||||
getMatchPlayerNames(match) {
|
||||
const classId = match.classId;
|
||||
if (classId) {
|
||||
const tournamentClass = this.tournamentClasses.find(c => c.id === classId);
|
||||
if (tournamentClass && tournamentClass.isDoubles) {
|
||||
const pairing1 = this.pairings.find(p =>
|
||||
p.classId === classId &&
|
||||
(p.member1Id === match.player1Id || p.external1Id === match.player1Id ||
|
||||
p.member2Id === match.player1Id || p.external2Id === match.player1Id)
|
||||
);
|
||||
const pairing2 = this.pairings.find(p =>
|
||||
p.classId === classId &&
|
||||
(p.member1Id === match.player2Id || p.external1Id === match.player2Id ||
|
||||
p.member2Id === match.player2Id || p.external2Id === match.player2Id)
|
||||
);
|
||||
|
||||
if (pairing1 && pairing2) {
|
||||
const name1 = this.getPairingPlayerName(pairing1, 1) + ' / ' + this.getPairingPlayerName(pairing1, 2);
|
||||
const name2 = this.getPairingPlayerName(pairing2, 1) + ' / ' + this.getPairingPlayerName(pairing2, 2);
|
||||
return { name1, name2 };
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
name1: this.getPlayerName(match.player1, match),
|
||||
name2: this.getPlayerName(match.player2, match)
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.final-placements,
|
||||
.group-placements {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.class-section {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.class-header {
|
||||
margin-bottom: 1rem;
|
||||
color: #333;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.group-table {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.group-table h5 {
|
||||
margin-bottom: 0.5rem;
|
||||
color: #666;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
thead {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
th, td {
|
||||
padding: 0.5rem;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
th {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.seeded-star {
|
||||
color: #ff9800;
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
.no-placements {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
color: #666;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -589,6 +589,10 @@
|
||||
"tabGroups": "Gruppen",
|
||||
"tabParticipants": "Teilnehmer",
|
||||
"tabResults": "Ergebnisse",
|
||||
"tabPlacements": "Platzierungen",
|
||||
"finalPlacements": "Endplatzierungen",
|
||||
"groupPlacements": "Gruppenplatzierungen",
|
||||
"noPlacementsYet": "Noch keine Platzierungen vorhanden.",
|
||||
"participants": "Teilnehmer",
|
||||
"seeded": "Gesetzt",
|
||||
"club": "Verein",
|
||||
|
||||
@@ -61,6 +61,11 @@
|
||||
:class="['tab-button', { 'active': activeTab === 'results' }]">
|
||||
{{ $t('tournaments.tabResults') }}
|
||||
</button>
|
||||
<button
|
||||
@click="setActiveTab('placements')"
|
||||
:class="['tab-button', { 'active': activeTab === 'placements' }]">
|
||||
{{ $t('tournaments.tabPlacements') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Tab: Konfiguration -->
|
||||
@@ -203,6 +208,21 @@
|
||||
@start-knockout="startKnockout"
|
||||
@reset-knockout="resetKnockout"
|
||||
/>
|
||||
|
||||
<!-- Tab: Platzierungen -->
|
||||
<TournamentPlacementsTab
|
||||
v-if="activeTab === 'placements'"
|
||||
:selected-date="selectedDate"
|
||||
:selected-view-class="selectedViewClass"
|
||||
:tournament-classes="tournamentClasses"
|
||||
:knockout-matches="knockoutMatches"
|
||||
:groups="groups"
|
||||
:group-rankings="groupRankings"
|
||||
:participants="participants"
|
||||
:external-participants="externalParticipants"
|
||||
:pairings="pairings"
|
||||
@update:selectedViewClass="selectedViewClass = $event"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -239,6 +259,7 @@ import TournamentConfigTab from '../components/tournament/TournamentConfigTab.vu
|
||||
import TournamentGroupsTab from '../components/tournament/TournamentGroupsTab.vue';
|
||||
import TournamentParticipantsTab from '../components/tournament/TournamentParticipantsTab.vue';
|
||||
import TournamentResultsTab from '../components/tournament/TournamentResultsTab.vue';
|
||||
import TournamentPlacementsTab from '../components/tournament/TournamentPlacementsTab.vue';
|
||||
export default {
|
||||
name: 'TournamentTab',
|
||||
components: {
|
||||
@@ -247,7 +268,8 @@ export default {
|
||||
TournamentConfigTab,
|
||||
TournamentGroupsTab,
|
||||
TournamentParticipantsTab,
|
||||
TournamentResultsTab
|
||||
TournamentResultsTab,
|
||||
TournamentPlacementsTab
|
||||
},
|
||||
props: {
|
||||
allowsExternal: {
|
||||
@@ -1670,28 +1692,34 @@ export default {
|
||||
},
|
||||
|
||||
async removeParticipant(p) {
|
||||
if (this.allowsExternal && p.isExternal) {
|
||||
// Externer Teilnehmer
|
||||
await apiClient.delete('/tournament/external-participant', {
|
||||
data: {
|
||||
clubId: this.currentClub,
|
||||
tournamentId: this.selectedDate,
|
||||
participantId: p.id
|
||||
}
|
||||
});
|
||||
this.externalParticipants = this.externalParticipants.filter(x => x.id !== p.id);
|
||||
} else {
|
||||
// Interner Teilnehmer
|
||||
await apiClient.delete('/tournament/participant', {
|
||||
data: {
|
||||
clubId: this.currentClub,
|
||||
tournamentId: this.selectedDate,
|
||||
participantId: p.id
|
||||
}
|
||||
});
|
||||
this.participants = this.participants.filter(x => x.id !== p.id);
|
||||
try {
|
||||
if (this.allowsExternal && p.isExternal) {
|
||||
// Externer Teilnehmer
|
||||
await apiClient.delete('/tournament/external-participant', {
|
||||
data: {
|
||||
clubId: this.currentClub,
|
||||
tournamentId: this.selectedDate,
|
||||
participantId: p.id
|
||||
}
|
||||
});
|
||||
this.externalParticipants = this.externalParticipants.filter(x => x.id !== p.id);
|
||||
} else {
|
||||
// Interner Teilnehmer
|
||||
await apiClient.delete('/tournament/participant', {
|
||||
data: {
|
||||
clubId: this.currentClub,
|
||||
tournamentId: this.selectedDate,
|
||||
participantId: p.id
|
||||
}
|
||||
});
|
||||
this.participants = this.participants.filter(x => x.id !== p.id);
|
||||
}
|
||||
await this.loadTournamentData();
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Entfernen des Teilnehmers:', error);
|
||||
const message = safeErrorMessage(error, 'Fehler beim Entfernen des Teilnehmers.');
|
||||
await this.showInfo('Fehler', message, '', 'error');
|
||||
}
|
||||
await this.loadTournamentData();
|
||||
},
|
||||
|
||||
async onGroupCountChange() {
|
||||
|
||||
Reference in New Issue
Block a user