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 @@
+
+ Kürzel: {{ getFullCode() }} Titel: {{ getFullTitle() }}Tischtennis-Übungszeichnung
+ Übung konfigurieren
+