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 @@
-Datum: {{ formatDate(playerSelectionDialog.match?.date) }}
+Uhrzeit: {{ playerSelectionDialog.match?.time ? playerSelectionDialog.match.time.toString().slice(0, 5) + ' Uhr' : 'N/A' }}
+| Spieler | +Bereit | +Vorgesehen | +Gespielt | +
|---|---|---|---|
| + {{ member.firstName }} {{ member.lastName }} + | ++ + | ++ + | ++ + | +