// === Autopost client script === // --- CSRF global --- (function () { const meta = document.querySelector('meta[name="csrf-token"]'); const CSRF_TOKEN = meta ? meta.content : (window.__BOOTSTRAP__ && window.__BOOTSTRAP__.csrf) || ''; if (window.jQuery) { $.ajaxSetup({ headers: { 'x-csrf-token': CSRF_TOKEN } }); } })(); // --- XSS escape côté client --- function esc(s) { // ATTENTION: backtick échappé dans la classe de caractères: \` return String(s).replace(/[&<>"'\`=]/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', '\`': '`', '=': '=' }[c])); } // --- Bootstrap values --- const INITIAL_PAGE = (window.__BOOTSTRAP__ && window.__BOOTSTRAP__.page) || 1; const INITIAL_TOTAL_PAGES= (window.__BOOTSTRAP__ && window.__BOOTSTRAP__.totalPages) || 1; const PAGE_LIMIT = (window.__BOOTSTRAP__ && window.__BOOTSTRAP__.limit) || 100; let currentPage = INITIAL_PAGE; let currentTotalPages = INITIAL_TOTAL_PAGES; let activeFilter = null; // number | null let activeQuery = ''; // string // --- Rendering: table --- function updateTable(rows) { var tbody = $('table tbody'); tbody.empty(); rows.forEach(function(row) { var statusText = ''; var statusClass = ''; switch (parseInt(row.status)) { case 0: statusText = 'EN ATTENTE'; statusClass = 'bg-cyan-500 text-black font-bold'; break; case 1: statusText = 'ENVOI TERMINÉ'; statusClass = 'bg-green-300 text-black font-bold'; break; case 2: statusText = 'ERREUR'; statusClass = 'bg-red-300 text-black font-bold'; break; case 3: statusText = 'DEJA DISPONIBLE';statusClass = 'bg-pink-300 text-black font-bold'; break; case 4: statusText = 'EN COURS'; statusClass = 'bg-yellow-300 text-black font-bold'; break; default: statusText = 'INCONNU'; } var logLink = (parseInt(row.status) === 1 || parseInt(row.status) === 2 || parseInt(row.status) === 4) ? ' | Log' : ''; var mediainfoLink = (parseInt(row.status) === 0 || parseInt(row.status) === 1 || parseInt(row.status) === 2) ? ' | Mediainfo' : ''; var dlLink = (parseInt(row.status) === 1) ? ' | DL' : ''; var resendLink = (parseInt(row.status) === 1) ? ' | Renvoyer' : ''; var tr = '' + '' + '' + '' + '' + esc(row.nom) + '' + '' + statusText + '' + '' + row.id + '' + '' + 'Editer | ' + 'Supprimer' + logLink + mediainfoLink + dlLink + resendLink + '' + ''; tbody.append(tr); }); } // --- Rendering: pagination --- function renderPaginationHTML(page, totalPages) { var prevDisabled = page <= 1; var nextDisabled = page >= totalPages; var h = ''; h += ''; return h; } function updatePagination(page, totalPages) { $('#pagination').html(renderPaginationHTML(page, totalPages)); } // --- Data loading --- function loadPage(p) { const targetPage = Math.max(1, parseInt(p, 10) || 1); const isFilter = activeFilter !== null; const url = isFilter ? '/autopost/filter' : '/autopost/search'; const data = isFilter ? { status: activeFilter, page: targetPage, limit: PAGE_LIMIT } : { q: activeQuery || '', page: targetPage, limit: PAGE_LIMIT }; $.ajax({ url: url, type: 'GET', data: data, dataType: 'json', success: function(resp) { updateTable(resp.rows); currentPage = resp.page; currentTotalPages = resp.totalPages; updatePagination(currentPage, currentTotalPages); // Mettre à jour les actions en lot après le chargement des nouvelles données updateBulkActions(); }, error: function(jqXHR, textStatus, errorThrown) { if (textStatus !== 'abort') { console.error('Erreur AJAX :', textStatus, errorThrown); alert('Erreur lors du chargement de la page.'); } } }); } // --- Long polling: refresh table when new mediainfo/log apparaît --- (function () { var lastVersion = null; function poll() { $.ajax({ url: '/autopost/updates', type: 'GET', data: { since: lastVersion || 0 }, timeout: 30000, success: function (resp) { if (resp && typeof resp.version === 'number') { if (lastVersion === null) { lastVersion = resp.version; // 1ère synchro: on se cale } else if (resp.version > lastVersion) { lastVersion = resp.version; if (typeof loadPage === 'function') loadPage(currentPage || 1); if (typeof updateStatsUI === 'function') { $.getJSON('/autopost/stats', function (s) { if (s) updateStatsUI(s); }); } } } setTimeout(poll, 100); }, error: function () { setTimeout(poll, 2000); } }); } $(document).ready(poll); })(); function updateStatsUI(s) { $('.filter-card[data-status="0"] .tabular-nums').text(s.attente || 0); $('.filter-card[data-status="1"] .tabular-nums').text(s.termine || 0); $('.filter-card[data-status="2"] .tabular-nums').text(s.erreur || 0); $('.filter-card[data-status="3"] .tabular-nums').text(s.deja || 0); } function refreshAfterChange() { loadPage(currentPage || 1); $.getJSON('/autopost/stats', function(s) { updateStatsUI(s); }); } // --- DOM bindings --- $(document).ready(function() { // Recherche let searchTimer = null; $('#searchInput').on('keyup', function() { clearTimeout(searchTimer); let q = $(this).val(); searchTimer = setTimeout(function() { activeQuery = q || ''; activeFilter = null; // on sort du mode filtre $('.filter-card').removeClass('ring-4 ring-white/40'); loadPage(1); // charge page 1 avec pagination AJAX toggleShowAllButton(); }, 300); }); // Pagination: intercepter et paginer en AJAX $(document).on('click', '#pagination a[data-page]', function(e) { e.preventDefault(); const p = parseInt($(this).data('page'), 10); if (!isNaN(p)) loadPage(p); }); // Affichage/masquage du bouton "Tout afficher" function toggleShowAllButton() { if ($('.filter-card.ring-4').length === 0) { $('#showAll').addClass('hidden'); } else { $('#showAll').removeClass('hidden'); } } // Filtrage par clic sur une card $(document).on('click', '.filter-card', function() { const status = parseInt($(this).data('status'), 10); $('.filter-card').removeClass('ring-4 ring-white/40'); $(this).addClass('ring-4 ring-white/40'); activeFilter = status; activeQuery = ''; // on sort du mode recherche loadPage(1); // page 1 du filtre toggleShowAllButton(); }); // Bouton tout afficher $(document).on('click', '#showAll', function() { $('.filter-card').removeClass('ring-4 ring-white/40'); activeFilter = null; activeQuery = $('#searchInput').val() || ''; loadPage(1); toggleShowAllButton(); }); // Edition (ouvrir modale) $(document).on('click', '.edit-link', function(e) { e.preventDefault(); var releaseId = $(this).data('id'); var currentStatus = $(this).data('status'); $('#releaseId').val(releaseId); $('#modalReleaseId').text(releaseId); $('#statusSelect').val(currentStatus); $('#editModal').removeClass('hidden').fadeIn(); }); // ---------- Confirmation de suppression (modale) ---------- var pendingDeleteId = null; function openConfirmModal(id, name) { pendingDeleteId = id; $('#confirmItemName').text(name || ('ID ' + id)); $('#confirmDeleteModal').removeClass('hidden').fadeIn(120); } function closeConfirmModal() { $('#confirmDeleteModal').fadeOut(120, function() { $(this).addClass('hidden'); }); pendingDeleteId = null; } // Fonctions pour la modal de renvoi var pendingResendId = null; var pendingResendFilename = null; function openConfirmResendModal(id, filename) { pendingResendId = id; pendingResendFilename = filename; $('#confirmResendItemName').text(filename || ('ID ' + id)); $('#confirmResendModal').removeClass('hidden').fadeIn(120); } function closeConfirmResendModal() { $('#confirmResendModal').fadeOut(120, function() { $(this).addClass('hidden'); }); pendingResendId = null; pendingResendFilename = null; } function showToast(msg) { $('#toastMsg').text(msg || 'Opération effectuée'); $('#toast').removeClass('hidden').fadeIn(120); setTimeout(function() { $('#toast').fadeOut(150, function() { $(this).addClass('hidden'); }); }, 1800); } // Ouvrir la modale au clic sur "Supprimer" $(document).on('click', '.delete-link', function(e) { e.preventDefault(); var releaseId = $(this).data('id'); var name = $(this).closest('tr').find('td:first').text().trim(); openConfirmModal(releaseId, name); }); // Boutons de la modale $('#cancelDeleteBtn, #confirmDeleteClose').on('click', function() { closeConfirmModal(); }); // Clic sur l'overlay pour fermer $('#confirmDeleteModal').on('click', function(e) { if (e.target === this) { closeConfirmModal(); } }); // Gestionnaires pour la modal de renvoi $('#cancelResendBtn, #confirmResendClose').on('click', function() { closeConfirmResendModal(); }); // Clic sur l'overlay pour fermer la modal de renvoi $('#confirmResendModal').on('click', function(e) { if (e.target === this) { closeConfirmResendModal(); } }); // Accessibilité clavier: Esc = fermer, Enter = confirmer $(document).on('keydown', function(e) { var deleteModalVisible = !$('#confirmDeleteModal').hasClass('hidden'); var resendModalVisible = !$('#confirmResendModal').hasClass('hidden'); if (deleteModalVisible) { if (e.key === 'Escape') { closeConfirmModal(); } if (e.key === 'Enter') { $('#confirmDeleteBtn').click(); } } else if (resendModalVisible) { if (e.key === 'Escape') { closeConfirmResendModal(); } if (e.key === 'Enter') { $('#confirmResendBtn').click(); } } }); // Confirmer et supprimer via AJAX $('#confirmDeleteBtn').on('click', function() { if (!pendingDeleteId) return; var releaseId = pendingDeleteId; $.ajax({ url: '/autopost/delete/' + releaseId, type: 'POST', success: function() { $('#row-' + releaseId) .css('outline', '2px solid rgba(239,68,68,0.6)') .fadeOut('300', function(){ $(this).remove(); }); showToast('Enregistrement supprimé'); }, error: function() { alert('Erreur lors de la suppression.'); }, complete: function() { closeConfirmModal(); } }); }); // Confirmer et renvoyer via AJAX $('#confirmResendBtn').on('click', function() { if (!pendingResendId) return; var releaseId = pendingResendId; var filename = pendingResendFilename; var $button = $(this); var originalText = $button.text(); $button.text('Envoi...'); $.ajax({ url: '/autopost/resend/' + releaseId, type: 'POST', success: function(data) { showToast(data.message || 'Renvoi effectué avec succès'); console.log('Renvoi réussi pour', filename); }, error: function(xhr) { var errorMsg = 'Erreur lors du renvoi'; if (xhr.responseJSON && xhr.responseJSON.error) { errorMsg = xhr.responseJSON.error; } alert(errorMsg); console.error('Erreur renvoi:', xhr.responseJSON); }, complete: function() { $button.text(originalText); closeConfirmResendModal(); } }); }); // Affichage log $(document).on('click', '.log-link', function(e) { e.preventDefault(); var filename = $(this).data('filename'); $.ajax({ url: '/autopost/log', type: 'GET', data: { name: filename }, dataType: 'json', success: function(data) { $('#logContent').html(data.content); $('#logModal').removeClass('hidden').fadeIn(); }, error: function() { alert('Erreur lors du chargement du fichier log.'); } }); }); // Affichage mediainfo $(document).on('click', '.mediainfo-link', function(e) { e.preventDefault(); var filename = $(this).data('filename'); $.ajax({ url: '/autopost/mediainfo', type: 'GET', data: { name: filename }, dataType: 'json', success: function(data) { $('#mediainfoContent').text(data.content); $('#mediainfoModal').removeClass('hidden').fadeIn(); }, error: function() { alert('Erreur lors du chargement du fichier mediainfo.'); } }); }); // Fermeture modales (croix + clic overlay) $('.close').click(function() { $(this).closest('.fixed').fadeOut(function() { $(this).addClass('hidden'); }); }); $('.fixed').click(function(e) { if (e.target === this) { $(this).fadeOut(function() { $(this).addClass('hidden'); }); } }); // Edition: submit $('#editForm').submit(function(e) { e.preventDefault(); var releaseId = $('#releaseId').val(); var newStatus = $('#statusSelect').val(); $.ajax({ url: '/autopost/edit/' + releaseId, type: 'POST', data: { status: newStatus }, success: function() { var statusText = ''; var statusClass = ''; switch (parseInt(newStatus)) { case 0: statusText = 'EN ATTENTE'; statusClass = 'bg-cyan-500 text-black font-bold'; break; case 1: statusText = 'ENVOI TERMINÉ'; statusClass = 'bg-green-300 text-black font-bold'; break; case 2: statusText = 'ERREUR'; statusClass = 'bg-red-300 text-black font-bold'; break; case 3: statusText = 'DEJA DISPONIBLE';statusClass = 'bg-pink-300 text-black font-bold'; break; case 4: statusText = 'EN COURS'; statusClass = 'bg-yellow-300 text-black font-bold'; break; default: statusText = 'INCONNU'; } var row = $('#row-' + releaseId); row.find('.status-text') .removeClass('bg-cyan-500 bg-green-300 bg-red-300 bg-pink-300 bg-yellow-300') .addClass(statusClass) .text(statusText); $('#editModal').fadeOut(function() { $(this).addClass('hidden'); }); }, error: function() { alert('Erreur lors de la mise à jour.'); } }); }); // Bouton copier Mediainfo $('#copyMediainfoBtn').on('click', function() { const content = document.getElementById('mediainfoContent').textContent; navigator.clipboard.writeText(content).then(() => { this.textContent = '✅ Copié !'; setTimeout(() => this.textContent = '📋 Copier JSON', 2000); }).catch(function(err) { alert('Erreur lors de la copie : ' + err); }); }); // Renvoi vers le site principal $(document).on('click', '.resend-link', function(e) { e.preventDefault(); var releaseId = $(this).data('id'); var filename = $(this).data('filename'); openConfirmResendModal(releaseId, filename); }); // ==================== GESTION SÉLECTION MULTIPLE ==================== function updateBulkActions() { const selectedCheckboxes = $('.row-checkbox:checked'); const count = selectedCheckboxes.length; if (count > 0) { $('#bulkActions').removeClass('hidden'); $('#selectedCount').text(count + ' élément' + (count > 1 ? 's' : '') + ' sélectionné' + (count > 1 ? 's' : '')); } else { $('#bulkActions').addClass('hidden'); } // Mettre à jour la checkbox "Tout sélectionner" const totalCheckboxes = $('.row-checkbox').length; const allChecked = count === totalCheckboxes && totalCheckboxes > 0; const someChecked = count > 0 && count < totalCheckboxes; $('#selectAll').prop('checked', allChecked); $('#selectAll').prop('indeterminate', someChecked); } // Checkbox "Tout sélectionner" $(document).on('click', '#selectAll', function() { const isChecked = $(this).is(':checked'); $('.row-checkbox').prop('checked', isChecked); updateBulkActions(); }); // Checkboxes individuelles $(document).on('click', '.row-checkbox', function() { updateBulkActions(); }); // Bouton "Éditer la sélection" $(document).on('click', '#bulkEditBtn', function() { const selectedCheckboxes = $('.row-checkbox:checked'); const count = selectedCheckboxes.length; if (count === 0) return; $('#bulkEditCount').text(count); $('#bulkEditModal').removeClass('hidden').fadeIn(120); }); // Bouton "Supprimer la sélection" $(document).on('click', '#bulkDeleteBtn', function() { const selectedCheckboxes = $('.row-checkbox:checked'); const count = selectedCheckboxes.length; if (count === 0) return; // Construire la liste des éléments à supprimer let listHTML = ''; selectedCheckboxes.each(function() { const name = $(this).data('name'); const id = $(this).data('id'); listHTML += `
• ${name} (ID: ${id})
`; }); $('#bulkDeleteCount').text(count); $('#bulkDeleteList').html(listHTML); $('#bulkDeleteModal').removeClass('hidden').fadeIn(120); }); // Fermeture des modals en lot $('#bulkEditClose, #bulkDeleteClose, #cancelBulkDeleteBtn').on('click', function() { $('#bulkEditModal, #bulkDeleteModal').fadeOut(120, function() { $(this).addClass('hidden'); }); }); // Clic sur l'overlay pour fermer les modals en lot $('#bulkEditModal, #bulkDeleteModal').on('click', function(e) { if (e.target === this) { $(this).fadeOut(120, function() { $(this).addClass('hidden'); }); } }); // Soumission du formulaire d'édition en lot $('#bulkEditForm').on('submit', function(e) { e.preventDefault(); const selectedCheckboxes = $('.row-checkbox:checked'); const newStatus = $('#bulkStatusSelect').val(); const ids = []; selectedCheckboxes.each(function() { ids.push($(this).data('id')); }); if (ids.length === 0) return; const $submitBtn = $('#bulkEditForm button[type="submit"]'); const originalText = $submitBtn.text(); $submitBtn.text('Mise à jour...').prop('disabled', true); $.ajax({ url: '/autopost/bulk-edit', type: 'POST', data: { ids: ids, status: newStatus, _csrf: window.__BOOTSTRAP__.csrf }, success: function(data) { // Mettre à jour l'interface pour chaque ligne modifiée ids.forEach(function(id) { updateRowStatus(id, newStatus); }); showToast(`${ids.length} élément(s) mis à jour`); // Fermer la modal et désélectionner $('#bulkEditModal').fadeOut(120, function() { $(this).addClass('hidden'); }); $('.row-checkbox').prop('checked', false); updateBulkActions(); }, error: function(xhr) { let errorMsg = 'Erreur lors de la mise à jour en lot'; if (xhr.responseJSON && xhr.responseJSON.error) { errorMsg = xhr.responseJSON.error; } alert(errorMsg); }, complete: function() { $submitBtn.text(originalText).prop('disabled', false); } }); }); // Confirmation de suppression en lot $('#confirmBulkDeleteBtn').on('click', function() { const selectedCheckboxes = $('.row-checkbox:checked'); const ids = []; selectedCheckboxes.each(function() { ids.push($(this).data('id')); }); if (ids.length === 0) return; const $button = $(this); const originalText = $button.text(); $button.text('Suppression...').prop('disabled', true); $.ajax({ url: '/autopost/bulk-delete', type: 'POST', data: { ids: ids, _csrf: window.__BOOTSTRAP__.csrf }, success: function(data) { // Supprimer visuellement les lignes ids.forEach(function(id) { $('#row-' + id) .css('outline', '2px solid rgba(239,68,68,0.6)') .fadeOut('300', function(){ $(this).remove(); }); }); showToast(`${ids.length} élément(s) supprimé(s)`); // Fermer la modal $('#bulkDeleteModal').fadeOut(120, function() { $(this).addClass('hidden'); }); updateBulkActions(); }, error: function(xhr) { let errorMsg = 'Erreur lors de la suppression en lot'; if (xhr.responseJSON && xhr.responseJSON.error) { errorMsg = xhr.responseJSON.error; } alert(errorMsg); }, complete: function() { $button.text(originalText).prop('disabled', false); } }); }); // Fonction utilitaire pour mettre à jour le statut d'une ligne function updateRowStatus(id, newStatus) { const $row = $('#row-' + id); const $statusCell = $row.find('.status-text'); let statusText = ''; let statusClass = ''; switch (parseInt(newStatus)) { case 0: statusText = 'EN ATTENTE'; statusClass = 'bg-cyan-500 text-black font-bold'; break; case 1: statusText = 'ENVOI TERMINÉ'; statusClass = 'bg-green-300 text-black font-bold'; break; case 2: statusText = 'ERREUR'; statusClass = 'bg-red-300 text-black font-bold'; break; case 3: statusText = 'DEJA DISPONIBLE'; statusClass = 'bg-pink-300 text-black font-bold'; break; case 4: statusText = 'EN COURS'; statusClass = 'bg-yellow-300 text-black font-bold'; break; default: statusText = 'INCONNU'; } $statusCell.removeClass().addClass('px-4 py-2 border border-gray-700 status-text whitespace-nowrap ' + statusClass); $statusCell.text(statusText); $row.attr('data-status', newStatus); } });