From 9b8dcd8561cd67f5a979333619adc0189c1a2b5d Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Thu, 13 Nov 2025 18:48:51 +0100 Subject: [PATCH] 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. --- backend/controllers/groupController.js | 45 +++++++- backend/controllers/tournamentController.js | 40 ++++++- backend/routes/groupRoutes.js | 3 +- backend/services/groupService.js | 16 +++ backend/services/socketService.js | 10 ++ frontend/src/services/socketService.js | 34 ++++++ frontend/src/views/DiaryView.vue | 77 +++++++++++++- frontend/src/views/TournamentsView.vue | 111 +++++++++++++++++++- 8 files changed, 326 insertions(+), 10 deletions(-) diff --git a/backend/controllers/groupController.js b/backend/controllers/groupController.js index 5ddb95a..c1bdfea 100644 --- a/backend/controllers/groupController.js +++ b/backend/controllers/groupController.js @@ -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}; \ No newline at end of file +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}; \ No newline at end of file diff --git a/backend/controllers/tournamentController.js b/backend/controllers/tournamentController.js index dd197f1..dd5e36f 100644 --- a/backend/controllers/tournamentController.js +++ b/backend/controllers/tournamentController.js @@ -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); diff --git a/backend/routes/groupRoutes.js b/backend/routes/groupRoutes.js index 5cc30d6..693a97f 100644 --- a/backend/routes/groupRoutes.js +++ b/backend/routes/groupRoutes.js @@ -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; diff --git a/backend/services/groupService.js b/backend/services/groupService.js index 13f5a70..4f47e7b 100644 --- a/backend/services/groupService.js +++ b/backend/services/groupService.js @@ -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(); \ No newline at end of file diff --git a/backend/services/socketService.js b/backend/services/socketService.js index 0defb3b..e49cd9c 100644 --- a/backend/services/socketService.js +++ b/backend/services/socketService.js @@ -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 }); +}; + diff --git a/frontend/src/services/socketService.js b/frontend/src/services/socketService.js index f6fea2a..5c7b342 100644 --- a/frontend/src/services/socketService.js +++ b/frontend/src/services/socketService.js @@ -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); + } +}; + diff --git a/frontend/src/views/DiaryView.vue b/frontend/src/views/DiaryView.vue index d96c54b..641ad9b 100644 --- a/frontend/src/views/DiaryView.vue +++ b/frontend/src/views/DiaryView.vue @@ -96,6 +96,10 @@ + @@ -104,11 +108,11 @@
- +
- +
@@ -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(); diff --git a/frontend/src/views/TournamentsView.vue b/frontend/src/views/TournamentsView.vue index 3979af6..254691a 100644 --- a/frontend/src/views/TournamentsView.vue +++ b/frontend/src/views/TournamentsView.vue @@ -30,7 +30,7 @@