feat(tournament): add participant gave-up functionality and UI updates

- Implemented setParticipantGaveUp and setExternalParticipantGaveUp methods in tournamentController to handle participant resignation.
- Updated ExternalTournamentParticipant and TournamentMember models to include a gaveUp field for tracking resignation status.
- Enhanced tournamentRoutes to include new endpoints for updating gave-up status.
- Modified TournamentGroupsTab and TournamentParticipantsTab components to display and manage gave-up status visually.
- Added localization strings for "gave up" and related hints in German.
- Updated TournamentResultsTab to reflect gave-up status in match results.
This commit is contained in:
Torsten Schulz (local)
2026-01-30 22:45:54 +01:00
parent 18a191f686
commit 7e1b09fa97
11 changed files with 344 additions and 41 deletions

View File

@@ -128,7 +128,7 @@
<tr v-for="(pl, idx) in groupRankings[group.groupId]" :key="pl.id">
<td><strong>G{{ String.fromCharCode(96 + group.groupNumber) }}{{ idx + 1 }}</strong></td>
<td>{{ pl.position }}.</td>
<td><span v-if="pl.seeded" class="seeded-star"></span>{{ pl.name }}</td>
<td><span v-if="pl.seeded" class="seeded-star">★</span>{{ pl.name }}<span v-if="pl.gaveUp" class="gave-up-badge" :title="$t('tournaments.gaveUpHint')">{{ $t('tournaments.gaveUp') }}</span></td>
<td>{{ (pl.matchesWon || 0) * 2 }}:{{ (pl.matchesLost || 0) * 2 }}</td>
<td>{{ pl.setsWon }}:{{ pl.setsLost }}</td>
<td>
@@ -513,5 +513,13 @@ export default {
.merge-pools-actions {
margin-top: 0.5rem;
}
.gave-up-badge {
margin-left: 0.35rem;
padding: 0.1rem 0.35rem;
font-size: 0.75rem;
background: #f8d7da;
color: #721c24;
border-radius: 4px;
}
</style>

View File

@@ -93,6 +93,7 @@
<thead>
<tr>
<th class="participant-seeded-cell">{{ $t('tournaments.seeded') }}</th>
<th class="participant-gave-up-cell">{{ $t('tournaments.gaveUp') }}</th>
<th class="participant-name">{{ $t('tournaments.name') }}</th>
<th v-if="allowsExternal" class="participant-gender-cell">{{ $t('members.gender') }}</th>
<th v-if="allowsExternal" class="participant-club-cell">{{ $t('tournaments.club') }}</th>
@@ -110,6 +111,11 @@
<input type="checkbox" :checked="participant.seeded" @change="$emit('update-participant-seeded', participant, $event)" />
</label>
</td>
<td class="participant-gave-up-cell">
<label class="gave-up-checkbox-label" :title="$t('tournaments.gaveUpHint')">
<input type="checkbox" :checked="participant.gaveUp" @change="$emit('update-participant-gave-up', participant, $event)" />
</label>
</td>
<td class="participant-name">
<template v-if="participant.member">
{{ participant.member.firstName || $t('tournaments.unknown') }}
@@ -168,6 +174,7 @@
<thead>
<tr>
<th class="participant-seeded-cell">{{ $t('tournaments.seeded') }}</th>
<th class="participant-gave-up-cell">{{ $t('tournaments.gaveUp') }}</th>
<th class="participant-name">{{ $t('tournaments.name') }}</th>
<th v-if="allowsExternal" class="participant-gender-cell">{{ $t('members.gender') }}</th>
<th v-if="allowsExternal" class="participant-club-cell">{{ $t('tournaments.club') }}</th>
@@ -186,6 +193,11 @@
<input type="checkbox" :checked="participant.seeded" @change="$emit('update-participant-seeded', participant, $event)" />
</label>
</td>
<td class="participant-gave-up-cell">
<label class="gave-up-checkbox-label" :title="$t('tournaments.gaveUpHint')">
<input type="checkbox" :checked="participant.gaveUp" @change="$emit('update-participant-gave-up', participant, $event)" />
</label>
</td>
<td class="participant-name">
<template v-if="participant.member">
{{ participant.member.firstName || $t('tournaments.unknown') }}
@@ -422,6 +434,7 @@ export default {
'update:newExternalParticipant',
'add-external-participant',
'update-participant-seeded',
'update-participant-gave-up',
'update-participant-group',
'update-participant-class',
'remove-participant',

View File

@@ -48,6 +48,10 @@
<template v-if="m.result === 'BYE'">
BYE
</template>
<template v-else-if="matchHasGaveUp(m)">
<span class="result-text gave-up-result">{{ formatResult(m) }}</span>
<span v-if="m.player1?.gaveUp && m.player2?.gaveUp" class="gave-up-badge-small">({{ $t('tournaments.gaveUp') }})</span>
</template>
<template v-else-if="!m.isFinished">
<template v-for="r in m.tournamentResults" :key="r.set">
<template v-if="isEditing(m, r.set)">
@@ -86,10 +90,15 @@
{{ getSetsString(m) }}
</td>
<td>
<button v-if="!m.isFinished" @click="$emit('finish-match', m)">Abschließen</button>
<button v-else @click="$emit('reopen-match', m)" class="btn-correct">Korrigieren</button>
<button v-if="!m.isFinished && !m.isActive" @click.stop="$emit('set-match-active', m, true)" class="btn-live" title="Als laufend markieren"></button>
<button v-if="!m.isFinished && m.isActive" @click.stop="$emit('set-match-active', m, false)" class="btn-live active" title="Laufend-Markierung entfernen"></button>
<template v-if="matchHasGaveUp(m)">
<span class="no-edit-hint">{{ $t('tournaments.gaveUp') }}</span>
</template>
<template v-else>
<button v-if="!m.isFinished" @click="$emit('finish-match', m)">Abschließen</button>
<button v-else @click="$emit('reopen-match', m)" class="btn-correct">Korrigieren</button>
<button v-if="!m.isFinished && !m.isActive" @click.stop="$emit('set-match-active', m, true)" class="btn-live" title="Als laufend markieren"></button>
<button v-if="!m.isFinished && m.isActive" @click.stop="$emit('set-match-active', m, false)" class="btn-live active" title="Laufend-Markierung entfernen"></button>
</template>
</td>
</tr>
</tbody>
@@ -502,6 +511,9 @@ export default {
isLastResult(match, result) {
const arr = match.tournamentResults || [];
return arr.length > 0 && arr[arr.length - 1].set === result.set;
},
matchHasGaveUp(match) {
return match.gaveUpMatch || match.player1?.gaveUp || match.player2?.gaveUp;
}
}
};
@@ -555,6 +567,19 @@ export default {
color: #856404 !important;
}
.gave-up-result {
color: #626262;
}
.gave-up-badge-small {
margin-left: 0.25rem;
font-size: 0.8rem;
color: #721c24;
}
.no-edit-hint {
font-size: 0.85rem;
color: #6c757d;
}
.active-match:hover {
background-color: #ffe69c !important;
}

View File

@@ -595,6 +595,8 @@
"noPlacementsYet": "Noch keine Platzierungen vorhanden.",
"participants": "Teilnehmer",
"seeded": "Gesetzt",
"gaveUp": "Aufgegeben",
"gaveUpHint": "Spieler hat aufgegeben alle Spiele zählen für den Gegner (11:0) bzw. 0:0 bei beiden aufgegeben.",
"club": "Verein",
"class": "Klasse",
"group": "Gruppe",

View File

@@ -137,6 +137,7 @@
@update:newExternalParticipant="newExternalParticipant = $event"
@add-external-participant="addExternalParticipant()"
@update-participant-seeded="updateParticipantSeeded"
@update-participant-gave-up="updateParticipantGaveUp"
@update-participant-group="updateParticipantGroup"
@update-participant-class="updateParticipantClass"
@remove-participant="removeParticipant"
@@ -485,6 +486,7 @@ export default {
id: p.id,
name: p.name,
seeded: p.seeded || false,
gaveUp: p.gaveUp || false,
position: p.position || 0,
points: p.points || 0,
setsWon: p.setsWon || 0,
@@ -1233,6 +1235,7 @@ export default {
this.participants = (Array.isArray(pRes.data) ? pRes.data : []).map(p => ({
...p,
seeded: p.seeded || false,
gaveUp: p.gaveUp || false,
groupNumber: p.groupId ? (groupIdToNumberMap[p.groupId] || null) : null
}));
@@ -1275,6 +1278,7 @@ export default {
this.externalParticipants = (Array.isArray(allExternalParticipants) ? allExternalParticipants : []).map(p => ({
...p,
seeded: p.seeded || false,
gaveUp: p.gaveUp || false,
isExternal: true,
groupNumber: p.groupId ? (groupIdToNumberMap[p.groupId] || null) : null
}));
@@ -2297,6 +2301,21 @@ export default {
}
},
async updateParticipantGaveUp(participant, event) {
const gaveUp = event.target.checked;
participant.gaveUp = gaveUp;
try {
if (this.allowsExternal && participant.isExternal) {
await apiClient.put(`/tournament/external-participant/${this.currentClub}/${this.selectedDate}/${participant.id}/gave-up`, { gaveUp });
} else {
await apiClient.put(`/tournament/participant/${this.currentClub}/${this.selectedDate}/${participant.id}/gave-up`, { gaveUp });
}
} catch (error) {
console.error('Fehler beim Aktualisieren des Aufgabe-Status:', error);
await this.loadTournamentData();
}
},
async updateParticipantGroup(participant, event) {
const value = event.target.value;
const groupNumber = value === '' || value === 'null' ? null : parseInt(value);
@@ -3467,7 +3486,9 @@ button {
/* Synchronisiere Spaltenbreiten zwischen Header und Body */
.participants-table-header .participant-seeded-cell,
.participants-table-body .participant-seeded-cell {
.participants-table-body .participant-seeded-cell,
.participants-table-header .participant-gave-up-cell,
.participants-table-body .participant-gave-up-cell {
width: 60px;
}
@@ -3500,10 +3521,17 @@ button {
line-height: 1.2;
}
.participant-seeded-cell {
.participant-seeded-cell,
.participant-gave-up-cell {
white-space: nowrap;
}
.gave-up-checkbox-label {
display: flex;
align-items: center;
justify-content: center;
}
.seeded-checkbox-label {
display: flex;
align-items: center;