(function () { "use strict"; var $ = function (s) { return document.querySelector(s); }; var STATE = { sain: "Sain", attention: "Attention", critique: "Critique", calibration: "Calibration" }; var LEVEL = { attention: "Attention", critique: "Critique" }; var plan = getPlan(); var cur = null; var charts = {}; $("#planBadge").textContent = "Plan " + PLANS[plan].label; function setMsg(el, text, err) { el.textContent = text || ""; el.className = "msg" + (err ? " err" : ""); } async function errorText(res) { var e = {}; try { e = await res.json(); } catch (x) { /* corps non JSON */ } return typeof e.detail === "string" ? e.detail : "Erreur " + res.status; } // ---- liste des exemples fetch("/api/demo").then(function (r) { return r.json(); }).then(function (names) { var sel = $("#demo"); if (!names.length) return; sel.innerHTML = ""; names.forEach(function (n) { var o = document.createElement("option"); o.value = n; o.textContent = n; sel.appendChild(o); }); }).catch(function () { /* serveur sans exemples */ }); // ---- analyse async function analyse() { setMsg($("#msg"), "Analyse en cours…"); var f = $("#file").files[0], res; try { if (f) { var fd = new FormData(); fd.append("file", f); res = await fetch("/api/analyse", { method: "POST", body: fd }); cur = { file: f }; } else { var n = $("#demo").value; if (!n) { setMsg($("#msg"), "Choisis un exemple ou importe un fichier CSV.", true); return; } res = await fetch("/api/demo/" + encodeURIComponent(n) + "/analyse"); cur = { demo: n }; } if (!res.ok) throw new Error(await errorText(res)); cur.data = await res.json(); render(cur.data); setMsg($("#msg"), ""); } catch (e) { setMsg($("#msg"), e.message || "Erreur inattendue.", true); } } function mk(id, cfg) { if (charts[id]) charts[id].destroy(); charts[id] = new Chart(document.getElementById(id), cfg); } function line(label, data, color, extra) { return Object.assign({ label: label, data: data, borderColor: color, backgroundColor: color, borderWidth: 2, pointRadius: 0, tension: 0.2 }, extra || {}); } function chartOpts(yType) { return { responsive: true, maintainAspectRatio: false, animation: false, interaction: { mode: "index", intersect: false }, plugins: { legend: { position: "bottom", labels: { boxWidth: 12, font: { size: 12 } } } }, scales: { x: { title: { display: true, text: "N° de coupe" }, ticks: { maxTicksLimit: 10 } }, y: { type: yType || "linear" } } }; } function render(d) { $("#result").classList.remove("hidden"); var s = d.series, n = s.cut.length; $("#vState").textContent = STATE[d.state] || d.state; $("#kState").className = "card kpi st-" + d.state; $("#sState").textContent = d.machine; $("#vWear").textContent = d.wear_now.toFixed(1) + " µm"; $("#sWear").textContent = "limite " + d.wear_limit.toFixed(1) + " µm (" + Math.round(100 * d.wear_now / d.wear_limit) + " %)"; $("#vRul").textContent = d.rul_now >= d.rul_cap * 0.99 ? "≥ " + d.rul_cap : Math.round(d.rul_now); $("#vAlert").textContent = d.first_alert_cut ? "Coupe " + d.first_alert_cut : "Aucune"; $("#sAlert").textContent = d.alert_threshold === null ? "seuil calculé à partir de " + 2 * d.n_ref + " coupes" : "seuil de l'indice : " + d.alert_threshold.toFixed(1); var limit = new Array(n).fill(d.wear_limit); var wearSets = [line("Usure estimée", s.wear_pred, "#0B4F8A"), line("Limite d'usure", limit, "#C2570C", { borderDash: [6, 5], borderWidth: 1.5 })]; if (s.wear_true) wearSets.unshift(line("Usure mesurée (démo)", s.wear_true, "#9AA7B5", { borderWidth: 1.5 })); mk("cWear", { type: "line", data: { labels: s.cut, datasets: wearSets }, options: chartOpts() }); var hi = s.health_index.map(function (v) { return v !== null && v > 0 ? v : null; }); var healthSets = [line("Indice de santé", hi, "#0B4F8A")]; if (d.alert_threshold !== null) healthSets.push(line("Seuil d'alerte", new Array(n).fill(d.alert_threshold), "#C2570C", { borderDash: [6, 5], borderWidth: 1.5 })); mk("cHealth", { type: "line", data: { labels: s.cut, datasets: healthSets }, options: chartOpts("logarithmic") }); var o = chartOpts(); o.scales.y.min = 0; o.scales.y.max = Math.ceil(d.rul_cap * 1.05); mk("cRul", { type: "line", data: { labels: s.cut, datasets: [line("RUL estimée", s.rul_pred, "#0B4F8A"), line("Seuil critique (30)", new Array(n).fill(30), "#B42318", { borderDash: [6, 5], borderWidth: 1.5 })] }, options: o }); // événements var tb = $("#events"); tb.innerHTML = ""; d.events.forEach(function (e) { var tr = document.createElement("tr"); [e.cut, LEVEL[e.level] || e.level, e.text].forEach(function (v) { var td = document.createElement("td"); td.textContent = v; tr.appendChild(td); }); tb.appendChild(tr); }); $("#noEvents").textContent = d.events.length ? "" : "Aucun événement détecté."; // restrictions du plan (simulées) var P = PLANS[plan]; $("#aSend").disabled = !P.mail; $("#aNote").textContent = P.mail ? "" : "Les alertes e-mail et WhatsApp sont disponibles avec les plans Pro et Entreprise."; $("#rGo").disabled = !P.pdf; $("#rNote").textContent = P.pdf ? "" : "Le rapport PDF est disponible avec les plans Pro et Entreprise."; } // ---- alerte async function sendAlert() { if (!cur || !cur.data) return; var d = cur.data, ch = channel(); var to = (ch === "email" ? $("#aMail") : $("#aWa")).value.trim(); if (!to) { setMsg($("#aMsg"), ch === "email" ? "Indique une adresse e-mail." : "Indique un numéro WhatsApp.", true); return; } setMsg($("#aMsg"), "Envoi…"); try { var res = await fetch("/api/alert", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ channel: ch, to: to, machine: d.machine, state: d.state, wear_now: d.wear_now, wear_limit: d.wear_limit, rul_now: d.rul_now, first_alert_cut: d.first_alert_cut }) }); if (!res.ok) throw new Error(await errorText(res)); var r = await res.json(); setMsg($("#aMsg"), (ch === "email" ? "E-mail : " : "WhatsApp : ") + r.detail, !r.sent && !r.simulated); $("#aNote").textContent = r.simulated ? "Message préparé : " + r.message : ""; } catch (e) { setMsg($("#aMsg"), e.message, true); } } function channel() { return document.querySelector('input[name="ch"]:checked').value; } function syncChannel() { var ch = channel(); $("#fMail").classList.toggle("hidden", ch !== "email"); $("#fWa").classList.toggle("hidden", ch !== "whatsapp"); setMsg($("#aMsg"), ""); } // ---- rapport async function report() { if (!cur) return; setMsg($("#rMsg"), "Génération du PDF…"); try { var res; if (cur.file) { var fd = new FormData(); fd.append("file", cur.file); res = await fetch("/api/report", { method: "POST", body: fd }); } else res = await fetch("/api/demo/" + encodeURIComponent(cur.demo) + "/report"); if (!res.ok) throw new Error(await errorText(res)); var blob = await res.blob(), a = document.createElement("a"); a.href = URL.createObjectURL(blob); a.download = "rapport_millguard.pdf"; document.body.appendChild(a); a.click(); a.remove(); setMsg($("#rMsg"), "Rapport téléchargé."); } catch (e) { setMsg($("#rMsg"), e.message, true); } } // ---- onglets document.querySelectorAll(".tab").forEach(function (b) { b.addEventListener("click", function () { document.querySelectorAll(".tab").forEach(function (t) { t.classList.toggle("on", t === b); }); ["etat", "hist", "rap"].forEach(function (k) { $("#tab-" + k).classList.toggle("hidden", k !== b.dataset.tab); }); }); }); document.querySelectorAll('input[name="ch"]').forEach(function (r) { r.addEventListener("change", syncChannel); }); $("#go").addEventListener("click", analyse); $("#aSend").addEventListener("click", sendAlert); $("#rGo").addEventListener("click", report); })();