Add training times management and enhance diary view with group selection dialog
This commit introduces the `TrainingTime` model and related functionality, allowing for the management of training times associated with training groups. The backend is updated to include new routes for training times, while the frontend is enhanced with a new dialog in the `DiaryView` for selecting training groups and suggesting available training times. This improves user experience by streamlining the process of scheduling training sessions and managing associated data.
This commit is contained in:
483
frontend/src/components/TrainingTimesTab.vue
Normal file
483
frontend/src/components/TrainingTimesTab.vue
Normal file
@@ -0,0 +1,483 @@
|
||||
<template>
|
||||
<div class="training-times-tab">
|
||||
<div v-if="loading" class="loading">Lade Trainingszeiten...</div>
|
||||
|
||||
<div v-else class="groups-section">
|
||||
<div
|
||||
v-for="group in groups"
|
||||
:key="group.id"
|
||||
class="group-card"
|
||||
>
|
||||
<div class="group-header">
|
||||
<h3>{{ group.name }}</h3>
|
||||
<button
|
||||
@click="showAddTimeForm(group.id)"
|
||||
class="btn-primary btn-small"
|
||||
>
|
||||
+ Zeit hinzufügen
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Add Time Form -->
|
||||
<div v-if="addTimeFormGroupId === group.id" class="add-time-form">
|
||||
<div class="form-row">
|
||||
<label>
|
||||
<span>Wochentag:</span>
|
||||
<select v-model="newTime.weekday" class="input-field">
|
||||
<option :value="0">Sonntag</option>
|
||||
<option :value="1">Montag</option>
|
||||
<option :value="2">Dienstag</option>
|
||||
<option :value="3">Mittwoch</option>
|
||||
<option :value="4">Donnerstag</option>
|
||||
<option :value="5">Freitag</option>
|
||||
<option :value="6">Samstag</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>Von:</span>
|
||||
<input
|
||||
v-model="newTime.startTime"
|
||||
type="time"
|
||||
class="input-field"
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
<span>Bis:</span>
|
||||
<input
|
||||
v-model="newTime.endTime"
|
||||
type="time"
|
||||
class="input-field"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button @click="createTime(group.id)" class="btn-primary">Erstellen</button>
|
||||
<button @click="cancelAddTime" class="btn-secondary">Abbrechen</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Times List -->
|
||||
<div v-if="group.trainingTimes && group.trainingTimes.length > 0" class="times-list">
|
||||
<div
|
||||
v-for="time in group.trainingTimes"
|
||||
:key="time.id"
|
||||
class="time-item"
|
||||
>
|
||||
<div class="time-info">
|
||||
<span class="weekday">{{ getWeekdayName(time.weekday) }}</span>
|
||||
<span class="time-range">{{ formatTime(time.startTime) }} - {{ formatTime(time.endTime) }}</span>
|
||||
</div>
|
||||
<div class="time-actions">
|
||||
<button
|
||||
@click="editTime(time)"
|
||||
class="btn-icon"
|
||||
title="Bearbeiten"
|
||||
>
|
||||
✏️
|
||||
</button>
|
||||
<button
|
||||
@click="deleteTime(time.id)"
|
||||
class="btn-icon btn-danger"
|
||||
title="Löschen"
|
||||
>
|
||||
🗑️
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="no-times">
|
||||
Keine Trainingszeiten definiert
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit Time Dialog -->
|
||||
<div v-if="editingTime" class="edit-time-dialog">
|
||||
<div class="dialog-content">
|
||||
<h3>Trainingszeit bearbeiten</h3>
|
||||
<div class="form-row">
|
||||
<label>
|
||||
<span>Wochentag:</span>
|
||||
<select v-model="editingTime.weekday" class="input-field">
|
||||
<option :value="0">Sonntag</option>
|
||||
<option :value="1">Montag</option>
|
||||
<option :value="2">Dienstag</option>
|
||||
<option :value="3">Mittwoch</option>
|
||||
<option :value="4">Donnerstag</option>
|
||||
<option :value="5">Freitag</option>
|
||||
<option :value="6">Samstag</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>Von:</span>
|
||||
<input
|
||||
v-model="editingTime.startTime"
|
||||
type="time"
|
||||
class="input-field"
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
<span>Bis:</span>
|
||||
<input
|
||||
v-model="editingTime.endTime"
|
||||
type="time"
|
||||
class="input-field"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button @click="saveTime" class="btn-primary">Speichern</button>
|
||||
<button @click="cancelEdit" class="btn-secondary">Abbrechen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import apiClient from '../apiClient.js';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { getSafeErrorMessage } from '../utils/errorMessages.js';
|
||||
|
||||
export default {
|
||||
name: 'TrainingTimesTab',
|
||||
computed: {
|
||||
...mapGetters(['currentClub']),
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
groups: [],
|
||||
loading: false,
|
||||
addTimeFormGroupId: null,
|
||||
newTime: {
|
||||
weekday: 1,
|
||||
startTime: '',
|
||||
endTime: ''
|
||||
},
|
||||
editingTime: null
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.loadTrainingTimes();
|
||||
},
|
||||
methods: {
|
||||
async loadTrainingTimes() {
|
||||
this.loading = true;
|
||||
try {
|
||||
const response = await apiClient.get(`/training-times/${this.currentClub}`);
|
||||
this.groups = Array.isArray(response.data) ? response.data : [];
|
||||
} catch (error) {
|
||||
console.error('[loadTrainingTimes] Error:', error);
|
||||
const msg = getSafeErrorMessage(error, 'Fehler beim Laden der Trainingszeiten');
|
||||
alert(msg);
|
||||
this.groups = [];
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
showAddTimeForm(groupId) {
|
||||
this.addTimeFormGroupId = groupId;
|
||||
this.newTime = {
|
||||
weekday: 1,
|
||||
startTime: '',
|
||||
endTime: ''
|
||||
};
|
||||
},
|
||||
|
||||
cancelAddTime() {
|
||||
this.addTimeFormGroupId = null;
|
||||
this.newTime = {
|
||||
weekday: 1,
|
||||
startTime: '',
|
||||
endTime: ''
|
||||
};
|
||||
},
|
||||
|
||||
async createTime(groupId) {
|
||||
if (!this.newTime.startTime || !this.newTime.endTime) {
|
||||
alert('Bitte füllen Sie alle Felder aus');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await apiClient.post(`/training-times/${this.currentClub}`, {
|
||||
trainingGroupId: groupId,
|
||||
weekday: this.newTime.weekday,
|
||||
startTime: this.newTime.startTime,
|
||||
endTime: this.newTime.endTime
|
||||
});
|
||||
this.cancelAddTime();
|
||||
await this.loadTrainingTimes();
|
||||
} catch (error) {
|
||||
console.error('[createTime] Error:', error);
|
||||
const msg = getSafeErrorMessage(error, 'Fehler beim Erstellen der Trainingszeit');
|
||||
alert(msg);
|
||||
}
|
||||
},
|
||||
|
||||
editTime(time) {
|
||||
this.editingTime = {
|
||||
id: time.id,
|
||||
weekday: time.weekday,
|
||||
startTime: this.formatTimeForInput(time.startTime),
|
||||
endTime: this.formatTimeForInput(time.endTime)
|
||||
};
|
||||
},
|
||||
|
||||
cancelEdit() {
|
||||
this.editingTime = null;
|
||||
},
|
||||
|
||||
async saveTime() {
|
||||
if (!this.editingTime.startTime || !this.editingTime.endTime) {
|
||||
alert('Bitte füllen Sie alle Felder aus');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await apiClient.put(`/training-times/${this.currentClub}/${this.editingTime.id}`, {
|
||||
weekday: this.editingTime.weekday,
|
||||
startTime: this.editingTime.startTime,
|
||||
endTime: this.editingTime.endTime
|
||||
});
|
||||
this.cancelEdit();
|
||||
await this.loadTrainingTimes();
|
||||
} catch (error) {
|
||||
console.error('[saveTime] Error:', error);
|
||||
const msg = getSafeErrorMessage(error, 'Fehler beim Speichern der Trainingszeit');
|
||||
alert(msg);
|
||||
}
|
||||
},
|
||||
|
||||
async deleteTime(timeId) {
|
||||
if (!confirm('Möchten Sie diese Trainingszeit wirklich löschen?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await apiClient.delete(`/training-times/${this.currentClub}/${timeId}`);
|
||||
await this.loadTrainingTimes();
|
||||
} catch (error) {
|
||||
console.error('[deleteTime] Error:', error);
|
||||
const msg = getSafeErrorMessage(error, 'Fehler beim Löschen der Trainingszeit');
|
||||
alert(msg);
|
||||
}
|
||||
},
|
||||
|
||||
getWeekdayName(weekday) {
|
||||
const days = ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag'];
|
||||
return days[weekday] || '';
|
||||
},
|
||||
|
||||
formatTime(time) {
|
||||
if (!time) return '';
|
||||
// Time comes as HH:MM:SS or HH:MM, we want HH:MM
|
||||
return time.substring(0, 5);
|
||||
},
|
||||
|
||||
formatTimeForInput(time) {
|
||||
if (!time) return '';
|
||||
// Ensure format HH:MM for time input
|
||||
return time.substring(0, 5);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.training-times-tab {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.groups-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.group-card {
|
||||
background: #fff;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.group-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.group-header h3 {
|
||||
margin: 0;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.btn-small {
|
||||
padding: 6px 12px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.add-time-form {
|
||||
background: #f9f9f9;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 6px;
|
||||
padding: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.form-row label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
flex: 1;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.form-row span {
|
||||
font-size: 0.9rem;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.input-field {
|
||||
padding: 6px 8px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.times-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.time-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
background: #f9f9f9;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.time-info {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.weekday {
|
||||
font-weight: 600;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.time-range {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.time-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 1.2rem;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.btn-icon:hover {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background: #fee;
|
||||
}
|
||||
|
||||
.no-times {
|
||||
color: #999;
|
||||
font-style: italic;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.edit-time-dialog {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.dialog-content {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 24px;
|
||||
max-width: 500px;
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
.dialog-content h3 {
|
||||
margin: 0 0 20px 0;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #28a745;
|
||||
color: #fff;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #218838;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #6c757d;
|
||||
color: #fff;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #5a6268;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -16,6 +16,12 @@
|
||||
>
|
||||
👨👩👧👦 Trainingsgruppen
|
||||
</button>
|
||||
<button
|
||||
:class="['tab-button', { active: activeTab === 'training-times' }]"
|
||||
@click="activeTab = 'training-times'"
|
||||
>
|
||||
🕐 Trainingszeiten
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Settings Tab -->
|
||||
@@ -56,17 +62,25 @@
|
||||
<TrainingGroupsTab />
|
||||
</div>
|
||||
<!-- End Training Groups Tab -->
|
||||
|
||||
<!-- Training Times Tab -->
|
||||
<div v-if="activeTab === 'training-times'">
|
||||
<TrainingTimesTab />
|
||||
</div>
|
||||
<!-- End Training Times Tab -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import apiClient from '../apiClient';
|
||||
import TrainingGroupsTab from '../components/TrainingGroupsTab.vue';
|
||||
import TrainingTimesTab from '../components/TrainingTimesTab.vue';
|
||||
|
||||
export default {
|
||||
name: 'ClubSettings',
|
||||
components: {
|
||||
TrainingGroupsTab,
|
||||
TrainingTimesTab,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
@@ -4,12 +4,13 @@
|
||||
<div class="diary-header-row">
|
||||
<label>Datum:
|
||||
<select v-model="date" @change="handleDateChange">
|
||||
<option value="new">Neu anlegen</option>
|
||||
<option :value="null" v-if="dates.length === 0">Keine Einträge</option>
|
||||
<option v-for="entry in dates" :key="entry.id" :value="entry">{{ getFormattedDate(entry.date) }}
|
||||
</option>
|
||||
</select>
|
||||
<button v-if="date && date !== 'new' && canDeleteCurrentDate" class="btn-secondary"
|
||||
<button v-if="date && canDeleteCurrentDate" class="btn-secondary"
|
||||
@click="deleteCurrentDate">Datum löschen</button>
|
||||
<button @click="openNewDateDialog" class="btn-primary">Neu anlegen</button>
|
||||
</label>
|
||||
<button
|
||||
class="btn-secondary gallery-trigger"
|
||||
@@ -19,7 +20,43 @@
|
||||
{{ galleryLoading ? 'Galerie wird erstellt…' : 'Mitglieder-Galerie' }}
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="showForm && date === 'new'">
|
||||
<!-- Training Group Selection Dialog -->
|
||||
<div v-if="showTrainingGroupDialog" class="dialog-overlay" @click.self="closeTrainingGroupDialog">
|
||||
<div class="dialog-content">
|
||||
<h3>Trainingsgruppe auswählen</h3>
|
||||
<div class="dialog-body">
|
||||
<label>
|
||||
<span>Trainingsgruppe:</span>
|
||||
<select v-model="selectedTrainingGroupId" class="input-field" @change="onTrainingGroupSelected">
|
||||
<option value="">Bitte wählen...</option>
|
||||
<option v-for="group in trainingGroups" :key="group.id" :value="group.id">
|
||||
{{ group.name }}
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
<div v-if="suggestedDate" class="suggestion-info">
|
||||
<p><strong>Vorschlag:</strong></p>
|
||||
<p>Nächster Termin: <strong>{{ getFormattedDate(suggestedDate) }}</strong></p>
|
||||
<p v-if="suggestedStartTime && suggestedEndTime">
|
||||
Zeit: <strong>{{ suggestedStartTime }} - {{ suggestedEndTime }}</strong>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dialog-actions">
|
||||
<button @click="applySuggestion" class="btn-primary" :disabled="!suggestedDate">
|
||||
Vorschlag übernehmen
|
||||
</button>
|
||||
<button @click="closeTrainingGroupDialog" class="btn-secondary">
|
||||
Abbrechen
|
||||
</button>
|
||||
<button @click="skipSuggestion" class="btn-secondary">
|
||||
Ohne Vorschlag fortfahren
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="showForm">
|
||||
<h3>Neues Datum anlegen</h3>
|
||||
<form @submit.prevent="createDate">
|
||||
<div>
|
||||
@@ -39,7 +76,7 @@
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div v-if="!showForm && date !== null && date !== 'new'">
|
||||
<div v-if="!showForm && date !== null">
|
||||
<h3>Trainingszeiten bearbeiten <span @click="toggleShowGeneralData" class="clickable">{{ showGeneralData ?
|
||||
'-' : '+' }}</span></h3>
|
||||
<form @submit.prevent="updateTrainingTimes" v-if="showGeneralData">
|
||||
@@ -54,7 +91,7 @@
|
||||
<button type="submit">Zeiten aktualisieren</button>
|
||||
</form>
|
||||
</div>
|
||||
<div v-if="date !== 'new' && date !== null" class="diary-content">
|
||||
<div v-if="date !== null && !showForm" class="diary-content">
|
||||
<!-- Tab-Navigation für Mobile -->
|
||||
<div class="mobile-tabs">
|
||||
<button
|
||||
@@ -752,6 +789,13 @@ export default {
|
||||
trainingEnd: '',
|
||||
members: [],
|
||||
participants: [],
|
||||
showTrainingGroupDialog: false,
|
||||
trainingGroups: [],
|
||||
selectedTrainingGroupId: null,
|
||||
trainingTimes: [],
|
||||
suggestedDate: null,
|
||||
suggestedStartTime: null,
|
||||
suggestedEndTime: null,
|
||||
newActivity: '',
|
||||
activities: [],
|
||||
notes: [],
|
||||
@@ -891,7 +935,7 @@ export default {
|
||||
},
|
||||
|
||||
canDeleteCurrentDate() {
|
||||
if (!this.date || this.date === 'new') return false;
|
||||
if (!this.date) return false;
|
||||
|
||||
// Prüfe ob keine Inhalte vorhanden sind
|
||||
const hasTrainingPlan = this.trainingPlan && this.trainingPlan.length > 0;
|
||||
@@ -943,7 +987,7 @@ export default {
|
||||
this.showGalleryDialog = true;
|
||||
},
|
||||
async handleGalleryMemberClick(member) {
|
||||
if (!this.date || this.date === 'new') {
|
||||
if (!this.date) {
|
||||
return;
|
||||
}
|
||||
console.log('[handleGalleryMemberClick] Clicked member:', member);
|
||||
@@ -1044,7 +1088,7 @@ export default {
|
||||
|
||||
async autoSelectDateWithEntries() {
|
||||
if (this.dates.length === 0) {
|
||||
this.date = 'new';
|
||||
this.date = null;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1062,9 +1106,13 @@ export default {
|
||||
}
|
||||
|
||||
// 2. Falls heutiges Datum nicht gefunden oder keine Einträge vorhanden
|
||||
// → Bleibe bei "Neu anlegen"
|
||||
this.date = 'new';
|
||||
this.showForm = true;
|
||||
// → Wähle das erste Datum aus der Liste
|
||||
if (this.dates.length > 0) {
|
||||
this.date = this.dates[0];
|
||||
await this.handleDateChange();
|
||||
} else {
|
||||
this.date = null;
|
||||
}
|
||||
},
|
||||
|
||||
async countEntriesForDate(dateId) {
|
||||
@@ -1104,9 +1152,16 @@ export default {
|
||||
this.newDate = today;
|
||||
},
|
||||
|
||||
async openNewDateDialog() {
|
||||
// Zeige Dialog zur Trainingsgruppen-Auswahl
|
||||
await this.loadTrainingGroups();
|
||||
this.showTrainingGroupDialog = true;
|
||||
this.showForm = false;
|
||||
},
|
||||
|
||||
async handleDateChange() {
|
||||
this.showForm = this.date === 'new';
|
||||
if (this.date && this.date !== 'new') {
|
||||
this.showForm = false;
|
||||
if (this.date) {
|
||||
const dateId = this.date.id;
|
||||
const response = await apiClient.get(`/diary/${this.currentClub}`);
|
||||
const dateData = response.data.find(entry => entry.id === dateId);
|
||||
@@ -1184,6 +1239,126 @@ export default {
|
||||
// Erstelle ein neues Array, um Vue-Reaktivität sicherzustellen
|
||||
this.members = Array.isArray(response.data) ? [...response.data] : [];
|
||||
},
|
||||
|
||||
async loadTrainingGroups() {
|
||||
try {
|
||||
const response = await apiClient.get(`/training-groups/${this.currentClub}`);
|
||||
this.trainingGroups = Array.isArray(response.data) ? response.data : [];
|
||||
} catch (error) {
|
||||
console.error('[loadTrainingGroups] Error:', error);
|
||||
this.trainingGroups = [];
|
||||
}
|
||||
},
|
||||
|
||||
async onTrainingGroupSelected() {
|
||||
if (!this.selectedTrainingGroupId) {
|
||||
this.suggestedDate = null;
|
||||
this.suggestedStartTime = null;
|
||||
this.suggestedEndTime = null;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Lade Trainingszeiten für diese Gruppe
|
||||
const response = await apiClient.get(`/training-times/${this.currentClub}`);
|
||||
const groups = Array.isArray(response.data) ? response.data : [];
|
||||
const selectedGroup = groups.find(g => g.id === parseInt(this.selectedTrainingGroupId));
|
||||
|
||||
if (!selectedGroup || !selectedGroup.trainingTimes || selectedGroup.trainingTimes.length === 0) {
|
||||
this.suggestedDate = null;
|
||||
this.suggestedStartTime = null;
|
||||
this.suggestedEndTime = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// Finde den nächsten verfügbaren Wochentag
|
||||
const nextDate = this.findNextAvailableDate(selectedGroup.trainingTimes);
|
||||
if (nextDate) {
|
||||
this.suggestedDate = nextDate.date;
|
||||
this.suggestedStartTime = nextDate.startTime;
|
||||
this.suggestedEndTime = nextDate.endTime;
|
||||
} else {
|
||||
this.suggestedDate = null;
|
||||
this.suggestedStartTime = null;
|
||||
this.suggestedEndTime = null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[onTrainingGroupSelected] Error:', error);
|
||||
this.suggestedDate = null;
|
||||
this.suggestedStartTime = null;
|
||||
this.suggestedEndTime = null;
|
||||
}
|
||||
},
|
||||
|
||||
findNextAvailableDate(trainingTimes) {
|
||||
if (!trainingTimes || trainingTimes.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Sortiere Trainingszeiten nach Wochentag und Startzeit
|
||||
const sortedTimes = [...trainingTimes].sort((a, b) => {
|
||||
if (a.weekday !== b.weekday) return a.weekday - b.weekday;
|
||||
return a.startTime.localeCompare(b.startTime);
|
||||
});
|
||||
|
||||
const today = new Date();
|
||||
|
||||
// Prüfe die nächsten 14 Tage (2 Wochen)
|
||||
for (let dayOffset = 0; dayOffset < 14; dayOffset++) {
|
||||
const checkDate = new Date(today);
|
||||
checkDate.setDate(today.getDate() + dayOffset);
|
||||
const checkWeekday = checkDate.getDay();
|
||||
|
||||
// Finde Trainingszeiten für diesen Wochentag
|
||||
const timesForWeekday = sortedTimes.filter(tt => tt.weekday === checkWeekday);
|
||||
|
||||
if (timesForWeekday.length > 0) {
|
||||
// Nimm die erste Trainingszeit für diesen Tag
|
||||
const time = timesForWeekday[0];
|
||||
// Prüfe, ob dieses Datum bereits existiert
|
||||
const dateStr = checkDate.toISOString().split('T')[0];
|
||||
const exists = this.dates.some(d => d.date === dateStr);
|
||||
|
||||
if (!exists) {
|
||||
return {
|
||||
date: dateStr,
|
||||
startTime: this.formatTimeForInput(time.startTime),
|
||||
endTime: this.formatTimeForInput(time.endTime)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
formatTimeForInput(time) {
|
||||
if (!time) return '';
|
||||
return time.substring(0, 5); // HH:MM für time input
|
||||
},
|
||||
|
||||
applySuggestion() {
|
||||
if (this.suggestedDate) {
|
||||
this.newDate = this.suggestedDate;
|
||||
this.trainingStart = this.suggestedStartTime || '';
|
||||
this.trainingEnd = this.suggestedEndTime || '';
|
||||
}
|
||||
this.closeTrainingGroupDialog();
|
||||
this.showForm = true;
|
||||
},
|
||||
|
||||
skipSuggestion() {
|
||||
this.closeTrainingGroupDialog();
|
||||
this.showForm = true;
|
||||
},
|
||||
|
||||
closeTrainingGroupDialog() {
|
||||
this.showTrainingGroupDialog = false;
|
||||
this.selectedTrainingGroupId = null;
|
||||
this.suggestedDate = null;
|
||||
this.suggestedStartTime = null;
|
||||
this.suggestedEndTime = null;
|
||||
},
|
||||
|
||||
async loadParticipants(dateId) {
|
||||
const response = await apiClient.get(`/participants/${dateId}`);
|
||||
@@ -2699,7 +2874,7 @@ export default {
|
||||
|
||||
async handleParticipantAdded(data) {
|
||||
// Nur aktualisieren, wenn das aktuelle Datum betroffen ist
|
||||
if (this.date && this.date !== 'new' && this.date.id === data.dateId) {
|
||||
if (this.date && this.date.id === data.dateId) {
|
||||
// Lade Teilnehmer neu
|
||||
await this.loadParticipants(data.dateId);
|
||||
}
|
||||
@@ -2707,7 +2882,7 @@ export default {
|
||||
|
||||
async handleParticipantRemoved(data) {
|
||||
// Nur aktualisieren, wenn das aktuelle Datum betroffen ist
|
||||
if (this.date && this.date !== 'new' && this.date.id === data.dateId) {
|
||||
if (this.date && this.date.id === data.dateId) {
|
||||
// Entferne aus participants-Array
|
||||
this.participants = this.participants.filter(memberId => memberId !== data.participantId);
|
||||
// Entferne aus Maps
|
||||
@@ -2745,7 +2920,7 @@ export default {
|
||||
|
||||
async handleDiaryNoteAdded(data) {
|
||||
// Nur aktualisieren, wenn das aktuelle Datum betroffen ist
|
||||
if (this.date && this.date !== 'new' && this.date.id === data.dateId) {
|
||||
if (this.date && this.date.id === data.dateId) {
|
||||
// Lade Notizen neu, falls das betroffene Mitglied ausgewählt ist
|
||||
if (this.selectedMember && data.note.memberId === this.selectedMember.id) {
|
||||
try {
|
||||
@@ -2760,7 +2935,7 @@ export default {
|
||||
|
||||
async handleDiaryNoteDeleted(data) {
|
||||
// Nur aktualisieren, wenn das aktuelle Datum betroffen ist
|
||||
if (this.date && this.date !== 'new' && this.date.id === data.dateId) {
|
||||
if (this.date && this.date.id === data.dateId) {
|
||||
// Entferne Notiz aus notes-Array
|
||||
this.notes = this.notes.filter(note => note.id !== data.noteId);
|
||||
}
|
||||
@@ -2768,7 +2943,7 @@ export default {
|
||||
|
||||
async handleDiaryTagAdded(data) {
|
||||
// Nur aktualisieren, wenn das aktuelle Datum betroffen ist
|
||||
if (this.date && this.date !== 'new' && this.date.id === data.dateId) {
|
||||
if (this.date && this.date.id === data.dateId) {
|
||||
// Lade Tags neu
|
||||
await this.loadTags();
|
||||
// Aktualisiere selectedActivityTags
|
||||
@@ -2783,7 +2958,7 @@ export default {
|
||||
|
||||
async handleDiaryTagRemoved(data) {
|
||||
// Nur aktualisieren, wenn das aktuelle Datum betroffen ist
|
||||
if (this.date && this.date !== 'new' && this.date.id === data.dateId) {
|
||||
if (this.date && this.date.id === data.dateId) {
|
||||
// Entferne Tag aus selectedActivityTags
|
||||
this.selectedActivityTags = this.selectedActivityTags.filter(t => t.id !== data.tagId);
|
||||
}
|
||||
@@ -2791,7 +2966,7 @@ export default {
|
||||
|
||||
async handleDiaryDateUpdated(data) {
|
||||
// Nur aktualisieren, wenn das aktuelle Datum betroffen ist
|
||||
if (this.date && this.date !== 'new' && this.date.id === data.dateId) {
|
||||
if (this.date && this.date.id === data.dateId) {
|
||||
// Aktualisiere Trainingszeiten
|
||||
if (data.updates.trainingStart !== undefined) {
|
||||
this.trainingStart = data.updates.trainingStart;
|
||||
@@ -2804,7 +2979,7 @@ export default {
|
||||
|
||||
async handleActivityMemberAdded(data) {
|
||||
// Nur aktualisieren, wenn das aktuelle Datum betroffen ist
|
||||
if (this.date && this.date !== 'new' && this.date.id === data.dateId) {
|
||||
if (this.date && this.date.id === data.dateId) {
|
||||
// Lade Training Plan neu
|
||||
try {
|
||||
this.trainingPlan = await apiClient.get(`/diary-date-activities/${this.currentClub}/${this.date.id}`).then(response => response.data);
|
||||
@@ -2817,7 +2992,7 @@ export default {
|
||||
|
||||
async handleActivityMemberRemoved(data) {
|
||||
// Nur aktualisieren, wenn das aktuelle Datum betroffen ist
|
||||
if (this.date && this.date !== 'new' && this.date.id === data.dateId) {
|
||||
if (this.date && this.date.id === data.dateId) {
|
||||
// Lade Training Plan neu
|
||||
try {
|
||||
this.trainingPlan = await apiClient.get(`/diary-date-activities/${this.currentClub}/${this.date.id}`).then(response => response.data);
|
||||
@@ -2830,7 +3005,7 @@ export default {
|
||||
|
||||
async handleActivityChanged(data) {
|
||||
// Nur aktualisieren, wenn das aktuelle Datum betroffen ist
|
||||
if (this.date && this.date !== 'new' && this.date.id === data.dateId) {
|
||||
if (this.date && this.date.id === data.dateId) {
|
||||
// Lade Training Plan neu
|
||||
try {
|
||||
this.trainingPlan = await apiClient.get(`/diary-date-activities/${this.currentClub}/${this.date.id}`).then(response => response.data);
|
||||
@@ -3803,6 +3978,101 @@ img {
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.dialog-content {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 24px;
|
||||
max-width: 500px;
|
||||
width: 90%;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.dialog-content h3 {
|
||||
margin: 0 0 20px 0;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.dialog-body {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.dialog-body label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.dialog-body .input-field {
|
||||
padding: 8px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.suggestion-info {
|
||||
background: #e3f2fd;
|
||||
border: 1px solid #90caf9;
|
||||
border-radius: 4px;
|
||||
padding: 12px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.suggestion-info p {
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
.dialog-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
justify-content: flex-end;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.dialog-actions button {
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.dialog-actions .btn-primary {
|
||||
background: #28a745;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.dialog-actions .btn-primary:hover:not(:disabled) {
|
||||
background: #218838;
|
||||
}
|
||||
|
||||
.dialog-actions .btn-primary:disabled {
|
||||
background: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.dialog-actions .btn-secondary {
|
||||
background: #6c757d;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.dialog-actions .btn-secondary:hover {
|
||||
background: #5a6268;
|
||||
}
|
||||
|
||||
@media (min-width: 769px) {
|
||||
.mobile-tabs {
|
||||
display: none !important;
|
||||
|
||||
Reference in New Issue
Block a user