6 Commits

Author SHA1 Message Date
Torsten Schulz (local)
dbede48d4f Entfernt Konsolenausgaben aus der MyTischtennisClient-Klasse, um die Codequalität zu verbessern und die Lesbarkeit zu erhöhen. Diese Änderungen betreffen die Methoden getUserProfile und getClubRankings und tragen zur Optimierung der Protokollierung und Performance bei. 2025-10-02 10:40:24 +02:00
Torsten Schulz (local)
6cd3c3a020 Entfernt Konsolenausgaben aus verschiedenen Controllern und Services, um die Codequalität zu verbessern und die Lesbarkeit zu erhöhen. Diese Änderungen betreffen die Controller für Clubs, Club-Teams, Mitglieder, Tagebuch-Tags, Saisons und Teams sowie die zugehörigen Services. Ziel ist es, die Protokollierung zu optimieren und die Performance zu steigern. 2025-10-02 10:34:56 +02:00
Torsten Schulz (local)
7ecbef806d Entfernt Konsolenausgaben aus mehreren Komponenten, um den Code zu bereinigen und die Lesbarkeit zu verbessern. Betroffene Dateien sind CourtDrawingRender.vue, CourtDrawingTool.vue, SeasonSelector.vue, PredefinedActivities.vue, ScheduleView.vue, TeamManagementView.vue und TournamentsView.vue. Diese Änderungen tragen zur Optimierung der Performance und zur Reduzierung von unnötigen Protokollierungen bei. 2025-10-02 10:13:03 +02:00
Torsten Schulz (local)
1c70ca97bb Fügt Unterstützung für Team-Dokumente hinzu. Aktualisiert die Backend-Modelle und -Routen, um Team-Dokumente zu verwalten, einschließlich Upload- und Parsing-Funktionen für Code- und Pin-Listen. Ergänzt die Benutzeroberfläche in TeamManagementView.vue zur Anzeige und Verwaltung von Team-Dokumenten sowie zur Integration von PDF-Parsing. Aktualisiert die Match-Modelle, um zusätzliche Felder für Spiel-Codes und PINs zu berücksichtigen. 2025-10-02 09:04:19 +02:00
Torsten Schulz (local)
a6493990d3 Erweitert die Backend- und Frontend-Funktionalität zur Unterstützung von Teams und Saisons. Fügt neue Routen für Team- und Club-Team-Management hinzu, aktualisiert die Match- und Team-Modelle zur Berücksichtigung von Saisons, und implementiert die Saison-Auswahl in der Benutzeroberfläche. Optimiert die Logik zur Abfrage von Ligen und Spielen basierend auf der ausgewählten Saison. 2025-10-01 22:47:13 +02:00
Torsten Schulz (local)
f8f4d23c4e Aktualisiert die Schaltflächen im MyTischtennisAccount.vue, um die Benutzeroberfläche zu verbessern. Ändert den Text der Schaltfläche "Verbindung testen" in "Erneut einloggen" und entfernt die Testausgabe für Login-Tests. Optimiert die Erfolgsmeldung nach erfolgreichem Login und aktualisiert die Account-Daten. Entfernt die nicht mehr benötigte Funktionalität für den Login-Flow-Test. 2025-10-01 13:52:14 +02:00
62 changed files with 4556 additions and 2459 deletions

View File

@@ -1,603 +0,0 @@
import axios from 'axios';
import fs from 'fs';
import path from 'path';
const BASE_URL = 'https://ttde-id.liga.nu';
const CLICK_TT_BASE = 'https://httv.click-tt.de';
class HettvClient {
constructor() {
this.baseURL = BASE_URL;
this.client = axios.create({
baseURL: this.baseURL,
timeout: 15000,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language': 'de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7'
},
maxRedirects: 5, // Folge den OAuth2-Redirects
validateStatus: (status) => status >= 200 && status < 400
});
// Einfache Cookie-Jar nach Host -> { name: value }
this.cookieJar = new Map();
this.defaultHeaders = {
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:141.0) Gecko/20100101 Firefox/141.0',
'Accept-Language': 'de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7'
};
}
/**
* Login to HeTTV via OAuth2
* @param {string} username - HeTTV username (email)
* @param {string} password - HeTTV password
* @returns {Promise<Object>} Login response with session data
*/
async login(username, password) {
try {
console.log('[HettvClient] - Starting login for:', username);
// Schritt 1: OAuth2-Authorization-Endpoint aufrufen - das sollte zur Login-Seite weiterleiten
const oauthParams = new URLSearchParams({
'scope': 'nuLiga',
'response_type': 'code',
'redirect_uri': 'https://httv.click-tt.de/cgi-bin/WebObjects/nuLigaTTDE.woa/wa/oAuthLogin',
'state': 'nonce=' + Math.random().toString(36).substring(2, 15),
'client_id': 'XtVpGjXKAhz3BZuu'
});
// OAuth2 Start
// Der OAuth2-Endpoint sollte direkt zur Login-Seite weiterleiten
const loginPageResponse = await this.client.get(`/oauth2/authz/ttde?${oauthParams.toString()}`, {
maxRedirects: 5, // Folge den Redirects zur Login-Seite
validateStatus: (status) => status >= 200 && status < 400,
headers: {
...this.defaultHeaders
}
});
// Login-Seite erreicht
// Session-Cookie aus der Login-Seite extrahieren
const setCookies = loginPageResponse.headers['set-cookie'];
if (!setCookies || !Array.isArray(setCookies)) {
console.error('[HettvClient] - No cookies from login page');
return {
success: false,
error: 'Keine Session-Cookie von Login-Seite erhalten'
};
}
const sessionCookie = setCookies.find(cookie => cookie.startsWith('nusportingress='));
if (!sessionCookie) {
console.error('[HettvClient] - No nusportingress cookie from login page');
return {
success: false,
error: 'Keine nusportingress Session von Login-Seite erhalten'
};
}
// Extrahiere t:formdata aus dem HTML der Login-Seite
const htmlContent = loginPageResponse.data;
// HTML erhalten
// Suche nach t:formdata im HTML - verschiedene mögliche Formate
let formDataMatch = htmlContent.match(/name="t:formdata"\s+value="([^"]+)"/);
if (!formDataMatch) {
// Versuche andere Formate
formDataMatch = htmlContent.match(/name='t:formdata'\s+value='([^']+)'/);
}
if (!formDataMatch) {
// Suche nach hidden input mit t:formdata (value vor name)
formDataMatch = htmlContent.match(/<input[^>]*value="([^"]+)"[^>]*name="t:formdata"/);
}
if (!formDataMatch) {
// Suche nach hidden input mit t:formdata (name vor value)
formDataMatch = htmlContent.match(/<input[^>]*name="t:formdata"[^>]*value="([^"]+)"/);
}
if (!formDataMatch) {
// Suche nach t:formdata ohne Anführungszeichen
formDataMatch = htmlContent.match(/name=t:formdata\s+value=([^\s>]+)/);
}
if (!formDataMatch) {
console.error('[HettvClient] - No t:formdata found in login page');
console.log('[HettvClient] - HTML snippet:', htmlContent.substring(0, 2000));
// Debug: Suche nach allen hidden inputs
const hiddenInputs = htmlContent.match(/<input[^>]*type="hidden"[^>]*>/g);
console.log('[HettvClient] - Hidden inputs found:', hiddenInputs);
return {
success: false,
error: 'Keine t:formdata von Login-Seite erhalten'
};
}
const tFormData = formDataMatch[1];
// CSRF-Token gefunden
// Schritt 2: Login mit den korrekten Daten durchführen
// Verwende die Session-Cookie für den Login-Request
const formData = new URLSearchParams();
formData.append('t:submit', '["submit_0","submit_0"]');
formData.append('t:ac', 'ttde');
formData.append('t:formdata', tFormData);
formData.append('username', username);
formData.append('password', password);
const loginResponse = await this.client.post('/oauth2/login.loginform', formData.toString(), {
headers: {
'Cookie': sessionCookie.split(';')[0],
'Content-Type': 'application/x-www-form-urlencoded',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
...this.defaultHeaders,
'Referer': `${BASE_URL}/oauth2/login.loginform`
},
maxRedirects: 5,
validateStatus: (status) => status >= 200 && status < 400
});
// Login-Antwort erhalten
// Prüfe ob wir erfolgreich eingeloggt sind
// Login-Response geprüft
// Prüfe den Response-Inhalt um zu sehen ob wir noch auf der Login-Seite sind
const responseContent = loginResponse.data;
const isLoginPage = responseContent.includes('click-TT ID') &&
responseContent.includes('Username') &&
responseContent.includes('Password');
// Login-Page-Erkennung durchgeführt
if (isLoginPage) {
console.log('[HettvClient] - Still on login page, login failed');
console.log('[HettvClient] - Response snippet:', responseContent.substring(0, 500));
return {
success: false,
error: 'Login fehlgeschlagen - ungültige Zugangsdaten'
};
}
// Prüfe auf OAuth2-Redirect oder Erfolg
const hasOAuthRedirect = responseContent.includes('oauth2') ||
responseContent.includes('redirect') ||
loginResponse.status >= 300;
// OAuth Redirect erkannt
// Extrahiere die finale Session-Cookie
const finalCookies = loginResponse.headers['set-cookie'];
const finalSessionCookie = finalCookies?.find(cookie => cookie.startsWith('nusportingress='));
const sessionId = (finalSessionCookie || sessionCookie).match(/nusportingress=([^;]+)/)?.[1];
console.log('[HettvClient] - Login erfolgreich (HeTTV).');
// Versuche die finale OAuth-Weiterleitung zu httv.click-tt.de aufzurufen, um PHPSESSID zu erhalten
let finalUrl = loginResponse.request?.res?.responseUrl;
console.log('[HettvClient] - Login finalUrl:', finalUrl);
let phpSessIdCookie = null;
let finalHtml = null;
try {
if (finalUrl && finalUrl.includes('oAuthLogin')) {
const clickTTClient = axios.create({
timeout: 15000,
maxRedirects: 0,
validateStatus: (status) => status >= 200 && status < 400
});
// Folge der Redirect-Kette manuell, übernehme Cookies
let currentUrl = finalUrl;
let lastResp = null;
let hop = 0;
const maxHops = 10;
while (hop++ < maxHops && currentUrl) {
lastResp = await clickTTClient.get(currentUrl, {
headers: {
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
...this.defaultHeaders,
'Referer': hop === 1 ? `${BASE_URL}/oauth2/login.loginform` : (lastResp?.request?.res?.responseUrl || currentUrl),
'Cookie': this._cookieHeaderForUrl(currentUrl)
}
});
this._ingestSetCookiesFromResponse(currentUrl, lastResp.headers['set-cookie']);
const loc = lastResp.headers['location'];
if (loc) {
// Absolut vs relativ
if (/^https?:\/\//i.test(loc)) {
currentUrl = loc;
} else {
const u = new URL(currentUrl);
currentUrl = `${u.origin}${loc}`;
}
continue;
}
break; // keine weitere Location => final
}
const clickTTResp = lastResp;
finalHtml = typeof clickTTResp.data === 'string' ? clickTTResp.data : '';
const ctSetCookies = clickTTResp.headers['set-cookie'];
if (Array.isArray(ctSetCookies)) {
phpSessIdCookie = ctSetCookies.find(c => c.startsWith('PHPSESSID='))?.split(';')[0] || null;
}
// Finale click-TT URL ermittelt
}
} catch (e) {
// Finale click-TT Seite konnte nicht geladen werden
}
// Baue kombinierte Cookie-Kette (falls PHPSESSID vorhanden)
const baseCookie = (finalSessionCookie || sessionCookie).split(';')[0];
const combinedCookie = phpSessIdCookie ? `${baseCookie}; ${phpSessIdCookie}` : baseCookie;
return {
success: true,
sessionId: sessionId,
cookie: combinedCookie,
accessToken: null,
refreshToken: null,
expiresAt: null,
user: {
finalUrl: finalUrl || null,
htmlSnippet: finalHtml ? finalHtml.substring(0, 2000) : null
}
};
} catch (error) {
console.error('HeTTV login error:', error.message);
console.error('Error details:', error.response?.status, error.response?.statusText);
return {
success: false,
error: error.response?.data?.message || 'Login fehlgeschlagen',
status: error.response?.status || 500
};
}
}
/**
* Verify login credentials
* @param {string} username - HeTTV username
* @param {string} password - HeTTV password
* @returns {Promise<boolean>} True if credentials are valid
*/
async verifyCredentials(username, password) {
const result = await this.login(username, password);
return result.success;
}
/**
* Make an authenticated request to click-TT
* @param {string} endpoint - API endpoint
* @param {string} cookie - JSESSIONID cookie
* @param {Object} options - Additional axios options
* @returns {Promise<Object>} API response
*/
async authenticatedRequest(endpoint, cookie, options = {}, finalUrl = null) {
try {
// Bestimme Basis-URL dynamisch aus finalUrl, falls vorhanden
let baseURL = CLICK_TT_BASE;
if (finalUrl) {
try {
const url = new URL(finalUrl);
baseURL = url.origin;
} catch (_) {}
}
const isAbsolute = /^https?:\/\//i.test(endpoint);
const client = axios.create({
baseURL: isAbsolute ? undefined : baseURL,
timeout: 15000,
maxRedirects: 0,
validateStatus: (status) => status >= 200 && status < 400
});
// Manuelles Redirect-Following inkl. Cookies/Referer
let currentUrl = isAbsolute ? endpoint : `${baseURL}${endpoint.startsWith('/') ? '' : '/'}${endpoint}`;
let lastResp = null;
const trace = [];
let hop = 0;
const maxHops = 10;
console.log(`[HettvClient] - Starting redirect chain from: ${currentUrl}`);
while (hop++ < maxHops && currentUrl) {
console.log(`[HettvClient] - Redirect ${hop}: GET ${currentUrl}`);
lastResp = await client.request({
method: options.method || 'GET',
url: currentUrl,
data: options.data,
headers: {
...this.defaultHeaders,
...(options.headers || {}),
'Cookie': this._mergeCookieHeader(cookie, this._cookieHeaderForUrl(currentUrl)),
'Referer': hop === 1 ? (finalUrl || baseURL) : (lastResp?.request?.res?.responseUrl || currentUrl)
}
});
this._ingestSetCookiesFromResponse(currentUrl, lastResp.headers['set-cookie']);
const loc = lastResp.headers['location'];
console.log(`[HettvClient] - Response: ${lastResp.status} ${lastResp.statusText}`);
console.log(`[HettvClient] - Location header: ${loc || 'none'}`);
console.log(`[HettvClient] - Set-Cookie header: ${lastResp.headers['set-cookie'] ? 'present' : 'none'}`);
console.log(`[HettvClient] - Content-Type: ${lastResp.headers['content-type'] || 'none'}`);
// Speichere jede Seite zur Analyse
try {
const dir = path.resolve(process.cwd(), 'backend', 'uploads');
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
const filename = `hettv_redirect_${hop}_${Date.now()}.html`;
const filePath = path.join(dir, filename);
const content = typeof lastResp.data === 'string' ? lastResp.data : JSON.stringify(lastResp.data, null, 2);
fs.writeFileSync(filePath, content, 'utf8');
console.log(`[HettvClient] - Saved page to: ${filename}`);
} catch (e) {
console.log(`[HettvClient] - Could not save page ${hop}:`, e.message);
}
trace.push({
url: currentUrl,
status: lastResp.status,
location: loc || null
});
if (loc) {
const newUrl = /^https?:\/\//i.test(loc) ? loc : `${new URL(currentUrl).origin}${loc}`;
console.log(`[HettvClient] - Following redirect to: ${newUrl}`);
currentUrl = newUrl;
continue;
}
console.log(`[HettvClient] - Final response: ${lastResp.status} (no more redirects)`);
break;
}
return {
success: true,
data: lastResp?.data,
trace
};
} catch (error) {
console.error('HeTTV API error:', error.message);
return {
success: false,
error: error.response?.data?.message || 'API-Anfrage fehlgeschlagen',
status: error.response?.status || 500
};
}
}
/**
* Navigate to main HeTTV page and find Downloads menu
* @param {string} cookie - Session cookie
* @returns {Promise<Object>} Response with main page content and download links
*/
async getMainPageWithDownloads(cookie, finalUrl = null) {
try {
console.log('[HettvClient] - Loading main HeTTV page...');
// Kandidaten für Einstiegs-URL bestimmen
let origin = CLICK_TT_BASE;
if (finalUrl) {
try { origin = new URL(finalUrl).origin; } catch (_) {}
}
const candidates = [];
// Direkt zu HeTTV navigieren
candidates.push('http://httv.click-tt.de/');
candidates.push('http://httv.click-tt.de/wa/');
candidates.push('http://httv.click-tt.de/cgi-bin/WebObjects/nuLigaTTDE.woa/wa/');
// Wenn wir eine finalUrl haben, verwende diese auch
if (finalUrl) {
candidates.push(finalUrl);
}
console.log('[HettvClient] - URL candidates:', candidates);
let mainPageResponse = null;
let mainTrace = [];
let lastError = null;
for (const candidate of candidates) {
const resp = await this.authenticatedRequest(candidate, cookie, {}, finalUrl);
if (resp.success && typeof resp.data === 'string' && resp.data.length > 0) {
mainPageResponse = resp;
mainTrace = resp.trace || [];
break;
}
lastError = resp;
}
if (!mainPageResponse) {
return lastError || { success: false, error: 'HeTTV Einstiegsseite nicht erreichbar', status: 404 };
}
const htmlContent = mainPageResponse.data;
console.log('[HettvClient] - Main page loaded, HTML length:', htmlContent.length);
// Erkenne Fehlerseite (Session ungültig)
if (/click-TT\s*-\s*Fehlerseite/i.test(htmlContent) || /ungültige oder nicht mehr gültige URL/i.test(htmlContent)) {
return {
success: false,
error: 'Session ungültig oder abgelaufen',
status: 401,
data: { htmlSnippet: htmlContent.substring(0, 1000) }
};
}
// Speichere HTML zur Analyse
let savedFile = null;
try {
const dir = path.resolve(process.cwd(), 'backend', 'uploads');
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
const filename = `hettv_main_${Date.now()}.html`;
const filePath = path.join(dir, filename);
fs.writeFileSync(filePath, htmlContent, 'utf8');
savedFile = filePath;
} catch (e) {
// Ignoriere Speicherfehler still, nur für Debug
}
// Suche nach Downloads-Links im HTML
const downloadLinks = [];
// 1) URL-Heuristiken
const urlPatterns = [
/href="([^"]*download[^"]*)"/gi,
/href="([^"]*downloads[^"]*)"/gi,
/href="([^"]*Download[^"]*)"/gi,
/href="([^"]*Downloads[^"]*)"/gi
];
urlPatterns.forEach(pattern => {
let match;
while ((match = pattern.exec(htmlContent)) !== null) {
const link = match[1];
if (link && !downloadLinks.includes(link)) {
downloadLinks.push(link);
}
}
});
// 2) Linktext-Heuristik: <a ...>Downloads</a>
const anchorPattern = /<a[^>]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/gi;
let aMatch;
while ((aMatch = anchorPattern.exec(htmlContent)) !== null) {
const href = aMatch[1];
const text = aMatch[2].replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim();
if (/\bdownloads?\b/i.test(text)) {
if (href && !downloadLinks.includes(href)) {
downloadLinks.push(href);
}
}
}
// 3) Fallback: Menüpunkte in Navigationen (role="navigation" etc.)
if (downloadLinks.length === 0) {
const navSectionRegex = /<nav[\s\S]*?<\/nav>/gi;
let nav;
while ((nav = navSectionRegex.exec(htmlContent)) !== null) {
const section = nav[0];
let m;
anchorPattern.lastIndex = 0;
while ((m = anchorPattern.exec(section)) !== null) {
const href = m[1];
const text = m[2].replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim();
if (/\bdownloads?\b/i.test(text)) {
if (href && !downloadLinks.includes(href)) {
downloadLinks.push(href);
}
}
}
}
}
console.log('[HettvClient] - Found download links:', downloadLinks);
return {
success: true,
data: {
htmlContent: htmlContent,
downloadLinks: downloadLinks,
htmlSnippet: htmlContent.substring(0, 2000), // Erste 2000 Zeichen für Analyse
savedFile,
trace: mainTrace,
lastUrl: mainTrace.length ? mainTrace[mainTrace.length - 1].url : null,
lastStatus: mainTrace.length ? mainTrace[mainTrace.length - 1].status : null
}
};
} catch (error) {
console.error('HeTTV main page error:', error.message);
return {
success: false,
error: error.message || 'Fehler beim Laden der Hauptseite',
status: 500
};
}
}
/**
* Load a specific download page
* @param {string} downloadUrl - URL to the download page
* @param {string} cookie - Session cookie
* @returns {Promise<Object>} Response with download page content
*/
async loadDownloadPage(downloadUrl, cookie, finalUrl = null) {
try {
console.log('[HettvClient] - Loading download page:', downloadUrl);
const response = await this.authenticatedRequest(downloadUrl, cookie, {}, finalUrl);
if (!response.success) {
return response;
}
const htmlContent = response.data;
console.log('[HettvClient] - Download page loaded, HTML length:', htmlContent.length);
return {
success: true,
data: {
url: downloadUrl,
htmlContent: htmlContent,
htmlSnippet: htmlContent.substring(0, 3000) // Erste 3000 Zeichen für Analyse
}
};
} catch (error) {
console.error('HeTTV download page error:', error.message);
return {
success: false,
error: error.message || 'Fehler beim Laden der Download-Seite',
status: 500
};
}
}
// --- Cookie-Helfer ---
_ingestSetCookiesFromResponse(currentUrl, setCookies) {
if (!Array.isArray(setCookies) || setCookies.length === 0) return;
const { host } = new URL(currentUrl);
if (!this.cookieJar.has(host)) this.cookieJar.set(host, new Map());
const jar = this.cookieJar.get(host);
setCookies.forEach((cookieStr) => {
const pair = cookieStr.split(';')[0];
const eq = pair.indexOf('=');
if (eq > 0) {
const name = pair.substring(0, eq).trim();
const value = pair.substring(eq + 1).trim();
jar.set(name, value);
}
});
}
_cookieHeaderForUrl(currentUrl) {
const { host } = new URL(currentUrl);
const jar = this.cookieJar.get(host);
if (!jar || jar.size === 0) return '';
return Array.from(jar.entries()).map(([k, v]) => `${k}=${v}`).join('; ');
}
_mergeCookieHeader(primary, secondary) {
const items = [];
if (primary) items.push(primary);
if (secondary) items.push(secondary);
return items.filter(Boolean).join('; ');
}
}
export default new HettvClient();

View File

@@ -149,13 +149,11 @@ class MyTischtennisClient {
* @returns {Promise<Object>} User profile with club info
*/
async getUserProfile(cookie) {
console.log('[getUserProfile] - Calling /?_data=root with cookie:', cookie?.substring(0, 50) + '...');
const result = await this.authenticatedRequest('/?_data=root', cookie, {
method: 'GET'
});
console.log('[getUserProfile] - Result success:', result.success);
if (result.success) {
console.log('[getUserProfile] - Response structure:', {
@@ -169,8 +167,6 @@ class MyTischtennisClient {
qttr: result.data?.userProfile?.qttr
});
console.log('[getUserProfile] - Full userProfile.club:', result.data?.userProfile?.club);
console.log('[getUserProfile] - Full userProfile.organization:', result.data?.userProfile?.organization);
return {
success: true,
@@ -199,12 +195,10 @@ class MyTischtennisClient {
let currentPage = 0;
let hasMorePages = true;
console.log('[getClubRankings] - Starting to fetch rankings for club', clubId);
while (hasMorePages) {
const endpoint = `/rankings/andro-rangliste?all-players=on&clubnr=${clubId}&fednickname=${fedNickname}&results-per-page=100&page=${currentPage}&_data=routes%2F%24`;
console.log(`[getClubRankings] - Fetching page ${currentPage}...`);
const result = await this.authenticatedRequest(endpoint, cookie, {
method: 'GET'
@@ -245,7 +239,6 @@ class MyTischtennisClient {
};
}
console.log(`[getClubRankings] - Page ${currentPage}: Found ${entries.length} entries`);
// Füge Entries hinzu
allEntries.push(...entries);
@@ -255,19 +248,15 @@ class MyTischtennisClient {
// Oder wenn wir alle erwarteten Einträge haben
if (entries.length === 0) {
hasMorePages = false;
console.log('[getClubRankings] - No more entries, stopping');
} else if (rankingData.numberOfPages && currentPage >= rankingData.numberOfPages - 1) {
hasMorePages = false;
console.log(`[getClubRankings] - Reached last page (${rankingData.numberOfPages})`);
} else if (allEntries.length >= rankingData.resultLength) {
hasMorePages = false;
console.log(`[getClubRankings] - Got all entries (${allEntries.length}/${rankingData.resultLength})`);
} else {
currentPage++;
}
}
console.log(`[getClubRankings] - Total entries fetched: ${allEntries.length}`);
return {
success: true,

View File

@@ -0,0 +1,128 @@
import ClubTeamService from '../services/clubTeamService.js';
import { getUserByToken } from '../utils/userUtils.js';
import { devLog } from '../utils/logger.js';
export const getClubTeams = async (req, res) => {
try {
const { authcode: token } = req.headers;
const { clubid: clubId } = req.params;
const { seasonid: seasonId } = req.query;
const user = await getUserByToken(token);
// Check if user has access to this club
const clubTeams = await ClubTeamService.getAllClubTeamsByClub(clubId, seasonId);
res.status(200).json(clubTeams);
} catch (error) {
console.error('[getClubTeams] - Error:', error);
res.status(500).json({ error: "internalerror" });
}
};
export const getClubTeam = async (req, res) => {
try {
const { authcode: token } = req.headers;
const { clubteamid: clubTeamId } = req.params;
const user = await getUserByToken(token);
const clubTeam = await ClubTeamService.getClubTeamById(clubTeamId);
if (!clubTeam) {
return res.status(404).json({ error: "notfound" });
}
res.status(200).json(clubTeam);
} catch (error) {
console.error('[getClubTeam] - Error:', error);
res.status(500).json({ error: "internalerror" });
}
};
export const createClubTeam = async (req, res) => {
try {
const { authcode: token } = req.headers;
const { clubid: clubId } = req.params;
const { name, leagueId, seasonId } = req.body;
const user = await getUserByToken(token);
if (!name) {
return res.status(400).json({ error: "missingname" });
}
const clubTeamData = {
name,
clubId: parseInt(clubId),
leagueId: leagueId ? parseInt(leagueId) : null,
seasonId: seasonId ? parseInt(seasonId) : null
};
const newClubTeam = await ClubTeamService.createClubTeam(clubTeamData);
res.status(201).json(newClubTeam);
} catch (error) {
console.error('[createClubTeam] - Error:', error);
res.status(500).json({ error: "internalerror" });
}
};
export const updateClubTeam = async (req, res) => {
try {
const { authcode: token } = req.headers;
const { clubteamid: clubTeamId } = req.params;
const { name, leagueId, seasonId } = req.body;
const user = await getUserByToken(token);
const updateData = {};
if (name !== undefined) updateData.name = name;
if (leagueId !== undefined) updateData.leagueId = leagueId ? parseInt(leagueId) : null;
if (seasonId !== undefined) updateData.seasonId = seasonId ? parseInt(seasonId) : null;
const success = await ClubTeamService.updateClubTeam(clubTeamId, updateData);
if (!success) {
return res.status(404).json({ error: "notfound" });
}
const updatedClubTeam = await ClubTeamService.getClubTeamById(clubTeamId);
res.status(200).json(updatedClubTeam);
} catch (error) {
console.error('[updateClubTeam] - Error:', error);
res.status(500).json({ error: "internalerror" });
}
};
export const deleteClubTeam = async (req, res) => {
try {
const { authcode: token } = req.headers;
const { clubteamid: clubTeamId } = req.params;
const user = await getUserByToken(token);
const success = await ClubTeamService.deleteClubTeam(clubTeamId);
if (!success) {
return res.status(404).json({ error: "notfound" });
}
res.status(200).json({ message: "Club team deleted successfully" });
} catch (error) {
console.error('[deleteClubTeam] - Error:', error);
res.status(500).json({ error: "internalerror" });
}
};
export const getLeagues = async (req, res) => {
try {
const { authcode: token } = req.headers;
const { clubid: clubId } = req.params;
const { seasonid: seasonId } = req.query;
const user = await getUserByToken(token);
const leagues = await ClubTeamService.getLeaguesByClub(clubId, seasonId);
res.status(200).json(leagues);
} catch (error) {
console.error('[getLeagues] - Error:', error);
res.status(500).json({ error: "internalerror" });
}
};

View File

@@ -4,11 +4,8 @@ import { devLog } from '../utils/logger.js';
export const getClubs = async (req, res) => {
try {
devLog('[getClubs] - get clubs');
const clubs = await ClubService.getAllClubs();
devLog('[getClubs] - prepare response');
res.status(200).json(clubs);
devLog('[getClubs] - done');
} catch (error) {
console.error('[getClubs] - error:', error);
res.status(500).json({ error: "internalerror" });
@@ -16,28 +13,20 @@ export const getClubs = async (req, res) => {
};
export const addClub = async (req, res) => {
devLog('[addClub] - Read out parameters');
const { authcode: token } = req.headers;
const { name: clubName } = req.body;
try {
devLog('[addClub] - find club by name');
const club = await ClubService.findClubByName(clubName);
devLog('[addClub] - get user');
const user = await getUserByToken(token);
devLog('[addClub] - check if club already exists');
if (club) {
res.status(409).json({ error: "alreadyexists" });
return;
}
devLog('[addClub] - create club');
const newClub = await ClubService.createClub(clubName);
devLog('[addClub] - add user to new club');
await ClubService.addUserToClub(user.id, newClub.id);
devLog('[addClub] - prepare response');
res.status(200).json(newClub);
devLog('[addClub] - done');
} catch (error) {
console.error('[addClub] - error:', error);
res.status(500).json({ error: "internalerror" });
@@ -45,30 +34,22 @@ export const addClub = async (req, res) => {
};
export const getClub = async (req, res) => {
devLog('[getClub] - start');
try {
const { authcode: token } = req.headers;
const { clubid: clubId } = req.params;
devLog('[getClub] - get user');
const user = await getUserByToken(token);
devLog('[getClub] - get users club');
const access = await ClubService.getUserClubAccess(user.id, clubId);
devLog('[getClub] - check access');
if (access.length === 0 || !access[0].approved) {
res.status(403).json({ error: "noaccess", status: access.length === 0 ? "notrequested" : "requested" });
return;
}
devLog('[getClub] - get club');
const club = await ClubService.findClubById(clubId);
devLog('[getClub] - check club exists');
if (!club) {
return res.status(404).json({ message: 'Club not found' });
}
devLog('[getClub] - set response');
res.status(200).json(club);
devLog('[getClub] - done');
} catch (error) {
console.error('[getClub] - error:', error);
res.status(500).json({ message: 'Server error' });
@@ -81,7 +62,6 @@ export const requestClubAccess = async (req, res) => {
try {
const user = await getUserByToken(token);
devLog('[requestClubAccess] - user:', user);
await ClubService.requestAccessToClub(user.id, clubId);
res.status(200).json({});

View File

@@ -17,7 +17,6 @@ export const createTag = async (req, res) => {
const newTag = await DiaryTag.findOrCreate({ where: { name }, defaults: { name } });
res.status(201).json(newTag);
} catch (error) {
devLog('[createTag] - Error:', error);
res.status(500).json({ error: 'Error creating tag' });
}
};

View File

@@ -1,172 +0,0 @@
import externalServiceService from '../services/externalServiceService.js';
import HttpError from '../exceptions/HttpError.js';
class ExternalServiceController {
/**
* GET /api/mytischtennis/account?service=mytischtennis
* Get current user's external service account
*/
async getAccount(req, res, next) {
try {
const userId = req.user.id;
const service = req.query.service || 'mytischtennis';
const account = await externalServiceService.getAccount(userId, service);
if (!account) {
return res.status(200).json({ account: null });
}
res.status(200).json({ account });
} catch (error) {
next(error);
}
}
/**
* GET /api/mytischtennis/status?service=mytischtennis
* Check account configuration status
*/
async getStatus(req, res, next) {
try {
const userId = req.user.id;
const service = req.query.service || 'mytischtennis';
const status = await externalServiceService.checkAccountStatus(userId, service);
res.status(200).json(status);
} catch (error) {
next(error);
}
}
/**
* POST /api/mytischtennis/account
* Create or update external service account
*/
async upsertAccount(req, res, next) {
try {
const userId = req.user.id;
const { email, password, savePassword, userPassword, service = 'mytischtennis' } = req.body;
if (!email) {
throw new HttpError(400, 'E-Mail-Adresse erforderlich');
}
// Wenn ein Passwort gesetzt wird, muss das App-Passwort angegeben werden
if (password && !userPassword) {
throw new HttpError(400, 'App-Passwort erforderlich zum Setzen des myTischtennis-Passworts');
}
const account = await externalServiceService.upsertAccount(
userId,
email,
password,
savePassword || false,
userPassword,
service
);
res.status(200).json({
message: `${service}-Account erfolgreich gespeichert`,
account
});
} catch (error) {
next(error);
}
}
/**
* DELETE /api/mytischtennis/account?service=mytischtennis
* Delete external service account
*/
async deleteAccount(req, res, next) {
try {
const userId = req.user.id;
const service = req.query.service || 'mytischtennis';
const deleted = await externalServiceService.deleteAccount(userId, service);
if (!deleted) {
throw new HttpError(404, `Kein ${service}-Account gefunden`);
}
res.status(200).json({ message: `${service}-Account gelöscht` });
} catch (error) {
next(error);
}
}
/**
* POST /api/mytischtennis/verify
* Verify login credentials
*/
async verifyLogin(req, res, next) {
try {
const userId = req.user.id;
const { password, service = 'mytischtennis' } = req.body;
const result = await externalServiceService.verifyLogin(userId, password, service);
res.status(200).json({
message: 'Login erfolgreich',
success: true,
accessToken: result.accessToken,
expiresAt: result.expiresAt,
clubId: result.clubId,
clubName: result.clubName
});
} catch (error) {
next(error);
}
}
/**
* GET /api/mytischtennis/session?service=mytischtennis
* Get stored session data for authenticated requests
*/
async getSession(req, res, next) {
try {
const userId = req.user.id;
const service = req.query.service || 'mytischtennis';
const session = await externalServiceService.getSession(userId, service);
res.status(200).json({ session });
} catch (error) {
next(error);
}
}
/**
* GET /api/external-service/hettv/main-page
* Load HeTTV main page and find download links
*/
async loadHettvMainPage(req, res, next) {
try {
const userId = req.user.id;
const result = await externalServiceService.loadHettvMainPage(userId);
res.status(200).json(result);
} catch (error) {
next(error);
}
}
/**
* POST /api/external-service/hettv/download-page
* Load specific HeTTV download page
*/
async loadHettvDownloadPage(req, res, next) {
try {
const userId = req.user.id;
const { downloadUrl } = req.body;
if (!downloadUrl) {
throw new HttpError(400, 'Download-URL ist erforderlich');
}
const result = await externalServiceService.loadHettvDownloadPage(userId, downloadUrl);
res.status(200).json(result);
} catch (error) {
next(error);
}
}
}
export default new ExternalServiceController();

View File

@@ -25,7 +25,8 @@ export const getLeaguesForCurrentSeason = async (req, res) => {
devLog(req.headers, req.params);
const { authcode: userToken } = req.headers;
const { clubId } = req.params;
const leagues = await MatchService.getLeaguesForCurrentSeason(userToken, clubId);
const { seasonid: seasonId } = req.query;
const leagues = await MatchService.getLeaguesForCurrentSeason(userToken, clubId, seasonId);
return res.status(200).json(leagues);
} catch (error) {
console.error('Error retrieving leagues:', error);
@@ -37,7 +38,8 @@ export const getMatchesForLeagues = async (req, res) => {
try {
const { authcode: userToken } = req.headers;
const { clubId } = req.params;
const matches = await MatchService.getMatchesForLeagues(userToken, clubId);
const { seasonid: seasonId } = req.query;
const matches = await MatchService.getMatchesForLeagues(userToken, clubId, seasonId);
return res.status(200).json(matches);
} catch (error) {
console.error('Error retrieving matches:', error);

View File

@@ -10,24 +10,17 @@ const getClubMembers = async(req, res) => {
}
res.status(200).json(await MemberService.getClubMembers(userToken, clubId, showAll));
} catch(error) {
devLog('[getClubMembers] - Error: ', error);
res.status(500).json({ error: 'systemerror' });
}
}
const getWaitingApprovals = async(req, res) => {
try {
devLog('[getWaitingApprovals] - Start');
const { id: clubId } = req.params;
devLog('[getWaitingApprovals] - get token');
const { authcode: userToken } = req.headers;
devLog('[getWaitingApprovals] - load for waiting approvals');
const waitingApprovals = await MemberService.getApprovalRequests(userToken, clubId);
devLog('[getWaitingApprovals] - set response');
res.status(200).json(waitingApprovals);
devLog('[getWaitingApprovals] - done');
} catch(error) {
devLog('[getWaitingApprovals] - Error: ', error);
res.status(403).json({ error: error });
}
}
@@ -60,7 +53,6 @@ const uploadMemberImage = async (req, res) => {
};
const getMemberImage = async (req, res) => {
devLog('[getMemberImage]');
try {
const { clubId, memberId } = req.params;
const { authcode: userToken } = req.headers;
@@ -77,7 +69,6 @@ const getMemberImage = async (req, res) => {
};
const updateRatingsFromMyTischtennis = async (req, res) => {
devLog('[updateRatingsFromMyTischtennis]');
try {
const { id: clubId } = req.params;
const { authcode: userToken } = req.headers;

View File

@@ -6,11 +6,9 @@ const getMemberNotes = async (req, res) => {
const { authcode: userToken } = req.headers;
const { memberId } = req.params;
const { clubId } = req.query;
devLog('[getMemberNotes]', userToken, memberId, clubId);
const notes = await MemberNoteService.getNotesForMember(userToken, clubId, memberId);
res.status(200).json(notes);
} catch (error) {
devLog('[getMemberNotes] - Error: ', error);
res.status(500).json({ error: 'systemerror' });
}
};
@@ -19,12 +17,10 @@ const addMemberNote = async (req, res) => {
try {
const { authcode: userToken } = req.headers;
const { memberId, content, clubId } = req.body;
devLog('[addMemberNote]', userToken, memberId, content, clubId);
await MemberNoteService.addNoteToMember(userToken, clubId, memberId, content);
const notes = await MemberNoteService.getNotesForMember(userToken, clubId, memberId);
res.status(201).json(notes);
} catch (error) {
devLog('[addMemberNote] - Error: ', error);
res.status(500).json({ error: 'systemerror' });
}
};
@@ -34,13 +30,11 @@ const deleteMemberNote = async (req, res) => {
const { authcode: userToken } = req.headers;
const { noteId } = req.params;
const { clubId } = req.body;
devLog('[deleteMemberNote]', userToken, noteId, clubId);
const memberId = await MemberNoteService.getMemberIdForNote(noteId); // Member ID ermitteln
await MemberNoteService.deleteNoteForMember(userToken, clubId, noteId);
const notes = await MemberNoteService.getNotesForMember(userToken, clubId, memberId);
res.status(200).json(notes);
} catch (error) {
devLog('[deleteMemberNote] - Error: ', error);
res.status(500).json({ error: 'systemerror' });
}
};

View File

@@ -0,0 +1,133 @@
import myTischtennisService from '../services/myTischtennisService.js';
import HttpError from '../exceptions/HttpError.js';
class MyTischtennisController {
/**
* GET /api/mytischtennis/account
* Get current user's myTischtennis account
*/
async getAccount(req, res, next) {
try {
const userId = req.user.id;
const account = await myTischtennisService.getAccount(userId);
if (!account) {
return res.status(200).json({ account: null });
}
res.status(200).json({ account });
} catch (error) {
next(error);
}
}
/**
* GET /api/mytischtennis/status
* Check account configuration status
*/
async getStatus(req, res, next) {
try {
const userId = req.user.id;
const status = await myTischtennisService.checkAccountStatus(userId);
res.status(200).json(status);
} catch (error) {
next(error);
}
}
/**
* POST /api/mytischtennis/account
* Create or update myTischtennis account
*/
async upsertAccount(req, res, next) {
try {
const userId = req.user.id;
const { email, password, savePassword, userPassword } = req.body;
if (!email) {
throw new HttpError(400, 'E-Mail-Adresse erforderlich');
}
// Wenn ein Passwort gesetzt wird, muss das App-Passwort angegeben werden
if (password && !userPassword) {
throw new HttpError(400, 'App-Passwort erforderlich zum Setzen des myTischtennis-Passworts');
}
const account = await myTischtennisService.upsertAccount(
userId,
email,
password,
savePassword || false,
userPassword
);
res.status(200).json({
message: 'myTischtennis-Account erfolgreich gespeichert',
account
});
} catch (error) {
next(error);
}
}
/**
* DELETE /api/mytischtennis/account
* Delete myTischtennis account
*/
async deleteAccount(req, res, next) {
try {
const userId = req.user.id;
const deleted = await myTischtennisService.deleteAccount(userId);
if (!deleted) {
throw new HttpError(404, 'Kein myTischtennis-Account gefunden');
}
res.status(200).json({ message: 'myTischtennis-Account gelöscht' });
} catch (error) {
next(error);
}
}
/**
* POST /api/mytischtennis/verify
* Verify login credentials
*/
async verifyLogin(req, res, next) {
try {
const userId = req.user.id;
const { password } = req.body;
const result = await myTischtennisService.verifyLogin(userId, password);
res.status(200).json({
message: 'Login erfolgreich',
success: true,
accessToken: result.accessToken,
expiresAt: result.expiresAt,
clubId: result.clubId,
clubName: result.clubName
});
} catch (error) {
next(error);
}
}
/**
* GET /api/mytischtennis/session
* Get stored session data for authenticated requests
*/
async getSession(req, res, next) {
try {
const userId = req.user.id;
const session = await myTischtennisService.getSession(userId);
res.status(200).json({ session });
} catch (error) {
next(error);
}
}
}
export default new MyTischtennisController();

View File

@@ -36,7 +36,6 @@ export const uploadPredefinedActivityImage = async (req, res) => {
// Extrahiere Zeichnungsdaten aus dem Request
const drawingData = req.body.drawingData ? JSON.parse(req.body.drawingData) : null;
devLog('[uploadPredefinedActivityImage] - drawingData:', drawingData);
const imageRecord = await PredefinedActivityImage.create({
predefinedActivityId: id,

View File

@@ -0,0 +1,103 @@
import SeasonService from '../services/seasonService.js';
import { getUserByToken } from '../utils/userUtils.js';
import { devLog } from '../utils/logger.js';
export const getSeasons = async (req, res) => {
try {
const { authcode: token } = req.headers;
const user = await getUserByToken(token);
const seasons = await SeasonService.getAllSeasons();
res.status(200).json(seasons);
} catch (error) {
console.error('[getSeasons] - Error:', error);
res.status(500).json({ error: "internalerror" });
}
};
export const getCurrentSeason = async (req, res) => {
try {
const { authcode: token } = req.headers;
const user = await getUserByToken(token);
const season = await SeasonService.getOrCreateCurrentSeason();
res.status(200).json(season);
} catch (error) {
console.error('[getCurrentSeason] - Error:', error);
res.status(500).json({ error: "internalerror" });
}
};
export const createSeason = async (req, res) => {
try {
const { authcode: token } = req.headers;
const { season } = req.body;
const user = await getUserByToken(token);
if (!season) {
return res.status(400).json({ error: "missingseason" });
}
// Validiere Saison-Format (z.B. "2023/2024")
const seasonRegex = /^\d{4}\/\d{4}$/;
if (!seasonRegex.test(season)) {
return res.status(400).json({ error: "invalidseasonformat" });
}
const newSeason = await SeasonService.createSeason(season);
res.status(201).json(newSeason);
} catch (error) {
console.error('[createSeason] - Error:', error);
if (error.message === 'Season already exists') {
res.status(409).json({ error: "alreadyexists" });
} else {
res.status(500).json({ error: "internalerror" });
}
}
};
export const getSeason = async (req, res) => {
try {
const { authcode: token } = req.headers;
const { seasonid: seasonId } = req.params;
const user = await getUserByToken(token);
const season = await SeasonService.getSeasonById(seasonId);
if (!season) {
return res.status(404).json({ error: "notfound" });
}
res.status(200).json(season);
} catch (error) {
console.error('[getSeason] - Error:', error);
res.status(500).json({ error: "internalerror" });
}
};
export const deleteSeason = async (req, res) => {
try {
const { authcode: token } = req.headers;
const { seasonid: seasonId } = req.params;
const user = await getUserByToken(token);
const success = await SeasonService.deleteSeason(seasonId);
if (!success) {
return res.status(404).json({ error: "notfound" });
}
res.status(200).json({ message: "deleted" });
} catch (error) {
console.error('[deleteSeason] - Error:', error);
if (error.message === 'Season is used by teams' || error.message === 'Season is used by leagues') {
res.status(409).json({ error: "seasoninuse" });
} else {
res.status(500).json({ error: "internalerror" });
}
}
};

View File

@@ -0,0 +1,130 @@
import TeamService from '../services/teamService.js';
import { getUserByToken } from '../utils/userUtils.js';
import { devLog } from '../utils/logger.js';
export const getTeams = async (req, res) => {
try {
const { authcode: token } = req.headers;
const { clubid: clubId } = req.params;
const { seasonid: seasonId } = req.query;
const user = await getUserByToken(token);
// Check if user has access to this club
const teams = await TeamService.getAllTeamsByClub(clubId, seasonId);
res.status(200).json(teams);
} catch (error) {
console.error('[getTeams] - Error:', error);
res.status(500).json({ error: "internalerror" });
}
};
export const getTeam = async (req, res) => {
try {
const { authcode: token } = req.headers;
const { teamid: teamId } = req.params;
const user = await getUserByToken(token);
const team = await TeamService.getTeamById(teamId);
if (!team) {
return res.status(404).json({ error: "notfound" });
}
res.status(200).json(team);
} catch (error) {
console.error('[getTeam] - Error:', error);
res.status(500).json({ error: "internalerror" });
}
};
export const createTeam = async (req, res) => {
try {
const { authcode: token } = req.headers;
const { clubid: clubId } = req.params;
const { name, leagueId, seasonId } = req.body;
const user = await getUserByToken(token);
if (!name) {
return res.status(400).json({ error: "missingname" });
}
const teamData = {
name,
clubId: parseInt(clubId),
leagueId: leagueId ? parseInt(leagueId) : null,
seasonId: seasonId ? parseInt(seasonId) : null
};
const newTeam = await TeamService.createTeam(teamData);
res.status(201).json(newTeam);
} catch (error) {
console.error('[createTeam] - Error:', error);
res.status(500).json({ error: "internalerror" });
}
};
export const updateTeam = async (req, res) => {
try {
const { authcode: token } = req.headers;
const { teamid: teamId } = req.params;
const { name, leagueId, seasonId } = req.body;
const user = await getUserByToken(token);
const updateData = {};
if (name !== undefined) updateData.name = name;
if (leagueId !== undefined) updateData.leagueId = leagueId ? parseInt(leagueId) : null;
if (seasonId !== undefined) updateData.seasonId = seasonId ? parseInt(seasonId) : null;
const success = await TeamService.updateTeam(teamId, updateData);
if (!success) {
return res.status(404).json({ error: "notfound" });
}
const updatedTeam = await TeamService.getTeamById(teamId);
res.status(200).json(updatedTeam);
} catch (error) {
console.error('[updateTeam] - Error:', error);
res.status(500).json({ error: "internalerror" });
}
};
export const deleteTeam = async (req, res) => {
try {
const { authcode: token } = req.headers;
const { teamid: teamId } = req.params;
const user = await getUserByToken(token);
const success = await TeamService.deleteTeam(teamId);
if (!success) {
return res.status(404).json({ error: "notfound" });
}
res.status(200).json({ message: "deleted" });
} catch (error) {
console.error('[deleteTeam] - Error:', error);
res.status(500).json({ error: "internalerror" });
}
};
export const getLeagues = async (req, res) => {
try {
const { authcode: token } = req.headers;
const { clubid: clubId } = req.params;
const { seasonid: seasonId } = req.query;
const user = await getUserByToken(token);
const leagues = await TeamService.getLeaguesByClub(clubId, seasonId);
res.status(200).json(leagues);
} catch (error) {
console.error('[getLeagues] - Error:', error);
res.status(500).json({ error: "internalerror" });
}
};

View File

@@ -0,0 +1,215 @@
import multer from 'multer';
import path from 'path';
import TeamDocumentService from '../services/teamDocumentService.js';
import PDFParserService from '../services/pdfParserService.js';
import { getUserByToken } from '../utils/userUtils.js';
import { devLog } from '../utils/logger.js';
// Multer-Konfiguration für Datei-Uploads
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, 'uploads/temp/');
},
filename: (req, file, cb) => {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
cb(null, file.fieldname + '-' + uniqueSuffix + path.extname(file.originalname));
}
});
const upload = multer({
storage: storage,
limits: {
fileSize: 10 * 1024 * 1024 // 10MB Limit
},
fileFilter: (req, file, cb) => {
// Erlaube nur PDF, DOC, DOCX, TXT, CSV Dateien
const allowedTypes = /pdf|doc|docx|txt|csv/;
const extname = allowedTypes.test(path.extname(file.originalname).toLowerCase());
const mimetype = allowedTypes.test(file.mimetype);
if (mimetype && extname) {
return cb(null, true);
} else {
cb(new Error('Nur PDF, DOC, DOCX, TXT und CSV Dateien sind erlaubt!'));
}
}
});
export const uploadMiddleware = upload.single('document');
export const uploadDocument = async (req, res) => {
try {
const { authcode: token } = req.headers;
const { clubteamid: clubTeamId } = req.params;
const { documentType } = req.body;
const user = await getUserByToken(token);
if (!req.file) {
return res.status(400).json({ error: "nofile" });
}
if (!documentType || !['code_list', 'pin_list'].includes(documentType)) {
return res.status(400).json({ error: "invaliddocumenttype" });
}
const document = await TeamDocumentService.uploadDocument(req.file, clubTeamId, documentType);
res.status(201).json(document);
} catch (error) {
console.error('[uploadDocument] - Error:', error);
// Lösche temporäre Datei bei Fehler
if (req.file && req.file.path) {
try {
const fs = await import('fs');
fs.unlinkSync(req.file.path);
} catch (cleanupError) {
console.error('Fehler beim Löschen der temporären Datei:', cleanupError);
}
}
if (error.message === 'Club-Team nicht gefunden') {
return res.status(404).json({ error: "clubteamnotfound" });
}
res.status(500).json({ error: "internalerror" });
}
};
export const getDocuments = async (req, res) => {
try {
const { authcode: token } = req.headers;
const { clubteamid: clubTeamId } = req.params;
const user = await getUserByToken(token);
const documents = await TeamDocumentService.getDocumentsByClubTeam(clubTeamId);
res.status(200).json(documents);
} catch (error) {
console.error('[getDocuments] - Error:', error);
res.status(500).json({ error: "internalerror" });
}
};
export const getDocument = async (req, res) => {
try {
const { authcode: token } = req.headers;
const { documentid: documentId } = req.params;
const user = await getUserByToken(token);
const document = await TeamDocumentService.getDocumentById(documentId);
if (!document) {
return res.status(404).json({ error: "notfound" });
}
res.status(200).json(document);
} catch (error) {
console.error('[getDocument] - Error:', error);
res.status(500).json({ error: "internalerror" });
}
};
export const downloadDocument = async (req, res) => {
try {
const { authcode: token } = req.headers;
const { documentid: documentId } = req.params;
const user = await getUserByToken(token);
const document = await TeamDocumentService.getDocumentById(documentId);
if (!document) {
return res.status(404).json({ error: "notfound" });
}
const filePath = await TeamDocumentService.getDocumentPath(documentId);
if (!filePath) {
return res.status(404).json({ error: "filenotfound" });
}
// Prüfe ob Datei existiert
const fs = await import('fs');
if (!fs.existsSync(filePath)) {
return res.status(404).json({ error: "filenotfound" });
}
// Setze Headers für Inline-Anzeige (PDF-Viewer)
res.setHeader('Content-Disposition', `inline; filename="${document.originalFileName}"`);
res.setHeader('Content-Type', document.mimeType);
// Sende die Datei
res.sendFile(filePath);
} catch (error) {
console.error('[downloadDocument] - Error:', error);
res.status(500).json({ error: "internalerror" });
}
};
export const deleteDocument = async (req, res) => {
try {
const { authcode: token } = req.headers;
const { documentid: documentId } = req.params;
const user = await getUserByToken(token);
const success = await TeamDocumentService.deleteDocument(documentId);
if (!success) {
return res.status(404).json({ error: "notfound" });
}
res.status(200).json({ message: "Document deleted successfully" });
} catch (error) {
console.error('[deleteDocument] - Error:', error);
res.status(500).json({ error: "internalerror" });
}
};
export const parsePDF = async (req, res) => {
try {
const { authcode: token } = req.headers;
const { documentid: documentId } = req.params;
const { leagueid: leagueId } = req.query;
const user = await getUserByToken(token);
if (!leagueId) {
return res.status(400).json({ error: "missingleagueid" });
}
// Hole Dokument-Informationen
const document = await TeamDocumentService.getDocumentById(documentId);
if (!document) {
return res.status(404).json({ error: "documentnotfound" });
}
// Prüfe ob es eine PDF- oder TXT-Datei ist
if (!document.mimeType.includes('pdf') && !document.mimeType.includes('text/plain')) {
return res.status(400).json({ error: "notapdfortxt" });
}
// Parse PDF
const parseResult = await PDFParserService.parsePDF(document.filePath, document.clubTeam.clubId);
// Speichere Matches in Datenbank
const saveResult = await PDFParserService.saveMatchesToDatabase(parseResult.matches, parseInt(leagueId));
res.status(200).json({
parseResult: {
matchesFound: parseResult.matches.length,
debugInfo: parseResult.debugInfo,
allLines: parseResult.allLines,
rawText: parseResult.rawText
},
saveResult: {
created: saveResult.created,
updated: saveResult.updated,
errors: saveResult.errors
}
});
} catch (error) {
console.error('[parsePDF] - Error:', error);
res.status(500).json({ error: "internalerror" });
}
};

View File

@@ -0,0 +1,44 @@
-- Migration: Add season_id to teams table
-- First, add the column as nullable
ALTER TABLE `team` ADD COLUMN `season_id` INT NULL;
-- Get or create current season
SET @current_season_id = (
SELECT id FROM `season`
WHERE season = (
CASE
WHEN MONTH(CURDATE()) >= 7 THEN CONCAT(YEAR(CURDATE()), '/', YEAR(CURDATE()) + 1)
ELSE CONCAT(YEAR(CURDATE()) - 1, '/', YEAR(CURDATE()))
END
)
LIMIT 1
);
-- If no season exists, create it
INSERT IGNORE INTO `season` (season) VALUES (
CASE
WHEN MONTH(CURDATE()) >= 7 THEN CONCAT(YEAR(CURDATE()), '/', YEAR(CURDATE()) + 1)
ELSE CONCAT(YEAR(CURDATE()) - 1, '/', YEAR(CURDATE()))
END
);
-- Get the season ID again (in case we just created it)
SET @current_season_id = (
SELECT id FROM `season`
WHERE season = (
CASE
WHEN MONTH(CURDATE()) >= 7 THEN CONCAT(YEAR(CURDATE()), '/', YEAR(CURDATE()) + 1)
ELSE CONCAT(YEAR(CURDATE()) - 1, '/', YEAR(CURDATE()))
END
)
LIMIT 1
);
-- Update all existing teams to use the current season
UPDATE `team` SET `season_id` = @current_season_id WHERE `season_id` IS NULL;
-- Now make the column NOT NULL and add the foreign key constraint
ALTER TABLE `team` MODIFY COLUMN `season_id` INT NOT NULL;
ALTER TABLE `team` ADD CONSTRAINT `team_season_id_foreign_idx`
FOREIGN KEY (`season_id`) REFERENCES `season` (`id`)
ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -0,0 +1,54 @@
import { DataTypes } from 'sequelize';
import sequelize from '../database.js';
import Club from './Club.js';
import League from './League.js';
import Season from './Season.js';
const ClubTeam = sequelize.define('ClubTeam', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
allowNull: false,
},
name: {
type: DataTypes.STRING,
allowNull: false,
},
clubId: {
type: DataTypes.INTEGER,
allowNull: false,
references: {
model: Club,
key: 'id',
},
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
},
leagueId: {
type: DataTypes.INTEGER,
allowNull: true,
references: {
model: League,
key: 'id',
},
onDelete: 'SET NULL',
onUpdate: 'CASCADE',
},
seasonId: {
type: DataTypes.INTEGER,
allowNull: true,
references: {
model: Season,
key: 'id',
},
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
},
}, {
underscored: true,
tableName: 'club_team',
timestamps: true,
});
export default ClubTeam;

View File

@@ -1,132 +0,0 @@
import { DataTypes } from 'sequelize';
import sequelize from '../database.js';
import { encryptData, decryptData } from '../utils/encrypt.js';
const ExternalServiceAccount = sequelize.define('ExternalServiceAccount', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
allowNull: false
},
userId: {
type: DataTypes.INTEGER,
allowNull: false,
references: {
model: 'user',
key: 'id'
},
onDelete: 'CASCADE'
},
service: {
type: DataTypes.STRING(50),
allowNull: false,
defaultValue: 'mytischtennis'
},
email: {
type: DataTypes.STRING,
allowNull: false,
},
encryptedPassword: {
type: DataTypes.TEXT,
allowNull: true,
field: 'encrypted_password'
},
savePassword: {
type: DataTypes.BOOLEAN,
defaultValue: false,
allowNull: false,
field: 'save_password'
},
accessToken: {
type: DataTypes.TEXT,
allowNull: true,
field: 'access_token'
},
refreshToken: {
type: DataTypes.TEXT,
allowNull: true,
field: 'refresh_token'
},
expiresAt: {
type: DataTypes.BIGINT,
allowNull: true,
field: 'expires_at'
},
cookie: {
type: DataTypes.TEXT,
allowNull: true
},
userData: {
type: DataTypes.JSON,
allowNull: true,
field: 'user_data'
},
clubId: {
type: DataTypes.STRING,
allowNull: true,
field: 'club_id'
},
clubName: {
type: DataTypes.STRING,
allowNull: true,
field: 'club_name'
},
fedNickname: {
type: DataTypes.STRING,
allowNull: true,
field: 'fed_nickname'
},
lastLoginAttempt: {
type: DataTypes.DATE,
allowNull: true,
field: 'last_login_attempt'
},
lastLoginSuccess: {
type: DataTypes.DATE,
allowNull: true,
field: 'last_login_success'
}
}, {
underscored: true,
tableName: 'external_service_account',
timestamps: true,
indexes: [
{
unique: true,
fields: ['user_id', 'service']
}
],
hooks: {
beforeSave: async (instance) => {
// Wenn savePassword false ist, password auf null setzen
if (!instance.savePassword) {
instance.encryptedPassword = null;
}
}
}
});
// Virtuelle Felder für password handling
ExternalServiceAccount.prototype.setPassword = function(password) {
if (password && this.savePassword) {
this.encryptedPassword = encryptData(password);
} else {
this.encryptedPassword = null;
}
};
ExternalServiceAccount.prototype.getPassword = function() {
if (this.encryptedPassword) {
try {
return decryptData(this.encryptedPassword);
} catch (error) {
console.error('Error decrypting password:', error);
return null;
}
}
return null;
};
export default ExternalServiceAccount;

View File

@@ -3,7 +3,6 @@ import sequelize from '../database.js';
import Club from './Club.js';
import League from './League.js';
import Team from './Team.js';
import Season from './Season.js';
import Location from './Location.js';
const Match = sequelize.define('Match', {
@@ -21,14 +20,6 @@ const Match = sequelize.define('Match', {
type: DataTypes.TIME,
allowNull: true,
},
seasonId: {
type: DataTypes.INTEGER,
references: {
model: Season,
key: 'id',
},
allowNull: false,
},
locationId: {
type: DataTypes.INTEGER,
references: {
@@ -69,6 +60,21 @@ const Match = sequelize.define('Match', {
},
allowNull: false,
},
code: {
type: DataTypes.STRING,
allowNull: true,
comment: 'Spiel-Code aus PDF-Parsing'
},
homePin: {
type: DataTypes.STRING,
allowNull: true,
comment: 'Pin-Code für Heimteam aus PDF-Parsing'
},
guestPin: {
type: DataTypes.STRING,
allowNull: true,
comment: 'Pin-Code für Gastteam aus PDF-Parsing'
},
}, {
underscored: true,
tableName: 'match',

View File

@@ -2,7 +2,7 @@ import { DataTypes } from 'sequelize';
import sequelize from '../database.js';
import { encryptData, decryptData } from '../utils/encrypt.js';
const ExternalServiceAccount = sequelize.define('ExternalServiceAccount', {
const MyTischtennis = sequelize.define('MyTischtennis', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
@@ -12,17 +12,13 @@ const ExternalServiceAccount = sequelize.define('ExternalServiceAccount', {
userId: {
type: DataTypes.INTEGER,
allowNull: false,
unique: true,
references: {
model: 'user',
key: 'id'
},
onDelete: 'CASCADE'
},
service: {
type: DataTypes.STRING(50),
allowNull: false,
defaultValue: 'mytischtennis'
},
email: {
type: DataTypes.STRING,
allowNull: false,
@@ -89,14 +85,8 @@ const ExternalServiceAccount = sequelize.define('ExternalServiceAccount', {
}
}, {
underscored: true,
tableName: 'external_service_account',
tableName: 'my_tischtennis',
timestamps: true,
indexes: [
{
unique: true,
fields: ['user_id', 'service']
}
],
hooks: {
beforeSave: async (instance) => {
// Wenn savePassword false ist, password auf null setzen
@@ -108,7 +98,7 @@ const ExternalServiceAccount = sequelize.define('ExternalServiceAccount', {
});
// Virtuelle Felder für password handling
ExternalServiceAccount.prototype.setPassword = function(password) {
MyTischtennis.prototype.setPassword = function(password) {
if (password && this.savePassword) {
this.encryptedPassword = encryptData(password);
} else {
@@ -116,17 +106,17 @@ ExternalServiceAccount.prototype.setPassword = function(password) {
}
};
ExternalServiceAccount.prototype.getPassword = function() {
MyTischtennis.prototype.getPassword = function() {
if (this.encryptedPassword) {
try {
return decryptData(this.encryptedPassword);
} catch (error) {
console.error('Error decrypting password:', error);
console.error('Error decrypting myTischtennis password:', error);
return null;
}
}
return null;
};
export default ExternalServiceAccount;
export default MyTischtennis;

View File

@@ -1,6 +1,8 @@
import { DataTypes } from 'sequelize';
import sequelize from '../database.js';
import Club from './Club.js';
import League from './League.js';
import Season from './Season.js';
const Team = sequelize.define('Team', {
id: {
@@ -23,6 +25,26 @@ const Team = sequelize.define('Team', {
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
},
leagueId: {
type: DataTypes.INTEGER,
allowNull: true,
references: {
model: League,
key: 'id',
},
onDelete: 'SET NULL',
onUpdate: 'CASCADE',
},
seasonId: {
type: DataTypes.INTEGER,
allowNull: true,
references: {
model: Season,
key: 'id',
},
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
},
}, {
underscored: true,
tableName: 'team',

View File

@@ -0,0 +1,52 @@
import { DataTypes } from 'sequelize';
import sequelize from '../database.js';
import ClubTeam from './ClubTeam.js';
const TeamDocument = sequelize.define('TeamDocument', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
allowNull: false,
},
fileName: {
type: DataTypes.STRING,
allowNull: false,
},
originalFileName: {
type: DataTypes.STRING,
allowNull: false,
},
filePath: {
type: DataTypes.STRING,
allowNull: false,
},
fileSize: {
type: DataTypes.INTEGER,
allowNull: false,
},
mimeType: {
type: DataTypes.STRING,
allowNull: false,
},
documentType: {
type: DataTypes.ENUM('code_list', 'pin_list'),
allowNull: false,
},
clubTeamId: {
type: DataTypes.INTEGER,
allowNull: false,
references: {
model: ClubTeam,
key: 'id',
},
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
},
}, {
underscored: true,
tableName: 'team_document',
timestamps: true,
});
export default TeamDocument;

View File

@@ -19,6 +19,8 @@ import DiaryDateActivity from './DiaryDateActivity.js';
import Match from './Match.js';
import League from './League.js';
import Team from './Team.js';
import ClubTeam from './ClubTeam.js';
import TeamDocument from './TeamDocument.js';
import Season from './Season.js';
import Location from './Location.js';
import Group from './Group.js';
@@ -33,7 +35,7 @@ import UserToken from './UserToken.js';
import OfficialTournament from './OfficialTournament.js';
import OfficialCompetition from './OfficialCompetition.js';
import OfficialCompetitionMember from './OfficialCompetitionMember.js';
import ExternalServiceAccount from './ExternalServiceAccount.js';
import MyTischtennis from './MyTischtennis.js';
// Official tournaments relations
OfficialTournament.hasMany(OfficialCompetition, { foreignKey: 'tournamentId', as: 'competitions' });
OfficialCompetition.belongsTo(OfficialTournament, { foreignKey: 'tournamentId', as: 'tournament' });
@@ -118,8 +120,25 @@ Team.belongsTo(Club, { foreignKey: 'clubId', as: 'club' });
Club.hasMany(League, { foreignKey: 'clubId', as: 'leagues' });
League.belongsTo(Club, { foreignKey: 'clubId', as: 'club' });
Match.belongsTo(Season, { foreignKey: 'seasonId', as: 'season' });
Season.hasMany(Match, { foreignKey: 'seasonId', as: 'matches' });
League.hasMany(Team, { foreignKey: 'leagueId', as: 'teams' });
Team.belongsTo(League, { foreignKey: 'leagueId', as: 'league' });
Season.hasMany(Team, { foreignKey: 'seasonId', as: 'teams' });
Team.belongsTo(Season, { foreignKey: 'seasonId', as: 'season' });
// ClubTeam relationships
Club.hasMany(ClubTeam, { foreignKey: 'clubId', as: 'clubTeams' });
ClubTeam.belongsTo(Club, { foreignKey: 'clubId', as: 'club' });
League.hasMany(ClubTeam, { foreignKey: 'leagueId', as: 'clubTeams' });
ClubTeam.belongsTo(League, { foreignKey: 'leagueId', as: 'league' });
Season.hasMany(ClubTeam, { foreignKey: 'seasonId', as: 'clubTeams' });
ClubTeam.belongsTo(Season, { foreignKey: 'seasonId', as: 'season' });
// TeamDocument relationships
ClubTeam.hasMany(TeamDocument, { foreignKey: 'clubTeamId', as: 'documents' });
TeamDocument.belongsTo(ClubTeam, { foreignKey: 'clubTeamId', as: 'clubTeam' });
Match.belongsTo(Location, { foreignKey: 'locationId', as: 'location' });
Location.hasMany(Match, { foreignKey: 'locationId', as: 'matches' });
@@ -205,8 +224,8 @@ Member.hasMany(Accident, { foreignKey: 'memberId', as: 'accidents' });
Accident.belongsTo(DiaryDate, { foreignKey: 'diaryDateId', as: 'diaryDates' });
DiaryDate.hasMany(Accident, { foreignKey: 'diaryDateId', as: 'accidents' });
User.hasMany(ExternalServiceAccount, { foreignKey: 'userId', as: 'externalServiceAccounts' });
ExternalServiceAccount.belongsTo(User, { foreignKey: 'userId', as: 'user' });
User.hasOne(MyTischtennis, { foreignKey: 'userId', as: 'myTischtennis' });
MyTischtennis.belongsTo(User, { foreignKey: 'userId', as: 'user' });
export {
User,
@@ -231,6 +250,8 @@ export {
Match,
League,
Team,
ClubTeam,
TeamDocument,
Group,
GroupActivity,
Tournament,
@@ -243,5 +264,5 @@ export {
OfficialTournament,
OfficialCompetition,
OfficialCompetitionMember,
ExternalServiceAccount,
MyTischtennis,
};

View File

@@ -0,0 +1,32 @@
import express from 'express';
import { authenticate } from '../middleware/authMiddleware.js';
import {
getClubTeams,
getClubTeam,
createClubTeam,
updateClubTeam,
deleteClubTeam,
getLeagues
} from '../controllers/clubTeamController.js';
const router = express.Router();
// Get all club teams for a club
router.get('/club/:clubid', authenticate, getClubTeams);
// Create a new club team
router.post('/club/:clubid', authenticate, createClubTeam);
// Get leagues for a club
router.get('/leagues/:clubid', authenticate, getLeagues);
// Get a specific club team
router.get('/:clubteamid', authenticate, getClubTeam);
// Update a club team
router.put('/:clubteamid', authenticate, updateClubTeam);
// Delete a club team
router.delete('/:clubteamid', authenticate, deleteClubTeam);
export default router;

View File

@@ -1,36 +0,0 @@
import express from 'express';
import externalServiceController from '../controllers/externalServiceController.js';
import { authenticate } from '../middleware/authMiddleware.js';
const router = express.Router();
// All routes require authentication
router.use(authenticate);
// GET /api/external-service/account?service=mytischtennis - Get account
router.get('/account', externalServiceController.getAccount);
// GET /api/external-service/status?service=mytischtennis - Check status
router.get('/status', externalServiceController.getStatus);
// POST /api/external-service/account - Create or update account
router.post('/account', externalServiceController.upsertAccount);
// DELETE /api/external-service/account?service=mytischtennis - Delete account
router.delete('/account', externalServiceController.deleteAccount);
// POST /api/external-service/verify - Verify login
router.post('/verify', externalServiceController.verifyLogin);
// GET /api/external-service/session?service=mytischtennis - Get stored session
router.get('/session', externalServiceController.getSession);
// HeTTV specific routes
// GET /api/external-service/hettv/main-page - Load HeTTV main page and find downloads
router.get('/hettv/main-page', externalServiceController.loadHettvMainPage);
// POST /api/external-service/hettv/download-page - Load specific HeTTV download page
router.post('/hettv/download-page', externalServiceController.loadHettvDownloadPage);
export default router;

View File

@@ -0,0 +1,29 @@
import express from 'express';
import myTischtennisController from '../controllers/myTischtennisController.js';
import { authenticate } from '../middleware/authMiddleware.js';
const router = express.Router();
// All routes require authentication
router.use(authenticate);
// GET /api/mytischtennis/account - Get account
router.get('/account', myTischtennisController.getAccount);
// GET /api/mytischtennis/status - Check status
router.get('/status', myTischtennisController.getStatus);
// POST /api/mytischtennis/account - Create or update account
router.post('/account', myTischtennisController.upsertAccount);
// DELETE /api/mytischtennis/account - Delete account
router.delete('/account', myTischtennisController.deleteAccount);
// POST /api/mytischtennis/verify - Verify login
router.post('/verify', myTischtennisController.verifyLogin);
// GET /api/mytischtennis/session - Get stored session
router.get('/session', myTischtennisController.getSession);
export default router;

View File

@@ -0,0 +1,28 @@
import express from 'express';
import { authenticate } from '../middleware/authMiddleware.js';
import {
getSeasons,
getCurrentSeason,
createSeason,
getSeason,
deleteSeason
} from '../controllers/seasonController.js';
const router = express.Router();
// Get all seasons
router.get('/', authenticate, getSeasons);
// Get current season (creates if not exists)
router.get('/current', authenticate, getCurrentSeason);
// Get a specific season
router.get('/:seasonid', authenticate, getSeason);
// Create a new season
router.post('/', authenticate, createSeason);
// Delete a season
router.delete('/:seasonid', authenticate, deleteSeason);
export default router;

View File

@@ -0,0 +1,33 @@
import express from 'express';
import { authenticate } from '../middleware/authMiddleware.js';
import {
uploadMiddleware,
uploadDocument,
getDocuments,
getDocument,
downloadDocument,
deleteDocument,
parsePDF
} from '../controllers/teamDocumentController.js';
const router = express.Router();
// Upload eines Dokuments für ein Club-Team
router.post('/club-team/:clubteamid/upload', authenticate, uploadMiddleware, uploadDocument);
// Alle Dokumente für ein Club-Team abrufen
router.get('/club-team/:clubteamid', authenticate, getDocuments);
// Ein spezifisches Dokument abrufen
router.get('/:documentid', authenticate, getDocument);
// Ein Dokument herunterladen
router.get('/:documentid/download', authenticate, downloadDocument);
// Ein Dokument löschen
router.delete('/:documentid', authenticate, deleteDocument);
// PDF parsen und Matches extrahieren
router.post('/:documentid/parse', authenticate, parsePDF);
export default router;

View File

@@ -0,0 +1,32 @@
import express from 'express';
import { authenticate } from '../middleware/authMiddleware.js';
import {
getTeams,
getTeam,
createTeam,
updateTeam,
deleteTeam,
getLeagues
} from '../controllers/teamController.js';
const router = express.Router();
// Get all teams for a club
router.get('/club/:clubid', authenticate, getTeams);
// Get leagues for a club
router.get('/leagues/:clubid', authenticate, getLeagues);
// Get a specific team
router.get('/:teamid', authenticate, getTeam);
// Create a new team
router.post('/club/:clubid', authenticate, createTeam);
// Update a team
router.put('/:teamid', authenticate, updateTeam);
// Delete a team
router.delete('/:teamid', authenticate, deleteTeam);
export default router;

View File

@@ -6,7 +6,7 @@ import cors from 'cors';
import {
User, Log, Club, UserClub, Member, DiaryDate, Participant, Activity, MemberNote,
DiaryNote, DiaryTag, MemberDiaryTag, DiaryDateTag, DiaryMemberNote, DiaryMemberTag,
PredefinedActivity, PredefinedActivityImage, DiaryDateActivity, DiaryMemberActivity, Match, League, Team, Group,
PredefinedActivity, PredefinedActivityImage, DiaryDateActivity, DiaryMemberActivity, Match, League, Team, ClubTeam, TeamDocument, Group,
GroupActivity, Tournament, TournamentGroup, TournamentMatch, TournamentResult,
TournamentMember, Accident, UserToken, OfficialTournament, OfficialCompetition, OfficialCompetitionMember, MyTischtennis
} from './models/index.js';
@@ -34,6 +34,10 @@ import accidentRoutes from './routes/accidentRoutes.js';
import trainingStatsRoutes from './routes/trainingStatsRoutes.js';
import officialTournamentRoutes from './routes/officialTournamentRoutes.js';
import myTischtennisRoutes from './routes/myTischtennisRoutes.js';
import teamRoutes from './routes/teamRoutes.js';
import clubTeamRoutes from './routes/clubTeamRoutes.js';
import teamDocumentRoutes from './routes/teamDocumentRoutes.js';
import seasonRoutes from './routes/seasonRoutes.js';
const app = express();
const port = process.env.PORT || 3000;
@@ -79,6 +83,10 @@ app.use('/api/accident', accidentRoutes);
app.use('/api/training-stats', trainingStatsRoutes);
app.use('/api/official-tournaments', officialTournamentRoutes);
app.use('/api/mytischtennis', myTischtennisRoutes);
app.use('/api/teams', teamRoutes);
app.use('/api/club-teams', clubTeamRoutes);
app.use('/api/team-documents', teamDocumentRoutes);
app.use('/api/seasons', seasonRoutes);
app.use(express.static(path.join(__dirname, '../frontend/dist')));

View File

@@ -0,0 +1,180 @@
import ClubTeam from '../models/ClubTeam.js';
import League from '../models/League.js';
import Season from '../models/Season.js';
import SeasonService from './seasonService.js';
import { devLog } from '../utils/logger.js';
class ClubTeamService {
/**
* Holt alle ClubTeams für einen Verein, optional gefiltert nach Saison.
* Wenn keine Saison-ID angegeben ist, wird die aktuelle Saison verwendet.
* @param {number} clubId - Die ID des Vereins.
* @param {number|null} seasonId - Optionale Saison-ID.
* @returns {Promise<Array<ClubTeam>>} Eine Liste von ClubTeams.
*/
static async getAllClubTeamsByClub(clubId, seasonId = null) {
try {
// Wenn keine Saison angegeben, verwende die aktuelle
if (!seasonId) {
const currentSeason = await SeasonService.getOrCreateCurrentSeason();
seasonId = currentSeason.id;
}
const clubTeams = await ClubTeam.findAll({
where: { clubId, seasonId },
order: [['name', 'ASC']]
});
// Manuelle Datenanreicherung für Liga und Saison
const enrichedClubTeams = [];
for (const clubTeam of clubTeams) {
const enrichedTeam = {
id: clubTeam.id,
name: clubTeam.name,
clubId: clubTeam.clubId,
leagueId: clubTeam.leagueId,
seasonId: clubTeam.seasonId,
createdAt: clubTeam.createdAt,
updatedAt: clubTeam.updatedAt,
league: { name: 'Unbekannt' },
season: { season: 'Unbekannt' }
};
// Lade Liga-Daten
if (clubTeam.leagueId) {
const league = await League.findByPk(clubTeam.leagueId, { attributes: ['name'] });
if (league) enrichedTeam.league = league;
}
// Lade Saison-Daten
if (clubTeam.seasonId) {
const season = await Season.findByPk(clubTeam.seasonId, { attributes: ['season'] });
if (season) enrichedTeam.season = season;
}
enrichedClubTeams.push(enrichedTeam);
}
return enrichedClubTeams;
} catch (error) {
console.error('[ClubTeamService.getAllClubTeamsByClub] - Error:', error);
throw error;
}
}
/**
* Holt ein ClubTeam anhand seiner ID
* @param {number} clubTeamId - Die ID des ClubTeams
* @returns {Promise<ClubTeam|null>} Das ClubTeam oder null, wenn nicht gefunden
*/
static async getClubTeamById(clubTeamId) {
try {
const clubTeam = await ClubTeam.findByPk(clubTeamId, {
include: [
{
model: League,
as: 'league',
attributes: ['id', 'name']
},
{
model: Season,
as: 'season',
attributes: ['id', 'season']
}
]
});
return clubTeam;
} catch (error) {
console.error('[ClubTeamService.getClubTeamById] - Error:', error);
throw error;
}
}
/**
* Erstellt ein neues ClubTeam.
* Wenn keine Saison-ID angegeben ist, wird die aktuelle Saison zugewiesen.
* @param {object} clubTeamData - Die Daten des neuen ClubTeams (name, clubId, optional leagueId, seasonId).
* @returns {Promise<ClubTeam>} Das erstellte ClubTeam.
*/
static async createClubTeam(clubTeamData) {
try {
// Wenn keine Saison angegeben, verwende die aktuelle
if (!clubTeamData.seasonId) {
const currentSeason = await SeasonService.getOrCreateCurrentSeason();
clubTeamData.seasonId = currentSeason.id;
}
const clubTeam = await ClubTeam.create(clubTeamData);
return clubTeam;
} catch (error) {
console.error('[ClubTeamService.createClubTeam] - Error:', error);
throw error;
}
}
/**
* Aktualisiert ein bestehendes ClubTeam.
* @param {number} clubTeamId - Die ID des zu aktualisierenden ClubTeams.
* @param {object} updateData - Die zu aktualisierenden Daten.
* @returns {Promise<boolean>} True, wenn das ClubTeam aktualisiert wurde, sonst false.
*/
static async updateClubTeam(clubTeamId, updateData) {
try {
const [updatedRowsCount] = await ClubTeam.update(updateData, {
where: { id: clubTeamId }
});
return updatedRowsCount > 0;
} catch (error) {
console.error('[ClubTeamService.updateClubTeam] - Error:', error);
throw error;
}
}
/**
* Löscht ein ClubTeam.
* @param {number} clubTeamId - Die ID des zu löschenden ClubTeams.
* @returns {Promise<boolean>} True, wenn das ClubTeam gelöscht wurde, sonst false.
*/
static async deleteClubTeam(clubTeamId) {
try {
const deletedRows = await ClubTeam.destroy({
where: { id: clubTeamId }
});
return deletedRows > 0;
} catch (error) {
console.error('[ClubTeamService.deleteClubTeam] - Error:', error);
throw error;
}
}
/**
* Holt alle Ligen für einen Verein, optional gefiltert nach Saison.
* Wenn keine Saison-ID angegeben ist, wird die aktuelle Saison verwendet.
* @param {number} clubId - Die ID des Vereins.
* @param {number|null} seasonId - Optionale Saison-ID.
* @returns {Promise<Array<League>>} Eine Liste von Ligen.
*/
static async getLeaguesByClub(clubId, seasonId = null) {
try {
// Wenn keine Saison angegeben, verwende die aktuelle
if (!seasonId) {
const currentSeason = await SeasonService.getOrCreateCurrentSeason();
seasonId = currentSeason.id;
}
const leagues = await League.findAll({
where: { clubId, seasonId },
attributes: ['id', 'name', 'seasonId'],
order: [['name', 'ASC']]
});
return leagues;
} catch (error) {
console.error('[ClubTeamService.getLeaguesByClub] - Error:', error);
throw error;
}
}
}
export default ClubTeamService;

View File

@@ -10,9 +10,7 @@ import { devLog } from '../utils/logger.js';
class DiaryDateActivityService {
async createActivity(userToken, clubId, data) {
devLog('[DiaryDateActivityService::createActivity] - check user access');
await checkAccess(userToken, clubId);
devLog('[DiaryDateActivityService::createActivity] - add: ', data);
const { activity, ...restData } = data;
// Versuche, die PredefinedActivity robust zu finden:
// 1) per übergebener ID
@@ -60,23 +58,18 @@ class DiaryDateActivityService {
});
const newOrderId = maxOrderId !== null ? maxOrderId + 1 : 1;
restData.orderId = newOrderId;
devLog('[DiaryDateActivityService::createActivity] - create diary date activity');
return await DiaryDateActivity.create(restData);
}
async updateActivity(userToken, clubId, id, data) {
devLog('[DiaryDateActivityService::updateActivity] - check user access');
await checkAccess(userToken, clubId);
devLog('[DiaryDateActivityService::updateActivity] - load activity', id);
const activity = await DiaryDateActivity.findByPk(id);
if (!activity) {
devLog('[DiaryDateActivityService::updateActivity] - activity not found');
throw new Error('Activity not found');
}
// Wenn customActivityName gesendet wird, müssen wir die PredefinedActivity behandeln
if (data.customActivityName) {
devLog('[DiaryDateActivityService::updateActivity] - handling customActivityName:', data.customActivityName);
// Suche nach einer existierenden PredefinedActivity mit diesem Namen
let predefinedActivity = await PredefinedActivity.findOne({
@@ -85,7 +78,6 @@ class DiaryDateActivityService {
if (!predefinedActivity) {
// Erstelle eine neue PredefinedActivity
devLog('[DiaryDateActivityService::updateActivity] - creating new PredefinedActivity');
predefinedActivity = await PredefinedActivity.create({
name: data.customActivityName,
description: data.description || '',
@@ -100,7 +92,6 @@ class DiaryDateActivityService {
delete data.customActivityName;
}
devLog('[DiaryDateActivityService::updateActivity] - update activity', clubId, id, data, JSON.stringify(data));
return await activity.update(data);
}
@@ -114,22 +105,14 @@ class DiaryDateActivityService {
}
async updateActivityOrder(userToken, clubId, id, newOrderId) {
devLog(`[DiaryDateActivityService::updateActivityOrder] - Start update for activity id: ${id}`);
devLog(`[DiaryDateActivityService::updateActivityOrder] - User token: ${userToken}, Club id: ${clubId}, New order id: ${newOrderId}`);
devLog('[DiaryDateActivityService::updateActivityOrder] - Checking user access');
await checkAccess(userToken, clubId);
devLog('[DiaryDateActivityService::updateActivityOrder] - User access confirmed');
devLog(`[DiaryDateActivityService::updateActivityOrder] - Finding activity with id: ${id}`);
const activity = await DiaryDateActivity.findByPk(id);
if (!activity) {
console.error('[DiaryDateActivityService::updateActivityOrder] - Activity not found, throwing error');
throw new Error('Activity not found');
}
devLog('[DiaryDateActivityService::updateActivityOrder] - Activity found:', activity);
const currentOrderId = activity.orderId;
devLog(`[DiaryDateActivityService::updateActivityOrder] - Current order id: ${currentOrderId}`);
if (newOrderId < currentOrderId) {
devLog(`[DiaryDateActivityService::updateActivityOrder] - Shifting items down. Moving activities with orderId between ${newOrderId} and ${currentOrderId - 1}`);
await DiaryDateActivity.increment(
{ orderId: 1 },
{
@@ -139,9 +122,7 @@ class DiaryDateActivityService {
},
}
);
devLog(`[DiaryDateActivityService::updateActivityOrder] - Items shifted down`);
} else if (newOrderId > currentOrderId) {
devLog(`[DiaryDateActivityService::updateActivityOrder] - Shifting items up. Moving activities with orderId between ${currentOrderId + 1} and ${newOrderId}`);
await DiaryDateActivity.decrement(
{ orderId: 1 },
{
@@ -151,16 +132,10 @@ class DiaryDateActivityService {
},
}
);
devLog(`[DiaryDateActivityService::updateActivityOrder] - Items shifted up`);
} else {
devLog('[DiaryDateActivityService::updateActivityOrder] - New order id is the same as the current order id. No shift required.');
}
devLog(`[DiaryDateActivityService::updateActivityOrder] - Setting new order id for activity id: ${id}`);
activity.orderId = newOrderId;
devLog('[DiaryDateActivityService::updateActivityOrder] - Saving activity with new order id');
const savedActivity = await activity.save();
devLog('[DiaryDateActivityService::updateActivityOrder] - Activity saved:', savedActivity);
devLog(`[DiaryDateActivityService::updateActivityOrder] - Finished update for activity id: ${id}`);
return savedActivity;
}
@@ -257,9 +232,7 @@ class DiaryDateActivityService {
}
async addGroupActivity(userToken, clubId, diaryDateId, groupId, activity) {
devLog('[DiaryDateActivityService::addGroupActivity] Check user access');
await checkAccess(userToken, clubId);
devLog('[DiaryDateActivityService::addGroupActivity] Check diary date');
const diaryDateActivity = await DiaryDateActivity.findOne({
where: {
diaryDateId,
@@ -272,19 +245,16 @@ class DiaryDateActivityService {
console.error('[DiaryDateActivityService::addGroupActivity] Activity not found');
throw new Error('Activity not found');
}
devLog('[DiaryDateActivityService::addGroupActivity] Check group');
const group = await Group.findByPk(groupId);
if (!group || group.diaryDateId !== diaryDateActivity.diaryDateId) {
console.error('[DiaryDateActivityService::addGroupActivity] Group and date don\'t fit');
throw new Error('Group isn\'t related to date');
}
devLog('[DiaryDateActivityService::addGroupActivity] Get predefined activity');
const [predefinedActivity, created] = await PredefinedActivity.findOrCreate({
where: {
name: activity
}
});
devLog('[DiaryDateActivityService::addGroupActivity] Add group activity');
devLog(predefinedActivity);
const activityData = {
diaryDateActivity: diaryDateActivity.id,

View File

@@ -10,14 +10,11 @@ import HttpError from '../exceptions/HttpError.js';
import { devLog } from '../utils/logger.js';
class DiaryService {
async getDatesForClub(userToken, clubId) {
devLog('[DiaryService::getDatesForClub] - Check user access');
await checkAccess(userToken, clubId);
devLog('[DiaryService::getDatesForClub] - Validate club existence');
const club = await Club.findByPk(clubId);
if (!club) {
throw new HttpError('Club not found', 404);
}
devLog('[DiaryService::getDatesForClub] - Load diary dates');
const dates = await DiaryDate.findAll({
where: { clubId },
include: [
@@ -30,14 +27,11 @@ class DiaryService {
}
async createDateForClub(userToken, clubId, date, trainingStart, trainingEnd) {
devLog('[DiaryService::createDateForClub] - Check user access');
await checkAccess(userToken, clubId);
devLog('[DiaryService::createDateForClub] - Validate club existence');
const club = await Club.findByPk(clubId);
if (!club) {
throw new HttpError('Club not found', 404);
}
devLog('[DiaryService::createDateForClub] - Validate date');
const parsedDate = new Date(date);
if (isNaN(parsedDate.getTime())) {
throw new HttpError('Invalid date format', 400);
@@ -45,7 +39,6 @@ class DiaryService {
if (trainingStart && trainingEnd && trainingStart >= trainingEnd) {
throw new HttpError('Training start time must be before training end time', 400);
}
devLog('[DiaryService::createDateForClub] - Create new diary date');
const newDate = await DiaryDate.create({
date: parsedDate,
clubId,
@@ -57,9 +50,7 @@ class DiaryService {
}
async updateTrainingTimes(userToken, clubId, dateId, trainingStart, trainingEnd) {
devLog('[DiaryService::updateTrainingTimes] - Check user access');
await checkAccess(userToken, clubId);
devLog('[DiaryService::updateTrainingTimes] - Validate date');
const diaryDate = await DiaryDate.findOne({ where: { clubId, id: dateId } });
if (!diaryDate) {
throw new HttpError('Diary entry not found', 404);
@@ -67,7 +58,6 @@ class DiaryService {
if (trainingStart && trainingEnd && trainingStart >= trainingEnd) {
throw new HttpError('Training start time must be before training end time', 400);
}
devLog('[DiaryService::updateTrainingTimes] - Update training times');
diaryDate.trainingStart = trainingStart || null;
diaryDate.trainingEnd = trainingEnd || null;
await diaryDate.save();
@@ -75,14 +65,12 @@ class DiaryService {
}
async addNoteToDate(userToken, diaryDateId, content) {
devLog('[DiaryService::addNoteToDate] - Add note');
await checkAccess(userToken, diaryDateId);
await DiaryNote.create({ diaryDateId, content });
return await DiaryNote.findAll({ where: { diaryDateId }, order: [['createdAt', 'DESC']] });
}
async deleteNoteFromDate(userToken, noteId) {
devLog('[DiaryService::deleteNoteFromDate] - Delete note');
const note = await DiaryNote.findByPk(noteId);
if (!note) {
throw new HttpError('Note not found', 404);
@@ -93,7 +81,6 @@ class DiaryService {
}
async addTagToDate(userToken, diaryDateId, tagName) {
devLog('[DiaryService::addTagToDate] - Add tag');
await checkAccess(userToken, diaryDateId);
let tag = await DiaryTag.findOne({ where: { name: tagName } });
if (!tag) {
@@ -106,29 +93,24 @@ class DiaryService {
async addTagToDiaryDate(userToken, clubId, diaryDateId, tagId) {
checkAccess(userToken, clubId);
devLog(`[DiaryService::addTagToDiaryDate] - diaryDateId: ${diaryDateId}, tagId: ${tagId}`);
const diaryDate = await DiaryDate.findByPk(diaryDateId);
if (!diaryDate) {
throw new HttpError('DiaryDate not found', 404);
}
devLog('[DiaryService::addTagToDiaryDate] - Add tag to diary date');
const existingEntry = await DiaryDateTag.findOne({
where: { diaryDateId, tagId }
});
if (existingEntry) {
return;
}
devLog('[DiaryService::addTagToDiaryDate] - Tag not found, creating new entry');
const tag = await DiaryTag.findByPk(tagId);
if (!tag) {
throw new HttpError('Tag not found', 404);
}
devLog('[DiaryService::addTagToDiaryDate] - Add tag to diary date');
await DiaryDateTag.create({
diaryDateId,
tagId
});
devLog('[DiaryService::addTagToDiaryDate] - Get tags');
const tags = await DiaryDateTag.findAll({ where: {
diaryDateId: diaryDateId },
include: {
@@ -141,7 +123,6 @@ class DiaryService {
}
async getDiaryNotesForDateAndMember(diaryDateId, memberId) {
devLog('[DiaryService::getDiaryNotesForDateAndMember] - Fetching notes');
return await DiaryNote.findAll({
where: { diaryDateId, memberId },
order: [['createdAt', 'DESC']]
@@ -154,19 +135,15 @@ class DiaryService {
}
async removeDateForClub(userToken, clubId, dateId) {
devLog('[DiaryService::removeDateForClub] - Check user access');
await checkAccess(userToken, clubId);
devLog('[DiaryService::removeDateForClub] - Validate date');
const diaryDate = await DiaryDate.findOne({ where: { id: dateId, clubId } });
if (!diaryDate) {
throw new HttpError('Diary entry not found', 404);
}
devLog('[DiaryService::removeDateForClub] - Check for activities');
const activityCount = await DiaryDateActivity.count({ where: { diaryDateId: dateId } });
if (activityCount > 0) {
throw new HttpError('Cannot delete date with activities', 409);
}
devLog('[DiaryService::removeDateForClub] - Delete diary date');
await diaryDate.destroy();
return { ok: true };
}

View File

@@ -1,347 +0,0 @@
import ExternalServiceAccount from '../models/ExternalServiceAccount.js';
import User from '../models/User.js';
import myTischtennisClient from '../clients/myTischtennisClient.js';
import hettvClient from '../clients/hettvClient.js';
import HttpError from '../exceptions/HttpError.js';
class ExternalServiceService {
/**
* Get the appropriate client for a service
*/
getClientForService(service) {
switch (service) {
case 'mytischtennis':
return myTischtennisClient;
case 'hettv':
return hettvClient;
default:
throw new HttpError(400, `Unbekannter Service: ${service}`);
}
}
/**
* Get account for user and service
*/
async getAccount(userId, service = 'mytischtennis') {
const account = await ExternalServiceAccount.findOne({
where: { userId, service },
attributes: ['id', 'service', 'email', 'savePassword', 'lastLoginAttempt', 'lastLoginSuccess', 'expiresAt', 'userData', 'clubId', 'clubName', 'fedNickname', 'createdAt', 'updatedAt']
});
return account;
}
/**
* Create or update external service account
*/
async upsertAccount(userId, email, password, savePassword, userPassword, service = 'mytischtennis') {
// Verify user's app password
const user = await User.findByPk(userId);
if (!user) {
throw new HttpError(404, 'Benutzer nicht gefunden');
}
let loginResult = null;
// Wenn ein Passwort gesetzt/geändert wird, App-Passwort verifizieren
if (password) {
const isValidPassword = await user.validatePassword(userPassword);
if (!isValidPassword) {
throw new HttpError(401, 'Ungültiges Passwort');
}
// Login-Versuch beim entsprechenden Service
const client = this.getClientForService(service);
loginResult = await client.login(email, password);
if (!loginResult.success) {
throw new HttpError(401, loginResult.error || `${service}-Login fehlgeschlagen. Bitte überprüfen Sie Ihre Zugangsdaten.`);
}
}
// Find or create account
let account = await ExternalServiceAccount.findOne({ where: { userId, service } });
const now = new Date();
if (account) {
// Update existing
account.email = email;
account.savePassword = savePassword;
if (password && savePassword) {
account.setPassword(password);
} else if (!savePassword) {
account.encryptedPassword = null;
}
if (loginResult && loginResult.success) {
account.lastLoginAttempt = now;
account.lastLoginSuccess = now;
account.accessToken = loginResult.accessToken;
account.refreshToken = loginResult.refreshToken;
account.expiresAt = loginResult.expiresAt;
account.cookie = loginResult.cookie;
account.userData = loginResult.user;
// Hole Club-ID und Federation (nur für myTischtennis)
if (service === 'mytischtennis') {
console.log('[externalServiceService] - Getting user profile...');
const profileResult = await myTischtennisClient.getUserProfile(loginResult.cookie);
console.log('[externalServiceService] - Profile result:', {
success: profileResult.success,
clubId: profileResult.clubId,
clubName: profileResult.clubName,
fedNickname: profileResult.fedNickname
});
if (profileResult.success) {
account.clubId = profileResult.clubId;
account.clubName = profileResult.clubName;
account.fedNickname = profileResult.fedNickname;
console.log('[externalServiceService] - Updated account with club data');
} else {
console.error('[externalServiceService] - Failed to get profile:', profileResult.error);
}
}
} else if (password) {
account.lastLoginAttempt = now;
}
console.log('[externalServiceService] - Speichere Account (update).');
try {
await account.save();
} catch (e) {
console.error('[externalServiceService] - Fehler beim Speichern (update):', e.message, e.parent?.sqlMessage);
throw e;
}
} else {
// Create new
const accountData = {
userId,
service,
email,
savePassword,
lastLoginAttempt: password ? now : null,
lastLoginSuccess: loginResult?.success ? now : null
};
if (loginResult && loginResult.success) {
accountData.accessToken = loginResult.accessToken;
accountData.refreshToken = loginResult.refreshToken;
accountData.expiresAt = loginResult.expiresAt;
accountData.cookie = loginResult.cookie;
accountData.userData = loginResult.user;
// Hole Club-/Verbandsdaten nur für myTischtennis
if (service === 'mytischtennis') {
const profileResult = await myTischtennisClient.getUserProfile(loginResult.cookie);
if (profileResult.success) {
accountData.clubId = profileResult.clubId;
accountData.clubName = profileResult.clubName;
accountData.fedNickname = profileResult.fedNickname;
}
}
}
console.log('[externalServiceService] - Erstelle Account (create).');
try {
account = await ExternalServiceAccount.create(accountData);
} catch (e) {
console.error('[externalServiceService] - Fehler beim Erstellen (create):', e.message, e.parent?.sqlMessage);
throw e;
}
if (password && savePassword) {
account.setPassword(password);
console.log('[externalServiceService] - Speichere Passwort (nach create).');
try {
await account.save();
} catch (e) {
console.error('[externalServiceService] - Fehler beim Speichern (nach create):', e.message, e.parent?.sqlMessage);
throw e;
}
}
}
return {
id: account.id,
email: account.email,
savePassword: account.savePassword,
lastLoginAttempt: account.lastLoginAttempt,
lastLoginSuccess: account.lastLoginSuccess,
expiresAt: account.expiresAt
};
}
/**
* Delete external service account
*/
async deleteAccount(userId, service = 'mytischtennis') {
const deleted = await ExternalServiceAccount.destroy({
where: { userId, service }
});
return deleted > 0;
}
/**
* Verify login with stored or provided credentials
*/
async verifyLogin(userId, providedPassword = null, service = 'mytischtennis') {
const account = await ExternalServiceAccount.findOne({ where: { userId, service } });
if (!account) {
throw new HttpError(404, `Kein ${service}-Account verknüpft`);
}
let password = providedPassword;
// Wenn kein Passwort übergeben wurde, versuche gespeichertes Passwort zu verwenden
if (!password) {
if (!account.savePassword || !account.encryptedPassword) {
throw new HttpError(400, 'Kein Passwort gespeichert. Bitte geben Sie Ihr Passwort ein.');
}
password = account.getPassword();
}
// Login-Versuch
const now = new Date();
account.lastLoginAttempt = now;
const client = this.getClientForService(service);
const loginResult = await client.login(account.email, password);
if (loginResult.success) {
account.lastLoginSuccess = now;
account.accessToken = loginResult.accessToken;
account.refreshToken = loginResult.refreshToken;
account.expiresAt = loginResult.expiresAt;
account.cookie = loginResult.cookie;
account.userData = loginResult.user;
// Hole Club-/Verbandsdaten nur für myTischtennis
if (service === 'mytischtennis') {
console.log('[externalServiceService] - Getting myTischtennis user profile...');
const profileResult = await myTischtennisClient.getUserProfile(loginResult.cookie);
console.log('[externalServiceService] - Profile result:', {
success: profileResult.success,
clubId: profileResult.clubId,
clubName: profileResult.clubName,
fedNickname: profileResult.fedNickname
});
if (profileResult.success) {
account.clubId = profileResult.clubId;
account.clubName = profileResult.clubName;
account.fedNickname = profileResult.fedNickname;
console.log('[externalServiceService] - Updated account with club data');
} else {
console.error('[externalServiceService] - Failed to get profile:', profileResult.error);
}
}
await account.save();
return {
success: true,
accessToken: loginResult.accessToken,
refreshToken: loginResult.refreshToken,
expiresAt: loginResult.expiresAt,
user: loginResult.user,
clubId: account.clubId,
clubName: account.clubName
};
} else {
await account.save(); // Save lastLoginAttempt
throw new HttpError(401, loginResult.error || 'myTischtennis-Login fehlgeschlagen');
}
}
/**
* Check if account is configured and ready
*/
async checkAccountStatus(userId, service = 'mytischtennis') {
const account = await ExternalServiceAccount.findOne({ where: { userId, service } });
return {
exists: !!account,
hasEmail: !!account?.email,
hasPassword: !!(account?.savePassword && account?.encryptedPassword),
hasValidSession: !!account?.accessToken && account?.expiresAt > Date.now() / 1000,
needsConfiguration: !account || !account.email,
needsPassword: !!account && (!account.savePassword || !account.encryptedPassword)
};
}
/**
* Get stored session for user (for authenticated API requests)
*/
async getSession(userId, service = 'mytischtennis') {
const account = await ExternalServiceAccount.findOne({ where: { userId, service } });
if (!account) {
throw new HttpError(404, `Kein ${service}-Account verknüpft`);
}
// Check if session is valid
if (service === 'hettv') {
// HeTTV nutzt Cookie-basierte Session, kein expiresAt verfügbar
if (!account.cookie) {
throw new HttpError(401, 'Session abgelaufen. Bitte erneut einloggen.');
}
} else {
if (!account.accessToken || !account.expiresAt || account.expiresAt < Date.now() / 1000) {
throw new HttpError(401, 'Session abgelaufen. Bitte erneut einloggen.');
}
}
return {
accessToken: account.accessToken,
refreshToken: account.refreshToken,
cookie: account.cookie,
expiresAt: account.expiresAt,
userData: account.userData
};
}
/**
* Load HeTTV main page and find download links
*/
async loadHettvMainPage(userId) {
const client = this.getClientForService('hettv');
let session = await this.getSession(userId, 'hettv');
const finalUrl = session.userData?.finalUrl || null;
let result = await client.getMainPageWithDownloads(session.cookie, finalUrl);
// Wenn Session abgelaufen: versuche automatischen Re-Login mit gespeichertem Passwort
if (!result.success && result.status === 401) {
const account = await ExternalServiceAccount.findOne({ where: { userId, service: 'hettv' } });
if (account && account.savePassword && account.encryptedPassword) {
const password = account.getPassword();
const login = await client.login(account.email, password);
if (login.success) {
// Session aktualisieren
account.cookie = login.cookie;
account.userData = login.user;
await account.save();
// Erneut versuchen
result = await client.getMainPageWithDownloads(login.cookie, login.user?.finalUrl || null);
}
}
}
return result;
}
/**
* Load specific HeTTV download page
*/
async loadHettvDownloadPage(userId, downloadUrl) {
const session = await this.getSession(userId, 'hettv');
const client = this.getClientForService('hettv');
const finalUrl = session.userData?.finalUrl || null;
return await client.loadDownloadPage(downloadUrl, session.cookie, finalUrl);
}
}
export default new ExternalServiceService();

View File

@@ -0,0 +1,94 @@
import League from '../models/League.js';
import Season from '../models/Season.js';
import SeasonService from './seasonService.js';
import { devLog } from '../utils/logger.js';
class LeagueService {
static async getAllLeaguesByClub(clubId, seasonId = null) {
try {
// Wenn keine Saison angegeben, verwende die aktuelle
if (!seasonId) {
const currentSeason = await SeasonService.getOrCreateCurrentSeason();
seasonId = currentSeason.id;
}
const leagues = await League.findAll({
where: { clubId, seasonId },
include: [
{
model: Season,
as: 'season',
attributes: ['id', 'season']
}
],
order: [['name', 'ASC']]
});
return leagues;
} catch (error) {
console.error('[LeagueService.getAllLeaguesByClub] - Error:', error);
throw error;
}
}
static async getLeagueById(leagueId) {
try {
const league = await League.findByPk(leagueId, {
include: [
{
model: Season,
as: 'season',
attributes: ['id', 'season']
}
]
});
return league;
} catch (error) {
console.error('[LeagueService.getLeagueById] - Error:', error);
throw error;
}
}
static async createLeague(leagueData) {
try {
// Wenn keine Saison angegeben, verwende die aktuelle
if (!leagueData.seasonId) {
const currentSeason = await SeasonService.getOrCreateCurrentSeason();
leagueData.seasonId = currentSeason.id;
}
const league = await League.create(leagueData);
return league;
} catch (error) {
console.error('[LeagueService.createLeague] - Error:', error);
throw error;
}
}
static async updateLeague(leagueId, updateData) {
try {
const [updatedRowsCount] = await League.update(updateData, {
where: { id: leagueId }
});
return updatedRowsCount > 0;
} catch (error) {
console.error('[LeagueService.updateLeague] - Error:', error);
throw error;
}
}
static async deleteLeague(leagueId) {
try {
const deletedRowsCount = await League.destroy({
where: { id: leagueId }
});
return deletedRowsCount > 0;
} catch (error) {
console.error('[LeagueService.deleteLeague] - Error:', error);
throw error;
}
}
}
export default LeagueService;

View File

@@ -7,6 +7,7 @@ import Season from '../models/Season.js';
import Location from '../models/Location.js';
import League from '../models/League.js';
import Team from '../models/Team.js';
import SeasonService from './seasonService.js';
import { checkAccess } from '../utils/userUtils.js';
import { Op } from 'sequelize';
@@ -22,8 +23,7 @@ class MatchService {
seasonStartYear = currentYear - 1;
}
const seasonEndYear = seasonStartYear + 1;
const seasonEndYearString = seasonEndYear.toString().slice(-2);
return `${seasonStartYear}/${seasonEndYearString}`;
return `${seasonStartYear}/${seasonEndYear}`;
}
async importCSV(userToken, clubId, filePath) {
@@ -58,7 +58,6 @@ class MatchService {
},
});
matches.push({
seasonId: season.id,
date: parsedDate,
time: row['Termin'].split(' ')[1],
homeTeamId: homeTeamId,
@@ -72,7 +71,14 @@ class MatchService {
if (seasonString) {
season = await Season.findOne({ where: { season: seasonString } });
if (season) {
await Match.destroy({ where: { clubId, seasonId: season.id } });
// Lösche alle Matches für Ligen dieser Saison
const leagues = await League.findAll({
where: { seasonId: season.id, clubId }
});
const leagueIds = leagues.map(league => league.id);
if (leagueIds.length > 0) {
await Match.destroy({ where: { clubId, leagueId: leagueIds } });
}
}
}
const result = await Match.bulkCreate(matches);
@@ -99,33 +105,28 @@ class MatchService {
}
async getLeaguesForCurrentSeason(userToken, clubId) {
async getLeaguesForCurrentSeason(userToken, clubId, seasonId = null) {
await checkAccess(userToken, clubId);
const seasonString = this.generateSeasonString();
const season = await Season.findOne({
where: {
season: {
[Op.like]: `%${seasonString}%`
}
// Verwende SeasonService für korrekte Saison-Verwaltung
let season;
if (!seasonId) {
season = await SeasonService.getOrCreateCurrentSeason();
} else {
season = await SeasonService.getSeasonById(seasonId);
if (!season) {
throw new Error('Season not found');
}
});
if (!season) {
await Season.create({ season: seasonString });
throw new Error('Season not found');
}
try {
const leagues = await League.findAll({
include: [{
model: Match,
as: 'leagueMatches',
where: {
seasonId: season.id,
clubId: clubId
},
attributes: [],
}],
where: {
clubId: clubId,
seasonId: season.id
},
attributes: ['id', 'name'],
group: ['League.id'],
order: [['name', 'ASC']]
});
return leagues;
} catch (error) {
@@ -134,48 +135,69 @@ class MatchService {
}
}
async getMatchesForLeagues(userToken, clubId) {
async getMatchesForLeagues(userToken, clubId, seasonId = null) {
await checkAccess(userToken, clubId);
const seasonString = this.generateSeasonString();
const season = await Season.findOne({
where: {
season: {
[Op.like]: `%${seasonString}%`
}
// Wenn keine Saison angegeben, verwende die aktuelle
let season;
if (!seasonId) {
season = await SeasonService.getOrCreateCurrentSeason();
} else {
season = await SeasonService.getSeasonById(seasonId);
if (!season) {
throw new Error('Season not found');
}
});
if (!season) {
throw new Error('Season not found');
}
const matches = await Match.findAll({
where: {
seasonId: season.id,
clubId: clubId,
},
include: [
{
model: League,
as: 'leagueDetails',
attributes: ['name'],
},
{
model: Team,
as: 'homeTeam', // Assuming your associations are set correctly
attributes: ['name'],
},
{
model: Team,
as: 'guestTeam',
attributes: ['name'],
},
{
model: Location,
as: 'location',
attributes: ['name', 'address', 'city', 'zip'],
}
]
}
});
return matches;
// Filtere Matches nach Liga-Saison und lade Daten manuell
const enrichedMatches = [];
for (const match of matches) {
// Lade Liga-Daten
const league = await League.findByPk(match.leagueId, { attributes: ['name', 'seasonId'] });
if (!league || league.seasonId !== season.id) {
continue; // Skip matches from other seasons
}
const enrichedMatch = {
id: match.id,
date: match.date,
time: match.time,
homeTeamId: match.homeTeamId,
guestTeamId: match.guestTeamId,
locationId: match.locationId,
leagueId: match.leagueId,
code: match.code,
homePin: match.homePin,
guestPin: match.guestPin,
homeTeam: { name: 'Unbekannt' },
guestTeam: { name: 'Unbekannt' },
location: { name: 'Unbekannt', address: '', city: '', zip: '' },
leagueDetails: { name: league.name }
};
if (match.homeTeamId) {
const homeTeam = await Team.findByPk(match.homeTeamId, { attributes: ['name'] });
if (homeTeam) enrichedMatch.homeTeam = homeTeam;
}
if (match.guestTeamId) {
const guestTeam = await Team.findByPk(match.guestTeamId, { attributes: ['name'] });
if (guestTeam) enrichedMatch.guestTeam = guestTeam;
}
if (match.locationId) {
const location = await Location.findByPk(match.locationId, {
attributes: ['name', 'address', 'city', 'zip']
});
if (location) enrichedMatch.location = location;
}
enrichedMatches.push(enrichedMatch);
}
return enrichedMatches;
}
async getMatchesForLeague(userToken, clubId, leagueId) {
@@ -193,34 +215,53 @@ class MatchService {
}
const matches = await Match.findAll({
where: {
seasonId: season.id,
clubId: clubId,
leagueId: leagueId
},
include: [
{
model: League,
as: 'leagueDetails',
attributes: ['name'],
},
{
model: Team,
as: 'homeTeam',
attributes: ['name'],
},
{
model: Team,
as: 'guestTeam',
attributes: ['name'],
},
{
model: Location,
as: 'location',
attributes: ['name', 'address', 'city', 'zip'],
}
]
}
});
return matches;
// Lade Team- und Location-Daten manuell
const enrichedMatches = [];
for (const match of matches) {
const enrichedMatch = {
id: match.id,
date: match.date,
time: match.time,
homeTeamId: match.homeTeamId,
guestTeamId: match.guestTeamId,
locationId: match.locationId,
leagueId: match.leagueId,
code: match.code,
homePin: match.homePin,
guestPin: match.guestPin,
homeTeam: { name: 'Unbekannt' },
guestTeam: { name: 'Unbekannt' },
location: { name: 'Unbekannt', address: '', city: '', zip: '' },
leagueDetails: { name: 'Unbekannt' }
};
if (match.homeTeamId) {
const homeTeam = await Team.findByPk(match.homeTeamId, { attributes: ['name'] });
if (homeTeam) enrichedMatch.homeTeam = homeTeam;
}
if (match.guestTeamId) {
const guestTeam = await Team.findByPk(match.guestTeamId, { attributes: ['name'] });
if (guestTeam) enrichedMatch.guestTeam = guestTeam;
}
if (match.locationId) {
const location = await Location.findByPk(match.locationId, {
attributes: ['name', 'address', 'city', 'zip']
});
if (location) enrichedMatch.location = location;
}
if (match.leagueId) {
const league = await League.findByPk(match.leagueId, { attributes: ['name'] });
if (league) enrichedMatch.leagueDetails = league;
}
enrichedMatches.push(enrichedMatch);
}
return enrichedMatches;
}
}

View File

@@ -9,11 +9,8 @@ import sharp from 'sharp';
import { devLog } from '../utils/logger.js';
class MemberService {
async getApprovalRequests(userToken, clubId) {
devLog('[MemberService::getApprovalRequest] - Check user access');
await checkAccess(userToken, clubId);
devLog('[MemberService::getApprovalRequest] - Load user');
const user = await getUserByToken(userToken);
devLog('[MemberService::getApprovalRequest] - Load userclub');
return await UserClub.findAll({
where: {
clubId: clubId,
@@ -24,9 +21,7 @@ class MemberService {
}
async getClubMembers(userToken, clubId, showAll) {
devLog('[getClubMembers] - Check access');
await checkAccess(userToken, clubId);
devLog('[getClubMembers] - Find members');
const where = {
clubId: clubId
};
@@ -45,7 +40,6 @@ class MemberService {
});
})
.then(membersWithImageStatus => {
devLog('[getClubMembers] - return members');
return membersWithImageStatus;
})
.catch(error => {
@@ -57,15 +51,11 @@ class MemberService {
async setClubMember(userToken, clubId, memberId, firstName, lastName, street, city, birthdate, phone, email, active = true, testMembership = false,
picsInInternetAllowed = false, gender = 'unknown', ttr = null, qttr = null) {
try {
devLog('[setClubMembers] - Check access');
await checkAccess(userToken, clubId);
devLog('[setClubMembers] - set default member');
let member = null;
devLog('[setClubMembers] - load member if possible');
if (memberId) {
member = await Member.findOne({ where: { id: memberId } });
}
devLog('[setClubMembers] - set member');
if (member) {
member.firstName = firstName;
member.lastName = lastName;
@@ -99,7 +89,6 @@ class MemberService {
qttr: qttr,
});
}
devLog('[setClubMembers] - return response');
return {
status: 200,
response: { result: "success" },
@@ -153,34 +142,18 @@ class MemberService {
}
async updateRatingsFromMyTischtennis(userToken, clubId) {
devLog('[updateRatingsFromMyTischtennis] - Check access');
await checkAccess(userToken, clubId);
const user = await getUserByToken(userToken);
devLog('[updateRatingsFromMyTischtennis] - User:', user.id);
const externalServiceService = (await import('./externalServiceService.js')).default;
const myTischtennisService = (await import('./myTischtennisService.js')).default;
const myTischtennisClient = (await import('../clients/myTischtennisClient.js')).default;
try {
// 1. myTischtennis-Session abrufen
devLog('[updateRatingsFromMyTischtennis] - Get session for user', user.id);
const session = await externalServiceService.getSession(user.id, 'mytischtennis');
devLog('[updateRatingsFromMyTischtennis] - Session retrieved:', {
hasAccessToken: !!session.accessToken,
hasCookie: !!session.cookie,
expiresAt: session.expiresAt
});
const session = await myTischtennisService.getSession(user.id);
const account = await externalServiceService.getAccount(user.id, 'mytischtennis');
devLog('[updateRatingsFromMyTischtennis] - Account data:', {
id: account?.id,
email: account?.email,
clubId: account?.clubId,
clubName: account?.clubName,
fedNickname: account?.fedNickname,
hasSession: !!(account?.accessToken)
});
const account = await myTischtennisService.getAccount(user.id);
if (!account) {
console.error('[updateRatingsFromMyTischtennis] - No account found!');
@@ -217,24 +190,12 @@ class MemberService {
}
// 2. Rangliste vom Verein abrufen
devLog('[updateRatingsFromMyTischtennis] - Get club rankings', {
clubId: account.clubId,
fedNickname: account.fedNickname,
hasCookie: !!session.cookie
});
const rankings = await myTischtennisClient.getClubRankings(
session.cookie,
account.clubId,
account.fedNickname
);
devLog('[updateRatingsFromMyTischtennis] - Rankings result:', {
success: rankings.success,
entriesCount: rankings.entries?.length || 0,
error: rankings.error
});
if (!rankings.success) {
return {
status: 500,
@@ -252,9 +213,7 @@ class MemberService {
}
// 3. Alle Mitglieder des Clubs laden
devLog('[updateRatingsFromMyTischtennis] - Load club members for clubId:', clubId);
const members = await Member.findAll({ where: { clubId } });
devLog('[updateRatingsFromMyTischtennis] - Found members:', members.length);
let updated = 0;
const errors = [];
@@ -285,7 +244,6 @@ class MemberService {
oldTtr: oldTtr,
newTtr: rankingEntry.fedRank
});
devLog(`[updateRatingsFromMyTischtennis] - Updated ${firstName} ${lastName}: TTR ${oldTtr}${rankingEntry.fedRank}`);
} catch (error) {
console.error(`[updateRatingsFromMyTischtennis] - Error updating ${firstName} ${lastName}:`, error);
errors.push({
@@ -295,11 +253,9 @@ class MemberService {
}
} else {
notFound.push(`${firstName} ${lastName}`);
devLog(`[updateRatingsFromMyTischtennis] - Not found in rankings: ${firstName} ${lastName}`);
}
}
devLog('[updateRatingsFromMyTischtennis] - Update complete');
devLog(`Updated: ${updated}, Not found: ${notFound.length}, Errors: ${errors.length}`);
let message = `${updated} Mitglied(er) aktualisiert.`;

View File

@@ -0,0 +1,241 @@
import MyTischtennis from '../models/MyTischtennis.js';
import User from '../models/User.js';
import myTischtennisClient from '../clients/myTischtennisClient.js';
import HttpError from '../exceptions/HttpError.js';
import { devLog } from '../utils/logger.js';
class MyTischtennisService {
/**
* Get myTischtennis account for user
*/
async getAccount(userId) {
const account = await MyTischtennis.findOne({
where: { userId },
attributes: ['id', 'email', 'savePassword', 'lastLoginAttempt', 'lastLoginSuccess', 'expiresAt', 'userData', 'clubId', 'clubName', 'fedNickname', 'createdAt', 'updatedAt']
});
return account;
}
/**
* Create or update myTischtennis account
*/
async upsertAccount(userId, email, password, savePassword, userPassword) {
// Verify user's app password
const user = await User.findByPk(userId);
if (!user) {
throw new HttpError(404, 'Benutzer nicht gefunden');
}
let loginResult = null;
// Wenn ein Passwort gesetzt/geändert wird, App-Passwort verifizieren
if (password) {
const isValidPassword = await user.validatePassword(userPassword);
if (!isValidPassword) {
throw new HttpError(401, 'Ungültiges Passwort');
}
// Login-Versuch bei myTischtennis
loginResult = await myTischtennisClient.login(email, password);
if (!loginResult.success) {
throw new HttpError(401, loginResult.error || 'myTischtennis-Login fehlgeschlagen. Bitte überprüfen Sie Ihre Zugangsdaten.');
}
}
// Find or create account
let account = await MyTischtennis.findOne({ where: { userId } });
const now = new Date();
if (account) {
// Update existing
account.email = email;
account.savePassword = savePassword;
if (password && savePassword) {
account.setPassword(password);
} else if (!savePassword) {
account.encryptedPassword = null;
}
if (loginResult && loginResult.success) {
account.lastLoginAttempt = now;
account.lastLoginSuccess = now;
account.accessToken = loginResult.accessToken;
account.refreshToken = loginResult.refreshToken;
account.expiresAt = loginResult.expiresAt;
account.cookie = loginResult.cookie;
account.userData = loginResult.user;
// Hole Club-ID und Federation
const profileResult = await myTischtennisClient.getUserProfile(loginResult.cookie);
if (profileResult.success) {
account.clubId = profileResult.clubId;
account.clubName = profileResult.clubName;
account.fedNickname = profileResult.fedNickname;
} else {
console.error('[myTischtennisService] - Failed to get profile:', profileResult.error);
}
} else if (password) {
account.lastLoginAttempt = now;
}
await account.save();
} else {
// Create new
const accountData = {
userId,
email,
savePassword,
lastLoginAttempt: password ? now : null,
lastLoginSuccess: loginResult?.success ? now : null
};
if (loginResult && loginResult.success) {
accountData.accessToken = loginResult.accessToken;
accountData.refreshToken = loginResult.refreshToken;
accountData.expiresAt = loginResult.expiresAt;
accountData.cookie = loginResult.cookie;
accountData.userData = loginResult.user;
// Hole Club-ID
const profileResult = await myTischtennisClient.getUserProfile(loginResult.cookie);
if (profileResult.success) {
accountData.clubId = profileResult.clubId;
accountData.clubName = profileResult.clubName;
}
}
account = await MyTischtennis.create(accountData);
if (password && savePassword) {
account.setPassword(password);
await account.save();
}
}
return {
id: account.id,
email: account.email,
savePassword: account.savePassword,
lastLoginAttempt: account.lastLoginAttempt,
lastLoginSuccess: account.lastLoginSuccess,
expiresAt: account.expiresAt
};
}
/**
* Delete myTischtennis account
*/
async deleteAccount(userId) {
const deleted = await MyTischtennis.destroy({
where: { userId }
});
return deleted > 0;
}
/**
* Verify login with stored or provided credentials
*/
async verifyLogin(userId, providedPassword = null) {
const account = await MyTischtennis.findOne({ where: { userId } });
if (!account) {
throw new HttpError(404, 'Kein myTischtennis-Account verknüpft');
}
let password = providedPassword;
// Wenn kein Passwort übergeben wurde, versuche gespeichertes Passwort zu verwenden
if (!password) {
if (!account.savePassword || !account.encryptedPassword) {
throw new HttpError(400, 'Kein Passwort gespeichert. Bitte geben Sie Ihr Passwort ein.');
}
password = account.getPassword();
}
// Login-Versuch
const now = new Date();
account.lastLoginAttempt = now;
const loginResult = await myTischtennisClient.login(account.email, password);
if (loginResult.success) {
account.lastLoginSuccess = now;
account.accessToken = loginResult.accessToken;
account.refreshToken = loginResult.refreshToken;
account.expiresAt = loginResult.expiresAt;
account.cookie = loginResult.cookie;
account.userData = loginResult.user;
// Hole Club-ID und Federation
const profileResult = await myTischtennisClient.getUserProfile(loginResult.cookie);
if (profileResult.success) {
account.clubId = profileResult.clubId;
account.clubName = profileResult.clubName;
account.fedNickname = profileResult.fedNickname;
} else {
console.error('[myTischtennisService] - Failed to get profile:', profileResult.error);
}
await account.save();
return {
success: true,
accessToken: loginResult.accessToken,
refreshToken: loginResult.refreshToken,
expiresAt: loginResult.expiresAt,
user: loginResult.user,
clubId: account.clubId,
clubName: account.clubName
};
} else {
await account.save(); // Save lastLoginAttempt
throw new HttpError(401, loginResult.error || 'myTischtennis-Login fehlgeschlagen');
}
}
/**
* Check if account is configured and ready
*/
async checkAccountStatus(userId) {
const account = await MyTischtennis.findOne({ where: { userId } });
return {
exists: !!account,
hasEmail: !!account?.email,
hasPassword: !!(account?.savePassword && account?.encryptedPassword),
hasValidSession: !!account?.accessToken && account?.expiresAt > Date.now() / 1000,
needsConfiguration: !account || !account.email,
needsPassword: !!account && (!account.savePassword || !account.encryptedPassword)
};
}
/**
* Get stored session for user (for authenticated API requests)
*/
async getSession(userId) {
const account = await MyTischtennis.findOne({ where: { userId } });
if (!account) {
throw new HttpError(404, 'Kein myTischtennis-Account verknüpft');
}
// Check if session is valid
if (!account.accessToken || !account.expiresAt || account.expiresAt < Date.now() / 1000) {
throw new HttpError(401, 'Session abgelaufen. Bitte erneut einloggen.');
}
return {
accessToken: account.accessToken,
refreshToken: account.refreshToken,
cookie: account.cookie,
expiresAt: account.expiresAt,
userData: account.userData
};
}
}
export default new MyTischtennisService();

View File

@@ -0,0 +1,638 @@
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const pdfParse = require('pdf-parse/lib/pdf-parse.js');
import { Op } from 'sequelize';
import Match from '../models/Match.js';
import Team from '../models/Team.js';
import ClubTeam from '../models/ClubTeam.js';
import League from '../models/League.js';
import Location from '../models/Location.js';
import { devLog } from '../utils/logger.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
class PDFParserService {
/**
* Parst eine PDF-Datei und extrahiert Spiel-Daten
* @param {string} filePath - Pfad zur PDF-Datei
* @param {number} clubId - ID des Vereins
* @returns {Promise<Object>} Geparste Spiel-Daten
*/
static async parsePDF(filePath, clubId) {
try {
if (!fs.existsSync(filePath)) {
throw new Error('PDF-Datei nicht gefunden');
}
// Bestimme Dateityp basierend auf Dateiendung
const fileExtension = path.extname(filePath).toLowerCase();
let fileContent;
if (fileExtension === '.pdf') {
// Echte PDF-Parsing
const pdfBuffer = fs.readFileSync(filePath);
const pdfData = await pdfParse(pdfBuffer);
fileContent = pdfData.text;
} else {
// Fallback für TXT-Dateien (für Tests)
fileContent = fs.readFileSync(filePath, 'utf8');
}
// Parse den Text nach Spiel-Daten
const parsedData = this.extractMatchData(fileContent, clubId);
return parsedData;
} catch (error) {
console.error('[PDFParserService.parsePDF] - Error:', error);
throw error;
}
}
/**
* Extrahiert Spiel-Daten aus dem PDF-Text
* @param {string} text - Der extrahierte Text aus der PDF
* @param {number} clubId - ID des Vereins
* @returns {Object} Geparste Daten mit Matches und Metadaten
*/
static extractMatchData(text, clubId) {
const matches = [];
const errors = [];
const metadata = {
totalLines: 0,
parsedMatches: 0,
errors: 0
};
try {
// Teile Text in Zeilen auf
const lines = text.split('\n').map(line => line.trim()).filter(line => line.length > 0);
metadata.totalLines = lines.length;
// Verschiedene Parsing-Strategien je nach PDF-Format
const strategies = [
{ name: 'Standard Format', fn: this.parseStandardFormat },
{ name: 'Table Format', fn: this.parseTableFormat },
{ name: 'List Format', fn: this.parseListFormat }
];
for (const strategy of strategies) {
try {
const result = strategy.fn(lines, clubId);
if (result.matches.length > 0) {
matches.push(...result.matches);
metadata.parsedMatches += result.matches.length;
break; // Erste erfolgreiche Strategie verwenden
}
} catch (strategyError) {
errors.push(`Strategy ${strategy.name} failed: ${strategyError.message}`);
}
}
metadata.errors = errors.length;
return {
matches,
errors,
metadata,
rawText: text.substring(0, 1000), // Erste 1000 Zeichen für Debugging
allLines: lines, // Alle Zeilen für Debugging
debugInfo: {
totalTextLength: text.length,
totalLines: lines.length,
firstFewLines: lines.slice(0, 10),
lastFewLines: lines.slice(-5)
}
};
} catch (error) {
console.error('[PDFParserService.extractMatchData] - Error:', error);
throw error;
}
}
/**
* Standard-Format Parser (Datum, Zeit, Heimteam, Gastteam, Code, Pins)
* @param {Array} lines - Textzeilen
* @param {number} clubId - ID des Vereins
* @returns {Object} Geparste Matches
*/
static parseStandardFormat(lines, clubId) {
const matches = [];
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// Suche nach Datum-Pattern (dd.mm.yyyy oder dd/mm/yyyy)
const dateMatch = line.match(/(\d{1,2})[./](\d{1,2})[./](\d{4})/);
if (dateMatch) {
// Debug: Zeige die gesamte Zeile mit sichtbaren Whitespaces
const debugLine = line.replace(/\s/g, (match) => {
if (match === ' ') return '·'; // Mittelpunkt für normales Leerzeichen
if (match === '\t') return '→'; // Pfeil für Tab
if (match === '\n') return '↵'; // Enter-Zeichen
if (match === '\r') return '⏎'; // Carriage Return
return `[${match.charCodeAt(0)}]`; // Zeichencode für andere Whitespaces
});
try {
const [, day, month, year] = dateMatch;
const date = new Date(`${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`);
// Suche nach Zeit-Pattern direkt nach dem Datum (hh:mm) - Format: Wt.dd.mm.yyyyhh:MM
const timeMatch = line.match(/(\d{1,2})[./](\d{1,2})[./](\d{4})(\d{1,2}):(\d{2})/);
let time = null;
if (timeMatch) {
time = `${timeMatch[4].padStart(2, '0')}:${timeMatch[5]}`;
}
// Entferne Datum und Zeit vom Anfang der Zeile
const cleanLine = line.replace(/^[A-Za-z]{2}\.(\d{1,2})[./](\d{1,2})[./](\d{4})(\d{1,2}):(\d{2})\s*/, '');
// Entferne Nummerierung am Anfang (z.B. "(1)")
const cleanLine2 = cleanLine.replace(/^\(\d+\)/, '');
// Entferne alle Inhalte in Klammern (z.B. "(J11)")
const cleanLine3 = cleanLine2.replace(/\([^)]*\)/g, '');
// Suche nach Code (12 Zeichen) oder PIN (4 Ziffern) am Ende
const codeMatch = cleanLine3.match(/([A-Z0-9]{12})$/);
const pinMatch = cleanLine3.match(/(\d{4})$/);
let code = null;
let homePin = null;
let guestPin = null;
let teamsPart = cleanLine3;
if (codeMatch) {
// Code gefunden (12 Zeichen)
code = codeMatch[1];
teamsPart = cleanLine3.substring(0, cleanLine3.length - code.length).trim();
} else if (pinMatch) {
// PIN gefunden (4 Ziffern)
const pin = pinMatch[1];
teamsPart = cleanLine3.substring(0, cleanLine3.length - pin.length).trim();
// PIN gehört zu dem Team, das direkt vor der PIN steht
// Analysiere die Position der PIN in der ursprünglichen Zeile
const pinIndex = cleanLine3.lastIndexOf(pin);
const teamsPartIndex = cleanLine3.indexOf(teamsPart);
// Wenn PIN direkt nach dem Teams-Part steht, gehört sie zur Heimmannschaft
// Wenn PIN zwischen den Teams steht, gehört sie zur Gastmannschaft
if (pinIndex === teamsPartIndex + teamsPart.length) {
// PIN steht direkt nach den Teams -> Heimmannschaft
homePin = pin;
} else {
// PIN steht zwischen den Teams -> Gastmannschaft
guestPin = pin;
}
}
if (code || pinMatch) {
// Debug: Zeige Whitespaces als lesbare Zeichen
const debugTeamsPart = teamsPart.replace(/\s/g, (match) => {
if (match === ' ') return '·'; // Mittelpunkt für normales Leerzeichen
if (match === '\t') return '→'; // Pfeil für Tab
return `[${match.charCodeAt(0)}]`; // Zeichencode für andere Whitespaces
});
// Neue Strategie: Teile die Zeile durch mehrere Leerzeichen (wie in der Tabelle)
// Die Struktur ist: Heimmannschaft Gastmannschaft Code
const parts = teamsPart.split(/\s{2,}/); // Mindestens 2 Leerzeichen als Trenner
let homeTeamName = '';
let guestTeamName = '';
if (parts.length >= 2) {
homeTeamName = parts[0].trim();
guestTeamName = parts[1].trim();
// Entferne noch verbleibende Klammern aus den Team-Namen
homeTeamName = homeTeamName.replace(/\([^)]*\)/g, '').trim();
guestTeamName = guestTeamName.replace(/\([^)]*\)/g, '').trim();
// Erkenne römische Ziffern am Ende der Team-Namen
// Römische Ziffern: I, II, III, IV, V, VI, VII, VIII, IX, X, XI, XII, etc.
const romanNumeralPattern = /\s+(I{1,3}|IV|V|VI{0,3}|IX|X|XI{0,2})$/;
// Prüfe Heimteam auf römische Ziffern
const homeRomanMatch = homeTeamName.match(romanNumeralPattern);
if (homeRomanMatch) {
const romanNumeral = homeRomanMatch[1];
const baseName = homeTeamName.replace(romanNumeralPattern, '').trim();
homeTeamName = `${baseName} ${romanNumeral}`;
}
// Prüfe Gastteam auf römische Ziffern
const guestRomanMatch = guestTeamName.match(romanNumeralPattern);
if (guestRomanMatch) {
const romanNumeral = guestRomanMatch[1];
const baseName = guestTeamName.replace(romanNumeralPattern, '').trim();
guestTeamName = `${baseName} ${romanNumeral}`;
}
} else {
// Fallback: Versuche mit einzelnen Leerzeichen zu trennen
// Strategie 1: Suche nach "Harheimer TC" als Heimteam
if (teamsPart.includes('Harheimer TC')) {
const harheimerIndex = teamsPart.indexOf('Harheimer TC');
homeTeamName = 'Harheimer TC';
guestTeamName = teamsPart.substring(harheimerIndex + 'Harheimer TC'.length).trim();
// Entferne Klammern aus Gastteam
guestTeamName = guestTeamName.replace(/\([^)]*\)/g, '').trim();
} else {
// Strategie 2: Suche nach Großbuchstaben am Anfang des zweiten Teams
const teamSplitMatch = teamsPart.match(/^([A-Za-z0-9\s\-\.]+?)\s+([A-Z][A-Za-z0-9\s\-\.]+)$/);
if (teamSplitMatch) {
homeTeamName = teamSplitMatch[1].trim();
guestTeamName = teamSplitMatch[2].trim();
} else {
continue;
}
}
}
if (homeTeamName && guestTeamName) {
let debugInfo;
if (code) {
debugInfo = `code: "${code}"`;
} else if (homePin && guestPin) {
debugInfo = `homePin: "${homePin}", guestPin: "${guestPin}"`;
} else if (homePin) {
debugInfo = `homePin: "${homePin}"`;
} else if (guestPin) {
debugInfo = `guestPin: "${guestPin}"`;
}
matches.push({
date: date,
time: time,
homeTeamName: homeTeamName,
guestTeamName: guestTeamName,
code: code,
homePin: homePin,
guestPin: guestPin,
clubId: clubId,
rawLine: line
});
}
} else {
}
} catch (parseError) {
}
}
}
return { matches };
}
/**
* Tabellen-Format Parser
* @param {Array} lines - Textzeilen
* @param {number} clubId - ID des Vereins
* @returns {Object} Geparste Matches
*/
static parseTableFormat(lines, clubId) {
const matches = [];
// Suche nach Tabellen-Header
let headerIndex = -1;
for (let i = 0; i < lines.length; i++) {
if (lines[i].toLowerCase().includes('datum') &&
lines[i].toLowerCase().includes('zeit') &&
lines[i].toLowerCase().includes('heim') &&
lines[i].toLowerCase().includes('gast')) {
headerIndex = i;
break;
}
}
if (headerIndex >= 0) {
// Parse Tabellen-Zeilen
for (let i = headerIndex + 1; i < lines.length; i++) {
const line = lines[i];
const columns = line.split(/\s{2,}|\t/); // Split bei mehreren Leerzeichen oder Tabs
if (columns.length >= 4) {
try {
const dateStr = columns[0];
const timeStr = columns[1];
const homeTeam = columns[2];
const guestTeam = columns[3];
const code = columns[4] || null;
const homePin = columns[5] || null;
const guestPin = columns[6] || null;
// Parse Datum
const dateMatch = dateStr.match(/(\d{1,2})[./](\d{1,2})[./](\d{4})/);
if (dateMatch) {
const [, day, month, year] = dateMatch;
const date = new Date(`${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`);
matches.push({
date: date,
time: timeStr || null,
homeTeamName: homeTeam.trim(),
guestTeamName: guestTeam.trim(),
code: code ? code.trim() : null,
homePin: homePin ? homePin.trim() : null,
guestPin: guestPin ? guestPin.trim() : null,
clubId: clubId,
rawLine: line
});
}
} catch (parseError) {
}
}
}
}
return { matches };
}
/**
* Listen-Format Parser
* @param {Array} lines - Textzeilen
* @param {number} clubId - ID des Vereins
* @returns {Object} Geparste Matches
*/
static parseListFormat(lines, clubId) {
const matches = [];
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// Suche nach Nummerierten Listen (1., 2., etc.)
const listMatch = line.match(/^\d+\.\s*(.+)/);
if (listMatch) {
const content = listMatch[1];
// Versuche verschiedene Formate zu parsen
const patterns = [
/(\d{1,2}[./]\d{1,2}[./]\d{4})\s+(\d{1,2}:\d{2})?\s+(.+?)\s+vs?\s+(.+?)(?:\s+code[:\s]*([A-Za-z0-9]+))?(?:\s+home[:\s]*pin[:\s]*([A-Za-z0-9]+))?(?:\s+guest[:\s]*pin[:\s]*([A-Za-z0-9]+))?/i,
/(\d{1,2}[./]\d{1,2}[./]\d{4})\s+(\d{1,2}:\d{2})?\s+(.+?)\s+-\s+(.+?)(?:\s+code[:\s]*([A-Za-z0-9]+))?(?:\s+heim[:\s]*pin[:\s]*([A-Za-z0-9]+))?(?:\s+gast[:\s]*pin[:\s]*([A-Za-z0-9]+))?/i
];
for (const pattern of patterns) {
const match = content.match(pattern);
if (match) {
try {
const [, dateStr, timeStr, homeTeam, guestTeam, code, homePin, guestPin] = match;
// Parse Datum
const dateMatch = dateStr.match(/(\d{1,2})[./](\d{1,2})[./](\d{4})/);
if (dateMatch) {
const [, day, month, year] = dateMatch;
const date = new Date(`${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`);
matches.push({
date: date,
time: timeStr || null,
homeTeamName: homeTeam.trim(),
guestTeamName: guestTeam.trim(),
code: code ? code.trim() : null,
homePin: homePin ? homePin.trim() : null,
guestPin: guestPin ? guestPin.trim() : null,
clubId: clubId,
rawLine: line
});
break; // Erste erfolgreiche Pattern verwenden
}
} catch (parseError) {
}
}
}
}
}
return { matches };
}
/**
* Speichert geparste Matches in der Datenbank
* @param {Array} matches - Array von Match-Objekten
* @param {number} leagueId - ID der Liga
* @returns {Promise<Object>} Ergebnis der Speicherung
*/
static async saveMatchesToDatabase(matches, leagueId) {
try {
const results = {
created: 0,
updated: 0,
errors: []
};
for (const matchData of matches) {
try {
let debugInfo;
if (matchData.code) {
debugInfo = `Code: ${matchData.code}`;
} else if (matchData.homePin && matchData.guestPin) {
debugInfo = `HomePin: ${matchData.homePin}, GuestPin: ${matchData.guestPin}`;
} else if (matchData.homePin) {
debugInfo = `HomePin: ${matchData.homePin}`;
} else if (matchData.guestPin) {
debugInfo = `GuestPin: ${matchData.guestPin}`;
}
// Lade alle Matches für das Datum und die Liga
// Konvertiere das Datum zu einem Datum ohne Zeit für den Vergleich
const dateOnly = new Date(matchData.date.getFullYear(), matchData.date.getMonth(), matchData.date.getDate());
const nextDay = new Date(dateOnly);
nextDay.setDate(nextDay.getDate() + 1);
const existingMatches = await Match.findAll({
where: {
date: {
[Op.gte]: dateOnly, // Größer oder gleich dem Datum
[Op.lt]: nextDay // Kleiner als der nächste Tag
},
leagueId: leagueId,
...(matchData.time && { time: matchData.time }) // Füge Zeit hinzu wenn vorhanden
},
include: [
{
model: Team,
as: 'homeTeam',
attributes: ['id', 'name']
},
{
model: Team,
as: 'guestTeam',
attributes: ['id', 'name']
}
]
});
const timeFilter = matchData.time ? ` and time ${matchData.time}` : '';
// Debug: Zeige alle gefundenen Matches und lade Teams manuell
for (let i = 0; i < existingMatches.length; i++) {
const match = existingMatches[i];
// Lade Teams manuell
const homeTeam = await Team.findByPk(match.homeTeamId);
const guestTeam = await Team.findByPk(match.guestTeamId);
// Füge die Teams zum Match-Objekt hinzu
match.homeTeam = homeTeam;
match.guestTeam = guestTeam;
}
// Suche nach dem passenden Match basierend auf Gastmannschaft
const matchingMatch = existingMatches.find(match => {
if (!match.guestTeam) return false;
const guestTeamName = match.guestTeam.name.toLowerCase();
const searchGuestName = matchData.guestTeamName.toLowerCase();
// Exakte Übereinstimmung oder Teilstring-Match
return guestTeamName === searchGuestName ||
guestTeamName.includes(searchGuestName) ||
searchGuestName.includes(guestTeamName);
});
if (matchingMatch) {
// Update das bestehende Match mit Code und Pins
// Erstelle Update-Objekt nur mit vorhandenen Feldern
const updateData = {};
if (matchData.code) {
updateData.code = matchData.code;
}
if (matchData.homePin) {
updateData.homePin = matchData.homePin;
}
if (matchData.guestPin) {
updateData.guestPin = matchData.guestPin;
}
await matchingMatch.update(updateData);
results.updated++;
let updateInfo;
if (matchData.code) {
updateInfo = `code: ${matchData.code}`;
} else if (matchData.homePin && matchData.guestPin) {
updateInfo = `homePin: ${matchData.homePin}, guestPin: ${matchData.guestPin}`;
} else if (matchData.homePin) {
updateInfo = `homePin: ${matchData.homePin}`;
} else if (matchData.guestPin) {
updateInfo = `guestPin: ${matchData.guestPin}`;
}
// Lade das aktualisierte Match neu, um die aktuellen Werte zu zeigen
await matchingMatch.reload();
const currentValues = [];
if (matchingMatch.code) currentValues.push(`code: ${matchingMatch.code}`);
if (matchingMatch.homePin) currentValues.push(`homePin: ${matchingMatch.homePin}`);
if (matchingMatch.guestPin) currentValues.push(`guestPin: ${matchingMatch.guestPin}`);
} else {
// Fallback: Versuche Teams direkt zu finden
const homeTeam = await Team.findOne({
where: {
name: matchData.homeTeamName,
clubId: matchData.clubId
}
});
const guestTeam = await Team.findOne({
where: {
name: matchData.guestTeamName,
clubId: matchData.clubId
}
});
// Debug: Zeige alle verfügbaren Teams für diesen Club
if (!homeTeam || !guestTeam) {
const allTeams = await Team.findAll({
where: { clubId: matchData.clubId },
attributes: ['id', 'name']
});
// Versuche Fuzzy-Matching für Team-Namen
const homeTeamFuzzy = allTeams.find(t =>
t.name.toLowerCase().includes(matchData.homeTeamName.toLowerCase()) ||
matchData.homeTeamName.toLowerCase().includes(t.name.toLowerCase())
);
const guestTeamFuzzy = allTeams.find(t =>
t.name.toLowerCase().includes(matchData.guestTeamName.toLowerCase()) ||
matchData.guestTeamName.toLowerCase().includes(t.name.toLowerCase())
);
if (homeTeamFuzzy) {
}
if (guestTeamFuzzy) {
}
}
if (!homeTeam || !guestTeam) {
let errorInfo;
if (matchData.code) {
errorInfo = `Code: ${matchData.code}`;
} else if (matchData.homePin && matchData.guestPin) {
errorInfo = `HomePin: ${matchData.homePin}, GuestPin: ${matchData.guestPin}`;
} else if (matchData.homePin) {
errorInfo = `HomePin: ${matchData.homePin}`;
} else if (matchData.guestPin) {
errorInfo = `GuestPin: ${matchData.guestPin}`;
}
results.errors.push(`Teams nicht gefunden: "${matchData.homeTeamName}" oder "${matchData.guestTeamName}" (Datum: ${matchData.date.toISOString().split('T')[0]}, Zeit: ${matchData.time}, ${errorInfo})`);
continue;
}
// Erstelle neues Match (Fallback)
await Match.create({
date: matchData.date,
time: matchData.time,
homeTeamId: homeTeam.id,
guestTeamId: guestTeam.id,
leagueId: leagueId,
clubId: matchData.clubId,
code: matchData.code,
homePin: matchData.homePin,
guestPin: matchData.guestPin,
locationId: 1 // Default Location, kann später angepasst werden
});
results.created++;
}
} catch (matchError) {
console.error('[PDFParserService.saveMatchesToDatabase] - Error:', matchError);
results.errors.push(`Fehler beim Speichern von Match: ${matchData.rawLine} - ${matchError.message}`);
}
}
return results;
} catch (error) {
console.error('[PDFParserService.saveMatchesToDatabase] - Error:', error);
throw error;
}
}
}
export default PDFParserService;

View File

@@ -8,7 +8,6 @@ import { Op } from 'sequelize';
import { devLog } from '../utils/logger.js';
class PredefinedActivityService {
async createPredefinedActivity(data) {
devLog('[PredefinedActivityService::createPredefinedActivity] - Creating predefined activity');
return await PredefinedActivity.create({
name: data.name,
code: data.code,
@@ -21,10 +20,8 @@ class PredefinedActivityService {
}
async updatePredefinedActivity(id, data) {
devLog(`[PredefinedActivityService::updatePredefinedActivity] - Updating predefined activity with id: ${id}`);
const activity = await PredefinedActivity.findByPk(id);
if (!activity) {
devLog('[PredefinedActivityService::updatePredefinedActivity] - Activity not found');
throw new Error('Predefined activity not found');
}
return await activity.update({
@@ -39,7 +36,6 @@ class PredefinedActivityService {
}
async getAllPredefinedActivities() {
devLog('[PredefinedActivityService::getAllPredefinedActivities] - Fetching all predefined activities');
return await PredefinedActivity.findAll({
order: [
[sequelize.literal('code IS NULL'), 'ASC'], // Non-null codes first
@@ -50,10 +46,8 @@ class PredefinedActivityService {
}
async getPredefinedActivityById(id) {
devLog(`[PredefinedActivityService::getPredefinedActivityById] - Fetching predefined activity with id: ${id}`);
const activity = await PredefinedActivity.findByPk(id);
if (!activity) {
devLog('[PredefinedActivityService::getPredefinedActivityById] - Activity not found');
throw new Error('Predefined activity not found');
}
return activity;
@@ -81,7 +75,6 @@ class PredefinedActivityService {
}
async mergeActivities(sourceId, targetId) {
devLog(`[PredefinedActivityService::mergeActivities] - Merge ${sourceId} -> ${targetId}`);
if (!sourceId || !targetId) throw new Error('sourceId and targetId are required');
if (Number(sourceId) === Number(targetId)) throw new Error('sourceId and targetId must differ');
@@ -121,7 +114,6 @@ class PredefinedActivityService {
}
async deduplicateActivities() {
devLog('[PredefinedActivityService::deduplicateActivities] - Start');
const all = await PredefinedActivity.findAll();
const nameToActivities = new Map();
for (const activity of all) {
@@ -143,7 +135,6 @@ class PredefinedActivityService {
mergedCount++;
}
}
devLog('[PredefinedActivityService::deduplicateActivities] - Done', { mergedCount, groupCount });
return { mergedCount, groupCount };
}
}

View File

@@ -0,0 +1,149 @@
import Season from '../models/Season.js';
import { devLog } from '../utils/logger.js';
class SeasonService {
/**
* Ermittelt die aktuelle Saison basierend auf dem aktuellen Datum
* @returns {string} Saison im Format "2023/2024"
*/
static getCurrentSeasonString() {
const now = new Date();
const currentYear = now.getFullYear();
const currentMonth = now.getMonth() + 1; // getMonth() ist 0-basiert
// Ab 1. Juli: neue Saison beginnt
if (currentMonth >= 7) {
return `${currentYear}/${currentYear + 1}`;
} else {
return `${currentYear - 1}/${currentYear}`;
}
}
/**
* Holt oder erstellt die aktuelle Saison
* @returns {Promise<Season>} Die aktuelle Saison
*/
static async getOrCreateCurrentSeason() {
try {
const currentSeasonString = this.getCurrentSeasonString();
// Versuche die aktuelle Saison zu finden
let season = await Season.findOne({
where: { season: currentSeasonString }
});
// Falls nicht vorhanden, erstelle sie
if (!season) {
season = await Season.create({
season: currentSeasonString
});
}
return season;
} catch (error) {
console.error('[SeasonService.getOrCreateCurrentSeason] - Error:', error);
throw error;
}
}
/**
* Holt alle verfügbaren Saisons
* @returns {Promise<Array<Season>>} Alle Saisons sortiert nach Name
*/
static async getAllSeasons() {
try {
const seasons = await Season.findAll({
order: [['season', 'DESC']] // Neueste zuerst
});
return seasons;
} catch (error) {
console.error('[SeasonService.getAllSeasons] - Error:', error);
throw error;
}
}
/**
* Erstellt eine neue Saison
* @param {string} seasonString - Saison im Format "2023/2024"
* @returns {Promise<Season>} Die erstellte Saison
*/
static async createSeason(seasonString) {
try {
// Prüfe ob Saison bereits existiert
const existingSeason = await Season.findOne({
where: { season: seasonString }
});
if (existingSeason) {
throw new Error('Season already exists');
}
const season = await Season.create({
season: seasonString
});
return season;
} catch (error) {
console.error('[SeasonService.createSeason] - Error:', error);
throw error;
}
}
/**
* Holt eine Saison nach ID
* @param {number} seasonId - Die Saison-ID
* @returns {Promise<Season|null>} Die Saison oder null
*/
static async getSeasonById(seasonId) {
try {
const season = await Season.findByPk(seasonId);
return season;
} catch (error) {
console.error('[SeasonService.getSeasonById] - Error:', error);
throw error;
}
}
/**
* Löscht eine Saison (nur wenn keine Teams/Ligen damit verknüpft sind)
* @param {number} seasonId - Die Saison-ID
* @returns {Promise<boolean>} True wenn gelöscht, false wenn nicht möglich
*/
static async deleteSeason(seasonId) {
try {
// Prüfe ob Saison verwendet wird
const season = await Season.findByPk(seasonId, {
include: [
{ association: 'teams' },
{ association: 'leagues' }
]
});
if (!season) {
return false;
}
// Prüfe ob Saison verwendet wird
if (season.teams && season.teams.length > 0) {
throw new Error('Season is used by teams');
}
if (season.leagues && season.leagues.length > 0) {
throw new Error('Season is used by leagues');
}
await Season.destroy({
where: { id: seasonId }
});
return true;
} catch (error) {
console.error('[SeasonService.deleteSeason] - Error:', error);
throw error;
}
}
}
export default SeasonService;

View File

@@ -0,0 +1,188 @@
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import TeamDocument from '../models/TeamDocument.js';
import ClubTeam from '../models/ClubTeam.js';
import { devLog } from '../utils/logger.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
class TeamDocumentService {
/**
* Speichert ein hochgeladenes Dokument für ein Club-Team
* @param {Object} file - Das hochgeladene File-Objekt (von multer)
* @param {number} clubTeamId - Die ID des Club-Teams
* @param {string} documentType - Der Typ des Dokuments ('code_list' oder 'pin_list')
* @returns {Promise<TeamDocument>} Das erstellte TeamDocument
*/
static async uploadDocument(file, clubTeamId, documentType) {
try {
// Prüfe ob das Club-Team existiert
const clubTeam = await ClubTeam.findByPk(clubTeamId);
if (!clubTeam) {
throw new Error('Club-Team nicht gefunden');
}
// Generiere einen eindeutigen Dateinamen
const fileExtension = path.extname(file.originalname);
const uniqueFileName = `${clubTeamId}_${documentType}_${Date.now()}${fileExtension}`;
// Zielverzeichnis für Team-Dokumente
const uploadDir = path.join(__dirname, '..', 'uploads', 'team-documents');
// Erstelle Upload-Verzeichnis falls es nicht existiert
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir, { recursive: true });
}
const filePath = path.join(uploadDir, uniqueFileName);
// Verschiebe die Datei vom temporären Verzeichnis zum finalen Speicherort
fs.renameSync(file.path, filePath);
// Lösche alte Dokumente des gleichen Typs für dieses Team
await this.deleteDocumentsByType(clubTeamId, documentType);
// Erstelle Datenbankeintrag
const teamDocument = await TeamDocument.create({
fileName: uniqueFileName,
originalFileName: file.originalname,
filePath: filePath,
fileSize: file.size,
mimeType: file.mimetype,
documentType: documentType,
clubTeamId: clubTeamId
});
return teamDocument;
} catch (error) {
console.error('[TeamDocumentService.uploadDocument] - Error:', error);
throw error;
}
}
/**
* Holt alle Dokumente für ein Club-Team
* @param {number} clubTeamId - Die ID des Club-Teams
* @returns {Promise<Array<TeamDocument>>} Liste der Dokumente
*/
static async getDocumentsByClubTeam(clubTeamId) {
try {
const documents = await TeamDocument.findAll({
where: { clubTeamId },
order: [['createdAt', 'DESC']]
});
return documents;
} catch (error) {
console.error('[TeamDocumentService.getDocumentsByClubTeam] - Error:', error);
throw error;
}
}
/**
* Holt ein spezifisches Dokument
* @param {number} documentId - Die ID des Dokuments
* @returns {Promise<TeamDocument|null>} Das Dokument oder null
*/
static async getDocumentById(documentId) {
try {
const document = await TeamDocument.findByPk(documentId, {
include: [{
model: ClubTeam,
as: 'clubTeam',
attributes: ['id', 'name', 'clubId']
}]
});
return document;
} catch (error) {
console.error('[TeamDocumentService.getDocumentById] - Error:', error);
throw error;
}
}
/**
* Löscht ein Dokument
* @param {number} documentId - Die ID des Dokuments
* @returns {Promise<boolean>} True wenn gelöscht, sonst false
*/
static async deleteDocument(documentId) {
try {
const document = await TeamDocument.findByPk(documentId);
if (!document) {
return false;
}
// Lösche die physische Datei
if (fs.existsSync(document.filePath)) {
fs.unlinkSync(document.filePath);
}
// Lösche den Datenbankeintrag
const deletedRows = await TeamDocument.destroy({
where: { id: documentId }
});
return deletedRows > 0;
} catch (error) {
console.error('[TeamDocumentService.deleteDocument] - Error:', error);
throw error;
}
}
/**
* Löscht alle Dokumente eines bestimmten Typs für ein Club-Team
* @param {number} clubTeamId - Die ID des Club-Teams
* @param {string} documentType - Der Typ des Dokuments
* @returns {Promise<number>} Anzahl der gelöschten Dokumente
*/
static async deleteDocumentsByType(clubTeamId, documentType) {
try {
const documents = await TeamDocument.findAll({
where: { clubTeamId, documentType }
});
let deletedCount = 0;
for (const document of documents) {
// Lösche die physische Datei
if (fs.existsSync(document.filePath)) {
fs.unlinkSync(document.filePath);
}
deletedCount++;
}
// Lösche die Datenbankeinträge
const deletedRows = await TeamDocument.destroy({
where: { clubTeamId, documentType }
});
return deletedRows;
} catch (error) {
console.error('[TeamDocumentService.deleteDocumentsByType] - Error:', error);
throw error;
}
}
/**
* Holt den Dateipfad für ein Dokument
* @param {number} documentId - Die ID des Dokuments
* @returns {Promise<string|null>} Der Dateipfad oder null
*/
static async getDocumentPath(documentId) {
try {
const document = await TeamDocument.findByPk(documentId);
return document ? document.filePath : null;
} catch (error) {
console.error('[TeamDocumentService.getDocumentPath] - Error:', error);
throw error;
}
}
}
export default TeamDocumentService;

View File

@@ -0,0 +1,132 @@
import Team from '../models/Team.js';
import League from '../models/League.js';
import Club from '../models/Club.js';
import Season from '../models/Season.js';
import SeasonService from './seasonService.js';
import { devLog } from '../utils/logger.js';
class TeamService {
static async getAllTeamsByClub(clubId, seasonId = null) {
try {
// Wenn keine Saison angegeben, verwende die aktuelle
if (!seasonId) {
const currentSeason = await SeasonService.getOrCreateCurrentSeason();
seasonId = currentSeason.id;
}
const teams = await Team.findAll({
where: { clubId, seasonId },
include: [
{
model: League,
as: 'league',
attributes: ['id', 'name']
},
{
model: Season,
as: 'season',
attributes: ['id', 'season']
}
],
order: [['name', 'ASC']]
});
return teams;
} catch (error) {
console.error('[TeamService.getAllTeamsByClub] - Error:', error);
throw error;
}
}
static async getTeamById(teamId) {
try {
const team = await Team.findByPk(teamId, {
include: [
{
model: League,
as: 'league',
attributes: ['id', 'name']
},
{
model: Club,
as: 'club',
attributes: ['id', 'name']
},
{
model: Season,
as: 'season',
attributes: ['id', 'season']
}
]
});
return team;
} catch (error) {
console.error('[TeamService.getTeamById] - Error:', error);
throw error;
}
}
static async createTeam(teamData) {
try {
// Wenn keine Saison angegeben, verwende die aktuelle
if (!teamData.seasonId) {
const currentSeason = await SeasonService.getOrCreateCurrentSeason();
teamData.seasonId = currentSeason.id;
}
const team = await Team.create(teamData);
return team;
} catch (error) {
console.error('[TeamService.createTeam] - Error:', error);
throw error;
}
}
static async updateTeam(teamId, updateData) {
try {
const [updatedRowsCount] = await Team.update(updateData, {
where: { id: teamId }
});
return updatedRowsCount > 0;
} catch (error) {
console.error('[TeamService.updateTeam] - Error:', error);
throw error;
}
}
static async deleteTeam(teamId) {
try {
const deletedRowsCount = await Team.destroy({
where: { id: teamId }
});
return deletedRowsCount > 0;
} catch (error) {
console.error('[TeamService.deleteTeam] - Error:', error);
throw error;
}
}
static async getLeaguesByClub(clubId, seasonId = null) {
try {
// Wenn keine Saison angegeben, verwende die aktuelle
if (!seasonId) {
const currentSeason = await SeasonService.getOrCreateCurrentSeason();
seasonId = currentSeason.id;
}
const leagues = await League.findAll({
where: { clubId, seasonId },
attributes: ['id', 'name', 'seasonId'],
order: [['name', 'ASC']]
});
return leagues;
} catch (error) {
console.error('[TeamService.getLeaguesByClub] - Error:', error);
throw error;
}
}
}
export default TeamService;

View File

@@ -203,11 +203,8 @@ class TournamentService {
}
// 4) RoundRobin anlegen wie gehabt - NUR innerhalb jeder Gruppe
devLog(`[fillGroups] Erstelle Matches für ${groups.length} Gruppen`);
for (const g of groups) {
devLog(`[fillGroups] Verarbeite Gruppe ${g.id}`);
const gm = await TournamentMember.findAll({ where: { groupId: g.id } });
devLog(`[fillGroups] Gruppe ${g.id} hat ${gm.length} Teilnehmer:`, gm.map(m => ({ id: m.id, name: m.member?.firstName + ' ' + m.member?.lastName })));
if (gm.length < 2) {
console.warn(`Gruppe ${g.id} hat nur ${gm.length} Teilnehmer - keine Matches erstellt`);
@@ -215,10 +212,8 @@ class TournamentService {
}
const rounds = this.generateRoundRobinSchedule(gm);
devLog(`[fillGroups] Gruppe ${g.id} hat ${rounds.length} Runden`);
for (let roundIndex = 0; roundIndex < rounds.length; roundIndex++) {
devLog(`[fillGroups] Runde ${roundIndex + 1} für Gruppe ${g.id}:`, rounds[roundIndex]);
for (const [p1Id, p2Id] of rounds[roundIndex]) {
// Prüfe, ob beide Spieler zur gleichen Gruppe gehören
const p1 = gm.find(p => p.id === p1Id);
@@ -232,7 +227,6 @@ class TournamentService {
player2Id: p2Id,
groupRound: roundIndex + 1
});
devLog(`[fillGroups] Match erstellt: ${match.id} - Spieler ${p1Id} vs ${p2Id} in Gruppe ${g.id}`);
} else {
console.warn(`Spieler gehören nicht zur gleichen Gruppe: ${p1Id} (${p1?.groupId}) vs ${p2Id} (${p2?.groupId}) in Gruppe ${g.id}`);
}

View File

@@ -0,0 +1,7 @@
Spielplan 2025/2026 - Test Liga
1. 15.01.2025 19:00 Team Alpha vs Team Beta code: ABC123 home pin: PIN001 guest pin: PIN002
2. 22.01.2025 20:00 Team Gamma gegen Team Delta code: DEF456 heim pin: PIN003 gast pin: PIN004
3. 29.01.2025 18:30 Team Epsilon - Team Zeta code: GHI789 home pin: PIN005 guest pin: PIN006
4. 05.02.2025 19:30 Team Alpha vs Team Gamma code: JKL012 home pin: PIN007 guest pin: PIN008
5. 12.02.2025 20:00 Team Beta gegen Team Delta code: MNO345 heim pin: PIN009 gast pin: PIN010

View File

@@ -42,7 +42,6 @@ export const getUserByToken = async (token) => {
export const hasUserClubAccess = async (userId, clubId) => {
try {
devLog('[hasUserClubAccess]');
const userClub = await UserClub.findOne({
where: {
user_id: userId,
@@ -62,11 +61,9 @@ export const checkAccess = async (userToken, clubId) => {
const user = await getUserByToken(userToken);
const hasAccess = await hasUserClubAccess(user.id, clubId);
if (!hasAccess) {
devLog('[checkAccess] - no club access');
throw new HttpError('noaccess', 403);
}
} catch (error) {
devLog('[checkAccess] - error:', error);
throw error;
}
};
@@ -76,7 +73,6 @@ export const checkGlobalAccess = async (userToken) => {
const user = await getUserByToken(userToken);
return user; // Einfach den User zurückgeben, da globale Zugriffe nur Authentifizierung benötigen
} catch (error) {
devLog('[checkGlobalAccess] - error:', error);
throw error;
}
};

View File

@@ -65,6 +65,10 @@
<span class="nav-icon"></span>
Vordefinierte Aktivitäten
</a>
<a href="/team-management" class="nav-link">
<span class="nav-icon">👥</span>
Team-Verwaltung
</a>
</div>
</nav>
@@ -75,10 +79,6 @@
<span class="nav-icon">🔗</span>
myTischtennis-Account
</a>
<a href="/hettv-account" class="nav-link">
<span class="nav-icon">🔗</span>
HeTTV-Account
</a>
</div>
</nav>
@@ -174,7 +174,7 @@ export default {
},
loadClub() {
this.setCurrentClub(this.currentClub);
this.setCurrentClub(this.selectedClub);
this.$router.push('/training-stats');
},

View File

@@ -110,7 +110,6 @@ export default {
this.config.canvas.width = this.width || 600;
this.config.canvas.height = this.height || 400;
this.$nextTick(() => {
console.log('CourtDrawingRender: mounted with drawingData =', this.drawingData);
this.init();
this.redraw();
});
@@ -120,11 +119,9 @@ export default {
this.canvas = this.$refs.canvas;
if (this.canvas) {
this.ctx = this.canvas.getContext('2d');
console.log('CourtDrawingRender: canvas/context initialized');
}
},
redraw() {
console.log('CourtDrawingRender: redraw called with data =', this.drawingData);
if (!this.ctx) return;
const { width, height } = this.config.canvas;
// clear and background

View File

@@ -344,7 +344,6 @@ export default {
this.loadDrawingFromMetadata();
} else if (oldVal && !newVal) {
// drawingData wurde auf null gesetzt - reset alle Werte und zeichne leeres Canvas
console.log('CourtDrawingTool: drawingData set to null, resetting all values');
this.resetAllValues();
this.clearCanvas();
this.drawCourt(true); // forceRedraw = true
@@ -354,7 +353,6 @@ export default {
}
},
mounted() {
console.log('CourtDrawingTool: mounted');
this.$nextTick(() => {
this.initCanvas();
this.drawCourt();
@@ -365,12 +363,9 @@ export default {
},
methods: {
initCanvas() {
console.log('CourtDrawingTool: initCanvas called');
this.canvas = this.$refs.drawingCanvas;
console.log('CourtDrawingTool: canvas =', this.canvas);
if (this.canvas) {
this.ctx = this.canvas.getContext('2d');
console.log('CourtDrawingTool: ctx =', this.ctx);
this.ctx.lineCap = this.config.pen.cap;
this.ctx.lineJoin = this.config.pen.join;
} else {
@@ -379,7 +374,6 @@ export default {
},
drawCourt(forceRedraw = false) {
console.log('CourtDrawingTool: drawCourt called, forceRedraw:', forceRedraw);
const ctx = this.ctx;
const canvas = this.canvas;
const config = this.config;
@@ -389,8 +383,6 @@ export default {
return;
}
console.log('CourtDrawingTool: Drawing court...');
console.log('Canvas dimensions:', canvas.width, 'x', canvas.height);
// Hintergrund immer zeichnen wenn forceRedraw=true, sonst nur wenn Canvas leer ist
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
@@ -400,9 +392,7 @@ export default {
// Hintergrund
ctx.fillStyle = '#f0f0f0';
ctx.fillRect(0, 0, canvas.width, canvas.height);
console.log('Background drawn');
} else {
console.log('Canvas not empty, skipping background');
}
// Tischtennis-Tisch
@@ -411,8 +401,6 @@ export default {
const tableX = (canvas.width - tableWidth) / 2;
const tableY = (canvas.height - tableHeight) / 2;
console.log('Table dimensions:', tableWidth, 'x', tableHeight);
console.log('Table position:', tableX, ',', tableY);
// Tischtennis-Tisch Hintergrund
ctx.fillStyle = config.table.color;
@@ -1074,9 +1062,6 @@ export default {
},
testDraw() {
console.log('CourtDrawingTool: testDraw called');
console.log('Canvas:', this.canvas);
console.log('Context:', this.ctx);
if (!this.canvas || !this.ctx) {
console.error('Canvas or context not available, trying to reinitialize...');
@@ -1084,7 +1069,6 @@ export default {
}
if (this.canvas && this.ctx) {
console.log('Drawing simple test...');
// Einfacher Test: Roter Kreis
this.ctx.fillStyle = 'red';
@@ -1092,18 +1076,15 @@ export default {
this.ctx.arc(300, 200, 50, 0, 2 * Math.PI);
this.ctx.fill();
console.log('Red circle drawn');
} else {
console.error('Still no canvas or context available');
}
},
async saveDrawing() {
console.log('CourtDrawingTool: saveDrawing called');
async saveDrawing() {
try {
const dataURL = this.canvas.toDataURL('image/png');
console.log('CourtDrawingTool: dataURL created, length:', dataURL.length);
this.$emit('input', dataURL);
@@ -1121,7 +1102,6 @@ export default {
timestamp: new Date().toISOString()
};
console.log('CourtDrawingTool: drawingData created:', drawingData);
// Immer Metadaten nach oben geben
this.$emit('update-drawing-data', drawingData);
@@ -1129,18 +1109,12 @@ export default {
// Konvertiere DataURL zu Blob für Upload
const response = await fetch(dataURL);
const blob = await response.blob();
console.log('CourtDrawingTool: blob created, size:', blob.size);
// Erstelle File-Objekt
const file = new File([blob], `exercise-${Date.now()}.png`, { type: 'image/png' });
console.log('CourtDrawingTool: file created:', file);
console.log('CourtDrawingTool: file type:', file.type);
console.log('CourtDrawingTool: file size:', file.size);
// Emittiere das File und die Zeichnungsdaten für Upload
console.log('CourtDrawingTool: emitting upload-image event');
this.$emit('upload-image', file, drawingData);
console.log('CourtDrawingTool: upload-image event emitted');
} else {
// Kein Bild-Upload mehr: gebe lediglich die Zeichnungsdaten an den Parent weiter,
// damit Felder (Kürzel/Name/Beschreibung) gefüllt werden können
@@ -1173,7 +1147,6 @@ export default {
},
resetAllValues() {
console.log('CourtDrawingTool: Resetting all values to initial state');
this.selectedStartPosition = null;
this.selectedCirclePosition = null;
this.strokeType = null;
@@ -1189,7 +1162,6 @@ export default {
loadDrawingFromMetadata() {
if (this.drawingData) {
console.log('CourtDrawingTool: Loading drawing from metadata:', this.drawingData);
// Lade alle Zeichnungsdaten
this.selectedStartPosition = this.drawingData.selectedStartPosition || null;
@@ -1213,7 +1185,7 @@ export default {
this.selectedCirclePosition = 'bottom';
}
console.log('CourtDrawingTool: Loaded values:', {
this.$emit('drawing-data', {
selectedStartPosition: this.selectedStartPosition,
selectedCirclePosition: this.selectedCirclePosition,
strokeType: this.strokeType,
@@ -1229,7 +1201,6 @@ export default {
this.drawCourt();
});
console.log('CourtDrawingTool: Drawing loaded from metadata');
}
},

View File

@@ -1,291 +0,0 @@
<template>
<div class="modal-overlay" @click.self="$emit('close')">
<div class="modal">
<div class="modal-header">
<h3>{{ account ? 'HeTTV-Account bearbeiten' : 'HeTTV-Account verknüpfen' }}</h3>
</div>
<div class="modal-body">
<div class="form-group">
<label for="hettv-email">HeTTV-E-Mail:</label>
<input
type="email"
id="hettv-email"
v-model="formData.email"
placeholder="Ihre HeTTV-E-Mail-Adresse"
required
/>
</div>
<div class="form-group">
<label for="hettv-password">HeTTV-Passwort:</label>
<input
type="password"
id="hettv-password"
v-model="formData.password"
:placeholder="account && account.savePassword ? 'Leer lassen um beizubehalten' : 'Ihr HeTTV-Passwort'"
/>
</div>
<div class="form-group checkbox-group">
<label>
<input
type="checkbox"
v-model="formData.savePassword"
/>
<span>HeTTV-Passwort speichern</span>
</label>
<p class="hint">
Wenn aktiviert, wird Ihr HeTTV-Passwort verschlüsselt gespeichert,
sodass automatische Synchronisationen möglich sind.
</p>
</div>
<div class="form-group" v-if="formData.password">
<label for="app-password">Ihr App-Passwort zur Bestätigung:</label>
<input
type="password"
id="app-password"
v-model="formData.userPassword"
placeholder="Ihr Passwort für diese App"
required
/>
<p class="hint">
Aus Sicherheitsgründen benötigen wir Ihr App-Passwort,
um das HeTTV-Passwort zu speichern.
</p>
</div>
<div v-if="error" class="error-message">
{{ error }}
</div>
</div>
<div class="modal-footer">
<button class="btn-secondary" @click="$emit('close')" :disabled="saving">
Abbrechen
</button>
<button class="btn-primary" @click="saveAccount" :disabled="!canSave || saving">
{{ saving ? 'Speichere...' : 'Speichern' }}
</button>
</div>
</div>
</div>
</template>
<script>
import apiClient from '../apiClient.js';
export default {
name: 'HettvDialog',
props: {
account: {
type: Object,
default: null
}
},
data() {
return {
formData: {
email: this.account?.email || '',
password: '',
savePassword: this.account?.savePassword || false,
userPassword: ''
},
saving: false,
error: null
};
},
computed: {
canSave() {
if (!this.formData.email.trim()) {
return false;
}
if (this.formData.password && !this.formData.userPassword) {
return false;
}
return true;
}
},
methods: {
async saveAccount() {
if (!this.canSave) return;
this.error = null;
this.saving = true;
try {
const payload = {
email: this.formData.email,
savePassword: this.formData.savePassword,
service: 'hettv'
};
if (this.formData.password) {
payload.password = this.formData.password;
payload.userPassword = this.formData.userPassword;
}
await apiClient.post('/external-service/account', payload);
this.$emit('saved');
} catch (error) {
console.error('Fehler beim Speichern:', error);
this.error = error.response?.data?.message || 'Fehler beim Speichern des Accounts';
} finally {
this.saving = false;
}
}
}
};
</script>
<style scoped>
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal {
background: white;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
max-width: 600px;
width: 90%;
max-height: 90vh;
overflow-y: auto;
display: flex;
flex-direction: column;
}
.modal-header {
padding: 1.5rem;
border-bottom: 1px solid #dee2e6;
}
.modal-header h3 {
margin: 0;
color: #495057;
}
.modal-body {
padding: 1.5rem;
flex: 1;
}
.modal-footer {
padding: 1rem 1.5rem;
border-top: 1px solid #dee2e6;
display: flex;
justify-content: flex-end;
gap: 1rem;
}
.form-group {
margin-bottom: 1.5rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 600;
color: #495057;
}
.form-group input[type="text"],
.form-group input[type="email"],
.form-group input[type="password"] {
width: 100%;
padding: 0.75rem;
border: 1px solid #ced4da;
border-radius: 4px;
font-size: 1rem;
box-sizing: border-box;
}
.form-group input[type="text"]:focus,
.form-group input[type="email"]:focus,
.form-group input[type="password"]:focus {
outline: none;
border-color: #007bff;
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
}
.checkbox-group label {
display: flex;
align-items: center;
gap: 0.5rem;
font-weight: normal;
cursor: pointer;
}
.checkbox-group input[type="checkbox"] {
width: auto;
margin: 0;
cursor: pointer;
}
.hint {
margin-top: 0.5rem;
font-size: 0.875rem;
color: #6c757d;
font-style: italic;
}
.error-message {
padding: 0.75rem;
background-color: #f8d7da;
border: 1px solid #f5c6cb;
border-radius: 4px;
color: #721c24;
margin-top: 1rem;
}
.btn-primary, .btn-secondary {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 4px;
font-size: 1rem;
cursor: pointer;
transition: background-color 0.2s ease;
}
.btn-primary {
background-color: #007bff;
color: white;
}
.btn-primary:hover:not(:disabled) {
background-color: #0056b3;
}
.btn-primary:disabled {
background-color: #6c757d;
cursor: not-allowed;
opacity: 0.6;
}
.btn-secondary {
background-color: #6c757d;
color: white;
}
.btn-secondary:hover:not(:disabled) {
background-color: #545b62;
}
.btn-secondary:disabled {
cursor: not-allowed;
opacity: 0.6;
}
</style>

View File

@@ -121,8 +121,7 @@ export default {
try {
const payload = {
email: this.formData.email,
savePassword: this.formData.savePassword,
service: 'mytischtennis'
savePassword: this.formData.savePassword
};
// Nur password und userPassword hinzufügen, wenn ein Passwort eingegeben wurde
@@ -131,7 +130,7 @@ export default {
payload.userPassword = this.formData.userPassword;
}
await apiClient.post('/external-service/account', payload);
await apiClient.post('/mytischtennis/account', payload);
this.$emit('saved');
} catch (error) {
console.error('Fehler beim Speichern:', error);

View File

@@ -0,0 +1,299 @@
<template>
<div class="season-selector">
<label>
<span>Saison:</span>
<div class="season-input-group">
<select v-model="selectedSeasonId" @change="onSeasonChange" class="season-select" :disabled="loading">
<option value="">{{ loading ? 'Lade...' : 'Saison wählen...' }}</option>
<option v-for="season in seasons" :key="season.id" :value="season.id">
{{ season.season }}
</option>
</select>
<button @click="showNewSeasonForm = !showNewSeasonForm" class="btn-add-season" title="Neue Saison hinzufügen">
{{ showNewSeasonForm ? '✕' : '+' }}
</button>
</div>
</label>
<div v-if="error" class="error-message">
{{ error }}
</div>
<div v-if="showNewSeasonForm" class="new-season-form">
<label>
<span>Neue Saison:</span>
<input
type="text"
v-model="newSeasonString"
placeholder="z.B. 2023/2024"
@keyup.enter="createSeason"
class="season-input"
>
</label>
<div class="form-actions">
<button @click="createSeason" :disabled="!isValidSeasonFormat" class="btn-create">
Erstellen
</button>
<button @click="cancelNewSeason" class="btn-cancel">
Abbrechen
</button>
</div>
</div>
</div>
</template>
<script>
import { ref, computed, onMounted, watch } from 'vue';
import { useStore } from 'vuex';
import apiClient from '../apiClient.js';
export default {
name: 'SeasonSelector',
props: {
modelValue: {
type: [String, Number],
default: null
},
showCurrentSeason: {
type: Boolean,
default: true
}
},
emits: ['update:modelValue', 'season-change'],
setup(props, { emit }) {
const store = useStore();
// Reactive data
const seasons = ref([]);
const selectedSeasonId = ref(props.modelValue);
const showNewSeasonForm = ref(false);
const newSeasonString = ref('');
const loading = ref(false);
const error = ref(null);
// Computed
const isValidSeasonFormat = computed(() => {
const seasonRegex = /^\d{4}\/\d{4}$/;
return seasonRegex.test(newSeasonString.value);
});
// Methods
const loadSeasons = async () => {
loading.value = true;
error.value = null;
try {
const response = await apiClient.get('/seasons');
seasons.value = response.data;
// Wenn showCurrentSeason true ist und keine Saison ausgewählt, wähle die aktuelle
if (props.showCurrentSeason && !selectedSeasonId.value && seasons.value.length > 0) {
// Die erste Saison ist die neueste (sortiert nach DESC)
selectedSeasonId.value = seasons.value[0].id;
emit('update:modelValue', selectedSeasonId.value);
emit('season-change', seasons.value[0]);
}
} catch (err) {
console.error('Fehler beim Laden der Saisons:', err);
error.value = 'Fehler beim Laden der Saisons';
} finally {
loading.value = false;
}
};
const onSeasonChange = () => {
const selectedSeason = seasons.value.find(s => s.id == selectedSeasonId.value);
emit('update:modelValue', selectedSeasonId.value);
emit('season-change', selectedSeason);
};
const createSeason = async () => {
if (!isValidSeasonFormat.value) return;
try {
const response = await apiClient.post('/seasons', {
season: newSeasonString.value
});
const newSeason = response.data;
seasons.value.unshift(newSeason); // Am Anfang einfügen (neueste zuerst)
selectedSeasonId.value = newSeason.id;
emit('update:modelValue', selectedSeasonId.value);
emit('season-change', newSeason);
// Formular zurücksetzen
newSeasonString.value = '';
showNewSeasonForm.value = false;
} catch (error) {
console.error('Fehler beim Erstellen der Saison:', error);
if (error.response?.data?.error === 'alreadyexists') {
alert('Diese Saison existiert bereits!');
} else {
alert('Fehler beim Erstellen der Saison');
}
}
};
const cancelNewSeason = () => {
newSeasonString.value = '';
showNewSeasonForm.value = false;
};
// Watch for prop changes
watch(() => props.modelValue, (newValue) => {
selectedSeasonId.value = newValue;
});
// Lifecycle
onMounted(() => {
loadSeasons();
});
return {
seasons,
selectedSeasonId,
showNewSeasonForm,
newSeasonString,
loading,
error,
isValidSeasonFormat,
onSeasonChange,
createSeason,
cancelNewSeason
};
}
};
</script>
<style scoped>
.season-selector {
margin-bottom: 1rem;
}
.season-selector label {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.season-selector label span {
font-weight: 600;
color: var(--text-color);
}
.season-input-group {
display: flex;
gap: 0.5rem;
align-items: center;
}
.season-select {
flex: 1;
padding: 0.75rem;
border: 1px solid var(--border-color);
border-radius: var(--border-radius-small);
font-size: 1rem;
background: white;
}
.season-select:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 2px var(--primary-light);
}
.btn-add-season {
background: var(--primary-color);
color: white;
border: none;
border-radius: var(--border-radius-small);
width: 2.5rem;
height: 2.5rem;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-weight: bold;
transition: var(--transition);
}
.btn-add-season:hover {
background: var(--primary-dark);
}
.new-season-form {
background: var(--background-light);
padding: 1rem;
border-radius: var(--border-radius);
border: 1px solid var(--border-color);
margin-top: 0.5rem;
}
.new-season-form label {
margin-bottom: 1rem;
}
.season-input {
width: 100%;
padding: 0.75rem;
border: 1px solid var(--border-color);
border-radius: var(--border-radius-small);
font-size: 1rem;
}
.season-input:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 2px var(--primary-light);
}
.form-actions {
display: flex;
gap: 0.5rem;
}
.btn-create {
background: var(--primary-color);
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: var(--border-radius-small);
cursor: pointer;
font-weight: 600;
transition: var(--transition);
}
.btn-create:hover:not(:disabled) {
background: var(--primary-dark);
}
.btn-create:disabled {
background: var(--text-muted);
cursor: not-allowed;
}
.btn-cancel {
background: var(--background-light);
color: var(--text-color);
border: 1px solid var(--border-color);
padding: 0.5rem 1rem;
border-radius: var(--border-radius-small);
cursor: pointer;
font-weight: 600;
transition: var(--transition);
}
.btn-cancel:hover {
background: var(--border-color);
}
.error-message {
color: #dc3545;
font-size: 0.875rem;
margin-top: 0.5rem;
padding: 0.5rem;
background: #f8d7da;
border: 1px solid #f5c6cb;
border-radius: var(--border-radius-small);
}
</style>

View File

@@ -14,7 +14,7 @@ import TrainingStatsView from './views/TrainingStatsView.vue';
import PredefinedActivities from './views/PredefinedActivities.vue';
import OfficialTournaments from './views/OfficialTournaments.vue';
import MyTischtennisAccount from './views/MyTischtennisAccount.vue';
import HettvAccount from './views/HettvAccount.vue';
import TeamManagementView from './views/TeamManagementView.vue';
import Impressum from './views/Impressum.vue';
import Datenschutz from './views/Datenschutz.vue';
@@ -34,7 +34,7 @@ const routes = [
{ path: '/predefined-activities', component: PredefinedActivities },
{ path: '/official-tournaments', component: OfficialTournaments },
{ path: '/mytischtennis-account', component: MyTischtennisAccount },
{ path: '/hettv-account', component: HettvAccount },
{ path: '/team-management', component: TeamManagementView },
{ path: '/impressum', component: Impressum },
{ path: '/datenschutz', component: Datenschutz },
];

View File

@@ -1,314 +0,0 @@
<template>
<div class="page-container">
<h1>HeTTV-Account</h1>
<div class="account-container">
<div v-if="loading" class="loading">Lade...</div>
<div v-else-if="account" class="account-info">
<div class="info-section">
<h2>Verknüpfter Account</h2>
<div class="info-row">
<label>E-Mail:</label>
<span>{{ account.email }}</span>
</div>
<div class="info-row">
<label>Passwort gespeichert:</label>
<span>{{ account.savePassword ? 'Ja' : 'Nein' }}</span>
</div>
<div class="info-row" v-if="account.lastLoginSuccess">
<label>Letzter erfolgreicher Login:</label>
<span>{{ formatDate(account.lastLoginSuccess) }}</span>
</div>
<div class="info-row" v-if="account.lastLoginAttempt">
<label>Letzter Login-Versuch:</label>
<span>{{ formatDate(account.lastLoginAttempt) }}</span>
</div>
<div class="button-group">
<button class="btn-primary" @click="openEditDialog">Account bearbeiten</button>
<button class="btn-secondary" @click="testConnection">Erneut einloggen</button>
<button class="btn-danger" @click="deleteAccount">Account trennen</button>
</div>
</div>
</div>
<div v-else class="no-account">
<p>Kein HeTTV-Account verknüpft.</p>
<button class="btn-primary" @click="openEditDialog">Account verknüpfen</button>
</div>
<div class="info-box">
<h3>Über HeTTV</h3>
<p>Durch die Verknüpfung Ihres HeTTV-Accounts können Sie:</p>
<ul>
<li>Spielerdaten automatisch synchronisieren</li>
<li>Ligainformationen abrufen</li>
<li>Wettkampfdaten direkt importieren</li>
</ul>
<p><strong>Hinweis:</strong> Das Speichern des Passworts ist optional. Wenn Sie es nicht speichern, werden Sie bei jeder Synchronisation nach dem Passwort gefragt.</p>
</div>
</div>
<!-- Edit Dialog -->
<HettvDialog
v-if="showDialog"
:account="account"
@close="closeDialog"
@saved="onAccountSaved"
/>
</div>
</template>
<script>
import apiClient from '../apiClient.js';
import HettvDialog from '../components/HettvDialog.vue';
export default {
name: 'HettvAccount',
components: {
HettvDialog
},
data() {
return {
loading: true,
account: null,
showDialog: false
};
},
mounted() {
this.loadAccount();
},
methods: {
async loadAccount() {
try {
this.loading = true;
const response = await apiClient.get('/external-service/account', {
params: { service: 'hettv' }
});
this.account = response.data.account;
} catch (error) {
console.error('Fehler beim Laden des Accounts:', error);
this.$store.dispatch('showMessage', {
text: 'Fehler beim Laden des HeTTV-Accounts',
type: 'error'
});
} finally {
this.loading = false;
}
},
openEditDialog() {
this.showDialog = true;
},
closeDialog() {
this.showDialog = false;
},
async onAccountSaved() {
this.closeDialog();
await this.loadAccount();
this.$store.dispatch('showMessage', {
text: 'HeTTV-Account erfolgreich gespeichert',
type: 'success'
});
},
async testConnection() {
try {
await apiClient.post('/external-service/verify', {
service: 'hettv'
});
this.$store.dispatch('showMessage', {
text: 'Login erfolgreich! Verbindungsdaten aktualisiert.',
type: 'success'
});
await this.loadAccount();
} catch (error) {
const message = error.response?.data?.message || 'Login fehlgeschlagen';
if (error.response?.status === 400 && message.includes('Kein Passwort gespeichert')) {
this.showDialog = true;
}
this.$store.dispatch('showMessage', {
text: message,
type: 'error'
});
}
},
async deleteAccount() {
if (!confirm('Möchten Sie die Verknüpfung zum HeTTV-Account wirklich trennen?')) {
return;
}
try {
await apiClient.delete('/external-service/account', {
params: { service: 'hettv' }
});
this.account = null;
this.$store.dispatch('showMessage', {
text: 'HeTTV-Account erfolgreich getrennt',
type: 'success'
});
} catch (error) {
console.error('Fehler beim Löschen des Accounts:', error);
this.$store.dispatch('showMessage', {
text: 'Fehler beim Trennen des Accounts',
type: 'error'
});
}
},
formatDate(dateString) {
if (!dateString) return '-';
const date = new Date(dateString);
return date.toLocaleString('de-DE', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
}
}
};
</script>
<style scoped>
.page-container {
max-width: 800px;
margin: 0 auto;
padding: 2rem;
}
h1 {
color: var(--text-color, #333);
margin-bottom: 2rem;
}
.account-container {
display: flex;
flex-direction: column;
gap: 2rem;
}
.loading {
text-align: center;
padding: 2rem;
color: #666;
}
.account-info, .no-account {
background: white;
border-radius: 8px;
padding: 2rem;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.info-section h2 {
margin-top: 0;
margin-bottom: 1.5rem;
color: var(--primary-color, #007bff);
}
.info-row {
display: flex;
justify-content: space-between;
padding: 0.75rem 0;
border-bottom: 1px solid #eee;
}
.info-row:last-of-type {
border-bottom: none;
}
.info-row label {
font-weight: 600;
color: #555;
}
.info-row span {
color: #333;
}
.button-group {
display: flex;
gap: 1rem;
margin-top: 2rem;
flex-wrap: wrap;
}
.no-account {
text-align: center;
}
.no-account p {
margin-bottom: 1.5rem;
color: #666;
}
.info-box {
background: #f8f9fa;
border-left: 4px solid var(--primary-color, #007bff);
padding: 1.5rem;
border-radius: 4px;
}
.info-box h3 {
margin-top: 0;
color: var(--primary-color, #007bff);
}
.info-box ul {
margin: 1rem 0;
padding-left: 1.5rem;
}
.info-box li {
margin: 0.5rem 0;
}
.btn-primary, .btn-secondary, .btn-danger {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 4px;
font-size: 1rem;
cursor: pointer;
transition: background-color 0.2s ease;
}
.btn-primary {
background-color: #007bff;
color: white;
}
.btn-primary:hover {
background-color: #0056b3;
}
.btn-secondary {
background-color: #6c757d;
color: white;
}
.btn-secondary:hover {
background-color: #545b62;
}
.btn-danger {
background-color: #dc3545;
color: white;
}
.btn-danger:hover {
background-color: #c82333;
}
</style>

View File

@@ -387,6 +387,10 @@ export default {
return '';
},
async updateRatingsFromMyTischtennis() {
if (!confirm('TTR/QTTR-Werte von myTischtennis aktualisieren?')) {
return;
}
this.isUpdatingRatings = true;
try {
const response = await apiClient.post(`/clubmembers/update-ratings/${this.currentClub}`);

View File

@@ -36,17 +36,10 @@
<div class="button-group">
<button class="btn-primary" @click="openEditDialog">Account bearbeiten</button>
<button class="btn-secondary" @click="testConnection">Verbindung testen</button>
<button class="btn-secondary" @click="testLoginFlow">Test: Login-Flow</button>
<button class="btn-secondary" @click="testConnection">Erneut einloggen</button>
<button class="btn-danger" @click="deleteAccount">Account trennen</button>
</div>
</div>
<!-- Test-Ausgabe -->
<div v-if="testResult" class="test-result" :class="testResult.type">
<h3>Test-Ergebnis:</h3>
<pre>{{ testResult.data }}</pre>
</div>
</div>
<div v-else class="no-account">
@@ -89,8 +82,7 @@ export default {
return {
loading: true,
account: null,
showDialog: false,
testResult: null
showDialog: false
};
},
mounted() {
@@ -131,16 +123,15 @@ export default {
},
async testConnection() {
this.testResult = null;
try {
await apiClient.post('/mytischtennis/verify');
this.$store.dispatch('showMessage', {
text: 'Verbindung erfolgreich! Login funktioniert.',
text: 'Login erfolgreich! Verbindungsdaten aktualisiert.',
type: 'success'
});
await this.loadAccount(); // Aktualisiere lastLoginSuccess
await this.loadAccount(); // Aktualisiere Account-Daten inkl. clubId, fedNickname
} catch (error) {
const message = error.response?.data?.message || 'Verbindung fehlgeschlagen';
const message = error.response?.data?.message || 'Login fehlgeschlagen';
if (error.response?.status === 400 && message.includes('Kein Passwort gespeichert')) {
// Passwort-Dialog öffnen
@@ -154,71 +145,6 @@ export default {
}
},
async testLoginFlow() {
this.testResult = null;
try {
// 1. Verify Login
console.log('Testing login...');
const verifyResponse = await apiClient.post('/mytischtennis/verify');
console.log('Login successful:', verifyResponse.data);
// 2. Get Session
console.log('Fetching session...');
const sessionResponse = await apiClient.get('/mytischtennis/session');
console.log('Session data:', sessionResponse.data);
// 3. Check Status
console.log('Checking status...');
const statusResponse = await apiClient.get('/mytischtennis/status');
console.log('Status:', statusResponse.data);
this.testResult = {
type: 'success',
data: {
message: 'Alle Tests erfolgreich!',
login: {
accessToken: verifyResponse.data.accessToken ? '✓ vorhanden' : '✗ fehlt',
expiresAt: verifyResponse.data.expiresAt,
clubId: verifyResponse.data.clubId || '✗ nicht gefunden',
clubName: verifyResponse.data.clubName || '✗ nicht gefunden'
},
session: {
accessToken: sessionResponse.data.session?.accessToken ? '✓ vorhanden' : '✗ fehlt',
refreshToken: sessionResponse.data.session?.refreshToken ? '✓ vorhanden' : '✗ fehlt',
cookie: sessionResponse.data.session?.cookie ? '✓ vorhanden' : '✗ fehlt',
userData: sessionResponse.data.session?.userData ? '✓ vorhanden' : '✗ fehlt',
expiresAt: sessionResponse.data.session?.expiresAt
},
status: statusResponse.data
}
};
this.$store.dispatch('showMessage', {
text: 'Test erfolgreich! Details siehe unten.',
type: 'success'
});
} catch (error) {
console.error('Test failed:', error);
this.testResult = {
type: 'error',
data: {
message: 'Test fehlgeschlagen',
error: error.response?.data?.message || error.message,
status: error.response?.status,
details: error.response?.data
}
};
this.$store.dispatch('showMessage', {
text: `Test fehlgeschlagen: ${error.response?.data?.message || error.message}`,
type: 'error'
});
}
},
async deleteAccount() {
if (!confirm('Möchten Sie die Verknüpfung zum myTischtennis-Account wirklich trennen?')) {
return;
@@ -384,40 +310,5 @@ h1 {
.btn-danger:hover {
background-color: #c82333;
}
.test-result {
background: white;
border-radius: 8px;
padding: 1.5rem;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
margin-top: 1rem;
}
.test-result.success {
border-left: 4px solid #28a745;
}
.test-result.error {
border-left: 4px solid #dc3545;
}
.test-result h3 {
margin-top: 0;
margin-bottom: 1rem;
color: #333;
}
.test-result pre {
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 4px;
padding: 1rem;
overflow-x: auto;
font-family: 'Courier New', monospace;
font-size: 0.875rem;
line-height: 1.5;
white-space: pre-wrap;
word-wrap: break-word;
}
</style>

View File

@@ -179,7 +179,6 @@ export default {
const r = await apiClient.get(`/predefined-activities/${this.editModel.id}`);
const { images } = r.data;
this.images = images || [];
console.log('Images reloaded:', this.images);
} catch (error) {
console.error('Error reloading images:', error);
}
@@ -218,7 +217,6 @@ export default {
async save() {
if (!this.editModel) return;
console.log('Save: selectedFile =', this.selectedFile);
if (this.editModel.id) {
const { id, ...payload } = this.editModel;
@@ -234,10 +232,8 @@ export default {
// Nach dem Speichern (sowohl CREATE als auch UPDATE): Bild hochladen falls vorhanden
if (this.selectedFile) {
console.log('Uploading image after save...');
await this.uploadImage();
} else {
console.log('No selectedFile to upload');
}
await this.reload();
@@ -250,16 +246,9 @@ export default {
},
async uploadImage() {
if (!this.editModel || !this.editModel.id || !this.selectedFile) {
console.log('Upload skipped: editModel=', this.editModel, 'selectedFile=', this.selectedFile);
return;
}
console.log('Starting image upload...');
console.log('editModel:', this.editModel);
console.log('selectedActivity:', this.selectedActivity);
console.log('Activity ID (editModel.id):', this.editModel.id);
console.log('Activity ID (selectedActivity.id):', this.selectedActivity?.id);
console.log('File:', this.selectedFile);
const fd = new FormData();
fd.append('image', this.selectedFile);
@@ -267,20 +256,17 @@ export default {
// Füge Zeichnungsdaten hinzu, falls vorhanden
if (this.selectedDrawingData) {
fd.append('drawingData', JSON.stringify(this.selectedDrawingData));
console.log('Added drawingData to FormData:', this.selectedDrawingData);
}
// Verwende PUT für Updates, POST für neue Activities
const isUpdate = this.selectedActivity && this.selectedActivity.id === this.editModel.id;
const method = isUpdate ? 'put' : 'post';
console.log('Using method:', method);
try {
const response = await apiClient[method](`/predefined-activities/${this.editModel.id}/image`, fd, {
headers: { 'Content-Type': 'multipart/form-data' }
});
console.log('Upload successful:', response);
// Nach Upload Details neu laden
await this.select(this.editModel);
@@ -334,24 +320,15 @@ export default {
},
async onDrawingImageUpload(file, drawingData) {
console.log('onDrawingImageUpload called with file:', file);
console.log('onDrawingImageUpload called with drawingData:', drawingData);
console.log('File type:', file?.type);
console.log('File size:', file?.size);
console.log('File name:', file?.name);
// Setze das File und die Zeichnungsdaten für den Upload
this.selectedFile = file;
this.selectedDrawingData = drawingData;
console.log('selectedFile set to:', this.selectedFile);
console.log('selectedDrawingData set to:', this.selectedDrawingData);
// Upload wird erst beim Speichern durchgeführt
console.log('File and drawing data ready for upload when saving');
},
async onImageUploaded() {
console.log('Image uploaded successfully, refreshing image list...');
// Bildliste aktualisieren
if (this.editModel && this.editModel.id) {
await this.select(this.editModel);

View File

@@ -1,12 +1,14 @@
<template>
<div>
<h2>Spielpläne</h2>
<div class="button-group">
<button @click="openImportModal">Spielplanimport</button>
<button @click="loadHettvData" :disabled="loadingHettv" class="hettv-button">
{{ loadingHettv ? 'Lade HeTTV-Daten...' : 'HeTTV-Daten laden' }}
</button>
</div>
<SeasonSelector
v-model="selectedSeasonId"
@season-change="onSeasonChange"
:show-current-season="true"
/>
<button @click="openImportModal">Spielplanimport</button>
<div v-if="hoveredMatch && hoveredMatch.location" class="hover-info">
<p><strong>{{ hoveredMatch.location.name || 'N/A' }}</strong></p>
<p>{{ hoveredMatch.location.address || 'N/A' }}</p>
@@ -17,8 +19,9 @@
<li class="special-link" @click="loadAllMatches">Gesamtspielplan</li>
<li class="special-link" @click="loadAdultMatches">Spielplan Erwachsene</li>
<li class="divider"></li>
<li v-for="league in leagues" :key="league" @click="loadMatchesForLeague(league.id, league.name)">{{
<li v-for="league in leagues" :key="league.id" @click="loadMatchesForLeague(league.id, league.name)">{{
league.name }}</li>
<li v-if="leagues.length === 0" class="no-leagues">Keine Ligen für diese Saison gefunden</li>
</ul>
<div class="flex-item">
<button @click="generatePDF">Download PDF</button>
@@ -32,6 +35,9 @@
<th>Heimmannschaft</th>
<th>Gastmannschaft</th>
<th v-if="selectedLeague === 'Gesamtspielplan' || selectedLeague === 'Spielplan Erwachsene'">Altersklasse</th>
<th>Code</th>
<th>Heim-PIN</th>
<th>Gast-PIN</th>
</tr>
</thead>
<tbody>
@@ -42,6 +48,18 @@
<td v-html="highlightClubName(match.homeTeam?.name || 'N/A')"></td>
<td v-html="highlightClubName(match.guestTeam?.name || 'N/A')"></td>
<td v-if="selectedLeague === 'Gesamtspielplan' || selectedLeague === 'Spielplan Erwachsene'">{{ match.leagueDetails?.name || 'N/A' }}</td>
<td class="code-cell">
<span v-if="match.code" class="code-value clickable" @click="copyToClipboard(match.code, 'Code')" :title="'Code kopieren: ' + match.code">{{ match.code }}</span>
<span v-else class="no-data">-</span>
</td>
<td class="pin-cell">
<span v-if="match.homePin" class="pin-value clickable" @click="copyToClipboard(match.homePin, 'Heim-PIN')" :title="'Heim-PIN kopieren: ' + match.homePin">{{ match.homePin }}</span>
<span v-else class="no-data">-</span>
</td>
<td class="pin-cell">
<span v-if="match.guestPin" class="pin-value clickable" @click="copyToClipboard(match.guestPin, 'Gast-PIN')" :title="'Gast-PIN kopieren: ' + match.guestPin">{{ match.guestPin }}</span>
<span v-else class="no-data">-</span>
</td>
</tr>
</tbody>
</table>
@@ -70,9 +88,13 @@
import { mapGetters } from 'vuex';
import apiClient from '../apiClient.js';
import PDFGenerator from '../components/PDFGenerator.js';
import SeasonSelector from '../components/SeasonSelector.vue';
export default {
name: 'ScheduleView',
components: {
SeasonSelector
},
computed: {
...mapGetters(['isAuthenticated', 'currentClub', 'clubs', 'currentClubName']),
},
@@ -84,8 +106,8 @@ export default {
matches: [],
selectedLeague: '',
hoveredMatch: null,
loadingHettv: false,
hettvData: null,
selectedSeasonId: null,
currentSeason: null,
};
},
methods: {
@@ -181,17 +203,21 @@ export default {
async loadLeagues() {
try {
const clubId = this.currentClub;
if (!clubId || clubId === 'null') {
console.log('Kein Club ausgewählt, überspringe Liga-Laden');
return;
}
const response = await apiClient.get(`/matches/leagues/current/${clubId}`);
const seasonParam = this.selectedSeasonId ? `?seasonid=${this.selectedSeasonId}` : '';
const response = await apiClient.get(`/matches/leagues/current/${clubId}${seasonParam}`);
this.leagues = this.sortLeagues(response.data);
} catch (error) {
console.log('Fehler beim Laden der Ligen:', error.message);
// Keine Alert mehr, da das normal ist wenn kein Club-Zugriff vorhanden
console.error('ScheduleView: Error loading leagues:', error);
alert('Fehler beim Laden der Ligen');
}
},
onSeasonChange(season) {
this.currentSeason = season;
this.loadLeagues();
// Leere die aktuellen Matches, da sie für eine andere Saison sind
this.matches = [];
this.selectedLeague = '';
},
async loadMatchesForLeague(leagueId, leagueName) {
this.selectedLeague = leagueName;
try {
@@ -205,7 +231,8 @@ export default {
async loadAllMatches() {
this.selectedLeague = 'Gesamtspielplan';
try {
const response = await apiClient.get(`/matches/leagues/${this.currentClub}/matches`);
const seasonParam = this.selectedSeasonId ? `?seasonid=${this.selectedSeasonId}` : '';
const response = await apiClient.get(`/matches/leagues/${this.currentClub}/matches${seasonParam}`);
this.matches = response.data;
} catch (error) {
alert('Fehler beim Laden des Gesamtspielplans');
@@ -215,7 +242,8 @@ export default {
async loadAdultMatches() {
this.selectedLeague = 'Spielplan Erwachsene';
try {
const response = await apiClient.get(`/matches/leagues/${this.currentClub}/matches`);
const seasonParam = this.selectedSeasonId ? `?seasonid=${this.selectedSeasonId}` : '';
const response = await apiClient.get(`/matches/leagues/${this.currentClub}/matches${seasonParam}`);
// Filtere nur Erwachsenenligen (keine Jugendligen)
const allMatches = response.data;
this.matches = allMatches.filter(match => {
@@ -313,42 +341,35 @@ export default {
return ''; // Keine besondere Farbe
},
async loadHettvData() {
this.loadingHettv = true;
async copyToClipboard(text, type) {
try {
console.log('Lade HeTTV-Hauptseite...');
const response = await apiClient.get('/external-service/hettv/main-page');
await navigator.clipboard.writeText(text);
// Zeige eine kurze Bestätigung
const originalText = event.target.textContent;
event.target.textContent = '✓';
event.target.style.color = '#4CAF50';
if (response.data.success) {
this.hettvData = response.data.data;
console.log('HeTTV-Daten geladen:', {
downloadLinks: this.hettvData.downloadLinks,
htmlLength: this.hettvData.htmlContent.length,
trace: this.hettvData.trace,
lastUrl: this.hettvData.lastUrl,
lastStatus: this.hettvData.lastStatus,
savedFile: this.hettvData.savedFile
});
// Zeige gefundene Download-Links
if (this.hettvData.downloadLinks.length > 0) {
alert(`HeTTV-Daten erfolgreich geladen!\n\nGefundene Download-Links:\n${this.hettvData.downloadLinks.join('\n')}`);
} else {
alert('HeTTV-Hauptseite geladen, aber keine Download-Links gefunden.');
}
} else {
alert('Fehler beim Laden der HeTTV-Daten: ' + response.data.error);
}
} catch (error) {
console.error('HeTTV-Fehler:', error);
alert('Fehler beim Laden der HeTTV-Daten: ' + (error.response?.data?.error || error.message));
} finally {
this.loadingHettv = false;
}
setTimeout(() => {
event.target.textContent = originalText;
event.target.style.color = '';
}, 1000);
} catch (err) {
console.error('Fehler beim Kopieren:', err);
// Fallback für ältere Browser
const textArea = document.createElement('textarea');
textArea.value = text;
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
}
},
},
async created() {
await this.loadLeagues();
// Ligen werden geladen, sobald eine Saison ausgewählt ist
// Die SeasonSelector-Komponente wird automatisch die aktuelle Saison auswählen
// und dann onSeasonChange aufrufen, was loadLeagues() triggert
}
};
</script>
@@ -473,6 +494,12 @@ li {
transition: all 0.3s ease;
}
.no-leagues {
color: #666;
font-style: italic;
padding: 0.5rem;
}
.divider {
height: 1px;
background-color: #ddd;
@@ -481,6 +508,61 @@ li {
color: transparent !important;
}
/* Code und PIN Styles */
.code-cell, .pin-cell {
text-align: center;
font-family: 'Courier New', monospace;
font-weight: bold;
min-width: 80px;
}
.code-value {
background: #e3f2fd;
color: #1976d2;
padding: 4px 8px;
border-radius: 4px;
font-size: 0.9rem;
display: inline-block;
border: 1px solid #bbdefb;
}
.code-value.clickable {
cursor: pointer;
transition: all 0.2s ease;
}
.code-value.clickable:hover {
background: #bbdefb;
transform: scale(1.05);
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.pin-value {
background: #fff3e0;
color: #f57c00;
padding: 4px 8px;
border-radius: 4px;
font-size: 0.9rem;
display: inline-block;
border: 1px solid #ffcc02;
}
.pin-value.clickable {
cursor: pointer;
transition: all 0.2s ease;
}
.pin-value.clickable:hover {
background: #ffcc02;
transform: scale(1.05);
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.no-data {
color: #999;
font-style: italic;
}
.match-today {
background-color: #fff3cd !important; /* Gelb für heute */
}
@@ -496,37 +578,4 @@ li {
.match-next-week:hover {
background-color: #b8daff !important; /* Dunkleres Blau beim Hover */
}
.button-group {
margin-bottom: 20px;
}
.button-group button {
margin-right: 10px;
padding: 8px 16px;
border: 1px solid #ddd;
background-color: #f8f9fa;
cursor: pointer;
border-radius: 4px;
}
.button-group button:hover {
background-color: #e9ecef;
}
.hettv-button {
background-color: #007bff !important;
color: white !important;
border-color: #007bff !important;
}
.hettv-button:hover {
background-color: #0056b3 !important;
}
.hettv-button:disabled {
background-color: #6c757d !important;
border-color: #6c757d !important;
cursor: not-allowed;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -586,7 +586,6 @@ export default {
try {
const d = await apiClient.get(`/tournament/${this.currentClub}`);
this.dates = d.data;
console.log('Loaded tournaments:', this.dates);
// Prüfe, ob es einen Trainingstag heute gibt
await this.checkTrainingToday();
@@ -601,7 +600,6 @@ export default {
const today = new Date().toISOString().split('T')[0]; // YYYY-MM-DD Format
const response = await apiClient.get(`/diary/${this.currentClub}`);
console.log('Training check response:', response.data);
// Die API gibt alle Trainingstage zurück, filtere nach heute
const trainingData = response.data;
@@ -610,7 +608,6 @@ export default {
} else {
this.hasTrainingToday = false;
}
console.log('Training today:', this.hasTrainingToday);
} catch (error) {
console.error('Fehler beim Prüfen des Trainingstags:', error);
this.hasTrainingToday = false;
@@ -625,7 +622,6 @@ export default {
date: this.newDate
});
console.log('Tournament created, response:', r.data);
// Speichere die ID des neuen Turniers
const newTournamentId = r.data.id;
@@ -982,7 +978,6 @@ export default {
},
highlightMatch(player1Id, player2Id, groupId) {
console.log('highlightMatch called:', { player1Id, player2Id, groupId });
// Finde das entsprechende Match (auch unbeendete)
const match = this.matches.find(m =>
@@ -992,17 +987,14 @@ export default {
(m.player1.id === player2Id && m.player2.id === player1Id))
);
console.log('Found match:', match);
if (!match) {
console.log('No match found');
return;
}
// Setze Highlight-Klasse
this.$nextTick(() => {
const matchElement = document.querySelector(`tr[data-match-id="${match.id}"]`);
console.log('Match element:', matchElement);
if (matchElement) {
// Entferne vorherige Highlights
@@ -1019,7 +1011,6 @@ export default {
block: 'center'
});
} else {
console.log('Match element not found in DOM');
}
});
},
@@ -1083,23 +1074,18 @@ export default {
const today = new Date().toISOString().split('T')[0]; // YYYY-MM-DD Format
const response = await apiClient.get(`/diary/${this.currentClub}`);
console.log('Training response:', response.data);
console.log('Looking for date:', today);
// Die API gibt alle Trainingstage zurück, filtere nach heute
const trainingData = response.data;
if (Array.isArray(trainingData)) {
console.log('Available training dates:', trainingData.map(t => t.date));
// Finde den Trainingstag für heute
const todayTraining = trainingData.find(training => training.date === today);
console.log('Today training found:', todayTraining);
if (todayTraining) {
// Lade die Teilnehmer für diesen Trainingstag über die Participant-API
const participantsResponse = await apiClient.get(`/participants/${todayTraining.id}`);
console.log('Participants response:', participantsResponse.data);
const participants = participantsResponse.data;