diff --git a/public/control.html b/public/control.html index 4bafe42..f3128d9 100644 --- a/public/control.html +++ b/public/control.html @@ -122,16 +122,85 @@ Titre Catégorie Année + Actions - Charge la liste… + Charge la liste… + + + diff --git a/public/js/app_control.js b/public/js/app_control.js index 7df46a2..f1bd0a8 100644 --- a/public/js/app_control.js +++ b/public/js/app_control.js @@ -40,6 +40,24 @@ window.onload = async function () { const selectAllBox = document.getElementById('select-all-movies'); const selectedCountEl = document.getElementById('selected-count'); + const editMovieModalEl = document.getElementById('editMovieModal'); + const editMovieModal = editMovieModalEl ? new bootstrap.Modal(editMovieModalEl) : null; + const editMovieModalTitleEl = document.getElementById('editMovieModalTitle'); + const saveMovieChangesBtn = document.getElementById('save-movie-changes'); + + const editMovieIdEl = document.getElementById('edit_movie_id'); + const editImdbIdEl = document.getElementById('edit_imdb_id'); + const editYearEl = document.getElementById('edit_year'); + const editCategoryEl = document.getElementById('edit_category'); + const editVodLinkEl = document.getElementById('edit_vod_link'); + const editRefreshOmdbEl = document.getElementById('edit_refresh_omdb'); + const editTitleEl = document.getElementById('edit_title'); + const editRatingEl = document.getElementById('edit_rating'); + const editPosterEl = document.getElementById('edit_poster'); + const editDescriptionEl = document.getElementById('edit_description'); + + let moviesById = new Map(); + function showResponse(kind, message) { responseEl.classList.remove('d-none', 'alert-success', 'alert-danger', 'alert-warning'); responseEl.classList.add(kind === 'success' ? 'alert-success' : kind === 'warning' ? 'alert-warning' : 'alert-danger'); @@ -53,6 +71,10 @@ window.onload = async function () { return n; } + function isValidImdbId(value) { + return /^tt\d{5,}$/.test(String(value || '').trim()); + } + document.getElementById('reset-form').addEventListener('click', function () { form.reset(); responseEl.classList.add('d-none'); @@ -117,18 +139,24 @@ window.onload = async function () { const yearsRes = await fetch('/api/movies/years'); const years = yearsRes.ok ? await yearsRes.json() : []; - // Populate both the add form (default) and manage dropdown - const existingValues = new Set([...manageYearSelect.options].map((o) => o.value)); + // Rebuild manage dropdown from scratch (so removed years disappear) + const previousSelection = manageYearSelect.value; + manageYearSelect.innerHTML = ''; + const allOpt = document.createElement('option'); + allOpt.value = ''; + allOpt.textContent = 'Toutes'; + manageYearSelect.appendChild(allOpt); + years.forEach((y) => { - const value = String(y); - if (!existingValues.has(value)) { - const opt = document.createElement('option'); - opt.value = value; - opt.textContent = value; - manageYearSelect.appendChild(opt); - } + const opt = document.createElement('option'); + opt.value = String(y); + opt.textContent = String(y); + manageYearSelect.appendChild(opt); }); + const stillExists = years.map(String).includes(previousSelection); + manageYearSelect.value = stillExists ? previousSelection : ''; + // Default add-year to latest year if empty if (years.length > 0 && !yearInput.value) { yearInput.value = String(years[0]); @@ -155,22 +183,140 @@ window.onload = async function () { selectAllBox.checked = allBoxes.length > 0 && checked === allBoxes.length; } + function openEditModal(movieId) { + if (!editMovieModal) return; + const movie = moviesById.get(movieId); + if (!movie) return; + + editMovieIdEl.value = movie._id || ''; + editImdbIdEl.value = movie.imdb_id || ''; + editYearEl.value = movie.year ? String(movie.year) : ''; + editCategoryEl.value = movie.category || ''; + editVodLinkEl.value = movie.vod_link || ''; + editRefreshOmdbEl.checked = false; + + editTitleEl.value = movie.title || ''; + editRatingEl.value = movie.rating || ''; + editPosterEl.value = movie.poster || ''; + editDescriptionEl.value = movie.description || ''; + + if (editMovieModalTitleEl) { + const label = movie.title ? `Modifier: ${movie.title}` : 'Modifier le film'; + editMovieModalTitleEl.textContent = label; + } + + editMovieModal.show(); + } + + async function saveMovieChanges() { + const movieId = (editMovieIdEl.value || '').trim(); + if (!movieId) return; + + const imdb_id = (editImdbIdEl.value || '').trim(); + const yearRaw = (editYearEl.value || '').trim(); + const category = (editCategoryEl.value || '').trim(); + const vod_link = (editVodLinkEl.value || '').trim(); + const refreshOmdb = !!editRefreshOmdbEl.checked; + + const title = editTitleEl.value; + const rating = editRatingEl.value; + const poster = editPosterEl.value; + const description = editDescriptionEl.value; + + if (!isValidImdbId(imdb_id)) { + showResponse('warning', 'IMDB ID invalide. Exemple attendu: tt1234567'); + return; + } + + let year = null; + if (yearRaw !== '') { + year = parseYear(yearRaw); + if (!year) { + showResponse('warning', 'Année invalide. Exemple attendu: 2026'); + return; + } + } + + if (!category) { + showResponse('warning', 'Catégorie invalide.'); + return; + } + + if (!vod_link) { + showResponse('warning', 'Lien VOD invalide.'); + return; + } + + const body = { + imdb_id, + year: yearRaw === '' ? null : year, + category, + vod_link, + refreshOmdb + }; + + // Only send manual fields if we're not refreshing from OMDb + if (!refreshOmdb) { + body.title = title; + body.description = description; + body.rating = rating; + body.poster = poster; + } + + try { + saveMovieChangesBtn.disabled = true; + saveMovieChangesBtn.textContent = 'Enregistrement...'; + + const res = await fetch(`/api/movies/${encodeURIComponent(movieId)}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + }, + body: JSON.stringify(body) + }); + + if (!res.ok) { + let msg = `Erreur (${res.status})`; + try { + const data = await res.json(); + msg = data.message || data.error || msg; + } catch (_) {} + showResponse('danger', msg); + return; + } + + const updated = await res.json(); + moviesById.set(updated._id, updated); + showResponse('success', 'Film mis à jour avec succès.'); + editMovieModal.hide(); + await refreshYears(); + await loadMoviesForManagement(); + } catch (err) { + showResponse('danger', err.message || 'Erreur réseau'); + } finally { + saveMovieChangesBtn.disabled = false; + saveMovieChangesBtn.textContent = 'Enregistrer'; + } + } + async function loadMoviesForManagement() { const year = manageYearSelect.value; const url = year ? `/api/movies?year=${encodeURIComponent(year)}` : '/api/movies'; - adminMoviesBody.innerHTML = `Chargement…`; + adminMoviesBody.innerHTML = `Chargement…`; try { const res = await fetch(url, { method: 'GET' }); if (!res.ok) { - adminMoviesBody.innerHTML = `Erreur lors du chargement (${res.status})`; + adminMoviesBody.innerHTML = `Erreur lors du chargement (${res.status})`; return; } const movies = await res.json(); movies.sort((a, b) => (a.title || '').localeCompare((b.title || ''), 'fr', { sensitivity: 'base' })); + moviesById = new Map(movies.map((m) => [m._id, m])); if (!movies.length) { - adminMoviesBody.innerHTML = `Aucun film.`; + adminMoviesBody.innerHTML = `Aucun film.`; updateSelectionUI(); return; } @@ -188,6 +334,11 @@ window.onload = async function () { ${m.category || ''} ${m.year || ''} + + + `; adminMoviesBody.appendChild(tr); }); @@ -196,9 +347,13 @@ window.onload = async function () { cb.addEventListener('change', updateSelectionUI); }); + adminMoviesBody.querySelectorAll('button[data-edit-movie-id]').forEach((btn) => { + btn.addEventListener('click', () => openEditModal(btn.getAttribute('data-edit-movie-id'))); + }); + updateSelectionUI(); } catch (err) { - adminMoviesBody.innerHTML = `${err.message || 'Erreur réseau'}`; + adminMoviesBody.innerHTML = `${err.message || 'Erreur réseau'}`; } } @@ -249,6 +404,10 @@ window.onload = async function () { } }); + if (saveMovieChangesBtn) { + saveMovieChangesBtn.addEventListener('click', saveMovieChanges); + } + // Initial load await refreshYears(); await loadMoviesForManagement(); diff --git a/routes/movies.js b/routes/movies.js index 6642d5b..f941274 100644 --- a/routes/movies.js +++ b/routes/movies.js @@ -14,6 +14,10 @@ function parseOscarYear(raw) { return n; } +function isValidImdbId(value) { + return typeof value === "string" && /^tt\d{5,}$/.test(value.trim()); +} + // Fetch movie details from OMDb API async function fetchMovieDetailsFromOmdb(imdb_id) { const apiKey = process.env.OMDB_API; // Replace with your actual OMDb API key @@ -233,4 +237,85 @@ router.delete("/delete", async (req, res) => { } }); +// Admin: update a movie (edit fields, optionally refresh from OMDb) +router.put("/:id", 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 { id } = req.params; + const { + imdb_id, + title, + description, + rating, + poster, + year, + category, + vod_link, + refreshOmdb, + } = req.body || {}; + + if (imdb_id !== undefined && imdb_id !== null && !isValidImdbId(imdb_id)) { + return res.status(400).json({ message: "IMDB ID invalide. Exemple attendu: tt1234567" }); + } + + if (year !== undefined && year !== null && year !== "") { + const parsedYear = parseOscarYear(year); + if (!parsedYear) { + return res.status(400).json({ message: "Année invalide (ex: 2026)" }); + } + } + + if (typeof category === "string" && category.trim() === "") { + return res.status(400).json({ message: "Catégorie invalide." }); + } + + if (typeof vod_link === "string" && vod_link.trim() === "") { + return res.status(400).json({ message: "Lien VOD invalide." }); + } + + const movie = await Movie.findById(id); + if (!movie) return res.status(404).json({ message: "Movie not found" }); + + if (imdb_id !== undefined) movie.imdb_id = String(imdb_id).trim(); + if (category !== undefined) movie.category = String(category).trim(); + if (vod_link !== undefined) movie.vod_link = String(vod_link).trim(); + + if (year !== undefined && year !== null && year !== "") { + movie.year = parseOscarYear(year); + } else if (year === null || year === "") { + // allow clearing the year explicitly + movie.year = null; + } + + // If refreshOmdb is true, overwrite OMDb-derived fields from the imdb_id + if (refreshOmdb) { + const details = await fetchMovieDetailsFromOmdb(movie.imdb_id); + movie.title = details.title; + movie.description = details.description; + movie.rating = details.rating; + movie.poster = details.poster; + } else { + if (title !== undefined) movie.title = title; + if (description !== undefined) movie.description = description; + if (rating !== undefined) movie.rating = rating; + if (poster !== undefined) movie.poster = poster; + } + + await movie.save(); + return res.status(200).json(movie); + } 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;