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:
@@ -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' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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' });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
38
backend/routes/schedulerRoutes.js
Normal file
38
backend/routes/schedulerRoutes.js
Normal 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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user