From 912bf88c3ff34d6152a56da550afee334a638682 Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Fri, 8 May 2026 11:58:57 +0200 Subject: [PATCH] feat(DiaryDateActivity, DiaryView): integrate groupId for enhanced activity management - Added `groupId` field to the DiaryDateActivity model to support group-specific activities. - Updated create and update methods in DiaryDateActivityService to handle groupId, ensuring proper validation and association with groups. - Enhanced DiaryView component to allow selection of groups for activities, improving user experience and activity organization. - Implemented filtering logic in DiaryView to display activities based on selected group, enhancing data clarity and usability. --- .../diaryDateActivityController.js | 8 +- ..._activities_and_migrate_group_activity.sql | 30 ++++ backend/models/DiaryDateActivity.js | 10 ++ backend/models/index.js | 3 + backend/services/diaryDateActivityService.js | 145 +++++++++++----- frontend/src/views/DiaryView.vue | 155 ++++++++++++++++-- 6 files changed, 287 insertions(+), 64 deletions(-) create mode 100644 backend/migrations/20260508_add_group_id_to_diary_date_activities_and_migrate_group_activity.sql diff --git a/backend/controllers/diaryDateActivityController.js b/backend/controllers/diaryDateActivityController.js index a8403aa7..250f7d51 100644 --- a/backend/controllers/diaryDateActivityController.js +++ b/backend/controllers/diaryDateActivityController.js @@ -7,7 +7,7 @@ export const createDiaryDateActivity = async (req, res) => { try { const { authcode: userToken } = req.headers; const { clubId } = req.params; - const { diaryDateId, activity, predefinedActivityId, duration, durationText, orderId, isTimeblock } = req.body; + const { diaryDateId, activity, predefinedActivityId, duration, durationText, orderId, isTimeblock, groupId } = req.body; const activityItem = await diaryDateActivityService.createActivity(userToken, clubId, { diaryDateId, activity, @@ -16,6 +16,7 @@ export const createDiaryDateActivity = async (req, res) => { durationText, orderId, isTimeblock, + groupId, }); // Emit Socket-Event @@ -152,7 +153,7 @@ export const updateGroupActivity = async(req, res) => { try { const { authcode: userToken } = req.headers; const { clubId, groupActivityId } = req.params; - const { predefinedActivityId, duration, durationText, orderId } = req.body; + const { predefinedActivityId, duration, durationText, orderId, groupId } = req.body; const activityItem = await diaryDateActivityService.updateGroupActivity( userToken, clubId, @@ -160,7 +161,8 @@ export const updateGroupActivity = async(req, res) => { predefinedActivityId, duration, durationText, - orderId + orderId, + groupId ); // Emit Socket-Event diff --git a/backend/migrations/20260508_add_group_id_to_diary_date_activities_and_migrate_group_activity.sql b/backend/migrations/20260508_add_group_id_to_diary_date_activities_and_migrate_group_activity.sql new file mode 100644 index 00000000..4f82d267 --- /dev/null +++ b/backend/migrations/20260508_add_group_id_to_diary_date_activities_and_migrate_group_activity.sql @@ -0,0 +1,30 @@ +ALTER TABLE `diary_date_activities` + ADD COLUMN `group_id` INT NULL AFTER `predefined_activity_id`, + ADD CONSTRAINT `fk_diary_date_activities_group_id` + FOREIGN KEY (`group_id`) REFERENCES `group`(`id`) + ON DELETE SET NULL + ON UPDATE CASCADE; + +INSERT INTO `diary_date_activities` ( + `diary_date_id`, + `is_timeblock`, + `predefined_activity_id`, + `group_id`, + `duration`, + `duration_text`, + `order_id`, + `created_at`, + `updated_at` +) +SELECT + d.`diary_date_id`, + 0 AS `is_timeblock`, + ga.`custom_activity` AS `predefined_activity_id`, + ga.`group_id`, + ga.`duration`, + ga.`duration_text`, + (d.`order_id` * 100) + COALESCE(ga.`order_id`, 1) AS `order_id`, + COALESCE(ga.`created_at`, NOW()) AS `created_at`, + COALESCE(ga.`updated_at`, NOW()) AS `updated_at` +FROM `group_activity` ga +INNER JOIN `diary_date_activities` d ON d.`id` = ga.`diary_date_activity`; diff --git a/backend/models/DiaryDateActivity.js b/backend/models/DiaryDateActivity.js index 7fa4f8d2..35c3fce8 100644 --- a/backend/models/DiaryDateActivity.js +++ b/backend/models/DiaryDateActivity.js @@ -2,6 +2,7 @@ import { DataTypes } from 'sequelize'; import sequelize from '../database.js'; import DiaryDate from './DiaryDates.js'; import PredefinedActivity from './PredefinedActivity.js'; +import Group from './Group.js'; const DiaryDateActivity = sequelize.define('DiaryDateActivity', { id: { @@ -31,6 +32,15 @@ const DiaryDateActivity = sequelize.define('DiaryDateActivity', { key: 'id', }, onDelete: 'SET NULL', + }, + groupId: { + type: DataTypes.INTEGER, + allowNull: true, + references: { + model: Group, + key: 'id' + }, + onDelete: 'SET NULL' }, duration: { type: DataTypes.INTEGER, diff --git a/backend/models/index.js b/backend/models/index.js index 024e9d3c..1723e7d1 100644 --- a/backend/models/index.js +++ b/backend/models/index.js @@ -219,6 +219,9 @@ GroupActivity.belongsTo(Group, { foreignKey: 'groupId', as: 'groupsGroupActivity GroupActivity.belongsTo(PredefinedActivity, { foreignKey: 'customActivity', as: 'groupPredefinedActivity' }); PredefinedActivity.hasMany(GroupActivity, { foreignKey: 'predefinedActivityId', as: 'groupPredefinedActivities' }); +DiaryDateActivity.belongsTo(Group, { foreignKey: 'groupId', as: 'planGroup' }); +Group.hasMany(DiaryDateActivity, { foreignKey: 'groupId', as: 'groupPlanItems' }); + DiaryTag.hasMany(DiaryDateTag, { foreignKey: 'tagId', as: 'diaryDateTags' }); DiaryDateTag.belongsTo(DiaryTag, { foreignKey: 'tagId', as: 'tag' }); diff --git a/backend/services/diaryDateActivityService.js b/backend/services/diaryDateActivityService.js index 00f50bd4..3751d1b5 100644 --- a/backend/services/diaryDateActivityService.js +++ b/backend/services/diaryDateActivityService.js @@ -12,6 +12,9 @@ class DiaryDateActivityService { async createActivity(userToken, clubId, data) { await checkAccess(userToken, clubId); const { activity, ...restData } = data; + const normalizedGroupId = data.groupId === '' || data.groupId === undefined || data.groupId === null + ? null + : parseInt(data.groupId, 10); // Versuche, die PredefinedActivity robust zu finden: // 1) per übergebener ID // 2) per Name ODER Code (das Feld "activity" kann Kürzel oder Name sein) @@ -52,9 +55,20 @@ class DiaryDateActivityService { } else if (typeof restData.duration === 'string') { restData.duration = parseInt(restData.duration); } + restData.groupId = Number.isFinite(normalizedGroupId) ? normalizedGroupId : null; + + if (restData.groupId) { + const group = await Group.findByPk(restData.groupId); + if (!group || Number(group.diaryDateId) !== Number(data.diaryDateId)) { + throw new Error('Group isn\'t related to date'); + } + } const maxOrderId = await DiaryDateActivity.max('orderId', { - where: { diaryDateId: data.diaryDateId } + where: { + diaryDateId: data.diaryDateId, + groupId: restData.groupId + } }); const newOrderId = maxOrderId !== null ? maxOrderId + 1 : 1; restData.orderId = newOrderId; @@ -92,6 +106,19 @@ class DiaryDateActivityService { delete data.customActivityName; } + if (data.groupId !== undefined) { + const normalizedGroupId = data.groupId === '' || data.groupId === null + ? null + : parseInt(data.groupId, 10); + data.groupId = Number.isFinite(normalizedGroupId) ? normalizedGroupId : null; + if (data.groupId) { + const group = await Group.findByPk(data.groupId); + if (!group || Number(group.diaryDateId) !== Number(activity.diaryDateId)) { + throw new Error('Group isn\'t related to date'); + } + } + } + return await activity.update(data); } @@ -112,12 +139,14 @@ class DiaryDateActivityService { throw new Error('Activity not found'); } const currentOrderId = activity.orderId; + const scopeGroupId = activity.groupId || null; if (newOrderId < currentOrderId) { await DiaryDateActivity.increment( { orderId: 1 }, { where: { diaryDateId: activity.diaryDateId, + groupId: scopeGroupId, orderId: { [Op.gte]: newOrderId, [Op.lt]: currentOrderId }, }, } @@ -128,6 +157,7 @@ class DiaryDateActivityService { { where: { diaryDateId: activity.diaryDateId, + groupId: scopeGroupId, orderId: { [Op.lte]: newOrderId, [Op.gt]: currentOrderId }, }, } @@ -155,6 +185,10 @@ class DiaryDateActivityService { } ] }, + { + model: Group, + as: 'planGroup' + }, { model: GroupActivity, as: 'groupActivities', @@ -314,42 +348,15 @@ class DiaryDateActivityService { async addGroupActivity(userToken, clubId, diaryDateId, groupId, activity, predefinedActivityId = null, timeblockId = null, duration = null, durationText = null) { await checkAccess(userToken, clubId); - let diaryDateActivity; - - if (timeblockId) { - // Verwende die spezifische Zeitblock-ID, falls angegeben - diaryDateActivity = await DiaryDateActivity.findOne({ - where: { - id: timeblockId, - diaryDateId, - isTimeblock: true, - } - }); - } else { - // Fallback: Verwende den letzten Zeitblock - diaryDateActivity = await DiaryDateActivity.findOne({ - where: { - diaryDateId, - isTimeblock: true, - }, - order: [['order_id', 'DESC']], - limit: 1 - }); - } - - if (!diaryDateActivity) { - console.error('[DiaryDateActivityService::addGroupActivity] Activity not found'); - throw new Error('Activity not found'); - } const group = await Group.findByPk(groupId); if (!group) { console.error('[DiaryDateActivityService::addGroupActivity] Group not found:', groupId); throw new Error('Group not found'); } - - if (group.diaryDateId !== diaryDateActivity.diaryDateId) { + + if (Number(group.diaryDateId) !== Number(diaryDateId)) { console.error('[DiaryDateActivityService::addGroupActivity] Group and date don\'t fit'); - console.error('Group diaryDateId:', group.diaryDateId, 'Activity diaryDateId:', diaryDateActivity.diaryDateId); + console.error('Group diaryDateId:', group.diaryDateId, 'Activity diaryDateId:', diaryDateId); throw new Error('Group isn\'t related to date'); } @@ -384,9 +391,9 @@ class DiaryDateActivityService { } devLog(predefinedActivity); - const maxOrderId = await GroupActivity.max('orderId', { + const maxOrderId = await DiaryDateActivity.max('orderId', { where: { - diaryDateActivity: diaryDateActivity.id, + diaryDateId, groupId: Number(groupId) } }); @@ -400,34 +407,80 @@ class DiaryDateActivityService { : String(durationText).trim() || null; const activityData = { - diaryDateActivity: diaryDateActivity.id, + diaryDateId, + isTimeblock: false, + predefinedActivityId: predefinedActivity.id, groupId: groupId, - customActivity: predefinedActivity.id, duration: Number.isFinite(parsedDuration) ? parsedDuration : null, durationText: normalizedDurationText, orderId: nextOrderId } devLog(activityData); - return await GroupActivity.create(activityData); + return await DiaryDateActivity.create(activityData); } - async updateGroupActivity(userToken, clubId, groupActivityId, predefinedActivityId, duration, durationText, orderId) { + async updateGroupActivity(userToken, clubId, groupActivityId, predefinedActivityId, duration, durationText, orderId, groupId) { await checkAccess(userToken, clubId); + const planActivity = await DiaryDateActivity.findOne({ + where: { id: groupActivityId, groupId: { [Op.not]: null } } + }); + if (planActivity) { + const payload = {}; + if (predefinedActivityId !== undefined && predefinedActivityId !== null) { + const predefinedActivity = await PredefinedActivity.findByPk(predefinedActivityId); + if (!predefinedActivity) { + throw new Error('Predefined activity not found'); + } + payload.predefinedActivityId = predefinedActivityId; + } + if (groupId !== undefined && groupId !== null) { + const parsedGroupId = parseInt(groupId, 10); + if (Number.isFinite(parsedGroupId) && parsedGroupId > 0) { + const group = await Group.findByPk(parsedGroupId); + if (!group || Number(group.diaryDateId) !== Number(planActivity.diaryDateId)) { + throw new Error('Group isn\'t related to date'); + } + payload.groupId = parsedGroupId; + } + } + if (duration !== undefined) { + const parsedDuration = duration === '' || duration === null + ? null + : parseInt(duration, 10); + payload.duration = Number.isFinite(parsedDuration) ? parsedDuration : null; + } + if (durationText !== undefined) { + payload.durationText = durationText === null ? null : (String(durationText).trim() || null); + } + if (orderId !== undefined) { + const parsedOrderId = parseInt(orderId, 10); + if (Number.isFinite(parsedOrderId) && parsedOrderId > 0) { + payload.orderId = parsedOrderId; + } + } + return await planActivity.update(payload); + } + const groupActivity = await GroupActivity.findByPk(groupActivityId); if (!groupActivity) { throw new Error('Group activity not found'); } - // Prüfe, ob die PredefinedActivity existiert - const predefinedActivity = await PredefinedActivity.findByPk(predefinedActivityId); - if (!predefinedActivity) { - throw new Error('Predefined activity not found'); - } - if (predefinedActivityId !== undefined && predefinedActivityId !== null) { + // Prüfe, ob die PredefinedActivity existiert + const predefinedActivity = await PredefinedActivity.findByPk(predefinedActivityId); + if (!predefinedActivity) { + throw new Error('Predefined activity not found'); + } // Aktualisiere die customActivity (die auf die PredefinedActivity verweist) groupActivity.customActivity = predefinedActivityId; } + if (groupId !== undefined && groupId !== null) { + const parsedGroupId = parseInt(groupId, 10); + if (Number.isFinite(parsedGroupId) && parsedGroupId > 0) { + groupActivity.groupId = parsedGroupId; + } + } if (duration !== undefined) { const parsedDuration = duration === '' || duration === null ? null @@ -448,6 +501,12 @@ class DiaryDateActivityService { async deleteGroupActivity(userToken, clubId, groupActivityId) { await checkAccess(userToken, clubId); + const planActivity = await DiaryDateActivity.findOne({ + where: { id: groupActivityId, groupId: { [Op.not]: null } } + }); + if (planActivity) { + return await planActivity.destroy(); + } const groupActivity = await GroupActivity.findByPk(groupActivityId); if (!groupActivity) { throw new Error('Group activity not found'); diff --git a/frontend/src/views/DiaryView.vue b/frontend/src/views/DiaryView.vue index 4d24e79b..0dfb6267 100644 --- a/frontend/src/views/DiaryView.vue +++ b/frontend/src/views/DiaryView.vue @@ -150,7 +150,7 @@ - +