feat(Scheduler, MatchService, PredefinedActivity): enhance scheduling and match fetching features

- Added new scheduler routes to manage scheduling functionalities.
- Updated match fetching logic to include a scope parameter for more flexible data retrieval.
- Introduced a new field `excludeFromStats` in the PredefinedActivity model to manage activity visibility in statistics.
- Enhanced the diary date activity controller to handle predefined activities, improving activity management.
- Refactored various services to support new features and improve overall data handling.
This commit is contained in:
Torsten Schulz (local)
2026-03-17 14:10:35 +01:00
parent f1cfd1147d
commit afe51f399c
53 changed files with 2846 additions and 926 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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