diff --git a/backend/controllers/memberActivityController.js b/backend/controllers/memberActivityController.js new file mode 100644 index 0000000..337a9d0 --- /dev/null +++ b/backend/controllers/memberActivityController.js @@ -0,0 +1,120 @@ +import { checkAccess } from '../utils/userUtils.js'; +import DiaryMemberActivity from '../models/DiaryMemberActivity.js'; +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 { Op } from 'sequelize'; + +export const getMemberActivities = async (req, res) => { + try { + const { authcode: userToken } = req.headers; + const { clubId, memberId } = req.params; + const { period } = req.query; // 'month', '3months', '6months', 'year', 'all' + + await checkAccess(userToken, clubId); + + // Calculate date range based on period + const now = new Date(); + let startDate = null; + + switch (period) { + case 'month': + startDate = new Date(now.getFullYear(), now.getMonth() - 1, now.getDate()); + break; + case '3months': + startDate = new Date(now.getFullYear(), now.getMonth() - 3, now.getDate()); + break; + case '6months': + startDate = new Date(now.getFullYear(), now.getMonth() - 6, now.getDate()); + break; + case 'year': + startDate = new Date(now.getFullYear() - 1, now.getMonth(), now.getDate()); + break; + case 'all': + default: + startDate = null; + break; + } + + // 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 all diary member activities for this member + const whereClause = { + participantId: participantIds + }; + + const memberActivities = await DiaryMemberActivity.findAll({ + where: whereClause, + include: [ + { + model: DiaryDateActivity, + as: 'activity', + include: [ + { + model: DiaryDates, + as: 'diaryDate', + where: startDate ? { + date: { + [Op.gte]: startDate + } + } : {} + }, + { + model: PredefinedActivity, + as: 'predefinedActivity' + } + ] + } + ], + order: [[{ model: DiaryDateActivity, as: 'activity' }, { model: DiaryDates, as: 'diaryDate' }, 'date', 'DESC']] + }); + + // Group activities by name and count occurrences + const activityMap = new Map(); + + for (const ma of memberActivities) { + if (!ma.activity || !ma.activity.predefinedActivity) { + continue; + } + + const activity = ma.activity.predefinedActivity; + const activityName = activity.name; + const date = ma.activity.diaryDate?.date; + + if (!activityMap.has(activityName)) { + activityMap.set(activityName, { + name: activityName, + count: 0, + dates: [] + }); + } + + const activityData = activityMap.get(activityName); + activityData.count++; + if (date) { + activityData.dates.push(date); + } + } + + // Convert map to array and sort by count + const activities = Array.from(activityMap.values()) + .sort((a, b) => b.count - a.count); + + return res.status(200).json(activities); + + } catch (error) { + console.error('Error fetching member activities:', error); + return res.status(500).json({ error: 'Failed to fetch member activities' }); + } +}; + diff --git a/backend/routes/memberActivityRoutes.js b/backend/routes/memberActivityRoutes.js new file mode 100644 index 0000000..23355fb --- /dev/null +++ b/backend/routes/memberActivityRoutes.js @@ -0,0 +1,12 @@ +import express from 'express'; +import { authenticate } from '../middleware/authMiddleware.js'; +import { getMemberActivities } from '../controllers/memberActivityController.js'; + +const router = express.Router(); + +router.use(authenticate); + +router.get('/:clubId/:memberId', getMemberActivities); + +export default router; + diff --git a/backend/server.js b/backend/server.js index ce58807..67d8805 100644 --- a/backend/server.js +++ b/backend/server.js @@ -38,6 +38,7 @@ import teamRoutes from './routes/teamRoutes.js'; import clubTeamRoutes from './routes/clubTeamRoutes.js'; import teamDocumentRoutes from './routes/teamDocumentRoutes.js'; import seasonRoutes from './routes/seasonRoutes.js'; +import memberActivityRoutes from './routes/memberActivityRoutes.js'; import schedulerService from './services/schedulerService.js'; const app = express(); @@ -88,6 +89,7 @@ app.use('/api/teams', teamRoutes); app.use('/api/club-teams', clubTeamRoutes); app.use('/api/team-documents', teamDocumentRoutes); app.use('/api/seasons', seasonRoutes); +app.use('/api/member-activities', memberActivityRoutes); app.use(express.static(path.join(__dirname, '../frontend/dist'))); diff --git a/frontend/src/components/MemberActivitiesDialog.vue b/frontend/src/components/MemberActivitiesDialog.vue new file mode 100644 index 0000000..9881c2c --- /dev/null +++ b/frontend/src/components/MemberActivitiesDialog.vue @@ -0,0 +1,318 @@ + + + + + + diff --git a/frontend/src/views/MembersView.vue b/frontend/src/views/MembersView.vue index ba052f5..9366b82 100644 --- a/frontend/src/views/MembersView.vue +++ b/frontend/src/views/MembersView.vue @@ -120,6 +120,7 @@ {{ member.email }} + @@ -143,6 +144,13 @@ @confirm="handleConfirmResult(true)" @cancel="handleConfirmResult(false)" /> + + + @@ -205,6 +213,7 @@ import ConfirmDialog from '../components/ConfirmDialog.vue'; import ImageViewerDialog from '../components/ImageViewerDialog.vue'; import BaseDialog from '../components/BaseDialog.vue'; import MemberNotesDialog from '../components/MemberNotesDialog.vue'; +import MemberActivitiesDialog from '../components/MemberActivitiesDialog.vue'; export default { name: 'MembersView', components: { @@ -212,7 +221,8 @@ export default { ConfirmDialog, ImageViewerDialog, BaseDialog, - MemberNotesDialog + MemberNotesDialog, + MemberActivitiesDialog }, computed: { ...mapGetters(['isAuthenticated', 'currentClub']), @@ -271,7 +281,9 @@ export default { showInactiveMembers: false, newPicsInInternetAllowed: false, isUpdatingRatings: false, - showMemberInfo: false + showMemberInfo: false, + showActivitiesModal: false, + selectedMemberForActivities: null } }, async mounted() { @@ -483,6 +495,10 @@ export default { closeNotesModal() { this.showNotesModal = false; }, + openActivitiesModal(member) { + this.selectedMemberForActivities = member; + this.showActivitiesModal = true; + }, openImageModal(imageUrl, memberId) { this.selectedImageUrl = imageUrl; this.selectedMemberId = memberId; @@ -915,4 +931,21 @@ table td { min-width: 2rem; text-align: center; } + +/* Button Styles */ +.btn-activities { + margin-left: 0.5rem; + background-color: #28a745; + color: white; + border: none; + padding: 0.5rem 1rem; + border-radius: 4px; + cursor: pointer; + font-size: 0.9em; + transition: background-color 0.2s ease; +} + +.btn-activities:hover { + background-color: #218838; +}