some enhancements for tournaments

This commit is contained in:
Torsten Schulz
2025-07-15 18:06:07 +02:00
parent f29185dd33
commit 69b4302e23
7 changed files with 646 additions and 492 deletions

View File

@@ -1,107 +1,127 @@
<template>
<div>
<div class="tournaments-view">
<h2>Turnier</h2>
<div>
<div>
<h3>Datum</h3>
<div>
<select v-model="selectedDate">
<option value="new">Neues Turnier</option>
<option v-for="date in dates" :key="date.id" :value="date.id">
{{ new Date(date.date).toLocaleDateString('de-DE', {
year: 'numeric',
month: '2-digit',
day: '2-digit'
}) }}
</option>
</select>
</div>
<template v-if="selectedDate === 'new'">
<div>
<input type="date" v-model="newDate" />
<button @click="createTournament">Erstellen</button>
</div>
</template>
<template v-else>
<div>
<h3>Turnier</h3>
<div>
<div>
<label>
<input type="checkbox" v-model="isGroupTournament">
Spielen in Gruppen
</label>
</div>
<div>
<h4>Teilnehmer</h4>
<ul>
<li v-for="participant in participants" :key="participant.id">
{{ participant.member.firstName }} {{ participant.member.lastName }}
</li>
</ul>
<select v-model="selectedMember">
<option v-for="member in clubMembers" :key="member.id" :value="member.id">
{{ member.firstName }} {{ member.lastName }}
</option>
</select>
<button type="button" @click="addParticipant">Hinzufügen</button>
</div>
<div v-if="isGroupTournament && participants.length > 1">
<label>
Anzahl Gruppen:
<input type="number" v-model="numberOfGroups">
</label>
<button @click="createGroups">Gruppen erstellen</button>
<button @click="randomizeGroups">Zufällig verteilen</button>
</div>
<div v-if="groups && groups.length > 0">
<h4>Gruppen</h4>
<ul class="groupoverview">
<li v-for="group in groups" :key="group.groupId">
<h4>Gruppe {{ group.groupId }}</h4>
<table>
<thead>
<tr>
<th>Spielername</th>
<th>Bilanz</th>
</tr>
</thead>
<tbody>
<tr v-for="participant in group.participants" :key="participant.id">
<td>{{ participant.name }}</td>
<td></td>
</tr>
</tbody>
</table>
</li>
</ul>
<div>
<table>
<thead>
<tr>
<th>&nbsp;</th>
<th>Begegnung</th>
<th>Sätze</th>
</tr>
</thead>
<tbody>
<tr v-for="match in matches" :key="match.id">
<td>{{ match.groupId ? "Gr " + match.groupId : match.round }}</td>
<td>{{ getPlayerName(match.player1) }} - {{ getPlayerName(match.player2) }}</td>
<td v-for="result in match.tournamentResults">{{ result.pointsPlayer1 }}:{{ result.pointsPlayer2 }}</td>
<td><input size="5" type="text" v-model="match.result" @keyup.enter="saveMatchResult(match, match.tournamentResults.length + 1, match.result)" /></td>
<td v-if="match.isFinished">{{ match.result ?? '0:0' }}</td>
<td v-else><button @click="finishMatch(match)">Abschließen</button></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</template>
<!-- Datumsauswahl / Neues Turnier -->
<div class="tournament-config">
<h3>Datum</h3>
<select v-model="selectedDate">
<option value="new">Neues Turnier</option>
<option v-for="date in dates" :key="date.id" :value="date.id">
{{ new Date(date.date).toLocaleDateString('de-DE', {
year: 'numeric',
month: '2-digit',
day: '2-digit'
}) }}
</option>
</select>
<div v-if="selectedDate === 'new'" class="new-tournament">
<input type="date" v-model="newDate" />
<button @click="createTournament">Erstellen</button>
</div>
</div>
<!-- Konfiguration & Gruppenphase -->
<div v-if="selectedDate !== 'new'" class="tournament-setup">
<label>
<input type="checkbox" v-model="isGroupTournament" />
Spielen in Gruppen
</label>
<section class="participants">
<h4>Teilnehmer</h4>
<ul>
<li v-for="participant in participants" :key="participant.id">
{{ participant.member.firstName }}
{{ participant.member.lastName }}
</li>
</ul>
<select v-model="selectedMember">
<option v-for="member in clubMembers" :key="member.id" :value="member.id">
{{ member.firstName }}
{{ member.lastName }}
</option>
</select>
<button @click="addParticipant">Hinzufügen</button>
</section>
<section v-if="isGroupTournament && participants.length > 1" class="group-controls">
<label>
Anzahl Gruppen:
<input type="number" v-model.number="numberOfGroups" min="1" />
</label>
<button @click="createGroups">Gruppen erstellen</button>
<button @click="randomizeGroups">Zufällig verteilen</button>
</section>
<section v-if="groups.length" class="groups-overview">
<h3>Gruppenübersicht</h3>
<div v-for="group in groups" :key="group.groupId" class="group-table">
<h4>Gruppe {{ group.groupId }}</h4>
<table>
<thead>
<tr>
<th>Platz</th>
<th>Spieler</th>
<th>Punkte</th>
<th>Satz</th>
<th>Diff</th>
</tr>
</thead>
<tbody>
<tr v-for="pl in groupRankings[group.groupId]" :key="pl.id">
<td>{{ pl.position }}.</td>
<td>{{ 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>
</section>
</div>
<!-- K.o.-Runde starten -->
<div v-if="participants.length > 1 && !showKnockout" class="ko-start">
<button @click="startKnockout">
K.o.-Runde starten
</button>
</div>
<!-- K.o.-Runde anzeigen -->
<section v-if="showKnockout" class="ko-round">
<h4>K.-o.-Runde</h4>
<table>
<thead>
<tr>
<th>Runde</th>
<th>Begegnung</th>
<th>Ergebnis</th>
<th>Aktion</th>
</tr>
</thead>
<tbody>
<tr v-for="m in knockoutMatches" :key="m.id">
<td>{{ m.round }}</td>
<td>
{{ getPlayerName(m.player1) }}
{{ getPlayerName(m.player2) }}
</td>
<td>{{ m.result || '-' }}</td>
<td v-if="!m.isFinished">
<input v-model="m.resultInput" placeholder="z.B. 11:4, 4:11, 4, -4"
@keyup.enter="saveMatchResult(m, m.resultInput)" />
<button @click="finishMatch(m)">
Fertig
</button>
</td>
</tr>
</tbody>
</table>
</section>
</div>
</template>
@@ -111,60 +131,77 @@ import apiClient from '../apiClient';
export default {
name: 'TournamentsView',
computed: {
...mapGetters(['isAuthenticated', 'currentClub', 'clubs']),
},
data() {
return {
selectedDate: 'new',
newDate: '',
dates: [],
participants: [],
selectedMember: null,
clubMembers: [],
numberOfGroups: 1,
isGroupTournament: false,
groups: [],
matches: [],
showKnockout: false,
};
},
computed: {
...mapGetters(['isAuthenticated', 'currentClub']),
knockoutMatches() {
return this.matches.filter(m => m.round !== 'group');
},
groupRankings() {
const byGroup = {};
this.groups.forEach(g => {
byGroup[g.groupId] = g.participants.map(p => ({
id: p.id,
name: p.name,
points: 0,
setsWon: 0,
setsLost: 0,
setDiff: 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 += 2;
else if (s2 > s1) e2.points += 2;
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);
});
rankings[gid] = arr.map((p, i) => ({
...p, position: i + 1
}));
});
return rankings;
}
},
watch: {
selectedDate: async function (newVal) {
if (newVal !== 'new') {
try {
const groupResponse = await apiClient.get(`/tournament/${this.currentClub}/${newVal}`);
this.isGroupTournament = groupResponse.data.type === 'groups';
const participantsResponse = await apiClient.post('/tournament/participants', {
clubId: this.currentClub,
tournamentId: newVal,
});
this.participants = participantsResponse.data;
} catch (error) {
console.error(error);
}
await this.fetchGroups();
selectedDate: {
immediate: true,
handler: async function (val) {
if (val === 'new') return;
await this.loadTournamentData();
}
},
isGroupTournament: async function (newVal) {
if (newVal) {
this.numberOfGroups = 2;
} else {
this.numberOfGroups = 1;
}
await apiClient.post('/tournament/modus', {
clubId: this.currentClub,
tournamentId: this.selectedDate,
type: newVal ? 'groups' : 'bestOf',
numberOfGroups: this.numberOfGroups,
});
},
numberOfGroups: async function (newVal) {
await apiClient.post('/tournament/modus', {
clubId: this.currentClub,
tournamentId: this.selectedDate,
type: this.isGroupTournament ? 'groups' : 'bestOf',
numberOfGroups: newVal,
});
}
},
async created() {
@@ -172,132 +209,163 @@ export default {
this.$router.push('/login');
return;
}
try {
const responseDates = await apiClient.get(`/tournament/${this.currentClub}`);
this.dates = responseDates.data;
} catch (error) {
console.error('Error fetching tournaments:', error);
}
try {
const responseMembers = await apiClient.get(`/clubmembers/get/${this.currentClub}/false`);
this.clubMembers = responseMembers.data;
} catch (error) {
console.error('Error fetching club members:', error);
}
// Turniere und Mitglieder laden
const d = await apiClient.get(`/tournament/${this.currentClub}`);
this.dates = d.data;
const m = await apiClient.get(
`/clubmembers/get/${this.currentClub}/false`
);
this.clubMembers = m.data;
},
methods: {
async loadTournamentData() {
// 1) TurnierMetadaten holen (Typ + Anzahl Gruppen)
const tRes = await apiClient.get(
`/tournament/${this.currentClub}/${this.selectedDate}`
);
const tournament = tRes.data;
this.isGroupTournament = tournament.type === 'groups';
this.numberOfGroups = tournament.numberOfGroups;
// 2) Teilnehmer
const pRes = await apiClient.post('/tournament/participants', {
clubId: this.currentClub,
tournamentId: this.selectedDate
});
this.participants = pRes.data;
// 3) Gruppen (mit Teilnehmern)
const gRes = await apiClient.get('/tournament/groups', {
params: {
clubId: this.currentClub,
tournamentId: this.selectedDate
}
});
this.groups = gRes.data;
// 4) Alle Matches
const mRes = await apiClient.get(
`/tournament/matches/${this.currentClub}/${this.selectedDate}`
);
this.matches = mRes.data;
// 5) Steuere K.o.-Anzeige
this.showKnockout = this.matches.some(m => m.round !== 'group');
},
getPlayerName(p) {
return p.member.firstName + ' ' + p.member.lastName;
},
async createTournament() {
try {
const response = await apiClient.post('/tournament', {
clubId: this.currentClub,
name: this.newDate,
date: this.newDate,
});
this.dates = response.data;
this.newDate = '';
} catch (error) {
console.error('Error creating tournament:', error);
}
const r = await apiClient.post('/tournament', {
clubId: this.currentClub,
tournamentName: this.newDate,
date: this.newDate
});
this.dates = r.data;
this.selectedDate = this.dates[this.dates.length - 1].id;
this.newDate = '';
},
async addParticipant() {
try {
const response = await apiClient.post('/tournament/participant', {
clubId: this.currentClub,
tournamentId: this.selectedDate,
participant: this.selectedMember,
});
this.participants = response.data;
} catch (error) {
console.error('Error adding participant:', error);
}
const r = await apiClient.post('/tournament/participant', {
clubId: this.currentClub,
tournamentId: this.selectedDate,
participant: this.selectedMember
});
this.participants = r.data;
},
async createGroups() {
await apiClient.put('/tournament/groups', {
clubId: this.currentClub,
tournamentId: this.selectedDate,
tournamentId: this.selectedDate
});
await this.fetchGroups();
await this.loadTournamentData();
},
async randomizeGroups() {
try {
const response = await apiClient.post('/tournament/groups', {
const r = await apiClient.post('/tournament/groups', {
clubId: this.currentClub,
tournamentId: this.selectedDate,
tournamentId: this.selectedDate
});
} catch (error) {
console.error('Error randomizing groups:', error);
this.participants = r.data;
} catch (err) {
alert('Fehler beim ZufälligVerteilen:\n' +
(err.response?.data?.error || err.message));
}
await this.fetchGroups();
await this.loadTournamentData();
},
async fetchGroups() {
try {
const response = await apiClient.get('/tournament/groups', {
params: {
clubId: this.currentClub,
tournamentId: this.selectedDate
}
});
this.groups = response.data;
const matchesResponse = await apiClient.get(`/tournament/matches/${this.currentClub}/${this.selectedDate}`);
this.matches = matchesResponse.data;
console.log(this.matches);
} catch (error) {
console.error('Error fetching groups:', error);
}
},
getPlayerName(player) {
return player.member.firstName + ' ' + player.member.lastName;
},
async saveMatchResult(match, set, result) {
try {
await apiClient.post('/tournament/match/result', {
clubId: this.currentClub,
tournamentId: this.selectedDate,
matchId: match.id,
set,
result: result,
});
this.fetchGroups();
} catch (error) {
console.error('Error saving match result:', error);
async saveMatchResult(match, result) {
// wenn kein ':' dabei, ergänzen
if (result.indexOf(':') === -1) {
result = result.indexOf('-') > -1
? '11:' + result
: (result * -1) + ':11';
}
await apiClient.post('/tournament/match/result', {
clubId: this.currentClub,
tournamentId: this.selectedDate,
matchId: match.id,
set: (match.tournamentResults?.length || 0) + 1,
result
});
await this.loadTournamentData();
},
async finishMatch(match) {
try {
await apiClient.post('/tournament/match/finish', {
clubId: this.currentClub,
tournamentId: this.selectedDate,
matchId: match.id,
});
this.fetchGroups();
} catch (error) {
console.error('Error finishing match:', error);
}
await apiClient.post('/tournament/match/finish', {
clubId: this.currentClub,
tournamentId: this.selectedDate,
matchId: match.id
});
await this.loadTournamentData();
},
},
async startKnockout() {
await apiClient.post('/tournament/knockout', {
clubId: this.currentClub,
tournamentId: this.selectedDate
});
await this.loadTournamentData();
}
}
};
</script>
<style scoped>
.tournaments {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
.tournaments-view {
padding: 1rem;
}
.groupoverview {
display: flex;
flex-direction: row;
align-items: baseline;
justify-content: left;
padding: 0;
.participants,
.group-controls,
.groups-overview,
.ko-round,
.ko-start {
margin-top: 1.5rem;
}
.groupoverview li {
list-style-type: none;
margin: 0;
padding: 0;
.group-table {
margin-bottom: 2rem;
}
table {
width: 100%;
border-collapse: collapse;
}
th,
td {
padding: 0.5em;
border: 1px solid #ccc;
text-align: left;
}
button {
margin-left: 0.5em;
}
</style>