Compare commits
6 Commits
83f4e1c45e
...
mytischten
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e81a1c4a7 | ||
|
|
b2017b7365 | ||
|
|
b3bbca3887 | ||
|
|
0ee9e486b5 | ||
|
|
00e058a665 | ||
|
|
e5a0dfdddc |
@@ -1,4 +1,5 @@
|
||||
import axios from 'axios';
|
||||
import { chromium } from 'playwright';
|
||||
|
||||
const BASE_URL = 'https://www.mytischtennis.de';
|
||||
|
||||
@@ -24,36 +25,136 @@ class MyTischtennisClient {
|
||||
async getLoginPage() {
|
||||
try {
|
||||
const response = await this.client.get('/login?next=%2F');
|
||||
const html = response.data;
|
||||
const html = typeof response.data === 'string' ? response.data : String(response.data || '');
|
||||
|
||||
const extractFirst = (patterns) => {
|
||||
for (const pattern of patterns) {
|
||||
const match = html.match(pattern);
|
||||
if (match && (match[1] || match[2] || match[3])) {
|
||||
return match[1] || match[2] || match[3];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// Parse form action and input fields for frontend login-form endpoint
|
||||
const formMatch = html.match(/<form[^>]*action=(?:"([^"]+)"|'([^']+)')[^>]*>([\s\S]*?)<\/form>/i);
|
||||
const loginAction = formMatch ? (formMatch[1] || formMatch[2] || '/login') : '/login';
|
||||
const formHtml = formMatch ? formMatch[3] : html;
|
||||
const fields = [];
|
||||
|
||||
const inputRegex = /<input\b([\s\S]*?)>/gi;
|
||||
let inputMatch = null;
|
||||
while ((inputMatch = inputRegex.exec(formHtml)) !== null) {
|
||||
const rawAttributes = inputMatch[1] || '';
|
||||
const attributes = {};
|
||||
|
||||
// Parses key="value", key='value', key=value and boolean attributes.
|
||||
const attributeRegex = /([a-zA-Z_:][-a-zA-Z0-9_:.]*)(?:=(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g;
|
||||
let attributeMatch = null;
|
||||
while ((attributeMatch = attributeRegex.exec(rawAttributes)) !== null) {
|
||||
const key = attributeMatch[1];
|
||||
const value = attributeMatch[2] ?? attributeMatch[3] ?? attributeMatch[4] ?? true;
|
||||
attributes[key] = value;
|
||||
}
|
||||
|
||||
fields.push({
|
||||
name: typeof attributes.name === 'string' ? attributes.name : null,
|
||||
id: typeof attributes.id === 'string' ? attributes.id : null,
|
||||
type: typeof attributes.type === 'string' ? attributes.type : 'text',
|
||||
placeholder: typeof attributes.placeholder === 'string' ? attributes.placeholder : null,
|
||||
autocomplete: typeof attributes.autocomplete === 'string' ? attributes.autocomplete : null,
|
||||
minlength: typeof attributes.minlength === 'string' ? attributes.minlength : null,
|
||||
required: attributes.required === true || attributes.required === 'required',
|
||||
value: typeof attributes.value === 'string' ? attributes.value : null
|
||||
});
|
||||
}
|
||||
|
||||
// Fallback: if page is JS-rendered and no input tags are server-rendered, provide usable defaults.
|
||||
const hasEmailField = fields.some((f) => f?.name === 'email' || f?.type === 'email');
|
||||
const hasPasswordField = fields.some((f) => f?.name === 'password' || f?.type === 'password');
|
||||
if (!hasEmailField) {
|
||||
fields.push({
|
||||
name: 'email',
|
||||
id: null,
|
||||
type: 'email',
|
||||
placeholder: null,
|
||||
autocomplete: 'email',
|
||||
minlength: null,
|
||||
required: true,
|
||||
value: null
|
||||
});
|
||||
}
|
||||
if (!hasPasswordField) {
|
||||
fields.push({
|
||||
name: 'password',
|
||||
id: null,
|
||||
type: 'password',
|
||||
placeholder: null,
|
||||
autocomplete: 'current-password',
|
||||
minlength: null,
|
||||
required: true,
|
||||
value: null
|
||||
});
|
||||
}
|
||||
|
||||
// Extract XSRF token from hidden input
|
||||
const xsrfMatch = html.match(/<input[^>]*name="xsrf"[^>]*value="([^"]+)"/);
|
||||
const xsrfToken = xsrfMatch ? xsrfMatch[1] : null;
|
||||
const xsrfToken = extractFirst([
|
||||
/<input[^>]*name=(?:"xsrf"|'xsrf')[^>]*value=(?:"([^"]+)"|'([^']+)')/i,
|
||||
/(?:^|[,{])\s*"xsrf"\s*:\s*"([^"]+)"/i
|
||||
]);
|
||||
|
||||
// Extract CAPTCHA token from hidden input (if present)
|
||||
const captchaMatch = html.match(/<input[^>]*name="captcha"[^>]*value="([^"]+)"/);
|
||||
const captchaToken = captchaMatch ? captchaMatch[1] : null;
|
||||
const captchaToken = extractFirst([
|
||||
/<input[^>]*name=(?:"captcha"|'captcha')[^>]*value=(?:"([^"]+)"|'([^']+)')/i,
|
||||
/(?:^|[,{])\s*"captcha"\s*:\s*"([^"]+)"/i
|
||||
]);
|
||||
|
||||
// Check if captcha_clicked is true or false
|
||||
const captchaClickedMatch = html.match(/<input[^>]*name="captcha_clicked"[^>]*value="([^"]+)"/);
|
||||
const captchaClicked = captchaClickedMatch ? captchaClickedMatch[1] === 'true' : false;
|
||||
const captchaClickedRaw = extractFirst([
|
||||
/<input[^>]*name=(?:"captcha_clicked"|'captcha_clicked')[^>]*value=(?:"([^"]+)"|'([^']+)')/i,
|
||||
/(?:^|[,{])\s*"captcha_clicked"\s*:\s*"([^"]+)"/i
|
||||
]);
|
||||
const captchaClicked = String(captchaClickedRaw || '').toLowerCase() === 'true';
|
||||
|
||||
// Check if CAPTCHA is required (look for private-captcha element or captcha input)
|
||||
const requiresCaptcha = html.includes('private-captcha') || html.includes('name="captcha"');
|
||||
const requiresCaptcha = html.includes('private-captcha')
|
||||
|| html.includes('name="captcha"')
|
||||
|| html.includes("name='captcha'")
|
||||
|| /captcha/i.test(html);
|
||||
|
||||
// Extract CAPTCHA metadata used by frontend
|
||||
const captchaSiteKey = extractFirst([
|
||||
/data-sitekey=(?:"([^"]+)"|'([^']+)'|([^\s>]+))/i,
|
||||
/(?:^|[,{])\s*"sitekey"\s*:\s*"([^"]+)"/i,
|
||||
/(?:^|[,{])\s*"captchaSiteKey"\s*:\s*"([^"]+)"/i
|
||||
]);
|
||||
const captchaPuzzleEndpoint = extractFirst([
|
||||
/data-puzzle-endpoint=(?:"([^"]+)"|'([^']+)'|([^\s>]+))/i,
|
||||
/(?:^|[,{])\s*"puzzle_endpoint"\s*:\s*"([^"]+)"/i,
|
||||
/(?:^|[,{])\s*"captchaPuzzleEndpoint"\s*:\s*"([^"]+)"/i
|
||||
]);
|
||||
|
||||
console.log('[myTischtennisClient.getLoginPage]', {
|
||||
hasXsrfToken: !!xsrfToken,
|
||||
hasCaptchaToken: !!captchaToken,
|
||||
captchaClicked,
|
||||
requiresCaptcha
|
||||
requiresCaptcha,
|
||||
fieldsCount: fields.length,
|
||||
hasCaptchaSiteKey: !!captchaSiteKey,
|
||||
hasCaptchaPuzzleEndpoint: !!captchaPuzzleEndpoint
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
loginAction,
|
||||
fields,
|
||||
xsrfToken,
|
||||
captchaToken,
|
||||
captchaClicked,
|
||||
requiresCaptcha
|
||||
requiresCaptcha,
|
||||
captchaSiteKey,
|
||||
captchaPuzzleEndpoint
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error fetching login page:', error.message);
|
||||
@@ -247,6 +348,202 @@ class MyTischtennisClient {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Browser-based fallback login for CAPTCHA flows.
|
||||
* @param {string} email
|
||||
* @param {string} password
|
||||
* @returns {Promise<Object>} Login response with token and session data
|
||||
*/
|
||||
async loginWithBrowserAutomation(email, password) {
|
||||
let browser = null;
|
||||
let context = null;
|
||||
try {
|
||||
console.log('[myTischtennisClient.playwright] Start browser login flow');
|
||||
browser = await chromium.launch({
|
||||
headless: true,
|
||||
args: ['--no-sandbox', '--disable-dev-shm-usage']
|
||||
});
|
||||
context = await browser.newContext();
|
||||
const page = await context.newPage();
|
||||
await page.goto(`${this.baseURL}/login?next=%2F`, { waitUntil: 'domcontentloaded', timeout: 45000 });
|
||||
console.log('[myTischtennisClient.playwright] Page loaded');
|
||||
|
||||
// Best-effort: Consent/overlay dialogs that can block form interaction.
|
||||
const consentSelectors = [
|
||||
'#onetrust-accept-btn-handler',
|
||||
'button:has-text("Alle akzeptieren")',
|
||||
'button:has-text("Akzeptieren")',
|
||||
'button:has-text("Einverstanden")'
|
||||
];
|
||||
for (const selector of consentSelectors) {
|
||||
try {
|
||||
const button = page.locator(selector).first();
|
||||
if (await button.count()) {
|
||||
await button.click({ timeout: 1500 });
|
||||
console.log('[myTischtennisClient.playwright] Consent dialog accepted');
|
||||
break;
|
||||
}
|
||||
} catch (_e) {
|
||||
// ignore and try next selector
|
||||
}
|
||||
}
|
||||
|
||||
// Fill credentials
|
||||
await page.locator('input[name="email"]').first().fill(email, { timeout: 10000 });
|
||||
await page.locator('input[name="password"]').first().fill(password, { timeout: 10000 });
|
||||
console.log('[myTischtennisClient.playwright] Credentials filled');
|
||||
|
||||
// Try to interact with private-captcha if present.
|
||||
const captchaHost = page.locator('private-captcha').first();
|
||||
if (await captchaHost.count()) {
|
||||
try {
|
||||
await page.waitForTimeout(1200);
|
||||
const interaction = await page.evaluate(() => {
|
||||
const host = document.querySelector('private-captcha');
|
||||
const checkbox = host?.shadowRoot?.querySelector('#pc-checkbox');
|
||||
if (!checkbox) {
|
||||
return { clicked: false, reason: 'checkbox-missing' };
|
||||
}
|
||||
checkbox.click();
|
||||
checkbox.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
checkbox.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
return {
|
||||
clicked: true,
|
||||
viaShadowRoot: true,
|
||||
className: checkbox.className || null,
|
||||
checked: !!checkbox.checked
|
||||
};
|
||||
});
|
||||
console.log('[myTischtennisClient.playwright] evaluate interaction result:', interaction);
|
||||
|
||||
// Wait until hidden captcha fields are populated by site scripts.
|
||||
try {
|
||||
await page.waitForFunction(() => {
|
||||
const captchaField = document.querySelector('input[name="captcha"]');
|
||||
const clickedField = document.querySelector('input[name="captcha_clicked"]');
|
||||
const captchaValue = (captchaField && captchaField.value ? captchaField.value.trim() : '');
|
||||
const clickedValue = (clickedField && clickedField.value ? clickedField.value.toLowerCase() : '');
|
||||
return captchaValue.length > 80 && (clickedValue === 'true' || clickedValue === '1');
|
||||
}, { timeout: 15000 });
|
||||
const captchaState = await page.evaluate(() => {
|
||||
const captchaField = document.querySelector('input[name="captcha"]');
|
||||
const clickedField = document.querySelector('input[name="captcha_clicked"]');
|
||||
return {
|
||||
captchaLen: captchaField?.value?.length || 0,
|
||||
captchaClicked: clickedField?.value || null
|
||||
};
|
||||
});
|
||||
console.log('[myTischtennisClient.playwright] Captcha value ready:', captchaState);
|
||||
} catch (_waitErr) {
|
||||
// Keep going; some flows still succeed without explicit hidden field update.
|
||||
console.warn('[myTischtennisClient.playwright] Captcha value not ready in time');
|
||||
}
|
||||
} catch (captchaError) {
|
||||
console.warn('[myTischtennisClient.playwright] Captcha interaction warning:', captchaError?.message || captchaError);
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure captcha_clicked field is set if available.
|
||||
await page.evaluate(() => {
|
||||
const clickedField = document.querySelector('input[name="captcha_clicked"]');
|
||||
if (clickedField && !clickedField.value) {
|
||||
clickedField.value = 'true';
|
||||
}
|
||||
});
|
||||
|
||||
// Submit form
|
||||
const submitButton = page.locator('button[type="submit"], input[type="submit"]').first();
|
||||
if (await submitButton.count()) {
|
||||
await submitButton.click({ timeout: 15000, noWaitAfter: true });
|
||||
} else {
|
||||
await page.keyboard.press('Enter');
|
||||
}
|
||||
console.log('[myTischtennisClient.playwright] Submit clicked');
|
||||
|
||||
// Wait for auth cookie after submit (polling avoids timing races).
|
||||
let authCookieObj = null;
|
||||
const maxAttempts = 20;
|
||||
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
||||
const cookies = await context.cookies();
|
||||
authCookieObj = cookies.find((c) => c.name === 'sb-10-auth-token');
|
||||
if (authCookieObj?.value) {
|
||||
break;
|
||||
}
|
||||
await page.waitForTimeout(500);
|
||||
}
|
||||
if (!authCookieObj || !authCookieObj.value) {
|
||||
let errorText = null;
|
||||
try {
|
||||
const textContent = await page.locator('body').innerText({ timeout: 1000 });
|
||||
if (textContent?.includes('Captcha-Bestätigung fehlgeschlagen')) {
|
||||
errorText = 'Captcha-Bestätigung fehlgeschlagen';
|
||||
}
|
||||
} catch (_e) {
|
||||
// ignore text read errors
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
error: errorText
|
||||
? `Playwright-Login fehlgeschlagen: ${errorText}`
|
||||
: 'Playwright-Login fehlgeschlagen: Kein sb-10-auth-token Cookie gefunden'
|
||||
};
|
||||
}
|
||||
|
||||
// Cookie value is expected as "base64-<tokenData>"
|
||||
const tokenMatch = String(authCookieObj.value).match(/^base64-(.+)$/);
|
||||
if (!tokenMatch) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Playwright-Login fehlgeschlagen: Token-Format ungültig'
|
||||
};
|
||||
}
|
||||
|
||||
let tokenData;
|
||||
try {
|
||||
tokenData = JSON.parse(Buffer.from(tokenMatch[1], 'base64').toString('utf-8'));
|
||||
} catch (decodeError) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Playwright-Login fehlgeschlagen: Token konnte nicht dekodiert werden (${decodeError.message})`
|
||||
};
|
||||
}
|
||||
|
||||
const cookie = `sb-10-auth-token=${authCookieObj.value}`;
|
||||
console.log('[myTischtennisClient.playwright] Browser login successful');
|
||||
return {
|
||||
success: true,
|
||||
accessToken: tokenData.access_token,
|
||||
refreshToken: tokenData.refresh_token,
|
||||
expiresAt: tokenData.expires_at,
|
||||
expiresIn: tokenData.expires_in,
|
||||
user: tokenData.user,
|
||||
cookie
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[myTischtennisClient.playwright] Browser login failed:', error?.message || error);
|
||||
return {
|
||||
success: false,
|
||||
error: error?.message || 'Playwright-Login fehlgeschlagen'
|
||||
};
|
||||
} finally {
|
||||
if (context) {
|
||||
try {
|
||||
await context.close();
|
||||
} catch (contextCloseError) {
|
||||
console.warn('[myTischtennisClient.playwright] Context close warning:', contextCloseError?.message || contextCloseError);
|
||||
}
|
||||
}
|
||||
if (browser) {
|
||||
try {
|
||||
await browser.close();
|
||||
} catch (browserCloseError) {
|
||||
console.warn('[myTischtennisClient.playwright] Browser close warning:', browserCloseError?.message || browserCloseError);
|
||||
}
|
||||
console.log('[myTischtennisClient.playwright] Browser closed');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify login credentials
|
||||
* @param {string} email - myTischtennis email
|
||||
@@ -411,4 +708,3 @@ class MyTischtennisClient {
|
||||
}
|
||||
|
||||
export default new MyTischtennisClient();
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import MatchService from '../services/matchService.js';
|
||||
import fs from 'fs';
|
||||
|
||||
import { emitScheduleMatchUpdated } from '../services/socketService.js';
|
||||
import { devLog } from '../utils/logger.js';
|
||||
export const uploadCSV = async (req, res) => {
|
||||
try {
|
||||
@@ -116,7 +116,11 @@ export const updateMatchPlayers = async (req, res) => {
|
||||
playersPlanned,
|
||||
playersPlayed
|
||||
);
|
||||
|
||||
|
||||
if (result.clubId) {
|
||||
emitScheduleMatchUpdated(result.clubId, result.id, result.match || null);
|
||||
}
|
||||
|
||||
return res.status(200).json({
|
||||
message: 'Match players updated successfully',
|
||||
data: result
|
||||
|
||||
@@ -1,6 +1,52 @@
|
||||
import myTischtennisService from '../services/myTischtennisService.js';
|
||||
import HttpError from '../exceptions/HttpError.js';
|
||||
import axios from 'axios';
|
||||
import myTischtennisClient from '../clients/myTischtennisClient.js';
|
||||
|
||||
const MYTT_ORIGIN = 'https://www.mytischtennis.de';
|
||||
const MYTT_PROXY_PREFIX = '/api/mytischtennis/proxy';
|
||||
|
||||
function rewriteMytischtennisContent(content) {
|
||||
if (typeof content !== 'string' || !content) {
|
||||
return content;
|
||||
}
|
||||
|
||||
let rewritten = content;
|
||||
|
||||
// Root-relative Build/Fonts über unseren Same-Origin-Proxy laden.
|
||||
rewritten = rewritten.replace(
|
||||
/(["'])\/build\//g,
|
||||
`$1${MYTT_PROXY_PREFIX}/build/`
|
||||
);
|
||||
rewritten = rewritten.replace(
|
||||
/(["'])\/fonts\//g,
|
||||
`$1${MYTT_PROXY_PREFIX}/fonts/`
|
||||
);
|
||||
|
||||
// Absolute Build/Fonts-URLs ebenfalls auf den Proxy biegen.
|
||||
rewritten = rewritten.replace(
|
||||
/https:\/\/www\.mytischtennis\.de\/build\//g,
|
||||
`${MYTT_PROXY_PREFIX}/build/`
|
||||
);
|
||||
rewritten = rewritten.replace(
|
||||
/https:\/\/www\.mytischtennis\.de\/fonts\//g,
|
||||
`${MYTT_PROXY_PREFIX}/fonts/`
|
||||
);
|
||||
|
||||
// CSS url(/fonts/...) Fälle.
|
||||
rewritten = rewritten.replace(
|
||||
/url\((["']?)\/fonts\//g,
|
||||
`url($1${MYTT_PROXY_PREFIX}/fonts/`
|
||||
);
|
||||
|
||||
// Captcha-Endpunkt muss ebenfalls same-origin über Proxy erreichbar sein.
|
||||
rewritten = rewritten.replace(
|
||||
/(["'])\/api\/private-captcha/g,
|
||||
`$1${MYTT_PROXY_PREFIX}/api/private-captcha`
|
||||
);
|
||||
|
||||
return rewritten;
|
||||
}
|
||||
|
||||
class MyTischtennisController {
|
||||
/**
|
||||
@@ -36,6 +82,49 @@ class MyTischtennisController {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/mytischtennis/login-form
|
||||
* Parsed login form data from mytischtennis.de
|
||||
*/
|
||||
async getLoginForm(req, res, next) {
|
||||
try {
|
||||
const myTischtennisClient = (await import('../clients/myTischtennisClient.js')).default;
|
||||
const result = await myTischtennisClient.getLoginPage();
|
||||
|
||||
if (!result.success) {
|
||||
throw new HttpError('Login-Formular konnte nicht geladen werden', 502);
|
||||
}
|
||||
|
||||
const publicFields = (result.fields || [])
|
||||
.filter((field) => ['email', 'password'].includes(field.type) || field.name === 'email' || field.name === 'password')
|
||||
.map((field) => ({
|
||||
name: field.name,
|
||||
id: field.id,
|
||||
type: field.type,
|
||||
placeholder: field.placeholder || null,
|
||||
required: !!field.required,
|
||||
autocomplete: field.autocomplete || null,
|
||||
minlength: field.minlength ? Number(field.minlength) : null
|
||||
}));
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
form: {
|
||||
action: result.loginAction,
|
||||
fields: publicFields
|
||||
},
|
||||
captcha: {
|
||||
required: !!result.requiresCaptcha,
|
||||
siteKey: result.captchaSiteKey || null,
|
||||
puzzleEndpoint: result.captchaPuzzleEndpoint || null,
|
||||
solutionField: result.captchaSolutionField || 'captcha'
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/mytischtennis/account
|
||||
* Create or update myTischtennis account
|
||||
@@ -43,7 +132,9 @@ class MyTischtennisController {
|
||||
async upsertAccount(req, res, next) {
|
||||
try {
|
||||
const userId = req.user.id;
|
||||
const { email, password, savePassword, autoUpdateRatings, userPassword } = req.body;
|
||||
const { email, password, savePassword, userPassword } = req.body;
|
||||
const hasAutoUpdateRatings = Object.prototype.hasOwnProperty.call(req.body, 'autoUpdateRatings');
|
||||
const autoUpdateRatings = hasAutoUpdateRatings ? req.body.autoUpdateRatings : undefined;
|
||||
|
||||
if (!email) {
|
||||
throw new HttpError('E-Mail-Adresse erforderlich', 400);
|
||||
@@ -59,7 +150,7 @@ class MyTischtennisController {
|
||||
email,
|
||||
password,
|
||||
savePassword || false,
|
||||
autoUpdateRatings || false,
|
||||
autoUpdateRatings,
|
||||
userPassword
|
||||
);
|
||||
|
||||
@@ -226,7 +317,7 @@ class MyTischtennisController {
|
||||
req.userId = userId;
|
||||
|
||||
// Lade die Login-Seite von mytischtennis.de
|
||||
const response = await axios.get('https://www.mytischtennis.de/login?next=%2F', {
|
||||
const response = await axios.get(`${MYTT_ORIGIN}/login?next=%2F`, {
|
||||
headers: {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
|
||||
@@ -262,6 +353,14 @@ class MyTischtennisController {
|
||||
/action="\/login/g,
|
||||
'action="/api/mytischtennis/login-submit'
|
||||
);
|
||||
html = rewriteMytischtennisContent(html);
|
||||
|
||||
// MyTischtennis bootet eine große React-App, die im Proxy-Kontext häufig mit
|
||||
// Runtime-Fehlern abstürzt ("Da ist etwas schiefgelaufen"). Für den iframe-Login
|
||||
// reicht die serverseitig gerenderte Form aus; deshalb Bootstrap-Skripte entfernen.
|
||||
html = html.replace(/<script\b[^>]*type=(?:"|')module(?:"|')[^>]*>[\s\S]*?<\/script>/gi, '');
|
||||
html = html.replace(/<script\b[^>]*src=(?:"|')[^"']*\/build\/[^"']*(?:"|')[^>]*>\s*<\/script>/gi, '');
|
||||
html = html.replace(/<link\b[^>]*rel=(?:"|')modulepreload(?:"|')[^>]*>/gi, '');
|
||||
}
|
||||
|
||||
// Setze Content-Type
|
||||
@@ -275,6 +374,55 @@ class MyTischtennisController {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/mytischtennis/proxy/*
|
||||
* Same-Origin-Proxy für mytischtennis Build-/Font-/Captcha-Ressourcen
|
||||
*/
|
||||
async proxyRemote(req, res, next) {
|
||||
try {
|
||||
const proxyPath = req.params[0] || '';
|
||||
const queryString = new URLSearchParams(req.query || {}).toString();
|
||||
const targetUrl = `${MYTT_ORIGIN}/${proxyPath}${queryString ? `?${queryString}` : ''}`;
|
||||
|
||||
const upstream = await axios.get(targetUrl, {
|
||||
responseType: 'arraybuffer',
|
||||
headers: {
|
||||
'User-Agent': req.headers['user-agent'] || 'Mozilla/5.0',
|
||||
'Accept': req.headers.accept || '*/*',
|
||||
'Accept-Language': req.headers['accept-language'] || 'de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7',
|
||||
...(req.headers.cookie ? { 'Cookie': req.headers.cookie } : {})
|
||||
},
|
||||
validateStatus: () => true
|
||||
});
|
||||
|
||||
// Wichtige Header durchreichen
|
||||
const passthroughHeaders = ['content-type', 'cache-control', 'etag', 'last-modified', 'expires'];
|
||||
for (const headerName of passthroughHeaders) {
|
||||
const value = upstream.headers[headerName];
|
||||
if (value) {
|
||||
res.setHeader(headerName, value);
|
||||
}
|
||||
}
|
||||
if (upstream.headers['set-cookie']) {
|
||||
res.setHeader('Set-Cookie', upstream.headers['set-cookie']);
|
||||
}
|
||||
|
||||
const contentType = String(upstream.headers['content-type'] || '').toLowerCase();
|
||||
const isTextLike = /(text\/|javascript|json|xml|svg)/.test(contentType);
|
||||
|
||||
if (isTextLike) {
|
||||
const asText = Buffer.from(upstream.data).toString('utf-8');
|
||||
const rewritten = rewriteMytischtennisContent(asText);
|
||||
return res.status(upstream.status).send(rewritten);
|
||||
}
|
||||
|
||||
return res.status(upstream.status).send(upstream.data);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Proxy von mytischtennis-Ressourcen:', error.message);
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/mytischtennis/login-submit
|
||||
* Proxy für Login-Form-Submission
|
||||
@@ -300,14 +448,81 @@ class MyTischtennisController {
|
||||
if (req.body.__token) {
|
||||
delete req.body.__token;
|
||||
}
|
||||
|
||||
// Hole Cookies aus dem Request
|
||||
|
||||
// Hole Cookies aus dem Request (wird auch für CAPTCHA-Fallback benötigt)
|
||||
const cookies = req.headers.cookie || '';
|
||||
|
||||
// Normalisiere Payload
|
||||
const payload = { ...(req.body || {}) };
|
||||
const mask = (v) => (typeof v === 'string' && v.length > 12 ? `${v.slice(0, 12)}...(${v.length})` : v);
|
||||
|
||||
// Falls captcha im Browser-Kontext nicht gesetzt wurde, versuche serverseitigen Fallback
|
||||
if (!payload.captcha) {
|
||||
try {
|
||||
const loginPageResponse = await axios.get('https://www.mytischtennis.de/login?next=%2F', {
|
||||
headers: {
|
||||
'Cookie': cookies,
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
||||
'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',
|
||||
'Referer': 'https://www.mytischtennis.de/'
|
||||
},
|
||||
validateStatus: () => true
|
||||
});
|
||||
|
||||
const html = typeof loginPageResponse.data === 'string' ? loginPageResponse.data : '';
|
||||
const siteKeyMatch = html.match(/data-sitekey=(?:"([^"]+)"|'([^']+)')/i);
|
||||
const puzzleEndpointMatch = html.match(/data-puzzle-endpoint=(?:"([^"]+)"|'([^']+)')/i);
|
||||
const siteKey = siteKeyMatch ? (siteKeyMatch[1] || siteKeyMatch[2]) : null;
|
||||
const puzzleEndpoint = puzzleEndpointMatch ? (puzzleEndpointMatch[1] || puzzleEndpointMatch[2]) : null;
|
||||
|
||||
if (siteKey && puzzleEndpoint) {
|
||||
const puzzleResponse = await axios.get(`${puzzleEndpoint}?sitekey=${encodeURIComponent(siteKey)}`, {
|
||||
headers: {
|
||||
'Cookie': cookies,
|
||||
'Accept': '*/*',
|
||||
'Origin': 'https://www.mytischtennis.de',
|
||||
'Referer': 'https://www.mytischtennis.de/'
|
||||
},
|
||||
validateStatus: () => true
|
||||
});
|
||||
|
||||
if (puzzleResponse.status === 200 && typeof puzzleResponse.data === 'string' && puzzleResponse.data.trim()) {
|
||||
payload.captcha = puzzleResponse.data.trim();
|
||||
payload.captcha_clicked = 'true';
|
||||
}
|
||||
}
|
||||
} catch (captchaFallbackError) {
|
||||
console.warn('[submitLogin] CAPTCHA-Fallback fehlgeschlagen:', captchaFallbackError.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Wenn captcha vorhanden ist, als bestätigt markieren
|
||||
if (payload.captcha && !payload.captcha_clicked) {
|
||||
payload.captcha_clicked = 'true';
|
||||
}
|
||||
|
||||
console.log('[submitLogin] Incoming payload fields:', {
|
||||
keys: Object.keys(payload),
|
||||
hasEmail: !!payload.email,
|
||||
hasPassword: !!payload.password,
|
||||
xsrf: mask(payload.xsrf),
|
||||
captchaClicked: payload.captcha_clicked,
|
||||
captcha: mask(payload.captcha)
|
||||
});
|
||||
|
||||
// Form-Daten sauber als x-www-form-urlencoded serialisieren
|
||||
const formData = new URLSearchParams();
|
||||
for (const [key, value] of Object.entries(payload)) {
|
||||
if (value !== undefined && value !== null) {
|
||||
formData.append(key, String(value));
|
||||
}
|
||||
}
|
||||
|
||||
// Leite den Login-Request an mytischtennis.de weiter
|
||||
const response = await axios.post(
|
||||
'https://www.mytischtennis.de/login?next=%2F&_data=routes%2F_auth%2B%2Flogin',
|
||||
req.body, // Form-Daten
|
||||
formData.toString(),
|
||||
{
|
||||
headers: {
|
||||
'Cookie': cookies,
|
||||
@@ -321,6 +536,34 @@ class MyTischtennisController {
|
||||
}
|
||||
);
|
||||
|
||||
console.log('[submitLogin] Upstream response:', {
|
||||
status: response.status,
|
||||
hasSetCookie: Array.isArray(response.headers['set-cookie']) && response.headers['set-cookie'].length > 0,
|
||||
bodyPreview: typeof response.data === 'string'
|
||||
? response.data.slice(0, 220)
|
||||
: JSON.stringify(response.data || {}).slice(0, 220)
|
||||
});
|
||||
|
||||
// Falls CAPTCHA-Bestätigung im Proxy-Flow fehlschlägt:
|
||||
// Fallback auf echten Browser-Login (Playwright), dann Session direkt speichern.
|
||||
const upstreamBody = typeof response.data === 'string' ? response.data : JSON.stringify(response.data || {});
|
||||
const isCaptchaFailure = response.status === 400
|
||||
&& (upstreamBody.includes('Captcha-Bestätigung fehlgeschlagen') || upstreamBody.includes('Captcha-Bestätigung ist erforderlich'));
|
||||
|
||||
if (isCaptchaFailure && userId && payload.email && payload.password) {
|
||||
console.log('[submitLogin] CAPTCHA-Fehler erkannt, starte Playwright-Fallback...');
|
||||
const browserLogin = await myTischtennisClient.loginWithBrowserAutomation(payload.email, payload.password);
|
||||
|
||||
if (browserLogin.success && browserLogin.cookie) {
|
||||
await this.saveSessionFromCookie(userId, browserLogin.cookie);
|
||||
return res.status(200).send(
|
||||
'<!doctype html><html><body><p>Login erfolgreich. Fenster kann geschlossen werden.</p></body></html>'
|
||||
);
|
||||
}
|
||||
|
||||
console.warn('[submitLogin] Playwright-Fallback fehlgeschlagen:', browserLogin.error);
|
||||
}
|
||||
|
||||
// Setze Cookies aus der Response
|
||||
const setCookieHeaders = response.headers['set-cookie'];
|
||||
if (setCookieHeaders) {
|
||||
|
||||
73
backend/package-lock.json
generated
73
backend/package-lock.json
generated
@@ -27,6 +27,7 @@
|
||||
"nodemailer": "^7.0.9",
|
||||
"pdf-parse": "^1.1.1",
|
||||
"pdfjs-dist": "^5.4.394",
|
||||
"playwright": "^1.58.2",
|
||||
"sequelize": "^6.37.3",
|
||||
"sharp": "^0.33.5",
|
||||
"socket.io": "^4.8.1"
|
||||
@@ -975,9 +976,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ajv": {
|
||||
"version": "6.12.6",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
|
||||
"integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
@@ -1059,13 +1060,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.13.4",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.4.tgz",
|
||||
"integrity": "sha512-1wVkUaAO6WyaYtCkcYCOx12ZgpGf9Zif+qXa4n+oYzK558YryKqiL6UWwd5DqiH3VRW0GYhTZQ/vlgJrCoNQlg==",
|
||||
"version": "1.13.5",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz",
|
||||
"integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.4",
|
||||
"follow-redirects": "^1.15.11",
|
||||
"form-data": "^4.0.5",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
@@ -2796,9 +2797,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
|
||||
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
@@ -3275,6 +3276,50 @@
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.58.2",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz",
|
||||
"integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.58.2"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.58.2",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz",
|
||||
"integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright/node_modules/fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prelude-ls": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
||||
@@ -3330,9 +3375,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.14.1",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz",
|
||||
"integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==",
|
||||
"version": "6.14.2",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz",
|
||||
"integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"side-channel": "^1.1.0"
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
"nodemailer": "^7.0.9",
|
||||
"pdf-parse": "^1.1.1",
|
||||
"pdfjs-dist": "^5.4.394",
|
||||
"playwright": "^1.58.2",
|
||||
"sequelize": "^6.37.3",
|
||||
"sharp": "^0.33.5",
|
||||
"socket.io": "^4.8.1"
|
||||
|
||||
@@ -10,6 +10,9 @@ const router = express.Router();
|
||||
// GET /api/mytischtennis/login-page - Proxy für Login-Seite (für iframe)
|
||||
router.get('/login-page', myTischtennisController.getLoginPage);
|
||||
|
||||
// GET /api/mytischtennis/proxy/* - Same-Origin-Proxy für mytischtennis Assets/APIs
|
||||
router.get('/proxy/*', myTischtennisController.proxyRemote);
|
||||
|
||||
// POST /api/mytischtennis/login-submit - Proxy für Login-Form-Submission
|
||||
router.post('/login-submit', myTischtennisController.submitLogin);
|
||||
|
||||
@@ -25,6 +28,9 @@ router.get('/account', myTischtennisController.getAccount);
|
||||
// GET /api/mytischtennis/status - Check status (alle dürfen lesen)
|
||||
router.get('/status', myTischtennisController.getStatus);
|
||||
|
||||
// GET /api/mytischtennis/login-form - Parse mytischtennis Login-Form
|
||||
router.get('/login-form', myTischtennisController.getLoginForm);
|
||||
|
||||
// POST /api/mytischtennis/account - Create or update account (alle dürfen bearbeiten)
|
||||
router.post('/account', myTischtennisController.upsertAccount);
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import express from 'express';
|
||||
import fetch from 'node-fetch';
|
||||
import { emitMatchReportSubmitted } from '../services/socketService.js';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
@@ -256,6 +257,12 @@ router.put('/submit/:uuid', async (req, res) => {
|
||||
'Cache-Control': 'no-cache, no-store, must-revalidate'
|
||||
});
|
||||
|
||||
const clubId = reportData.clubId;
|
||||
const matchCode = reportData.gameCode || reportData.code;
|
||||
if (clubId && (matchCode || uuid)) {
|
||||
emitMatchReportSubmitted(clubId, matchCode || uuid, reportData);
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: responseData,
|
||||
@@ -271,6 +278,17 @@ router.put('/submit/:uuid', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Nur Broadcast: aktueller Spielberichtsentwurf (z. B. Satzergebnisse) an andere Clients senden, ohne bei nuscore zu speichern
|
||||
router.post('/broadcast-draft', (req, res) => {
|
||||
const { clubId, gameCode, matchData } = req.body || {};
|
||||
if (!clubId || !(gameCode ?? matchData?.gameCode ?? matchData?.code)) {
|
||||
return res.status(400).json({ error: 'clubId und gameCode erforderlich' });
|
||||
}
|
||||
const code = String(gameCode ?? matchData?.gameCode ?? matchData?.code ?? '');
|
||||
emitMatchReportSubmitted(clubId, code, matchData || null);
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
// Validate Meeting Report API-Endpunkt (für Zwischenspeicherung)
|
||||
router.put('/validate/:uuid', async (req, res) => {
|
||||
const { uuid } = req.params;
|
||||
|
||||
@@ -70,6 +70,7 @@ app.use(cors({
|
||||
}));
|
||||
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
|
||||
// Request Logging Middleware - loggt alle API-Requests
|
||||
// Wichtig: userId wird später in authMiddleware gesetzt, aber Middleware funktioniert auch ohne
|
||||
|
||||
@@ -12,6 +12,7 @@ import Club from '../models/Club.js';
|
||||
import SeasonService from './seasonService.js';
|
||||
import { checkAccess } from '../utils/userUtils.js';
|
||||
import { Op } from 'sequelize';
|
||||
import HttpError from '../exceptions/HttpError.js';
|
||||
|
||||
import { devLog } from '../utils/logger.js';
|
||||
class MatchService {
|
||||
@@ -467,12 +468,59 @@ class MatchService {
|
||||
playersPlanned: plannedList !== null ? plannedList : (match.playersPlanned || []),
|
||||
playersPlayed: playedList !== null ? playedList : (match.playersPlayed || [])
|
||||
});
|
||||
|
||||
|
||||
// Aktualisiertes Match nochmals laden und für WebSocket-Broadcast anreichern (gleiche Struktur wie getMatchesForLeague)
|
||||
const updated = await Match.findByPk(matchId);
|
||||
if (!updated) {
|
||||
throw new HttpError('Match not found after update', 404);
|
||||
}
|
||||
const enriched = {
|
||||
id: updated.id,
|
||||
date: updated.date,
|
||||
time: updated.time,
|
||||
homeTeamId: updated.homeTeamId,
|
||||
guestTeamId: updated.guestTeamId,
|
||||
locationId: updated.locationId,
|
||||
leagueId: updated.leagueId,
|
||||
code: updated.code,
|
||||
homePin: updated.homePin,
|
||||
guestPin: updated.guestPin,
|
||||
homeMatchPoints: updated.homeMatchPoints || 0,
|
||||
guestMatchPoints: updated.guestMatchPoints || 0,
|
||||
isCompleted: updated.isCompleted || false,
|
||||
pdfUrl: updated.pdfUrl,
|
||||
playersReady: updated.playersReady || [],
|
||||
playersPlanned: updated.playersPlanned || [],
|
||||
playersPlayed: updated.playersPlayed || [],
|
||||
homeTeam: { name: 'Unbekannt' },
|
||||
guestTeam: { name: 'Unbekannt' },
|
||||
location: { name: 'Unbekannt', address: '', city: '', zip: '' },
|
||||
leagueDetails: { name: 'Unbekannt' }
|
||||
};
|
||||
if (updated.homeTeamId) {
|
||||
const homeTeam = await Team.findByPk(updated.homeTeamId, { attributes: ['name'] });
|
||||
if (homeTeam) enriched.homeTeam = homeTeam;
|
||||
}
|
||||
if (updated.guestTeamId) {
|
||||
const guestTeam = await Team.findByPk(updated.guestTeamId, { attributes: ['name'] });
|
||||
if (guestTeam) enriched.guestTeam = guestTeam;
|
||||
}
|
||||
if (updated.locationId) {
|
||||
const location = await Location.findByPk(updated.locationId, { attributes: ['name', 'address', 'city', 'zip'] });
|
||||
if (location) enriched.location = location;
|
||||
}
|
||||
if (updated.leagueId) {
|
||||
const league = await League.findByPk(updated.leagueId, { attributes: ['name'] });
|
||||
if (league) enriched.leagueDetails = league;
|
||||
}
|
||||
|
||||
return {
|
||||
id: match.id,
|
||||
playersReady: match.playersReady,
|
||||
playersPlanned: match.playersPlanned,
|
||||
playersPlayed: match.playersPlayed
|
||||
id: updated.id,
|
||||
clubId: updated.clubId,
|
||||
playersReady: updated.playersReady,
|
||||
playersPlanned: updated.playersPlanned,
|
||||
playersPlayed: updated.playersPlayed,
|
||||
match: enriched
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -58,6 +58,15 @@ class MyTischtennisService {
|
||||
|
||||
// Login-Versuch bei myTischtennis
|
||||
loginResult = await myTischtennisClient.login(email, password);
|
||||
if (!loginResult.success && loginResult.requiresCaptcha) {
|
||||
console.log('[myTischtennisService.upsertAccount] CAPTCHA-Fehler, versuche Playwright-Fallback...');
|
||||
const playwrightResult = await myTischtennisClient.loginWithBrowserAutomation(email, password);
|
||||
if (playwrightResult.success) {
|
||||
loginResult = playwrightResult;
|
||||
} else {
|
||||
console.warn('[myTischtennisService.upsertAccount] Playwright-Fallback fehlgeschlagen:', playwrightResult.error);
|
||||
}
|
||||
}
|
||||
if (!loginResult.success) {
|
||||
const statusCode = loginResult.requiresCaptcha ? 400 : 401;
|
||||
const errorMessage = loginResult.error || 'myTischtennis-Login fehlgeschlagen. Bitte überprüfen Sie Ihre Zugangsdaten.';
|
||||
@@ -74,10 +83,14 @@ class MyTischtennisService {
|
||||
const now = new Date();
|
||||
|
||||
if (account) {
|
||||
const effectiveAutoUpdateRatings = autoUpdateRatings === undefined
|
||||
? account.autoUpdateRatings
|
||||
: Boolean(autoUpdateRatings);
|
||||
|
||||
// Update existing
|
||||
account.email = email;
|
||||
account.savePassword = savePassword;
|
||||
account.autoUpdateRatings = autoUpdateRatings;
|
||||
account.autoUpdateRatings = savePassword ? effectiveAutoUpdateRatings : false;
|
||||
|
||||
if (password && savePassword) {
|
||||
account.setPassword(password);
|
||||
@@ -115,7 +128,7 @@ class MyTischtennisService {
|
||||
userId,
|
||||
email,
|
||||
savePassword,
|
||||
autoUpdateRatings,
|
||||
autoUpdateRatings: savePassword ? Boolean(autoUpdateRatings) : false,
|
||||
lastLoginAttempt: password ? now : null,
|
||||
lastLoginSuccess: loginResult?.success ? now : null
|
||||
};
|
||||
@@ -225,18 +238,48 @@ class MyTischtennisService {
|
||||
// Login-Versuch mit Passwort
|
||||
console.log('[myTischtennisService.verifyLogin] Attempting login for user:', account.email);
|
||||
const loginResult = await myTischtennisClient.login(account.email, password);
|
||||
console.log('[myTischtennisService.verifyLogin] Login result:', { success: loginResult.success, error: loginResult.error, requiresCaptcha: loginResult.requiresCaptcha });
|
||||
let effectiveLoginResult = loginResult;
|
||||
if (!effectiveLoginResult.success && effectiveLoginResult.requiresCaptcha) {
|
||||
console.log('[myTischtennisService.verifyLogin] CAPTCHA-Fehler, versuche Playwright-Fallback...');
|
||||
try {
|
||||
const playwrightResult = await myTischtennisClient.loginWithBrowserAutomation(account.email, password);
|
||||
if (playwrightResult.success) {
|
||||
effectiveLoginResult = playwrightResult;
|
||||
} else {
|
||||
console.warn('[myTischtennisService.verifyLogin] Playwright-Fallback fehlgeschlagen:', playwrightResult.error);
|
||||
effectiveLoginResult = {
|
||||
success: false,
|
||||
error: playwrightResult.error || 'Playwright-Fallback fehlgeschlagen',
|
||||
requiresCaptcha: true,
|
||||
status: 400
|
||||
};
|
||||
}
|
||||
} catch (playwrightError) {
|
||||
console.warn('[myTischtennisService.verifyLogin] Playwright-Fallback Exception:', playwrightError?.message || playwrightError);
|
||||
effectiveLoginResult = {
|
||||
success: false,
|
||||
error: `Playwright-Fallback Exception: ${playwrightError?.message || 'Unbekannter Fehler'}`,
|
||||
requiresCaptcha: true,
|
||||
status: 400
|
||||
};
|
||||
}
|
||||
}
|
||||
console.log('[myTischtennisService.verifyLogin] Login result:', {
|
||||
success: effectiveLoginResult.success,
|
||||
error: effectiveLoginResult.error,
|
||||
requiresCaptcha: effectiveLoginResult.requiresCaptcha
|
||||
});
|
||||
|
||||
if (loginResult.success) {
|
||||
if (effectiveLoginResult.success) {
|
||||
account.lastLoginSuccess = now;
|
||||
account.accessToken = loginResult.accessToken;
|
||||
account.refreshToken = loginResult.refreshToken;
|
||||
account.expiresAt = loginResult.expiresAt;
|
||||
account.cookie = loginResult.cookie;
|
||||
account.userData = loginResult.user;
|
||||
account.accessToken = effectiveLoginResult.accessToken;
|
||||
account.refreshToken = effectiveLoginResult.refreshToken;
|
||||
account.expiresAt = effectiveLoginResult.expiresAt;
|
||||
account.cookie = effectiveLoginResult.cookie;
|
||||
account.userData = effectiveLoginResult.user;
|
||||
|
||||
// Hole Club-ID und Federation
|
||||
const profileResult = await myTischtennisClient.getUserProfile(loginResult.cookie);
|
||||
const profileResult = await myTischtennisClient.getUserProfile(effectiveLoginResult.cookie);
|
||||
|
||||
if (profileResult.success) {
|
||||
account.clubId = profileResult.clubId || account.clubId;
|
||||
@@ -250,25 +293,31 @@ class MyTischtennisService {
|
||||
|
||||
return {
|
||||
success: true,
|
||||
accessToken: loginResult.accessToken,
|
||||
refreshToken: loginResult.refreshToken,
|
||||
expiresAt: loginResult.expiresAt,
|
||||
user: loginResult.user,
|
||||
accessToken: effectiveLoginResult.accessToken,
|
||||
refreshToken: effectiveLoginResult.refreshToken,
|
||||
expiresAt: effectiveLoginResult.expiresAt,
|
||||
user: effectiveLoginResult.user,
|
||||
clubId: account.clubId,
|
||||
clubName: account.clubName
|
||||
};
|
||||
} else {
|
||||
// Prevent stale "success" from previously valid sessions after an explicit failed login attempt.
|
||||
account.accessToken = null;
|
||||
account.refreshToken = null;
|
||||
account.cookie = null;
|
||||
account.expiresAt = null;
|
||||
account.userData = null;
|
||||
await account.save(); // Save lastLoginAttempt
|
||||
const errorMessage = loginResult.error || 'myTischtennis-Login fehlgeschlagen';
|
||||
const errorMessage = effectiveLoginResult.error || 'myTischtennis-Login fehlgeschlagen';
|
||||
// Verwende den Status-Code vom myTischtennisClient, falls vorhanden, sonst 401
|
||||
// Wenn CAPTCHA erforderlich ist, verwende 400 statt 401
|
||||
const statusCode = loginResult.requiresCaptcha
|
||||
const statusCode = effectiveLoginResult.requiresCaptcha
|
||||
? 400
|
||||
: (loginResult.status && loginResult.status >= 400 && loginResult.status < 600
|
||||
? loginResult.status
|
||||
: (effectiveLoginResult.status && effectiveLoginResult.status >= 400 && effectiveLoginResult.status < 600
|
||||
? effectiveLoginResult.status
|
||||
: 401);
|
||||
console.error('[myTischtennisService.verifyLogin] Login failed:', errorMessage, `(Status: ${statusCode})`, loginResult.requiresCaptcha ? '(CAPTCHA erforderlich)' : '');
|
||||
if (loginResult.requiresCaptcha) {
|
||||
console.error('[myTischtennisService.verifyLogin] Login failed:', errorMessage, `(Status: ${statusCode})`, effectiveLoginResult.requiresCaptcha ? '(CAPTCHA erforderlich)' : '');
|
||||
if (effectiveLoginResult.requiresCaptcha) {
|
||||
throw new HttpError({ code: 'ERROR_MYTISCHTENNIS_CAPTCHA_REQUIRED', params: { message: errorMessage } }, statusCode);
|
||||
}
|
||||
throw new HttpError(errorMessage, statusCode);
|
||||
|
||||
@@ -115,7 +115,6 @@ class SchedulerService {
|
||||
|
||||
/**
|
||||
* Manually trigger rating updates (for testing)
|
||||
* HINWEIS: Deaktiviert - automatische MyTischtennis-Abrufe sind nicht mehr verfügbar
|
||||
*/
|
||||
async triggerRatingUpdates() {
|
||||
devLog('[Scheduler] Manual rating updates trigger called');
|
||||
@@ -124,7 +123,6 @@ class SchedulerService {
|
||||
|
||||
/**
|
||||
* Manually trigger match results fetch (for testing)
|
||||
* HINWEIS: Deaktiviert - automatische MyTischtennis-Abrufe sind nicht mehr verfügbar
|
||||
*/
|
||||
async triggerMatchResultsFetch() {
|
||||
devLog('[Scheduler] Manual match results fetch trigger called');
|
||||
|
||||
@@ -225,3 +225,13 @@ export const emitTournamentChanged = (clubId, tournamentId) => {
|
||||
emitToClub(clubId, 'tournament:changed', { tournamentId });
|
||||
};
|
||||
|
||||
// Event wenn Spielerauswahl (Bereit/Geplant/Gespielt) für ein Match geändert wurde (match = vollständiges angereichertes Match-Objekt)
|
||||
export const emitScheduleMatchUpdated = (clubId, matchId, match = null) => {
|
||||
emitToClub(clubId, 'schedule:match:updated', { clubId, matchId, match });
|
||||
};
|
||||
|
||||
// Event wenn Spielbericht (nuscore) abgesendet wurde – matchData = vollständiges Objekt für andere Clients
|
||||
export const emitMatchReportSubmitted = (clubId, matchCode, matchData = null) => {
|
||||
emitToClub(clubId, 'schedule:match-report:submitted', { clubId, matchCode, matchData });
|
||||
};
|
||||
|
||||
|
||||
269
frontend/package-lock.json
generated
269
frontend/package-lock.json
generated
@@ -1080,9 +1080,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||
"version": "4.53.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.2.tgz",
|
||||
"integrity": "sha512-yDPzwsgiFO26RJA4nZo8I+xqzh7sJTZIWQOxn+/XOdPE31lAvLIYCKqjV+lNH/vxE2L2iH3plKxDCRK6i+CwhA==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz",
|
||||
"integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -1094,9 +1094,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm64": {
|
||||
"version": "4.53.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.2.tgz",
|
||||
"integrity": "sha512-k8FontTxIE7b0/OGKeSN5B6j25EuppBcWM33Z19JoVT7UTXFSo3D9CdU39wGTeb29NO3XxpMNauh09B+Ibw+9g==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz",
|
||||
"integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1108,9 +1108,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||
"version": "4.53.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.2.tgz",
|
||||
"integrity": "sha512-A6s4gJpomNBtJ2yioj8bflM2oogDwzUiMl2yNJ2v9E7++sHrSrsQ29fOfn5DM/iCzpWcebNYEdXpaK4tr2RhfQ==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz",
|
||||
"integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1122,9 +1122,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-x64": {
|
||||
"version": "4.53.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.2.tgz",
|
||||
"integrity": "sha512-e6XqVmXlHrBlG56obu9gDRPW3O3hLxpwHpLsBJvuI8qqnsrtSZ9ERoWUXtPOkY8c78WghyPHZdmPhHLWNdAGEw==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz",
|
||||
"integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1136,9 +1136,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-arm64": {
|
||||
"version": "4.53.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.2.tgz",
|
||||
"integrity": "sha512-v0E9lJW8VsrwPux5Qe5CwmH/CF/2mQs6xU1MF3nmUxmZUCHazCjLgYvToOk+YuuUqLQBio1qkkREhxhc656ViA==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz",
|
||||
"integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1150,9 +1150,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-x64": {
|
||||
"version": "4.53.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.2.tgz",
|
||||
"integrity": "sha512-ClAmAPx3ZCHtp6ysl4XEhWU69GUB1D+s7G9YjHGhIGCSrsg00nEGRRZHmINYxkdoJehde8VIsDC5t9C0gb6yqA==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz",
|
||||
"integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1164,9 +1164,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||
"version": "4.53.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.2.tgz",
|
||||
"integrity": "sha512-EPlb95nUsz6Dd9Qy13fI5kUPXNSljaG9FiJ4YUGU1O/Q77i5DYFW5KR8g1OzTcdZUqQQ1KdDqsTohdFVwCwjqg==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz",
|
||||
"integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -1178,9 +1178,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
||||
"version": "4.53.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.2.tgz",
|
||||
"integrity": "sha512-BOmnVW+khAUX+YZvNfa0tGTEMVVEerOxN0pDk2E6N6DsEIa2Ctj48FOMfNDdrwinocKaC7YXUZ1pHlKpnkja/Q==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz",
|
||||
"integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -1192,9 +1192,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||
"version": "4.53.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.2.tgz",
|
||||
"integrity": "sha512-Xt2byDZ+6OVNuREgBXr4+CZDJtrVso5woFtpKdGPhpTPHcNG7D8YXeQzpNbFRxzTVqJf7kvPMCub/pcGUWgBjA==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz",
|
||||
"integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1206,9 +1206,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||
"version": "4.53.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.2.tgz",
|
||||
"integrity": "sha512-+LdZSldy/I9N8+klim/Y1HsKbJ3BbInHav5qE9Iy77dtHC/pibw1SR/fXlWyAk0ThnpRKoODwnAuSjqxFRDHUQ==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz",
|
||||
"integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1220,9 +1220,23 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-loong64-gnu": {
|
||||
"version": "4.53.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.2.tgz",
|
||||
"integrity": "sha512-8ms8sjmyc1jWJS6WdNSA23rEfdjWB30LH8Wqj0Cqvv7qSHnvw6kgMMXRdop6hkmGPlyYBdRPkjJnj3KCUHV/uQ==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz",
|
||||
"integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-loong64-musl": {
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz",
|
||||
"integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
@@ -1234,9 +1248,23 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
|
||||
"version": "4.53.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.2.tgz",
|
||||
"integrity": "sha512-3HRQLUQbpBDMmzoxPJYd3W6vrVHOo2cVW8RUo87Xz0JPJcBLBr5kZ1pGcQAhdZgX9VV7NbGNipah1omKKe23/g==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz",
|
||||
"integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-ppc64-musl": {
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz",
|
||||
"integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -1248,9 +1276,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||
"version": "4.53.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.2.tgz",
|
||||
"integrity": "sha512-fMjKi+ojnmIvhk34gZP94vjogXNNUKMEYs+EDaB/5TG/wUkoeua7p7VCHnE6T2Tx+iaghAqQX8teQzcvrYpaQA==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz",
|
||||
"integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@@ -1262,9 +1290,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
||||
"version": "4.53.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.2.tgz",
|
||||
"integrity": "sha512-XuGFGU+VwUUV5kLvoAdi0Wz5Xbh2SrjIxCtZj6Wq8MDp4bflb/+ThZsVxokM7n0pcbkEr2h5/pzqzDYI7cCgLQ==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz",
|
||||
"integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@@ -1276,9 +1304,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
||||
"version": "4.53.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.2.tgz",
|
||||
"integrity": "sha512-w6yjZF0P+NGzWR3AXWX9zc0DNEGdtvykB03uhonSHMRa+oWA6novflo2WaJr6JZakG2ucsyb+rvhrKac6NIy+w==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz",
|
||||
"integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
@@ -1290,9 +1318,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||
"version": "4.53.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.2.tgz",
|
||||
"integrity": "sha512-yo8d6tdfdeBArzC7T/PnHd7OypfI9cbuZzPnzLJIyKYFhAQ8SvlkKtKBMbXDxe1h03Rcr7u++nFS7tqXz87Gtw==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz",
|
||||
"integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1304,9 +1332,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||
"version": "4.53.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.2.tgz",
|
||||
"integrity": "sha512-ah59c1YkCxKExPP8O9PwOvs+XRLKwh/mV+3YdKqQ5AMQ0r4M4ZDuOrpWkUaqO7fzAHdINzV9tEVu8vNw48z0lA==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz",
|
||||
"integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1317,10 +1345,24 @@
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-openbsd-x64": {
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz",
|
||||
"integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-openharmony-arm64": {
|
||||
"version": "4.53.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.2.tgz",
|
||||
"integrity": "sha512-4VEd19Wmhr+Zy7hbUsFZ6YXEiP48hE//KPLCSVNY5RMGX2/7HZ+QkN55a3atM1C/BZCGIgqN+xrVgtdak2S9+A==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz",
|
||||
"integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1332,9 +1374,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||
"version": "4.53.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.2.tgz",
|
||||
"integrity": "sha512-IlbHFYc/pQCgew/d5fslcy1KEaYVCJ44G8pajugd8VoOEI8ODhtb/j8XMhLpwHCMB3yk2J07ctup10gpw2nyMA==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz",
|
||||
"integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1346,9 +1388,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||
"version": "4.53.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.2.tgz",
|
||||
"integrity": "sha512-lNlPEGgdUfSzdCWU176ku/dQRnA7W+Gp8d+cWv73jYrb8uT7HTVVxq62DUYxjbaByuf1Yk0RIIAbDzp+CnOTFg==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz",
|
||||
"integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -1360,9 +1402,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-gnu": {
|
||||
"version": "4.53.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.2.tgz",
|
||||
"integrity": "sha512-S6YojNVrHybQis2lYov1sd+uj7K0Q05NxHcGktuMMdIQ2VixGwAfbJ23NnlvvVV1bdpR2m5MsNBViHJKcA4ADw==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz",
|
||||
"integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1374,9 +1416,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||
"version": "4.53.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.2.tgz",
|
||||
"integrity": "sha512-k+/Rkcyx//P6fetPoLMb8pBeqJBNGx81uuf7iljX9++yNBVRDQgD04L+SVXmXmh5ZP4/WOp4mWF0kmi06PW2tA==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz",
|
||||
"integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1574,9 +1616,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ajv": {
|
||||
"version": "6.12.6",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
|
||||
"integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -1620,13 +1662,13 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.13.2",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
|
||||
"integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
|
||||
"version": "1.13.5",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz",
|
||||
"integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.4",
|
||||
"follow-redirects": "^1.15.11",
|
||||
"form-data": "^4.0.5",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
@@ -2405,9 +2447,9 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.9",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
|
||||
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
|
||||
"version": "1.15.11",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
|
||||
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
@@ -2425,9 +2467,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
|
||||
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
|
||||
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
@@ -2727,12 +2769,12 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/jspdf": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/jspdf/-/jspdf-4.1.0.tgz",
|
||||
"integrity": "sha512-xd1d/XRkwqnsq6FP3zH1Q+Ejqn2ULIJeDZ+FTKpaabVpZREjsJKRJwuokTNgdqOU+fl55KgbvgZ1pRTSWCP2kQ==",
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/jspdf/-/jspdf-4.2.0.tgz",
|
||||
"integrity": "sha512-hR/hnRevAXXlrjeqU5oahOE+Ln9ORJUB5brLHHqH67A+RBQZuFr5GkbI9XQI8OUFSEezKegsi45QRpc4bGj75Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.28.4",
|
||||
"@babel/runtime": "^7.28.6",
|
||||
"fast-png": "^6.2.0",
|
||||
"fflate": "^0.8.1"
|
||||
},
|
||||
@@ -2861,9 +2903,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
|
||||
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
@@ -3177,9 +3219,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "4.53.2",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.2.tgz",
|
||||
"integrity": "sha512-MHngMYwGJVi6Fmnk6ISmnk7JAHRNF0UkuucA0CUW3N3a4KnONPEZz+vUanQP/ZC/iY1Qkf3bwPWzyY84wEks1g==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz",
|
||||
"integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -3193,28 +3235,31 @@
|
||||
"npm": ">=8.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rollup/rollup-android-arm-eabi": "4.53.2",
|
||||
"@rollup/rollup-android-arm64": "4.53.2",
|
||||
"@rollup/rollup-darwin-arm64": "4.53.2",
|
||||
"@rollup/rollup-darwin-x64": "4.53.2",
|
||||
"@rollup/rollup-freebsd-arm64": "4.53.2",
|
||||
"@rollup/rollup-freebsd-x64": "4.53.2",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.53.2",
|
||||
"@rollup/rollup-linux-arm-musleabihf": "4.53.2",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.53.2",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.53.2",
|
||||
"@rollup/rollup-linux-loong64-gnu": "4.53.2",
|
||||
"@rollup/rollup-linux-ppc64-gnu": "4.53.2",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.53.2",
|
||||
"@rollup/rollup-linux-riscv64-musl": "4.53.2",
|
||||
"@rollup/rollup-linux-s390x-gnu": "4.53.2",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.53.2",
|
||||
"@rollup/rollup-linux-x64-musl": "4.53.2",
|
||||
"@rollup/rollup-openharmony-arm64": "4.53.2",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.53.2",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.53.2",
|
||||
"@rollup/rollup-win32-x64-gnu": "4.53.2",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.53.2",
|
||||
"@rollup/rollup-android-arm-eabi": "4.59.0",
|
||||
"@rollup/rollup-android-arm64": "4.59.0",
|
||||
"@rollup/rollup-darwin-arm64": "4.59.0",
|
||||
"@rollup/rollup-darwin-x64": "4.59.0",
|
||||
"@rollup/rollup-freebsd-arm64": "4.59.0",
|
||||
"@rollup/rollup-freebsd-x64": "4.59.0",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.59.0",
|
||||
"@rollup/rollup-linux-arm-musleabihf": "4.59.0",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.59.0",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.59.0",
|
||||
"@rollup/rollup-linux-loong64-gnu": "4.59.0",
|
||||
"@rollup/rollup-linux-loong64-musl": "4.59.0",
|
||||
"@rollup/rollup-linux-ppc64-gnu": "4.59.0",
|
||||
"@rollup/rollup-linux-ppc64-musl": "4.59.0",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.59.0",
|
||||
"@rollup/rollup-linux-riscv64-musl": "4.59.0",
|
||||
"@rollup/rollup-linux-s390x-gnu": "4.59.0",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.59.0",
|
||||
"@rollup/rollup-linux-x64-musl": "4.59.0",
|
||||
"@rollup/rollup-openbsd-x64": "4.59.0",
|
||||
"@rollup/rollup-openharmony-arm64": "4.59.0",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.59.0",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.59.0",
|
||||
"@rollup/rollup-win32-x64-gnu": "4.59.0",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.59.0",
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -126,7 +126,11 @@
|
||||
</div>
|
||||
|
||||
<main class="main-content">
|
||||
<router-view class="content fade-in"></router-view>
|
||||
<router-view v-slot="{ Component }">
|
||||
<div class="content fade-in">
|
||||
<component :is="Component" />
|
||||
</div>
|
||||
</router-view>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -449,11 +449,25 @@
|
||||
{{ editingSetValue || 'Satz z.B. 11:9' }}
|
||||
</div>
|
||||
<div class="set-keyboard-keys">
|
||||
<button type="button" v-for="n in 9" :key="n" class="set-key" @click="setKeyboardKey(String(n))">{{ n }}</button>
|
||||
<!-- 1 2 3 -->
|
||||
<button type="button" class="set-key" @click="setKeyboardKey('1')">1</button>
|
||||
<button type="button" class="set-key" @click="setKeyboardKey('2')">2</button>
|
||||
<button type="button" class="set-key" @click="setKeyboardKey('3')">3</button>
|
||||
<!-- 4 5 6 -->
|
||||
<button type="button" class="set-key" @click="setKeyboardKey('4')">4</button>
|
||||
<button type="button" class="set-key" @click="setKeyboardKey('5')">5</button>
|
||||
<button type="button" class="set-key" @click="setKeyboardKey('6')">6</button>
|
||||
<!-- 7 8 9 -->
|
||||
<button type="button" class="set-key" @click="setKeyboardKey('7')">7</button>
|
||||
<button type="button" class="set-key" @click="setKeyboardKey('8')">8</button>
|
||||
<button type="button" class="set-key" @click="setKeyboardKey('9')">9</button>
|
||||
<!-- : 0 - -->
|
||||
<button type="button" class="set-key" @click="setKeyboardKey(':')">:</button>
|
||||
<button type="button" class="set-key" @click="setKeyboardKey('0')">0</button>
|
||||
<button type="button" class="set-key" @click="setKeyboardKey('-')">−</button>
|
||||
<!-- <- C OK -->
|
||||
<button type="button" class="set-key set-key-backspace" @click="setKeyboardBackspace">⌫</button>
|
||||
<button type="button" class="set-key set-key-clear" @click="setKeyboardClear">C</button>
|
||||
<button type="button" class="set-key set-key-ok" @click="setKeyboardOk">OK</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -682,6 +696,7 @@
|
||||
<script>
|
||||
import CryptoJS from 'crypto-js';
|
||||
import apiClient, { backendBaseUrl } from '../apiClient';
|
||||
import { onMatchReportSubmitted, offMatchReportSubmitted } from '../services/socketService.js';
|
||||
|
||||
export default {
|
||||
name: 'MatchReportDialog',
|
||||
@@ -718,6 +733,7 @@ export default {
|
||||
errors: [],
|
||||
// Aktive Zelle der schwebenden Satz-Tastatur: { matchIndex, setIndex } oder null
|
||||
editingSetCell: null,
|
||||
broadcastDraftTimer: null,
|
||||
// Abschluss-Felder
|
||||
protestText: '',
|
||||
finalHomePin: '',
|
||||
@@ -849,6 +865,25 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
|
||||
await this.loadData();
|
||||
this.initializeResults();
|
||||
this.initializeFinalPins();
|
||||
this._matchReportSubmittedHandler = (payload) => {
|
||||
if (!payload?.matchCode || !this.match?.code || String(payload.matchCode) !== String(this.match.code)) {
|
||||
return;
|
||||
}
|
||||
if (payload.matchData && this.meetingDetails) {
|
||||
this.applyReceivedMatchData(payload.matchData);
|
||||
} else {
|
||||
this.loadData();
|
||||
}
|
||||
};
|
||||
onMatchReportSubmitted(this._matchReportSubmittedHandler);
|
||||
},
|
||||
beforeUnmount() {
|
||||
if (this._matchReportSubmittedHandler) {
|
||||
offMatchReportSubmitted(this._matchReportSubmittedHandler);
|
||||
}
|
||||
if (this.broadcastDraftTimer) {
|
||||
clearTimeout(this.broadcastDraftTimer);
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
teamNotAppeared(newValue, oldValue) {
|
||||
@@ -1447,6 +1482,11 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
|
||||
this.updateMatchData(matchData);
|
||||
console.log('✅ Match-Daten aktualisiert');
|
||||
|
||||
// Für WebSocket-Broadcast: clubId und gameCode mitsenden
|
||||
const clubId = this.$store?.getters?.currentClub;
|
||||
if (clubId) matchData.clubId = String(clubId);
|
||||
if (this.match?.code) matchData.gameCode = this.match.code;
|
||||
|
||||
// Sende die Daten an den Backend-Endpunkt
|
||||
console.log('📤 Sende Spielbericht an Backend...');
|
||||
const uuid = this.meetingData.nuLigaMeetingUuid;
|
||||
@@ -2083,6 +2123,68 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
|
||||
this.isGuestLineupCertified = true;
|
||||
}
|
||||
},
|
||||
|
||||
/** Entwurf für Broadcast: meetingDetails-Form mit aktuellen Satzergebnissen aus results. */
|
||||
getDraftMatchData() {
|
||||
if (!this.meetingDetails || !Array.isArray(this.meetingDetails.matches)) return null;
|
||||
const matches = this.meetingDetails.matches.map((m, i) => {
|
||||
const r = this.results[i];
|
||||
const out = { ...m };
|
||||
if (r && Array.isArray(r.sets)) {
|
||||
for (let s = 0; s < 5; s++) {
|
||||
const setStr = r.sets[s];
|
||||
if (setStr && String(setStr).includes(':')) {
|
||||
const parts = String(setStr).split(':').map(x => parseInt(x, 10) || 0);
|
||||
out[`set${s + 1}A`] = parts[0];
|
||||
out[`set${s + 1}B`] = parts[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
return out;
|
||||
});
|
||||
return {
|
||||
matches,
|
||||
homePin: this.meetingDetails.homePin,
|
||||
guestPin: this.meetingDetails.guestPin,
|
||||
startDate: this.meetingData?.startDate ?? this.match?.startDate,
|
||||
endDate: this.meetingData?.endDate ?? this.match?.endDate
|
||||
};
|
||||
},
|
||||
async broadcastDraft() {
|
||||
const clubId = this.$store?.getters?.currentClub;
|
||||
const gameCode = this.match?.code;
|
||||
const matchData = this.getDraftMatchData();
|
||||
if (!clubId || !gameCode || !matchData) return;
|
||||
try {
|
||||
await apiClient.post('/nuscore/broadcast-draft', { clubId: String(clubId), gameCode, matchData });
|
||||
} catch (e) {
|
||||
// still, no hard error for draft broadcast
|
||||
}
|
||||
},
|
||||
broadcastDraftDebounced() {
|
||||
if (this.broadcastDraftTimer) clearTimeout(this.broadcastDraftTimer);
|
||||
this.broadcastDraftTimer = setTimeout(() => {
|
||||
this.broadcastDraftTimer = null;
|
||||
this.broadcastDraft();
|
||||
}, 500);
|
||||
},
|
||||
|
||||
/**
|
||||
* Übernimmt per WebSocket empfangene Match-Daten (von anderem Gerät) in den Dialog.
|
||||
* Aktualisiert meetingDetails und meetingData, dann Ergebnisse und Aufstellungs-Bestätigung.
|
||||
*/
|
||||
applyReceivedMatchData(matchData) {
|
||||
if (!matchData || !this.meetingDetails) return;
|
||||
if (Array.isArray(matchData.matches)) {
|
||||
this.meetingDetails.matches = matchData.matches;
|
||||
}
|
||||
if (matchData.homePin != null) this.meetingDetails.homePin = matchData.homePin;
|
||||
if (matchData.guestPin != null) this.meetingDetails.guestPin = matchData.guestPin;
|
||||
if (matchData.startDate != null && this.meetingData) this.meetingData.startDate = matchData.startDate;
|
||||
if (matchData.endDate != null && this.meetingData) this.meetingData.endDate = matchData.endDate;
|
||||
this.populateResultsFromMeetingDetails();
|
||||
this.applyLineupCertificationFromMeetingDetails();
|
||||
},
|
||||
resolveSide(label, side) {
|
||||
// label z.B. "A1 – B2" oder "DA1 – DB1"
|
||||
try {
|
||||
@@ -2230,6 +2332,7 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
|
||||
|
||||
// Synchronisiere zurück ins Match-Objekt
|
||||
this.syncResultsToMatch();
|
||||
this.broadcastDraftDebounced();
|
||||
},
|
||||
|
||||
appendToSet(matchIndex, setIndex, char) {
|
||||
@@ -2260,6 +2363,7 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
|
||||
const current = sets[setIndex] || '';
|
||||
if (current.length >= 6) return;
|
||||
this.results[matchIndex].sets[setIndex] = current + char;
|
||||
this.broadcastDraftDebounced();
|
||||
},
|
||||
setKeyboardBackspace() {
|
||||
if (!this.editingSetCell || !this.results[this.editingSetCell.matchIndex]) return;
|
||||
@@ -2267,6 +2371,16 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
|
||||
const sets = this.results[matchIndex].sets || [];
|
||||
const current = sets[setIndex] || '';
|
||||
this.results[matchIndex].sets[setIndex] = current.slice(0, -1);
|
||||
this.broadcastDraftDebounced();
|
||||
},
|
||||
setKeyboardClear() {
|
||||
if (!this.editingSetCell || !this.results[this.editingSetCell.matchIndex]) return;
|
||||
const { matchIndex, setIndex } = this.editingSetCell;
|
||||
if (!this.results[matchIndex].sets) {
|
||||
this.$set(this.results[matchIndex], 'sets', ['', '', '', '', '']);
|
||||
}
|
||||
this.results[matchIndex].sets[setIndex] = '';
|
||||
this.broadcastDraftDebounced();
|
||||
},
|
||||
setKeyboardOk() {
|
||||
if (!this.editingSetCell) return;
|
||||
@@ -2278,6 +2392,7 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
|
||||
const m = this.results[idx];
|
||||
m.completed = false;
|
||||
m.result = '1:0';
|
||||
this.broadcastDraftDebounced();
|
||||
},
|
||||
async loadClubSettings() {
|
||||
try {
|
||||
@@ -2353,16 +2468,15 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
|
||||
|
||||
processScoreInput(matchIndex, setIndex) {
|
||||
const match = this.results[matchIndex];
|
||||
const inputValue = match.sets[setIndex];
|
||||
|
||||
if (!inputValue || inputValue.trim() === '') {
|
||||
const inputValue = String(match.sets[setIndex] ?? '').trim();
|
||||
|
||||
if (!inputValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Validiere Eingabe: nur Zahlen, Bindestriche und Doppelpunkte
|
||||
const validPattern = /^[0-9:\-]*$/;
|
||||
const validPattern = /^[0-9:\-\s]*$/;
|
||||
if (!validPattern.test(inputValue)) {
|
||||
// Entferne ungültige Zeichen
|
||||
const cleanedValue = inputValue.replace(/[^0-9:\-]/g, '');
|
||||
match.sets[setIndex] = cleanedValue;
|
||||
return;
|
||||
@@ -2384,6 +2498,7 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
|
||||
|
||||
// Aktualisiere die Anzeige
|
||||
this.$forceUpdate();
|
||||
this.broadcastDraftDebounced();
|
||||
},
|
||||
|
||||
// Berechne gewonnene Sätze für einen Spieler in einem Match
|
||||
@@ -2496,44 +2611,46 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
|
||||
|
||||
parseAndValidateScore(inputValue, matchIndex, setIndex) {
|
||||
const match = this.results[matchIndex];
|
||||
|
||||
// Entferne Leerzeichen
|
||||
const cleanedValue = inputValue.trim();
|
||||
|
||||
const cleanedValue = String(inputValue ?? '').trim();
|
||||
|
||||
// Prüfe ob das Feld überhaupt bearbeitet werden darf
|
||||
if (this.isSetInputDisabled(matchIndex, setIndex)) {
|
||||
// Wenn das Feld deaktiviert werden soll, Eingabe ignorieren
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Verschiedene Eingabeformate unterstützen:
|
||||
// "3:1", "3-1", "3 1", "3:1 ", "3", "-3"
|
||||
let homeScore, guestScore;
|
||||
|
||||
|
||||
if (cleanedValue.includes(':')) {
|
||||
// Format: "3:1"
|
||||
// Format: "3:1" (explizit mit Doppelpunkt)
|
||||
const parts = cleanedValue.split(':');
|
||||
homeScore = parseInt(parts[0]) || 0;
|
||||
guestScore = parseInt(parts[1]) || 0;
|
||||
homeScore = parseInt(parts[0], 10) || 0;
|
||||
guestScore = parseInt(parts[1], 10) || 0;
|
||||
} else if (cleanedValue.startsWith('-')) {
|
||||
// Format: "-0" bis "-X" -> "X:max(11, X+2)"
|
||||
const valueStr = cleanedValue.substring(1); // Entferne das Minus
|
||||
const value = parseInt(valueStr) || 0;
|
||||
// Format: "-8" -> Gast gewinnt 8:11 (Heim hat 8, Gast 11)
|
||||
const valueStr = cleanedValue.substring(1).trim();
|
||||
const value = parseInt(valueStr, 10) || 0;
|
||||
homeScore = value;
|
||||
guestScore = Math.max(11, value + 2);
|
||||
} else if (/^\d+$/.test(cleanedValue)) {
|
||||
// Nur eine positive Zahl: "8" -> Heim gewinnt 11:8 (Gast hat 8 Punkte)
|
||||
const value = parseInt(cleanedValue, 10) || 0;
|
||||
homeScore = Math.max(11, value + 2);
|
||||
guestScore = value;
|
||||
} else if (cleanedValue.includes('-') && cleanedValue.split('-').length === 2) {
|
||||
// Format: "3-1" (zwei Teile mit Bindestrich)
|
||||
const parts = cleanedValue.split('-');
|
||||
homeScore = parseInt(parts[0]) || 0;
|
||||
guestScore = parseInt(parts[1]) || 0;
|
||||
homeScore = parseInt(parts[0], 10) || 0;
|
||||
guestScore = parseInt(parts[1], 10) || 0;
|
||||
} else if (cleanedValue.includes(' ')) {
|
||||
// Format: "3 1"
|
||||
const parts = cleanedValue.split(' ');
|
||||
homeScore = parseInt(parts[0]) || 0;
|
||||
guestScore = parseInt(parts[1]) || 0;
|
||||
const parts = cleanedValue.split(/\s+/);
|
||||
homeScore = parseInt(parts[0], 10) || 0;
|
||||
guestScore = parseInt(parts[1], 10) || 0;
|
||||
} else {
|
||||
// Format: "0" bis "X" -> "max(11,X+2):X"
|
||||
const value = parseInt(cleanedValue) || 0;
|
||||
// Fallback: wie eine einzelne Zahl behandeln
|
||||
const value = parseInt(cleanedValue, 10) || 0;
|
||||
homeScore = Math.max(11, value + 2);
|
||||
guestScore = value;
|
||||
}
|
||||
@@ -3914,22 +4031,22 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
|
||||
}
|
||||
.set-keyboard-value {
|
||||
text-align: center;
|
||||
font-size: 1.25rem;
|
||||
font-size: 1.1rem;
|
||||
font-family: monospace;
|
||||
padding: 10px 12px;
|
||||
margin-bottom: 10px;
|
||||
padding: 4px 10px;
|
||||
margin-bottom: 6px;
|
||||
background: #fff;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 8px;
|
||||
min-height: 44px;
|
||||
line-height: 1.4;
|
||||
min-height: 24px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
.set-keyboard-value.placeholder {
|
||||
color: #999;
|
||||
}
|
||||
.set-keyboard-keys {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 8px;
|
||||
}
|
||||
.set-key {
|
||||
@@ -3938,6 +4055,7 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
|
||||
border: 1px solid #bbb;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
color: #333;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
@@ -3946,11 +4064,17 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
|
||||
.set-key:active {
|
||||
background: #e0e0e0;
|
||||
}
|
||||
.set-key-backspace {
|
||||
.set-key-backspace,
|
||||
.set-key-clear {
|
||||
font-size: 1.1rem;
|
||||
color: #c62828;
|
||||
border-color: #c62828;
|
||||
}
|
||||
.set-key-backspace:active,
|
||||
.set-key-clear:active {
|
||||
background: #ffebee;
|
||||
}
|
||||
.set-key-ok {
|
||||
grid-column: span 2;
|
||||
background: var(--primary-color, #1976d2);
|
||||
color: #fff;
|
||||
border-color: var(--primary-hover, #1565c0);
|
||||
|
||||
@@ -6,18 +6,29 @@
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<!-- Im Login-Modus: Zeige MyTischtennis-Login-Formular in iframe -->
|
||||
<div v-if="loginMode" class="login-iframe-container">
|
||||
<iframe
|
||||
ref="loginIframe"
|
||||
:src="loginUrl"
|
||||
class="login-iframe"
|
||||
@load="onIframeLoad"
|
||||
></iframe>
|
||||
<div v-if="loading" class="iframe-loading">
|
||||
{{ $t('myTischtennisDialog.loadingLoginForm') }}
|
||||
<!-- Im Login-Modus: lokales Formular, Login serverseitig via Playwright-Fallback -->
|
||||
<template v-if="loginMode">
|
||||
<div class="form-group">
|
||||
<label for="mtt-login-email">{{ $t('myTischtennisDialog.email') }}:</label>
|
||||
<input
|
||||
type="email"
|
||||
id="mtt-login-email"
|
||||
v-model="formData.email"
|
||||
readonly
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="mtt-login-password">{{ $t('myTischtennisDialog.password') }}:</label>
|
||||
<input
|
||||
type="password"
|
||||
id="mtt-login-password"
|
||||
v-model="formData.password"
|
||||
:placeholder="$t('myTischtennisDialog.passwordPlaceholder')"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Im Bearbeiten-Modus: Zeige normales Formular -->
|
||||
<template v-else>
|
||||
@@ -55,7 +66,22 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Auto-Update-Checkbox entfernt - automatische Abrufe wurden deaktiviert -->
|
||||
<div class="form-group checkbox-group">
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
v-model="formData.autoUpdateRatings"
|
||||
:disabled="!formData.savePassword"
|
||||
/>
|
||||
<span>{{ $t('myTischtennisDialog.autoUpdateRatings') }}</span>
|
||||
</label>
|
||||
<p class="hint">
|
||||
{{ $t('myTischtennisDialog.autoUpdateRatingsHint') }}
|
||||
</p>
|
||||
<p v-if="formData.autoUpdateRatings && !formData.savePassword" class="warning">
|
||||
{{ $t('myTischtennisDialog.autoUpdateWarning') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group" v-if="formData.password">
|
||||
<label for="app-password">{{ $t('myTischtennisDialog.appPassword') }}:</label>
|
||||
@@ -81,6 +107,9 @@
|
||||
<button class="btn-secondary" @click="$emit('close')" :disabled="saving">
|
||||
{{ $t('myTischtennisDialog.cancel') }}
|
||||
</button>
|
||||
<button v-if="loginMode" class="btn-primary" @click="performLogin" :disabled="!canLogin || saving">
|
||||
{{ saving ? $t('myTischtennisDialog.saving') : $t('myTischtennisDialog.login') }}
|
||||
</button>
|
||||
<button v-if="!loginMode" class="btn-primary" @click="saveAccount()" :disabled="!canSave || saving">
|
||||
{{ saving ? $t('myTischtennisDialog.saving') : $t('myTischtennisDialog.save') }}
|
||||
</button>
|
||||
@@ -110,24 +139,16 @@ export default {
|
||||
email: this.account?.email || '',
|
||||
password: '',
|
||||
savePassword: this.account?.savePassword || false,
|
||||
autoUpdateRatings: false, // Automatische Updates deaktiviert
|
||||
autoUpdateRatings: this.account?.autoUpdateRatings || false,
|
||||
userPassword: ''
|
||||
},
|
||||
saving: false,
|
||||
loading: false,
|
||||
error: null,
|
||||
urlCheckInterval: null
|
||||
error: null
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
loginUrl() {
|
||||
// Verwende Backend-Proxy für Login-Seite, damit Cookies im Backend-Kontext bleiben
|
||||
// Verwende absolute URL für iframe
|
||||
const baseUrl = import.meta.env.VITE_BACKEND || window.location.origin;
|
||||
// Füge Token als Query-Parameter hinzu, damit Backend userId extrahieren kann
|
||||
const token = this.$store.state.token;
|
||||
const tokenParam = token ? `?token=${encodeURIComponent(token)}` : '';
|
||||
return `${baseUrl}/api/mytischtennis/login-page${tokenParam}`;
|
||||
canLogin() {
|
||||
return !!this.formData.password;
|
||||
},
|
||||
canSave() {
|
||||
// E-Mail ist erforderlich
|
||||
@@ -135,11 +156,6 @@ export default {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Im Login-Modus: Passwort ist erforderlich
|
||||
if (this.loginMode) {
|
||||
return !!this.formData.password;
|
||||
}
|
||||
|
||||
// Wenn ein Passwort eingegeben wurde, muss auch das App-Passwort eingegeben werden
|
||||
if (this.formData.password && !this.formData.userPassword) {
|
||||
return false;
|
||||
@@ -148,66 +164,26 @@ export default {
|
||||
return true;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (this.loginMode) {
|
||||
this.loading = true;
|
||||
// URL-Überwachung wird erst gestartet, nachdem das iframe geladen wurde
|
||||
}
|
||||
},
|
||||
beforeUnmount() {
|
||||
// URL-Überwachung stoppen
|
||||
if (this.urlCheckInterval) {
|
||||
clearInterval(this.urlCheckInterval);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onIframeLoad() {
|
||||
this.loading = false;
|
||||
console.log('[MyTischtennisDialog] Iframe geladen');
|
||||
|
||||
// Starte URL-Überwachung erst NACH dem Laden des iframes
|
||||
// Warte 3 Sekunden, damit der Benutzer Zeit hat, sich einzuloggen
|
||||
setTimeout(() => {
|
||||
this.startUrlMonitoring();
|
||||
}, 3000);
|
||||
},
|
||||
|
||||
checkIframeUrl() {
|
||||
async performLogin() {
|
||||
if (!this.canLogin) return;
|
||||
this.error = null;
|
||||
this.saving = true;
|
||||
try {
|
||||
const iframe = this.$refs.loginIframe;
|
||||
if (!iframe || !iframe.contentWindow) return;
|
||||
|
||||
// Versuche, die URL zu lesen (funktioniert nur bei gleicher Origin)
|
||||
// Da mytischtennis.de eine andere Origin ist, können wir die URL nicht direkt lesen
|
||||
// Stattdessen überwachen wir über PostMessage oder Polling
|
||||
await apiClient.post('/mytischtennis/verify', {
|
||||
password: this.formData.password
|
||||
});
|
||||
this.$emit('logged-in');
|
||||
} catch (error) {
|
||||
// Cross-Origin-Zugriff nicht möglich - das ist normal
|
||||
console.log('[MyTischtennisDialog] Cross-Origin-Zugriff nicht möglich (erwartet)');
|
||||
console.error('Fehler beim Login:', error);
|
||||
this.error = error.response?.data?.error
|
||||
|| error.response?.data?.message
|
||||
|| this.$t('myTischtennisDialog.errorSaving');
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
},
|
||||
|
||||
startUrlMonitoring() {
|
||||
// Überwache, ob der Login erfolgreich war
|
||||
// Prüfe, ob bereits eine gültige Session existiert
|
||||
this.urlCheckInterval = setInterval(async () => {
|
||||
try {
|
||||
// Prüfe, ob bereits eine gültige Session existiert
|
||||
// Nach erfolgreichem Login im iframe sollte submitLogin die Session gespeichert haben
|
||||
const sessionResponse = await apiClient.get('/mytischtennis/session');
|
||||
if (sessionResponse.data && sessionResponse.data.session && sessionResponse.data.session.accessToken) {
|
||||
// Session vorhanden - Login erfolgreich!
|
||||
clearInterval(this.urlCheckInterval);
|
||||
this.urlCheckInterval = null;
|
||||
this.$emit('logged-in');
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
// Noch nicht eingeloggt oder Fehler - ignorieren
|
||||
// (wird alle 3 Sekunden wieder versucht)
|
||||
}
|
||||
}, 3000); // Alle 3 Sekunden prüfen
|
||||
},
|
||||
|
||||
async saveAccount() {
|
||||
if (!this.canSave) return;
|
||||
|
||||
@@ -218,7 +194,7 @@ export default {
|
||||
const payload = {
|
||||
email: this.formData.email,
|
||||
savePassword: this.formData.savePassword,
|
||||
autoUpdateRatings: false // Automatische Updates immer deaktiviert
|
||||
autoUpdateRatings: this.formData.savePassword ? this.formData.autoUpdateRatings : false
|
||||
};
|
||||
|
||||
// Nur password und userPassword hinzufügen, wenn ein Passwort eingegeben wurde
|
||||
@@ -231,7 +207,9 @@ export default {
|
||||
this.$emit('saved');
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Speichern:', error);
|
||||
this.error = error.response?.data?.message || this.$t('myTischtennisDialog.errorSaving');
|
||||
this.error = error.response?.data?.error
|
||||
|| error.response?.data?.message
|
||||
|| this.$t('myTischtennisDialog.errorSaving');
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
@@ -266,33 +244,6 @@ export default {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.login-iframe-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 600px;
|
||||
min-height: 600px;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.login-iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.iframe-loading {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
padding: 1rem;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border-radius: 4px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: 1.5rem;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
|
||||
@@ -155,11 +155,6 @@ if (typeof window !== 'undefined' && (process.env.NODE_ENV === 'development' ||
|
||||
window.setLanguage = setLanguage;
|
||||
window.getCurrentLanguage = getCurrentLanguage;
|
||||
window.getAvailableLanguages = getAvailableLanguages;
|
||||
console.log('🌐 Sprache-Test-Funktionen verfügbar:');
|
||||
console.log(' - setLanguage("de") - Sprache ändern');
|
||||
console.log(' - getCurrentLanguage() - Aktuelle Sprache abrufen');
|
||||
console.log(' - getAvailableLanguages() - Verfügbare Sprachen anzeigen');
|
||||
console.log(' - Oder URL-Parameter verwenden: ?lang=de');
|
||||
}
|
||||
|
||||
export default i18n;
|
||||
|
||||
@@ -308,6 +308,18 @@ export const onTournamentChanged = (callback) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const onScheduleMatchUpdated = (callback) => {
|
||||
if (socket) {
|
||||
socket.on('schedule:match:updated', callback);
|
||||
}
|
||||
};
|
||||
|
||||
export const onMatchReportSubmitted = (callback) => {
|
||||
if (socket) {
|
||||
socket.on('schedule:match-report:submitted', callback);
|
||||
}
|
||||
};
|
||||
|
||||
// Event-Listener entfernen
|
||||
export const offParticipantAdded = (callback) => {
|
||||
if (socket) {
|
||||
@@ -399,3 +411,15 @@ export const offTournamentChanged = (callback) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const offScheduleMatchUpdated = (callback) => {
|
||||
if (socket) {
|
||||
socket.off('schedule:match:updated', callback);
|
||||
}
|
||||
};
|
||||
|
||||
export const offMatchReportSubmitted = (callback) => {
|
||||
if (socket) {
|
||||
socket.off('schedule:match-report:submitted', callback);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -2068,12 +2068,34 @@ export default {
|
||||
},
|
||||
sortedMembers() {
|
||||
// Erstelle eine Kopie des Arrays, um Mutation zu vermeiden
|
||||
// Sortierung: zuerst nach Vorname, dann nach Nachname (beides case-insensitive, deutsch)
|
||||
// Fallback: ID, damit die Reihenfolge bei komplett gleichen Namen stabil ist
|
||||
const locale = 'de-DE';
|
||||
const options = { sensitivity: 'base' };
|
||||
|
||||
const safe = (value) => (value ?? '').toString().trim();
|
||||
|
||||
return [...this.members].sort((a, b) => {
|
||||
const firstNameComparison = a.firstName.localeCompare(b.firstName);
|
||||
if (firstNameComparison === 0) {
|
||||
return a.lastName.localeCompare(b.lastName);
|
||||
const firstNameA = safe(a.firstName);
|
||||
const firstNameB = safe(b.firstName);
|
||||
const firstNameComparison = firstNameA.localeCompare(firstNameB, locale, options);
|
||||
|
||||
if (firstNameComparison !== 0) {
|
||||
return firstNameComparison;
|
||||
}
|
||||
return firstNameComparison;
|
||||
|
||||
const lastNameA = safe(a.lastName);
|
||||
const lastNameB = safe(b.lastName);
|
||||
const lastNameComparison = lastNameA.localeCompare(lastNameB, locale, options);
|
||||
|
||||
if (lastNameComparison !== 0) {
|
||||
return lastNameComparison;
|
||||
}
|
||||
|
||||
// Stabiler Fallback, falls sowohl Vor- als auch Nachname identisch sind
|
||||
const idA = Number.isFinite(a.id) ? a.id : parseInt(a.id, 10) || 0;
|
||||
const idB = Number.isFinite(b.id) ? b.id : parseInt(b.id, 10) || 0;
|
||||
return idA - idB;
|
||||
});
|
||||
},
|
||||
async showPic(member) {
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
<template>
|
||||
<div>
|
||||
<h2>{{ $t('auth.login') }}</h2>
|
||||
<form @submit.prevent="executeLogin">
|
||||
<input v-model="email" type="email" :placeholder="$t('auth.email')" required />
|
||||
<input v-model="password" type="password" :placeholder="$t('auth.password')" required />
|
||||
<button type="submit">{{ $t('auth.login') }}</button>
|
||||
</form>
|
||||
<div class="forgot-password-link">
|
||||
<router-link to="/forgot-password">{{ $t('auth.forgotPassword') }}</router-link>
|
||||
<div class="login-page">
|
||||
<div>
|
||||
<h2>{{ $t('auth.login') }}</h2>
|
||||
<form @submit.prevent="executeLogin">
|
||||
<input v-model="email" type="email" :placeholder="$t('auth.email')" required />
|
||||
<input v-model="password" type="password" :placeholder="$t('auth.password')" required />
|
||||
<button type="submit">{{ $t('auth.login') }}</button>
|
||||
</form>
|
||||
<div class="forgot-password-link">
|
||||
<router-link to="/forgot-password">{{ $t('auth.forgotPassword') }}</router-link>
|
||||
</div>
|
||||
<div class="register-link">
|
||||
<p>{{ $t('auth.noAccount') }} <router-link to="/register">{{ $t('auth.register') }}</router-link></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="register-link">
|
||||
<p>{{ $t('auth.noAccount') }} <router-link to="/register">{{ $t('auth.register') }}</router-link></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Info Dialog -->
|
||||
<InfoDialog
|
||||
v-model="infoDialog.isOpen"
|
||||
@@ -34,6 +34,7 @@
|
||||
@confirm="handleConfirmResult(true)"
|
||||
@cancel="handleConfirmResult(false)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -43,6 +44,11 @@ import InfoDialog from '../components/InfoDialog.vue';
|
||||
import ConfirmDialog from '../components/ConfirmDialog.vue';
|
||||
import { buildInfoConfig, buildConfirmConfig, safeErrorMessage } from '../utils/dialogUtils.js';
|
||||
export default {
|
||||
name: 'Login',
|
||||
components: {
|
||||
InfoDialog,
|
||||
ConfirmDialog
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
// Dialog States
|
||||
|
||||
@@ -1,77 +1,81 @@
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<h1>{{ $t('myTischtennisAccount.title') }}</h1>
|
||||
|
||||
<div class="account-container">
|
||||
<div v-if="loading" class="loading">{{ $t('myTischtennisAccount.loading') }}</div>
|
||||
<div class="mytt-account-page">
|
||||
<div class="page-container">
|
||||
<h1>{{ $t('myTischtennisAccount.title') }}</h1>
|
||||
|
||||
<div v-else-if="account" class="account-info">
|
||||
<div class="info-section">
|
||||
<h2>{{ $t('myTischtennisAccount.linkedAccount') }}</h2>
|
||||
|
||||
<div class="info-row">
|
||||
<label>{{ $t('myTischtennisAccount.email') }}</label>
|
||||
<span>{{ account.email }}</span>
|
||||
</div>
|
||||
|
||||
<div class="info-row">
|
||||
<label>{{ $t('myTischtennisAccount.passwordSaved') }}</label>
|
||||
<span>{{ accountStatus && accountStatus.hasPassword ? $t('myTischtennisAccount.yes') : $t('myTischtennisAccount.no') }}</span>
|
||||
</div>
|
||||
|
||||
<div class="info-row" v-if="account.clubId">
|
||||
<label>{{ $t('myTischtennisAccount.club') }}</label>
|
||||
<span>{{ account.clubName }} ({{ account.clubId }}{{ account.fedNickname ? ' - ' + account.fedNickname : '' }})</span>
|
||||
</div>
|
||||
|
||||
<div class="info-row" v-if="account.lastLoginSuccess">
|
||||
<label>{{ $t('myTischtennisAccount.lastSuccessfulLogin') }}</label>
|
||||
<span>{{ formatDate(account.lastLoginSuccess) }}</span>
|
||||
</div>
|
||||
|
||||
<div class="info-row" v-if="account.lastLoginAttempt">
|
||||
<label>{{ $t('myTischtennisAccount.lastLoginAttempt') }}</label>
|
||||
<span>{{ formatDate(account.lastLoginAttempt) }}</span>
|
||||
</div>
|
||||
|
||||
<div class="button-group">
|
||||
<button class="btn-primary" @click="openEditDialog">{{ $t('myTischtennisAccount.editAccount') }}</button>
|
||||
<button type="button" class="btn-secondary" @click="testConnection">{{ $t('myTischtennisAccount.loginAgain') }}</button>
|
||||
<button class="btn-danger" @click="deleteAccount">{{ $t('myTischtennisAccount.unlinkAccount') }}</button>
|
||||
<div class="account-container">
|
||||
<div v-if="loading" class="loading">{{ $t('myTischtennisAccount.loading') }}</div>
|
||||
|
||||
<div v-else-if="account" class="account-info">
|
||||
<div class="info-section">
|
||||
<h2>{{ $t('myTischtennisAccount.linkedAccount') }}</h2>
|
||||
|
||||
<div class="info-row">
|
||||
<label>{{ $t('myTischtennisAccount.email') }}</label>
|
||||
<span>{{ account.email }}</span>
|
||||
</div>
|
||||
|
||||
<div class="info-row">
|
||||
<label>{{ $t('myTischtennisAccount.passwordSaved') }}</label>
|
||||
<span>{{ accountStatus && accountStatus.hasPassword ? $t('myTischtennisAccount.yes') : $t('myTischtennisAccount.no') }}</span>
|
||||
</div>
|
||||
|
||||
<div class="info-row" v-if="account.clubId">
|
||||
<label>{{ $t('myTischtennisAccount.club') }}</label>
|
||||
<span>{{ account.clubName }} ({{ account.clubId }}{{ account.fedNickname ? ' - ' + account.fedNickname : '' }})</span>
|
||||
</div>
|
||||
|
||||
<div class="info-row" v-if="account.lastLoginSuccess">
|
||||
<label>{{ $t('myTischtennisAccount.lastSuccessfulLogin') }}</label>
|
||||
<span>{{ formatDate(account.lastLoginSuccess) }}</span>
|
||||
</div>
|
||||
|
||||
<div class="info-row" v-if="account.lastLoginAttempt">
|
||||
<label>{{ $t('myTischtennisAccount.lastLoginAttempt') }}</label>
|
||||
<span>{{ formatDate(account.lastLoginAttempt) }}</span>
|
||||
</div>
|
||||
|
||||
<div class="button-group">
|
||||
<button class="btn-primary" @click="openEditDialog">{{ $t('myTischtennisAccount.editAccount') }}</button>
|
||||
<button type="button" class="btn-secondary" @click="testConnection" :disabled="verifyingLogin">
|
||||
{{ verifyingLogin ? 'Login wird durchgeführt…' : $t('myTischtennisAccount.loginAgain') }}
|
||||
</button>
|
||||
<button class="btn-danger" @click="deleteAccount">{{ $t('myTischtennisAccount.unlinkAccount') }}</button>
|
||||
</div>
|
||||
<p v-if="loginFeedback.message" class="login-feedback" :class="`login-feedback--${loginFeedback.type}`">
|
||||
{{ loginFeedback.message }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="no-account">
|
||||
<p>{{ $t('myTischtennisAccount.noAccountLinked') }}</p>
|
||||
<button class="btn-primary" @click="openEditDialog">{{ $t('myTischtennisAccount.linkAccount') }}</button>
|
||||
|
||||
<div v-else class="no-account">
|
||||
<p>{{ $t('myTischtennisAccount.noAccountLinked') }}</p>
|
||||
<button class="btn-primary" @click="openEditDialog">{{ $t('myTischtennisAccount.linkAccount') }}</button>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<h3>{{ $t('myTischtennisAccount.aboutMyTischtennis') }}</h3>
|
||||
<p>{{ $t('myTischtennisAccount.aboutDescription') }}</p>
|
||||
<ul>
|
||||
<li>{{ $t('myTischtennisAccount.aboutFeature1') }}</li>
|
||||
<li>{{ $t('myTischtennisAccount.aboutFeature2') }}</li>
|
||||
<li>{{ $t('myTischtennisAccount.aboutFeature3') }}</li>
|
||||
</ul>
|
||||
<p><strong>{{ $t('messages.info') }}:</strong> {{ $t('myTischtennisAccount.aboutHint') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<h3>{{ $t('myTischtennisAccount.aboutMyTischtennis') }}</h3>
|
||||
<p>{{ $t('myTischtennisAccount.aboutDescription') }}</p>
|
||||
<ul>
|
||||
<li>{{ $t('myTischtennisAccount.aboutFeature1') }}</li>
|
||||
<li>{{ $t('myTischtennisAccount.aboutFeature2') }}</li>
|
||||
<li>{{ $t('myTischtennisAccount.aboutFeature3') }}</li>
|
||||
</ul>
|
||||
<p><strong>{{ $t('messages.info') }}:</strong> {{ $t('myTischtennisAccount.aboutHint') }}</p>
|
||||
</div>
|
||||
<!-- Edit Dialog -->
|
||||
<MyTischtennisDialog
|
||||
v-if="showDialog"
|
||||
:account="account"
|
||||
:login-mode="loginMode"
|
||||
@close="closeDialog"
|
||||
@saved="onAccountSaved"
|
||||
@logged-in="onLoggedIn"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Edit Dialog -->
|
||||
<MyTischtennisDialog
|
||||
v-if="showDialog"
|
||||
:account="account"
|
||||
:login-mode="loginMode"
|
||||
@close="closeDialog"
|
||||
@saved="onAccountSaved"
|
||||
@logged-in="onLoggedIn"
|
||||
/>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Info Dialog -->
|
||||
<InfoDialog
|
||||
v-model="infoDialog.isOpen"
|
||||
@@ -91,6 +95,7 @@
|
||||
@confirm="handleConfirmResult(true)"
|
||||
@cancel="handleConfirmResult(false)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -129,7 +134,12 @@ export default {
|
||||
account: null,
|
||||
accountStatus: null,
|
||||
showDialog: false,
|
||||
loginMode: false
|
||||
loginMode: false,
|
||||
verifyingLogin: false,
|
||||
loginFeedback: {
|
||||
type: '',
|
||||
message: ''
|
||||
}
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
@@ -181,10 +191,12 @@ export default {
|
||||
console.error('Fehler beim Laden des Accounts:', error);
|
||||
this.account = null;
|
||||
this.accountStatus = null;
|
||||
this.$store.dispatch('showMessage', {
|
||||
text: this.$t('myTischtennisAccount.errorLoadingAccount'),
|
||||
type: 'error'
|
||||
});
|
||||
await this.showInfo(
|
||||
this.$t('messages.error'),
|
||||
this.$t('myTischtennisAccount.errorLoadingAccount'),
|
||||
'',
|
||||
'error'
|
||||
);
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
@@ -203,25 +215,55 @@ export default {
|
||||
async onAccountSaved() {
|
||||
this.closeDialog();
|
||||
await this.loadAccount();
|
||||
this.$store.dispatch('showMessage', {
|
||||
text: this.$t('myTischtennisAccount.accountSaved'),
|
||||
type: 'success'
|
||||
});
|
||||
await this.showInfo(this.$t('messages.success'), this.$t('myTischtennisAccount.accountSaved'), '', 'success');
|
||||
},
|
||||
|
||||
async onLoggedIn() {
|
||||
this.closeDialog();
|
||||
await this.loadAccount();
|
||||
this.$store.dispatch('showMessage', {
|
||||
text: this.$t('myTischtennisAccount.loginSuccessful'),
|
||||
type: 'success'
|
||||
});
|
||||
await this.showInfo(this.$t('messages.success'), this.$t('myTischtennisAccount.loginSuccessful'), '', 'success');
|
||||
},
|
||||
|
||||
async testConnection() {
|
||||
// Öffne das Login-Dialog mit vorausgefüllter E-Mail
|
||||
this.showDialog = true;
|
||||
this.loginMode = true;
|
||||
if (this.verifyingLogin) return;
|
||||
this.verifyingLogin = true;
|
||||
this.loginFeedback = {
|
||||
type: 'info',
|
||||
message: 'Login wird durchgeführt...'
|
||||
};
|
||||
try {
|
||||
// 1-Klick-Re-Login: zuerst gespeicherte Session/Passwort serverseitig verwenden
|
||||
await apiClient.post('/mytischtennis/verify', {});
|
||||
await this.loadAccount();
|
||||
this.loginFeedback = {
|
||||
type: 'success',
|
||||
message: 'Login erfolgreich.'
|
||||
};
|
||||
await this.showInfo(this.$t('messages.success'), this.$t('myTischtennisAccount.loginSuccessful'), '', 'success');
|
||||
} catch (error) {
|
||||
// Falls gespeicherte Daten nicht ausreichen, Passwort-Dialog öffnen
|
||||
const needsPassword = error?.response?.status === 400;
|
||||
|
||||
if (needsPassword) {
|
||||
this.loginFeedback = {
|
||||
type: 'error',
|
||||
message: 'Bitte Passwort eingeben, um den Login erneut durchzuführen.'
|
||||
};
|
||||
this.showDialog = true;
|
||||
this.loginMode = true;
|
||||
this.verifyingLogin = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const message = getSafeErrorMessage(error, this.$t('myTischtennisAccount.errorLoadingAccount'));
|
||||
this.loginFeedback = {
|
||||
type: 'error',
|
||||
message
|
||||
};
|
||||
await this.showInfo(this.$t('messages.error'), message, '', 'error');
|
||||
} finally {
|
||||
this.verifyingLogin = false;
|
||||
}
|
||||
},
|
||||
|
||||
async deleteAccount() {
|
||||
@@ -406,6 +448,28 @@ h1 {
|
||||
background-color: #545b62;
|
||||
}
|
||||
|
||||
.btn-secondary:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.65;
|
||||
}
|
||||
|
||||
.login-feedback {
|
||||
margin-top: 0.75rem;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.login-feedback--info {
|
||||
color: #0c5460;
|
||||
}
|
||||
|
||||
.login-feedback--success {
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.login-feedback--error {
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
/* Fetch Statistics */
|
||||
.fetch-stats-section {
|
||||
margin-top: 2rem;
|
||||
|
||||
@@ -303,6 +303,14 @@ import InfoDialog from '../components/InfoDialog.vue';
|
||||
import ConfirmDialog from '../components/ConfirmDialog.vue';
|
||||
import BaseDialog from '../components/BaseDialog.vue';
|
||||
import CsvImportDialog from '../components/CsvImportDialog.vue';
|
||||
import {
|
||||
connectSocket,
|
||||
disconnectSocket,
|
||||
onScheduleMatchUpdated,
|
||||
offScheduleMatchUpdated,
|
||||
onMatchReportSubmitted,
|
||||
offMatchReportSubmitted
|
||||
} from '../services/socketService.js';
|
||||
export default {
|
||||
name: 'ScheduleView',
|
||||
components: {
|
||||
@@ -316,6 +324,21 @@ export default {
|
||||
computed: {
|
||||
...mapGetters(['isAuthenticated', 'currentClub', 'clubs', 'currentClubName']),
|
||||
},
|
||||
watch: {
|
||||
currentClub: {
|
||||
immediate: true,
|
||||
handler(newVal) {
|
||||
offScheduleMatchUpdated(this.handleScheduleMatchUpdated);
|
||||
offMatchReportSubmitted(this.handleMatchReportSubmitted);
|
||||
disconnectSocket();
|
||||
if (newVal) {
|
||||
connectSocket(newVal);
|
||||
onScheduleMatchUpdated(this.handleScheduleMatchUpdated);
|
||||
onMatchReportSubmitted(this.handleMatchReportSubmitted);
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
// Dialog States
|
||||
@@ -1016,13 +1039,59 @@ export default {
|
||||
this.fetchingTable = false;
|
||||
}
|
||||
},
|
||||
|
||||
refreshScheduleData() {
|
||||
if (!this.selectedLeague) return;
|
||||
if (this.selectedTeam) {
|
||||
this.loadMatchesForTeam(this.selectedTeam);
|
||||
} else if (this.selectedLeague === this.$t('schedule.overallSchedule')) {
|
||||
this.loadAllMatches();
|
||||
} else if (this.selectedLeague === this.$t('schedule.adultSchedule')) {
|
||||
this.loadAdultMatches();
|
||||
}
|
||||
},
|
||||
|
||||
handleScheduleMatchUpdated(payload) {
|
||||
if (payload?.match && payload.matchId != null) {
|
||||
const idx = this.matches.findIndex(m => m.id === payload.matchId);
|
||||
if (idx !== -1) {
|
||||
this.matches.splice(idx, 1, payload.match);
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.refreshScheduleData();
|
||||
},
|
||||
|
||||
handleMatchReportSubmitted(payload) {
|
||||
if (payload?.matchData && (payload.matchCode != null || payload.matchData?.gameCode || payload.matchData?.code)) {
|
||||
const code = String(payload.matchCode ?? payload.matchData?.gameCode ?? payload.matchData?.code ?? '');
|
||||
const idx = this.matches.findIndex(m => m.code === code);
|
||||
if (idx !== -1) {
|
||||
const m = { ...this.matches[idx] };
|
||||
const d = payload.matchData;
|
||||
if (d.homeMatches != null) m.homeMatchPoints = d.homeMatches;
|
||||
if (d.guestMatches != null) m.guestMatchPoints = d.guestMatches;
|
||||
if (typeof d.isCompleted === 'boolean') m.isCompleted = d.isCompleted;
|
||||
if (d.startDate) m.startDate = d.startDate;
|
||||
if (d.endDate) m.endDate = d.endDate;
|
||||
this.matches.splice(idx, 1, m);
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.refreshScheduleData();
|
||||
},
|
||||
},
|
||||
async created() {
|
||||
// Teams werden geladen, sobald eine Saison ausgewählt ist
|
||||
// Die SeasonSelector-Komponente wählt automatisch die aktuelle Saison aus
|
||||
// und ruft anschließend onSeasonChange auf, was loadTeams() ausführt
|
||||
this.loadTeams();
|
||||
}
|
||||
},
|
||||
beforeUnmount() {
|
||||
offScheduleMatchUpdated(this.handleScheduleMatchUpdated);
|
||||
offMatchReportSubmitted(this.handleMatchReportSubmitted);
|
||||
disconnectSocket();
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@@ -608,13 +608,13 @@ export default {
|
||||
},
|
||||
|
||||
activeAssignmentClassLabel() {
|
||||
if (this.activeAssignmentClassId === undefined) {
|
||||
return this.$t('tournaments.selectClassPrompt');
|
||||
}
|
||||
let label = this.$t('tournaments.selectClassPrompt');
|
||||
if (this.activeAssignmentClassId === null) {
|
||||
return this.$t('tournaments.withoutClass');
|
||||
label = this.$t('tournaments.withoutClass');
|
||||
} else if (this.activeAssignmentClassId !== undefined) {
|
||||
label = this.getClassName(this.activeAssignmentClassId) || this.$t('tournaments.unknown');
|
||||
}
|
||||
return this.getClassName(this.activeAssignmentClassId) || this.$t('tournaments.unknown');
|
||||
return label;
|
||||
},
|
||||
|
||||
canAssignClass() {
|
||||
|
||||
12
package-lock.json
generated
12
package-lock.json
generated
@@ -1826,9 +1826,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ajv": {
|
||||
"version": "6.12.6",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
|
||||
"integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
@@ -4171,9 +4171,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
|
||||
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
|
||||
Reference in New Issue
Block a user