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.
This commit is contained in:
Torsten Schulz (local)
2025-11-13 16:54:31 +01:00
parent 0caa31e3eb
commit c589c11607
14 changed files with 1403 additions and 53 deletions

View File

@@ -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);

View File

@@ -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);

View File

@@ -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' });

View File

@@ -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' });

View File

@@ -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);

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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);
}

View File

@@ -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 });
};

View File

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

View File

@@ -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",

View File

@@ -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);
}
};

View File

@@ -110,23 +110,25 @@
<span v-if="item.isTimeblock"><i>Zeitblock</i></span>
<span v-else-if="editingActivityId === item.id">
<div
style="display: flex; gap: 5px; align-items: center; position: relative;">
<input type="text" v-model="editingActivityText"
@input="onEditInputChangeText(item)"
@keyup.enter="saveActivityEdit(item)"
@keyup.esc="cancelActivityEdit" ref="activityInput"
style="flex: 1;" />
style="display: flex; gap: 5px; align-items: center;">
<div style="flex: 1; position: relative;">
<input type="text" v-model="editingActivityText"
@input="onEditInputChangeText(item)"
@keyup.enter="saveActivityEdit(item)"
@keyup.esc="cancelActivityEdit" ref="activityInput"
style="width: 100%;" />
<div v-if="editShowDropdown && editSearchForId === item.id && editSearchResults.length"
class="dropdown" style="max-height: 9.5em;">
<div v-for="s in editSearchResults" :key="s.id"
@click="chooseEditSuggestion(s, item)">
<span v-if="s.code && s.code.trim() !== ''"><strong>[{{ s.code }}]</strong> </span>{{ s.name }}
</div>
</div>
</div>
<button @click="saveActivityEdit(item)" class="btn-primary"
style="padding: 2px 8px; font-size: 12px;"></button>
<button @click="cancelActivityEdit" class="btn-secondary"
style="padding: 2px 8px; font-size: 12px;"></button>
<div v-if="editShowDropdown && editSearchForId === item.id && editSearchResults.length"
class="dropdown" style="max-height: 9.5em;">
<div v-for="s in editSearchResults" :key="s.id"
@click="chooseEditSuggestion(s, item)">
<span v-if="s.code && s.code.trim() !== ''"><strong>[{{ s.code }}]</strong> </span>{{ s.name }}
</div>
</div>
</div>
</span>
<span v-else @click="startActivityEdit(item)"
@@ -209,14 +211,16 @@
<option v-for="group in groups" :key="group.id" :value="String(group.id)">
{{ group.name }}</option>
</select>
<input type="text" v-model="newPlanItem.activity" placeholder="Aktivität"
@input="onNewItemInputChange" style="flex: 1;" />
<div v-if="newItemShowDropdown && newItemSearchResults.length"
class="dropdown" style="max-height: 9.5em;">
<div v-for="s in newItemSearchResults" :key="s.id"
@click="chooseNewItemSuggestion(s)">
<span v-if="s.code && s.code.trim() !== ''"><strong>[{{ s.code
}}]</strong> </span>{{ s.name }}
<div style="flex: 1; position: relative;">
<input type="text" v-model="newPlanItem.activity" placeholder="Aktivität"
@input="onNewItemInputChange" style="width: 100%;" />
<div v-if="newItemShowDropdown && newItemSearchResults.length"
class="dropdown" style="max-height: 9.5em;">
<div v-for="s in newItemSearchResults" :key="s.id"
@click="chooseNewItemSuggestion(s)">
<span v-if="s.code && s.code.trim() !== ''"><strong>[{{ s.code
}}]</strong> </span>{{ s.name }}
</div>
</div>
</div>
</div>
@@ -295,7 +299,7 @@
@click="addGroupActivity">Gruppen-Aktivität</button>
</td>
<td v-if="addNewItem || addNewGroupActivity">
<div v-if="addtype === 'activity'" style="position: relative; display: flex; align-items: center; gap: 0.5rem;">
<div v-if="addtype === 'activity'" style="display: flex; align-items: center; gap: 0.5rem;">
<button
type="button"
class="btn-palette"
@@ -305,16 +309,18 @@
>
🎨
</button>
<input type="text" v-model="newPlanItem.activity"
placeholder="Aktivität / Zeitblock" required
@input="onNewItemInputChange"
style="flex: 1;" />
<div v-if="newItemShowDropdown && newItemSearchResults.length"
class="dropdown" style="max-height: 9.5em;">
<div v-for="s in newItemSearchResults" :key="s.id"
@click="chooseNewItemSuggestion(s)">
<span v-if="s.code && s.code.trim() !== ''"><strong>[{{ s.code
}}]</strong> </span>{{ s.name }}
<div style="flex: 1; position: relative;">
<input type="text" v-model="newPlanItem.activity"
placeholder="Aktivität / Zeitblock" required
@input="onNewItemInputChange"
style="width: 100%;" />
<div v-if="newItemShowDropdown && newItemSearchResults.length"
class="dropdown" style="max-height: 9.5em;">
<div v-for="s in newItemSearchResults" :key="s.id"
@click="chooseNewItemSuggestion(s)">
<span v-if="s.code && s.code.trim() !== ''"><strong>[{{ s.code
}}]</strong> </span>{{ s.name }}
</div>
</div>
</div>
</div>
@@ -327,12 +333,16 @@
<option v-for="group in groups" :key="group.id" :value="String(group.id)">
{{ group.name }}</option>
</select>
<div v-if="newItemShowDropdown && newItemSearchResults.length"
class="dropdown" style="max-height: 9.5em;">
<div v-for="s in newItemSearchResults" :key="s.id"
@click="chooseNewItemSuggestion(s)">
<span v-if="s.code && s.code.trim() !== ''"><strong>[{{ s.code
}}]</strong> </span>{{ s.name }}
<div style="flex: 1; position: relative;">
<input type="text" v-model="newPlanItem.activity" placeholder="Aktivität"
@input="onNewItemInputChange" style="width: 100%;" />
<div v-if="newItemShowDropdown && newItemSearchResults.length"
class="dropdown" style="max-height: 9.5em;">
<div v-for="s in newItemSearchResults" :key="s.id"
@click="chooseNewItemSuggestion(s)">
<span v-if="s.code && s.code.trim() !== ''"><strong>[{{ s.code
}}]</strong> </span>{{ s.name }}
</div>
</div>
</div>
</div>
@@ -555,6 +565,32 @@ import MemberActivityStatsDialog from '../components/MemberActivityStatsDialog.v
import AccidentFormDialog from '../components/AccidentFormDialog.vue';
import QuickAddMemberDialog from '../components/QuickAddMemberDialog.vue';
import MemberGalleryDialog from '../components/MemberGalleryDialog.vue';
import {
connectSocket,
disconnectSocket,
onParticipantAdded,
onParticipantRemoved,
onParticipantUpdated,
onDiaryNoteAdded,
onDiaryNoteDeleted,
onDiaryTagAdded,
onDiaryTagRemoved,
onDiaryDateUpdated,
onActivityMemberAdded,
onActivityMemberRemoved,
onActivityChanged,
offParticipantAdded,
offParticipantRemoved,
offParticipantUpdated,
offDiaryNoteAdded,
offDiaryNoteDeleted,
offDiaryTagAdded,
offDiaryTagRemoved,
offDiaryDateUpdated,
offActivityMemberAdded,
offActivityMemberRemoved,
offActivityChanged
} from '../services/socketService.js';
export default {
name: 'DiaryView',
@@ -698,6 +734,14 @@ export default {
selectedActivityTags(newTags) {
this.updateActivityTags(newTags);
},
currentClub(newClubId, oldClubId) {
// Wenn Club wechselt, Socket neu verbinden
if (newClubId && newClubId !== oldClubId) {
this.removeSocketListeners();
connectSocket(newClubId);
this.setupSocketListeners();
}
}
},
computed: {
...mapGetters(['isAuthenticated', 'currentClub', 'currentClubName']),
@@ -1992,9 +2036,7 @@ export default {
// Suche nach existierender Aktivität mit diesem Code
const searchResults = await this.searchPredefinedActivities(code);
const existing = searchResults.find(a =>
a.code && a.code.trim().toLowerCase() === code.toLowerCase()
);
const existing = searchResults.find(a => a.code && a.code.trim().toLowerCase() === code.toLowerCase());
let activityToUse;
@@ -2004,9 +2046,9 @@ export default {
} else {
// Erstelle neue PredefinedActivity
const newActivity = {
name: result.name || result.fields?.name || '',
name: result.name || (result.fields && result.fields.name) || '',
code: code,
description: result.description || result.fields?.description || '',
description: result.description || (result.fields && result.fields.description) || '',
drawingData: result.drawingData || null
};
@@ -2389,9 +2431,209 @@ export default {
this.showInfo('Fehler', 'Fehler beim Erstellen des Mitglieds', getSafeErrorMessage(error), 'error');
}
},
// Socket.IO Event-Handler
setupSocketListeners() {
// Event-Handler für Teilnehmer-Änderungen
onParticipantAdded(this.handleParticipantAdded);
onParticipantRemoved(this.handleParticipantRemoved);
onParticipantUpdated(this.handleParticipantUpdated);
// Event-Handler für Tagebuch-Änderungen
onDiaryNoteAdded(this.handleDiaryNoteAdded);
onDiaryNoteDeleted(this.handleDiaryNoteDeleted);
onDiaryTagAdded(this.handleDiaryTagAdded);
onDiaryTagRemoved(this.handleDiaryTagRemoved);
onDiaryDateUpdated(this.handleDiaryDateUpdated);
// Event-Handler für Aktivitäts-Änderungen
console.log('🔧 [DiaryView] Registriere Activity-Event-Handler');
onActivityMemberAdded(this.handleActivityMemberAdded);
onActivityMemberRemoved(this.handleActivityMemberRemoved);
onActivityChanged(this.handleActivityChanged);
console.log('✅ [DiaryView] Alle Event-Handler registriert');
},
removeSocketListeners() {
offParticipantAdded(this.handleParticipantAdded);
offParticipantRemoved(this.handleParticipantRemoved);
offParticipantUpdated(this.handleParticipantUpdated);
offDiaryNoteAdded(this.handleDiaryNoteAdded);
offDiaryNoteDeleted(this.handleDiaryNoteDeleted);
offDiaryTagAdded(this.handleDiaryTagAdded);
offDiaryTagRemoved(this.handleDiaryTagRemoved);
offDiaryDateUpdated(this.handleDiaryDateUpdated);
offActivityMemberAdded(this.handleActivityMemberAdded);
offActivityMemberRemoved(this.handleActivityMemberRemoved);
offActivityChanged(this.handleActivityChanged);
},
async handleParticipantAdded(data) {
// Nur aktualisieren, wenn das aktuelle Datum betroffen ist
if (this.date && this.date !== 'new' && this.date.id === data.dateId) {
console.log('📡 Teilnehmer hinzugefügt (Socket):', data);
// Lade Teilnehmer neu
await this.loadParticipants(data.dateId);
}
},
async handleParticipantRemoved(data) {
// Nur aktualisieren, wenn das aktuelle Datum betroffen ist
if (this.date && this.date !== 'new' && this.date.id === data.dateId) {
console.log('📡 Teilnehmer entfernt (Socket):', data);
// Entferne aus participants-Array
this.participants = this.participants.filter(memberId => memberId !== data.participantId);
// Entferne aus Maps
delete this.participantMapByMemberId[data.participantId];
delete this.memberGroupsMap[data.participantId];
}
},
async handleParticipantUpdated(data) {
// Nur aktualisieren, wenn das aktuelle Datum betroffen ist
if (this.date && this.date !== 'new' && this.date.id === data.dateId) {
console.log('📡 Teilnehmer aktualisiert (Socket):', data);
// Aktualisiere groupId in memberGroupsMap
const groupValue = (data.participant.groupId !== null && data.participant.groupId !== undefined)
? String(data.participant.groupId)
: '';
this.memberGroupsMap = {
...this.memberGroupsMap,
[data.participant.memberId]: groupValue
};
}
},
async handleDiaryNoteAdded(data) {
// Nur aktualisieren, wenn das aktuelle Datum betroffen ist
if (this.date && this.date !== 'new' && this.date.id === data.dateId) {
console.log('📡 Tagebuch-Notiz hinzugefügt (Socket):', data);
// Lade Notizen neu, falls das betroffene Mitglied ausgewählt ist
if (this.selectedMember && data.note.memberId === this.selectedMember.id) {
try {
const notesResponse = await apiClient.get(`/notes?diaryDateId=${this.date.id}&memberId=${this.selectedMember.id}`);
this.notes = notesResponse.data;
} catch (error) {
console.error('Fehler beim Laden der Notizen:', error);
}
}
}
},
async handleDiaryNoteDeleted(data) {
// Nur aktualisieren, wenn das aktuelle Datum betroffen ist
if (this.date && this.date !== 'new' && this.date.id === data.dateId) {
console.log('📡 Tagebuch-Notiz gelöscht (Socket):', data);
// Entferne Notiz aus notes-Array
this.notes = this.notes.filter(note => note.id !== data.noteId);
}
},
async handleDiaryTagAdded(data) {
// Nur aktualisieren, wenn das aktuelle Datum betroffen ist
if (this.date && this.date !== 'new' && this.date.id === data.dateId) {
console.log('📡 Tagebuch-Tag hinzugefügt (Socket):', data);
// Lade Tags neu
await this.loadTags();
// Aktualisiere selectedActivityTags
if (data.tag && !this.selectedActivityTags.find(t => t.id === data.tag.id)) {
this.selectedActivityTags.push({
id: data.tag.id,
name: data.tag.name
});
}
}
},
async handleDiaryTagRemoved(data) {
// Nur aktualisieren, wenn das aktuelle Datum betroffen ist
if (this.date && this.date !== 'new' && this.date.id === data.dateId) {
console.log('📡 Tagebuch-Tag entfernt (Socket):', data);
// Entferne Tag aus selectedActivityTags
this.selectedActivityTags = this.selectedActivityTags.filter(t => t.id !== data.tagId);
}
},
async handleDiaryDateUpdated(data) {
// Nur aktualisieren, wenn das aktuelle Datum betroffen ist
if (this.date && this.date !== 'new' && this.date.id === data.dateId) {
console.log('📡 Tagebuch-Datum aktualisiert (Socket):', data);
// Aktualisiere Trainingszeiten
if (data.updates.trainingStart !== undefined) {
this.trainingStart = data.updates.trainingStart;
}
if (data.updates.trainingEnd !== undefined) {
this.trainingEnd = data.updates.trainingEnd;
}
}
},
async handleActivityMemberAdded(data) {
console.log('📡 [DiaryView] handleActivityMemberAdded aufgerufen:', data);
console.log('📡 [DiaryView] Aktuelles Datum:', this.date?.id, 'Event dateId:', data.dateId);
// Nur aktualisieren, wenn das aktuelle Datum betroffen ist
if (this.date && this.date !== 'new' && this.date.id === data.dateId) {
console.log('✅ [DiaryView] Datum stimmt überein, lade Training Plan neu');
// Lade Training Plan neu
try {
this.trainingPlan = await apiClient.get(`/diary-date-activities/${this.currentClub}/${this.date.id}`).then(response => response.data);
this.calculateIntermediateTimes();
console.log('✅ [DiaryView] Training Plan neu geladen');
} catch (error) {
console.error('❌ [DiaryView] Fehler beim Neuladen des Trainingsplans:', error);
}
} else {
console.log('⚠️ [DiaryView] Datum stimmt nicht überein oder kein Datum ausgewählt');
}
},
async handleActivityMemberRemoved(data) {
console.log('📡 [DiaryView] handleActivityMemberRemoved aufgerufen:', data);
console.log('📡 [DiaryView] Aktuelles Datum:', this.date?.id, 'Event dateId:', data.dateId);
// Nur aktualisieren, wenn das aktuelle Datum betroffen ist
if (this.date && this.date !== 'new' && this.date.id === data.dateId) {
console.log('✅ [DiaryView] Datum stimmt überein, lade Training Plan neu');
// Lade Training Plan neu
try {
this.trainingPlan = await apiClient.get(`/diary-date-activities/${this.currentClub}/${this.date.id}`).then(response => response.data);
this.calculateIntermediateTimes();
console.log('✅ [DiaryView] Training Plan neu geladen');
} catch (error) {
console.error('❌ [DiaryView] Fehler beim Neuladen des Trainingsplans:', error);
}
} else {
console.log('⚠️ [DiaryView] Datum stimmt nicht überein oder kein Datum ausgewählt');
}
},
async handleActivityChanged(data) {
console.log('📡 [DiaryView] handleActivityChanged aufgerufen:', data);
console.log('📡 [DiaryView] Aktuelles Datum:', this.date?.id, 'Event dateId:', data.dateId);
// Nur aktualisieren, wenn das aktuelle Datum betroffen ist
if (this.date && this.date !== 'new' && this.date.id === data.dateId) {
console.log('✅ [DiaryView] Datum stimmt überein, lade Training Plan neu');
// Lade Training Plan neu
try {
this.trainingPlan = await apiClient.get(`/diary-date-activities/${this.currentClub}/${this.date.id}`).then(response => response.data);
this.calculateIntermediateTimes();
console.log('✅ [DiaryView] Training Plan neu geladen');
} catch (error) {
console.error('❌ [DiaryView] Fehler beim Neuladen des Trainingsplans:', error);
}
} else {
console.log('⚠️ [DiaryView] Datum stimmt nicht überein oder kein Datum ausgewählt');
}
},
},
async mounted() {
await this.init();
// Socket.IO verbinden
if (this.currentClub) {
connectSocket(this.currentClub);
this.setupSocketListeners();
}
// Versuche, Audio erst bei Nutzerinteraktion zu initialisieren (Autoplay-Policy)
const tryInit = () => {
if (!this.bellSound) this.bellSound = new Audio('/sound/bell-123742.mp3');
@@ -2405,6 +2647,12 @@ export default {
if (this.timeChecker) {
clearInterval(this.timeChecker);
}
// Socket.IO Event-Listener entfernen
this.removeSocketListeners();
// Socket.IO trennen
disconnectSocket();
}
};
</script>
@@ -2670,9 +2918,12 @@ input[type="number"] {
max-height: 200px;
overflow-y: auto;
position: absolute;
top: 100%;
left: 0;
margin-top: 2px;
background-color: white;
z-index: 9999;
width: calc(100% - 20px);
width: 100%;
box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.1);
max-width: 30em;
}