diff --git a/public/checklist.html b/public/checklist.html index 4eb1a56..80f8993 100644 --- a/public/checklist.html +++ b/public/checklist.html @@ -72,7 +72,7 @@ Films Checklist Statistiques - Mes choix + Admin diff --git a/public/control.html b/public/control.html index d7da70d..d9f64b3 100644 --- a/public/control.html +++ b/public/control.html @@ -43,7 +43,7 @@ Films Checklist Statistiques - Mes choix + Admin
@@ -136,6 +136,11 @@ Points +
@@ -245,6 +250,32 @@
+ + +
+
+
+ Contrôle la visibilité de certains éléments dans l'interface. +
+ + + +
+ + +
Contrôle l'affichage du bouton "Mes picks" dans la navigation et de la colonne "Bon picks" dans les statistiques.
+
+ +
+
+ + +
+
+
+
diff --git a/public/js/app_control.js b/public/js/app_control.js index 449f35f..fd37b69 100644 --- a/public/js/app_control.js +++ b/public/js/app_control.js @@ -2676,6 +2676,90 @@ window.onload = async function () { await loadPointsConfig(); + // Visibility config + const visibilityShowPicksFeaturesEl = document.getElementById('visibility-show-picks-features'); + const visibilityConfigReloadBtn = document.getElementById('visibility-config-reload'); + const visibilityConfigSaveBtn = document.getElementById('visibility-config-save'); + const visibilityConfigAlert = document.getElementById('visibility-config-alert'); + + async function loadVisibilityConfig() { + try { + const res = await fetch('/api/settings/visibility-config', { cache: 'no-store' }); + if (!res.ok) throw new Error('Failed to load visibility config'); + const config = await res.json(); + + // Both settings should be the same - use showPicksButton as the master + const isEnabled = config.showPicksButton !== false && config.showBonPicksColumn !== false; + if (visibilityShowPicksFeaturesEl) visibilityShowPicksFeaturesEl.checked = isEnabled; + } catch (err) { + console.error('Error loading visibility config:', err); + // Default to showing both + if (visibilityShowPicksFeaturesEl) visibilityShowPicksFeaturesEl.checked = true; + } + } + + if (visibilityConfigReloadBtn) { + visibilityConfigReloadBtn.addEventListener('click', async () => { + await loadVisibilityConfig(); + showResponse('success', 'Configuration rechargée'); + }); + } + + if (visibilityConfigSaveBtn) { + visibilityConfigSaveBtn.addEventListener('click', async () => { + const isEnabled = visibilityShowPicksFeaturesEl ? visibilityShowPicksFeaturesEl.checked : true; + const showPicksButton = isEnabled; + const showBonPicksColumn = isEnabled; + + try { + visibilityConfigSaveBtn.disabled = true; + const oldText = visibilityConfigSaveBtn.textContent; + visibilityConfigSaveBtn.textContent = 'Enregistrement...'; + + const res = await fetch('/api/settings/visibility-config', { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + }, + body: JSON.stringify({ + showPicksButton, + showBonPicksColumn + }) + }); + + if (!res.ok) { + const error = await res.json(); + throw new Error(error.message || 'Failed to save'); + } + + const data = await res.json(); + + // Clear cache so all pages get the new setting immediately + localStorage.removeItem('visibility_config_cache'); + + showResponse('success', 'Configuration de visibilité enregistrée'); + } catch (err) { + if (visibilityConfigAlert) { + visibilityConfigAlert.className = 'alert alert-danger'; + visibilityConfigAlert.textContent = 'Erreur: ' + err.message; + visibilityConfigAlert.classList.remove('d-none'); + } + } finally { + visibilityConfigSaveBtn.disabled = false; + visibilityConfigSaveBtn.textContent = oldText; + } + }); + } + + // Load visibility config when visibility tab is shown + const visibilitySubTab = document.getElementById('admin-settings-subtab-visibility'); + if (visibilitySubTab) { + visibilitySubTab.addEventListener('shown.bs.tab', async () => { + await loadVisibilityConfig(); + }); + } + // Categories management const importPdfForm = document.getElementById('import-pdf-form'); const createCategoryForm = document.getElementById('create-category-form'); diff --git a/public/js/app_header_boot.js b/public/js/app_header_boot.js index e4381a4..47c49f2 100644 --- a/public/js/app_header_boot.js +++ b/public/js/app_header_boot.js @@ -50,5 +50,59 @@ if (adminLink && decoded.admin) { adminLink.classList.remove('d-none'); } + + // Check visibility config for picks button + const picksButton = document.getElementById('nav-link-picks'); + if (picksButton) { + // Check cached value first for immediate display + const cachedConfig = localStorage.getItem('visibility_config_cache'); + let cachedValue = null; + let cachedTimestamp = null; + + if (cachedConfig) { + try { + const parsed = JSON.parse(cachedConfig); + cachedValue = parsed.value; + cachedTimestamp = parsed.timestamp; + } catch (e) { + // Invalid cache, ignore + } + } + + // Use cached value if it's less than 5 minutes old + const cacheAge = cachedTimestamp ? Date.now() - cachedTimestamp : Infinity; + const useCache = cachedValue !== null && cacheAge < 5 * 60 * 1000; + + if (useCache) { + // Show button immediately based on cached value + if (cachedValue.showPicksButton !== false) { + picksButton.style.display = ''; + } + } else { + // Default to showing while we fetch + picksButton.style.display = ''; + } + + // Fetch fresh value from server + fetch('/api/settings/visibility-config', { cache: 'no-store' }) + .then(res => res.json()) + .then(config => { + // Cache the result + localStorage.setItem('visibility_config_cache', JSON.stringify({ + value: config, + timestamp: Date.now() + })); + + // Update display based on fresh value + if (config.showPicksButton !== false) { + picksButton.style.display = ''; + } else { + picksButton.style.display = 'none'; + } + }) + .catch(() => { + // On error, keep current state (already set from cache or default) + }); + } })(); diff --git a/public/js/app_picks.js b/public/js/app_picks.js index 6769561..abfc4c9 100644 --- a/public/js/app_picks.js +++ b/public/js/app_picks.js @@ -24,8 +24,6 @@ // Check if admin and show admin tabs if (decoded.admin) { - const adminTabAllPicks = document.getElementById('tab-all-picks'); - if (adminTabAllPicks) adminTabAllPicks.classList.remove('d-none'); const adminTabWinners = document.getElementById('tab-winners'); if (adminTabWinners) adminTabWinners.classList.remove('d-none'); } @@ -80,7 +78,7 @@ // Use active year if not set if (!currentYear) { currentYear = await fetchActiveYear(); - document.title = `Pool Oscars (${currentYear}) - Mes choix`; + document.title = `Pool Oscars (${currentYear}) - Mes picks`; } const res = await fetch(`/api/picks/categories?year=${currentYear}`, { @@ -101,9 +99,6 @@ currentYear = data.year || currentYear; document.getElementById('current-year').textContent = currentYear; - if (document.getElementById('all-picks-year')) { - document.getElementById('all-picks-year').textContent = currentYear; - } if (categories.length === 0) { document.getElementById('categories-container').innerHTML = ` @@ -156,29 +151,49 @@ return; } - container.innerHTML = categories.map(cat => ` -
-
-
${cat.categoryNumber}. ${cat.categoryName}
-
-
-
- ${cat.nominees.map(nominee => { - const isSelected = myPicks[cat.categoryNumber] === nominee.name; + container.innerHTML = ` +
+ + + + + + + + + + + ${categories.map(cat => { + const selectedNominee = myPicks[cat.categoryNumber]; return ` -
-
- ${nominee.name} -
-
+ + + + + + `; }).join('')} - - + +
#CatégorieChoixNommés
${cat.categoryNumber}${cat.categoryName} + ${selectedNominee ? `${selectedNominee}` : '—'} + +
+ ${cat.nominees.map(nominee => { + const isSelected = selectedNominee === nominee.name; + return ` + + ${nominee.name} + + `; + }).join('')} +
+
- `).join(''); + `; // Add click handlers container.querySelectorAll('.nominee-option').forEach(el => { @@ -186,15 +201,17 @@ const category = parseInt(this.dataset.category); const nominee = this.dataset.nominee; - // Remove selection from other options in same category - container.querySelectorAll(`[data-category="${category}"]`).forEach(opt => { - opt.classList.remove('selected'); - }); + // Toggle selection + if (myPicks[category] === nominee) { + // Deselect if clicking the same nominee + delete myPicks[category]; + } else { + // Select this nominee + myPicks[category] = nominee; + } - // Select this option - this.classList.add('selected'); - myPicks[category] = nominee; - updateProgress(); + // Re-render to update the display + renderCategories(); }); }); @@ -250,144 +267,12 @@ } } - // All picks functionality (admin only) - let allPicks = []; - let allPicksCategories = []; - - async function fetchAllPicks() { - try { - // Use active year if not set - if (!currentYear) { - currentYear = await fetchActiveYear(); - } - - const res = await fetch(`/api/picks/all?year=${currentYear}`, { - headers: { 'Authorization': `Bearer ${token}` } - }); - - if (!res.ok) { - if (res.status === 401) { - localStorage.removeItem('auth_token'); - window.location.href = '/'; - return; - } - if (res.status === 403) { - showAlert('Vous n\'avez pas les privilèges administrateur', 'warning'); - return; - } - throw new Error('Failed to fetch picks'); - } - - const data = await res.json(); - allPicks = data.picks || []; - allPicksCategories = data.categories || []; - currentYear = data.year || currentYear; - - const yearEl = document.getElementById('all-picks-year'); - if (yearEl) yearEl.textContent = currentYear; - - const countEl = document.getElementById('users-count'); - if (countEl) countEl.textContent = allPicks.length; - - renderAllPicks(); - } catch (err) { - console.error('Error fetching all picks:', err); - showAlert('Erreur lors du chargement des choix: ' + err.message, 'danger'); - } - } - - function renderAllPicks() { - const container = document.getElementById('all-picks-container'); - if (!container) return; - - if (allPicks.length === 0) { - container.innerHTML = ` -
- Aucun utilisateur n'a encore soumis ses choix pour l'année ${currentYear}. -
- `; - return; - } - - // Create a map of category info - const categoryMap = new Map(); - allPicksCategories.forEach(cat => { - categoryMap.set(cat.categoryNumber, cat); - }); - - container.innerHTML = allPicks.map(pick => { - const userName = pick.userId?.name || 'Utilisateur inconnu'; - const userEmail = pick.userId?.email || ''; - const submittedDate = pick.submittedAt ? new Date(pick.submittedAt).toLocaleString('fr-FR') : '—'; - - // Create a map of picks by category - const picksMap = new Map(); - pick.picks.forEach(p => { - picksMap.set(p.categoryNumber, p.selectedNominee); - }); - - // Build picks list - const picksList = allPicksCategories.map(cat => { - const selected = picksMap.get(cat.categoryNumber); - if (!selected) return null; - - // Check if it's a winner (if winners are set) - const nominee = cat.nominees.find(n => n.name === selected); - const isWinner = nominee?.isWinner || false; - - return ` -
-
-
- ${cat.categoryNumber}. ${cat.categoryName}
- ${selected} -
- ${isWinner ? 'Gagnant' : ''} -
-
- `; - }).filter(Boolean).join(''); - - return ` -
-
-
-
-
${userName}
- ${userEmail} -
-
- Soumis le: ${submittedDate}
- ${pick.picks.length} / ${allPicksCategories.length} catégories -
-
-
-
- ${picksList || '
Aucun choix enregistré
'} -
-
- `; - }).join(''); - } - // Event listeners document.getElementById('submit-picks')?.addEventListener('click', submitPicks); document.getElementById('load-my-picks')?.addEventListener('click', () => { loadMyPicks(); showAlert('Choix rechargés', 'info'); }); - document.getElementById('refresh-all-picks')?.addEventListener('click', () => { - fetchAllPicks(); - showAlert('Choix rafraîchis', 'info'); - }); - - // Load all picks when admin tab is shown - const allPicksTab = document.getElementById('tab-all-picks'); - if (allPicksTab) { - allPicksTab.addEventListener('shown.bs.tab', () => { - fetchAllPicks(); - }); - } // Winners & Scores functionality (admin only) let winnersCategories = []; @@ -437,28 +322,42 @@ 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 ` -
-
-
${cat.categoryNumber}. ${cat.categoryName}
-
-
-
- - -
- ${winner ? `
Gagnant actuel: ${winner.name}
` : ''} -
-
- `; - }).join(''); + container.innerHTML = ` +
+ + + + + + + + + + + ${winnersCategories.map(cat => { + const winner = cat.nominees.find(n => n.isWinner); + return ` + + + + + + + `; + }).join('')} + +
#CatégorieSélectionGagnant
${cat.categoryNumber}${cat.categoryName} + + + ${winner ? `${winner.name}` : '—'} +
+
+ `; // Add event listeners for winner selection container.querySelectorAll('.winner-select').forEach(select => { @@ -466,34 +365,35 @@ const categoryId = this.dataset.categoryId; const winnerName = this.value; - if (!winnerName) return; - 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 }) + body: JSON.stringify({ winnerName: winnerName || null }) }); if (!res.ok) { const error = await res.json(); - throw new Error(error.message || 'Failed to mark winner'); + throw new Error(error.message || 'Failed to update winner'); } - showAlert('Gagnant marqué avec succès', 'success'); + 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 marking winner:', err); + console.error('Error updating winner:', err); showAlert('Erreur: ' + err.message, 'danger'); } }); }); } - async function calculateScores() { + async function calculateScores(silent = false) { try { if (!currentYear) { currentYear = await fetchActiveYear(); @@ -516,15 +416,29 @@ 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(); - showAlert(`Scores calculés avec succès! ${data.scores.length} utilisateur(s) évalué(s).`, 'success'); + 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); - showAlert('Erreur: ' + err.message, 'danger'); + if (!silent) { + showAlert('Erreur: ' + err.message, 'danger'); + } } finally { const btn = document.getElementById('calculate-scores'); if (btn) { @@ -540,21 +454,29 @@ 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) { - if (res.status === 403) { - return; // Not admin, skip - } - throw new Error('Failed to fetch scores'); + 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.
'; + } } } @@ -569,15 +491,15 @@ container.innerHTML = `
- - +
+ - + - - - - + + + + @@ -588,13 +510,13 @@ return ` - + - + @@ -723,19 +645,53 @@ 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', () => { - fetchWinnersCategories(); + 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 + } + } })(); })(); diff --git a/public/js/app_user-stat.js b/public/js/app_user-stat.js index 7ce213c..44a9a27 100644 --- a/public/js/app_user-stat.js +++ b/public/js/app_user-stat.js @@ -649,6 +649,24 @@ window.addEventListener('DOMContentLoaded', async function () { pageLoader.setProgress(70); const stats = await statsRes.json(); const userTableBody = document.getElementById('user-table-body'); + + // Check visibility config for Bon picks column + let showBonPicksColumn = true; + try { + const visibilityRes = await fetch('/api/settings/visibility-config', { cache: 'no-store' }); + if (visibilityRes.ok) { + const visibilityConfig = await visibilityRes.json(); + showBonPicksColumn = visibilityConfig.showBonPicksColumn !== false; + } + } catch (err) { + console.error('Error loading visibility config:', err); + } + + // Hide/show Bon picks header + const bonPicksHeader = document.getElementById('bon-picks-header'); + if (bonPicksHeader) { + bonPicksHeader.style.display = showBonPicksColumn ? '' : 'none'; + } // Sort by total points (descending), then by watched ratio stats.sort((a, b) => { @@ -686,6 +704,9 @@ window.addEventListener('DOMContentLoaded', async function () { const picksTd = document.createElement('td'); const pickPointsConfig = userStat.pointsConfig || { pointsPerCorrectPick: 1 }; picksTd.innerHTML = `${userStat.correctPicks || 0} (${userStat.pickPoints || 0} pts)`; + if (!showBonPicksColumn) { + picksTd.style.display = 'none'; + } const pointsTd = document.createElement('td'); pointsTd.className = 'text-center'; diff --git a/public/movies.html b/public/movies.html index 29853d0..a266569 100644 --- a/public/movies.html +++ b/public/movies.html @@ -59,7 +59,7 @@ FilmsChecklistStatistiques - Mes choix + Admin diff --git a/public/picks.html b/public/picks.html index 3aa4a75..cb0eb2c 100644 --- a/public/picks.html +++ b/public/picks.html @@ -3,7 +3,7 @@ - Pool Oscars - Mes choix + Pool Oscars - Mes picks @@ -26,22 +26,21 @@ .navbar-toggler-icon { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3E%3Cpath stroke='white' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3E%3C/svg%3E"); } - .category-card { - margin-bottom: 1.5rem; - } .nominee-option { cursor: pointer; - transition: background-color 0.2s; + transition: all 0.2s; } .nominee-option:hover { - background-color: #f8f9fa; + opacity: 0.8; + transform: scale(1.05); } - .nominee-option.selected { - background-color: #0d6efd; - color: white; + .table-sm td { + padding: 0.5rem; + vertical-align: middle; } - .nominee-option.selected:hover { - background-color: #0b5ed7; + .table-sm th { + padding: 0.5rem; + font-size: 0.875rem; } .user-picks-card { margin-bottom: 1.5rem; @@ -76,7 +75,7 @@ FilmsChecklistStatistiques - Mes choix + Admin
@@ -88,7 +87,7 @@
-

Mes choix Oscars

+

Mes picks Oscars

@@ -96,18 +95,17 @@
- + diff --git a/public/view-picks.html b/public/view-picks.html index b21cb41..dc8b213 100644 --- a/public/view-picks.html +++ b/public/view-picks.html @@ -59,7 +59,7 @@ FilmsChecklistStatistiques - Mes choix + AdminVoir Tous les Choix diff --git a/routes/picks.js b/routes/picks.js index 0c58e8d..8b71bd9 100644 --- a/routes/picks.js +++ b/routes/picks.js @@ -248,13 +248,9 @@ router.post("/calculate-scores", verifyToken, async (req, res) => { } }); -// Get scores for all users (admin only) +// Get scores for all users (available to all authenticated users) router.get("/scores", verifyToken, async (req, res) => { try { - if (!isAdmin(req.user)) { - return res.status(403).json({ message: "You do not have admin privileges" }); - } - const year = parseOscarYear(req.query.year) || await getOrInitActiveYear(); const picks = await OscarPick.find({ year }) diff --git a/routes/settings.js b/routes/settings.js index 30ef9e5..e7b7024 100644 --- a/routes/settings.js +++ b/routes/settings.js @@ -15,6 +15,7 @@ const OSCAR_DATES_BY_YEAR_KEY = "oscar_date_by_year"; const APP_VERSION_KEY = "app_version_control"; const PLAYER_ADMIN_STATUS_UI_KEY = "player_admin_status_ui"; const POINTS_CONFIG_KEY = "points_config"; +const VISIBILITY_CONFIG_KEY = "visibility_config"; const crypto = require("crypto"); @@ -36,6 +37,11 @@ const DEFAULT_POINTS_CONFIG = Object.freeze({ pointsPerCorrectPick: 1, }); +const DEFAULT_VISIBILITY_CONFIG = Object.freeze({ + showPicksButton: true, + showBonPicksColumn: true, +}); + function parseOscarYear(raw) { if (raw === undefined || raw === null || raw === "") return null; const n = Number(raw); @@ -762,6 +768,13 @@ function normalizePointsConfig(rawValue) { return { pointsPerMovie, pointsPerCorrectPick }; } +function normalizeVisibilityConfig(rawValue) { + const v = rawValue && typeof rawValue === "object" && !Array.isArray(rawValue) ? rawValue : {}; + const showPicksButton = typeof v.showPicksButton === "boolean" ? v.showPicksButton : DEFAULT_VISIBILITY_CONFIG.showPicksButton; + const showBonPicksColumn = typeof v.showBonPicksColumn === "boolean" ? v.showBonPicksColumn : DEFAULT_VISIBILITY_CONFIG.showBonPicksColumn; + return { showPicksButton, showBonPicksColumn }; +} + // Public: get points configuration router.get("/points-config", async (req, res) => { try { @@ -800,5 +813,43 @@ router.put("/points-config", async (req, res) => { } }); +// Public: get visibility configuration +router.get("/visibility-config", async (req, res) => { + try { + const existing = await Setting.findOne({ key: VISIBILITY_CONFIG_KEY }).select("value"); + const normalized = normalizeVisibilityConfig(existing?.value); + return res.status(200).json(normalized); + } catch (err) { + return res.status(200).json(DEFAULT_VISIBILITY_CONFIG); + } +}); + +// Admin-only: set visibility configuration +router.put("/visibility-config", async (req, res) => { + const token = req.headers.authorization?.split(" ")[1]; + if (!token) return res.status(401).json({ message: "Authentication token is required" }); + + try { + const decoded = jwt.verify(token, process.env.JWT_SECRET); + if (!decoded.admin) { + return res.status(403).json({ message: "You do not have admin privileges" }); + } + + const normalized = normalizeVisibilityConfig(req.body); + const updated = await Setting.findOneAndUpdate( + { key: VISIBILITY_CONFIG_KEY }, + { $set: { value: normalized } }, + { upsert: true, new: true } + ).select("value"); + + return res.status(200).json(normalizeVisibilityConfig(updated?.value)); + } catch (err) { + if (err instanceof jwt.JsonWebTokenError) { + return res.status(401).json({ message: "Invalid or expired token" }); + } + return res.status(500).json({ error: err.message }); + } +}); + module.exports = router; diff --git a/routes/users.js b/routes/users.js index 0ac3da0..b2d733b 100644 --- a/routes/users.js +++ b/routes/users.js @@ -7,6 +7,7 @@ const Movie = require("../models/Movie"); const Setting = require("../models/Setting"); const PlaybackProgress = require("../models/PlaybackProgress"); const OscarPick = require("../models/OscarPick"); +const OscarCategory = require("../models/OscarCategory"); const axios = require("axios"); const router = express.Router(); @@ -153,16 +154,38 @@ router.get("/stats", verifyToken, async (req, res) => { } : { pointsPerMovie: 1, pointsPerCorrectPick: 1 }; - // Get all picks for this year with scores + // Get all categories with winners marked for this year + const categories = await OscarCategory.find({ year }).lean(); + const categoryWinners = new Map(); + categories.forEach(cat => { + const winner = cat.nominees.find(n => n.isWinner); + if (winner) { + categoryWinners.set(cat.categoryNumber, winner.name); + } + }); + + // Get all picks for this year and calculate correct picks on-the-fly let picksByUserId = new Map(); try { const allPicks = await OscarPick.find({ year }) - .select("userId score") + .select("userId picks") .lean(); allPicks.forEach(pick => { const userId = String(pick.userId); - picksByUserId.set(userId, pick.score !== null && pick.score !== undefined ? pick.score : 0); + let correctCount = 0; + + // Calculate correct picks based on current winners + if (Array.isArray(pick.picks)) { + pick.picks.forEach(p => { + const correctWinner = categoryWinners.get(p.categoryNumber); + if (correctWinner && p.selectedNominee === correctWinner) { + correctCount++; + } + }); + } + + picksByUserId.set(userId, correctCount); }); } catch (err) { console.error("Error fetching picks for stats:", err);
## UtilisateurScoreTotal%ActionsScoreTotal%Actions
${rank}${medal ? ' ' + medal : ''}${score.userName}${score.userEmail ? ` (${score.userEmail})` : ''}${score.userName} ${score.score}${totalCategories}${totalCategories} ${percentage}%
# Nom FilmsBonnes réponsesBon picks Points Ratio