Div Erweiterungen
This commit is contained in:
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
1044
routes/api.js
1044
routes/api.js
File diff suppressed because it is too large
Load Diff
46
test-achievements.js
Normal file
46
test-achievements.js
Normal 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();
|
||||||
Reference in New Issue
Block a user