Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dbede48d4f | ||
|
|
6cd3c3a020 | ||
|
|
7ecbef806d | ||
|
|
1c70ca97bb | ||
|
|
a6493990d3 | ||
|
|
f8f4d23c4e |
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
128
backend/controllers/clubTeamController.js
Normal file
128
backend/controllers/clubTeamController.js
Normal 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" });
|
||||
}
|
||||
};
|
||||
@@ -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({});
|
||||
|
||||
@@ -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' });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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' });
|
||||
}
|
||||
};
|
||||
|
||||
133
backend/controllers/myTischtennisController.js
Normal file
133
backend/controllers/myTischtennisController.js
Normal 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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
103
backend/controllers/seasonController.js
Normal file
103
backend/controllers/seasonController.js
Normal 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" });
|
||||
}
|
||||
}
|
||||
};
|
||||
130
backend/controllers/teamController.js
Normal file
130
backend/controllers/teamController.js
Normal 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" });
|
||||
}
|
||||
};
|
||||
215
backend/controllers/teamDocumentController.js
Normal file
215
backend/controllers/teamDocumentController.js
Normal 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" });
|
||||
}
|
||||
};
|
||||
44
backend/migrations/add_season_to_teams.sql
Normal file
44
backend/migrations/add_season_to_teams.sql
Normal 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;
|
||||
54
backend/models/ClubTeam.js
Normal file
54
backend/models/ClubTeam.js
Normal 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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
52
backend/models/TeamDocument.js
Normal file
52
backend/models/TeamDocument.js
Normal 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;
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
32
backend/routes/clubTeamRoutes.js
Normal file
32
backend/routes/clubTeamRoutes.js
Normal 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;
|
||||
@@ -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;
|
||||
|
||||
29
backend/routes/myTischtennisRoutes.js
Normal file
29
backend/routes/myTischtennisRoutes.js
Normal 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;
|
||||
|
||||
28
backend/routes/seasonRoutes.js
Normal file
28
backend/routes/seasonRoutes.js
Normal 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;
|
||||
33
backend/routes/teamDocumentRoutes.js
Normal file
33
backend/routes/teamDocumentRoutes.js
Normal 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;
|
||||
32
backend/routes/teamRoutes.js
Normal file
32
backend/routes/teamRoutes.js
Normal 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;
|
||||
@@ -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')));
|
||||
|
||||
|
||||
180
backend/services/clubTeamService.js
Normal file
180
backend/services/clubTeamService.js
Normal 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;
|
||||
@@ -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,
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
94
backend/services/leagueService.js
Normal file
94
backend/services/leagueService.js
Normal 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;
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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.`;
|
||||
|
||||
241
backend/services/myTischtennisService.js
Normal file
241
backend/services/myTischtennisService.js
Normal 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();
|
||||
|
||||
638
backend/services/pdfParserService.js
Normal file
638
backend/services/pdfParserService.js
Normal 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;
|
||||
@@ -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 };
|
||||
}
|
||||
}
|
||||
|
||||
149
backend/services/seasonService.js
Normal file
149
backend/services/seasonService.js
Normal 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;
|
||||
188
backend/services/teamDocumentService.js
Normal file
188
backend/services/teamDocumentService.js
Normal 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;
|
||||
132
backend/services/teamService.js
Normal file
132
backend/services/teamService.js
Normal 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;
|
||||
@@ -203,11 +203,8 @@ class TournamentService {
|
||||
}
|
||||
|
||||
// 4) Round‑Robin 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}`);
|
||||
}
|
||||
|
||||
7
backend/test-spielplan.txt
Normal file
7
backend/test-spielplan.txt
Normal 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
|
||||
BIN
backend/uploads/team-documents/7_code_list_1759354257578.pdf
Normal file
BIN
backend/uploads/team-documents/7_code_list_1759354257578.pdf
Normal file
Binary file not shown.
BIN
backend/uploads/team-documents/9_code_list_1759357969975.pdf
Normal file
BIN
backend/uploads/team-documents/9_code_list_1759357969975.pdf
Normal file
Binary file not shown.
BIN
backend/uploads/team-documents/9_pin_list_1759386673266.pdf
Normal file
BIN
backend/uploads/team-documents/9_pin_list_1759386673266.pdf
Normal file
Binary file not shown.
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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');
|
||||
},
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
299
frontend/src/components/SeasonSelector.vue
Normal file
299
frontend/src/components/SeasonSelector.vue
Normal 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>
|
||||
@@ -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 },
|
||||
];
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
1251
frontend/src/views/TeamManagementView.vue
Normal file
1251
frontend/src/views/TeamManagementView.vue
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user