diff --git a/backend/controllers/predefinedActivityController.js b/backend/controllers/predefinedActivityController.js index 102a2f1..1320a7a 100644 --- a/backend/controllers/predefinedActivityController.js +++ b/backend/controllers/predefinedActivityController.js @@ -5,8 +5,8 @@ import fs from 'fs'; export const createPredefinedActivity = async (req, res) => { try { - const { name, code, description, durationText, duration, imageLink } = req.body; - const predefinedActivity = await predefinedActivityService.createPredefinedActivity({ name, code, description, durationText, duration, imageLink }); + const { name, code, description, durationText, duration, imageLink, drawingData } = req.body; + const predefinedActivity = await predefinedActivityService.createPredefinedActivity({ name, code, description, durationText, duration, imageLink, drawingData }); res.status(201).json(predefinedActivity); } catch (error) { console.error('[createPredefinedActivity] - Error:', error); @@ -42,8 +42,8 @@ export const getPredefinedActivityById = async (req, res) => { export const updatePredefinedActivity = async (req, res) => { try { const { id } = req.params; - const { name, code, description, durationText, duration, imageLink } = req.body; - const updatedActivity = await predefinedActivityService.updatePredefinedActivity(id, { name, code, description, durationText, duration, imageLink }); + const { name, code, description, durationText, duration, imageLink, drawingData } = req.body; + const updatedActivity = await predefinedActivityService.updatePredefinedActivity(id, { name, code, description, durationText, duration, imageLink, drawingData }); res.status(200).json(updatedActivity); } catch (error) { console.error('[updatePredefinedActivity] - Error:', error); diff --git a/backend/controllers/predefinedActivityImageController.js b/backend/controllers/predefinedActivityImageController.js index 9966af7..80e3e94 100644 --- a/backend/controllers/predefinedActivityImageController.js +++ b/backend/controllers/predefinedActivityImageController.js @@ -33,10 +33,15 @@ export const uploadPredefinedActivityImage = async (req, res) => { .jpeg({ quality: 85 }) .toFile(filePath); + // Extrahiere Zeichnungsdaten aus dem Request + const drawingData = req.body.drawingData ? JSON.parse(req.body.drawingData) : null; + console.log('[uploadPredefinedActivityImage] - drawingData:', drawingData); + const imageRecord = await PredefinedActivityImage.create({ predefinedActivityId: id, imagePath: filePath, mimeType: 'image/jpeg', + drawingData: drawingData ? JSON.stringify(drawingData) : null, }); // Optional: als imageLink am Activity-Datensatz setzen diff --git a/backend/migrations/add_drawing_data_to_predefined_activity_images.sql b/backend/migrations/add_drawing_data_to_predefined_activity_images.sql new file mode 100644 index 0000000..0eadc30 --- /dev/null +++ b/backend/migrations/add_drawing_data_to_predefined_activity_images.sql @@ -0,0 +1,11 @@ +-- Migration: Add drawing_data column to predefined_activity_images table +-- Date: 2025-09-22 +-- Description: Adds drawing_data column to store Court Drawing Tool metadata + +ALTER TABLE `predefined_activity_images` +ADD COLUMN `drawing_data` TEXT NULL +COMMENT 'JSON string containing drawing metadata for Court Drawing Tool' +AFTER `mime_type`; + +-- Verify the column was added +DESCRIBE `predefined_activity_images`; diff --git a/backend/models/PredefinedActivity.js b/backend/models/PredefinedActivity.js index 749c6a5..63c7d8d 100644 --- a/backend/models/PredefinedActivity.js +++ b/backend/models/PredefinedActivity.js @@ -19,6 +19,11 @@ const PredefinedActivity = sequelize.define('PredefinedActivity', { type: DataTypes.TEXT, allowNull: true, }, + drawingData: { + type: DataTypes.TEXT, + allowNull: true, + comment: 'JSON string with metadata for Court Drawing Tool' + }, durationText: { type: DataTypes.STRING, allowNull: true, diff --git a/backend/models/PredefinedActivityImage.js b/backend/models/PredefinedActivityImage.js index c385931..cc0db92 100644 --- a/backend/models/PredefinedActivityImage.js +++ b/backend/models/PredefinedActivityImage.js @@ -19,6 +19,11 @@ const PredefinedActivityImage = sequelize.define('PredefinedActivityImage', { type: DataTypes.STRING, allowNull: true, }, + drawingData: { + type: DataTypes.TEXT, + allowNull: true, + comment: 'JSON string containing drawing metadata for Court Drawing Tool' + }, }, { tableName: 'predefined_activity_images', timestamps: true, diff --git a/backend/routes/predefinedActivityRoutes.js b/backend/routes/predefinedActivityRoutes.js index 42873a7..ce74def 100644 --- a/backend/routes/predefinedActivityRoutes.js +++ b/backend/routes/predefinedActivityRoutes.js @@ -23,6 +23,7 @@ router.get('/', authenticate, getAllPredefinedActivities); router.get('/:id', authenticate, getPredefinedActivityById); router.put('/:id', authenticate, updatePredefinedActivity); router.post('/:id/image', authenticate, upload.single('image'), uploadPredefinedActivityImage); +router.put('/:id/image', authenticate, upload.single('image'), uploadPredefinedActivityImage); router.delete('/:id/image/:imageId', authenticate, deletePredefinedActivityImage); router.get('/search/query', authenticate, searchPredefinedActivities); router.post('/merge', authenticate, mergePredefinedActivities); diff --git a/backend/services/authService.js b/backend/services/authService.js index f5d0f78..a054fa0 100644 --- a/backend/services/authService.js +++ b/backend/services/authService.js @@ -33,11 +33,11 @@ const login = async (email, password) => { if (!user || !(await bcrypt.compare(password, user.password))) { throw { status: 401, message: 'Ungültige Anmeldedaten' }; } - const token = jwt.sign({ userId: user.id }, process.env.JWT_SECRET, { expiresIn: '1h' }); + const token = jwt.sign({ userId: user.id }, process.env.JWT_SECRET, { expiresIn: '3h' }); await UserToken.create({ userId: user.id, token, - expiresAt: new Date(Date.now() + 3600 * 1000), + expiresAt: new Date(Date.now() + 3 * 3600 * 1000), }); return { token }; }; diff --git a/backend/services/diaryDateActivityService.js b/backend/services/diaryDateActivityService.js index f546a0c..9cff7f6 100644 --- a/backend/services/diaryDateActivityService.js +++ b/backend/services/diaryDateActivityService.js @@ -2,6 +2,7 @@ import DiaryDateActivity from '../models/DiaryDateActivity.js'; import GroupActivity from '../models/GroupActivity.js'; import Group from '../models/Group.js'; import PredefinedActivity from '../models/PredefinedActivity.js'; +import PredefinedActivityImage from '../models/PredefinedActivityImage.js'; import { checkAccess } from '../utils/userUtils.js'; import { Op } from 'sequelize'; @@ -12,15 +13,47 @@ class DiaryDateActivityService { await checkAccess(userToken, clubId); console.log('[DiaryDateActivityService::createActivity] - add: ', data); const { activity, ...restData } = data; - let predefinedActivity = await PredefinedActivity.findOne({ where: { name: data.activity } }); + // Versuche, die PredefinedActivity robust zu finden: + // 1) per übergebener ID + // 2) per Name ODER Code (das Feld "activity" kann Kürzel oder Name sein) + // 3) erst dann neu anlegen + let predefinedActivity = null; + + if (data.predefinedActivityId) { + predefinedActivity = await PredefinedActivity.findByPk(data.predefinedActivityId); + } + + if (!predefinedActivity) { + const normalized = (data.activity || '').trim(); + if (normalized) { + predefinedActivity = await PredefinedActivity.findOne({ + where: { + [Op.or]: [ + { name: normalized }, + { code: normalized } + ] + } + }); + } + } + if (!predefinedActivity) { predefinedActivity = await PredefinedActivity.create({ - name: data.activity, - description: '', - duration: data.duration + name: data.name || data.activity || '', + code: data.code || (data.activity || ''), + description: data.description || '', + duration: data.duration && data.duration !== '' ? parseInt(data.duration) : null }); } restData.predefinedActivityId = predefinedActivity.id; + + // Bereinige duration-Feld für DiaryDateActivity + if (restData.duration === '' || restData.duration === undefined) { + restData.duration = null; + } else if (typeof restData.duration === 'string') { + restData.duration = parseInt(restData.duration); + } + const maxOrderId = await DiaryDateActivity.max('orderId', { where: { diaryDateId: data.diaryDateId } }); @@ -54,8 +87,8 @@ class DiaryDateActivityService { console.log('[DiaryDateActivityService::updateActivity] - creating new PredefinedActivity'); predefinedActivity = await PredefinedActivity.create({ name: data.customActivityName, - description: '', - duration: data.duration || activity.duration + description: data.description || '', + duration: data.duration && data.duration !== '' ? parseInt(data.duration) : (activity.duration || null) }); } @@ -131,9 +164,7 @@ class DiaryDateActivityService { } async getActivities(userToken, clubId, diaryDateId) { - console.log('[DiaryDateActivityService::getActivities] - check user access'); await checkAccess(userToken, clubId); - console.log(`[DiaryDateActivityService::getActivities] - fetch activities for diaryDateId: ${diaryDateId}`); const activities = await DiaryDateActivity.findAll({ where: { diaryDateId }, order: [['orderId', 'ASC']], @@ -141,6 +172,12 @@ class DiaryDateActivityService { { model: PredefinedActivity, as: 'predefinedActivity', + include: [ + { + model: PredefinedActivityImage, + as: 'images' + } + ] }, { model: GroupActivity, @@ -158,8 +195,64 @@ class DiaryDateActivityService { } ] }); - console.log(`[DiaryDateActivityService::getActivities] - found ${activities.length} activities`); - return activities; + + // Füge imageUrl zu jeder PredefinedActivity hinzu + const activitiesWithImages = await Promise.all(activities.map(async activity => { + // Konvertiere zu JSON und zurück, um alle Eigenschaften zu serialisieren + const activityData = activity.toJSON(); + + if (activityData.predefinedActivity) { + // Hole die erste verfügbare Image-ID direkt aus der Datenbank + const allImages = await PredefinedActivityImage.findAll({ + where: { predefinedActivityId: activityData.predefinedActivity.id }, + order: [['createdAt', 'ASC']] + }); + + const firstImage = allImages.length > 0 ? allImages[0] : null; + + // Füge Zeichnungsdaten hinzu, falls vorhanden + if (firstImage && firstImage.drawingData) { + try { + activityData.predefinedActivity.drawingData = JSON.parse(firstImage.drawingData); + } catch (error) { + console.error(`Activity ${activityData.predefinedActivity.id}: Error parsing drawingData:`, error); + } + } + + if (firstImage) { + // Füge sowohl imageUrl als auch imageLink mit Image-ID hinzu + activityData.predefinedActivity.imageUrl = `/api/predefined-activities/${activityData.predefinedActivity.id}/image/${firstImage.id}`; + activityData.predefinedActivity.imageLink = `/api/predefined-activities/${activityData.predefinedActivity.id}/image/${firstImage.id}`; + } else { + // Fallback: Verwende den Basis-Pfad ohne Image-ID + activityData.predefinedActivity.imageUrl = `/api/predefined-activities/${activityData.predefinedActivity.id}/image`; + activityData.predefinedActivity.imageLink = `/api/predefined-activities/${activityData.predefinedActivity.id}/image`; + } + } + + // Auch für GroupActivities + if (activityData.groupActivities && activityData.groupActivities.length > 0) { + for (const groupActivity of activityData.groupActivities) { + if (groupActivity.groupPredefinedActivity) { + // Hole die erste verfügbare Image-ID direkt aus der Datenbank + const firstImage = await PredefinedActivityImage.findOne({ + where: { predefinedActivityId: groupActivity.groupPredefinedActivity.id }, + order: [['createdAt', 'ASC']] + }); + + if (firstImage) { + groupActivity.groupPredefinedActivity.imageUrl = `/api/predefined-activities/${groupActivity.groupPredefinedActivity.id}/image/${firstImage.id}`; + groupActivity.groupPredefinedActivity.imageLink = `/api/predefined-activities/${groupActivity.groupPredefinedActivity.id}/image/${firstImage.id}`; + } else { + groupActivity.groupPredefinedActivity.imageUrl = `/api/predefined-activities/${groupActivity.groupPredefinedActivity.id}/image`; + groupActivity.groupPredefinedActivity.imageLink = `/api/predefined-activities/${groupActivity.groupPredefinedActivity.id}/image`; + } + } + } + } + return activityData; + })); + return activitiesWithImages; } async addGroupActivity(userToken, clubId, diaryDateId, groupId, activity) { diff --git a/backend/services/predefinedActivityService.js b/backend/services/predefinedActivityService.js index 8152d3a..2dd9cdd 100644 --- a/backend/services/predefinedActivityService.js +++ b/backend/services/predefinedActivityService.js @@ -15,6 +15,7 @@ class PredefinedActivityService { durationText: data.durationText, duration: data.duration, imageLink: data.imageLink, + drawingData: data.drawingData ? JSON.stringify(data.drawingData) : null, }); } @@ -32,6 +33,7 @@ class PredefinedActivityService { durationText: data.durationText, duration: data.duration, imageLink: data.imageLink, + drawingData: data.drawingData ? JSON.stringify(data.drawingData) : null, }); } diff --git a/frontend/src/components/CourtDrawingRender.vue b/frontend/src/components/CourtDrawingRender.vue new file mode 100644 index 0000000..24225b2 --- /dev/null +++ b/frontend/src/components/CourtDrawingRender.vue @@ -0,0 +1,442 @@ + + + + + + + diff --git a/frontend/src/components/CourtDrawingTool.vue b/frontend/src/components/CourtDrawingTool.vue new file mode 100644 index 0000000..76cdafb --- /dev/null +++ b/frontend/src/components/CourtDrawingTool.vue @@ -0,0 +1,1618 @@ + + + + + diff --git a/frontend/src/views/DiaryView.vue b/frontend/src/views/DiaryView.vue index 7d24f50..fb8cc55 100644 --- a/frontend/src/views/DiaryView.vue +++ b/frontend/src/views/DiaryView.vue @@ -126,10 +126,11 @@ - 🖼️ + + 🖼️ {{ (item.predefinedActivity && item.predefinedActivity.code && item.predefinedActivity.code.trim() !== '') ? item.predefinedActivity.code : (item.predefinedActivity ? item.predefinedActivity.name : item.activity) }} @@ -161,10 +162,12 @@ - 🖼️ + + 🖼️ + {{ (groupItem.groupPredefinedActivity && groupItem.groupPredefinedActivity.code && groupItem.groupPredefinedActivity.code.trim() !== '') ? groupItem.groupPredefinedActivity.code : groupItem.groupPredefinedActivity.name }} @@ -319,8 +322,13 @@ -
- +
+ +
@@ -345,7 +353,7 @@ + accident.accident}}
-
+