feat(clubSettings): enhance club settings with myTischtennis integration

- Added new fields for myTischtennis club ID, federation nickname, and auto-fetch rankings in the club settings.
- Updated the backend to handle the new settings in the updateClubSettings method.
- Implemented automatic ranking updates for clubs based on the new settings in the autoUpdateRatingsService.
- Enhanced the frontend to support the new settings, including validation and user interface updates for better user experience.
This commit is contained in:
Torsten Schulz (local)
2026-03-12 10:25:49 +01:00
parent 595e2eb141
commit ad09a45b17
8 changed files with 198 additions and 118 deletions

View File

@@ -2,38 +2,35 @@ import myTischtennisService from './myTischtennisService.js';
import myTischtennisFetchLogService from './myTischtennisFetchLogService.js';
import memberService from './memberService.js';
import MyTischtennis from '../models/MyTischtennis.js';
import { devLog } from '../utils/logger.js';
import Club from '../models/Club.js';
import UserClub from '../models/UserClub.js';
import { Op } from 'sequelize';
import { devLog } from '../utils/logger.js';
class AutoUpdateRatingsService {
/**
* Execute automatic rating updates for all users with enabled auto-updates
* Execute automatic rating updates for all clubs with autoFetchRankings enabled.
* For each club, finds a user with MyTischtennis account and updates that club's members.
*/
async executeAutomaticUpdates() {
devLog('Starting automatic rating updates...');
try {
// Find all users with auto-updates enabled
const accounts = await MyTischtennis.findAll({
where: {
autoUpdateRatings: true,
savePassword: true
}
// No attributes restriction — all fields needed for session handling and re-login
const clubs = await Club.findAll({
where: { autoFetchRankings: true }
});
devLog(`Found ${accounts.length} accounts with auto-updates enabled`);
devLog(`Found ${clubs.length} clubs with autoFetchRankings enabled`);
if (accounts.length === 0) {
devLog('No accounts found with auto-updates enabled');
return;
if (clubs.length === 0) {
devLog('No clubs found with autoFetchRankings enabled');
return { success: true, totalUpdated: 0, summaries: [] };
}
// Process each account and collect summaries
const summaries = [];
for (const account of accounts) {
const summary = await this.processAccount(account);
summaries.push({ userId: account.userId, ...summary });
for (const club of clubs) {
const summary = await this.processClub(club);
summaries.push({ clubId: club.id, clubName: club.name, ...summary });
}
devLog('Automatic rating updates completed');
@@ -41,28 +38,51 @@ class AutoUpdateRatingsService {
return { success: true, totalUpdated, summaries };
} catch (error) {
console.error('Error in automatic rating updates:', error);
throw error;
}
}
/**
* Process a single account for rating updates
* Process a single club for rating updates.
* Finds a user with MyTischtennis account (autoUpdateRatings, savePassword) and club access.
*/
async processAccount(account) {
async processClub(club) {
const startTime = Date.now();
let success = false;
let message = '';
let errorDetails = null;
let updatedCount = 0;
let userId = null;
try {
devLog(`Processing account ${account.email} (User ID: ${account.userId})`);
const userClubs = await UserClub.findAll({
where: { clubId: club.id, approved: true },
attributes: ['userId']
});
const userIds = userClubs.map((uc) => uc.userId);
if (userIds.length === 0) {
throw new Error(`Keine freigeschalteten Benutzer für Verein "${club.name}" gefunden`);
}
const account = await MyTischtennis.findOne({
where: {
userId: { [Op.in]: userIds },
autoUpdateRatings: true,
savePassword: true
}
});
if (!account) {
throw new Error(`Kein myTischtennis-Account mit Auto-Update für Verein "${club.name}" gefunden`);
}
userId = account.userId;
devLog(`Processing club ${club.name} (Club ID: ${club.id}) via User ${userId}`);
// Check if session is still valid
if (!account.accessToken || !account.expiresAt || account.expiresAt < Date.now() / 1000) {
devLog(`Session expired for ${account.email}, attempting re-login via verifyLogin (incl. Playwright fallback)`);
// verifyLogin handles CAPTCHA via Playwright and persists the session to DB.
devLog(`Session expired, attempting re-login via verifyLogin (incl. Playwright fallback)`);
await myTischtennisService.verifyLogin(account.userId);
// Reload the account to get the fresh session data written by verifyLogin.
const refreshed = await MyTischtennis.findOne({ where: { userId: account.userId } });
if (!refreshed?.cookie) {
throw new Error('Re-login via verifyLogin did not produce a valid session');
@@ -71,91 +91,36 @@ class AutoUpdateRatingsService {
account.refreshToken = refreshed.refreshToken;
account.expiresAt = refreshed.expiresAt;
account.cookie = refreshed.cookie;
devLog(`Successfully re-logged in for ${account.email}`);
devLog(`Successfully re-logged in`);
}
// Perform rating update
const updateResult = await this.updateRatings(account);
updatedCount = updateResult.updatedCount || 0;
const result = await memberService.updateRatingsFromMyTischtennisByUserId(account.userId, club.id);
updatedCount = result?.response?.updated ?? 0;
success = true;
message = `Successfully updated ${updatedCount} ratings`;
devLog(`Updated ${updatedCount} ratings for ${account.email}`);
devLog(`Updated ${updatedCount} ratings for club ${club.name}`);
} catch (error) {
success = false;
message = 'Update failed';
errorDetails = error.message;
console.error(`Error updating ratings for ${account.email}:`, error);
console.error(`Error updating ratings for club ${club.name}:`, error);
}
const executionTime = Date.now() - startTime;
// Log the attempt to MyTischtennisFetchLog
await myTischtennisFetchLogService.logFetch(
account.userId,
'ratings',
success,
message,
{
errorDetails,
recordsProcessed: updatedCount,
executionTime,
isAutomatic: true
}
);
// Also log to update history (for backwards compatibility)
await myTischtennisService.logUpdateAttempt(
account.userId,
success,
message,
errorDetails,
updatedCount,
executionTime
);
// Return summary for scheduler
return { success, message, updatedCount, errorDetails, executionTime };
}
/**
* Update ratings for a specific account
*/
async updateRatings(account) {
devLog(`Updating ratings for ${account.email}`);
try {
// Ermittle einen freigeschalteten Vereinszugang für den Benutzer
const userClub = await UserClub.findOne({
where: {
userId: account.userId,
approved: true
},
order: [['createdAt', 'ASC']],
attributes: ['clubId']
});
if (!userClub) {
throw new Error('Kein freigeschalteter Vereinszugang gefunden');
}
const clubId = userClub.clubId;
// Verwende den Service-Aufruf, der mit userId/clubId arbeitet
const result = await memberService.updateRatingsFromMyTischtennisByUserId(
account.userId,
clubId
if (userId) {
await myTischtennisFetchLogService.logFetch(
userId,
'ratings',
success,
message,
{ errorDetails, recordsProcessed: updatedCount, executionTime, isAutomatic: true, clubId: club.id }
);
return {
success: true,
updatedCount: result.updatedCount || 0
};
} catch (error) {
devLog(`Error updating ratings: ${error.message}`);
throw error;
await myTischtennisService.logUpdateAttempt(userId, success, message, errorDetails, updatedCount, executionTime);
}
return { success, message, updatedCount, errorDetails, executionTime };
}
/**

View File

@@ -67,13 +67,23 @@ class ClubService {
});
}
async updateClubSettings(userToken, clubId, { greetingText, associationMemberNumber }) {
async updateClubSettings(userToken, clubId, {
greetingText,
associationMemberNumber,
myTischtennisClubId,
myTischtennisFedNickname,
autoFetchRankings
}) {
await checkAccess(userToken, clubId);
const club = await Club.findByPk(clubId);
if (!club) {
throw new Error('clubnotfound');
}
return await club.update({ greetingText, associationMemberNumber });
const updates = { greetingText, associationMemberNumber };
if (myTischtennisClubId !== undefined) updates.myTischtennisClubId = myTischtennisClubId || null;
if (myTischtennisFedNickname !== undefined) updates.myTischtennisFedNickname = myTischtennisFedNickname || null;
if (autoFetchRankings !== undefined) updates.autoFetchRankings = !!autoFetchRankings;
return await club.update(updates);
}
async approveUserClubAccess(userToken, clubId, toApproveUserId) {

View File

@@ -1,4 +1,5 @@
import UserClub from "../models/UserClub.js";
import Club from "../models/Club.js";
import { checkAccess, getUserByToken, hasUserClubAccess } from "../utils/userUtils.js";
import Member from "../models/Member.js";
import MemberImage from "../models/MemberImage.js";
@@ -375,23 +376,44 @@ class MemberService {
}
};
}
if (!account.clubId || !account.fedNickname) {
const club = await Club.findByPk(clubId);
if (!club) {
return {
status: 404,
response: { message: 'Verein nicht gefunden.', updated: 0, errors: [] }
};
}
if (!club.autoFetchRankings) {
return {
status: 200,
response: {
message: 'Ranglisten-Abruf für diesen Verein ist deaktiviert.',
updated: 0,
errors: []
}
};
}
const effectiveClubId = (club.myTischtennisClubId || '').trim() || account.clubId;
const effectiveFedNickname = (club.myTischtennisFedNickname || '').trim() || account.fedNickname;
if (!effectiveClubId || !effectiveFedNickname) {
console.error('[updateRatingsFromMyTischtennis] - Missing clubId or fedNickname:', {
clubId: account.clubId,
fedNickname: account.fedNickname
clubId: effectiveClubId,
fedNickname: effectiveFedNickname,
fromClub: !!(club.myTischtennisClubId && club.myTischtennisFedNickname)
});
return {
status: 400,
response: {
message: 'Club-ID oder Verbandskürzel nicht verfügbar. Bitte einmal einloggen.',
message: 'Club-ID oder Verbandskürzel nicht verfügbar. Bitte in den Vereinseinstellungen hinterlegen oder einmal in myTischtennis einloggen.',
updated: 0,
errors: [],
debug: {
hasClubId: !!account.clubId,
hasFedNickname: !!account.fedNickname,
clubId: account.clubId,
fedNickname: account.fedNickname
hasClubId: !!effectiveClubId,
hasFedNickname: !!effectiveFedNickname
}
}
};
@@ -403,7 +425,7 @@ class MemberService {
await (await import('./apiLogService.js')).default.logRequest({
userId,
method: 'GET',
path: `/rankings/andro-rangliste?club=${account.clubId}&fed=${account.fedNickname}&current=yes`,
path: `/rankings/andro-rangliste?club=${effectiveClubId}&fed=${effectiveFedNickname}&current=yes`,
statusCode: null,
requestBody: null,
responseBody: null,
@@ -416,15 +438,15 @@ class MemberService {
const ttrStart = Date.now();
const rankingsCurrent = await myTischtennisClient.getClubRankings(
session.cookie,
account.clubId,
account.fedNickname,
effectiveClubId,
effectiveFedNickname,
'yes'
);
try {
await (await import('./apiLogService.js')).default.logRequest({
userId,
method: 'GET',
path: `/rankings/andro-rangliste?club=${account.clubId}&fed=${account.fedNickname}&current=yes`,
path: `/rankings/andro-rangliste?club=${effectiveClubId}&fed=${effectiveFedNickname}&current=yes`,
statusCode: rankingsCurrent.success ? 200 : 500,
requestBody: null,
responseBody: JSON.stringify(rankingsCurrent),
@@ -440,7 +462,7 @@ class MemberService {
await (await import('./apiLogService.js')).default.logRequest({
userId,
method: 'GET',
path: `/rankings/andro-rangliste?club=${account.clubId}&fed=${account.fedNickname}&current=no`,
path: `/rankings/andro-rangliste?club=${effectiveClubId}&fed=${effectiveFedNickname}&current=no`,
statusCode: null,
requestBody: null,
responseBody: null,
@@ -453,8 +475,8 @@ class MemberService {
const qttrStart = Date.now();
const rankingsQuarter = await myTischtennisClient.getClubRankings(
session.cookie,
account.clubId,
account.fedNickname,
effectiveClubId,
effectiveFedNickname,
'no'
);
let qttrWarning = null;
@@ -462,7 +484,7 @@ class MemberService {
await (await import('./apiLogService.js')).default.logRequest({
userId,
method: 'GET',
path: `/rankings/andro-rangliste?club=${account.clubId}&fed=${account.fedNickname}&current=no`,
path: `/rankings/andro-rangliste?club=${effectiveClubId}&fed=${effectiveFedNickname}&current=no`,
statusCode: rankingsQuarter.success ? 200 : 500,
requestBody: null,
responseBody: JSON.stringify(rankingsQuarter),
@@ -481,8 +503,8 @@ class MemberService {
updated: 0,
errors: [],
debug: {
clubId: account.clubId,
fedNickname: account.fedNickname,
clubId: effectiveClubId,
fedNickname: effectiveFedNickname,
rankingsError: rankingsCurrent.error
}
}