+
+
+
+ | {{ $t('tournaments.name') }} |
+ {{ detailsLabel() }} |
+ {{ $t('tournaments.class') }} |
+ {{ $t('tournaments.group') }} |
+ {{ statusLabel() }} |
+ {{ $t('tournaments.action') }} |
+
+
-
+
|
{{ participantDisplayName(participant) }}
|
@@ -341,6 +240,18 @@
+
+
+ |
@@ -373,6 +284,167 @@
|
+
+
+
+
+
@@ -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') }}
@@ -434,15 +507,23 @@
{{ $t('tournaments.seeded') }}
-
+
@@ -456,18 +537,25 @@
-
+
| {{ getPairingPlayerName(pairing, 1) }} |
{{ getPairingPlayerName(pairing, 2) }} |
|
-
+
|
@@ -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;
+ }
+}
diff --git a/frontend/src/i18n/locales/de-CH.json b/frontend/src/i18n/locales/de-CH.json
index 87858706..8d537662 100644
--- a/frontend/src/i18n/locales/de-CH.json
+++ b/frontend/src/i18n/locales/de-CH.json
@@ -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",
diff --git a/frontend/src/i18n/locales/de-extended.json b/frontend/src/i18n/locales/de-extended.json
index 72775711..381474bb 100644
--- a/frontend/src/i18n/locales/de-extended.json
+++ b/frontend/src/i18n/locales/de-extended.json
@@ -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...",
diff --git a/frontend/src/i18n/locales/de.json b/frontend/src/i18n/locales/de.json
index d55d51f6..c85b8c06 100644
--- a/frontend/src/i18n/locales/de.json
+++ b/frontend/src/i18n/locales/de.json
@@ -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",
diff --git a/frontend/src/i18n/locales/en-AU.json b/frontend/src/i18n/locales/en-AU.json
index 681b6602..e2d114d3 100644
--- a/frontend/src/i18n/locales/en-AU.json
+++ b/frontend/src/i18n/locales/en-AU.json
@@ -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",
diff --git a/frontend/src/i18n/locales/en-GB.json b/frontend/src/i18n/locales/en-GB.json
index 059fcedb..38c7a4af 100644
--- a/frontend/src/i18n/locales/en-GB.json
+++ b/frontend/src/i18n/locales/en-GB.json
@@ -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",
diff --git a/frontend/src/i18n/locales/en-US.json b/frontend/src/i18n/locales/en-US.json
index 81593519..df0b0e8d 100644
--- a/frontend/src/i18n/locales/en-US.json
+++ b/frontend/src/i18n/locales/en-US.json
@@ -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",
diff --git a/frontend/src/views/TournamentTab.vue b/frontend/src/views/TournamentTab.vue
index 544972c5..08951c7e 100644
--- a/frontend/src/views/TournamentTab.vue
+++ b/frontend/src/views/TournamentTab.vue
@@ -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"
/>
@@ -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 {