Fixed Diary

This commit is contained in:
Torsten Schulz
2024-09-17 10:24:31 +02:00
parent eeda29e6ed
commit 868747b214
2 changed files with 473 additions and 48 deletions

View File

@@ -81,15 +81,18 @@ class PDFGenerator {
}
addTrainingPlan(clubName, trainingDate, trainingStart, trainingEnd, trainingPlan) {
const formattedDate = new Date(trainingDate).toLocaleDateString('de-DE');
const formattedStartTime = trainingStart.slice(0, 5);
const formattedEndTime = trainingEnd.slice(0, 5);
this.pdf.setFontSize(14);
this.pdf.setFont('helvetica', 'bold');
this.pdf.text(`${clubName} - Trainingsplan`, this.margin, this.yPos);
this.yPos += 10;
this.pdf.setFontSize(12);
this.pdf.setFont('helvetica', 'normal');
this.pdf.text(`Datum: ${trainingDate}`, this.margin, this.yPos);
this.pdf.text(`Datum: ${formattedDate}`, this.margin, this.yPos);
this.yPos += 7;
this.pdf.text(`Uhrzeit: ${trainingStart} - ${trainingEnd}`, this.margin, this.yPos);
this.pdf.text(`Uhrzeit: ${formattedStartTime} - ${formattedEndTime}`, this.margin, this.yPos);
this.yPos += 10;
this.pdf.setFont('helvetica', 'bold');
this.pdf.text('Uhrzeit', this.margin, this.yPos);
@@ -99,8 +102,8 @@ class PDFGenerator {
this.pdf.setFont('helvetica', 'normal');
trainingPlan.forEach((item, index) => {
const time = this.calculatePlanItemTime(index, trainingStart, trainingPlan);
this.pdf.text(time, this.margin, this.yPos);
this.pdf.text(item.activity, this.margin + 60, this.yPos);
this.pdf.text(time.slice(0, 5), this.margin, this.yPos);
this.pdf.text(item.predefinedActivity.name, this.margin + 60, this.yPos);
this.pdf.text(item.duration.toString(), this.margin + 150, this.yPos);
this.yPos += 7;

View File

@@ -3,14 +3,13 @@
<h2>Trainingstagebuch</h2>
<div>
<label>Datum:
<select v-model="selectedDate" @change="handleDateChange">
<select v-model="date" @change="handleDateChange">
<option value="new">Neu anlegen</option>
<option v-for="entry in dates" :key="entry.id" :value="entry">{{ entry.date }} </option>
</select>
</label>
</div>
<div v-if="showForm && selectedDate === 'new'">
<div v-if="showForm && date === 'new'">
<h3>Neues Datum anlegen</h3>
<form @submit.prevent="createDate">
<div>
@@ -29,8 +28,7 @@
<button type="submit">Datum anlegen</button>
</form>
</div>
<div v-if="!showForm && selectedDate !== null && selectedDate !== 'new'">
<div v-if="!showForm && date !== null && date !== 'new'">
<h3>Trainingszeiten bearbeiten</h3>
<form @submit.prevent="updateTrainingTimes">
<div>
@@ -45,7 +43,7 @@
</form>
</div>
<div v-if="selectedDate !== 'new' && selectedDate !== null">
<div v-if="date !== 'new' && date !== null">
<div class="columns">
<div class="column">
<h3>Trainingsplan</h3>
@@ -62,7 +60,7 @@
<tr v-for="(planItem, index) in trainingPlan" :key="planItem.id">
<td class="drag-handle"></td>
<td>{{ calculatePlanItemTime(index) }}</td>
<td>{{ planItem.activity }}</td>
<td>{{ planItem.predefinedActivity.name }}</td>
<td>
<span @click="removePlanItem(planItem.id)" class="add-plan-item">-</span>
{{ planItem.duration }}
@@ -78,7 +76,7 @@
<div v-for="activity in filteredPredefinedActivities" :key="activity.id"
@click="selectPredefinedActivity(activity)">
{{ activity.name }} ({{ activity.durationText || '' }} / {{
activity.duration }} Minuten)
activity.duration }} Minuten)
</div>
</div>
</td>
@@ -91,7 +89,62 @@
</tr>
</tbody>
</table>
<button @click="generatePDF">PDF speichern</button>
<button v-if="trainingPlan && trainingPlan.length && trainingPlan.length > 0" @click="generatePDF">Als PDF herunterladen</button>
</div>
<div class="column">
<h3>Teilnehmer</h3>
<ul>
<li v-for="member in members" :key="member.id">
<label>
<input type="checkbox" :value="member.id" @change="toggleParticipant(member.id)"
:checked="isParticipant(member.id)">
<span @click="openNotesModal(member)" class="clickable">{{ member.firstName }} {{
member.lastName }}</span>
</label>
</li>
</ul>
<h3>Aktivitäten</h3>
<textarea v-model="newActivity"></textarea>
<button @click="addActivity">Aktivität hinzufügen</button>
<ul>
<li v-for="activity in activities" :key="activity.id">
{{ activity.description }}
</li>
</ul>
<multiselect v-model="selectedActivityTags" :options="availableTags" placeholder="Tags auswählen"
label="name" track-by="id" multiple :close-on-select="true" @tag="addNewTag"
@remove="removeActivityTag" @input="updateActivityTags" :allow-empty="false"
@keydown.enter.prevent="addNewTagFromInput" />
</div>
</div>
</div>
<div v-if="showNotesModal" class="modal">
<div class="modal-content">
<span class="close" @click="closeNotesModal">&times;</span>
<h3>Notizen für {{ selectedMember.firstName }} {{ selectedMember.lastName }}</h3>
<div class="modal-body">
<div class="modal-left">
<img v-if="selectedMember.imageUrl" :src="selectedMember.imageUrl" alt="Mitgliedsbild"
style="width: 250px; height: 250px; object-fit: cover;" />
</div>
<div class="modal-right">
<multiselect v-model="selectedMemberTags" :options="availableTags" placeholder="Tags auswählen"
label="name" track-by="id" multiple :close-on-select="true" @tag="addNewTagForMember"
@remove="removeMemberTag" @input="updateMemberTags" :allow-empty="false"
@keydown.enter.prevent="addNewTagForMemberFromInput" />
<div>
<textarea v-model="newNoteContent" placeholder="Neue Notiz" rows="4" cols="30"></textarea>
<button @click="addMemberNote">Hinzufügen</button>
</div>
<ul>
<li v-for="note in notes" :key="note.id">
<button @click="deleteNote(note.id)" class="cancel-action">Löschen</button>
{{ note.content }}
</li>
</ul>
</div>
</div>
</div>
</div>
@@ -101,19 +154,34 @@
<script>
import { mapGetters } from 'vuex';
import apiClient from '../apiClient.js';
import Multiselect from 'vue-multiselect';
import Sortable from 'sortablejs';
import PDFGenerator from '../components/PDFGenerator.js';
import PDFGenerator from '../components/PDFGenerator.js';
export default {
name: 'DiaryView',
components: { Multiselect },
data() {
return {
selectedDate: null,
date: null,
dates: [],
showForm: false,
newDate: '',
trainingStart: '',
trainingEnd: '',
members: [],
participants: [],
newActivity: '',
activities: [],
notes: [],
newNoteContent: '',
selectedMember: null,
showNotesModal: false,
selectedActivityTags: [],
selectedMemberTags: [],
availableTags: [],
previousActivityTags: [],
previousMemberTags: [],
trainingPlan: [],
newPlanItem: {
activity: '',
@@ -124,8 +192,16 @@ export default {
showDropdown: false,
};
},
watch: {
selectedMemberTags(newTags) {
this.updateMemberTags(newTags);
},
selectedActivityTags(newTags) {
this.updateActivityTags(newTags);
},
},
computed: {
...mapGetters(['isAuthenticated', 'currentClub']),
...mapGetters(['isAuthenticated', 'currentClub', 'currentClubName']),
calculateNextTime() {
let lastTime = this.trainingStart;
for (let item of this.trainingPlan) {
@@ -145,6 +221,7 @@ export default {
if (this.isAuthenticated && this.currentClub) {
const response = await apiClient.get(`/diary/${this.currentClub}`);
this.dates = response.data.map(entry => ({ id: entry.id, date: entry.date }));
this.loadTags();
this.loadPredefinedActivities();
}
},
@@ -153,15 +230,24 @@ export default {
this.newDate = today;
},
async handleDateChange() {
this.showForm = this.selectedDate === 'new';
if (this.selectedDate && this.selectedDate !== 'new') {
const dateId = this.selectedDate.id;
const response = await apiClient.get(`/diary/${this.currentClub}/${dateId}`);
const dateData = response.data;
this.showForm = this.date === 'new';
if (this.date && this.date !== 'new') {
const dateId = this.date.id;
const response = await apiClient.get(`/diary/${this.currentClub}`);
const dateData = response.data.find(entry => entry.id === dateId);
this.trainingStart = dateData.trainingStart;
this.trainingEnd = dateData.trainingEnd;
this.selectedActivityTags = dateData.diaryTags.map(tag => ({
id: tag.id,
name: tag.name
}));
this.previousActivityTags = [...this.selectedActivityTags]; // Hier setzen
await this.loadMembers();
await this.loadParticipants(dateId);
await this.loadActivities(dateId);
this.trainingPlan = await apiClient
.get(`/diary-date-activities/${this.currentClub}/${dateId}`)
.get(`/diary-date-activities/${this.currentClub}/${this.date.id}`)
.then(response => response.data);
this.initializeSortable();
@@ -169,6 +255,7 @@ export default {
this.newDate = '';
this.trainingStart = '';
this.trainingEnd = '';
this.participants = [];
}
},
initializeSortable() {
@@ -186,7 +273,7 @@ export default {
trainingEnd: this.trainingEnd || null,
});
this.dates.push({ id: response.data.id, date: response.data.date });
this.selectedDate = { id: response.data.id, date: response.data.date };
this.date = { id: response.data.id, date: response.data.date };
this.showForm = false;
this.newDate = '';
this.trainingStart = '';
@@ -197,7 +284,7 @@ export default {
},
async updateTrainingTimes() {
try {
const dateId = this.selectedDate.id;
const dateId = this.date.id;
await apiClient.put(`/diary/${this.currentClub}`, {
dateId,
trainingStart: this.trainingStart || null,
@@ -209,6 +296,22 @@ export default {
alert('Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.');
}
},
async loadMembers() {
const response = await apiClient.get(`/clubmembers/${this.currentClub}`);
this.members = response.data;
},
async loadParticipants(dateId) {
const response = await apiClient.get(`/participants/${dateId}`);
this.participants = response.data.map(participant => participant.memberId);
},
async loadActivities(dateId) {
const response = await apiClient.get(`/activities/${dateId}`);
this.activities = response.data;
},
async loadTags() {
const response = await apiClient.get('/tags');
this.availableTags = response.data;
},
async loadPredefinedActivities() {
try {
const response = await apiClient.get('/predefined-activities');
@@ -217,6 +320,216 @@ export default {
console.error('Fehler beim Laden der vordefinierten Aktivitäten:', error);
}
},
isParticipant(memberId) {
return this.participants.includes(memberId);
},
async toggleParticipant(memberId) {
const isParticipant = this.isParticipant(memberId);
const dateId = this.date.id;
if (isParticipant) {
await apiClient.post('/participants/remove', {
diaryDateId: dateId,
memberId
});
this.participants = this.participants.filter(id => id !== memberId);
} else {
await apiClient.post('/participants/add', {
diaryDateId: dateId,
memberId
});
this.participants.push(memberId);
}
},
async addActivity() {
const dateId = this.date.id;
if (this.newActivity) {
const response = await apiClient.post('/activities/add', {
diaryDateId: dateId,
description: this.newActivity,
tags: this.selectedActivityTags.map(tag => tag.id)
});
this.activities.push(response.data);
this.newActivity = '';
this.selectedActivityTags = [];
}
},
async openNotesModal(member) {
this.selectedMember = member;
await this.loadMemberImage(member);
this.loadMemberNotesAndTags(this.date.id, member.id);
this.showNotesModal = true;
},
async loadMemberNotesAndTags(diaryDateId, memberId) {
try {
const notesResponse = await apiClient.get(`/diarymember/${this.currentClub}/note`, {
params: { diaryDateId, memberId }
});
this.notes = notesResponse.data;
const tagsResponse = await apiClient.get(`/diarymember/${this.currentClub}/tag`, {
params: { diaryDateId, memberId }
});
this.selectedMemberTags = tagsResponse.data.map(tag => ({
id: tag.tag.id,
name: tag.tag.name
}));
} catch (error) {
console.error('Error loading member notes and tags:', error);
alert('Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.');
}
},
async addMemberNote() {
if (this.newNoteContent) {
const response = await apiClient.post(`/diarymember/${this.currentClub}/note`, {
memberId: this.selectedMember.id,
diaryDateId: this.date.id,
content: this.newNoteContent
});
this.notes = response.data;
this.newNoteContent = '';
this.selectedTagsNotes = [];
}
},
async deleteNote(noteId) {
const response = await apiClient.delete(`/diarymember/${this.currentClub}/note/${noteId}`, {
clubId: this.currentClub
});
this.notes = response.data;
},
closeNotesModal() {
this.showNotesModal = false;
},
async addNewTagFromInput(event) {
const inputValue = event.target.value.trim();
if (inputValue) {
await this.addNewTag(inputValue);
}
},
async addNewTag(newTagName) {
try {
const response = await apiClient.post('/tags', { name: newTagName });
const newTag = response.data;
this.availableTags.push(newTag);
this.selectedActivityTags.push(newTag);
} catch (error) {
console.error('Fehler beim Hinzufügen eines neuen Tags:', error);
alert('Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.');
}
},
async addNewTagForMemberFromInput(event) {
const inputValue = event.target.value.trim();
if (inputValue) {
await this.addNewTagForMember(inputValue);
}
},
async addNewTagForMember(newTagName) {
try {
const response = await apiClient.post('/tags', { name: newTagName });
const newTag = response.data;
this.availableTags.push(newTag);
this.selectedMemberTags.push(newTag);
await this.linkTagToMemberAndDate(newTag);
} catch (error) {
console.error('Fehler beim Hinzufügen eines neuen Tags für das Mitglied:', error);
alert('Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.');
}
},
async linkTagToDiaryDate(tag) {
try {
const tagId = tag.id;
await apiClient.post(`/diary/tag/${this.currentClub}/add-tag`, {
diaryDateId: this.date.id,
tagId: tagId
});
} catch (error) {
console.error('Fehler beim Verknüpfen des Tags mit dem Trainingstag:', error);
alert('Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.');
}
},
async linkTagToMemberAndDate(tag) {
try {
const tagId = tag.id;
await apiClient.post(`/diarymember/${this.currentClub}/tag`, {
diaryDateId: this.date.id,
memberId: this.selectedMember.id,
tagId: tagId
});
} catch (error) {
console.error('Fehler beim Verknüpfen des Tags mit dem Mitglied und Datum:', error);
alert('Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.');
}
},
async updateActivityTags() {
try {
const selectedTags = this.selectedActivityTags;
if (!selectedTags || !Array.isArray(selectedTags)) {
console.log(typeof selectedTags, JSON.stringify(selectedTags));
throw new TypeError('Expected selectedTags to be an array');
}
for (let tag of selectedTags) {
if (!this.previousActivityTags.includes(tag)) {
await this.linkTagToDiaryDate(tag);
}
}
this.previousActivityTags = [...selectedTags];
} catch (error) {
console.error('Fehler beim Verknüpfen der Tags mit dem Trainingstag:', error);
alert('Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.');
}
},
async updateMemberTags() {
try {
for (let tag of this.selectedMemberTags) {
if (!this.previousMemberTags.includes(tag)) {
await this.linkTagToMemberAndDate(tag);
}
}
this.previousMemberTags = [...this.selectedMemberTags];
} catch (error) {
console.error('Fehler beim Verknüpfen der Tags mit dem Mitglied und Datum:', error);
alert('Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.');
}
},
async removeMemberTag(tagId) {
try {
await apiClient.post(`/diarymember/${this.currentClub}/tag/remove`, {
diaryDateId: this.date.id,
memberId: this.selectedMember.id,
tagId: tagId
});
this.selectedMemberTags = this.selectedMemberTags.filter(tag => tag.id !== tagId);
} catch (error) {
console.error('Fehler beim Entfernen des Tags:', error);
alert('Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.');
}
},
async removeMemberNote(noteContent) {
try {
await apiClient.post(`/diarymember/${this.currentClub}/note/remove`, {
diaryDateId: this.date.id,
memberId: this.selectedMember.id,
content: noteContent
});
this.notes = this.notes.filter(note => note.content !== noteContent);
} catch (error) {
console.error('Fehler beim Entfernen der Notiz:', error);
alert('Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.');
}
},
async removeActivityTag(tag) {
try {
const tagId = tag.id;
await apiClient.delete(`/diary/${this.currentClub}/tag`, {
params: { tagId }
});
this.selectedActivityTags = this.selectedActivityTags.filter(t => t.id !== tagId);
} catch (error) {
console.error('Fehler beim Entfernen des Tags:', error);
alert('Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.');
}
},
handleActivityInput() {
if (this.newPlanItem.activity) {
this.showDropdown = true;
@@ -233,14 +546,14 @@ export default {
async addPlanItem() {
try {
await apiClient.post(`/diary-date-activities/${this.currentClub}`, {
diaryDateId: this.selectedDate.id,
diaryDateId: this.date.id,
activity: this.newPlanItem.activity,
duration: this.newPlanItem.duration,
durationText: this.newPlanItem.durationText,
orderId: this.trainingPlan.length
});
this.newPlanItem = { activity: '', duration: '', durationText: '' };
this.trainingPlan = await apiClient.get(`/diary-date-activities/${this.currentClub}/${this.selectedDate.id}`).then(response => response.data);
this.trainingPlan = await apiClient.get(`/diary-date-activities/${this.currentClub}/${this.date.id}`).then(response => response.data);
} catch (error) {
console.error('Fehler beim Hinzufügen des Planungsitems:', error);
alert('Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.');
@@ -248,8 +561,10 @@ export default {
},
async removePlanItem(planItemId) {
try {
await apiClient.delete(`/diary-date-activities/${this.currentClub}/${planItemId}`);
this.trainingPlan = this.trainingPlan.filter(item => item.id !== planItemId);
await apiClient.delete(`/diary-date-activities/${this.currentClub}`, {
params: { planItemId }
});
this.planItems = this.planItems.filter(item => item.id !== planItemId);
} catch (error) {
console.error('Fehler beim Entfernen des Planungsitems:', error);
}
@@ -262,18 +577,39 @@ export default {
return time;
},
addDurationToTime(startTime, duration) {
let [hours, minutes] = startTime.split(':').map(Number);
minutes += Number(duration);
if (minutes >= 60) {
hours += Math.floor(minutes / 60);
minutes = minutes % 60;
}
hours = hours % 24;
return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}`;
},
calculateDuration() {
const input = this.newPlanItem.durationInput;
let calculatedDuration = 0;
const multiplyPattern = /(\d+)\s*[x*]\s*(\d+)/i;
const match = input.match(multiplyPattern);
if (match) {
const [, num1, num2] = match;
calculatedDuration = parseInt(num1) * parseInt(num2);
} else if (!isNaN(input)) {
calculatedDuration = parseInt(input);
}
calculatedDuration = Math.ceil(calculatedDuration / 5) * 5;
if (!this.newPlanItem.durationText || this.newPlanItem.durationText === input) {
this.newPlanItem.duration = calculatedDuration;
this.newPlanItem = { ...this.newPlanItem, duration: calculatedDuration };
}
},
async removePlanItem(planItemId) {
try {
let [hours, minutes] = startTime.split(':').map(Number);
minutes += Number(duration);
if (minutes >= 60) {
hours += Math.floor(minutes / 60);
minutes = minutes % 60;
}
hours = hours % 24;
return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}`;
} catch(error) {
console.log(error);
return '';
await apiClient.delete(`/diary-date-activities/${this.currentClub}/${planItemId}`);
this.trainingPlan = this.trainingPlan.filter(item => item.id !== planItemId);
} catch (error) {
console.error('Fehler beim Entfernen des Planungsitems:', error);
alert('Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.');
}
},
async onDragEnd(evt) {
@@ -287,18 +623,26 @@ export default {
alert('Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.');
}
},
async loadMemberImage(member) {
try {
const response = await apiClient.get(`/clubmembers/${this.currentClub}/image/${member.id}`, {
responseType: 'blob',
});
const imageUrl = URL.createObjectURL(response.data);
member.imageUrl = imageUrl;
} catch (error) {
console.error("Failed to load member image:", error);
member.imageUrl = null;
}
},
async generatePDF() {
const clubName = this.$store.getters.currentClubName;
const trainingDate = this.selectedDate.date;
const trainingStart = this.trainingStart;
const trainingEnd = this.trainingEnd;
const pdfGenerator = new PDFGenerator();
pdfGenerator.addTrainingPlan(clubName, trainingDate, trainingStart, trainingEnd, this.trainingPlan);
pdfGenerator.save('Trainingsplan.pdf');
}
const pdf = new PDFGenerator();
pdf.addTrainingPlan(this.currentClubName, this.date.date, this.trainingStart, this.trainingEnd, this.trainingPlan);
pdf.save('trainingsplan.pdf');
},
},
async mounted() {
await this.init();
}
};
@@ -337,6 +681,62 @@ ul {
padding: 0;
}
.modal {
display: flex;
justify-content: center;
align-items: center;
position: fixed;
z-index: 1000;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 50%;
height: 50%;
overflow: auto;
background-color: rgba(200, 200, 200, 0.5);
}
.modal-content {
background-color: #fefefe;
padding: 20px;
border: 1px solid #555;
width: 100%;
height: 100%;
position: relative;
box-shadow: 4px 3px 2px #999;
}
.close {
position: absolute;
top: 10px;
right: 15px;
color: #aaa;
font-size: 28px;
font-weight: bold;
}
.close:hover,
.close:focus {
color: black;
text-decoration: none;
cursor: pointer;
}
ul {
list-style-type: none;
padding: 0;
}
li {
padding: 0;
margin-bottom: 5px;
}
.multiselect {
margin-bottom: 10px;
width: 100%;
}
table {
width: 100%;
border-collapse: collapse;
@@ -386,6 +786,11 @@ input[type="number"] {
background-color: #f0f0f0;
}
.clickable {
cursor: pointer;
color: #45a049;
}
.add-plan-item {
border: 1px solid black;
cursor: pointer;
@@ -406,4 +811,21 @@ input[type="number"] {
.drag-handle {
cursor: pointer;
}
.modal-body {
display: flex;
justify-content: space-between;
}
.modal-left {
flex: 0 0 250px;
display: flex;
justify-content: center;
align-items: center;
padding-right: 20px;
}
.modal-right {
flex: 1;
}
</style>