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

@@ -0,0 +1,52 @@
import DiaryMemberActivity from '../models/DiaryMemberActivity.js';
import Participant from '../models/Participant.js';
import { checkAccess } from '../utils/userUtils.js';
export const getMembersForActivity = async (req, res) => {
try {
const { authcode: userToken } = req.headers;
const { clubId, diaryDateActivityId } = req.params;
await checkAccess(userToken, clubId);
const list = await DiaryMemberActivity.findAll({ where: { diaryDateActivityId } });
res.status(200).json(list);
} catch (e) {
res.status(500).json({ error: 'Error fetching members for activity' });
}
};
export const addMembersToActivity = async (req, res) => {
try {
const { authcode: userToken } = req.headers;
const { clubId, diaryDateActivityId } = req.params;
const { participantIds } = req.body; // array of participant ids
await checkAccess(userToken, clubId);
const validParticipants = await Participant.findAll({ where: { id: participantIds } });
const validIds = new Set(validParticipants.map(p => p.id));
const created = [];
for (const pid of participantIds) {
if (!validIds.has(pid)) continue;
const existing = await DiaryMemberActivity.findOne({ where: { diaryDateActivityId, participantId: pid } });
if (!existing) {
const rec = await DiaryMemberActivity.create({ diaryDateActivityId, participantId: pid });
created.push(rec);
}
}
res.status(201).json(created);
} catch (e) {
res.status(500).json({ error: 'Error adding members to activity' });
}
};
export const removeMemberFromActivity = async (req, res) => {
try {
const { authcode: userToken } = req.headers;
const { clubId, diaryDateActivityId, participantId } = req.params;
await checkAccess(userToken, clubId);
await DiaryMemberActivity.destroy({ where: { diaryDateActivityId, participantId } });
res.status(200).json({ ok: true });
} catch (e) {
res.status(500).json({ error: 'Error removing member from activity' });
}
};

View File

@@ -0,0 +1,26 @@
import { DataTypes } from 'sequelize';
import sequelize from '../database.js';
const DiaryMemberActivity = sequelize.define('DiaryMemberActivity', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
},
diaryDateActivityId: {
type: DataTypes.INTEGER,
allowNull: false,
},
participantId: {
type: DataTypes.INTEGER,
allowNull: false,
},
}, {
tableName: 'diary_member_activities',
timestamps: true,
underscored: true,
});
export default DiaryMemberActivity;

View File

@@ -13,6 +13,7 @@ import DiaryDateTag from './DiaryDateTag.js';
import DiaryMemberNote from './DiaryMemberNote.js';
import DiaryMemberTag from './DiaryMemberTag.js';
import PredefinedActivity from './PredefinedActivity.js';
import DiaryMemberActivity from './DiaryMemberActivity.js';
import PredefinedActivityImage from './PredefinedActivityImage.js';
import DiaryDateActivity from './DiaryDateActivity.js';
import Match from './Match.js';
@@ -77,6 +78,11 @@ DiaryDateActivity.belongsTo(DiaryDate, { foreignKey: 'diaryDateId', as: 'diaryDa
PredefinedActivity.hasMany(DiaryDateActivity, { foreignKey: 'predefinedActivityId', as: 'predefinedActivities' });
DiaryDateActivity.belongsTo(PredefinedActivity, { foreignKey: 'predefinedActivityId', as: 'predefinedActivity' });
// DiaryMemberActivity links a Participant to a DiaryDateActivity
DiaryMemberActivity.belongsTo(DiaryDateActivity, { foreignKey: 'diaryDateActivityId', as: 'activity' });
DiaryDateActivity.hasMany(DiaryMemberActivity, { foreignKey: 'diaryDateActivityId', as: 'activityMembers' });
DiaryMemberActivity.belongsTo(Participant, { foreignKey: 'participantId', as: 'participant' });
Participant.hasMany(DiaryMemberActivity, { foreignKey: 'participantId', as: 'memberActivities' });
// PredefinedActivity Images
PredefinedActivity.hasMany(PredefinedActivityImage, { foreignKey: 'predefinedActivityId', as: 'images' });
PredefinedActivityImage.belongsTo(PredefinedActivity, { foreignKey: 'predefinedActivityId', as: 'predefinedActivity' });
@@ -202,6 +208,7 @@ export {
DiaryMemberNote,
DiaryMemberTag,
PredefinedActivity,
DiaryMemberActivity,
PredefinedActivityImage,
DiaryDateActivity,
Match,

View File

@@ -0,0 +1,15 @@
import express from 'express';
import { authenticate } from '../middleware/authMiddleware.js';
import { addMembersToActivity, removeMemberFromActivity, getMembersForActivity } from '../controllers/diaryMemberActivityController.js';
const router = express.Router();
router.use(authenticate);
router.get('/:clubId/:diaryDateActivityId', getMembersForActivity);
router.post('/:clubId/:diaryDateActivityId', addMembersToActivity);
router.delete('/:clubId/:diaryDateActivityId/:participantId', removeMemberFromActivity);
export default router;

View File

@@ -6,7 +6,7 @@ import cors from 'cors';
import {
User, Log, Club, UserClub, Member, DiaryDate, Participant, Activity, MemberNote,
DiaryNote, DiaryTag, MemberDiaryTag, DiaryDateTag, DiaryMemberNote, DiaryMemberTag,
PredefinedActivity, PredefinedActivityImage, DiaryDateActivity, Match, League, Team, Group,
PredefinedActivity, PredefinedActivityImage, DiaryDateActivity, DiaryMemberActivity, Match, League, Team, Group,
GroupActivity, Tournament, TournamentGroup, TournamentMatch, TournamentResult,
TournamentMember, Accident, UserToken
} from './models/index.js';
@@ -22,6 +22,7 @@ import diaryNoteRoutes from './routes/diaryNoteRoutes.js';
import diaryMemberRoutes from './routes/diaryMemberRoutes.js';
import predefinedActivityRoutes from './routes/predefinedActivityRoutes.js';
import diaryDateActivityRoutes from './routes/diaryDateActivityRoutes.js';
import diaryMemberActivityRoutes from './routes/diaryMemberActivityRoutes.js';
import matchRoutes from './routes/matchRoutes.js';
import Season from './models/Season.js';
import Location from './models/Location.js';
@@ -53,6 +54,7 @@ app.use('/api/tags', diaryTagRoutes);
app.use('/api/diarymember', diaryMemberRoutes);
app.use('/api/predefined-activities', predefinedActivityRoutes);
app.use('/api/diary-date-activities', diaryDateActivityRoutes);
app.use('/api/diary-member-activities', diaryMemberActivityRoutes);
app.use('/api/matches', matchRoutes);
app.use('/api/group', groupRoutes);
app.use('/api/diarydatetags', diaryDateTagRoutes);
@@ -90,6 +92,7 @@ app.get('*', (req, res) => {
await PredefinedActivity.sync({ alter: true });
await PredefinedActivityImage.sync({ alter: true });
await DiaryDateActivity.sync({ alter: true });
await DiaryMemberActivity.sync({ alter: true });
await Season.sync({ alter: true });
await League.sync({ alter: true });
await Team.sync({ alter: true });

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>