From ea3cca563b60fb7ed3a4561b4fcacfea2eff51df Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Thu, 16 Oct 2025 21:09:13 +0200 Subject: [PATCH] Enhance match management functionality by adding player selection capabilities. Introduce new endpoints for updating match players and retrieving player match statistics in matchController and matchService. Update Match model to include fields for players ready, planned, and played. Modify frontend components to support player selection dialog, allowing users to manage player statuses effectively. Improve UI for better user experience and data visibility. --- backend/controllers/matchController.js | 43 ++ .../add_player_tracking_to_match.sql | 8 + backend/models/Match.js | 18 + backend/routes/matchRoutes.js | 4 +- backend/server.js | 2 +- backend/services/matchService.js | 118 ++++ frontend/src/views/ScheduleView.vue | 279 ++++++++- frontend/src/views/TeamManagementView.vue | 572 ++++++++++++++---- 8 files changed, 919 insertions(+), 125 deletions(-) create mode 100644 backend/migrations/add_player_tracking_to_match.sql diff --git a/backend/controllers/matchController.js b/backend/controllers/matchController.js index 8448860..0547674 100644 --- a/backend/controllers/matchController.js +++ b/backend/controllers/matchController.js @@ -102,3 +102,46 @@ export const fetchLeagueTableFromMyTischtennis = async (req, res) => { return res.status(500).json({ error: 'Failed to fetch league table from MyTischtennis' }); } }; + +export const updateMatchPlayers = async (req, res) => { + try { + const { authcode: userToken } = req.headers; + const { matchId } = req.params; + const { playersReady, playersPlanned, playersPlayed } = req.body; + + const result = await MatchService.updateMatchPlayers( + userToken, + matchId, + playersReady, + playersPlanned, + playersPlayed + ); + + return res.status(200).json({ + message: 'Match players updated successfully', + data: result + }); + } catch (error) { + console.error('Error updating match players:', error); + return res.status(error.statusCode || 500).json({ + error: error.message || 'Failed to update match players' + }); + } +}; + +export const getPlayerMatchStats = async (req, res) => { + try { + const { authcode: userToken } = req.headers; + const { clubId, leagueId } = req.params; + const { seasonid: seasonId } = req.query; + + const stats = await MatchService.getPlayerMatchStats(userToken, clubId, leagueId, seasonId); + + return res.status(200).json(stats); + } catch (error) { + console.error('Error retrieving player match stats:', error); + return res.status(error.statusCode || 500).json({ + error: error.message || 'Failed to retrieve player match stats' + }); + } +}; diff --git a/backend/migrations/add_player_tracking_to_match.sql b/backend/migrations/add_player_tracking_to_match.sql new file mode 100644 index 0000000..b1a93f6 --- /dev/null +++ b/backend/migrations/add_player_tracking_to_match.sql @@ -0,0 +1,8 @@ +-- Add player tracking fields to match table +-- These fields store arrays of member IDs for different participation states + +ALTER TABLE `match` +ADD COLUMN `players_ready` JSON NULL COMMENT 'Array of member IDs who are ready to play' AFTER `pdf_url`, +ADD COLUMN `players_planned` JSON NULL COMMENT 'Array of member IDs who are planned to play' AFTER `players_ready`, +ADD COLUMN `players_played` JSON NULL COMMENT 'Array of member IDs who actually played' AFTER `players_planned`; + diff --git a/backend/models/Match.js b/backend/models/Match.js index b05d57b..591bbac 100644 --- a/backend/models/Match.js +++ b/backend/models/Match.js @@ -109,6 +109,24 @@ const Match = sequelize.define('Match', { comment: 'PDF URL from myTischtennis', field: 'pdf_url' }, + playersReady: { + type: DataTypes.JSON, + allowNull: true, + comment: 'Array of member IDs who are ready to play', + field: 'players_ready' + }, + playersPlanned: { + type: DataTypes.JSON, + allowNull: true, + comment: 'Array of member IDs who are planned to play', + field: 'players_planned' + }, + playersPlayed: { + type: DataTypes.JSON, + allowNull: true, + comment: 'Array of member IDs who actually played', + field: 'players_played' + }, }, { underscored: true, tableName: 'match', diff --git a/backend/routes/matchRoutes.js b/backend/routes/matchRoutes.js index 5c14f35..de0f6d4 100644 --- a/backend/routes/matchRoutes.js +++ b/backend/routes/matchRoutes.js @@ -1,5 +1,5 @@ import express from 'express'; -import { uploadCSV, getLeaguesForCurrentSeason, getMatchesForLeagues, getMatchesForLeague, getLeagueTable, fetchLeagueTableFromMyTischtennis } from '../controllers/matchController.js'; +import { uploadCSV, getLeaguesForCurrentSeason, getMatchesForLeagues, getMatchesForLeague, getLeagueTable, fetchLeagueTableFromMyTischtennis, updateMatchPlayers, getPlayerMatchStats } from '../controllers/matchController.js'; import { authenticate } from '../middleware/authMiddleware.js'; import multer from 'multer'; @@ -13,6 +13,8 @@ router.get('/leagues/:clubId/matches/:leagueId', authenticate, getMatchesForLeag router.get('/leagues/:clubId/matches', authenticate, getMatchesForLeagues); router.get('/leagues/:clubId/table/:leagueId', authenticate, getLeagueTable); router.post('/leagues/:clubId/table/:leagueId/fetch', authenticate, fetchLeagueTableFromMyTischtennis); +router.patch('/:matchId/players', authenticate, updateMatchPlayers); +router.get('/leagues/:clubId/stats/:leagueId', authenticate, getPlayerMatchStats); export default router; diff --git a/backend/server.js b/backend/server.js index 9d32e55..ce58807 100644 --- a/backend/server.js +++ b/backend/server.js @@ -49,7 +49,7 @@ const __dirname = path.dirname(__filename); app.use(cors({ origin: true, credentials: true, - methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], + methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], allowedHeaders: ['Content-Type', 'Authorization', 'authcode', 'userid'] })); app.use(express.json()); diff --git a/backend/services/matchService.js b/backend/services/matchService.js index a2676e6..3655be9 100644 --- a/backend/services/matchService.js +++ b/backend/services/matchService.js @@ -241,6 +241,9 @@ class MatchService { guestMatchPoints: match.guestMatchPoints || 0, isCompleted: match.isCompleted || false, pdfUrl: match.pdfUrl, + playersReady: match.playersReady || [], + playersPlanned: match.playersPlanned || [], + playersPlayed: match.playersPlayed || [], homeTeam: { name: 'Unbekannt' }, guestTeam: { name: 'Unbekannt' }, location: { name: 'Unbekannt', address: '', city: '', zip: '' }, @@ -353,6 +356,9 @@ class MatchService { guestMatchPoints: match.guestMatchPoints || 0, isCompleted: match.isCompleted || false, pdfUrl: match.pdfUrl, + playersReady: match.playersReady || [], + playersPlanned: match.playersPlanned || [], + playersPlayed: match.playersPlayed || [], homeTeam: { name: 'Unbekannt' }, guestTeam: { name: 'Unbekannt' }, location: { name: 'Unbekannt', address: '', city: '', zip: '' }, @@ -430,6 +436,118 @@ class MatchService { } } + async updateMatchPlayers(userToken, matchId, playersReady, playersPlanned, playersPlayed) { + // Find the match and verify access + const match = await Match.findByPk(matchId, { + include: [ + { model: Club, as: 'club' } + ] + }); + + if (!match) { + throw new HttpError(404, 'Match not found'); + } + + await checkAccess(userToken, match.clubId); + + // Update player arrays + await match.update({ + playersReady: playersReady || [], + playersPlanned: playersPlanned || [], + playersPlayed: playersPlayed || [] + }); + + return { + id: match.id, + playersReady: match.playersReady, + playersPlanned: match.playersPlanned, + playersPlayed: match.playersPlayed + }; + } + + async getPlayerMatchStats(userToken, clubId, leagueId, seasonId) { + await checkAccess(userToken, clubId); + + // Get all matches for this league/season + const matches = await Match.findAll({ + where: { + clubId: clubId, + leagueId: leagueId + }, + attributes: ['id', 'date', 'playersPlayed'] + }); + + // Get all members + const Member = (await import('../models/Member.js')).default; + const members = await Member.findAll({ + where: { clubId: clubId, active: true }, + attributes: ['id', 'firstName', 'lastName'] + }); + + // Calculate stats + const stats = {}; + const now = new Date(); + + // Saison startet am 1. Juli + const seasonStart = new Date(); + seasonStart.setMonth(6, 1); // 1. Juli + seasonStart.setHours(0, 0, 0, 0); + if (seasonStart > now) { + seasonStart.setFullYear(seasonStart.getFullYear() - 1); + } + + // Vorrunde: 1. Juli bis 31. Dezember + const firstHalfEnd = new Date(seasonStart.getFullYear(), 11, 31, 23, 59, 59, 999); // 31. Dezember + + // Rückrunde startet am 1. Januar (im Jahr nach Saisonstart) + const secondHalfStart = new Date(seasonStart.getFullYear() + 1, 0, 1, 0, 0, 0, 0); // 1. Januar + + for (const member of members) { + stats[member.id] = { + memberId: member.id, + firstName: member.firstName, + lastName: member.lastName, + totalSeason: 0, + totalFirstHalf: 0, + totalSecondHalf: 0 + }; + } + + for (const match of matches) { + // Parse playersPlayed if it's a JSON string + let playersPlayed = match.playersPlayed; + if (typeof playersPlayed === 'string') { + try { + playersPlayed = JSON.parse(playersPlayed); + } catch (e) { + continue; + } + } + + if (!playersPlayed || !Array.isArray(playersPlayed) || playersPlayed.length === 0) { + continue; + } + + const matchDate = new Date(match.date); + const isInSeason = matchDate >= seasonStart; + const isInFirstHalf = matchDate >= seasonStart && matchDate <= firstHalfEnd; + const isInSecondHalf = matchDate >= secondHalfStart; + + for (const playerId of playersPlayed) { + if (stats[playerId]) { + if (isInSeason) stats[playerId].totalSeason++; + if (isInFirstHalf) stats[playerId].totalFirstHalf++; + if (isInSecondHalf) stats[playerId].totalSecondHalf++; + } + } + } + + // Convert to array, filter out players with 0 matches, and sort by totalSeason descending + return Object.values(stats) + .filter(player => player.totalSeason > 0) + .sort((a, b) => b.totalSeason - a.totalSeason); + } + } export default new MatchService(); diff --git a/frontend/src/views/ScheduleView.vue b/frontend/src/views/ScheduleView.vue index 474aeb2..5d34500 100644 --- a/frontend/src/views/ScheduleView.vue +++ b/frontend/src/views/ScheduleView.vue @@ -61,8 +61,12 @@ - + {{ formatDate(match.date) }} {{ match.time ? match.time.toString().slice(0, 5) + ' Uhr' : 'N/A' }} @@ -162,6 +166,75 @@ + + + +
+ Lade Mitglieder... +
+ +
+
+

Datum: {{ formatDate(playerSelectionDialog.match?.date) }}

+

Uhrzeit: {{ playerSelectionDialog.match?.time ? playerSelectionDialog.match.time.toString().slice(0, 5) + ' Uhr' : 'N/A' }}

+
+ +
+ + + + + + + + + + + + + + + + + +
SpielerBereitVorgesehenGespielt
+ {{ member.firstName }} {{ member.lastName }} + + + + + + +
+ +
+ Keine aktiven Mitglieder gefunden +
+
+ +
+ + +
+
+