// view-admin-dashboard.jsx — Tableau de bord d'accueil des vues Admin KODAMA & BlackRock.
// Contenu : avancement par jeu / print, prochaines échéances (fins de prod, transports), activité récente.
// Trois dispositions comparables via Tweaks → « Page Admin / Tableau de bord » :
//   A · Vue d'ensemble   — jeux à gauche, échéances + activité à droite
//   B · Liste compacte   — tous les prints en lignes serrées, le reste dessous
//   C · Échéances d'abord — bandeau d'échéances en haut, jeux en grille

function dashDaysFromToday(iso) {
  if (!iso) return null;
  const [y, m, d] = iso.slice(0, 10).split("-").map(Number);
  const now = new Date();
  const a = new Date(y, m - 1, d);
  const b = new Date(now.getFullYear(), now.getMonth(), now.getDate());
  return Math.round((a - b) / 86400000);
}

// Agrégats : prints actifs (avec compteurs d'état), groupés par jeu + liste d'échéances triée
function dashData(db) {
  const prodsByPrint = {};
  db.productions.forEach((p) => { (prodsByPrint[p.printId] = prodsByPrint[p.printId] || []).push(p); });

  const prints = db.prints.map((pr) => {
    const prods = prodsByPrint[pr.id] || [];
    const counts = stateCounts(prods);
    const qty = prods.reduce((s, p) => s + (p.qty || 0), 0);
    const end = prods.reduce((m, p) => (p.prodEnd && (!m || p.prodEnd > m) ? p.prodEnd : m), null);
    return { print: pr, prods, counts, validated: counts.ok + counts.done, total: prods.length, qty, end,
      active: prods.some((p) => p.statut !== "Terminé") };
  }).filter((x) => x.total > 0 && x.active);

  const byGame = {};
  prints.forEach((x) => {
    const g = byGame[x.print.gameId] || (byGame[x.print.gameId] = { id: x.print.gameId, name: x.print.game, prints: [] });
    g.prints.push(x);
  });
  const games = Object.values(byGame).sort((a, b) => a.name.localeCompare(b.name, "fr"));

  // Transport → jeu, via les productions qui le référencent
  const printGame = {};
  db.prints.forEach((pr) => { printGame[pr.id] = { id: pr.gameId, name: pr.game }; });
  const trGame = {};
  db.productions.forEach((p) => (p.transportIds || []).forEach((tid) => { trGame[tid] = printGame[p.printId]; }));

  const deadlines = [];
  prints.forEach((x) => {
    const d = dashDaysFromToday(x.end);
    if (d == null || d < -30) return;
    deadlines.push({ date: x.end, days: d, icon: "🏁", what: "Fin de production", sub: x.print.game + " · " + x.print.label, gameId: x.print.gameId, late: d < 0 });
  });
  (db.transports || []).forEach((tr) => {
    if (/termin|livr/i.test(tr.status || "")) return;
    const g = trGame[tr.id] || {};
    const dep = dashDaysFromToday(tr.departure);
    if (dep != null && dep >= 0) deadlines.push({ date: tr.departure, days: dep, icon: "🚢", what: "Départ transport", sub: tr.name, gameId: g.id, late: false });
    const arr = dashDaysFromToday(tr.arrival);
    if (arr != null && arr >= -7) deadlines.push({ date: tr.arrival, days: arr, icon: "📦", what: "Arrivée estimée", sub: tr.name, gameId: g.id, late: arr < 0 });
  });
  deadlines.sort((a, b) => (a.date < b.date ? -1 : a.date > b.date ? 1 : 0));

  return { games, prints, deadlines, printGames: Object.values(printGame) };
}

// ——— Résumé « à faire / en attente / alertes » pour le haut du tableau de bord ———
function dashSummary(db, prints, deadlines) {
  const partnerName = (id) => { const p = db.partners.find((x) => x.id === id); return p ? (p.flag ? p.flag + " " : "") + p.name.replace(/ - .*$/, "") : "?"; };
  const printOf = (pid) => db.prints.find((pr) => pr.id === pid);
  const todo = [], waiting = [], warn = [];
  const seenConsignee = {};

  db.productions.forEach((p) => {
    const pr = printOf(p.printId);
    if (!pr || p.statut === "Terminé") return;
    const label = pr.game + " · " + pr.label;
    const who = partnerName(p.partnerId);
    const gameId = pr.gameId;

    // Les prods encore en amont (En discussion / Pré-production) ne remontent dans
    // aucune colonne — trop tôt. Dès « Prod Planifiée », elles réapparaissent (eProofs).
    const earlyStage = p.statut === "En discussion" || p.statut === "Pré-production";
    if (earlyStage) return;

    // À FAIRE (action KODAMA)
    if (p.statut === "Modifications des fichiers" && p.comment) {
      todo.push({ icon: "✎", label, who, sub: "Modifications à traiter — mettre à jour les fichiers", gameId, tone: "red" });
      return; // prod en pause : on ne cumule pas d'autres items
    }
    if (!p.eproofsOk) {
      todo.push({ icon: "🧾", label, who, sub: "Valider les eProofs", gameId, tone: "violet" });
    }

    // EN ATTENTE (on attend partenaire / usine)
    if (p.eproofsOk && needsApproval(p)) {
      waiting.push({ icon: "⏳", label, who, sub: "Validation MPC en attente du partenaire", gameId });
    }
    const wantsMpc = !p.validation || p.validation.includes("MPC");
    if (p.eproofsOk && !p.mpcOk && wantsMpc && !p.mpcTracking && p.statut !== "Modifications des fichiers") {
      waiting.push({ icon: "📦", label, who, sub: "N° de tracking MPC à venir (usine)", gameId });
    }

    // ALERTES — adresse Consignee manquante chez un partenaire actif
    if (!seenConsignee[p.partnerId]) {
      const pa = db.partners.find((x) => x.id === p.partnerId);
      if (pa && (!pa.consignee || !pa.consignee.trim())) {
        warn.push({ icon: "📍", label: who, who: "", sub: "Adresse Consignee manquante — à relancer", gameId });
      }
      seenConsignee[p.partnerId] = true;
    }
  });

  // EN ATTENTE — fin de prod à renseigner (tous les eProofs d'un print validés, pas de date)
  prints.forEach((x) => {
    if (x.prods.length > 0 && x.prods.every((p) => p.eproofsOk) && !x.end) {
      waiting.push({ icon: "🏁", label: x.print.game + " · " + x.print.label, who: "", sub: "Date de fin de prod à renseigner (usine)", gameId: x.print.gameId });
    }
  });

  // ALERTES — échéances en retard
  deadlines.filter((d) => d.late).forEach((d) => {
    warn.push({ icon: d.icon, label: d.what, who: "", sub: d.sub + " · " + Math.abs(d.days) + " j de retard", gameId: d.gameId });
  });

  return { todo, waiting, warn };
}

function DashSummaryCol({ title, items, accent, accentBg, setNav, emptyText, gameName }) {
  // Regroupement par jeu (ordre alphabétique) — les items sans jeu vont dans « Autre ».
  const groups = [];
  items.forEach((it) => {
    const key = it.gameId || "__none";
    let g = groups.find((x) => x.key === key);
    if (!g) { g = { key, gameId: it.gameId, name: it.gameId ? gameName(it.gameId) : "Autre", items: [] }; groups.push(g); }
    g.items.push(it);
  });
  groups.sort((a, b) => a.name.localeCompare(b.name, "fr"));
  const itemRow = (it, i, gname) => {
    // On retire le nom du jeu en préfixe du libellé puisqu'il figure dans l'en-tête de groupe.
    let lbl = it.label;
    if (gname && typeof lbl === "string" && lbl.indexOf(gname + " · ") === 0) lbl = lbl.slice(gname.length + 3);
    return (
      <button key={i} onClick={it.gameId ? () => setNav({ page: "game", gameId: it.gameId }) : undefined}
        style={{ display: "flex", gap: 9, alignItems: "flex-start", textAlign: "left", background: "none", border: "none", cursor: it.gameId ? "pointer" : "default", padding: "5px 7px 5px 8px", borderRadius: 8, fontFamily: K.font, width: "100%" }}
        onMouseEnter={(e) => { if (it.gameId) e.currentTarget.style.background = accentBg; }} onMouseLeave={(e) => e.currentTarget.style.background = "transparent"}>
        <span style={{ display: "inline-flex", alignItems: "center", justifyContent: "center", width: 18, height: 18, fontSize: 14, flexShrink: 0 }}>{it.icon}</span>
        <span style={{ minWidth: 0, flex: 1 }}>
          <span style={{ display: "block", fontSize: 12.5, fontWeight: 700, color: K.ink, lineHeight: "18px" }}>{lbl}{it.who ? <span style={{ color: K.sub, fontWeight: 600 }}> — {it.who}</span> : null}</span>
          <span style={{ display: "block", fontSize: 11.5, color: K.sub }}>{it.sub}</span>
        </span>
      </button>
    );
  };
  return (
    <div className="k-card" style={{ padding: "13px 16px", borderTop: `3px solid ${accent}` }}>
      <div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 6 }}>
        <span style={{ fontSize: 12.5, fontWeight: 800, letterSpacing: ".04em", textTransform: "uppercase", color: accent }}>{title}</span>
        <span style={{ marginLeft: "auto", fontSize: 13, fontWeight: 800, color: items.length ? "#FFF" : K.sub, background: items.length ? accent : "transparent", border: items.length ? "none" : `1.5px solid ${K.line}`, borderRadius: 99, minWidth: 24, textAlign: "center", padding: "1px 8px" }}>{items.length}</span>
      </div>
      {items.length === 0
        ? <div style={{ fontSize: 12.5, color: K.sub, padding: "4px 0 6px" }}>{emptyText}</div>
        : <div style={{ display: "flex", flexDirection: "column", gap: 2 }}>
            {groups.map((g) => {
              const gc = g.gameId ? gameColor(g.name) : { mid: K.line, fg: K.sub };
              return (
                <div key={g.key} style={{ marginTop: 6 }}>
                  <button onClick={g.gameId ? () => setNav({ page: "game", gameId: g.gameId }) : undefined}
                    style={{ display: "flex", alignItems: "center", gap: 8, width: "100%", textAlign: "left", background: "none", border: "none", cursor: g.gameId ? "pointer" : "default", padding: "4px 7px", fontFamily: K.font }}>
                    <span style={{ width: 9, height: 9, borderRadius: 99, background: gc.mid, flexShrink: 0 }}></span>
                    <span className="k-round" style={{ fontSize: 16, fontWeight: 700, color: K.ink, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{g.name}</span>
                    <span style={{ flexShrink: 0, display: "inline-flex", alignItems: "center", justifyContent: "center", minWidth: 19, height: 19, padding: "0 5px", borderRadius: 99, background: K.black, color: "#FFF", fontSize: 11, fontWeight: 800 }}>{g.items.length}</span>
                  </button>
                  {g.items.map((it, i) => itemRow(it, i, g.name))}
                </div>
              );
            })}
          </div>}
    </div>
  );
}

function DashSummary({ db, prints, deadlines, setNav }) {
  const { todo, waiting, warn } = dashSummary(db, prints, deadlines);
  if (todo.length === 0 && waiting.length === 0 && warn.length === 0) return null;
  const gameName = (id) => {
    const pr = db.prints.find((p) => p.gameId === id);
    if (pr) return pr.game;
    const g = (db.games || []).find((x) => x.id === id);
    return g ? g.name : "Autre";
  };
  return (
    <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(250px, 1fr))", gap: 12, marginBottom: 22, alignItems: "start" }}>
      <DashSummaryCol title="À faire" items={todo} accent={K.blueS} accentBg={K.blueBg} setNav={setNav} gameName={gameName} emptyText="✓ Rien à traiter de votre côté." />
      <DashSummaryCol title="En attente" items={waiting} accent={K.amber} accentBg={K.amberBg} setNav={setNav} gameName={gameName} emptyText="Rien en attente." />
      <DashSummaryCol title="Alertes" items={warn} accent={K.red} accentBg={K.redBg} setNav={setNav} gameName={gameName} emptyText="✓ Aucune alerte." />
    </div>
  );
}

function DashDelay({ days, late }) {
  const txt = days === 0 ? "aujourd'hui" : late ? Math.abs(days) + " j de retard" : "J−" + days;
  const fg = late ? K.red : days <= 7 ? "#B07916" : K.teal;
  const bg = late ? K.redBg : days <= 7 ? K.amberBg : K.tealBg;
  return <span style={{ fontSize: 11, fontWeight: 800, color: fg, background: bg, borderRadius: 99, padding: "2px 9px", whiteSpace: "nowrap" }}>{txt}</span>;
}

// Une ligne print : libellé, barre d'état segmentée, compteur validés
function DashPrintRow({ x, withGame, onOpen }) {
  return (
    <div onClick={onOpen} style={{ display: "grid", gridTemplateColumns: "minmax(120px, 200px) minmax(0, 1fr) auto", gap: 14, alignItems: "center", padding: "9px 0", cursor: "pointer" }}>
      <div style={{ minWidth: 0 }}>
        <div style={{ fontSize: 13, fontWeight: 700, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{withGame ? x.print.game + " · " : ""}{x.print.label}</div>
        <div style={{ fontSize: 11.5, color: K.sub }}>{x.qty.toLocaleString("fr-FR")} unités · fin {fmtDate(x.end)}</div>
      </div>
      <StateBar counts={x.counts} height={9} />
      <div style={{ fontSize: 12, fontWeight: 700, color: x.validated === x.total ? K.green : K.sub, whiteSpace: "nowrap" }}>{x.validated}/{x.total} validés</div>
    </div>
  );
}

function DashGameCard({ g, setNav }) {
  const gc = gameColor(g.name);
  const open = () => setNav({ page: "game", gameId: g.id });
  return (
    <div className="k-card" style={{ overflow: "hidden" }}>
      <div style={{ height: 4, background: `linear-gradient(90deg, ${gc.mid}, ${gc.soft})` }}></div>
      <div style={{ padding: "12px 18px 10px" }}>
        <button onClick={open} style={{ display: "flex", alignItems: "center", gap: 10, background: "none", border: "none", cursor: "pointer", padding: 0, fontFamily: K.font, width: "100%" }}>
          <span style={{ width: 10, height: 10, borderRadius: 99, background: gc.mid, flexShrink: 0 }}></span>
          <span className="k-round" style={{ fontSize: 19, fontWeight: 700, color: K.ink }}>{g.name}</span>
          <span style={{ marginLeft: "auto", fontSize: 12, fontWeight: 700, color: gc.fg }}>ouvrir →</span>
        </button>
        <div style={{ marginTop: 2 }}>
          {g.prints.map((x) => <DashPrintRow key={x.print.id} x={x} onOpen={open} />)}
        </div>
      </div>
    </div>
  );
}

function DashDeadlineRow({ d, setNav, divider }) {
  const click = d.gameId ? () => setNav({ page: "game", gameId: d.gameId }) : undefined;
  return (
    <div onClick={click} style={{ display: "flex", gap: 12, alignItems: "center", padding: "9px 0", borderTop: divider ? `1px solid ${K.line}` : "none", cursor: click ? "pointer" : "default" }}>
      <span style={{ fontSize: 17 }}>{d.icon}</span>
      <div style={{ minWidth: 0, flex: 1 }}>
        <div style={{ fontSize: 13, fontWeight: 700 }}>{d.what}</div>
        <div style={{ fontSize: 11.5, color: K.sub, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{d.sub}</div>
      </div>
      <div style={{ textAlign: "right" }}>
        <div style={{ fontSize: 12, fontWeight: 700, whiteSpace: "nowrap" }}>{fmtDate(d.date)}</div>
        <div style={{ marginTop: 2 }}><DashDelay days={d.days} late={d.late} /></div>
      </div>
    </div>
  );
}

function DashDeadlines({ deadlines, setNav, limit }) {
  const items = deadlines.slice(0, limit || 7);
  return (
    <div className="k-card" style={{ padding: "8px 18px" }}>
      {items.map((d, i) => <DashDeadlineRow key={i} d={d} setNav={setNav} divider={i > 0} />)}
      {items.length === 0 && <div style={{ padding: "12px 0", color: K.sub, fontSize: 13 }}>Aucune échéance à venir.</div>}
    </div>
  );
}

function DashActivity({ db, setNav, count, printGames }) {
  const findGame = (l) => printGames.find((g) => (l.detail || "").includes(g.name));
  return (
    <div className="k-card" style={{ padding: "8px 18px" }}>
      {db.log.slice(0, count || 6).map((l, i) => {
        const g = findGame(l);
        return (
          <div key={i} onClick={g ? () => setNav({ page: "game", gameId: g.id }) : undefined}
            style={{ display: "flex", gap: 10, padding: "8px 0", borderTop: i === 0 ? "none" : `1px solid ${K.line}`, fontSize: 12.5, alignItems: "baseline", cursor: g ? "pointer" : "default" }}>
            <span className="k-mono" style={{ fontSize: 10.5, color: K.sub, whiteSpace: "nowrap" }}>{(l.ts || "").slice(5, 10)}</span>
            <span style={{ minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
              <strong>{l.actor}</strong> <span style={{ color: K.blueS, fontWeight: 600 }}>{l.action}</span> <span style={{ color: K.sub }}>{l.detail}</span>
            </span>
          </div>
        );
      })}
      {db.log.length === 0 && <div style={{ padding: "12px 0", color: K.sub, fontSize: 13 }}>Aucune activité pour l'instant.</div>}
    </div>
  );
}

function DashTitle({ children, style }) {
  return <div className="k-label" style={{ marginBottom: 8, ...style }}>{children}</div>;
}

// ——— Petit mot d'accueil rigolo au-dessus du tableau de bord ———
// Météo du jour à Vendes (Open-Meteo, live) + citation tournante + fait « éphéméride » (Wikipédia).
// Tout dégrade proprement si une API n'est pas joignable (ex. aperçu hors-ligne).
const VENDES_COORDS = { lat: 45.1333, lon: 2.2667, name: "Vendes" }; // Vendes (Cantal) — ajustez si besoin
const VEYRE_COORDS = { lat: 45.6167, lon: 3.1833, name: "Veyre-Monton" }; // Veyre-Monton (Puy-de-Dôme) — météo de l'admin BlackRock
const DASH_QUOTES = [
  { t: "Rien ne sert de courir ; il faut partir à point.", a: "Jean de La Fontaine" },
  { t: "Ce n'est pas que nous ayons peu de temps, c'est que nous en perdons beaucoup.", a: "Sénèque" },
  { t: "Ceux qui emploient mal leur temps sont les premiers à se plaindre de sa brièveté.", a: "Jean de La Bruyère" },
  { t: "Seul, on va plus vite ; ensemble, on va plus loin.", a: "Proverbe africain" },
  { t: "Je ne puis méditer qu'en marchant ; sitôt que je m'arrête, je ne pense plus.", a: "Jean-Jacques Rousseau" },
  { t: "La Terre fournit assez pour les besoins de chacun, mais pas pour l'avidité de tous.", a: "Gandhi" },
  { t: "La nature ne se presse jamais, et pourtant tout est accompli.", a: "Lao Tseu" },
  { t: "La Terre ne nous appartient pas : nous l'empruntons à nos enfants.", a: "Proverbe amérindien" },
  { t: "Cela se résout en marchant.", a: "Solvitur ambulando" },
  { t: "Il n'y a pas de chemin vers le bonheur : le bonheur est le chemin.", a: "Proverbe" },
  { t: "Un voyage de mille lieues commence toujours par un premier pas.", a: "Lao Tseu" },
  { t: "Adopte le rythme de la nature : son secret, c'est la patience.", a: "Ralph W. Emerson" },
];
function dashWeather(code) {
  const m = {
    0: ["☀️", "grand soleil"], 1: ["🌤️", "ciel dégagé"], 2: ["⛅", "quelques nuages"], 3: ["☁️", "ciel couvert"],
    45: ["🌫️", "brouillard"], 48: ["🌫️", "brouillard givrant"],
    51: ["🌦️", "légère bruine"], 53: ["🌦️", "bruine"], 55: ["🌦️", "forte bruine"],
    61: ["🌧️", "pluie faible"], 63: ["🌧️", "pluie"], 65: ["🌧️", "forte pluie"],
    66: ["🌧️", "pluie verglaçante"], 67: ["🌧️", "pluie verglaçante"],
    71: ["🌨️", "neige faible"], 73: ["🌨️", "neige"], 75: ["🌨️", "forte neige"], 77: ["🌨️", "grésil"],
    80: ["🌦️", "averses"], 81: ["🌦️", "averses"], 82: ["⛈️", "fortes averses"],
    85: ["🌨️", "averses de neige"], 86: ["🌨️", "averses de neige"],
    95: ["⛈️", "orage"], 96: ["⛈️", "orage et grêle"], 99: ["⛈️", "orage et grêle"],
  };
  return m[code] || ["🌡️", ""];
}
function DashGreetingChip({ icon, children }) {
  return (
    <div style={{ flex: "1 1 270px", minWidth: 0, display: "flex", gap: 9, alignItems: "flex-start",
      background: "rgba(255,255,255,0.72)", border: "1px solid rgba(45,156,219,0.22)", borderRadius: 12, padding: "10px 13px" }}>
      <span style={{ fontSize: 16, lineHeight: "19px", flexShrink: 0 }}>{icon}</span>
      <span style={{ minWidth: 0, fontSize: 12.7, lineHeight: 1.45, color: K.ink }}>{children}</span>
    </div>
  );
}
// Étiquette de rubrique facon bandeau télé (Météo / Citation / Éphéméride).
function TickerCat({ color, children }) {
  return (
    <span style={{ display: "inline-flex", alignItems: "center", height: 18, padding: "0 8px", borderRadius: 4,
      background: color, color: "#fff", fontFamily: K.font, fontSize: 10, fontWeight: 800,
      letterSpacing: ".1em", textTransform: "uppercase", whiteSpace: "nowrap" }}>{children}</span>
  );
}
// ——— « Performances production » : points marquants calculés en direct depuis nos données.
// Remplace l'ancien fil « éphéméride » (dates peu fiables) par des faits réels et vérifiables.
function dashPerfFacts(db) {
  const facts = [];
  if (!db || !db.productions || !db.prints) return facts;
  const printById = {};
  db.prints.forEach((pr) => { printById[pr.id] = pr; });
  const gameOf = (pid) => { const pr = printById[pid]; return pr ? pr.game : null; };
  const fmt = (n) => n.toLocaleString("fr-FR").replace(/\u202f/g, " ");
  const active = db.productions.filter((p) => p.statut !== "Terminé");

  // Plus grosse production en cours (par jeu)
  const volActive = {};
  active.forEach((p) => { const g = gameOf(p.printId); if (g) volActive[g] = (volActive[g] || 0) + (p.qty || 0); });
  const topActive = Object.entries(volActive).sort((a, b) => b[1] - a[1])[0];
  if (topActive) facts.push(`🏆 « ${topActive[0]} » est notre plus grosse production en cours — ${fmt(topActive[1])} exemplaires.`);

  // Volume total en production, tous jeux confondus
  const totalActive = active.reduce((s, p) => s + (p.qty || 0), 0);
  if (totalActive > 0) facts.push(`📦 ${fmt(totalActive)} exemplaires sont actuellement en production sur l'ensemble de nos jeux.`);

  // Tirage le plus collectif (le print qui réunit le plus de partenaires)
  const partnersPerPrint = {};
  active.forEach((p) => { (partnersPerPrint[p.printId] = partnersPerPrint[p.printId] || new Set()).add(p.partnerId); });
  let topPrint = null;
  Object.entries(partnersPerPrint).forEach(([pid, set]) => { if (!topPrint || set.size > topPrint.n) topPrint = { pid, n: set.size }; });
  if (topPrint && topPrint.n > 1) { const pr = printById[topPrint.pid]; if (pr) facts.push(`🤝 ${pr.game} · ${pr.label} réunit ${topPrint.n} partenaires — notre tirage le plus collectif.`); }

  // Best-seller toutes éditions confondues
  const volAll = {};
  db.productions.forEach((p) => { const g = gameOf(p.printId); if (g) volAll[g] = (volAll[g] || 0) + (p.qty || 0); });
  const topAll = Object.entries(volAll).sort((a, b) => b[1] - a[1])[0];
  if (topAll) facts.push(`⭐ Toutes éditions confondues, « ${topAll[0]} » totalise ${fmt(topAll[1])} exemplaires produits.`);

  // Un print dont tous les partenaires ont déjà validé leur MPC
  const printAllOk = db.prints.find((pr) => { const ps = active.filter((p) => p.printId === pr.id); return ps.length > 1 && ps.every((p) => p.mpcOk); });
  if (printAllOk) facts.push(`✅ Tous les partenaires de ${printAllOk.game} · ${printAllOk.label} ont validé leur MPC.`);

  return facts;
}
// Actualité « jeu de société » (tendances BGG) — repli si l'API BGG est injoignable (CORS / hors-ligne).
const BGG_FALLBACK = [
  "Brass: Birmingham et Ark Nova se disputent toujours la tête du classement BoardGameGeek.",
  "Les campagnes au long cours (Frosthaven, Sleeping Gods) confirment l'engouement pour le jeu narratif.",
  "Wingspan reste l'une des portes d'entrée préférées vers le jeu de gestion moderne.",
  "Les jeux « à l'allemande » dominent encore le top des sorties les plus attendues sur BGG.",
  "Le legacy et le « roll-and-write » continuent de séduire un large public sur BoardGameGeek.",
  "Spirit Island et Gloomhaven figurent parmi les coopératifs les mieux notés de la communauté BGG.",
];
function DashGreeting({ db, coords }) {
  const C = coords || VENDES_COORDS;
  const now = new Date();
  const [weather, setWeather] = React.useState(null);
  const [bgg, setBgg] = React.useState(null);
  const perf = React.useMemo(() => dashPerfFacts(db), [db]);
  const [perfIdx, setPerfIdx] = React.useState(0);
  React.useEffect(() => {
    if (perf.length <= 1) return;
    const id = setInterval(() => setPerfIdx((i) => (i + 1) % perf.length), 8000);
    return () => clearInterval(id);
  }, [perf.length]);
  const perfFact = perf.length ? perf[perfIdx % perf.length] : null;
  const fmtClock = (d) => d.toLocaleTimeString("fr-FR", { hour: "2-digit", minute: "2-digit" });
  const [clock, setClock] = React.useState(() => fmtClock(new Date()));
  const doy = Math.floor((now - new Date(now.getFullYear(), 0, 0)) / 86400000);
  const quote = DASH_QUOTES[doy % DASH_QUOTES.length];
  const hello = now.getHours() < 18 ? "Bonjour" : "Bonsoir";
  const dateStr = now.toLocaleDateString("fr-FR", { day: "numeric", month: "long" });
  React.useEffect(() => {
    let alive = true;
    fetch(`https://api.open-meteo.com/v1/forecast?latitude=${C.lat}&longitude=${C.lon}&current=temperature_2m,weather_code&daily=temperature_2m_max,temperature_2m_min&forecast_days=1&timezone=Europe%2FParis`)
      .then((r) => r.json()).then((d) => { if (!alive || !d.current) return; const [emoji, text] = dashWeather(d.current.weather_code);
        const dl = d.daily || {};
        setWeather({ temp: Math.round(d.current.temperature_2m), emoji, text,
          min: dl.temperature_2m_min ? Math.round(dl.temperature_2m_min[0]) : null,
          max: dl.temperature_2m_max ? Math.round(dl.temperature_2m_max[0]) : null }); }).catch(() => {});
    // Tendance « jeu de société » : top BGG (XML) → repli liste curée si CORS/hors-ligne.
    fetch("https://boardgamegeek.com/xmlapi2/hot?type=boardgame")
      .then((r) => r.text()).then((xml) => { if (!alive) return;
        const doc = new DOMParser().parseFromString(xml, "text/xml");
        const names = [...doc.querySelectorAll("item > name")].map((n) => n.getAttribute("value")).filter(Boolean);
        if (names.length) { const name = names[doy % Math.min(names.length, 10)];
          setBgg(`« ${name} » grimpe parmi les jeux les plus consultés du moment sur BoardGameGeek 🎲`); }
        else setBgg(BGG_FALLBACK[doy % BGG_FALLBACK.length]);
      }).catch(() => { if (alive) setBgg(BGG_FALLBACK[doy % BGG_FALLBACK.length]); });
    return () => { alive = false; };
  }, [C.lat, C.lon]);
  // Horloge live facon bandeau info.
  React.useEffect(() => {
    const id = setInterval(() => setClock(fmtClock(new Date())), 15000);
    return () => clearInterval(id);
  }, []);
  // CSS du bandeau (defilement, pastille, fondus) injecte une seule fois.
  React.useEffect(() => {
    if (document.getElementById("dash-ticker-css")) return;
    const s = document.createElement("style");
    s.id = "dash-ticker-css";
    s.textContent = `
      @keyframes dashTickerScroll { from { transform: translateX(0); } to { transform: translateX(-50%); } }
      @keyframes dashDotPulse { 0% { box-shadow: 0 0 0 0 rgba(255,255,255,.55); } 70% { box-shadow: 0 0 0 7px rgba(255,255,255,0); } 100% { box-shadow: 0 0 0 0 rgba(255,255,255,0); } }
      .dash-ticker-track { animation: dashTickerScroll 46s linear infinite; will-change: transform; }
      .dash-ticker-mask:hover .dash-ticker-track { animation-play-state: paused; }
      .dash-ticker-dot { width: 9px; height: 9px; border-radius: 99px; background: #fff; animation: dashDotPulse 1.7s ease-out infinite; flex-shrink: 0; }
      .dash-ticker-mask::before, .dash-ticker-mask::after { content: ""; position: absolute; top: 0; bottom: 0; width: 42px; z-index: 2; pointer-events: none; }
      .dash-ticker-mask::before { left: 0; background: linear-gradient(90deg,#191613,rgba(25,22,19,0)); }
      .dash-ticker-mask::after { right: 0; background: linear-gradient(270deg,#191613,rgba(25,22,19,0)); }
      @media (prefers-reduced-motion: reduce) { .dash-ticker-track { animation-duration: 150s; } }
      @media (max-width: 560px) { .dash-ticker-clock, .dash-ticker-label { display: none !important; } }
    `;
    document.head.appendChild(s);
  }, []);

  const Sep = () => <span aria-hidden="true" style={{ color: K.orange, margin: "0 26px", fontSize: 11 }}>◆</span>;
  const itemStyle = { display: "inline-flex", alignItems: "center", gap: 9, whiteSpace: "nowrap" };
  const hi = { fontWeight: 800, color: "#fff" };
  // Une passe complete du bandeau, dupliquee pour un defilement sans couture.
  const Run = (k) => (
    <span key={k} style={{ display: "inline-flex", alignItems: "center", whiteSpace: "nowrap" }}>
      <span style={itemStyle}>
        <TickerCat color={K.blue}>Météo</TickerCat>
        <span><strong style={hi}>{hello} !</strong>{" "}
          {weather
            ? <React.Fragment>À {C.name} ce {now.getHours() < 18 ? "matin" : "soir"}, il fait <strong style={hi}>{weather.temp}°</strong> — {weather.text} {weather.emoji}{(weather.min != null && weather.max != null) ? <span style={{ color: "rgba(255,255,255,0.6)", fontWeight: 600 }}>{" "}(min {weather.min}° / max {weather.max}°)</span> : null}</React.Fragment>
            : <React.Fragment>Belle journée à {C.name} ☀️</React.Fragment>}
        </span>
      </span>
      <Sep />
      <span style={itemStyle}>
        <TickerCat color={K.purple}>Citation</TickerCat>
        <span><span style={{ fontStyle: "italic" }}>« {quote.t} »</span>{" "}<span style={{ color: "rgba(255,255,255,0.6)", fontWeight: 600 }}>— {quote.a}</span></span>
      </span>
      <Sep />
      <span style={itemStyle}>
        <TickerCat color={K.green}>Jeu de société</TickerCat>
        <span>{bgg
          ? <React.Fragment>{bgg}</React.Fragment>
          : <React.Fragment>Des nouvelles fraîches de la table de jeu arrivent… 🎲</React.Fragment>}</span>
      </span>
      <Sep />
      <span style={itemStyle}>
        <TickerCat color={K.orange}>Performance</TickerCat>
        <span>{perfFact
          ? <React.Fragment>{perfFact}</React.Fragment>
          : <React.Fragment>Nos productions tournent à plein régime 🎲</React.Fragment>}</span>
      </span>
      <Sep />
    </span>
  );

  return (
    <div style={{ display: "flex", alignItems: "stretch", background: "#191613", borderRadius: 14,
      overflow: "hidden", marginBottom: 18, minHeight: 46, boxShadow: "0 1px 3px rgba(25,22,19,0.22)" }}>
      <div style={{ display: "flex", alignItems: "center", gap: 9, padding: "0 15px", background: K.grad, flexShrink: 0 }}>
        <span className="dash-ticker-dot"></span>
        <span className="dash-ticker-label" style={{ color: "#fff", fontFamily: K.font, fontSize: 11.5, fontWeight: 800, letterSpacing: ".12em", textTransform: "uppercase", whiteSpace: "nowrap" }}>En direct</span>
      </div>
      <div className="dash-ticker-mask" style={{ position: "relative", flex: 1, minWidth: 0, overflow: "hidden", display: "flex", alignItems: "center", color: "#EDEBE6", fontSize: 13 }}>
        <div className="dash-ticker-track" style={{ display: "inline-flex", alignItems: "center" }}>
          {Run("a")}
          {Run("b")}
        </div>
      </div>
      <div className="dash-ticker-clock" style={{ display: "flex", alignItems: "center", padding: "0 16px", borderLeft: "1px solid rgba(255,255,255,0.1)", color: "#EDEBE6", fontFamily: K.mono, fontSize: 13, fontWeight: 600, letterSpacing: ".04em", whiteSpace: "nowrap", flexShrink: 0 }}>{clock}</div>
    </div>
  );
}

// ——— Notifications : dernières actions des partenaires et de l'usine, à « accuser réception » ———
// On filtre le journal pour ne garder que ce qui vient de l'extérieur (pas les actions KODAMA),
// et on mémorise localement (localStorage) ce qui a déjà été lu. Rien n'est écrit dans Airtable :
// l'état « lu / non lu » est propre à ce navigateur.
const NOTIF_READ_KEY = "kodama-notif-read-v1";
function notifLoadRead() { try { return new Set(JSON.parse(localStorage.getItem(NOTIF_READ_KEY) || "[]")); } catch (e) { return new Set(); } }
function notifSaveRead(set) { try { localStorage.setItem(NOTIF_READ_KEY, JSON.stringify([...set])); } catch (e) {} }
function notifKey(l) { return [l.ts, l.actor, l.action, l.detail].join("|"); }

// Traduction + pastille visuelle selon le type d'action enregistrée dans le journal (libellés en anglais).
const NOTIF_KINDS = [
  { re: /MPC approved/i,                 fr: "a validé le MPC",                     icon: "✅", tone: "green" },
  { re: /eProofs validated/i,            fr: "a validé les eProofs",                icon: "🧾", tone: "green" },
  { re: /Changes requested/i,            fr: "a demandé des modifications",         icon: "✎",  tone: "red" },
  { re: /MPC tracking added/i,           fr: "a ajouté un tracking MPC",            icon: "📦", tone: "blue" },
  { re: /MPC tracking removed/i,         fr: "a retiré un tracking MPC",            icon: "📦", tone: "gray" },
  { re: /MPC video link added/i,         fr: "a ajouté la vidéo MPC",               icon: "🎬", tone: "blue" },
  { re: /MPC video link removed/i,       fr: "a retiré la vidéo MPC",               icon: "🎬", tone: "gray" },
  { re: /Packing list uploaded/i,        fr: "a déposé une packing list",           icon: "📄", tone: "teal" },
  { re: /Production end date set/i,      fr: "a renseigné la fin de production",    icon: "🏁", tone: "amber" },
  { re: /Production end date cleared/i,  fr: "a retiré la date de fin de prod",     icon: "🏁", tone: "gray" },
  { re: /Carton marks link updated/i,    fr: "a mis à jour les carton marks",       icon: "🔖", tone: "teal" },
  { re: /Notes updated/i,                fr: "a mis à jour des notes",              icon: "📝", tone: "gray" },
  { re: /Logistics (data updated|record created)/i, fr: "a mis à jour la logistique", icon: "🚚", tone: "teal" },
  { re: /Validation method chosen/i,     fr: "a choisi sa méthode de validation",   icon: "⚙️", tone: "gray" },
  { re: /Pallet preference updated/i,    fr: "a précisé sa préférence palettes",    icon: "📦", tone: "gray" },
  { re: /incoterm chosen/i,              fr: "a choisi son incoterm",               icon: "📋", tone: "gray" },
  { re: /contact email updated/i,        fr: "a mis à jour son e-mail de contact",  icon: "✉️", tone: "gray" },
  { re: /Shipping info confirmed/i,      fr: "a confirmé ses infos d'expédition",   icon: "📍", tone: "green" },
];
function notifKind(action) {
  const k = NOTIF_KINDS.find((x) => x.re.test(action || ""));
  return k || { fr: action || "", icon: "•", tone: "gray" };
}
const NOTIF_TONES = {
  green: K.green, red: K.red, blue: K.blueS, teal: K.teal, amber: K.amber, gray: K.sub,
};

// Priorité d'affichage : les décisions du partenaire (validation MPC, demande de modifs)
// remontent en tête des notifications et sont mises en avant — pour ne jamais les manquer.
function notifPriority(action) {
  if (/MPC approved|Changes requested/i.test(action || "")) return 3;
  if (/eProofs validated|Shipping info confirmed/i.test(action || "")) return 1;
  return 0;
}

// ——— Validations MPC reconstituées depuis les fiches production ———
// Le journal (db.log) ne contient une ligne « MPC approved » que si la validation est passée
// par le portail partenaire. Or une validation synchronisée depuis Airtable met seulement
// à jour la fiche (mpcOk / mpcBy / mpcOn) — sans ligne de journal, donc sans notification.
// On reconstitue donc une notification par fiche validée, fusionnée et dédoublonnée avec le journal,
// pour qu'aucune validation partenaire ne passe inaperçue dans le tableau de bord.
function synthApprovalNotifs(db) {
  const printById = {}; (db.prints || []).forEach((pr) => { printById[pr.id] = pr; });
  const partnerById = {}; (db.partners || []).forEach((p) => { partnerById[p.id] = p; });
  const out = [];
  (db.productions || []).forEach((p) => {
    if (!p.mpcOk || !p.mpcBy || !p.mpcOn) return;
    if (p.statut === "Terminé") return; // on n'exhume pas les validations des prods clôturées
    const pr = printById[p.printId]; if (!pr) return;
    const pa = partnerById[p.partnerId];
    const company = pa ? ((pa.company || pa.name || "").replace(/ - .*$/, "").split(/[\s,]+/)[0]) : "";
    const label = `${pr.game} · ${pr.label}`;
    out.push({
      ts: (p.mpcOn.length <= 10 ? p.mpcOn + " 12:00" : p.mpcOn),
      actor: company ? `${p.mpcBy} (${company})` : p.mpcBy,
      action: "MPC approved",
      detail: `${label} — ${valLabel(p.mpcVia || p.validation)}`,
      _gameLabel: label, _approver: p.mpcBy,
    });
  });
  return out;
}
// Fusionne le journal et les validations reconstituées, en évitant les doublons
// (une validation déjà présente dans le journal n'est pas ré-ajoutée).
function mergedNotifLog(db) {
  const log = db.log || [];
  const synth = synthApprovalNotifs(db).filter((s) =>
    !log.some((l) => /MPC approved/i.test(l.action) &&
      (l.detail || "").includes(s._gameLabel) &&
      (l.actor || "").includes(s._approver))
  );
  return [...log, ...synth].sort((a, b) => (a.ts < b.ts ? 1 : a.ts > b.ts ? -1 : 0));
}

function DashNotifRow({ entry, onToggle, onOpen }) {
  const { l, k, isRead } = entry;
  const kind = notifKind(l.action);
  const fg = NOTIF_TONES[kind.tone] || K.sub;
  const pinned = notifPriority(l.action) >= 3 && !isRead;
  const isFactory = /^whatz games/i.test(l.actor || "");
  const srcLabel = isFactory ? "Usine" : "Partenaire";
  const srcFg = isFactory ? K.teal : K.blueS;
  const srcBg = isFactory ? K.tealBg : K.blueBg;
  return (
    <div style={{ display: "flex", gap: 11, alignItems: "flex-start", padding: pinned ? "11px 10px 11px 7px" : "10px 6px 10px 4px",
      borderTop: `1px solid ${K.line}`, opacity: isRead ? 0.58 : 1,
      background: pinned ? (kind.tone === "red" ? K.redBg : K.greenBg) : "transparent",
      boxShadow: pinned ? `inset 3px 0 0 ${fg}` : "none", borderRadius: pinned ? 8 : 0 }}>
      {/* pastille lu / non lu */}
      <span aria-hidden="true" style={{ width: 9, height: 9, borderRadius: 99, marginTop: 6, flexShrink: 0,
        background: isRead ? "transparent" : fg, border: isRead ? `1.5px solid ${K.line}` : "none" }}></span>
      <span style={{ fontSize: 16, lineHeight: "20px", flexShrink: 0 }}>{kind.icon}</span>
      <button onClick={onOpen} style={{ minWidth: 0, flex: 1, textAlign: "left", background: "none", border: "none",
        padding: 0, cursor: onOpen ? "pointer" : "default", fontFamily: K.font }}>
        <div style={{ fontSize: 12.5, lineHeight: 1.45, color: K.ink, fontWeight: isRead ? 500 : 600 }}>
          <span style={{ display: "inline-flex", alignItems: "center", height: 15, padding: "0 6px", borderRadius: 4,
            background: srcBg, color: srcFg, fontSize: 9.5, fontWeight: 800, letterSpacing: ".06em",
            textTransform: "uppercase", marginRight: 7, verticalAlign: "1px" }}>{srcLabel}</span>
          <strong style={{ fontWeight: 800 }}>{l.actor}</strong>{" "}
          <span style={{ color: fg, fontWeight: 700 }}>{kind.fr}</span>
          {pinned ? <span style={{ display: "inline-flex", alignItems: "center", height: 15, padding: "0 6px", borderRadius: 4, background: fg, color: "#FFF", fontSize: 9.5, fontWeight: 800, letterSpacing: ".06em", textTransform: "uppercase", marginLeft: 7, verticalAlign: "1px" }}>📌 à ne pas manquer</span> : null}
        </div>
        {l.detail ? <div style={{ fontSize: 11.5, color: K.sub, marginTop: 1, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{l.detail}</div> : null}
      </button>
      <span className="k-mono" style={{ fontSize: 10.5, color: K.sub, whiteSpace: "nowrap", marginTop: 2, flexShrink: 0 }}>{(l.ts || "").slice(5, 16)}</span>
      <button onClick={onToggle} title={isRead ? "Marquer comme non lu" : "Marquer comme lu"}
        style={{ flexShrink: 0, width: 24, height: 24, borderRadius: 99, cursor: "pointer", fontFamily: K.font,
          display: "inline-flex", alignItems: "center", justifyContent: "center", fontSize: 13, lineHeight: 1,
          border: `1.5px solid ${isRead ? K.line : K.green}`, background: isRead ? "transparent" : K.greenBg,
          color: isRead ? K.sub : K.green, fontWeight: 800 }}>
        {isRead ? "↺" : "✓"}
      </button>
    </div>
  );
}

function DashNotifications({ db, setNav, printGames }) {
  const [read, setRead] = React.useState(notifLoadRead);
  const [showRead, setShowRead] = React.useState(false);

  // Actions extérieures uniquement (on exclut les actions internes KODAMA).
  // On part du journal fusionné avec les validations MPC reconstituées des fiches production.
  const ext = mergedNotifLog(db).filter((l) => l.actor && !/^kodama/i.test(l.actor));
  const entries = ext.map((l, i) => { const k = notifKey(l); return { l, k, uid: k + "#" + i, isRead: read.has(k) }; });
  const unread = entries.filter((e) => !e.isRead).sort((a, b) => notifPriority(b.l.action) - notifPriority(a.l.action));
  const readEntries = entries.filter((e) => e.isRead);

  const setReadKeys = (keys, makeRead) => {
    setRead((prev) => { const n = new Set(prev); keys.forEach((k) => makeRead ? n.add(k) : n.delete(k)); notifSaveRead(n); return n; });
  };
  const openGame = (l) => {
    const g = (printGames || []).find((g) => (l.detail || "").includes(g.name));
    if (g) setNav({ page: "game", gameId: g.id });
  };

  const NOTIF_LIMIT = 10;
  const source = showRead ? entries : (unread.length ? unread : entries.slice(0, 4));
  const visible = source.slice(0, NOTIF_LIMIT);
  const hiddenCount = source.length - visible.length;

  // Rien à signaler → bandeau réduit à une simple ligne (dépliable si historique).
  if (unread.length === 0 && !showRead) {
    return (
      <div className="k-card" style={{ padding: "8px 16px", borderTop: `3px solid ${K.line}`, marginBottom: 22,
        display: "flex", alignItems: "center", gap: 9, flexWrap: "wrap" }}>
        <span style={{ fontSize: 14 }}>🔔</span>
        <span style={{ fontSize: 12, fontWeight: 800, letterSpacing: ".04em", textTransform: "uppercase", color: K.sub }}>Notifications</span>
        <span style={{ fontSize: 12.5, color: K.green, fontWeight: 700 }}>✓ Tout est à jour</span>
        {readEntries.length > 0 && (
          <button onClick={() => setShowRead(true)} className="k-btn k-btn-ghost k-btn-sm" style={{ marginLeft: "auto", fontSize: 12 }}>
            Voir l'historique ({readEntries.length})
          </button>
        )}
      </div>
    );
  }

  return (
    <div className="k-card" style={{ padding: "13px 18px 8px", borderTop: `3px solid ${K.purple}`, marginBottom: 22 }}>
      <div style={{ display: "flex", alignItems: "center", gap: 9, marginBottom: 2, flexWrap: "wrap" }}>
        <span style={{ fontSize: 15 }}>🔔</span>
        <span style={{ fontSize: 12.5, fontWeight: 800, letterSpacing: ".04em", textTransform: "uppercase", color: K.purple }}>Notifications</span>
        <span style={{ fontSize: 13, fontWeight: 800, color: unread.length ? "#FFF" : K.sub,
          background: unread.length ? K.purple : "transparent", border: unread.length ? "none" : `1.5px solid ${K.line}`,
          borderRadius: 99, minWidth: 24, textAlign: "center", padding: "1px 8px" }}>{unread.length}</span>
        <span style={{ fontSize: 12, color: K.sub, fontWeight: 600, whiteSpace: "nowrap" }}>{unread.length ? "non lue" + (unread.length > 1 ? "s" : "") : "tout est à jour"}</span>
        <div style={{ marginLeft: "auto", display: "flex", gap: 8 }}>
          {readEntries.length > 0 && (
            <button onClick={() => setShowRead((s) => !s)} className="k-btn k-btn-ghost k-btn-sm" style={{ fontSize: 12 }}>
              {showRead ? "Masquer les lues" : `Voir tout (${entries.length})`}
            </button>
          )}
          {unread.length > 0 && (
            <button onClick={() => setReadKeys(unread.map((e) => e.k), true)} className="k-btn k-btn-sm"
              style={{ background: K.purple, borderColor: K.purple, color: "#FFF", fontSize: 12, fontWeight: 700, whiteSpace: "nowrap" }}>
              Tout marquer comme lu
            </button>
          )}
        </div>
      </div>
      {entries.length === 0 ? (
        <div style={{ fontSize: 12.5, color: K.sub, padding: "10px 2px 12px" }}>Aucune action des partenaires ou de l'usine pour l'instant.</div>
      ) : (
        <div>
          {visible.map((e) => (
            <DashNotifRow key={e.uid} entry={e} onOpen={() => openGame(e.l)}
              onToggle={() => setReadKeys([e.k], !e.isRead)} />
          ))}
          {hiddenCount > 0 && (
            <div style={{ padding: "9px 2px 4px", fontSize: 11.5, color: K.sub }}>
              + {hiddenCount} autre{hiddenCount > 1 ? "s" : ""}{showRead ? "" : " non lue" + (hiddenCount > 1 ? "s" : "")} —{" "}
              <button onClick={() => setReadKeys(unread.map((e) => e.k), true)} style={{ background: "none", border: "none", padding: 0, cursor: "pointer", color: K.blueS, fontWeight: 700, fontFamily: K.font, fontSize: 11.5 }}>tout marquer comme lu</button>
            </div>
          )}
          {hiddenCount === 0 && !showRead && unread.length > 0 && readEntries.length > 0 && (
            <div style={{ padding: "9px 2px 4px", fontSize: 11.5, color: K.sub }}>
              {readEntries.length} notification{readEntries.length > 1 ? "s" : ""} déjà lue{readEntries.length > 1 ? "s" : ""} —{" "}
              <button onClick={() => setShowRead(true)} style={{ background: "none", border: "none", padding: 0, cursor: "pointer", color: K.blueS, fontWeight: 700, fontFamily: K.font, fontSize: 11.5 }}>les afficher</button>
            </div>
          )}
        </div>
      )}
    </div>
  );
}

function AdminDashboard({ db, setNav, restricted, variant, only }) {
  const { games, prints, deadlines, printGames } = dashData(db);
  const v = ((window.__dashVariant || variant || "A") + "")[0]; // __dashVariant : override de test
  // only="top" → en-tête + résumé des tâches seuls ; only="body" → reste du tableau de bord
  const showTop = only !== "body";
  const today = new Date();
  const dateStr = today.toLocaleDateString("fr-FR", { weekday: "long", day: "numeric", month: "long" });

  const header = (
    <div style={{ display: "flex", alignItems: "baseline", gap: 14, flexWrap: "wrap", marginBottom: 16 }}>
      <div className="k-round" style={{ fontSize: 22, fontWeight: 700, whiteSpace: "nowrap" }}>Tableau de bord</div>
      <div style={{ fontSize: 13, color: K.sub, textTransform: "capitalize", whiteSpace: "nowrap" }}>{dateStr}</div>
      <div style={{ marginLeft: "auto", fontSize: 12.5, color: K.sub, whiteSpace: "nowrap" }}>{prints.length} print{prints.length > 1 ? "s" : ""} en cours · {games.length} jeu{games.length > 1 ? "x" : ""}</div>
    </div>
  );

  const empty = games.length === 0 && (
    <div className="k-card" style={{ padding: 22, color: K.sub, textAlign: "center" }}>Aucune production en cours.</div>
  );

  // Encart résumé « à faire / en attente / alertes » + notifications — seulement pour KODAMA (pas BlackRock)
  const summary = !restricted ? (
    <React.Fragment>
      <DashSummary db={db} prints={prints} deadlines={deadlines} setNav={setNav} />
      <DashNotifications db={db} setNav={setNav} printGames={printGames} />
    </React.Fragment>
  ) : null;

  // Bloc haut isolé (en-tête + tâches), affichable en pleine largeur au-dessus de la grille
  if (only === "top") return <div data-comment-anchor="admin-dashboard"><DashGreeting db={db} coords={restricted ? VEYRE_COORDS : VENDES_COORDS} />{header}{summary}</div>;

  // ——— C · Échéances d'abord ———
  if (v === "C") {
    return (
      <div data-comment-anchor="admin-dashboard">
        {showTop && header}
        {showTop && summary}
        <DashTitle>Prochaines échéances</DashTitle>
        <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(230px, 1fr))", gap: 12 }}>
          {deadlines.slice(0, 4).map((d, i) => (
            <div key={i} className="k-card" onClick={d.gameId ? () => setNav({ page: "game", gameId: d.gameId }) : undefined}
              style={{ padding: "12px 16px", cursor: d.gameId ? "pointer" : "default" }}>
              <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", gap: 8 }}>
                <span style={{ fontSize: 18 }}>{d.icon}</span>
                <DashDelay days={d.days} late={d.late} />
              </div>
              <div style={{ fontSize: 13, fontWeight: 700, marginTop: 8 }}>{d.what} — {fmtDate(d.date)}</div>
              <div style={{ fontSize: 11.5, color: K.sub, marginTop: 2, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{d.sub}</div>
            </div>
          ))}
          {deadlines.length === 0 && <div className="k-card" style={{ padding: 16, color: K.sub, fontSize: 13 }}>Aucune échéance à venir.</div>}
        </div>
        <DashTitle style={{ marginTop: 26 }}>Avancement par jeu</DashTitle>
        {empty}
        <div className="k-cardgrid" style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(380px, 1fr))", gap: 14 }}>
          {games.map((g) => <DashGameCard key={g.id} g={g} setNav={setNav} />)}
        </div>
        <DashTitle style={{ marginTop: 26 }}>Activité récente</DashTitle>
        <DashActivity db={db} setNav={setNav} count={6} printGames={printGames} />
      </div>
    );
  }

  // ——— B · Liste compacte ———
  if (v === "B") {
    return (
      <div data-comment-anchor="admin-dashboard">
        {showTop && header}
        {showTop && summary}
        <DashTitle>Avancement des prints</DashTitle>
        {empty}
        {prints.length > 0 && (
          <div className="k-card" style={{ padding: "6px 18px" }}>
            {prints.map((x, i) => (
              <div key={x.print.id} style={{ borderTop: i === 0 ? "none" : `1px solid ${K.line}` }}>
                <DashPrintRow x={x} withGame={true} onOpen={() => setNav({ page: "game", gameId: x.print.gameId })} />
              </div>
            ))}
          </div>
        )}
        <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(320px, 1fr))", gap: 16, marginTop: 26 }}>
          <div>
            <DashTitle>Prochaines échéances</DashTitle>
            <DashDeadlines deadlines={deadlines} setNav={setNav} limit={6} />
          </div>
          <div>
            <DashTitle>Activité récente</DashTitle>
            <DashActivity db={db} setNav={setNav} count={6} printGames={printGames} />
          </div>
        </div>
      </div>
    );
  }

  // ——— A · Vue d'ensemble (défaut) ———
  return (
    <div data-comment-anchor="admin-dashboard">
      {showTop && header}
      {showTop && summary}
      <div style={{ display: "flex", flexWrap: "wrap", gap: 18, alignItems: "flex-start" }}>
        <div style={{ flex: "1 1 420px", minWidth: 0, display: "flex", flexDirection: "column", gap: 14 }}>
          <DashTitle style={{ marginBottom: -4 }}>Avancement par jeu</DashTitle>
          {empty}
          {games.map((g) => <DashGameCard key={g.id} g={g} setNav={setNav} />)}
        </div>
        <div style={{ flex: "0 1 340px", minWidth: 300 }}>
          <DashTitle>Prochaines échéances</DashTitle>
          <DashDeadlines deadlines={deadlines} setNav={setNav} limit={6} />
          <DashTitle style={{ marginTop: 22 }}>Activité récente</DashTitle>
          <DashActivity db={db} setNav={setNav} count={6} printGames={printGames} />
        </div>
      </div>
    </div>
  );
}

Object.assign(window, { AdminDashboard, dashData });
