Added dayplanning

This commit is contained in:
Torsten Schulz
2024-09-11 15:44:56 +02:00
parent 28bf98a169
commit a22d2bcfc6
19 changed files with 778 additions and 52 deletions

View File

@@ -10,6 +10,7 @@
"dependencies": {
"axios": "^1.7.3",
"core-js": "^3.8.3",
"sortablejs": "^1.15.3",
"vue": "^3.2.13",
"vue-multiselect": "^3.0.0",
"vue-router": "^4.4.0",
@@ -33,11 +34,18 @@
"@babel/highlight": "^7.10.4"
}
},
"node_modules/@babel/helper-string-parser": {
"version": "7.24.8",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz",
"integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-validator-identifier": {
"version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz",
"integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==",
"dev": true,
"engines": {
"node": ">=6.9.0"
}
@@ -129,9 +137,12 @@
}
},
"node_modules/@babel/parser": {
"version": "7.25.0",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.0.tgz",
"integrity": "sha512-CzdIU9jdP0dg7HdyB+bHvDJGagUv+qtzZt5rYCWwW6tITNqV9odjp6Qu41gkG0ca5UfdDUWrKkiAnHHdGRnOrA==",
"version": "7.25.6",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.6.tgz",
"integrity": "sha512-trGdfBdbD0l1ZPmcJ83eNxB9rbEax4ALFTF7fN386TMYbeCQbyme5cOEXQhbGXKebwGaB/J52w1mrklMcbgy6Q==",
"dependencies": {
"@babel/types": "^7.25.6"
},
"bin": {
"parser": "bin/babel-parser.js"
},
@@ -139,6 +150,19 @@
"node": ">=6.0.0"
}
},
"node_modules/@babel/types": {
"version": "7.25.6",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.6.tgz",
"integrity": "sha512-/l42B1qxpG6RdfYf343Uw1vmDjeNhneUXtzhojE7pDgfpEypmRhI6j1kr17XCVv4Cgl9HdAiQY2x0GwKm7rWCw==",
"dependencies": {
"@babel/helper-string-parser": "^7.24.8",
"@babel/helper-validator-identifier": "^7.24.7",
"to-fast-properties": "^2.0.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz",
@@ -1673,11 +1697,11 @@
"dev": true
},
"node_modules/magic-string": {
"version": "0.30.10",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.10.tgz",
"integrity": "sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==",
"version": "0.30.11",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz",
"integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.4.15"
"@jridgewell/sourcemap-codec": "^1.5.0"
}
},
"node_modules/mime-db": {
@@ -2093,6 +2117,11 @@
"url": "https://github.com/chalk/slice-ansi?sponsor=1"
}
},
"node_modules/sortablejs": {
"version": "1.15.3",
"resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.15.3.tgz",
"integrity": "sha512-zdK3/kwwAK1cJgy1rwl1YtNTbRmc8qW/+vgXf75A7NHag5of4pyI6uK86ktmQETyWRH7IGaE73uZOOBcGxgqZg=="
},
"node_modules/source-map-js": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz",
@@ -2201,6 +2230,14 @@
"integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
"dev": true
},
"node_modules/to-fast-properties": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
"integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==",
"engines": {
"node": ">=4"
}
},
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",

View File

@@ -10,6 +10,7 @@
"dependencies": {
"axios": "^1.7.3",
"core-js": "^3.8.3",
"sortablejs": "^1.15.3",
"vue": "^3.2.13",
"vue-multiselect": "^3.0.0",
"vue-router": "^4.4.0",

View File

@@ -19,11 +19,11 @@
</div>
<div>
<label for="trainingStart">Trainingsbeginn:</label>
<input type="time" id="trainingStart" v-model="trainingStart" />
<input type="time" step="300" id="trainingStart" v-model="trainingStart" />
</div>
<div>
<label for="trainingEnd">Trainingsende:</label>
<input type="time" id="trainingEnd" v-model="trainingEnd" />
<input type="time" step="300" id="trainingEnd" v-model="trainingEnd" />
</div>
<button type="submit">Datum anlegen</button>
</form>
@@ -33,11 +33,11 @@
<form @submit.prevent="updateTrainingTimes">
<div>
<label for="editTrainingStart">Trainingsbeginn:</label>
<input type="time" id="editTrainingStart" v-model="trainingStart" />
<input type="time" step="300" id="editTrainingStart" v-model="trainingStart" />
</div>
<div>
<label for="editTrainingEnd">Trainingsende:</label>
<input type="time" id="editTrainingEnd" v-model="trainingEnd" />
<input type="time" step="300" id="editTrainingEnd" v-model="trainingEnd" />
</div>
<button type="submit">Zeiten aktualisieren</button>
</form>
@@ -45,6 +45,52 @@
<div v-if="date !== 'new' && date !== null">
<div class="columns">
<div class="column">
<h3>Trainingsplan</h3>
<table>
<thead>
<tr>
<th></th>
<th>Uhrzeit</th>
<th>Aktivität</th>
<th>Länge / Gesamtzeit (Min)</th>
</tr>
</thead>
<tbody ref="sortableList">
<tr v-for="(planItem, index) in trainingPlan" :key="planItem.id">
<td class="drag-handle"></td>
<td>{{ calculatePlanItemTime(index) }}</td>
<td>{{ planItem.predefinedActivity.name }}</td>
<td>
<span @click="removePlanItem(planItem.id)" class="add-plan-item">-</span>
{{ planItem.duration }}
</td>
</tr>
<tr>
<td></td>
<td>{{ calculateNextTime }}</td>
<td>
<input type="text" v-model="newPlanItem.activity" @input="handleActivityInput"
placeholder="Aktivität eingeben" />
<div v-if="showDropdown" class="dropdown">
<div v-for="activity in filteredPredefinedActivities" :key="activity.id"
@click="selectPredefinedActivity(activity)">
{{ activity.name }} ({{ activity.durationText || '' }} / {{
activity.duration }} Minuten)
</div>
</div>
</td>
<td>
<input type="text" v-model="newPlanItem.durationInput" @input="calculateDuration"
placeholder="z.B. 2x7 oder 3*5" style="width:10em" />
<input type="number" v-model="newPlanItem.duration" placeholder="Minuten" />
<span class="add-plan-item" @click="addPlanItem">+</span>
</td>
</tr>
</tbody>
</table>
</div>
<div class="column">
<h3>Teilnehmer</h3>
<ul>
@@ -52,13 +98,11 @@
<label>
<input type="checkbox" :value="member.id" @change="toggleParticipant(member.id)"
:checked="isParticipant(member.id)">
{{ member.firstName }} {{ member.lastName }}
<span @click="openNotesModal(member)" class="clickable">{{ member.firstName }} {{
member.lastName }}</span>
</label>
<button @click="openNotesModal(member)">Notizen</button>
</li>
</ul>
</div>
<div class="column">
<h3>Aktivitäten</h3>
<textarea v-model="newActivity"></textarea>
<button @click="addActivity">Aktivität hinzufügen</button>
@@ -100,6 +144,7 @@
import { mapGetters } from 'vuex';
import apiClient from '../apiClient.js';
import Multiselect from 'vue-multiselect';
import Sortable from 'sortablejs';
export default {
name: 'DiaryView',
@@ -125,22 +170,39 @@ export default {
availableTags: [],
previousActivityTags: [],
previousMemberTags: [],
trainingPlan: [],
newPlanItem: {
activity: '',
duration: '',
durationText: '',
},
predefinedActivities: [],
showDropdown: false,
};
},
watch: {
selectedMemberTags(newTags, oldTags) {
selectedMemberTags(newTags) {
this.updateMemberTags(newTags);
},
selectedMemberNotes(newNotes, oldNotes) {
const removedNotes = oldNotes.filter(note => !newNotes.includes(note));
removedNotes.forEach(note => this.removeMemberNote(note.content));
},
selectedActivityTags(newTags, oldTags) {
selectedActivityTags(newTags) {
this.updateActivityTags(newTags);
},
},
computed: {
...mapGetters(['isAuthenticated', 'currentClub']),
calculateNextTime() {
let lastTime = this.trainingStart;
for (let item of this.trainingPlan) {
lastTime = this.addDurationToTime(lastTime, item.duration);
}
return lastTime;
},
filteredPredefinedActivities() {
const input = this.newPlanItem.activity.toLowerCase();
return this.predefinedActivities.filter(activity =>
activity.name.toLowerCase().includes(input)
);
},
},
methods: {
async init() {
@@ -148,16 +210,7 @@ export default {
const response = await apiClient.get(`/diary/${this.currentClub}`);
this.dates = response.data.map(entry => ({ id: entry.id, date: entry.date }));
this.loadTags();
}
},
handleEnterKey(event) {
const newTagName = event.target.value.trim();
if (newTagName) {
if (this.showNotesModal) {
this.addNewTagForMember(newTagName);
} else {
this.addNewTag(newTagName);
}
this.loadPredefinedActivities();
}
},
async handleDateChange() {
@@ -172,11 +225,16 @@ export default {
id: tag.id,
name: tag.name
}));
this.previousActivityTags = [...this.selectedActivityTags]; // Hier setzen
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}/${this.date.id}`)
.then(response => response.data);
this.initializeSortable();
} else {
this.newDate = '';
this.trainingStart = '';
@@ -184,9 +242,12 @@ export default {
this.participants = [];
}
},
setCurrentDate() {
const today = new Date().toISOString().split('T')[0];
this.newDate = today;
initializeSortable() {
const el = this.$refs.sortableList;
Sortable.create(el, {
handle: ".drag-handle",
onEnd: this.onDragEnd,
});
},
async createDate() {
try {
@@ -202,7 +263,6 @@ export default {
this.trainingStart = '';
this.trainingEnd = '';
} catch (error) {
console.error('Fehler beim Erstellen des Datums:', error);
alert('Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.');
}
},
@@ -236,6 +296,14 @@ export default {
const response = await apiClient.get('/tags');
this.availableTags = response.data;
},
async loadPredefinedActivities() {
try {
const response = await apiClient.get('/predefined-activities');
this.predefinedActivities = response.data;
} catch (error) {
console.error('Fehler beim Laden der vordefinierten Aktivitäten:', error);
}
},
isParticipant(memberId) {
return this.participants.includes(memberId);
},
@@ -284,7 +352,7 @@ export default {
params: { diaryDateId, memberId }
});
this.selectedMemberTags = tagsResponse.data.map(tag => ({
id: tag.tag.id,
id: tag.tag.id,
name: tag.tag.name
}));
} catch (error) {
@@ -294,7 +362,7 @@ export default {
},
async addMemberNote() {
if (this.newNoteContent) {
const response = await apiClient.post(`/diarymember/${this.currentClub}/notes`, {
const response = await apiClient.post(`/diarymember/${this.currentClub}/note`, {
memberId: this.selectedMember.id,
diaryDateId: this.date.id,
content: this.newNoteContent
@@ -305,8 +373,8 @@ export default {
}
},
async deleteNote(noteId) {
const response = await apiClient.delete(`/diarymember/note/${noteId}`, {
data: { clubId: this.currentClub }
const response = await apiClient.delete(`/diarymember/${this.currentClub}/note/${noteId}`, {
clubId: this.currentClub
});
this.notes = response.data;
},
@@ -362,7 +430,6 @@ export default {
}
},
async updateActivityTags(selectedTags) {
console.log('test');
try {
for (let tag of selectedTags) {
if (!this.previousActivityTags.includes(tag)) {
@@ -426,8 +493,102 @@ export default {
alert('Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.');
}
},
handleActivityInput() {
if (this.newPlanItem.activity) {
this.showDropdown = true;
} else {
this.showDropdown = false;
}
},
selectPredefinedActivity(activity) {
this.newPlanItem.activity = activity.name;
this.newPlanItem.durationText = activity.durationText;
this.newPlanItem.duration = activity.duration || '';
this.showDropdown = false;
},
async addPlanItem() {
try {
await apiClient.post(`/diary-date-activities/${this.currentClub}`, {
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.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.');
}
},
async removePlanItem(planItemId) {
try {
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);
}
},
calculatePlanItemTime(index) {
let time = this.trainingStart;
for (let i = 0; i < index; i++) {
time = this.addDurationToTime(time, this.trainingPlan[i].duration);
}
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 {
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) {
const movedItem = this.trainingPlan[evt.oldIndex];
try {
await apiClient.put(`/diary-date-activities/${this.currentClub}/${movedItem.id}/order`, {
orderId: evt.newIndex
});
} catch (error) {
console.error('Fehler beim Aktualisieren der Reihenfolge:', error);
alert('Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.');
}
},
},
async mounted() {
await this.init();
}
};
@@ -532,4 +693,78 @@ li {
margin-bottom: 10px;
width: 100%;
}
table {
width: 100%;
border-collapse: collapse;
}
th,
td {
padding: 8px;
text-align: left;
border-bottom: 1px solid #ddd;
}
input[type="text"] {
width: 100%;
padding: 5px;
box-sizing: border-box;
}
input[type="time"] {
width: 7em;
}
input[type="number"] {
width: 5em;
padding: 5px;
box-sizing: border-box;
}
.dropdown {
border: 1px solid #ccc;
max-height: 200px;
overflow-y: auto;
position: absolute;
background-color: white;
z-index: 1000;
width: calc(100% - 20px);
box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.1);
}
.dropdown div {
padding: 8px;
cursor: pointer;
}
.dropdown div:hover {
background-color: #f0f0f0;
}
.clickable {
cursor: pointer;
color: #45a049;
}
.add-plan-item {
border: 1px solid black;
cursor: pointer;
display: inline-block;
width: 1.2em;
height: 1.2em;
text-align: center;
line-height: 1.2em;
font-weight: bold;
margin-left: 5px;
}
.add-plan-item:hover {
background-color: #45a049;
color: white;
}
.drag-handle {
cursor: pointer;
}
</style>