// public/js/historiqueParcours.js document.addEventListener("DOMContentLoaded", async function () { // Récupération du token const token = localStorage.getItem("jwtToken"); if (!token) { throw new Error("Aucun token trouvé dans le localStorage."); } const userData = parseJwt(token); if (!userData) { displayError("Erreur lors de l'extraction des données utilisateur à partir du token."); return; } // Initialiser DataTables en mode server-side (obligé pour pagination) const table = initServerSideDataTable(); // Variable pour suivre l'état des exports let isExporting = false; // Fonction pour désactiver/activer les boutons d'export function setExportButtonsState(disabled) { isExporting = disabled; const buttons = ["#exportCSV", "#exportCSVFilter", "#exportXlxs", "#exportXlxsFilter"]; buttons.forEach(selector => { const $btn = $(selector); $btn.prop("disabled", disabled); if (disabled) { $btn.css("opacity", "0.5"); $btn.css("cursor", "not-allowed"); } else { $btn.css("opacity", "1"); $btn.css("cursor", "pointer"); } }); } // Exports CSV/XLSX $("#exportCSV").on("click", function () { if (isExporting) return; const dt = $("#historiqueParcours").DataTable(); if (!dt) { displayError("Impossible d'accéder à la table de données."); return; } const settings = dt.settings()[0]; if (!settings || !settings.aoColumns) { displayError("Structure de données invalide."); return; } setExportButtonsState(true); const payload = { mode: "full", // export total search: { value: "" }, columns: settings.aoColumns.map((c, i) => ({ data: i, search: { value: "" } })), // on garde l'ordre actuel pour la récuperation order: (dt.order() || []).map(([col, dir]) => ({ column: col || 0, dir: dir || "asc" })) }; fetch("/historiqueParcours/export/csv", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }) .then(resp => { if (!resp.ok) throw new Error("Export CSV (complet) impossible"); return resp.blob(); }) .then(blob => { const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = "historique_parcours_complet.csv"; document.body.appendChild(a); a.click(); URL.revokeObjectURL(url); a.remove(); setExportButtonsState(false); }) .catch((err) => { displayError("Export CSV (complet) impossible"); setExportButtonsState(false); }); }); $("#exportCSVFilter").on("click", function () { if (isExporting) return; const dt = $("#historiqueParcours").DataTable(); if (!dt) { displayError("Impossible d'accéder à la table de données."); return; } const settings = dt.settings()[0]; if (!settings || !settings.aoColumns) { displayError("Structure de données invalide."); return; } setExportButtonsState(true); const payload = { mode: "filtered", // export avec les filtres/colonnes/tri actuels search: { value: dt.search() || "" }, // recherche globale columns: settings.aoColumns.map((c, i) => ({ data: i, search: { value: dt.column(i).search() || "" } // filtres par colonne })), order: (dt.order() || []).map(([col, dir]) => ({ column: col || 0, dir: dir || "asc" })) }; fetch("/historiqueParcours/export/csv", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }) .then(resp => { if (!resp.ok) throw new Error("Export CSV (filtré) impossible"); return resp.blob(); }) .then(blob => { const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = "historique_parcours_filtre.csv"; document.body.appendChild(a); a.click(); URL.revokeObjectURL(url); a.remove(); setExportButtonsState(false); }) .catch((err) => { displayError("Export CSV (filtré) impossible"); setExportButtonsState(false); }); }); $("#exportXlxs").on("click", function () { if (isExporting) return; const dt = $("#historiqueParcours").DataTable(); if (!dt) { displayError("Impossible d'accéder à la table de données."); return; } const settings = dt.settings()[0]; if (!settings || !settings.aoColumns) { displayError("Structure de données invalide."); return; } setExportButtonsState(true); const payload = { mode: "full", search: { value: "" }, columns: settings.aoColumns.map((c, i) => ({ data: i, search: { value: "" } })), order: (dt.order() || []).map(([col, dir]) => ({ column: col || 0, dir: dir || "asc" })) }; fetch("/historiqueParcours/export/xls", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }) .then(resp => { if (!resp.ok) throw new Error(); return resp.blob(); }) .then(blob => { const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = "historique_parcours_complet.xls"; document.body.appendChild(a); a.click(); URL.revokeObjectURL(url); a.remove(); setExportButtonsState(false); }) .catch((err) => { displayError("Export XLS (complet) impossible"); setExportButtonsState(false); }); }); $("#exportXlxsFilter").on("click", function () { if (isExporting) return; const dt = $("#historiqueParcours").DataTable(); if (!dt) { displayError("Impossible d'accéder à la table de données."); return; } const settings = dt.settings()[0]; if (!settings || !settings.aoColumns) { displayError("Structure de données invalide."); return; } setExportButtonsState(true); const payload = { mode: "filtered", // export avec les filtres/colonnes/tri actuels search: { value: dt.search() || "" }, // recherche globale columns: settings.aoColumns.map((c, i) => ({ data: i, search: { value: dt.column(i).search() || "" } // filtres par colonne })), order: (dt.order() || []).map(([col, dir]) => ({ column: col || 0, dir: dir || "asc" })) }; fetch("/historiqueParcours/export/xls", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }) .then(resp => { if (!resp.ok) throw new Error("Export XLS (filtré) impossible"); return resp.blob(); }) .then(blob => { const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = "historique_parcours_filtre.xls"; document.body.appendChild(a); a.click(); URL.revokeObjectURL(url); a.remove(); setExportButtonsState(false); }) .catch((err) => { displayError("Export XLS (filtré) impossible"); setExportButtonsState(false); }); }); // Délégation pour la génération de projet $("#historiqueParcours").on("click", "button#btnGenerate", function () { const numParcours = $(this).data("num-parcours"); const produit = $(this).data("produit"); generateProject(numParcours, produit); }); }); /* ========================= * Helpers spécifiques server-side * ========================= */ // Initialisation DataTables en server-side (recherche globale + par colonnes + tri + pagination) function initServerSideDataTable() { let inflightController = null; const table = $("#historiqueParcours").DataTable({ processing: true, serverSide: true, searching: true, paging: true, orderCellsTop: true, fixedHeader: true, responsive: { details: false }, pageLength: 10, retrieve: true, order: [[0, "desc"]], searchDelay: 350, language: { search: "Rechercher", lengthMenu: "Afficher _MENU_ entrées par page", info: "Affichage de _START_ à _END_ sur _TOTAL_ entrées", infoEmpty: "Affichage de 0 à 0 sur 0 entrée", infoFiltered: "(filtré de _MAX_ entrées au total)", paginate: { first: "Début", previous: "Précédent", next: "Suivant", last: "Fin" }, }, ajax: function (data, callback) { const body = { draw: data.draw || 1, start: data.start || 0, length: data.length || 10, order: data.order || [], columns: data.columns || [], search: data.search || { value: "" }, }; if (inflightController) inflightController.abort(); // action en cours inflightController = new AbortController(); const currentController = inflightController; fetch("/historiqueParcours/datatable", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), signal: currentController.signal, }) .then(res => { // Vérifier si la requête a été annulée if (currentController.signal.aborted || inflightController !== currentController) { return null; } if (!res.ok) throw new Error(`HTTP ${res.status}`); return res.json(); }) .then(payload => { // Vérifier si la requête a été annulée if (currentController.signal.aborted || inflightController !== currentController) { return; } if (!payload || typeof payload !== 'object') { throw new Error("Réponse invalide du serveur"); } callback({ draw: payload.draw || 0, recordsTotal: payload.recordsTotal || 0, recordsFiltered: payload.recordsFiltered || payload.recordsTotal || 0, data: Array.isArray(payload.data) ? payload.data : [] }); }) .catch(err => { // Ignorer silencieusement toutes les erreurs d'abort if (err && (err.name === "AbortError" || err.name === "DOMException")) { return; } // Vérifier aussi si le signal a été aborted if (currentController.signal.aborted || inflightController !== currentController) { return; } // Seulement afficher une erreur si ce n'est PAS un abort displayError("Failed to fetch data. Please try again later."); callback({ draw: 0, recordsTotal: 0, recordsFiltered: 0, data: [] }); }); }, initComplete: function () { const api = this.api(); // Recherche globale : debounce y compris ENTER (plus de bypass immédiat) const $globalInput = $('div.dataTables_filter input[type="search"]'); const $filterLabel = $globalInput.closest('label'); $globalInput.off('.DT'); // nettoie handlers datatables const debouncedGlobal = debounce((v) => { api.search(v); api.ajax.reload(); }, 350); // Fonction pour gérer l'affichage du texte "Rechercher" function toggleSearchPlaceholder() { if ($globalInput.val().trim() !== '') { $filterLabel.addClass('has-value'); } else { $filterLabel.removeClass('has-value'); } } $globalInput.on('input keyup keydown', function () { debouncedGlobal(this.value); toggleSearchPlaceholder(); }); // Vérifier l'état initial toggleSearchPlaceholder(); // Recherche par colonne avec DEBOUNCE (ENTER inclus) const debouncedColSearch = debounce((i, val) => { api.column(i).search(val); api.ajax.reload(); }, 350); $("#historiqueParcours thead tr:eq(1) th").each(function (i) { $("input", this).on("input keyup keydown change", function () { debouncedColSearch(i, this.value); }); }); api.on("responsive-resize", function (e, datatable, columns) { for (let i = 0; i < columns.length; i++) { if (columns[i]) { $(api.column(i).header()).show(); $(api.column(i).footer()).show(); $($("#historiqueParcours thead tr:eq(1) th")[i]).show(); } else { $(api.column(i).header()).hide(); $(api.column(i).footer()).hide(); $($("#historiqueParcours thead tr:eq(1) th")[i]).hide(); } } }); $("#divToggleSearch").on("click", function () { const $row = $("#historiqueParcours thead tr:eq(1)"); $row.toggle(); const $btn = $("#toggleSearch"); if ($row.is(":visible")) { $btn.text("ENLEVER LA RECHERCHE PAR COLONNE"); } else { $btn.text("ACTIVER LA RECHERCHE PAR COLONNE"); } }); // Cacher la 2e ligne au départ $("#historiqueParcours thead tr:eq(1)").hide(); }, // --- Ajout bouton détails en 1re colonne columnDefs: [ { targets: 0, render: function (data, type, row) { const np = String(data ?? ""); const numPacours = np.replace(/&/g,"&").replace(//g,">"); return '' + '' + '' + numPacours + '' + ''; } }, { type: "date-eu", targets: 1 }, // Date de Création (colonne 1) { // Appliquer la classe nc-value aux cellules contenant "NC" targets: "_all", createdCell: function (td, cellData, rowData, row, col) { // Exclure la première colonne (bouton détails) et les deux dernières (boutons) if (col !== 0 && col < rowData.length - 2) { const cellText = String(cellData || "").trim(); if (cellText === "NC") { td.classList.add("nc-value"); } } } }, { responsivePriority: 1, targets: -1 }, { responsivePriority: 2, targets: -2 } ], }); // --- Toggle des détails $('#historiqueParcours tbody').on('click', '.btn-row-details', function (e) { e.preventDefault(); e.stopPropagation(); const api = $('#historiqueParcours').DataTable(); const $tr = $(this).closest('tr'); // si c'est une ligne enfant (responsive), remonter à la parent const row = api.row($tr.hasClass('child') ? $tr.prev() : $tr); if (row.child.isShown()) { row.child.hide(); // change icône this.textContent = "?"; this.style.background = "#e74c3c"; return; } // récupérer le numParcours depuis la 1re cellule const raw = row.data()?.[0] ?? ""; const tmp = document.createElement('div'); tmp.innerHTML = String(raw); const numParcours = tmp.textContent.replace("?", "").trim(); if (!numParcours) return; row.child('