Add MyTischtennis fetch log functionality and new endpoints
Enhance MyTischtennis integration by introducing fetch log capabilities. Implement new controller methods to retrieve fetch logs and latest successful fetches for users. Update routes to include these new endpoints. Modify the MyTischtennis model to support fetch logs and ensure proper logging of fetch operations in various services. Update frontend components to display fetch statistics, improving user experience and data visibility.
This commit is contained in:
@@ -142,6 +142,63 @@ class MyTischtennisController {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get fetch logs for current user
|
||||
*/
|
||||
async getFetchLogs(req, res, next) {
|
||||
try {
|
||||
const { userid: userIdOrEmail } = req.headers;
|
||||
|
||||
// Convert email to userId if needed
|
||||
let userId = userIdOrEmail;
|
||||
if (isNaN(userIdOrEmail)) {
|
||||
const User = (await import('../models/User.js')).default;
|
||||
const user = await User.findOne({ where: { email: userIdOrEmail } });
|
||||
if (!user) {
|
||||
return res.status(404).json({ error: 'User not found' });
|
||||
}
|
||||
userId = user.id;
|
||||
}
|
||||
|
||||
const fetchLogService = (await import('../services/myTischtennisFetchLogService.js')).default;
|
||||
const logs = await fetchLogService.getFetchLogs(userId, {
|
||||
limit: req.query.limit ? parseInt(req.query.limit) : 50,
|
||||
fetchType: req.query.type
|
||||
});
|
||||
|
||||
res.status(200).json({ logs });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get latest successful fetches for each type
|
||||
*/
|
||||
async getLatestFetches(req, res, next) {
|
||||
try {
|
||||
const { userid: userIdOrEmail } = req.headers;
|
||||
|
||||
// Convert email to userId if needed
|
||||
let userId = userIdOrEmail;
|
||||
if (isNaN(userIdOrEmail)) {
|
||||
const User = (await import('../models/User.js')).default;
|
||||
const user = await User.findOne({ where: { email: userIdOrEmail } });
|
||||
if (!user) {
|
||||
return res.status(404).json({ error: 'User not found' });
|
||||
}
|
||||
userId = user.id;
|
||||
}
|
||||
|
||||
const fetchLogService = (await import('../services/myTischtennisFetchLogService.js')).default;
|
||||
const latestFetches = await fetchLogService.getLatestSuccessfulFetches(userId);
|
||||
|
||||
res.status(200).json({ latestFetches });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new MyTischtennisController();
|
||||
|
||||
@@ -314,35 +314,105 @@ class MyTischtennisUrlController {
|
||||
}
|
||||
|
||||
// Fetch data for this specific team
|
||||
const result = await autoFetchMatchResultsService.fetchTeamResults(
|
||||
{
|
||||
userId: account.userId,
|
||||
email: account.email,
|
||||
cookie: session.cookie,
|
||||
accessToken: session.accessToken,
|
||||
expiresAt: session.expiresAt,
|
||||
getPassword: () => null // Not needed for manual fetch
|
||||
},
|
||||
team
|
||||
);
|
||||
const startTime = Date.now();
|
||||
let matchResultsSuccess = false;
|
||||
let tableUpdateSuccess = false;
|
||||
let matchResultsCount = 0;
|
||||
let tableUpdateCount = 0;
|
||||
|
||||
try {
|
||||
const result = await autoFetchMatchResultsService.fetchTeamResults(
|
||||
{
|
||||
userId: account.userId,
|
||||
email: account.email,
|
||||
cookie: session.cookie,
|
||||
accessToken: session.accessToken,
|
||||
expiresAt: session.expiresAt,
|
||||
getPassword: () => null // Not needed for manual fetch
|
||||
},
|
||||
team
|
||||
);
|
||||
|
||||
matchResultsSuccess = true;
|
||||
matchResultsCount = result.fetchedCount || 0;
|
||||
|
||||
// Log match results fetch
|
||||
const fetchLogService = (await import('../services/myTischtennisFetchLogService.js')).default;
|
||||
await fetchLogService.logFetch(
|
||||
account.userId,
|
||||
'match_results',
|
||||
true,
|
||||
`${matchResultsCount} Spielergebnisse erfolgreich abgerufen`,
|
||||
{
|
||||
recordsProcessed: matchResultsCount,
|
||||
executionTime: Date.now() - startTime,
|
||||
isAutomatic: false
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error fetching match results:', error);
|
||||
const fetchLogService = (await import('../services/myTischtennisFetchLogService.js')).default;
|
||||
await fetchLogService.logFetch(
|
||||
account.userId,
|
||||
'match_results',
|
||||
false,
|
||||
'Fehler beim Abrufen der Spielergebnisse',
|
||||
{
|
||||
errorDetails: error.message,
|
||||
executionTime: Date.now() - startTime,
|
||||
isAutomatic: false
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Also fetch and update league table data
|
||||
let tableUpdateResult = null;
|
||||
const tableStartTime = Date.now();
|
||||
try {
|
||||
await autoFetchMatchResultsService.fetchAndUpdateLeagueTable(account.userId, team.league.id);
|
||||
tableUpdateResult = 'League table updated successfully';
|
||||
tableUpdateSuccess = true;
|
||||
tableUpdateCount = 1; // One table updated
|
||||
console.log('✓ League table updated for league:', team.league.id);
|
||||
|
||||
// Log league table fetch
|
||||
const fetchLogService = (await import('../services/myTischtennisFetchLogService.js')).default;
|
||||
await fetchLogService.logFetch(
|
||||
account.userId,
|
||||
'league_table',
|
||||
true,
|
||||
'Ligatabelle erfolgreich aktualisiert',
|
||||
{
|
||||
recordsProcessed: tableUpdateCount,
|
||||
executionTime: Date.now() - tableStartTime,
|
||||
isAutomatic: false
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error fetching league table data:', error);
|
||||
tableUpdateResult = 'League table update failed: ' + error.message;
|
||||
|
||||
// Log league table fetch failure
|
||||
const fetchLogService = (await import('../services/myTischtennisFetchLogService.js')).default;
|
||||
await fetchLogService.logFetch(
|
||||
account.userId,
|
||||
'league_table',
|
||||
false,
|
||||
'Fehler beim Aktualisieren der Ligatabelle',
|
||||
{
|
||||
errorDetails: error.message,
|
||||
executionTime: Date.now() - tableStartTime,
|
||||
isAutomatic: false
|
||||
}
|
||||
);
|
||||
// Don't fail the entire request if table update fails
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `${result.fetchedCount} Datensätze abgerufen und verarbeitet`,
|
||||
message: `${matchResultsCount} Datensätze abgerufen und verarbeitet`,
|
||||
data: {
|
||||
fetchedCount: result.fetchedCount,
|
||||
fetchedCount: matchResultsCount,
|
||||
teamName: team.name,
|
||||
tableUpdate: tableUpdateResult
|
||||
}
|
||||
|
||||
20
backend/migrations/create_my_tischtennis_fetch_log.sql
Normal file
20
backend/migrations/create_my_tischtennis_fetch_log.sql
Normal file
@@ -0,0 +1,20 @@
|
||||
-- Create my_tischtennis_fetch_log table for tracking data fetches
|
||||
CREATE TABLE IF NOT EXISTS my_tischtennis_fetch_log (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT NOT NULL,
|
||||
fetch_type ENUM('ratings', 'match_results', 'league_table') NOT NULL COMMENT 'Type of data fetch',
|
||||
success BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
message TEXT,
|
||||
error_details TEXT,
|
||||
records_processed INT NOT NULL DEFAULT 0 COMMENT 'Number of records processed',
|
||||
execution_time INT COMMENT 'Execution time in milliseconds',
|
||||
is_automatic BOOLEAN NOT NULL DEFAULT FALSE COMMENT 'Automatic or manual fetch',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
|
||||
FOREIGN KEY (user_id) REFERENCES user(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
|
||||
INDEX idx_user_fetch_type_created (user_id, fetch_type, created_at),
|
||||
INDEX idx_created_at (created_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
72
backend/models/MyTischtennisFetchLog.js
Normal file
72
backend/models/MyTischtennisFetchLog.js
Normal file
@@ -0,0 +1,72 @@
|
||||
import { DataTypes } from 'sequelize';
|
||||
import sequelize from '../database.js';
|
||||
import User from './User.js';
|
||||
|
||||
const MyTischtennisFetchLog = sequelize.define('MyTischtennisFetchLog', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
allowNull: false,
|
||||
},
|
||||
userId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: User,
|
||||
key: 'id',
|
||||
},
|
||||
onDelete: 'CASCADE',
|
||||
onUpdate: 'CASCADE',
|
||||
},
|
||||
fetchType: {
|
||||
type: DataTypes.ENUM('ratings', 'match_results', 'league_table'),
|
||||
allowNull: false,
|
||||
comment: 'Type of data fetch: ratings, match_results, or league_table'
|
||||
},
|
||||
success: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false,
|
||||
},
|
||||
message: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
},
|
||||
errorDetails: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
},
|
||||
recordsProcessed: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
comment: 'Number of records processed (e.g., players updated, matches fetched)'
|
||||
},
|
||||
executionTime: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
comment: 'Execution time in milliseconds'
|
||||
},
|
||||
isAutomatic: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false,
|
||||
comment: 'Whether this was an automatic or manual fetch'
|
||||
},
|
||||
}, {
|
||||
underscored: true,
|
||||
tableName: 'my_tischtennis_fetch_log',
|
||||
timestamps: true,
|
||||
indexes: [
|
||||
{
|
||||
fields: ['user_id', 'fetch_type', 'created_at']
|
||||
},
|
||||
{
|
||||
fields: ['created_at']
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
export default MyTischtennisFetchLog;
|
||||
|
||||
@@ -37,6 +37,7 @@ import OfficialCompetition from './OfficialCompetition.js';
|
||||
import OfficialCompetitionMember from './OfficialCompetitionMember.js';
|
||||
import MyTischtennis from './MyTischtennis.js';
|
||||
import MyTischtennisUpdateHistory from './MyTischtennisUpdateHistory.js';
|
||||
import MyTischtennisFetchLog from './MyTischtennisFetchLog.js';
|
||||
// Official tournaments relations
|
||||
OfficialTournament.hasMany(OfficialCompetition, { foreignKey: 'tournamentId', as: 'competitions' });
|
||||
OfficialCompetition.belongsTo(OfficialTournament, { foreignKey: 'tournamentId', as: 'tournament' });
|
||||
@@ -234,6 +235,9 @@ MyTischtennis.belongsTo(User, { foreignKey: 'userId', as: 'user' });
|
||||
User.hasMany(MyTischtennisUpdateHistory, { foreignKey: 'userId', as: 'updateHistory' });
|
||||
MyTischtennisUpdateHistory.belongsTo(User, { foreignKey: 'userId', as: 'user' });
|
||||
|
||||
User.hasMany(MyTischtennisFetchLog, { foreignKey: 'userId', as: 'fetchLogs' });
|
||||
MyTischtennisFetchLog.belongsTo(User, { foreignKey: 'userId', as: 'user' });
|
||||
|
||||
export {
|
||||
User,
|
||||
Log,
|
||||
@@ -273,4 +277,5 @@ export {
|
||||
OfficialCompetitionMember,
|
||||
MyTischtennis,
|
||||
MyTischtennisUpdateHistory,
|
||||
MyTischtennisFetchLog,
|
||||
};
|
||||
|
||||
@@ -29,6 +29,12 @@ router.get('/session', myTischtennisController.getSession);
|
||||
// GET /api/mytischtennis/update-history - Get update ratings history
|
||||
router.get('/update-history', myTischtennisController.getUpdateHistory);
|
||||
|
||||
// GET /api/mytischtennis/fetch-logs - Get fetch logs
|
||||
router.get('/fetch-logs', myTischtennisController.getFetchLogs);
|
||||
|
||||
// GET /api/mytischtennis/latest-fetches - Get latest successful fetches
|
||||
router.get('/latest-fetches', myTischtennisController.getLatestFetches);
|
||||
|
||||
// POST /api/mytischtennis/parse-url - Parse myTischtennis URL
|
||||
router.post('/parse-url', myTischtennisUrlController.parseUrl);
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
DiaryNote, DiaryTag, MemberDiaryTag, DiaryDateTag, DiaryMemberNote, DiaryMemberTag,
|
||||
PredefinedActivity, PredefinedActivityImage, DiaryDateActivity, DiaryMemberActivity, Match, League, Team, ClubTeam, TeamDocument, Group,
|
||||
GroupActivity, Tournament, TournamentGroup, TournamentMatch, TournamentResult,
|
||||
TournamentMember, Accident, UserToken, OfficialTournament, OfficialCompetition, OfficialCompetitionMember, MyTischtennis, MyTischtennisUpdateHistory
|
||||
TournamentMember, Accident, UserToken, OfficialTournament, OfficialCompetition, OfficialCompetitionMember, MyTischtennis, MyTischtennisUpdateHistory, MyTischtennisFetchLog
|
||||
} from './models/index.js';
|
||||
import authRoutes from './routes/authRoutes.js';
|
||||
import clubRoutes from './routes/clubRoutes.js';
|
||||
@@ -189,6 +189,7 @@ app.get('*', (req, res) => {
|
||||
await safeSync(UserToken);
|
||||
await safeSync(MyTischtennis);
|
||||
await safeSync(MyTischtennisUpdateHistory);
|
||||
await safeSync(MyTischtennisFetchLog);
|
||||
|
||||
// Start scheduler service
|
||||
schedulerService.start();
|
||||
|
||||
@@ -145,9 +145,11 @@ class MemberService {
|
||||
await checkAccess(userToken, clubId);
|
||||
|
||||
const user = await getUserByToken(userToken);
|
||||
const startTime = Date.now();
|
||||
|
||||
const myTischtennisService = (await import('./myTischtennisService.js')).default;
|
||||
const myTischtennisClient = (await import('../clients/myTischtennisClient.js')).default;
|
||||
const fetchLogService = (await import('./myTischtennisFetchLogService.js')).default;
|
||||
|
||||
try {
|
||||
// 1. myTischtennis-Session abrufen oder Login durchführen
|
||||
@@ -296,6 +298,19 @@ class MemberService {
|
||||
message += ` ${errors.length} Fehler beim Speichern.`;
|
||||
}
|
||||
|
||||
// Log successful ratings fetch
|
||||
await fetchLogService.logFetch(
|
||||
user.id,
|
||||
'ratings',
|
||||
true,
|
||||
message,
|
||||
{
|
||||
recordsProcessed: updated,
|
||||
executionTime: Date.now() - startTime,
|
||||
isAutomatic: false
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
response: {
|
||||
@@ -310,6 +325,20 @@ class MemberService {
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[updateRatingsFromMyTischtennis] - Error:', error);
|
||||
|
||||
// Log failed ratings fetch
|
||||
await fetchLogService.logFetch(
|
||||
user.id,
|
||||
'ratings',
|
||||
false,
|
||||
'Fehler beim Aktualisieren der Wertungen',
|
||||
{
|
||||
errorDetails: error.message,
|
||||
executionTime: Date.now() - startTime,
|
||||
isAutomatic: false
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
status: 500,
|
||||
response: {
|
||||
|
||||
129
backend/services/myTischtennisFetchLogService.js
Normal file
129
backend/services/myTischtennisFetchLogService.js
Normal file
@@ -0,0 +1,129 @@
|
||||
import MyTischtennisFetchLog from '../models/MyTischtennisFetchLog.js';
|
||||
import { devLog } from '../utils/logger.js';
|
||||
import { Op } from 'sequelize';
|
||||
import sequelize from '../database.js';
|
||||
|
||||
class MyTischtennisFetchLogService {
|
||||
/**
|
||||
* Log a fetch attempt
|
||||
*/
|
||||
async logFetch(userId, fetchType, success, message, options = {}) {
|
||||
try {
|
||||
await MyTischtennisFetchLog.create({
|
||||
userId,
|
||||
fetchType,
|
||||
success,
|
||||
message,
|
||||
errorDetails: options.errorDetails || null,
|
||||
recordsProcessed: options.recordsProcessed || 0,
|
||||
executionTime: options.executionTime || null,
|
||||
isAutomatic: options.isAutomatic || false
|
||||
});
|
||||
|
||||
devLog(`[FetchLog] ${fetchType} - ${success ? 'SUCCESS' : 'FAILED'} - User ${userId}`);
|
||||
} catch (error) {
|
||||
console.error('Error logging fetch:', error);
|
||||
// Don't throw - logging failures shouldn't break the main operation
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get fetch logs for a user
|
||||
*/
|
||||
async getFetchLogs(userId, options = {}) {
|
||||
try {
|
||||
const where = { userId };
|
||||
|
||||
if (options.fetchType) {
|
||||
where.fetchType = options.fetchType;
|
||||
}
|
||||
|
||||
if (options.success !== undefined) {
|
||||
where.success = options.success;
|
||||
}
|
||||
|
||||
const logs = await MyTischtennisFetchLog.findAll({
|
||||
where,
|
||||
order: [['createdAt', 'DESC']],
|
||||
limit: options.limit || 50,
|
||||
attributes: [
|
||||
'id', 'fetchType', 'success', 'message', 'errorDetails',
|
||||
'recordsProcessed', 'executionTime', 'isAutomatic', 'createdAt'
|
||||
]
|
||||
});
|
||||
|
||||
return logs;
|
||||
} catch (error) {
|
||||
console.error('Error getting fetch logs:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get latest successful fetch for each type
|
||||
*/
|
||||
async getLatestSuccessfulFetches(userId) {
|
||||
try {
|
||||
const fetchTypes = ['ratings', 'match_results', 'league_table'];
|
||||
const results = {};
|
||||
|
||||
for (const fetchType of fetchTypes) {
|
||||
const latestFetch = await MyTischtennisFetchLog.findOne({
|
||||
where: {
|
||||
userId,
|
||||
fetchType,
|
||||
success: true
|
||||
},
|
||||
order: [['createdAt', 'DESC']],
|
||||
attributes: ['createdAt', 'recordsProcessed', 'executionTime']
|
||||
});
|
||||
|
||||
results[fetchType] = latestFetch ? {
|
||||
lastFetch: latestFetch.createdAt,
|
||||
recordsProcessed: latestFetch.recordsProcessed,
|
||||
executionTime: latestFetch.executionTime
|
||||
} : null;
|
||||
}
|
||||
|
||||
return results;
|
||||
} catch (error) {
|
||||
console.error('Error getting latest successful fetches:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get fetch statistics
|
||||
*/
|
||||
async getFetchStatistics(userId, days = 30) {
|
||||
try {
|
||||
const since = new Date();
|
||||
since.setDate(since.getDate() - days);
|
||||
|
||||
const stats = await MyTischtennisFetchLog.findAll({
|
||||
where: {
|
||||
userId,
|
||||
createdAt: {
|
||||
[Op.gte]: since
|
||||
}
|
||||
},
|
||||
attributes: [
|
||||
'fetchType',
|
||||
[sequelize.fn('COUNT', sequelize.col('id')), 'totalFetches'],
|
||||
[sequelize.fn('SUM', sequelize.literal('CASE WHEN success = true THEN 1 ELSE 0 END')), 'successfulFetches'],
|
||||
[sequelize.fn('SUM', sequelize.col('records_processed')), 'totalRecordsProcessed'],
|
||||
[sequelize.fn('AVG', sequelize.col('execution_time')), 'avgExecutionTime']
|
||||
],
|
||||
group: ['fetchType']
|
||||
});
|
||||
|
||||
return stats;
|
||||
} catch (error) {
|
||||
console.error('Error getting fetch statistics:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new MyTischtennisFetchLogService();
|
||||
|
||||
@@ -51,6 +51,58 @@
|
||||
<button class="btn-danger" @click="deleteAccount">Account trennen</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Fetch Statistics Section -->
|
||||
<div class="info-section fetch-stats-section" v-if="account">
|
||||
<h2>Datenabruf-Statistiken</h2>
|
||||
|
||||
<div v-if="loadingStats" class="loading-stats">Lade Statistiken...</div>
|
||||
|
||||
<div v-else-if="latestFetches" class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">📊</div>
|
||||
<div class="stat-content">
|
||||
<h3>Spielerwertungen</h3>
|
||||
<div v-if="latestFetches.ratings">
|
||||
<p class="stat-date">{{ formatDateRelative(latestFetches.ratings.lastFetch) }}</p>
|
||||
<p class="stat-detail">{{ latestFetches.ratings.recordsProcessed }} Spieler aktualisiert</p>
|
||||
<p class="stat-time" v-if="latestFetches.ratings.executionTime">{{ latestFetches.ratings.executionTime }}ms</p>
|
||||
</div>
|
||||
<p v-else class="stat-never">Noch nie abgerufen</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">🏓</div>
|
||||
<div class="stat-content">
|
||||
<h3>Spielergebnisse</h3>
|
||||
<div v-if="latestFetches.match_results">
|
||||
<p class="stat-date">{{ formatDateRelative(latestFetches.match_results.lastFetch) }}</p>
|
||||
<p class="stat-detail">{{ latestFetches.match_results.recordsProcessed }} Ergebnisse</p>
|
||||
<p class="stat-time" v-if="latestFetches.match_results.executionTime">{{ latestFetches.match_results.executionTime }}ms</p>
|
||||
</div>
|
||||
<p v-else class="stat-never">Noch nie abgerufen</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">📋</div>
|
||||
<div class="stat-content">
|
||||
<h3>Ligatabellen</h3>
|
||||
<div v-if="latestFetches.league_table">
|
||||
<p class="stat-date">{{ formatDateRelative(latestFetches.league_table.lastFetch) }}</p>
|
||||
<p class="stat-detail">{{ latestFetches.league_table.recordsProcessed }} Teams</p>
|
||||
<p class="stat-time" v-if="latestFetches.league_table.executionTime">{{ latestFetches.league_table.executionTime }}ms</p>
|
||||
</div>
|
||||
<p v-else class="stat-never">Noch nie abgerufen</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn-secondary refresh-stats-btn" @click="loadLatestFetches">
|
||||
🔄 Statistiken aktualisieren
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="no-account">
|
||||
@@ -141,13 +193,16 @@ export default {
|
||||
resolveCallback: null
|
||||
},
|
||||
loading: true,
|
||||
loadingStats: false,
|
||||
account: null,
|
||||
latestFetches: null,
|
||||
showDialog: false,
|
||||
showHistoryDialog: false
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.loadAccount();
|
||||
this.loadLatestFetches();
|
||||
},
|
||||
methods: {
|
||||
// Dialog Helper Methods
|
||||
@@ -247,23 +302,34 @@ export default {
|
||||
},
|
||||
|
||||
async deleteAccount() {
|
||||
if (!confirm('Möchten Sie die Verknüpfung zum myTischtennis-Account wirklich trennen?')) {
|
||||
return;
|
||||
}
|
||||
const confirmed = await this.showConfirm(
|
||||
'Account trennen',
|
||||
'Möchten Sie die Verknüpfung zum myTischtennis-Account wirklich trennen?',
|
||||
'',
|
||||
'danger'
|
||||
);
|
||||
|
||||
if (!confirmed) return;
|
||||
|
||||
try {
|
||||
await apiClient.delete('/mytischtennis/account');
|
||||
this.account = null;
|
||||
this.$store.dispatch('showMessage', {
|
||||
text: 'myTischtennis-Account erfolgreich getrennt',
|
||||
type: 'success'
|
||||
});
|
||||
this.showInfo('Erfolg', 'myTischtennis-Account erfolgreich getrennt', '', 'success');
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Löschen des Accounts:', error);
|
||||
this.$store.dispatch('showMessage', {
|
||||
text: 'Fehler beim Trennen des Accounts',
|
||||
type: 'error'
|
||||
});
|
||||
this.showInfo('Fehler', 'Fehler beim Trennen des Accounts', error.message, 'error');
|
||||
}
|
||||
},
|
||||
|
||||
async loadLatestFetches() {
|
||||
this.loadingStats = true;
|
||||
try {
|
||||
const response = await apiClient.get('/mytischtennis/latest-fetches');
|
||||
this.latestFetches = response.data.latestFetches;
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Fetch-Statistiken:', error);
|
||||
} finally {
|
||||
this.loadingStats = false;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -277,6 +343,31 @@ export default {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
},
|
||||
|
||||
formatDateRelative(dateString) {
|
||||
if (!dateString) return 'Nie';
|
||||
|
||||
const date = new Date(dateString);
|
||||
const now = new Date();
|
||||
const diffMs = now - date;
|
||||
const diffMins = Math.floor(diffMs / 60000);
|
||||
const diffHours = Math.floor(diffMs / 3600000);
|
||||
const diffDays = Math.floor(diffMs / 86400000);
|
||||
|
||||
if (diffMins < 1) return 'Gerade eben';
|
||||
if (diffMins < 60) return `vor ${diffMins} Min.`;
|
||||
if (diffHours < 24) return `vor ${diffHours} Std.`;
|
||||
if (diffDays === 1) return 'Gestern';
|
||||
if (diffDays < 7) return `vor ${diffDays} Tagen`;
|
||||
|
||||
return date.toLocaleDateString('de-DE', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -403,6 +494,85 @@ h1 {
|
||||
background-color: #545b62;
|
||||
}
|
||||
|
||||
/* Fetch Statistics */
|
||||
.fetch-stats-section {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.loading-stats {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 1rem;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
font-size: 2.5rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.stat-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.stat-content h3 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
font-size: 1rem;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.stat-date {
|
||||
font-weight: 600;
|
||||
color: #28a745;
|
||||
margin: 0.25rem 0;
|
||||
}
|
||||
|
||||
.stat-detail {
|
||||
font-size: 0.9rem;
|
||||
color: #666;
|
||||
margin: 0.25rem 0;
|
||||
}
|
||||
|
||||
.stat-time {
|
||||
font-size: 0.8rem;
|
||||
color: #999;
|
||||
margin: 0.25rem 0;
|
||||
}
|
||||
|
||||
.stat-never {
|
||||
font-style: italic;
|
||||
color: #999;
|
||||
margin: 0.25rem 0;
|
||||
}
|
||||
|
||||
.refresh-stats-btn {
|
||||
width: 100%;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background-color: #dc3545;
|
||||
color: white;
|
||||
|
||||
Reference in New Issue
Block a user