Files
Ninjaserver/public/js/dashboard.js

2105 lines
81 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Supabase configuration
const SUPABASE_URL = 'https://lfxlplnypzvjrhftaoog.supabase.co';
const SUPABASE_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImxmeGxwbG55cHp2anJoZnRhb29nIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDkyMTQ3NzIsImV4cCI6MjA2NDc5MDc3Mn0.XR4preBqWAQ1rT4PFbpkmRdz57BTwIusBI89fIxDHM8';
// Initialize Supabase client
const supabase = window.supabase.createClient(SUPABASE_URL, SUPABASE_ANON_KEY);
// Global variables
let currentUser = null;
let currentLanguage = 'en'; // Default to English
// Check authentication and load dashboard
async function initDashboard() {
try {
// Get current session
const { data: { session }, error } = await supabase.auth.getSession();
if (error) {
console.error('Error checking authentication:', error);
// Temporarily show dashboard for testing
currentUser = { id: '9966cffd-2088-423c-b852-0ca7996cda97', email: 'admin@speedrun-arena.com' };
displayUserInfo({ email: 'admin@speedrun-arena.com' });
showDashboard();
// Check times section
checkLinkStatusAndLoadTimes();
return;
}
if (!session) {
// No session, redirect to login
window.location.href = '/login';
return;
}
// User is authenticated, show dashboard
if (session.user) {
currentUser = session.user;
displayUserInfo(session.user);
} else {
// Fallback if no user data
currentUser = { id: '9966cffd-2088-423c-b852-0ca7996cda97', email: 'admin@speedrun-arena.com' };
displayUserInfo({ email: 'admin@speedrun-arena.com' });
}
showDashboard();
// Load times section
checkLinkStatusAndLoadTimes();
// Update analytics and statistics cards
updateAnalyticsAndStatisticsCards();
} catch (error) {
console.error('An unexpected error occurred:', error);
// window.location.href = '/login';
}
}
// Display user information
function displayUserInfo(user) {
const userEmail = document.getElementById('userEmail');
const userAvatar = document.getElementById('userAvatar');
userEmail.textContent = user.email;
userAvatar.textContent = user.email.charAt(0).toUpperCase();
}
// Show dashboard content
function showDashboard() {
document.getElementById('loading').style.display = 'none';
document.getElementById('dashboardContent').style.display = 'block';
}
// Logout function
async function logout() {
try {
const { error } = await supabase.auth.signOut();
if (error) {
console.error('Error logging out:', error);
} else {
window.location.href = '/';
}
} catch (error) {
console.error('Error during logout:', error);
}
}
// Listen for auth state changes
supabase.auth.onAuthStateChange((event, session) => {
if (event === 'SIGNED_OUT' || !session) {
window.location.href = '/login';
}
});
// Language Management
function translateElement(element, language) {
if (element.dataset[language]) {
// Check if the content contains HTML tags
if (element.dataset[language].includes('<')) {
element.innerHTML = element.dataset[language];
} else {
element.textContent = element.dataset[language];
}
}
}
// Change language function
function changeLanguage() {
const languageSelect = document.getElementById('languageSelect');
currentLanguage = languageSelect.value;
// Save language preference
localStorage.setItem('ninjacross_language', currentLanguage);
// Update flag in select
updateLanguageFlag();
// Translate all elements with data attributes
const elementsToTranslate = document.querySelectorAll('[data-de][data-en]');
elementsToTranslate.forEach(element => {
translateElement(element, currentLanguage);
});
// Update dynamic content
updateDynamicContent();
console.log(`🌐 Language changed to: ${currentLanguage}`);
}
// Update flag in language selector
function updateLanguageFlag() {
const languageSelect = document.getElementById('languageSelect');
if (languageSelect) {
if (currentLanguage === 'de') {
// German flag (black-red-gold)
languageSelect.style.backgroundImage = `url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="20" height="15" viewBox="0 0 20 15"><rect width="20" height="5" fill="%23000000"/><rect y="5" width="20" height="5" fill="%23DD0000"/><rect y="10" width="20" height="5" fill="%23FFCE00"/></svg>')`;
} else {
// USA flag
languageSelect.style.backgroundImage = `url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="20" height="15" viewBox="0 0 20 15"><rect width="20" height="15" fill="%23B22234"/><rect width="20" height="1.15" fill="%23FFFFFF"/><rect y="2.3" width="20" height="1.15" fill="%23FFFFFF"/><rect y="4.6" width="20" height="1.15" fill="%23FFFFFF"/><rect y="6.9" width="20" height="1.15" fill="%23FFFFFF"/><rect y="9.2" width="20" height="1.15" fill="%23FFFFFF"/><rect y="11.5" width="20" height="1.15" fill="%23FFFFFF"/><rect y="13.8" width="20" height="1.15" fill="%23FFFFFF"/><rect width="7.7" height="8.05" fill="%230033A0"/></svg>')`;
}
}
}
// Update dynamic content that's not in HTML
function updateDynamicContent() {
// Update achievement notifications
updateAchievementNotifications();
// Update time display formats
updateTimeDisplayFormats();
// Update achievement progress text
updateAchievementProgressText();
// Update analytics and statistics cards
updateAnalyticsAndStatisticsCards();
// Reload achievements if they're loaded
if (window.allAchievements && window.allAchievements.length > 0) {
displayAchievements();
}
// Reload times if they're loaded
if (document.getElementById('timesDisplay').style.display !== 'none') {
// Times are displayed, reload them
checkLinkStatusAndLoadTimes();
}
}
// Load saved language preference
function loadLanguagePreference() {
const savedLanguage = localStorage.getItem('ninjacross_language');
if (savedLanguage && (savedLanguage === 'de' || savedLanguage === 'en')) {
currentLanguage = savedLanguage;
const languageSelect = document.getElementById('languageSelect');
if (languageSelect) {
languageSelect.value = currentLanguage;
// Update flag when loading
updateLanguageFlag();
}
}
}
// Update analytics and statistics cards based on link status
function updateAnalyticsAndStatisticsCards() {
const analyticsCard = document.getElementById('analyticsCard');
const statisticsCard = document.getElementById('statisticsCard');
if (!currentPlayerId) {
// User not linked - show appropriate message
if (analyticsCard) {
const isGerman = currentLanguage === 'de';
const message = isGerman ?
'RFID verknüpfen erforderlich' :
'RFID linking required';
const description = isGerman ?
'Verknüpfe deine RFID-Karte, um Analytics zu sehen.' :
'Link your RFID card to view analytics.';
analyticsCard.innerHTML = `
<h3>📊 Analytics</h3>
<div style="background: rgba(239, 68, 68, 0.1); border: 1px solid rgba(239, 68, 68, 0.3); border-radius: 0.5rem; padding: 1rem; margin: 1rem 0;">
<div style="display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.5rem;">
<span style="color: #ef4444; font-weight: 600;">⚠️</span>
<span style="color: #ef4444; font-weight: 600;">${message}</span>
</div>
<div style="font-size: 0.9rem; color: #8892b0;">
${description}
</div>
</div>
<button class="btn btn-primary" style="margin-top: 1rem;" onclick="event.stopPropagation(); showRFIDSettings();" data-de="RFID verknüpfen" data-en="Link RFID">RFID verknüpfen</button>
`;
}
if (statisticsCard) {
const isGerman = currentLanguage === 'de';
const message = isGerman ?
'RFID verknüpfen erforderlich' :
'RFID linking required';
const description = isGerman ?
'Verknüpfe deine RFID-Karte, um Statistiken zu sehen.' :
'Link your RFID card to view statistics.';
statisticsCard.innerHTML = `
<h3>📊 Statistiken</h3>
<div style="background: rgba(239, 68, 68, 0.1); border: 1px solid rgba(239, 68, 68, 0.3); border-radius: 0.5rem; padding: 1rem; margin: 1rem 0;">
<div style="display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.5rem;">
<span style="color: #ef4444; font-weight: 600;">⚠️</span>
<span style="color: #ef4444; font-weight: 600;">${message}</span>
</div>
<div style="font-size: 0.9rem; color: #8892b0;">
${description}
</div>
</div>
<button class="btn btn-primary" style="margin-top: 1rem;" onclick="event.stopPropagation(); showRFIDSettings();" data-de="RFID verknüpfen" data-en="Link RFID">RFID verknüpfen</button>
`;
}
} else {
// User is linked - restore original cards
if (analyticsCard) {
const isGerman = currentLanguage === 'de';
analyticsCard.innerHTML = `
<h3>📊 Analytics</h3>
<div id="analyticsPreview" style="display: none;">
<div class="analytics-stats">
<div class="mini-stat">
<div class="mini-stat-number" id="avgTimeThisWeek">--:--</div>
<div class="mini-stat-label">${isGerman ? 'Durchschnitt diese Woche' : 'Average this week'}</div>
</div>
<div class="mini-stat">
<div class="mini-stat-number" id="improvementThisWeek">+0.0%</div>
<div class="mini-stat-label">${isGerman ? 'Verbesserung' : 'Improvement'}</div>
</div>
<div class="mini-stat">
<div class="mini-stat-number" id="runsThisWeek">0</div>
<div class="mini-stat-label">${isGerman ? 'Läufe diese Woche' : 'Runs this week'}</div>
</div>
</div>
</div>
<p>${isGerman ? 'Verfolge deine Leistung und überwache wichtige Metriken.' : 'Track your performance and monitor important metrics.'}</p>
<button class="btn btn-primary" style="margin-top: 1rem;" onclick="event.stopPropagation(); showAnalytics();" data-de="Analytics öffnen" data-en="Open Analytics">${isGerman ? 'Analytics öffnen' : 'Open Analytics'}</button>
`;
}
if (statisticsCard) {
const isGerman = currentLanguage === 'de';
statisticsCard.innerHTML = `
<h3>📊 Statistiken</h3>
<div id="statisticsPreview" style="display: none;">
<div class="statistics-stats">
<div class="mini-stat">
<div class="mini-stat-number" id="personalBest">--:--</div>
<div class="mini-stat-label">${isGerman ? 'Persönliche Bestzeit' : 'Personal Best'}</div>
</div>
<div class="mini-stat">
<div class="mini-stat-number" id="totalRunsCount">0</div>
<div class="mini-stat-label">${isGerman ? 'Gesamte Läufe' : 'Total Runs'}</div>
</div>
<div class="mini-stat">
<div class="mini-stat-number" id="rankPosition">-</div>
<div class="mini-stat-label">${isGerman ? 'Ranglisten-Position' : 'Ranking Position'}</div>
</div>
</div>
</div>
<p>${isGerman ? 'Detaillierte Statistiken zu deinen Läufen - beste Zeiten, Verbesserungen und Vergleiche.' : 'Detailed statistics about your runs - best times, improvements and comparisons.'}</p>
<button class="btn btn-primary" style="margin-top: 1rem;" onclick="event.stopPropagation(); showStatistics();" data-de="Statistiken öffnen" data-en="Open Statistics">${isGerman ? 'Statistiken öffnen' : 'Open Statistics'}</button>
`;
}
}
}
// Update achievement notifications
function updateAchievementNotifications() {
// This will be called when achievements are displayed
}
// Update time display formats
function updateTimeDisplayFormats() {
// This will be called when times are displayed
}
// Update achievement progress text
function updateAchievementProgressText() {
// This will be called when achievements are displayed
}
// Achievement translations are now handled by the database
// Translate achievement data using database translations
function translateAchievement(achievement) {
if (currentLanguage === 'en' && achievement.name_en) {
return {
...achievement,
name: achievement.name_en,
description: achievement.description_en || achievement.description
};
}
return achievement;
}
// Initialize dashboard when page loads
document.addEventListener('DOMContentLoaded', function () {
loadLanguagePreference();
changeLanguage(); // Apply saved language
initDashboard();
// Push notifications are now handled manually via the button
// No automatic subscription on page load
});
// Modal functions
function openModal(modalId) {
document.getElementById(modalId).style.display = 'block';
}
function closeModal(modalId) {
document.getElementById(modalId).style.display = 'none';
// Reset modal state
if (modalId === 'rfidModal') {
stopQRScanner();
document.getElementById('manualRfidInput').value = '';
}
}
// Close modal when clicking outside
window.onclick = function (event) {
if (event.target.classList.contains('modal')) {
closeModal(event.target.id);
}
}
// QR Scanner variables
let qrStream = null;
let qrScanning = false;
// Show RFID Settings
async function showRFIDSettings() {
openModal('rfidModal');
// Reset scanner state
stopQRScanner();
// Check if user is already linked and hide/show create player section
await updateCreatePlayerSectionVisibility();
}
// Update visibility of create player section based on link status
async function updateCreatePlayerSectionVisibility() {
const createPlayerSection = document.getElementById('createPlayerSection');
if (!currentUser) {
// No user logged in - hide create player section
if (createPlayerSection) {
createPlayerSection.style.display = 'none';
}
return;
}
try {
// Check if user has a linked player
const response = await fetch(`/api/v1/public/user-player/${currentUser.id}?t=${Date.now()}`);
if (response.ok) {
const result = await response.json();
if (result.success && result.data && result.data.id) {
// User is already linked - hide create player section
if (createPlayerSection) {
createPlayerSection.style.display = 'none';
}
} else {
// User is not linked - show create player section
if (createPlayerSection) {
createPlayerSection.style.display = 'block';
}
}
} else {
// Error checking link status - show create player section as fallback
if (createPlayerSection) {
createPlayerSection.style.display = 'block';
}
}
} catch (error) {
console.error('Error checking link status:', error);
// Error occurred - show create player section as fallback
if (createPlayerSection) {
createPlayerSection.style.display = 'block';
}
}
}
// Check link status and load times
async function checkLinkStatusAndLoadTimes() {
if (!currentUser) {
showTimesNotLinked();
return;
}
try {
// Check if user has a linked player
const response = await fetch(`/api/v1/public/user-player/${currentUser.id}?t=${Date.now()}`);
if (response.ok) {
const result = await response.json();
// User is linked, load times
await loadUserTimesSection(result.data);
} else {
// User is not linked
showTimesNotLinked();
}
} catch (error) {
console.error('Error checking link status:', error);
showTimesNotLinked();
}
}
// Start QR Scanner
async function startQRScanner() {
try {
// Request camera access
qrStream = await navigator.mediaDevices.getUserMedia({
video: {
facingMode: 'environment', // Use back camera if available
width: { ideal: 1280 },
height: { ideal: 720 }
}
});
const video = document.getElementById('qrVideo');
const canvas = document.getElementById('qrCanvas');
const context = canvas.getContext('2d');
video.srcObject = qrStream;
video.play();
// Show camera container and update buttons
document.getElementById('cameraContainer').style.display = 'block';
document.getElementById('startScanBtn').style.display = 'none';
document.getElementById('stopScanBtn').style.display = 'inline-block';
document.getElementById('scanningStatus').style.display = 'block';
qrScanning = true;
// Start scanning loop
video.addEventListener('loadedmetadata', () => {
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
scanQRCode();
});
} catch (error) {
console.error('Error accessing camera:', error);
const cameraErrorMsg = currentLanguage === 'de' ?
'Kamera-Zugriff fehlgeschlagen. Bitte verwende die manuelle Eingabe.' :
'Camera access failed. Please use manual input.';
showMessage('rfidMessage', cameraErrorMsg, 'error');
}
}
// Stop QR Scanner
function stopQRScanner() {
qrScanning = false;
if (qrStream) {
qrStream.getTracks().forEach(track => track.stop());
qrStream = null;
}
// Reset UI
document.getElementById('cameraContainer').style.display = 'none';
document.getElementById('startScanBtn').style.display = 'inline-block';
document.getElementById('stopScanBtn').style.display = 'none';
document.getElementById('scanningStatus').style.display = 'none';
}
// Scan QR Code from video stream
function scanQRCode() {
if (!qrScanning) return;
const video = document.getElementById('qrVideo');
const canvas = document.getElementById('qrCanvas');
const context = canvas.getContext('2d');
if (video.readyState === video.HAVE_ENOUGH_DATA) {
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
context.drawImage(video, 0, 0, canvas.width, canvas.height);
const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
const code = jsQR(imageData.data, imageData.width, imageData.height);
if (code) {
console.log('QR Code detected:', code.data);
handleQRCodeDetected(code.data);
return;
}
}
// Continue scanning
if (qrScanning) {
requestAnimationFrame(scanQRCode);
}
}
// Format RFID UID to match database format
function formatRfidUid(rawUid) {
// Remove any existing formatting (spaces, colons, etc.)
let cleanUid = rawUid.replace(/[^a-fA-F0-9]/g, '').toUpperCase();
// Handle different UID lengths
if (cleanUid.length === 6) {
// Pad 6-digit UID to 8 digits by adding leading zeros
cleanUid = '00' + cleanUid;
} else if (cleanUid.length === 8) {
// Already correct length
} else if (cleanUid.length < 6) {
// Pad shorter UIDs to 8 digits
cleanUid = cleanUid.padStart(8, '0');
} else {
throw new Error(`Ungültige RFID UID Länge: ${cleanUid.length} Zeichen (unterstützt: 6-8)`);
}
// Format as XX:XX:XX:XX
return cleanUid.match(/.{2}/g).join(':');
}
// Handle detected QR code
async function handleQRCodeDetected(qrData) {
stopQRScanner();
try {
// Extract and format RFID UID from QR code
const rawUid = qrData.trim();
if (!rawUid) {
const qrErrorMsg = currentLanguage === 'de' ?
'QR-Code enthält keine gültige RFID UID' :
'QR code contains no valid RFID UID';
showMessage('rfidMessage', qrErrorMsg, 'error');
return;
}
// Format the UID to match database format (XX:XX:XX:XX)
const formattedUid = formatRfidUid(rawUid);
const qrDetectedMsg = currentLanguage === 'de' ?
`QR-Code erkannt: ${rawUid}${formattedUid}` :
`QR code detected: ${rawUid}${formattedUid}`;
showMessage('rfidMessage', qrDetectedMsg, 'info');
// Link the user using the formatted RFID UID
await linkUserByRfidUid(formattedUid);
} catch (error) {
console.error('Error formatting RFID UID:', error);
const formatErrorMsg = currentLanguage === 'de' ?
`Fehler beim Formatieren der RFID UID: ${error.message}` :
`Error formatting RFID UID: ${error.message}`;
showMessage('rfidMessage', formatErrorMsg, 'error');
}
}
// Manual RFID linking
async function linkManualRfid() {
const rawUid = document.getElementById('manualRfidInput').value.trim();
if (!rawUid) {
const inputErrorMsg = currentLanguage === 'de' ?
'Bitte gib eine RFID UID ein' :
'Please enter a RFID UID';
showMessage('rfidMessage', inputErrorMsg, 'error');
return;
}
try {
// Format the UID to match database format
const formattedUid = formatRfidUid(rawUid);
const formattedMsg = currentLanguage === 'de' ?
`Formatiert: ${rawUid}${formattedUid}` :
`Formatted: ${rawUid}${formattedUid}`;
showMessage('rfidMessage', formattedMsg, 'info');
await linkUserByRfidUid(formattedUid);
} catch (error) {
console.error('Error formatting manual RFID UID:', error);
const formatErrorMsg = currentLanguage === 'de' ?
`Fehler beim Formatieren: ${error.message}` :
`Error formatting: ${error.message}`;
showMessage('rfidMessage', formatErrorMsg, 'error');
}
}
// Create new RFID player record
async function createRfidPlayerRecord() {
const rawUid = document.getElementById('manualRfidInput').value.trim();
const firstname = document.getElementById('playerFirstname').value.trim();
const lastname = document.getElementById('playerLastname').value.trim();
const birthdate = document.getElementById('playerBirthdate').value;
// Validation
if (!rawUid) {
const inputErrorMsg = currentLanguage === 'de' ?
'Bitte gib eine RFID UID ein' :
'Please enter a RFID UID';
showMessage('rfidMessage', inputErrorMsg, 'error');
return;
}
if (!firstname) {
const inputErrorMsg = currentLanguage === 'de' ?
'Bitte gib einen Vornamen ein' :
'Please enter a first name';
showMessage('rfidMessage', inputErrorMsg, 'error');
return;
}
if (!lastname) {
const inputErrorMsg = currentLanguage === 'de' ?
'Bitte gib einen Nachnamen ein' :
'Please enter a last name';
showMessage('rfidMessage', inputErrorMsg, 'error');
return;
}
if (!birthdate) {
const inputErrorMsg = currentLanguage === 'de' ?
'Bitte gib ein Geburtsdatum ein' :
'Please enter a birth date';
showMessage('rfidMessage', inputErrorMsg, 'error');
return;
}
try {
// Format the UID to match database format
const formattedUid = formatRfidUid(rawUid);
const formattedMsg = currentLanguage === 'de' ?
`Erstelle Spieler: ${firstname} ${lastname} (${formattedUid})` :
`Creating player: ${firstname} ${lastname} (${formattedUid})`;
showMessage('rfidMessage', formattedMsg, 'info');
// Create player record
const response = await fetch('/api/v1/public/players/create-with-rfid', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
rfiduid: formattedUid,
firstname: firstname,
lastname: lastname,
birthdate: birthdate,
supabase_user_id: currentUser?.id || null
})
});
const result = await response.json();
if (result.success) {
const successMsg = currentLanguage === 'de' ?
`Spieler erfolgreich erstellt: ${result.data.firstname} ${result.data.lastname}` :
`Player successfully created: ${result.data.firstname} ${result.data.lastname}`;
showMessage('rfidMessage', successMsg, 'success');
// Clear form
document.getElementById('manualRfidInput').value = '';
document.getElementById('playerFirstname').value = '';
document.getElementById('playerLastname').value = '';
document.getElementById('playerBirthdate').value = '';
// Hide create player section since user is now linked
const createPlayerSection = document.getElementById('createPlayerSection');
if (createPlayerSection) {
createPlayerSection.style.display = 'none';
}
// Refresh times if user is linked
if (currentUser) {
await checkLinkStatusAndLoadTimes();
}
} else {
let errorMsg = result.message;
// Erweitere Fehlermeldung um Levenshtein-Details falls vorhanden
if (result.details) {
if (result.details.matchType === 'similar') {
const similarityPercent = Math.round((1 - result.details.similarity) * 100);
errorMsg += `\n\nÄhnlichkeit: ${similarityPercent}% (Distanz: ${result.details.levenshteinDistance})`;
}
}
showMessage('rfidMessage', errorMsg, 'error');
}
} catch (error) {
console.error('Error creating RFID player record:', error);
const errorMsg = currentLanguage === 'de' ?
`Fehler beim Erstellen: ${error.message}` :
`Error creating: ${error.message}`;
showMessage('rfidMessage', errorMsg, 'error');
}
}
// Link user by RFID UID (core function)
async function linkUserByRfidUid(rfidUid) {
if (!currentUser) {
const authErrorMsg = currentLanguage === 'de' ?
'Benutzer nicht authentifiziert' :
'User not authenticated';
showMessage('rfidMessage', authErrorMsg, 'error');
return;
}
try {
// First, find the player with this RFID UID
const response = await fetch('/api/v1/public/link-by-rfid', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
rfiduid: rfidUid,
supabase_user_id: currentUser.id
})
});
const result = await response.json();
if (response.ok) {
const successMsg = currentLanguage === 'de' ?
`✅ RFID erfolgreich verknüpft!\nSpieler: ${result.data.firstname} ${result.data.lastname}` :
`✅ RFID successfully linked!\nPlayer: ${result.data.firstname} ${result.data.lastname}`;
showMessage('rfidMessage', successMsg, 'success');
setTimeout(() => {
closeModal('rfidModal');
// Reload times section after successful linking
checkLinkStatusAndLoadTimes();
}, 2000);
} else {
const errorMsg = currentLanguage === 'de' ?
result.message || 'Fehler beim Verknüpfen' :
result.message || 'Error linking';
showMessage('rfidMessage', errorMsg, 'error');
}
} catch (error) {
console.error('Error linking RFID:', error);
const linkErrorMsg = currentLanguage === 'de' ?
'Fehler beim Verknüpfen der RFID' :
'Error linking RFID';
showMessage('rfidMessage', linkErrorMsg, 'error');
}
}
// Show not linked state
function showTimesNotLinked() {
document.getElementById('timesLoading').style.display = 'none';
document.getElementById('timesNotLinked').style.display = 'block';
document.getElementById('timesDisplay').style.display = 'none';
// Update the text content for the not linked state
const notLinkedTitle = document.querySelector('#timesNotLinked h3');
const notLinkedDescription = document.querySelector('#timesNotLinked p');
const notLinkedButton = document.querySelector('#timesNotLinked button');
const notLinkedSteps = document.querySelectorAll('#timesNotLinked li');
if (notLinkedTitle) {
notLinkedTitle.textContent = currentLanguage === 'de' ?
'RFID noch nicht verknüpft' :
'RFID not linked yet';
}
if (notLinkedDescription) {
notLinkedDescription.textContent = currentLanguage === 'de' ?
'Um deine persönlichen Zeiten zu sehen, musst du zuerst deine RFID-Karte mit deinem Account verknüpfen.' :
'To see your personal times, you must first link your RFID card with your account.';
}
if (notLinkedButton) {
notLinkedButton.textContent = currentLanguage === 'de' ?
'🏷️ RFID jetzt verknüpfen' :
'🏷️ Link RFID now';
}
if (notLinkedSteps.length >= 3) {
notLinkedSteps[0].textContent = currentLanguage === 'de' ?
'Klicke auf "RFID jetzt verknüpfen"' :
'Click on "Link RFID now"';
notLinkedSteps[1].textContent = currentLanguage === 'de' ?
'Scanne den QR-Code auf deiner RFID-Karte' :
'Scan the QR code on your RFID card';
notLinkedSteps[2].textContent = currentLanguage === 'de' ?
'Fertig! Deine Zeiten werden automatisch hier angezeigt' :
'Done! Your times will be displayed here automatically';
}
// Update the "So funktioniert's" title
const howItWorksTitle = document.querySelector('#timesNotLinked h4');
if (howItWorksTitle) {
howItWorksTitle.textContent = currentLanguage === 'de' ?
'So funktioniert\'s:' :
'How it works:';
}
}
// Show RFID linked info with help button
function showRFIDLinkedInfo(playerData) {
// Find the RFID card and update it
const rfidCard = document.querySelector('.card[onclick="showRFIDSettings()"]');
if (rfidCard) {
const isGerman = currentLanguage === 'de';
rfidCard.innerHTML = `
<h3>${isGerman ? '🏷️ RFID Verknüpft' : '🏷️ RFID Linked'}</h3>
<div style="background: rgba(16, 185, 129, 0.1); border: 1px solid rgba(16, 185, 129, 0.3); border-radius: 0.5rem; padding: 1rem; margin: 1rem 0;">
<div style="display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.5rem;">
<span style="color: #10b981; font-weight: 600;">✅</span>
<span style="color: #10b981; font-weight: 600;">
${isGerman ? 'Erfolgreich verknüpft' : 'Successfully linked'}
</span>
</div>
<div style="font-size: 0.9rem; color: #8892b0;">
<div><strong>${isGerman ? 'Spieler:' : 'Player:'}</strong> ${playerData.firstname} ${playerData.lastname}</div>
<div><strong>RFID:</strong> <code style="background: rgba(255,255,255,0.1); padding: 0.2rem 0.4rem; border-radius: 0.25rem; font-family: monospace;">${playerData.rfiduid}</code></div>
</div>
</div>
<button class="btn btn-secondary" onclick="requestRFIDHelp()" style="margin-top: 1rem; font-size: 0.9rem; padding: 0.5rem 1rem;">
${isGerman ? '❓ Hilfe anfordern' : '❓ Request Help'}
</button>
`;
// Remove the onclick from the card since we don't want it to open the modal
rfidCard.removeAttribute('onclick');
rfidCard.style.cursor = 'default';
}
}
// Request RFID help
function requestRFIDHelp() {
const isGerman = currentLanguage === 'de';
const message = isGerman ?
'Hilfe-Anfrage gesendet! Ein Administrator wird sich bei dir melden, um bei der RFID-Änderung zu helfen.' :
'Help request sent! An administrator will contact you to help with the RFID change.';
alert(message);
// Here you could send a notification to admins or log the help request
console.log('RFID help requested by user:', currentUser?.email);
}
// Show loading state
function showTimesLoading() {
document.getElementById('timesLoading').style.display = 'block';
document.getElementById('timesNotLinked').style.display = 'none';
document.getElementById('timesDisplay').style.display = 'none';
// Update the loading text
const loadingText = document.querySelector('#timesLoading p');
if (loadingText) {
loadingText.textContent = currentLanguage === 'de' ?
'Lade deine Zeiten...' :
'Loading your times...';
}
}
// Load user times for the section
async function loadUserTimesSection(playerData) {
showTimesLoading();
try {
const response = await fetch(`/api/v1/public/user-times/${currentUser.id}?t=${Date.now()}`);
const result = await response.json();
if (!response.ok) {
throw new Error(result.message || 'Failed to load times');
}
const times = result.data || result;
// Update stats
updateTimesStats(times, playerData);
// Display times
displayUserTimes(times);
// Show RFID info and help button
showRFIDLinkedInfo(playerData);
// Show the times display
document.getElementById('timesLoading').style.display = 'none';
document.getElementById('timesNotLinked').style.display = 'none';
document.getElementById('timesDisplay').style.display = 'block';
// Initialize achievements for this player
initializeAchievements(playerData.id);
// Update analytics and statistics cards (user is now linked)
updateAnalyticsAndStatisticsCards();
} catch (error) {
console.error('Error loading user times:', error);
showTimesNotLinked();
}
}
// Update stats cards
function updateTimesStats(times, playerData) {
// Total runs
document.getElementById('totalRuns').textContent = times.length;
// Best time
if (times.length > 0) {
const bestTimeValue = times.reduce((best, current) => {
const currentSeconds = convertTimeToSeconds(current.recorded_time);
const bestSeconds = convertTimeToSeconds(best.recorded_time);
return currentSeconds < bestSeconds ? current : best;
});
document.getElementById('bestTime').textContent = formatTime(bestTimeValue.recorded_time);
} else {
document.getElementById('bestTime').textContent = '--:--';
}
// Unique locations count
const uniqueLocations = [...new Set(times.map(time => time.location_name))];
document.getElementById('locationsCount').textContent = uniqueLocations.length;
// Linked player name
document.getElementById('linkedPlayer').textContent = `${playerData.firstname} ${playerData.lastname}`;
}
// Display user times in grid
function displayUserTimes(times) {
const timesGrid = document.getElementById('userTimesGrid');
if (times.length === 0) {
const noTimesTitle = currentLanguage === 'de' ? 'Noch keine Zeiten aufgezeichnet' : 'No times recorded yet';
const noTimesDescription = currentLanguage === 'de' ?
'Deine ersten Läufe werden hier angezeigt, sobald du sie abgeschlossen hast!' :
'Your first runs will be displayed here as soon as you complete them!';
timesGrid.innerHTML = `
<div style="grid-column: 1 / -1; text-align: center; padding: 3rem; color: #8892b0;">
<h3>${noTimesTitle}</h3>
<p>${noTimesDescription}</p>
</div>
`;
return;
}
// Group times by location
const timesByLocation = times.reduce((acc, time) => {
if (!acc[time.location_name]) {
acc[time.location_name] = [];
}
acc[time.location_name].push(time);
return acc;
}, {});
// Generate cards for each location
const cards = Object.entries(timesByLocation).map(([locationName, locationTimes], index) => {
// Sort times by performance (best first)
const sortedTimes = locationTimes.sort((a, b) => {
return convertTimeToSeconds(a.recorded_time) - convertTimeToSeconds(b.recorded_time);
});
// Get best time for this location
const bestTime = sortedTimes[0];
// Generate all runs for expanded view
const allRunsHtml = sortedTimes.map((run, runIndex) => {
let rankBadge = '';
let rankClass = '';
if (runIndex === 0) {
rankBadge = currentLanguage === 'de' ? '🥇 Beste' : '🥇 Best';
rankClass = 'best';
} else if (runIndex === 1) {
rankBadge = '🥈 2.';
rankClass = 'second';
} else if (runIndex === 2) {
rankBadge = '🥉 3.';
rankClass = 'third';
} else {
rankBadge = `${runIndex + 1}.`;
rankClass = '';
}
return `
<div class="run-item">
<div>
<div class="run-time">${formatTime(run.recorded_time)}</div>
</div>
<div class="run-details">
<div>${new Date(run.created_at).toLocaleDateString(currentLanguage === 'de' ? 'de-DE' : 'en-US')}</div>
<div>${new Date(run.created_at).toLocaleTimeString(currentLanguage === 'de' ? 'de-DE' : 'en-US', { hour: '2-digit', minute: '2-digit' })}</div>
<span class="run-rank-badge ${rankClass}">${rankBadge}</span>
</div>
</div>
`;
}).join('');
return `
<div class="user-time-card" onclick="toggleTimeCard(this)" data-location="${locationName}">
<div class="card-header">
<div class="time-location-name">${locationName}</div>
<div class="expand-indicator">▼</div>
</div>
<div class="card-main-content">
<div class="time-value-large">${formatTime(bestTime.recorded_time)}</div>
<div class="time-date-info">
<span>${new Date(bestTime.created_at).toLocaleDateString(currentLanguage === 'de' ? 'de-DE' : 'en-US')}</span>
<span class="time-rank">${locationTimes.length} ${currentLanguage === 'de' ? 'Läufe' : 'Runs'}</span>
</div>
</div>
<div class="expanded-content">
<div class="all-runs-title">${currentLanguage === 'de' ? 'Alle Läufe an diesem Standort:' : 'All runs at this location:'}</div>
${allRunsHtml}
</div>
</div>
`;
}).join('');
timesGrid.innerHTML = cards;
}
// Toggle time card expansion
function toggleTimeCard(cardElement) {
const isExpanded = cardElement.classList.contains('expanded');
// Close all other cards first
document.querySelectorAll('.user-time-card.expanded').forEach(card => {
if (card !== cardElement) {
card.classList.remove('expanded');
}
});
// Toggle current card
if (isExpanded) {
cardElement.classList.remove('expanded');
} else {
cardElement.classList.add('expanded');
}
}
// Helper function to convert time to seconds for comparison
function convertTimeToSeconds(timeValue) {
if (typeof timeValue === 'string') {
// Handle HH:MM:SS format
const parts = timeValue.split(':');
if (parts.length === 3) {
return parseInt(parts[0]) * 3600 + parseInt(parts[1]) * 60 + parseFloat(parts[2]);
}
// Handle MM:SS format
if (parts.length === 2) {
return parseInt(parts[0]) * 60 + parseFloat(parts[1]);
}
}
return parseFloat(timeValue) || 0;
}
// Format time interval to readable format
function formatTime(interval) {
// Handle numeric values (seconds)
if (typeof interval === 'number') {
return formatSeconds(interval);
}
// Postgres interval format: {"hours":0,"minutes":1,"seconds":23.45}
if (typeof interval === 'object') {
const { hours = 0, minutes = 0, seconds = 0 } = interval;
const totalSeconds = hours * 3600 + minutes * 60 + seconds;
return formatSeconds(totalSeconds);
}
// Fallback for string format
if (typeof interval === 'string') {
// Parse format like "00:01:23.45"
const parts = interval.split(':');
if (parts.length === 3) {
const hours = parseInt(parts[0]);
const minutes = parseInt(parts[1]);
const seconds = parseFloat(parts[2]);
const totalSeconds = hours * 3600 + minutes * 60 + seconds;
return formatSeconds(totalSeconds);
}
}
return interval;
}
function formatSeconds(totalSeconds) {
const minutes = Math.floor(totalSeconds / 60);
const seconds = (totalSeconds % 60).toFixed(2);
if (minutes > 0) {
return `${minutes}:${seconds.padStart(5, '0')}`;
} else {
return `${seconds}s`;
}
}
// Show message in modal
function showMessage(containerId, message, type) {
const container = document.getElementById(containerId);
container.innerHTML = `<div class="message ${type}">${message}</div>`;
}
// Initialize when DOM is loaded
// ==================== ACHIEVEMENT FUNCTIONS ====================
// Global variables for achievements
let currentPlayerId = null;
let allAchievements = [];
let playerAchievements = [];
let currentAchievementCategory = 'all';
// Load achievements for the current player
async function loadPlayerAchievements() {
if (!currentPlayerId) {
showAchievementsNotAvailable();
return;
}
try {
// Show loading state
document.getElementById('achievementsLoading').style.display = 'block';
document.getElementById('achievementStats').style.display = 'none';
document.getElementById('achievementCategories').style.display = 'none';
document.getElementById('achievementsNotAvailable').style.display = 'none';
// Update loading text
const loadingText = document.querySelector('#achievementsLoading p');
if (loadingText) {
loadingText.textContent = currentLanguage === 'de' ?
'Lade deine Achievements...' :
'Loading your achievements...';
}
// Load player achievements (includes all achievements with player status)
const response = await fetch(`/api/achievements/player/${currentPlayerId}?t=${Date.now()}`);
if (!response.ok) {
throw new Error('Failed to load player achievements');
}
const result = await response.json();
window.allAchievements = result.data;
playerAchievements = result.data.filter(achievement => achievement.is_completed);
// Load achievement statistics
await loadAchievementStats();
// Show achievements
displayAchievementStats();
displayAchievements();
// Hide loading state
document.getElementById('achievementsLoading').style.display = 'none';
document.getElementById('achievementStats').style.display = 'flex';
document.getElementById('achievementCategories').style.display = 'block';
} catch (error) {
console.error('Error loading achievements:', error);
document.getElementById('achievementsLoading').style.display = 'none';
showAchievementsNotAvailable();
}
}
// Load achievement statistics
async function loadAchievementStats() {
try {
const response = await fetch(`/api/achievements/player/${currentPlayerId}/stats?t=${Date.now()}`);
if (response.ok) {
const result = await response.json();
window.achievementStats = result.data;
}
} catch (error) {
console.error('Error loading achievement stats:', error);
}
}
// Display achievement statistics
function displayAchievementStats() {
if (!window.achievementStats) return;
const stats = window.achievementStats;
document.getElementById('totalPoints').textContent = stats.total_points;
document.getElementById('completedAchievements').textContent = `${stats.completed_achievements}/${stats.total_achievements}`;
document.getElementById('achievementsToday').textContent = stats.achievements_today;
document.getElementById('completionPercentage').textContent = `${stats.completion_percentage}%`;
}
// Display achievements in grid
function displayAchievements() {
const achievementsGrid = document.getElementById('achievementsGrid');
if (!window.allAchievements || window.allAchievements.length === 0) {
const noAchievementsTitle = currentLanguage === 'de' ? 'Noch keine Achievements' : 'No Achievements Yet';
const noAchievementsDescription = currentLanguage === 'de' ?
'Starte deine ersten Läufe, um Achievements zu sammeln!' :
'Start your first runs to collect achievements!';
achievementsGrid.innerHTML = `
<div class="no-achievements">
<div class="no-achievements-icon">🏆</div>
<h3>${noAchievementsTitle}</h3>
<p>${noAchievementsDescription}</p>
</div>
`;
return;
}
// Filter achievements by category
let filteredAchievements = window.allAchievements;
if (currentAchievementCategory !== 'all') {
filteredAchievements = window.allAchievements.filter(achievement =>
achievement.category === currentAchievementCategory
);
}
// Generate achievement cards
const achievementCards = filteredAchievements.map(achievement => {
const isCompleted = achievement.is_completed;
const progress = achievement.progress || 0;
const earnedAt = achievement.earned_at;
const completionCount = achievement.completion_count || 0;
// Translate achievement
const translatedAchievement = translateAchievement(achievement);
let progressText = '';
if (isCompleted) {
const achievedText = currentLanguage === 'de' ? 'Erreicht am' : 'Achieved on';
const completedText = currentLanguage === 'de' ? 'Abgeschlossen' : 'Completed';
const timesText = currentLanguage === 'de' ? 'x geschafft' : 'x completed';
if (completionCount > 1) {
progressText = `${completionCount}${timesText}`;
} else {
progressText = earnedAt ?
`${achievedText} ${new Date(earnedAt).toLocaleDateString(currentLanguage === 'de' ? 'de-DE' : 'en-US')}` :
completedText;
}
} else if (progress > 0) {
// Show progress for incomplete achievements
const conditionValue = getAchievementConditionValue(achievement.name);
if (conditionValue) {
progressText = `${progress}/${conditionValue}`;
}
}
const pointsText = currentLanguage === 'de' ? 'Punkte' : 'Points';
const totalPoints = completionCount > 0 ? achievement.points * completionCount : achievement.points;
return `
<div class="achievement-card ${isCompleted ? 'completed' : 'incomplete'}"
onclick="showAchievementDetails('${achievement.id}')">
<div class="achievement-icon">${achievement.icon}</div>
<div class="achievement-content">
<h4 class="achievement-name">${translatedAchievement.name}</h4>
<p class="achievement-description">${translatedAchievement.description}</p>
<div class="achievement-meta">
<span class="achievement-points">+${totalPoints} ${pointsText}</span>
${progressText ? `<span class="achievement-progress">${progressText}</span>` : ''}
</div>
</div>
<div class="achievement-status">
${isCompleted ? '✅' : '⏳'}
</div>
</div>
`;
}).join('');
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');
return;
}
// Load analytics data (will show fallback if not linked)
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');
return;
}
// Load statistics data (will show fallback if not linked)
loadStatisticsData();
}
async function loadAnalyticsData() {
try {
if (!currentPlayerId) {
console.error('No player ID available - user not linked');
// Show fallback data when user is not linked
displayAnalyticsFallback();
return;
}
// Load analytics data from API
const response = await fetch(`/api/analytics/player/${currentPlayerId}`);
if (!response.ok) {
throw new Error('Failed to load analytics data');
}
const analyticsData = await response.json();
displayAnalyticsData(analyticsData.data);
// Update preview in main card
updateAnalyticsPreview(analyticsData.data);
} 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 - user not linked');
// Show fallback data when user is not linked
displayStatisticsFallback();
return;
}
// Load statistics data from API
const response = await fetch(`/api/statistics/player/${currentPlayerId}`);
if (!response.ok) {
throw new Error('Failed to load statistics data');
}
const statisticsData = await response.json();
displayStatisticsData(statisticsData.data);
// Update preview in main card
updateStatisticsPreview(statisticsData.data);
} 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.toFixed(2) + '%';
// Activity Stats
document.getElementById('runsToday').textContent = data.activity.runsToday + ' Läufe';
document.getElementById('runsThisWeekDetail').textContent = data.activity.runsThisWeek + ' Läufe';
document.getElementById('avgRunsPerDay').textContent = data.activity.avgRunsPerDay.toFixed(2);
// 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.toFixed(2) + '%';
// 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) {
if (data && data.performance && data.activity) {
document.getElementById('avgTimeThisWeek').textContent = formatTime(data.performance.avgTimeThisWeek);
document.getElementById('improvementThisWeek').textContent = data.performance.improvement.toFixed(2) + '%';
document.getElementById('runsThisWeek').textContent = data.activity.runsThisWeek;
document.getElementById('analyticsPreview').style.display = 'block';
} else {
// Hide preview if no data
document.getElementById('analyticsPreview').style.display = 'none';
}
}
function updateStatisticsPreview(data) {
if (data && data.personalRecords && data.progress) {
document.getElementById('personalBest').textContent = formatTime(data.personalRecords[0]?.time || 0);
document.getElementById('totalRunsCount').textContent = data.progress.totalRuns;
document.getElementById('rankPosition').textContent = data.rankings[0]?.position || '-';
document.getElementById('statisticsPreview').style.display = 'block';
} else {
// Hide preview if no data
document.getElementById('statisticsPreview').style.display = 'none';
}
}
function displayAnalyticsFallback() {
// Show fallback data when API fails or user not linked
const notLinkedMessage = currentLanguage === 'de' ?
'RFID nicht verknüpft - Analytics nicht verfügbar' :
'RFID not linked - Analytics not available';
document.getElementById('avgTimeThisWeekDetail').textContent = '--:--';
document.getElementById('avgTimeLastWeek').textContent = '--:--';
document.getElementById('improvementDetail').textContent = '+0.0%';
document.getElementById('runsToday').textContent = '0 Läufe';
document.getElementById('runsThisWeekDetail').textContent = '0 Läufe';
document.getElementById('avgRunsPerDay').textContent = '0.0';
document.getElementById('locationPerformance').innerHTML = `<p style="color: #8892b0; text-align: center; padding: 2rem;">${notLinkedMessage}</p>`;
document.getElementById('runsThisMonth').textContent = '0 Läufe';
document.getElementById('runsLastMonth').textContent = '0 Läufe';
document.getElementById('bestTimeThisMonth').textContent = '--:--';
}
function displayStatisticsFallback() {
// Show fallback data when API fails or user not linked
const notLinkedMessage = currentLanguage === 'de' ?
'RFID nicht verknüpft - Statistiken nicht verfügbar' :
'RFID not linked - Statistics not available';
document.getElementById('personalRecords').innerHTML = `<p style="color: #8892b0; text-align: center; padding: 2rem;">${notLinkedMessage}</p>`;
document.getElementById('averageTime').textContent = '--:--';
document.getElementById('timeDeviation').textContent = '--:--';
document.getElementById('consistencyScore').textContent = '0%';
document.getElementById('rankingStats').innerHTML = `<p style="color: #8892b0; text-align: center; padding: 2rem;">${notLinkedMessage}</p>`;
document.getElementById('totalRunsStats').textContent = '0';
document.getElementById('activeDays').textContent = '0';
document.getElementById('locationsVisited').textContent = '0';
}
// Get achievement condition value for progress display
function getAchievementConditionValue(achievementName) {
const conditionMap = {
'Erste Schritte': 1,
'Durchhalter': 3,
'Fleißig': 5,
'Besessen': 10,
'Regelmäßig': 5,
'Stammgast': 10,
'Treue': 20,
'Veteran': 50,
'Fortschritt': 5,
'Durchbruch': 10,
'Transformation': 15,
'Perfektionist': 20
};
return conditionMap[achievementName] || null;
}
// Show achievement category
function showAchievementCategory(category) {
currentAchievementCategory = category;
// Update active tab
document.querySelectorAll('.category-tab').forEach(tab => {
tab.classList.remove('active');
});
document.querySelector(`[data-category="${category}"]`).classList.add('active');
// Display filtered achievements
displayAchievements();
}
// Show achievement details (placeholder for future modal)
function showAchievementDetails(achievementId) {
const achievement = playerAchievements.find(a => a.id === achievementId);
if (achievement) {
console.log('Achievement details:', achievement);
// TODO: Implement achievement details modal
}
}
// Show achievements not available state
function showAchievementsNotAvailable() {
document.getElementById('achievementsLoading').style.display = 'none';
document.getElementById('achievementStats').style.display = 'none';
document.getElementById('achievementCategories').style.display = 'none';
document.getElementById('achievementsNotAvailable').style.display = 'block';
// Update the text content for the not available state
const notAvailableTitle = document.querySelector('#achievementsNotAvailable h3');
const notAvailableDescription = document.querySelector('#achievementsNotAvailable p');
const notAvailableButton = document.querySelector('#achievementsNotAvailable button');
if (notAvailableTitle) {
notAvailableTitle.textContent = currentLanguage === 'de' ?
'Achievements noch nicht verfügbar' :
'Achievements not available yet';
}
if (notAvailableDescription) {
notAvailableDescription.textContent = currentLanguage === 'de' ?
'Um Achievements zu sammeln, musst du zuerst deine RFID-Karte mit deinem Account verknüpfen und einige Läufe absolvieren.' :
'To collect achievements, you must first link your RFID card with your account and complete some runs.';
}
if (notAvailableButton) {
notAvailableButton.textContent = currentLanguage === 'de' ?
'🏷️ RFID jetzt verknüpfen' :
'🏷️ Link RFID now';
}
// Update the "So funktioniert's" title
const howItWorksTitle = document.querySelector('#achievementsNotAvailable h4');
if (howItWorksTitle) {
howItWorksTitle.textContent = currentLanguage === 'de' ?
'So funktioniert\'s:' :
'How it works:';
}
// Update the steps
const notAvailableSteps = document.querySelectorAll('#achievementsNotAvailable li');
if (notAvailableSteps.length >= 3) {
notAvailableSteps[0].textContent = currentLanguage === 'de' ?
'Klicke auf "RFID jetzt verknüpfen"' :
'Click on "Link RFID now"';
notAvailableSteps[1].textContent = currentLanguage === 'de' ?
'Scanne den QR-Code auf deiner RFID-Karte' :
'Scan the QR code on your RFID card';
notAvailableSteps[2].textContent = currentLanguage === 'de' ?
'Fertig! Deine Zeiten werden automatisch hier angezeigt' :
'Done! Your times will be displayed here automatically';
}
}
// Check achievements for current player
async function checkPlayerAchievements() {
if (!currentPlayerId) return;
try {
const response = await fetch(`/api/achievements/check/${currentPlayerId}?t=${Date.now()}`, {
method: 'POST'
});
if (response.ok) {
const result = await response.json();
if (result.data.count > 0) {
// Show notification for new achievements
showAchievementNotification(result.data.new_achievements);
// Reload achievements
await loadPlayerAchievements();
}
}
} catch (error) {
console.error('Error checking achievements:', error);
}
}
// Show achievement notification
function showAchievementNotification(newAchievements) {
// Create notification element
const notification = document.createElement('div');
notification.className = 'achievement-notification';
const titleText = currentLanguage === 'de' ? 'Neue Achievements erreicht!' : 'New Achievements Unlocked!';
const descriptionText = currentLanguage === 'de' ?
`Du hast ${newAchievements.length} neue Achievement${newAchievements.length > 1 ? 's' : ''} erhalten!` :
`You have received ${newAchievements.length} new achievement${newAchievements.length > 1 ? 's' : ''}!`;
notification.innerHTML = `
<div class="notification-content">
<div class="notification-icon">🏆</div>
<div class="notification-text">
<h4>${titleText}</h4>
<p>${descriptionText}</p>
</div>
<button class="notification-close" onclick="this.parentElement.parentElement.remove()">×</button>
</div>
`;
// Add to page
document.body.appendChild(notification);
// Auto-remove after 5 seconds
setTimeout(() => {
if (notification.parentElement) {
notification.remove();
}
}, 5000);
}
// Initialize achievements when player is loaded
function initializeAchievements(playerId) {
currentPlayerId = playerId;
loadPlayerAchievements();
}
// Convert VAPID key from base64url to Uint8Array
function urlBase64ToUint8Array(base64String) {
const padding = '='.repeat((4 - base64String.length % 4) % 4);
const base64 = (base64String + padding)
.replace(/\-/g, '+')
.replace(/_/g, '/');
const rawData = window.atob(base64);
const outputArray = new Uint8Array(rawData.length);
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
}
// Push notifications are now handled in dashboard.html
// This function has been moved to the HTML file for better integration
// Web Notification Functions
function showWebNotification(title, message, icon = '🏆') {
if ('Notification' in window && Notification.permission === 'granted') {
const notification = new Notification(title, {
body: message,
icon: '/pictures/icon-192.png',
badge: '/pictures/icon-192.png',
tag: 'ninjacross-achievement',
requireInteraction: true
});
// Auto-close after 10 seconds
setTimeout(() => {
notification.close();
}, 10000);
// Handle click
notification.onclick = function () {
window.focus();
notification.close();
};
}
}
// Track which notifications have been sent today
let notificationsSentToday = {
daily: false,
weekly: false,
monthly: false
};
// Check for best time achievements and show notifications
async function checkBestTimeNotifications() {
try {
// Check if push notifications are enabled
const pushPlayerId = localStorage.getItem('pushPlayerId');
if (!pushPlayerId) {
console.log('🔕 Push notifications disabled, skipping best time check');
return;
}
const response = await fetch('/api/v1/public/best-times');
const result = await response.json();
if (result.success && result.data) {
const { daily, weekly, monthly } = result.data;
// Check if current player has best times
if (currentPlayerId) {
const now = new Date();
const isEvening = now.getHours() >= 19;
// Check daily best time (only in the evening, only once per day)
if (daily && daily.player_id === currentPlayerId && isEvening) {
// Check if notification was already sent today
const dailyCheck = await fetch(`/api/v1/public/notification-sent/${currentPlayerId}/daily_best`);
const dailyResult = await dailyCheck.json();
if (!dailyResult.wasSent) {
const title = currentLanguage === 'de' ? '🏆 Tageskönig!' : '🏆 Daily King!';
const message = currentLanguage === 'de' ?
`Glückwunsch! Du hast die beste Zeit des Tages mit ${daily.best_time} erreicht!` :
`Congratulations! You achieved the best time of the day with ${daily.best_time}!`;
showWebNotification(title, message, '👑');
// Mark as sent in database
await fetch('/api/v1/public/notification-sent', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
playerId: currentPlayerId,
notificationType: 'daily_best'
})
});
console.log('🏆 Daily best notification sent');
}
}
// Check weekly best time (only on Sunday evening, only once per week)
if (weekly && weekly.player_id === currentPlayerId) {
const isSunday = now.getDay() === 0; // 0 = Sunday
if (isSunday && isEvening) {
// Check if notification was already sent this week
const weeklyCheck = await fetch(`/api/v1/public/notification-sent/${currentPlayerId}/weekly_best`);
const weeklyResult = await weeklyCheck.json();
if (!weeklyResult.wasSent) {
const title = currentLanguage === 'de' ? '🏆 Wochenchampion!' : '🏆 Weekly Champion!';
const message = currentLanguage === 'de' ?
`Fantastisch! Du bist der Wochenchampion mit ${weekly.best_time}!` :
`Fantastic! You are the weekly champion with ${weekly.best_time}!`;
showWebNotification(title, message, '🏆');
// Mark as sent in database
await fetch('/api/v1/public/notification-sent', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
playerId: currentPlayerId,
notificationType: 'weekly_best'
})
});
console.log('🏆 Weekly best notification sent');
}
}
}
// Check monthly best time (only on last evening of month at 19:00, only once per month)
if (monthly && monthly.player_id === currentPlayerId) {
const lastDayOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0);
const isLastDayOfMonth = now.getDate() === lastDayOfMonth.getDate();
// Only show monthly notification on last day of month at 19:00 or later
if (isLastDayOfMonth && isEvening) {
// Check if notification was already sent this month
const monthlyCheck = await fetch(`/api/v1/public/notification-sent/${currentPlayerId}/monthly_best`);
const monthlyResult = await monthlyCheck.json();
if (!monthlyResult.wasSent) {
const title = currentLanguage === 'de' ? '🏆 Monatsmeister!' : '🏆 Monthly Master!';
const message = currentLanguage === 'de' ?
`Unglaublich! Du bist der Monatsmeister mit ${monthly.best_time}!` :
`Incredible! You are the monthly master with ${monthly.best_time}!`;
showWebNotification(title, message, '🥇');
// Mark as sent in database
await fetch('/api/v1/public/notification-sent', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
playerId: currentPlayerId,
notificationType: 'monthly_best'
})
});
console.log('🏆 Monthly best notification sent');
}
}
}
}
}
} catch (error) {
console.error('Error checking best time notifications:', error);
}
}
// Check for new achievements and show notifications
async function checkAchievementNotifications() {
try {
if (!currentPlayerId) return;
// Check if push notifications are enabled
const pushPlayerId = localStorage.getItem('pushPlayerId');
if (!pushPlayerId) {
console.log('🔕 Push notifications disabled, skipping achievement check');
return;
}
const response = await fetch(`/api/achievements/player/${currentPlayerId}?t=${Date.now()}`);
const result = await response.json();
if (result.success && result.data) {
const newAchievements = result.data.filter(achievement => {
// Check if achievement was earned in the last 5 minutes
const earnedAt = new Date(achievement.earned_at);
const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000);
return earnedAt > fiveMinutesAgo;
});
if (newAchievements.length > 0) {
for (const achievement of newAchievements) {
// Check if notification was already sent for this achievement
const achievementCheck = await fetch(`/api/v1/public/notification-sent/${currentPlayerId}/achievement?achievementId=${achievement.achievement_id}&locationId=${achievement.location_id || ''}`);
const achievementResult = await achievementCheck.json();
if (!achievementResult.wasSent) {
const translatedAchievement = translateAchievement(achievement);
showWebNotification(
`🏆 ${translatedAchievement.name}`,
translatedAchievement.description,
achievement.icon || '🏆'
);
// Mark as sent in database
await fetch('/api/v1/public/notification-sent', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
playerId: currentPlayerId,
notificationType: 'achievement',
achievementId: achievement.achievement_id,
locationId: achievement.location_id || null
})
});
console.log(`🏆 Achievement notification sent: ${achievement.name}`);
}
}
}
}
} catch (error) {
console.error('Error checking achievement notifications:', error);
}
}
// Periodic check for notifications (every 30 seconds)
setInterval(() => {
checkBestTimeNotifications();
checkAchievementNotifications();
}, 30000);
// Settings Functions
function showSettings() {
const modal = document.getElementById('settingsModal');
if (modal) {
modal.style.display = 'block';
loadSettings();
}
}
async function loadSettings() {
try {
if (!currentUser || !currentUser.id) {
console.error('No user ID available');
return;
}
// Load current player settings using user ID
const response = await fetch(`/api/v1/public/user-player/${currentUser.id}`);
const result = await response.json();
if (result.success && result.data) {
const showInLeaderboard = result.data.show_in_leaderboard || false;
document.getElementById('showInLeaderboard').checked = showInLeaderboard;
console.log('Loaded settings - showInLeaderboard:', showInLeaderboard);
} else {
console.error('Failed to load player settings:', result.message);
// Set default to false if loading fails
document.getElementById('showInLeaderboard').checked = false;
}
} catch (error) {
console.error('Error loading settings:', error);
// Set default to false if loading fails
document.getElementById('showInLeaderboard').checked = false;
}
}
function updateLeaderboardSetting() {
const checkbox = document.getElementById('showInLeaderboard');
console.log('Leaderboard setting changed:', checkbox.checked);
}
async function saveSettings() {
try {
if (!currentPlayerId) {
console.error('No player ID available');
return;
}
const showInLeaderboard = document.getElementById('showInLeaderboard').checked;
// Update player settings using public endpoint (no API key needed)
const response = await fetch(`/api/v1/public/update-player-settings`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
player_id: currentPlayerId,
show_in_leaderboard: showInLeaderboard
})
});
const result = await response.json();
if (result.success) {
const successMsg = currentLanguage === 'de' ?
'Einstellungen erfolgreich gespeichert!' :
'Settings saved successfully!';
showNotification(successMsg, 'success');
closeModal('settingsModal');
} else {
const errorMsg = currentLanguage === 'de' ?
'Fehler beim Speichern der Einstellungen: ' + result.message :
'Error saving settings: ' + result.message;
showNotification(errorMsg, 'error');
}
} catch (error) {
console.error('Error saving settings:', error);
const errorMsg = currentLanguage === 'de' ?
'Fehler beim Speichern der Einstellungen' :
'Error saving settings';
showNotification(errorMsg, 'error');
}
}
function showNotification(message, type = 'info') {
// Create notification element
const notification = document.createElement('div');
notification.className = `notification notification-${type}`;
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: ${type === 'success' ? '#10b981' : type === 'error' ? '#ef4444' : '#3b82f6'};
color: white;
padding: 1rem 1.5rem;
border-radius: 0.5rem;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
z-index: 10000;
max-width: 300px;
font-weight: 500;
`;
notification.textContent = message;
document.body.appendChild(notification);
// Remove notification after 3 seconds
setTimeout(() => {
if (notification.parentNode) {
notification.parentNode.removeChild(notification);
}
}, 3000);
}
document.addEventListener('DOMContentLoaded', function () {
// Add cookie settings button functionality
const cookieSettingsBtn = document.getElementById('cookie-settings-footer');
if (cookieSettingsBtn) {
cookieSettingsBtn.addEventListener('click', function () {
if (window.cookieConsent) {
window.cookieConsent.resetConsent();
}
});
}
// Check push notification status on dashboard load
if (typeof checkPushStatus === 'function') {
checkPushStatus();
}
});