Files
mon-petit-cinema/js/admin.js
T
2026-06-21 14:58:52 +02:00

252 lines
19 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.
const API_URL = '../api.php';
let allItems = [];
let currentAdminTab = 'critique';
let currentPage = 1;
const itemsPerPage = 12;
let selectedIds = new Set();
let pendingDeleteAction = null;
function safeGetValue(id, defaultValue = '') { const el = document.getElementById(id); return el ? el.value : defaultValue; }
function safeSetValue(id, value) { const el = document.getElementById(id); if (el) el.value = value; }
function getStarsHTML(rating) {
const r = parseFloat(rating) || 0; const full = Math.floor(r); const hasHalf = (r - full) >= 0.5; const empty = 5 - Math.ceil(r);
let html = '★'.repeat(full); if (hasHalf) html += '<span class="half-star">★</span>'; html += `<span class="stars-muted">${'☆'.repeat(empty)}</span>`; return html;
}
function parseCSV(text) {
if (text.charCodeAt(0) === 0xFEFF) text = text.slice(1);
const rows = []; let col = '', row = [], inQuotes = false;
for (let i = 0; i < text.length; i++) {
const c = text[i];
if (inQuotes) { if (c === '"') { if (text[i+1] === '"') { col += '"'; i++; } else inQuotes = false; } else col += c; }
else { if (c === '"') inQuotes = true; else if (c === ',') { row.push(col); col = ''; } else if (c === '\n' || c === '\r') { if (c === '\r' && text[i+1] === '\n') i++; row.push(col); col = ''; if (row.length > 1 || row[0] !== '') rows.push(row); row = []; } else col += c; }
}
if (col !== '' || row.length > 0) { row.push(col); rows.push(row); }
if (rows.length === 0) return [];
const headers = rows[0].map(h => h.trim()); const data = [];
for (let i = 1; i < rows.length; i++) { if (rows[i].length === headers.length) { const obj = {}; headers.forEach((h, idx) => obj[h] = rows[i][idx]); data.push(obj); } }
return data;
}
document.addEventListener('DOMContentLoaded', () => {
loadDashboardData(); initEventListeners();
const confirmBtn = document.getElementById('confirm-btn');
if (confirmBtn) confirmBtn.addEventListener('click', () => { if (pendingDeleteAction) pendingDeleteAction(); closeConfirmModal(); });
});
function initEventListeners() {
const filmForm = document.getElementById('film-form'); if (filmForm) filmForm.addEventListener('submit', saveFilmForm);
const csvInput = document.getElementById('csv-file'); if (csvInput) csvInput.addEventListener('change', (e) => handleCsvUpload(e.target));
const searchInput = document.getElementById('search-input');
if (searchInput) searchInput.addEventListener('input', () => { currentPage = 1; renderAdminTable(); });
const selectAll = document.getElementById('select-all-checkbox');
if (selectAll) selectAll.addEventListener('change', (e) => toggleSelectAll(e.target));
document.addEventListener('click', (e) => {
if (e.target.classList.contains('modal-close') || e.target.closest('.modal-close')) { const overlay = e.target.closest('.overlay'); if (overlay) overlay.classList.remove('open'); }
if (e.target.classList.contains('overlay')) e.target.classList.remove('open');
});
}
async function loadDashboardData() {
try {
const res = await fetch(`${API_URL}?action=get_films`, { cache: 'no-store' }); allItems = await res.json();
const secRes = await fetch(`${API_URL}?action=check_security_status`, { cache: 'no-store' }); const secData = await secRes.json();
const banner = document.getElementById('security-banner'); if (banner) banner.style.display = secData.is_blank ? 'flex' : 'none';
renderAdminTable();
} catch (err) { console.error('Erreur chargement :', err); }
}
function getFilteredItems() {
const searchInput = document.getElementById('search-input'); const currentSearch = searchInput ? searchInput.value.toLowerCase() : '';
let filtered = allItems.filter(item => item.type === currentAdminTab);
if (currentSearch) filtered = filtered.filter(f => (f.title && f.title.toLowerCase().includes(currentSearch)) || (f.director && f.director.toLowerCase().includes(currentSearch)));
return filtered;
}
function renderAdminTable() {
const tbody = document.getElementById('admin-table-body'); if (!tbody) return; tbody.innerHTML = '';
const filtered = getFilteredItems();
const countLabel = document.getElementById('admin-count-label'); if(countLabel) countLabel.textContent = `${filtered.length} élément(s)`;
const totalPages = Math.ceil(filtered.length / itemsPerPage) || 1;
if (currentPage > totalPages) currentPage = totalPages;
const startIdx = (currentPage - 1) * itemsPerPage;
const pageItems = filtered.slice(startIdx, startIdx + itemsPerPage);
pageItems.forEach(f => {
const tr = document.createElement('tr');
const isChecked = selectedIds.has(String(f.id)) ? 'checked' : '';
tr.innerHTML = `
<td style="text-align:center;"><input type="checkbox" class="film-checkbox" value="${f.id}" ${isChecked} onclick="toggleSingleSelect('${f.id}', this)"></td>
<td style="text-align:center;">${f.poster ? `<img src="${f.poster}" class="thumb" alt="Affiche">` : '<div class="thumb-ph"><i class="ti ti-photo"></i></div>'}</td>
<td><strong>${f.title}</strong></td>
<td>${f.year || '-'}</td>
<td>${f.director || '-'}</td>
<td>${currentAdminTab === 'critique' ? `<span class="tbl-stars">${getStarsHTML(f.rating)}</span>` : `<span class="badge-format">${f.format || '-'}</span>`}</td>
<td><div class="tbl-actions"><button onclick="openEditModal('${f.id}')" title="Éditer"><i class="ti ti-edit"></i></button><button class="del" onclick="deleteSingleFilm('${f.id}')" title="Supprimer"><i class="ti ti-trash"></i></button></div></td>`;
tbody.appendChild(tr);
});
renderPagination(totalPages, filtered.length);
const selectAll = document.getElementById('select-all-checkbox');
if (selectAll) selectAll.checked = pageItems.length > 0 && pageItems.every(f => selectedIds.has(String(f.id)));
}
function toggleSingleSelect(id, checkbox) {
if (checkbox.checked) selectedIds.add(String(id)); else selectedIds.delete(String(id));
updateBulkBar();
const pageItems = getFilteredItems().slice((currentPage - 1) * itemsPerPage, currentPage * itemsPerPage);
const selectAll = document.getElementById('select-all-checkbox');
if (selectAll) selectAll.checked = pageItems.length > 0 && pageItems.every(f => selectedIds.has(String(f.id)));
}
function toggleSelectAll(source) {
const filtered = getFilteredItems();
if (source.checked) filtered.forEach(f => selectedIds.add(String(f.id))); else filtered.forEach(f => selectedIds.delete(String(f.id)));
document.querySelectorAll('.film-checkbox').forEach(cb => { cb.checked = selectedIds.has(cb.value); });
updateBulkBar();
}
function updateBulkBar() {
const bulkBar = document.getElementById('bulk-actions-bar'); const bulkCount = document.getElementById('bulk-count');
if (selectedIds.size > 0) { if (bulkBar) bulkBar.style.display = 'flex'; if (bulkCount) bulkCount.textContent = selectedIds.size; }
else { if (bulkBar) bulkBar.style.display = 'none'; const selectAll = document.getElementById('select-all-checkbox'); if (selectAll) selectAll.checked = false; }
}
function renderPagination(totalPages, totalItems) {
const container = document.getElementById('pagination-container'); if (!container) return; container.innerHTML = '';
if (totalItems === 0) { container.innerHTML = '<p style="color:var(--muted); text-align:center; width:100%;">Aucun élément trouvé.</p>'; return; }
if (totalPages <= 1) return;
const info = document.createElement('span'); info.className = 'pagination-info'; info.textContent = `Page ${currentPage} sur ${totalPages}`; container.appendChild(info);
const prevBtn = document.createElement('button'); prevBtn.innerHTML = '<i class="ti ti-chevron-left"></i>'; prevBtn.disabled = currentPage === 1; prevBtn.onclick = () => { currentPage--; renderAdminTable(); }; container.appendChild(prevBtn);
const maxButtons = 5; let startPage = Math.max(1, currentPage - Math.floor(maxButtons / 2)); let endPage = Math.min(totalPages, startPage + maxButtons - 1);
if (endPage - startPage + 1 < maxButtons) startPage = Math.max(1, endPage - maxButtons + 1);
if (startPage > 1) { container.appendChild(createPageBtn(1)); if (startPage > 2) container.appendChild(createEllipsis()); }
for (let i = startPage; i <= endPage; i++) container.appendChild(createPageBtn(i));
if (endPage < totalPages) { if (endPage < totalPages - 1) container.appendChild(createEllipsis()); container.appendChild(createPageBtn(totalPages)); }
const nextBtn = document.createElement('button'); nextBtn.innerHTML = '<i class="ti ti-chevron-right"></i>'; nextBtn.disabled = currentPage === totalPages; nextBtn.onclick = () => { currentPage++; renderAdminTable(); }; container.appendChild(nextBtn);
}
function createPageBtn(num) { const btn = document.createElement('button'); btn.textContent = num; if (num === currentPage) btn.classList.add('active'); btn.onclick = () => { currentPage = num; renderAdminTable(); }; return btn; }
function createEllipsis() { const span = document.createElement('span'); span.textContent = '...'; span.style.color = 'var(--muted)'; span.style.padding = '0 0.5rem'; return span; }
function showConfirmModal(actionFn) { pendingDeleteAction = actionFn; const modal = document.getElementById('confirm-modal'); if (modal) modal.classList.add('open'); }
function closeConfirmModal() { const modal = document.getElementById('confirm-modal'); if (modal) modal.classList.remove('open'); pendingDeleteAction = null; }
async function executeBulkDelete() {
const ids = Array.from(selectedIds); if (ids.length === 0) return;
showConfirmModal(async () => {
try { const res = await fetch(`${API_URL}?action=bulk_delete`, { method: 'POST', headers: { 'Authorization': localStorage.getItem('token'), 'Content-Type': 'application/json' }, body: JSON.stringify({ ids, type: currentAdminTab }) }); if (!res.ok) throw new Error("Erreur serveur."); selectedIds.clear(); updateBulkBar(); loadDashboardData(); }
catch (err) { console.error('Erreur bulk delete :', err); alert("Une erreur est survenue."); }
});
}
async function deleteSingleFilm(id) {
showConfirmModal(async () => {
try { const res = await fetch(`${API_URL}?action=delete_film&id=${id}&type=${currentAdminTab}`, { method: 'DELETE', headers: { 'Authorization': localStorage.getItem('token') } }); if (!res.ok) throw new Error("Erreur serveur."); selectedIds.delete(String(id)); updateBulkBar(); loadDashboardData(); }
catch (err) { console.error('Erreur delete :', err); alert("Une erreur est survenue."); }
});
}
function toggleFormFields() { const critFields = document.getElementById('form-critique-fields'); const vidFields = document.getElementById('form-videotheque-fields'); if (critFields) critFields.style.display = currentAdminTab === 'critique' ? 'block' : 'none'; if (vidFields) vidFields.style.display = currentAdminTab === 'videotheque' ? 'block' : 'none'; }
function switchAdminTab(tabName) { currentAdminTab = tabName; currentPage = 1; selectedIds.clear(); const searchInput = document.getElementById('search-input'); if (searchInput) searchInput.value = ''; document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active')); const btn = document.getElementById(`btn-tab-${tabName}`); if (btn) btn.classList.add('active'); toggleFormFields(); updateBulkBar(); renderAdminTable(); }
function openAddModal() { const form = document.getElementById('film-form'); if (form) form.reset(); safeSetValue('f-id', ''); toggleFormFields(); const modal = document.getElementById('admin-modal'); if (modal) modal.classList.add('open'); }
function openEditModal(id) {
const item = allItems.find(x => String(x.id) === String(id)); if (!item) return;
safeSetValue('f-id', item.id); safeSetValue('f-title', item.title); safeSetValue('f-year', item.year); safeSetValue('f-director', item.director); safeSetValue('f-poster', item.poster);
if (currentAdminTab === 'critique') { document.getElementById('f-rating').value = parseFloat(item.rating || 3); document.getElementById('f-review').value = item.review || ''; document.getElementById('f-streaming').value = item.streaming || ''; }
else { safeSetValue('f-format', item.format); safeSetValue('f-length', item.length); safeSetValue('f-publisher', item.publisher); safeSetValue('f-aspect', item.aspect_ratio); safeSetValue('f-ean', item.ean_isbn13); safeSetValue('f-discs', item.number_of_discs || 1); safeSetValue('f-description', item.description); }
toggleFormFields(); const modal = document.getElementById('admin-modal'); if (modal) modal.classList.add('open');
}
function closeAdminModal() { document.getElementById('admin-modal').classList.remove('open'); }
function openConfigModal() { document.getElementById('config-modal').classList.add('open'); }
function closeConfigModal() { document.getElementById('config-modal').classList.remove('open'); }
function openPasswordModal() { document.getElementById('pwd-error').style.display = 'none'; document.getElementById('password-modal').classList.add('open'); }
function closePasswordModal() { document.getElementById('password-modal').classList.remove('open'); }
function logout() { localStorage.removeItem('token'); window.location.href = 'login.html'; }
function showProgressModal(total) { document.getElementById('progress-text').textContent = 'Traitement et récupération des jaquettes...'; document.getElementById('progress-bar').style.width = '0%'; document.getElementById('progress-count').textContent = `0 / ${total}`; document.getElementById('progress-overlay').classList.add('open'); }
function updateProgressModal(current, total) { const pct = Math.round((current / total) * 100); document.getElementById('progress-bar').style.width = pct + '%'; document.getElementById('progress-count').textContent = `${current} / ${total}`; }
function closeProgressModal() { document.getElementById('progress-overlay').classList.remove('open'); }
async function saveFilmForm(e) {
e.preventDefault();
const payload = { type: currentAdminTab, id: safeGetValue('f-id'), title: safeGetValue('f-title'), year: safeGetValue('f-year'), director: safeGetValue('f-director'), poster: safeGetValue('f-poster'), rating: safeGetValue('f-rating', 3), review: safeGetValue('f-review'), streaming: safeGetValue('f-streaming'), format: safeGetValue('f-format'), length: safeGetValue('f-length'), publisher: safeGetValue('f-publisher'), aspect_ratio: safeGetValue('f-aspect'), ean_isbn13: safeGetValue('f-ean'), number_of_discs: safeGetValue('f-discs', 1), description: safeGetValue('f-description') };
try { await fetch(`${API_URL}?action=save_film`, { method: 'POST', headers: { 'Authorization': localStorage.getItem('token'), 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); closeAdminModal(); loadDashboardData(); } catch (err) { console.error('Erreur sauvegarde :', err); }
}
// ── IMPORT CSV OPTIMISÉ (lots de 15 + stats) ──
async function handleCsvUpload(input) {
if (!input.files || input.files.length === 0) return;
const file = input.files[0];
input.value = '';
try {
const text = await file.text();
const allData = parseCSV(text);
if (allData.length === 0) {
alert(' Le fichier CSV est vide ou mal formaté.');
return;
}
closeConfigModal();
showProgressModal(allData.length);
// 🚀 Lots de 15 au lieu de 5 (3× moins de requêtes HTTP)
const batchSize = 15;
let processed = 0;
let totalTmdbCalls = 0;
let totalEanHits = 0;
const startTime = Date.now();
for (let i = 0; i < allData.length; i += batchSize) {
const batch = allData.slice(i, i + batchSize);
try {
const res = await fetch(`${API_URL}?action=import_batch`, {
method: 'POST',
headers: {
'Authorization': localStorage.getItem('token'),
'Content-Type': 'application/json'
},
body: JSON.stringify({ items: batch, type: currentAdminTab })
});
const result = await res.json();
if (result.stats) {
totalTmdbCalls += (result.stats.tmdb_calls || 0);
totalEanHits += (result.stats.ean_hits || 0);
}
} catch (err) { console.error('Erreur sur un lot:', err); }
processed += batch.length;
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
const speed = (processed / (Date.now() - startTime) * 1000).toFixed(1);
// Affichage enrichi : progression + vitesse + stats
const pct = Math.round((processed / allData.length) * 100);
document.getElementById('progress-bar').style.width = pct + '%';
document.getElementById('progress-text').textContent =
`Import en cours... (${speed} films/s)`;
document.getElementById('progress-count').textContent =
`${processed} / ${allData.length} | 🖼️ ${totalEanHits} jaquettes | 🎬 ${totalTmdbCalls} appels TMDB | ⏱️ ${elapsed}s`;
}
closeProgressModal();
const totalTime = ((Date.now() - startTime) / 1000).toFixed(1);
alert(`✅ Import terminé en ${totalTime}s !\n📦 ${allData.length} film(s)\n🖼️ ${totalEanHits} jaquette(s) récupérée(s)\n ${totalTmdbCalls} appel(s) TMDB`);
loadDashboardData();
} catch (err) {
closeProgressModal();
alert('❌ Impossible de lire le fichier CSV.');
}
}
async function saveTmdbKey() {
const input = document.getElementById('tmdb-key-input'); if (input && input.value) {
try { const res = await fetch(`${API_URL}?action=save_config`, { method: 'POST', headers: { 'Authorization': localStorage.getItem('token'), 'Content-Type': 'application/json' }, body: JSON.stringify({ key_name: 'tmdb_api_key', key_value: input.value }) }); const data = await res.json(); if (data.success) { alert('✅ Clé API sauvegardée !'); closeConfigModal(); } else { alert('❌ Erreur : ' + (data.error || 'Impossible de sauvegarder.')); } }
catch (err) { alert('Erreur de communication avec le serveur.'); }
}
}
async function saveNewPassword() {
const pwdInput = document.getElementById('new-password-input'); const pwdConfirm = document.getElementById('new-password-confirm'); const errorMsg = document.getElementById('pwd-error');
if (!pwdInput || !pwdConfirm) return;
if (pwdInput.value !== pwdConfirm.value) { errorMsg.textContent = "Les mots de passe ne correspondent pas."; errorMsg.style.display = "block"; return; }
if (pwdInput.value.length < 4) { errorMsg.textContent = "Le mot de passe doit contenir au moins 4 caractères."; errorMsg.style.display = "block"; return; }
try { const response = await fetch(`${API_URL}?action=update_password`, { method: 'POST', headers: { 'Authorization': localStorage.getItem('token'), 'Content-Type': 'application/json' }, body: JSON.stringify({ new_password: pwdInput.value }) }); const data = await response.json(); if (data.success) { pwdInput.value = ''; pwdConfirm.value = ''; errorMsg.style.display = "none"; closePasswordModal(); alert('Mot de passe mis à jour.'); loadDashboardData(); } }
catch (err) { console.error('Erreur :', err); }
}