From c589c1160710dcf8726f95d99924a97b93c51f1a Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Thu, 13 Nov 2025 16:54:31 +0100 Subject: [PATCH] Add Socket.IO integration for real-time updates in diary features This commit introduces Socket.IO to the backend and frontend, enabling real-time communication for diary-related events. Key updates include the addition of socket event emissions for diary date updates, tag additions/removals, and activity member changes in the backend controllers. The frontend DiaryView component has been enhanced to connect to the socket server and handle incoming events, ensuring that users receive immediate feedback on changes. Additionally, new dependencies for Socket.IO have been added to both the backend and frontend package files, improving the overall interactivity and responsiveness of the application. --- backend/controllers/diaryController.js | 34 +- .../diaryDateActivityController.js | 69 ++++ .../diaryMemberActivityController.js | 27 ++ backend/controllers/diaryNoteController.js | 34 +- backend/controllers/participantController.js | 33 +- backend/node_modules/.package-lock.json | 216 +++++++++++ backend/package-lock.json | 219 ++++++++++- backend/package.json | 3 +- backend/server.js | 12 +- backend/services/socketService.js | 106 ++++++ frontend/package-lock.json | 138 ++++++- frontend/package.json | 1 + frontend/src/services/socketService.js | 225 ++++++++++++ frontend/src/views/DiaryView.vue | 339 +++++++++++++++--- 14 files changed, 1403 insertions(+), 53 deletions(-) create mode 100644 backend/services/socketService.js create mode 100644 frontend/src/services/socketService.js diff --git a/backend/controllers/diaryController.js b/backend/controllers/diaryController.js index 91bf905..e2f0587 100644 --- a/backend/controllers/diaryController.js +++ b/backend/controllers/diaryController.js @@ -1,7 +1,7 @@ import diaryService from '../services/diaryService.js'; import HttpError from '../exceptions/HttpError.js'; - import { devLog } from '../utils/logger.js'; +import { emitDiaryDateUpdated, emitDiaryTagAdded, emitDiaryTagRemoved } from '../services/socketService.js'; const getDatesForClub = async (req, res) => { try { const { clubId } = req.params; @@ -43,6 +43,10 @@ const updateTrainingTimes = async (req, res) => { throw new HttpError('notallfieldsfilled', 400); } const updatedDate = await diaryService.updateTrainingTimes(userToken, clubId, dateId, trainingStart, trainingEnd); + + // Emit Socket-Event + emitDiaryDateUpdated(clubId, dateId, { trainingStart, trainingEnd }); + res.status(200).json(updatedDate); } catch (error) { console.error('[updateTrainingTimes] - Error:', error); @@ -79,6 +83,14 @@ const addDiaryTag = async (req, res) => { const { authcode: userToken } = req.headers; const { diaryDateId, tagName } = req.body; const tags = await diaryService.addTagToDate(userToken, diaryDateId, tagName); + + // Hole clubId für Event + const { DiaryDate } = await import('../models/index.js'); + const diaryDate = await DiaryDate.findByPk(diaryDateId); + if (diaryDate?.clubId && tags && tags.length > 0) { + emitDiaryTagAdded(diaryDate.clubId, diaryDateId, tags[tags.length - 1]); + } + res.status(201).json(tags); } catch (error) { console.error('[addDiaryTag] - Error:', error); @@ -95,6 +107,12 @@ const addTagToDiaryDate = async (req, res) => { return res.status(400).json({ message: 'diaryDateId and tagId are required.' }); } const result = await diaryService.addTagToDiaryDate(userToken, clubId, diaryDateId, tagId); + + // Emit Socket-Event + if (result && result.tag) { + emitDiaryTagAdded(clubId, diaryDateId, result.tag); + } + res.status(200).json(result); } catch (error) { console.error('[addTagToDiaryDate] - Error:', error); @@ -106,8 +124,20 @@ const deleteTagFromDiaryDate = async (req, res) => { try { const { tagId } = req.query; const { authcode: userToken } = req.headers; - const { clubId } = req.params; + const { clubId } = req.params; + + // Hole diaryDateId vor dem Löschen + const { DiaryDateTag } = await import('../models/index.js'); + const diaryDateTag = await DiaryDateTag.findByPk(tagId); + const diaryDateId = diaryDateTag?.diaryDateId; + await diaryService.removeTagFromDiaryDate(userToken, clubId, tagId); + + // Emit Socket-Event + if (diaryDateId) { + emitDiaryTagRemoved(clubId, diaryDateId, tagId); + } + res.status(200).json({ message: 'Tag deleted' }); } catch (error) { console.error('[deleteTag] - Error:', error); diff --git a/backend/controllers/diaryDateActivityController.js b/backend/controllers/diaryDateActivityController.js index d617b5d..9e34b19 100644 --- a/backend/controllers/diaryDateActivityController.js +++ b/backend/controllers/diaryDateActivityController.js @@ -1,4 +1,6 @@ import diaryDateActivityService from '../services/diaryDateActivityService.js'; +import { emitActivityChanged } from '../services/socketService.js'; +import DiaryDate from '../models/DiaryDates.js'; import { devLog } from '../utils/logger.js'; export const createDiaryDateActivity = async (req, res) => { @@ -14,6 +16,13 @@ export const createDiaryDateActivity = async (req, res) => { orderId, isTimeblock, }); + + // Emit Socket-Event + const diaryDate = await DiaryDate.findByPk(diaryDateId); + if (diaryDate?.clubId) { + emitActivityChanged(diaryDate.clubId, diaryDateId); + } + res.status(201).json(activityItem); } catch (error) { devLog(error); @@ -34,6 +43,15 @@ export const updateDiaryDateActivity = async (req, res) => { orderId, groupId, // Pass groupId to the service }); + + // Emit Socket-Event + if (updatedActivity?.diaryDateId) { + const diaryDate = await DiaryDate.findByPk(updatedActivity.diaryDateId); + if (diaryDate?.clubId) { + emitActivityChanged(diaryDate.clubId, updatedActivity.diaryDateId); + } + } + res.status(200).json(updatedActivity); } catch (error) { res.status(500).json({ error: 'Error updating activity' }); @@ -44,7 +62,22 @@ export const deleteDiaryDateActivity = async (req, res) => { try { const { authcode: userToken } = req.headers; const { clubId, id } = req.params; + + // Hole diaryDateId vor dem Löschen + const DiaryDateActivity = (await import('../models/DiaryDateActivity.js')).default; + const activity = await DiaryDateActivity.findByPk(id); + const diaryDateId = activity?.diaryDateId; + await diaryDateActivityService.deleteActivity(userToken, clubId, id); + + // Emit Socket-Event + if (diaryDateId) { + const diaryDate = await DiaryDate.findByPk(diaryDateId); + if (diaryDate?.clubId) { + emitActivityChanged(diaryDate.clubId, diaryDateId); + } + } + res.status(200).json({ message: 'Activity deleted' }); } catch (error) { res.status(500).json({ error: 'Error deleting activity' }); @@ -57,6 +90,15 @@ export const updateDiaryDateActivityOrder = async (req, res) => { const { clubId, id } = req.params; const { orderId } = req.body; const updatedActivity = await diaryDateActivityService.updateActivityOrder(userToken, clubId, id, orderId); + + // Emit Socket-Event + if (updatedActivity?.diaryDateId) { + const diaryDate = await DiaryDate.findByPk(updatedActivity.diaryDateId); + if (diaryDate?.clubId) { + emitActivityChanged(diaryDate.clubId, updatedActivity.diaryDateId); + } + } + res.status(200).json(updatedActivity); } catch (error) { devLog(error); @@ -81,6 +123,13 @@ export const addGroupActivity = async(req, res) => { const { authcode: userToken } = req.headers; const { clubId, diaryDateId, groupId, activity, timeblockId } = req.body; const activityItem = await diaryDateActivityService.addGroupActivity(userToken, clubId, diaryDateId, groupId, activity, timeblockId); + + // Emit Socket-Event + const diaryDate = await DiaryDate.findByPk(diaryDateId); + if (diaryDate?.clubId) { + emitActivityChanged(diaryDate.clubId, diaryDateId); + } + res.status(201).json(activityItem); } catch (error) { devLog(error); @@ -92,7 +141,27 @@ export const deleteGroupActivity = async(req, res) => { try { const { authcode: userToken } = req.headers; const { clubId, groupActivityId } = req.params; + + // Hole diaryDateId vor dem Löschen + const GroupActivity = (await import('../models/GroupActivity.js')).default; + const DiaryDateActivity = (await import('../models/DiaryDateActivity.js')).default; + const groupActivity = await GroupActivity.findByPk(groupActivityId); + let diaryDateId = null; + if (groupActivity?.diaryDateActivity) { + const activity = await DiaryDateActivity.findByPk(groupActivity.diaryDateActivity); + diaryDateId = activity?.diaryDateId; + } + await diaryDateActivityService.deleteGroupActivity(userToken, clubId, groupActivityId); + + // Emit Socket-Event + if (diaryDateId) { + const diaryDate = await DiaryDate.findByPk(diaryDateId); + if (diaryDate?.clubId) { + emitActivityChanged(diaryDate.clubId, diaryDateId); + } + } + res.status(200).json({ message: 'Group activity deleted' }); } catch (error) { devLog(error); diff --git a/backend/controllers/diaryMemberActivityController.js b/backend/controllers/diaryMemberActivityController.js index 7f96a50..efc238e 100644 --- a/backend/controllers/diaryMemberActivityController.js +++ b/backend/controllers/diaryMemberActivityController.js @@ -1,6 +1,9 @@ import DiaryMemberActivity from '../models/DiaryMemberActivity.js'; +import DiaryDateActivity from '../models/DiaryDateActivity.js'; +import DiaryDates from '../models/DiaryDates.js'; import Participant from '../models/Participant.js'; import { checkAccess } from '../utils/userUtils.js'; +import { emitActivityMemberAdded, emitActivityMemberRemoved } from '../services/socketService.js'; export const getMembersForActivity = async (req, res) => { try { @@ -31,6 +34,13 @@ export const addMembersToActivity = async (req, res) => { const validIds = new Set(validParticipants.map(p => p.id)); const created = []; + + // Hole clubId und dateId für Events (falls nicht aus params verfügbar) + const activity = await DiaryDateActivity.findByPk(diaryDateActivityId); + const diaryDate = activity ? await DiaryDates.findByPk(activity.diaryDateId) : null; + const eventClubId = diaryDate?.clubId || clubId; + const dateId = diaryDate?.id || null; + for (const pid of participantIds) { if (!validIds.has(pid)) { continue; @@ -39,6 +49,11 @@ export const addMembersToActivity = async (req, res) => { if (!existing) { const rec = await DiaryMemberActivity.create({ diaryDateActivityId, participantId: pid }); created.push(rec); + + // Emit Socket-Event + if (eventClubId && dateId) { + emitActivityMemberAdded(eventClubId, diaryDateActivityId, pid, dateId); + } } else { } } @@ -54,7 +69,19 @@ export const removeMemberFromActivity = async (req, res) => { const { authcode: userToken } = req.headers; const { clubId, diaryDateActivityId, participantId } = req.params; await checkAccess(userToken, clubId); + + // Hole dateId für Event + const activity = await DiaryDateActivity.findByPk(diaryDateActivityId); + const diaryDate = activity ? await DiaryDates.findByPk(activity.diaryDateId) : null; + const dateId = diaryDate?.id || null; + await DiaryMemberActivity.destroy({ where: { diaryDateActivityId, participantId } }); + + // Emit Socket-Event + if (dateId) { + emitActivityMemberRemoved(clubId, diaryDateActivityId, participantId, dateId); + } + res.status(200).json({ ok: true }); } catch (e) { res.status(500).json({ error: 'Error removing member from activity' }); diff --git a/backend/controllers/diaryNoteController.js b/backend/controllers/diaryNoteController.js index 1e7dec1..e91d28f 100644 --- a/backend/controllers/diaryNoteController.js +++ b/backend/controllers/diaryNoteController.js @@ -1,5 +1,6 @@ -import { DiaryNote, DiaryTag } from '../models/index.js'; +import { DiaryNote, DiaryTag, DiaryDate } from '../models/index.js'; import diaryService from '../services/diaryService.js'; +import { emitDiaryNoteAdded, emitDiaryNoteDeleted } from '../services/socketService.js'; export const getNotes = async (req, res) => { try { @@ -26,6 +27,9 @@ export const createNote = async (req, res) => { const newNote = await DiaryNote.create({ memberId, diaryDateId, content }); + // Hole DiaryDate für clubId + const diaryDate = await DiaryDate.findByPk(diaryDateId); + if (Array.isArray(tags) && tags.length > 0 && typeof newNote.addTags === 'function') { const tagInstances = await DiaryTag.findAll({ where: { id: tags } }); await newNote.addTags(tagInstances); @@ -34,9 +38,19 @@ export const createNote = async (req, res) => { include: [{ model: DiaryTag, as: 'tags', required: false }], }); + // Emit Socket-Event + if (diaryDate?.clubId) { + emitDiaryNoteAdded(diaryDate.clubId, diaryDateId, noteWithTags ?? newNote); + } + return res.status(201).json(noteWithTags ?? newNote); } + // Emit Socket-Event + if (diaryDate?.clubId) { + emitDiaryNoteAdded(diaryDate.clubId, diaryDateId, newNote); + } + res.status(201).json(newNote); } catch (error) { console.error('[createNote] - Error:', error); @@ -47,7 +61,25 @@ export const createNote = async (req, res) => { export const deleteNote = async (req, res) => { try { const { noteId } = req.params; + + // Hole Note für diaryDateId vor dem Löschen + const note = await DiaryNote.findByPk(noteId); + const diaryDateId = note?.diaryDateId; + + // Hole DiaryDate für clubId + let clubId = null; + if (diaryDateId) { + const diaryDate = await DiaryDate.findByPk(diaryDateId); + clubId = diaryDate?.clubId; + } + await DiaryNote.destroy({ where: { id: noteId } }); + + // Emit Socket-Event + if (clubId && diaryDateId) { + emitDiaryNoteDeleted(clubId, diaryDateId, noteId); + } + res.status(200).json({ message: 'Note deleted' }); } catch (error) { res.status(500).json({ error: 'Error deleting note' }); diff --git a/backend/controllers/participantController.js b/backend/controllers/participantController.js index 4787477..0469a78 100644 --- a/backend/controllers/participantController.js +++ b/backend/controllers/participantController.js @@ -1,6 +1,7 @@ import Participant from '../models/Participant.js'; - +import DiaryDates from '../models/DiaryDates.js'; import { devLog } from '../utils/logger.js'; +import { emitParticipantAdded, emitParticipantRemoved, emitParticipantUpdated } from '../services/socketService.js'; export const getParticipants = async (req, res) => { try { const { dateId } = req.params; @@ -24,7 +25,12 @@ export const updateParticipantGroup = async (req, res) => { where: { diaryDateId: dateId, memberId: memberId - } + }, + include: [{ + model: DiaryDates, + as: 'diaryDate', + attributes: ['clubId'] + }] }); if (!participant) { @@ -34,6 +40,11 @@ export const updateParticipantGroup = async (req, res) => { participant.groupId = groupId || null; await participant.save(); + // Emit Socket-Event + if (participant.diaryDate?.clubId) { + emitParticipantUpdated(participant.diaryDate.clubId, dateId, participant); + } + res.status(200).json(participant); } catch (error) { devLog(error); @@ -45,6 +56,13 @@ export const addParticipant = async (req, res) => { try { const { diaryDateId, memberId } = req.body; const participant = await Participant.create({ diaryDateId, memberId }); + + // Hole DiaryDate für clubId + const diaryDate = await DiaryDates.findByPk(diaryDateId); + if (diaryDate?.clubId) { + emitParticipantAdded(diaryDate.clubId, diaryDateId, participant); + } + res.status(201).json(participant); } catch (error) { devLog(error); @@ -55,7 +73,18 @@ export const addParticipant = async (req, res) => { export const removeParticipant = async (req, res) => { try { const { diaryDateId, memberId } = req.body; + + // Hole DiaryDate für clubId vor dem Löschen + const diaryDate = await DiaryDates.findByPk(diaryDateId); + const clubId = diaryDate?.clubId; + await Participant.destroy({ where: { diaryDateId, memberId } }); + + // Emit Socket-Event + if (clubId) { + emitParticipantRemoved(clubId, diaryDateId, memberId); + } + res.status(200).json({ message: 'Teilnehmer entfernt' }); } catch (error) { devLog(error); diff --git a/backend/node_modules/.package-lock.json b/backend/node_modules/.package-lock.json index da95e02..6c37409 100644 --- a/backend/node_modules/.package-lock.json +++ b/backend/node_modules/.package-lock.json @@ -432,6 +432,12 @@ "linux" ] }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, "node_modules/@standard-schema/spec": { "version": "1.0.0", "dev": true, @@ -455,6 +461,15 @@ "assertion-error": "^2.0.1" } }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/debug": { "version": "4.1.12", "license": "MIT", @@ -807,6 +822,15 @@ ], "license": "MIT" }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "license": "MIT", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, "node_modules/bcrypt": { "version": "5.1.1", "hasInstallScript": true, @@ -1490,6 +1514,67 @@ "once": "^1.4.0" } }, + "node_modules/engine.io": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz", + "integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==", + "license": "MIT", + "dependencies": { + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/env-paths": { "version": "2.2.1", "dev": true, @@ -4305,6 +4390,116 @@ "npm": ">= 3.0.0" } }, + "node_modules/socket.io": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", + "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", + "license": "MIT", + "dependencies": { + "debug": "~4.3.4", + "ws": "~8.17.1" + } + }, + "node_modules/socket.io-adapter/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-adapter/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/socket.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/socks": { "version": "2.8.7", "dev": true, @@ -5191,6 +5386,27 @@ "version": "1.0.2", "license": "ISC" }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/xtend": { "version": "4.0.2", "license": "MIT", diff --git a/backend/package-lock.json b/backend/package-lock.json index 4741be0..96ba2d6 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -28,7 +28,8 @@ "pdf-parse": "^1.1.1", "pdfjs-dist": "^5.4.394", "sequelize": "^6.37.3", - "sharp": "^0.33.5" + "sharp": "^0.33.5", + "socket.io": "^4.8.1" }, "devDependencies": { "cross-env": "^7.0.3", @@ -467,6 +468,12 @@ "linux" ] }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, "node_modules/@standard-schema/spec": { "version": "1.0.0", "dev": true, @@ -490,6 +497,15 @@ "assertion-error": "^2.0.1" } }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/debug": { "version": "4.1.12", "license": "MIT", @@ -842,6 +858,15 @@ ], "license": "MIT" }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "license": "MIT", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, "node_modules/bcrypt": { "version": "5.1.1", "hasInstallScript": true, @@ -1525,6 +1550,67 @@ "once": "^1.4.0" } }, + "node_modules/engine.io": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz", + "integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==", + "license": "MIT", + "dependencies": { + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/env-paths": { "version": "2.2.1", "dev": true, @@ -4340,6 +4426,116 @@ "npm": ">= 3.0.0" } }, + "node_modules/socket.io": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", + "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", + "license": "MIT", + "dependencies": { + "debug": "~4.3.4", + "ws": "~8.17.1" + } + }, + "node_modules/socket.io-adapter/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-adapter/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/socket.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/socks": { "version": "2.8.7", "dev": true, @@ -5226,6 +5422,27 @@ "version": "1.0.2", "license": "ISC" }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/xtend": { "version": "4.0.2", "license": "MIT", diff --git a/backend/package.json b/backend/package.json index ddbfa92..a84edfa 100644 --- a/backend/package.json +++ b/backend/package.json @@ -33,7 +33,8 @@ "pdf-parse": "^1.1.1", "pdfjs-dist": "^5.4.394", "sequelize": "^6.37.3", - "sharp": "^0.33.5" + "sharp": "^0.33.5", + "socket.io": "^4.8.1" }, "devDependencies": { "cross-env": "^7.0.3", diff --git a/backend/server.js b/backend/server.js index d265a5f..5025dbb 100644 --- a/backend/server.js +++ b/backend/server.js @@ -1,8 +1,10 @@ import express from 'express'; import path from 'path'; import { fileURLToPath } from 'url'; +import { createServer } from 'http'; import sequelize from './database.js'; import cors from 'cors'; +import { initializeSocketIO } from './services/socketService.js'; import { User, Log, Club, UserClub, Member, DiaryDate, Participant, Activity, MemberNote, DiaryNote, DiaryTag, MemberDiaryTag, DiaryDateTag, DiaryMemberNote, DiaryMemberTag, @@ -256,7 +258,15 @@ app.get('*', (req, res) => { // Start scheduler service schedulerService.start(); - app.listen(port); + // Erstelle HTTP-Server für Socket.IO + const httpServer = createServer(app); + + // Initialisiere Socket.IO + initializeSocketIO(httpServer); + + httpServer.listen(port, () => { + console.log(`🚀 Server läuft auf Port ${port}`); + }); } catch (err) { console.error('Unable to synchronize the database:', err); } diff --git a/backend/services/socketService.js b/backend/services/socketService.js new file mode 100644 index 0000000..7edfce6 --- /dev/null +++ b/backend/services/socketService.js @@ -0,0 +1,106 @@ +import { Server } from 'socket.io'; + +let io = null; + +export const initializeSocketIO = (httpServer) => { + io = new Server(httpServer, { + cors: { + origin: true, + credentials: true, + methods: ['GET', 'POST'] + } + }); + + io.on('connection', (socket) => { + console.log('🔌 Socket verbunden:', socket.id); + + // Client tritt einem Club-Raum bei + socket.on('join-club', (clubId) => { + const room = `club-${clubId}`; + socket.join(room); + console.log(`👤 Socket ${socket.id} ist Club ${clubId} beigetreten`); + }); + + // Client verlässt einen Club-Raum + socket.on('leave-club', (clubId) => { + const room = `club-${clubId}`; + socket.leave(room); + console.log(`👤 Socket ${socket.id} hat Club ${clubId} verlassen`); + }); + + socket.on('disconnect', () => { + console.log('🔌 Socket getrennt:', socket.id); + }); + }); + + return io; +}; + +export const getIO = () => { + if (!io) { + throw new Error('Socket.IO wurde noch nicht initialisiert. Rufe zuerst initializeSocketIO() auf.'); + } + return io; +}; + +// Helper-Funktionen zum Emittieren von Events +export const emitToClub = (clubId, event, data) => { + if (!io) { + console.warn('⚠️ [Socket] emitToClub: io nicht initialisiert'); + return; + } + const room = `club-${clubId}`; + console.log(`📡 [Socket] Emit ${event} an Raum ${room}:`, data); + io.to(room).emit(event, data); +}; + +// Events für Diary-Änderungen +export const emitParticipantAdded = (clubId, dateId, participant) => { + emitToClub(clubId, 'participant:added', { dateId, participant }); +}; + +export const emitParticipantRemoved = (clubId, dateId, participantId) => { + emitToClub(clubId, 'participant:removed', { dateId, participantId }); +}; + +export const emitParticipantUpdated = (clubId, dateId, participant) => { + emitToClub(clubId, 'participant:updated', { dateId, participant }); +}; + +export const emitDiaryNoteAdded = (clubId, dateId, note) => { + emitToClub(clubId, 'diary:note:added', { dateId, note }); +}; + +export const emitDiaryNoteUpdated = (clubId, dateId, note) => { + emitToClub(clubId, 'diary:note:updated', { dateId, note }); +}; + +export const emitDiaryNoteDeleted = (clubId, dateId, noteId) => { + emitToClub(clubId, 'diary:note:deleted', { dateId, noteId }); +}; + +export const emitDiaryTagAdded = (clubId, dateId, tag) => { + emitToClub(clubId, 'diary:tag:added', { dateId, tag }); +}; + +export const emitDiaryTagRemoved = (clubId, dateId, tagId) => { + emitToClub(clubId, 'diary:tag:removed', { dateId, tagId }); +}; + +export const emitDiaryDateUpdated = (clubId, dateId, updates) => { + emitToClub(clubId, 'diary:date:updated', { dateId, updates }); +}; + +export const emitActivityMemberAdded = (clubId, activityId, participantId, dateId) => { + emitToClub(clubId, 'activity:member:added', { activityId, participantId, dateId }); +}; + +export const emitActivityMemberRemoved = (clubId, activityId, participantId, dateId) => { + emitToClub(clubId, 'activity:member:removed', { activityId, participantId, dateId }); +}; + +// Event für Aktivitäts-Änderungen (erstellen, aktualisieren, löschen, Reihenfolge ändern) +export const emitActivityChanged = (clubId, dateId) => { + emitToClub(clubId, 'activity:changed', { dateId }); +}; + diff --git a/frontend/package-lock.json b/frontend/package-lock.json index a0edde5..dff3e0f 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -15,6 +15,7 @@ "jspdf": "^2.5.2", "jspdf-autotable": "^5.0.2", "node-cron": "^4.2.1", + "socket.io-client": "^4.8.1", "sortablejs": "^1.15.3", "vue": "^3.2.13", "vue-multiselect": "^3.0.0", @@ -924,6 +925,12 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, "node_modules/@types/raf": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz", @@ -1500,6 +1507,45 @@ "dev": true, "license": "MIT" }, + "node_modules/engine.io-client": { + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz", + "integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1", + "xmlhttprequest-ssl": "~2.1.1" + } + }, + "node_modules/engine.io-client/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/enquirer": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", @@ -2510,7 +2556,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/nanoid": { @@ -2965,6 +3010,68 @@ "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, + "node_modules/socket.io-client": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz", + "integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.2", + "engine.io-client": "~6.6.1", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-client/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/sortablejs": { "version": "1.15.6", "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.15.6.tgz", @@ -3433,6 +3540,35 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true, "license": "ISC" + }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", + "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", + "engines": { + "node": ">=0.4.0" + } } } } diff --git a/frontend/package.json b/frontend/package.json index df7254a..695d747 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -15,6 +15,7 @@ "jspdf": "^2.5.2", "jspdf-autotable": "^5.0.2", "node-cron": "^4.2.1", + "socket.io-client": "^4.8.1", "sortablejs": "^1.15.3", "vue": "^3.2.13", "vue-multiselect": "^3.0.0", diff --git a/frontend/src/services/socketService.js b/frontend/src/services/socketService.js new file mode 100644 index 0000000..ae1571c --- /dev/null +++ b/frontend/src/services/socketService.js @@ -0,0 +1,225 @@ +import { io } from 'socket.io-client'; +import { backendBaseUrl } from '../apiClient.js'; + +let socket = null; + +export const connectSocket = (clubId) => { + if (socket && socket.connected) { + // Wenn bereits verbunden, verlasse den alten Club-Raum und trete dem neuen bei + if (socket.currentClubId) { + socket.emit('leave-club', socket.currentClubId); + } + } else { + // Neue Verbindung erstellen + // Verwende backendBaseUrl direkt, Socket.IO erkennt automatisch den Port + socket = io(backendBaseUrl, { + transports: ['websocket', 'polling'], + reconnection: true, + reconnectionDelay: 1000, + reconnectionAttempts: 5 + }); + + 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.on('connect_error', (error) => { + console.error('❌ Socket.IO Verbindungsfehler:', error); + }); + } + + // Club-Raum beitreten + if (clubId) { + socket.emit('join-club', clubId); + socket.currentClubId = clubId; + console.log(`👤 Club ${clubId} beigetreten`); + } + + return socket; +}; + +export const disconnectSocket = () => { + if (socket) { + if (socket.currentClubId) { + socket.emit('leave-club', socket.currentClubId); + } + socket.disconnect(); + socket = null; + console.log('🔌 Socket.IO getrennt'); + } +}; + +export const getSocket = () => { + return socket; +}; + +// Event-Listener registrieren +export const onParticipantAdded = (callback) => { + if (socket) { + socket.on('participant:added', callback); + } +}; + +export const onParticipantRemoved = (callback) => { + if (socket) { + socket.on('participant:removed', callback); + } +}; + +export const onParticipantUpdated = (callback) => { + if (socket) { + socket.on('participant:updated', callback); + } +}; + +export const onDiaryNoteAdded = (callback) => { + if (socket) { + socket.on('diary:note:added', callback); + } +}; + +export const onDiaryNoteUpdated = (callback) => { + if (socket) { + socket.on('diary:note:updated', callback); + } +}; + +export const onDiaryNoteDeleted = (callback) => { + if (socket) { + socket.on('diary:note:deleted', callback); + } +}; + +export const onDiaryTagAdded = (callback) => { + if (socket) { + socket.on('diary:tag:added', callback); + } +}; + +export const onDiaryTagRemoved = (callback) => { + if (socket) { + socket.on('diary:tag:removed', callback); + } +}; + +export const onDiaryDateUpdated = (callback) => { + if (socket) { + socket.on('diary:date:updated', 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'); + } +}; + +// Event-Listener entfernen +export const offParticipantAdded = (callback) => { + if (socket) { + socket.off('participant:added', callback); + } +}; + +export const offParticipantRemoved = (callback) => { + if (socket) { + socket.off('participant:removed', callback); + } +}; + +export const offParticipantUpdated = (callback) => { + if (socket) { + socket.off('participant:updated', callback); + } +}; + +export const offDiaryNoteAdded = (callback) => { + if (socket) { + socket.off('diary:note:added', callback); + } +}; + +export const offDiaryNoteUpdated = (callback) => { + if (socket) { + socket.off('diary:note:updated', callback); + } +}; + +export const offDiaryNoteDeleted = (callback) => { + if (socket) { + socket.off('diary:note:deleted', callback); + } +}; + +export const offDiaryTagAdded = (callback) => { + if (socket) { + socket.off('diary:tag:added', callback); + } +}; + +export const offDiaryTagRemoved = (callback) => { + if (socket) { + socket.off('diary:tag:removed', callback); + } +}; + +export const offDiaryDateUpdated = (callback) => { + if (socket) { + socket.off('diary:date:updated', callback); + } +}; + +export const offActivityMemberAdded = (callback) => { + if (socket) { + socket.off('activity:member:added', callback); + } +}; + +export const offActivityMemberRemoved = (callback) => { + if (socket) { + socket.off('activity:member:removed', callback); + } +}; + +export const offActivityChanged = (callback) => { + if (socket) { + socket.off('activity:changed', callback); + } +}; + diff --git a/frontend/src/views/DiaryView.vue b/frontend/src/views/DiaryView.vue index 2977401..c1f8427 100644 --- a/frontend/src/views/DiaryView.vue +++ b/frontend/src/views/DiaryView.vue @@ -110,23 +110,25 @@ Zeitblock
- + style="display: flex; gap: 5px; align-items: center;"> +
+ + +
-
{{ group.name }} - -