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.

This commit is contained in:
Torsten Schulz (local)
2025-10-16 21:09:13 +02:00
parent e0d56ddadd
commit ea3cca563b
8 changed files with 919 additions and 125 deletions

View File

@@ -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'
});
}
};

View File

@@ -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`;

View File

@@ -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',

View File

@@ -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;

View File

@@ -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());

View File

@@ -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();