Fügt Unterstützung für Aktivitätenmitglieder in DiaryView.vue hinzu. Ermöglicht das Zuordnen von Teilnehmern zu Aktivitäten, einschließlich der Verwaltung von Teilnehmern über das Backend. Aktualisiert die Datenbankmodelle und -routen, um die neuen Funktionen zu unterstützen.

This commit is contained in:
Torsten Schulz (local)
2025-08-28 14:43:04 +02:00
parent 244b61c901
commit b82a80a11d
6 changed files with 199 additions and 4 deletions

View File

@@ -136,7 +136,19 @@
v-if="item.durationText && item.durationText.trim() !== ''"> ({{
item.durationText }})</span>
</td>
<td><button @click="removePlanItem(item.id)" class="trash-btn">🗑</button></td>
<td>
<button @click="toggleActivityMembers(item)" title="Teilnehmer zuordnen" class="person-btn">👤</button>
<button @click="removePlanItem(item.id)" class="trash-btn">🗑</button>
<div v-if="activityMembersOpenId === item.id" class="dropdown" style="max-height: 12em; padding: 0.25rem;">
<div style="margin-bottom: 0.25rem; font-weight: 600;">Teilnehmer zuordnen</div>
<div style="max-height: 9.5em; overflow-y: auto;">
<label v-for="m in presentMembers" :key="m.id" style="display:flex; align-items:center; gap:0.5rem;">
<input type="checkbox" :checked="isAssignedToActivity(item.id, m.id)" @change="toggleMemberForActivity(item.id, m.id, $event.target.checked)">
<span>{{ m.firstName }} {{ m.lastName }}</span>
</label>
</div>
</div>
</td>
</tr>
<template v-for="groupItem in item.groupActivities">
<tr>
@@ -220,10 +232,10 @@
{{ activity.description }}
</li>
</ul>
</div>
<multiselect v-model="selectedActivityTags" :options="availableTags" placeholder="Tags auswählen"
<multiselect v-model="selectedActivityTags" :options="availableTags" placeholder="Tags auswählen"
label="name" track-by="id" multiple :close-on-select="true" @tag="addNewTag"
@remove="removeActivityTag" :allow-empty="false" @keydown.enter.prevent="addNewTagFromInput" />
</div>
<h3>Teilnehmer ({{ participants.length }})</h3>
<ul>
<li v-for="member in sortedMembers()" :key="member.id" class="checkbox-item">
@@ -406,6 +418,9 @@ export default {
newItemSearchResults: [],
// Aktivitäten-Box (rechts)
showActivitiesBox: false,
// Aktivitäts-Teilnehmer
activityMembersOpenId: null,
activityMembersMap: {}, // key: activityId, value: Set(participantIds)
};
},
watch: {
@@ -433,6 +448,11 @@ export default {
activity.name.toLowerCase().includes(input)
);
},
presentMembers() {
// participants enthält memberIds der anwesenden
const presentSet = new Set(this.participants);
return this.members.filter(m => presentSet.has(m.id));
},
},
methods: {
async init() {
@@ -529,6 +549,8 @@ export default {
async loadParticipants(dateId) {
const response = await apiClient.get(`/participants/${dateId}`);
this.participants = response.data.map(participant => participant.memberId);
// Map für memberId -> participantId speichern
this.participantMapByMemberId = response.data.reduce((map, p) => { map[p.memberId] = p.id; return map; }, {});
},
async loadActivities(dateId) {
@@ -1262,6 +1284,72 @@ export default {
toggleActivitiesBox() {
this.showActivitiesBox = !this.showActivitiesBox;
},
async toggleActivityMembers(item) {
if (this.activityMembersOpenId === item.id) {
this.activityMembersOpenId = null;
return;
}
this.activityMembersOpenId = item.id;
await this.ensureActivityMembersLoaded(item.id);
},
async ensureActivityMembersLoaded(activityId) {
if (this.activityMembersMap[activityId]) return;
try {
const r = await apiClient.get(`/diary-member-activities/${this.currentClub}/${activityId}`);
const setIds = new Set(r.data.map(x => x.participantId));
this.$set ? this.$set(this.activityMembersMap, activityId, setIds) : (this.activityMembersMap[activityId] = setIds);
} catch (e) {
this.activityMembersMap[activityId] = new Set();
}
},
isAssignedToActivity(activityId, memberId) {
const participantId = this.participantIdForMember(memberId);
const setIds = this.activityMembersMap[activityId];
return setIds ? setIds.has(participantId) : false;
},
participantIdForMember(memberId) {
// In diesem UI haben wir nur memberIds; Participant-IDs müssen über current date abgeleitet werden.
// Vereinfachung: Participant-ID ist Kombination aus (date.id, memberId) → wir müssen sie vom Backend bekommen.
// Als Pragmatik: wir holen sie über /participants und mappen nach memberId. Bereits vorhanden in participants-Liste? Nein, nur IDs.
// Für add/remove nutzen wir einen Helper-Endpoint? Wir haben bereits /participants für dateId → wir bauen Map:
// Wir speichern eine lokale Map beim Laden der Teilnehmer.
const map = this.participantMapByMemberId || {};
return map[memberId];
},
async toggleMemberForActivity(activityId, memberId, checked) {
let participantId = this.participantIdForMember(memberId);
try {
if (checked) {
// Falls Mitglied noch kein Participant ist: zuerst hinzufügen
if (!participantId) {
const created = await apiClient.post('/participants/add', {
diaryDateId: this.date.id,
memberId: memberId,
});
participantId = created.data.id;
if (!this.participantMapByMemberId) this.participantMapByMemberId = {};
this.participantMapByMemberId[memberId] = participantId;
if (!this.participants.includes(memberId)) this.participants.push(memberId);
}
// Danach Zuordnung zur Aktivität anlegen
await apiClient.post(`/diary-member-activities/${this.currentClub}/${activityId}`, { participantIds: [participantId] });
if (!this.activityMembersMap[activityId]) this.activityMembersMap[activityId] = new Set();
this.activityMembersMap[activityId].add(participantId);
} else {
// Nur die Aktivitäts-Zuordnung entfernen (Participant bleibt)
if (!participantId) return;
await apiClient.delete(`/diary-member-activities/${this.currentClub}/${activityId}/${participantId}`);
if (this.activityMembersMap[activityId]) this.activityMembersMap[activityId].delete(participantId);
}
} catch (e) {
alert('Fehler beim Aktualisieren der Aktivitäts-Teilnehmer');
}
},
},
async mounted() {
await this.init();
@@ -1630,4 +1718,8 @@ img {
.collapsible-box.expanded h3 span {
transform: rotate(90deg);
}
.person-btn {
margin-right: 0.5rem;
}
</style>