Erweitert die Funktionalität in PredefinedActivityImageController.js, um Zeichnungsdaten aus dem Request zu extrahieren und in der Datenbank zu speichern. Aktualisiert das Datenmodell in PredefinedActivityImage.js, um ein neues Feld für Zeichnungsdaten hinzuzufügen. Passt die Routen in predefinedActivityRoutes.js an, um die neue PUT-Methode für das Hochladen von Bildern zu unterstützen. Integriert die Zeichnungsdaten in die Aktivitätenlogik in diaryDateActivityService.js und aktualisiert die Benutzeroberfläche in CourtDrawingTool.vue zur Unterstützung von Zeichnungsdaten. Verbessert die Handhabung von Bild-Uploads in PredefinedActivities.vue und implementiert die Logik zum Laden von Zeichnungsdaten beim Bearbeiten von Aktivitäten.

This commit is contained in:
Torsten Schulz (local)
2025-09-23 08:39:13 +02:00
parent d70a5ca63e
commit 091599b745
8 changed files with 562 additions and 96 deletions

View File

@@ -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

View File

@@ -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`;

View File

@@ -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,

View File

@@ -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);

View File

@@ -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) {

View File

@@ -2,14 +2,9 @@
<div class="court-drawing-tool">
<div class="tool-header">
<h4>Tischtennis-Übungszeichnung</h4>
<div class="tool-controls">
<button @click="clearCanvas" class="btn-secondary btn-small">Löschen</button>
<button @click="saveDrawing" class="btn-primary btn-small">Speichern</button>
<button @click="loadDrawing" class="btn-secondary btn-small">Laden</button>
</div>
</div>
<div class="canvas-container">
</div>
<div class="canvas-container">
<canvas
ref="drawingCanvas"
@mousedown="startDrawing"
@@ -19,73 +14,81 @@
:width="config.canvas.width"
:height="config.canvas.height"
></canvas>
</div>
</div>
<!-- Startposition und Schlagart Auswahl -->
<!-- Startposition und Schlagart Auswahl -->
<div class="exercise-selection" v-if="selectedStartPosition">
<h5>Übung konfigurieren</h5>
<div class="selection-group">
<!-- Schlagart Auswahl -->
<div class="stroke-selection">
<span>Aufschlag:</span>
<div class="stroke-buttons">
<button
type="button"
:class="['btn-small', { 'btn-primary': strokeType === 'VH', 'btn-secondary': strokeType !== 'VH' }]"
:class="['btn-small', 'btn-stroke', { 'btn-primary': strokeType === 'VH', 'btn-secondary': strokeType !== 'VH' }]"
@click="strokeType = 'VH'"
title="Vorhand"
>
Vorhand (VH)
VH
</button>
<button
type="button"
:class="['btn-small', { 'btn-primary': strokeType === 'RH', 'btn-secondary': strokeType !== 'RH' }]"
:class="['btn-small', 'btn-stroke', { 'btn-primary': strokeType === 'RH', 'btn-secondary': strokeType !== 'RH' }]"
@click="strokeType = 'RH'"
title="Rückhand"
>
Rückhand (RH)
RH
</button>
</div>
</div>
<!-- Schnittoption Auswahl -->
<div class="spin-selection">
<div class="spin-buttons">
<button
type="button"
:class="['btn-small', { 'btn-primary': spinType === 'Unterschnitt', 'btn-secondary': spinType !== 'Unterschnitt' }]"
:class="['btn-small', 'btn-stroke-type', { 'btn-primary': spinType === 'Unterschnitt', 'btn-secondary': spinType !== 'Unterschnitt' }]"
@click="spinType = 'Unterschnitt'"
title="Unterschnitt"
>
Unterschnitt
US
</button>
<button
type="button"
:class="['btn-small', { 'btn-primary': spinType === 'Überschnitt', 'btn-secondary': spinType !== 'Überschnitt' }]"
:class="['btn-small', 'btn-stroke-type', { 'btn-primary': spinType === 'Überschnitt', 'btn-secondary': spinType !== 'Überschnitt' }]"
@click="spinType = 'Überschnitt'"
title="Überschnitt"
>
Überschnitt
OS
</button>
<button
type="button"
:class="['btn-small', { 'btn-primary': spinType === 'Seitschnitt', 'btn-secondary': spinType !== 'Seitschnitt' }]"
:class="['btn-small', 'btn-stroke-type', { 'btn-primary': spinType === 'Seitschnitt', 'btn-secondary': spinType !== 'Seitschnitt' }]"
@click="spinType = 'Seitschnitt'"
title="Seitschnitt"
>
Seitschnitt
SR
</button>
<button
type="button"
:class="['btn-small', { 'btn-primary': spinType === 'Seitunterschnitt', 'btn-secondary': spinType !== 'Seitunterschnitt' }]"
:class="['btn-small', 'btn-stroke-type', { 'btn-primary': spinType === 'Seitunterschnitt', 'btn-secondary': spinType !== 'Seitunterschnitt' }]"
@click="spinType = 'Seitunterschnitt'"
title="Seitunterschnitt"
>
Seitunterschnitt
SU
</button>
<button
type="button"
:class="['btn-small', { 'btn-primary': spinType === 'Gegenläufer', 'btn-secondary': spinType !== 'Gegenläufer' }]"
:class="['btn-small', 'btn-stroke-type', { 'btn-primary': spinType === 'Gegenläufer', 'btn-secondary': spinType !== 'Gegenläufer' }]"
@click="spinType = 'Gegenläufer'"
title="Gegenläufer"
>
Gegenläufer
GL
</button>
</div>
</div>
</div>
<!-- Zusätzliche Schläge hinzufügen -->
<div class="additional-strokes" v-if="strokeType && spinType && targetPosition">
@@ -94,55 +97,62 @@
<div class="next-stroke-type">
<button
type="button"
:class="['btn-small', { 'btn-primary': nextStrokeSide === 'VH', 'btn-secondary': nextStrokeSide !== 'VH' }]"
:class="['btn-small', 'btn-stroke', { 'btn-primary': nextStrokeSide === 'VH', 'btn-secondary': nextStrokeSide !== 'VH' }]"
@click="nextStrokeSide = 'VH'"
title="Vorhand"
>
VH (Vorhand)
VH
</button>
<button
type="button"
:class="['btn-small', { 'btn-primary': nextStrokeSide === 'RH', 'btn-secondary': nextStrokeSide !== 'RH' }]"
:class="['btn-small', 'btn-stroke', { 'btn-primary': nextStrokeSide === 'RH', 'btn-secondary': nextStrokeSide !== 'RH' }]"
@click="nextStrokeSide = 'RH'"
title="Rückhand"
>
RH (Rückhand)
RH
</button>
</div>
<div class="next-stroke-buttons">
<button
type="button"
:class="['btn-small', { 'btn-primary': nextStrokeType === 'US', 'btn-secondary': nextStrokeType !== 'US' }]"
:class="['btn-small', 'btn-stroke-type', { 'btn-primary': nextStrokeType === 'US', 'btn-secondary': nextStrokeType !== 'US' }]"
@click="nextStrokeType = 'US'"
title="Schupf"
>
US (Schupf)
US
</button>
<button
type="button"
:class="['btn-small', { 'btn-primary': nextStrokeType === 'OS', 'btn-secondary': nextStrokeType !== 'OS' }]"
:class="['btn-small', 'btn-stroke-type', { 'btn-primary': nextStrokeType === 'OS', 'btn-secondary': nextStrokeType !== 'OS' }]"
@click="nextStrokeType = 'OS'"
title="Konter"
>
OS (Konter)
OS
</button>
<button
type="button"
:class="['btn-small', { 'btn-primary': nextStrokeType === 'TS', 'btn-secondary': nextStrokeType !== 'TS' }]"
:class="['btn-small', 'btn-stroke-type', { 'btn-primary': nextStrokeType === 'TS', 'btn-secondary': nextStrokeType !== 'TS' }]"
@click="nextStrokeType = 'TS'"
title="Topspin"
>
TS (Topspin)
TS
</button>
<button
type="button"
:class="['btn-small', { 'btn-primary': nextStrokeType === 'FL', 'btn-secondary': nextStrokeType !== 'FL' }]"
:class="['btn-small', 'btn-stroke-type', { 'btn-primary': nextStrokeType === 'FL', 'btn-secondary': nextStrokeType !== 'FL' }]"
@click="nextStrokeType = 'FL'"
title="Flip"
>
FL (Flip)
FL
</button>
<button
type="button"
:class="['btn-small', { 'btn-primary': nextStrokeType === 'BL', 'btn-secondary': nextStrokeType !== 'BL' }]"
:class="['btn-small', 'btn-stroke-type', { 'btn-primary': nextStrokeType === 'BL', 'btn-secondary': nextStrokeType !== 'BL' }]"
@click="nextStrokeType = 'BL'"
title="Block"
>
BL (Block)
BL
</button>
</div>
<button
@@ -161,6 +171,13 @@
</div>
</div>
</div>
<!-- Buttons ganz nach unten -->
<div class="tool-controls">
<button @click="clearCanvas" class="btn-secondary btn-small">Löschen</button>
<button @click="saveDrawing" class="btn-primary btn-small">Speichern</button>
<button @click="loadDrawing" class="btn-secondary btn-small">Laden</button>
</div>
</div>
</template>
@@ -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 {

View File

@@ -345,7 +345,7 @@
+ accident.accident}}</li>
</ul>
</form>
</div>
</div>
<!-- Schnell hinzufügen Dialog -->
<div v-if="showQuickAddDialog" class="modal-overlay" @click.self="closeQuickAddDialog">
@@ -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) {

View File

@@ -77,11 +77,15 @@
<!-- Zeichen-Tool -->
<div class="drawing-section">
<h5>Übungszeichnung erstellen</h5>
<CourtDrawingTool
v-model="editModel.drawingData"
@save="onDrawingSave"
@update-fields="onUpdateFields"
/>
<CourtDrawingTool
v-model="editModel.drawingData"
:activity-id="editModel.id"
:drawing-data="editModel.drawingData"
@save="onDrawingSave"
@update-fields="onUpdateFields"
@upload-image="onDrawingImageUpload"
@image-uploaded="onImageUploaded"
/>
</div>
<div class="image-list" v-if="images && images.length">
@@ -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() {