Entfernt die myTischtennis-Integration aus dem Backend und Frontend. Löscht Controller, Routen und Service für myTischtennis. Aktualisiert die Datenmodelle, um die neue ExternalServiceAccount-Integration zu unterstützen. Ändert die API-Routen und Frontend-Komponenten, um die neuen Endpunkte zu verwenden.

This commit is contained in:
Torsten Schulz (local)
2025-10-01 21:01:09 +02:00
parent 1ef1711eea
commit 7be98ffeeb
18 changed files with 2008 additions and 441 deletions

View File

@@ -75,6 +75,10 @@
<span class="nav-icon">🔗</span>
myTischtennis-Account
</a>
<a href="/hettv-account" class="nav-link">
<span class="nav-icon">🔗</span>
HeTTV-Account
</a>
</div>
</nav>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,12 @@
<template>
<div>
<h2>Spielpläne</h2>
<button @click="openImportModal">Spielplanimport</button>
<div class="button-group">
<button @click="openImportModal">Spielplanimport</button>
<button @click="loadHettvData" :disabled="loadingHettv" class="hettv-button">
{{ loadingHettv ? 'Lade HeTTV-Daten...' : 'HeTTV-Daten laden' }}
</button>
</div>
<div v-if="hoveredMatch && hoveredMatch.location" class="hover-info">
<p><strong>{{ hoveredMatch.location.name || 'N/A' }}</strong></p>
<p>{{ hoveredMatch.location.address || 'N/A' }}</p>
@@ -79,6 +84,8 @@ export default {
matches: [],
selectedLeague: '',
hoveredMatch: null,
loadingHettv: false,
hettvData: null,
};
},
methods: {
@@ -174,10 +181,15 @@ export default {
async loadLeagues() {
try {
const clubId = this.currentClub;
if (!clubId || clubId === 'null') {
console.log('Kein Club ausgewählt, überspringe Liga-Laden');
return;
}
const response = await apiClient.get(`/matches/leagues/current/${clubId}`);
this.leagues = this.sortLeagues(response.data);
} catch (error) {
alert('Fehler beim Laden der Ligen');
console.log('Fehler beim Laden der Ligen:', error.message);
// Keine Alert mehr, da das normal ist wenn kein Club-Zugriff vorhanden
}
},
async loadMatchesForLeague(leagueId, leagueName) {
@@ -301,6 +313,39 @@ export default {
return ''; // Keine besondere Farbe
},
async loadHettvData() {
this.loadingHettv = true;
try {
console.log('Lade HeTTV-Hauptseite...');
const response = await apiClient.get('/external-service/hettv/main-page');
if (response.data.success) {
this.hettvData = response.data.data;
console.log('HeTTV-Daten geladen:', {
downloadLinks: this.hettvData.downloadLinks,
htmlLength: this.hettvData.htmlContent.length,
trace: this.hettvData.trace,
lastUrl: this.hettvData.lastUrl,
lastStatus: this.hettvData.lastStatus,
savedFile: this.hettvData.savedFile
});
// Zeige gefundene Download-Links
if (this.hettvData.downloadLinks.length > 0) {
alert(`HeTTV-Daten erfolgreich geladen!\n\nGefundene Download-Links:\n${this.hettvData.downloadLinks.join('\n')}`);
} else {
alert('HeTTV-Hauptseite geladen, aber keine Download-Links gefunden.');
}
} else {
alert('Fehler beim Laden der HeTTV-Daten: ' + response.data.error);
}
} catch (error) {
console.error('HeTTV-Fehler:', error);
alert('Fehler beim Laden der HeTTV-Daten: ' + (error.response?.data?.error || error.message));
} finally {
this.loadingHettv = false;
}
},
},
async created() {
await this.loadLeagues();
@@ -451,4 +496,37 @@ li {
.match-next-week:hover {
background-color: #b8daff !important; /* Dunkleres Blau beim Hover */
}
.button-group {
margin-bottom: 20px;
}
.button-group button {
margin-right: 10px;
padding: 8px 16px;
border: 1px solid #ddd;
background-color: #f8f9fa;
cursor: pointer;
border-radius: 4px;
}
.button-group button:hover {
background-color: #e9ecef;
}
.hettv-button {
background-color: #007bff !important;
color: white !important;
border-color: #007bff !important;
}
.hettv-button:hover {
background-color: #0056b3 !important;
}
.hettv-button:disabled {
background-color: #6c757d !important;
border-color: #6c757d !important;
cursor: not-allowed;
}
</style>