feat(tournaments): add update functionality for official tournaments

- Implemented an API endpoint to update the title of official tournaments, including error handling for non-existent tournaments.
- Enhanced the frontend to allow users to edit tournament titles directly, with input validation and feedback for successful updates or errors.
- Updated the German localization file to include new strings for editing titles and error messages.
This commit is contained in:
Torsten Schulz (local)
2026-03-10 21:54:03 +01:00
parent ea6acd8c6c
commit c13f426b3d
6 changed files with 136 additions and 5 deletions

View File

@@ -1400,6 +1400,8 @@
"showParticipations": "Turnierbeteiligungen anzeigen",
"savedEvents": "Gespeicherte Veranstaltungen",
"tournament": "Turnier",
"editTitle": "Titel bearbeiten",
"updateTitleError": "Titel konnte nicht gespeichert werden.",
"delete": "Löschen",
"timeRange": "Zeitraum:",
"last3Months": "Letzte 3 Monate",

View File

@@ -13,12 +13,35 @@
</div>
<div v-if="topActiveTab==='events'">
<h3>{{ $t('officialTournaments.savedEvents') }}</h3>
<ul>
<li v-for="t in list" :key="t.id" style="display:flex; align-items:center; gap:.5rem;">
<a href="#" @click.prevent="uploadedId = String(t.id); reload();" style="flex:1;">
<ul class="event-list">
<li v-for="t in list" :key="t.id" class="event-item">
<template v-if="editingTournamentId === t.id">
<input
ref="titleEditInput"
v-model="editingTitle"
class="title-input"
@blur="saveTournamentTitle(t)"
@keydown.enter="saveTournamentTitle(t)"
@keydown.esc="cancelTitleEdit"
/>
</template>
<a
v-else
href="#"
class="event-title"
@click.prevent="uploadedId = String(t.id); reload();"
>
{{ t.title || ($t('officialTournaments.tournament') + ' #' + t.id) }}
</a>
<span v-if="t.termin || t.eventDate"> {{ t.termin || t.eventDate }}</span>
<span v-if="editingTournamentId !== t.id && (t.termin || t.eventDate)" class="event-date"> {{ t.termin || t.eventDate }}</span>
<button
v-if="editingTournamentId !== t.id"
class="btn-icon"
:title="$t('officialTournaments.editTitle')"
@click.prevent="startTitleEdit(t)"
>
</button>
<button class="btn-secondary" @click.prevent="removeTournament(t)" :title="$t('officialTournaments.delete')">🗑</button>
</li>
</ul>
@@ -419,6 +442,8 @@ export default {
loadingClubParticipations: false,
clubParticipationRowsData: [],
participationRange: 'all',
editingTournamentId: null,
editingTitle: '',
};
},
computed: {
@@ -1237,6 +1262,35 @@ export default {
return `${dd}.${mm}.${yyyy}`;
},
startTitleEdit(t) {
this.editingTournamentId = t.id;
this.editingTitle = t.title || '';
this.$nextTick(() => {
const el = this.$refs.titleEditInput;
if (Array.isArray(el)) {
if (el[0]) el[0].focus();
} else if (el) el.focus();
});
},
cancelTitleEdit() {
this.editingTournamentId = null;
this.editingTitle = '';
},
async saveTournamentTitle(t) {
const newTitle = (this.editingTitle || '').trim();
this.editingTournamentId = null;
this.editingTitle = '';
if (newTitle === (t.title || '').trim()) return;
try {
await apiClient.patch(`/official-tournaments/${this.currentClub}/${t.id}`, { title: newTitle || null });
t.title = newTitle || null;
if (String(this.uploadedId) === String(t.id) && this.parsed) {
this.parsed.parsedData = { ...this.parsed.parsedData, title: newTitle || this.parsed.parsedData.title };
}
} catch (error) {
this.showInfo(this.$t('common.error'), this.$t('officialTournaments.updateTitleError'), getSafeErrorMessage(error), 'error');
}
},
async removeTournament(t) {
const confirmed = await this.showConfirm(
'Turnier löschen',
@@ -1275,6 +1329,13 @@ export default {
<style scoped>
.official-tournaments { display: flex; flex-direction: column; gap: 0.75rem; }
.event-list { list-style: none; padding: 0; margin: 0; }
.event-item { display: flex; align-items: center; gap: 0.5rem; }
.event-title { flex: 1; }
.event-date { flex-shrink: 0; }
.title-input { flex: 1; min-width: 0; padding: 0.25rem 0.4rem; font-size: inherit; }
.btn-icon { background: none; border: none; cursor: pointer; padding: 0.25rem; opacity: 0.7; }
.btn-icon:hover { opacity: 1; }
.top-actions { display: flex; gap: .5rem; margin-bottom: .5rem; }
.tabs { display: flex; gap: .25rem; border-bottom: 1px solid var(--border-color); margin: .25rem 0 .5rem; }
.tab { background: #f8f9fb; color: var(--text-color, #222); border: none; padding: .4rem .6rem; cursor: pointer; border-bottom: 2px solid transparent; }