Refactor backend to enhance MyTischtennis integration. Update package.json to change main entry point to server.js. Modify server.js to improve scheduler service logging. Add new fields to ClubTeam, League, Match, and Member models for MyTischtennis data. Update routes to include new MyTischtennis URL parsing and configuration endpoints. Enhance services for fetching team data and scheduling match results. Improve frontend components for MyTischtennis URL configuration and display match results with scores.

This commit is contained in:
Torsten Schulz (local)
2025-10-14 21:58:21 +02:00
parent 993e12d4a5
commit 1517d83f6c
31 changed files with 4538 additions and 56 deletions

View File

@@ -34,6 +34,7 @@
<th>Uhrzeit</th>
<th>Heimmannschaft</th>
<th>Gastmannschaft</th>
<th>Ergebnis</th>
<th v-if="selectedLeague === 'Gesamtspielplan' || selectedLeague === 'Spielplan Erwachsene'">Altersklasse</th>
<th>Code</th>
<th>Heim-PIN</th>
@@ -47,6 +48,12 @@
<td>{{ match.time ? match.time.toString().slice(0, 5) + ' Uhr' : 'N/A' }}</td>
<td v-html="highlightClubName(match.homeTeam?.name || 'N/A')"></td>
<td v-html="highlightClubName(match.guestTeam?.name || 'N/A')"></td>
<td class="result-cell" :class="getResultClass(match)">
<span v-if="match.isCompleted" class="result-score">
{{ match.homeMatchPoints }}:{{ match.guestMatchPoints }}
</span>
<span v-else class="result-pending"></span>
</td>
<td v-if="selectedLeague === 'Gesamtspielplan' || selectedLeague === 'Spielplan Erwachsene'">{{ match.leagueDetails?.name || 'N/A' }}</td>
<td class="code-cell">
<span v-if="match.code && selectedLeague && selectedLeague !== ''">
@@ -157,6 +164,34 @@ export default {
};
},
methods: {
getResultClass(match) {
if (!match.isCompleted) {
return '';
}
// Check if our club's team won or lost
const isOurTeamHome = this.isOurTeam(match.homeTeam?.name);
const isOurTeamGuest = this.isOurTeam(match.guestTeam?.name);
if (isOurTeamHome) {
// We are home team
return match.homeMatchPoints > match.guestMatchPoints ? 'completed won' : 'completed lost';
} else if (isOurTeamGuest) {
// We are guest team
return match.guestMatchPoints > match.homeMatchPoints ? 'completed won' : 'completed lost';
}
return 'completed';
},
isOurTeam(teamName) {
if (!teamName || !this.currentClubName) {
return false;
}
// Check if team name starts with our club name
return teamName.startsWith(this.currentClubName);
},
// Dialog Helper Methods
async showInfo(title, message, details = '', type = 'info') {
this.infoDialog = {
@@ -512,6 +547,36 @@ td {
white-space: nowrap;
}
.result-cell {
text-align: center;
font-weight: 600;
}
.result-score {
font-size: 1.1em;
}
.result-pending {
color: var(--text-muted);
font-style: italic;
}
.result-cell.completed.won {
background-color: #f0f9f0;
}
.result-cell.completed.won .result-score {
color: #28a745;
}
.result-cell.completed.lost {
background-color: #fff5f5;
}
.result-cell.completed.lost .result-score {
color: #dc3545;
}
.hover-info {
margin-top: 10px;
background-color: #eef;

View File

@@ -57,6 +57,62 @@
</button>
</div>
<!-- MyTischtennis URL Konfiguration -->
<div class="mytischtennis-config">
<div class="mytischtennis-header">
<h4>🏓 MyTischtennis Integration</h4>
<div class="header-actions">
<div v-if="teamToEdit" class="current-status">
<span v-if="getMyTischtennisStatus(teamToEdit).complete" class="status-badge complete">
Vollständig konfiguriert
</span>
<span v-else-if="getMyTischtennisStatus(teamToEdit).partial" class="status-badge partial">
{{ getMyTischtennisStatus(teamToEdit).missing }}
</span>
<span v-else class="status-badge missing">
Nicht konfiguriert
</span>
</div>
<button
v-if="teamToEdit && getMyTischtennisStatus(teamToEdit).complete"
@click="fetchTeamDataManually"
:disabled="fetchingTeamData"
class="fetch-btn"
title="Spielergebnisse und Statistiken von myTischtennis abrufen"
>
{{ fetchingTeamData ? '⏳ Abrufen...' : '🔄 Daten jetzt abrufen' }}
</button>
</div>
</div>
<div class="mytischtennis-url-input">
<label>
<span>MyTischtennis Team-URL einfügen und Enter drücken:</span>
<input
type="text"
v-model="myTischtennisUrl"
@keyup.enter="parseMyTischtennisUrl"
@blur="parseMyTischtennisUrl"
placeholder="https://www.mytischtennis.de/click-tt/..."
class="url-input"
:disabled="parsingUrl"
>
</label>
<div v-if="parsingUrl" class="parsing-indicator">
Konfiguriere automatisch...
</div>
</div>
<!-- Fehleranzeige -->
<div v-if="myTischtennisError" class="error-message">
{{ myTischtennisError }}
</div>
<!-- Erfolgsanzeige -->
<div v-if="myTischtennisSuccess" class="success-message">
{{ myTischtennisSuccess }}
</div>
</div>
<!-- Upload-Bestätigung -->
<div v-if="showLeagueSelection" class="upload-confirmation">
<div class="selected-file-info">
@@ -162,6 +218,20 @@
<span class="label">Erstellt:</span>
<span class="value">{{ formatDate(team.createdAt) }}</span>
</div>
<!-- MyTischtennis Status Badge -->
<div class="info-row mytischtennis-status">
<span class="label">🏓 MyTischtennis:</span>
<span v-if="getMyTischtennisStatus(team).complete" class="status-badge complete" :title="getMyTischtennisStatus(team).tooltip">
Vollständig konfiguriert
</span>
<span v-else-if="getMyTischtennisStatus(team).partial" class="status-badge partial" :title="getMyTischtennisStatus(team).tooltip">
Teilweise konfiguriert
</span>
<span v-else class="status-badge missing" :title="getMyTischtennisStatus(team).tooltip">
Nicht konfiguriert
</span>
</div>
</div>
<!-- PDF-Dokumente Icons -->
@@ -210,8 +280,6 @@
</div>
</div>
</div>
</div>
<!-- Info Dialog -->
<InfoDialog
@@ -232,6 +300,7 @@
@confirm="handleConfirmResult(true)"
@cancel="handleConfirmResult(false)"
/>
</div>
</template>
<script>
@@ -289,6 +358,15 @@ export default {
const pdfUrl = ref('');
const pdfDialogTitle = ref('');
// MyTischtennis URL Parser
const myTischtennisUrl = ref('');
const parsedMyTischtennisData = ref(null);
const parsingUrl = ref(false);
const configuringTeam = ref(false);
const fetchingTeamData = ref(false);
const myTischtennisError = ref('');
const myTischtennisSuccess = ref('');
// Computed
const selectedClub = computed(() => store.state.currentClub);
const authToken = computed(() => store.state.token);
@@ -540,12 +618,18 @@ export default {
parseResult.debugInfo.lastFewLines.forEach((line, index) => {
message += `${parseResult.debugInfo.totalLines - 5 + index + 1}: "${line}"\n`;
});
// Fehler-Dialog wenn nichts gefunden wurde
await showInfo('Fehler', message, '', 'error');
} else if (saveResult.errors.length > 0) {
// Warnung wenn Spiele gefunden wurden, aber Fehler auftraten
await showInfo('Warnung', message, '', 'warning');
} else {
// Erfolg wenn alles geklappt hat
await showInfo('Erfolg', message, '', 'success');
}
this.showInfo('Fehler', message, '', 'error');
} else {
// Für andere Dateitypen nur Upload-Bestätigung
this.showInfo('Information', `${pendingUploadType.value === 'code_list' ? 'Code-Liste' : 'Pin-Liste'} "${pendingUploadFile.value.name}" wurde erfolgreich hochgeladen!`, '', 'info');
await showInfo('Information', `${pendingUploadType.value === 'code_list' ? 'Code-Liste' : 'Pin-Liste'} "${pendingUploadFile.value.name}" wurde erfolgreich hochgeladen!`, '', 'info');
}
// Dokumente neu laden
@@ -553,7 +637,7 @@ export default {
} catch (error) {
console.error('Fehler beim Hochladen und Parsen der Datei:', error);
this.showInfo('Fehler', 'Fehler beim Hochladen und Parsen der Datei', '', 'error');
await showInfo('Fehler', 'Fehler beim Hochladen und Parsen der Datei', '', 'error');
} finally {
parsingInProgress.value = false;
pendingUploadFile.value = null;
@@ -695,6 +779,175 @@ export default {
confirmDialog.value.isOpen = false;
};
// MyTischtennis URL Parser Methods
const parseMyTischtennisUrl = async () => {
if (!myTischtennisUrl.value || !myTischtennisUrl.value.trim()) {
return;
}
parsingUrl.value = true;
myTischtennisError.value = '';
myTischtennisSuccess.value = '';
parsedMyTischtennisData.value = null;
try {
const response = await apiClient.post('/mytischtennis/parse-url', {
url: myTischtennisUrl.value.trim()
});
if (response.data.success) {
parsedMyTischtennisData.value = response.data.data;
// Automatisch konfigurieren
await configureTeamFromUrl();
}
} catch (error) {
console.error('Fehler beim Parsen der URL:', error);
myTischtennisError.value = error.response?.data?.message || 'URL konnte nicht geparst werden. Bitte überprüfen Sie das Format.';
await showInfo('Fehler', myTischtennisError.value, '', 'error');
} finally {
parsingUrl.value = false;
}
};
const configureTeamFromUrl = async () => {
if (!parsedMyTischtennisData.value || !teamToEdit.value) {
return;
}
configuringTeam.value = true;
myTischtennisError.value = '';
myTischtennisSuccess.value = '';
try {
const response = await apiClient.post('/mytischtennis/configure-team', {
url: myTischtennisUrl.value.trim(),
clubTeamId: teamToEdit.value.id,
createLeague: true,
createSeason: true
});
if (response.data.success) {
myTischtennisSuccess.value = 'Team erfolgreich konfiguriert! Automatischer Datenabruf ist jetzt aktiv.';
await showInfo(
'Erfolg',
'Team erfolgreich konfiguriert!',
`Liga: ${response.data.data.league.name}\nSaison: ${response.data.data.season.name}\nAutomatischer Datenabruf ist jetzt aktiv.`,
'success'
);
// Teams neu laden
await loadTeams();
// Parsed Data löschen
clearParsedData();
}
} catch (error) {
console.error('Fehler bei der Konfiguration:', error);
myTischtennisError.value = error.response?.data?.message || 'Team konnte nicht konfiguriert werden.';
await showInfo('Fehler', myTischtennisError.value, '', 'error');
} finally {
configuringTeam.value = false;
}
};
const clearParsedData = () => {
parsedMyTischtennisData.value = null;
myTischtennisUrl.value = '';
myTischtennisError.value = '';
myTischtennisSuccess.value = '';
};
const getMyTischtennisStatus = (team) => {
if (!team) {
return { complete: false, partial: false, missing: '', tooltip: '' };
}
const hasTeamId = !!team.myTischtennisTeamId;
const hasLeague = !!team.league;
const hasLeagueConfig = hasLeague &&
!!team.league.myTischtennisGroupId &&
!!team.league.association &&
!!team.league.groupname;
const missingItems = [];
if (!hasTeamId) missingItems.push('Team-ID');
if (!hasLeague) missingItems.push('Liga');
if (hasLeague && !team.league.myTischtennisGroupId) missingItems.push('Gruppen-ID');
if (hasLeague && !team.league.association) missingItems.push('Verband');
if (hasLeague && !team.league.groupname) missingItems.push('Gruppenname');
const complete = hasTeamId && hasLeagueConfig;
const partial = (hasTeamId || hasLeagueConfig) && !complete;
let tooltip = '';
if (complete) {
tooltip = 'Automatischer Datenabruf ist aktiviert';
} else if (partial) {
tooltip = `Fehlend: ${missingItems.join(', ')}`;
} else {
tooltip = 'MyTischtennis-URL eingeben für automatische Konfiguration';
}
return {
complete,
partial,
missing: missingItems.length > 0 ? `Fehlt: ${missingItems.join(', ')}` : '',
tooltip
};
};
const fetchTeamDataManually = async () => {
if (!teamToEdit.value || !teamToEdit.value.id) {
return;
}
fetchingTeamData.value = true;
myTischtennisError.value = '';
myTischtennisSuccess.value = '';
try {
const response = await apiClient.post('/mytischtennis/fetch-team-data', {
clubTeamId: teamToEdit.value.id
});
if (response.data.success) {
myTischtennisSuccess.value = response.data.message;
await showInfo(
'Erfolg',
response.data.message,
`Team: ${response.data.data.teamName}\nAbgerufene Datensätze: ${response.data.data.fetchedCount}`,
'success'
);
}
} catch (error) {
console.error('Fehler beim Abrufen der Team-Daten:', error);
const errorMsg = error.response?.data?.message || 'Daten konnten nicht abgerufen werden.';
myTischtennisError.value = errorMsg;
// Spezielle Behandlung für Account-nicht-verknüpft Fehler
if (error.response?.status === 404) {
await showInfo(
'MyTischtennis-Account nicht verknüpft',
errorMsg,
'Gehen Sie zu den MyTischtennis-Einstellungen, um Ihren Account zu verknüpfen.',
'warning'
);
} else if (error.response?.status === 401) {
await showInfo(
'Login erforderlich',
errorMsg,
'Aktivieren Sie "Passwort speichern" in den MyTischtennis-Einstellungen für automatischen Login.',
'warning'
);
} else {
await showInfo('Fehler', errorMsg, '', 'error');
}
} finally {
fetchingTeamData.value = false;
}
};
return {
infoDialog,
confirmDialog,
@@ -717,6 +970,13 @@ export default {
showPDFViewer,
pdfUrl,
pdfDialogTitle,
myTischtennisUrl,
parsedMyTischtennisData,
parsingUrl,
configuringTeam,
fetchingTeamData,
myTischtennisError,
myTischtennisSuccess,
filteredLeagues,
toggleNewTeam,
resetToNewTeam,
@@ -737,7 +997,12 @@ export default {
closePDFDialog,
formatDate,
formatFileSize,
onSeasonChange
onSeasonChange,
parseMyTischtennisUrl,
configureTeamFromUrl,
clearParsedData,
getMyTischtennisStatus,
fetchTeamDataManually
};
}
};
@@ -1379,4 +1644,170 @@ export default {
min-width: auto;
}
}
/* MyTischtennis URL Parser Styles */
.mytischtennis-config {
margin-top: 1.5rem;
padding: 1rem;
background: var(--background-light);
border-radius: var(--border-radius-medium);
border: 1px solid var(--border-color);
}
.mytischtennis-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
gap: 1rem;
}
.mytischtennis-config h4 {
margin: 0;
color: var(--primary-color);
font-size: 1.1rem;
}
.header-actions {
display: flex;
align-items: center;
gap: 0.75rem;
flex-shrink: 0;
}
.current-status {
flex-shrink: 0;
}
.fetch-btn {
padding: 0.5rem 1rem;
background: #2196F3;
color: white;
border: none;
border-radius: var(--border-radius-small);
cursor: pointer;
font-weight: 600;
font-size: 0.85rem;
transition: var(--transition);
white-space: nowrap;
}
.fetch-btn:hover:not(:disabled) {
background: #1976D2;
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(33, 150, 243, 0.3);
}
.fetch-btn:disabled {
background: var(--text-muted);
cursor: not-allowed;
opacity: 0.5;
}
.status-badge {
display: inline-block;
padding: 0.25rem 0.75rem;
border-radius: var(--border-radius-small);
font-size: 0.85rem;
font-weight: 600;
white-space: nowrap;
}
.status-badge.complete {
background: #e8f5e9;
color: #2e7d32;
border: 1px solid #4caf50;
}
.status-badge.partial {
background: #fff3e0;
color: #e65100;
border: 1px solid #ff9800;
}
.status-badge.missing {
background: #ffebee;
color: #c62828;
border: 1px solid #ef5350;
}
.mytischtennis-status {
margin-top: 0.5rem;
padding-top: 0.5rem;
border-top: 1px solid var(--border-color);
}
.mytischtennis-status .status-badge {
font-size: 0.8rem;
padding: 0.2rem 0.6rem;
}
.mytischtennis-url-input {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin-bottom: 1rem;
}
.mytischtennis-url-input label {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.mytischtennis-url-input span {
font-weight: 600;
font-size: 0.9rem;
color: var(--text-secondary);
}
.url-input {
width: 100%;
padding: 0.5rem;
border: 1px solid var(--border-color);
border-radius: var(--border-radius-small);
font-size: 0.9rem;
}
.url-input:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 2px rgba(33, 150, 243, 0.1);
}
.url-input:disabled {
background: var(--background-light);
cursor: wait;
}
.parsing-indicator {
padding: 0.5rem;
background: #e3f2fd;
border: 1px solid #2196F3;
border-radius: var(--border-radius-small);
color: #1976D2;
font-size: 0.9rem;
text-align: center;
font-weight: 600;
}
.error-message {
margin-top: 1rem;
padding: 0.75rem;
background: #ffebee;
border: 1px solid #ef5350;
border-radius: var(--border-radius-small);
color: #c62828;
font-size: 0.9rem;
}
.success-message {
margin-top: 1rem;
padding: 0.75rem;
background: #e8f5e9;
border: 1px solid #66bb6a;
border-radius: var(--border-radius-small);
color: #2e7d32;
font-size: 0.9rem;
}
</style>