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 @@
▼
-
+
-
+
+
-
+ {{ $t('navigation.clickTtAccount') }}
+
+
-
+
+
-
+
+
-
+
+
+ {{ $t('navigation.clickTtBrowser') }}
+
-
+
+