Enhance tournament management with new features and UI improvements
This commit introduces several enhancements to the tournament management system, including the addition of winning sets to tournament creation and updates. The `updateTournament` and `addTournament` methods in the backend now accept winning sets as a parameter, ensuring proper validation and handling. New functionality for updating participant seeded status and setting match activity is also implemented, along with corresponding routes and controller methods. The frontend is updated to reflect these changes, featuring new input fields for winning sets and improved participant management UI, enhancing overall user experience and interactivity.
This commit is contained in:
@@ -20,20 +20,18 @@ export const connectSocket = (clubId) => {
|
||||
});
|
||||
|
||||
socket.on('connect', () => {
|
||||
console.log('🔌 Socket.IO verbunden:', socket.id);
|
||||
// Wenn bereits ein Club ausgewählt war, trete dem Raum bei
|
||||
if (socket.currentClubId) {
|
||||
socket.emit('join-club', socket.currentClubId);
|
||||
console.log(`👤 Club ${socket.currentClubId} beigetreten (nach Reconnect)`);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
console.log('🔌 Socket.IO getrennt');
|
||||
// Socket getrennt
|
||||
});
|
||||
|
||||
socket.on('connect_error', (error) => {
|
||||
console.error('❌ Socket.IO Verbindungsfehler:', error);
|
||||
console.error('Socket.IO Verbindungsfehler:', error);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -41,7 +39,6 @@ export const connectSocket = (clubId) => {
|
||||
if (clubId) {
|
||||
socket.emit('join-club', clubId);
|
||||
socket.currentClubId = clubId;
|
||||
console.log(`👤 Club ${clubId} beigetreten`);
|
||||
}
|
||||
|
||||
return socket;
|
||||
@@ -54,7 +51,6 @@ export const disconnectSocket = () => {
|
||||
}
|
||||
socket.disconnect();
|
||||
socket = null;
|
||||
console.log('🔌 Socket.IO getrennt');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -78,11 +74,8 @@ export const onParticipantRemoved = (callback) => {
|
||||
export const onParticipantUpdated = (callback) => {
|
||||
if (socket) {
|
||||
socket.on('participant:updated', (data) => {
|
||||
console.log('📡 [Socket] participant:updated empfangen:', data);
|
||||
callback(data);
|
||||
});
|
||||
} else {
|
||||
console.warn('⚠️ [Socket] onParticipantUpdated: Socket nicht verbunden');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -125,66 +118,48 @@ export const onDiaryDateUpdated = (callback) => {
|
||||
export const onActivityMemberAdded = (callback) => {
|
||||
if (socket) {
|
||||
socket.on('activity:member:added', (data) => {
|
||||
console.log('📡 [Socket] activity:member:added empfangen:', data);
|
||||
callback(data);
|
||||
});
|
||||
} else {
|
||||
console.warn('⚠️ [Socket] onActivityMemberAdded: Socket nicht verbunden');
|
||||
}
|
||||
};
|
||||
|
||||
export const onActivityMemberRemoved = (callback) => {
|
||||
if (socket) {
|
||||
socket.on('activity:member:removed', (data) => {
|
||||
console.log('📡 [Socket] activity:member:removed empfangen:', data);
|
||||
callback(data);
|
||||
});
|
||||
} else {
|
||||
console.warn('⚠️ [Socket] onActivityMemberRemoved: Socket nicht verbunden');
|
||||
}
|
||||
};
|
||||
|
||||
export const onActivityChanged = (callback) => {
|
||||
if (socket) {
|
||||
socket.on('activity:changed', (data) => {
|
||||
console.log('📡 [Socket] activity:changed empfangen:', data);
|
||||
callback(data);
|
||||
});
|
||||
} else {
|
||||
console.warn('⚠️ [Socket] onActivityChanged: Socket nicht verbunden');
|
||||
}
|
||||
};
|
||||
|
||||
export const onMemberChanged = (callback) => {
|
||||
if (socket) {
|
||||
socket.on('member:changed', (data) => {
|
||||
console.log('📡 [Socket] member:changed empfangen:', data);
|
||||
callback(data);
|
||||
});
|
||||
} else {
|
||||
console.warn('⚠️ [Socket] onMemberChanged: Socket nicht verbunden');
|
||||
}
|
||||
};
|
||||
|
||||
export const onGroupChanged = (callback) => {
|
||||
if (socket) {
|
||||
socket.on('group:changed', (data) => {
|
||||
console.log('📡 [Socket] group:changed empfangen:', data);
|
||||
callback(data);
|
||||
});
|
||||
} else {
|
||||
console.warn('⚠️ [Socket] onGroupChanged: Socket nicht verbunden');
|
||||
}
|
||||
};
|
||||
|
||||
export const onTournamentChanged = (callback) => {
|
||||
if (socket) {
|
||||
socket.on('tournament:changed', (data) => {
|
||||
console.log('📡 [Socket] tournament:changed empfangen:', data);
|
||||
callback(data);
|
||||
});
|
||||
} else {
|
||||
console.warn('⚠️ [Socket] onTournamentChanged: Socket nicht verbunden');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1175,7 +1175,6 @@ export default {
|
||||
const response = await apiClient.get(`/clubmembers/get/${this.currentClub}/false`);
|
||||
// Erstelle ein neues Array, um Vue-Reaktivität sicherzustellen
|
||||
this.members = Array.isArray(response.data) ? [...response.data] : [];
|
||||
console.log('📡 [DiaryView] loadMembers: Mitglieder geladen, Anzahl:', this.members?.length || 0);
|
||||
},
|
||||
|
||||
async loadParticipants(dateId) {
|
||||
@@ -2251,16 +2250,11 @@ export default {
|
||||
},
|
||||
|
||||
async toggleActivityMembers(item) {
|
||||
console.log('🔍 [toggleActivityMembers] Aufgerufen für item.id:', item.id);
|
||||
console.log('🔍 [toggleActivityMembers] Aktuelle activityMembersOpenId:', this.activityMembersOpenId);
|
||||
|
||||
if (this.activityMembersOpenId === item.id) {
|
||||
console.log('🔍 [toggleActivityMembers] Schließe Dropdown');
|
||||
this.activityMembersOpenId = null;
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('🔍 [toggleActivityMembers] Öffne Dropdown für item.id:', item.id);
|
||||
this.activityMembersOpenId = item.id;
|
||||
|
||||
// Verwende $set für Vue 2 Reaktivität
|
||||
@@ -2273,7 +2267,6 @@ export default {
|
||||
// Force update um sicherzustellen, dass das Dropdown angezeigt wird
|
||||
await this.$nextTick();
|
||||
this.$forceUpdate();
|
||||
console.log('🔍 [toggleActivityMembers] Dropdown sollte jetzt sichtbar sein');
|
||||
},
|
||||
|
||||
async ensureActivityMembersLoaded(activityId) {
|
||||
@@ -2397,16 +2390,11 @@ export default {
|
||||
|
||||
// Group Activity Members Methods
|
||||
async toggleGroupActivityMembers(groupItem) {
|
||||
console.log('🔍 [toggleGroupActivityMembers] Aufgerufen für groupItem.id:', groupItem.id);
|
||||
console.log('🔍 [toggleGroupActivityMembers] Aktuelle groupActivityMembersOpenId:', this.groupActivityMembersOpenId);
|
||||
|
||||
if (this.groupActivityMembersOpenId === groupItem.id) {
|
||||
console.log('🔍 [toggleGroupActivityMembers] Schließe Dropdown');
|
||||
this.groupActivityMembersOpenId = null;
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('🔍 [toggleGroupActivityMembers] Öffne Dropdown für groupItem.id:', groupItem.id);
|
||||
this.groupActivityMembersOpenId = groupItem.id;
|
||||
|
||||
// Verwende $set für Vue 2 Reaktivität
|
||||
@@ -2419,7 +2407,6 @@ export default {
|
||||
// Force update um sicherzustellen, dass das Dropdown angezeigt wird
|
||||
await this.$nextTick();
|
||||
this.$forceUpdate();
|
||||
console.log('🔍 [toggleGroupActivityMembers] Dropdown sollte jetzt sichtbar sein');
|
||||
},
|
||||
|
||||
async ensureGroupActivityMembersLoaded(groupActivityId) {
|
||||
@@ -2650,7 +2637,6 @@ export default {
|
||||
onDiaryDateUpdated(this.handleDiaryDateUpdated);
|
||||
|
||||
// Event-Handler für Aktivitäts-Änderungen
|
||||
console.log('🔧 [DiaryView] Registriere Activity-Event-Handler');
|
||||
onActivityMemberAdded(this.handleActivityMemberAdded);
|
||||
onActivityMemberRemoved(this.handleActivityMemberRemoved);
|
||||
onActivityChanged(this.handleActivityChanged);
|
||||
@@ -2660,7 +2646,6 @@ export default {
|
||||
|
||||
// Event-Handler für Gruppen-Änderungen
|
||||
onGroupChanged(this.handleGroupChanged);
|
||||
console.log('✅ [DiaryView] Alle Event-Handler registriert');
|
||||
},
|
||||
|
||||
removeSocketListeners() {
|
||||
@@ -2682,7 +2667,6 @@ export default {
|
||||
async handleParticipantAdded(data) {
|
||||
// Nur aktualisieren, wenn das aktuelle Datum betroffen ist
|
||||
if (this.date && this.date !== 'new' && this.date.id === data.dateId) {
|
||||
console.log('📡 Teilnehmer hinzugefügt (Socket):', data);
|
||||
// Lade Teilnehmer neu
|
||||
await this.loadParticipants(data.dateId);
|
||||
}
|
||||
@@ -2691,7 +2675,6 @@ export default {
|
||||
async handleParticipantRemoved(data) {
|
||||
// Nur aktualisieren, wenn das aktuelle Datum betroffen ist
|
||||
if (this.date && this.date !== 'new' && this.date.id === data.dateId) {
|
||||
console.log('📡 Teilnehmer entfernt (Socket):', data);
|
||||
// Entferne aus participants-Array
|
||||
this.participants = this.participants.filter(memberId => memberId !== data.participantId);
|
||||
// Entferne aus Maps
|
||||
@@ -2703,47 +2686,33 @@ export default {
|
||||
async handleParticipantUpdated(data) {
|
||||
// Nur aktualisieren, wenn das aktuelle Datum betroffen ist
|
||||
if (this.date && this.date !== 'new' && String(this.date.id) === String(data.dateId)) {
|
||||
console.log('📡 Teilnehmer aktualisiert (Socket):', data);
|
||||
console.log('📡 [DiaryView] Aktuelle memberGroupsMap vor Update:', JSON.parse(JSON.stringify(this.memberGroupsMap)));
|
||||
|
||||
// Aktualisiere groupId in memberGroupsMap
|
||||
const groupValue = (data.participant.groupId !== null && data.participant.groupId !== undefined)
|
||||
? String(data.participant.groupId)
|
||||
: '';
|
||||
|
||||
console.log('📡 [DiaryView] Setze groupValue für memberId', data.participant.memberId, 'auf:', groupValue);
|
||||
console.log('📡 [DiaryView] data.participant:', JSON.parse(JSON.stringify(data.participant)));
|
||||
|
||||
// Verwende $set für Vue 2 - das ist wichtig für Reaktivität
|
||||
if (this.$set) {
|
||||
this.$set(this.memberGroupsMap, data.participant.memberId, groupValue);
|
||||
console.log('📡 [DiaryView] Verwendet $set für Vue 2');
|
||||
} else {
|
||||
// Vue 3: Erstelle neues Objekt für Reaktivität
|
||||
this.memberGroupsMap = {
|
||||
...this.memberGroupsMap,
|
||||
[data.participant.memberId]: groupValue
|
||||
};
|
||||
console.log('📡 [DiaryView] Verwendet Spread-Operator für Vue 3');
|
||||
}
|
||||
|
||||
// Warte auf Vue-Update und force dann ein Re-Render
|
||||
await this.$nextTick();
|
||||
console.log('📡 [DiaryView] memberGroupsMap nach Update:', JSON.parse(JSON.stringify(this.memberGroupsMap)));
|
||||
console.log('📡 [DiaryView] getMemberGroup für memberId', data.participant.memberId, ':', this.getMemberGroup(data.participant.memberId));
|
||||
|
||||
// Force Vue update um sicherzustellen, dass die UI aktualisiert wird
|
||||
this.$forceUpdate();
|
||||
console.log('✅ [DiaryView] UI-Update erzwungen');
|
||||
} else {
|
||||
console.log('⚠️ [DiaryView] Datum stimmt nicht überein - Event dateId:', data.dateId, 'Aktuelles Datum:', this.date?.id);
|
||||
}
|
||||
},
|
||||
|
||||
async handleDiaryNoteAdded(data) {
|
||||
// Nur aktualisieren, wenn das aktuelle Datum betroffen ist
|
||||
if (this.date && this.date !== 'new' && this.date.id === data.dateId) {
|
||||
console.log('📡 Tagebuch-Notiz hinzugefügt (Socket):', data);
|
||||
// Lade Notizen neu, falls das betroffene Mitglied ausgewählt ist
|
||||
if (this.selectedMember && data.note.memberId === this.selectedMember.id) {
|
||||
try {
|
||||
@@ -2759,7 +2728,6 @@ export default {
|
||||
async handleDiaryNoteDeleted(data) {
|
||||
// Nur aktualisieren, wenn das aktuelle Datum betroffen ist
|
||||
if (this.date && this.date !== 'new' && this.date.id === data.dateId) {
|
||||
console.log('📡 Tagebuch-Notiz gelöscht (Socket):', data);
|
||||
// Entferne Notiz aus notes-Array
|
||||
this.notes = this.notes.filter(note => note.id !== data.noteId);
|
||||
}
|
||||
@@ -2768,7 +2736,6 @@ export default {
|
||||
async handleDiaryTagAdded(data) {
|
||||
// Nur aktualisieren, wenn das aktuelle Datum betroffen ist
|
||||
if (this.date && this.date !== 'new' && this.date.id === data.dateId) {
|
||||
console.log('📡 Tagebuch-Tag hinzugefügt (Socket):', data);
|
||||
// Lade Tags neu
|
||||
await this.loadTags();
|
||||
// Aktualisiere selectedActivityTags
|
||||
@@ -2784,7 +2751,6 @@ export default {
|
||||
async handleDiaryTagRemoved(data) {
|
||||
// Nur aktualisieren, wenn das aktuelle Datum betroffen ist
|
||||
if (this.date && this.date !== 'new' && this.date.id === data.dateId) {
|
||||
console.log('📡 Tagebuch-Tag entfernt (Socket):', data);
|
||||
// Entferne Tag aus selectedActivityTags
|
||||
this.selectedActivityTags = this.selectedActivityTags.filter(t => t.id !== data.tagId);
|
||||
}
|
||||
@@ -2793,7 +2759,6 @@ export default {
|
||||
async handleDiaryDateUpdated(data) {
|
||||
// Nur aktualisieren, wenn das aktuelle Datum betroffen ist
|
||||
if (this.date && this.date !== 'new' && this.date.id === data.dateId) {
|
||||
console.log('📡 Tagebuch-Datum aktualisiert (Socket):', data);
|
||||
// Aktualisiere Trainingszeiten
|
||||
if (data.updates.trainingStart !== undefined) {
|
||||
this.trainingStart = data.updates.trainingStart;
|
||||
@@ -2805,99 +2770,68 @@ export default {
|
||||
},
|
||||
|
||||
async handleActivityMemberAdded(data) {
|
||||
console.log('📡 [DiaryView] handleActivityMemberAdded aufgerufen:', data);
|
||||
console.log('📡 [DiaryView] Aktuelles Datum:', this.date?.id, 'Event dateId:', data.dateId);
|
||||
// Nur aktualisieren, wenn das aktuelle Datum betroffen ist
|
||||
if (this.date && this.date !== 'new' && this.date.id === data.dateId) {
|
||||
console.log('✅ [DiaryView] Datum stimmt überein, lade Training Plan neu');
|
||||
// Lade Training Plan neu
|
||||
try {
|
||||
this.trainingPlan = await apiClient.get(`/diary-date-activities/${this.currentClub}/${this.date.id}`).then(response => response.data);
|
||||
this.calculateIntermediateTimes();
|
||||
console.log('✅ [DiaryView] Training Plan neu geladen');
|
||||
} catch (error) {
|
||||
console.error('❌ [DiaryView] Fehler beim Neuladen des Trainingsplans:', error);
|
||||
console.error('Fehler beim Neuladen des Trainingsplans:', error);
|
||||
}
|
||||
} else {
|
||||
console.log('⚠️ [DiaryView] Datum stimmt nicht überein oder kein Datum ausgewählt');
|
||||
}
|
||||
},
|
||||
|
||||
async handleActivityMemberRemoved(data) {
|
||||
console.log('📡 [DiaryView] handleActivityMemberRemoved aufgerufen:', data);
|
||||
console.log('📡 [DiaryView] Aktuelles Datum:', this.date?.id, 'Event dateId:', data.dateId);
|
||||
// Nur aktualisieren, wenn das aktuelle Datum betroffen ist
|
||||
if (this.date && this.date !== 'new' && this.date.id === data.dateId) {
|
||||
console.log('✅ [DiaryView] Datum stimmt überein, lade Training Plan neu');
|
||||
// Lade Training Plan neu
|
||||
try {
|
||||
this.trainingPlan = await apiClient.get(`/diary-date-activities/${this.currentClub}/${this.date.id}`).then(response => response.data);
|
||||
this.calculateIntermediateTimes();
|
||||
console.log('✅ [DiaryView] Training Plan neu geladen');
|
||||
} catch (error) {
|
||||
console.error('❌ [DiaryView] Fehler beim Neuladen des Trainingsplans:', error);
|
||||
console.error('Fehler beim Neuladen des Trainingsplans:', error);
|
||||
}
|
||||
} else {
|
||||
console.log('⚠️ [DiaryView] Datum stimmt nicht überein oder kein Datum ausgewählt');
|
||||
}
|
||||
},
|
||||
|
||||
async handleActivityChanged(data) {
|
||||
console.log('📡 [DiaryView] handleActivityChanged aufgerufen:', data);
|
||||
console.log('📡 [DiaryView] Aktuelles Datum:', this.date?.id, 'Event dateId:', data.dateId);
|
||||
// Nur aktualisieren, wenn das aktuelle Datum betroffen ist
|
||||
if (this.date && this.date !== 'new' && this.date.id === data.dateId) {
|
||||
console.log('✅ [DiaryView] Datum stimmt überein, lade Training Plan neu');
|
||||
// Lade Training Plan neu
|
||||
try {
|
||||
this.trainingPlan = await apiClient.get(`/diary-date-activities/${this.currentClub}/${this.date.id}`).then(response => response.data);
|
||||
this.calculateIntermediateTimes();
|
||||
console.log('✅ [DiaryView] Training Plan neu geladen');
|
||||
} catch (error) {
|
||||
console.error('❌ [DiaryView] Fehler beim Neuladen des Trainingsplans:', error);
|
||||
console.error('Fehler beim Neuladen des Trainingsplans:', error);
|
||||
}
|
||||
} else {
|
||||
console.log('⚠️ [DiaryView] Datum stimmt nicht überein oder kein Datum ausgewählt');
|
||||
}
|
||||
},
|
||||
|
||||
async handleMemberChanged(data) {
|
||||
console.log('📡 [DiaryView] handleMemberChanged aufgerufen:', data);
|
||||
console.log('📡 [DiaryView] Aktueller Club:', this.currentClub, 'Event Club:', data.clubId);
|
||||
// Prüfe, ob der Club übereinstimmt
|
||||
if (data.clubId && String(data.clubId) === String(this.currentClub)) {
|
||||
console.log('✅ [DiaryView] Club stimmt überein, lade Mitgliederliste neu');
|
||||
console.log('📡 [DiaryView] Aktuelle Mitgliederanzahl vor Reload:', this.members?.length || 0);
|
||||
// Lade Mitgliederliste neu
|
||||
try {
|
||||
await this.loadMembers();
|
||||
console.log('✅ [DiaryView] Mitgliederliste neu geladen, neue Anzahl:', this.members?.length || 0);
|
||||
console.log('📡 [DiaryView] Mitgliederliste:', this.members);
|
||||
// Force Vue update
|
||||
this.$forceUpdate();
|
||||
} catch (error) {
|
||||
console.error('❌ [DiaryView] Fehler beim Neuladen der Mitgliederliste:', error);
|
||||
console.error('Fehler beim Neuladen der Mitgliederliste:', error);
|
||||
}
|
||||
} else {
|
||||
console.log('⚠️ [DiaryView] Club stimmt nicht überein - Event Club:', data.clubId, 'Aktueller Club:', this.currentClub);
|
||||
}
|
||||
},
|
||||
|
||||
async handleGroupChanged(data) {
|
||||
console.log('📡 [DiaryView] handleGroupChanged aufgerufen:', data);
|
||||
// Nur aktualisieren, wenn das aktuelle Datum betroffen ist
|
||||
if (this.date && this.date !== 'new' && String(this.date.id) === String(data.dateId)) {
|
||||
console.log('✅ [DiaryView] Datum stimmt überein, lade Gruppenliste neu');
|
||||
try {
|
||||
await this.loadGroups();
|
||||
console.log('✅ [DiaryView] Gruppenliste neu geladen');
|
||||
// Force Vue update
|
||||
this.$forceUpdate();
|
||||
} catch (error) {
|
||||
console.error('❌ [DiaryView] Fehler beim Neuladen der Gruppenliste:', error);
|
||||
console.error('Fehler beim Neuladen der Gruppenliste:', error);
|
||||
}
|
||||
} else {
|
||||
console.log('⚠️ [DiaryView] Datum stimmt nicht überein - Event dateId:', data.dateId, 'Aktuelles Datum:', this.date?.id);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -31,6 +31,10 @@
|
||||
Datum:
|
||||
<input type="date" v-model="newDate" />
|
||||
</label>
|
||||
<label>
|
||||
Gewinnsätze:
|
||||
<input type="number" v-model.number="newWinningSets" min="1" />
|
||||
</label>
|
||||
<button @click="createTournament">Erstellen</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -38,11 +42,15 @@
|
||||
<div class="tournament-info">
|
||||
<label>
|
||||
Name:
|
||||
<input type="text" v-model="currentTournamentName" @blur="updateTournament" />
|
||||
<input type="text" v-model="currentTournamentName" @input="updateTournament" />
|
||||
</label>
|
||||
<label>
|
||||
Datum:
|
||||
<input type="date" v-model="currentTournamentDate" @blur="updateTournament" />
|
||||
<input type="date" v-model="currentTournamentDate" @change="updateTournament" />
|
||||
</label>
|
||||
<label>
|
||||
Gewinnsätze:
|
||||
<input type="number" v-model.number="currentWinningSets" min="1" @input="updateTournament" />
|
||||
</label>
|
||||
</div>
|
||||
<label class="checkbox-item">
|
||||
@@ -55,27 +63,46 @@
|
||||
<span class="collapse-icon" :class="{ 'expanded': showParticipants }">▼</span>
|
||||
</div>
|
||||
<div v-show="showParticipants" class="participants-content">
|
||||
<ul class="participants-list">
|
||||
<li v-for="participant in sortedParticipants" :key="participant.id" class="participant-item">
|
||||
<span class="participant-name">
|
||||
{{ participant.member?.firstName || 'Unbekannt' }}
|
||||
{{ participant.member?.lastName || '' }}
|
||||
</span>
|
||||
<template v-if="isGroupTournament">
|
||||
<span class="participant-group-cell">
|
||||
<select v-model.number="participant.groupNumber" @change="updateParticipantGroup(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>
|
||||
</span>
|
||||
</template>
|
||||
<span class="participant-action-cell">
|
||||
<button @click="removeParticipant(participant)" class="trash-btn-small" title="Löschen">🗑️</button>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="participants-table-container">
|
||||
<table class="participants-table participants-table-header">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="participant-seeded-cell">Gesetzt</th>
|
||||
<th class="participant-name">Name</th>
|
||||
<th v-if="isGroupTournament" class="participant-group-cell">Gruppe</th>
|
||||
<th class="participant-action-cell">Aktion</th>
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
<div class="participants-table-body-wrapper">
|
||||
<table class="participants-table participants-table-body">
|
||||
<tbody>
|
||||
<tr v-for="participant in sortedParticipants" :key="participant.id" class="participant-item">
|
||||
<td class="participant-seeded-cell">
|
||||
<label class="seeded-checkbox-label">
|
||||
<input type="checkbox" :checked="participant.seeded" @change="updateParticipantSeeded(participant, $event)" />
|
||||
</label>
|
||||
</td>
|
||||
<td class="participant-name">
|
||||
{{ participant.member?.firstName || 'Unbekannt' }}
|
||||
{{ participant.member?.lastName || '' }}
|
||||
</td>
|
||||
<td v-if="isGroupTournament" class="participant-group-cell">
|
||||
<select v-model.number="participant.groupNumber" @change="updateParticipantGroup(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-action-cell">
|
||||
<button @click="removeParticipant(participant)" class="trash-btn-small" title="Löschen">🗑️</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="add-participant">
|
||||
<select v-model="selectedMember" class="member-select">
|
||||
<option :value="null">-- Teilnehmer auswählen --</option>
|
||||
@@ -133,7 +160,7 @@
|
||||
<tr v-for="(pl, idx) in groupRankings[group.groupId]" :key="pl.id">
|
||||
<td><strong>G{{ String.fromCharCode(96 + group.groupNumber) }}{{ idx + 1 }}</strong></td>
|
||||
<td>{{ pl.position }}.</td>
|
||||
<td>{{ pl.name }}</td>
|
||||
<td><span v-if="pl.seeded" class="seeded-star">★</span>{{ pl.name }}</td>
|
||||
<td>{{ pl.points }}</td>
|
||||
<td>{{ pl.setsWon }}:{{ pl.setsLost }}</td>
|
||||
<td>
|
||||
@@ -176,7 +203,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="m in groupMatches" :key="m.id" :data-match-id="m.id" :class="{ 'active-match': activeMatchId === m.id }" @click="activeMatchId = m.id">
|
||||
<tr v-for="m in groupMatches" :key="m.id" :data-match-id="m.id" :class="{ 'active-match': activeMatchId === m.id, 'match-finished': m.isFinished, 'match-live': m.isActive }" @click="activeMatchId = m.id">
|
||||
<td>{{ m.groupRound }}</td>
|
||||
<td>{{ m.groupNumber }}</td>
|
||||
<td>
|
||||
@@ -380,10 +407,13 @@
|
||||
{{ getSetsString(m) }}
|
||||
</td>
|
||||
<td>
|
||||
<!-- „Abschließen“-Button nur, wenn noch nicht fertig -->
|
||||
<!-- „Abschließen"-Button nur, wenn noch nicht fertig -->
|
||||
<button v-if="!m.isFinished" @click="finishMatch(m)">Abschließen</button>
|
||||
<!-- „Korrigieren“-Button nur, wenn fertig -->
|
||||
<button v-else @click="reopenMatch(m)">Korrigieren</button>
|
||||
<!-- „Korrigieren"-Button nur, wenn fertig -->
|
||||
<button v-else @click="reopenMatch(m)" class="btn-correct">Korrigieren</button>
|
||||
<!-- „Läuft gerade"-Button nur, wenn nicht abgeschlossen -->
|
||||
<button v-if="!m.isFinished && !m.isActive" @click.stop="setMatchActive(m, true)" class="btn-live" title="Als laufend markieren">▶️</button>
|
||||
<button v-if="!m.isFinished && m.isActive" @click.stop="setMatchActive(m, false)" class="btn-live active" title="Laufend-Markierung entfernen">⏸️</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -420,7 +450,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="m in knockoutMatches" :key="m.id" :class="{ 'active-match': activeMatchId === m.id }" @click="activeMatchId = m.id">
|
||||
<tr v-for="m in knockoutMatches" :key="m.id" :class="{ 'active-match': activeMatchId === m.id, 'match-finished': m.isFinished, 'match-live': m.isActive }" @click="activeMatchId = m.id">
|
||||
<td>{{ m.round }}</td>
|
||||
<td>
|
||||
<template v-if="m.isFinished">
|
||||
@@ -623,7 +653,10 @@
|
||||
</td>
|
||||
<td>
|
||||
<button v-if="!m.isFinished" @click="finishMatch(m)">Fertig</button>
|
||||
<button v-else @click="reopenMatch(m)">Korrigieren</button>
|
||||
<button v-else @click="reopenMatch(m)" class="btn-correct">Korrigieren</button>
|
||||
<!-- „Läuft gerade"-Button nur, wenn nicht abgeschlossen -->
|
||||
<button v-if="!m.isFinished && !m.isActive" @click.stop="setMatchActive(m, true)" class="btn-live" title="Als laufend markieren">▶️</button>
|
||||
<button v-if="!m.isFinished && m.isActive" @click.stop="setMatchActive(m, false)" class="btn-live active" title="Laufend-Markierung entfernen">⏸️</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -703,8 +736,10 @@ export default {
|
||||
selectedDate: 'new',
|
||||
newDate: '',
|
||||
newTournamentName: '',
|
||||
newWinningSets: 3,
|
||||
currentTournamentName: '',
|
||||
currentTournamentDate: '',
|
||||
currentWinningSets: 3,
|
||||
dates: [],
|
||||
participants: [],
|
||||
selectedMember: null,
|
||||
@@ -820,6 +855,7 @@ export default {
|
||||
byGroup[g.groupId] = g.participants.map(p => ({
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
seeded: p.seeded || false,
|
||||
points: 0,
|
||||
setsWon: 0,
|
||||
setsLost: 0,
|
||||
@@ -969,11 +1005,9 @@ export default {
|
||||
if (this.currentClub) {
|
||||
connectSocket(this.currentClub);
|
||||
onTournamentChanged(this.handleTournamentChanged);
|
||||
} else {
|
||||
console.warn('⚠️ [TournamentsView] currentClub nicht gesetzt, Socket-Verbindung übersprungen');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ [TournamentsView] Fehler beim Verbinden mit Socket:', error);
|
||||
console.error('Fehler beim Verbinden mit Socket:', error);
|
||||
}
|
||||
|
||||
// Event-Listener für das Entfernen des Highlights
|
||||
@@ -1059,6 +1093,7 @@ export default {
|
||||
const tournament = tRes.data;
|
||||
this.currentTournamentName = tournament.name || '';
|
||||
this.currentTournamentDate = tournament.date || '';
|
||||
this.currentWinningSets = tournament.winningSets || 3;
|
||||
this.isGroupTournament = tournament.type === 'groups';
|
||||
this.numberOfGroups = tournament.numberOfGroups;
|
||||
this.advancingPerGroup = tournament.advancingPerGroup;
|
||||
@@ -1069,7 +1104,11 @@ export default {
|
||||
clubId: this.currentClub,
|
||||
tournamentId: this.selectedDate
|
||||
});
|
||||
this.participants = pRes.data;
|
||||
// Stelle sicher, dass seeded-Feld vorhanden ist (für alte Einträge)
|
||||
this.participants = pRes.data.map(p => ({
|
||||
...p,
|
||||
seeded: p.seeded || false
|
||||
}));
|
||||
const gRes = await apiClient.get('/tournament/groups', {
|
||||
params: {
|
||||
clubId: this.currentClub,
|
||||
@@ -1099,7 +1138,8 @@ export default {
|
||||
groupId: matchGroupId,
|
||||
groupNumber: groupNumber,
|
||||
groupRound: groupRound, // Stelle sicher, dass groupRound gesetzt ist
|
||||
resultInput: ''
|
||||
resultInput: '',
|
||||
isActive: m.isActive || false
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1113,34 +1153,27 @@ export default {
|
||||
},
|
||||
|
||||
async handleTournamentChanged(data) {
|
||||
console.log('📡 [TournamentsView] handleTournamentChanged aufgerufen:', data);
|
||||
if (!data || !data.tournamentId) {
|
||||
console.warn('⚠️ [TournamentsView] Ungültige Daten im Event:', data);
|
||||
return;
|
||||
}
|
||||
// Nur aktualisieren, wenn das aktuelle Turnier betroffen ist
|
||||
if (this.selectedDate && this.selectedDate !== 'new' && String(this.selectedDate) === String(data.tournamentId)) {
|
||||
console.log('✅ [TournamentsView] Turnier stimmt überein, lade Daten neu');
|
||||
try {
|
||||
await this.loadTournamentData();
|
||||
console.log('✅ [TournamentsView] Turnier-Daten neu geladen');
|
||||
// Force Vue update
|
||||
this.$forceUpdate();
|
||||
} catch (error) {
|
||||
console.error('❌ [TournamentsView] Fehler beim Neuladen der Turnier-Daten:', error);
|
||||
console.error('Fehler beim Neuladen der Turnier-Daten:', error);
|
||||
}
|
||||
} else {
|
||||
// Wenn ein neues Turnier erstellt wurde, lade die Turnierliste neu
|
||||
if (data.tournamentId && this.selectedDate === 'new') {
|
||||
console.log('📡 [TournamentsView] Neues Turnier erstellt, lade Turnierliste neu');
|
||||
try {
|
||||
const d = await apiClient.get(`/tournament/${this.currentClub}`);
|
||||
this.dates = d.data;
|
||||
} catch (error) {
|
||||
console.error('❌ [TournamentsView] Fehler beim Neuladen der Turnierliste:', error);
|
||||
console.error('Fehler beim Neuladen der Turnierliste:', error);
|
||||
}
|
||||
} else {
|
||||
console.log('⚠️ [TournamentsView] Turnier stimmt nicht überein - Event tournamentId:', data.tournamentId, 'Aktuelles Turnier:', this.selectedDate);
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1213,18 +1246,21 @@ export default {
|
||||
|
||||
async updateTournament() {
|
||||
if (!this.currentTournamentDate) {
|
||||
await this.showInfo('Fehler', 'Bitte geben Sie ein Datum ein!', '', 'error');
|
||||
// Wenn kein Datum vorhanden ist, nicht speichern (z.B. beim ersten Laden)
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await apiClient.put(`/tournament/${this.currentClub}/${this.selectedDate}`, {
|
||||
name: this.currentTournamentName || this.currentTournamentDate,
|
||||
date: this.currentTournamentDate
|
||||
date: this.currentTournamentDate,
|
||||
winningSets: this.currentWinningSets
|
||||
});
|
||||
// Prüfe, ob es einen Trainingstag für das neue Datum gibt
|
||||
await this.checkTrainingForDate(this.currentTournamentDate);
|
||||
// Lade Turnierliste neu, damit der Name in der Dropdown-Liste aktualisiert wird
|
||||
await this.loadTournaments();
|
||||
// Lade Turnierdaten neu, um sicherzustellen, dass alle Werte korrekt sind
|
||||
await this.loadTournamentData();
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Aktualisieren des Turniers:', error);
|
||||
const message = safeErrorMessage(error, 'Fehler beim Aktualisieren des Turniers.');
|
||||
@@ -1243,7 +1279,8 @@ export default {
|
||||
const r = await apiClient.post('/tournament', {
|
||||
clubId: this.currentClub,
|
||||
tournamentName: this.newTournamentName || this.newDate,
|
||||
date: this.newDate
|
||||
date: this.newDate,
|
||||
winningSets: this.newWinningSets
|
||||
});
|
||||
|
||||
|
||||
@@ -1258,6 +1295,7 @@ export default {
|
||||
|
||||
this.newDate = '';
|
||||
this.newTournamentName = '';
|
||||
this.newWinningSets = 3;
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Erstellen des Turniers:', error);
|
||||
const message = safeErrorMessage(error, 'Fehler beim Erstellen des Turniers.');
|
||||
@@ -1340,25 +1378,8 @@ export default {
|
||||
set: (match.tournamentResults?.length || 0) + 1,
|
||||
result
|
||||
});
|
||||
const allRes = await apiClient.get(
|
||||
`/tournament/matches/${this.currentClub}/${this.selectedDate}`
|
||||
);
|
||||
const updated = allRes.data.find(m2 => m2.id === match.id);
|
||||
if (!updated) {
|
||||
const message = safeErrorMessage(error, 'Fehler beim Aktualisieren des Matches.');
|
||||
await this.showInfo('Fehler', message, '', 'error');
|
||||
return;
|
||||
}
|
||||
match.tournamentResults = updated.tournamentResults || [];
|
||||
const resultString = match.tournamentResults.length
|
||||
? match.tournamentResults
|
||||
.sort((a, b) => a.set - b.set)
|
||||
.map(r => `${Math.abs(r.pointsPlayer1)}:${Math.abs(r.pointsPlayer2)}`)
|
||||
.join(', ')
|
||||
: null;
|
||||
|
||||
match.result = resultString;
|
||||
match.resultInput = '';
|
||||
// Lade Turnierdaten neu, da das Match möglicherweise automatisch abgeschlossen wurde
|
||||
await this.loadTournamentData();
|
||||
},
|
||||
|
||||
async finishMatch(match) {
|
||||
@@ -1370,6 +1391,27 @@ export default {
|
||||
await this.loadTournamentData();
|
||||
},
|
||||
|
||||
async setMatchActive(match, isActive) {
|
||||
try {
|
||||
await apiClient.put(`/tournament/match/${this.currentClub}/${this.selectedDate}/${match.id}/active`, {
|
||||
isActive: isActive
|
||||
});
|
||||
// Aktualisiere lokal
|
||||
match.isActive = isActive;
|
||||
// Wenn ein Match als aktiv gesetzt wird, setze alle anderen auf inaktiv
|
||||
if (isActive) {
|
||||
this.matches.forEach(m => {
|
||||
if (m.id !== match.id) {
|
||||
m.isActive = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Setzen des Match-Status:', error);
|
||||
await this.loadTournamentData();
|
||||
}
|
||||
},
|
||||
|
||||
async startKnockout() {
|
||||
await apiClient.post('/tournament/knockout', {
|
||||
clubId: this.currentClub,
|
||||
@@ -1677,6 +1719,24 @@ export default {
|
||||
});
|
||||
},
|
||||
|
||||
async updateParticipantSeeded(participant, event) {
|
||||
const seeded = event.target.checked;
|
||||
|
||||
// Aktualisiere lokal
|
||||
participant.seeded = seeded;
|
||||
|
||||
// Sende an Backend
|
||||
try {
|
||||
await apiClient.put(`/tournament/participant/${this.currentClub}/${this.selectedDate}/${participant.id}/seeded`, {
|
||||
seeded: seeded
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Aktualisieren des Gesetzt-Status:', error);
|
||||
// Bei Fehler: Lade Daten neu
|
||||
await this.loadTournamentData();
|
||||
}
|
||||
},
|
||||
|
||||
async updateParticipantGroup(participant, event) {
|
||||
const groupNumber = parseInt(event.target.value);
|
||||
|
||||
@@ -1964,7 +2024,8 @@ export default {
|
||||
}
|
||||
|
||||
.new-tournament input[type="text"],
|
||||
.new-tournament input[type="date"] {
|
||||
.new-tournament input[type="date"],
|
||||
.new-tournament input[type="number"] {
|
||||
padding: 0.4rem;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
@@ -1990,7 +2051,8 @@ export default {
|
||||
}
|
||||
|
||||
.tournament-info input[type="text"],
|
||||
.tournament-info input[type="date"] {
|
||||
.tournament-info input[type="date"],
|
||||
.tournament-info input[type="number"] {
|
||||
padding: 0.4rem;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
@@ -2148,32 +2210,97 @@ button {
|
||||
}
|
||||
}
|
||||
|
||||
.participants-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
.participants-table-container {
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
display: inline-block;
|
||||
width: auto;
|
||||
display: table;
|
||||
}
|
||||
|
||||
.participants-table {
|
||||
width: auto;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.participants-table-header {
|
||||
margin: 0;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.participants-table-header th {
|
||||
font-weight: bold;
|
||||
font-size: 0.85em;
|
||||
color: #495057;
|
||||
padding: 0.15rem 0.5rem;
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.participants-table-body-wrapper {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
overflow-x: auto;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.participants-table-body td {
|
||||
padding: 0.1rem 0.5rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Synchronisiere Spaltenbreiten zwischen Header und Body */
|
||||
.participants-table-header .participant-seeded-cell,
|
||||
.participants-table-body .participant-seeded-cell {
|
||||
width: 60px;
|
||||
}
|
||||
|
||||
.participants-table-header .participant-name,
|
||||
.participants-table-body .participant-name {
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.participants-table-header .participant-group-cell,
|
||||
.participants-table-body .participant-group-cell {
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.participants-table-header .participant-action-cell,
|
||||
.participants-table-body .participant-action-cell {
|
||||
width: 50px;
|
||||
}
|
||||
|
||||
.participant-item {
|
||||
display: table-row;
|
||||
font-size: 0.9em;
|
||||
line-height: 1.1;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.participant-seeded-cell {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.seeded-checkbox-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.seeded-checkbox-label input[type="checkbox"] {
|
||||
cursor: pointer;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.participant-name {
|
||||
display: table-cell;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.participant-group-cell {
|
||||
display: table-cell;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.participant-action-cell {
|
||||
display: table-cell;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@@ -2277,6 +2404,11 @@ button {
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.seeded-star {
|
||||
color: #ffd700;
|
||||
margin-right: 0.25em;
|
||||
}
|
||||
|
||||
.match-cell.clickable {
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
@@ -2363,4 +2495,85 @@ tbody tr:hover:not(.active-match) {
|
||||
border: 2px solid #ffc107 !important;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Abgeschlossene Spiele in grauer Schrift */
|
||||
.match-finished {
|
||||
color: #828a91 !important;
|
||||
background-color: #fafbfc !important;
|
||||
}
|
||||
|
||||
.match-finished td {
|
||||
color: #828a91 !important;
|
||||
}
|
||||
|
||||
/* Laufende Spiele farblich hervorheben - nur wenn nicht abgeschlossen */
|
||||
.match-live:not(.match-finished) {
|
||||
background-color: #d4edda !important;
|
||||
border-left: 4px solid #28a745 !important;
|
||||
}
|
||||
|
||||
.match-live:not(.match-finished) td {
|
||||
color: #155724;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Button für "Läuft gerade" */
|
||||
.btn-live {
|
||||
margin-left: 0.5rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 0.9em;
|
||||
background-color: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-live:hover {
|
||||
background-color: #e9ecef;
|
||||
border-color: #adb5bd;
|
||||
}
|
||||
|
||||
.btn-live.active {
|
||||
background-color: #28a745;
|
||||
border-color: #28a745;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-live.active:hover {
|
||||
background-color: #218838;
|
||||
border-color: #1e7e34;
|
||||
}
|
||||
|
||||
/* Button für "Korrigieren" - weißer Hintergrund mit grüner Schrift */
|
||||
.match-finished td .btn-correct,
|
||||
.btn-correct {
|
||||
background: white !important;
|
||||
background-color: white !important;
|
||||
background-image: none !important;
|
||||
color: #4CAF50 !important;
|
||||
border: 1px solid #4CAF50 !important;
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 0.9em;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: none !important;
|
||||
min-height: auto !important;
|
||||
}
|
||||
|
||||
.match-finished td .btn-correct::before,
|
||||
.btn-correct::before {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.match-finished td .btn-correct:hover,
|
||||
.btn-correct:hover {
|
||||
background: #f8f9fa !important;
|
||||
background-color: #f8f9fa !important;
|
||||
background-image: none !important;
|
||||
border-color: #45a049 !important;
|
||||
color: #45a049 !important;
|
||||
transform: none !important;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user