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:
138
frontend/package-lock.json
generated
138
frontend/package-lock.json
generated
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
225
frontend/src/services/socketService.js
Normal file
225
frontend/src/services/socketService.js
Normal 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);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user