Statistiken und Analytics im dashboard gefixed

This commit is contained in:
2025-09-16 01:40:40 +02:00
parent 10a150cb39
commit 69e3985af3
2 changed files with 182 additions and 36 deletions

View File

@@ -46,6 +46,9 @@ async function initDashboard() {
// Load times section
checkLinkStatusAndLoadTimes();
// Update analytics and statistics cards
updateAnalyticsAndStatisticsCards();
} catch (error) {
console.error('An unexpected error occurred:', error);
@@ -148,6 +151,9 @@ function updateDynamicContent() {
// Update achievement progress text
updateAchievementProgressText();
// Update analytics and statistics cards
updateAnalyticsAndStatisticsCards();
// Reload achievements if they're loaded
if (window.allAchievements && window.allAchievements.length > 0) {
@@ -175,6 +181,114 @@ function loadLanguagePreference() {
}
}
// Update analytics and statistics cards based on link status
function updateAnalyticsAndStatisticsCards() {
const analyticsCard = document.getElementById('analyticsCard');
const statisticsCard = document.getElementById('statisticsCard');
if (!currentPlayerId) {
// User not linked - show appropriate message
if (analyticsCard) {
const isGerman = currentLanguage === 'de';
const message = isGerman ?
'RFID verknüpfen erforderlich' :
'RFID linking required';
const description = isGerman ?
'Verknüpfe deine RFID-Karte, um Analytics zu sehen.' :
'Link your RFID card to view analytics.';
analyticsCard.innerHTML = `
<h3>📊 Analytics</h3>
<div style="background: rgba(239, 68, 68, 0.1); border: 1px solid rgba(239, 68, 68, 0.3); border-radius: 0.5rem; padding: 1rem; margin: 1rem 0;">
<div style="display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.5rem;">
<span style="color: #ef4444; font-weight: 600;">⚠️</span>
<span style="color: #ef4444; font-weight: 600;">${message}</span>
</div>
<div style="font-size: 0.9rem; color: #8892b0;">
${description}
</div>
</div>
<button class="btn btn-primary" style="margin-top: 1rem;" onclick="event.stopPropagation(); showRFIDSettings();" data-de="RFID verknüpfen" data-en="Link RFID">RFID verknüpfen</button>
`;
}
if (statisticsCard) {
const isGerman = currentLanguage === 'de';
const message = isGerman ?
'RFID verknüpfen erforderlich' :
'RFID linking required';
const description = isGerman ?
'Verknüpfe deine RFID-Karte, um Statistiken zu sehen.' :
'Link your RFID card to view statistics.';
statisticsCard.innerHTML = `
<h3>📊 Statistiken</h3>
<div style="background: rgba(239, 68, 68, 0.1); border: 1px solid rgba(239, 68, 68, 0.3); border-radius: 0.5rem; padding: 1rem; margin: 1rem 0;">
<div style="display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.5rem;">
<span style="color: #ef4444; font-weight: 600;">⚠️</span>
<span style="color: #ef4444; font-weight: 600;">${message}</span>
</div>
<div style="font-size: 0.9rem; color: #8892b0;">
${description}
</div>
</div>
<button class="btn btn-primary" style="margin-top: 1rem;" onclick="event.stopPropagation(); showRFIDSettings();" data-de="RFID verknüpfen" data-en="Link RFID">RFID verknüpfen</button>
`;
}
} else {
// User is linked - restore original cards
if (analyticsCard) {
const isGerman = currentLanguage === 'de';
analyticsCard.innerHTML = `
<h3>📊 Analytics</h3>
<div id="analyticsPreview" style="display: none;">
<div class="analytics-stats">
<div class="mini-stat">
<div class="mini-stat-number" id="avgTimeThisWeek">--:--</div>
<div class="mini-stat-label">${isGerman ? 'Durchschnitt diese Woche' : 'Average this week'}</div>
</div>
<div class="mini-stat">
<div class="mini-stat-number" id="improvementThisWeek">+0.0%</div>
<div class="mini-stat-label">${isGerman ? 'Verbesserung' : 'Improvement'}</div>
</div>
<div class="mini-stat">
<div class="mini-stat-number" id="runsThisWeek">0</div>
<div class="mini-stat-label">${isGerman ? 'Läufe diese Woche' : 'Runs this week'}</div>
</div>
</div>
</div>
<p>${isGerman ? 'Verfolge deine Leistung und überwache wichtige Metriken.' : 'Track your performance and monitor important metrics.'}</p>
<button class="btn btn-primary" style="margin-top: 1rem;" onclick="event.stopPropagation(); showAnalytics();" data-de="Analytics öffnen" data-en="Open Analytics">${isGerman ? 'Analytics öffnen' : 'Open Analytics'}</button>
`;
}
if (statisticsCard) {
const isGerman = currentLanguage === 'de';
statisticsCard.innerHTML = `
<h3>📊 Statistiken</h3>
<div id="statisticsPreview" style="display: none;">
<div class="statistics-stats">
<div class="mini-stat">
<div class="mini-stat-number" id="personalBest">--:--</div>
<div class="mini-stat-label">${isGerman ? 'Persönliche Bestzeit' : 'Personal Best'}</div>
</div>
<div class="mini-stat">
<div class="mini-stat-number" id="totalRunsCount">0</div>
<div class="mini-stat-label">${isGerman ? 'Gesamte Läufe' : 'Total Runs'}</div>
</div>
<div class="mini-stat">
<div class="mini-stat-number" id="rankPosition">-</div>
<div class="mini-stat-label">${isGerman ? 'Ranglisten-Position' : 'Ranking Position'}</div>
</div>
</div>
</div>
<p>${isGerman ? 'Detaillierte Statistiken zu deinen Läufen - beste Zeiten, Verbesserungen und Vergleiche.' : 'Detailed statistics about your runs - best times, improvements and comparisons.'}</p>
<button class="btn btn-primary" style="margin-top: 1rem;" onclick="event.stopPropagation(); showStatistics();" data-de="Statistiken öffnen" data-en="Open Statistics">${isGerman ? 'Statistiken öffnen' : 'Open Statistics'}</button>
`;
}
}
}
// Update achievement notifications
function updateAchievementNotifications() {
// This will be called when achievements are displayed
@@ -797,6 +911,9 @@ async function loadUserTimesSection(playerData) {
// Initialize achievements for this player
initializeAchievements(playerData.id);
// Update analytics and statistics cards (user is now linked)
updateAnalyticsAndStatisticsCards();
} catch (error) {
console.error('Error loading user times:', error);
@@ -963,6 +1080,11 @@ function convertTimeToSeconds(timeValue) {
// Format time interval to readable format
function formatTime(interval) {
// Handle numeric values (seconds)
if (typeof interval === 'number') {
return formatSeconds(interval);
}
// Postgres interval format: {"hours":0,"minutes":1,"seconds":23.45}
if (typeof interval === 'object') {
const { hours = 0, minutes = 0, seconds = 0 } = interval;
@@ -1212,9 +1334,10 @@ function showAnalytics() {
console.log('Analytics section shown');
} else {
console.error('Analytics section not found');
return;
}
// Load analytics data
// Load analytics data (will show fallback if not linked)
loadAnalyticsData();
}
@@ -1237,30 +1360,33 @@ function showStatistics() {
console.log('Statistics section shown');
} else {
console.error('Statistics section not found');
return;
}
// Load statistics data
// Load statistics data (will show fallback if not linked)
loadStatisticsData();
}
async function loadAnalyticsData() {
try {
if (!currentPlayerId) {
console.error('No player ID available');
console.error('No player ID available - user not linked');
// Show fallback data when user is not linked
displayAnalyticsFallback();
return;
}
// Load analytics data from API
const response = await fetch(`/api/v1/analytics/player/${currentPlayerId}`);
const response = await fetch(`/api/analytics/player/${currentPlayerId}`);
if (!response.ok) {
throw new Error('Failed to load analytics data');
}
const analyticsData = await response.json();
displayAnalyticsData(analyticsData);
displayAnalyticsData(analyticsData.data);
// Update preview in main card
updateAnalyticsPreview(analyticsData);
updateAnalyticsPreview(analyticsData.data);
} catch (error) {
console.error('Error loading analytics data:', error);
@@ -1272,21 +1398,23 @@ async function loadAnalyticsData() {
async function loadStatisticsData() {
try {
if (!currentPlayerId) {
console.error('No player ID available');
console.error('No player ID available - user not linked');
// Show fallback data when user is not linked
displayStatisticsFallback();
return;
}
// Load statistics data from API
const response = await fetch(`/api/v1/statistics/player/${currentPlayerId}`);
const response = await fetch(`/api/statistics/player/${currentPlayerId}`);
if (!response.ok) {
throw new Error('Failed to load statistics data');
}
const statisticsData = await response.json();
displayStatisticsData(statisticsData);
displayStatisticsData(statisticsData.data);
// Update preview in main card
updateStatisticsPreview(statisticsData);
updateStatisticsPreview(statisticsData.data);
} catch (error) {
console.error('Error loading statistics data:', error);
@@ -1299,12 +1427,12 @@ function displayAnalyticsData(data) {
// Performance Trends
document.getElementById('avgTimeThisWeekDetail').textContent = formatTime(data.performance.avgTimeThisWeek);
document.getElementById('avgTimeLastWeek').textContent = formatTime(data.performance.avgTimeLastWeek);
document.getElementById('improvementDetail').textContent = data.performance.improvement + '%';
document.getElementById('improvementDetail').textContent = data.performance.improvement.toFixed(2) + '%';
// Activity Stats
document.getElementById('runsToday').textContent = data.activity.runsToday + ' Läufe';
document.getElementById('runsThisWeekDetail').textContent = data.activity.runsThisWeek + ' Läufe';
document.getElementById('avgRunsPerDay').textContent = data.activity.avgRunsPerDay.toFixed(1);
document.getElementById('avgRunsPerDay').textContent = data.activity.avgRunsPerDay.toFixed(2);
// Location Performance
displayLocationPerformance(data.locationPerformance);
@@ -1322,7 +1450,7 @@ function displayStatisticsData(data) {
// Consistency Metrics
document.getElementById('averageTime').textContent = formatTime(data.consistency.averageTime);
document.getElementById('timeDeviation').textContent = formatTime(data.consistency.timeDeviation);
document.getElementById('consistencyScore').textContent = data.consistency.consistencyScore + '%';
document.getElementById('consistencyScore').textContent = data.consistency.consistencyScore.toFixed(2) + '%';
// Ranking Stats
displayRankingStats(data.rankings);
@@ -1397,40 +1525,58 @@ function displayRankingStats(rankings) {
}
function updateAnalyticsPreview(data) {
document.getElementById('avgTimeThisWeek').textContent = formatTime(data.performance.avgTimeThisWeek);
document.getElementById('improvementThisWeek').textContent = data.performance.improvement + '%';
document.getElementById('runsThisWeek').textContent = data.activity.runsThisWeek;
document.getElementById('analyticsPreview').style.display = 'block';
if (data && data.performance && data.activity) {
document.getElementById('avgTimeThisWeek').textContent = formatTime(data.performance.avgTimeThisWeek);
document.getElementById('improvementThisWeek').textContent = data.performance.improvement.toFixed(2) + '%';
document.getElementById('runsThisWeek').textContent = data.activity.runsThisWeek;
document.getElementById('analyticsPreview').style.display = 'block';
} else {
// Hide preview if no data
document.getElementById('analyticsPreview').style.display = 'none';
}
}
function updateStatisticsPreview(data) {
document.getElementById('personalBest').textContent = formatTime(data.personalRecords[0]?.time || 0);
document.getElementById('totalRunsCount').textContent = data.progress.totalRuns;
document.getElementById('rankPosition').textContent = data.rankings[0]?.position || '-';
document.getElementById('statisticsPreview').style.display = 'block';
if (data && data.personalRecords && data.progress) {
document.getElementById('personalBest').textContent = formatTime(data.personalRecords[0]?.time || 0);
document.getElementById('totalRunsCount').textContent = data.progress.totalRuns;
document.getElementById('rankPosition').textContent = data.rankings[0]?.position || '-';
document.getElementById('statisticsPreview').style.display = 'block';
} else {
// Hide preview if no data
document.getElementById('statisticsPreview').style.display = 'none';
}
}
function displayAnalyticsFallback() {
// Show fallback data when API fails
// Show fallback data when API fails or user not linked
const notLinkedMessage = currentLanguage === 'de' ?
'RFID nicht verknüpft - Analytics nicht verfügbar' :
'RFID not linked - Analytics not available';
document.getElementById('avgTimeThisWeekDetail').textContent = '--:--';
document.getElementById('avgTimeLastWeek').textContent = '--:--';
document.getElementById('improvementDetail').textContent = '+0.0%';
document.getElementById('runsToday').textContent = '0 Läufe';
document.getElementById('runsThisWeekDetail').textContent = '0 Läufe';
document.getElementById('avgRunsPerDay').textContent = '0.0';
document.getElementById('locationPerformance').innerHTML = '<p>Daten nicht verfügbar</p>';
document.getElementById('locationPerformance').innerHTML = `<p style="color: #8892b0; text-align: center; padding: 2rem;">${notLinkedMessage}</p>`;
document.getElementById('runsThisMonth').textContent = '0 Läufe';
document.getElementById('runsLastMonth').textContent = '0 Läufe';
document.getElementById('bestTimeThisMonth').textContent = '--:--';
}
function displayStatisticsFallback() {
// Show fallback data when API fails
document.getElementById('personalRecords').innerHTML = '<p>Daten nicht verfügbar</p>';
// Show fallback data when API fails or user not linked
const notLinkedMessage = currentLanguage === 'de' ?
'RFID nicht verknüpft - Statistiken nicht verfügbar' :
'RFID not linked - Statistics not available';
document.getElementById('personalRecords').innerHTML = `<p style="color: #8892b0; text-align: center; padding: 2rem;">${notLinkedMessage}</p>`;
document.getElementById('averageTime').textContent = '--:--';
document.getElementById('timeDeviation').textContent = '--:--';
document.getElementById('consistencyScore').textContent = '0%';
document.getElementById('rankingStats').innerHTML = '<p>Daten nicht verfügbar</p>';
document.getElementById('rankingStats').innerHTML = `<p style="color: #8892b0; text-align: center; padding: 2rem;">${notLinkedMessage}</p>`;
document.getElementById('totalRunsStats').textContent = '0';
document.getElementById('activeDays').textContent = '0';
document.getElementById('locationsVisited').textContent = '0';

View File

@@ -2990,9 +2990,9 @@ async function getPerformanceTrends(playerId) {
((avgTimeLastWeek - avgTimeThisWeek) / avgTimeLastWeek * 100) : 0;
return {
avgTimeThisWeek: avgTimeThisWeek,
avgTimeLastWeek: avgTimeLastWeek,
improvement: Math.round(improvement * 10) / 10
avgTimeThisWeek: Math.round(avgTimeThisWeek * 100) / 100,
avgTimeLastWeek: Math.round(avgTimeLastWeek * 100) / 100,
improvement: Math.round(improvement * 100) / 100
};
}
@@ -3028,7 +3028,7 @@ async function getActivityStats(playerId) {
return {
runsToday: parseInt(todayResult.rows[0].count),
runsThisWeek: parseInt(weekResult.rows[0].count),
avgRunsPerDay: parseFloat(avgResult.rows[0].avg_runs) || 0
avgRunsPerDay: Math.round((parseFloat(avgResult.rows[0].avg_runs) || 0) * 100) / 100
};
}
@@ -3048,7 +3048,7 @@ async function getLocationPerformance(playerId) {
return result.rows.map(row => ({
name: row.name,
bestTime: convertTimeToSeconds(row.best_time),
bestTime: Math.round(convertTimeToSeconds(row.best_time) * 100) / 100,
runs: parseInt(row.runs)
}));
}
@@ -3086,7 +3086,7 @@ async function getMonthlyStats(playerId) {
return {
runsThisMonth: parseInt(thisMonthResult.rows[0].count),
runsLastMonth: parseInt(lastMonthResult.rows[0].count),
bestTimeThisMonth: convertTimeToSeconds(bestTimeResult.rows[0].best_time) || 0
bestTimeThisMonth: Math.round((convertTimeToSeconds(bestTimeResult.rows[0].best_time) || 0) * 100) / 100
};
}
@@ -3103,7 +3103,7 @@ async function getPersonalRecords(playerId) {
`, [playerId]);
return result.rows.map(row => ({
time: convertTimeToSeconds(row.recorded_time),
time: Math.round(convertTimeToSeconds(row.recorded_time) * 100) / 100,
location: row.location
}));
}
@@ -3125,9 +3125,9 @@ async function getConsistencyMetrics(playerId) {
Math.max(0, Math.min(100, (1 - (stddevSeconds / avgSeconds)) * 100)) : 0;
return {
averageTime: avgSeconds,
timeDeviation: stddevSeconds,
consistencyScore: Math.round(consistencyScore)
averageTime: Math.round(avgSeconds * 100) / 100,
timeDeviation: Math.round(stddevSeconds * 100) / 100,
consistencyScore: Math.round(consistencyScore * 100) / 100
};
}