diff --git a/backend/services/diaryDateActivityService.js b/backend/services/diaryDateActivityService.js index 92136c1..9cff7f6 100644 --- a/backend/services/diaryDateActivityService.js +++ b/backend/services/diaryDateActivityService.js @@ -13,10 +13,34 @@ 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, + name: data.name || data.activity || '', + code: data.code || (data.activity || ''), description: data.description || '', duration: data.duration && data.duration !== '' ? parseInt(data.duration) : null }); @@ -140,9 +164,7 @@ class DiaryDateActivityService { } async getActivities(userToken, clubId, diaryDateId) { - 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({ where: { diaryDateId }, order: [['orderId', 'ASC']], @@ -173,32 +195,25 @@ class DiaryDateActivityService { } ] }); - console.log(`[DiaryDateActivityService::getActivities] - found ${activities.length} 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); } diff --git a/frontend/src/components/CourtDrawingRender.vue b/frontend/src/components/CourtDrawingRender.vue new file mode 100644 index 0000000..295c80e --- /dev/null +++ b/frontend/src/components/CourtDrawingRender.vue @@ -0,0 +1,420 @@ + + + + + + + diff --git a/frontend/src/components/CourtDrawingTool.vue b/frontend/src/components/CourtDrawingTool.vue index 736592a..065ecc9 100644 --- a/frontend/src/components/CourtDrawingTool.vue +++ b/frontend/src/components/CourtDrawingTool.vue @@ -588,8 +588,12 @@ export default { this.drawArrowToTarget(ctx, tableX, tableY, tableWidth, tableHeight); } - // Kreise für zusätzliche Schläge auf der linken Seite anzeigen (nur nach Schlag-Auswahl für rechte Seite) - if (this.nextStrokeType && this.nextStrokeSide && this.targetPosition) { + // Kreise für zusätzliche Schläge auf der linken Seite anzeigen + // Sollen bereits erscheinen, sobald rechts alles gewählt ist (Aufschlag-Seite): + // also wenn strokeType, spinType und targetPosition gesetzt sind. + // Zusätzlich weiterhin, wenn bereits nextStrokeType/Side gewählt wurden. + if ((this.strokeType && this.spinType && this.targetPosition) || + (this.nextStrokeType && this.nextStrokeSide && this.targetPosition)) { this.drawLeftSideTargetCircles(ctx, tableX, tableY, tableWidth, tableHeight); } diff --git a/frontend/src/views/DiaryView.vue b/frontend/src/views/DiaryView.vue index ecd4a33..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 @@ -
- +
+ +
@@ -397,10 +405,11 @@ import apiClient from '../apiClient.js'; import Multiselect from 'vue-multiselect'; import Sortable from 'sortablejs'; import PDFGenerator from '../components/PDFGenerator.js'; +import CourtDrawingRender from '../components/CourtDrawingRender.vue'; export default { name: 'DiaryView', - components: { Multiselect }, + components: { Multiselect, CourtDrawingRender }, data() { return { date: null, @@ -436,6 +445,8 @@ export default { showDropdown: false, showImage: false, imageUrl: '', + showRenderModal: false, + renderModalData: null, groups: [], currentTimeBlockId: null, newGroupName: '', @@ -526,6 +537,36 @@ export default { }, }, methods: { + drawingDataFor(pa) { + // Zeichnungsdaten können bereits als Objekt vorliegen oder als JSON-String + try { + if (!pa) return null; + if (pa.drawingData && typeof pa.drawingData === 'object') { + console.debug('DiaryView: drawingData (object) gefunden für', pa.id); + return pa.drawingData; + } + if (pa.drawingData && typeof pa.drawingData === 'string') { + const parsed = JSON.parse(pa.drawingData); + console.debug('DiaryView: drawingData (string→parsed) für', pa.id, parsed); + return parsed; + } + // Fallback: falls über images[0].drawingData geliefert wurde + if (pa.images && pa.images.length) { + // Nimm das erste Bild, das drawingData enthält + const withData = pa.images.find(img => !!img.drawingData); + if (withData) { + const parsedImg = typeof withData.drawingData === 'string' + ? JSON.parse(withData.drawingData) + : withData.drawingData; + console.debug('DiaryView: drawingData aus images für', pa.id, 'imageId=', withData.id, parsedImg); + return parsedImg; + } + } + } catch (e) { + console.warn('DiaryView: drawingData parse error:', e); + } + return null; + }, async init() { if (this.isAuthenticated && this.currentClub) { const response = await apiClient.get(`/diary/${this.currentClub}`); @@ -1119,17 +1160,43 @@ export default { }, closeImage() { this.showImage = false; + this.showRenderModal = false; + this.renderModalData = null; this.imageUrl = ''; }, - showActivityImage(imageLink) { - // Erstelle vollständige URL mit korrektem Port - if (imageLink && imageLink.startsWith('/api/')) { - this.imageUrl = `http://localhost:3000${imageLink}`; - } else { - this.imageUrl = imageLink; + async showActivityImage(imageLink) { + try { + // Vorherige Object-URL freigeben + if (this.imageUrl && this.imageUrl.startsWith('blob:')) { + URL.revokeObjectURL(this.imageUrl); + } + + if (imageLink && imageLink.startsWith('/api/')) { + // Über API mit Auth und als Blob laden → vermeidet ORB + const path = imageLink.replace(/^\/api\//, ''); + const resp = await apiClient.get(`/${path}`, { responseType: 'blob' }); + this.imageUrl = URL.createObjectURL(resp.data); + } else { + this.imageUrl = imageLink; + } + this.showImage = true; + } catch (e) { + console.error('Bild laden fehlgeschlagen:', e); + alert('Bild konnte nicht geladen werden.'); + } + }, + async openActivityVisual(pa) { + // Bevorzugt Rendering, ansonsten Bild + const data = this.drawingDataFor(pa); + if (data) { + // Erzeuge temporär ein PNG im Client: Canvas offscreen rendern über Renderer-Komponente ist aufwändig. + // Einfacher: öffne ein kleines Modal mit der Renderer-Komponente statt imageUrl. + this.renderModalData = data; + this.showRenderModal = true; + } else if (pa.imageLink) { + await this.showActivityImage(pa.imageLink); } - this.showImage = true; }, async loadMemberImage(member) { try { @@ -1915,6 +1982,12 @@ img { margin: 0 auto; } +.memberImage > div, .memberImage canvas { + /* falls Komponenten-Inhalt (Renderer) da ist, mittig zeigen */ + display: block; + margin: 0 auto; +} + .groups { display: flex; flex-direction: row;