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/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/diaryDateActivityService.js b/backend/services/diaryDateActivityService.js
index f546a0c..92136c1 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';
@@ -16,11 +17,19 @@ class DiaryDateActivityService {
if (!predefinedActivity) {
predefinedActivity = await PredefinedActivity.create({
name: data.activity,
- description: '',
- duration: data.duration
+ 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 +63,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,7 +140,7 @@ class DiaryDateActivityService {
}
async getActivities(userToken, clubId, diaryDateId) {
- console.log('[DiaryDateActivityService::getActivities] - check user access');
+ console.log('[DiaryDateActivityService::getActivities] - check user access - SERVER RESTARTED');
await checkAccess(userToken, clubId);
console.log(`[DiaryDateActivityService::getActivities] - fetch activities for diaryDateId: ${diaryDateId}`);
const activities = await DiaryDateActivity.findAll({
@@ -141,6 +150,12 @@ class DiaryDateActivityService {
{
model: PredefinedActivity,
as: 'predefinedActivity',
+ include: [
+ {
+ model: PredefinedActivityImage,
+ as: 'images'
+ }
+ ]
},
{
model: GroupActivity,
@@ -159,7 +174,70 @@ class DiaryDateActivityService {
]
});
console.log(`[DiaryDateActivityService::getActivities] - found ${activities.length} activities`);
- return activities;
+
+ // Füge imageUrl zu jeder PredefinedActivity hinzu
+ console.log('[DiaryDateActivityService::getActivities] - Adding imageUrl to activities');
+ 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) {
+ console.log(JSON.parse(JSON.stringify(activityData)));
+ // Hole die erste verfügbare Image-ID direkt aus der Datenbank
+ const allImages = await PredefinedActivityImage.findAll({
+ where: { predefinedActivityId: activityData.predefinedActivity.id },
+ order: [['createdAt', 'ASC']]
+ });
+
+ console.log(`Activity ${activityData.predefinedActivity.id}: allImages =`, allImages.map(img => ({ id: img.id, activityId: img.predefinedActivityId, hasDrawingData: !!img.drawingData })));
+
+ const firstImage = allImages.length > 0 ? allImages[0] : null;
+ console.log(`Activity ${activityData.predefinedActivity.id}: firstImage =`, firstImage?.id);
+
+ // Füge Zeichnungsdaten hinzu, falls vorhanden
+ if (firstImage && firstImage.drawingData) {
+ try {
+ activityData.predefinedActivity.drawingData = JSON.parse(firstImage.drawingData);
+ console.log(`Activity ${activityData.predefinedActivity.id}: drawingData loaded:`, activityData.predefinedActivity.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/frontend/src/components/CourtDrawingTool.vue b/frontend/src/components/CourtDrawingTool.vue
index dabc61b..3119c7b 100644
--- a/frontend/src/components/CourtDrawingTool.vue
+++ b/frontend/src/components/CourtDrawingTool.vue
@@ -2,14 +2,9 @@
-
-
+
Übung konfigurieren
+
Aufschlag:
-
+
+
@@ -94,55 +97,62 @@
+
+
+
+
+
+
+
@@ -171,6 +188,14 @@ export default {
value: {
type: String,
default: ''
+ },
+ activityId: {
+ type: [String, Number],
+ default: null
+ },
+ drawingData: { // Added for loading saved drawing data
+ type: Object,
+ default: null
}
},
data() {
@@ -309,31 +334,73 @@ export default {
if (this.canvas && this.ctx) {
this.drawCourt();
}
+ },
+ drawingData: {
+ handler(newVal, oldVal) {
+ if (this.drawingData) {
+ this.loadDrawingFromMetadata();
+ } else if (oldVal && !newVal) {
+ // drawingData wurde auf null gesetzt - reset alle Werte und zeichne leeres Canvas
+ console.log('CourtDrawingTool: drawingData set to null, resetting all values');
+ this.resetAllValues();
+ this.clearCanvas();
+ this.drawCourt(true); // forceRedraw = true
+ }
+ },
+ immediate: true
}
},
mounted() {
- this.initCanvas();
- this.drawCourt();
- if (this.value) {
- this.loadDrawingFromData(this.value);
- }
+ console.log('CourtDrawingTool: mounted');
+ this.$nextTick(() => {
+ this.initCanvas();
+ this.drawCourt();
+ if (this.value) {
+ this.loadDrawingFromData(this.value);
+ }
+ });
},
methods: {
initCanvas() {
+ console.log('CourtDrawingTool: initCanvas called');
this.canvas = this.$refs.drawingCanvas;
- this.ctx = this.canvas.getContext('2d');
- this.ctx.lineCap = this.config.pen.cap;
- this.ctx.lineJoin = this.config.pen.join;
+ console.log('CourtDrawingTool: canvas =', this.canvas);
+ if (this.canvas) {
+ this.ctx = this.canvas.getContext('2d');
+ console.log('CourtDrawingTool: ctx =', this.ctx);
+ this.ctx.lineCap = this.config.pen.cap;
+ this.ctx.lineJoin = this.config.pen.join;
+ } else {
+ console.error('CourtDrawingTool: Canvas not found!');
+ }
},
- drawCourt() {
+ drawCourt(forceRedraw = false) {
+ console.log('CourtDrawingTool: drawCourt called, forceRedraw:', forceRedraw);
const ctx = this.ctx;
const canvas = this.canvas;
const config = this.config;
- // Hintergrund
- ctx.fillStyle = '#f0f0f0';
- ctx.fillRect(0, 0, canvas.width, canvas.height);
+ if (!ctx || !canvas) {
+ console.error('CourtDrawingTool: Canvas or context not available');
+ return;
+ }
+
+ console.log('CourtDrawingTool: Drawing court...');
+ console.log('Canvas dimensions:', canvas.width, 'x', canvas.height);
+
+ // Hintergrund immer zeichnen wenn forceRedraw=true, sonst nur wenn Canvas leer ist
+ const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
+ const isEmpty = imageData.data.every(pixel => pixel === 0);
+
+ if (forceRedraw || isEmpty) {
+ // Hintergrund
+ ctx.fillStyle = '#f0f0f0';
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
+ console.log('Background drawn');
+ } else {
+ console.log('Canvas not empty, skipping background');
+ }
// Tischtennis-Tisch
const tableWidth = config.table.width;
@@ -341,6 +408,9 @@ export default {
const tableX = (canvas.width - tableWidth) / 2;
const tableY = (canvas.height - tableHeight) / 2;
+ console.log('Table dimensions:', tableWidth, 'x', tableHeight);
+ console.log('Table position:', tableX, ',', tableY);
+
// Tischtennis-Tisch Hintergrund
ctx.fillStyle = config.table.color;
ctx.fillRect(tableX, tableY, tableWidth, tableHeight);
@@ -425,6 +495,15 @@ export default {
circles.forEach(circle => {
const isSelected = this.selectedCirclePosition === circle.position;
+ const hasSelection = this.selectedCirclePosition !== null;
+
+ // Transparenz setzen für nicht-ausgewählte Kreise
+ if (hasSelection && !isSelected) {
+ ctx.globalAlpha = 0.3; // Teiltransparent für nicht-ausgewählte
+ } else {
+ ctx.globalAlpha = 1.0; // Vollständig sichtbar für ausgewählten
+ }
+
// Kreis füllen
ctx.fillStyle = isSelected ? config.startCircles.selectedColor : config.startCircles.unselectedColor;
ctx.beginPath();
@@ -435,6 +514,9 @@ export default {
ctx.strokeStyle = isSelected ? config.startCircles.selectedBorderColor : config.startCircles.unselectedBorderColor;
ctx.lineWidth = isSelected ? config.startCircles.selectedBorderWidth : config.startCircles.unselectedBorderWidth;
ctx.stroke();
+
+ // Transparenz zurücksetzen
+ ctx.globalAlpha = 1.0;
});
// Zielpositionen (9 Kreise mit Zahlen) - nur anzeigen wenn Schnittoption gewählt
@@ -600,7 +682,7 @@ export default {
const midX = (startX + targetX - targetRadius) / 2;
const midY = (actualStartY + targetY) / 2 - config.arrow.counterOffset;
- ctx.fillText(this.exerciseCounter.toString(), midX, midY);
+ ctx.fillText("1", midX, midY);
// Linie zeichnen
ctx.beginPath();
@@ -694,9 +776,9 @@ export default {
return;
}
- // Pfeil zeichnen
- ctx.strokeStyle = config.arrow.color;
- ctx.fillStyle = config.arrow.color;
+ // Pfeil zeichnen (von rechts nach links = blau)
+ ctx.strokeStyle = '#007bff'; // Blau
+ ctx.fillStyle = '#007bff'; // Blau
ctx.lineWidth = config.arrow.width;
ctx.lineCap = config.arrow.cap;
@@ -709,7 +791,7 @@ export default {
const endY = targetY;
// Übungsnummer über der Linie zeichnen
- ctx.fillStyle = config.arrow.color;
+ ctx.fillStyle = '#007bff'; // Blau
ctx.font = config.arrow.counterFont;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
@@ -718,7 +800,7 @@ export default {
const midX = (startX + endX) / 2;
const midY = (startY + endY) / 2 - config.arrow.counterOffset;
- ctx.fillText((this.exerciseCounter + 1).toString(), midX, midY);
+ ctx.fillText("2", midX, midY);
// Linie zeichnen
ctx.beginPath();
@@ -974,10 +1056,77 @@ export default {
this.drawCourt();
},
- saveDrawing() {
- const dataURL = this.canvas.toDataURL('image/png');
- this.$emit('input', dataURL);
- this.$emit('save', dataURL);
+ testDraw() {
+ console.log('CourtDrawingTool: testDraw called');
+ console.log('Canvas:', this.canvas);
+ console.log('Context:', this.ctx);
+
+ if (!this.canvas || !this.ctx) {
+ console.error('Canvas or context not available, trying to reinitialize...');
+ this.initCanvas();
+ }
+
+ if (this.canvas && this.ctx) {
+ console.log('Drawing simple test...');
+
+ // Einfacher Test: Roter Kreis
+ this.ctx.fillStyle = 'red';
+ this.ctx.beginPath();
+ this.ctx.arc(300, 200, 50, 0, 2 * Math.PI);
+ this.ctx.fill();
+
+ console.log('Red circle drawn');
+ } else {
+ console.error('Still no canvas or context available');
+ }
+ },
+
+ async saveDrawing() {
+ console.log('CourtDrawingTool: saveDrawing called');
+
+ try {
+ const dataURL = this.canvas.toDataURL('image/png');
+ console.log('CourtDrawingTool: dataURL created, length:', dataURL.length);
+
+ this.$emit('input', dataURL);
+
+ // Erstelle Zeichnungsdaten für Metadaten
+ const drawingData = {
+ selectedStartPosition: this.selectedStartPosition,
+ strokeType: this.strokeType,
+ spinType: this.spinType,
+ targetPosition: this.targetPosition,
+ nextStrokeType: this.nextStrokeType,
+ nextStrokeSide: this.nextStrokeSide,
+ nextStrokeTargetPosition: this.nextStrokeTargetPosition,
+ exerciseCounter: this.exerciseCounter,
+ additionalStrokes: this.additionalStrokes,
+ timestamp: new Date().toISOString()
+ };
+
+ console.log('CourtDrawingTool: drawingData created:', drawingData);
+
+ // Konvertiere DataURL zu Blob für Upload
+ const response = await fetch(dataURL);
+ const blob = await response.blob();
+ console.log('CourtDrawingTool: blob created, size:', blob.size);
+
+ // Erstelle File-Objekt
+ const file = new File([blob], `exercise-${Date.now()}.png`, { type: 'image/png' });
+ console.log('CourtDrawingTool: file created:', file);
+ console.log('CourtDrawingTool: file type:', file.type);
+ console.log('CourtDrawingTool: file size:', file.size);
+
+ // Emittiere das File und die Zeichnungsdaten für Upload
+ console.log('CourtDrawingTool: emitting upload-image event');
+ this.$emit('upload-image', file, drawingData);
+ console.log('CourtDrawingTool: upload-image event emitted');
+
+ // Emittiere das File für Upload (Parent-Komponente macht den Upload)
+ console.log('CourtDrawingTool: File ready for upload');
+ } catch (error) {
+ console.error('CourtDrawingTool: Error in saveDrawing:', error);
+ }
},
loadDrawing() {
@@ -997,6 +1146,67 @@ export default {
input.click();
},
+ resetAllValues() {
+ console.log('CourtDrawingTool: Resetting all values to initial state');
+ this.selectedStartPosition = null;
+ this.selectedCirclePosition = null;
+ this.strokeType = null;
+ this.spinType = null;
+ this.targetPosition = null;
+ this.nextStrokeType = null;
+ this.nextStrokeSide = null;
+ this.nextStrokeTargetPosition = null;
+ this.exerciseCounter = 1;
+ this.additionalStrokes = [];
+ this.updateTextFields();
+ },
+
+ loadDrawingFromMetadata() {
+ if (this.drawingData) {
+ console.log('CourtDrawingTool: Loading drawing from metadata:', this.drawingData);
+
+ // Lade alle Zeichnungsdaten
+ this.selectedStartPosition = this.drawingData.selectedStartPosition || null;
+ this.strokeType = this.drawingData.strokeType || null;
+ this.spinType = this.drawingData.spinType || null;
+ this.targetPosition = this.drawingData.targetPosition || null;
+ this.nextStrokeType = this.drawingData.nextStrokeType || null;
+ this.nextStrokeSide = this.drawingData.nextStrokeSide || null;
+ this.nextStrokeTargetPosition = this.drawingData.nextStrokeTargetPosition || null;
+ this.exerciseCounter = this.drawingData.exerciseCounter || 1;
+ this.additionalStrokes = this.drawingData.additionalStrokes || [];
+
+ // Setze selectedCirclePosition basierend auf selectedStartPosition
+ if (this.selectedStartPosition === 'AS') {
+ this.selectedCirclePosition = 'middle'; // Standard für AS
+ } else if (this.selectedStartPosition === 'AS1') {
+ this.selectedCirclePosition = 'top';
+ } else if (this.selectedStartPosition === 'AS2') {
+ this.selectedCirclePosition = 'middle';
+ } else if (this.selectedStartPosition === 'AS3') {
+ this.selectedCirclePosition = 'bottom';
+ }
+
+ console.log('CourtDrawingTool: Loaded values:', {
+ selectedStartPosition: this.selectedStartPosition,
+ selectedCirclePosition: this.selectedCirclePosition,
+ strokeType: this.strokeType,
+ spinType: this.spinType,
+ targetPosition: this.targetPosition
+ });
+
+ // Aktualisiere die Textfelder
+ this.updateTextFields();
+
+ // Zeichne das Canvas neu
+ this.$nextTick(() => {
+ this.drawCourt();
+ });
+
+ console.log('CourtDrawingTool: Drawing loaded from metadata');
+ }
+ },
+
loadDrawingFromData(dataURL) {
const img = new Image();
img.onload = () => {
@@ -1173,12 +1383,14 @@ canvas {
.btn-small {
- padding: 0.25rem 0.5rem;
- font-size: 0.8rem;
+ padding: 0.2rem 0.4rem;
+ font-size: 0.75rem;
border-radius: 4px;
border: 1px solid #ddd;
cursor: pointer;
transition: all 0.2s;
+ min-width: 2.5rem;
+ text-align: center;
}
.btn-primary {
@@ -1203,6 +1415,51 @@ canvas {
border-color: #5a6268;
}
+/* Schlagart-Buttons (VH/RH) - Grün */
+.btn-stroke {
+ background-color: #28a745;
+ color: white;
+ border-color: #28a745;
+}
+
+.btn-stroke:hover {
+ background-color: #218838;
+ border-color: #218838;
+}
+
+.btn-stroke.btn-secondary {
+ background-color: #6c757d;
+ color: white;
+ border-color: #6c757d;
+}
+
+.btn-stroke.btn-secondary:hover {
+ background-color: #5a6268;
+ border-color: #5a6268;
+}
+
+/* Schlagtyp-Buttons (US/OS/TS/FL/BL) - Orange Hintergrund */
+.btn-stroke-type {
+ background: #fd7e14;
+ color: white;
+}
+
+.btn-stroke-type:hover {
+ background: #e8650e;
+ border-color: #e8650e;
+}
+
+.btn-stroke-type.btn-secondary {
+ color: white !important;
+ opacity: 0.6;
+}
+
+.btn-stroke-type.btn-secondary:hover {
+ background-color: #e8650e !important;
+ border-color: #e8650e !important;
+ opacity: 0.8;
+}
+
input[type="color"] {
width: 40px;
height: 30px;
@@ -1236,6 +1493,10 @@ input[type="range"] {
.stroke-selection {
margin-top: 0;
+ display: flex;
+ flex-direction: row;
+ gap: 0.5rem;
+ vertical-align: middle;
}
.stroke-selection label {
@@ -1248,7 +1509,7 @@ input[type="range"] {
.stroke-buttons {
display: flex;
gap: 0.5rem;
- flex-wrap: wrap;
+ flex-wrap: nowrap;
}
.spin-selection {
@@ -1265,7 +1526,7 @@ input[type="range"] {
.spin-buttons {
display: flex;
gap: 0.5rem;
- flex-wrap: wrap;
+ flex-wrap: nowrap;
}
.additional-strokes {
@@ -1274,21 +1535,20 @@ input[type="range"] {
.next-stroke-selection {
display: flex;
- flex-direction: column;
+ flex-direction: row;
gap: 0.5rem;
}
.next-stroke-type {
display: flex;
gap: 0.5rem;
- flex-wrap: wrap;
- margin-bottom: 0.5rem;
+ flex-wrap: nowrap;
}
.next-stroke-buttons {
display: flex;
gap: 0.5rem;
- flex-wrap: wrap;
+ flex-wrap: nowrap;
}
.exercise-info {
diff --git a/frontend/src/views/DiaryView.vue b/frontend/src/views/DiaryView.vue
index 7d24f50..ecd4a33 100644
--- a/frontend/src/views/DiaryView.vue
+++ b/frontend/src/views/DiaryView.vue
@@ -345,7 +345,7 @@
+ accident.accident}}
-
+
@@ -1123,7 +1123,12 @@ export default {
},
showActivityImage(imageLink) {
- this.imageUrl = imageLink;
+ // Erstelle vollständige URL mit korrektem Port
+ if (imageLink && imageLink.startsWith('/api/')) {
+ this.imageUrl = `http://localhost:3000${imageLink}`;
+ } else {
+ this.imageUrl = imageLink;
+ }
this.showImage = true;
},
async loadMemberImage(member) {
diff --git a/frontend/src/views/PredefinedActivities.vue b/frontend/src/views/PredefinedActivities.vue
index b9aee47..4ca2b5b 100644
--- a/frontend/src/views/PredefinedActivities.vue
+++ b/frontend/src/views/PredefinedActivities.vue
@@ -77,11 +77,15 @@
Übungszeichnung erstellen
-
+
@@ -121,6 +125,7 @@ export default {
editModel: null,
images: [],
selectedFile: null,
+ selectedDrawingData: null,
mergeSourceId: '',
mergeTargetId: '',
};
@@ -154,7 +159,35 @@ export default {
const r = await apiClient.get(`/predefined-activities/${a.id}`);
const { images, ...activity } = r.data;
this.images = images || [];
- this.editModel = { ...activity };
+
+ // Lade Zeichnungsdaten aus dem ersten Bild, falls vorhanden
+ let drawingData = null;
+ if (images && images.length > 0 && images[0].drawingData) {
+ try {
+ drawingData = JSON.parse(images[0].drawingData);
+ console.log('PredefinedActivities: Loaded drawingData:', drawingData);
+ } catch (error) {
+ console.error('PredefinedActivities: Error parsing drawingData:', error);
+ }
+ } else {
+ // Keine Bilder vorhanden - setze drawingData explizit auf null
+ console.log('PredefinedActivities: No images found, setting drawingData to null');
+ drawingData = null;
+ }
+
+ this.editModel = { ...activity, drawingData };
+ },
+ async reloadImages() {
+ if (this.editModel && this.editModel.id) {
+ try {
+ const r = await apiClient.get(`/predefined-activities/${this.editModel.id}`);
+ const { images } = r.data;
+ this.images = images || [];
+ console.log('Images reloaded:', this.images);
+ } catch (error) {
+ console.error('Error reloading images:', error);
+ }
+ }
},
formatItem(a) {
return `${a.code ? '[' + a.code + '] ' : ''}${a.name}`;
@@ -188,6 +221,9 @@ export default {
},
async save() {
if (!this.editModel) return;
+
+ console.log('Save: selectedFile =', this.selectedFile);
+
if (this.editModel.id) {
const { id, ...payload } = this.editModel;
const r = await apiClient.put(`/predefined-activities/${id}`, payload);
@@ -195,29 +231,68 @@ export default {
} else {
const r = await apiClient.post('/predefined-activities', this.editModel);
this.editModel = r.data;
- // Nach dem Erstellen einer neuen Aktivität, falls ein Bild ausgewählt wurde, hochladen
- if (this.selectedFile) {
- await this.uploadImage();
- }
}
+
+ // Nach dem Speichern (sowohl CREATE als auch UPDATE): Bild hochladen falls vorhanden
+ if (this.selectedFile) {
+ console.log('Uploading image after save...');
+ await this.uploadImage();
+ } else {
+ console.log('No selectedFile to upload');
+ }
+
await this.reload();
},
onFileChange(e) {
this.selectedFile = e.target.files && e.target.files[0] ? e.target.files[0] : null;
},
imageUrl(img) {
- return `/api/predefined-activities/${this.editModel.id}/image/${img.id}`;
+ return `http://localhost:3000/api/predefined-activities/${this.editModel.id}/image/${img.id}`;
},
async uploadImage() {
- if (!this.editModel || !this.editModel.id || !this.selectedFile) return;
+ if (!this.editModel || !this.editModel.id || !this.selectedFile) {
+ console.log('Upload skipped: editModel=', this.editModel, 'selectedFile=', this.selectedFile);
+ return;
+ }
+
+ console.log('Starting image upload...');
+ console.log('editModel:', this.editModel);
+ console.log('selectedActivity:', this.selectedActivity);
+ console.log('Activity ID (editModel.id):', this.editModel.id);
+ console.log('Activity ID (selectedActivity.id):', this.selectedActivity?.id);
+ console.log('File:', this.selectedFile);
+
const fd = new FormData();
fd.append('image', this.selectedFile);
- await apiClient.post(`/predefined-activities/${this.editModel.id}/image`, fd, {
- headers: { 'Content-Type': 'multipart/form-data' }
- });
- // Nach Upload Details neu laden
- await this.select(this.editModel);
- this.selectedFile = null;
+
+ // Füge Zeichnungsdaten hinzu, falls vorhanden
+ if (this.selectedDrawingData) {
+ fd.append('drawingData', JSON.stringify(this.selectedDrawingData));
+ console.log('Added drawingData to FormData:', this.selectedDrawingData);
+ }
+
+ // Verwende PUT für Updates, POST für neue Activities
+ const isUpdate = this.selectedActivity && this.selectedActivity.id === this.editModel.id;
+ const method = isUpdate ? 'put' : 'post';
+
+ console.log('Using method:', method);
+
+ try {
+ const response = await apiClient[method](`/predefined-activities/${this.editModel.id}/image`, fd, {
+ headers: { 'Content-Type': 'multipart/form-data' }
+ });
+ console.log('Upload successful:', response);
+
+ // Nach Upload Details neu laden
+ await this.select(this.editModel);
+ this.selectedFile = null;
+
+ // Bildliste explizit aktualisieren
+ await this.reloadImages();
+ } catch (error) {
+ console.error('Upload failed:', error);
+ throw error;
+ }
},
async deleteImage(imageId) {
if (!this.editModel || !this.editModel.id) return;
@@ -244,6 +319,7 @@ export default {
this.editModel.imageLink = drawingData;
}
}
+ // Nicht automatisch speichern, nur wenn User explizit "Speichern" klickt
},
onUpdateFields(fields) {
if (this.editModel) {
@@ -251,6 +327,31 @@ export default {
this.editModel.name = fields.name;
this.editModel.description = fields.description;
}
+ },
+
+ async onDrawingImageUpload(file, drawingData) {
+ console.log('onDrawingImageUpload called with file:', file);
+ console.log('onDrawingImageUpload called with drawingData:', drawingData);
+ console.log('File type:', file?.type);
+ console.log('File size:', file?.size);
+ console.log('File name:', file?.name);
+
+ // Setze das File und die Zeichnungsdaten für den Upload
+ this.selectedFile = file;
+ this.selectedDrawingData = drawingData;
+ console.log('selectedFile set to:', this.selectedFile);
+ console.log('selectedDrawingData set to:', this.selectedDrawingData);
+
+ // Upload wird erst beim Speichern durchgeführt
+ console.log('File and drawing data ready for upload when saving');
+ },
+
+ async onImageUploaded() {
+ console.log('Image uploaded successfully, refreshing image list...');
+ // Bildliste aktualisieren
+ if (this.editModel && this.editModel.id) {
+ await this.select(this.editModel);
+ }
}
},
async mounted() {