(function() { 'use strict'; const token = localStorage.getItem('auth_token'); if (!token) { window.location.href = '/'; return; } function decodeJwt(token) { try { return JSON.parse(atob(token.split('.')[1])); } catch { return null; } } const decoded = decodeJwt(token); if (!decoded) { localStorage.removeItem('auth_token'); window.location.href = '/'; return; } // Check if admin and show admin tabs if (decoded.admin) { const adminTabWinners = document.getElementById('tab-winners'); if (adminTabWinners) adminTabWinners.classList.remove('d-none'); } // Logout const logOffBtn = document.getElementById('log-off'); if (logOffBtn) { logOffBtn.addEventListener('click', () => { localStorage.removeItem('auth_token'); window.location.href = '/'; }); } let categories = []; let myPicks = {}; let currentYear = null; let isSaving = false; // Prevent multiple simultaneous saves async function fetchActiveYear() { try { const res = await fetch('/api/settings/year'); if (!res.ok) throw new Error('Failed to fetch active year'); const data = await res.json(); return Number(data.year) || new Date().getFullYear(); } catch (err) { console.error('Error fetching active year:', err); return new Date().getFullYear(); } } function showAlert(message, type = 'info') { const container = document.getElementById('alert-container'); if (!container) return; container.innerHTML = ` `; setTimeout(() => { const alert = container.querySelector('.alert'); if (alert) { const bsAlert = new bootstrap.Alert(alert); bsAlert.close(); } }, 5000); } async function fetchCategories() { try { // Use active year if not set if (!currentYear) { currentYear = await fetchActiveYear(); document.title = `Pool Oscars (${currentYear}) - Mes picks`; } const res = await fetch(`/api/picks/categories?year=${currentYear}`, { headers: { 'Authorization': `Bearer ${token}` } }); if (!res.ok) { if (res.status === 401) { localStorage.removeItem('auth_token'); window.location.href = '/'; return; } throw new Error('Failed to fetch categories'); } const data = await res.json(); categories = data.categories || []; currentYear = data.year || currentYear; document.getElementById('current-year').textContent = currentYear; if (categories.length === 0) { document.getElementById('categories-container').innerHTML = `
Aucune catégorie trouvée pour l'année ${currentYear}. Un administrateur doit d'abord importer les catégories.
`; return; } renderCategories(); await loadMyPicks(); } catch (err) { console.error('Error fetching categories:', err); showAlert('Erreur lors du chargement des catégories: ' + err.message, 'danger'); } } async function loadMyPicks() { try { const res = await fetch(`/api/picks/my-picks?year=${currentYear}`, { headers: { 'Authorization': `Bearer ${token}` } }); if (!res.ok && res.status !== 404) { throw new Error('Failed to load picks'); } const data = await res.json(); if (data.pick && data.pick.picks) { myPicks = {}; // Create a map of existing category numbers const existingCategoryNumbers = new Set(categories.map(c => c.categoryNumber)); // Only load picks for categories that still exist data.pick.picks.forEach(p => { if (existingCategoryNumbers.has(p.categoryNumber)) { myPicks[p.categoryNumber] = p.selectedNominee; } }); renderCategories(); updateProgress(); } } catch (err) { console.error('Error loading picks:', err); } } function renderCategories() { const container = document.getElementById('categories-container'); if (!container) return; if (categories.length === 0) { container.innerHTML = '
Aucune catégorie disponible
'; return; } container.innerHTML = `
${categories.map(cat => { const selectedNominee = myPicks[cat.categoryNumber]; return ` `; }).join('')}
# Catégorie Choix Nommés
${cat.categoryNumber} ${cat.categoryName} ${selectedNominee ? ` ${selectedNominee} ` : 'Aucun choix'}
${cat.nominees.map(nominee => { const isSelected = selectedNominee === nominee.name; return ` ${isSelected ? '✓' : ''} ${nominee.name} `; }).join('')}
`; // Add click handlers container.querySelectorAll('.nominee-option').forEach(el => { el.addEventListener('click', function() { const category = parseInt(this.dataset.category); const nominee = this.dataset.nominee; // Toggle selection if (myPicks[category] === nominee) { // Deselect if clicking the same nominee delete myPicks[category]; } else { // Select this nominee myPicks[category] = nominee; } // Re-render to update the display renderCategories(); // Auto-save after selection change autoSavePicks(); }); }); updateProgress(); } async function autoSavePicks() { // Clear any existing timeout to reset the debounce if (autoSavePicks.timeout) { clearTimeout(autoSavePicks.timeout); } // Debounce: wait for user to stop clicking before saving autoSavePicks.timeout = setTimeout(async () => { // Skip if already saving (will retry after current save completes) if (isSaving) { // Retry after a short delay setTimeout(() => autoSavePicks(), 200); return; } isSaving = true; try { await submitPicks(true); // Pass true for silent mode (no alert) } catch (err) { console.error('Auto-save failed:', err); // Show error alert only on failure showAlert('Erreur lors de l\'enregistrement automatique: ' + err.message, 'danger'); } finally { isSaving = false; } }, 250); // Reduced to 250ms for better responsiveness } function updateProgress() { const total = categories.length; const completed = Object.keys(myPicks).length; document.getElementById('picks-progress').textContent = `${completed} / ${total}`; } async function submitPicks(silent = false) { const picks = Object.keys(myPicks).map(catNum => ({ categoryNumber: parseInt(catNum), selectedNominee: myPicks[catNum] })); // Allow empty picks to be saved (to clear all selections) // Only show warning in non-silent mode if user explicitly tries to submit with no picks if (picks.length === 0 && !silent) { // Don't block - allow saving empty picks to clear selections } // Skip confirmation dialog in silent mode (auto-save) if (!silent && picks.length < categories.length) { if (!confirm(`Vous n'avez sélectionné que ${picks.length} sur ${categories.length} catégories. Voulez-vous enregistrer quand même?`)) { return; } } try { const res = await fetch('/api/picks/submit', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify({ year: currentYear, picks }) }); if (!res.ok) { const error = await res.json(); throw new Error(error.message || 'Failed to submit picks'); } const data = await res.json(); if (!silent) { showAlert('Vos choix ont été enregistrés avec succès!', 'success'); } } catch (err) { console.error('Error submitting picks:', err); showAlert('Erreur lors de l\'enregistrement: ' + err.message, 'danger'); } } // Event listeners document.getElementById('load-my-picks')?.addEventListener('click', () => { loadMyPicks(); showAlert('Choix rechargés', 'info'); }); // Winners & Scores functionality (admin only) let winnersCategories = []; async function fetchWinnersCategories() { try { if (!currentYear) { currentYear = await fetchActiveYear(); } const res = await fetch(`/api/picks/categories?year=${currentYear}`, { headers: { 'Authorization': `Bearer ${token}` } }); if (!res.ok) { throw new Error('Failed to fetch categories'); } const data = await res.json(); winnersCategories = data.categories || []; currentYear = data.year || currentYear; const yearEl = document.getElementById('winners-year'); if (yearEl) yearEl.textContent = currentYear; renderWinnersCategories(); await fetchScores(); } catch (err) { console.error('Error fetching winners categories:', err); showAlert('Erreur lors du chargement des catégories: ' + err.message, 'danger'); } } function renderWinnersCategories() { const container = document.getElementById('winners-container'); if (!container) return; if (winnersCategories.length === 0) { container.innerHTML = '
Aucune catégorie trouvée pour cette année.
'; return; } const winnersCount = winnersCategories.filter(cat => cat.nominees.some(n => n.isWinner) ).length; const progressEl = document.getElementById('winners-progress'); if (progressEl) progressEl.textContent = `${winnersCount} / ${winnersCategories.length}`; container.innerHTML = `
${winnersCategories.map(cat => { const winner = cat.nominees.find(n => n.isWinner); return ` `; }).join('')}
# Catégorie Sélection Gagnant
${cat.categoryNumber} ${cat.categoryName} ${winner ? `${winner.name}` : '—'}
`; // Add event listeners for winner selection container.querySelectorAll('.winner-select').forEach(select => { select.addEventListener('change', async function() { const categoryId = this.dataset.categoryId; const winnerName = this.value; try { // If empty value, clear the winner (set to null/empty) const res = await fetch(`/api/categories/${categoryId}/winner`, { method: 'PUT', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify({ winnerName: winnerName || null }) }); if (!res.ok) { const error = await res.json(); throw new Error(error.message || 'Failed to update winner'); } showAlert(winnerName ? 'Gagnant marqué avec succès' : 'Gagnant supprimé', 'success'); await fetchWinnersCategories(); // Auto-refresh scores after marking/clearing a winner (silent mode) await calculateScores(true); } catch (err) { console.error('Error updating winner:', err); showAlert('Erreur: ' + err.message, 'danger'); } }); }); } async function calculateScores(silent = false) { try { if (!currentYear) { currentYear = await fetchActiveYear(); } const btn = document.getElementById('calculate-scores'); if (btn) { btn.disabled = true; btn.textContent = 'Calcul en cours...'; } const res = await fetch('/api/picks/calculate-scores', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify({ year: currentYear }) }); if (!res.ok) { const error = await res.json(); // If no winners marked, handle gracefully (especially for auto-refresh) if (error.message && error.message.includes('No winners marked')) { if (!silent) { // Only show as info/warning, not error showAlert('Aucun gagnant marqué. Les scores seront calculés une fois les gagnants définis.', 'info'); } // Still try to fetch existing scores await fetchScores(); return; } throw new Error(error.message || 'Failed to calculate scores'); } const data = await res.json(); if (!silent) { showAlert(`Scores calculés avec succès! ${data.scores.length} utilisateur(s) évalué(s).`, 'success'); } await fetchScores(); } catch (err) { console.error('Error calculating scores:', err); if (!silent) { showAlert('Erreur: ' + err.message, 'danger'); } } finally { const btn = document.getElementById('calculate-scores'); if (btn) { btn.disabled = false; btn.textContent = 'Calculer les scores'; } } } async function fetchScores() { try { if (!currentYear) { currentYear = await fetchActiveYear(); } // Update scores year display const scoresYearEl = document.getElementById('scores-year'); if (scoresYearEl) { scoresYearEl.textContent = currentYear; } const res = await fetch(`/api/picks/scores?year=${currentYear}`, { headers: { 'Authorization': `Bearer ${token}` } }); if (!res.ok) { const errorData = await res.json().catch(() => ({ message: 'Failed to fetch scores' })); throw new Error(errorData.message || 'Failed to fetch scores'); } const data = await res.json(); renderScores(data.scores || [], data.totalCategories || 0); } catch (err) { console.error('Error fetching scores:', err); const container = document.getElementById('scores-container'); if (container) { container.innerHTML = '
Erreur lors du chargement des scores.
'; } } } function renderScores(scores, totalCategories) { const container = document.getElementById('scores-container'); if (!container) return; // Filter out scores for removed users (Unknown users or null userId) let validScores = scores.filter(score => score.userName && score.userName !== 'Unknown' && score.userId ); // Calculate actual scores and sort by them validScores = validScores.map(score => { // Always calculate actual score from pickDetails if available (more accurate than stored score) let actualScore; if (score.pickDetails && Array.isArray(score.pickDetails) && score.pickDetails.length > 0) { // Calculate from pickDetails - this is the source of truth actualScore = score.pickDetails.filter(p => p.isCorrect === true).length; } else { // Fallback to stored score only if pickDetails not available actualScore = score.score || 0; } return { ...score, actualScore }; }).sort((a, b) => b.actualScore - a.actualScore); if (validScores.length === 0) { container.innerHTML = '
Aucun score disponible. Les utilisateurs doivent d\'abord soumettre leurs choix.
'; return; } container.innerHTML = `
${validScores.map((score, index) => { // Use the pre-calculated actualScore (always use it, even if 0) const actualScore = score.actualScore !== undefined ? score.actualScore : (score.score || 0); const rank = index + 1; const medal = rank === 1 ? '🥇' : rank === 2 ? '🥈' : rank === 3 ? '🥉' : ''; return ` `; }).join('')}
# Utilisateur Score Total Actions
${rank}${medal ? ' ' + medal : ''} ${score.userName} ${actualScore} ${totalCategories}
`; // Store valid scores data globally for modal access window.scoresData = validScores; // Add event listeners for view picks buttons container.querySelectorAll('.view-picks-btn').forEach(btn => { btn.addEventListener('click', function() { const scoreIndex = parseInt(this.dataset.scoreIndex); const score = validScores[scoreIndex]; showUserPicksModal(score); }); }); } function showUserPicksModal(score) { const modal = new bootstrap.Modal(document.getElementById('userPicksModal')); const modalTitle = document.getElementById('userPicksModalLabel'); const modalContent = document.getElementById('user-picks-modal-content'); modalTitle.textContent = `Choix de ${score.userName}`; if (!score.pickDetails || score.pickDetails.length === 0) { modalContent.innerHTML = '
Aucun détail disponible.
'; modal.show(); return; } // Separate correct and incorrect picks const correctPicks = score.pickDetails.filter(p => p.isCorrect); const incorrectPicks = score.pickDetails.filter(p => !p.isCorrect); // Calculate actual score from pickDetails (more accurate than stored score) const actualScore = correctPicks.length; const totalCategories = score.totalCategories || score.pickDetails.length; modalContent.innerHTML = `
Résumé
${correctPicks.length} correct(s) | ${incorrectPicks.length} incorrect(s) | ${score.pickDetails.length} total
${actualScore} / ${totalCategories}
${totalCategories > 0 ? Math.round((actualScore / totalCategories) * 100) : 0}%
${correctPicks.length > 0 ? `
✓ Corrects (${correctPicks.length})
${correctPicks.map(pick => `
${pick.categoryNumber}. ${pick.categoryName}
✓ ${pick.selectedNominee}
`).join('')}
` : ''} ${incorrectPicks.length > 0 ? `
✗ Incorrects (${incorrectPicks.length})
${incorrectPicks.map(pick => `
${pick.categoryNumber}. ${pick.categoryName}
✗ Choix: ${pick.selectedNominee}
${pick.correctWinner ? `
✓ Gagnant: ${pick.correctWinner}
` : '
Aucun gagnant marqué
'}
`).join('')}
` : ''} ${correctPicks.length === 0 && incorrectPicks.length === 0 ? `
Aucun choix à afficher.
` : ''} `; modal.show(); } // Event listeners for winners tab document.getElementById('refresh-winners')?.addEventListener('click', () => { fetchWinnersCategories(); showAlert('Données rafraîchies', 'info'); }); // Load scores when scores tab is shown const scoresTab = document.getElementById('tab-scores'); if (scoresTab) { scoresTab.addEventListener('shown.bs.tab', async () => { await fetchScores(); }); } // Load winners when admin tab is shown const winnersTab = document.getElementById('tab-winners'); if (winnersTab) { winnersTab.addEventListener('shown.bs.tab', async () => { await fetchWinnersCategories(); // Auto-refresh scores when entering the winners tab (silent mode) await calculateScores(true); }); } // Refresh scores button document.getElementById('refresh-scores')?.addEventListener('click', async () => { await fetchScores(); showAlert('Scores rafraîchis', 'info'); }); // Initial load (async () => { currentYear = await fetchActiveYear(); document.title = `Pool Oscars (${currentYear}) - Mes choix`; await fetchCategories(); // Auto-refresh scores on page load if scores tab is active const scoresTab = document.getElementById('tab-scores'); const scoresPane = document.getElementById('pane-scores'); if (scoresTab && scoresPane && (scoresTab.classList.contains('active') || scoresPane.classList.contains('active'))) { await fetchScores(); } // Auto-refresh winners on page load if admin and winners tab is active if (decoded.admin) { const winnersTab = document.getElementById('tab-winners'); const winnersPane = document.getElementById('pane-winners'); // Check if winners tab is active (either by default or if it becomes active) if (winnersTab && winnersPane && (winnersTab.classList.contains('active') || winnersPane.classList.contains('active'))) { await fetchWinnersCategories(); await calculateScores(true); // Silent mode for auto-refresh } } })(); })();