personnal/ecole/public/js/historiqueParcours.js

597 lines
21 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;");
return '<span style="display:inline-flex; align-items:center; gap:8px;">'
+ '<button class="btn-row-details" title="Détails" aria-label="Afficher les détails"'
+ ' style="display:inline-block; width:22px; height:22px; border:none; border-radius:4px;'
+ ' background:#2ecc71; color:#fff; font-weight:700; cursor:pointer; line-height:22px;'
+ ' text-align:center;">+</button>'
+ '<span class="np">' + numPacours + '</span>'
+ '</span>';
}
},
{ 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('<div style="padding:12px 16px; font-style:italic;">Chargement des détails…</div>').show();
this.textContent = "";
this.style.background = "#27ae60";
fetch(`/historiqueParcours/details/${encodeURIComponent(numParcours)}`)
.then(r => r.json())
.then(payload => {
if (!payload?.valid) {
row.child('<div style="padding:12px 16px; color:#b00020;">Impossible de charger les détails.</div>');
this.textContent = "+";
this.style.background = "#2ecc71";
return;
}
row.child(formatDetailsPanel(payload));
})
.catch(() => {
row.child('<div style="padding:12px 16px; color:#b00020;">Erreur de chargement.</div>');
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 `<div style="display:flex; gap:8px; margin:2px 0;">
<div style="min-width:220px;"><strong>${label}</strong> :</div>
<div>${v}</div>
</div>`;
}
//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 = `<em>Produit non renseigné.</em>`;
return `
<div class="parcours-details" style="padding:14px 16px; background:#fafafa; border-top:1px solid #e0e0e0;">
<div style="font-weight:600; margin:0 0 8px;">Détails ${prodKey || ""}</div>
${body}
</div>
`;
}
//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 `<div style="display:grid;grid-template-columns:repeat(2,minmax(280px,1fr));gap:14px;">${innerLeft}${innerRight}</div>`;
}
// ---------- 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 `<em style="color:#a00;">Fiche TPPC introuvable.</em>`;
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 `<em style="color:#a00;">Fiche RC introuvable.</em>`;
//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 `<em style="color:#a00;">Fiche FAC introuvable.</em>`;
//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);
}