Add group deletion functionality and socket event emissions for real-time updates

This commit introduces the ability to delete groups in the groupController, along with the necessary backend service updates. It also adds socket event emissions for group and activity changes, ensuring real-time updates are sent to clients when groups are deleted. The frontend is updated to include a delete button in the DiaryView, allowing users to remove groups easily. Additionally, the groupRoutes and socketService are modified to support these new features, enhancing the overall interactivity of the application.
This commit is contained in:
Torsten Schulz (local)
2025-11-13 18:48:51 +01:00
parent 2b06a8dd10
commit 9b8dcd8561
8 changed files with 326 additions and 10 deletions

View File

@@ -1,5 +1,7 @@
import HttpError from '../exceptions/HttpError.js';
import groupService from '../services/groupService.js';
import { emitActivityChanged, emitGroupChanged } from '../services/socketService.js';
import DiaryDate from '../models/DiaryDates.js';
import { devLog } from '../utils/logger.js';
const addGroup = async(req, res) => {
@@ -7,6 +9,15 @@ const addGroup = async(req, res) => {
const { authcode: userToken } = req.headers;
const { clubid: clubId, dateid: dateId, name, lead } = req.body;
const result = await groupService.addGroup(userToken, clubId, dateId, name, lead);
// Emit Socket-Event für Gruppen-Änderungen
if (dateId) {
const diaryDate = await DiaryDate.findByPk(dateId);
if (diaryDate?.clubId) {
emitGroupChanged(diaryDate.clubId, dateId);
}
}
res.status(201).json(result);
} catch (error) {
console.error('[addGroup] - Error:', error);
@@ -33,6 +44,15 @@ const changeGroup = async(req, res) => {
const { groupId } = req.params;
const { clubid: clubId, dateid: dateId, name, lead } = req.body;
const result = await groupService.changeGroup(userToken, groupId, clubId, dateId, name, lead);
// Emit Socket-Event für Gruppen-Änderungen
if (dateId) {
const diaryDate = await DiaryDate.findByPk(dateId);
if (diaryDate?.clubId) {
emitGroupChanged(diaryDate.clubId, dateId);
}
}
res.status(200).json(result);
} catch (error) {
console.error('[changeGroup] - Error:', error);
@@ -40,4 +60,27 @@ const changeGroup = async(req, res) => {
}
}
export { addGroup, getGroups, changeGroup};
const deleteGroup = async(req, res) => {
try {
const { authcode: userToken } = req.headers;
const { groupId } = req.params;
const { clubid: clubId, dateid: dateId } = req.body;
const result = await groupService.deleteGroup(userToken, groupId, clubId, dateId);
// Emit Socket-Events für Gruppen- und Aktivitäts-Änderungen (Gruppen werden in Aktivitäten verwendet)
if (dateId) {
const diaryDate = await DiaryDate.findByPk(dateId);
if (diaryDate?.clubId) {
emitGroupChanged(diaryDate.clubId, dateId);
emitActivityChanged(diaryDate.clubId, dateId);
}
}
res.status(200).json(result);
} catch (error) {
console.error('[deleteGroup] - Error:', error);
res.status(error.statusCode || 500).json({ error: error.message });
}
}
export { addGroup, getGroups, changeGroup, deleteGroup};

View File

@@ -1,5 +1,6 @@
// controllers/tournamentController.js
import tournamentService from "../services/tournamentService.js";
import { emitTournamentChanged } from '../services/socketService.js';
// 1. Alle Turniere eines Vereins
export const getTournaments = async (req, res) => {
@@ -20,9 +21,13 @@ export const addTournament = async (req, res) => {
const { clubId, tournamentName, date } = req.body;
try {
const tournament = await tournamentService.addTournament(token, clubId, tournamentName, date);
// Emit Socket-Event
if (clubId && tournament && tournament.id) {
emitTournamentChanged(clubId, tournament.id);
}
res.status(201).json(tournament);
} catch (error) {
console.error(error);
console.error('[addTournament] Error:', error);
res.status(500).json({ error: error.message });
}
};
@@ -32,11 +37,16 @@ export const addParticipant = async (req, res) => {
const { authcode: token } = req.headers;
const { clubId, tournamentId, participant: participantId } = req.body;
try {
if (!participantId) {
return res.status(400).json({ error: 'Teilnehmer-ID ist erforderlich' });
}
await tournamentService.addParticipant(token, clubId, tournamentId, participantId);
const participants = await tournamentService.getParticipants(token, clubId, tournamentId);
// Emit Socket-Event
emitTournamentChanged(clubId, tournamentId);
res.status(200).json(participants);
} catch (error) {
console.error(error);
console.error('[addParticipant] Error:', error);
res.status(500).json({ error: error.message });
}
};
@@ -60,6 +70,8 @@ export const setModus = async (req, res) => {
const { clubId, tournamentId, type, numberOfGroups, advancingPerGroup } = req.body;
try {
await tournamentService.setModus(token, clubId, tournamentId, type, numberOfGroups, advancingPerGroup);
// Emit Socket-Event
emitTournamentChanged(clubId, tournamentId);
res.sendStatus(204);
} catch (error) {
console.error(error);
@@ -73,6 +85,8 @@ export const createGroups = async (req, res) => {
const { clubId, tournamentId } = req.body;
try {
await tournamentService.createGroups(token, clubId, tournamentId);
// Emit Socket-Event
emitTournamentChanged(clubId, tournamentId);
res.sendStatus(204);
} catch (error) {
console.error(error);
@@ -86,6 +100,8 @@ export const fillGroups = async (req, res) => {
const { clubId, tournamentId } = req.body;
try {
const updatedMembers = await tournamentService.fillGroups(token, clubId, tournamentId);
// Emit Socket-Event
emitTournamentChanged(clubId, tournamentId);
res.status(200).json(updatedMembers);
} catch (error) {
console.error(error);
@@ -138,6 +154,8 @@ export const addMatchResult = async (req, res) => {
const { clubId, tournamentId, matchId, set, result } = req.body;
try {
await tournamentService.addMatchResult(token, clubId, tournamentId, matchId, set, result);
// Emit Socket-Event
emitTournamentChanged(clubId, tournamentId);
res.status(200).json({ message: "Result added successfully" });
} catch (error) {
console.error(error);
@@ -151,6 +169,8 @@ export const finishMatch = async (req, res) => {
const { clubId, tournamentId, matchId } = req.body;
try {
await tournamentService.finishMatch(token, clubId, tournamentId, matchId);
// Emit Socket-Event
emitTournamentChanged(clubId, tournamentId);
res.status(200).json({ message: "Match finished successfully" });
} catch (error) {
console.error(error);
@@ -164,6 +184,8 @@ export const startKnockout = async (req, res) => {
try {
await tournamentService.startKnockout(token, clubId, tournamentId);
// Emit Socket-Event
emitTournamentChanged(clubId, tournamentId);
res.status(200).json({ message: "K.o.-Runde erfolgreich gestartet" });
} catch (error) {
const status = /Gruppenmodus|Zu wenige Qualifikanten/.test(error.message) ? 400 : 500;
@@ -190,6 +212,8 @@ export const manualAssignGroups = async (req, res) => {
numberOfGroups, // neu
maxGroupSize // neu
);
// Emit Socket-Event
emitTournamentChanged(clubId, tournamentId);
res.status(200).json(groupsWithParts);
} catch (error) {
console.error('Error in manualAssignGroups:', error);
@@ -202,6 +226,8 @@ export const resetGroups = async (req, res) => {
const { clubId, tournamentId } = req.body;
try {
await tournamentService.resetGroups(token, clubId, tournamentId);
// Emit Socket-Event
emitTournamentChanged(clubId, tournamentId);
res.sendStatus(204);
} catch (err) {
console.error(err);
@@ -214,6 +240,8 @@ export const resetMatches = async (req, res) => {
const { clubId, tournamentId } = req.body;
try {
await tournamentService.resetMatches(token, clubId, tournamentId);
// Emit Socket-Event
emitTournamentChanged(clubId, tournamentId);
res.sendStatus(204);
} catch (err) {
console.error(err);
@@ -227,6 +255,8 @@ export const removeParticipant = async (req, res) => {
try {
await tournamentService.removeParticipant(token, clubId, tournamentId, participantId);
const participants = await tournamentService.getParticipants(token, clubId, tournamentId);
// Emit Socket-Event
emitTournamentChanged(clubId, tournamentId);
res.status(200).json(participants);
} catch (err) {
console.error(err);
@@ -245,6 +275,8 @@ export const deleteMatchResult = async (req, res) => {
matchId,
set
);
// Emit Socket-Event
emitTournamentChanged(clubId, tournamentId);
res.status(200).json({ message: 'Einzelsatz gelöscht' });
} catch (error) {
console.error('Error in deleteMatchResult:', error);
@@ -258,6 +290,8 @@ export const reopenMatch = async (req, res) => {
const { clubId, tournamentId, matchId } = req.body;
try {
await tournamentService.reopenMatch(token, clubId, tournamentId, matchId);
// Emit Socket-Event
emitTournamentChanged(clubId, tournamentId);
// Gib optional das aktualisierte Match zurück
res.status(200).json({ message: "Match reopened" });
} catch (error) {
@@ -271,6 +305,8 @@ export const deleteKnockoutMatches = async (req, res) => {
const { clubId, tournamentId } = req.body;
try {
await tournamentService.resetKnockout(token, clubId, tournamentId);
// Emit Socket-Event
emitTournamentChanged(clubId, tournamentId);
res.status(200).json({ message: "K.o.-Runde gelöscht" });
} catch (error) {
console.error("Error in deleteKnockoutMatches:", error);

View File

@@ -1,6 +1,6 @@
import express from 'express';
import { authenticate } from '../middleware/authMiddleware.js';
import { addGroup, getGroups, changeGroup } from '../controllers/groupController.js';
import { addGroup, getGroups, changeGroup, deleteGroup } from '../controllers/groupController.js';
const router = express.Router();
@@ -9,5 +9,6 @@ router.use(authenticate);
router.post('/', addGroup);
router.get('/:clubId/:dateId', getGroups);
router.put('/:groupId', changeGroup);
router.delete('/:groupId', deleteGroup);
export default router;

View File

@@ -58,6 +58,22 @@ class GroupService {
await group.save();
return group;
}
async deleteGroup(userToken, groupId, clubId, dateId) {
await checkAccess(userToken, clubId);
await this.checkDiaryDateToClub(clubId, dateId);
const group = await Group.findOne({
where: {
id: groupId,
diaryDateId: dateId
}
});
if (!group) {
throw new HttpError('Gruppe nicht gefunden oder passt nicht zum angegebenen Datum und Verein', 404);
}
await group.destroy();
return { success: true };
}
}
export default new GroupService();

View File

@@ -110,3 +110,13 @@ export const emitMemberChanged = (clubId) => {
emitToClub(clubId, 'member:changed', { clubId });
};
// Event für Gruppen-Änderungen (erstellen, aktualisieren, löschen)
export const emitGroupChanged = (clubId, dateId) => {
emitToClub(clubId, 'group:changed', { dateId });
};
// Event für Tournament-Änderungen (alle Aktionen)
export const emitTournamentChanged = (clubId, tournamentId) => {
emitToClub(clubId, 'tournament:changed', { tournamentId });
};

View File

@@ -166,6 +166,28 @@ export const onMemberChanged = (callback) => {
}
};
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');
}
};
// Event-Listener entfernen
export const offParticipantAdded = (callback) => {
if (socket) {
@@ -245,3 +267,15 @@ export const offMemberChanged = (callback) => {
}
};
export const offGroupChanged = (callback) => {
if (socket) {
socket.off('group:changed', callback);
}
};
export const offTournamentChanged = (callback) => {
if (socket) {
socket.off('tournament:changed', callback);
}
};

View File

@@ -96,6 +96,10 @@
<input v-else type="text" v-model="group.lead" @blur="saveGroup(group)"
@keyup.enter="saveGroup(group)" @keyup.esc="cancelEditGroup"
style="display: inline;width:10em" />
<button v-if="editingGroupId !== group.id" @click="deleteGroup(group.id)"
class="trash-btn"
style="margin-left: 10px;"
title="Gruppe löschen">🗑</button>
</li>
</ul>
</div>
@@ -104,11 +108,11 @@
<div class="groups">
<div>
<label for="groupCount">Anzahl Gruppen:</label>
<input type="number" id="groupCount" v-model="newGroupCount" min="2" max="10" required />
<input type="number" id="groupCount" v-model="newGroupCount" :min="groups.length > 0 ? 1 : 2" max="10" required />
</div>
<div>
<label>&nbsp;</label>
<button type="submit" @click="createGroups">Gruppen erstellen</button>
<button type="submit" @click="createGroups">{{ groups.length > 0 ? 'Gruppe hinzufügen' : 'Gruppen erstellen' }}</button>
</div>
</div>
</div>
@@ -689,6 +693,7 @@ import {
onActivityMemberRemoved,
onActivityChanged,
onMemberChanged,
onGroupChanged,
offParticipantAdded,
offParticipantRemoved,
offParticipantUpdated,
@@ -700,7 +705,8 @@ import {
offActivityMemberAdded,
offActivityMemberRemoved,
offActivityChanged,
offMemberChanged
offMemberChanged,
offGroupChanged
} from '../services/socketService.js';
export default {
@@ -1215,6 +1221,8 @@ export default {
try {
const response = await apiClient.get(`/group/${this.currentClub}/${this.date.id}`);
this.groups = response.data;
// Setze newGroupCount basierend auf vorhandenen Gruppen
this.newGroupCount = this.groups.length > 0 ? 1 : 2;
} catch (error) {
// ignore
}
@@ -1807,6 +1815,13 @@ export default {
},
async createGroups() {
try {
// Validierung: Wenn keine Gruppen existieren, müssen mindestens 2 erstellt werden
if (this.groups.length === 0 && this.newGroupCount < 2) {
this.showInfo('Fehler', 'Beim ersten Erstellen müssen mindestens 2 Gruppen erstellt werden!', '', 'error');
this.newGroupCount = 2;
return;
}
// Bestimme Startnummer basierend auf vorhandenen Gruppen
const existingNumbers = (this.groups || [])
.map(g => parseInt((g.name || '').trim(), 10))
@@ -1824,8 +1839,14 @@ export default {
}
await apiClient.post('/group', form);
}
const countCreated = this.newGroupCount;
await this.loadGroups();
this.showInfo('Erfolg', `${this.newGroupCount} Gruppen wurden erfolgreich erstellt!`, '', 'success');
// Setze newGroupCount zurück: 1 wenn bereits Gruppen existieren, 2 wenn keine
this.newGroupCount = this.groups.length > 0 ? 1 : 2;
const message = countCreated === 1
? '1 Gruppe wurde erfolgreich hinzugefügt!'
: `${countCreated} Gruppen wurden erfolgreich erstellt!`;
this.showInfo('Erfolg', message, '', 'success');
} catch (error) {
console.error('Fehler beim Erstellen der Gruppen:', error);
this.showInfo('Fehler', 'Fehler beim Erstellen der Gruppen', '', 'error');
@@ -1887,6 +1908,32 @@ export default {
cancelEditGroup() {
this.editingGroupId = null;
},
async deleteGroup(groupId) {
try {
const group = this.groups.find(g => g.id === groupId);
if (!group) {
return;
}
const confirmed = confirm(`Möchten Sie die Gruppe "${group.name}" wirklich löschen?`);
if (!confirmed) {
return;
}
await apiClient.delete(`/group/${groupId}`, {
data: {
clubid: this.currentClub,
dateid: this.date.id
}
});
await this.loadGroups();
this.showInfo('Erfolg', 'Gruppe wurde erfolgreich gelöscht!', '', 'success');
} catch (error) {
console.error('Fehler beim Löschen der Gruppe:', error);
this.showInfo('Fehler', 'Fehler beim Löschen der Gruppe', '', 'error');
}
},
async openTagInfos(member) {
if (!member) {
return;
@@ -2610,6 +2657,9 @@ export default {
// Event-Handler für Member-Änderungen
onMemberChanged(this.handleMemberChanged);
// Event-Handler für Gruppen-Änderungen
onGroupChanged(this.handleGroupChanged);
console.log('✅ [DiaryView] Alle Event-Handler registriert');
},
@@ -2626,6 +2676,7 @@ export default {
offActivityMemberRemoved(this.handleActivityMemberRemoved);
offActivityChanged(this.handleActivityChanged);
offMemberChanged(this.handleMemberChanged);
offGroupChanged(this.handleGroupChanged);
},
async handleParticipantAdded(data) {
@@ -2831,6 +2882,24 @@ export default {
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);
}
} else {
console.log('⚠️ [DiaryView] Datum stimmt nicht überein - Event dateId:', data.dateId, 'Aktuelles Datum:', this.date?.id);
}
},
},
async mounted() {
await this.init();

View File

@@ -30,7 +30,7 @@
</div>
<div v-show="showParticipants" class="participants-content">
<ul>
<li v-for="participant in participants" :key="participant.id">
<li v-for="participant in sortedParticipants" :key="participant.id">
{{ participant.member?.firstName || 'Unbekannt' }}
{{ participant.member?.lastName || '' }}
<template v-if="isGroupTournament">
@@ -72,6 +72,7 @@
</ul>
<div class="add-participant">
<select v-model="selectedMember">
<option :value="null">-- Teilnehmer auswählen --</option>
<option v-for="member in clubMembers" :key="member.id" :value="member.id">
{{ member.firstName }}
{{ member.lastName }}
@@ -674,6 +675,7 @@ import apiClient from '../apiClient.js';
import InfoDialog from '../components/InfoDialog.vue';
import ConfirmDialog from '../components/ConfirmDialog.vue';
import { buildInfoConfig, buildConfirmConfig, safeErrorMessage } from '../utils/dialogUtils.js';
import { connectSocket, disconnectSocket, onTournamentChanged, offTournamentChanged } from '../services/socketService.js';
export default {
name: 'TournamentsView',
data() {
@@ -720,6 +722,22 @@ export default {
computed: {
...mapGetters(['isAuthenticated', 'currentClub']),
sortedParticipants() {
if (!this.participants || this.participants.length === 0) {
return [];
}
return [...this.participants].sort((a, b) => {
const firstNameA = (a.member?.firstName || '').toLowerCase();
const firstNameB = (b.member?.firstName || '').toLowerCase();
if (firstNameA !== firstNameB) {
return firstNameA.localeCompare(firstNameB, 'de');
}
const lastNameA = (a.member?.lastName || '').toLowerCase();
const lastNameB = (b.member?.lastName || '').toLowerCase();
return lastNameA.localeCompare(lastNameB, 'de');
});
},
knockoutMatches() {
return this.matches.filter(m => m.round !== 'group');
},
@@ -883,12 +901,35 @@ export default {
const m = await apiClient.get(
`/clubmembers/get/${this.currentClub}/false`
);
this.clubMembers = m.data;
// Sortiere alphabetisch: zuerst nach Vorname, dann nach Nachname
const sorted = (m.data || []).sort((a, b) => {
const firstNameA = (a.firstName || '').toLowerCase();
const firstNameB = (b.firstName || '').toLowerCase();
if (firstNameA !== firstNameB) {
return firstNameA.localeCompare(firstNameB, 'de');
}
const lastNameA = (a.lastName || '').toLowerCase();
const lastNameB = (b.lastName || '').toLowerCase();
return lastNameA.localeCompare(lastNameB, 'de');
});
this.clubMembers = sorted;
},
mounted() {
// Lade Turniere beim Start
this.loadTournaments();
// Socket.IO verbinden
try {
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);
}
// Event-Listener für das Entfernen des Highlights
document.addEventListener('click', (e) => {
// Entferne Highlight nur wenn nicht auf eine Matrix-Zelle geklickt wird
@@ -902,6 +943,11 @@ export default {
this.clearHighlight();
});
},
beforeUnmount() {
// Socket-Listener entfernen
offTournamentChanged(this.handleTournamentChanged);
disconnectSocket();
},
methods: {
// Dialog Helper Methods
async showInfo(title, message, details = '', type = 'info') {
@@ -1010,6 +1056,39 @@ export default {
this.showKnockout = this.matches.some(m => m.round !== 'group');
},
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);
}
} 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);
}
} else {
console.log('⚠️ [TournamentsView] Turnier stimmt nicht überein - Event tournamentId:', data.tournamentId, 'Aktuelles Turnier:', this.selectedDate);
}
}
},
getPlayerName(p) {
return p.member.firstName + ' ' + p.member.lastName;
@@ -1020,6 +1099,30 @@ export default {
const d = await apiClient.get(`/tournament/${this.currentClub}`);
this.dates = d.data;
// Lade Mitgliederliste, falls noch nicht geladen
if (!this.clubMembers || this.clubMembers.length === 0) {
try {
const m = await apiClient.get(
`/clubmembers/get/${this.currentClub}/false`
);
// Sortiere alphabetisch: zuerst nach Vorname, dann nach Nachname
const sorted = (m.data || []).sort((a, b) => {
const firstNameA = (a.firstName || '').toLowerCase();
const firstNameB = (b.firstName || '').toLowerCase();
if (firstNameA !== firstNameB) {
return firstNameA.localeCompare(firstNameB, 'de');
}
const lastNameA = (a.lastName || '').toLowerCase();
const lastNameB = (b.lastName || '').toLowerCase();
return lastNameA.localeCompare(lastNameB, 'de');
});
this.clubMembers = sorted;
} catch (error) {
console.error('Fehler beim Laden der Mitgliederliste:', error);
this.clubMembers = [];
}
}
// Prüfe, ob es einen Trainingstag heute gibt
await this.checkTrainingToday();
} catch (error) {
@@ -1074,6 +1177,10 @@ export default {
},
async addParticipant() {
if (!this.selectedMember) {
await this.showInfo('Fehler', 'Bitte wählen Sie einen Teilnehmer aus!', '', 'error');
return;
}
const oldMap = this.participants.reduce((map, p) => {
map[p.id] = p.groupNumber
return map