/* global React, Icon, sectorById, sectorLabel, NewSectorModal */
const { useState: useStateI } = React;

// ==================== INDICATORS ====================
function Indicators({ t, lang, setRoute, isSuperAdmin, actingOrgId, myOrgId, canEditIndicators }) {
  // Indicators already declares its own hasPerm + isAdmin locally below
  // (lines 28-29). Don't re-declare them as props — that would create a
  // duplicate-identifier SyntaxError and white-page the whole screen.
  // canEditIndicators (computed at the App level) is passed through as
  // a hint; the local hasPerm is still used as the source of truth.
  const [filter, setFilter] = useStateI("all");
  const [sectorFilter, setSectorFilter] = useStateI("all");
  const [levelFilter, setLevelFilter] = useStateI("all"); // all | logframe | output | outcome | mixed | impact | context

  // Friendly label for level slugs. Keeps the pill readable when the value
  // is "mixed" (which would otherwise render the raw slug).
  const levelLabelI = (slug) => {
    const s = (slug || "").toLowerCase();
    if (s === "output")  return lang === "fr" ? "Output"  : "Output";
    if (s === "outcome") return lang === "fr" ? "Outcome" : "Outcome";
    if (s === "mixed")   return lang === "fr" ? "Mixte (Output/Outcome)" : "Mixed (Output/Outcome)";
    if (s === "impact")  return "Impact";
    if (s === "context") return "Context";
    return slug || "";
  };
  const { data: liveIndicators, error: liveErr, refresh: refreshIndicators, realtime } = window.melr.useIndicators();
  const LiveBadge = window.melr.LiveBadge;
  const [createOpen, setCreateOpen] = useStateI(false);
  const [valueFor, setValueFor]     = useStateI(null); // indicator object when "Valeur" was clicked
  const [editFor, setEditFor]       = useStateI(null); // indicator being edited
  const [multiSiteOpen, setMultiSiteOpen] = useStateI(false);
  // Admin gate for edit/delete + multi-site entry
  const { has: hasPerm } = window.melr.useCurrentUserPermissions();
  const isAdmin = hasPerm && hasPerm("users.manage");
  const [busyId, setBusyId] = useStateI(null);
  const [genFormBusyId, setGenFormBusyId] = useStateI(null);

  // Pass 3: create a data collection form pre-filled with this indicator
  // (and its disaggregation grid). On success, show a toast and offer to
  // navigate to the FormBuilder for fine-tuning.
  const onGenerateForm = async (i) => {
    if (!i || !i.uuid) return;
    setGenFormBusyId(i.uuid);
    try {
      const form = await window.melr.createFormForIndicator(
        {
          uuid: i.uuid,
          code: i.code || i.id,
          name_fr: i.name_fr || i.name,
          name_en: i.name_en,
          project_id: i.project_id,
        },
        lang
      );
      window.alert(lang === "fr"
        ? "Formulaire cree avec succes :\n\n  " + (form.name_fr || form.code) +
          "\n\nVous pouvez maintenant l'editer ou l'activer dans le module Formulaires."
        : "Form created successfully:\n\n  " + (form.name_fr || form.code) +
          "\n\nYou can now edit or activate it in the Forms module.");
    } catch (e) {
      window.alert((lang === "fr" ? "Erreur : " : "Error: ") + (e.message || e));
    } finally {
      setGenFormBusyId(null);
    }
  };
  // Super-admin acting-as-org: when set, narrow the list to indicators
  // belonging to projects in that org (via projects.organization_id embed).
  const scopeByActingOrg = (rows) => {
    if (!isSuperAdmin || !actingOrgId || actingOrgId === myOrgId) return rows;
    return (rows || []).filter((i) => i.projects && i.projects.organization_id === actingOrgId);
  };

  const onDeleteIndicator = async (i) => {
    if (!window.confirm(lang === "fr"
      ? "Supprimer l'indicateur « " + i.id + " · " + i.name + " » ?\nToutes les valeurs saisies seront aussi supprimées (cascade)."
      : "Delete indicator '" + i.id + " · " + i.name + "'?\nAll captured values will also be removed (cascade).")) return;
    setBusyId(i.uuid);
    try {
      await window.melr.removeIndicator(i.uuid);
      await refreshIndicators();
    } catch (e) {
      window.alert((lang === "fr" ? "Erreur : " : "Error: ") + e.message);
    } finally { setBusyId(null); }
  };
  if (liveErr) console.error("[MELR] useIndicators error:", liveErr);
  if (liveIndicators && liveIndicators.length) {
    console.log("[MELR] Indicators raw from database:");
    liveIndicators.forEach((i) => console.log(
      "  code=" + i.code +
      " · sector_id=" + i.sector_id +
      " · project_id=" + i.project_id +
      " · projects.code=" + (i.projects && i.projects.code)
    ));
  }
  const FIXTURE_INDICATORS = [
    { id: "SANT.1", sector: "sante",       name: lang === "fr" ? "Femmes enceintes ayant reçu ≥4 CPN" : "Pregnant women with ≥4 ANC visits", level: "Outcome", baseline: 0.42, target: 0.78, actual: 0.71, unit: "%", trend: [42,48,53,58,63,67,69,71], project: "P-241", site: "42 sites", status: "ok" },
    { id: "SANT.2", sector: "sante",       name: lang === "fr" ? "Accouchements assistés par personnel qualifié" : "Births attended by skilled personnel", level: "Outcome", baseline: 0.51, target: 0.85, actual: 0.79, unit: "%", trend: [51,56,61,66,71,75,77,79], project: "P-241", site: "42 sites", status: "ok" },
    { id: "VIH.1",  sector: "vih",         name: lang === "fr" ? "Personnes sous TARV (cumul jeunes urbains)" : "People on ART (urban youth cumulative)", level: "Outcome", baseline: 1840, target: 6200, actual: 4310, unit: "p.", trend: [1840,2200,2640,3050,3490,3820,4080,4310], project: "P-220", site: "24 sites", status: "ok" },
    { id: "VIH.2",  sector: "vih",         name: lang === "fr" ? "Charge virale supprimée à 12 mois" : "Viral suppression at 12 months", level: "Outcome", baseline: 0.68, target: 0.90, actual: 0.74, unit: "%", trend: [68,70,71,72,73,74,74,74], project: "P-220", site: "24 sites", status: "warn" },
    { id: "AGR.1",  sector: "agriculture", name: lang === "fr" ? "Rendement moyen riz paddy (t/ha)" : "Average paddy rice yield (t/ha)", level: "Outcome", baseline: 4.1, target: 6.5, actual: 5.4, unit: "t/ha", trend: [4.1,4.3,4.6,4.8,5.0,5.2,5.3,5.4], project: "P-302", site: "18 sites", status: "ok" },
    { id: "AGR.2",  sector: "agriculture", name: lang === "fr" ? "Exploitations irriguées (cumul)" : "Irrigated farms (cumulative)", level: "Output", baseline: 142, target: 600, actual: 318, unit: "expl.", trend: [142,178,210,238,267,288,304,318], project: "P-302", site: "18 sites", status: "warn" },
    { id: "PECH.1", sector: "peche",       name: lang === "fr" ? "Captures débarquées valorisées (t/an)" : "Landed catches with added value (t/yr)", level: "Outcome", baseline: 0, target: 1200, actual: 280, unit: "t", trend: [0,40,80,140,180,220,250,280], project: "P-415", site: lang === "fr" ? "8 ports" : "8 ports", status: "warn" },
    { id: "PECH.2", sector: "peche",       name: lang === "fr" ? "Pirogues équipées GPS+frigo" : "Boats equipped with GPS+cold storage", level: "Output", baseline: 0, target: 240, actual: 38, unit: "u.", trend: [0,4,8,14,20,26,32,38], project: "P-415", site: "8 ports", status: "risk" },
    { id: "EDU.1",  sector: "education",   name: lang === "fr" ? "Taux d'achèvement secondaire 1er cycle" : "Lower secondary completion rate", level: "Outcome", baseline: 0.48, target: 0.75, actual: 0.62, unit: "%", trend: [48,51,54,56,58,60,61,62], project: "P-156", site: "34 sites", status: "ok" },
    { id: "EDU.2",  sector: "education",   name: lang === "fr" ? "Collèges équipés tableaux numériques" : "Schools with digital boards", level: "Output", baseline: 0, target: 34, actual: 22, unit: "", trend: [0,3,6,10,14,17,20,22], project: "P-156", site: "34 sites", status: "ok" },
    { id: "FIN.1",  sector: "finances",    name: lang === "fr" ? "Recouvrement de l'impôt sur le revenu (% PIB)" : "Income tax collection (% GDP)", level: "Impact", baseline: 4.2, target: 7.0, actual: 5.6, unit: "%", trend: [4.2,4.4,4.7,5.0,5.2,5.4,5.5,5.6], project: "P-098", site: lang === "fr" ? "national" : "national", status: "ok" },
    { id: "FIN.2",  sector: "finances",    name: lang === "fr" ? "Contribuables enregistrés numériquement" : "Digitally registered taxpayers", level: "Output", baseline: 12000, target: 80000, actual: 38400, unit: "", trend: [12000,16400,20800,25400,30100,33800,36500,38400], project: "P-098", site: "8 régions", status: "warn" },
    { id: "GOU.1",  sector: "gouvernance", name: lang === "fr" ? "Plans communaux participatifs adoptés" : "Adopted participatory municipal plans", level: "Output", baseline: 0, target: 78, actual: 41, unit: "", trend: [0,8,16,22,28,32,37,41], project: "P-462", site: "78 communes", status: "ok" },
    { id: "GOU.2",  sector: "gouvernance", name: lang === "fr" ? "Indice de redevabilité sociale" : "Social accountability index", level: "Outcome", baseline: 3.2, target: 6.0, actual: 4.4, unit: "/10", trend: [3.2,3.5,3.7,3.9,4.1,4.2,4.3,4.4], project: "P-462", site: lang === "fr" ? "régional" : "regional", status: "warn" },
    { id: "EAU.1",  sector: "eau",         name: lang === "fr" ? "Centres santé avec eau potable continue" : "Health centers with continuous safe water", level: "Output", baseline: 4, target: 19, actual: 6, unit: "", trend: [4,4,5,5,5,5,6,6], project: "P-377", site: "19 sites", status: "risk" },
    { id: "NUT.1",  sector: "nutrition",   name: lang === "fr" ? "Élèves dépistés malnutrition aiguë modérée" : "Pupils screened for moderate acute malnutrition", level: "Output", baseline: 0, target: 64000, actual: 42800, unit: "", trend: [0,5500,11200,17400,24100,30200,36500,42800], project: "P-188", site: "26 sites", status: "ok" },
    { id: "NUT.2",  sector: "nutrition",   name: lang === "fr" ? "Prévalence retard de croissance < 5 ans" : "Stunting prevalence < 5 yrs", level: "Impact", baseline: 0.34, target: 0.22, actual: 0.28, unit: "%", trend: [34,33,32,31,30,29.5,28.5,28], project: "P-188", site: "26 sites", status: "ok", invert: true },
    { id: "GEN.1",  sector: "genre",       name: lang === "fr" ? "Femmes accédant à un crédit productif" : "Women accessing productive credit", level: "Outcome", baseline: 240, target: 2400, actual: 1180, unit: "", trend: [240,420,580,720,860,980,1080,1180], project: "P-507", site: "32 sites", status: "ok" },
    { id: "ENE.1",  sector: "energie",     name: lang === "fr" ? "Foyers raccordés au mini-réseau solaire" : "Households connected to solar mini-grid", level: "Output", baseline: 0, target: 14000, actual: 4820, unit: "", trend: [0,420,1100,1900,2700,3400,4200,4820], project: "P-491", site: "48 sites", status: "warn" },
    { id: "AGR.3",  sector: "agriculture", name: lang === "fr" ? "Animaux vaccinés (campagne transfrontalière)" : "Animals vaccinated (cross-border campaign)", level: "Output", baseline: 0, target: 380000, actual: 364000, unit: "", trend: [0,42000,98000,164000,228000,287000,332000,364000], project: "P-538", site: "22 zones", status: "ok" },
    { id: "PORT.1", sector: "port",        name: lang === "fr" ? "Temps moyen d'escale navire (heures)" : "Average vessel turnaround time (hours)", level: "Outcome", baseline: 72, target: 36, actual: 48, unit: "h", trend: [72,68,64,60,56,52,50,48], project: "P-612", site: lang === "fr" ? "6 postes à quai" : "6 berths", status: "warn", invert: true },
    { id: "PORT.2", sector: "port",        name: lang === "fr" ? "Volume conteneurs traités (EVP/an)" : "Containers handled (TEU/yr)", level: "Outcome", baseline: 480000, target: 820000, actual: 612000, unit: "EVP", trend: [480,498,520,545,568,584,598,612], project: "P-612", site: "Dakar", status: "ok" },
    { id: "PORT.3", sector: "port",        name: lang === "fr" ? "Recettes douanières portuaires (M€)" : "Port customs revenue (M€)", level: "Impact", baseline: 142, target: 240, actual: 178, unit: "M€", trend: [142,148,154,160,165,170,174,178], project: "P-612", site: "Dakar", status: "ok" },
  ];

  // Live indicators from the database when available, else the fixture.
  // When indicator_values exist, build a real trend from them; otherwise
  // fall back to a flat baseline series (Sparkline2 needs >=2 points).
  // The fixture fallback uses the RAW liveIndicators length so we only
  // show demo data when the user truly has none. Once they have any
  // indicator (even from another org as super-admin), we switch to live
  // mode and rely on scopeByActingOrg to narrow the displayed set.
  const indicators = (liveIndicators && liveIndicators.length > 0)
    ? scopeByActingOrg(liveIndicators).map((i) => {
        const b = Number(i.baseline) || 0;
        const t = Number(i.target)   || 0;
        const values = (i.indicator_values || [])
          .slice()
          .sort((a, b) => (a.period_start || "").localeCompare(b.period_start || ""));
        // Build trend: prepend baseline, then real values. Pad if too short.
        let trend;
        let actual;
        let actualText;     // for PEFA: the latest letter as captured
        let liveStatus;
        if (values.length > 0) {
          trend = [b, ...values.map((v) => Number(v.value) || 0)];
          actual = trend[trend.length - 1];
          actualText = values[values.length - 1].value_text || null;
          liveStatus = values[values.length - 1].status || "pending";
        } else {
          trend = [b, b, b, b, b, b, b, b];
          actual = b;
          actualText = null;
          liveStatus = "pending";
        }
        return {
          uuid: i.id,                        // expose UUID for createIndicatorValue
          id: i.code,
          sector: i.sector_id || "sante",
          name: lang === "en" ? (i.name_en || i.name_fr) : i.name_fr,
          level: i.level ? i.level.charAt(0).toUpperCase() + i.level.slice(1) : "Outcome",
          baseline: b,
          baselineText: i.baseline_text || null,
          target: t,
          targetText: i.target_text || null,
          actual,
          actualText,
          unit: i.unit || "",
          value_kind: i.value_kind || "numeric",
          trend,
          project: i.projects && i.projects.code ? i.projects.code : "—",
          site: "—",
          status: liveStatus,
          valuesCount: values.length,
        };
      })
    : FIXTURE_INDICATORS;

  // Status pill filter (Sur cible / Attention / Critique)
  const filteredByStatus = filter === "all" ? indicators : indicators.filter((i) => i.status === filter);
  // Sector chips filter
  const filteredBySector = sectorFilter === "all" ? filteredByStatus : filteredByStatus.filter((i) => i.sector === sectorFilter);
  // Top-tab filter by level. "logframe" is Output + Outcome + Impact + Mixed
  // (the classic logframe extended with cross-cutting Output/Outcome rows),
  // excluding contextual indicators.
  const matchLevel = (i) => {
    const lvl = (i.level || "").toLowerCase();
    if (levelFilter === "all")     return true;
    if (levelFilter === "logframe") return lvl === "output" || lvl === "outcome" || lvl === "impact" || lvl === "mixed";
    // "Mixed" rows surface under BOTH Output and Outcome tabs so they're not
    // hidden from teams who navigate by single level. They also have their
    // own dedicated tab. The Impact / Context tabs stay strict.
    if (levelFilter === "output")  return lvl === "output"  || lvl === "mixed";
    if (levelFilter === "outcome") return lvl === "outcome" || lvl === "mixed";
    return lvl === levelFilter;
  };
  const filtered = filteredBySector.filter(matchLevel);

  // Tab counts use ALL indicators (not filtered ones) so the badge stays
  // informative even when other filters are active. We mirror the matchLevel
  // logic for Output / Outcome so the badge reflects what the user will see
  // when they click the tab (i.e. includes mixed rows).
  const countLevel = (l) => {
    if (l === "all") return indicators.length;
    if (l === "logframe") return indicators.filter((i) => {
      const lv = (i.level || "").toLowerCase();
      return lv === "output" || lv === "outcome" || lv === "impact" || lv === "mixed";
    }).length;
    if (l === "output")  return indicators.filter((i) => { const lv = (i.level || "").toLowerCase(); return lv === "output"  || lv === "mixed"; }).length;
    if (l === "outcome") return indicators.filter((i) => { const lv = (i.level || "").toLowerCase(); return lv === "outcome" || lv === "mixed"; }).length;
    return indicators.filter((i) => (i.level || "").toLowerCase() === l).length;
  };

  return (
    <div className="page">
      <div className="page-header">
        <div className="page-eyebrow">{lang === "fr" ? "MODULE / SUIVI" : "MODULE / TRACKING"}</div>
        <div className="page-header-row">
          <div>
            <h1 className="page-title">{t("nav.indicators")} <LiveBadge on={realtime} lang={lang} /></h1>
            <div className="page-sub">
              {lang === "fr"
                ? `${indicators.length}+ indicateurs · ${[...new Set(indicators.map((i) => i.sector))].length} secteurs · cadre logique consolidé · saisie multi-sites avec validation à 4 yeux`
                : `${indicators.length}+ indicators · ${[...new Set(indicators.map((i) => i.sector))].length} sectors · consolidated logframe · multi-site entry with four-eyes validation`}
            </div>
          </div>
          <div className="page-header-actions">
            <button className="btn sm"
              onClick={() => setRoute && setRoute("indicator_defs")}
              title={lang === "fr"
                ? "L'import se fait depuis « Définition des indicateurs »"
                : "Import is in 'Indicator definitions'"}>
              <Icon.upload /> {t("c.import")} Excel
            </button>
            <button className="btn sm" onClick={() => {
              const date = new Date().toISOString().slice(0, 10);
              window.melr.exportCSV(`indicators-${date}.csv`, filtered, [
                { key: "id",       label: "Code" },
                { key: "name",     label: lang === "fr" ? "Indicateur" : "Indicator" },
                { key: "level",    label: lang === "fr" ? "Niveau" : "Level" },
                { key: "sector",   label: lang === "fr" ? "Secteur" : "Sector" },
                { key: "project",  label: lang === "fr" ? "Projet" : "Project" },
                { key: "unit",     label: lang === "fr" ? "Unité" : "Unit" },
                { key: "baseline", label: "Baseline" },
                { key: "actual",   label: lang === "fr" ? "Valeur actuelle" : "Current value" },
                { key: "target",   label: lang === "fr" ? "Cible" : "Target" },
                { key: "status",   label: lang === "fr" ? "Statut" : "Status" },
              ]);
            }}><Icon.download /> {t("c.export")}</button>
            {/* "Nouvel indicateur" — gated on indicators.edit / users.manage /
                super-admin. Reviewer and data-entry roles don't see it.
                The RLS policy in performance-rls-permissive-consolidation-
                phase5-indicators.sql also blocks the write server-side
                once applied, but we hide the button so the UI doesn't
                offer an action that can't succeed. */}
            {(isSuperAdmin || isAdmin || canEditIndicators
              || (hasPerm && hasPerm("indicators.edit"))) && (
              <button className="btn sm primary" onClick={() => setCreateOpen(true)}><Icon.plus /> {lang === "fr" ? "Nouvel indicateur" : "New indicator"}</button>
            )}
          </div>
        </div>
        <div className="page-tabs">
          {[
            { k: "all",      l: lang === "fr" ? "Tous"          : "All" },
            { k: "logframe", l: lang === "fr" ? "Cadre logique" : "Logframe" },
            { k: "output",   l: "Outputs" },
            { k: "outcome",  l: "Outcomes" },
            { k: "mixed",    l: lang === "fr" ? "Mixte" : "Mixed" },
            { k: "impact",   l: "Impact" },
            { k: "context",  l: "Context" },
          ].map((tab) => (
            <div key={tab.k} className={"tab" + (levelFilter === tab.k ? " active" : "")}
              onClick={() => setLevelFilter(tab.k)}
              style={{ cursor: "pointer" }}>
              {tab.l} <span className="tag-mono" style={{ marginLeft: 4 }}>{countLevel(tab.k)}</span>
            </div>
          ))}
        </div>
      </div>

      <div className="page-body">
        <div className="row" style={{ marginBottom: 14, gap: 6 }}>
          <button className={"btn sm " + (filter === "all" ? "primary" : "")} onClick={() => setFilter("all")}>
            {lang === "fr" ? "Tous" : "All"} <span className="tag-mono">{indicators.length}</span>
          </button>
          <button className={"btn sm " + (filter === "ok" ? "" : "")} onClick={() => setFilter("ok")}>
            <span className="pill green dot" style={{ padding: 0, border: 0, background: "transparent" }}></span>
            {lang === "fr" ? "Sur cible" : "On target"} <span className="tag-mono">{indicators.filter(i => i.status === "ok").length}</span>
          </button>
          <button className={"btn sm "} onClick={() => setFilter("warn")}>
            {lang === "fr" ? "Attention" : "At risk"} <span className="tag-mono">{indicators.filter(i => i.status === "warn").length}</span>
          </button>
          <button className={"btn sm "} onClick={() => setFilter("risk")}>
            {lang === "fr" ? "Critique" : "Critical"} <span className="tag-mono">{indicators.filter(i => i.status === "risk").length}</span>
          </button>
          <div style={{ flex: 1 }}></div>
          <button className="btn sm"><Icon.filter /> {lang === "fr" ? "Filtres" : "Filters"} <span className="tag-mono">3</span></button>
          <button className="btn sm ghost"><Icon.layout /></button>
        </div>

        <div className="sector-filter-row" style={{ marginBottom: 14 }}>
          <button className={"sector-chip-btn" + (sectorFilter === "all" ? " active" : "")} onClick={() => setSectorFilter("all")}>
            <span className="sector-dot" style={{ background: "var(--text)" }}></span>
            {lang === "fr" ? "Tous secteurs" : "All sectors"}
            <span className="seg-count">{indicators.length}</span>
          </button>
          {[...new Set(indicators.map((i) => i.sector))].map((sid) => {
            const s = sectorById(sid);
            const c = indicators.filter((i) => i.sector === sid).length;
            return (
              <button key={sid} className={"sector-chip-btn" + (sectorFilter === sid ? " active" : "")}
                style={sectorFilter === sid ? { background: s.bg, borderColor: s.color, color: s.color } : {}}
                onClick={() => setSectorFilter(sid)}>
                <span className="sector-dot" style={{ background: s.color }}></span>
                {sectorLabel(s, lang)}
                <span className="seg-count">{c}</span>
              </button>
            );
          })}
        </div>

        <div className="card">
          <div className="indicator-row" style={{ background: "var(--bg-sunken)", color: "var(--text-faint)", fontSize: 10.5, textTransform: "uppercase", letterSpacing: "0.05em", fontWeight: 500, cursor: "default" }}>
            <span></span>
            <span>{lang === "fr" ? "Indicateur" : "Indicator"}</span>
            <span style={{ textAlign: "right" }}>{t("c.baseline")}</span>
            <span style={{ textAlign: "right" }}>{t("c.actual")}</span>
            <span style={{ textAlign: "right" }}>{t("c.target")}</span>
            <span>{lang === "fr" ? "Tendance" : "Trend"}</span>
            <span>{t("c.progress")}</span>
            <span>{t("c.status")}</span>
            <span></span>
          </div>
          {filtered.map((i) => {
            const isPefa = i.value_kind === "score_pefa";
            const fmt = (v, alphaFallback) => {
              if (isPefa) {
                // Prefer the captured letter; fall back to deriving from the
                // numeric so a freshly-created indicator without entries still
                // shows a sensible label.
                if (alphaFallback) return alphaFallback;
                return (window.melr.scoreFromNumeric && window.melr.scoreFromNumeric("score_pefa", v)) || "—";
              }
              return i.unit === "%" ? (v * 100).toFixed(0) + "%" : v.toLocaleString("fr-FR") + (i.unit === "%" ? "" : " " + i.unit);
            };
            const denom = i.invert ? (i.baseline - i.target) : (i.target - i.baseline);
            const num = i.invert ? (i.baseline - i.actual) : (i.actual - i.baseline);
            const pct = Math.max(0, Math.min(1, num / (denom || 1)));
            return (
              <div key={i.id + "@" + i.project} className="indicator-row">
                <span className="ind-id">{i.id}</span>
                <div>
                  <div className="ind-name">{i.name}</div>
                  <div className="ind-sub">
                    <span className="tag-mono">{i.project}</span> · {i.site}
                    {(() => { const s = sectorById(i.sector); return <span className="sector-chip" style={{ background: s.bg, color: s.color, borderColor: s.color, marginLeft: 6, fontSize: 9.5 }}>{sectorLabel(s, lang)}</span>; })()}
                    <span className="pill" style={{ padding: "0 5px", marginLeft: 4, fontSize: 10 }}
                      title={(i.level || "").toLowerCase() === "mixed"
                        ? (lang === "fr" ? "Niveau couvrant Output et Outcome" : "Spans Output and Outcome")
                        : undefined}>
                      {levelLabelI(i.level)}
                    </span>
                  </div>
                </div>
                <span className="num text-faint">{fmt(i.baseline, i.baselineText)}</span>
                <span className="num strong" style={{ color: "var(--text)" }}>{fmt(i.actual, i.actualText)}</span>
                <span className="num text-muted">{fmt(i.target, i.targetText)}</span>
                <Sparkline2 values={i.trend} status={i.status} />
                <div className="row" style={{ gap: 6 }}>
                  <div className={"progress-bar " + (i.status === "ok" ? "green" : i.status === "warn" ? "amber" : "red")} style={{ width: 70 }}>
                    <div className="fill" style={{ width: (pct * 100) + "%" }}></div>
                  </div>
                  <span className="num" style={{ fontSize: 11, width: 30 }}>{Math.round(pct * 100)}%</span>
                </div>
                <span>
                  {i.status === "ok" && <span className="pill green dot">{lang === "fr" ? "Sur cible" : "On"}</span>}
                  {i.status === "warn" && <span className="pill amber dot">{lang === "fr" ? "Attention" : "Risk"}</span>}
                  {i.status === "risk" && <span className="pill red dot">{lang === "fr" ? "Critique" : "Crit"}</span>}
                  {i.status === "pending" && <span className="pill" style={{ background: "#e0e7ff", color: "#3730a3" }}>{lang === "fr" ? "En attente" : "Pending"}</span>}
                </span>
                {i.uuid ? (
                  <div className="row gap-xs" style={{ justifyContent: "flex-end" }}>
                    <button className="btn sm" style={{ padding: "2px 6px", fontSize: 11 }} onClick={() => setValueFor(i)} title={lang === "fr" ? "Saisir une valeur" : "Add a value"}>
                      <Icon.plus /> {lang === "fr" ? "Valeur" : "Value"}
                    </button>
                    {isAdmin && (
                      <button className="btn sm ghost" style={{ padding: "2px 6px", fontSize: 11 }}
                        onClick={() => onGenerateForm(i)}
                        disabled={genFormBusyId === i.uuid}
                        title={lang === "fr"
                          ? "Generer un formulaire de collecte pre-rempli pour cet indicateur"
                          : "Generate a pre-filled data collection form for this indicator"}>
                        {genFormBusyId === i.uuid
                          ? "..."
                          : (lang === "fr" ? "Formulaire" : "Form")}
                      </button>
                    )}
                    {isAdmin && (
                      <>
                        <button className="btn sm ghost" style={{ padding: "2px 6px", fontSize: 11 }} onClick={() => setEditFor(i)}
                          title={lang === "fr" ? "Modifier l'indicateur" : "Edit indicator"}>
                          <Icon.edit />
                        </button>
                        <button className="btn sm ghost" style={{ padding: "2px 6px", fontSize: 11 }} onClick={() => onDeleteIndicator(i)} disabled={busyId === i.uuid}
                          title={lang === "fr" ? "Supprimer l'indicateur" : "Delete indicator"}>
                          <Icon.x />
                        </button>
                      </>
                    )}
                  </div>
                ) : (
                  <button className="btn sm ghost" style={{ padding: 4 }}><Icon.more /></button>
                )}
              </div>
            );
          })}
        </div>

        <div style={{ height: 16 }}></div>

        <div className="grid cols-2">
          <div className="card">
            <div className="card-head">
              <div className="card-title">{lang === "fr" ? "Saisie multi-sites" : "Multi-site data entry"}</div>
              <span className="pill accent">
                {lang === "fr"
                  ? "Saisie groupée pour un indicateur sur plusieurs sites"
                  : "Batch entry for one indicator across several sites"}
              </span>
              <button className="btn sm primary" style={{ marginLeft: "auto" }}
                onClick={() => setMultiSiteOpen(true)}
                disabled={indicators.filter((i) => i.uuid).length === 0}
                title={indicators.filter((i) => i.uuid).length === 0
                  ? (lang === "fr" ? "Créez d'abord un indicateur." : "Create an indicator first.")
                  : ""}>
                <Icon.edit /> {lang === "fr" ? "Saisir" : "Enter"}
              </button>
            </div>
            <div className="card-body flush">
              <table className="tbl">
                <thead>
                  <tr>
                    <th>{t("c.site")}</th>
                    <th>OUT.1.1</th>
                    <th>OUT.1.2</th>
                    <th>OUT.5.2</th>
                    <th>{t("c.status")}</th>
                  </tr>
                </thead>
                <tbody>
                  {[
                    { s: "Tahoua CSI-A", v1: "82%", v2: "71%", v3: "94%", st: "ok" },
                    { s: "Maradi CSI-B", v1: "76%", v2: "68%", v3: "88%", st: "ok" },
                    { s: "Tillabéri H-1", v1: "—", v2: "—", v3: "—", st: "pending" },
                    { s: "Niamey CHR", v1: "91%", v2: "85%", v3: "97%", st: "ok" },
                    { s: "Ouahigouya CMA", v1: "64%", v2: "52%", v3: "73%", st: "warn" },
                    { s: "Bobo-Dlsso H-2", v1: "—", v2: "—", v3: "82%", st: "partial" },
                  ].map((r, i) => (
                    <tr key={i}>
                      <td className="strong">{r.s}</td>
                      <td className="num">{r.v1}</td>
                      <td className="num">{r.v2}</td>
                      <td className="num">{r.v3}</td>
                      <td>
                        {r.st === "ok" && <span className="pill green dot">{lang === "fr" ? "Validé" : "Validated"}</span>}
                        {r.st === "warn" && <span className="pill amber dot">{lang === "fr" ? "À revoir" : "Review"}</span>}
                        {r.st === "pending" && <span className="pill">{lang === "fr" ? "Attendu" : "Pending"}</span>}
                        {r.st === "partial" && <span className="pill amber">{lang === "fr" ? "Partiel" : "Partial"}</span>}
                      </td>
                    </tr>
                  ))}
                </tbody>
              </table>
            </div>
          </div>
          <div className="card">
            <div className="card-head">
              <div className="card-title">{lang === "fr" ? "Détail · OUT.3.1 — Couverture DTC3" : "Detail · OUT.3.1 — DTP3 coverage"}</div>
              <span className="pill red dot">Critique</span>
            </div>
            <div className="card-body">
              <div className="row" style={{ gap: 24, marginBottom: 12 }}>
                <div>
                  <div className="text-faint" style={{ fontSize: 10.5, textTransform: "uppercase", letterSpacing: "0.05em" }}>{t("c.baseline")}</div>
                  <div className="mono" style={{ fontSize: 18, fontWeight: 500 }}>61%</div>
                </div>
                <div>
                  <div className="text-faint" style={{ fontSize: 10.5, textTransform: "uppercase", letterSpacing: "0.05em" }}>{t("c.actual")}</div>
                  <div className="mono" style={{ fontSize: 22, fontWeight: 600, color: "var(--red)" }}>43%</div>
                </div>
                <div>
                  <div className="text-faint" style={{ fontSize: 10.5, textTransform: "uppercase", letterSpacing: "0.05em" }}>{t("c.target")}</div>
                  <div className="mono" style={{ fontSize: 18, fontWeight: 500 }}>92%</div>
                </div>
                <div style={{ flex: 1 }}></div>
                <div>
                  <div className="text-faint" style={{ fontSize: 10.5, textTransform: "uppercase", letterSpacing: "0.05em" }}>{lang === "fr" ? "Tendance" : "Trend"}</div>
                  <div className="kpi-delta down"><Icon.arrowDown sw="2" />−18 pts</div>
                </div>
              </div>
              <BigChart />
              <div className="text-faint" style={{ fontSize: 11, marginTop: 8 }}>
                {lang === "fr"
                  ? "Sources : DHIS2 (auto-pull) · validation manuelle 27 avril 2026 par K. Diabaté · 3 outliers à instruire."
                  : "Sources: DHIS2 (auto-pull) · manual validation Apr 27 2026 by K. Diabaté · 3 outliers to instruct."}
              </div>
            </div>
          </div>
        </div>
      </div>
      {createOpen && (
        <CreateIndicatorModal
          lang={lang}
          onClose={() => setCreateOpen(false)}
          onCreated={refreshIndicators}
        />
      )}
      {valueFor && (
        <EnterValueModal
          lang={lang}
          indicator={valueFor}
          onClose={() => setValueFor(null)}
          onCreated={refreshIndicators}
        />
      )}
      {editFor && (
        <EditIndicatorModal
          lang={lang}
          indicator={editFor}
          onClose={() => setEditFor(null)}
          onSaved={refreshIndicators}
        />
      )}
      {multiSiteOpen && (
        <MultiSiteEntryModal
          lang={lang}
          indicators={indicators.filter((i) => i.uuid)}
          onClose={() => setMultiSiteOpen(false)}
          onSaved={refreshIndicators}
        />
      )}
    </div>
  );
}

// ==================== ENTER VALUE MODAL ====================
// For numeric indicators: free-text number input.
// For PEFA-scored indicators: letter dropdown (A · B+ · B · …). The
// numeric companion is computed on save via window.melr.scoreToNumeric.
// ── Pivot grid for disaggregated value entry (Phase D) ────────────────────
// Generates the cartesian product of all configured axes' values, then
// shows one row per combination with numerator + denominator inputs.
//
// For example, if the indicator is configured with sex (M/F/NB/NP) × age
// (<15/15-24/25-49/50+), the grid has 4×4 = 16 rows.
//
// Each row's "computed" cell shows the ratio (numerator/denominator) or
// the raw count if denominator is empty. The totals row at the bottom
// sums all numerators + denominators across the grid.
//
// We keep the data model flat (one row per combination), no nested pivot
// 2D headers, because it scales gracefully to 3+ axes (where 2D would
// become unwieldy). A future refinement could add a "view as pivot" toggle
// for the 2-axis case, but the flat list works for any axis count today.
// ── PEFA components grid (E1) ─────────────────────────────────────────────
// Specialised grid used in EnterValueModal when the indicator is PEFA AND
// its definition declares 1+ structured components (PI-27.1, PI-27.2, …).
// One row per component, alphabetic A/B+/B/.../D/NR dropdown per row.
// Stores in the same `cells` map as the generic grid, keyed by
// JSON.stringify({pefa_component: "<code>"}) — the parent EnterValueModal
// later persists each non-empty row into indicator_value_disaggregations
// using that axis_values object verbatim.
function PefaComponentsGrid({ lang, components, cells, setCells, pefaLabels }) {
  // Stable cell key for a given component
  const keyFor = (code) => JSON.stringify({ pefa_component: code });

  const onChangeGrade = (compCode, val) => {
    const k = keyFor(compCode);
    setCells((prev) => ({
      ...prev,
      [k]: { ...(prev[k] || {}), value_text: val, axis_values: { pefa_component: compCode } },
    }));
  };

  // Count of components that have a grade assigned, for the status footer
  const filled = components.reduce((acc, c) => {
    const cell = cells[keyFor(c.code)];
    const vt = cell && typeof cell.value_text === "string" && cell.value_text.trim();
    return acc + (vt ? 1 : 0);
  }, 0);

  return (
    <div style={{ border: "1px solid var(--line)", borderRadius: 8, overflow: "hidden" }}>
      <div style={{
        padding: "8px 12px", background: "var(--bg-sunken, #f3f4f6)",
        fontSize: 11.5, color: "var(--text-muted)",
        borderBottom: "1px solid var(--line)",
      }}>
        {lang === "fr"
          ? <>Composantes PEFA · <strong>{components.length}</strong> sous-composante{components.length > 1 ? "s" : ""}. Saisir la note (A · B+ · B · C+ · C · D+ · D · NR) pour chacune. <strong>Aucun calcul automatique</strong> — la note finale est saisie au-dessus.</>
          : <>PEFA components · <strong>{components.length}</strong> sub-component{components.length > 1 ? "s" : ""}. Pick a grade (A · B+ · B · C+ · C · D+ · D · NR) for each. <strong>No auto-computation</strong> — the final grade is entered above.</>}
      </div>
      <div style={{ maxHeight: 360, overflowY: "auto" }}>
        <table style={{ width: "100%", borderCollapse: "collapse", fontSize: 12 }}>
          <thead style={{ position: "sticky", top: 0, background: "var(--bg, white)", zIndex: 1 }}>
            <tr style={{ borderBottom: "1px solid var(--line)" }}>
              <th style={{ padding: "8px 10px", width: 110, fontSize: 10.5, textAlign: "left", color: "var(--text-faint)", textTransform: "uppercase", letterSpacing: "0.04em", fontWeight: 600 }}>
                {lang === "fr" ? "Code" : "Code"}
              </th>
              <th style={{ padding: "8px 10px", fontSize: 10.5, textAlign: "left", color: "var(--text-faint)", textTransform: "uppercase", letterSpacing: "0.04em", fontWeight: 600 }}>
                {lang === "fr" ? "Nom de la composante" : "Component name"}
              </th>
              <th style={{ padding: "8px 10px", width: 140, fontSize: 10.5, textAlign: "center", color: "var(--text-faint)", textTransform: "uppercase", letterSpacing: "0.04em", fontWeight: 600 }}>
                {lang === "fr" ? "Note PEFA" : "PEFA grade"}
              </th>
            </tr>
          </thead>
          <tbody>
            {components.map((comp) => {
              const k = keyFor(comp.code);
              const cell = cells[k] || {};
              const name = lang === "fr"
                ? (comp.name_fr || comp.name_en || comp.code)
                : (comp.name_en || comp.name_fr || comp.code);
              return (
                <tr key={comp.code} style={{ borderBottom: "1px solid var(--line-faint, #f1f5f9)" }}>
                  <td style={{ padding: "6px 10px", fontFamily: "var(--font-mono, monospace)", fontSize: 11.5, fontWeight: 600 }}>
                    {comp.code}
                  </td>
                  <td style={{ padding: "6px 10px", fontSize: 12 }}>
                    {name}
                  </td>
                  <td style={{ padding: "4px 8px", textAlign: "center" }}>
                    <select
                      value={cell.value_text || ""}
                      onChange={(e) => onChangeGrade(comp.code, e.target.value)}
                      style={{
                        width: 120, padding: "4px 6px",
                        border: "1px solid var(--line)", borderRadius: 4, fontSize: 12,
                        background: "var(--bg, white)", color: "var(--text)",
                        fontFamily: "monospace",
                        textAlign: "center",
                      }}>
                      <option value="">— —</option>
                      {(pefaLabels || []).map((l) => <option key={l} value={l}>{l}</option>)}
                    </select>
                  </td>
                </tr>
              );
            })}
          </tbody>
          <tfoot>
            <tr style={{ borderTop: "2px solid var(--line)", background: "var(--bg-sunken, #f3f4f6)" }}>
              <td colSpan={3} style={{ padding: "8px 10px", fontSize: 11, fontStyle: "italic", color: "var(--text-muted)" }}>
                {lang === "fr"
                  ? `${filled} / ${components.length} composante${components.length > 1 ? "s" : ""} notée${filled > 1 ? "s" : ""} · pas de calcul automatique`
                  : `${filled} / ${components.length} component${components.length > 1 ? "s" : ""} graded · no auto-computation`}
              </td>
            </tr>
          </tfoot>
        </table>
      </div>
    </div>
  );
}

function DisaggregationGrid({ lang, assignedAxes, cells, setCells, isPefa, pefaLabels, pefaComponents }) {
  // Special PEFA-components mode: when the definition declares structured
  // components (PI-X.1 … PI-X.N), bypass the generic axis machinery and
  // render one row per component with a name + alphabetic dropdown. The
  // assignedAxes are ignored in this mode — components are the only
  // disaggregation dimension that matters for a PEFA indicator's score.
  const isPefaComponentsMode = isPefa && Array.isArray(pefaComponents) && pefaComponents.length > 0;
  if (isPefaComponentsMode) {
    return (
      <PefaComponentsGrid
        lang={lang}
        components={pefaComponents}
        cells={cells}
        setCells={setCells}
        pefaLabels={pefaLabels} />
    );
  }

  // Load values for every axis. We use useEffect to fetch in parallel and
  // assemble a [{ axis, values }] structure.
  const [axesWithValues, setAxesWithValues] = useStateI([]);
  const [loading, setLoading] = useStateI(true);

  // axisIds joined to a stable key so the useEffect can depend on the axis set
  const axisIdsKey = (assignedAxes || []).map((a) => a.axis_id).sort().join(",");

  // Fetch values for each axis in parallel
  React.useEffect(() => {
    let alive = true;
    (async () => {
      try {
        const sb = await window.melr.supabase || (await waitForSb());
        const results = await Promise.all((assignedAxes || []).map(async (a) => {
          const r = await sb.from("indicator_disaggregation_values")
            .select("id, axis_id, organization_id, code, label_fr, label_en, description_fr, description_en, sort_order, active")
            .eq("axis_id", a.axis_id)
            .eq("active", true)
            .order("sort_order", { ascending: true });
          return { axis: a.axis, values: (r.data || []) };
        }));
        if (alive) {
          setAxesWithValues(results);
          setLoading(false);
        }
      } catch (e) {
        console.warn("[DisaggregationGrid] load values:", e.message || e);
        if (alive) setLoading(false);
      }
    })();
    return () => { alive = false; };
  }, [axisIdsKey]);

  // Helper to wait for supabase if not ready yet
  async function waitForSb() {
    for (let i = 0; i < 50; i++) {
      if (window.melr && window.melr.supabase) return window.melr.supabase;
      await new Promise((r) => setTimeout(r, 50));
    }
    throw new Error("Supabase not ready");
  }

  // Build the cartesian product of axis values → array of combinations
  // Each combination = { axisValues: {code1:v1, code2:v2}, label: "v1 · v2" }
  const combinations = React.useMemo(() => {
    if (axesWithValues.length === 0) return [];
    let combos = [[]];
    axesWithValues.forEach(({ axis, values }) => {
      const next = [];
      combos.forEach((c) => {
        values.forEach((v) => next.push([...c, { axisCode: axis.code, value: v }]));
      });
      combos = next;
    });
    return combos.map((c) => {
      const axisValues = {};
      const labels = [];
      c.forEach((entry) => {
        axisValues[entry.axisCode] = entry.value.code;
        labels.push(lang === "fr" ? entry.value.label_fr : (entry.value.label_en || entry.value.label_fr));
      });
      return {
        key: JSON.stringify(axisValues),
        axisValues,
        labelParts: c.map((entry) => ({
          axisCode: entry.axisCode,
          code: entry.value.code,
          label: lang === "fr" ? entry.value.label_fr : (entry.value.label_en || entry.value.label_fr),
          description: lang === "fr" ? (entry.value.description_fr || entry.value.label_fr) : (entry.value.description_en || entry.value.label_en || entry.value.label_fr),
          isOrgLocal: !!entry.value.organization_id,
        })),
      };
    });
  }, [axesWithValues, lang]);

  // Compute totals across the grid for the footer. In PEFA mode we only
  // count how many sub-components were graded (no numeric aggregation).
  const totals = React.useMemo(() => {
    let totN = 0, totD = 0, filled = 0;
    Object.values(cells).forEach((c) => {
      const n  = c && c.numerator   !== "" && c.numerator   != null ? Number(c.numerator)   : null;
      const d  = c && c.denominator !== "" && c.denominator != null ? Number(c.denominator) : null;
      const vt = c && typeof c.value_text === "string" && c.value_text.trim();
      if (n != null || d != null || vt) filled += 1;
      if (Number.isFinite(n)) totN += n;
      if (Number.isFinite(d)) totD += d;
    });
    return {
      numerator:   totN,
      denominator: totD,
      ratio:       totD > 0 ? totN / totD : null,
      filledCount: filled,
      totalCount:  combinations.length,
    };
  }, [cells, combinations.length]);

  const onChangeCell = (key, field, val) => {
    setCells((prev) => ({
      ...prev,
      [key]: { ...(prev[key] || {}), [field]: val },
    }));
  };

  if (loading) {
    return (
      <div className="text-faint" style={{ padding: 14, fontSize: 12, textAlign: "center" }}>
        {lang === "fr" ? "Chargement des valeurs de désagrégation…" : "Loading disaggregation values…"}
      </div>
    );
  }

  if (combinations.length === 0) {
    return (
      <div className="text-faint" style={{ padding: 14, fontSize: 12, textAlign: "center" }}>
        {lang === "fr"
          ? "Aucune combinaison à saisir — vérifier que les axes ont des valeurs."
          : "No combinations to enter — check that axes have values."}
      </div>
    );
  }

  return (
    <div style={{ border: "1px solid var(--line)", borderRadius: 8, overflow: "hidden" }}>
      <div style={{
        padding: "8px 12px", background: "var(--bg-sunken, #f3f4f6)",
        fontSize: 11.5, color: "var(--text-muted)",
        borderBottom: "1px solid var(--line)",
      }}>
        {isPefa
          ? (lang === "fr"
              ? <>Grille des sous-composantes · <strong>{combinations.length}</strong> combinaisons. Saisir la note PEFA (A · B+ · B · C+ · C · D+ · D · NR) pour chaque sous-composante. <strong>Aucun calcul automatique</strong> — la note finale est saisie au-dessus.</>
              : <>Sub-components grid · <strong>{combinations.length}</strong> combinations. Pick a PEFA grade (A · B+ · B · C+ · C · D+ · D · NR) for each sub-component. <strong>No auto-computation</strong> — the final grade is entered above.</>)
          : (lang === "fr"
              ? <>Grille de saisie · <strong>{combinations.length}</strong> combinaisons sur {assignedAxes.length} axe{assignedAxes.length > 1 ? "s" : ""}. Saisir le <strong>numérateur</strong> (N) et le <strong>dénominateur</strong> (D) — la valeur est calculée automatiquement.</>
              : <>Entry grid · <strong>{combinations.length}</strong> combinations across {assignedAxes.length} axis{assignedAxes.length > 1 ? "es" : ""}. Enter <strong>numerator</strong> (N) and <strong>denominator</strong> (D) — the value is computed automatically.</>)}
      </div>
      <div style={{ maxHeight: 360, overflowY: "auto" }}>
        <table style={{ width: "100%", borderCollapse: "collapse", fontSize: 12 }}>
          <thead style={{ position: "sticky", top: 0, background: "var(--bg, white)", zIndex: 1 }}>
            <tr style={{ borderBottom: "1px solid var(--line)" }}>
              {assignedAxes.map((a, i) => (
                <th key={i} style={{ padding: "8px 10px", fontSize: 10.5, textAlign: "left", color: "var(--text-faint)", textTransform: "uppercase", letterSpacing: "0.04em", fontWeight: 600 }}>
                  {lang === "fr" ? (a.axis.name_fr || a.axis.code) : (a.axis.name_en || a.axis.name_fr || a.axis.code)}
                </th>
              ))}
              {isPefa ? (
                <th style={{ padding: "8px 10px", width: 140, fontSize: 10.5, textAlign: "center", color: "var(--text-faint)", textTransform: "uppercase", letterSpacing: "0.04em", fontWeight: 600 }}>
                  {lang === "fr" ? "Note PEFA" : "PEFA grade"}
                </th>
              ) : (
                <>
                  <th style={{ padding: "8px 10px", width: 100, fontSize: 10.5, textAlign: "right", color: "var(--text-faint)", textTransform: "uppercase", letterSpacing: "0.04em", fontWeight: 600 }}>N</th>
                  <th style={{ padding: "8px 10px", width: 100, fontSize: 10.5, textAlign: "right", color: "var(--text-faint)", textTransform: "uppercase", letterSpacing: "0.04em", fontWeight: 600 }}>D</th>
                  <th style={{ padding: "8px 10px", width: 90, fontSize: 10.5, textAlign: "right", color: "var(--text-faint)", textTransform: "uppercase", letterSpacing: "0.04em", fontWeight: 600 }}>{lang === "fr" ? "Calculé" : "Computed"}</th>
                </>
              )}
            </tr>
          </thead>
          <tbody>
            {combinations.map((combo) => {
              const c = cells[combo.key] || {};
              const n = c.numerator   === "" || c.numerator   == null ? null : Number(c.numerator);
              const d = c.denominator === "" || c.denominator == null ? null : Number(c.denominator);
              let computed = null;
              if (n != null && d != null && d > 0) computed = n / d;     // ratio
              else if (n != null && (d == null || d === 0)) computed = n; // raw count
              return (
                <tr key={combo.key} style={{ borderBottom: "1px solid var(--line-faint, #f1f5f9)" }}>
                  {combo.labelParts.map((p, i) => (
                    <td key={i} style={{ padding: "6px 10px" }}>
                      <span title={p.description} style={{
                        display: "inline-flex", alignItems: "center", gap: 3,
                        fontSize: 11.5, fontWeight: 500,
                        padding: "2px 7px", borderRadius: 999,
                        background: p.isOrgLocal ? "oklch(0.96 0.04 75)" : "var(--bg-sunken, #f3f4f6)",
                        color:      p.isOrgLocal ? "oklch(0.42 0.10 75)" : "var(--text)",
                        border: "1px solid " + (p.isOrgLocal ? "oklch(0.82 0.10 75)" : "var(--line)"),
                      }}>{p.label}{p.isOrgLocal && <span style={{ fontSize: 9 }}>★</span>}</span>
                    </td>
                  ))}
                  {isPefa ? (
                    <td style={{ padding: "4px 8px", textAlign: "center" }}>
                      <select
                        value={c.value_text || ""}
                        onChange={(e) => onChangeCell(combo.key, "value_text", e.target.value)}
                        style={{
                          width: 120, padding: "4px 6px",
                          border: "1px solid var(--line)", borderRadius: 4, fontSize: 12,
                          background: "var(--bg, white)", color: "var(--text)",
                          fontFamily: "monospace",
                          textAlign: "center",
                        }}>
                        <option value="">— —</option>
                        {(pefaLabels || []).map((l) => <option key={l} value={l}>{l}</option>)}
                      </select>
                    </td>
                  ) : (
                    <>
                      <td style={{ padding: "4px 8px", textAlign: "right" }}>
                        <input type="number" step="any"
                          value={c.numerator || ""}
                          onChange={(e) => onChangeCell(combo.key, "numerator", e.target.value)}
                          style={{
                            width: 80, padding: "4px 6px", textAlign: "right",
                            border: "1px solid var(--line)", borderRadius: 4, fontSize: 12,
                            background: "var(--bg, white)", color: "var(--text)",
                            fontFamily: "monospace",
                          }} />
                      </td>
                      <td style={{ padding: "4px 8px", textAlign: "right" }}>
                        <input type="number" step="any"
                          value={c.denominator || ""}
                          onChange={(e) => onChangeCell(combo.key, "denominator", e.target.value)}
                          style={{
                            width: 80, padding: "4px 6px", textAlign: "right",
                            border: "1px solid var(--line)", borderRadius: 4, fontSize: 12,
                            background: "var(--bg, white)", color: "var(--text)",
                            fontFamily: "monospace",
                          }} />
                      </td>
                      <td style={{ padding: "4px 10px", textAlign: "right", fontFamily: "monospace", color: "var(--text-muted)", fontSize: 11.5 }}>
                        {computed == null ? "—" : (d != null && d > 0 ? (computed * 100).toFixed(1) + " %" : computed.toFixed(2))}
                      </td>
                    </>
                  )}
                </tr>
              );
            })}
          </tbody>
          <tfoot>
            {isPefa ? (
              // PEFA mode: just a status line, no aggregation. The final
              // grade lives in the dropdown ABOVE the grid — see the
              // explanatory caption on that dropdown.
              <tr style={{ borderTop: "2px solid var(--line)", background: "var(--bg-sunken, #f3f4f6)" }}>
                <td colSpan={assignedAxes.length + 1} style={{ padding: "8px 10px", fontSize: 11, fontStyle: "italic", color: "var(--text-muted)" }}>
                  {lang === "fr"
                    ? `${totals.filledCount} / ${totals.totalCount} sous-composantes notées · pas de calcul automatique`
                    : `${totals.filledCount} / ${totals.totalCount} sub-components graded · no auto-computation`}
                </td>
              </tr>
            ) : (
              <tr style={{ borderTop: "2px solid var(--line)", background: "var(--bg-sunken, #f3f4f6)", fontWeight: 600 }}>
                <td colSpan={assignedAxes.length} style={{ padding: "8px 10px", fontSize: 11.5 }}>
                  {lang === "fr" ? "TOTAL" : "TOTAL"} <span style={{ fontWeight: 400, color: "var(--text-faint)", fontSize: 11 }}>· {totals.filledCount} / {totals.totalCount} {lang === "fr" ? "cellules remplies" : "cells filled"}</span>
                </td>
                <td style={{ padding: "8px 10px", textAlign: "right", fontFamily: "monospace", fontSize: 12.5 }}>{totals.numerator || "—"}</td>
                <td style={{ padding: "8px 10px", textAlign: "right", fontFamily: "monospace", fontSize: 12.5 }}>{totals.denominator || "—"}</td>
                <td style={{ padding: "8px 10px", textAlign: "right", fontFamily: "monospace", fontSize: 12.5, color: "var(--accent, #4f46e5)" }}>
                  {totals.ratio != null ? (totals.ratio * 100).toFixed(1) + " %" : (totals.numerator ? totals.numerator : "—")}
                </td>
              </tr>
            )}
          </tfoot>
        </table>
      </div>
    </div>
  );
}

// ── Disaggregation context banner (E4) ────────────────────────────────────
// Shown at the top of EnterValueModal to make the disaggregation rules
// fully visible BEFORE the user starts typing.
//   - linked to catalogue + axes/components configured → list them as chips
//   - linked to catalogue but nothing configured        → neutral notice
//   - not linked to catalogue (manual indicator)        → explanatory notice
//     so the user understands why there is no grid (and how to fix it).
function DisaggregationContextBanner({ lang, indicator, definition, assignedAxes, pefaComponents, isPefa }) {
  const linked = !!(indicator && indicator.definition_code);
  const axes = Array.isArray(assignedAxes) ? assignedAxes : [];
  const comps = Array.isArray(pefaComponents) ? pefaComponents : [];
  const hasSomething = axes.length > 0 || comps.length > 0;

  // Case 3 : manual indicator, no link to catalogue → explanatory chip
  if (!linked) {
    return (
      <div style={{
        padding: "8px 12px", borderRadius: 6, fontSize: 11.5,
        background: "var(--bg-sunken)", border: "1px dashed var(--line)",
        color: "var(--text-muted)",
      }}>
        {lang === "fr"
          ? <>Cet indicateur n'est pas lié au catalogue. <strong>Aucune désagrégation héritée</strong>. Pour activer la désagrégation, ouvrir « Définition des indicateurs » et lier cet indicateur à une définition (code importé).</>
          : <>This indicator isn't linked to the catalogue. <strong>No inherited disaggregation</strong>. To enable disaggregation, open "Indicator definitions" and link this indicator to a definition (imported code).</>}
      </div>
    );
  }
  // Case 2 : linked but nothing configured
  if (!hasSomething) {
    return (
      <div style={{
        padding: "8px 12px", borderRadius: 6, fontSize: 11.5,
        background: "var(--bg-sunken)", border: "1px dashed var(--line)",
        color: "var(--text-muted)",
      }}>
        {lang === "fr"
          ? <>Indicateur lié au catalogue (<code style={{ fontSize: 10.5 }}>{indicator.definition_code}</code>) mais aucune désagrégation configurée. Pour en ajouter, ouvrir « Définition des indicateurs » → modifier cette définition.</>
          : <>Indicator linked to catalogue (<code style={{ fontSize: 10.5 }}>{indicator.definition_code}</code>) but no disaggregation configured. To add some, open "Indicator definitions" → edit this definition.</>}
      </div>
    );
  }
  // Case 1 : list axes/components as chips
  return (
    <div style={{
      padding: "10px 12px", borderRadius: 6,
      background: "oklch(0.97 0.04 195)",
      border: "1px solid color-mix(in oklch, oklch(0.58 0.13 195) 30%, transparent)",
    }}>
      <div style={{ fontSize: 10.5, fontWeight: 700, color: "oklch(0.36 0.10 195)", marginBottom: 5, letterSpacing: "0.04em", textTransform: "uppercase" }}>
        {lang === "fr" ? "Désagrégation à saisir" : "Disaggregation to enter"}
      </div>
      <div style={{ display: "flex", gap: 6, flexWrap: "wrap" }}>
        {/* PEFA components first if any */}
        {isPefa && comps.length > 0 && comps.map((c) => {
          const name = lang === "fr"
            ? (c.name_fr || c.name_en || c.code)
            : (c.name_en || c.name_fr || c.code);
          return (
            <span key={c.code} title={name} style={{
              display: "inline-flex", alignItems: "center", gap: 4,
              fontSize: 11.5, fontWeight: 500,
              padding: "2px 9px", borderRadius: 999,
              background: "white", color: "oklch(0.36 0.10 195)",
              border: "1px solid oklch(0.75 0.10 195)",
            }}>
              <code style={{ fontSize: 10, opacity: 0.7 }}>{c.code}</code>
              {name && name !== c.code && <span>· {name}</span>}
            </span>
          );
        })}
        {/* Generic axes */}
        {axes.map((row, i) => {
          const label = lang === "fr" ? (row.axis && (row.axis.name_fr || row.axis.code)) : (row.axis && (row.axis.name_en || row.axis.name_fr || row.axis.code));
          return (
            <span key={"a-" + i} style={{
              display: "inline-flex", alignItems: "center", gap: 3,
              fontSize: 11.5, fontWeight: 500,
              padding: "2px 9px", borderRadius: 999,
              background: "white", color: "oklch(0.36 0.10 195)",
              border: "1px solid oklch(0.75 0.10 195)",
            }}>
              {label}
              {row.is_required && <span style={{ fontSize: 9, opacity: 0.7 }}>*</span>}
            </span>
          );
        })}
      </div>
    </div>
  );
}

function EnterValueModal({ lang, indicator, onClose, onCreated }) {
  // Default period: current quarter
  const today = new Date();
  const q = Math.floor(today.getMonth() / 3);
  const periodStartDefault = new Date(today.getFullYear(), q * 3, 1).toISOString().slice(0, 10);
  const periodEndDefault   = new Date(today.getFullYear(), q * 3 + 3, 0).toISOString().slice(0, 10);

  const valueKind = (indicator && indicator.value_kind) || "numeric";
  const isPefa = valueKind === "score_pefa";
  const pefaLabels = (window.melr.scoreLabels && window.melr.scoreLabels("score_pefa")) || [];

  // ── Disaggregation lookup (Phase D) ───────────────────────────────────
  // Resolve definition_code → definition_id, then load the axes configured
  // for that definition. If 1+ axes configured, switch to the pivot grid
  // mode (PEFA included — sub-components scored alphabetically without
  // auto-summation, see PEFA branch in DisaggregationGrid).
  const { data: definition } = window.melr.useDefinitionByCode(indicator && indicator.definition_code);
  const { data: assignedAxes, loading: axesLoading } = window.melr.useDefinitionDisaggregations(definition && definition.id);
  // E1: PEFA structured components live on the definition itself (jsonb
  // array). When present AND the indicator is PEFA, they REPLACE the
  // generic axis-based grid with a 1-row-per-component layout.
  const pefaComponents = (isPefa && definition && Array.isArray(definition.components)) ? definition.components : [];
  const hasPefaComponents = pefaComponents.length > 0;
  // Grid mode triggers if the indicator either has axes assigned OR (PEFA)
  // has structured components declared on its definition.
  const useGrid = (assignedAxes && assignedAxes.length > 0) || hasPefaComponents;

  // ── Simple-mode state (no disaggregation) ─────────────────────────────
  const [value, setValue]         = useStateI("");
  const [valueText, setValueText] = useStateI("");
  const [periodStart, setPeriodStart] = useStateI(periodStartDefault);
  const [periodEnd,   setPeriodEnd]   = useStateI(periodEndDefault);
  const [comment, setComment] = useStateI("");
  const [submitting, setSubmitting] = useStateI(false);
  const [err, setErr] = useStateI(null);

  // ── Grid-mode state (disaggregated) ───────────────────────────────────
  // gridCells is keyed by JSON.stringify(axis_values). Each entry holds the
  // user-entered N/D for one combination.
  const [gridCells, setGridCells] = useStateI({});

  const submit = async (e) => {
    e.preventDefault();
    setErr(null); setSubmitting(true);
    try {
      let numeric;
      let payloadValue = null;
      let payloadValueText = null;
      let payloadNumerator = null;
      let payloadDenominator = null;
      // Cells to upsert AFTER the parent indicator_value is created (only
      // populated in grid mode).
      let gridRowsToSave = null;

      if (useGrid && isPefa) {
        // PEFA + grid : the final score comes from the dropdown above the
        // grid (valueText). The grid cells carry the alphabetic grades of
        // each sub-component — NO auto-summation, NO numeric computation
        // of the parent from the cells (PEFA framework decision-making is
        // qualitative, not a simple average).
        if (!valueText) throw new Error(lang === "fr" ? "Veuillez saisir la note finale PEFA." : "Please pick the final PEFA grade.");
        numeric = window.melr.scoreToNumeric("score_pefa", valueText);
        payloadValueText = valueText;
        // Collect non-empty grid cells with their alphabetic grade
        const rows = [];
        Object.entries(gridCells).forEach(([key, c]) => {
          const vt = (c.value_text || "").trim();
          if (!vt) return;
          const v = window.melr.scoreToNumeric("score_pefa", vt);
          rows.push({
            axis_values: JSON.parse(key),
            value:       Number.isFinite(v) ? v : null,
            value_text:  vt,
          });
        });
        gridRowsToSave = rows;
      } else if (useGrid) {
        // Aggregate the grid: sum all numerators and denominators across
        // every filled cell. The parent indicator_value gets the totals
        // (so charts + progress bars still work), each cell goes into
        // indicator_value_disaggregations.
        let totN = 0, totD = 0, anyFilled = false, allCellsHaveDenom = true;
        const rows = [];
        Object.entries(gridCells).forEach(([key, c]) => {
          const n = c.numerator   === "" || c.numerator   == null ? null : Number(c.numerator);
          const d = c.denominator === "" || c.denominator == null ? null : Number(c.denominator);
          if (n == null && d == null) return;            // empty cell, skip
          anyFilled = true;
          if (Number.isFinite(n)) totN += n;
          if (Number.isFinite(d)) { totD += d; } else { allCellsHaveDenom = false; }
          rows.push({
            axis_values: JSON.parse(key),
            numerator:   n,
            denominator: d,
          });
        });
        if (!anyFilled) throw new Error(lang === "fr" ? "Au moins une cellule doit être remplie." : "At least one cell must be filled.");
        gridRowsToSave = rows;
        // Parent value: ratio if all rows have a denominator > 0, else
        // just the total count (numerator sum).
        if (allCellsHaveDenom && totD > 0) {
          numeric        = totN / totD;
          payloadValue   = numeric;
          payloadNumerator   = totN;
          payloadDenominator = totD;
        } else {
          numeric      = totN;
          payloadValue = totN;
          payloadNumerator = totN;     // also store as numerator for traceability
        }
      } else if (isPefa) {
        numeric = window.melr.scoreToNumeric("score_pefa", valueText);
        payloadValueText = valueText || null;
      } else {
        numeric = value === "" ? null : parseFloat(value);
        payloadValue = value;
      }

      // Auto-derive status from numeric vs baseline/target.
      let status = "pending";
      const b = Number(indicator.baseline) || 0;
      const t = Number(indicator.target)   || 0;
      if (numeric != null && !isNaN(numeric) && b !== t) {
        const progress = (t > b) ? (numeric - b) / (t - b) : (b - numeric) / (b - t);
        if (progress >= 0.9)      status = "ok";
        else if (progress >= 0.6) status = "warn";
        else                       status = "risk";
      }

      const saved = await window.melr.createIndicatorValue(indicator.uuid, {
        period_start: periodStart,
        period_end:   periodEnd,
        value_kind:   valueKind,
        value:        payloadValue,
        value_text:   payloadValueText,
        numerator:    payloadNumerator,
        denominator:  payloadDenominator,
        status,
        comment,
      });

      // If we used the grid, persist the per-cell breakdown
      if (gridRowsToSave && gridRowsToSave.length > 0 && saved && saved.id) {
        await window.melr.disaggregatedValuesCrud.upsertGrid(saved.id, gridRowsToSave);
      }

      if (onCreated) await onCreated();
      onClose();
    } catch (e2) {
      setErr(e2.message);
    } finally {
      setSubmitting(false);
    }
  };

  const inp = { padding: "8px 10px", borderRadius: 6, border: "1px solid var(--line)", fontSize: 13 };
  const lbl = { fontSize: 11, opacity: 0.75 };

  return (
    <div onClick={onClose} style={{
      position: "fixed", inset: 0, background: "rgba(0,0,0,.45)", zIndex: 9999,
      display: "flex", alignItems: "center", justifyContent: "center",
      padding: 16, overflowY: "auto",
    }}>
      <form onClick={(e) => e.stopPropagation()} onSubmit={submit} style={{
        background: "var(--bg, white)", color: "var(--text, #111)",
        padding: 22, borderRadius: 10,
        // Responsive width: small (compact) modal vs grid modal that fits
        // the viewport. min(95vw, 900px) prevents the modal from spilling
        // off-screen on narrow displays AND from looking absurd on huge
        // monitors. Internal scroll handled by overflowY + inner table
        // wrappers; horizontal scroll is contained by box-sizing.
        width: useGrid ? "min(95vw, 900px)" : 460,
        maxWidth: "95vw",
        maxHeight: "90vh", overflowY: "auto", overflowX: "hidden",
        boxSizing: "border-box",
        boxShadow: "0 10px 30px rgba(0,0,0,.25)", display: "grid", gap: 10,
      }}>
        <div>
          <div style={{ fontSize: 18, fontWeight: 600 }}>
            {lang === "fr" ? "Saisir une valeur" : "Add a value"}
            {useGrid && (
              <span style={{
                fontSize: 10.5, fontWeight: 700, marginLeft: 8, padding: "2px 8px",
                borderRadius: 4, background: "oklch(0.96 0.05 264)", color: "var(--accent, #4f46e5)",
                letterSpacing: "0.04em", textTransform: "uppercase",
              }}>
                {lang === "fr" ? "Désagrégé" : "Disaggregated"}
              </span>
            )}
          </div>
          <div className="text-faint" style={{ fontSize: 12, marginTop: 2 }}>
            <span className="tag-mono">{indicator.id}</span> · {indicator.name}
            {indicator.unit && <span className="text-faint"> · {lang === "fr" ? "unité :" : "unit:"} {indicator.unit}</span>}
          </div>
          <div className="text-faint" style={{ fontSize: 11, marginTop: 2 }}>
            {lang === "fr" ? `Baseline ${indicator.baseline} · Cible ${indicator.target}` : `Baseline ${indicator.baseline} · Target ${indicator.target}`}
          </div>
        </div>
        <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 10 }}>
          <label style={{ display: "grid", gap: 4 }}>
            <span style={lbl}>{lang === "fr" ? "Début de période" : "Period start"}</span>
            <input required type="date" value={periodStart} onChange={(e) => setPeriodStart(e.target.value)} style={inp} />
          </label>
          <label style={{ display: "grid", gap: 4 }}>
            <span style={lbl}>{lang === "fr" ? "Fin de période" : "Period end"}</span>
            <input required type="date" value={periodEnd} onChange={(e) => setPeriodEnd(e.target.value)} style={inp} />
          </label>
        </div>

        {axesLoading ? (
          <div className="text-faint" style={{ fontSize: 12, padding: 12, textAlign: "center" }}>
            {lang === "fr" ? "Vérification de la désagrégation requise…" : "Checking required disaggregation…"}
          </div>
        ) : (
          <>
            {/* E4: Visibility of the disaggregation context BEFORE the user
                starts typing. Three cases :
                  1. Indicator linked to catalogue + axes/components → list them
                  2. Indicator linked to catalogue but no axes/components → say so
                  3. Indicator not linked to catalogue (manual) → explain why
                     there is no inherited disaggregation. */}
            <DisaggregationContextBanner
              lang={lang}
              indicator={indicator}
              definition={definition}
              assignedAxes={assignedAxes}
              pefaComponents={pefaComponents}
              isPefa={isPefa} />

            {/* For PEFA indicators the FINAL grade is always entered via
                a dropdown — even when sub-components are disaggregated
                in the grid below. PEFA framework decision: the final
                score is NOT a simple average, so the user picks it. */}
            {isPefa && (
              <label style={{ display: "grid", gap: 4 }}>
                <span style={lbl}>{lang === "fr" ? "Note finale (PEFA)" : "Final score (PEFA)"}</span>
                <select required value={valueText} onChange={(e) => setValueText(e.target.value)} style={inp}>
                  <option value="">{lang === "fr" ? "— Choisir un score —" : "— Pick a score —"}</option>
                  {pefaLabels.map((l) => <option key={l} value={l}>{l}</option>)}
                </select>
                {useGrid && (
                  <span className="text-faint" style={{ fontSize: 10.5, fontStyle: "italic" }}>
                    {lang === "fr"
                      ? "La note finale est saisie séparément. La grille ci-dessous capture les notes des sous-composantes (aucun calcul automatique)."
                      : "The final grade is entered separately. The grid below captures sub-component grades (no auto-computation)."}
                  </span>
                )}
              </label>
            )}

            {useGrid ? (
              <DisaggregationGrid
                lang={lang}
                assignedAxes={assignedAxes}
                cells={gridCells}
                setCells={setGridCells}
                isPefa={isPefa}
                pefaLabels={pefaLabels}
                pefaComponents={pefaComponents}
              />
            ) : !isPefa ? (
              <label style={{ display: "grid", gap: 4 }}>
                <span style={lbl}>{lang === "fr" ? "Valeur" : "Value"}</span>
                <input required type="number" step="any" value={value} onChange={(e) => setValue(e.target.value)} placeholder={String((indicator.baseline + indicator.target) / 2)} style={inp} />
              </label>
            ) : null}
          </>
        )}
        <label style={{ display: "grid", gap: 4 }}>
          <span style={lbl}>{lang === "fr" ? "Commentaire (optionnel)" : "Comment (optional)"}</span>
          <textarea value={comment} onChange={(e) => setComment(e.target.value)} rows={2}
            placeholder={lang === "fr" ? "Méthode de collecte, sources, observations…" : "Collection method, sources, observations…"}
            style={{ ...inp, resize: "vertical" }} />
        </label>
        <div className="text-faint" style={{ fontSize: 11 }}>
          {lang === "fr"
            ? "Le statut (ok / warn / risk) est calculé automatiquement à partir de la cible."
            : "Status (ok / warn / risk) is auto-derived from the target."}
        </div>
        {err && <div style={{ color: "#b91c1c", fontSize: 12 }}>{err}</div>}
        <div style={{ display: "flex", gap: 8, justifyContent: "flex-end", marginTop: 4 }}>
          <button type="button" onClick={onClose} disabled={submitting}
            style={{ padding: "8px 14px", borderRadius: 6, border: "1px solid var(--line)", background: "transparent", cursor: "pointer" }}>
            {lang === "fr" ? "Annuler" : "Cancel"}
          </button>
          <button type="submit" disabled={submitting}
            style={{ padding: "8px 14px", borderRadius: 6, border: 0, background: "#2563eb", color: "white", cursor: "pointer", fontWeight: 600 }}>
            {submitting ? "…" : (lang === "fr" ? "Enregistrer" : "Save")}
          </button>
        </div>
      </form>
    </div>
  );
}

// ==================== CREATE INDICATOR MODAL ====================
// The Code field offers two modes: pick from the org catalogue (sourced
// from indicator_definitions) or type a custom code manually. When a
// catalogue entry is picked, code/name/level are auto-filled and frozen so
// the project instance stays consistent with the definition.
function CreateIndicatorModal({ lang, onClose, onCreated, defaultProject }) {
  const { data: defs }          = window.melr.useIndicatorDefinitions();
  const { projects: liveProjects, loading: projectsLoading } = window.melr.useProjects();
  // Live sectors (DB-backed catalogue). Picking the imported code in the
  // catalogue auto-fills the sector below.
  const { data: liveSectors }   = window.melr.useSectors();
  const [definitionCode, setDefinitionCode] = useStateI(""); // "" = manual mode
  const [code, setCode]         = useStateI("");
  const [project, setProject]   = useStateI(defaultProject || "");
  const [level, setLevel]       = useStateI("outcome");
  const [nameFr, setNameFr]     = useStateI("");
  const [nameEn, setNameEn]     = useStateI("");
  const [unit, setUnit]         = useStateI("");
  const [sectorId, setSectorId] = useStateI("");
  const [newSectorOpen, setNewSectorOpen] = useStateI(false);
  const [baseline, setBaseline]         = useStateI(""); // numeric (when kind=numeric)
  const [baselineText, setBaselineText] = useStateI(""); // PEFA letter (when kind=score_pefa)
  const [target, setTarget]             = useStateI("");
  const [targetText, setTargetText]     = useStateI("");
  const [valueKind, setValueKind]       = useStateI("numeric");
  const [frequency, setFrequency] = useStateI("quarterly");
  const [submitting, setSubmitting] = useStateI(false);
  const [err, setErr] = useStateI(null);

  const onSectorChange = (val) => {
    if (val === "__NEW__") setNewSectorOpen(true);
    else                   setSectorId(val);
  };

  // Default the project picker to the first real project so the user
  // isn't stuck on a stale placeholder. If a defaultProject was passed
  // (e.g. when opened from the project detail page), keep it locked in.
  React.useEffect(() => {
    if (defaultProject) { setProject(defaultProject); return; }
    if (!project && liveProjects && liveProjects.length > 0) {
      setProject(liveProjects[0].id);
    }
  }, [liveProjects, defaultProject]);

  // When the user picks a catalogue code, autofill the metadata + adopt
  // the catalogue's measurement kind (PEFA / numeric) and sector.
  React.useEffect(() => {
    if (!definitionCode) return;
    const def = (defs || []).find((d) => d.code === definitionCode);
    if (!def) return;
    setCode(def.code);
    if (def.name_fr) setNameFr(def.name_fr);
    if (def.name_en) setNameEn(def.name_en);
    if (def.level)   setLevel(def.level);
    setValueKind(def.value_kind || "numeric");
    if (def.sector_id) setSectorId(def.sector_id);
  }, [definitionCode, defs]);

  const fromCatalogue = !!definitionCode;
  const isPefa = valueKind === "score_pefa";
  const pefaLabels = (window.melr.scoreLabels && window.melr.scoreLabels("score_pefa")) || [];

  const submit = async (e) => {
    e.preventDefault();
    setErr(null); setSubmitting(true);
    try {
      await window.melr.createIndicator(project, {
        code:            code.trim(),
        level,
        name_fr:         nameFr.trim(),
        name_en:         nameEn.trim() || nameFr.trim(),
        unit:            unit.trim(),
        value_kind:      valueKind,
        baseline:        isPefa ? null : (baseline === "" ? null : Number(baseline)),
        target:          isPefa ? null : (target   === "" ? null : Number(target)),
        baseline_text:   isPefa ? (baselineText || null) : null,
        target_text:     isPefa ? (targetText   || null) : null,
        frequency,
        definition_code: definitionCode || null,
        // Explicit sector override; falls back to the project's sector
        // server-side when empty (see createIndicator in melr-data).
        sector_id:       sectorId || null,
      });
      if (onCreated) await onCreated();
      onClose();
    } catch (e2) {
      setErr(e2.message);
    } finally {
      setSubmitting(false);
    }
  };

  const inp = { padding: "8px 10px", borderRadius: 6, border: "1px solid var(--line)", fontSize: 13 };
  const inpFrozen = { ...inp, background: "var(--bg-sunken, #f3f4f6)", color: "var(--text-faint)" };
  const lbl = { fontSize: 11, opacity: 0.75 };

  return (
    <div onClick={onClose} style={{
      position: "fixed", inset: 0, background: "rgba(0,0,0,.45)", zIndex: 9999,
      display: "flex", alignItems: "center", justifyContent: "center",
      padding: 16,
    }}>
      <form onClick={(e) => e.stopPropagation()} onSubmit={submit} style={{
        background: "var(--bg, white)", color: "var(--text, #111)",
        borderRadius: 10, width: 560, maxWidth: "92vw",
        maxHeight: "calc(100vh - 32px)",
        boxShadow: "0 10px 30px rgba(0,0,0,.25)",
        display: "flex", flexDirection: "column",
      }}>
        <div style={{ fontSize: 18, fontWeight: 600, padding: "18px 22px 8px" }}>
          {lang === "fr" ? "Nouvel indicateur" : "New indicator"}
        </div>

        <div style={{ overflowY: "auto", padding: "8px 22px", display: "grid", gap: 10, flex: 1, minHeight: 0 }}>

        {/* Source: pick from catalogue (Définition des indicateurs) or manual */}
        <label style={{ display: "grid", gap: 4 }}>
          <span style={lbl}>
            {lang === "fr"
              ? "Code importé (depuis la définition des indicateurs)"
              : "Imported code (from indicator definitions)"}
          </span>
          <select value={definitionCode} onChange={(e) => {
            setDefinitionCode(e.target.value);
            // Reset to clean state if user switches back to manual
            if (!e.target.value) { setCode(""); /* keep typed name */ }
          }} style={inp}>
            <option value="">
              {lang === "fr"
                ? "— Saisie manuelle (code personnalisé) —"
                : "— Manual entry (custom code) —"}
            </option>
            {(defs || []).map((d) => (
              <option key={d.id} value={d.code}>
                {d.code} · {lang === "en" ? (d.name_en || d.name_fr) : d.name_fr}
                {d.origin_institution ? " · " + d.origin_institution : ""}
              </option>
            ))}
          </select>
          {fromCatalogue && (
            <div className="text-faint" style={{ fontSize: 10.5 }}>
              {lang === "fr"
                ? "📋 Code, nom et niveau sont issus du catalogue (modifiables uniquement dans « Définition des indicateurs »)."
                : "📋 Code, name and level come from the catalogue (edit in 'Indicator definitions' only)."}
            </div>
          )}
        </label>

        {/* Inherited disaggregation preview — shows which axes will be
            required at data-entry time for this indicator. Read-only here
            (config lives in "Définition des indicateurs"). */}
        {fromCatalogue && (
          <InheritedDisaggregationInfo
            lang={lang}
            definitionCode={definitionCode}
            defs={defs} />
        )}

        <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: 10 }}>
          <label style={{ display: "grid", gap: 4 }}>
            <span style={lbl}>{lang === "fr" ? "Code" : "Code"}</span>
            <input required value={code} onChange={(e) => setCode(e.target.value)}
              placeholder="OC-04"
              readOnly={fromCatalogue}
              style={fromCatalogue ? inpFrozen : inp} />
          </label>
          <label style={{ display: "grid", gap: 4 }}>
            <span style={lbl}>{lang === "fr" ? "Projet" : "Project"}</span>
            <select required value={project} onChange={(e) => setProject(e.target.value)} style={inp}
              disabled={projectsLoading}>
              <option value="">{lang === "fr" ? "— Choisir —" : "— Pick —"}</option>
              {(liveProjects || []).map((p) => (
                <option key={p.uuid} value={p.id}>
                  {p.id} · {lang === "en" ? (p.nameEn || p.nameFr) : p.nameFr}
                </option>
              ))}
            </select>
          </label>
          <label style={{ display: "grid", gap: 4 }}>
            <span style={lbl}>{lang === "fr" ? "Niveau" : "Level"}</span>
            <select value={level} onChange={(e) => setLevel(e.target.value)}
              disabled={fromCatalogue}
              style={fromCatalogue ? inpFrozen : inp}>
              <option value="output">Output</option>
              <option value="outcome">Outcome</option>
              <option value="mixed">{lang === "fr" ? "Mixte (Output/Outcome)" : "Mixed (Output/Outcome)"}</option>
              <option value="impact">Impact</option>
              <option value="context">Context</option>
            </select>
          </label>
        </div>
        <label style={{ display: "grid", gap: 4 }}>
          <span style={lbl}>{lang === "fr" ? "Nom (FR)" : "Name (FR)"}</span>
          <input required value={nameFr} onChange={(e) => setNameFr(e.target.value)}
            placeholder={lang === "fr" ? "Taux de fréquentation des CSCom" : "CSCom attendance rate"}
            readOnly={fromCatalogue}
            style={fromCatalogue ? inpFrozen : inp} />
        </label>
        <label style={{ display: "grid", gap: 4 }}>
          <span style={lbl}>{lang === "fr" ? "Nom (EN — optionnel)" : "Name (EN — optional)"}</span>
          <input value={nameEn} onChange={(e) => setNameEn(e.target.value)}
            placeholder="CSCom attendance rate"
            readOnly={fromCatalogue}
            style={fromCatalogue ? inpFrozen : inp} />
        </label>
        <label style={{ display: "grid", gap: 4 }}>
          <span style={lbl}>{lang === "fr" ? "Secteur (optionnel)" : "Sector (optional)"}</span>
          <select value={sectorId} onChange={(e) => onSectorChange(e.target.value)}
            disabled={fromCatalogue}
            style={fromCatalogue ? inpFrozen : inp}>
            <option value="">{lang === "fr" ? "— Hériter du projet —" : "— Inherit from project —"}</option>
            {(liveSectors || []).map((s) => (
              <option key={s.id} value={s.id}>{lang === "en" ? s.en : s.fr}</option>
            ))}
            {!fromCatalogue && <option value="__NEW__">{lang === "fr" ? "+ Nouveau secteur…" : "+ New sector…"}</option>}
          </select>
          <div className="text-faint" style={{ fontSize: 10.5 }}>
            {fromCatalogue
              ? (lang === "fr"
                  ? "📋 Secteur issu du catalogue. Vide ⇒ l'indicateur hérite du secteur du projet à l'enregistrement."
                  : "📋 Sector inherited from the catalogue. Empty ⇒ indicator falls back to the project's sector at save time.")
              : (lang === "fr"
                  ? "Si vide, l'indicateur héritera automatiquement du secteur du projet à l'enregistrement."
                  : "If empty, the indicator will inherit the project's sector at save time.")}
          </div>
        </label>
        {isPefa && (
          <div style={{ padding: "6px 10px", background: "#eef2ff", color: "#3730a3", borderRadius: 5, fontSize: 11.5 }}>
            {lang === "fr"
              ? "📊 Indicateur PEFA — baseline et cible se saisissent en lettres (A · B+ · B · C+ · C · D+ · D). La conversion numérique se fait automatiquement à l'enregistrement."
              : "📊 PEFA indicator — baseline and target are entered as letters (A · B+ · B · C+ · C · D+ · D). Numeric conversion happens automatically on save."}
          </div>
        )}
        <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr 1fr", gap: 10 }}>
          <label style={{ display: "grid", gap: 4 }}>
            <span style={lbl}>{lang === "fr" ? "Unité" : "Unit"}</span>
            <input value={unit} onChange={(e) => setUnit(e.target.value)} placeholder="%"
              disabled={isPefa}
              style={isPefa ? { ...inp, background: "var(--bg-sunken, #f3f4f6)", color: "var(--text-faint)" } : inp} />
          </label>
          <label style={{ display: "grid", gap: 4 }}>
            <span style={lbl}>{lang === "fr" ? "Baseline" : "Baseline"}</span>
            {isPefa ? (
              <select value={baselineText} onChange={(e) => setBaselineText(e.target.value)} style={inp}>
                <option value="">{lang === "fr" ? "—" : "—"}</option>
                {pefaLabels.map((l) => <option key={l} value={l}>{l}</option>)}
              </select>
            ) : (
              <input type="number" step="any" value={baseline} onChange={(e) => setBaseline(e.target.value)} placeholder="32.5" style={inp} />
            )}
          </label>
          <label style={{ display: "grid", gap: 4 }}>
            <span style={lbl}>{lang === "fr" ? "Cible" : "Target"}</span>
            {isPefa ? (
              <select value={targetText} onChange={(e) => setTargetText(e.target.value)} style={inp}>
                <option value="">{lang === "fr" ? "—" : "—"}</option>
                {pefaLabels.map((l) => <option key={l} value={l}>{l}</option>)}
              </select>
            ) : (
              <input type="number" step="any" value={target} onChange={(e) => setTarget(e.target.value)} placeholder="65" style={inp} />
            )}
          </label>
          <label style={{ display: "grid", gap: 4 }}>
            <span style={lbl}>{lang === "fr" ? "Fréquence" : "Frequency"}</span>
            <select value={frequency} onChange={(e) => setFrequency(e.target.value)} style={inp}>
              <option value="monthly">{lang === "fr" ? "Mensuel" : "Monthly"}</option>
              <option value="quarterly">{lang === "fr" ? "Trimestriel" : "Quarterly"}</option>
              <option value="biannual">{lang === "fr" ? "Semestriel" : "Biannual"}</option>
              <option value="yearly">{lang === "fr" ? "Annuel" : "Yearly"}</option>
            </select>
          </label>
        </div>
        {err && <div style={{ color: "#b91c1c", fontSize: 12 }}>{err}</div>}
        </div>
        <div style={{
          display: "flex", gap: 8, justifyContent: "flex-end",
          padding: "12px 22px 18px",
          borderTop: "1px solid var(--line)",
          background: "var(--bg, white)",
        }}>
          <button type="button" onClick={onClose} disabled={submitting}
            style={{ padding: "8px 14px", borderRadius: 6, border: "1px solid var(--line)", background: "transparent", cursor: "pointer" }}>
            {lang === "fr" ? "Annuler" : "Cancel"}
          </button>
          <button type="submit" disabled={submitting}
            style={{ padding: "8px 14px", borderRadius: 6, border: 0, background: "#2563eb", color: "white", cursor: "pointer", fontWeight: 600 }}>
            {submitting ? "…" : (lang === "fr" ? "Enregistrer" : "Save")}
          </button>
        </div>
      </form>
      {newSectorOpen && (
        <NewSectorModal lang={lang}
          onClose={() => setNewSectorOpen(false)}
          onCreated={(sec) => { setSectorId(sec.id); setNewSectorOpen(false); }} />
      )}
    </div>
  );
}

function Sparkline2({ values, status }) {
  const w = 110, h = 28;
  const min = Math.min(...values), max = Math.max(...values);
  const range = max - min || 1;
  const pts = values.map((v, i) => `${(i / (values.length - 1)) * w},${h - ((v - min) / range) * (h - 4) - 2}`).join(" ");
  const color = status === "ok" ? "var(--green)" : status === "warn" ? "var(--amber)" : "var(--red)";
  return (
    <svg width={w} height={h} viewBox={`0 0 ${w} ${h}`}>
      <polyline points={pts} fill="none" stroke={color} strokeWidth="1.4" />
      <circle cx={w} cy={h - ((values[values.length - 1] - min) / range) * (h - 4) - 2} r="2" fill={color} />
    </svg>
  );
}

function BigChart() {
  const W = 540, H = 160, padL = 40, padR = 12, padT = 14, padB = 24;
  const months = ["Jan", "Fév", "Mar", "Avr", "Mai", "Jun", "Jul", "Aoû", "Sep", "Oct", "Nov", "Déc"];
  const actual = [61, 58, 55, 52, 49, 47, 45, 43, null, null, null, null];
  const target = [61, 65, 69, 73, 77, 81, 85, 88, 90, 91, 92, 92];
  const yScale = (v) => padT + (1 - v / 100) * (H - padT - padB);
  const xScale = (i) => padL + (i / (months.length - 1)) * (W - padL - padR);
  const linePts = (arr) => arr.map((v, i) => v == null ? null : `${xScale(i)},${yScale(v)}`).filter(Boolean).join(" ");
  return (
    <svg viewBox={`0 0 ${W} ${H}`} style={{ width: "100%" }}>
      {[0, 25, 50, 75, 100].map(g => (
        <g key={g}>
          <line x1={padL} y1={yScale(g)} x2={W - padR} y2={yScale(g)} stroke="var(--line-faint)" strokeDasharray="2 3" />
          <text x={padL - 6} y={yScale(g) + 3} fontSize="9" textAnchor="end" fill="var(--text-faint)" fontFamily="var(--font-mono)">{g}%</text>
        </g>
      ))}
      <polyline points={linePts(target)} fill="none" stroke="var(--text)" strokeWidth="1.3" strokeDasharray="3 3" opacity="0.6" />
      <polyline points={linePts(actual)} fill="none" stroke="var(--red)" strokeWidth="2" />
      {actual.map((v, i) => v != null && <circle key={i} cx={xScale(i)} cy={yScale(v)} r="2.5" fill="var(--red)" />)}
      {months.map((m, i) => (
        <text key={i} x={xScale(i)} y={H - 8} textAnchor="middle" fontSize="9.5" fill="var(--text-faint)">{m}</text>
      ))}
    </svg>
  );
}

// ──── Inherited disaggregation info ────────────────────────────────────────
// Shown in CreateIndicatorModal + EditIndicatorModal as a read-only summary
// of which axes the indicator is configured to be disaggregated along.
// The config itself lives at the definition level — this is just a preview.
function InheritedDisaggregationInfo({ lang, definitionCode, defs }) {
  // Find the matching definition from the in-memory list
  const def = (defs || []).find((d) => d.code === definitionCode);
  const definitionId = def && def.id;
  const { data: assigned, loading } = window.melr.useDefinitionDisaggregations(definitionId);

  if (!definitionId) return null;
  if (loading) {
    return (
      <div style={{ padding: "8px 10px", fontSize: 11, color: "var(--text-faint)", borderRadius: 6, background: "var(--bg-sunken)" }}>
        {lang === "fr" ? "Chargement de la désagrégation héritée…" : "Loading inherited disaggregation…"}
      </div>
    );
  }
  if (!assigned || assigned.length === 0) {
    return (
      <div style={{
        padding: "8px 10px", fontSize: 11, color: "var(--text-faint)",
        borderRadius: 6, background: "var(--bg-sunken)",
        border: "1px dashed var(--line)",
      }}>
        {lang === "fr"
          ? "Aucune désagrégation configurée pour cet indicateur. Pour en ajouter, ouvrez « Définition des indicateurs » → modifier cette définition."
          : "No disaggregation configured for this indicator. To add one, open 'Indicator definitions' → edit this definition."}
      </div>
    );
  }

  return (
    <div style={{
      padding: "10px 12px", borderRadius: 6,
      background: "oklch(0.97 0.04 195)",
      border: "1px solid color-mix(in oklch, oklch(0.58 0.13 195) 30%, transparent)",
    }}>
      <div style={{ fontSize: 10.5, fontWeight: 700, color: "oklch(0.36 0.10 195)", marginBottom: 5, letterSpacing: "0.04em", textTransform: "uppercase" }}>
        {lang === "fr" ? "🎯 Désagrégation héritée du catalogue" : "🎯 Inherited disaggregation"}
      </div>
      <div style={{ display: "flex", gap: 6, flexWrap: "wrap" }}>
        {assigned.map((row, i) => {
          const label = lang === "fr" ? (row.axis && (row.axis.name_fr || row.axis.code)) : (row.axis && (row.axis.name_en || row.axis.name_fr || row.axis.code));
          return (
            <span key={i} style={{
              display: "inline-flex", alignItems: "center", gap: 3,
              fontSize: 11.5, fontWeight: 500,
              padding: "2px 9px", borderRadius: 999,
              background: "white", color: "oklch(0.36 0.10 195)",
              border: "1px solid oklch(0.75 0.10 195)",
            }}>
              {label}
              {row.is_required && <span style={{ fontSize: 9, opacity: 0.7 }}>*</span>}
            </span>
          );
        })}
      </div>
      <div style={{ fontSize: 10, color: "oklch(0.50 0.06 195)", marginTop: 5, fontStyle: "italic" }}>
        {lang === "fr"
          ? "Lors de la saisie d'une valeur, vous devrez compléter une grille croisant ces axes (numérateur / dénominateur par cellule)."
          : "When entering a value, you'll need to fill a grid crossing these axes (numerator / denominator per cell)."}
      </div>
    </div>
  );
}

// ==================== EDIT INDICATOR MODAL ====================
// Admin-only. Pre-fills the form from the current indicator state. The Code
// field is read-only — to renumber an indicator, delete and recreate it.
// Supports PEFA indicators (letter dropdowns for baseline / target).
function EditIndicatorModal({ lang, indicator, onClose, onSaved }) {
  const valueKind = (indicator && indicator.value_kind) || "numeric";
  const isPefa = valueKind === "score_pefa";
  const pefaLabels = (window.melr.scoreLabels && window.melr.scoreLabels("score_pefa")) || [];
  // Load defs once for the inherited-disaggregation info chip
  const { data: defs } = window.melr.useIndicatorDefinitions();

  const [nameFr, setNameFr]     = useStateI(indicator.name || "");
  const [nameEn, setNameEn]     = useStateI("");
  const [unit, setUnit]         = useStateI(indicator.unit || "");
  // level on the live row comes capitalised ("Outcome"); normalise back
  const initLevel = (indicator.level || "outcome").toLowerCase();
  const [level, setLevel]       = useStateI(initLevel);
  const [frequency, setFrequency] = useStateI("quarterly");
  const [baseline, setBaseline]         = useStateI(isPefa ? "" : (indicator.baseline != null ? String(indicator.baseline) : ""));
  const [baselineText, setBaselineText] = useStateI(isPefa ? (indicator.baselineText || "") : "");
  const [target, setTarget]             = useStateI(isPefa ? "" : (indicator.target != null ? String(indicator.target) : ""));
  const [targetText, setTargetText]     = useStateI(isPefa ? (indicator.targetText || "") : "");
  const [busy, setBusy] = useStateI(false);
  const [err, setErr]   = useStateI(null);

  const submit = async (e) => {
    e.preventDefault();
    setErr(null); setBusy(true);
    try {
      await window.melr.updateIndicator(indicator.uuid, {
        name_fr: nameFr.trim(),
        name_en: nameEn.trim() || null,
        unit:    unit.trim() || null,
        level,
        frequency,
        value_kind: valueKind,
        baseline:      isPefa ? null : (baseline === "" ? null : Number(baseline)),
        target:        isPefa ? null : (target   === "" ? null : Number(target)),
        baseline_text: isPefa ? (baselineText || null) : null,
        target_text:   isPefa ? (targetText   || null) : null,
      });
      if (onSaved) await onSaved();
      onClose();
    } catch (e2) { setErr(e2.message); }
    finally { setBusy(false); }
  };

  const inp = { padding: "8px 10px", borderRadius: 6, border: "1px solid var(--line)", fontSize: 13 };
  const inpRo = { ...inp, background: "var(--bg-sunken, #f3f4f6)", color: "var(--text-faint)" };
  const lbl = { fontSize: 11, opacity: 0.75 };

  return (
    <div onClick={onClose} style={{
      position: "fixed", inset: 0, background: "rgba(0,0,0,.45)", zIndex: 9999,
      display: "flex", alignItems: "center", justifyContent: "center",
    }}>
      <form onClick={(e) => e.stopPropagation()} onSubmit={submit} style={{
        background: "var(--bg, white)", color: "var(--text, #111)",
        padding: 22, borderRadius: 10, width: 560, maxWidth: "92vw",
        boxShadow: "0 10px 30px rgba(0,0,0,.25)", display: "grid", gap: 10,
      }}>
        <div>
          <div style={{ fontSize: 18, fontWeight: 600 }}>
            {lang === "fr" ? "Modifier l'indicateur" : "Edit indicator"}
          </div>
          <div className="text-faint" style={{ fontSize: 12, marginTop: 2 }}>
            <span className="tag-mono">{indicator.id}</span> · {indicator.project}
            {isPefa && <span style={{ marginLeft: 6 }}>· <span className="pill accent" style={{ fontSize: 10 }}>PEFA</span></span>}
          </div>
        </div>

        {/* Inherited disaggregation preview — same component as
            CreateIndicatorModal. Shows the axes that apply at data-entry
            time for this indicator, read-only here.
            E4: when the indicator is NOT linked to the catalogue, show
            a neutral explanation instead of hiding the section entirely. */}
        {indicator.definition_code ? (
          <InheritedDisaggregationInfo
            lang={lang}
            definitionCode={indicator.definition_code}
            defs={defs} />
        ) : (
          <div style={{
            padding: "8px 12px", borderRadius: 6, fontSize: 11.5,
            background: "var(--bg-sunken)", border: "1px dashed var(--line)",
            color: "var(--text-muted)",
          }}>
            {lang === "fr"
              ? <>Indicateur non lié au catalogue — <strong>aucune désagrégation héritée</strong>. Pour activer la désagrégation, supprimer cet indicateur et le recréer depuis le catalogue (champ « Code importé »).</>
              : <>Indicator not linked to the catalogue — <strong>no inherited disaggregation</strong>. To enable disaggregation, delete this indicator and recreate it from the catalogue (use the "Imported code" field).</>}
          </div>
        )}

        <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 10 }}>
          <label style={{ display: "grid", gap: 4 }}>
            <span style={lbl}>{lang === "fr" ? "Code" : "Code"}</span>
            <input value={indicator.id} readOnly style={inpRo}
              title={lang === "fr" ? "Le code n'est pas modifiable. Supprimer et recréer si besoin." : "Code can't be edited. Delete and recreate if needed."} />
          </label>
          <label style={{ display: "grid", gap: 4 }}>
            <span style={lbl}>{lang === "fr" ? "Niveau" : "Level"}</span>
            <select value={level} onChange={(e) => setLevel(e.target.value)} style={inp}>
              <option value="output">Output</option>
              <option value="outcome">Outcome</option>
              <option value="mixed">{lang === "fr" ? "Mixte (Output/Outcome)" : "Mixed (Output/Outcome)"}</option>
              <option value="impact">Impact</option>
              <option value="context">Context</option>
            </select>
          </label>
        </div>
        <label style={{ display: "grid", gap: 4 }}>
          <span style={lbl}>{lang === "fr" ? "Nom (FR)" : "Name (FR)"}</span>
          <input required value={nameFr} onChange={(e) => setNameFr(e.target.value)} style={inp} />
        </label>
        <label style={{ display: "grid", gap: 4 }}>
          <span style={lbl}>{lang === "fr" ? "Nom (EN — optionnel)" : "Name (EN — optional)"}</span>
          <input value={nameEn} onChange={(e) => setNameEn(e.target.value)} style={inp} />
        </label>
        <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr 1fr", gap: 10 }}>
          <label style={{ display: "grid", gap: 4 }}>
            <span style={lbl}>{lang === "fr" ? "Unité" : "Unit"}</span>
            <input value={unit} onChange={(e) => setUnit(e.target.value)} placeholder="%"
              disabled={isPefa} style={isPefa ? inpRo : inp} />
          </label>
          <label style={{ display: "grid", gap: 4 }}>
            <span style={lbl}>{lang === "fr" ? "Baseline" : "Baseline"}</span>
            {isPefa ? (
              <select value={baselineText} onChange={(e) => setBaselineText(e.target.value)} style={inp}>
                <option value="">—</option>
                {pefaLabels.map((l) => <option key={l} value={l}>{l}</option>)}
              </select>
            ) : (
              <input type="number" step="any" value={baseline} onChange={(e) => setBaseline(e.target.value)} style={inp} />
            )}
          </label>
          <label style={{ display: "grid", gap: 4 }}>
            <span style={lbl}>{lang === "fr" ? "Cible" : "Target"}</span>
            {isPefa ? (
              <select value={targetText} onChange={(e) => setTargetText(e.target.value)} style={inp}>
                <option value="">—</option>
                {pefaLabels.map((l) => <option key={l} value={l}>{l}</option>)}
              </select>
            ) : (
              <input type="number" step="any" value={target} onChange={(e) => setTarget(e.target.value)} style={inp} />
            )}
          </label>
          <label style={{ display: "grid", gap: 4 }}>
            <span style={lbl}>{lang === "fr" ? "Fréquence" : "Frequency"}</span>
            <select value={frequency} onChange={(e) => setFrequency(e.target.value)} style={inp}>
              <option value="monthly">{lang === "fr" ? "Mensuel" : "Monthly"}</option>
              <option value="quarterly">{lang === "fr" ? "Trimestriel" : "Quarterly"}</option>
              <option value="biannual">{lang === "fr" ? "Semestriel" : "Biannual"}</option>
              <option value="yearly">{lang === "fr" ? "Annuel" : "Yearly"}</option>
            </select>
          </label>
        </div>
        {err && <div style={{ color: "#b91c1c", fontSize: 12 }}>{err}</div>}
        <div style={{ display: "flex", gap: 8, justifyContent: "flex-end", marginTop: 4 }}>
          <button type="button" onClick={onClose} disabled={busy}
            style={{ padding: "8px 14px", borderRadius: 6, border: "1px solid var(--line)", background: "transparent", cursor: "pointer" }}>
            {lang === "fr" ? "Annuler" : "Cancel"}
          </button>
          <button type="submit" disabled={busy}
            style={{ padding: "8px 14px", borderRadius: 6, border: 0, background: "#2563eb", color: "white", cursor: "pointer", fontWeight: 600 }}>
            {busy ? "…" : (lang === "fr" ? "Enregistrer" : "Save")}
          </button>
        </div>
      </form>
    </div>
  );
}

// ==================== MULTI-SITE BATCH ENTRY MODAL ====================
// Pick one indicator, set a single period, then enter one value per site.
// Supports PEFA letter dropdowns when the indicator is score_pefa. The
// numeric companion is computed on save (see bulkInsertIndicatorValues).
function MultiSiteEntryModal({ lang, indicators, onClose, onSaved }) {
  // Default period: current quarter
  const today = new Date();
  const q = Math.floor(today.getMonth() / 3);
  const periodStartDefault = new Date(today.getFullYear(), q * 3, 1).toISOString().slice(0, 10);
  const periodEndDefault   = new Date(today.getFullYear(), q * 3 + 3, 0).toISOString().slice(0, 10);

  const [indicatorId, setIndicatorId] = useStateI(indicators[0] ? indicators[0].uuid : "");
  const [periodStart, setPeriodStart] = useStateI(periodStartDefault);
  const [periodEnd,   setPeriodEnd]   = useStateI(periodEndDefault);
  const [values, setValues]   = useStateI({}); // site_id → { value, value_text }
  const [busy, setBusy]   = useStateI(false);
  const [err, setErr]     = useStateI(null);
  const [done, setDone]   = useStateI(null);

  const currentIndicator = indicators.find((i) => i.uuid === indicatorId);
  const projectCode = currentIndicator && currentIndicator.project;
  const isPefa = currentIndicator && currentIndicator.value_kind === "score_pefa";
  const pefaLabels = (window.melr.scoreLabels && window.melr.scoreLabels("score_pefa")) || [];

  // Load sites for the indicator's project. Whenever the indicator changes,
  // pull the sites and reset the values map.
  const [sites, setSites] = useStateI([]);
  const [sitesLoading, setSitesLoading] = useStateI(false);
  React.useEffect(() => {
    if (!projectCode) { setSites([]); return; }
    let alive = true;
    setSitesLoading(true);
    (async () => {
      try {
        const sb = window.melr.supabase;
        if (!sb) return;
        const r = await sb.from("projects").select("id, code").eq("code", projectCode).maybeSingle();
        if (!alive || !r.data) { setSites([]); return; }
        const s = await sb.from("sites").select("id, code, name, kind, region").eq("project_id", r.data.id).order("code");
        if (!alive) return;
        setSites(s.data || []);
        setValues({}); // reset
      } catch (e) { console.warn("[multi-site] sites:", e.message || e); }
      finally { if (alive) setSitesLoading(false); }
    })();
    return () => { alive = false; };
  }, [projectCode]);

  const setRow = (siteId, patch) => {
    setValues((m) => ({ ...m, [siteId]: { ...(m[siteId] || {}), ...patch } }));
  };

  const submit = async (e) => {
    e.preventDefault();
    setErr(null); setDone(null); setBusy(true);
    try {
      const rows = sites.map((s) => {
        const v = values[s.id] || {};
        return {
          site_id:    s.id,
          value:      isPefa ? null : (v.value      || ""),
          value_text: isPefa ? (v.value_text || "") : null,
        };
      });
      const r = await window.melr.bulkInsertIndicatorValues(indicatorId,
        { period_start: periodStart, period_end: periodEnd, value_kind: currentIndicator.value_kind || "numeric" },
        rows
      );
      if (r.errors && r.errors.length > 0) throw new Error(r.errors[0].message);
      setDone({ inserted: r.inserted });
      if (onSaved) await onSaved();
    } catch (e2) { setErr(e2.message); }
    finally { setBusy(false); }
  };

  const inp = { padding: "6px 8px", borderRadius: 5, border: "1px solid var(--line)", fontSize: 12, background: "var(--bg, white)", color: "var(--text)", boxSizing: "border-box" };
  const lbl = { fontSize: 11, opacity: 0.75 };

  return (
    <div onClick={onClose} style={{
      position: "fixed", inset: 0, background: "rgba(0,0,0,.45)", zIndex: 9999,
      display: "flex", alignItems: "center", justifyContent: "center",
    }}>
      <form onClick={(e) => e.stopPropagation()} onSubmit={submit} style={{
        background: "var(--bg, white)", color: "var(--text, #111)",
        padding: 22, borderRadius: 10, width: 720, maxWidth: "94vw",
        maxHeight: "calc(100vh - 32px)", display: "flex", flexDirection: "column", gap: 10,
        boxShadow: "0 10px 30px rgba(0,0,0,.25)",
      }}>
        <div>
          <div style={{ fontSize: 18, fontWeight: 600 }}>
            {lang === "fr" ? "Saisie multi-sites" : "Multi-site entry"}
          </div>
          <div className="text-faint" style={{ fontSize: 12 }}>
            {lang === "fr"
              ? "Une valeur par site pour le même indicateur, sur la même période."
              : "One value per site for the same indicator, on the same period."}
          </div>
        </div>

        <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: 10 }}>
          <label style={{ display: "grid", gap: 4 }}>
            <span style={lbl}>{lang === "fr" ? "Indicateur" : "Indicator"}</span>
            <select value={indicatorId} onChange={(e) => setIndicatorId(e.target.value)} style={{ ...inp, padding: "8px 10px", fontSize: 13 }}>
              {indicators.map((i) => (
                <option key={i.uuid} value={i.uuid}>
                  {i.id} · {i.name}
                </option>
              ))}
            </select>
          </label>
          <label style={{ display: "grid", gap: 4 }}>
            <span style={lbl}>{lang === "fr" ? "Début de période" : "Period start"}</span>
            <input required type="date" value={periodStart} onChange={(e) => setPeriodStart(e.target.value)} style={{ ...inp, padding: "8px 10px", fontSize: 13 }} />
          </label>
          <label style={{ display: "grid", gap: 4 }}>
            <span style={lbl}>{lang === "fr" ? "Fin de période" : "Period end"}</span>
            <input required type="date" value={periodEnd} onChange={(e) => setPeriodEnd(e.target.value)} style={{ ...inp, padding: "8px 10px", fontSize: 13 }} />
          </label>
        </div>

        {isPefa && (
          <div style={{ padding: "6px 10px", background: "#eef2ff", color: "#3730a3", borderRadius: 5, fontSize: 11.5 }}>
            {lang === "fr"
              ? "📊 Indicateur PEFA — choisir un score par site dans la liste déroulante."
              : "📊 PEFA indicator — pick a score per site from the dropdown."}
          </div>
        )}

        {sitesLoading ? (
          <div className="text-faint" style={{ padding: 14, textAlign: "center" }}>{lang === "fr" ? "Chargement des sites…" : "Loading sites…"}</div>
        ) : sites.length === 0 ? (
          <div className="text-faint" style={{ padding: 14, textAlign: "center", border: "1px dashed var(--line)", borderRadius: 6 }}>
            {lang === "fr"
              ? "Aucun site rattaché à ce projet. Créez d'abord des sites depuis Baseline."
              : "No site for this project. Create sites from Baseline first."}
          </div>
        ) : (
          <div style={{ overflowY: "auto", flex: 1, border: "1px solid var(--line)", borderRadius: 6 }}>
            <table className="tbl" style={{ width: "100%" }}>
              <thead>
                <tr>
                  <th>{lang === "fr" ? "Site" : "Site"}</th>
                  <th>{lang === "fr" ? "Type" : "Kind"}</th>
                  <th>{lang === "fr" ? "Valeur" : "Value"}</th>
                </tr>
              </thead>
              <tbody>
                {sites.map((s) => (
                  <tr key={s.id}>
                    <td>
                      <div className="strong">{s.code} · {s.name}</div>
                      {s.region && <div className="text-faint" style={{ fontSize: 11 }}>{s.region}</div>}
                    </td>
                    <td className="text-faint">{s.kind || "—"}</td>
                    <td>
                      {isPefa ? (
                        <select value={(values[s.id] && values[s.id].value_text) || ""}
                          onChange={(e) => setRow(s.id, { value_text: e.target.value })}
                          style={{ ...inp, width: "100%" }}>
                          <option value="">{lang === "fr" ? "— Choisir —" : "— Pick —"}</option>
                          {pefaLabels.map((l) => <option key={l} value={l}>{l}</option>)}
                        </select>
                      ) : (
                        <input type="number" step="any"
                          value={(values[s.id] && values[s.id].value) || ""}
                          onChange={(e) => setRow(s.id, { value: e.target.value })}
                          style={{ ...inp, width: "100%" }} />
                      )}
                    </td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
        )}

        <div className="text-faint" style={{ fontSize: 11 }}>
          {lang === "fr"
            ? "Les lignes sans valeur sont ignorées. La conversion numérique (PEFA) est automatique."
            : "Empty rows are skipped. Numeric conversion (PEFA) is automatic."}
        </div>

        {done && (
          <div style={{ padding: "8px 12px", background: "#dcfce7", color: "#166534", borderRadius: 6, fontSize: 12.5 }}>
            ✓ {done.inserted} {lang === "fr" ? "valeur(s) enregistrée(s)" : "value(s) saved"}
          </div>
        )}
        {err && <div style={{ color: "#b91c1c", fontSize: 12 }}>{err}</div>}

        <div style={{ display: "flex", gap: 8, justifyContent: "flex-end" }}>
          <button type="button" onClick={onClose} disabled={busy}
            style={{ padding: "8px 14px", borderRadius: 6, border: "1px solid var(--line)", background: "transparent", cursor: "pointer" }}>
            {done ? (lang === "fr" ? "Fermer" : "Close") : (lang === "fr" ? "Annuler" : "Cancel")}
          </button>
          <button type="submit" disabled={busy || sites.length === 0}
            style={{ padding: "8px 14px", borderRadius: 6, border: 0, background: "#2563eb", color: "white", cursor: busy ? "wait" : "pointer", fontWeight: 600, opacity: sites.length === 0 ? 0.5 : 1 }}>
            {busy ? "…" : (lang === "fr" ? "Enregistrer toutes les valeurs" : "Save all values")}
          </button>
        </div>
      </form>
    </div>
  );
}

window.Indicators = Indicators;
// Expose so other screens (e.g. ProjectDetail → PDIndicators → "Ajouter")
// can mount the same modal without duplicating the form.
window.CreateIndicatorModal = CreateIndicatorModal;
