// 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('
Chargement des détails…
').show(); this.textContent = "–"; this.style.background = "#27ae60"; fetch(`/historiqueParcours/details/${encodeURIComponent(numParcours)}`) .then(r => r.json()) .then(payload => { if (!payload?.valid) { row.child('
Impossible de charger les détails.
'); this.textContent = "+"; this.style.background = "#2ecc71"; return; } row.child(formatDetailsPanel(payload)); }) .catch(() => { row.child('
Erreur de chargement.
'); this.textContent = "+"; this.style.background = "#2ecc71"; }); }); return table; } /* ========================= * Fonctions annexes importantes * ========================= */ async function fetchUserDetails(matriculeUser) { try { const response = await fetch(`/user/read/matricule/${matriculeUser}`); const data = await response.json(); return data.valid ? data : null; } catch (error) { displayError(`Erreur lors de la récupération du contrat avec le matricule ${matriculeUser} :`, error); return null; } } async function generateProject(numParcours, produit) { try { const response = await fetch(`/generate/${produit}/projet/${numParcours}`, { method: "POST", headers: { "Content-Type": "application/json" }, }); if (!response.ok) throw new Error("Erreur réseau ou serveur"); const disposition = response.headers.get("content-disposition"); const filename = disposition.split(";")[1].trim().split("=")[1]; const blob = await response.blob(); const url = window.URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); window.URL.revokeObjectURL(url); a.remove(); } catch (error) { console.error("Erreur lors de la génération du projet:", error); } } // Décoder le JWT function parseJwt(token) { try { const base64Url = token.split(".")[1]; const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/"); const jsonPayload = decodeURIComponent( atob(base64) .split("") .map(function (c) { return "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2); }) .join("") ); return JSON.parse(jsonPayload); } catch (error) { console.error("Erreur lors du décodage du token:", error); return null; } } function displayError(message) { const errorElement = document.getElementById("error"); errorElement.textContent = message; errorElement.style.display = "block"; } // ---------- Helpers rendu ---------- // kv : rend l'HTML pour l'affichage des données détails function kv(label, value) { const v = (value === true) ? "Oui" : (value === false) ? "Non" : (value ?? "NC"); return `
${label} :
${v}
`; } //formatDetailsPanel : rend l'HTML pour la récuperation des données par parcours function formatDetailsPanel(payload) { const prodKey = (payload.produit || "").toUpperCase(); const prod = payload.produitRecord || null; let body = ""; if (prodKey === "TPPC") body = sectionTPPC(prod); else if (prodKey === "RC") body = sectionRC(prod); else if (prodKey === "FAC") body = sectionFAC(prod); else body = `Produit non renseigné.`; return `
Détails ${prodKey || ""}
${body}
`; } //fmtDate : formatage des Dates (sections détails) function fmtDate(iso, withTime=true) { if (!iso) return "NC"; const d = new Date(iso); const dd = String(d.getDate()).padStart(2,"0"); const mm = String(d.getMonth()+1).padStart(2,"0"); const yyyy = d.getFullYear(); if (!withTime) return `${dd}/${mm}/${yyyy}`; const hh = String(d.getHours()).padStart(2,"0"); const mi = String(d.getMinutes()).padStart(2,"0"); return `${dd}/${mm}/${yyyy} ${hh}:${mi}`; } //gridWrap2cols : rend les détails sur 2 colonnes function gridWrap2cols(innerLeft, innerRight) { return `
${innerLeft}${innerRight}
`; } // ---------- Sections produit ---------- //sectionTTPC : récupere les détails d'un parcours TPPC pour le mettre en forme (2 colonnes) function sectionTPPC(produit) { if (!produit) return `Fiche TPPC introuvable.`; const tarif = produit?.["@expand"]?.tarif || null; // tppctarif const projet = produit?.["@expand"]?.projet || null; // tppcprojet const garant = Array.isArray(produit.garanties) ? produit.garanties.join(", ") : (produit.garanties || "NC"); // 6 infos, affichées en 2 colonnes (3 / colonne) const gauche = [ kv("Activité assurée", produit.actAssuree), kv("Nombre de véhicules", produit.nbVehic), kv("Garanties", garant), ].join(""); const droite = [ kv("Prime HT", produit.primeHT), kv("Tarif - Référence", tarif?.tarifRef), kv("Projet - Dates", `Effet: ${projet?.dateEffet || "NC"} / Échéance: ${projet?.dateEcheance || "NC"} / Fin: ${projet?.dateFin || "NC"}`), ].join(""); return gridWrap2cols(gauche, droite); } //sectionRC : récupere les détails d'un parcours RC pour le mettre en forme (2 colonnes) function sectionRC(produit) { if (!produit) return `Fiche RC introuvable.`; //nombre d'éléments incorrect, il faut revoir quand parcours RC fait const gauche = [ kv("Dates", `Effet: ${produit.dateEffet || "NC"} / Échéance: ${produit.dateEcheance || "NC"} / Fin: ${produit.dateFin || "NC"}`), kv("Tempo", produit.tempo), kv("Type cotisation", produit.typeCot), kv("Chiffre d'affaires", produit.ca), kv("Taux RCC HT / TTC", `${produit.tauxRCCHT ?? "NC"} / ${produit.tauxRCCTTC ?? "NC"}`), kv("Taux RCE HT / TTC", `${produit.tauxRCEHT ?? "NC"} / ${produit.tauxRCETTC ?? "NC"}`), kv("Taux total HT / TTC", `${produit.tauxTotalHT ?? "NC"} / ${produit.tauxTotalTTC ?? "NC"}`), kv("Cotisations RCC HT / TTC", `${produit.cotRCCHT ?? "NC"} / ${produit.cotRCCTTC ?? "NC"}`), kv("Cotisations RCE HT / TTC", `${produit.cotRCEHT ?? "NC"} / ${produit.cotRCETTC ?? "NC"}`), kv("PJ", produit.pj), kv("Programme international", produit.programmeInternationale), kv("Participation résultat", produit.participationResultat), kv("Cot. totale HT / TTC", `${produit.cotTotalHT ?? "NC"} / ${produit.cotTotalTTC ?? "NC"}`), kv("Cot. frais HT / TTC", `${produit.cotFraisHT ?? "NC"} / ${produit.cotFraisTTC ?? "NC"}`), kv("Cot. irréductible", produit.cotIrreductible), ].join(""); const droite = [ kv("Act. Voiturier", produit.actVoiturier), kv("Act. Loueur", produit.actLoueur), kv("Act. Multimodal", produit.actMultimodal), kv("Act. Douane", produit.actDouane), kv("Act. Levageur", produit.actLevageur), kv("Act. Transitaire", produit.actTransitaire), kv("March. Roulant", produit.marRoulant), kv("March. Engins", produit.marEngins), kv("March. Perissable", produit.marPerissable), kv("Zone 1..6", `1:${produit.zone1?"Oui":"Non"} 2:${produit.zone2?"Oui":"Non"} 3:${produit.zone3?"Oui":"Non"} 4:${produit.zone4?"Oui":"Non"} 5:${produit.zone5?"Oui":"Non"} 6:${produit.zone6?"Oui":"Non"}`), ].join(""); return gridWrap2cols(gauche, droite); } //sectionFAC : récupere les détails d'un parcours FAC pour le mettre en forme (1 colonnes) function sectionFAC(produit) { if (!produit) return `Fiche FAC introuvable.`; //nombre d'éléments incorrect, il faut revoir quand parcours RC fait const gauche = [ kv("Zones 1..6", `1:${produit.zone1?"Oui":"Non"} 2:${produit.zone2?"Oui":"Non"} 3:${produit.zone3?"Oui":"Non"} 4:${produit.zone4?"Oui":"Non"} 5:${produit.zone5?"Oui":"Non"} 6:${produit.zone6?"Oui":"Non"}`), kv("Terrestre", produit.terrestre), kv("Maritime", produit.maritime), kv("Aérien", produit.aerien), kv("Postal", produit.postal), kv("Fluvial", produit.fluvial), kv("Type TPPC", produit.typeTPPC), kv("Capital max / expo / expédition / colis / TPPC", `${produit.capitalMax ?? "NC"} / ${produit.capitalExpo ?? "NC"} / ${produit.capitalExped ?? "NC"} / ${produit.capitalColis ?? "NC"} / ${produit.capitalTPPC ?? "NC"}`), kv("Franchise Transport / Expo / TPPC", `${produit.franchiseTransport ?? "NC"} / ${produit.franchiseExpo ?? "NC"} / ${produit.franchiseTPPC ?? "NC"}`), ].join(""); const droite = [ kv("Taux RO / RG", `${produit.tauxCotRO ?? "NC"} / ${produit.tauxCotRG ?? "NC"}`), kv("CA", produit.ca), kv("Cot. annuelle HT / TTC", `${produit.cotAnnuelleHT ?? "NC"} / ${produit.cotAnnuelleTTC ?? "NC"}`), kv("Cot. provisoire RO / RG", `${produit.cotProvRO ?? "NC"} / ${produit.cotProvRG ?? "NC"}`), kv("Cot. irréductible", produit.cotIrred), kv("Cot. RO / RG / Comptant", `${produit.cotRO ?? "NC"} / ${produit.cotRG ?? "NC"} / ${produit.cotComptant ?? "NC"}`), kv("Dates", `Effet: ${produit.dateEffet || "NC"} / Échéance: ${produit.dateEcheance || "NC"} / Fin: ${produit.dateFin || "NC"}`), kv("Act. assurée", produit.actAssure), kv("Type marchandises", produit.typeMar), ].join(""); return gridWrap2cols(gauche, droite); }