personnal/ecole/public/js/historiqueParcours.js

859 lines
28 KiB
JavaScript

// 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: false,
autoWidth: false,
scrollX: 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);
});
});
$("#toggleSearch").on("click", function () {
const row = $("#historiqueParcours thead tr:eq(1)");
row.toggle();
if (row.is(":visible")) {
$(this).text("ENLEVER LA RECHERCHE PAR COLONNE");
} else {
$(this).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 class="col-with-text">
<button class="btn-row-details" title="Détails" aria-label="Afficher les détails">?</button>
<span class="np">${numPacours}</span>
</span>
`;
}
},
{ 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('<div style="padding:12px 16px; font-style:italic;">Chargement des détails…</div>').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 `
<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>
`;
}
// Retourne le tarif en fonction du type de tarif
function buildTarifBlock({ tarifRef, ht, ttc }) {
if (tarifRef && String(tarifRef).trim() !== '') {
return kv("Tarif de référence", tarifRef);
}
const htStr = (ht !== undefined && ht !== null) ? formatEuro(ht) : "NC";
const ttcStr = (ttc !== undefined && ttc !== null) ? formatEuro(ttc) : "NC";
return kv("Tarif commercial HT / TTC", `${htStr} / ${ttcStr}`);
}
function safeCA(value) {
return (value === null || value === undefined || value === '' || value === 'NC')
? "NC"
: formatEuro(value);
}
// ---------- Sections produit ----------
//sectionTTPC : récupere les détails d'un parcours TPPC pour le mettre en forme (2 colonnes)
// ---------- TPPC ----------
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.<br>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
// 1) CA
const caStr = safeCA(produit.ca);
// 2) Type de cotisation
let typeCotStr = "NC";
if (projet?.typeCot) {
typeCotStr = (projet.typeCot === "revisable") ? "Révisable"
: (projet.typeCot === "forfaitaire") ? "Forfaitaire"
: projet.typeCot;
}
// 3) Activité
const activiteAssuree = produit.actAssuree || "NC";
const nbVehicules = (produit.nbVehic !== undefined && produit.nbVehic !== null) ? String(produit.nbVehic) : "NC";
// 4) Marchandises / Garanties
const garanties = Array.isArray(produit.garanties)
? produit.garanties.join(", ")
: (produit.garanties || "NC");
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";
// 5) Zones (supprimer pour TPPC)
const zonesStr = "NC";
// 6) Dates (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 datesStr = `Effet: ${dateEffet} / Échéance: ${dateEcheance} / Fin: ${dateFin}`;
// 7) Tarif (ref ou com HT/TTC)
const blocTarif = buildTarifBlock({
tarifRef: tarif?.tarifRef,
ht: (produit.primeHT ?? produit.cotTotalHT ?? null),
ttc: (produit.primeTTC ?? produit.cotTotalTTC ?? null)
});
// Disposition en 2 colonnes
const gauche = [
kv("Chiffre d'affaires", caStr),
kv("Type de cotisation", typeCotStr),
kv("Activité assurée", activiteAssuree),
kv("Nombre de véhicules", nbVehicules),
kv("Garanties", garanties),
kv("Extensions de garanties", extensionsStr),
].join("");
const droite = [
kv("Zones", zonesStr),
kv("Dates", datesStr),
blocTarif,
].join("");
return gridWrap2cols(gauche, droite);
}
//sectionRC : récupere les détails d'un parcours RC pour le mettre en forme (2 colonnes)
// ---------- RC ----------
function sectionRC(produit, contrat) {
if (!produit && contrat) {
return createMessageBox(
'info',
'Informations non disponibles',
'Les informations sur ce Parcours RC ne sont pas encore disponibles.<br>La fiche RC n\'a pas encore été créée pour ce parcours.'
);
}
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.<br>Cette fonctionnalité sera bientôt implémentée.'
);
}
// 1) CA
const caStr = safeCA(produit.ca);
// 2) Type de cotisation
let typeCotStr = "NC";
if (produit.typeCot) {
typeCotStr = (produit.typeCot === "revisable") ? "Révisable"
: (produit.typeCot === "forfaitaire") ? "Forfaitaire"
: produit.typeCot;
}
// 3) 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";
// 4) 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";
// 5) 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";
// 6) 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";
const datesStr = `Effet: ${dateEffet} / Échéance: ${dateEcheance} / Fin: ${dateFin}`;
// 7) Tarif
const tarif = produit?.["@expand"]?.tarif || null;
const blocTarif = buildTarifBlock({
tarifRef: tarif?.tarifRef,
ht: produit.cotTotalHT ?? null,
ttc: produit.cotTotalTTC ?? null
});
const gauche = [
kv("Chiffre d'affaires", caStr),
kv("Type de cotisation", typeCotStr),
kv("Activités assurées", activiteAssuree),
kv("Marchandises", garantiesStr),
].join("");
const droite = [
kv("Zones", zonesStr),
kv("Dates", datesStr),
blocTarif,
].join("");
return gridWrap2cols(gauche, droite);
}
//sectionFAC : récupere les détails d'un parcours FAC pour le mettre en forme (2 colonnes)
// ---------- FAC ----------
function sectionFAC(produit, contrat) {
if (!produit && contrat) {
return createMessageBox(
'info',
'Informations non disponibles',
'Les informations sur ce Parcours FAC ne sont pas encore disponibles.<br>La fiche FAC n\'a pas encore été créée pour ce parcours.'
);
}
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.<br>Cette fonctionnalité sera bientôt implémentée.'
);
}
// 1) CA
const caStr = safeCA(produit.ca);
// 2) Type de cotisation
const typeCotStr = "NC";
// 3) Activité
const activiteAssuree = produit.actAssure || "NC";
// 4) Garanties (modes de transport déclarés)
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";
// 5) 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";
// 6) 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";
const datesStr = `Effet: ${dateEffet} / Échéance: ${dateEcheance} / Fin: ${dateFin}`;
// 7) Tarif
const tarif = produit?.["@expand"]?.tarif || null;
const blocTarif = buildTarifBlock({
tarifRef: tarif?.tarifRef,
ht: produit.cotAnnuelleHT ?? null,
ttc: produit.cotAnnuelleTTC ?? null
});
const gauche = [
kv("Chiffre d'affaires", caStr),
kv("Type de cotisation", typeCotStr),
kv("Activité assurée", activiteAssuree),
kv("Garanties", garantiesStr),
].join("");
const droite = [
kv("Zones", zonesStr),
kv("Dates", datesStr),
blocTarif,
].join("");
return gridWrap2cols(gauche, droite);
}