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 }} - -