Add updateGroupActivity method and corresponding route for editing group activities

This commit introduces the updateGroupActivity method in the diaryDateActivityController, allowing users to update existing group activities by linking them to predefined activities. The method includes error handling and emits a socket event upon successful updates. Additionally, the diaryDateActivityRoutes file is updated to include a new PUT route for updating group activities. Frontend changes in DiaryView enhance the user experience by enabling inline editing of group activities, including search functionality for predefined activities.
This commit is contained in:
Torsten Schulz (local)
2025-11-17 10:12:21 +01:00
parent b7bbb92f86
commit b906ac64b3
5 changed files with 285 additions and 40 deletions

View File

@@ -352,7 +352,9 @@
"trainingTimesUpdated": "Trainingszeiten erfolgreich aktualisiert.",
"formMarkedAsHandedOver": "Mitgliedsformular als ausgehändigt markiert",
"errorMarkingForm": "Fehler beim Markieren des Mitgliedsformulars",
"dateNoLongerCurrent": "Ausgewähltes Datum war nicht mehr aktuell. Bitte erneut versuchen."
"dateNoLongerCurrent": "Ausgewähltes Datum war nicht mehr aktuell. Bitte erneut versuchen.",
"activityRequired": "Bitte geben Sie eine Aktivität ein.",
"activityNotFound": "Aktivität nicht gefunden. Bitte wählen Sie eine aus der Liste aus."
},
"home": {
"welcome": "Willkommen im TrainingsTagebuch",

View File

@@ -176,6 +176,15 @@
<span v-else-if="editingActivityId === item.id">
<div
style="display: flex; gap: 5px; align-items: center;">
<button
v-if="item.predefinedActivity || hasItemDrawingData(item)"
type="button"
@click.stop="editActivity(item)"
:title="$t('diary.createDrawing')"
style="margin: 0; padding: 4px 8px; background: #28a745; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 16px; min-width: 32px; height: 32px; display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0;"
>
🎨
</button>
<div style="flex: 1; position: relative;">
<input type="text" v-model="editingActivityText"
@input="onEditInputChangeText(item)"
@@ -273,11 +282,6 @@
<td></td>
<td colspan="2">
<div style="display: flex; gap: 0.5rem; align-items: center; padding: 0.5rem; background: #f0f0f0; border-radius: 4px;">
<select v-model="newPlanItem.groupId" style="flex: 1;">
<option value="">{{ $t('diary.selectGroup') }}</option>
<option v-for="group in groups" :key="group.id" :value="String(group.id)">
{{ group.name }}</option>
</select>
<div style="flex: 1; position: relative;">
<input type="text" v-model="newPlanItem.activity" :placeholder="$t('diary.activityPlaceholder')"
@input="onNewItemInputChange" style="width: 100%;" />
@@ -290,6 +294,11 @@
</div>
</div>
</div>
<select v-model="newPlanItem.groupId" style="flex: 1;">
<option value="">{{ $t('diary.selectGroup') }}</option>
<option v-for="group in groups" :key="group.id" :value="String(group.id)">
{{ group.name }}</option>
</select>
</div>
</td>
<td></td>
@@ -303,7 +312,38 @@
<td></td>
<td></td>
<td>
<span class="activity-label"
<span v-if="Number(editingGroupActivityId) === Number(groupItem.id)">
<div style="display: flex; gap: 5px; align-items: center;">
<button
type="button"
@click.stop="editGroupActivity(groupItem)"
:title="$t('diary.createDrawing')"
style="margin: 0; padding: 4px 8px; background: #28a745; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 16px; min-width: 32px; height: 32px; display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0;"
>
🎨
</button>
<div style="flex: 1; position: relative;">
<input type="text" v-model="editingGroupActivityText"
@input="onEditGroupActivityInputChange(groupItem)"
@keyup.enter="saveGroupActivityEdit(groupItem)"
@keyup.esc="cancelGroupActivityEdit"
style="width: 100%;" />
<div v-if="editShowDropdown && Number(editSearchForId) === Number(groupItem.id) && editSearchResults.length"
class="dropdown" style="max-height: 9.5em;">
<div v-for="s in editSearchResults" :key="s.id"
@click="chooseEditGroupActivitySuggestion(s, groupItem)">
<span v-if="s.code && s.code.trim() !== ''"><strong>[{{ s.code }}]</strong> </span>{{ s.name }}
</div>
</div>
</div>
<button @click="saveGroupActivityEdit(groupItem)" class="btn-primary"
style="padding: 2px 8px; font-size: 12px;"></button>
<button @click="cancelGroupActivityEdit" class="btn-secondary"
style="padding: 2px 8px; font-size: 12px;"></button>
</div>
</span>
<span v-else @click="startGroupActivityEdit(groupItem)"
class="clickable activity-label"
:title="(groupItem.groupPredefinedActivity && groupItem.groupPredefinedActivity.name) ? groupItem.groupPredefinedActivity.name : ''">
<!-- Icon öffnet Rendering (falls vorhanden) oder Bild im Modal -->
<span v-if="hasActivityVisual(groupItem.groupPredefinedActivity)"
@@ -321,8 +361,6 @@
<td></td>
<td>
<div style="position: relative; display: inline-block;">
<button @click="editGroupActivity(groupItem)" :title="$t('diary.edit')"
class="person-btn" style="background: #ffc107; color: #000;"></button>
<button @click="toggleGroupActivityMembers(groupItem)" :title="$t('diary.assignParticipants')"
class="person-btn">👤</button>
<button @click="removeGroupActivity(groupItem.id)" class="trash-btn" :title="$t('diary.delete')">🗑</button>
@@ -369,7 +407,7 @@
<button v-if="parentIsTimeblock()"
@click="addGroupActivity">{{ $t('diary.addGroupActivity') }}</button>
</td>
<td v-if="addNewItem || addNewGroupActivity">
<td v-if="addNewItem">
<div v-if="addtype === 'activity'" style="display: flex; align-items: center; gap: 0.5rem;">
<button
type="button"
@@ -397,22 +435,8 @@
</div>
</td>
<td v-else-if="addNewTimeblock">{{ $t('diary.timeblock') }}</td>
<td v-if="addNewGroupActivity" colspan="2">
<td v-if="addNewGroupActivity && !selectedTimeblockId" colspan="2">
<div style="display: flex; gap: 0.5rem; align-items: center;">
<select v-model="newPlanItem.groupId" style="flex: 1;">
<option value="">{{ $t('diary.selectGroup') }}</option>
<option v-for="group in groups" :key="group.id" :value="String(group.id)">
{{ group.name }}</option>
</select>
<button
type="button"
class="btn-palette"
@click="showDrawingDialog = true"
:title="$t('diary.createDrawing')"
style="margin: 0; margin-left: 0;"
>
🎨
</button>
<div style="flex: 1; position: relative;">
<input type="text" v-model="newPlanItem.activity" placeholder="Aktivität"
@input="onNewItemInputChange" style="width: 100%;" />
@@ -425,6 +449,11 @@
</div>
</div>
</div>
<select v-model="newPlanItem.groupId" style="flex: 1;">
<option value="">{{ $t('diary.selectGroup') }}</option>
<option v-for="group in groups" :key="group.id" :value="String(group.id)">
{{ group.name }}</option>
</select>
</div>
</td>
<td v-else-if="addNewItem || addNewTimeblock"></td>
@@ -675,9 +704,9 @@
<!-- Court Drawing Dialog -->
<CourtDrawingDialog
v-model="showDrawingDialog"
:initial-drawing-data="editingGroupActivity ? drawingDataFor(editingGroupActivity.groupPredefinedActivity) : null"
:initial-code="editingGroupActivity && editingGroupActivity.groupPredefinedActivity ? (editingGroupActivity.groupPredefinedActivity.code || editingGroupActivity.groupPredefinedActivity.name) : null"
:initial-name="editingGroupActivity && editingGroupActivity.groupPredefinedActivity ? editingGroupActivity.groupPredefinedActivity.name : null"
:initial-drawing-data="editingGroupActivity ? (editingGroupActivity.groupPredefinedActivity ? drawingDataFor(editingGroupActivity.groupPredefinedActivity) : (editingGroupActivity.activityItem && editingGroupActivity.activityItem.drawingData ? (typeof editingGroupActivity.activityItem.drawingData === 'string' ? JSON.parse(editingGroupActivity.activityItem.drawingData) : editingGroupActivity.activityItem.drawingData) : null)) : null"
:initial-code="editingGroupActivity && editingGroupActivity.groupPredefinedActivity ? (editingGroupActivity.groupPredefinedActivity.code || editingGroupActivity.groupPredefinedActivity.name) : (editingGroupActivity && editingGroupActivity.activityItem ? editingGroupActivity.activityItem.activity : null)"
:initial-name="editingGroupActivity && editingGroupActivity.groupPredefinedActivity ? editingGroupActivity.groupPredefinedActivity.name : (editingGroupActivity && editingGroupActivity.activityItem ? editingGroupActivity.activityItem.activity : null)"
:initial-description="editingGroupActivity && editingGroupActivity.groupPredefinedActivity ? editingGroupActivity.groupPredefinedActivity.description : null"
@ok="handleDrawingDialogOkForDiary"
@close="editingGroupActivity = null"
@@ -895,6 +924,8 @@ export default {
groupActivityMembersOpenId: null,
groupActivityMembersMap: {}, // key: groupActivityId, value: Set(participantIds)
editingGroupActivity: null, // Gruppenaktivität, die gerade bearbeitet wird
editingGroupActivityId: null, // ID der Gruppenaktivität, die inline bearbeitet wird
editingGroupActivityText: '', // Text für inline-edit
// Schnell hinzufügen Dialog
showQuickAddDialog: false,
newMember: {
@@ -1013,6 +1044,20 @@ export default {
await this.toggleParticipant(member.memberId);
},
hasItemDrawingData(item) {
if (!item) return false;
try {
// Prüfe direktes drawingData am item
if (item.drawingData) {
if (typeof item.drawingData === 'string' && item.drawingData.trim() !== '') return true;
if (typeof item.drawingData === 'object' && Object.keys(item.drawingData).length > 0) return true;
}
// Prüfe auch über predefinedActivity
if (item.predefinedActivity && this.hasActivityVisual(item.predefinedActivity)) return true;
} catch (e) { }
return false;
},
hasActivityVisual(pa) {
if (!pa) return false;
try {
@@ -2483,28 +2528,60 @@ export default {
try {
const code = result.code.trim();
// Wenn eine Gruppenaktivität bearbeitet wird
// Wenn eine Gruppenaktivität oder normale Aktivität bearbeitet wird
if (this.editingGroupActivity && this.editingGroupActivity.groupPredefinedActivity) {
const predefinedActivityId = this.editingGroupActivity.groupPredefinedActivity.id;
// Aktualisiere die PredefinedActivity
const updateData = {
name: result.name || (result.fields && result.fields.name) || this.editingGroupActivity.groupPredefinedActivity.name || '',
code: code,
description: result.description || (result.fields && result.fields.description) || this.editingGroupActivity.groupPredefinedActivity.description || '',
durationText: this.editingGroupActivity.groupPredefinedActivity.durationText || '',
duration: this.editingGroupActivity.groupPredefinedActivity.duration || null,
imageLink: this.editingGroupActivity.groupPredefinedActivity.imageLink || '',
drawingData: result.drawingData || null
};
let finalPredefinedActivityId = predefinedActivityId;
await apiClient.put(`/predefined-activities/${predefinedActivityId}`, updateData);
// Wenn keine predefinedActivityId vorhanden ist, erstelle eine neue
if (!predefinedActivityId) {
const newActivityData = {
name: result.name || (result.fields && result.fields.name) || this.editingGroupActivity.groupPredefinedActivity.name || '',
code: code,
description: result.description || (result.fields && result.fields.description) || this.editingGroupActivity.groupPredefinedActivity.description || '',
drawingData: result.drawingData || null
};
const createResponse = await apiClient.post('/predefined-activities', newActivityData);
finalPredefinedActivityId = createResponse.data.id;
// Wenn es eine normale Aktivität war, verknüpfe die DiaryDateActivity mit der neuen PredefinedActivity
if (this.editingGroupActivity.isNormalActivity && this.editingGroupActivity.activityItem) {
const activityItem = this.editingGroupActivity.activityItem;
if (activityItem.id && this.currentClub) {
// currentClub ist die Club-ID (Zahl), nicht ein Objekt
await apiClient.put(`/diary-date-activities/${this.currentClub}/${activityItem.id}`, {
predefinedActivityId: finalPredefinedActivityId
});
}
}
} else {
// Aktualisiere die existierende PredefinedActivity
const updateData = {
name: result.name || (result.fields && result.fields.name) || this.editingGroupActivity.groupPredefinedActivity.name || '',
code: code,
description: result.description || (result.fields && result.fields.description) || this.editingGroupActivity.groupPredefinedActivity.description || '',
durationText: this.editingGroupActivity.groupPredefinedActivity.durationText || '',
duration: this.editingGroupActivity.groupPredefinedActivity.duration || null,
imageLink: this.editingGroupActivity.groupPredefinedActivity.imageLink || '',
drawingData: result.drawingData || null
};
await apiClient.put(`/predefined-activities/${predefinedActivityId}`, updateData);
}
// Lade den Trainingsplan neu
if (this.date && this.date.id) {
await this.loadTrainingPlan();
}
// Wenn es eine normale Aktivität war, beende auch den Inline-Edit-Modus
if (this.editingGroupActivity.isNormalActivity && this.editingGroupActivity.activityItem) {
this.editingActivityId = null;
this.editingActivityText = '';
}
this.editingGroupActivity = null;
return;
}
@@ -2570,6 +2647,39 @@ export default {
}
},
editActivity(item) {
if (!item) {
this.showInfo('Fehler', 'Keine Aktivität zum Bearbeiten gefunden', '', 'error');
return;
}
// Wenn keine predefinedActivity vorhanden ist, aber drawingData, erstelle eine temporäre Struktur
let predefinedActivity = item.predefinedActivity;
if (!predefinedActivity && item.drawingData) {
// Erstelle eine temporäre Struktur für die Bearbeitung
predefinedActivity = {
id: null,
name: item.activity || '',
code: item.activity || '',
description: '',
drawingData: item.drawingData
};
}
if (!predefinedActivity) {
this.showInfo('Fehler', 'Keine Aktivität zum Bearbeiten gefunden', '', 'error');
return;
}
// Verwende editingGroupActivity auch für normale Aktivitäten, da die Struktur ähnlich ist
this.editingGroupActivity = {
groupPredefinedActivity: predefinedActivity,
isNormalActivity: true,
activityItem: item
};
this.showDrawingDialog = true;
},
editGroupActivity(groupItem) {
if (!groupItem || !groupItem.groupPredefinedActivity) {
this.showInfo('Fehler', 'Keine Aktivität zum Bearbeiten gefunden', '', 'error');
@@ -2578,6 +2688,89 @@ export default {
this.editingGroupActivity = groupItem;
this.showDrawingDialog = true;
},
startGroupActivityEdit(groupItem) {
console.log('[startGroupActivityEdit] groupItem.id:', groupItem.id, 'type:', typeof groupItem.id);
this.editingGroupActivityId = Number(groupItem.id);
this.editSearchForId = Number(groupItem.id);
console.log('[startGroupActivityEdit] editingGroupActivityId set to:', this.editingGroupActivityId);
this.$nextTick(() => {
const inputs = this.$el.querySelectorAll('input[type="text"]');
const lastInput = inputs[inputs.length - 1];
if (lastInput) {
lastInput.focus();
}
});
this.editingGroupActivityText = groupItem.groupPredefinedActivity
? (groupItem.groupPredefinedActivity.code && groupItem.groupPredefinedActivity.code.trim() !== ''
? groupItem.groupPredefinedActivity.code
: groupItem.groupPredefinedActivity.name)
: '';
},
async onEditGroupActivityInputChange(groupItem) {
const term = this.editingGroupActivityText;
this.editSearchForId = groupItem.id;
if (!term || term.trim().length < 2) {
this.editShowDropdown = false;
this.editSearchResults = [];
return;
}
const results = await this.searchPredefinedActivities(term);
this.editSearchResults = results;
this.editShowDropdown = results.length > 0;
},
chooseEditGroupActivitySuggestion(s, groupItem) {
this.editingGroupActivityText = (s.code && s.code.trim() !== '') ? s.code : s.name;
this.editShowDropdown = false;
this.editSearchResults = [];
},
async saveGroupActivityEdit(groupItem) {
try {
if (!this.editingGroupActivityText || !this.editingGroupActivityText.trim()) {
this.showInfo(this.$t('messages.note'), this.$t('diary.activityRequired'), '', 'warning');
return;
}
// Suche nach der PredefinedActivity
const searchResults = await this.searchPredefinedActivities(this.editingGroupActivityText.trim());
const existing = searchResults.find(a => {
const searchTerm = this.editingGroupActivityText.trim().toLowerCase();
return (a.code && a.code.trim().toLowerCase() === searchTerm) ||
(a.name && a.name.trim().toLowerCase() === searchTerm);
});
if (!existing) {
this.showInfo(this.$t('messages.note'), this.$t('diary.activityNotFound'), '', 'warning');
return;
}
// Aktualisiere die Gruppenaktivität mit der neuen PredefinedActivity
await apiClient.put(`/diary-date-activities/group/${this.currentClub}/${groupItem.id}`, {
predefinedActivityId: existing.id
});
// Lade den Trainingsplan neu
await this.loadTrainingPlan();
this.editingGroupActivityId = null;
this.editingGroupActivityText = '';
this.editShowDropdown = false;
this.editSearchResults = [];
} catch (error) {
const msg = getSafeErrorMessage(error, 'Fehler beim Speichern der Gruppenaktivität');
this.showInfo('Fehler', msg, '', 'error');
}
},
cancelGroupActivityEdit() {
this.editingGroupActivityId = null;
this.editingGroupActivityText = '';
this.editShowDropdown = false;
this.editSearchResults = [];
},
async loadTrainingPlan() {
try {