// ArmoireB3.jsx — Full stock view: KPI row, floor plan, drawer.
// B3 drawer shows live sensor items; all others show static data.

const LINEN = ["Draps coton bio", "Taies d'oreiller", "Alèses", "Blouses patient", "Linges de toilette", "Couvertures"];

function mkArmoire(id, name, fill, levels) {
  const status = fill < 15 ? "out" : fill < 40 ? "low" : "ok";
  return { id, name, fill, status, items: LINEN.map((l, i) => ({ name: l, ...levels[i] })) };
}

const ARMOIRES_STATIC = [
  mkArmoire("A1", "Armoire A1", 82, [{pct:88,qty:44,par:50},{pct:80,qty:32,par:40},{pct:74,qty:22,par:30},{pct:90,qty:27,par:30},{pct:85,qty:51,par:60},{pct:70,qty:14,par:20}]),
  mkArmoire("A2", "Armoire A2", 76, [{pct:78,qty:39,par:50},{pct:72,qty:29,par:40},{pct:80,qty:24,par:30},{pct:70,qty:21,par:30},{pct:82,qty:49,par:60},{pct:65,qty:13,par:20}]),
  mkArmoire("A3", "Armoire A3", 38, [{pct:30,qty:15,par:50},{pct:45,qty:18,par:40},{pct:33,qty:10,par:30},{pct:40,qty:12,par:30},{pct:48,qty:29,par:60},{pct:25,qty:5,par:20}]),
  mkArmoire("A4", "Armoire A4", 41, [{pct:42,qty:21,par:50},{pct:38,qty:15,par:40},{pct:50,qty:15,par:30},{pct:35,qty:10,par:30},{pct:44,qty:26,par:60},{pct:36,qty:7,par:20}]),
  mkArmoire("A5", "Armoire A5", 91, [{pct:95,qty:48,par:50},{pct:90,qty:36,par:40},{pct:88,qty:26,par:30},{pct:93,qty:28,par:30},{pct:90,qty:54,par:60},{pct:85,qty:17,par:20}]),
  mkArmoire("A6", "Armoire A6", 84, [{pct:86,qty:43,par:50},{pct:82,qty:33,par:40},{pct:80,qty:24,par:30},{pct:88,qty:26,par:30},{pct:84,qty:50,par:60},{pct:78,qty:16,par:20}]),
  mkArmoire("B1", "Armoire B1", 44, [{pct:46,qty:23,par:50},{pct:40,qty:16,par:40},{pct:50,qty:15,par:30},{pct:42,qty:13,par:30},{pct:48,qty:29,par:60},{pct:38,qty:8,par:20}]),
  mkArmoire("B2", "Armoire B2", 9,  [{pct:6,qty:3,par:50},{pct:12,qty:5,par:40},{pct:10,qty:3,par:30},{pct:7,qty:2,par:30},{pct:14,qty:8,par:60},{pct:5,qty:1,par:20}]),
  mkArmoire("B3", "Armoire B3", 12, [{pct:10,qty:5,par:50},{pct:15,qty:6,par:40},{pct:13,qty:4,par:30},{pct:9,qty:3,par:30},{pct:16,qty:10,par:60},{pct:8,qty:2,par:20}]),
  mkArmoire("B4", "Armoire B4", 79, [{pct:80,qty:40,par:50},{pct:76,qty:30,par:40},{pct:82,qty:25,par:30},{pct:78,qty:23,par:30},{pct:80,qty:48,par:60},{pct:72,qty:14,par:20}]),
  mkArmoire("B5", "Armoire B5", 88, [{pct:90,qty:45,par:50},{pct:85,qty:34,par:40},{pct:86,qty:26,par:30},{pct:92,qty:28,par:30},{pct:88,qty:53,par:60},{pct:82,qty:16,par:20}]),
  mkArmoire("B6", "Armoire B6", 81, [{pct:84,qty:42,par:50},{pct:78,qty:31,par:40},{pct:80,qty:24,par:30},{pct:82,qty:25,par:30},{pct:84,qty:50,par:60},{pct:76,qty:15,par:20}]),
];

const DOT = { ok: "#A0A0BE", low: "#E08A38", out: "#fff" };
const STLABEL = { ok: "Plein", low: "Sous seuil", out: "Rupture" };

const POS = {
  A1: { x: 9.29,  y: 34.35 }, A2: { x: 24.29, y: 34.35 }, A3: { x: 39.29, y: 34.35 },
  A4: { x: 9.29,  y: 65.65 }, A5: { x: 24.29, y: 65.65 }, A6: { x: 39.29, y: 65.65 },
  B1: { x: 60.71, y: 34.35 }, B2: { x: 75.71, y: 34.35 }, B3: { x: 90.71, y: 34.35 },
  B4: { x: 60.71, y: 65.65 }, B5: { x: 75.71, y: 65.65 }, B6: { x: 90.71, y: 65.65 },
};
const NAVY = "#2E3770", RULE_C = "#D0CFE0";

// --- helpers ---

function formatTime(isoStr) {
  if (!isoStr) return "—";
  try {
    return new Date(isoStr).toLocaleTimeString("fr-CH", { hour: "2-digit", minute: "2-digit", second: "2-digit" });
  } catch (e) { return isoStr; }
}

function timeSince(isoStr) {
  if (!isoStr) return "";
  try {
    var diff = Math.floor((Date.now() - new Date(isoStr).getTime()) / 1000);
    if (diff < 60) return "il y a " + diff + "s";
    if (diff < 3600) return "il y a " + Math.floor(diff / 60) + "min";
    return "il y a " + Math.floor(diff / 3600) + "h";
  } catch (e) { return ""; }
}

function getB3WithSensors(sensorData) {
  var base = ARMOIRES_STATIC.find(a => a.id === "B3");
  var laserLayers = sensorData.laser ? sensorData.laser.layers : null;
  var ultrasonLayers = sensorData.ultrason ? sensorData.ultrason.layers : null;

  var liveQty = 0, livePar = 0;
  if (laserLayers !== null) { liveQty += laserLayers; livePar += 9; }
  if (ultrasonLayers !== null) { liveQty += ultrasonLayers; livePar += 9; }
  var staticQty = base.items.reduce((a, it) => a + it.qty, 0);
  var staticPar = base.items.reduce((a, it) => a + it.par, 0);
  var totalPar = staticPar + livePar;
  var totalQty = staticQty + liveQty;
  var fill = totalPar > 0 ? Math.round((totalQty / totalPar) * 100) : base.fill;
  var status = fill < 15 ? "out" : fill < 40 ? "low" : "ok";

  return { ...base, fill, status };
}

function getArmoires(sensorData) {
  var base = (window.MOCK && window.MOCK.active && window.MOCK.armoires) || ARMOIRES_STATIC;
  return base.map(a => a.id === "B3" ? getB3WithSensors(sensorData) : a);
}

// --- KPI row ---

function KpiRow({ sensorData }) {
  var armoires = getArmoires(sensorData);
  var lowCount = armoires.filter(a => a.status === "low" || a.status === "out").length;

  return (
    <div className="kpis">
      <div className="kpi">
        <div className="lab">Consommation inhabituelle</div>
        <div className="val">{lowCount} <small>armoires</small></div>
      </div>
      <div className="kpi">
        <div className="lab">À charger</div>
        <div className="val">67 <small>pièces</small></div>
      </div>
      <div className="kpi">
        <div className="lab">Réserve de linge</div>
        <div className="val">28h</div>
      </div>
    </div>
  );
}

// --- floor plan SVG ---

function FloorPlanSVG() {
  const dividers = [188, 356, 764, 932];
  const topRoomNos = [["201", 104], ["202", 272], ["203", 440], ["211", 680], ["212", 848], ["213", 1016]];
  const botRoomNos = [["204", 104], ["205", 272], ["206", 440], ["214", 680], ["215", 848], ["216", 1016]];
  const doorsTop = [104, 272, 440, 680, 848, 1016];
  const doorsBot = [104, 272, 440, 680, 848, 1016];
  return (
    <svg viewBox="0 0 1120 460" xmlns="http://www.w3.org/2000/svg">
      <rect x="20" y="190" width="1080" height="80" fill="#F3F3F8" />
      <rect x="524" y="20" width="72" height="420" fill="#E7E6EE" />
      {dividers.map(x => (
        <g key={x} stroke={RULE_C} strokeWidth="1.5">
          <line x1={x} y1="20" x2={x} y2="190" />
          <line x1={x} y1="270" x2={x} y2="440" />
        </g>
      ))}
      <g stroke={NAVY} strokeWidth="1.5">
        <line x1="20" y1="190" x2="524" y2="190" />
        <line x1="596" y1="190" x2="1100" y2="190" />
        <line x1="20" y1="270" x2="524" y2="270" />
        <line x1="596" y1="270" x2="1100" y2="270" />
      </g>
      {doorsTop.map(cx => <rect key={"t"+cx} x={cx-18} y="187" width="36" height="6" fill="#F3F3F8" />)}
      {doorsBot.map(cx => <rect key={"b"+cx} x={cx-18} y="267" width="36" height="6" fill="#F3F3F8" />)}
      <rect x="524" y="20" width="72" height="420" fill="none" stroke={NAVY} strokeWidth="1.5" />
      <rect x="521" y="207" width="6" height="46" fill="#E7E6EE" />
      <rect x="593" y="207" width="6" height="46" fill="#E7E6EE" />
      <rect x="20" y="20" width="1080" height="420" fill="none" stroke={NAVY} strokeWidth="2" />
      <g fill={RULE_C} fontSize="13" fontWeight="600" fontFamily="inherit" textAnchor="middle">
        {topRoomNos.map(([n, x]) => <text key={"tn"+x} x={x} y="44">Ch. {n}</text>)}
        {botRoomNos.map(([n, x]) => <text key={"bn"+x} x={x} y="430">Ch. {n}</text>)}
      </g>
      <g fill="#A0A0BE" fontSize="11" fontWeight="600" fontFamily="inherit" letterSpacing="3" textAnchor="middle">
        <text x="272" y="234">COULOIR</text>
        <text x="848" y="234">COULOIR</text>
      </g>
    </svg>
  );
}

// --- 3D floor plan ---

function FloorPlan3D({ armoires, onSelect, selectedId, is3D }) {
  var containerRef = React.useRef(null);
  var meshMapRef = React.useRef({});
  var onSelectRef = React.useRef(onSelect);
  var armoiresRef = React.useRef(armoires);
  var controlsRef = React.useRef(null);
  var camGoal = React.useRef(null);
  var is3DRef = React.useRef(is3D);
  var transRef = React.useRef(true);
  onSelectRef.current = onSelect;
  armoiresRef.current = armoires;

  React.useEffect(function() {
    is3DRef.current = is3D;
    var g = camGoal.current;
    if (!g) return;
    if (is3D) {
      g.pos.set(0, 7, 6);
      g.look.set(0, 0, 0.3);
    } else {
      g.pos.set(0, 8.5, 0.01);
      g.look.set(0, 0, 0);
    }
    transRef.current = true;
    if (controlsRef.current) controlsRef.current.enabled = false;
  }, [is3D]);

  React.useEffect(function () {
    if (typeof THREE === "undefined") return;
    var container = containerRef.current;
    if (!container) return;

    var W = container.clientWidth;
    var H = Math.round(W / 2.43);
    container.style.height = H + "px";

    var scene = new THREE.Scene();
    scene.background = new THREE.Color("#F0F1F5");

    var camera = new THREE.PerspectiveCamera(40, W / H, 0.1, 100);
    camera.position.set(0, 8.5, 0.01);

    var renderer = new THREE.WebGLRenderer({ antialias: true });
    renderer.setSize(W, H);
    renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
    renderer.shadowMap.enabled = true;
    renderer.shadowMap.type = THREE.PCFSoftShadowMap;
    container.appendChild(renderer.domElement);

    var controls = new THREE.OrbitControls(camera, renderer.domElement);
    controls.enableDamping = true;
    controls.dampingFactor = 0.08;
    controls.maxPolarAngle = Math.PI / 2.1;
    controls.minDistance = 3;
    controls.maxDistance = 18;
    controls.target.set(0, 0, 0);
    controls.enabled = false;
    controls.update();

    controlsRef.current = controls;
    camGoal.current = {
      pos: new THREE.Vector3(0, 8.5, 0.01),
      look: new THREE.Vector3(0, 0, 0)
    };

    scene.add(new THREE.AmbientLight("#ffffff", 0.65));
    var sun = new THREE.DirectionalLight("#ffffff", 0.85);
    sun.position.set(4, 8, -2);
    sun.castShadow = true;
    sun.shadow.mapSize.set(2048, 2048);
    var sc = sun.shadow.camera;
    sc.left = -8; sc.right = 8; sc.top = 5; sc.bottom = -5;
    scene.add(sun);

    var SC = 0.01, CX = 560, CY = 230;
    function wx(v) { return (v - CX) * SC; }
    function wz(v) { return (v - CY) * SC; }

    function addFloor(x, y, w, h, color) {
      var g = new THREE.PlaneGeometry(w * SC, h * SC);
      var m = new THREE.MeshStandardMaterial({ color: color });
      var mesh = new THREE.Mesh(g, m);
      mesh.rotation.x = -Math.PI / 2;
      mesh.position.set(wx(x + w / 2), 0.001, wz(y + h / 2));
      mesh.receiveShadow = true;
      scene.add(mesh);
    }

    var WALL_COL = "#B0AFBF";
    function wallH(x1, x2, y, h, t) {
      var g = new THREE.BoxGeometry((x2 - x1) * SC, h, t);
      var m = new THREE.MeshStandardMaterial({ color: WALL_COL });
      var mesh = new THREE.Mesh(g, m);
      mesh.position.set(wx((x1 + x2) / 2), h / 2, wz(y));
      mesh.castShadow = true;
      scene.add(mesh);
    }

    function wallV(x, y1, y2, h, t) {
      var g = new THREE.BoxGeometry(t, h, (y2 - y1) * SC);
      var m = new THREE.MeshStandardMaterial({ color: WALL_COL });
      var mesh = new THREE.Mesh(g, m);
      mesh.position.set(wx(x), h / 2, wz((y1 + y2) / 2));
      mesh.castShadow = true;
      scene.add(mesh);
    }

    // Floor zones
    addFloor(20, 20, 504, 170, "#FAFAFC");
    addFloor(596, 20, 504, 170, "#FAFAFC");
    addFloor(20, 270, 504, 170, "#FAFAFC");
    addFloor(596, 270, 504, 170, "#FAFAFC");
    addFloor(20, 190, 504, 80, "#EEEDF5");
    addFloor(596, 190, 504, 80, "#EEEDF5");
    addFloor(524, 20, 72, 420, "#DDD9EE");

    // Walls
    var WH = 0.28, WT = 0.022;
    wallH(20, 1100, 20, WH, WT);
    wallH(20, 1100, 440, WH, WT);
    wallV(20, 20, 440, WH, WT);
    wallV(1100, 20, 440, WH, WT);

    // Corridor walls y=190 with door gaps
    [[20,86],[122,254],[290,422],[458,524]].forEach(function(s) { wallH(s[0],s[1],190,WH,WT); });
    [[596,662],[698,830],[866,998],[1034,1100]].forEach(function(s) { wallH(s[0],s[1],190,WH,WT); });
    [[20,86],[122,254],[290,422],[458,524]].forEach(function(s) { wallH(s[0],s[1],270,WH,WT); });
    [[596,662],[698,830],[866,998],[1034,1100]].forEach(function(s) { wallH(s[0],s[1],270,WH,WT); });

    // Reserve walls with corridor openings
    wallV(524, 20, 207, WH, WT);
    wallV(524, 253, 440, WH, WT);
    wallV(596, 20, 207, WH, WT);
    wallV(596, 253, 440, WH, WT);

    // Room dividers
    [188, 356, 764, 932].forEach(function(x) {
      wallV(x, 20, 190, 0.12, 0.01);
      wallV(x, 270, 440, 0.12, 0.01);
    });

    // Armoire boxes
    var COL = { ok: "#A0A0BE", low: "#E08A38", out: "#C8423C" };
    var meshMap = {};
    var armoireMeshes = [];

    armoires.forEach(function(a) {
      var p = POS[a.id];
      var svgX = (p.x / 100) * 1120 + 49;
      var isTop = p.y < 50;
      var svgY = isTop ? 167 : 293;
      var bH = WH;
      var g = new THREE.BoxGeometry(0.52, bH, 0.4);
      var m = new THREE.MeshStandardMaterial({ color: COL[a.status], roughness: 0.45, metalness: 0.05 });
      var mesh = new THREE.Mesh(g, m);
      mesh.position.set(wx(svgX), bH / 2 + 0.002, wz(svgY));
      mesh.castShadow = true;
      mesh.receiveShadow = true;
      mesh.userData.armoireId = a.id;
      scene.add(mesh);
      meshMap[a.id] = mesh;
      armoireMeshes.push(mesh);
    });

    meshMapRef.current = meshMap;

    // Labels
    function makeLabel(text, px, py, pz, scale, color) {
      var c = document.createElement("canvas");
      c.width = 512; c.height = 192;
      var ctx = c.getContext("2d");
      ctx.font = "bold 80px sans-serif";
      ctx.fillStyle = color || NAVY;
      ctx.textAlign = "center";
      ctx.textBaseline = "middle";
      ctx.fillText(text, 256, 96);
      var tex = new THREE.CanvasTexture(c);
      var mat = new THREE.SpriteMaterial({ map: tex, transparent: true, depthTest: false });
      var sp = new THREE.Sprite(mat);
      sp.scale.set(scale, scale * 0.375, 1);
      sp.position.set(px, py, pz);
      scene.add(sp);
    }

    armoires.forEach(function(a) {
      var p = POS[a.id];
      var svgX = (p.x / 100) * 1120 + 49;
      var isTop = p.y < 50;
      var svgY = isTop ? 167 : 293;
      makeLabel(a.id + "  " + a.fill + "%", wx(svgX), WH + 0.15, wz(svgY), 0.42, NAVY);
    });

    [[104,"201"],[272,"202"],[440,"203"],[680,"211"],[848,"212"],[1016,"213"]].forEach(function(r) {
      makeLabel("Ch." + r[1], wx(r[0]), 0.02, wz(55), 0.38, "#C0BFD0");
    });
    [[104,"204"],[272,"205"],[440,"206"],[680,"214"],[848,"215"],[1016,"216"]].forEach(function(r) {
      makeLabel("Ch." + r[1], wx(r[0]), 0.02, wz(405), 0.38, "#C0BFD0");
    });
    makeLabel("COULOIR", wx(272), 0.02, wz(230), 0.4, "#B8B7CC");
    makeLabel("COULOIR", wx(848), 0.02, wz(230), 0.4, "#B8B7CC");

    // Interaction
    var raycaster = new THREE.Raycaster();
    var mouse = new THREE.Vector2();
    var downPos = null;

    function onPointerDown(e) { downPos = { x: e.clientX, y: e.clientY }; }
    function onPointerUp(e) {
      if (!downPos) return;
      var dx = e.clientX - downPos.x, dy = e.clientY - downPos.y;
      downPos = null;
      if (Math.abs(dx) > 5 || Math.abs(dy) > 5) return;
      var r = renderer.domElement.getBoundingClientRect();
      mouse.set(((e.clientX - r.left) / r.width) * 2 - 1, -((e.clientY - r.top) / r.height) * 2 + 1);
      raycaster.setFromCamera(mouse, camera);
      var hits = raycaster.intersectObjects(armoireMeshes);
      if (hits.length) {
        var id = hits[0].object.userData.armoireId;
        var a = armoiresRef.current.find(function(x) { return x.id === id; });
        if (a) onSelectRef.current(a);
      }
    }
    function onPointerMove(e) {
      var r = renderer.domElement.getBoundingClientRect();
      mouse.set(((e.clientX - r.left) / r.width) * 2 - 1, -((e.clientY - r.top) / r.height) * 2 + 1);
      raycaster.setFromCamera(mouse, camera);
      renderer.domElement.style.cursor = raycaster.intersectObjects(armoireMeshes).length ? "pointer" : "";
    }

    var el = renderer.domElement;
    el.addEventListener("pointerdown", onPointerDown);
    el.addEventListener("pointerup", onPointerUp);
    el.addEventListener("pointermove", onPointerMove);

    var frameId;
    var goal = camGoal.current;
    (function loop() {
      frameId = requestAnimationFrame(loop);
      if (transRef.current) {
        camera.position.lerp(goal.pos, 0.07);
        controls.target.lerp(goal.look, 0.07);
        if (camera.position.distanceTo(goal.pos) < 0.05) {
          transRef.current = false;
          if (is3DRef.current) controls.enabled = true;
        }
      }
      controls.update();
      renderer.render(scene, camera);
    })();

    function onResize() {
      var w = container.clientWidth;
      var h = Math.round(w / 2.43);
      container.style.height = h + "px";
      camera.aspect = w / h;
      camera.updateProjectionMatrix();
      renderer.setSize(w, h);
    }
    window.addEventListener("resize", onResize);

    return function() {
      window.removeEventListener("resize", onResize);
      cancelAnimationFrame(frameId);
      controls.dispose();
      el.removeEventListener("pointerdown", onPointerDown);
      el.removeEventListener("pointerup", onPointerUp);
      el.removeEventListener("pointermove", onPointerMove);
      renderer.dispose();
      if (container.contains(el)) container.removeChild(el);
    };
  }, []);

  React.useEffect(function() {
    if (typeof THREE === "undefined") return;
    var map = meshMapRef.current;
    var COL = { ok: "#A0A0BE", low: "#E08A38", out: "#C8423C" };
    armoires.forEach(function(a) {
      var mesh = map[a.id];
      if (!mesh) return;
      mesh.material.color.set(COL[a.status]);
      mesh.material.emissive = new THREE.Color(a.id === selectedId ? "#4466ff" : "#000000");
      mesh.material.emissiveIntensity = a.id === selectedId ? 0.25 : 0;
    });
  }, [armoires, selectedId]);

  return <div ref={containerRef} className="floorplan3d" />;
}

// --- floor plan marker ---

function Marker({ a, selected, onClick, isLive }) {
  const p = POS[a.id];
  return (
    <button className={"fp-marker" + (a.status === "out" ? " out" : "") + (selected ? " sel" : "")}
            style={{ left: p.x + "%", top: p.y + "%" }} onClick={onClick}>
      <div className="r1">
        <span className="nm">
          {a.name}
          {false && isLive && <span className="live-dot-sm" />}
        </span>
        <span className="dot" style={{ background: DOT[a.status] }} />
      </div>
      <div className="pct">{a.fill}%</div>
      <div className="mini"><i style={{ width: a.fill + "%" }} /></div>
    </button>
  );
}

// --- floor plan panel ---

function FloorPlan({ armoires, onSelect, selectedId }) {
  var [is3D, setIs3D] = React.useState(false);
  return (
    <div className="panel">
      <div className="panelhead">
        <h2>Plan d'étage — niveaux de stock</h2>
        <div className="panelhead-right">
          <div className="legend">
            <span className="it"><span className="dot" style={{ background: "#A0A0BE" }} />Bon</span>
            <span className="it"><span className="dot" style={{ background: "var(--c-risk)" }} />À risque</span>
            <span className="it"><span className="dot" style={{ background: "var(--c-out)" }} />Rupture</span>
          </div>
          <div className="view-toggle">
            <button className={!is3D ? "active" : ""} onClick={() => setIs3D(false)}>2D</button>
            <button className={is3D ? "active" : ""} onClick={() => setIs3D(true)}>3D</button>
          </div>
        </div>
      </div>
      <div className="map">
        <div className="fp-wings"><span>Aile A — Niveau 2</span><span>Aile B — Niveau 2</span></div>
        <FloorPlan3D armoires={armoires} onSelect={onSelect} selectedId={selectedId} is3D={is3D} />
      </div>
    </div>
  );
}

// --- drawer: B3 has live sensor items, others show static items ---

function ArmoireDrawer({ armoire, onClose, sensorData, onShowChart }) {
  const open = !!armoire;
  const isB3 = armoire && armoire.id === "B3";
  const out = armoire && armoire.status === "out";

  return (
    <React.Fragment>
      <div className={"scrim" + (open ? " open" : "")} onClick={onClose} />
      <aside className={"drawer" + (open ? " open" : "")}>
        {armoire && (
          <React.Fragment>
            <div className="dh">
              <div>
                <div className="zl">{armoire.id.startsWith("A") ? "Aile A — Niveau 2" : "Aile B — Niveau 2"} · STOCK</div>
                <h3>{armoire.name}</h3>
                <span className={"statuspill" + (out ? " out" : "")}>
                  <span className="d" />{STLABEL[armoire.status]} · {armoire.fill}%
                </span>
              </div>
              <button className="closebtn" onClick={onClose}><Icon name="x" size={20} /></button>
            </div>
            <div className="dbody">
              {isB3 && (
                <React.Fragment>
                  <LiveItem label="Couches" sensorName="laser" data={sensorData.laser}
                    onClick={() => onShowChart && onShowChart({ deviceId: "laser", sensorName: "laser", label: "Couches" })} />
                  <LiveItem label="Alèses" sensorName="ultrason" data={sensorData.ultrason}
                    onClick={() => onShowChart && onShowChart({ deviceId: "leonardieu", sensorName: "ultrason", label: "Alèses" })} />
                </React.Fragment>
              )}
              {armoire.items.map(it => (
                <div className="linerow" key={it.name}>
                  <div className="top">
                    <span className="nm">{it.name}</span>
                    <span className="q">{it.qty} / {it.par}</span>
                  </div>
                  <div className="track"><div className={"fill" + (it.pct < 40 ? " low" : "")} style={{ width: it.pct + "%" }} /></div>
                </div>
              ))}
            </div>
            {isB3 && <DiagnosticToggle sensorData={sensorData} />}
          </React.Fragment>
        )}
      </aside>
    </React.Fragment>
  );
}

// --- live sensor line item ---

function LiveItem({ label, sensorName, data, onClick }) {
  var layers = data ? data.layers : null;
  var isError = data && data.flag === "erreur_mesure";
  var par = 9;
  var pct = layers !== null ? Math.round((layers / par) * 100) : 0;

  return (
    <div className={"linerow" + (onClick ? " clickable" : "")} onClick={onClick}>
      <div className="top">
        <span className="nm">
          {label}
          {onClick && <Icon name="chevron-right" size={14} style={{ color: "var(--pur)", marginLeft: 4 }} />}
        </span>
        <span className="q">
          {layers !== null ? (layers + " / " + par) : isError ? "Hors portée" : "— / " + par}
        </span>
      </div>
      <div className="track">
        <div className="fill" style={{ width: pct + "%" }} />
      </div>
    </div>
  );
}

// --- live "il y a Xs" counter ---

function TimeSince({ isoStr }) {
  var [now, setNow] = React.useState(Date.now());
  React.useEffect(function () {
    var id = setInterval(function () { setNow(Date.now()); }, 1000);
    return function () { clearInterval(id); };
  }, []);
  if (!isoStr) return <span>—</span>;
  var diff = Math.floor((now - new Date(isoStr).getTime()) / 1000);
  if (diff < 0) diff = 0;
  var text;
  if (diff < 60) text = "il y a " + diff + "s";
  else if (diff < 3600) text = "il y a " + Math.floor(diff / 60) + "min";
  else text = "il y a " + Math.floor(diff / 3600) + "h";
  return <span>{text}</span>;
}

// --- collapsible diagnostic panel ---

function DiagnosticToggle({ sensorData }) {
  const [open, setOpen] = React.useState(false);
  return (
    <div className="diag-toggle">
      <button className="diag-btn" onClick={() => setOpen(o => !o)}>
        <Icon name={open ? "chevron-down" : "chevron-right"} size={16} />
        Diagnostic capteurs
      </button>
      {open && (
        <div className="diag-body">
          <div className="debug-grid">
            <div>
              <div className="debug-section-title">Laser</div>
              {sensorData.laser ? (
                <React.Fragment>
                  <div className="debug-row"><span className="label">Distance</span><span className="value">{sensorData.laser.distance_mm} mm</span></div>
                  <div className="debug-row"><span className="label">Couches</span><span className="value">{sensorData.laser.layers} / 9</span></div>
                  <div className="debug-row"><span className="label">Confiance</span><span className="value">{Math.round(sensorData.laser.confidence * 100)}%</span></div>
                  <div className="debug-row"><span className="label">Flag</span><span className="value">{sensorData.laser.flag}</span></div>
                  <div className="debug-row"><span className="label">Dernière MAJ</span><span className="value"><TimeSince isoStr={sensorData.laser.received_at} /></span></div>
                </React.Fragment>
              ) : (
                <div className="debug-row"><span className="label">Status</span><span className="value">Aucune donnée</span></div>
              )}
            </div>
            <div>
              <div className="debug-section-title">Ultrason</div>
              {sensorData.ultrason ? (
                <React.Fragment>
                  <div className="debug-row"><span className="label">Distance</span><span className="value">{sensorData.ultrason.distance_mm} mm</span></div>
                  <div className="debug-row"><span className="label">Couches</span><span className="value">{sensorData.ultrason.layers} / 9</span></div>
                  <div className="debug-row"><span className="label">Confiance</span><span className="value">{Math.round(sensorData.ultrason.confidence * 100)}%</span></div>
                  <div className="debug-row"><span className="label">Flag</span><span className="value">{sensorData.ultrason.flag}</span></div>
                  <div className="debug-row"><span className="label">Dernière MAJ</span><span className="value"><TimeSince isoStr={sensorData.ultrason.received_at} /></span></div>
                </React.Fragment>
              ) : (
                <div className="debug-row"><span className="label">Status</span><span className="value">Aucune donnée</span></div>
              )}
            </div>
          </div>
        </div>
      )}
    </div>
  );
}

// --- stock view: alert + KPIs + floor plan ---

function StockView({ sensorData, pollStatus, corsMode, selected, onSelect, notificationActive }) {
  var armoires = getArmoires(sensorData);
  var ruptureCount = armoires.filter(a => a.status === "out").length;
  var showAlert = ruptureCount > 0 || notificationActive;
  var alertCount = notificationActive ? Math.max(ruptureCount, 1) : ruptureCount;

  return (
    <React.Fragment>
      {showAlert && (
        <button className="alert" onClick={() => onSelect(armoires.find(a => a.status === "out") || armoires.find(a => a.id === "B3"))}>
          <span className="dot" />
          {alertCount} armoire{alertCount > 1 ? "s" : ""} en rupture — Aile B, niveau 2
          <span className="chev"><Icon name="chevron-right" size={20} /></span>
        </button>
      )}
      <KpiRow sensorData={sensorData} />
      <FloorPlan armoires={armoires} onSelect={onSelect} selectedId={selected && selected.id} />
    </React.Fragment>
  );
}

// --- history chart view (full page) ---

var RANGES = [
  { key: "1h", label: "1h" },
  { key: "6h", label: "6h" },
  { key: "12h", label: "12h" },
  { key: "24h", label: "24h" },
  { key: "72h", label: "3j" },
  { key: "168h", label: "7j" },
];

function HistoryView({ chartView, sensorData, onBack }) {
  var [activePreset, setActivePreset] = React.useState("24h");
  var [dateRange, setDateRange] = React.useState(function () {
    var now = new Date();
    return { start: new Date(now.getTime() - 24 * 3600000), end: now };
  });
  var [records, setRecords] = React.useState(null);
  var [loading, setLoading] = React.useState(true);
  var [loadingMsg, setLoadingMsg] = React.useState("Chargement…");
  var [loadingPct, setLoadingPct] = React.useState(0);
  var canvasRef = React.useRef(null);
  var chartRef = React.useRef(null);
  var fetchGen = React.useRef(0);
  var pickerRef = React.useRef(null);
  var fpInstance = React.useRef(null);

  var currentData = sensorData[chartView.sensorName];
  var currentLayers = currentData ? currentData.layers : null;

  function selectPreset(key) {
    var durMs = window.TTN_parseDuration(key);
    var now = new Date();
    setActivePreset(key);
    setDateRange({ start: new Date(now.getTime() - durMs), end: now });
  }

  function selectCustomRange(dates) {
    if (dates.length === 2) {
      var start = dates[0];
      var end = new Date(dates[1].getTime() + 86400000 - 1);
      setActivePreset(null);
      setDateRange({ start: start, end: end });
    }
  }

  React.useEffect(function () {
    if (!pickerRef.current) return;
    fpInstance.current = flatpickr(pickerRef.current, {
      mode: "range",
      dateFormat: "d M Y",
      maxDate: "today",
      locale: { firstDayOfWeek: 1, rangeSeparator: "  →  " },
      defaultDate: [dateRange.start, dateRange.end],
      onChange: selectCustomRange,
    });
    return function () {
      if (fpInstance.current) fpInstance.current.destroy();
    };
  }, []);

  React.useEffect(function () {
    setLoading(true);
    setRecords(null);
    if (chartRef.current) {
      chartRef.current.destroy();
      chartRef.current = null;
    }
    setLoadingMsg("Chargement…");
    setLoadingPct(0);
    var gen = ++fetchGen.current;
    var startISO = dateRange.start.toISOString();
    var endISO = dateRange.end.toISOString();
    var spanMs = dateRange.end.getTime() - dateRange.start.getTime();
    var expectedRecords = Math.round(spanMs / 60000);
    console.log("[HISTORY] Loading", chartView.deviceId, "from:", startISO, "to:", endISO, "gen:", gen);

    window.TTN_fetchHistoryRange(chartView.deviceId, startISO, endISO, function (pageNum, recordCount) {
      if (gen !== fetchGen.current) return;
      var pct = Math.min(95, Math.round((recordCount / expectedRecords) * 100));
      setLoadingPct(pct);
      if (pageNum === 0) {
        setLoadingMsg(recordCount + " en cache…");
      } else {
        setLoadingMsg(recordCount + " mesures (page " + pageNum + ")…");
      }
    })
      .then(function (data) {
        if (gen !== fetchGen.current) {
          console.log("[HISTORY] Ignoring stale result gen:", gen, "current:", fetchGen.current);
          return;
        }
        console.log("[HISTORY] Got", data.length, "records for chart");
        setRecords(data);
        setLoading(false);
      })
      .catch(function (err) {
        if (gen !== fetchGen.current) return;
        console.error("[HISTORY] Fetch failed:", err);
        setRecords([]);
        setLoading(false);
      });
  }, [chartView.deviceId, dateRange.start.getTime(), dateRange.end.getTime()]);

  React.useEffect(function () {
    if (!records || !canvasRef.current) return;

    if (chartRef.current) {
      chartRef.current.destroy();
      chartRef.current = null;
    }

    var spanMs = dateRange.end.getTime() - dateRange.start.getTime();
    var isMultiDay = spanMs > 24 * 3600000;
    var sType = chartView.sensorName === "laser" ? "laser" : "ultrason";
    var rangeStart = dateRange.start;
    var rangeEnd = dateRange.end;

    function formatLabel(d) {
      if (isMultiDay) {
        return String(d.getDate()).padStart(2, "0") + "/" + String(d.getMonth() + 1).padStart(2, "0") + " " + String(d.getHours()).padStart(2, "0") + ":" + String(d.getMinutes()).padStart(2, "0");
      }
      return String(d.getHours()).padStart(2, "0") + ":" + String(d.getMinutes()).padStart(2, "0");
    }

    var points = [];
    points.push({ label: formatLabel(rangeStart), layers: null, distance: null, time: rangeStart.toISOString(), isError: false, isBoundary: true });

    records.forEach(function (r) {
      var isError = r.distance >= 65000;
      var det = isError ? { layers: 0 } : window.detectLayers(r.distance, sType);
      points.push({ label: formatLabel(new Date(r.time)), layers: det.layers, distance: r.distance, time: r.time, isError: isError, isBoundary: false });
    });

    points.push({ label: formatLabel(rangeEnd), layers: null, distance: null, time: rangeEnd.toISOString(), isError: false, isBoundary: true });

    var labels = points.map(function (p) { return p.label; });

    var segmentStyle = {
      borderColor: function (ctx) {
        var p = points[ctx.p0DataIndex];
        var p1 = points[ctx.p1DataIndex];
        if (p && p.isError && p1 && p1.isError) return "#A0A0BE";
        return "#2E3770";
      },
      borderDash: function (ctx) {
        var p = points[ctx.p0DataIndex];
        var p1 = points[ctx.p1DataIndex];
        if (p && p.isError && p1 && p1.isError) return [4, 4];
        return [];
      },
    };

    var ctx = canvasRef.current.getContext("2d");
    chartRef.current = new Chart(ctx, {
      type: "line",
      data: {
        labels: labels,
        datasets: [{
          label: chartView.label,
          data: points.map(function (p) { return p.layers; }),
          borderColor: "#2E3770",
          backgroundColor: "rgba(46,55,112,0.08)",
          fill: true,
          pointRadius: 0,
          borderWidth: 2,
          tension: 0.3,
          segment: segmentStyle,
        }],
      },
      options: {
        responsive: true,
        maintainAspectRatio: false,
        interaction: {
          mode: "index",
          intersect: false,
        },
        plugins: {
          legend: { display: false },
          tooltip: {
            enabled: true,
            callbacks: {
              title: function (items) {
                var idx = items[0].dataIndex;
                var d = new Date(points[idx].time);
                return d.toLocaleString("fr-CH");
              },
              label: function (item) {
                var idx = item.dataIndex;
                var p = points[idx];
                if (p.isBoundary) return "Pas de donnée";
                if (p.isError) return "Hors portée (" + p.distance + " mm)";
                if (p.layers === null) return "Pas de donnée";
                return p.layers + " couches (" + p.distance + " mm)";
              },
            },
          },
        },
        scales: {
          x: {
            display: true,
            grid: { color: "rgba(46,55,112,0.06)" },
            ticks: {
              font: { size: 11 },
              color: "#A0A0BE",
              maxRotation: 0,
              autoSkip: true,
              maxTicksLimit: 20,
            },
          },
          y: {
            display: true,
            min: 0, max: 9,
            ticks: { stepSize: 1, font: { size: 11 }, color: "#A0A0BE" },
            grid: { color: "rgba(46,55,112,0.06)" },
          },
        },
      },
    });

    return function () {
      if (chartRef.current) {
        chartRef.current.destroy();
        chartRef.current = null;
      }
    };
  }, [records]);

  return (
    <div className="history-page">
      <button className="history-back" onClick={onBack}>
        <Icon name="chevron-left" size={16} />
        Armoire B3
      </button>
      <div className="history-head">
        <h1>{chartView.label}</h1>
        <span className="history-current">
          {currentLayers !== null ? currentLayers + " / 9" : "—"}
        </span>
      </div>
      <div className="range-picker">
        {RANGES.map(function (r) {
          return (
            <button key={r.key}
              className={"range-btn" + (activePreset === r.key ? " active" : "")}
              onClick={function () {
                selectPreset(r.key);
                if (fpInstance.current) {
                  var durMs = window.TTN_parseDuration(r.key);
                  var now = new Date();
                  fpInstance.current.setDate([new Date(now.getTime() - durMs), now], false);
                }
              }}>
              {r.label}
            </button>
          );
        })}
        <span className="range-sep" />
        <div className="date-picker-wrap">
          <Icon name="calendar" size={15} />
          <input ref={pickerRef} className="date-picker-input" placeholder="Dates personnalisées" readOnly />
        </div>
      </div>
      <div className="chart-wrap">
        {loading && (
          <div className="chart-loading">
            <div className="chart-loading-inner">
              <div className="chart-loading-bar"><i style={{ width: loadingPct + "%" }} /></div>
              <span>{loadingMsg}</span>
            </div>
          </div>
        )}
        {records && records.length === 0 && !loading && (
          <div className="chart-loading">Aucune donnée pour cette période</div>
        )}
        <canvas ref={canvasRef} style={{ display: loading ? "none" : "block" }} />
      </div>
    </div>
  );
}

Object.assign(window, { StockView, ArmoireDrawer, HistoryView, getArmoires });
