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:
52
backend/controllers/diaryMemberActivityController.js
Normal file
52
backend/controllers/diaryMemberActivityController.js
Normal 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' });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
26
backend/models/DiaryMemberActivity.js
Normal file
26
backend/models/DiaryMemberActivity.js
Normal 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;
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
15
backend/routes/diaryMemberActivityRoutes.js
Normal file
15
backend/routes/diaryMemberActivityRoutes.js
Normal 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;
|
||||
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user