// public/js/historiqueParcours.js // --- debounce pour éviter de spam + erreur de la base --- function debounce(fn, delay = 300) { let t; function wrapped(...args) { clearTimeout(t); t = setTimeout(() => fn(...args), delay); } wrapped.cancel = () => clearTimeout(t); return wrapped; } 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; // Exports CSV/XLSX $("#exportCSV").on("click", function () { const dt = $("#historiqueParcours").DataTable(); const payload = { mode: "full", // export total search: { value: "" }, columns: dt.settings()[0].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, dir })) }; 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(); }) .catch(() => displayError("Export CSV (complet) impossible")); }); $("#exportCSVFilter").on("click", function () { const dt = $("#historiqueParcours").DataTable(); const payload = { mode: "filtered", // export avec les filtres/colonnes/tri actuels search: { value: dt.search() || "" }, // recherche globale columns: dt.settings()[0].aoColumns.map((c, i) => ({ data: i, search: { value: dt.column(i).search() || "" } // filtres par colonne })), order: dt.order().map(([col, dir]) => ({ column: col, dir })) }; 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(); }) .catch(() => displayError("Export CSV (filtré) impossible")); }); $("#exportXlxs").on("click", function () { const dt = $("#historiqueParcours").DataTable(); const payload = { mode: "full", search: { value: "" }, columns: dt.settings()[0].aoColumns.map((c, i) => ({ data: i, search: { value: "" } })), order: dt.order().map(([col, dir]) => ({ column: col, dir })) }; 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(); }) .catch(() => displayError("Export XLS (complet) impossible")); }); $("#exportXlxsFilter").on("click", function () { const dt = $("#historiqueParcours").DataTable(); const payload = { mode: "filtered", // export avec les filtres/colonnes/tri actuels search: { value: dt.search() || "" }, // recherche globale columns: dt.settings()[0].aoColumns.map((c, i) => ({ data: i, search: { value: dt.column(i).search() || "" } // filtres par colonne })), order: dt.order().map(([col, dir]) => ({ column: col, dir })) }; 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(); }) .catch(() => displayError("Export XLS (filtré) impossible")); }); // 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(); fetch("/historiqueParcours/datatable", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), signal: inflightController.signal, }) .then(res => res.json()) .then(payload => { callback({ draw: payload.draw, recordsTotal: payload.recordsTotal, recordsFiltered: payload.recordsTotal, data: payload.data }); }) .catch(err => { if (err && err.name === "AbortError") return; 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"]'); $globalInput.off('.DT'); // nettoie handlers datatables const debouncedGlobal = debounce((v) => { api.search(v); api.ajax.reload(); }, 350); $globalInput.on('input keyup keydown', function () { debouncedGlobal(this.value); }); // 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-uk", targets: 10 }, { type: "date-eu", targets: 4 }, { 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 = "#2ecc71"; 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('