Update MyTischtennis functionality to support automatic rating updates. Introduce new autoUpdateRatings field in MyTischtennis model and enhance MyTischtennisController to handle update history retrieval. Integrate node-cron for scheduling daily updates at 6:00 AM. Update frontend components to allow users to enable/disable automatic updates and display last update timestamps.

This commit is contained in:
Torsten Schulz (local)
2025-10-09 00:18:41 +02:00
parent 806cb527d4
commit 993e12d4a5
47 changed files with 1983 additions and 683 deletions

View File

@@ -13,6 +13,7 @@
"html2canvas": "^1.4.1",
"jspdf": "^2.5.2",
"jspdf-autotable": "^5.0.2",
"node-cron": "^4.2.1",
"sortablejs": "^1.15.3",
"vue": "^3.2.13",
"vue-multiselect": "^3.0.0",
@@ -2545,6 +2546,15 @@
"license": "MIT",
"optional": true
},
"node_modules/node-cron": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/node-cron/-/node-cron-4.2.1.tgz",
"integrity": "sha512-lgimEHPE/QDgFlywTd8yTR61ptugX3Qer29efeyWw2rv259HtGBNn1vZVmp8lB9uo9wC0t/AT4iGqXxia+CJFg==",
"license": "ISC",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/nth-check": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",

View File

@@ -13,6 +13,7 @@
"html2canvas": "^1.4.1",
"jspdf": "^2.5.2",
"jspdf-autotable": "^5.0.2",
"node-cron": "^4.2.1",
"sortablejs": "^1.15.3",
"vue": "^3.2.13",
"vue-multiselect": "^3.0.0",

View File

@@ -41,6 +41,24 @@
</p>
</div>
<div class="form-group checkbox-group">
<label>
<input
type="checkbox"
v-model="formData.autoUpdateRatings"
:disabled="!formData.savePassword"
/>
<span>Automatische Update-Ratings aktivieren</span>
</label>
<p class="hint">
Täglich um 6:00 Uhr werden automatisch die neuesten Ratings von myTischtennis abgerufen.
<strong>Erfordert gespeichertes Passwort.</strong>
</p>
<p v-if="!formData.savePassword" class="warning">
Für automatische Updates muss das myTischtennis-Passwort gespeichert werden.
</p>
</div>
<div class="form-group" v-if="formData.password">
<label for="app-password">Ihr App-Passwort zur Bestätigung:</label>
<input
@@ -90,6 +108,7 @@ export default {
email: this.account?.email || '',
password: '',
savePassword: this.account?.savePassword || false,
autoUpdateRatings: this.account?.autoUpdateRatings || false,
userPassword: ''
},
saving: false,
@@ -108,6 +127,11 @@ export default {
return false;
}
// Automatische Updates erfordern gespeichertes Passwort
if (this.formData.autoUpdateRatings && !this.formData.savePassword) {
return false;
}
return true;
}
},
@@ -121,7 +145,8 @@ export default {
try {
const payload = {
email: this.formData.email,
savePassword: this.formData.savePassword
savePassword: this.formData.savePassword,
autoUpdateRatings: this.formData.autoUpdateRatings
};
// Nur password und userPassword hinzufügen, wenn ein Passwort eingegeben wurde
@@ -243,6 +268,13 @@ export default {
font-style: italic;
}
.warning {
margin-top: 0.5rem;
font-size: 0.875rem;
color: #dc3545;
font-weight: 600;
}
.error-message {
padding: 0.75rem;
background-color: #f8d7da;

View File

@@ -0,0 +1,228 @@
<template>
<div class="modal-overlay" @click.self="$emit('close')">
<div class="modal">
<div class="modal-header">
<h3>Update-Ratings History</h3>
</div>
<div class="modal-body">
<div v-if="loading" class="loading">
Lade History...
</div>
<div v-else-if="history.length === 0" class="no-history">
<p>Noch keine automatischen Updates durchgeführt.</p>
</div>
<div v-else class="history-list">
<div v-for="entry in history" :key="entry.id" class="history-entry">
<div class="history-header">
<span class="history-date">{{ formatDate(entry.createdAt) }}</span>
<span class="history-status" :class="entry.success ? 'success' : 'error'">
{{ entry.success ? 'Erfolgreich' : 'Fehlgeschlagen' }}
</span>
</div>
<div v-if="entry.message" class="history-message">
{{ entry.message }}
</div>
<div v-if="entry.errorDetails" class="history-error">
{{ entry.errorDetails }}
</div>
<div v-if="entry.updatedCount !== undefined" class="history-stats">
{{ entry.updatedCount }} Ratings aktualisiert
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn-secondary" @click="$emit('close')">
Schließen
</button>
</div>
</div>
</div>
</template>
<script>
import apiClient from '../apiClient.js';
export default {
name: 'MyTischtennisHistoryDialog',
data() {
return {
loading: true,
history: []
};
},
async mounted() {
await this.loadHistory();
},
methods: {
async loadHistory() {
try {
this.loading = true;
const response = await apiClient.get('/mytischtennis/update-history');
this.history = response.data.history || [];
} catch (error) {
console.error('Fehler beim Laden der History:', error);
this.history = [];
} finally {
this.loading = false;
}
},
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',
second: '2-digit'
});
}
}
};
</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: 800px;
width: 90%;
max-height: 80vh;
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;
}
.loading {
text-align: center;
padding: 2rem;
color: #6c757d;
}
.no-history {
text-align: center;
padding: 2rem;
color: #6c757d;
}
.history-list {
max-height: 400px;
overflow-y: auto;
}
.history-entry {
border: 1px solid #dee2e6;
border-radius: 4px;
padding: 1rem;
margin-bottom: 1rem;
background: #f8f9fa;
}
.history-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem;
}
.history-date {
font-weight: 600;
color: #495057;
}
.history-status {
padding: 0.25rem 0.5rem;
border-radius: 3px;
font-size: 0.875rem;
font-weight: 600;
}
.history-status.success {
background-color: #d4edda;
color: #155724;
}
.history-status.error {
background-color: #f8d7da;
color: #721c24;
}
.history-message {
margin-top: 0.5rem;
color: #495057;
font-size: 0.875rem;
}
.history-error {
margin-top: 0.5rem;
color: #dc3545;
font-size: 0.875rem;
font-style: italic;
}
.history-stats {
margin-top: 0.5rem;
color: #28a745;
font-size: 0.875rem;
font-weight: 600;
}
.btn-secondary {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 4px;
font-size: 1rem;
cursor: pointer;
transition: background-color 0.2s ease;
background-color: #6c757d;
color: white;
}
.btn-secondary:hover {
background-color: #545b62;
}
</style>

View File

@@ -34,9 +34,20 @@
<span>{{ formatDate(account.lastLoginAttempt) }}</span>
</div>
<div class="info-row" v-if="account.lastUpdateRatings">
<label>Letzter Abruf:</label>
<span>{{ formatDate(account.lastUpdateRatings) }}</span>
</div>
<div class="info-row" v-if="account.autoUpdateRatings !== undefined">
<label>Automatische Updates:</label>
<span>{{ account.autoUpdateRatings ? 'Aktiviert' : 'Deaktiviert' }}</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-info" @click="openHistoryDialog" v-if="account">Update-History</button>
<button class="btn-danger" @click="deleteAccount">Account trennen</button>
</div>
</div>
@@ -66,6 +77,12 @@
@close="closeDialog"
@saved="onAccountSaved"
/>
<!-- History Dialog -->
<MyTischtennisHistoryDialog
v-if="showHistoryDialog"
@close="closeHistoryDialog"
/>
</div>
@@ -93,16 +110,18 @@
<script>
import apiClient from '../apiClient.js';
import MyTischtennisDialog from '../components/MyTischtennisDialog.vue';
import MyTischtennisHistoryDialog from '../components/MyTischtennisHistoryDialog.vue';
import InfoDialog from '../components/InfoDialog.vue';
import ConfirmDialog from '../components/ConfirmDialog.vue';
export default {
name: 'MyTischtennisAccount',
components: {
MyTischtennisDialog
,
InfoDialog,
ConfirmDialog},
MyTischtennisDialog,
MyTischtennisHistoryDialog,
InfoDialog,
ConfirmDialog
},
data() {
return {
// Dialog States
@@ -123,7 +142,8 @@ export default {
},
loading: true,
account: null,
showDialog: false
showDialog: false,
showHistoryDialog: false
};
},
mounted() {
@@ -186,6 +206,14 @@ export default {
this.showDialog = false;
},
openHistoryDialog() {
this.showHistoryDialog = true;
},
closeHistoryDialog() {
this.showHistoryDialog = false;
},
async onAccountSaved() {
this.closeDialog();
await this.loadAccount();
@@ -383,5 +411,14 @@ h1 {
.btn-danger:hover {
background-color: #c82333;
}
.btn-info {
background-color: #17a2b8;
color: white;
}
.btn-info:hover {
background-color: #138496;
}
</style>