${isGerman ? 'Detaillierte Statistiken zu deinen Läufen - beste Zeiten, Verbesserungen und Vergleiche.' : 'Detailed statistics about your runs - best times, improvements and comparisons.'}
`;
}
}
}
// 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();
});
// 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 = `
${isGerman ? '🏷️ RFID Verknüpft' : '🏷️ RFID Linked'}
`;
// 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 = `
${noTimesTitle}
${noTimesDescription}
`;
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 `
`).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 = `
${notLinkedMessage}
`;
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 = `