Files
mon-petit-cinema/js/admin.js
T
2026-06-21 15:39:04 +02:00

252 lines
19 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. 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 AVEC RÉCUPÉRATION JAQUETTES ──
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);
const batchSize = 3; // Réduit pour permettre la récupération d'images
let processed = 0;
let totalEanHits = 0;
let totalTmdbHits = 0;
let totalNoImage = 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) {
totalEanHits += (result.stats.ean_hits || 0);
totalTmdbHits += (result.stats.tmdb_hits || 0);
totalNoImage += (result.stats.no_image || 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);
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 | 🎬 ${totalTmdbHits} 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) via EAN\n🎬 ${totalTmdbHits} affiche(s) via TMDB\n${totalNoImage} sans image`);
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); }
}