Files
oscars-pool/public/js/app_control.js
T
2025-12-21 20:00:13 +00:00

1191 lines
44 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.
window.onload = async function () {
const token = localStorage.getItem('auth_token');
if (!token) {
window.location.href = '/';
return;
}
let decoded;
try {
decoded = JSON.parse(atob(token.split('.')[1]));
} catch (e) {
localStorage.removeItem('auth_token');
window.location.href = '/';
return;
}
const currentTime = Math.floor(Date.now() / 1000);
if (decoded.exp && decoded.exp < currentTime) {
localStorage.removeItem('auth_token');
window.location.href = '/';
return;
}
if (decoded.mustChangePassword) {
window.location.href = '/change-password.html';
return;
}
document.getElementById('user-name').textContent = decoded.name || '';
// Admin-only page: if token doesn't contain admin flag, bounce.
if (!decoded.admin) {
window.location.href = '/movies.html';
return;
}
const responseEl = document.getElementById('response');
const form = document.getElementById('add-movie-form');
const yearInput = document.getElementById('oscar_year');
const activeYearInput = document.getElementById('active_oscar_year');
const saveActiveYearBtn = document.getElementById('save-active-year');
// Modal 100% elements
const completionModalTitleEl = document.getElementById('completion_modal_title');
const completionModalBodyTextEl = document.getElementById('completion_modal_body_text');
const completionModalBodyHtmlEl = document.getElementById('completion_modal_body_html');
const completionModalVideoSrcEl = document.getElementById('completion_modal_video_src');
const completionModalReloadBtn = document.getElementById('completion_modal_reload');
const completionModalSaveBtn = document.getElementById('completion_modal_save');
const completionModalPreviewTitleEl = document.getElementById('completion_modal_preview_title');
const completionModalPreviewTextEl = document.getElementById('completion_modal_preview_text');
const completionModalPreviewCustomEl = document.getElementById('completion_modal_preview_custom');
const completionModalPreviewVideoEl = document.getElementById('completion_modal_preview_video');
const completionModalPreviewVideoSourceEl = document.getElementById('completion_modal_preview_video_source');
const manageYearSelect = document.getElementById('manage_year');
const adminMoviesBody = document.getElementById('admin-movies-body');
const refreshMoviesBtn = document.getElementById('refresh-movies');
const deleteSelectedBtn = document.getElementById('delete-selected');
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 editPlayerModeEl = document.getElementById('edit_player_mode');
const editVideoSrcEl = document.getElementById('edit_video_src');
const editEmbedSrcEl = document.getElementById('edit_embed_src');
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();
let activeYear = null;
const DEFAULT_COMPLETION_MODAL = Object.freeze({
title: 'Félicitations! very nice 🎉🎉🎉',
bodyText: '',
videoSrc: 'video/reward.mp4',
bodyHtml: '',
});
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');
responseEl.textContent = message;
}
function sanitizeHtml(raw) {
const html = String(raw || '');
const withoutScripts = html.replace(/<script\b[\s\S]*?<\/script>/gi, '');
const withoutOnAttrs = withoutScripts
.replace(/\son\w+="[^"]*"/gi, '')
.replace(/\son\w+='[^']*'/gi, '')
.replace(/\son\w+=\S+/gi, '');
return withoutOnAttrs.trim();
}
function normalizeCompletionModal(value) {
const v = value && typeof value === 'object' && !Array.isArray(value) ? value : {};
const title = typeof v.title === 'string' ? v.title : DEFAULT_COMPLETION_MODAL.title;
const bodyText = typeof v.bodyText === 'string' ? v.bodyText : DEFAULT_COMPLETION_MODAL.bodyText;
const videoSrc = typeof v.videoSrc === 'string' ? v.videoSrc : DEFAULT_COMPLETION_MODAL.videoSrc;
const bodyHtml = typeof v.bodyHtml === 'string' ? v.bodyHtml : DEFAULT_COMPLETION_MODAL.bodyHtml;
return {
title: String(title || '').trim().slice(0, 200) || DEFAULT_COMPLETION_MODAL.title,
bodyText: String(bodyText || '').slice(0, 8000),
videoSrc: String(videoSrc || '').trim().slice(0, 2048),
bodyHtml: String(bodyHtml || '').slice(0, 20000),
};
}
async function fetchCompletionModal() {
const res = await fetch('/api/settings/completion-modal', { method: 'GET' });
if (!res.ok) throw new Error(`Erreur (${res.status})`);
const data = await res.json().catch(() => ({}));
return normalizeCompletionModal(data);
}
async function saveCompletionModal(payload) {
const res = await fetch('/api/settings/completion-modal', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify(payload)
});
const data = await res.json().catch(() => ({}));
if (!res.ok) {
const msg = data.message || data.error || `Erreur (${res.status})`;
throw new Error(msg);
}
return normalizeCompletionModal(data);
}
function setCompletionModalForm(value) {
if (!completionModalTitleEl || !completionModalBodyTextEl || !completionModalBodyHtmlEl || !completionModalVideoSrcEl) return;
completionModalTitleEl.value = value.title || '';
completionModalBodyTextEl.value = value.bodyText || '';
completionModalBodyHtmlEl.value = value.bodyHtml || '';
completionModalVideoSrcEl.value = value.videoSrc || '';
}
function getCompletionModalFormValue() {
return normalizeCompletionModal({
title: completionModalTitleEl ? completionModalTitleEl.value : '',
bodyText: completionModalBodyTextEl ? completionModalBodyTextEl.value : '',
bodyHtml: completionModalBodyHtmlEl ? completionModalBodyHtmlEl.value : '',
videoSrc: completionModalVideoSrcEl ? completionModalVideoSrcEl.value : '',
});
}
function updateCompletionModalPreview(value) {
if (completionModalPreviewTitleEl) {
completionModalPreviewTitleEl.textContent = value.title || DEFAULT_COMPLETION_MODAL.title;
}
const text = String(value.bodyText || '');
if (completionModalPreviewTextEl) {
completionModalPreviewTextEl.textContent = text;
completionModalPreviewTextEl.classList.toggle('d-none', !text);
}
const safeHtml = sanitizeHtml(value.bodyHtml || '');
if (completionModalPreviewCustomEl) {
if (safeHtml) {
completionModalPreviewCustomEl.innerHTML = safeHtml;
completionModalPreviewCustomEl.classList.remove('d-none');
} else {
completionModalPreviewCustomEl.innerHTML = '';
completionModalPreviewCustomEl.classList.add('d-none');
}
}
const videoSrc = String(value.videoSrc || '').trim();
if (completionModalPreviewVideoEl && completionModalPreviewVideoSourceEl) {
if (videoSrc) {
completionModalPreviewVideoSourceEl.setAttribute('src', videoSrc);
try { completionModalPreviewVideoEl.load(); } catch (_) {}
completionModalPreviewVideoEl.classList.remove('d-none');
} else {
try { completionModalPreviewVideoEl.pause(); } catch (_) {}
completionModalPreviewVideoEl.classList.add('d-none');
}
}
}
async function loadCompletionModalIntoUi() {
// If the tab isn't in DOM (older HTML), just skip gracefully.
if (!completionModalTitleEl) return;
try {
const value = await fetchCompletionModal();
setCompletionModalForm(value);
updateCompletionModalPreview(getCompletionModalFormValue());
} catch (err) {
// Still show defaults so admin can edit even if network hiccups.
setCompletionModalForm(DEFAULT_COMPLETION_MODAL);
updateCompletionModalPreview(getCompletionModalFormValue());
showResponse('warning', err.message || 'Impossible de charger le contenu du modal.');
}
}
// Admin users list elements
const adminUsersBody = document.getElementById('admin-users-body');
const refreshUsersBtn = document.getElementById('refresh-users');
const adminUsersCountEl = document.getElementById('admin-users-count');
const adminUsersTabBtn = document.getElementById('admin-users-tab');
// Admin: add user form elements
const addUserForm = document.getElementById('add-user-form');
const addUserNameEl = document.getElementById('add_user_name');
const addUserEmailEl = document.getElementById('add_user_email');
const addUserAdminEl = document.getElementById('add_user_admin');
const addUserResponseEl = document.getElementById('add-user-response');
const addUserResultEl = document.getElementById('add-user-result');
const addUserTempPasswordEl = document.getElementById('add-user-temp-password');
const addUserExpiresAtEl = document.getElementById('add-user-expires-at');
const addUserSubmitBtn = document.getElementById('add-user-submit');
const addUserResetBtn = document.getElementById('add-user-reset');
const copyAddUserTempPasswordBtn = document.getElementById('copy-add-user-temp-password');
// Admin user modals
const resetUserPasswordModalEl = document.getElementById('resetUserPasswordModal');
const resetUserPasswordModal = resetUserPasswordModalEl ? new bootstrap.Modal(resetUserPasswordModalEl) : null;
const resetUserLabelEl = document.getElementById('reset_user_label');
const resetUserEmailLabelEl = document.getElementById('reset_user_email_label');
const resetUserModalResponseEl = document.getElementById('reset-user-modal-response');
const resetUserModalResultEl = document.getElementById('reset-user-modal-result');
const resetUserTempPasswordEl = document.getElementById('reset-user-temp-password');
const resetUserExpiresAtEl = document.getElementById('reset-user-expires-at');
const confirmResetUserPasswordBtn = document.getElementById('confirm-reset-user-password');
const copyTempPasswordBtn = document.getElementById('copy-temp-password');
const deleteUserModalEl = document.getElementById('deleteUserModal');
const deleteUserModal = deleteUserModalEl ? new bootstrap.Modal(deleteUserModalEl) : null;
const deleteUserLabelEl = document.getElementById('delete_user_label');
const deleteUserEmailLabelEl = document.getElementById('delete_user_email_label');
const deleteUserVerifyInputEl = document.getElementById('delete_user_verify_input');
const deleteUserModalResponseEl = document.getElementById('delete-user-modal-response');
const confirmDeleteUserBtn = document.getElementById('confirm-delete-user');
let usersLoadedOnce = false;
let currentResetTarget = null;
let currentDeleteTarget = null;
function setAlert(el, kind, message) {
if (!el) return;
el.classList.remove('d-none', 'alert-success', 'alert-danger', 'alert-warning');
el.classList.add(kind === 'success' ? 'alert-success' : kind === 'warning' ? 'alert-warning' : 'alert-danger');
el.textContent = message;
}
function hideAlert(el) {
if (!el) return;
el.classList.add('d-none');
el.textContent = '';
}
function resetAddUserUi(options = {}) {
hideAlert(addUserResponseEl);
if (addUserResultEl) addUserResultEl.classList.add('d-none');
if (addUserTempPasswordEl) addUserTempPasswordEl.textContent = '—';
if (addUserExpiresAtEl) addUserExpiresAtEl.textContent = '';
if (options.resetForm && addUserForm) {
try { addUserForm.reset(); } catch (_) {}
}
if (addUserSubmitBtn) addUserSubmitBtn.disabled = false;
}
function resetResetUserModalUi() {
hideAlert(resetUserModalResponseEl);
if (resetUserModalResultEl) resetUserModalResultEl.classList.add('d-none');
if (resetUserTempPasswordEl) resetUserTempPasswordEl.textContent = '—';
if (resetUserExpiresAtEl) resetUserExpiresAtEl.textContent = '';
if (confirmResetUserPasswordBtn) confirmResetUserPasswordBtn.disabled = false;
}
function openResetUserModal(user) {
currentResetTarget = user || null;
if (!resetUserPasswordModal) return;
resetResetUserModalUi();
if (resetUserLabelEl) resetUserLabelEl.textContent = user?.name ? user.name : '(sans nom)';
if (resetUserEmailLabelEl) resetUserEmailLabelEl.textContent = user?.email || '';
resetUserPasswordModal.show();
}
function resetDeleteUserModalUi() {
hideAlert(deleteUserModalResponseEl);
if (deleteUserVerifyInputEl) deleteUserVerifyInputEl.value = '';
if (confirmDeleteUserBtn) confirmDeleteUserBtn.disabled = true;
}
function openDeleteUserModal(user) {
currentDeleteTarget = user || null;
if (!deleteUserModal) return;
resetDeleteUserModalUi();
if (deleteUserLabelEl) deleteUserLabelEl.textContent = user?.name ? user.name : '(sans nom)';
if (deleteUserEmailLabelEl) deleteUserEmailLabelEl.textContent = user?.email || '';
deleteUserModal.show();
}
function setUsersCount(count) {
if (!adminUsersCountEl) return;
adminUsersCountEl.textContent = Number.isInteger(count) ? String(count) : '—';
}
function renderAdminUsers(users) {
if (!adminUsersBody) return;
const list = Array.isArray(users) ? users : [];
setUsersCount(list.length);
if (!list.length) {
adminUsersBody.innerHTML = '<tr><td colspan="4" class="text-muted">Aucun utilisateur.</td></tr>';
return;
}
adminUsersBody.innerHTML = '';
list.forEach((u) => {
const tr = document.createElement('tr');
const tdName = document.createElement('td');
tdName.textContent = u?.name || '(sans nom)';
const tdEmail = document.createElement('td');
tdEmail.textContent = u?.email || '';
const tdStatus = document.createElement('td');
tdStatus.className = 'text-nowrap';
const badges = [];
if (u?.admin) badges.push({ text: 'Admin', cls: 'bg-warning text-dark' });
if (u?.mustChangePassword) badges.push({ text: 'Reset MDP', cls: 'bg-danger' });
if (!badges.length) {
const span = document.createElement('span');
span.className = 'badge bg-secondary';
span.textContent = 'OK';
tdStatus.appendChild(span);
} else {
badges.forEach((b, idx) => {
const span = document.createElement('span');
span.className = `badge ${b.cls}`;
span.textContent = b.text;
tdStatus.appendChild(span);
if (idx < badges.length - 1) {
tdStatus.appendChild(document.createTextNode(' '));
}
});
}
const tdActions = document.createElement('td');
tdActions.className = 'text-nowrap';
const resetBtn = document.createElement('button');
resetBtn.type = 'button';
resetBtn.className = 'btn btn-outline-danger btn-sm me-1';
resetBtn.textContent = 'Reset MDP';
resetBtn.addEventListener('click', () => openResetUserModal(u));
const deleteBtn = document.createElement('button');
deleteBtn.type = 'button';
deleteBtn.className = 'btn btn-outline-danger btn-sm';
deleteBtn.textContent = 'Supprimer';
const isSelf = String(u?.id || '') && String(u.id) === String(decoded?.id || '');
if (isSelf || u?.admin) {
deleteBtn.disabled = true;
deleteBtn.title = isSelf ? 'Impossible de supprimer ton propre compte' : 'Suppression des admins désactivée';
}
deleteBtn.addEventListener('click', () => openDeleteUserModal(u));
tdActions.appendChild(resetBtn);
tdActions.appendChild(deleteBtn);
tr.appendChild(tdName);
tr.appendChild(tdEmail);
tr.appendChild(tdStatus);
tr.appendChild(tdActions);
adminUsersBody.appendChild(tr);
});
}
async function fetchAdminUsers() {
const res = await fetch('/api/users/admin/list', {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`,
}
});
const data = await res.json().catch(() => ([]));
if (!res.ok) {
const msg = data?.message || data?.error || `Erreur (${res.status})`;
throw new Error(msg);
}
return Array.isArray(data) ? data : [];
}
async function loadAdminUsers(options = {}) {
const force = !!options.force;
if (!adminUsersBody) return;
if (usersLoadedOnce && !force) return;
adminUsersBody.innerHTML = '<tr><td colspan="4" class="text-muted">Chargement…</td></tr>';
setUsersCount(null);
try {
const users = await fetchAdminUsers();
usersLoadedOnce = true;
users.sort((a, b) => (a?.name || '').localeCompare((b?.name || ''), 'fr', { sensitivity: 'base' }));
renderAdminUsers(users);
} catch (err) {
adminUsersBody.innerHTML = `<tr><td colspan="4" class="text-danger">${err.message || 'Erreur réseau'}</td></tr>`;
setUsersCount(null);
}
}
function parseYear(value) {
const n = Number(value);
if (!Number.isInteger(n)) return null;
if (n < 1900 || n > 3000) return null;
return n;
}
async function fetchActiveYear() {
try {
const res = await fetch('/api/settings/year', { method: 'GET' });
if (!res.ok) throw new Error('Failed to fetch active year');
const data = await res.json();
const year = Number(data?.year);
return Number.isInteger(year) ? year : null;
} catch (_) {
return null;
}
}
async function setActiveYear(newYear) {
const res = await fetch('/api/settings/year', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ year: newYear })
});
if (!res.ok) {
let msg = `Erreur (${res.status})`;
try {
const data = await res.json();
msg = data.message || data.error || msg;
} catch (_) {}
throw new Error(msg);
}
const data = await res.json();
const year = Number(data?.year);
return Number.isInteger(year) ? year : newYear;
}
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');
});
document.getElementById('log-off').addEventListener('click', function () {
localStorage.removeItem('auth_token');
window.location.href = '/';
});
// Modal 100% live preview wiring
function hookModal100Inputs() {
if (!completionModalTitleEl) return;
const inputs = [completionModalTitleEl, completionModalBodyTextEl, completionModalBodyHtmlEl, completionModalVideoSrcEl].filter(Boolean);
inputs.forEach((el) => {
el.addEventListener('input', () => {
updateCompletionModalPreview(getCompletionModalFormValue());
});
});
if (completionModalReloadBtn) {
completionModalReloadBtn.addEventListener('click', async () => {
completionModalReloadBtn.disabled = true;
const oldText = completionModalReloadBtn.textContent;
completionModalReloadBtn.textContent = 'Chargement...';
try {
await loadCompletionModalIntoUi();
showResponse('success', 'Contenu rechargé.');
} catch (_) {
// loadCompletionModalIntoUi already handles messaging
} finally {
completionModalReloadBtn.disabled = false;
completionModalReloadBtn.textContent = oldText;
}
});
}
if (completionModalSaveBtn) {
completionModalSaveBtn.addEventListener('click', async () => {
completionModalSaveBtn.disabled = true;
const oldText = completionModalSaveBtn.textContent;
completionModalSaveBtn.textContent = 'Enregistrement...';
try {
const value = getCompletionModalFormValue();
// send raw HTML; server sanitizes/caps it
const saved = await saveCompletionModal(value);
setCompletionModalForm(saved);
updateCompletionModalPreview(getCompletionModalFormValue());
showResponse('success', 'Modal 100% enregistré.');
} catch (err) {
showResponse('danger', err.message || 'Erreur réseau');
} finally {
completionModalSaveBtn.disabled = false;
completionModalSaveBtn.textContent = oldText;
}
});
}
}
if (deleteUserVerifyInputEl && confirmDeleteUserBtn) {
deleteUserVerifyInputEl.addEventListener('input', () => {
const expected = String(currentDeleteTarget?.email || '').trim();
const typed = String(deleteUserVerifyInputEl.value || '').trim();
confirmDeleteUserBtn.disabled = !(expected && typed && expected === typed);
});
}
if (copyTempPasswordBtn) {
copyTempPasswordBtn.addEventListener('click', async () => {
const text = String(resetUserTempPasswordEl?.textContent || '').trim();
if (!text || text === '—') return;
try {
await navigator.clipboard.writeText(text);
setAlert(resetUserModalResponseEl, 'success', 'Copié.');
setTimeout(() => hideAlert(resetUserModalResponseEl), 1200);
} catch (_) {
// Fallback: select text for manual copy
setAlert(resetUserModalResponseEl, 'warning', 'Impossible de copier automatiquement. Copie manuellement.');
}
});
}
if (copyAddUserTempPasswordBtn) {
copyAddUserTempPasswordBtn.addEventListener('click', async () => {
const text = String(addUserTempPasswordEl?.textContent || '').trim();
if (!text || text === '—') return;
try {
await navigator.clipboard.writeText(text);
setAlert(addUserResponseEl, 'success', 'Copié.');
setTimeout(() => hideAlert(addUserResponseEl), 1200);
} catch (_) {
setAlert(addUserResponseEl, 'warning', 'Impossible de copier automatiquement. Copie manuellement.');
}
});
}
if (addUserResetBtn) {
addUserResetBtn.addEventListener('click', () => {
resetAddUserUi({ resetForm: true });
});
}
if (addUserForm) {
addUserForm.addEventListener('submit', async (e) => {
e.preventDefault();
resetAddUserUi({ resetForm: false });
const name = String(addUserNameEl?.value || '').trim();
const email = String(addUserEmailEl?.value || '').trim();
const admin = !!addUserAdminEl?.checked;
if (!name) {
setAlert(addUserResponseEl, 'warning', 'Nom requis.');
return;
}
if (!email) {
setAlert(addUserResponseEl, 'warning', 'Email requis.');
return;
}
if (addUserSubmitBtn) addUserSubmitBtn.disabled = true;
setAlert(addUserResponseEl, 'warning', 'Création...');
try {
const res = await fetch('/api/users/admin/create', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
},
body: JSON.stringify({ name, email, admin }),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) {
setAlert(addUserResponseEl, 'danger', data.message || data.error || `Erreur (${res.status})`);
if (addUserSubmitBtn) addUserSubmitBtn.disabled = false;
return;
}
// Show temp password once
if (addUserTempPasswordEl) addUserTempPasswordEl.textContent = data?.tempPassword || '—';
if (addUserExpiresAtEl) addUserExpiresAtEl.textContent = data?.expiresAt ? `Expire: ${data.expiresAt}` : '';
if (addUserResultEl) addUserResultEl.classList.remove('d-none');
setAlert(addUserResponseEl, 'success', data.message || 'Utilisateur créé.');
if (addUserSubmitBtn) addUserSubmitBtn.disabled = false;
// Refresh list if already loaded or when user tab is active
await loadAdminUsers({ force: true });
} catch (err) {
setAlert(addUserResponseEl, 'danger', err.message || 'Erreur réseau');
if (addUserSubmitBtn) addUserSubmitBtn.disabled = false;
}
});
}
if (confirmResetUserPasswordBtn) {
confirmResetUserPasswordBtn.addEventListener('click', async () => {
const target = currentResetTarget;
const userId = String(target?.id || '').trim();
if (!userId) return;
confirmResetUserPasswordBtn.disabled = true;
resetResetUserModalUi();
setAlert(resetUserModalResponseEl, 'warning', 'Génération du mot de passe temporaire...');
try {
const res = await fetch('/api/users/admin/reset-temp-password', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
},
body: JSON.stringify({ userId }),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) {
setAlert(resetUserModalResponseEl, 'danger', data.message || data.error || `Erreur (${res.status})`);
confirmResetUserPasswordBtn.disabled = false;
return;
}
hideAlert(resetUserModalResponseEl);
if (resetUserTempPasswordEl) resetUserTempPasswordEl.textContent = data?.tempPassword || '—';
if (resetUserExpiresAtEl) {
resetUserExpiresAtEl.textContent = data?.expiresAt ? `Expire: ${data.expiresAt}` : '';
}
if (resetUserModalResultEl) resetUserModalResultEl.classList.remove('d-none');
setAlert(resetUserModalResponseEl, 'success', 'Mot de passe temporaire généré. Copie-le et envoie-le à l’utilisateur.');
confirmResetUserPasswordBtn.disabled = false;
await loadAdminUsers({ force: true });
} catch (err) {
setAlert(resetUserModalResponseEl, 'danger', err.message || 'Erreur réseau');
confirmResetUserPasswordBtn.disabled = false;
}
});
}
if (confirmDeleteUserBtn) {
confirmDeleteUserBtn.addEventListener('click', async () => {
const target = currentDeleteTarget;
const userId = String(target?.id || '').trim();
if (!userId) return;
confirmDeleteUserBtn.disabled = true;
hideAlert(deleteUserModalResponseEl);
setAlert(deleteUserModalResponseEl, 'warning', 'Suppression...');
try {
const res = await fetch('/api/users/admin/delete', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
},
body: JSON.stringify({ userId }),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) {
setAlert(deleteUserModalResponseEl, 'danger', data.message || data.error || `Erreur (${res.status})`);
// Keep disabled until the input matches again.
const expected = String(target?.email || '').trim();
const typed = String(deleteUserVerifyInputEl?.value || '').trim();
confirmDeleteUserBtn.disabled = !(expected && typed && expected === typed);
return;
}
setAlert(deleteUserModalResponseEl, 'success', data.message || 'Utilisateur supprimé.');
await loadAdminUsers({ force: true });
// Close shortly after success
setTimeout(() => {
try { deleteUserModal?.hide(); } catch (_) {}
}, 600);
} catch (err) {
setAlert(deleteUserModalResponseEl, 'danger', err.message || 'Erreur réseau');
}
});
}
if (refreshUsersBtn) {
refreshUsersBtn.addEventListener('click', async () => {
refreshUsersBtn.disabled = true;
const oldText = refreshUsersBtn.textContent;
refreshUsersBtn.textContent = 'Chargement...';
try {
await loadAdminUsers({ force: true });
} finally {
refreshUsersBtn.disabled = false;
refreshUsersBtn.textContent = oldText;
}
});
}
if (adminUsersTabBtn) {
// Only load when the tab is shown (and then cache), to keep initial load snappy.
adminUsersTabBtn.addEventListener('shown.bs.tab', () => loadAdminUsers({ force: false }));
}
form.addEventListener('submit', async function (e) {
e.preventDefault();
const year = parseYear(yearInput.value);
const imdb_id = document.getElementById('imdb_id').value.trim();
const category = document.getElementById('category').value.trim();
const vod_link = document.getElementById('vod_link').value.trim();
const player_mode = (document.getElementById('player_mode')?.value || 'auto').trim();
const video_src = document.getElementById('video_src')?.value?.trim() || '';
const embed_src = document.getElementById('embed_src')?.value?.trim() || '';
if (!year) {
showResponse('warning', 'Année invalide. Exemple attendu: 2026');
return;
}
if (!/^tt\d{5,}$/.test(imdb_id)) {
showResponse('warning', 'IMDB ID invalide. Exemple attendu: tt1234567');
return;
}
if (!vod_link && !video_src && !embed_src) {
showResponse('warning', 'Ajoute au moins une source (VOD / video_src / embed_src).');
return;
}
try {
const res = await fetch('/api/movies/add', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ year, imdb_id, category, vod_link, player_mode, video_src, embed_src })
});
if (res.ok) {
showResponse('success', 'Film ajouté avec succès.');
form.reset();
await refreshYears();
await loadMoviesForManagement();
return;
}
let errMessage = `Erreur (${res.status})`;
try {
const data = await res.json();
errMessage = data.message || data.error || errMessage;
} catch (_) {
// ignore JSON parse errors
}
showResponse('danger', errMessage);
} catch (err) {
showResponse('danger', err.message || 'Erreur réseau');
}
});
async function refreshYears() {
try {
const yearsRes = await fetch('/api/movies/years');
const years = yearsRes.ok ? await yearsRes.json() : [];
const cleanedYears = (Array.isArray(years) ? years : [])
.map((y) => Number(y))
.filter((y) => Number.isInteger(y) && y >= 1900 && y <= 3000)
.sort((a, b) => b - a);
// 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);
cleanedYears.forEach((y) => {
const opt = document.createElement('option');
opt.value = String(y);
opt.textContent = String(y);
manageYearSelect.appendChild(opt);
});
const stillExists = cleanedYears.map(String).includes(previousSelection);
manageYearSelect.value = stillExists ? previousSelection : '';
// Prefer defaulting UI to active year (if selection is empty/invalid).
const hasSelection = !!manageYearSelect.value;
if (!hasSelection && activeYear && cleanedYears.map(String).includes(String(activeYear))) {
manageYearSelect.value = String(activeYear);
}
// Default add-year to active year (or latest year) if empty
if (!yearInput.value) {
if (activeYear) {
yearInput.value = String(activeYear);
} else if (cleanedYears.length > 0) {
yearInput.value = String(cleanedYears[0]);
}
}
// Rebuild active-year dropdown from distinct movie years
if (activeYearInput && saveActiveYearBtn) {
const previousActiveSelection = String(activeYearInput.value || '');
activeYearInput.innerHTML = '';
if (cleanedYears.length === 0) {
const opt = document.createElement('option');
opt.value = '';
opt.textContent = 'Aucune année (ajoute un film)';
opt.disabled = true;
opt.selected = true;
activeYearInput.appendChild(opt);
activeYearInput.disabled = true;
saveActiveYearBtn.disabled = true;
} else {
cleanedYears.forEach((y) => {
const opt = document.createElement('option');
opt.value = String(y);
opt.textContent = String(y);
activeYearInput.appendChild(opt);
});
// Keep current selection if still valid, else use activeYear, else latest.
const desired =
(previousActiveSelection && cleanedYears.map(String).includes(previousActiveSelection))
? previousActiveSelection
: (activeYear && cleanedYears.map(String).includes(String(activeYear)))
? String(activeYear)
: String(cleanedYears[0]);
activeYearInput.disabled = false;
saveActiveYearBtn.disabled = false;
activeYearInput.value = desired;
}
}
} catch (_) {
// ignore
}
}
function getSelectedMovieIds() {
return Array.from(adminMoviesBody.querySelectorAll('input[type="checkbox"][data-movie-id]:checked'))
.map((el) => el.getAttribute('data-movie-id'))
.filter(Boolean);
}
function updateSelectionUI() {
const selectedIds = getSelectedMovieIds();
selectedCountEl.textContent = String(selectedIds.length);
deleteSelectedBtn.disabled = selectedIds.length === 0;
const allBoxes = Array.from(adminMoviesBody.querySelectorAll('input[type="checkbox"][data-movie-id]'));
const checked = allBoxes.filter((b) => b.checked).length;
selectAllBox.indeterminate = checked > 0 && checked < allBoxes.length;
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 || '';
if (editPlayerModeEl) editPlayerModeEl.value = movie.player_mode || 'auto';
if (editVideoSrcEl) editVideoSrcEl.value = movie.video_src || '';
if (editEmbedSrcEl) editEmbedSrcEl.value = movie.embed_src || '';
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 player_mode = (editPlayerModeEl?.value || 'auto').trim();
const video_src = (editVideoSrcEl?.value || '').trim();
const embed_src = (editEmbedSrcEl?.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 && !video_src && !embed_src) {
showResponse('warning', 'Ajoute au moins une source (VOD / video_src / embed_src).');
return;
}
const body = {
imdb_id,
year: yearRaw === '' ? null : year,
category,
vod_link,
player_mode,
video_src,
embed_src,
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 = `<tr><td colspan="6" class="text-muted">Chargement…</td></tr>`;
try {
const res = await fetch(url, { method: 'GET' });
if (!res.ok) {
adminMoviesBody.innerHTML = `<tr><td colspan="6" class="text-danger">Erreur lors du chargement (${res.status})</td></tr>`;
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 = `<tr><td colspan="6" class="text-muted">Aucun film.</td></tr>`;
updateSelectionUI();
return;
}
function buildPlayerBadges(m) {
const badges = [];
const hasVideo = !!(m && m.video_src);
const hasEmbed = !!(m && m.embed_src);
const hasLegacy = !!(m && m.vod_link);
if (hasVideo) badges.push({ text: 'Video', cls: 'bg-primary' });
if (hasEmbed) badges.push({ text: 'Embed', cls: 'bg-info text-dark' });
if (hasLegacy) badges.push({ text: 'Legacy', cls: 'bg-secondary' });
const modeRaw = String(m?.player_mode || 'auto').toLowerCase();
const mode = (modeRaw === 'video' || modeRaw === 'embed' || modeRaw === 'auto') ? modeRaw : 'auto';
const modeText = mode === 'auto' ? 'Auto' : mode === 'video' ? 'Mode: Video' : 'Mode: Embed';
badges.push({ text: modeText, cls: 'bg-light text-dark border' });
if (!hasVideo && !hasEmbed && !hasLegacy) {
return [{ text: '—', cls: 'bg-light text-dark border' }];
}
return badges;
}
adminMoviesBody.innerHTML = '';
movies.forEach((m) => {
const badges = buildPlayerBadges(m);
const badgesHtml = badges
.map((b) => `<span class="badge ${b.cls} me-1 mb-1">${b.text}</span>`)
.join('');
const tr = document.createElement('tr');
tr.innerHTML = `
<td>
<input type="checkbox" class="form-check-input" data-movie-id="${m._id}">
</td>
<td>
<div class="fw-semibold">${m.title || '(sans titre)'}</div>
<div class="text-muted small">${m.imdb_id || ''}</div>
</td>
<td>${badgesHtml}</td>
<td>${m.category || ''}</td>
<td>${m.year || ''}</td>
<td class="text-end">
<button type="button" class="btn btn-sm btn-outline-secondary" data-edit-movie-id="${m._id}">
Éditer
</button>
</td>
`;
adminMoviesBody.appendChild(tr);
});
adminMoviesBody.querySelectorAll('input[type="checkbox"][data-movie-id]').forEach((cb) => {
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 = `<tr><td colspan="6" class="text-danger">${err.message || 'Erreur réseau'}</td></tr>`;
}
}
refreshMoviesBtn.addEventListener('click', loadMoviesForManagement);
manageYearSelect.addEventListener('change', loadMoviesForManagement);
selectAllBox.addEventListener('change', () => {
const checked = selectAllBox.checked;
adminMoviesBody.querySelectorAll('input[type="checkbox"][data-movie-id]').forEach((cb) => {
cb.checked = checked;
});
updateSelectionUI();
});
deleteSelectedBtn.addEventListener('click', async () => {
const selectedIds = getSelectedMovieIds();
if (!selectedIds.length) return;
const yearLabel = manageYearSelect.value ? ` (${manageYearSelect.value})` : '';
const ok = window.confirm(`Supprimer ${selectedIds.length} film(s)${yearLabel} ? Cette action est irréversible.`);
if (!ok) return;
try {
const res = await fetch('/api/movies/delete', {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ ids: selectedIds })
});
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;
}
showResponse('success', 'Film(s) supprimé(s) avec succès.');
await refreshYears();
await loadMoviesForManagement();
} catch (err) {
showResponse('danger', err.message || 'Erreur réseau');
}
});
if (saveMovieChangesBtn) {
saveMovieChangesBtn.addEventListener('click', saveMovieChanges);
}
// Initial load
activeYear = await fetchActiveYear();
if (activeYear) {
document.title = `Pool Oscars ${activeYear} - Admin`;
}
hookModal100Inputs();
await loadCompletionModalIntoUi();
if (saveActiveYearBtn && activeYearInput) {
saveActiveYearBtn.addEventListener('click', async () => {
const y = parseYear(activeYearInput.value);
if (!y) {
showResponse('warning', 'Année invalide. Exemple attendu: 2026');
return;
}
saveActiveYearBtn.disabled = true;
const oldText = saveActiveYearBtn.textContent;
saveActiveYearBtn.textContent = 'Application...';
try {
activeYear = await setActiveYear(y);
activeYearInput.value = String(activeYear);
yearInput.value = String(activeYear);
document.title = `Pool Oscars ${activeYear} - Admin`;
showResponse('success', `Année active appliquée: ${activeYear}`);
await refreshYears();
await loadMoviesForManagement();
} catch (err) {
showResponse('danger', err.message || 'Erreur réseau');
} finally {
saveActiveYearBtn.disabled = false;
saveActiveYearBtn.textContent = oldText;
}
});
}
await refreshYears();
await loadMoviesForManagement();
};