From a8318c74cf920c79a645198740fb9566afe4d40e Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Fri, 31 Oct 2025 16:33:20 +0100 Subject: [PATCH] Implement last participations endpoint and enhance member activity retrieval Added a new endpoint to fetch the last participations of a member, including group assignment checks for activities. Updated the member activity controller to include logic for filtering activities based on participant group IDs. Enhanced the DiaryView component to display activity statistics and last participations in a modal, improving user experience and data accessibility. --- .../controllers/memberActivityController.js | 116 ++++++++++++- backend/routes/memberActivityRoutes.js | 3 +- .../components/MemberActivityStatsDialog.vue | 152 ++++++++++++++++++ frontend/src/views/DiaryView.vue | 48 ++++-- 4 files changed, 306 insertions(+), 13 deletions(-) create mode 100644 frontend/src/components/MemberActivityStatsDialog.vue diff --git a/backend/controllers/memberActivityController.js b/backend/controllers/memberActivityController.js index 337a9d0..d91791d 100644 --- a/backend/controllers/memberActivityController.js +++ b/backend/controllers/memberActivityController.js @@ -4,6 +4,7 @@ import DiaryDateActivity from '../models/DiaryDateActivity.js'; import DiaryDates from '../models/DiaryDates.js'; import Participant from '../models/Participant.js'; import PredefinedActivity from '../models/PredefinedActivity.js'; +import GroupActivity from '../models/GroupActivity.js'; import { Op } from 'sequelize'; export const getMemberActivities = async (req, res) => { @@ -56,6 +57,11 @@ export const getMemberActivities = async (req, res) => { const memberActivities = await DiaryMemberActivity.findAll({ where: whereClause, include: [ + { + model: Participant, + as: 'participant', + attributes: ['id', 'groupId', 'diaryDateId'] + }, { model: DiaryDateActivity, as: 'activity', @@ -72,6 +78,11 @@ export const getMemberActivities = async (req, res) => { { model: PredefinedActivity, as: 'predefinedActivity' + }, + { + model: GroupActivity, + as: 'groupActivities', + required: false } ] } @@ -79,11 +90,25 @@ export const getMemberActivities = async (req, res) => { order: [[{ model: DiaryDateActivity, as: 'activity' }, { model: DiaryDates, as: 'diaryDate' }, 'date', 'DESC']] }); - // Group activities by name and count occurrences + // Group activities by name and count occurrences, considering group assignment const activityMap = new Map(); for (const ma of memberActivities) { - if (!ma.activity || !ma.activity.predefinedActivity) { + if (!ma.activity || !ma.activity.predefinedActivity || !ma.participant) { + continue; + } + + // Check group assignment + const participantGroupId = ma.participant.groupId; + const activityGroupIds = ma.activity.groupActivities?.map(ga => ga.groupId) || []; + + // Filter: Only count if: + // 1. Activity has no group assignment (empty activityGroupIds) - activity is for all groups OR + // 2. Participant's group matches one of the activity's groups + const shouldCount = activityGroupIds.length === 0 || + (participantGroupId !== null && activityGroupIds.includes(participantGroupId)); + + if (!shouldCount) { continue; } @@ -118,3 +143,90 @@ export const getMemberActivities = async (req, res) => { } }; +export const getMemberLastParticipations = async (req, res) => { + try { + const { authcode: userToken } = req.headers; + const { clubId, memberId } = req.params; + const { limit = 3 } = req.query; + + await checkAccess(userToken, clubId); + + // Get participant ID for this member + const participants = await Participant.findAll({ + where: { memberId: memberId } + }); + + if (participants.length === 0) { + return res.status(200).json([]); + } + + const participantIds = participants.map(p => p.id); + + // Get last participations for this member + const memberActivities = await DiaryMemberActivity.findAll({ + where: { + participantId: participantIds + }, + include: [ + { + model: Participant, + as: 'participant', + attributes: ['id', 'groupId', 'diaryDateId'] + }, + { + model: DiaryDateActivity, + as: 'activity', + include: [ + { + model: DiaryDates, + as: 'diaryDate' + }, + { + model: PredefinedActivity, + as: 'predefinedActivity' + }, + { + model: GroupActivity, + as: 'groupActivities', + required: false + } + ] + } + ], + order: [[{ model: DiaryDateActivity, as: 'activity' }, { model: DiaryDates, as: 'diaryDate' }, 'date', 'DESC']], + limit: parseInt(limit) * 10 // Get more to filter by group + }); + + // Format the results, considering group assignment + const participations = memberActivities + .filter(ma => { + if (!ma.activity || !ma.activity.predefinedActivity || !ma.activity.diaryDate || !ma.participant) { + return false; + } + + // Check group assignment + const participantGroupId = ma.participant.groupId; + const activityGroupIds = ma.activity.groupActivities?.map(ga => ga.groupId) || []; + + // Filter: Only count if: + // 1. Activity has no group assignment (empty activityGroupIds) - activity is for all groups OR + // 2. Participant's group matches one of the activity's groups + return activityGroupIds.length === 0 || + (participantGroupId !== null && activityGroupIds.includes(participantGroupId)); + }) + .slice(0, parseInt(limit)) // Limit after filtering + .map(ma => ({ + id: ma.id, + activityName: ma.activity.predefinedActivity.name, + date: ma.activity.diaryDate.date, + diaryDateId: ma.activity.diaryDate.id + })); + + return res.status(200).json(participations); + + } catch (error) { + console.error('Error fetching member last participations:', error); + return res.status(500).json({ error: 'Failed to fetch member last participations' }); + } +}; + diff --git a/backend/routes/memberActivityRoutes.js b/backend/routes/memberActivityRoutes.js index 23355fb..a45fbe4 100644 --- a/backend/routes/memberActivityRoutes.js +++ b/backend/routes/memberActivityRoutes.js @@ -1,11 +1,12 @@ import express from 'express'; import { authenticate } from '../middleware/authMiddleware.js'; -import { getMemberActivities } from '../controllers/memberActivityController.js'; +import { getMemberActivities, getMemberLastParticipations } from '../controllers/memberActivityController.js'; const router = express.Router(); router.use(authenticate); +router.get('/:clubId/:memberId/last-participations', getMemberLastParticipations); router.get('/:clubId/:memberId', getMemberActivities); export default router; diff --git a/frontend/src/components/MemberActivityStatsDialog.vue b/frontend/src/components/MemberActivityStatsDialog.vue new file mode 100644 index 0000000..ccf4005 --- /dev/null +++ b/frontend/src/components/MemberActivityStatsDialog.vue @@ -0,0 +1,152 @@ + + + + + + diff --git a/frontend/src/views/DiaryView.vue b/frontend/src/views/DiaryView.vue index 2afaece..91f94b5 100644 --- a/frontend/src/views/DiaryView.vue +++ b/frontend/src/views/DiaryView.vue @@ -361,6 +361,15 @@ @close="closeTagHistoryModal" /> + + + { - return tag.diaryMemberTags.some(memberTag => - memberTag.diaryDates && memberTag.diaryDates.id === this.date.id - ); - }); + this.showActivityStatsModal = true; + this.activityStatsMember = member; + + try { + // Lade letzte 3 Teilnahmen + const lastParticipationsResponse = await apiClient.get(`/member-activities/${this.currentClub}/${member.id}/last-participations?limit=3`); + this.lastParticipations = lastParticipationsResponse.data || []; + + // Lade Statistik der Übungen + const statsResponse = await apiClient.get(`/member-activities/${this.currentClub}/${member.id}?period=all`); + this.activityStats = statsResponse.data || []; + } catch (error) { + console.error('Error loading activity stats:', error); + this.showInfo('Fehler', 'Fehler beim Laden der Statistiken.', '', 'error'); + this.lastParticipations = []; + this.activityStats = []; + } }, closeTagHistoryModal() { this.showTagHistoryModal = false; this.tagHistoryMember = null; }, + closeActivityStatsModal() { + this.showActivityStatsModal = false; + this.activityStatsMember = null; + this.lastParticipations = []; + this.activityStats = []; + }, async addNewTagForDay(tag) { await apiClient.post(`/diarydatetags/${this.currentClub}`, { dateId: this.date.id,