// ══ FloodsFlow — LA GRILLE : qui est programmé, quel jour, jusqu'à quand ═══════════════
//
// LA PLAINTE, MOT POUR MOT : « faudrait qu'on puisse savoir quel compte a déjà été programmé
// pour poster, pour vraiment qu'on soit pas perdu... quel compte a été programmé pendant
// combien de jours... le planning qu'on a mis dans le concept il est vraiment pété ».
//
// CE QU'IL Y AVAIT. Une liste à plat, triée par heure. Sur douze comptes qui publient trois
// fois par jour pendant sept jours, cela fait 252 lignes empilées, où @chloe.abc apparaît
// vingt et une fois et @chloe.def pas une seule — et rien, dans cet empilement, ne permet de
// s'en apercevoir. La liste répondait à « qu'est-ce qui part à 14 h 12 ». Personne ne pose
// cette question.
//
// CE QU'IL Y A. Une grille : un compte par ligne, un jour par colonne. C'est la forme qui
// répond aux deux questions réellement posées — « qui est programmé » se lit en balayant une
// colonne, « pendant combien de jours » en balayant une ligne. Aucune autre forme ne donne
// les deux à la fois.
//
// TROIS RÈGLES QUI TIENNENT TOUT LE FICHIER :
//
//   1. LES RANGÉES VIENNENT DES COMPTES, PAS DES LIGNES DE PLANNING. Le compte qu'on cherche
//      est celui qui n'a RIEN. Une grille bâtie sur le planning ne l'aurait pas.
//   2. LE JOUR D'UNE COLONNE EST UN JOUR UTC. C'est déjà le jour de `dates_faites`, de
//      `_partout` et du calendrier. En prendre un autre ferait dire à la grille qu'une
//      journée est libre pendant que le planificateur la refuse. On affiche donc la date UTC
//      et on le dit en pied.
//   3. UNE DATE-CHAÎNE NE PASSE JAMAIS PAR `new Date(iso)`. Voir FG_DATE : c'est la
//      correction directe du double comptage de l'ancien panneau, qui groupait en UTC et
//      titrait en heure locale.

// ── FG_DATE : la seule façon d'obtenir un objet Date depuis « 2026-09-03 » ─────────────
//
// `new Date("2026-09-03")` est interprété en UTC MINUIT. À l'ouest de Greenwich, cela tombe
// la veille au soir : l'ancien panneau titrait « mardi 2 » une colonne remplie de lignes du
// mercredi 3. On reconstruit donc la date à MIDI LOCAL — assez loin des deux bords pour
// qu'aucun décalage horaire, changement d'heure compris, ne la fasse changer de jour.
function FG_DATE(iso) {
  const [a, m, j] = String(iso || "").split("-").map(Number);
  return new Date(a, (m || 1) - 1, j || 1, 12, 0, 0);
}

const FG_JOURS = ["dim", "lun", "mar", "mer", "jeu", "ven", "sam"];
const FG_MOIS = ["janv.", "févr.", "mars", "avr.", "mai", "juin", "juil.", "août", "sept.",
                 "oct.", "nov.", "déc."];

function FG_COURT(iso) {
  const d = FG_DATE(iso);
  return FG_JOURS[d.getDay()] + " " + d.getDate();
}

function FG_LONG(iso, aujourdhui) {
  const d = FG_DATE(iso);
  const nom = FG_JOURS[d.getDay()] + " " + d.getDate() + " " + FG_MOIS[d.getMonth()];
  if (iso === aujourdhui) return "aujourd'hui · " + nom;
  return nom;
}

// ── Les cinq états d'une case ─────────────────────────────────────────────────────────
//
// Ce sont des ÉTATS, pas des séries : on reprend donc la palette de statut déjà employée par
// la carte, le planning et le calendrier — vert « c'est parti », ambre « à envoyer », rouge
// « un œil humain ». En changer ici ferait de cet écran le seul du produit à parler une autre
// langue.
//
// LA COULEUR NE PORTE JAMAIS SEULE. Chaque case affiche son NOMBRE, l'infobulle nomme l'état
// en toutes lettres, et « parti » (fond plein) se distingue de « à envoyer » (contour) par la
// forme autant que par la teinte — mesuré : ces deux-là sont la paire la plus serrée de la
// palette pour un œil protanope (ΔE 8,2).
const FG_ETATS = {
  bloque: { nom: "bloquée chez bundle.social", trait: "var(--danger, #ff5c6c)",
            fond: "rgba(255,92,108,.18)" },
  prevu:  { nom: "programmée ici, pas encore envoyée", trait: "var(--busy, #f5b942)",
            fond: "rgba(245,185,66,.15)" },
  parti:  { nom: "remise à bundle.social — elle partira seule", trait: "var(--accent)",
            fond: "rgba(0,224,138,.16)" },
  perime: { nom: "l'heure est passée — elle ne partira plus", trait: "var(--faint)",
            fond: "rgba(140,150,175,.10)" },
  vide:   { nom: "rien ce jour-là", trait: "var(--faint)", fond: "transparent" },
};

// L'ÉTAT DOMINANT D'UNE CASE, ET L'ORDRE EST UNE DÉCISION. Il ne suit pas le nombre le plus
// grand, il suit le GESTE À FAIRE : une bloquée réclame un humain, une prévue réclame un
// envoi, une partie ne réclame rien. Trier par quantité aurait peint en vert une journée qui
// attend qu'on l'envoie.
function FG_TON(c) {
  if (!c) return "vide";
  if (c[0]) return "bloque";
  if (c[4]) return "prevu";
  if (c[1] + c[2]) return "parti";
  if (c[3]) return "perime";
  return "vide";
}

// Ce qui sortira vraiment de cette case. Les périmées en sont exclues : elles ne partiront
// jamais, et les additionner ferait passer une journée morte pour une journée pleine.
const FG_SORTANT = (c) => (c ? c[0] + c[1] + c[2] + c[4] : 0);

// ── Styles, définis ici et pas empruntés ──────────────────────────────────────────────
// Les fichiers .jsx partagent une seule portée globale : emprunter `fcBoite` marcherait, mais
// lierait cet écran à l'ordre de chargement. Ces quatre objets ne coûtent rien.
const FG_VOILE = {
  position: "fixed", inset: 0, zIndex: 9000, background: "rgba(3,7,15,.72)",
  display: "flex", alignItems: "center", justifyContent: "center", padding: 20,
};
const FG_BOITE = {
  width: "min(1100px, 100%)", maxHeight: "90vh", display: "flex", flexDirection: "column",
  background: "var(--surface)", border: "1px solid var(--border)", borderRadius: 15,
  padding: "18px 20px", boxShadow: "0 24px 60px rgba(0,0,0,.55)",
};
const FG_BTN = (accent) => ({
  display: "inline-flex", alignItems: "center", gap: 6, padding: "6px 12px", borderRadius: 9,
  fontSize: 12.5, fontWeight: 600, cursor: "pointer", font: "inherit",
  background: accent ? "var(--accent)" : "var(--surface-2, rgba(255,255,255,.05))",
  color: accent ? "#06281c" : "var(--text)",
  border: "1px solid " + (accent ? "var(--accent)" : "var(--border)"),
});
const FG_ONGLET = (actif) => ({
  padding: "5px 11px", borderRadius: 8, fontSize: 12, fontWeight: 600, cursor: "pointer",
  font: "inherit", border: "1px solid " + (actif ? "var(--accent)" : "var(--border)"),
  background: actif ? "rgba(0,224,138,.12)" : "transparent",
  color: actif ? "var(--accent)" : "var(--muted)",
});

// ── La bande de couverture, en 6 px de haut ───────────────────────────────────────────
//
// UN SEUL COMPOSANT POUR DEUX ÉCRANS, et c'est délibéré : c'est la même donnée, donc ce doit
// être le même dessin. Dans la grille, c'est la ligne du compte ; dans « Lancer la
// publication », c'est la vignette posée à côté de son nom. Deux dessins différents pour la
// même chose obligeraient l'opérateur à réapprendre en changeant d'écran.
function FgBande({ cases, jours, hauteur }) {
  const h = hauteur || 6;
  return (
    <div style={{ display: "flex", gap: 1, height: h }}>
      {(jours || []).map((j, i) => {
        const c = cases ? cases[i] : null;
        const t = FG_ETATS[FG_TON(c)];
        return (
          <i key={j} title={j} style={{ flex: 1, minWidth: 2, borderRadius: 1,
            background: FG_SORTANT(c) || (c && c[3]) ? t.trait : "var(--border)",
            opacity: FG_SORTANT(c) ? 1 : .45 }} />
        );
      })}
    </div>
  );
}

// ── Ce qu'on écrit au bout d'une ligne ────────────────────────────────────────────────
//
// « COUVERT JUSQU'AU 9 » ET « 6 JOURS COUVERTS » NE DISENT PAS LA MÊME CHOSE, et l'opérateur
// lit le second dans le premier. Quand il y a des trous, c'est le trou qu'on nomme — c'est
// lui le problème, pas la date de fin.
function FgResume({ f, jours }) {
  const n = f.jours_couverts;
  // LE REFUS N'EST PAS ICI. Il a sa bande sous la rangee : le repeter dans cette colonne lui
  // volait la place de la seule information qu'on vient chercher -- combien de jours.
  // « PAS BRANCHE » NE PREND LE PAS QUE SUR LE VIDE. Un compte debranche qui porte encore
  // quarante-deux publications chez le fournisseur doit lire sa couverture, pas son statut :
  // ce sont ces lignes-la qui vont partir, et c'est le seul endroit qui les montre.
  if (!f.branche && f.membre && !n) {
    return <span style={{ color: "var(--faint)" }}>pas branché</span>;
  }
  if (!f.branche && f.membre) {
    return (
      <span style={{ color: "var(--busy, #f5b942)" }}
        title={"Ce compte n'est plus branché, mais ces publications sont déjà chez "
          + "bundle.social : elles partiront quand même."}>
        débranché · <b style={{ color: "var(--text)" }}>{n}</b> j déjà chez eux
      </span>
    );
  }
  if (!n) {
    return <span style={{ color: "var(--busy, #f5b942)" }}>rien de programmé</span>;
  }
  // `trous` est plafonne a douze par le backend ; le compte exact est a cote.
  const trous = f.trous_total !== undefined ? f.trous_total : (f.trous || []).length;
  return (
    <span style={{ color: trous ? "var(--busy, #f5b942)" : "var(--muted)" }}>
      <b style={{ color: "var(--text)" }}>{n}</b> j sur {jours.length}
      {trous ? " · " + trous + " trou" + (trous > 1 ? "s" : "") : ""}
      {!trous && f.couvert_jusqu_a ? " · jusqu'au " + FG_COURT(f.couvert_jusqu_a) : ""}
    </span>
  );
}

// ═══════════════════════════════════════════════════════════════════════════════════════
// LA GRILLE
// ═══════════════════════════════════════════════════════════════════════════════════════
function VueGrille({ concept, surFermer, surProgrammer, surAvis }) {
  const { useState, useEffect } = React;
  const [jours, setJours] = useState(14);
  const [d, setD] = useState(null);
  const [erreur, setErreur] = useState("");
  const [tri, setTri] = useState("urgence");        // urgence | nom
  const [choisis, setChoisis] = useState([]);
  const [tout, setTout] = useState(false);          // déplier les comptes sans souci
  const [bulle, setBulle] = useState(null);
  // TOUS LES ETATS SONT DECLARES ICI, EN HAUT. React exige que l'ordre des Hooks ne varie
  // jamais d'un rendu a l'autre ; en poser un au milieu du corps marche tant qu'aucun
  // « return » anticipe ne le precede -- c'est-a-dire jusqu'au jour ou quelqu'un en ajoute un.
  const [voirAttente, setVoirAttente] = useState(false);

  const base = () => (window.Backend && window.Backend.base) || "";

  const charger = async (n) => {
    try {
      // LE NOMBRE DE JOURS VOYAGE DANS LE CHEMIN. `do_GET` coupe la chaîne de requête avant
      // d'atteindre le routeur : un « ?jours= » n'arriverait jamais. Même cicatrice que le
      // mois du calendrier.
      const r = await fetch(base() + "/api/floodsflow/concepts/" + concept.id + "/grille/" + n,
        { cache: "no-store" });
      const j = await r.json();
      if (j && j.ok) {
        setD(j); setErreur("");
        // ON PURGE LA SELECTION A CHAQUE LECTURE. Un compte coche peut avoir bascule en
        // « refuse » entre-temps -- c'est ce que fait « Verifier les comptes ». Sa rangee
        // cesse alors d'etre cliquable, donc plus personne ne peut le decocher, et il
        // continue pourtant de compter dans « Programmer ces N ».
        const bons = {};
        (j.comptes || []).forEach((f) => { bons[f.compte] = f.branche && !f.refus; });
        setChoisis((l) => l.filter((u) => bons[u]));
      } else setErreur((j && j.error) || "grille indisponible");
    } catch (e) { setErreur("backend injoignable — " + e.message); }
  };

  useEffect(() => { setD(null); charger(jours); }, [concept.id, jours]);
  // Une publication qui part change la grille : sans ça elle continue d'annoncer « à envoyer »
  // pendant que la barre de progression monte à côté.
  useEffect(() => {
    const surEnvoi = (ev) => { if (!ev.detail || ev.detail.id === concept.id) setTimeout(() => charger(jours), 1400); };
    window.addEventListener("floodsflow:envoi", surEnvoi);
    return () => window.removeEventListener("floodsflow:envoi", surEnvoi);
  }, [concept.id, jours]);

  const colonnes = (d && d.jours) || [];
  const fiches = (d && d.comptes) || [];
  const hors = (d && d.hors_concept) || [];

  // LE TRI PAR DÉFAUT EST L'URGENCE, PAS L'ALPHABET. Un tri alphabétique met en haut de
  // l'écran ce qui va bien. On remonte donc : ce qui ne publiera pas, ce qui est bloqué, puis
  // le moins couvert. La bascule A→Z existe pour retrouver un compte qu'on cherche par son nom.
  const bloquees = (f) => (f.cases || []).reduce((a, c) => a + c[0], 0);
  const rangees = fiches.slice().sort((x, y) => {
    if (tri === "nom") return x.compte.localeCompare(y.compte);
    const p = (f) => (f.refus ? 0 : bloquees(f) ? 1 : 2);
    return p(x) - p(y) || x.jours_couverts - y.jours_couverts
      || x.reste_file - y.reste_file || x.compte.localeCompare(y.compte);
  });

  // CE QU'ON REPLIE : les comptes qui vont bien et sur lesquels il n'y a rien à décider. Sur
  // un parc de soixante comptes dont cinquante-deux tournent, ce sont les huit autres qu'on
  // ouvre l'écran pour voir.
  const paisible = (f) => !f.refus && f.branche && !bloquees(f)
    && f.jours_couverts >= colonnes.length && !(f.trous_total || 0);
  // PAS ENCORE BRANCHE N'EST PAS UNE PANNE, C'EST UNE ETAPE. Sur un concept neuf ils sont la
  // majorite -- neuf sur treize, mesure sur le parc -- et leur donner a chacun sa rangee
  // repoussait sous la ligne de flottaison les quatre comptes qui travaillent. Ils sont donc
  // groupes en une ligne, depliable, et ils gardent leur rangee des qu'on la deplie.
  // « PAS ENCORE BRANCHE » N'EST VRAI QUE S'IL N'Y A RIEN D'AUTRE A DIRE. Un compte peut
  // avoir ete branche, avoir recu quatorze jours de publications, puis etre repasse en
  // attente par « Verifier ». Ses quarante-deux lignes existent toujours chez le fournisseur
  // et partiront : le ranger dans le repli « rien ne peut leur etre programme » les effacait
  // de l'ecran. Des qu'un compte porte de la couverture, la couverture gagne.
  const attente = rangees.filter((f) => !f.branche && !f.jours_couverts);
  const actifs = rangees.filter((f) => f.branche || f.jours_couverts);
  const visibles = tout ? actifs : actifs.filter((f) => !paisible(f));
  const caches = actifs.length - visibles.length;

  // ON NE PRESELECTIONNE QUE CE QUI PEUT PUBLIER. Cocher un compte non branche ou malade
  // ferait programmer des lignes que `publier` sautera : du travail qui a l'air fait.
  const aSec = actifs.filter((f) => !f.refus && !f.jours_couverts).map((f) => f.compte);
  const incomplets = actifs.filter((f) => !f.refus && !paisible(f)).map((f) => f.compte);
  const bascule = (u) =>
    setChoisis((l) => (l.indexOf(u) >= 0 ? l.filter((x) => x !== u) : l.concat(u)));

  const largeurNom = 168;
  // UNE COLONNE NE DESCEND PAS SOUS LA LARGEUR DE SON CHIFFRE. Avec `minmax(0, 1fr)`, trente
  // colonnes dans une fenetre de 900 px (une demi-ecran, cas courant) tombaient a 13 px : les
  // nombres se chevauchaient et la grille devenait illisible sans qu'aucune barre de
  // defilement n'apparaisse. Un plancher de 20 px laisse le conteneur -- qui est deja en
  // `overflow: auto` -- produire enfin sa barre horizontale.
  const colonneJour = "minmax(20px, 1fr)";
  const cellule = (f, i) => {
    const c = f.cases ? f.cases[i] : null;
    const sortant = FG_SORTANT(c);
    const ton = FG_TON(c);
    const t = FG_ETATS[ton];
    const plein = ton === "parti";                  // le fond plein contre le contour : la
                                                    // forme redit ce que la teinte dit déjà
    const survol = (ev) => setBulle({
      x: ev.clientX, y: ev.clientY, compte: f.compte, jour: colonnes[i], c: c,
    });
    return (
      <div key={colonnes[i]} onMouseEnter={survol} onMouseMove={survol}
        onMouseLeave={() => setBulle(null)}
        style={{ height: 26, borderRadius: 5, display: "grid", placeItems: "center",
          fontSize: 11.5, fontWeight: 700, fontVariantNumeric: "tabular-nums",
          cursor: "default", position: "relative",
          color: sortant || (c && c[3]) ? "var(--text)" : "var(--faint)",
          background: plein ? t.fond : ton === "vide" ? "transparent" : t.fond,
          border: "1px solid " + (ton === "vide" ? "var(--border)"
            : plein ? "transparent" : t.trait) }}>
        {sortant ? sortant : c && c[3] ? "⌫" : "·"}
        {/* Le petit point du coin : ce compte publie AUSSI pour un autre concept ce jour-là.
            Sans lui, on programme une journée que le planificateur refusera en silence. */}
        {c && c[5] ? (
          <i style={{ position: "absolute", top: 2, right: 3, width: 4, height: 4,
            borderRadius: 9, background: "var(--accent-2, #22d3ee)" }} />
        ) : null}
      </div>
    );
  };

  // LA CAUSE S'AJOUTE À LA RANGÉE, ELLE NE LA REMPLACE PAS. Première version : un compte
  // refusé perdait ses cases au profit d'une bande rouge. Mesuré sur le parc réel, c'était
  // faux et grave : deux comptes en « checkpoint » portent chacun DIX-HUIT publications
  // programmées que `publier` sautera en bloc. Les cacher aurait fait disparaître trente-six
  // lignes de l'écran censé les montrer. On garde donc les cases — elles existent — on les
  // éteint, et la bande dit dessous pourquoi rien n'en sortira.
  const bandeRefus = (f) => (
    <div key={"r" + f.compte}
      style={{ display: "grid", gap: 3, marginBottom: 3,
        gridTemplateColumns: largeurNom + "px minmax(0, 1fr)" }}>
      <span style={{ position: "sticky", left: 0, zIndex: 1, alignSelf: "stretch",
        background: "var(--surface)" }} />
      <div title={f.refus}
        style={{ borderRadius: 5, display: "flex", alignItems: "center", gap: 7,
          padding: "3px 9px", fontSize: 11,
          background: "rgba(255,92,108,.10)", border: "1px solid rgba(255,92,108,.28)",
          color: "var(--danger, #ff5c6c)" }}>
        <b style={{ flexShrink: 0 }}>⨯ publier le sautera</b>
        <span style={{ color: "var(--muted)", overflow: "hidden", textOverflow: "ellipsis",
          whiteSpace: "nowrap" }}>{f.refus}</span>
      </div>
    </div>
  );

  const rangee = (f, membre) => {
    const on = choisis.indexOf(f.compte) >= 0;
    return (
      <React.Fragment key={(membre ? "m" : "h") + f.compte}>
      <div style={{ display: "grid", gap: 3, alignItems: "center",
        gridTemplateColumns: largeurNom + "px repeat(" + colonnes.length
          + ", " + colonneJour + ") 150px",
        padding: "3px 0", opacity: f.refus ? .55 : 1 }}>
        <div onClick={() => membre && !f.refus && bascule(f.compte)}
          title={f.refus ? "publier sautera ce compte : " + f.refus
            : membre ? "Cocher pour le programmer depuis le pied de cette fenêtre" : ""}
          style={{ display: "flex", alignItems: "center", gap: 6, minWidth: 0,
            // COLLEE A GAUCHE : sur trente colonnes il faut defiler, et une grille dont les
            // pseudos sont sortis de l'ecran ne dit plus de qui elle parle.
            // Toute la hauteur de la rangee : un bandeau moins haut que les cases laisse
            // leurs bords arrondis depasser au-dessus et au-dessous du pseudo.
            position: "sticky", left: 0, zIndex: 1, background: "var(--surface)",
            alignSelf: "stretch", paddingRight: 6,
            cursor: membre && !f.refus ? "pointer" : "default" }}>
          {membre && (
            <span style={{ width: 13, height: 13, borderRadius: 4, flexShrink: 0, fontSize: 10,
              fontWeight: 900, lineHeight: "12px", textAlign: "center", color: "#06281c",
              border: "1px solid " + (on ? "var(--accent)" : "var(--border)"),
              background: on ? "var(--accent)" : "transparent" }}>{on ? "✓" : ""}</span>
          )}
          <span style={{ flex: 1, minWidth: 0, fontSize: 12, overflow: "hidden",
            textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{f.compte}</span>
          {f.reste_file > 0 && (
            <span title={f.reste_file + " contenu(s) encore en file pour ce compte — "
              + "au-delà, il n'y a plus rien à programmer"}
              style={{ fontSize: 10, color: "var(--faint)",
                fontVariantNumeric: "tabular-nums" }}>{f.reste_file}</span>
          )}
        </div>
        {colonnes.map((j, i) => cellule(f, i))}
        <div style={{ fontSize: 11, textAlign: "right", overflow: "hidden",
          textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
          <FgResume f={f} jours={colonnes} />
        </div>
      </div>
      {f.refus && f.branche ? bandeRefus(f) : null}
      </React.Fragment>
    );
  };

  const b = (d && d.bilan) || {};
  const tt = (d && d.totaux) || {};

  return (
    <div style={FG_VOILE} onMouseDown={(e) => { if (e.target === e.currentTarget) surFermer(); }}>
      <div style={FG_BOITE}>
        {/* ── en-tête ─────────────────────────────────────────────────────────────── */}
        <div style={{ display: "flex", alignItems: "center", gap: 10, flexWrap: "wrap" }}>
          <div style={{ fontSize: 16, fontWeight: 800, flex: 1, minWidth: 200 }}>
            Planning de « {concept.nom} »
          </div>
          {[7, 14, 30].map((n) => (
            <button key={n} onClick={() => setJours(n)} style={FG_ONGLET(jours === n)}>
              {n} jours
            </button>
          ))}
          <button onClick={() => setTri(tri === "urgence" ? "nom" : "urgence")}
            style={FG_ONGLET(false)}
            title="Par défaut, ce qui demande une décision remonte en haut">
            {tri === "urgence" ? "⇅ par urgence" : "⇅ A → Z"}
          </button>
        </div>

        <div style={{ fontSize: 12, color: "var(--faint)", margin: "5px 0 12px" }}>
          {!d ? "chargement…" : (
            <React.Fragment>
              <b style={{ color: "var(--text)" }}>{d.branches}</b> compte(s) branché(s) ·
              une journée pleine en ferait <b style={{ color: "var(--text)" }}>{d.attendu_par_jour}</b>
              {" · "}
              <span style={{ color: "var(--accent)" }}>{tt.parties || 0} partie(s)</span>
              {" · "}
              <span style={{ color: "var(--busy, #f5b942)" }}>{tt.prevues || 0} à envoyer</span>
              {tt.perimees ? " · " + tt.perimees + " périmée(s)" : ""}
              {tt.brouillons ? " · " + tt.brouillons + " brouillon(s)" : ""}
              {tt.bloquees ? (
                <span style={{ color: "var(--danger, #ff5c6c)" }}> · {tt.bloquees} bloquée(s)</span>
              ) : null}
            </React.Fragment>
          )}
        </div>

        {/* UNE ALARME NE DOIT PAS EXIGER DE TROUVER LA BONNE CASE : elle est épinglée en
            haut, hors de la grille. */}
        {tt.bloquees ? (
          <div style={{ padding: "8px 12px", borderRadius: 9, fontSize: 12, marginBottom: 10,
            background: "rgba(255,92,108,.08)", border: "1px solid rgba(255,92,108,.25)",
            color: "var(--danger, #ff5c6c)" }}>
            ⚠ {tt.bloquees} publication(s) bloquée(s) : bundle.social n'a pas répondu et on
            ignore si le post existe. Elles se débloquent une par une depuis le planning de la
            carte (« c'est parti » ou « renvoyer »).
          </div>
        ) : null}

        {d && d.tronque ? (
          <div style={{ padding: "8px 12px", borderRadius: 9, fontSize: 12, marginBottom: 10,
            background: "rgba(245,185,66,.08)", border: "1px solid rgba(245,185,66,.25)",
            color: "var(--busy, #f5b942)" }}>⚠ {d.tronque}</div>
        ) : null}

        {erreur ? (
          <div style={{ padding: "10px 14px", borderRadius: 9, fontSize: 12.5,
            background: "rgba(255,92,108,.08)", border: "1px solid rgba(255,92,108,.25)",
            color: "var(--danger, #ff5c6c)" }}>⚠ {erreur}</div>
        ) : !d ? (
          <div style={{ padding: 40, textAlign: "center", color: "var(--faint)" }}>chargement…</div>
        ) : (
          <div style={{ flex: 1, minHeight: 0, overflow: "auto" }}>
            {/* ── en-tête des colonnes ──────────────────────────────────────────── */}
            <div style={{ display: "grid", gap: 3, position: "sticky", top: 0, zIndex: 2,
              background: "var(--surface)", paddingBottom: 4,
              gridTemplateColumns: largeurNom + "px repeat(" + colonnes.length
                + ", " + colonneJour + ") 150px" }}>
              <div style={{ fontSize: 10, textTransform: "uppercase", letterSpacing: ".06em",
                color: "var(--faint)", position: "sticky", left: 0, zIndex: 1,
                background: "var(--surface)", alignSelf: "stretch", display: "flex",
                alignItems: "flex-end", paddingRight: 6 }}>compte</div>
              {colonnes.map((j) => (
                <div key={j} style={{ fontSize: 9.5, textAlign: "center", lineHeight: 1.25,
                  color: j === d.aujourdhui ? "var(--accent)" : "var(--faint)",
                  fontWeight: j === d.aujourdhui ? 800 : 500 }}>
                  {FG_COURT(j).split(" ")[0]}<br />{FG_COURT(j).split(" ")[1]}
                </div>
              ))}
              <div style={{ fontSize: 10, textTransform: "uppercase", letterSpacing: ".06em",
                color: "var(--faint)", textAlign: "right", alignSelf: "end" }}>couverture</div>
            </div>

            {visibles.map((f) => rangee(f, true))}

            {/* CE QU'ON REPLIE : les comptes sur lesquels il n'y a rien à décider. Sur
                soixante comptes dont cinquante-deux tournent, ce sont les huit autres qu'on
                ouvre cet écran pour voir. */}
            {(caches > 0 || tout) && (
              <div onClick={() => setTout(!tout)}
                style={{ padding: "8px 4px", fontSize: 11.5, color: "var(--faint)",
                  cursor: "pointer" }}>
                {tout ? (
                  <React.Fragment>
                    tous les comptes sont affichés —
                    <b style={{ color: "var(--muted)" }}> ne montrer que ceux à traiter</b>
                  </React.Fragment>
                ) : (
                  <React.Fragment>
                    … et {caches} compte(s) de plus, couverts sur toute la fenêtre et sans
                    alerte —<b style={{ color: "var(--muted)" }}> les afficher</b>
                  </React.Fragment>
                )}
              </div>
            )}

            {attente.length > 0 && (
              <div style={{ padding: "8px 4px", fontSize: 11.5, color: "var(--faint)" }}>
                <span onClick={() => setVoirAttente(!voirAttente)} style={{ cursor: "pointer" }}>
                  {attente.length} compte(s) pas encore branché(s) à bundle.social — rien ne
                  peut leur être programmé tant qu'ils ne le sont pas.
                  <b style={{ color: "var(--muted)" }}>
                    {voirAttente ? " les replier" : " les voir"}
                  </b>
                </span>
              </div>
            )}
            {voirAttente && attente.map((f) => rangee(f, true))}

            {/* ── la rangée de pied : combien de comptes servis, jour par jour ──── */}
            <div style={{ display: "grid", gap: 3, alignItems: "center", marginTop: 6,
              paddingTop: 6, borderTop: "1px solid var(--border)",
              gridTemplateColumns: largeurNom + "px repeat(" + colonnes.length
                + ", " + colonneJour + ") 150px" }}>
              <div style={{ fontSize: 10.5, color: "var(--faint)" }}>comptes servis</div>
              {colonnes.map((j, i) => {
                const n = (d.servis || [])[i] || 0;
                return (
                  <div key={j} title={n + " compte(s) sur " + d.branches + " branché(s) "
                    + "publieront le " + FG_LONG(j, d.aujourdhui)}
                    style={{ fontSize: 10.5, textAlign: "center",
                      fontVariantNumeric: "tabular-nums",
                      color: !n ? "var(--faint)" : n >= d.branches ? "var(--accent)"
                        : "var(--busy, #f5b942)" }}>{n}</div>
                );
              })}
              <div style={{ fontSize: 10.5, color: "var(--faint)", textAlign: "right" }}>
                sur {d.branches} branchés
              </div>
            </div>

            {/* ── les comptes sortis du concept ─────────────────────────────────── */}
            {hors.length > 0 && (
              <div style={{ marginTop: 14, paddingTop: 10,
                borderTop: "1px dashed var(--border-2, var(--border))" }}>
                <div style={{ fontSize: 11.5, color: "var(--busy, #f5b942)", marginBottom: 5 }}>
                  {hors.length} compte(s) SORTI(S) de la catégorie — leurs publications
                  partiront quand même. C'est le seul écran qui les montre encore.
                </div>
                {hors.map((f) => rangee(f, false))}
              </div>
            )}
          </div>
        )}

        {/* ── pied : la légende, ce que la fenêtre laisse dehors, et le geste ────── */}
        {d && (
          <div style={{ borderTop: "1px solid var(--border)", marginTop: 10, paddingTop: 10,
            display: "flex", gap: 12, alignItems: "center", flexWrap: "wrap", fontSize: 11,
            color: "var(--faint)" }}>
            {[["parti", "partie"], ["prevu", "à envoyer"], ["bloque", "bloquée"],
              ["perime", "périmée ⌫"]].map(([k, l]) => (
              <span key={k} style={{ display: "inline-flex", alignItems: "center", gap: 5 }}>
                <i style={{ width: 9, height: 9, borderRadius: 2, display: "inline-block",
                  background: FG_ETATS[k].fond,
                  border: "1px solid " + FG_ETATS[k].trait }} />{l}
              </span>
            ))}
            <span style={{ display: "inline-flex", alignItems: "center", gap: 5 }}>
              <i style={{ width: 5, height: 5, borderRadius: 9, display: "inline-block",
                background: "var(--accent-2, #22d3ee)" }} />aussi pris par un autre concept
            </span>
            <span style={{ width: "100%", height: 0 }} />
            {/* CE QUE LA FENÊTRE LAISSE DEHORS. Une fenêtre qui tait ce qu'elle coupe est un
                écran qui ment par omission. */}
            {b.avant_fenetre ? <span>{b.avant_fenetre} avant la fenêtre</span> : null}
            {/* LES DEUX NOMBRES DE LA PURGE. « Nettoyer », sur la carte, balaie TOUT le
                planning du concept — comptes sortis de la catégorie compris — alors que
                l'écran ne comptait que les membres : il annonçait 12 et en retirait 30. On ne
                touche pas au geste, on dit les deux. */}
            {b.purge_toutes ? (
              <span title={"« Nettoyer », dans le planning de la carte, retire toute ligne "
                + "encore PRÉVUE dont l'heure est passée — brouillons d'essai à blanc compris, "
                + "et sur tout le concept, y compris les comptes sortis de la catégorie. Ce "
                + "n'est donc pas le même compte que les « périmées » de l'entête, qui ne "
                + "regardent que la fenêtre affichée."}>
                « Nettoyer » retirerait {b.purge_toutes} ligne(s)
                {b.purge_toutes > b.purge_membres
                  ? ", dont " + (b.purge_toutes - b.purge_membres)
                    + " sur des comptes sortis du concept" : ""}
              </span>
            ) : null}
            {b.apres_fenetre ? <span>{b.apres_fenetre} après la fenêtre
              {b.dernier ? " (jusqu'au " + FG_COURT(b.dernier.slice(0, 10)) + ")" : ""}</span> : null}
            <span title={"Les colonnes sont des journées UTC — la même unité que celle qui "
              + "décide des quotas. L'heure d'une publication, elle, s'affiche à ta montre "
              + "dans le planning de la carte."}>jours UTC</span>

            <span style={{ flex: 1 }} />
            {aSec.length > 0 && (
              <button onClick={() => setChoisis(aSec)} style={FG_BTN(false)}>
                {aSec.length === 1 ? "Le compte sans rien"
                  : "Les " + aSec.length + " sans rien"}
              </button>
            )}
            {incomplets.length > aSec.length && (
              <button onClick={() => setChoisis(incomplets)} style={FG_BTN(false)}>
                {incomplets.length === 1 ? "Le compte incomplet"
                  : "Les " + incomplets.length + " incomplets"}
              </button>
            )}
            {choisis.length > 0 && (
              <button onClick={() => setChoisis([])} style={FG_BTN(false)}>Aucun</button>
            )}
            <button onClick={surFermer} style={FG_BTN(false)}>Fermer</button>
            <button disabled={!choisis.length}
              onClick={() => choisis.length && surProgrammer && surProgrammer(choisis)}
              style={{ ...FG_BTN(!!choisis.length), opacity: choisis.length ? 1 : .45,
                cursor: choisis.length ? "pointer" : "default" }}>
              🗓 Programmer {!choisis.length ? "…"
                : choisis.length === 1 ? "ce compte" : "ces " + choisis.length}
            </button>
          </div>
        )}

        {/* ── l'infobulle ───────────────────────────────────────────────────────── */}
        {bulle && (
          <div style={{ position: "fixed", left: Math.min(bulle.x + 14, window.innerWidth - 250),
            top: bulle.y + 14, zIndex: 9100, pointerEvents: "none", width: 232,
            padding: "8px 10px", borderRadius: 9, fontSize: 11.5, lineHeight: 1.5,
            background: "var(--surface-3, #1d2740)", border: "1px solid var(--border-2)",
            boxShadow: "0 10px 30px rgba(0,0,0,.5)" }}>
            <div style={{ fontWeight: 700 }}>{bulle.compte}</div>
            <div style={{ color: "var(--muted)", marginBottom: 4 }}>
              {FG_LONG(bulle.jour, d && d.aujourdhui)}
            </div>
            {!FG_SORTANT(bulle.c) && !(bulle.c && bulle.c[3]) && !(bulle.c && bulle.c[5]) ? (
              <div style={{ color: "var(--faint)" }}>rien de programmé ce jour-là</div>
            ) : (
              [[0, "bloquée"], [1, "partie(s)"], [2, "brouillon(s) chez eux"],
               [3, "périmée(s)"], [4, "à envoyer"], [5, "sur un autre concept"]]
                .filter(([i]) => bulle.c && bulle.c[i])
                .map(([i, l]) => (
                  <div key={i} style={{ display: "flex", gap: 6 }}>
                    <b style={{ fontVariantNumeric: "tabular-nums" }}>{bulle.c[i]}</b>
                    <span style={{ color: "var(--muted)" }}>{l}</span>
                  </div>
                ))
            )}
          </div>
        )}
      </div>
    </div>
  );
}

Object.assign(window, { VueGrille, FgBande, FgResume, FG_DATE, FG_COURT, FG_LONG, FG_ETATS,
                        FG_TON, FG_SORTANT });
