Div Erweiterungen

This commit is contained in:
2025-09-15 23:52:59 +02:00
parent ad6ba66220
commit 5ca7b0b19c
9 changed files with 2567 additions and 24 deletions

View File

@@ -58,6 +58,9 @@ class AchievementSystem {
*/ */
async loadPlayerAchievements(playerId) { async loadPlayerAchievements(playerId) {
try { try {
// Initialisiere immer eine leere Map für den Spieler
this.playerAchievements.set(playerId, new Map());
const result = await pool.query(` const result = await pool.query(`
SELECT pa.achievement_id, pa.progress, pa.is_completed, pa.earned_at SELECT pa.achievement_id, pa.progress, pa.is_completed, pa.earned_at
FROM player_achievements pa FROM player_achievements pa
@@ -66,7 +69,6 @@ class AchievementSystem {
`, [playerId]); `, [playerId]);
// Gruppiere nach achievement_id und zähle Completions // Gruppiere nach achievement_id und zähle Completions
this.playerAchievements.set(playerId, new Map());
const achievementCounts = new Map(); const achievementCounts = new Map();
result.rows.forEach(pa => { result.rows.forEach(pa => {
@@ -87,6 +89,7 @@ class AchievementSystem {
}); });
}); });
console.log(`📋 ${result.rows.length} Achievements für Spieler ${playerId} geladen`);
return true; return true;
} catch (error) { } catch (error) {
console.error(`❌ Fehler beim Laden der Spieler-Achievements für ${playerId}:`, error); console.error(`❌ Fehler beim Laden der Spieler-Achievements für ${playerId}:`, error);
@@ -122,6 +125,12 @@ class AchievementSystem {
async checkImmediateAchievements(playerId) { async checkImmediateAchievements(playerId) {
console.log(`⚡ Prüfe sofortige Achievements für Spieler ${playerId}...`); console.log(`⚡ Prüfe sofortige Achievements für Spieler ${playerId}...`);
// Lade alle Achievements (falls noch nicht geladen)
if (this.achievements.size === 0) {
console.log('📋 Lade alle Achievements...');
await this.loadAchievements();
}
// Lade Spieler-Achievements // Lade Spieler-Achievements
await this.loadPlayerAchievements(playerId); await this.loadPlayerAchievements(playerId);
@@ -216,6 +225,17 @@ class AchievementSystem {
for (const achievement of achievements) { for (const achievement of achievements) {
if (this.isAchievementCompleted(playerId, achievement.id)) continue; if (this.isAchievementCompleted(playerId, achievement.id)) continue;
// Prüfe ob das Achievement heute bereits vergeben wurde
const alreadyEarnedToday = await pool.query(`
SELECT COUNT(*) as count
FROM player_achievements pa
WHERE pa.player_id = $1
AND pa.achievement_id = $2
AND DATE(pa.earned_at AT TIME ZONE 'Europe/Berlin') = CURRENT_DATE
`, [playerId, achievement.id]);
if (parseInt(alreadyEarnedToday.rows[0].count) > 0) continue;
if (attemptsToday >= achievement.condition_value) { if (attemptsToday >= achievement.condition_value) {
await this.awardAchievement(playerId, achievement, attemptsToday, newAchievements); await this.awardAchievement(playerId, achievement, attemptsToday, newAchievements);
} }
@@ -322,6 +342,17 @@ class AchievementSystem {
for (const achievement of achievements) { for (const achievement of achievements) {
if (this.isAchievementCompleted(playerId, achievement.id)) continue; if (this.isAchievementCompleted(playerId, achievement.id)) continue;
// Prüfe ob das Achievement heute bereits vergeben wurde
const alreadyEarnedToday = await pool.query(`
SELECT COUNT(*) as count
FROM player_achievements pa
WHERE pa.player_id = $1
AND pa.achievement_id = $2
AND DATE(pa.earned_at AT TIME ZONE 'Europe/Berlin') = CURRENT_DATE
`, [playerId, achievement.id]);
if (parseInt(alreadyEarnedToday.rows[0].count) > 0) continue;
// Prüfe ob Spieler zu dieser Zeit gespielt hat // Prüfe ob Spieler zu dieser Zeit gespielt hat
const hasPlayed = await this.checkTimeBasedPlay(playerId, timeAchievement.condition); const hasPlayed = await this.checkTimeBasedPlay(playerId, timeAchievement.condition);
@@ -510,7 +541,18 @@ class AchievementSystem {
const achievement = Array.from(this.achievements.values()) const achievement = Array.from(this.achievements.values())
.find(a => a.category === 'best_time' && a.condition_type === 'daily_best'); .find(a => a.category === 'best_time' && a.condition_type === 'daily_best');
if (!achievement || this.isAchievementCompleted(playerId, achievement.id)) return; if (!achievement) return;
// Prüfe ob das Achievement heute bereits vergeben wurde
const alreadyEarnedToday = await pool.query(`
SELECT COUNT(*) as count
FROM player_achievements pa
WHERE pa.player_id = $1
AND pa.achievement_id = $2
AND DATE(pa.earned_at AT TIME ZONE 'Europe/Berlin') = CURRENT_DATE
`, [playerId, achievement.id]);
if (parseInt(alreadyEarnedToday.rows[0].count) > 0) return;
// Hole beste Zeit des Spielers heute // Hole beste Zeit des Spielers heute
const playerResult = await pool.query(` const playerResult = await pool.query(`
@@ -542,15 +584,26 @@ class AchievementSystem {
const achievement = Array.from(this.achievements.values()) const achievement = Array.from(this.achievements.values())
.find(a => a.category === 'best_time' && a.condition_type === 'weekly_best'); .find(a => a.category === 'best_time' && a.condition_type === 'weekly_best');
if (!achievement || this.isAchievementCompleted(playerId, achievement.id)) return; if (!achievement) return;
// Berechne Wochenstart (Montag) // Prüfe ob das Achievement diese Woche bereits vergeben wurde
const currentDateObj = new Date(currentDate); const currentDateObj = new Date(currentDate);
const dayOfWeek = currentDateObj.getDay(); const dayOfWeek = currentDateObj.getDay();
const weekStart = new Date(currentDateObj); const weekStart = new Date(currentDateObj);
weekStart.setDate(currentDateObj.getDate() - (dayOfWeek === 0 ? 6 : dayOfWeek - 1)); weekStart.setDate(currentDateObj.getDate() - (dayOfWeek === 0 ? 6 : dayOfWeek - 1));
const weekStartStr = weekStart.toISOString().split('T')[0]; const weekStartStr = weekStart.toISOString().split('T')[0];
const alreadyEarnedThisWeek = await pool.query(`
SELECT COUNT(*) as count
FROM player_achievements pa
WHERE pa.player_id = $1
AND pa.achievement_id = $2
AND DATE(pa.earned_at AT TIME ZONE 'Europe/Berlin') >= $3
AND DATE(pa.earned_at AT TIME ZONE 'Europe/Berlin') <= $4
`, [playerId, achievement.id, weekStartStr, currentDate]);
if (parseInt(alreadyEarnedThisWeek.rows[0].count) > 0) return;
// Hole beste Zeit des Spielers diese Woche // Hole beste Zeit des Spielers diese Woche
const playerResult = await pool.query(` const playerResult = await pool.query(`
SELECT MIN(recorded_time) as best_time SELECT MIN(recorded_time) as best_time
@@ -583,13 +636,25 @@ class AchievementSystem {
const achievement = Array.from(this.achievements.values()) const achievement = Array.from(this.achievements.values())
.find(a => a.category === 'best_time' && a.condition_type === 'monthly_best'); .find(a => a.category === 'best_time' && a.condition_type === 'monthly_best');
if (!achievement || this.isAchievementCompleted(playerId, achievement.id)) return; if (!achievement) return;
// Berechne Monatsstart // Berechne Monatsstart
const currentDateObj = new Date(currentDate); const currentDateObj = new Date(currentDate);
const monthStart = new Date(currentDateObj.getFullYear(), currentDateObj.getMonth(), 1); const monthStart = new Date(currentDateObj.getFullYear(), currentDateObj.getMonth(), 1);
const monthStartStr = monthStart.toISOString().split('T')[0]; const monthStartStr = monthStart.toISOString().split('T')[0];
// Prüfe ob das Achievement diesen Monat bereits vergeben wurde
const alreadyEarnedThisMonth = await pool.query(`
SELECT COUNT(*) as count
FROM player_achievements pa
WHERE pa.player_id = $1
AND pa.achievement_id = $2
AND DATE(pa.earned_at AT TIME ZONE 'Europe/Berlin') >= $3
AND DATE(pa.earned_at AT TIME ZONE 'Europe/Berlin') <= $4
`, [playerId, achievement.id, monthStartStr, currentDate]);
if (parseInt(alreadyEarnedThisMonth.rows[0].count) > 0) return;
// Hole beste Zeit des Spielers diesen Monat // Hole beste Zeit des Spielers diesen Monat
const playerResult = await pool.query(` const playerResult = await pool.query(`
SELECT MIN(recorded_time) as best_time SELECT MIN(recorded_time) as best_time
@@ -656,7 +721,10 @@ class AchievementSystem {
// Für einmalige Achievements prüfen wir, ob sie bereits erreicht wurden // Für einmalige Achievements prüfen wir, ob sie bereits erreicht wurden
const playerAchievements = this.playerAchievements.get(playerId); const playerAchievements = this.playerAchievements.get(playerId);
if (!playerAchievements) return false; if (!playerAchievements) {
console.log(`⚠️ Player achievements not loaded for ${playerId}, assuming not completed`);
return false;
}
return playerAchievements.has(achievementId); return playerAchievements.has(achievementId);
} }

View File

@@ -128,6 +128,13 @@
<button class="btn" onclick="showAdminUserManagement()">Admins anzeigen</button> <button class="btn" onclick="showAdminUserManagement()">Admins anzeigen</button>
</div> </div>
<!-- Achievement-Verwaltung -->
<div class="card">
<h3><span class="icon">🏆</span> Achievement-Verwaltung</h3>
<p>Verwalte Achievements und Spieler-Achievements</p>
<button class="btn" onclick="showAchievementManagement()">Achievements verwalten</button>
</div>
<!-- System-Info --> <!-- System-Info -->
<div class="card"> <div class="card">
<h3><span class="icon">📊</span> System-Informationen</h3> <h3><span class="icon">📊</span> System-Informationen</h3>

View File

@@ -586,3 +586,168 @@ body {
gap: 1.5rem; gap: 1.5rem;
} }
} }
/* Achievement Management Styles */
.achievement-controls {
display: flex;
gap: 10px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.status-badge {
padding: 4px 8px;
border-radius: 12px;
font-size: 0.8em;
font-weight: 500;
}
.status-badge.active {
background: #4ade80;
color: #000;
}
.status-badge.inactive {
background: #6b7280;
color: #fff;
}
.progress-bar {
position: relative;
background: rgba(255, 255, 255, 0.1);
border-radius: 10px;
height: 20px;
overflow: hidden;
min-width: 100px;
}
.progress-fill {
background: linear-gradient(90deg, #4ade80, #22c55e);
height: 100%;
transition: width 0.3s ease;
}
.progress-text {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 0.8em;
font-weight: 500;
color: #fff;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
}
.player-achievements {
max-height: 70vh;
overflow-y: auto;
}
.achievement-stats {
display: flex;
gap: 20px;
margin-bottom: 20px;
padding: 15px;
background: rgba(255, 255, 255, 0.05);
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.stat-item {
font-size: 0.9em;
}
.achievements-list {
display: grid;
gap: 15px;
}
.achievement-item {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
padding: 15px;
transition: all 0.3s ease;
}
.achievement-item.completed {
border-color: #4ade80;
background: rgba(74, 222, 128, 0.1);
}
.achievement-item.not-completed {
border-color: #6b7280;
background: rgba(107, 114, 128, 0.1);
}
.achievement-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 10px;
}
.achievement-icon {
font-size: 1.5em;
}
.achievement-name {
font-weight: 600;
flex: 1;
}
.achievement-status {
font-size: 1.2em;
}
.achievement-details p {
margin-bottom: 10px;
color: rgba(255, 255, 255, 0.8);
line-height: 1.4;
}
.achievement-meta {
display: flex;
gap: 15px;
margin-bottom: 10px;
flex-wrap: wrap;
}
.achievement-meta span {
font-size: 0.8em;
color: rgba(255, 255, 255, 0.6);
background: rgba(255, 255, 255, 0.1);
padding: 2px 8px;
border-radius: 4px;
}
.achievement-actions {
display: flex;
gap: 10px;
justify-content: flex-end;
}
.achievement-actions .btn {
padding: 6px 12px;
font-size: 0.8em;
}
@media (max-width: 768px) {
.achievement-controls {
flex-direction: column;
}
.achievement-stats {
flex-direction: column;
gap: 10px;
}
.achievement-meta {
flex-direction: column;
gap: 5px;
}
.achievement-actions {
justify-content: center;
}
}

View File

@@ -1552,6 +1552,226 @@ input:checked+.toggle-slider:before {
min-width: 120px; min-width: 120px;
} }
/* Analytics and Statistics Styles */
.analytics-section, .statistics-section {
margin: 2rem 0;
padding: 1.5rem;
background: linear-gradient(135deg, #2c3e50 0%, #34495e 100%);
border-radius: 10px;
border: 1px solid #34495e;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
}
.section-header {
text-align: center;
margin-bottom: 2rem;
}
.section-header h2 {
color: #ecf0f1;
margin-bottom: 0.5rem;
font-size: 2rem;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
.section-header p {
color: #bdc3c7;
font-size: 1.1rem;
}
.analytics-grid, .statistics-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1.5rem;
margin-top: 1.5rem;
}
.analytics-card, .statistics-card {
background: linear-gradient(135deg, #34495e 0%, #2c3e50 100%);
padding: 1.5rem;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
border: 1px solid #34495e;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.analytics-card:hover, .statistics-card:hover {
transform: translateY(-2px);
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.4);
}
.analytics-card h3, .statistics-card h3 {
color: #ecf0f1;
margin-bottom: 1rem;
font-size: 1.2rem;
border-bottom: 2px solid #3498db;
padding-bottom: 0.5rem;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
}
/* Mini Stats in Cards */
.analytics-stats, .statistics-stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 1rem;
margin-bottom: 1rem;
}
.mini-stat {
text-align: center;
padding: 0.5rem;
background: rgba(52, 73, 94, 0.6);
border-radius: 5px;
border: 1px solid #34495e;
}
.mini-stat-number {
font-size: 1.2rem;
font-weight: bold;
color: #ecf0f1;
margin-bottom: 0.25rem;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
}
.mini-stat-label {
font-size: 0.8rem;
color: #bdc3c7;
}
/* Trend Stats */
.trend-stats, .activity-stats, .monthly-stats, .consistency-stats, .progress-stats {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.trend-item, .activity-item, .monthly-item, .consistency-item, .progress-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.5rem;
background: rgba(52, 73, 94, 0.6);
border-radius: 5px;
border: 1px solid #34495e;
margin-bottom: 0.5rem;
}
.trend-label, .activity-label, .monthly-label, .consistency-label, .progress-label {
font-weight: 500;
color: #bdc3c7;
}
.trend-value, .activity-value, .monthly-value, .consistency-value, .progress-value {
font-weight: bold;
color: #ecf0f1;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
}
/* Personal Records */
.personal-records {
max-height: 200px;
overflow-y: auto;
}
.record-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.5rem;
margin-bottom: 0.5rem;
background: rgba(52, 73, 94, 0.6);
border-radius: 5px;
border-left: 4px solid #3498db;
border: 1px solid #34495e;
}
.record-rank {
font-weight: bold;
color: #e74c3c;
font-size: 1.1rem;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
}
.record-time {
font-weight: bold;
color: #ecf0f1;
font-family: 'Courier New', monospace;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
}
.record-location {
color: #bdc3c7;
font-size: 0.9rem;
}
/* Location Performance */
.location-stats {
max-height: 200px;
overflow-y: auto;
}
.location-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.5rem;
margin-bottom: 0.5rem;
background: rgba(52, 73, 94, 0.6);
border-radius: 5px;
border: 1px solid #34495e;
}
.location-name {
font-weight: 500;
color: #ecf0f1;
}
.location-best {
font-weight: bold;
color: #27ae60;
font-family: 'Courier New', monospace;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
}
.location-runs {
color: #bdc3c7;
font-size: 0.9rem;
}
/* Ranking Stats */
.ranking-stats {
max-height: 200px;
overflow-y: auto;
}
.ranking-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.5rem;
margin-bottom: 0.5rem;
background: rgba(52, 73, 94, 0.6);
border-radius: 5px;
border: 1px solid #34495e;
}
.ranking-category {
font-weight: 500;
color: #ecf0f1;
}
.ranking-position {
font-weight: bold;
color: #e74c3c;
font-size: 1.1rem;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
}
.ranking-total {
color: #bdc3c7;
font-size: 0.9rem;
}
/* Responsive Settings */ /* Responsive Settings */
@media (max-width: 768px) { @media (max-width: 768px) {
.setting-item { .setting-item {

View File

@@ -104,9 +104,26 @@
</div> </div>
<div class="dashboard-grid"> <div class="dashboard-grid">
<div class="card"> <div class="card" id="analyticsCard" style="cursor: pointer;">
<h3 data-de="📊 Analytics" data-en="📊 Analytics">📊 Analytics</h3> <h3 data-de="📊 Analytics" data-en="📊 Analytics">📊 Analytics</h3>
<p data-de="Verfolge deine Leistung und überwache wichtige Metriken. Dieser Abschnitt wird detaillierte Analysen anzeigen, sobald wir die Funktion implementieren." data-en="Track your performance and monitor important metrics. This section will show detailed analyses once we implement the feature.">Verfolge deine Leistung und überwache wichtige Metriken. Dieser Abschnitt wird detaillierte Analysen anzeigen, sobald wir die Funktion implementieren.</p> <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">Durchschnitt diese Woche</div>
</div>
<div class="mini-stat">
<div class="mini-stat-number" id="improvementThisWeek">+0.0%</div>
<div class="mini-stat-label">Verbesserung</div>
</div>
<div class="mini-stat">
<div class="mini-stat-number" id="runsThisWeek">0</div>
<div class="mini-stat-label">Läufe diese Woche</div>
</div>
</div>
</div>
<p data-de="Verfolge deine Leistung und überwache wichtige Metriken." data-en="Track your performance and monitor important metrics.">Verfolge deine Leistung und überwache wichtige Metriken.</p>
<button class="btn btn-primary" style="margin-top: 1rem;" onclick="event.stopPropagation(); showAnalytics();" data-de="Analytics öffnen" data-en="Open Analytics">Analytics öffnen</button>
</div> </div>
<div class="card" onclick="showSettings()" style="cursor: pointer;"> <div class="card" onclick="showSettings()" style="cursor: pointer;">
@@ -121,9 +138,26 @@
<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> <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>
</div> </div>
<div class="card"> <div class="card" id="statisticsCard" style="cursor: pointer;">
<h3 data-de="📊 Statistiken" data-en="📊 Statistics">📊 Statistiken</h3> <h3 data-de="📊 Statistiken" data-en="📊 Statistics">📊 Statistiken</h3>
<p data-de="Hier werden bald detaillierte Statistiken zu deinen Läufen angezeigt - beste Zeiten, Verbesserungen und Vergleiche mit anderen Spielern." data-en="Detailed statistics about your runs will be displayed here soon - best times, improvements and comparisons with other players.">Hier werden bald detaillierte Statistiken zu deinen Läufen angezeigt - beste Zeiten, Verbesserungen und Vergleiche mit anderen Spielern.</p> <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">Persönliche Bestzeit</div>
</div>
<div class="mini-stat">
<div class="mini-stat-number" id="totalRunsCount">0</div>
<div class="mini-stat-label">Gesamte Läufe</div>
</div>
<div class="mini-stat">
<div class="mini-stat-number" id="rankPosition">-</div>
<div class="mini-stat-label">Ranglisten-Position</div>
</div>
</div>
</div>
<p data-de="Detaillierte Statistiken zu deinen Läufen - beste Zeiten, Verbesserungen und Vergleiche." data-en="Detailed statistics about your runs - best times, improvements and comparisons.">Detaillierte Statistiken zu deinen Läufen - beste Zeiten, Verbesserungen und Vergleiche.</p>
<button class="btn btn-primary" style="margin-top: 1rem;" onclick="event.stopPropagation(); showStatistics();" data-de="Statistiken öffnen" data-en="Open Statistics">Statistiken öffnen</button>
</div> </div>
</div> </div>
@@ -189,6 +223,139 @@
</div> </div>
</div> </div>
<!-- Analytics Section -->
<div id="analyticsSection" class="analytics-section" style="display: none;">
<div class="section-header">
<h2 data-de="📊 Analytics" data-en="📊 Analytics">📊 Analytics</h2>
<p data-de="Detaillierte Analyse deiner Performance und Trends" data-en="Detailed analysis of your performance and trends">Detaillierte Analyse deiner Performance und Trends</p>
</div>
<!-- Performance Overview -->
<div class="analytics-grid">
<div class="analytics-card">
<h3>📈 Performance-Trends</h3>
<div class="trend-stats">
<div class="trend-item">
<span class="trend-label">Diese Woche:</span>
<span class="trend-value" id="avgTimeThisWeekDetail">--:--</span>
</div>
<div class="trend-item">
<span class="trend-label">Letzte Woche:</span>
<span class="trend-value" id="avgTimeLastWeek">--:--</span>
</div>
<div class="trend-item">
<span class="trend-label">Verbesserung:</span>
<span class="trend-value" id="improvementDetail">+0.0%</span>
</div>
</div>
</div>
<div class="analytics-card">
<h3>🏃‍♂️ Aktivitäts-Heatmap</h3>
<div class="activity-stats">
<div class="activity-item">
<span class="activity-label">Heute:</span>
<span class="activity-value" id="runsToday">0 Läufe</span>
</div>
<div class="activity-item">
<span class="activity-label">Diese Woche:</span>
<span class="activity-value" id="runsThisWeekDetail">0 Läufe</span>
</div>
<div class="activity-item">
<span class="activity-label">Durchschnitt/Tag:</span>
<span class="activity-value" id="avgRunsPerDay">0.0</span>
</div>
</div>
</div>
<div class="analytics-card">
<h3>🏆 Standort-Performance</h3>
<div class="location-stats" id="locationPerformance">
<p data-de="Lade Standort-Daten..." data-en="Loading location data...">Lade Standort-Daten...</p>
</div>
</div>
<div class="analytics-card">
<h3>📅 Monatlicher Fortschritt</h3>
<div class="monthly-stats">
<div class="monthly-item">
<span class="monthly-label">Dieser Monat:</span>
<span class="monthly-value" id="runsThisMonth">0 Läufe</span>
</div>
<div class="monthly-item">
<span class="monthly-label">Letzter Monat:</span>
<span class="monthly-value" id="runsLastMonth">0 Läufe</span>
</div>
<div class="monthly-item">
<span class="monthly-label">Beste Zeit:</span>
<span class="monthly-value" id="bestTimeThisMonth">--:--</span>
</div>
</div>
</div>
</div>
</div>
<!-- Statistics Section -->
<div id="statisticsSection" class="statistics-section" style="display: none;">
<div class="section-header">
<h2 data-de="📊 Statistiken" data-en="📊 Statistics">📊 Statistiken</h2>
<p data-de="Detaillierte Statistiken zu deinen Läufen und Vergleiche" data-en="Detailed statistics about your runs and comparisons">Detaillierte Statistiken zu deinen Läufen und Vergleiche</p>
</div>
<!-- Personal Records -->
<div class="statistics-grid">
<div class="statistics-card">
<h3>🏆 Persönliche Bestzeiten</h3>
<div class="personal-records" id="personalRecords">
<p data-de="Lade Bestzeiten..." data-en="Loading best times...">Lade Bestzeiten...</p>
</div>
</div>
<div class="statistics-card">
<h3>📊 Konsistenz-Metriken</h3>
<div class="consistency-stats">
<div class="consistency-item">
<span class="consistency-label">Durchschnittszeit:</span>
<span class="consistency-value" id="averageTime">--:--</span>
</div>
<div class="consistency-item">
<span class="consistency-label">Standardabweichung:</span>
<span class="consistency-value" id="timeDeviation">--:--</span>
</div>
<div class="consistency-item">
<span class="consistency-label">Konsistenz-Score:</span>
<span class="consistency-value" id="consistencyScore">0%</span>
</div>
</div>
</div>
<div class="statistics-card">
<h3>🏅 Ranglisten-Positionen</h3>
<div class="ranking-stats" id="rankingStats">
<p data-de="Lade Ranglisten..." data-en="Loading rankings...">Lade Ranglisten...</p>
</div>
</div>
<div class="statistics-card">
<h3>📈 Fortschritt-Übersicht</h3>
<div class="progress-stats">
<div class="progress-item">
<span class="progress-label">Gesamte Läufe:</span>
<span class="progress-value" id="totalRunsStats">0</span>
</div>
<div class="progress-item">
<span class="progress-label">Aktive Tage:</span>
<span class="progress-value" id="activeDays">0</span>
</div>
<div class="progress-item">
<span class="progress-label">Standorte besucht:</span>
<span class="progress-value" id="locationsVisited">0</span>
</div>
</div>
</div>
</div>
</div>
<!-- Achievements Section --> <!-- Achievements Section -->
<div class="achievements-section"> <div class="achievements-section">
<div class="achievements-header"> <div class="achievements-header">

View File

@@ -317,7 +317,7 @@ function displayRunsTable(runs) {
function displayLocationsTable(locations) { function displayLocationsTable(locations) {
let html = '<div class="table-container"><table class="data-table">'; let html = '<div class="table-container"><table class="data-table">';
html += '<thead><tr><th>ID</th><th>Name</th><th>Latitude</th><th>Longitude</th><th>Erstellt</th><th>Aktionen</th></tr></thead><tbody>'; html += '<thead><tr><th>ID</th><th>Name</th><th>Latitude</th><th>Longitude</th><th>Mindestzeit (s)</th><th>Erstellt</th><th>Aktionen</th></tr></thead><tbody>';
locations.forEach(location => { locations.forEach(location => {
html += `<tr> html += `<tr>
@@ -325,6 +325,7 @@ function displayLocationsTable(locations) {
<td>${location.name}</td> <td>${location.name}</td>
<td>${location.latitude}</td> <td>${location.latitude}</td>
<td>${location.longitude}</td> <td>${location.longitude}</td>
<td>${location.time_threshold ? (typeof location.time_threshold === 'object' ? location.time_threshold.seconds : location.time_threshold).toFixed(3) : '-'}</td>
<td>${new Date(location.created_at).toLocaleDateString('de-DE')}</td> <td>${new Date(location.created_at).toLocaleDateString('de-DE')}</td>
<td class="action-buttons"> <td class="action-buttons">
<button class="btn btn-small btn-warning" onclick="editLocation('${location.id}')">Bearbeiten</button> <button class="btn btn-small btn-warning" onclick="editLocation('${location.id}')">Bearbeiten</button>
@@ -393,6 +394,15 @@ function filterData() {
case 'adminusers': case 'adminusers':
displayAdminUsersTable(filteredData); displayAdminUsersTable(filteredData);
break; break;
case 'achievements':
if (currentAchievementMode === 'achievements') {
currentAchievements = filteredData;
displayAchievements();
} else {
currentPlayers = filteredData;
displayPlayers();
}
break;
} }
} }
@@ -413,6 +423,13 @@ function refreshData() {
case 'system': case 'system':
loadSystemInfo(); loadSystemInfo();
break; break;
case 'achievements':
if (currentAchievementMode === 'achievements') {
loadAchievements();
} else {
loadPlayers();
}
break;
} }
} }
@@ -556,6 +573,10 @@ async function handleAddSubmit(e) {
successMessage = isEdit ? 'Lauf erfolgreich aktualisiert' : 'Lauf erfolgreich hinzugefügt'; successMessage = isEdit ? 'Lauf erfolgreich aktualisiert' : 'Lauf erfolgreich hinzugefügt';
method = isEdit ? 'PUT' : 'POST'; method = isEdit ? 'PUT' : 'POST';
break; break;
case 'achievements':
// Handle achievement form submission
await handleAchievementSubmit(formData);
return;
default: default:
showError('Unbekannter Datentyp'); showError('Unbekannter Datentyp');
return; return;
@@ -1196,7 +1217,552 @@ function displayBlacklistStats(stats) {
statsDiv.innerHTML = html; statsDiv.innerHTML = html;
} }
// ==================== ACHIEVEMENT MANAGEMENT ====================
let currentAchievementMode = 'achievements'; // 'achievements' or 'players'
let currentAchievements = [];
let currentPlayers = [];
// Show achievement management
async function showAchievementManagement() {
currentDataType = 'achievements';
currentAchievementMode = 'achievements';
document.getElementById('dataTitle').textContent = '🏆 Achievement-Verwaltung';
document.getElementById('dataSection').style.display = 'block';
// Update search placeholder
document.getElementById('searchInput').placeholder = 'Achievements durchsuchen...';
await loadAchievements();
}
// Load all achievements
async function loadAchievements() {
try {
const response = await fetch('/api/v1/admin/achievements');
const result = await response.json();
if (result.success) {
currentAchievements = result.data;
currentData = result.data; // Set for filtering
displayAchievements();
} else {
showError('Fehler beim Laden der Achievements: ' + result.message);
}
} catch (error) {
console.error('Error loading achievements:', error);
showError('Fehler beim Laden der Achievements');
}
}
// Display achievements in table
function displayAchievements() {
const content = document.getElementById('dataContent');
if (currentAchievements.length === 0) {
content.innerHTML = '<div class="no-data">Keine Achievements gefunden</div>';
return;
}
let html = `
<div class="achievement-controls">
<button class="btn btn-success" onclick="showAddAchievementModal()"> Neues Achievement</button>
<button class="btn" onclick="toggleAchievementMode()">👥 Spieler-Ansicht</button>
</div>
<div class="table-container">
<table class="data-table">
<thead>
<tr>
<th>Status</th>
<th>Icon</th>
<th>Name</th>
<th>Kategorie</th>
<th>Bedingung</th>
<th>Punkte</th>
<th>Mehrmals</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
`;
currentAchievements.forEach(achievement => {
const statusClass = achievement.is_active ? 'active' : 'inactive';
const statusText = achievement.is_active ? 'Aktiv' : 'Inaktiv';
const multipleText = achievement.can_be_earned_multiple_times ? 'Ja' : 'Nein';
html += `
<tr>
<td><span class="status-badge ${statusClass}">${statusText}</span></td>
<td>${achievement.icon || '🏆'}</td>
<td>
<strong>${achievement.name}</strong>
${achievement.name_en ? `<br><small>${achievement.name_en}</small>` : ''}
</td>
<td>${achievement.category}</td>
<td>${achievement.condition_type}: ${achievement.condition_value}</td>
<td>${achievement.points}</td>
<td>${multipleText}</td>
<td>
<button class="btn btn-sm" onclick="editAchievement('${achievement.id}')">✏️</button>
<button class="btn btn-sm btn-danger" onclick="deleteAchievement('${achievement.id}', '${achievement.name}')">🗑️</button>
</td>
</tr>
`;
});
html += `
</tbody>
</table>
</div>
`;
content.innerHTML = html;
}
// Handle achievement form submission
async function handleAchievementSubmit(formData) {
const isEdit = formData.has('achievement_id');
const url = isEdit ?
`/api/v1/admin/achievements/${formData.get('achievement_id')}` :
'/api/v1/admin/achievements';
const method = isEdit ? 'PUT' : 'POST';
const data = {
name: formData.get('name'),
name_en: formData.get('name_en') || null,
description: formData.get('description'),
description_en: formData.get('description_en') || null,
category: formData.get('category'),
condition_type: formData.get('condition_type'),
condition_value: parseInt(formData.get('condition_value')),
icon: formData.get('icon') || '🏆',
points: parseInt(formData.get('points')) || 10,
is_active: formData.has('is_active'),
can_be_earned_multiple_times: formData.has('can_be_earned_multiple_times')
};
try {
const response = await fetch(url, {
method: method,
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
const result = await response.json();
if (result.success) {
showSuccess(result.message);
closeModal();
await loadAchievements();
} else {
showError('Fehler beim Speichern: ' + result.message);
}
} catch (error) {
console.error('Error saving achievement:', error);
showError('Fehler beim Speichern des Achievements');
}
}
// Toggle between achievements and players view
async function toggleAchievementMode() {
if (currentAchievementMode === 'achievements') {
currentAchievementMode = 'players';
document.getElementById('dataTitle').textContent = '👥 Spieler-Achievements';
document.getElementById('searchInput').placeholder = 'Spieler durchsuchen...';
await loadPlayers();
} else {
currentAchievementMode = 'achievements';
document.getElementById('dataTitle').textContent = '🏆 Achievement-Verwaltung';
document.getElementById('searchInput').placeholder = 'Achievements durchsuchen...';
await loadAchievements();
}
}
// Load all players with achievement statistics
async function loadPlayers() {
try {
const response = await fetch('/api/v1/admin/achievements/players');
const result = await response.json();
if (result.success) {
currentPlayers = result.data;
currentData = result.data; // Set for filtering
displayPlayers();
} else {
showError('Fehler beim Laden der Spieler: ' + result.message);
}
} catch (error) {
console.error('Error loading players:', error);
showError('Fehler beim Laden der Spieler');
}
}
// Display players in table
function displayPlayers() {
const content = document.getElementById('dataContent');
if (currentPlayers.length === 0) {
content.innerHTML = '<div class="no-data">Keine Spieler gefunden</div>';
return;
}
let html = `
<div class="achievement-controls">
<button class="btn" onclick="toggleAchievementMode()">🏆 Achievement-Ansicht</button>
</div>
<div class="table-container">
<table class="data-table">
<thead>
<tr>
<th>Spieler</th>
<th>Abgeschlossen</th>
<th>Gesamt</th>
<th>Fortschritt</th>
<th>Punkte</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
`;
currentPlayers.forEach(player => {
const progressPercentage = player.completion_percentage || 0;
const progressBar = `
<div class="progress-bar">
<div class="progress-fill" style="width: ${progressPercentage}%"></div>
<span class="progress-text">${progressPercentage}%</span>
</div>
`;
html += `
<tr>
<td><strong>${player.firstname} ${player.lastname}</strong></td>
<td>${player.completed_achievements}</td>
<td>${player.total_achievements}</td>
<td>${progressBar}</td>
<td>${player.total_points}</td>
<td>
<button class="btn btn-sm" onclick="viewPlayerAchievements('${player.id}', '${player.firstname} ${player.lastname}')">👁️</button>
</td>
</tr>
`;
});
html += `
</tbody>
</table>
</div>
`;
content.innerHTML = html;
}
// Show add achievement modal
function showAddAchievementModal() {
const modal = document.getElementById('addModal');
const modalTitle = document.getElementById('modalTitle');
const formFields = document.getElementById('formFields');
modalTitle.textContent = 'Neues Achievement erstellen';
formFields.innerHTML = `
<div class="form-group">
<label for="achievement_name">Name *</label>
<input type="text" id="achievement_name" name="name" required>
</div>
<div class="form-group">
<label for="achievement_name_en">Name (Englisch)</label>
<input type="text" id="achievement_name_en" name="name_en">
</div>
<div class="form-group">
<label for="achievement_description">Beschreibung *</label>
<textarea id="achievement_description" name="description" required></textarea>
</div>
<div class="form-group">
<label for="achievement_description_en">Beschreibung (Englisch)</label>
<textarea id="achievement_description_en" name="description_en"></textarea>
</div>
<div class="form-group">
<label for="achievement_category">Kategorie *</label>
<select id="achievement_category" name="category" required>
<option value="">Kategorie wählen</option>
<option value="consistency">Konsistenz</option>
<option value="improvement">Verbesserung</option>
<option value="seasonal">Saisonal</option>
<option value="monthly">Monatlich</option>
<option value="best_time">Beste Zeit</option>
</select>
</div>
<div class="form-group">
<label for="achievement_condition_type">Bedingungstyp *</label>
<input type="text" id="achievement_condition_type" name="condition_type" required placeholder="z.B. first_time, attempts_per_day">
</div>
<div class="form-group">
<label for="achievement_condition_value">Bedingungswert *</label>
<input type="number" id="achievement_condition_value" name="condition_value" required>
</div>
<div class="form-group">
<label for="achievement_icon">Icon</label>
<input type="text" id="achievement_icon" name="icon" value="🏆">
</div>
<div class="form-group">
<label for="achievement_points">Punkte</label>
<input type="number" id="achievement_points" name="points" value="10">
</div>
<div class="form-group">
<label>
<input type="checkbox" id="achievement_is_active" name="is_active" checked>
Aktiv
</label>
</div>
<div class="form-group">
<label>
<input type="checkbox" id="achievement_multiple" name="can_be_earned_multiple_times">
Kann mehrmals erreicht werden
</label>
</div>
`;
modal.style.display = 'block';
}
// Edit achievement
async function editAchievement(achievementId) {
const achievement = currentAchievements.find(a => a.id === achievementId);
if (!achievement) return;
const modal = document.getElementById('addModal');
const modalTitle = document.getElementById('modalTitle');
const formFields = document.getElementById('formFields');
modalTitle.textContent = 'Achievement bearbeiten';
formFields.innerHTML = `
<input type="hidden" id="achievement_id" value="${achievement.id}">
<div class="form-group">
<label for="achievement_name">Name *</label>
<input type="text" id="achievement_name" name="name" value="${achievement.name}" required>
</div>
<div class="form-group">
<label for="achievement_name_en">Name (Englisch)</label>
<input type="text" id="achievement_name_en" name="name_en" value="${achievement.name_en || ''}">
</div>
<div class="form-group">
<label for="achievement_description">Beschreibung *</label>
<textarea id="achievement_description" name="description" required>${achievement.description}</textarea>
</div>
<div class="form-group">
<label for="achievement_description_en">Beschreibung (Englisch)</label>
<textarea id="achievement_description_en" name="description_en">${achievement.description_en || ''}</textarea>
</div>
<div class="form-group">
<label for="achievement_category">Kategorie *</label>
<select id="achievement_category" name="category" required>
<option value="consistency" ${achievement.category === 'consistency' ? 'selected' : ''}>Konsistenz</option>
<option value="improvement" ${achievement.category === 'improvement' ? 'selected' : ''}>Verbesserung</option>
<option value="seasonal" ${achievement.category === 'seasonal' ? 'selected' : ''}>Saisonal</option>
<option value="monthly" ${achievement.category === 'monthly' ? 'selected' : ''}>Monatlich</option>
<option value="best_time" ${achievement.category === 'best_time' ? 'selected' : ''}>Beste Zeit</option>
</select>
</div>
<div class="form-group">
<label for="achievement_condition_type">Bedingungstyp *</label>
<input type="text" id="achievement_condition_type" name="condition_type" value="${achievement.condition_type}" required>
</div>
<div class="form-group">
<label for="achievement_condition_value">Bedingungswert *</label>
<input type="number" id="achievement_condition_value" name="condition_value" value="${achievement.condition_value}" required>
</div>
<div class="form-group">
<label for="achievement_icon">Icon</label>
<input type="text" id="achievement_icon" name="icon" value="${achievement.icon || '🏆'}">
</div>
<div class="form-group">
<label for="achievement_points">Punkte</label>
<input type="number" id="achievement_points" name="points" value="${achievement.points}">
</div>
<div class="form-group">
<label>
<input type="checkbox" id="achievement_is_active" name="is_active" ${achievement.is_active ? 'checked' : ''}>
Aktiv
</label>
</div>
<div class="form-group">
<label>
<input type="checkbox" id="achievement_multiple" name="can_be_earned_multiple_times" ${achievement.can_be_earned_multiple_times ? 'checked' : ''}>
Kann mehrmals erreicht werden
</label>
</div>
`;
modal.style.display = 'block';
}
// Delete achievement
function deleteAchievement(achievementId, achievementName) {
document.getElementById('confirmMessage').textContent = `Möchten Sie das Achievement "${achievementName}" wirklich deaktivieren?`;
document.getElementById('confirmYes').onclick = () => confirmDeleteAchievement(achievementId);
document.getElementById('confirmModal').style.display = 'block';
}
// Confirm delete achievement
async function confirmDeleteAchievement(achievementId) {
try {
const response = await fetch(`/api/v1/admin/achievements/${achievementId}`, {
method: 'DELETE'
});
const result = await response.json();
if (result.success) {
showSuccess(result.message);
closeModal();
await loadAchievements();
} else {
showError('Fehler beim Löschen: ' + result.message);
}
} catch (error) {
console.error('Error deleting achievement:', error);
showError('Fehler beim Löschen des Achievements');
}
}
// View player achievements
async function viewPlayerAchievements(playerId, playerName) {
try {
const response = await fetch(`/api/v1/admin/achievements/players/${playerId}`);
const result = await response.json();
if (result.success) {
showPlayerAchievementsModal(result.player, result.data);
} else {
showError('Fehler beim Laden der Spieler-Achievements: ' + result.message);
}
} catch (error) {
console.error('Error loading player achievements:', error);
showError('Fehler beim Laden der Spieler-Achievements');
}
}
// Show player achievements modal
function showPlayerAchievementsModal(player, achievements) {
const modal = document.getElementById('addModal');
const modalTitle = document.getElementById('modalTitle');
const formFields = document.getElementById('formFields');
modalTitle.textContent = `Achievements von ${player.firstname} ${player.lastname}`;
let html = `
<div class="player-achievements">
<div class="achievement-stats">
<div class="stat-item">
<strong>Abgeschlossen:</strong> ${achievements.filter(a => a.is_completed).length} / ${achievements.length}
</div>
<div class="stat-item">
<strong>Gesamtpunkte:</strong> ${achievements.filter(a => a.is_completed).reduce((sum, a) => sum + a.points, 0)}
</div>
</div>
<div class="achievements-list">
`;
achievements.forEach(achievement => {
const statusClass = achievement.is_completed ? 'completed' : 'not-completed';
const statusIcon = achievement.is_completed ? '✅' : '❌';
const completionCount = achievement.completion_count || 0;
html += `
<div class="achievement-item ${statusClass}">
<div class="achievement-header">
<span class="achievement-icon">${achievement.icon}</span>
<span class="achievement-name">${achievement.name}</span>
<span class="achievement-status">${statusIcon}</span>
</div>
<div class="achievement-details">
<p>${achievement.description}</p>
<div class="achievement-meta">
<span>Kategorie: ${achievement.category}</span>
<span>Punkte: ${achievement.points}</span>
${achievement.is_completed ? `<span>Erreicht: ${new Date(achievement.earned_at).toLocaleDateString('de-DE')}</span>` : ''}
${completionCount > 1 ? `<span>Anzahl: ${completionCount}</span>` : ''}
</div>
<div class="achievement-actions">
${!achievement.is_completed ?
`<button class="btn btn-sm btn-success" onclick="awardAchievement('${player.id}', '${achievement.id}', '${achievement.name}')">Vergeben</button>` :
`<button class="btn btn-sm btn-danger" onclick="revokeAchievement('${player.id}', '${achievement.id}', '${achievement.name}')">Entfernen</button>`
}
</div>
</div>
</div>
`;
});
html += `
</div>
</div>
`;
formFields.innerHTML = html;
modal.style.display = 'block';
}
// Award achievement to player
async function awardAchievement(playerId, achievementId, achievementName) {
try {
const response = await fetch(`/api/v1/admin/achievements/players/${playerId}/award`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
achievement_id: achievementId,
progress: 1
})
});
const result = await response.json();
if (result.success) {
showSuccess(result.message);
// Refresh player achievements
await viewPlayerAchievements(playerId, '');
} else {
showError('Fehler beim Vergeben: ' + result.message);
}
} catch (error) {
console.error('Error awarding achievement:', error);
showError('Fehler beim Vergeben des Achievements');
}
}
// Revoke achievement from player
async function revokeAchievement(playerId, achievementId, achievementName) {
if (!confirm(`Möchten Sie das Achievement "${achievementName}" wirklich entfernen?`)) {
return;
}
try {
const response = await fetch(`/api/v1/admin/achievements/players/${playerId}/revoke`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
achievement_id: achievementId
})
});
const result = await response.json();
if (result.success) {
showSuccess(result.message);
// Refresh player achievements
await viewPlayerAchievements(playerId, '');
} else {
showError('Fehler beim Entfernen: ' + result.message);
}
} catch (error) {
console.error('Error revoking achievement:', error);
showError('Fehler beim Entfernen des Achievements');
}
}

View File

@@ -1177,6 +1177,266 @@ function displayAchievements() {
achievementsGrid.innerHTML = achievementCards; achievementsGrid.innerHTML = achievementCards;
} }
// Initialize Analytics and Statistics event listeners
document.addEventListener('DOMContentLoaded', function() {
const analyticsCard = document.getElementById('analyticsCard');
const statisticsCard = document.getElementById('statisticsCard');
if (analyticsCard) {
analyticsCard.addEventListener('click', showAnalytics);
console.log('Analytics card event listener added');
}
if (statisticsCard) {
statisticsCard.addEventListener('click', showStatistics);
console.log('Statistics card event listener added');
}
});
// Analytics Functions
function showAnalytics() {
console.log('showAnalytics called');
// Hide other sections
const timesDisplay = document.getElementById('timesDisplay');
const achievementsDisplay = document.getElementById('achievementsDisplay');
const statisticsSection = document.getElementById('statisticsSection');
if (timesDisplay) timesDisplay.style.display = 'none';
if (achievementsDisplay) achievementsDisplay.style.display = 'none';
if (statisticsSection) statisticsSection.style.display = 'none';
// Show analytics section
const analyticsSection = document.getElementById('analyticsSection');
if (analyticsSection) {
analyticsSection.style.display = 'block';
console.log('Analytics section shown');
} else {
console.error('Analytics section not found');
}
// Load analytics data
loadAnalyticsData();
}
function showStatistics() {
console.log('showStatistics called');
// Hide other sections
const timesDisplay = document.getElementById('timesDisplay');
const achievementsDisplay = document.getElementById('achievementsDisplay');
const analyticsSection = document.getElementById('analyticsSection');
if (timesDisplay) timesDisplay.style.display = 'none';
if (achievementsDisplay) achievementsDisplay.style.display = 'none';
if (analyticsSection) analyticsSection.style.display = 'none';
// Show statistics section
const statisticsSection = document.getElementById('statisticsSection');
if (statisticsSection) {
statisticsSection.style.display = 'block';
console.log('Statistics section shown');
} else {
console.error('Statistics section not found');
}
// Load statistics data
loadStatisticsData();
}
async function loadAnalyticsData() {
try {
if (!currentPlayerId) {
console.error('No player ID available');
return;
}
// Load analytics data from API
const response = await fetch(`/api/v1/analytics/player/${currentPlayerId}`);
if (!response.ok) {
throw new Error('Failed to load analytics data');
}
const analyticsData = await response.json();
displayAnalyticsData(analyticsData);
// Update preview in main card
updateAnalyticsPreview(analyticsData);
} catch (error) {
console.error('Error loading analytics data:', error);
// Show fallback data
displayAnalyticsFallback();
}
}
async function loadStatisticsData() {
try {
if (!currentPlayerId) {
console.error('No player ID available');
return;
}
// Load statistics data from API
const response = await fetch(`/api/v1/statistics/player/${currentPlayerId}`);
if (!response.ok) {
throw new Error('Failed to load statistics data');
}
const statisticsData = await response.json();
displayStatisticsData(statisticsData);
// Update preview in main card
updateStatisticsPreview(statisticsData);
} catch (error) {
console.error('Error loading statistics data:', error);
// Show fallback data
displayStatisticsFallback();
}
}
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 + '%';
// 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);
// Location Performance
displayLocationPerformance(data.locationPerformance);
// Monthly Stats
document.getElementById('runsThisMonth').textContent = data.monthly.runsThisMonth + ' Läufe';
document.getElementById('runsLastMonth').textContent = data.monthly.runsLastMonth + ' Läufe';
document.getElementById('bestTimeThisMonth').textContent = formatTime(data.monthly.bestTimeThisMonth);
}
function displayStatisticsData(data) {
// Personal Records
displayPersonalRecords(data.personalRecords);
// 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 + '%';
// Ranking Stats
displayRankingStats(data.rankings);
// Progress Stats
document.getElementById('totalRunsStats').textContent = data.progress.totalRuns;
document.getElementById('activeDays').textContent = data.progress.activeDays;
document.getElementById('locationsVisited').textContent = data.progress.locationsVisited;
}
function displayLocationPerformance(locations) {
const container = document.getElementById('locationPerformance');
if (!locations || locations.length === 0) {
container.innerHTML = '<p>Keine Standort-Daten verfügbar</p>';
return;
}
const locationHTML = locations.map(location => `
<div class="location-item">
<span class="location-name">${location.name}</span>
<div>
<span class="location-best">${formatTime(location.bestTime)}</span>
<span class="location-runs">(${location.runs} Läufe)</span>
</div>
</div>
`).join('');
container.innerHTML = locationHTML;
}
function displayPersonalRecords(records) {
const container = document.getElementById('personalRecords');
if (!records || records.length === 0) {
container.innerHTML = '<p>Keine Bestzeiten verfügbar</p>';
return;
}
const recordsHTML = records.map((record, index) => `
<div class="record-item">
<div>
<span class="record-rank">#${index + 1}</span>
<span class="record-time">${formatTime(record.time)}</span>
</div>
<span class="record-location">${record.location}</span>
</div>
`).join('');
container.innerHTML = recordsHTML;
}
function displayRankingStats(rankings) {
const container = document.getElementById('rankingStats');
if (!rankings || rankings.length === 0) {
container.innerHTML = '<p>Keine Ranglisten-Daten verfügbar</p>';
return;
}
const rankingsHTML = rankings.map(ranking => `
<div class="ranking-item">
<span class="ranking-category">${ranking.category}</span>
<div>
<span class="ranking-position">#${ranking.position}</span>
<span class="ranking-total">von ${ranking.total}</span>
</div>
</div>
`).join('');
container.innerHTML = rankingsHTML;
}
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';
}
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';
}
function displayAnalyticsFallback() {
// Show fallback data when API fails
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('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>';
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('totalRunsStats').textContent = '0';
document.getElementById('activeDays').textContent = '0';
document.getElementById('locationsVisited').textContent = '0';
}
// Get achievement condition value for progress display // Get achievement condition value for progress display
function getAchievementConditionValue(achievementName) { function getAchievementConditionValue(achievementName) {
const conditionMap = { const conditionMap = {

File diff suppressed because it is too large Load Diff

46
test-achievements.js Normal file
View File

@@ -0,0 +1,46 @@
const AchievementSystem = require('./lib/achievementSystem');
async function testAchievements() {
console.log('=== Testing Achievement System ===');
const achievementSystem = new AchievementSystem();
const playerId = '08476bfc-5f48-486c-9f0b-90b81e5ccd8d';
try {
// Test 1: Load achievements
console.log('\n1. Loading achievements...');
await achievementSystem.loadAchievements();
console.log(`✅ Loaded ${achievementSystem.achievements.size} achievements`);
// Test 2: Load player achievements
console.log('\n2. Loading player achievements...');
await achievementSystem.loadPlayerAchievements(playerId);
const playerAchievements = achievementSystem.playerAchievements.get(playerId);
console.log(`✅ Player has ${playerAchievements ? playerAchievements.size : 0} achievements`);
// Test 3: Check first time achievement
console.log('\n3. Checking first time achievement...');
const firstTimeAchievement = Array.from(achievementSystem.achievements.values())
.find(a => a.category === 'consistency' && a.condition_type === 'first_time');
console.log('First time achievement:', firstTimeAchievement ? firstTimeAchievement.name : 'NOT FOUND');
// Test 4: Check September achievement
console.log('\n4. Checking September achievement...');
const septemberAchievement = Array.from(achievementSystem.achievements.values())
.find(a => a.category === 'monthly' && a.condition_type === 'september');
console.log('September achievement:', septemberAchievement ? septemberAchievement.name : 'NOT FOUND');
// Test 5: Check immediate achievements
console.log('\n5. Running immediate achievement check...');
const newAchievements = await achievementSystem.checkImmediateAchievements(playerId);
console.log(`✅ Found ${newAchievements.length} new achievements`);
newAchievements.forEach(achievement => {
console.log(` 🏆 ${achievement.name} (+${achievement.points} points)`);
});
} catch (error) {
console.error('❌ Error:', error);
}
}
testAchievements();