Statistiken und Analytics im dashboard gefixed
This commit is contained in:
@@ -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';
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user