feat(tournament): add mini championship functionality and enhance tournament class handling

- Introduced addMiniChampionship method in tournamentService to create tournaments with predefined classes for mini championships.
- Updated getTournaments method to filter tournaments based on type, including support for mini championships.
- Enhanced TournamentClass model to include maxBirthYear for age class restrictions.
- Modified tournamentController and tournamentRoutes to support new mini championship endpoint.
- Updated frontend components to manage mini championship creation and display, including localization for new terms.
This commit is contained in:
Torsten Schulz (local)
2026-01-30 22:58:41 +01:00
parent 6cdcbfe0db
commit 85c26bc80d
10 changed files with 265 additions and 104 deletions

View File

@@ -461,45 +461,32 @@ export default {
return this.clubMembers;
}
// Filtere basierend auf Geschlechtsbeschränkung und Geburtsjahr
// Filtere basierend auf Geschlecht und Geburtsjahr (minBirthYear/maxBirthYear)
const classGender = selectedClass.gender;
const minBirthYear = selectedClass.minBirthYear;
const maxBirthYear = selectedClass.maxBirthYear;
return this.clubMembers.filter(member => {
// Filtere nach Geschlecht
const memberGender = member.gender || 'unknown';
const memberGender = (member.gender || 'unknown').toLowerCase();
let genderMatch = true;
if (classGender) {
// Wenn die Klasse "mixed" ist, erlaube alle Geschlechter
if (classGender === 'mixed') {
genderMatch = true;
} else if (classGender === 'male') {
genderMatch = memberGender === 'male';
} else if (classGender === 'female') {
genderMatch = memberGender === 'female';
}
if (classGender && classGender !== 'mixed') {
if (classGender === 'male') genderMatch = memberGender === 'male';
else if (classGender === 'female') genderMatch = memberGender === 'female';
}
// Filtere nach Geburtsjahr (geboren im Jahr X oder später, also >=)
let birthYearMatch = true;
if (minBirthYear && member.birthDate) {
// Parse das Geburtsdatum (Format: YYYY-MM-DD oder DD.MM.YYYY)
if (member.birthDate) {
let birthYear = null;
if (member.birthDate.includes('-')) {
// Format: YYYY-MM-DD
birthYear = parseInt(member.birthDate.split('-')[0]);
} else if (member.birthDate.includes('.')) {
// Format: DD.MM.YYYY
const parts = member.birthDate.split('.');
if (parts.length === 3) {
birthYear = parseInt(parts[2]);
}
const bd = String(member.birthDate);
if (bd.includes('-')) {
birthYear = parseInt(bd.split('-')[0], 10);
} else if (bd.includes('.')) {
const parts = bd.split('.');
if (parts.length >= 3) birthYear = parseInt(parts[2], 10);
}
if (birthYear && !isNaN(birthYear)) {
// Geboren im Jahr X oder später bedeutet: birthYear >= minBirthYear
birthYearMatch = birthYear >= minBirthYear;
if (birthYear != null && !isNaN(birthYear)) {
if (minBirthYear != null && birthYear < minBirthYear) birthYearMatch = false;
if (maxBirthYear != null && birthYear > maxBirthYear) birthYearMatch = false;
}
}

View File

@@ -561,6 +561,10 @@
"tournaments": {
"internalTournaments": "Interne Turniere",
"openTournaments": "Offene Turniere",
"miniChampionships": "Minimeisterschaften",
"newMiniChampionship": "Neue Minimeisterschaft",
"miniChampionshipYear": "Jahr (Altersstufen)",
"miniChampionshipYearHint": "12 = in diesem Jahr 11 oder 12 Jahre, 10 = 9 oder 10, 8 = 8 oder jünger",
"tournamentParticipations": "Turnierteilnahmen",
"date": "Datum",
"newTournament": "Neues Turnier",

View File

@@ -22,19 +22,40 @@
</option>
</select>
<div v-if="selectedDate === 'new'" class="new-tournament">
<label>
{{ $t('tournaments.name') }}:
<input type="text" v-model="newTournamentName" :placeholder="$t('tournaments.tournamentName')" />
</label>
<label>
{{ $t('tournaments.date') }}:
<input type="date" v-model="newDate" />
</label>
<label>
{{ $t('tournaments.winningSets') }}:
<input type="number" v-model.number="newWinningSets" min="1" />
</label>
<button @click="createTournament">{{ $t('tournaments.create') }}</button>
<template v-if="isMiniChampionship">
<label>
{{ $t('tournaments.name') }}:
<input type="text" v-model="newTournamentName" :placeholder="$t('tournaments.tournamentName')" />
</label>
<label>
{{ $t('tournaments.date') }}:
<input type="date" v-model="newDate" />
</label>
<label :title="$t('tournaments.miniChampionshipYearHint')">
{{ $t('tournaments.miniChampionshipYear') }}:
<input type="number" v-model.number="newMiniYear" min="2000" max="2100" step="1" />
</label>
<label>
{{ $t('tournaments.winningSets') }}:
<input type="number" v-model.number="newWinningSets" min="1" />
</label>
<button @click="createMiniChampionship">{{ $t('tournaments.newMiniChampionship') }}</button>
</template>
<template v-else>
<label>
{{ $t('tournaments.name') }}:
<input type="text" v-model="newTournamentName" :placeholder="$t('tournaments.tournamentName')" />
</label>
<label>
{{ $t('tournaments.date') }}:
<input type="date" v-model="newDate" />
</label>
<label>
{{ $t('tournaments.winningSets') }}:
<input type="number" v-model.number="newWinningSets" min="1" />
</label>
<button @click="createTournament">{{ $t('tournaments.create') }}</button>
</template>
</div>
</div>
<div v-if="selectedDate !== 'new'" class="tournament-setup">
@@ -122,7 +143,7 @@
:allows-external="allowsExternal"
:is-group-tournament="isGroupTournament"
:selected-member="selectedMember"
:club-members="clubMembers"
:club-members="clubMembersForParticipantAdd"
:has-training-today="hasTrainingToday"
:new-external-participant="newExternalParticipant"
:participants="participants"
@@ -283,6 +304,10 @@ export default {
allowsExternal: {
type: Boolean,
default: false
},
isMiniChampionship: {
type: Boolean,
default: false
}
},
data() {
@@ -308,6 +333,7 @@ export default {
newDate: '',
newTournamentName: '',
newWinningSets: 3,
newMiniYear: new Date().getFullYear(),
currentTournamentName: '',
currentTournamentDate: '',
currentWinningSets: 3,
@@ -506,6 +532,16 @@ export default {
groupRankingsKey(groupId, classId) {
return `${groupId}-${classId ?? 'null'}`;
},
clubMembersForParticipantAdd() {
if (!this.isMiniChampionship || !this.selectedViewClass || this.selectedViewClass === '__none__' || !this.tournamentClasses?.length) {
return this.clubMembers;
}
const classItem = this.tournamentClasses.find(c => String(c.id) === String(this.selectedViewClass));
if (!classItem || (classItem.minBirthYear == null && classItem.maxBirthYear == null && !classItem.gender)) {
return this.clubMembers;
}
return this.clubMembers.filter(m => this.memberEligibleForMiniClass(m, classItem));
},
// Mapping von groupId zu groupNumber für die Teilnehmer-Auswahl
groupIdToNumberMap() {
@@ -1452,12 +1488,16 @@ export default {
async loadTournaments() {
try {
const d = await apiClient.get(`/tournament/${this.currentClub}`);
// Filtere Turniere basierend auf allowsExternal Prop
if (this.allowsExternal) {
this.dates = d.data.filter(t => t.allowsExternal === true || t.allowsExternal === 1);
const url = this.isMiniChampionship
? `/tournament/${this.currentClub}?type=mini`
: `/tournament/${this.currentClub}`;
const d = await apiClient.get(url);
if (this.isMiniChampionship) {
this.dates = d.data || [];
} else if (this.allowsExternal) {
this.dates = (d.data || []).filter(t => !t.miniChampionshipYear && (t.allowsExternal === true || t.allowsExternal === 1));
} else {
this.dates = d.data.filter(t => !t.allowsExternal || t.allowsExternal === false || t.allowsExternal === 0);
this.dates = (d.data || []).filter(t => !t.miniChampionshipYear && (!t.allowsExternal || t.allowsExternal === false || t.allowsExternal === 0));
}
// Lade Mitgliederliste, falls noch nicht geladen
@@ -1556,17 +1596,9 @@ export default {
winningSets: this.newWinningSets,
allowsExternal: this.allowsExternal
});
// Speichere die ID des neuen Turniers
const newTournamentId = r.data.id;
// Lade die Turniere neu
await this.loadTournaments();
// Setze das neue Turnier als ausgewählt
this.selectedDate = newTournamentId;
this.newDate = '';
this.newTournamentName = '';
this.newWinningSets = 3;
@@ -1577,6 +1609,59 @@ export default {
}
},
async createMiniChampionship() {
if (!this.newDate) {
await this.showInfo(this.$t('messages.error'), this.$t('tournaments.pleaseEnterDate'), '', 'error');
return;
}
const year = Number(this.newMiniYear);
if (!Number.isFinite(year) || year < 2000 || year > 2100) {
await this.showInfo(this.$t('messages.error'), this.$t('tournaments.miniChampionshipYearHint'), '', 'error');
return;
}
try {
const r = await apiClient.post('/tournament/mini', {
clubId: this.currentClub,
tournamentName: this.newTournamentName || (`Minimeisterschaft ${year}`),
date: this.newDate,
year,
winningSets: this.newWinningSets
});
const newTournamentId = r.data.id;
await this.loadTournaments();
this.selectedDate = newTournamentId;
this.newDate = '';
this.newTournamentName = '';
this.newMiniYear = new Date().getFullYear();
this.newWinningSets = 3;
} catch (error) {
console.error('Fehler beim Anlegen der Minimeisterschaft:', error);
const message = safeErrorMessage(error, this.$t('tournaments.errorCreatingTournament'));
await this.showInfo(this.$t('messages.error'), message, '', 'error');
}
},
memberEligibleForMiniClass(member, classItem) {
if (classItem.gender && classItem.gender !== 'mixed') {
const g = (member.gender || 'unknown').toLowerCase();
if (classItem.gender === 'male' && g !== 'male') return false;
if (classItem.gender === 'female' && g !== 'female') return false;
}
const bd = member.birthDate;
if (!bd) return true;
let birthYear = null;
if (typeof bd === 'string' && bd.includes('-')) {
birthYear = parseInt(bd.split('-')[0], 10);
} else if (typeof bd === 'string' && bd.includes('.')) {
const parts = bd.split('.');
if (parts.length >= 3) birthYear = parseInt(parts[2], 10);
}
if (birthYear == null || !Number.isFinite(birthYear)) return true;
if (classItem.minBirthYear != null && birthYear < classItem.minBirthYear) return false;
if (classItem.maxBirthYear != null && birthYear > classItem.maxBirthYear) return false;
return true;
},
async addParticipant() {
if (!this.selectedMember) {
await this.showInfo(this.$t('messages.error'), this.$t('tournaments.pleaseSelectParticipant'), '', 'error');

View File

@@ -14,6 +14,12 @@
>
🌐 {{ $t('tournaments.openTournaments') }}
</button>
<button
:class="['tab-button', { active: activeTab === 'mini' }]"
@click="switchTab('mini')"
>
🏅 {{ $t('tournaments.miniChampionships') }}
</button>
<button
:class="['tab-button', { active: activeTab === 'official' }]"
@click="switchTab('official')"
@@ -30,6 +36,9 @@
<div v-else-if="activeTab === 'external'">
<TournamentTab :allowsExternal="true" />
</div>
<div v-else-if="activeTab === 'mini'">
<TournamentTab :allowsExternal="false" :isMiniChampionship="true" />
</div>
<div v-else-if="activeTab === 'official'">
<OfficialTournaments />
</div>