feat(Tournament): add doubles tournament support and related UI enhancements
- Updated tournamentController.js and tournamentService.js to include isDoublesTournament parameter for tournament creation and updates. - Modified Tournament model to add isDoublesTournament field, allowing differentiation between singles and doubles tournaments. - Enhanced frontend components (TournamentConfigTab, TournamentParticipantsTab, TournamentTab) to support doubles tournament configuration and display. - Added internationalization keys for doubles tournament labels and hints in multiple languages. - Improved participant assignment logic for doubles tournaments to ensure proper class assignments.
This commit is contained in:
@@ -62,9 +62,9 @@ export const getTournaments = async (req, res) => {
|
||||
// 2. Neues Turnier anlegen
|
||||
export const addTournament = async (req, res) => {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubId, tournamentName, date, winningSets, allowsExternal } = req.body;
|
||||
const { clubId, tournamentName, date, winningSets, allowsExternal, isDoublesTournament } = req.body;
|
||||
try {
|
||||
const tournament = await tournamentService.addTournament(token, clubId, tournamentName, date, winningSets, allowsExternal);
|
||||
const tournament = await tournamentService.addTournament(token, clubId, tournamentName, date, winningSets, allowsExternal, isDoublesTournament);
|
||||
if (clubId && tournament && tournament.id) {
|
||||
emitTournamentChanged(clubId, tournament.id);
|
||||
}
|
||||
@@ -271,9 +271,9 @@ export const getTournament = async (req, res) => {
|
||||
export const updateTournament = async (req, res) => {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubId, tournamentId } = req.params;
|
||||
const { name, date, winningSets, numberOfTables } = req.body;
|
||||
const { name, date, winningSets, numberOfTables, isDoublesTournament } = req.body;
|
||||
try {
|
||||
const tournament = await tournamentService.updateTournament(token, clubId, tournamentId, name, date, winningSets, numberOfTables);
|
||||
const tournament = await tournamentService.updateTournament(token, clubId, tournamentId, name, date, winningSets, numberOfTables, isDoublesTournament);
|
||||
// Emit Socket-Event
|
||||
emitTournamentChanged(clubId, tournamentId);
|
||||
res.status(200).json(tournament);
|
||||
@@ -748,4 +748,4 @@ export const deletePairing = async (req, res) => {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -51,6 +51,13 @@ const Tournament = sequelize.define('Tournament', {
|
||||
defaultValue: null,
|
||||
comment: 'Anzahl der Tische, auf denen gespielt wird'
|
||||
},
|
||||
isDoublesTournament: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false,
|
||||
field: 'is_doubles_tournament',
|
||||
comment: 'Turnierweite Markierung fuer Doppel-Turniere'
|
||||
},
|
||||
}, {
|
||||
underscored: true,
|
||||
tableName: 'tournament',
|
||||
|
||||
@@ -832,13 +832,13 @@ class TournamentService {
|
||||
const tournaments = await Tournament.findAll({
|
||||
where,
|
||||
order: [['date', 'DESC']],
|
||||
attributes: ['id', 'name', 'date', 'allowsExternal', 'miniChampionshipYear']
|
||||
attributes: ['id', 'name', 'date', 'allowsExternal', 'miniChampionshipYear', 'isDoublesTournament']
|
||||
});
|
||||
return JSON.parse(JSON.stringify(tournaments));
|
||||
}
|
||||
|
||||
Ve // 2. Neues Turnier anlegen
|
||||
async addTournament(userToken, clubId, tournamentName, date, winningSets, allowsExternal) {
|
||||
async addTournament(userToken, clubId, tournamentName, date, winningSets, allowsExternal, isDoublesTournament = false) {
|
||||
await checkAccess(userToken, clubId);
|
||||
const t = await Tournament.create({
|
||||
name: tournamentName,
|
||||
@@ -847,7 +847,8 @@ Ve // 2. Neues Turnier anlegen
|
||||
bestOfEndroundSize: 0,
|
||||
type: '',
|
||||
winningSets: winningSets || 3, // Default: 3 Sätze
|
||||
allowsExternal: allowsExternal || false
|
||||
allowsExternal: allowsExternal || false,
|
||||
isDoublesTournament: Boolean(isDoublesTournament)
|
||||
});
|
||||
return JSON.parse(JSON.stringify(t));
|
||||
}
|
||||
@@ -2281,7 +2282,7 @@ Ve // 2. Neues Turnier anlegen
|
||||
}
|
||||
|
||||
// Update Turnier (Name, Datum, Gewinnsätze und Tischanzahl)
|
||||
async updateTournament(userToken, clubId, tournamentId, name, date, winningSets, numberOfTables) {
|
||||
async updateTournament(userToken, clubId, tournamentId, name, date, winningSets, numberOfTables, isDoublesTournament) {
|
||||
await checkAccess(userToken, clubId);
|
||||
const tournament = await Tournament.findOne({ where: { id: tournamentId, clubId } });
|
||||
if (!tournament) {
|
||||
@@ -2310,6 +2311,9 @@ Ve // 2. Neues Turnier anlegen
|
||||
}
|
||||
tournament.numberOfTables = numberOfTables;
|
||||
}
|
||||
if (isDoublesTournament !== undefined) {
|
||||
tournament.isDoublesTournament = Boolean(isDoublesTournament);
|
||||
}
|
||||
|
||||
await tournament.save();
|
||||
return JSON.parse(JSON.stringify(tournament));
|
||||
@@ -3945,6 +3949,22 @@ Ve // 2. Neues Turnier anlegen
|
||||
if (existingPairing) {
|
||||
throw new Error('Diese Paarung existiert bereits');
|
||||
}
|
||||
const playerAssignments = [];
|
||||
if (player1Type === 'member') playerAssignments.push({ member1Id: player1Id }, { member2Id: player1Id });
|
||||
if (player1Type === 'external') playerAssignments.push({ external1Id: player1Id }, { external2Id: player1Id });
|
||||
if (player2Type === 'member') playerAssignments.push({ member1Id: player2Id }, { member2Id: player2Id });
|
||||
if (player2Type === 'external') playerAssignments.push({ external1Id: player2Id }, { external2Id: player2Id });
|
||||
|
||||
const participantAlreadyAssigned = await TournamentPairing.findOne({
|
||||
where: {
|
||||
tournamentId,
|
||||
classId,
|
||||
[Op.or]: playerAssignments
|
||||
}
|
||||
});
|
||||
if (participantAlreadyAssigned) {
|
||||
throw new Error('Mindestens ein Spieler ist bereits einer Doppelpaarung zugeordnet');
|
||||
}
|
||||
return await TournamentPairing.create({
|
||||
tournamentId,
|
||||
classId,
|
||||
|
||||
@@ -32,8 +32,15 @@
|
||||
<input type="checkbox" :checked="isGroupTournament" @change="$emit('update:isGroupTournament', $event.target.checked)" />
|
||||
<span>{{ $t('tournaments.playInGroups') }}</span>
|
||||
</label>
|
||||
<label class="checkbox-item">
|
||||
<input type="checkbox" :checked="tournamentWideIsDoubles" @change="$emit('set-all-classes-doubles', $event.target.checked)" />
|
||||
<span>{{ $t('tournaments.doublesTournament') }}</span>
|
||||
</label>
|
||||
<button @click="$emit('generate-pdf')" class="btn-primary">{{ $t('tournaments.exportPDF') }}</button>
|
||||
</div>
|
||||
<div class="config-inline-hint">
|
||||
{{ $t('tournaments.doublesTournamentHint') }}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="config-card">
|
||||
@@ -633,6 +640,10 @@ export default {
|
||||
newClassMinBirthYear: {
|
||||
type: [Number, null],
|
||||
default: null
|
||||
},
|
||||
tournamentWideIsDoubles: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@@ -1056,7 +1067,8 @@ export default {
|
||||
'update:newClassName',
|
||||
'update:newClassIsDoubles',
|
||||
'update:newClassGender',
|
||||
'update:newClassMinBirthYear'
|
||||
'update:newClassMinBirthYear',
|
||||
'set-all-classes-doubles'
|
||||
]
|
||||
,
|
||||
methods: {
|
||||
@@ -2045,6 +2057,13 @@ export default {
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.config-inline-hint {
|
||||
margin-top: 0.65rem;
|
||||
color: #6b7280;
|
||||
font-size: 0.88rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.checkbox-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -194,122 +194,21 @@
|
||||
<!-- Teilnehmer ohne Klasse zuerst -->
|
||||
<div v-if="shouldShowClass(null) && getFilteredParticipantsForClass(null).length > 0" class="participants-class-section participants-class-section-priority">
|
||||
<h5 class="participants-class-header">{{ $t('tournaments.withoutClass') }} <span class="class-count-badge">{{ getFilteredParticipantsForClass(null).length }}</span></h5>
|
||||
<table class="participants-table participants-table-header">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="participant-name">{{ $t('tournaments.name') }}</th>
|
||||
<th class="participant-details-cell">{{ detailsLabel() }}</th>
|
||||
<th class="participant-class-cell">{{ $t('tournaments.class') }}</th>
|
||||
<th v-if="isGroupTournament" class="participant-group-cell">{{ $t('tournaments.group') }}</th>
|
||||
<th class="participant-status-cell">{{ statusLabel() }}</th>
|
||||
<th class="participant-action-cell">{{ $t('tournaments.action') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
<div class="participants-table-body-wrapper">
|
||||
<table class="participants-table participants-table-body">
|
||||
<tbody>
|
||||
<tr v-for="participant in getFilteredParticipantsForClass(null)" :key="participant.id" class="participant-item">
|
||||
<td class="participant-name">
|
||||
{{ participantDisplayName(participant) }}
|
||||
</td>
|
||||
<td class="participant-details-cell">
|
||||
<div class="participant-meta" v-if="participantMetaItems(participant).length">
|
||||
<span v-for="(item, index) in participantMetaItems(participant)" :key="`${participant.id}-meta-${index}`" class="participant-meta-chip">
|
||||
{{ item }}
|
||||
</span>
|
||||
</div>
|
||||
<span v-else class="participant-meta-empty">–</span>
|
||||
<div v-if="participantAssignmentHint(participant)" class="participant-assignment-hint">
|
||||
{{ participantAssignmentHint(participant) }}
|
||||
</div>
|
||||
<div v-if="participantWarnings(participant).length" class="participant-warning-list">
|
||||
<span v-for="(warning, index) in participantWarnings(participant)" :key="`${participant.id}-warning-${index}`" class="participant-warning-chip">
|
||||
{{ warning }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="participantConflictSuggestions(participant).length" class="participant-suggestion-list">
|
||||
<span class="participant-suggestion-label">{{ $t('tournaments.conflictSuggestionLabel') }}</span>
|
||||
<button
|
||||
v-for="classItem in participantConflictSuggestions(participant)"
|
||||
:key="`${participant.id}-suggest-${classItem.id}`"
|
||||
type="button"
|
||||
class="participant-suggestion-button"
|
||||
@click="$emit('assign-participant-class', participant, classItem.id)"
|
||||
>
|
||||
{{ classItem.name }}
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
<td class="participant-class-cell">
|
||||
<select
|
||||
:value="participant.classId"
|
||||
@change="$emit('update-participant-class', participant, $event)"
|
||||
class="class-select-small"
|
||||
>
|
||||
<option :value="null">–</option>
|
||||
<option v-for="classItem in getClassOptionsForParticipant(participant)" :key="classItem.id" :value="classItem.id">
|
||||
{{ classItem.name }}
|
||||
</option>
|
||||
</select>
|
||||
</td>
|
||||
<td v-if="isGroupTournament" class="participant-group-cell">
|
||||
<select
|
||||
:value="participant.groupNumber"
|
||||
@change="$emit('update-participant-group', participant, $event)"
|
||||
class="group-select-small"
|
||||
>
|
||||
<option :value="null">–</option>
|
||||
<option v-for="group in groups" :key="group.groupId" :value="group.groupNumber">
|
||||
{{ group.groupNumber }}
|
||||
</option>
|
||||
</select>
|
||||
</td>
|
||||
<td class="participant-status-cell">
|
||||
<div class="participant-status-controls">
|
||||
<label class="status-toggle">
|
||||
<input type="checkbox" :checked="participant.seeded" @change="$emit('update-participant-seeded', participant, $event)" />
|
||||
<span>{{ $t('tournaments.seeded') }}</span>
|
||||
</label>
|
||||
<label class="status-toggle" :title="$t('tournaments.gaveUpHint')">
|
||||
<input type="checkbox" :checked="participant.gaveUp" @change="$emit('update-participant-gave-up', participant, $event)" />
|
||||
<span>{{ $t('tournaments.gaveUp') }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</td>
|
||||
<td class="participant-action-cell">
|
||||
<button @click="$emit('remove-participant', participant)" class="trash-btn-small" :title="$t('tournaments.delete')">🗑️</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Teilnehmer nach Klassen gruppiert - nur angezeigte Klasse -->
|
||||
<template v-for="classItem in tournamentClasses" :key="classItem.id">
|
||||
<div v-if="shouldShowClass(classItem.id) && getFilteredParticipantsForClass(classItem.id).length > 0" class="participants-class-section">
|
||||
<h5 class="participants-class-header">
|
||||
{{ classItem.name }}
|
||||
<span class="class-type-badge-small" :class="{ 'doubles': classItem.isDoubles }">
|
||||
({{ classItem.isDoubles ? $t('tournaments.doubles') : $t('tournaments.singles') }})
|
||||
</span>
|
||||
<span class="class-count-badge">{{ getFilteredParticipantsForClass(classItem.id).length }}</span>
|
||||
</h5>
|
||||
<table class="participants-table participants-table-header">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="participant-name">{{ $t('tournaments.name') }}</th>
|
||||
<th class="participant-details-cell">{{ detailsLabel() }}</th>
|
||||
<th v-if="isGroupTournament" class="participant-group-cell">{{ $t('tournaments.group') }}</th>
|
||||
<th class="participant-status-cell">{{ statusLabel() }}</th>
|
||||
<th class="participant-action-cell">{{ $t('tournaments.action') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
<div class="participants-desktop-table">
|
||||
<div class="participants-table-body-wrapper">
|
||||
<table class="participants-table participants-table-body">
|
||||
<table class="participants-table participants-table-unified">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="participant-name">{{ $t('tournaments.name') }}</th>
|
||||
<th class="participant-details-cell">{{ detailsLabel() }}</th>
|
||||
<th class="participant-class-cell">{{ $t('tournaments.class') }}</th>
|
||||
<th v-if="isGroupTournament" class="participant-group-cell">{{ $t('tournaments.group') }}</th>
|
||||
<th class="participant-status-cell">{{ statusLabel() }}</th>
|
||||
<th class="participant-action-cell">{{ $t('tournaments.action') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="participant in getFilteredParticipantsForClass(classItem.id)" :key="participant.id" class="participant-item">
|
||||
<tr v-for="participant in getFilteredParticipantsForClass(null)" :key="participant.id" class="participant-item">
|
||||
<td class="participant-name">
|
||||
{{ participantDisplayName(participant) }}
|
||||
</td>
|
||||
@@ -341,6 +240,18 @@
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
<td class="participant-class-cell">
|
||||
<select
|
||||
:value="participant.classId"
|
||||
@change="$emit('update-participant-class', participant, $event)"
|
||||
class="class-select-small"
|
||||
>
|
||||
<option :value="null">–</option>
|
||||
<option v-for="classItem in getClassOptionsForParticipant(participant)" :key="classItem.id" :value="classItem.id">
|
||||
{{ classItem.name }}
|
||||
</option>
|
||||
</select>
|
||||
</td>
|
||||
<td v-if="isGroupTournament" class="participant-group-cell">
|
||||
<select
|
||||
:value="participant.groupNumber"
|
||||
@@ -348,7 +259,7 @@
|
||||
class="group-select-small"
|
||||
>
|
||||
<option :value="null">–</option>
|
||||
<option v-for="group in getGroupsForClass(classItem.id)" :key="group.groupId" :value="group.groupNumber">
|
||||
<option v-for="group in groups" :key="group.groupId" :value="group.groupNumber">
|
||||
{{ group.groupNumber }}
|
||||
</option>
|
||||
</select>
|
||||
@@ -373,6 +284,167 @@
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="participants-mobile-list">
|
||||
<article v-for="participant in getFilteredParticipantsForClass(null)" :key="`mobile-${participant.id}`" class="participant-card">
|
||||
<div class="participant-card-header">
|
||||
<div class="participant-card-title">{{ participantDisplayName(participant) }}</div>
|
||||
<button @click="$emit('remove-participant', participant)" class="trash-btn-small participant-card-delete" :title="$t('tournaments.delete')">🗑️</button>
|
||||
</div>
|
||||
<div class="participant-card-grid">
|
||||
<label class="participant-card-field">
|
||||
<span>{{ $t('tournaments.class') }}</span>
|
||||
<select :value="participant.classId" @change="$emit('update-participant-class', participant, $event)" class="class-select-small participant-card-select">
|
||||
<option :value="null">–</option>
|
||||
<option v-for="classItem in getClassOptionsForParticipant(participant)" :key="`mobile-class-${participant.id}-${classItem.id}`" :value="classItem.id">
|
||||
{{ classItem.name }}
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
<label v-if="isGroupTournament" class="participant-card-field">
|
||||
<span>{{ $t('tournaments.group') }}</span>
|
||||
<select :value="participant.groupNumber" @change="$emit('update-participant-group', participant, $event)" class="group-select-small participant-card-select">
|
||||
<option :value="null">–</option>
|
||||
<option v-for="group in groups" :key="`mobile-group-${participant.id}-${group.groupId}`" :value="group.groupNumber">
|
||||
{{ group.groupNumber }}
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div class="participant-card-status">
|
||||
<label class="status-toggle">
|
||||
<input type="checkbox" :checked="participant.seeded" @change="$emit('update-participant-seeded', participant, $event)" />
|
||||
<span>{{ $t('tournaments.seeded') }}</span>
|
||||
</label>
|
||||
<label class="status-toggle" :title="$t('tournaments.gaveUpHint')">
|
||||
<input type="checkbox" :checked="participant.gaveUp" @change="$emit('update-participant-gave-up', participant, $event)" />
|
||||
<span>{{ $t('tournaments.gaveUp') }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Teilnehmer nach Klassen gruppiert - nur angezeigte Klasse -->
|
||||
<template v-for="classItem in tournamentClasses" :key="classItem.id">
|
||||
<div v-if="shouldShowClass(classItem.id) && getFilteredParticipantsForClass(classItem.id).length > 0" class="participants-class-section">
|
||||
<h5 class="participants-class-header">
|
||||
{{ classItem.name }}
|
||||
<span class="class-type-badge-small" :class="{ 'doubles': classItem.isDoubles }">
|
||||
({{ classItem.isDoubles ? $t('tournaments.doubles') : $t('tournaments.singles') }})
|
||||
</span>
|
||||
<span class="class-count-badge">{{ getFilteredParticipantsForClass(classItem.id).length }}</span>
|
||||
</h5>
|
||||
<div class="participants-desktop-table">
|
||||
<div class="participants-table-body-wrapper">
|
||||
<table class="participants-table participants-table-unified">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="participant-name">{{ $t('tournaments.name') }}</th>
|
||||
<th class="participant-details-cell">{{ detailsLabel() }}</th>
|
||||
<th v-if="isGroupTournament" class="participant-group-cell">{{ $t('tournaments.group') }}</th>
|
||||
<th class="participant-status-cell">{{ statusLabel() }}</th>
|
||||
<th class="participant-action-cell">{{ $t('tournaments.action') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="participant in getFilteredParticipantsForClass(classItem.id)" :key="participant.id" class="participant-item">
|
||||
<td class="participant-name">
|
||||
{{ participantDisplayName(participant) }}
|
||||
</td>
|
||||
<td class="participant-details-cell">
|
||||
<div class="participant-meta" v-if="participantMetaItems(participant).length">
|
||||
<span v-for="(item, index) in participantMetaItems(participant)" :key="`${participant.id}-meta-${index}`" class="participant-meta-chip">
|
||||
{{ item }}
|
||||
</span>
|
||||
</div>
|
||||
<span v-else class="participant-meta-empty">–</span>
|
||||
<div v-if="participantAssignmentHint(participant)" class="participant-assignment-hint">
|
||||
{{ participantAssignmentHint(participant) }}
|
||||
</div>
|
||||
<div v-if="participantWarnings(participant).length" class="participant-warning-list">
|
||||
<span v-for="(warning, index) in participantWarnings(participant)" :key="`${participant.id}-warning-${index}`" class="participant-warning-chip">
|
||||
{{ warning }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="participantConflictSuggestions(participant).length" class="participant-suggestion-list">
|
||||
<span class="participant-suggestion-label">{{ $t('tournaments.conflictSuggestionLabel') }}</span>
|
||||
<button
|
||||
v-for="classItem in participantConflictSuggestions(participant)"
|
||||
:key="`${participant.id}-suggest-${classItem.id}`"
|
||||
type="button"
|
||||
class="participant-suggestion-button"
|
||||
@click="$emit('assign-participant-class', participant, classItem.id)"
|
||||
>
|
||||
{{ classItem.name }}
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
<td v-if="isGroupTournament" class="participant-group-cell">
|
||||
<select
|
||||
:value="participant.groupNumber"
|
||||
@change="$emit('update-participant-group', participant, $event)"
|
||||
class="group-select-small"
|
||||
>
|
||||
<option :value="null">–</option>
|
||||
<option v-for="group in getGroupsForClass(classItem.id)" :key="group.groupId" :value="group.groupNumber">
|
||||
{{ group.groupNumber }}
|
||||
</option>
|
||||
</select>
|
||||
</td>
|
||||
<td class="participant-status-cell">
|
||||
<div class="participant-status-controls">
|
||||
<label class="status-toggle">
|
||||
<input type="checkbox" :checked="participant.seeded" @change="$emit('update-participant-seeded', participant, $event)" />
|
||||
<span>{{ $t('tournaments.seeded') }}</span>
|
||||
</label>
|
||||
<label class="status-toggle" :title="$t('tournaments.gaveUpHint')">
|
||||
<input type="checkbox" :checked="participant.gaveUp" @change="$emit('update-participant-gave-up', participant, $event)" />
|
||||
<span>{{ $t('tournaments.gaveUp') }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</td>
|
||||
<td class="participant-action-cell">
|
||||
<button @click="$emit('remove-participant', participant)" class="trash-btn-small" :title="$t('tournaments.delete')">🗑️</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="participants-mobile-list">
|
||||
<article v-for="participant in getFilteredParticipantsForClass(classItem.id)" :key="`mobile-class-${classItem.id}-${participant.id}`" class="participant-card">
|
||||
<div class="participant-card-header">
|
||||
<div>
|
||||
<div class="participant-card-title">{{ participantDisplayName(participant) }}</div>
|
||||
<div class="participant-card-subtitle">
|
||||
{{ classItem.isDoubles ? $t('tournaments.doubles') : $t('tournaments.singles') }}
|
||||
</div>
|
||||
</div>
|
||||
<button @click="$emit('remove-participant', participant)" class="trash-btn-small participant-card-delete" :title="$t('tournaments.delete')">🗑️</button>
|
||||
</div>
|
||||
<div v-if="isGroupTournament" class="participant-card-grid">
|
||||
<label class="participant-card-field">
|
||||
<span>{{ $t('tournaments.group') }}</span>
|
||||
<select :value="participant.groupNumber" @change="$emit('update-participant-group', participant, $event)" class="group-select-small participant-card-select">
|
||||
<option :value="null">–</option>
|
||||
<option v-for="group in getGroupsForClass(classItem.id)" :key="`mobile-class-group-${classItem.id}-${participant.id}-${group.groupId}`" :value="group.groupNumber">
|
||||
{{ group.groupNumber }}
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div class="participant-card-status">
|
||||
<label class="status-toggle">
|
||||
<input type="checkbox" :checked="participant.seeded" @change="$emit('update-participant-seeded', participant, $event)" />
|
||||
<span>{{ $t('tournaments.seeded') }}</span>
|
||||
</label>
|
||||
<label class="status-toggle" :title="$t('tournaments.gaveUpHint')">
|
||||
<input type="checkbox" :checked="participant.gaveUp" @change="$emit('update-participant-gave-up', participant, $event)" />
|
||||
<span>{{ $t('tournaments.gaveUp') }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
@@ -390,9 +462,10 @@
|
||||
v-if="selectedDoublesParticipantsWithoutPartnerCount >= 2"
|
||||
type="button"
|
||||
class="btn-secondary btn-callout-action"
|
||||
:disabled="pairingOperationInProgress"
|
||||
@click="$emit('create-random-pairings', selectedViewClass)"
|
||||
>
|
||||
{{ $t('tournaments.createSuggestedPairings') }}
|
||||
{{ pairingOperationInProgress ? $t('common.loading') : $t('tournaments.createSuggestedPairings') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -434,15 +507,23 @@
|
||||
{{ $t('tournaments.seeded') }}
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
@click="$emit('add-pairing')"
|
||||
class="btn-add"
|
||||
:disabled="!newPairing.player1Id || !newPairing.player2Id || newPairing.player1Id === newPairing.player2Id"
|
||||
:disabled="pairingOperationInProgress || !newPairing.player1Id || !newPairing.player2Id || newPairing.player1Id === newPairing.player2Id"
|
||||
>
|
||||
{{ $t('tournaments.add') }}
|
||||
{{ pairingOperationInProgress ? $t('common.loading') : $t('tournaments.add') }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="random-pairing-section">
|
||||
<button @click="$emit('create-random-pairings')" class="btn-random-pairings">{{ $t('tournaments.randomPairings') }}</button>
|
||||
<button
|
||||
type="button"
|
||||
@click="$emit('create-random-pairings')"
|
||||
class="btn-random-pairings"
|
||||
:disabled="pairingOperationInProgress"
|
||||
>
|
||||
{{ pairingOperationInProgress ? $t('common.loading') : $t('tournaments.randomPairings') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pairings-list">
|
||||
@@ -456,18 +537,25 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="pairing in pairings" :key="pairing.id">
|
||||
<tr v-for="pairing in visiblePairings" :key="pairing.id">
|
||||
<td>{{ getPairingPlayerName(pairing, 1) }}</td>
|
||||
<td>{{ getPairingPlayerName(pairing, 2) }}</td>
|
||||
<td>
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="pairing.seeded"
|
||||
:disabled="pairingOperationInProgress"
|
||||
@change="$emit('update-pairing-seeded', pairing, $event)"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<button @click="$emit('remove-pairing', pairing)" class="trash-btn-small" :title="$t('tournaments.delete')">🗑️</button>
|
||||
<button
|
||||
type="button"
|
||||
@click="$emit('remove-pairing', pairing)"
|
||||
class="trash-btn-small"
|
||||
:disabled="pairingOperationInProgress"
|
||||
:title="$t('tournaments.delete')"
|
||||
>🗑️</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -551,6 +639,10 @@ export default {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
pairingOperationInProgress: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
newPairing: {
|
||||
type: Object,
|
||||
required: true
|
||||
@@ -624,6 +716,10 @@ export default {
|
||||
// Parent (TournamentTab) filtert bereits nach Klasse (Geschlecht, min/maxBirthYear)
|
||||
// Wir geben die Liste direkt zurück
|
||||
return this.clubMembers;
|
||||
},
|
||||
visiblePairings() {
|
||||
if (this.selectedViewClass == null || this.selectedViewClass === '__none__') return [];
|
||||
return this.pairings.filter(pairing => Number(pairing.classId) === Number(this.selectedViewClass));
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -826,12 +922,15 @@ export default {
|
||||
return null;
|
||||
},
|
||||
hasPairingForParticipant(participant) {
|
||||
return this.pairings.some(pairing =>
|
||||
pairing.member1Id === participant.id ||
|
||||
pairing.member2Id === participant.id ||
|
||||
pairing.external1Id === participant.id ||
|
||||
pairing.external2Id === participant.id
|
||||
);
|
||||
return this.pairings.some(pairing => {
|
||||
if (participant.classId != null && Number(pairing.classId) !== Number(participant.classId)) {
|
||||
return false;
|
||||
}
|
||||
return pairing.member1Id === participant.id ||
|
||||
pairing.member2Id === participant.id ||
|
||||
pairing.external1Id === participant.id ||
|
||||
pairing.external2Id === participant.id;
|
||||
});
|
||||
},
|
||||
isClassDoubles(classId) {
|
||||
if (classId === null || classId === '__none__' || classId === 'null' || classId === undefined) {
|
||||
@@ -1075,6 +1174,13 @@ export default {
|
||||
.participants-table-container {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
touch-action: pan-x;
|
||||
overscroll-behavior-x: contain;
|
||||
}
|
||||
|
||||
.participants-class-section-priority {
|
||||
@@ -1083,10 +1189,43 @@ export default {
|
||||
padding: 0.75rem;
|
||||
background: #fffbeb;
|
||||
margin-bottom: 1rem;
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
touch-action: pan-x;
|
||||
}
|
||||
|
||||
.participants-class-section {
|
||||
margin-bottom: 1rem;
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
touch-action: pan-x;
|
||||
}
|
||||
|
||||
.participants-table {
|
||||
width: max-content;
|
||||
min-width: 820px;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
.participants-table-body-wrapper {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
max-height: 420px;
|
||||
overflow-y: auto;
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
touch-action: pan-x;
|
||||
overscroll-behavior-x: contain;
|
||||
}
|
||||
|
||||
.participants-table-unified thead th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
background: #2f7a5f;
|
||||
}
|
||||
|
||||
.participants-class-header {
|
||||
@@ -1212,6 +1351,72 @@ export default {
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.participants-mobile-list {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.participant-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
padding: 0.9rem;
|
||||
border: 1px solid #dbe3ee;
|
||||
border-radius: 14px;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.05);
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.participant-card-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.participant-card-title {
|
||||
font-size: 0.98rem;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.participant-card-subtitle {
|
||||
margin-top: 0.15rem;
|
||||
font-size: 0.78rem;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.participant-card-delete {
|
||||
margin-left: 0;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.participant-card-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
||||
gap: 0.7rem;
|
||||
}
|
||||
|
||||
.participant-card-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.35rem;
|
||||
font-size: 0.82rem;
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.participant-card-select {
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.participant-card-status {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.85rem;
|
||||
padding-top: 0.2rem;
|
||||
}
|
||||
|
||||
.status-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -1239,4 +1444,32 @@ export default {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.participants-desktop-table {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.participants-mobile-list {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.participants-table-container {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.participants-class-section,
|
||||
.participants-class-section-priority {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.participant-card-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.participant-card-status {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -268,6 +268,8 @@
|
||||
"problemDoublesTitle": "{count} offeni Doppelpartner",
|
||||
"problemDoublesDescription": "Ei oder meh Doppelklasse bruuched no Partnerzuewisige.",
|
||||
"problemDoublesAutoDescription": "D offeni Doppelklass cha direkt automatisch paarlet werde.",
|
||||
"doublesTournament": "Doppel-Turnier",
|
||||
"doublesTournamentHint": "Schaltet alli vorhandene Klasse uf Doppel und legt neui Klasse standardmässig als Doppel aa.",
|
||||
"problemGroupsMissingTitle": "Gruppe no nid erstellt",
|
||||
"problemGroupsMissingDescription": "Für s Gruppeturnier muesed zerscht Gruppe aagleit werde.",
|
||||
"problemGroupMatchesTitle": "Gruppespiel no nid erzeugt",
|
||||
|
||||
@@ -424,6 +424,8 @@
|
||||
"noTop3Yet": "Es stehen noch keine Top-3-Platzierungen fest.",
|
||||
"missingDataPDFTitleTop3": "Fehlende Daten – Top 3 Minimeisterschaft",
|
||||
"missingDataPDFSubtitleTop3": "Fehlende Daten der ersten 3 Plätze (markiert mit ____) bitte erfragen und hier notieren.",
|
||||
"doublesTournament": "Doppel-Turnier",
|
||||
"doublesTournamentHint": "Schaltet alle vorhandenen Klassen auf Doppel und legt neue Klassen standardmäßig als Doppel an.",
|
||||
"address": "Adresse",
|
||||
"phone": "Telefon",
|
||||
"generatingPDF": "PDF wird erstellt...",
|
||||
|
||||
@@ -723,6 +723,8 @@
|
||||
"noClassesYet": "Noch keine Klassen vorhanden. Fügen Sie eine neue Klasse hinzu.",
|
||||
"singles": "Einzel",
|
||||
"doubles": "Doppel",
|
||||
"doublesTournament": "Doppel-Turnier",
|
||||
"doublesTournamentHint": "Schaltet alle vorhandenen Klassen auf Doppel und legt neue Klassen standardmäßig als Doppel an.",
|
||||
"genderAll": "Alle",
|
||||
"genderMixed": "Mixed",
|
||||
"minBirthYear": "Geboren im Jahr oder später",
|
||||
|
||||
@@ -268,6 +268,8 @@
|
||||
"problemDoublesTitle": "{count} open doubles partners",
|
||||
"problemDoublesDescription": "One or more doubles classes still need partner assignments.",
|
||||
"problemDoublesAutoDescription": "The open doubles class can be paired automatically right away.",
|
||||
"doublesTournament": "Doubles tournament",
|
||||
"doublesTournamentHint": "Switches all existing classes to doubles and creates new classes as doubles by default.",
|
||||
"problemGroupsMissingTitle": "Groups not created yet",
|
||||
"problemGroupsMissingDescription": "The group tournament needs groups to be created first.",
|
||||
"problemGroupMatchesTitle": "Group matches not generated yet",
|
||||
|
||||
@@ -538,6 +538,8 @@
|
||||
"problemDoublesTitle": "{count} open doubles partners",
|
||||
"problemDoublesDescription": "One or more doubles classes still need partner assignments.",
|
||||
"problemDoublesAutoDescription": "The open doubles class can be paired automatically right away.",
|
||||
"doublesTournament": "Doubles tournament",
|
||||
"doublesTournamentHint": "Switches all existing classes to doubles and creates new classes as doubles by default.",
|
||||
"problemGroupsMissingTitle": "Groups not created yet",
|
||||
"problemGroupsMissingDescription": "The group tournament needs groups to be created first.",
|
||||
"problemGroupMatchesTitle": "Group matches not generated yet",
|
||||
|
||||
@@ -268,6 +268,8 @@
|
||||
"problemDoublesTitle": "{count} open doubles partners",
|
||||
"problemDoublesDescription": "One or more doubles classes still need partner assignments.",
|
||||
"problemDoublesAutoDescription": "The open doubles class can be paired automatically right away.",
|
||||
"doublesTournament": "Doubles tournament",
|
||||
"doublesTournamentHint": "Switches all existing classes to doubles and creates new classes as doubles by default.",
|
||||
"problemGroupsMissingTitle": "Groups not created yet",
|
||||
"problemGroupsMissingDescription": "The group tournament needs groups to be created first.",
|
||||
"problemGroupMatchesTitle": "Group matches not generated yet",
|
||||
|
||||
@@ -122,6 +122,7 @@
|
||||
:new-class-is-doubles="newClassIsDoubles"
|
||||
:new-class-gender="newClassGender"
|
||||
:new-class-max-birth-year="newClassMaxBirthYear"
|
||||
:tournament-wide-is-doubles="tournamentWideIsDoubles"
|
||||
@update:tournamentName="currentTournamentName = $event; updateTournament()"
|
||||
@update:tournamentDate="currentTournamentDate = $event; updateTournament()"
|
||||
@update:winningSets="currentWinningSets = $event; updateTournament()"
|
||||
@@ -143,6 +144,7 @@
|
||||
@update:newClassIsDoubles="newClassIsDoubles = $event"
|
||||
@update:newClassGender="newClassGender = $event"
|
||||
@update:newClassMaxBirthYear="newClassMaxBirthYear = $event"
|
||||
@set-all-classes-doubles="setTournamentWideDoubles"
|
||||
/>
|
||||
|
||||
<!-- Tab: Teilnehmer -->
|
||||
@@ -165,6 +167,7 @@
|
||||
:groups="groups"
|
||||
:pairings="pairings"
|
||||
:new-pairing="newPairing"
|
||||
:pairing-operation-in-progress="pairingOperationInProgress"
|
||||
@update:selectedViewClass="selectedViewClass = $event"
|
||||
@update:selectedMember="selectedMember = $event"
|
||||
@add-participant="addParticipant()"
|
||||
@@ -375,6 +378,7 @@ export default {
|
||||
currentTournamentDate: '',
|
||||
currentWinningSets: 3,
|
||||
currentNumberOfTables: null,
|
||||
currentTournamentWideIsDoubles: false,
|
||||
dates: [],
|
||||
participants: [],
|
||||
selectedMember: null,
|
||||
@@ -419,6 +423,7 @@ export default {
|
||||
},
|
||||
showPairings: false, // Kollaps-Status für Paarungen
|
||||
pairings: [], // Doppel-Paarungen
|
||||
pairingOperationInProgress: false, // Sperrt Mehrfachklicks bei Paarungsaktionen
|
||||
newPairing: { // Neue Paarung
|
||||
player1Id: null,
|
||||
player2Id: null,
|
||||
@@ -473,6 +478,9 @@ export default {
|
||||
}))
|
||||
.filter(item => item.count > 0);
|
||||
},
|
||||
tournamentWideIsDoubles() {
|
||||
return Boolean(this.currentTournamentWideIsDoubles);
|
||||
},
|
||||
readyParticipantCount() {
|
||||
return this.totalParticipantCount - this.participantConflictCount - this.unassignedParticipantCount;
|
||||
},
|
||||
@@ -946,6 +954,15 @@ export default {
|
||||
|
||||
activeAssignmentClassId() {
|
||||
if (this.selectedViewClass === null || this.selectedViewClass === undefined || this.selectedViewClass === '' || this.selectedViewClass === '__all__' || this.selectedViewClass === 'all') {
|
||||
if (this.tournamentClasses.length === 1) {
|
||||
return this.tournamentClasses[0].id;
|
||||
}
|
||||
if (this.currentTournamentWideIsDoubles) {
|
||||
const firstDoublesClass = this.tournamentClasses.find(classItem => Boolean(classItem.isDoubles));
|
||||
if (firstDoublesClass) {
|
||||
return firstDoublesClass.id;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
if (this.selectedViewClass === '__none__' || this.selectedViewClass === 'null') {
|
||||
@@ -1624,6 +1641,7 @@ export default {
|
||||
const tournament = tRes.data;
|
||||
this.currentTournamentName = tournament.name || '';
|
||||
this.currentTournamentDate = tournament.date || '';
|
||||
this.currentTournamentWideIsDoubles = Boolean(tournament.isDoublesTournament);
|
||||
const defaultSets = tournament.miniChampionshipYear != null ? 1 : 3;
|
||||
this.currentWinningSets = (tournament.winningSets != null && tournament.winningSets >= 1)
|
||||
? tournament.winningSets
|
||||
@@ -1641,6 +1659,9 @@ export default {
|
||||
await this.checkTrainingForDate(tournament.date);
|
||||
// Lade Klassen
|
||||
await this.loadTournamentClasses();
|
||||
if (this.currentTournamentWideIsDoubles && this.tournamentClasses.length === 0) {
|
||||
await this.ensureDefaultDoublesClass();
|
||||
}
|
||||
// Lade Paarungen für alle Doppel-Klassen
|
||||
await this.loadPairings();
|
||||
// Lade Teilnehmer für alle Klassen des Turniers
|
||||
@@ -1757,6 +1778,18 @@ export default {
|
||||
} else {
|
||||
this.externalParticipants = [];
|
||||
}
|
||||
|
||||
const normalizedSingleDoublesClass = this.currentTournamentWideIsDoubles && this.tournamentClasses.length === 1 && this.tournamentClasses[0].isDoubles
|
||||
? this.tournamentClasses[0]
|
||||
: null;
|
||||
if (normalizedSingleDoublesClass) {
|
||||
const assignmentsChanged = await this.ensureParticipantsAssignedToSingleDoublesClass(normalizedSingleDoublesClass.id);
|
||||
if (assignmentsChanged) {
|
||||
await this.loadTournamentData();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const mRes = await apiClient.get(
|
||||
`/tournament/matches/${this.currentClub}/${this.selectedDate}`
|
||||
);
|
||||
@@ -2042,7 +2075,8 @@ export default {
|
||||
name: this.currentTournamentName || this.currentTournamentDate,
|
||||
date: this.currentTournamentDate,
|
||||
winningSets: this.currentWinningSets,
|
||||
numberOfTables: this.currentNumberOfTables
|
||||
numberOfTables: this.currentNumberOfTables,
|
||||
isDoublesTournament: this.currentTournamentWideIsDoubles
|
||||
});
|
||||
// Prüfe, ob es einen Trainingstag für das neue Datum gibt
|
||||
await this.checkTrainingForDate(this.currentTournamentDate);
|
||||
@@ -2070,7 +2104,8 @@ export default {
|
||||
tournamentName: this.newTournamentName || this.newDate,
|
||||
date: this.newDate,
|
||||
winningSets: this.newWinningSets,
|
||||
allowsExternal: this.allowsExternal
|
||||
allowsExternal: this.allowsExternal,
|
||||
isDoublesTournament: false
|
||||
});
|
||||
const newTournamentId = r.data.id;
|
||||
await this.loadTournaments();
|
||||
@@ -3267,12 +3302,77 @@ export default {
|
||||
this.showPairings = !this.showPairings;
|
||||
},
|
||||
|
||||
async ensureParticipantsAssignedToSingleDoublesClass(classId) {
|
||||
const unassignedInternal = this.participants.filter(participant => participant.classId == null);
|
||||
const unassignedExternal = this.externalParticipants.filter(participant => participant.classId == null);
|
||||
if (unassignedInternal.length === 0 && unassignedExternal.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
for (const participant of unassignedInternal) {
|
||||
await apiClient.put(`/tournament/participant/${this.currentClub}/${this.selectedDate}/${participant.id}/class`, {
|
||||
classId,
|
||||
isExternal: false
|
||||
});
|
||||
}
|
||||
for (const participant of unassignedExternal) {
|
||||
await apiClient.put(`/tournament/participant/${this.currentClub}/${this.selectedDate}/${participant.id}/class`, {
|
||||
classId,
|
||||
isExternal: true
|
||||
});
|
||||
}
|
||||
return unassignedInternal.length > 0 || unassignedExternal.length > 0;
|
||||
} catch (error) {
|
||||
console.error('Fehler bei automatischer Klassenzuordnung fuer Doppel-Turnier:', error);
|
||||
const message = safeErrorMessage(error, this.$t('tournaments.errorUpdatingTournament'));
|
||||
await this.showInfo(this.$t('messages.error'), message, '', 'error');
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
async ensureDefaultDoublesClass() {
|
||||
if (!this.selectedDate || this.selectedDate === 'new' || !this.currentTournamentWideIsDoubles || this.tournamentClasses.length > 0) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await apiClient.post(`/tournament/class/${this.currentClub}/${this.selectedDate}`, {
|
||||
name: this.$t('tournaments.doubles'),
|
||||
isDoubles: true,
|
||||
gender: null,
|
||||
minBirthYear: null
|
||||
});
|
||||
const res = await apiClient.get(`/tournament/classes/${this.currentClub}/${this.selectedDate}`);
|
||||
const classes = res.data || [];
|
||||
this.tournamentClasses = classes.map(classItem => ({
|
||||
...classItem,
|
||||
isDoubles: Boolean(classItem.isDoubles)
|
||||
}));
|
||||
this.tournamentClasses.forEach(classItem => {
|
||||
if (!(classItem.id in this.groupsPerClass) && this.groupsPerClass[classItem.id] === undefined) {
|
||||
this.groupsPerClass[classItem.id] = 0;
|
||||
}
|
||||
});
|
||||
const firstDoublesClass = this.tournamentClasses.find(classItem => Boolean(classItem.isDoubles));
|
||||
if (firstDoublesClass) {
|
||||
this.selectedViewClass = firstDoublesClass.id;
|
||||
}
|
||||
this.showClasses = this.tournamentClasses.length > 0;
|
||||
this.newClassIsDoubles = true;
|
||||
} catch (error) {
|
||||
console.error('Fehler beim automatischen Anlegen der Doppel-Klasse:', error);
|
||||
const message = safeErrorMessage(error, this.$t('tournaments.errorUpdatingTournament'));
|
||||
await this.showInfo(this.$t('messages.error'), message, '', 'error');
|
||||
}
|
||||
},
|
||||
|
||||
async loadTournamentClasses() {
|
||||
if (!this.selectedDate || this.selectedDate === 'new') {
|
||||
this.tournamentClasses = [];
|
||||
this.groupsPerClass = {};
|
||||
this.selectedViewClass = '__none__';
|
||||
this.showClasses = false;
|
||||
this.newClassIsDoubles = false;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
@@ -3283,6 +3383,7 @@ export default {
|
||||
...classItem,
|
||||
isDoubles: Boolean(classItem.isDoubles)
|
||||
}));
|
||||
this.newClassIsDoubles = this.tournamentWideIsDoubles;
|
||||
// Öffne die Klassen-Sektion automatisch, wenn Klassen vorhanden sind
|
||||
if (this.tournamentClasses.length > 0) {
|
||||
this.showClasses = true;
|
||||
@@ -3308,12 +3409,19 @@ export default {
|
||||
if (this.selectedViewClass !== null && this.selectedViewClass !== '__none__' && !validIds.includes(String(this.selectedViewClass))) {
|
||||
this.selectedViewClass = null;
|
||||
}
|
||||
if (this.currentTournamentWideIsDoubles && (this.selectedViewClass === null || this.selectedViewClass === undefined || this.selectedViewClass === '__all__')) {
|
||||
const firstDoublesClass = this.tournamentClasses.find(classItem => Boolean(classItem.isDoubles));
|
||||
if (firstDoublesClass) {
|
||||
this.selectedViewClass = firstDoublesClass.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Klassen:', error);
|
||||
this.tournamentClasses = [];
|
||||
this.groupsPerClass = {};
|
||||
this.selectedViewClass = '__none__';
|
||||
this.newClassIsDoubles = false;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -3331,12 +3439,12 @@ export default {
|
||||
if (data && typeof data === 'object' && data !== null && 'name' in data) {
|
||||
// Daten wurden als Objekt übergeben
|
||||
className = data.name;
|
||||
isDoubles = data.isDoubles !== undefined ? data.isDoubles : this.newClassIsDoubles;
|
||||
isDoubles = this.tournamentWideIsDoubles ? true : (data.isDoubles !== undefined ? data.isDoubles : this.newClassIsDoubles);
|
||||
gender = data.gender !== undefined ? data.gender : this.newClassGender;
|
||||
} else {
|
||||
// Fallback auf Props (sollte nicht passieren, aber für Sicherheit)
|
||||
className = this.newClassName;
|
||||
isDoubles = this.newClassIsDoubles;
|
||||
isDoubles = this.tournamentWideIsDoubles ? true : this.newClassIsDoubles;
|
||||
gender = this.newClassGender;
|
||||
}
|
||||
|
||||
@@ -3356,7 +3464,7 @@ export default {
|
||||
minBirthYear: minBirthYear
|
||||
});
|
||||
this.newClassName = '';
|
||||
this.newClassIsDoubles = false;
|
||||
this.newClassIsDoubles = this.tournamentWideIsDoubles;
|
||||
this.newClassGender = null;
|
||||
this.newClassMaxBirthYear = null;
|
||||
await this.loadTournamentClasses();
|
||||
@@ -3422,6 +3530,7 @@ export default {
|
||||
classItem.isDoubles = this.editingClassIsDoubles;
|
||||
classItem.gender = this.editingClassGender;
|
||||
classItem.minBirthYear = this.editingClassMaxBirthYear;
|
||||
this.newClassIsDoubles = this.tournamentWideIsDoubles;
|
||||
this.cancelClassEdit();
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Aktualisieren der Klasse:', error);
|
||||
@@ -3432,6 +3541,56 @@ export default {
|
||||
this.cancelClassEdit();
|
||||
}
|
||||
},
|
||||
async setTournamentWideDoubles(enabled) {
|
||||
this.currentTournamentWideIsDoubles = enabled;
|
||||
this.newClassIsDoubles = enabled;
|
||||
|
||||
if (this.editingClassId !== null) {
|
||||
this.editingClassIsDoubles = enabled;
|
||||
}
|
||||
|
||||
if (!this.selectedDate || this.selectedDate === 'new') {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await apiClient.put(`/tournament/${this.currentClub}/${this.selectedDate}`, {
|
||||
name: this.currentTournamentName || this.currentTournamentDate,
|
||||
date: this.currentTournamentDate,
|
||||
winningSets: this.currentWinningSets,
|
||||
numberOfTables: this.currentNumberOfTables,
|
||||
isDoublesTournament: enabled
|
||||
});
|
||||
|
||||
if (enabled && this.tournamentClasses.length === 0) {
|
||||
await this.ensureDefaultDoublesClass();
|
||||
}
|
||||
|
||||
const updates = this.tournamentClasses
|
||||
.filter(classItem => Boolean(classItem.isDoubles) !== enabled)
|
||||
.map(classItem => apiClient.put(`/tournament/class/${this.currentClub}/${this.selectedDate}/${classItem.id}`, {
|
||||
name: classItem.name,
|
||||
isDoubles: enabled,
|
||||
gender: classItem.gender || null,
|
||||
minBirthYear: classItem.minBirthYear || null
|
||||
}));
|
||||
|
||||
if (updates.length > 0) {
|
||||
await Promise.all(updates);
|
||||
}
|
||||
|
||||
this.tournamentClasses = this.tournamentClasses.map(classItem => ({
|
||||
...classItem,
|
||||
isDoubles: enabled
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Umstellen des Doppel-Turniers:', error);
|
||||
const message = safeErrorMessage(error, this.$t('tournaments.errorUpdatingTournament'));
|
||||
await this.showInfo(this.$t('messages.error'), message, '', 'error');
|
||||
await this.loadTournamentData();
|
||||
await this.loadTournamentClasses();
|
||||
}
|
||||
},
|
||||
|
||||
cancelClassEdit() {
|
||||
this.editingClassId = null;
|
||||
@@ -3718,6 +3877,7 @@ export default {
|
||||
},
|
||||
|
||||
async addPairing() {
|
||||
if (this.pairingOperationInProgress) return;
|
||||
if (!this.newPairing.player1Id || !this.newPairing.player2Id || this.newPairing.player1Id === this.newPairing.player2Id) {
|
||||
await this.showInfo(this.$t('messages.error'), this.$t('tournaments.selectTwoDifferentPlayers'), '', 'error');
|
||||
return;
|
||||
@@ -3742,6 +3902,8 @@ export default {
|
||||
}
|
||||
const player1Type = participant1.isExternal ? 'external' : 'member';
|
||||
const player2Type = participant2.isExternal ? 'external' : 'member';
|
||||
this.pairingOperationInProgress = true;
|
||||
this.showPairings = true;
|
||||
try {
|
||||
await apiClient.post(`/tournament/pairing/${this.currentClub}/${this.selectedDate}/${this.selectedViewClass}`, {
|
||||
player1Type: player1Type,
|
||||
@@ -3761,21 +3923,31 @@ export default {
|
||||
console.error('Fehler beim Hinzufügen der Paarung:', error);
|
||||
const message = safeErrorMessage(error, 'Fehler beim Hinzufügen der Paarung.');
|
||||
await this.showInfo(this.$t('messages.error'), message, '', 'error');
|
||||
} finally {
|
||||
this.pairingOperationInProgress = false;
|
||||
}
|
||||
},
|
||||
|
||||
async removePairing(pairing) {
|
||||
if (this.pairingOperationInProgress) return;
|
||||
const previousPairings = [...this.pairings];
|
||||
this.pairingOperationInProgress = true;
|
||||
this.pairings = this.pairings.filter(item => item.id !== pairing.id);
|
||||
try {
|
||||
await apiClient.delete(`/tournament/pairing/${this.currentClub}/${this.selectedDate}/${pairing.id}`);
|
||||
await this.loadPairings();
|
||||
} catch (error) {
|
||||
this.pairings = previousPairings;
|
||||
console.error('Fehler beim Löschen der Paarung:', error);
|
||||
const message = safeErrorMessage(error, 'Fehler beim Löschen der Paarung.');
|
||||
await this.showInfo(this.$t('messages.error'), message, '', 'error');
|
||||
await this.loadPairings();
|
||||
} finally {
|
||||
this.pairingOperationInProgress = false;
|
||||
}
|
||||
},
|
||||
|
||||
async createRandomPairings(targetClassId = this.selectedViewClass) {
|
||||
if (this.pairingOperationInProgress) return;
|
||||
const classId = targetClassId ?? this.selectedViewClass;
|
||||
const participants = this.getParticipantsForClass(classId).filter(participant => !this.hasPairingForParticipant(participant));
|
||||
if (participants.length < 2) {
|
||||
@@ -3806,78 +3978,80 @@ export default {
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.pairingOperationInProgress = true;
|
||||
this.showPairings = true;
|
||||
|
||||
// Lösche alle bestehenden Paarungen
|
||||
for (const pairing of existingPairings) {
|
||||
try {
|
||||
await apiClient.delete(`/tournament/pairing/${this.currentClub}/${this.selectedDate}/${pairing.id}`);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Löschen der Paarung:', error);
|
||||
try {
|
||||
const deleteResults = await Promise.allSettled(
|
||||
existingPairings.map(pairing =>
|
||||
apiClient.delete(`/tournament/pairing/${this.currentClub}/${this.selectedDate}/${pairing.id}`)
|
||||
)
|
||||
);
|
||||
deleteResults
|
||||
.filter(result => result.status === 'rejected')
|
||||
.forEach(result => console.error('Fehler beim Löschen der Paarung:', result.reason));
|
||||
|
||||
const shuffledSeeded = [...seeded].sort(() => Math.random() - 0.5);
|
||||
const shuffledUnseeded = [...unseeded].sort(() => Math.random() - 0.5);
|
||||
|
||||
const newPairings = [];
|
||||
let unseededIndex = 0;
|
||||
|
||||
for (const seededPlayer of shuffledSeeded) {
|
||||
if (unseededIndex < shuffledUnseeded.length) {
|
||||
newPairings.push({
|
||||
player1: seededPlayer,
|
||||
player2: shuffledUnseeded[unseededIndex]
|
||||
});
|
||||
unseededIndex++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Erstelle zufällige Paarungen
|
||||
const shuffledSeeded = [...seeded].sort(() => Math.random() - 0.5);
|
||||
const shuffledUnseeded = [...unseeded].sort(() => Math.random() - 0.5);
|
||||
|
||||
const newPairings = [];
|
||||
let unseededIndex = 0;
|
||||
|
||||
// Paare jeden gesetzten Spieler mit einem ungesetzten
|
||||
for (const seededPlayer of shuffledSeeded) {
|
||||
if (unseededIndex < shuffledUnseeded.length) {
|
||||
newPairings.push({
|
||||
player1: seededPlayer,
|
||||
player2: shuffledUnseeded[unseededIndex]
|
||||
});
|
||||
unseededIndex++;
|
||||
|
||||
const remainingUnseeded = shuffledUnseeded.slice(unseededIndex);
|
||||
if (remainingUnseeded.length >= 2) {
|
||||
for (let i = 0; i < remainingUnseeded.length - 1; i += 2) {
|
||||
newPairings.push({
|
||||
player1: remainingUnseeded[i],
|
||||
player2: remainingUnseeded[i + 1]
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wenn noch ungesetzte Spieler übrig sind, paare sie untereinander
|
||||
const remainingUnseeded = shuffledUnseeded.slice(unseededIndex);
|
||||
if (remainingUnseeded.length >= 2) {
|
||||
// Paare die übrigen ungesetzten untereinander
|
||||
for (let i = 0; i < remainingUnseeded.length - 1; i += 2) {
|
||||
newPairings.push({
|
||||
player1: remainingUnseeded[i],
|
||||
player2: remainingUnseeded[i + 1]
|
||||
});
|
||||
|
||||
if (newPairings.length === 0) {
|
||||
await this.loadPairings();
|
||||
await this.showInfo(this.$t('messages.info'), this.$t('tournaments.minimumParticipantsForPairings'), '', 'info');
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Wenn ungerade Anzahl ungesetzter übrig, bleibt einer allein (wird nicht gepaart)
|
||||
|
||||
// Erstelle die Paarungen im Backend
|
||||
let successCount = 0;
|
||||
let errorCount = 0;
|
||||
for (const pairing of newPairings) {
|
||||
try {
|
||||
const participant1 = pairing.player1;
|
||||
const participant2 = pairing.player2;
|
||||
const player1Type = participant1.isExternal ? 'external' : 'member';
|
||||
const player2Type = participant2.isExternal ? 'external' : 'member';
|
||||
|
||||
await apiClient.post(`/tournament/pairing/${this.currentClub}/${this.selectedDate}/${classId}`, {
|
||||
player1Type: player1Type,
|
||||
player1Id: participant1.id,
|
||||
player2Type: player2Type,
|
||||
player2Id: participant2.id,
|
||||
seeded: false // Paarungen sind nicht gesetzt, nur einzelne Spieler können gesetzt sein
|
||||
});
|
||||
successCount++;
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Erstellen der Paarung:', error);
|
||||
errorCount++;
|
||||
|
||||
const createResults = await Promise.allSettled(
|
||||
newPairings.map(pairing => {
|
||||
const participant1 = pairing.player1;
|
||||
const participant2 = pairing.player2;
|
||||
return apiClient.post(`/tournament/pairing/${this.currentClub}/${this.selectedDate}/${classId}`, {
|
||||
player1Type: participant1.isExternal ? 'external' : 'member',
|
||||
player1Id: participant1.id,
|
||||
player2Type: participant2.isExternal ? 'external' : 'member',
|
||||
player2Id: participant2.id,
|
||||
seeded: false
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
const successCount = createResults.filter(result => result.status === 'fulfilled').length;
|
||||
const errorCount = createResults.length - successCount;
|
||||
createResults
|
||||
.filter(result => result.status === 'rejected')
|
||||
.forEach(result => console.error('Fehler beim Erstellen der Paarung:', result.reason));
|
||||
|
||||
await this.loadPairings();
|
||||
|
||||
if (errorCount > 0) {
|
||||
await this.showInfo(this.$t('messages.warning'), this.$t('tournaments.pairingsCreatedWithErrors', { successCount, errorCount }), '', 'warning');
|
||||
} else {
|
||||
await this.showInfo(this.$t('messages.success'), this.$t('tournaments.randomPairingsCreated'), '', 'success');
|
||||
}
|
||||
}
|
||||
|
||||
// Lade Pairings neu
|
||||
await this.loadPairings();
|
||||
|
||||
if (errorCount > 0) {
|
||||
await this.showInfo(this.$t('messages.warning'), this.$t('tournaments.pairingsCreatedWithErrors', { successCount, errorCount }), '', 'warning');
|
||||
} else {
|
||||
await this.showInfo(this.$t('messages.success'), this.$t('tournaments.randomPairingsCreated'), '', 'success');
|
||||
} finally {
|
||||
this.pairingOperationInProgress = false;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -4683,13 +4857,18 @@ button {
|
||||
.participants-table-container {
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
display: inline-block;
|
||||
width: auto;
|
||||
overflow-x: auto;
|
||||
overflow-y: visible;
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
touch-action: pan-x;
|
||||
}
|
||||
|
||||
.participants-table {
|
||||
width: auto;
|
||||
width: max-content;
|
||||
min-width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
@@ -4714,6 +4893,8 @@ button {
|
||||
overflow-y: auto;
|
||||
overflow-x: auto;
|
||||
margin: 0;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
touch-action: pan-x;
|
||||
}
|
||||
|
||||
.participants-table-body td {
|
||||
|
||||
Reference in New Issue
Block a user