// 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(); // Exports CSV/XLSX $("#exportCSV").on("click", function () { 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; } 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(); }) .catch(() => displayError("Export CSV (complet) impossible")); }); $("#exportCSVFilter").on("click", function () { 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; } 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(); }) .catch(() => displayError("Export CSV (filtré) impossible")); }); $("#exportXlxs").on("click", function () { 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; } 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(); }) .catch(() => displayError("Export XLS (complet) impossible")); }); $("#exportXlxsFilter").on("click", function () { 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; } 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(); }) .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 => { if (!res.ok) throw new Error(`HTTP ${res.status}`); return res.json(); }) .then(payload => { 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 => { 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-eu", targets: 1 }, // Date de Création (colonne 1) { 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('
Chargement des détails…
').show(); this.textContent = "?"; this.style.background = "#c0392b"; fetch(`/historiqueParcours/details/${encodeURIComponent(numParcours)}`) .then(r => { if (!r.ok) throw new Error(`HTTP ${r.status}`); return r.json(); }) .then(payload => { if (!payload || typeof payload !== 'object') { throw new Error("Réponse invalide"); } if (!payload.valid) { row.child(createMessageBox('error', 'Impossible de charger les détails', 'Une erreur est survenue lors du chargement des détails du parcours.')); this.textContent = "?"; this.style.background = "#e74c3c"; return; } row.child(formatDetailsPanel(payload)); }) .catch(() => { row.child(createMessageBox('error', 'Erreur de chargement', 'Une erreur réseau est survenue lors du chargement des détails.')); this.textContent = "?"; this.style.background = "#e74c3c"; }); }); 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 { if (!numParcours || !produit) { displayError("Paramètres manquants pour la génération du projet."); return; } const response = await fetch(`/generate/${produit}/projet/${encodeURIComponent(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"); let filename = "projet.docx"; if (disposition) { const parts = disposition.split(";"); if (parts.length > 1) { const filenamePart = parts[1].trim(); if (filenamePart.startsWith("filename=")) { filename = filenamePart.split("=")[1]?.replace(/"/g, "") || filename; } } } const blob = await response.blob(); if (!blob || blob.size === 0) { throw new Error("Fichier vide reçu"); } 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) { displayError("Erreur lors de la génération du projet : " + (error?.message || "Erreur inconnue")); } } // ---------- Helpers rendu ---------- // Les fonctions kv, fmtDate, gridWrap2cols, debounce, parseJwt et displayError sont dans global.js //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; const contrat = payload.contrat || null; let body = ""; if (prodKey === "TPPC") body = sectionTPPC(prod, contrat); else if (prodKey === "RC") body = sectionRC(prod, contrat); else if (prodKey === "FAC") body = sectionFAC(prod, contrat); else body = createMessageBox('warn', 'Produit non renseigné', 'Le type de produit n\'a pas été spécifié pour ce parcours.'); return `
Détails ${prodKey || ""}
${body}
`; } // ---------- Sections produit ---------- //sectionTTPC : récupere les détails d'un parcours TPPC pour le mettre en forme (2 colonnes) function sectionTPPC(produit, contrat) { if (!produit) { if (contrat) { return createMessageBox('info', 'Informations non disponibles', 'Les informations sur ce Parcours TPPC ne sont pas encore disponibles.
La fiche TPPC n\'a pas encore été créée pour ce parcours.'); } return createMessageBox('error', 'Fiche TPPC introuvable', 'Impossible de récupérer les informations de la fiche TPPC pour ce parcours.'); } 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"); // Extensions de garanties const extensions = []; if (produit.marCiternes) extensions.push("Citernes"); if (produit.marDenreesSousTemp) extensions.push("Denrées sous température"); if (produit.marAnimaux) extensions.push("Animaux vivants"); if (produit.marFranchise) extensions.push("Franchise"); const extensionsStr = extensions.length > 0 ? extensions.join(", ") : "Aucune"; // Type de cotisation et type de révision (couplés) let typeCotStr = "NC"; let typeRevStr = "NC"; if (projet) { if (projet.typeCot) { typeCotStr = projet.typeCot === "revisable" ? "Revisable" : (projet.typeCot === "forfaitaire" ? "Forfaitaire" : projet.typeCot); } if (projet.typeRev) { // Mapping des valeurs possibles du type de révision if (projet.typeRev === "CotCA") { typeRevStr = "Cotisation CA"; } else if (projet.typeRev === "CotFlotte" || projet.typeRev === "FlotteOuverte") { typeRevStr = "Cotisation Flotte"; } else { // Si autre valeur, on l'affiche telle quelle typeRevStr = projet.typeRev; } } else if (projet.typeCot === "revisable") { // Si revisable mais pas de typeRev, on regarde le tarif if (tarif?.typeContrat === "detaillee") { typeRevStr = "Cotisation Flotte"; } } } const gauche = [ kv("Activité assurée", produit.actAssuree), kv("Nombre de véhicules", produit.nbVehic), kv("Garanties", garant), kv("Extensions de garanties", extensionsStr), kv("Type de cotisation", typeCotStr), kv("Type de révision", typeRevStr), ].join(""); // Formatage des dates du projet const dateEffet = projet?.dateEffet ? fmtDate(projet.dateEffet, false) : "NC"; const dateEcheance = projet?.dateEcheance ? fmtDate(projet.dateEcheance, false) : "NC"; const dateFin = projet?.dateFin ? fmtDate(projet.dateFin, false) : "NC"; const droite = [ kv("Prime HT", produit.primeHT), kv("Tarif - Référence", tarif?.tarifRef), kv("Projet - Dates", `Effet: ${dateEffet} / Échéance: ${dateEcheance} / Fin: ${dateFin}`), ].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, contrat) { // Si le produit n'existe pas mais qu'on a un contrat RC, c'est que la fiche n'est pas encore créée if (!produit && contrat) { return createMessageBox('info', 'Informations non disponibles', 'Les informations sur ce Parcours RC ne sont pas encore disponibles.
La fiche RC n\'a pas encore été créée pour ce parcours.'); } // Si le produit n'existe pas et qu'on n'a pas de contrat, cas théorique (ne devrait jamais arriver) if (!produit) { return createMessageBox('dev', 'Fonctionnalité en cours de développement', 'L\'affichage des détails pour les parcours RC n\'est pas encore disponible.
Cette fonctionnalité sera bientôt implémentée.'); } // Construction de l'activité assurée à partir des activités const activites = []; if (produit.actVoiturier) activites.push("Voiturier"); if (produit.actLoueur) activites.push("Loueur"); if (produit.actMultimodal) activites.push("Multimodal"); if (produit.actDouane) activites.push("Douane"); if (produit.actLevageur) activites.push("Levageur"); if (produit.actTransitaire) activites.push("Transitaire"); const activiteAssuree = activites.length > 0 ? activites.join(", ") : "NC"; // Construction des garanties à partir des marchandises const garanties = []; if (produit.marRoulant) garanties.push("Roulant"); if (produit.marEngins) garanties.push("Engins"); if (produit.marPerissable) garanties.push("Périssable"); if (produit.marOrdinaire) garanties.push("Ordinaire"); if (produit.marAnimaux) garanties.push("Animaux"); if (produit.marCiterne) garanties.push("Citerne"); if (produit.marBeton) garanties.push("Béton"); if (produit.marExceptionnels) garanties.push("Exceptionnels"); if (produit.marMobilerUsag) garanties.push("Mobilier usagé"); if (produit.marVrac) garanties.push("Vrac"); if (produit.marRoulantDem) garanties.push("Roulant déménagement"); const garantiesStr = garanties.length > 0 ? garanties.join(", ") : "NC"; // Zones const zones = []; if (produit.zone1) zones.push("1"); if (produit.zone2) zones.push("2"); if (produit.zone3) zones.push("3"); if (produit.zone4) zones.push("4"); if (produit.zone5) zones.push("5"); if (produit.zone6) zones.push("6"); const zonesStr = zones.length > 0 ? zones.join(", ") : "NC"; // Type de cotisation let typeCotStr = "NC"; if (produit.typeCot) { typeCotStr = produit.typeCot === "revisable" ? "Revisable" : (produit.typeCot === "forfaitaire" ? "Forfaitaire" : produit.typeCot); } // Formatage des dates const dateEffet = produit.dateEffet ? fmtDate(produit.dateEffet, false) : "NC"; const dateEcheance = produit.dateEcheance ? fmtDate(produit.dateEcheance, false) : "NC"; const dateFin = produit.dateFin ? fmtDate(produit.dateFin, false) : "NC"; // Vérifier si on a un tarif (pas disponible pour RC actuellement) const tarif = produit?.["@expand"]?.tarif || null; const tarifRef = tarif?.tarifRef || "en cours de développement"; const gauche = [ kv("Activité assurée", activiteAssuree), kv("Garanties", garantiesStr), kv("Zones", zonesStr), kv("Type de cotisation", typeCotStr), kv("Tempo", produit.tempo || "NC"), kv("Chiffre d'affaires", produit.ca || "NC"), ].join(""); const droite = [ kv("Tarif - Référence", tarifRef), kv("Cot. totale HT / TTC", `${produit.cotTotalHT ?? "NC"} / ${produit.cotTotalTTC ?? "NC"}`), kv("Dates", `Effet: ${dateEffet} / Échéance: ${dateEcheance} / Fin: ${dateFin}`), ].join(""); return gridWrap2cols(gauche, droite); } //sectionFAC : récupere les détails d'un parcours FAC pour le mettre en forme (2 colonnes) function sectionFAC(produit, contrat) { // Si le produit n'existe pas mais qu'on a un contrat FAC, c'est que la fiche n'est pas encore créée if (!produit && contrat) { return createMessageBox('info', 'Informations non disponibles', 'Les informations sur ce Parcours FAC ne sont pas encore disponibles.
La fiche FAC n\'a pas encore été créée pour ce parcours.'); } // Si le produit n'existe pas et qu'on n'a pas de contrat, cas théorique (ne devrait jamais arriver) if (!produit) { return createMessageBox('dev', 'Fonctionnalité en cours de développement', 'L\'affichage des détails pour les parcours FAC n\'est pas encore disponible.
Cette fonctionnalité sera bientôt implémentée.'); } // Construction des garanties à partir des modes de transport const garanties = []; if (produit.terrestre && produit.terrestre !== "NC" && produit.terrestre !== "") garanties.push("Terrestre"); if (produit.maritime && produit.maritime !== "NC" && produit.maritime !== "") garanties.push("Maritime"); if (produit.aerien && produit.aerien !== "NC" && produit.aerien !== "") garanties.push("Aérien"); if (produit.postal && produit.postal !== "NC" && produit.postal !== "") garanties.push("Postal"); if (produit.fluvial && produit.fluvial !== "NC" && produit.fluvial !== "") garanties.push("Fluvial"); const garantiesStr = garanties.length > 0 ? garanties.join(", ") : "NC"; // Zones const zones = []; if (produit.zone1) zones.push("1"); if (produit.zone2) zones.push("2"); if (produit.zone3) zones.push("3"); if (produit.zone4) zones.push("4"); if (produit.zone5) zones.push("5"); if (produit.zone6) zones.push("6"); const zonesStr = zones.length > 0 ? zones.join(", ") : "NC"; // Type de contrat let typeContratStr = "NC"; if (produit.typeContrat) { typeContratStr = produit.typeContrat; } // Formatage des dates const dateEffet = produit.dateEffet ? fmtDate(produit.dateEffet, false) : "NC"; const dateEcheance = produit.dateEcheance ? fmtDate(produit.dateEcheance, false) : "NC"; const dateFin = produit.dateFin ? fmtDate(produit.dateFin, false) : "NC"; // Vérifier si on a un tarif (pas disponible pour FAC actuellement) const tarif = produit?.["@expand"]?.tarif || null; const tarifRef = tarif?.tarifRef || "en cours de développement"; const gauche = [ kv("Activité assurée", produit.actAssure || "NC"), kv("Garanties", garantiesStr), kv("Zones", zonesStr), kv("Type de contrat", typeContratStr), kv("Tempo", produit.tempo || "NC"), kv("Chiffre d'affaires", produit.ca || "NC"), ].join(""); const droite = [ kv("Tarif - Référence", tarifRef), kv("Cot. annuelle HT / TTC", `${produit.cotAnnuelleHT ?? "NC"} / ${produit.cotAnnuelleTTC ?? "NC"}`), kv("Dates", `Effet: ${dateEffet} / Échéance: ${dateEcheance} / Fin: ${dateFin}`), ].join(""); return gridWrap2cols(gauche, droite); }