diff --git a/backend/controllers/diaryDateActivityController.js b/backend/controllers/diaryDateActivityController.js index 9f80f51c..d34261c0 100644 --- a/backend/controllers/diaryDateActivityController.js +++ b/backend/controllers/diaryDateActivityController.js @@ -7,10 +7,11 @@ export const createDiaryDateActivity = async (req, res) => { try { const { authcode: userToken } = req.headers; const { clubId } = req.params; - const { diaryDateId, activity, duration, durationText, orderId, isTimeblock } = req.body; + const { diaryDateId, activity, predefinedActivityId, duration, durationText, orderId, isTimeblock } = req.body; const activityItem = await diaryDateActivityService.createActivity(userToken, clubId, { diaryDateId, activity, + predefinedActivityId, duration, durationText, orderId, @@ -197,4 +198,4 @@ export const deleteGroupActivity = async(req, res) => { devLog(error); res.status(500).json({ error: 'Error deleting group activity' }); } -} \ No newline at end of file +} diff --git a/backend/controllers/matchController.js b/backend/controllers/matchController.js index 13c7d989..b3e5fef1 100644 --- a/backend/controllers/matchController.js +++ b/backend/controllers/matchController.js @@ -51,7 +51,8 @@ export const getMatchesForLeague = async (req, res) => { try { const { authcode: userToken } = req.headers; const { clubId, leagueId } = req.params; - const matches = await MatchService.getMatchesForLeague(userToken, clubId, leagueId); + const { scope = 'own' } = req.query; + const matches = await MatchService.getMatchesForLeague(userToken, clubId, leagueId, scope); return res.status(200).json(matches); } catch (error) { console.error('Error retrieving matches:', error); diff --git a/backend/controllers/memberActivityController.js b/backend/controllers/memberActivityController.js index 3d3f21d9..c49339ec 100644 --- a/backend/controllers/memberActivityController.js +++ b/backend/controllers/memberActivityController.js @@ -7,6 +7,22 @@ import PredefinedActivity from '../models/PredefinedActivity.js'; import GroupActivity from '../models/GroupActivity.js'; import { Op } from 'sequelize'; +const STANDARD_ACTIVITY_NAMES = new Set([ + 'Begrüßung', + 'Aktivierung', + 'Aufbauen', + 'Turnier', + 'Abbauen', + 'Abschlussgespräch', +]); + +const isTrackablePredefinedActivity = (predefinedActivity) => { + if (!predefinedActivity) { + return false; + } + return !predefinedActivity.excludeFromStats && !STANDARD_ACTIVITY_NAMES.has(predefinedActivity.name); +}; + export const getMemberActivities = async (req, res) => { try { const { authcode: userToken } = req.headers; @@ -213,12 +229,18 @@ export const getMemberActivities = async (req, res) => { if (!ga.activity || !ga.activity.id || !ga.participant || !ga.participant.id) { return false; } + if (!isTrackablePredefinedActivity(ga.activity.predefinedActivity)) { + return false; + } const key = `${ga.activity.id}-${ga.participant.id}`; return !explicitActivityKeys.has(key); }); // Kombiniere beide Listen - const allActivities = [...filteredMemberActivities, ...uniqueGroupActivities]; + const allActivities = [...filteredMemberActivities, ...uniqueGroupActivities].filter((entry) => { + const predefinedActivity = entry?.activity?.predefinedActivity; + return isTrackablePredefinedActivity(predefinedActivity); + }); // Group activities by name and count occurrences // Verwende einen Set pro Aktivität, um eindeutige Datum-Aktivität-Kombinationen zu tracken @@ -228,6 +250,10 @@ export const getMemberActivities = async (req, res) => { if (!ma.activity || !ma.activity.predefinedActivity || !ma.participant) { continue; } + + if (!isTrackablePredefinedActivity(ma.activity.predefinedActivity)) { + continue; + } const activity = ma.activity.predefinedActivity; const activityName = activity.name; @@ -454,12 +480,18 @@ export const getMemberLastParticipations = async (req, res) => { if (!ga.activity || !ga.activity.id || !ga.participant || !ga.participant.id) { return false; } + if (!isTrackablePredefinedActivity(ga.activity.predefinedActivity)) { + return false; + } const key = `${ga.activity.id}-${ga.participant.id}`; return !explicitActivityKeys.has(key); }); // Kombiniere beide Listen - const allActivities = [...filteredMemberActivities, ...uniqueGroupActivities]; + const allActivities = [...filteredMemberActivities, ...uniqueGroupActivities].filter((entry) => { + const predefinedActivity = entry?.activity?.predefinedActivity; + return isTrackablePredefinedActivity(predefinedActivity); + }); // Gruppiere nach Datum const participationsByDate = new Map(); @@ -469,7 +501,7 @@ export const getMemberLastParticipations = async (req, res) => { if (!ma.activity || !ma.activity.predefinedActivity || !ma.activity.diaryDate || !ma.participant) { return false; } - return true; + return isTrackablePredefinedActivity(ma.activity.predefinedActivity); }) .forEach(ma => { const date = ma.activity.diaryDate.date; @@ -528,4 +560,3 @@ export const getMemberLastParticipations = async (req, res) => { return res.status(500).json({ error: 'Failed to fetch member last participations' }); } }; - diff --git a/backend/controllers/predefinedActivityController.js b/backend/controllers/predefinedActivityController.js index 1320a7ac..dcd91b8d 100644 --- a/backend/controllers/predefinedActivityController.js +++ b/backend/controllers/predefinedActivityController.js @@ -5,8 +5,8 @@ import fs from 'fs'; export const createPredefinedActivity = async (req, res) => { try { - const { name, code, description, durationText, duration, imageLink, drawingData } = req.body; - const predefinedActivity = await predefinedActivityService.createPredefinedActivity({ name, code, description, durationText, duration, imageLink, drawingData }); + const { name, code, description, durationText, duration, imageLink, drawingData, excludeFromStats } = req.body; + const predefinedActivity = await predefinedActivityService.createPredefinedActivity({ name, code, description, durationText, duration, imageLink, drawingData, excludeFromStats }); res.status(201).json(predefinedActivity); } catch (error) { console.error('[createPredefinedActivity] - Error:', error); @@ -16,7 +16,8 @@ export const createPredefinedActivity = async (req, res) => { export const getAllPredefinedActivities = async (req, res) => { try { - const predefinedActivities = await predefinedActivityService.getAllPredefinedActivities(); + const { scope = 'all' } = req.query; + const predefinedActivities = await predefinedActivityService.getAllPredefinedActivities(scope); res.status(200).json(predefinedActivities); } catch (error) { console.error('[getAllPredefinedActivities] - Error:', error); @@ -42,8 +43,8 @@ export const getPredefinedActivityById = async (req, res) => { export const updatePredefinedActivity = async (req, res) => { try { const { id } = req.params; - const { name, code, description, durationText, duration, imageLink, drawingData } = req.body; - const updatedActivity = await predefinedActivityService.updatePredefinedActivity(id, { name, code, description, durationText, duration, imageLink, drawingData }); + const { name, code, description, durationText, duration, imageLink, drawingData, excludeFromStats } = req.body; + const updatedActivity = await predefinedActivityService.updatePredefinedActivity(id, { name, code, description, durationText, duration, imageLink, drawingData, excludeFromStats }); res.status(200).json(updatedActivity); } catch (error) { console.error('[updatePredefinedActivity] - Error:', error); diff --git a/backend/models/PredefinedActivity.js b/backend/models/PredefinedActivity.js index 63c7d8dd..3744bbc6 100644 --- a/backend/models/PredefinedActivity.js +++ b/backend/models/PredefinedActivity.js @@ -36,6 +36,11 @@ const PredefinedActivity = sequelize.define('PredefinedActivity', { type: DataTypes.STRING, allowNull: true, }, + excludeFromStats: { + type: DataTypes.BOOLEAN, + allowNull: false, + defaultValue: false, + }, }, { tableName: 'predefined_activities', timestamps: true, diff --git a/backend/routes/schedulerRoutes.js b/backend/routes/schedulerRoutes.js new file mode 100644 index 00000000..664a5b9a --- /dev/null +++ b/backend/routes/schedulerRoutes.js @@ -0,0 +1,38 @@ +import express from 'express'; +import { authenticate } from '../middleware/authMiddleware.js'; +import schedulerService from '../services/schedulerService.js'; + +const router = express.Router(); + +router.post('/rating_updates', authenticate, async (req, res) => { + try { + const result = await schedulerService.triggerRatingUpdates(); + res.json(result); + } catch (error) { + res.status(500).json({ success: false, message: error.message }); + } +}); + +router.post('/match_results', authenticate, async (req, res) => { + try { + const result = await schedulerService.triggerMatchResultsFetch({ + userId: req.user.id, + clubTeamId: req.body?.clubTeamId ?? null, + currentSeasonOnly: true + }); + res.json(result); + } catch (error) { + res.status(500).json({ success: false, message: error.message }); + } +}); + +router.get('/status', authenticate, (req, res) => { + const status = schedulerService.getStatus(); + const nextRatingUpdate = schedulerService.getNextRatingUpdateTime(); + res.json({ + ...status, + nextRatingUpdate + }); +}); + +export default router; diff --git a/backend/routes/sessionRoutes.js b/backend/routes/sessionRoutes.js index 257130f4..5c0736b7 100644 --- a/backend/routes/sessionRoutes.js +++ b/backend/routes/sessionRoutes.js @@ -1,38 +1,9 @@ import express from 'express'; import { authenticate } from '../middleware/authMiddleware.js'; import sessionController from '../controllers/sessionController.js'; -import schedulerService from '../services/schedulerService.js'; const router = express.Router(); router.get('/status', authenticate, sessionController.checkSession); -// Manual trigger endpoints for testing -router.post('/trigger-rating-updates', authenticate, async (req, res) => { - try { - const result = await schedulerService.triggerRatingUpdates(); - res.json(result); - } catch (error) { - res.status(500).json({ success: false, message: error.message }); - } -}); - -router.post('/trigger-match-fetch', authenticate, async (req, res) => { - try { - const result = await schedulerService.triggerMatchResultsFetch(); - res.json(result); - } catch (error) { - res.status(500).json({ success: false, message: error.message }); - } -}); - -router.get('/scheduler-status', authenticate, (req, res) => { - const status = schedulerService.getStatus(); - const nextRatingUpdate = schedulerService.getNextRatingUpdateTime(); - res.json({ - ...status, - nextRatingUpdate - }); -}); - export default router; diff --git a/backend/server.js b/backend/server.js index 130264b0..770b6ca8 100644 --- a/backend/server.js +++ b/backend/server.js @@ -34,6 +34,7 @@ import Location from './models/Location.js'; import groupRoutes from './routes/groupRoutes.js'; import diaryDateTagRoutes from './routes/diaryDateTagRoutes.js'; import sessionRoutes from './routes/sessionRoutes.js'; +import schedulerRoutes from './routes/schedulerRoutes.js'; import tournamentRoutes from './routes/tournamentRoutes.js'; import accidentRoutes from './routes/accidentRoutes.js'; import trainingStatsRoutes from './routes/trainingStatsRoutes.js'; @@ -113,6 +114,7 @@ app.use('/api/matches', matchRoutes); app.use('/api/group', groupRoutes); app.use('/api/diarydatetags', diaryDateTagRoutes); app.use('/api/session', sessionRoutes); +app.use('/api/scheduler', schedulerRoutes); app.use('/api/tournament', tournamentRoutes); app.use('/api/accident', accidentRoutes); app.use('/api/training-stats', trainingStatsRoutes); diff --git a/backend/services/autoFetchMatchResultsService.js b/backend/services/autoFetchMatchResultsService.js index 4915b90f..7be70f25 100644 --- a/backend/services/autoFetchMatchResultsService.js +++ b/backend/services/autoFetchMatchResultsService.js @@ -11,18 +11,30 @@ import Match from '../models/Match.js'; import Team from '../models/Team.js'; import { Op } from 'sequelize'; import { devLog } from '../utils/logger.js'; +import SeasonService from './seasonService.js'; class AutoFetchMatchResultsService { /** * Execute automatic match results fetching for all users with enabled auto-updates */ - async executeAutomaticFetch() { + async executeAutomaticFetch(options = {}) { devLog('Starting automatic match results fetch...'); try { + const { + userId = null, + clubTeamId = null, + currentSeasonOnly = true + } = options; + + const currentSeason = currentSeasonOnly + ? await SeasonService.getOrCreateCurrentSeason() + : null; + // Find all users with auto-updates enabled const accounts = await MyTischtennis.findAll({ where: { + ...(userId ? { userId } : {}), autoUpdateRatings: true, savePassword: true } @@ -39,14 +51,22 @@ class AutoFetchMatchResultsService { // Process each account and collect summaries const summaries = []; for (const account of accounts) { - const summary = await this.processAccount(account); + const summary = await this.processAccount(account, { + clubTeamId, + currentSeasonId: currentSeason?.id ?? null + }); summaries.push({ userId: account.userId, ...summary }); } devLog('Automatic match results fetch completed'); // Return overall summary including per-account counts const totalFetched = summaries.reduce((acc, s) => acc + (s.fetchedCount || 0), 0); - return { success: true, totalFetched, summaries }; + return { + success: true, + totalFetched, + currentSeasonId: currentSeason?.id ?? null, + summaries + }; } catch (error) { console.error('Error in automatic match results fetch:', error); throw error; @@ -56,12 +76,13 @@ class AutoFetchMatchResultsService { /** * Process a single account for match results fetching */ - async processAccount(account) { + async processAccount(account, options = {}) { const startTime = Date.now(); let success = false; let message = ''; let errorDetails = null; let fetchedCount = 0; + let teamSummaries = []; try { devLog(`Processing match results for account ${account.email} (User ID: ${account.userId})`); @@ -84,8 +105,9 @@ class AutoFetchMatchResultsService { } // Perform match results fetch - const fetchResult = await this.fetchMatchResults(account); + const fetchResult = await this.fetchMatchResults(account, options); fetchedCount = fetchResult.fetchedCount || 0; + teamSummaries = fetchResult.teamSummaries || []; success = true; message = `Successfully fetched ${fetchedCount} match results`; @@ -117,21 +139,25 @@ class AutoFetchMatchResultsService { devLog(`Match results fetch for ${account.email}: ${success ? 'SUCCESS' : 'FAILED'} (${executionTime}ms)`); // Return a summary for scheduler - return { success, message, fetchedCount, errorDetails, executionTime }; + return { success, message, fetchedCount, errorDetails, executionTime, teamSummaries }; } /** * Fetch match results for a specific account */ - async fetchMatchResults(account) { + async fetchMatchResults(account, options = {}) { devLog(`Fetching match results for ${account.email}`); let totalFetched = 0; try { + const { clubTeamId = null, currentSeasonId = null } = options; + // Get all teams for this user's clubs that have myTischtennis IDs configured const teams = await ClubTeam.findAll({ where: { + ...(clubTeamId ? { id: clubTeamId } : {}), + ...(currentSeasonId ? { seasonId: currentSeasonId } : {}), myTischtennisTeamId: { [Op.ne]: null } @@ -160,19 +186,34 @@ class AutoFetchMatchResultsService { devLog(`Found ${teams.length} teams with myTischtennis configuration`); + const teamSummaries = []; + // Fetch results for each team for (const team of teams) { try { const result = await this.fetchTeamResults(account, team); totalFetched += result.fetchedCount; + teamSummaries.push({ + teamId: team.id, + teamName: team.name, + leagueId: team.leagueId, + ...result.debugSummary + }); } catch (error) { console.error(`Error fetching results for team ${team.name}:`, error); + teamSummaries.push({ + teamId: team.id, + teamName: team.name, + leagueId: team.leagueId, + error: error.message + }); } } return { success: true, - fetchedCount: totalFetched + fetchedCount: totalFetched, + teamSummaries }; } catch (error) { console.error('Error in fetchMatchResults:', error); @@ -211,8 +252,22 @@ class AutoFetchMatchResultsService { devLog(`MyTischtennis Team ID: ${team.myTischtennisTeamId}`); let totalProcessed = 0; + const debugSummary = { + scheduleMatchesProcessed: 0, + leagueScheduleMatchesProcessed: 0, + tableMatchesProcessed: 0, + playerStatsProcessed: 0 + }; try { + const scheduleProcessed = await this.fetchTeamSchedule(account, team, seasonStr, teamnameEncoded); + totalProcessed += scheduleProcessed; + debugSummary.scheduleMatchesProcessed = scheduleProcessed; + + const leagueScheduleProcessed = await this.fetchLeagueSchedule(account, team, seasonStr); + totalProcessed += leagueScheduleProcessed; + debugSummary.leagueScheduleMatchesProcessed = leagueScheduleProcessed; + // 1. Fetch player statistics (Spielerbilanzen) const playerStatsUrl = `https://www.mytischtennis.de/click-tt/${league.association}/${seasonStr}/ligen/${league.groupname}/gruppe/${league.myTischtennisGroupId}/mannschaft/${team.myTischtennisTeamId}/${teamnameEncoded}/spielerbilanzen/gesamt?_data=routes%2Fclick-tt%2B%2F%24association%2B%2F%24season%2B%2F%24type%2B%2F%28%24groupname%29.gruppe.%24urlid_.mannschaft.%24teamid.%24teamname%2B%2Fspielerbilanzen.%24filter`; @@ -246,6 +301,7 @@ class AutoFetchMatchResultsService { const playerCount = await this.processTeamData(team, playerStatsData); totalProcessed += playerCount; + debugSummary.playerStatsProcessed = playerCount; devLog(`Processed ${playerCount} player statistics`); } else { // Read error response body @@ -303,6 +359,8 @@ class AutoFetchMatchResultsService { const tableStartTime = Date.now(); const tableResult = await this.fetchAndUpdateLeagueTable(account.userId, team.leagueId); const tableExecutionTime = Date.now() - tableStartTime; + totalProcessed += tableResult?.matchesProcessed || 0; + debugSummary.tableMatchesProcessed = tableResult?.matchesProcessed || 0; devLog(`✓ League table updated for league ${team.leagueId}`); @@ -341,7 +399,8 @@ class AutoFetchMatchResultsService { return { success: true, - fetchedCount: totalProcessed + fetchedCount: totalProcessed, + debugSummary }; } catch (error) { console.error(`Error fetching team results for ${team.name}:`, error); @@ -349,6 +408,98 @@ class AutoFetchMatchResultsService { } } + async fetchTeamSchedule(account, team, seasonStr, teamnameEncoded) { + const league = team.league; + const schedulePath = `/click-tt/${league.association}/${seasonStr}/ligen/${league.groupname}/gruppe/${league.myTischtennisGroupId}/mannschaft/${team.myTischtennisTeamId}/${teamnameEncoded}/spielplan`; + const scheduleDataRoutes = [ + `${schedulePath}?_data=routes%2Fclick-tt%2B%2F%24association%2B%2F%24season%2B%2F%24type%2B%2F%28%24groupname%29.gruppe.%24urlid_.mannschaft.%24teamid.%24teamname%2B%2Fspielplan`, + `${schedulePath}?_data=routes%2Fclick-tt%2B%2F%24association%2B%2F%24season%2B%2F%24type%2B%2F%28%24groupname%29.gruppe.%24urlid_.mannschaft.%24teamid.%24teamname%2B%2Fspielplan.%24filter`, + schedulePath + ]; + + for (const endpoint of scheduleDataRoutes) { + try { + devLog(`Fetching schedule from: ${endpoint}`); + const response = await myTischtennisClient.authenticatedRequest(endpoint, account.cookie || '', { + method: 'GET' + }); + + if (!response.success) { + devLog(`Schedule fetch failed for endpoint ${endpoint}: ${response.error}`); + continue; + } + + const payload = response.data; + if (!payload) { + continue; + } + + const processed = await this.processMatchResults(team, payload); + if (processed > 0) { + devLog(`Processed ${processed} matches from team schedule for ${team.name}`); + return processed; + } + } catch (error) { + console.error(`Error fetching team schedule for ${team.name}:`, error); + } + } + + return 0; + } + + buildLeagueScheduleDataRoute(roundKey) { + return 'routes%2Fclick-tt%2B%2F%24association%2B%2F%24season%2B%2F%24type%2B%2F%24groupname.gruppe.%24urlid%2B%2Fspielplan.%24filter'; + } + + buildLeagueGroupNameVariants(groupname) { + const variants = []; + if (typeof groupname === 'string' && groupname.trim()) { + variants.push(groupname); + variants.push(groupname.replace(/\s/g, '_')); + variants.push(`_${groupname.replace(/\s/g, '_')}`); + } + return [...new Set(variants)]; + } + + async fetchLeagueSchedule(account, team, seasonStr) { + const league = team.league; + const groupNameVariants = this.buildLeagueGroupNameVariants(league.groupname); + const roundKeys = ['vr', 'rr']; + let totalProcessed = 0; + + for (const groupName of groupNameVariants) { + for (const roundKey of roundKeys) { + const endpoint = `/click-tt/${league.association}/${seasonStr}/ligen/${groupName}/gruppe/${league.myTischtennisGroupId}/spielplan/${roundKey}?_data=${this.buildLeagueScheduleDataRoute(roundKey)}`; + + try { + devLog(`Fetching league schedule from: ${endpoint}`); + const response = await myTischtennisClient.authenticatedRequest(endpoint, account.cookie || '', { + method: 'GET' + }); + + if (!response.success) { + devLog(`League schedule fetch failed for endpoint ${endpoint}: ${response.error}`); + continue; + } + + const processed = await this.processMatchResults(team, response.data); + if (processed > 0) { + devLog(`Processed ${processed} matches from league ${roundKey.toUpperCase()} schedule for ${team.name}`); + totalProcessed += processed; + } + } catch (error) { + console.error(`Error fetching league ${roundKey.toUpperCase()} schedule for ${team.name}:`, error); + } + } + + if (totalProcessed > 0) { + return totalProcessed; + } + } + + return 0; + } + /** * Process and store team data from myTischtennis */ @@ -435,19 +586,19 @@ class AutoFetchMatchResultsService { devLog(`Processing match results for team ${team.name}`); // Handle different response structures from different endpoints - const meetingsExcerpt = data.data?.meetings_excerpt || data.tableData?.meetings_excerpt; - - if (!meetingsExcerpt) { - devLog('No meetings_excerpt data found in response'); + const meetingSource = + data.data?.meetings_excerpt || + data.tableData?.meetings_excerpt || + data.meetings_excerpt || + data.data?.meetings || + data.meetings; + + if (!meetingSource) { + devLog('No meeting data found in response'); return 0; } - let processedCount = 0; - - // Handle both response structures: - // 1. With meetings property: meetings_excerpt.meetings (array of date objects) - // 2. Direct array: meetings_excerpt (array of date objects) - const meetings = meetingsExcerpt.meetings || meetingsExcerpt; + const meetings = meetingSource.meetings || meetingSource; if (!Array.isArray(meetings) || meetings.length === 0) { devLog('No meetings array found or empty'); @@ -456,47 +607,44 @@ class AutoFetchMatchResultsService { devLog(`Found ${meetings.length} items in meetings array`); - // Check if meetings is an array of date objects or an array of match objects const firstItem = meetings[0]; const isDateGrouped = firstItem && typeof firstItem === 'object' && !firstItem.meeting_id; - + const flattenedMatches = []; + if (isDateGrouped) { - // Format 1: Array of date objects (Spielerbilanzen, Spielplan) devLog('Processing date-grouped meetings...'); for (const dateGroup of meetings) { - for (const [date, matchList] of Object.entries(dateGroup)) { - for (const match of matchList) { - devLog(`Match: ${match.team_home} vs ${match.team_away}`); - devLog(` Date: ${match.date}`); - devLog(` Status: ${match.state} (${match.is_meeting_complete ? 'complete' : 'incomplete'})`); - devLog(` Result: ${match.matches_won}:${match.matches_lost}`); - devLog(` Meeting ID: ${match.meeting_id}`); - - try { - await this.storeMatchResult(team, match, false); - processedCount++; - } catch (error) { - console.error(`Error storing match result for meeting ${match.meeting_id}:`, error); - } + for (const matchList of Object.values(dateGroup)) { + if (Array.isArray(matchList)) { + flattenedMatches.push(...matchList); } } } } else { - // Format 2: Flat array of match objects (Tabelle) devLog('Processing flat meetings array...'); - for (const match of meetings) { - devLog(`Match: ${match.team_home} vs ${match.team_away}`); - devLog(` Date: ${match.date}`); - devLog(` Status: ${match.state} (${match.is_meeting_complete ? 'complete' : 'incomplete'})`); - devLog(` Result: ${match.matches_won}:${match.matches_lost}`); - devLog(` Meeting ID: ${match.meeting_id}`); + flattenedMatches.push(...meetings); + } - try { - await this.storeMatchResult(team, match, false); - processedCount++; - } catch (error) { - console.error(`Error storing match result for meeting ${match.meeting_id}:`, error); - } + const uniqueMatches = flattenedMatches.filter((match, index, list) => { + if (!match?.meeting_id) { + return true; + } + return list.findIndex((entry) => entry?.meeting_id === match.meeting_id) === index; + }); + + let processedCount = 0; + for (const match of uniqueMatches) { + devLog(`Match: ${match.team_home} vs ${match.team_away}`); + devLog(` Date: ${match.date}`); + devLog(` Status: ${match.state} (${match.is_meeting_complete ? 'complete' : 'incomplete'})`); + devLog(` Result: ${match.matches_won}:${match.matches_lost}`); + devLog(` Meeting ID: ${match.meeting_id}`); + + try { + await this.storeMatchResult(team, match, false); + processedCount++; + } catch (error) { + console.error(`Error storing match result for meeting ${match.meeting_id}:`, error); } } @@ -621,6 +769,8 @@ class AutoFetchMatchResultsService { } const updateData = { + date: matchData.date, + time: matchData.date ? `${String(new Date(matchData.date).getHours()).padStart(2, '0')}:${String(new Date(matchData.date).getMinutes()).padStart(2, '0')}:00` : match.time, homeMatchPoints: finalHomePoints, guestMatchPoints: finalGuestPoints, isCompleted: matchData.is_meeting_complete, @@ -831,7 +981,7 @@ class AutoFetchMatchResultsService { if (!response.success) { throw new Error(`Failed to fetch table data: ${response.error}`); } - + const tableData = await this.parseTableData(JSON.stringify(response.data), leagueId); // Update teams with table data @@ -841,7 +991,8 @@ class AutoFetchMatchResultsService { return { teamsUpdated, - totalTeams: tableData.length + totalTeams: tableData.length, + matchesProcessed: 0 }; } catch (error) { @@ -944,4 +1095,3 @@ class AutoFetchMatchResultsService { } export default new AutoFetchMatchResultsService(); - diff --git a/backend/services/matchService.js b/backend/services/matchService.js index 36bee54a..c560b8a4 100644 --- a/backend/services/matchService.js +++ b/backend/services/matchService.js @@ -271,7 +271,7 @@ class MatchService { return enrichedMatches; } - async getMatchesForLeague(userToken, clubId, leagueId) { + async getMatchesForLeague(userToken, clubId, leagueId, scope = 'own') { await checkAccess(userToken, clubId); const seasonString = this.generateSeasonString(); const season = await Season.findOne({ @@ -290,46 +290,41 @@ class MatchService { if (!club) { throw new Error('Club not found'); } - const clubName = club.name; - - // Find all club teams in this league - const clubTeams = await ClubTeam.findAll({ - where: { - clubId: clubId, - leagueId: leagueId - }, - attributes: ['id', 'name'] - }); - - // Find all Team entries that contain our club name - const ownTeams = await Team.findAll({ - where: { - name: { - [Op.like]: `${clubName}%` - }, - leagueId: leagueId - }, - attributes: ['id', 'name'] - }); - - const ownTeamIds = ownTeams.map(t => t.id); - - // Load matches let matches; - if (ownTeamIds.length > 0) { - // Load only matches where one of our teams is involved + if (scope === 'all') { matches = await Match.findAll({ where: { - leagueId: leagueId, - [Op.or]: [ - { homeTeamId: { [Op.in]: ownTeamIds } }, - { guestTeamId: { [Op.in]: ownTeamIds } } - ] + leagueId: leagueId } }); } else { - // No own teams found - show nothing - matches = []; + const clubName = club.name; + + const ownTeams = await Team.findAll({ + where: { + name: { + [Op.like]: `${clubName}%` + }, + leagueId: leagueId + }, + attributes: ['id', 'name'] + }); + + const ownTeamIds = ownTeams.map(t => t.id); + + if (ownTeamIds.length > 0) { + matches = await Match.findAll({ + where: { + leagueId: leagueId, + [Op.or]: [ + { homeTeamId: { [Op.in]: ownTeamIds } }, + { guestTeamId: { [Op.in]: ownTeamIds } } + ] + } + }); + } else { + matches = []; + } } // Lade Team- und Location-Daten manuell @@ -610,4 +605,3 @@ class MatchService { } export default new MatchService(); - diff --git a/backend/services/predefinedActivityService.js b/backend/services/predefinedActivityService.js index 69d09e9a..377f18a8 100644 --- a/backend/services/predefinedActivityService.js +++ b/backend/services/predefinedActivityService.js @@ -15,6 +15,7 @@ class PredefinedActivityService { durationText: data.durationText, duration: data.duration, imageLink: data.imageLink, + excludeFromStats: Boolean(data.excludeFromStats), drawingData: data.drawingData ? JSON.stringify(data.drawingData) : null, }); } @@ -31,12 +32,21 @@ class PredefinedActivityService { durationText: data.durationText, duration: data.duration, imageLink: data.imageLink, + excludeFromStats: Boolean(data.excludeFromStats), drawingData: data.drawingData ? JSON.stringify(data.drawingData) : null, }); } - async getAllPredefinedActivities() { + async getAllPredefinedActivities(scope = 'all') { + const where = {}; + if (scope === 'standard') { + where.excludeFromStats = true; + } else if (scope === 'custom') { + where.excludeFromStats = false; + } + return await PredefinedActivity.findAll({ + where, order: [ [sequelize.literal('code IS NULL'), 'ASC'], // Non-null codes first ['code', 'ASC'], diff --git a/backend/services/schedulerService.js b/backend/services/schedulerService.js index 22dcd82a..cba6fe60 100644 --- a/backend/services/schedulerService.js +++ b/backend/services/schedulerService.js @@ -24,10 +24,10 @@ class SchedulerService { } } - async runMatchResultsFetchJob(isAutomatic = true) { + async runMatchResultsFetchJob(isAutomatic = true, options = {}) { const startTime = Date.now(); try { - const result = await autoFetchMatchResultsService.executeAutomaticFetch(); + const result = await autoFetchMatchResultsService.executeAutomaticFetch(options); const executionTime = Date.now() - startTime; await apiLogService.logSchedulerExecution('match_results', true, result || { success: true }, executionTime, null); return { success: true, result, executionTime, isAutomatic }; @@ -124,9 +124,9 @@ class SchedulerService { /** * Manually trigger match results fetch (for testing) */ - async triggerMatchResultsFetch() { + async triggerMatchResultsFetch(options = {}) { devLog('[Scheduler] Manual match results fetch trigger called'); - return await this.runMatchResultsFetchJob(false); + return await this.runMatchResultsFetchJob(false, options); } /** diff --git a/frontend/src/App.vue b/frontend/src/App.vue index bc90c72b..1cd28402 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -14,35 +14,35 @@
- + + + + + + - +