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:
Torsten Schulz (local)
2026-03-25 11:49:47 +01:00
parent 64090d9ff0
commit c2a31d3b24
12 changed files with 686 additions and 214 deletions

View File

@@ -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 });
}
};

View File

@@ -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',

View File

@@ -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,

View File

@@ -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;

View File

@@ -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>

View File

@@ -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",

View File

@@ -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...",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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 {