/* global React, Icon, Modal, NewSectorModal, sectorById, sectorLabel */
const { useState: useStateID, useEffect: useEffectID, useRef: useRefID } = React;

// ============================================================================
// INDICATOR DEFINITIONS — org-wide catalogue (admin only)
// ----------------------------------------------------------------------------
// Each row carries metadata for one indicator: code, bilingual name, origin
// institution (REFT, USAID, WHO…), level (output/outcome/impact/context) and
// an optional PIRS attachment (Performance Indicator Reference Sheet) — a
// Word / Excel / PDF file stored in the private indicator-pirs bucket.
//
// The Suivi des indicateurs module picks codes from this catalogue when
// creating a project-instance indicator.
// ============================================================================

const LEVELS = [
  { v: "output",  fr: "Output",                 en: "Output", es: "Output"                  },
  { v: "outcome", fr: "Outcome",                en: "Outcome", es: "Outcome"                 },
  // "Mixte" indicators span both Output and Outcome (common case in real
  // M&E frameworks). Logframe-aware views treat them as part of the core
  // logframe (output ∪ outcome ∪ impact ∪ mixed).
  { v: "mixed",   fr: "Mixte (Output/Outcome)", en: "Mixed (Output/Outcome)", es: "Mixto (Output/Outcome)"  },
  { v: "impact",  fr: "Impact",                 en: "Impact", es: "Impacto"                  },
  { v: "context", fr: "Context",                en: "Context", es: "Contexto"                 },
];
// Friendly label for any level slug. Falls back to the raw slug if a new
// value has been added in DB but not yet listed in LEVELS.
function levelLabel(slug, lang) {
  const L = LEVELS.find((l) => l.v === slug);
  if (!L) return slug || "";
  return (lang === "es" ? (L.es != null ? L.es : L.en) : lang === "fr" ? L.fr : L.en);
}

// ── Référentiel IRMF / FVC (GCF) ───────────────────────────────────────────
// Affiche les 8 indicateurs centraux du cadre intégré de gestion des résultats
// du Fonds vert pour le climat (window.IRMF_CATALOG) et permet d'importer les 4
// QUANTITATIFS comme définitions d'indicateurs de l'org. Les 4 « environnement
// favorable » sont des fiches de notation à 3 points, affichées en référence.
function IrmfReferenceCard({ lang, effectiveOrgId, canManage, onImported }) {
  const C = window.IRMF_CATALOG;
  const [open, setOpen] = useStateID(false);
  const [busy, setBusy] = useStateID(false);
  const [msg, setMsg]   = useStateID(null);
  if (!C) return null;
  const nm = (x) => (lang === "es" ? (x.es != null ? x.es : x.en) : lang === "fr" ? x.fr : x.en);
  const unitNm = (u) => { const un = C.units[u]; return un ? nm(un) : u; };
  const quant = C.coreIndicators.filter((x) => x.kind === "quant");
  const enabling = C.coreIndicators.filter((x) => x.kind === "enabling");
  const importQuant = async () => {
    setBusy(true); setMsg(null);
    let created = 0, skipped = 0;
    for (const ci of quant) {
      const un = C.units[ci.unit] || {};
      try {
        await window.melr.indicatorDefinitionsCrud.create({
          code: "IRMF-" + ci.code,
          name_fr: ci.fr + " (" + (un.fr || ci.unit) + ")",
          name_en: ci.en + " (" + (un.en || ci.unit) + ")",
          name_es: ci.es + " (" + (un.es || un.en || ci.unit) + ")",
          origin_institution: "GCF/FVC (IRMF)",
          level: "outcome",
          value_kind: "numeric",
          organization_id: effectiveOrgId,
          is_shared: false,
        });
        created++;
      } catch (e) { skipped++; }
    }
    setBusy(false);
    setMsg(window.L(
      created + " indicateur(s) créé(s), " + skipped + " ignoré(s) (déjà présents).",
      created + " indicator(s) created, " + skipped + " skipped (already present).",
      created + " indicador(es) creado(s), " + skipped + " omitido(s) (ya presentes)."));
    onImported && onImported();
  };
  return (
    <div className="card" style={{ marginBottom: 14 }}>
      <div className="card-head" style={{ cursor: "pointer" }} onClick={() => setOpen(!open)}>
        <div className="card-title">🌍 {nm(C.meta)} <span className="text-faint" style={{ fontWeight: 400, fontSize: 11 }}>· {C.meta.version}</span></div>
        <span className="text-faint" style={{ fontSize: 11.5, marginLeft: 8 }}>{open ? "▲" : "▼"} {window.L("8 indicateurs centraux", "8 core indicators", "8 indicadores centrales")}</span>
      </div>
      {open && (
        <div className="card-body" style={{ fontSize: 12 }}>
          <div style={{ fontWeight: 700, marginBottom: 4 }}>{window.L("Quantitatifs (CI-1 à 4)", "Quantitative (CI-1 to 4)", "Cuantitativos (CI-1 a 4)")}</div>
          <ul style={{ margin: "0 0 8px 18px" }}>
            {quant.map((ci) => <li key={ci.code}><b>{ci.code}</b> — {nm(ci)} <span className="text-faint">({unitNm(ci.unit)})</span></li>)}
          </ul>
          <div style={{ fontWeight: 700, marginBottom: 4 }}>{window.L("Environnement favorable (CI-5 à 8) — fiche de notation 3 points", "Enabling environment (CI-5 to 8) — 3-point scorecard", "Entorno favorable (CI-5 a 8) — ficha de puntuación de 3 puntos")}</div>
          <ul style={{ margin: "0 0 8px 18px" }}>
            {enabling.map((ci) => <li key={ci.code}><b>{ci.code}</b> — {nm(ci)}</li>)}
          </ul>
          <div className="text-faint" style={{ fontSize: 11, marginBottom: 8 }}>
            {window.L("Désagrégations MAF (B.42/13, §20 a) — a minima le sexe. Potentiel de changement de paradigme : échelle · réplicabilité · durabilité.",
              "MAF disaggregations (B.42/13, §20 a) — at least sex. Paradigm shift potential: scale · replicability · sustainability.",
              "Desagregaciones MAF (B.42/13, §20 a) — al menos el sexo. Potencial de cambio de paradigma: escala · replicabilidad · sostenibilidad.")}
          </div>
          {canManage && (
            <button className="btn sm primary" disabled={busy} onClick={importQuant}>
              {busy ? "…" : "📥 " + window.L("Importer les 4 indicateurs quantitatifs", "Import the 4 quantitative indicators", "Importar los 4 indicadores cuantitativos")}
            </button>
          )}
          {msg && <span className="text-faint" style={{ fontSize: 11.5, marginLeft: 8 }}>{msg}</span>}
        </div>
      )}
    </div>
  );
}

function IndicatorDefinitions({ t, lang, isSuperAdmin, actingOrgId, effectiveOrgId, myOrgId, isAdmin, hasPerm }) {
  // Phase 5 RLS (performance-rls-permissive-consolidation-phase5-indicators.sql)
  // makes indicator_definitions write-restricted to admin + super-admin. UI
  // mirrors that: hide the write buttons (Nouvelle définition + Importer)
  // from users without users.manage. canManage is true for super-admin,
  // is_admin_user (=users.manage), or anyone whose hasPerm says so.
  const canManage = !!isSuperAdmin || !!isAdmin
    || (hasPerm && hasPerm("users.manage"));
  const { has, loading: permsLoading } = window.melr.useCurrentUserPermissions();
  const { data: rawDefs, loading, realtime, refresh } = window.melr.useIndicatorDefinitions();
  // Live sectors for the bulk "Affecter à un secteur" picker.
  const { data: liveSectors }   = window.melr.useSectors();
  // Super-admin acting-as-org: scope the catalogue to the target org so the
  // page reflects what that org's admin would see — BUT keep shared (cross-
  // org) definitions visible because they're universal reference data that
  // every org sees by design. The CRUD modal (below) also propagates
  // effectiveOrgId on create so new private definitions land in the right
  // org; shared definitions are super-admin-only and bypass org scoping.
  const defs = (isSuperAdmin && actingOrgId && actingOrgId !== myOrgId)
    ? (rawDefs || []).filter((d) => d.is_shared || d.organization_id === actingOrgId)
    : (rawDefs || []);
  const LiveBadge = window.melr.LiveBadge;
  const [editing, setEditing]   = useStateID(null);   // "new" | row.id
  const [filter, setFilter]     = useStateID("all");  // level filter
  const [importOpen, setImportOpen] = useStateID(false);
  const [busy, setBusy]         = useStateID(null);
  // Bulk-selection state: a Set of row ids. The toolbar appears as soon as
  // it's non-empty. Selection is cleared after a successful bulk action.
  const [selectedIds, setSelectedIds] = useStateID(() => new Set());
  const [bulkBusy, setBulkBusy]       = useStateID(false);
  const [bulkSectorId, setBulkSectorId] = useStateID("");
  const [bulkToast, setBulkToast]     = useStateID(null);
  // Bulk-delete is a separate code-path from bulkUpdate because it's
  // destructive and needs an explicit confirmation. The Set captures
  // the ids to delete at the moment the user opens the modal — so even
  // if they keep clicking around, the count shown stays accurate.
  const [bulkDeleteConfirm, setBulkDeleteConfirm] = useStateID(null);
  // Column sort: { key: 'sector' | 'origin' | 'level', dir: 'asc' | 'desc' }.
  // null = no sort (preserve the catalogue's natural "by code" ordering).
  // Clicking the same column header cycles: none → asc → desc → none.
  const [sort, setSort] = useStateID(null);
  // Filtres par colonne (Code → Niveau) : texte « contient » pour Code /
  // Nom FR / Nom EN / Origine, sélection exacte pour Secteur / Niveau.
  const [colFilters, setColFilters] = useStateID({ code: "", nameFr: "", nameEn: "", sector: "", origin: "", level: "" });
  const setColFilter = (k, v) => setColFilters((prev) => ({ ...prev, [k]: v }));
  const hasColFilters = Object.values(colFilters).some((v) => v !== "");
  const clearColFilters = () => setColFilters({ code: "", nameFr: "", nameEn: "", sector: "", origin: "", level: "" });
  const cycleSort = (key) => {
    setSort((prev) => {
      if (!prev || prev.key !== key) return { key, dir: "asc" };
      if (prev.dir === "asc")        return { key, dir: "desc" };
      return null;
    });
  };
  const sortArrow = (key) => {
    if (!sort || sort.key !== key) return "↕";
    return sort.dir === "asc" ? "↑" : "↓";
  };

  // Bulk-selection helpers and auto-cleanup. When the level filter changes
  // or rows are deleted/moved out of scope, we prune the selection so the
  // toolbar count stays accurate.
  const toggleRow = (id) => {
    setSelectedIds((prev) => {
      const next = new Set(prev);
      if (next.has(id)) next.delete(id); else next.add(id);
      return next;
    });
  };
  const clearSelection = () => setSelectedIds(new Set());
  const showBulkToast = (ok, msg, ttl) => {
    setBulkToast({ ok, msg });
    setTimeout(() => setBulkToast(null), ttl || 5000);
  };
  const runBulk = async (patch, label) => {
    const ids = Array.from(selectedIds);
    if (ids.length === 0) return;
    setBulkBusy(true);
    try {
      const res = await window.melr.indicatorDefinitionsCrud.bulkUpdate(ids, patch);
      const rejected = res.attempted - res.updated;
      const msg = window.L(
        `✓ ${res.updated} indicateur${res.updated > 1 ? "s" : ""} ${label.fr}` +
          (rejected > 0
            ? ` · ${rejected} ${rejected > 1 ? "lignes rejetées" : "ligne rejetée"} (permission insuffisante)`
            : ""),
        `✓ ${res.updated} indicator${res.updated > 1 ? "s" : ""} ${label.en}` +
          (rejected > 0
            ? ` · ${rejected} ${rejected > 1 ? "rows rejected" : "row rejected"} (insufficient permission)`
            : ""),
        `✓ ${res.updated} indicador${res.updated > 1 ? "es" : ""} ${label.es || label.en}` +
          (rejected > 0
            ? ` · ${rejected} ${rejected > 1 ? "filas rechazadas" : "fila rechazada"} (permiso insuficiente)`
            : ""));
      showBulkToast(true, msg, rejected > 0 ? 7500 : 4500);
      clearSelection();
      await refresh();
    } catch (e) {
      showBulkToast(false, (window.L("Erreur : ", "Error: ", "Error: ")) + e.message, 7500);
    } finally {
      setBulkBusy(false);
    }
  };

  // Bulk delete · super-admin only. Opens a confirmation modal first,
  // then on confirm calls bulkRemove and reports the outcome.
  const runBulkDelete = async () => {
    if (!bulkDeleteConfirm || bulkDeleteConfirm.ids.length === 0) return;
    const ids = bulkDeleteConfirm.ids;
    setBulkBusy(true);
    setBulkDeleteConfirm(null);
    try {
      const res = await window.melr.indicatorDefinitionsCrud.bulkRemove(ids);
      const rejected = res.attempted - res.deleted;
      const msg = window.L(
        `✓ ${res.deleted} indicateur${res.deleted > 1 ? "s" : ""} supprimé${res.deleted > 1 ? "s" : ""}` +
          (rejected > 0 ? ` · ${rejected} ${rejected > 1 ? "lignes rejetées" : "ligne rejetée"} (permission insuffisante)` : ""),
        `✓ ${res.deleted} indicator${res.deleted > 1 ? "s" : ""} deleted` +
          (rejected > 0 ? ` · ${rejected} ${rejected > 1 ? "rows rejected" : "row rejected"} (insufficient permission)` : ""),
        `✓ ${res.deleted} indicador${res.deleted > 1 ? "es" : ""} eliminado${res.deleted > 1 ? "s" : ""}` +
          (rejected > 0 ? ` · ${rejected} ${rejected > 1 ? "filas rechazadas" : "fila rechazada"} (permiso insuficiente)` : ""));
      showBulkToast(true, msg, rejected > 0 ? 7500 : 4500);
      clearSelection();
      await refresh();
    } catch (e) {
      showBulkToast(false, (window.L("Erreur suppression : ", "Delete error: ", "Error al eliminar: ")) + e.message, 7500);
    } finally {
      setBulkBusy(false);
    }
  };

  // Attachment counts per definition — one cheap query when the list of
  // definitions changes so the table can show a "🗎 N" badge per row.
  const [attachmentCounts, setAttachmentCounts] = useStateID({});
  useEffectID(() => {
    let alive = true;
    (async () => {
      const sb = window.melr.supabase;
      if (!sb) return;
      const r = await sb.from("indicator_definition_attachments").select("definition_id");
      if (!alive || r.error) return;
      const counts = {};
      (r.data || []).forEach((row) => { counts[row.definition_id] = (counts[row.definition_id] || 0) + 1; });
      setAttachmentCounts(counts);
    })();
    return () => { alive = false; };
  }, [defs]);

  // Prune the selection when a row leaves the visible set (deleted, level
  // filter changed, acting-as-org switched, etc.) so the toolbar count
  // stays in sync with what the user actually sees.
  useEffectID(() => {
    if (selectedIds.size === 0) return;
    const visibleIds = new Set(defs.map((d) => d.id));
    let mutated = false;
    const next = new Set();
    selectedIds.forEach((id) => { if (visibleIds.has(id)) next.add(id); else mutated = true; });
    if (mutated) setSelectedIds(next);
  }, [defs]);

  if (permsLoading) return <div className="page"><div style={{ padding: 28 }}>{window.L("Chargement…", "Loading…", "Cargando…")}</div></div>;
  if (!has("users.manage")) {
    return (
      <div className="page">
        <div className="page-header">
          <div className="page-eyebrow">{window.L("INDICATEURS", "INDICATORS", "INDICADORES")}</div>
          <h1 className="page-title">{window.L("Définition des indicateurs", "Indicator definitions", "Definición de indicadores")}</h1>
        </div>
        <div className="card" style={{ marginTop: 16, padding: 24 }}>
          <div className="text-faint" style={{ textAlign: "center" }}>
            {window.L("Accès réservé aux administrateurs (permission users.manage).", "Admins only (users.manage permission required).", "Acceso reservado a administradores (permiso users.manage).")}
          </div>
        </div>
      </div>
    );
  }

  const onDelete = async (row) => {
    if (!window.confirm(window.L(
      "Supprimer la définition « " + row.code + " » ? Les indicateurs projet qui s'y rattachent ne seront pas supprimés, juste détachés.",
      "Delete definition '" + row.code + "'? Project indicators linked to it stay; only the link is dropped.",
      "¿Eliminar la definición « " + row.code + " »? Los indicadores de proyecto vinculados no se eliminan, solo se desvinculan."))) return;
    setBusy(row.id);
    try { await window.melr.indicatorDefinitionsCrud.remove(row.id); }
    catch (e) { window.alert((window.L("Erreur : ", "Error: ", "Error: ")) + e.message); }
    finally { setBusy(null); }
  };

  const byChip = filter === "all" ? defs : defs.filter((d) => d.level === filter);
  // Filtres par colonne, appliqués après le filtre « niveau » des chips.
  const norm = (s) => (s == null ? "" : String(s)).toLowerCase();
  const filtered = !hasColFilters ? byChip : byChip.filter((d) => {
    if (colFilters.code   && !norm(d.code).includes(norm(colFilters.code)))                 return false;
    if (colFilters.nameFr && !norm(d.name_fr).includes(norm(colFilters.nameFr)))            return false;
    if (colFilters.nameEn && !norm(d.name_en).includes(norm(colFilters.nameEn)))            return false;
    if (colFilters.sector && (d.sector_id || "") !== colFilters.sector)                     return false;
    if (colFilters.origin && !norm(d.origin_institution).includes(norm(colFilters.origin))) return false;
    if (colFilters.level  && (d.level || "") !== colFilters.level)                          return false;
    return true;
  });

  // Apply the active column sort, if any. We use locale-aware comparison so
  // accented characters (Éducation, Énergie, …) sort the way a French
  // speaker would expect. Empty values always fall to the BOTTOM regardless
  // of direction so the user can keep "missing data" rows out of the way.
  const sortKeyValue = (d, key) => {
    if (key === "sector") {
      if (!d.sector_id) return "";
      const s = (typeof sectorById === "function") ? sectorById(d.sector_id) : null;
      return (s && s.fr) ? s.fr : d.sector_id;
    }
    if (key === "origin") return d.origin_institution || "";
    if (key === "level")  return d.level || "";
    return "";
  };
  const visible = !sort ? filtered : filtered.slice().sort((a, b) => {
    const va = sortKeyValue(a, sort.key);
    const vb = sortKeyValue(b, sort.key);
    const aEmpty = !va, bEmpty = !vb;
    if (aEmpty && bEmpty) return 0;
    if (aEmpty) return 1;          // empties always last
    if (bEmpty) return -1;
    const cmp = va.localeCompare(vb, window.L("fr", "en", "es"), { sensitivity: "base" });
    return sort.dir === "asc" ? cmp : -cmp;
  });

  // Select-all state for the header checkbox: "all" / "some" / "none".
  // We compare against the CURRENTLY VISIBLE rows so toggling the level
  // filter doesn't sweep up hidden rows accidentally.
  const visibleIds = visible.map((d) => d.id);
  const selectedCount = visibleIds.filter((id) => selectedIds.has(id)).length;
  const allVisibleSelected = visibleIds.length > 0 && selectedCount === visibleIds.length;
  const someVisibleSelected = selectedCount > 0 && !allVisibleSelected;
  const toggleAllVisible = () => {
    setSelectedIds((prev) => {
      const next = new Set(prev);
      if (allVisibleSelected) {
        // Deselect everything currently visible (keep selections from a
        // different filter, if any — unlikely but a defensive choice).
        visibleIds.forEach((id) => next.delete(id));
      } else {
        visibleIds.forEach((id) => next.add(id));
      }
      return next;
    });
  };

  return (
    <div className="page">
      <div className="page-header">
        <div className="page-eyebrow">{window.L("INDICATEURS · CATALOGUE", "INDICATORS · CATALOGUE", "INDICADORES · CATÁLOGO")}</div>
        <div className="page-header-row">
          <div>
            <h1 className="page-title">
              {window.L("Définition des indicateurs", "Indicator definitions", "Definición de indicadores")}{" "}
              <LiveBadge on={realtime} lang={lang} />
            </h1>
            <div className="page-sub">
              {window.L("Catalogue organisationnel des indicateurs (code, nom, origine, niveau, fiche PIRS). Les agents les sélectionnent depuis « Suivi des indicateurs ».", "Org-wide indicator catalogue (code, name, origin, level, PIRS sheet). Agents pick from these in 'Indicator tracking'.", "Catálogo organizacional de indicadores (código, nombre, origen, nivel, ficha PIRS). Los agentes los seleccionan desde « Seguimiento de indicadores ».")}
            </div>
          </div>
          <div className="page-header-actions">
            {/* Modeles Excel — dropdown a cote d'Importer pour telecharger
                un template pre-formate (PEFA, USAID, CNUCED, generique). */}
            {canManage && window.TemplatesDropdown && (
              <window.TemplatesDropdown lang={lang} />
            )}
            {canManage && (
              <button onClick={() => setImportOpen(true)}
                title={window.L("Importer un classeur Excel d'indicateurs", "Import indicator workbook", "Importar un libro Excel de indicadores")}
                style={{ padding: "6px 12px", borderRadius: 6, border: "1px solid #b45309", background: "#b45309", color: "white", cursor: "pointer", fontWeight: 600, fontSize: 12.5 }}>
                📥 {window.L("Importer Excel", "Import Excel", "Importar Excel")}
              </button>
            )}
            <button onClick={() => exportDefinitionsCsv(defs, lang)}
              title={window.L("Exporter le catalogue en CSV (ouvrable dans Excel)", "Export catalogue as CSV (opens in Excel)", "Exportar el catálogo en CSV (abrible en Excel)")}
              style={{ padding: "6px 12px", borderRadius: 6, border: "1px solid #16a34a", background: "#16a34a", color: "white", cursor: "pointer", fontWeight: 600, fontSize: 12.5 }}>
              📊 {window.L("Exporter CSV", "Export CSV", "Exportar CSV")}
            </button>
            {canManage && (
              <button className="btn sm primary" onClick={() => setEditing("new")}>
                <Icon.plus /> {window.L("Nouvelle définition", "New definition", "Nueva definición")}
              </button>
            )}
          </div>
        </div>
        <div className="page-tabs">
          {[
            { k: "all",     l: window.L("Tous", "All", "Todos"),  n: defs.length },
            { k: "output",  l: "Output",                                 n: defs.filter((d) => d.level === "output").length },
            { k: "outcome", l: "Outcome",                                n: defs.filter((d) => d.level === "outcome").length },
            { k: "mixed",   l: window.L("Mixte", "Mixed", "Mixto"),        n: defs.filter((d) => d.level === "mixed").length },
            { k: "impact",  l: "Impact",                                 n: defs.filter((d) => d.level === "impact").length },
            { k: "context", l: "Context",                                n: defs.filter((d) => d.level === "context").length },
          ].map((tab) => (
            <div key={tab.k} className={"tab" + (filter === tab.k ? " active" : "")} onClick={() => setFilter(tab.k)}
              style={{ cursor: "pointer" }}>
              {tab.l} <span className="tag-mono" style={{ marginLeft: 4 }}>{tab.n}</span>
            </div>
          ))}
        </div>
      </div>

      {/* E4 · Politique de désagrégation org-wide — collapsible card.
          Visible à tous (lecture), modifiable admin/super-admin. */}
      <DisaggregationPolicyCard lang={lang} effectiveOrgId={effectiveOrgId} canManage={canManage} />

      {/* Référentiel IRMF / FVC (GCF) — 8 indicateurs centraux, import des 4 quantitatifs. */}
      <IrmfReferenceCard lang={lang} effectiveOrgId={effectiveOrgId} canManage={canManage} onImported={refresh} />

      {/* Bulk-action toolbar — visible whenever ≥1 row is selected.
          Sticky to the top of the scrolling page container so the actions
          stay reachable while the user scrolls through the catalogue to
          tick more rows. z-index 20 sits above .page-header (z-index 4)
          so when both are stuck at top:0 the toolbar covers the header;
          well below modals (9999+). The translucent background + backdrop
          blur softens the transition so the toolbar feels like it's
          floating over the page-header rather than abruptly hiding it. */}
      {selectedIds.size > 0 && (
        <div style={{
          position: "sticky", top: 0, zIndex: 20,
          background: "rgba(238, 242, 255, 0.96)",
          border: "1px solid #c7d2fe", borderRadius: 6,
          padding: "10px 14px", marginTop: 12, marginBottom: 4,
          display: "flex", alignItems: "center", flexWrap: "wrap", gap: 10,
          boxShadow: "0 6px 16px rgba(49, 46, 129, 0.12), 0 2px 4px rgba(49, 46, 129, 0.08)",
          backdropFilter: "saturate(180%) blur(10px)",
          WebkitBackdropFilter: "saturate(180%) blur(10px)",
        }}>
          <div style={{ fontSize: 13, fontWeight: 600, color: "#3730a3" }}>
            {selectedIds.size}{" "}
            {window.L(
              (selectedIds.size > 1 ? "indicateurs sélectionnés" : "indicateur sélectionné"),
              (selectedIds.size > 1 ? "indicators selected" : "indicator selected"),
              (selectedIds.size > 1 ? "indicadores seleccionados" : "indicador seleccionado"))}
          </div>
          <span style={{ color: "#6b7280", fontSize: 12 }}>·</span>
          {/* Sector bulk-apply */}
          <div style={{ display: "flex", alignItems: "center", gap: 6 }}>
            <span style={{ fontSize: 11.5, color: "#374151" }}>
              {window.L("Affecter au secteur :", "Assign sector:", "Asignar al sector:")}
            </span>
            <select value={bulkSectorId} onChange={(e) => setBulkSectorId(e.target.value)}
              disabled={bulkBusy}
              style={{ padding: "5px 8px", borderRadius: 5, border: "1px solid var(--line)", fontSize: 12, background: "white", color: "var(--text)" }}>
              <option value="">{window.L("— Choisir —", "— Pick —", "— Elegir —")}</option>
              <option value="__CLEAR__">{window.L("(retirer le secteur)", "(clear sector)", "(quitar el sector)")}</option>
              {(liveSectors || []).map((s) => (
                <option key={s.id} value={s.id}>{lang === "en" ? s.en : s.fr}</option>
              ))}
            </select>
            <button className="btn xs primary" disabled={bulkBusy || !bulkSectorId}
              onClick={() => runBulk(
                { sector_id: bulkSectorId === "__CLEAR__" ? null : bulkSectorId },
                {
                  fr: bulkSectorId === "__CLEAR__"
                    ? "détaché(s) du secteur"
                    : "affecté(s) au secteur",
                  en: bulkSectorId === "__CLEAR__"
                    ? "detached from sector"
                    : "assigned to sector",
                }
              )}>
              {bulkBusy ? "…" : (window.L("Appliquer", "Apply", "Aplicar"))}
            </button>
          </div>
          {/* Share / unshare — super-admin only (RLS enforces it server-side too) */}
          {isSuperAdmin && (
            <>
              <span style={{ color: "#6b7280", fontSize: 12 }}>·</span>
              <button className="btn xs" disabled={bulkBusy}
                title={window.L("Marquer les indicateurs sélectionnés comme partagés entre toutes les organisations", "Mark the selected indicators as shared across all organisations", "Marcar los indicadores seleccionados como compartidos entre todas las organizaciones")}
                onClick={() => runBulk(
                  { is_shared: true },
                  { fr: "rendu(s) partagé(s)", en: "made shared", es: "entrega(s) compartida(s)" }
                )}>
                🌐 {window.L("Rendre partagés", "Make shared", "Hacer compartidos")}
              </button>
              <button className="btn xs ghost" disabled={bulkBusy}
                title={window.L("Retirer le partage : les indicateurs redeviendront privés à leur organisation", "Remove sharing: indicators become private to their organisation again", "Quitar el compartido: los indicadores vuelven a ser privados de su organización")}
                onClick={() => runBulk(
                  { is_shared: false },
                  { fr: "rendu(s) privé(s)", en: "made private", es: "entrega(s) privada(s)" }
                )}>
                🔒 {window.L("Rendre privés", "Make private", "Hacer privados")}
              </button>
            </>
          )}
          <div style={{ flex: 1 }} />
          {/* Bulk delete · super-admin only. Always sits at the far right
              of the toolbar, just before "Désélectionner", styled red so
              it stands out among the safer bulk actions. RLS also
              enforces super-admin server-side as a defence in depth. */}
          {isSuperAdmin && (
            <button
              className="btn xs"
              disabled={bulkBusy}
              onClick={() => setBulkDeleteConfirm({ ids: Array.from(selectedIds) })}
              style={{
                background: "#fee2e2",
                color: "#991b1b",
                border: "1px solid #fca5a5",
                fontWeight: 600,
              }}
              title={window.L("Supprimer les définitions sélectionnées (action destructive)", "Delete the selected definitions (destructive action)", "Eliminar las definiciones seleccionadas (acción destructiva)")}>
              🗑 {window.L(
                `Supprimer ${selectedIds.size} indicateur${selectedIds.size > 1 ? "s" : ""}`,
                `Delete ${selectedIds.size} indicator${selectedIds.size > 1 ? "s" : ""}`,
                `Eliminar ${selectedIds.size} indicador${selectedIds.size > 1 ? "es" : ""}`)}
            </button>
          )}
          <button className="btn xs ghost" onClick={clearSelection} disabled={bulkBusy}>
            {window.L("Désélectionner", "Clear", "Deseleccionar")}
          </button>
        </div>
      )}
      {bulkToast && (
        <div style={{
          marginTop: 8, padding: "8px 12px", borderRadius: 6, fontSize: 12.5,
          background: bulkToast.ok ? "#dcfce7" : "#fee2e2",
          color:      bulkToast.ok ? "#166534" : "#991b1b",
        }}>{bulkToast.msg}</div>
      )}

      <div className="card">
        {/* Make THIS card-body the scroll container (instead of letting the
            page scroll), so the sticky table headers actually stick : a
            sticky element latches onto its nearest scrollable ancestor.
            `.card { overflow: hidden }` would otherwise trap the sticky
            against an invisible non-scrolling parent. max-height keeps the
            list within the viewport while preserving the page-level layout
            (toolbar, filters, bulk bar stay where they are). */}
        <div className="card-body flush" style={{
          overflowX: "auto",
          overflowY: "auto",
          maxHeight: "calc(100vh - 240px)",
        }}>
          {loading ? (
            <div className="text-faint" style={{ padding: 24, textAlign: "center" }}>{window.L("Chargement…", "Loading…", "Cargando…")}</div>
          ) : (visible.length === 0 && !hasColFilters) ? (
            <div className="text-faint" style={{ padding: 24, textAlign: "center" }}>
              {window.L("Aucune définition pour le moment. Cliquez « Nouvelle définition » ou importez un Excel.", "No definition yet. Click 'New definition' or import an Excel file.", "Ninguna definición por ahora. Haga clic en « Nueva definición » o importe un Excel.")}
            </div>
          ) : (
            <table className="tbl tbl-zebra" style={{ minWidth: "100%" }}>
              <thead>
                <tr>
                  <th style={{ width: 32 }}>
                    <input type="checkbox"
                      checked={allVisibleSelected}
                      ref={(el) => { if (el) el.indeterminate = someVisibleSelected; }}
                      onChange={toggleAllVisible}
                      title={window.L("Tout sélectionner / désélectionner", "Select / deselect all", "Seleccionar / deseleccionar todo")}
                      style={{ cursor: "pointer" }} />
                  </th>
                  <th>{window.L("Code", "Code", "Código")}</th>
                  <th>{window.L("Nom (FR)", "Name (FR)", "Nombre (FR)")}</th>
                  <th>{window.L("Nom (EN)", "Name (EN)", "Nombre (EN)")}</th>
                  <th onClick={() => cycleSort("sector")}
                      style={{ cursor: "pointer", userSelect: "none" }}
                      title={window.L("Trier par secteur (clic pour cycler)", "Sort by sector (click to cycle)", "Ordenar por sector (clic para alternar)")}>
                    {window.L("Secteur", "Sector", "Sector")}{" "}
                    <span style={{
                      fontSize: 10, marginLeft: 2,
                      opacity: sort && sort.key === "sector" ? 1 : 0.35,
                      color: sort && sort.key === "sector" ? "var(--accent, #6366f1)" : "inherit",
                    }}>{sortArrow("sector")}</span>
                  </th>
                  <th onClick={() => cycleSort("origin")}
                      style={{ cursor: "pointer", userSelect: "none" }}
                      title={window.L("Trier par origine (clic pour cycler)", "Sort by origin (click to cycle)", "Ordenar por origen (clic para alternar)")}>
                    {window.L("Origine", "Origin", "Origen")}{" "}
                    <span style={{
                      fontSize: 10, marginLeft: 2,
                      opacity: sort && sort.key === "origin" ? 1 : 0.35,
                      color: sort && sort.key === "origin" ? "var(--accent, #6366f1)" : "inherit",
                    }}>{sortArrow("origin")}</span>
                  </th>
                  <th onClick={() => cycleSort("level")}
                      style={{ cursor: "pointer", userSelect: "none" }}
                      title={window.L("Trier par niveau (clic pour cycler)", "Sort by level (click to cycle)", "Ordenar por nivel (clic para alternar)")}>
                    {window.L("Niveau", "Level", "Nivel")}{" "}
                    <span style={{
                      fontSize: 10, marginLeft: 2,
                      opacity: sort && sort.key === "level" ? 1 : 0.35,
                      color: sort && sort.key === "level" ? "var(--accent, #6366f1)" : "inherit",
                    }}>{sortArrow("level")}</span>
                  </th>
                  <th>{window.L("PIRS", "PIRS", "PIRS")}</th>
                  {/* Actions column — sticky to the right edge so the
                      "Modifier" + "Supprimer" buttons stay visible even
                      if the table needs horizontal scrolling on narrow
                      screens. min-width prevents the column from being
                      squashed by long Name FR / Name EN columns. */}
                  <th style={{
                    position: "sticky", right: 0, zIndex: 2,
                    minWidth: 140,
                    background: "var(--bg-sunken)",
                    boxShadow: "-4px 0 6px -4px rgba(0,0,0,0.08)",
                  }}>{window.L("Actions", "Actions", "Acciones")}</th>
                </tr>
                {/* Ligne de filtres par colonne (Code → Niveau). Texte =
                    « contient », Secteur / Niveau = sélection exacte. */}
                <tr>
                  <th style={{ padding: "4px 6px" }}></th>
                  <th style={{ padding: "4px 6px" }}>
                    <input className="input" value={colFilters.code}
                      onChange={(e) => setColFilter("code", e.target.value)}
                      placeholder={window.L("Filtrer…", "Filter…", "Filtrar…")}
                      style={{ width: "100%", minWidth: 70, fontSize: 11, padding: "3px 6px" }} />
                  </th>
                  <th style={{ padding: "4px 6px" }}>
                    <input className="input" value={colFilters.nameFr}
                      onChange={(e) => setColFilter("nameFr", e.target.value)}
                      placeholder={window.L("Filtrer…", "Filter…", "Filtrar…")}
                      style={{ width: "100%", minWidth: 90, fontSize: 11, padding: "3px 6px" }} />
                  </th>
                  <th style={{ padding: "4px 6px" }}>
                    <input className="input" value={colFilters.nameEn}
                      onChange={(e) => setColFilter("nameEn", e.target.value)}
                      placeholder={window.L("Filtrer…", "Filter…", "Filtrar…")}
                      style={{ width: "100%", minWidth: 90, fontSize: 11, padding: "3px 6px" }} />
                  </th>
                  <th style={{ padding: "4px 6px" }}>
                    <select className="input" value={colFilters.sector}
                      onChange={(e) => setColFilter("sector", e.target.value)}
                      style={{ width: "100%", minWidth: 90, fontSize: 11, padding: "3px 4px" }}>
                      <option value="">{window.L("Tous", "All", "Todos")}</option>
                      {(liveSectors || []).map((s) => (
                        <option key={s.id} value={s.id}>{(lang === "es" ? (s.es != null ? s.es : s.en || s.fr) : lang === "fr" ? s.fr : s.en || s.fr)}</option>
                      ))}
                    </select>
                  </th>
                  <th style={{ padding: "4px 6px" }}>
                    <input className="input" value={colFilters.origin}
                      onChange={(e) => setColFilter("origin", e.target.value)}
                      placeholder={window.L("Filtrer…", "Filter…", "Filtrar…")}
                      style={{ width: "100%", minWidth: 70, fontSize: 11, padding: "3px 6px" }} />
                  </th>
                  <th style={{ padding: "4px 6px" }}>
                    <select className="input" value={colFilters.level}
                      onChange={(e) => setColFilter("level", e.target.value)}
                      style={{ width: "100%", minWidth: 80, fontSize: 11, padding: "3px 4px" }}>
                      <option value="">{window.L("Tous", "All", "Todos")}</option>
                      {["output", "outcome", "mixed", "impact", "context"].map((l) => (
                        <option key={l} value={l}>{levelLabel(l, lang)}</option>
                      ))}
                    </select>
                  </th>
                  <th style={{ padding: "4px 6px" }}></th>
                  <th style={{
                    position: "sticky", right: 0, zIndex: 2,
                    background: "var(--bg-sunken)",
                    boxShadow: "-4px 0 6px -4px rgba(0,0,0,0.08)",
                    padding: "4px 6px", textAlign: "right",
                  }}>
                    {hasColFilters && (
                      <button className="btn xs ghost" onClick={clearColFilters}
                        title={window.L("Effacer les filtres de colonnes", "Clear column filters", "Borrar los filtros de columnas")}>
                        ✕ {window.L("Effacer", "Clear", "Borrar")}
                      </button>
                    )}
                  </th>
                </tr>
              </thead>
              <tbody>
                {visible.length === 0 && (
                  <tr><td colSpan={9} className="text-faint" style={{ padding: 18, textAlign: "center" }}>
                    {window.L("Aucun résultat avec ces filtres.", "No result with these filters.", "Ningún resultado con estos filtros.")}
                  </td></tr>
                )}
                {visible.map((d) => {
                  const attCount = attachmentCounts[d.id] || 0;
                  return (
                  <tr key={d.id} style={selectedIds.has(d.id) ? { background: "var(--bg-sunken)" } : null}>
                    <td>
                      <input type="checkbox"
                        checked={selectedIds.has(d.id)}
                        onChange={() => toggleRow(d.id)}
                        style={{ cursor: "pointer" }} />
                    </td>
                    <td className="mono">
                      <div className="row gap-xs" style={{ alignItems: "center" }}>
                        <span>{d.code}</span>
                        {d.is_shared && (
                          <span className="pill" style={{ fontSize: 9.5, background: "#dbeafe", color: "#1e3a8a", padding: "1px 6px" }}
                                title={window.L("Indicateur partagé · visible par toutes les organisations · modifiable uniquement par un super-administrateur", "Shared indicator · visible to every organisation · editable by super-admins only", "Indicador compartido · visible por todas las organizaciones · editable solo por un superadministrador")}>
                            🌐 {window.L("Partagé", "Shared", "Compartido")}
                          </span>
                        )}
                      </div>
                    </td>
                    <td>{d.name_fr}</td>
                    <td className="text-faint">{d.name_en || "—"}</td>
                    <td>
                      {d.sector_id ? (() => {
                        const s = (typeof sectorById === "function") ? sectorById(d.sector_id) : null;
                        if (!s) return <span className="text-faint">{d.sector_id}</span>;
                        return (
                          <span className="sector-chip"
                            style={{ background: s.bg, color: s.color, borderColor: s.color, fontSize: 10.5 }}>
                            {lang === "en" ? s.en : s.fr}
                          </span>
                        );
                      })() : <span className="text-faint">—</span>}
                    </td>
                    <td className="text-faint">{d.origin_institution || "—"}</td>
                    <td>{d.level
                      ? <span className="pill" style={{ fontSize: 10.5 }}
                              title={d.level === "mixed" ? (window.L("Niveau couvrant Output et Outcome", "Spans Output and Outcome", "Nivel que abarca Producto y Resultado")) : undefined}>
                          {levelLabel(d.level, lang)}
                        </span>
                      : <span className="text-faint">—</span>}</td>
                    <td>
                      {attCount > 0 ? (
                        <button className="pill green dot" style={{ fontSize: 10.5, cursor: "pointer", border: 0 }}
                          onClick={() => setEditing(d.id)}
                          title={window.L("Voir les fichiers PIRS", "View PIRS files", "Ver los archivos PIRS")}>
                          🗎 {attCount}
                        </button>
                      ) : (
                        <span className="text-faint" style={{ fontSize: 11 }}>—</span>
                      )}
                    </td>
                    {/* Actions cell · sticky right (matches the th above).
                        White background so the buttons stay readable
                        when scrolling under the previous columns. */}
                    <td style={{
                      position: "sticky", right: 0,
                      background: selectedIds.has(d.id) ? "var(--bg-sunken)" : "var(--bg-elev, white)",
                      boxShadow: "-4px 0 6px -4px rgba(0,0,0,0.08)",
                      minWidth: 140,
                    }}>
                      <div className="row gap-xs" style={{ justifyContent: "flex-end" }}>
                        <button className="btn sm primary" onClick={() => setEditing(d.id)}
                          style={{ fontSize: 11.5, padding: "3px 10px" }}>
                          <Icon.edit /> {window.L("Modifier", "Edit", "Editar")}
                        </button>
                        <button className="btn xs ghost" onClick={() => onDelete(d)} disabled={busy === d.id}
                          title={window.L("Supprimer", "Delete", "Eliminar")}>
                          <Icon.x />
                        </button>
                      </div>
                    </td>
                  </tr>
                  );
                })}
              </tbody>
            </table>
          )}
        </div>
      </div>

      {editing && (
        <DefinitionEditor
          lang={lang}
          existing={editing === "new" ? null : defs.find((d) => d.id === editing)}
          onClose={() => setEditing(null)}
          onSaved={refresh}
          targetOrgId={effectiveOrgId}
          isSuperAdmin={isSuperAdmin}
        />
      )}
      {importOpen && (
        <ImportDefinitionsModal lang={lang} onClose={() => { setImportOpen(false); refresh(); }} />
      )}

      {/* Bulk-delete confirmation modal · super-admin only. Captures the
          count at the moment the user clicked, so changing selection
          later doesn't shift the message. */}
      {bulkDeleteConfirm && (
        <BulkDeleteConfirmModal
          lang={lang}
          count={bulkDeleteConfirm.ids.length}
          onCancel={() => setBulkDeleteConfirm(null)}
          onConfirm={runBulkDelete}
        />
      )}
    </div>
  );
}

// ── Bulk-delete confirmation modal (super-admin only) ───────────────────────
// Reused from the existing Modal component when available, falls back to a
// minimal centred panel if Modal is not registered globally.
function BulkDeleteConfirmModal({ lang, count, onCancel, onConfirm }) {
  const Modal = window.melr && window.melr.Modal;
  const body = (
    <div style={{ padding: "16px 18px" }}>
      <div style={{ fontSize: 14, lineHeight: 1.55, color: "#334155", marginBottom: 14 }}>
        {window.L(
          <>
            Vous êtes sur le point de supprimer <strong style={{ color: "#991b1b" }}>{count} définition{count > 1 ? "s" : ""}</strong> d'indicateurs.
            <br /><br />
            Les indicateurs projet déjà rattachés à ces définitions <strong>resteront en place</strong> — seul le lien vers la définition sera retiré. Les fichiers PIRS associés seront aussi supprimés du stockage.
            <br /><br />
            Cette action est <strong>irréversible</strong>. Confirmer ?
          </>,
          <>
            You are about to delete <strong style={{ color: "#991b1b" }}>{count} indicator definition{count > 1 ? "s" : ""}</strong>.
            <br /><br />
            Project indicators already linked to these definitions <strong>will stay</strong> — only the link to the definition is removed. PIRS files attached will also be removed from storage.
            <br /><br />
            This action is <strong>irreversible</strong>. Confirm?
          </>,
          <>
            Está a punto de eliminar <strong style={{ color: "#991b1b" }}>{count} definición{count > 1 ? "es" : ""}</strong> de indicadores.
            <br /><br />
            Los indicadores de proyecto ya vinculados a estas definiciones <strong>permanecerán</strong> — solo se quitará el vínculo a la definición. Los archivos PIRS asociados también se eliminarán del almacenamiento.
            <br /><br />
            Esta acción es <strong>irreversible</strong>. ¿Confirmar?
          </>)}
      </div>
      <div style={{ display: "flex", gap: 8, justifyContent: "flex-end" }}>
        <button className="btn ghost" onClick={onCancel}>
          {window.L("Annuler", "Cancel", "Cancelar")}
        </button>
        <button
          className="btn"
          onClick={onConfirm}
          style={{ background: "#dc2626", color: "white", border: "1px solid #b91c1c", fontWeight: 600 }}>
          🗑 {window.L(`Supprimer ${count} indicateur${count > 1 ? "s" : ""}`, `Delete ${count} indicator${count > 1 ? "s" : ""}`, `Eliminar ${count} indicador${count > 1 ? "es" : ""}`)}
        </button>
      </div>
    </div>
  );
  // Modal component path — same convention as the rest of the app.
  if (Modal) {
    return (
      <Modal
        open
        onClose={onCancel}
        title={window.L("Confirmer la suppression", "Confirm deletion", "Confirmar la eliminación")}
        width={520}>
        {body}
      </Modal>
    );
  }
  // Fallback if Modal isn't loaded yet (shouldn't happen in practice).
  return (
    <div style={{ position: "fixed", inset: 0, background: "rgba(0,0,0,.45)", zIndex: 9999, display: "flex", alignItems: "center", justifyContent: "center", padding: 16 }}>
      <div style={{ background: "white", borderRadius: 10, width: 520, maxWidth: "100%", boxShadow: "0 12px 32px rgba(0,0,0,.25)" }}>
        <div style={{ padding: "12px 18px", borderBottom: "1px solid #e5e7eb", fontWeight: 600 }}>
          {window.L("Confirmer la suppression", "Confirm deletion", "Confirmar la eliminación")}
        </div>
        {body}
      </div>
    </div>
  );
}

// ── PIRS link: signs a fresh URL on click, opens in a new tab ───────────────
function PirsLink({ path, filename, lang }) {
  const [busy, setBusy] = useStateID(false);
  const onOpen = async (e) => {
    e.preventDefault();
    if (busy) return;
    setBusy(true);
    try {
      const url = await window.melr.getIndicatorPirsUrl(path, 3600);
      if (url) window.open(url, "_blank", "noopener,noreferrer");
    } catch (err) {
      window.alert((window.L("Erreur : ", "Error: ", "Error: ")) + err.message);
    } finally { setBusy(false); }
  };
  return (
    <a href="#" onClick={onOpen} className="row" style={{ gap: 4, fontSize: 12, alignItems: "center", color: "var(--accent, #2563eb)" }}>
      <Icon.fileText className="sm" />
      <span style={{ maxWidth: 160, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{filename}</span>
      {busy && <span className="text-faint">…</span>}
    </a>
  );
}

// ── Definition editor (create + edit) ───────────────────────────────────────
function DefinitionEditor({ lang, existing, onClose, onSaved, targetOrgId, isSuperAdmin }) {
  const isNew = !existing;
  // Once an existing-id is in hand (either we opened an existing one OR we
  // just created one), the attachments section becomes available. We need
  // a stateful currentId so the attachments stay visible after a fresh save.
  const [currentId, setCurrentId] = useStateID(existing ? existing.id : null);
  const [code, setCode]         = useStateID(existing ? existing.code : "");
  const [nameFr, setNameFr]     = useStateID(existing ? existing.name_fr : "");
  const [nameEn, setNameEn]     = useStateID(existing ? (existing.name_en || "") : "");
  const [nameEs, setNameEs]     = useStateID(existing ? (existing.name_es || "") : "");
  const [origin, setOrigin]     = useStateID(existing ? (existing.origin_institution || "") : "");
  const [level, setLevel]       = useStateID(existing ? (existing.level || "") : "");
  const [valueKind, setValueKind] = useStateID(existing ? (existing.value_kind || "numeric") : "numeric");
  // PEFA components — array of {code, name_fr, name_en}. Only meaningful
  // when valueKind === 'score_pefa'. Stored in indicator_definitions.components
  // (jsonb). Auto-suggestion of codes (PI-X.1, PI-X.2…) based on the parent
  // code is computed at render time so renaming the code stays consistent.
  const [components, setComponents] = useStateID(() => {
    if (existing && Array.isArray(existing.components)) return existing.components;
    return [];
  });
  // Shared flag: only a super-admin can flip this (RLS enforces it server-
  // side too). Rendered as a checkbox visible to super-admins only.
  const [isShared, setIsShared] = useStateID(existing ? !!existing.is_shared : false);
  // Sector link — optional. Sourced from the DB-backed sectors catalogue
  // (useSectors / sectorsCrud). "+ Nouveau secteur" opens the inline modal.
  const [sectorId, setSectorId] = useStateID(existing ? (existing.sector_id || "") : "");
  const { data: liveSectors }   = window.melr.useSectors();
  const [newSectorOpen, setNewSectorOpen] = useStateID(false);
  const [busy, setBusy]         = useStateID(false);
  const [err, setErr]           = useStateID(null);

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

  const onSubmit = async (e) => {
    e.preventDefault();
    if (!code.trim()) { setErr(window.L("Code requis.", "Code required.", "Código requerido.")); return; }
    if (!nameFr.trim()) { setErr(window.L("Nom (FR) requis.", "Name (FR) required.", "Nombre (FR) requerido.")); return; }
    setBusy(true); setErr(null);
    try {
      const payload = {
        code:               code.trim(),
        name_fr:            nameFr.trim(),
        name_en:            nameEn.trim() || null,
        name_es:            nameEs.trim() || null,
        origin_institution: origin.trim() || null,
        level:              level || null,
        value_kind:         valueKind || "numeric",
        sector_id:          sectorId || null,
        // Only persist components for PEFA indicators — clear the array if
        // the type was switched back to numeric so we don't leave orphan
        // sub-components hanging around in the DB.
        components:         valueKind === "score_pefa"
          ? components.filter((c) => c && c.code && c.code.trim())
          : [],
      };
      // Only forward is_shared when the caller is a super-admin. Server-
      // side RLS would reject the flag anyway, but masking it client-side
      // keeps the payload clean for normal admins.
      if (isSuperAdmin) payload.is_shared = !!isShared;
      let saved;
      if (!currentId) {
        // Super-admin acting-as-org: route new PRIVATE definitions to the
        // target org. SHARED definitions don't need a target org (visible
        // to all) but we still record the creator's org for attribution.
        if (targetOrgId) payload.organization_id = targetOrgId;
        saved = await window.melr.indicatorDefinitionsCrud.create(payload);
      } else            saved = await window.melr.indicatorDefinitionsCrud.update(currentId, payload);
      // Remember the id so the user can stay in this modal and add PIRS
      // attachments without closing and reopening.
      setCurrentId(saved.id);
      if (onSaved) await onSaved();
      // Don't close — let the user add PIRS attachments after the row exists.
      // The "Fermer" button (modal-foot) closes the modal explicitly.
    } catch (e2) { setErr(e2.message); }
    finally { setBusy(false); }
  };

  const inp = { width: "100%", padding: "8px 10px", borderRadius: 6, border: "1px solid var(--line)", fontSize: 13, background: "var(--bg, white)", color: "var(--text)", boxSizing: "border-box", fontFamily: "inherit" };
  const lbl = { display: "block", fontSize: 11, color: "var(--text-faint)", marginBottom: 4, textTransform: "uppercase", letterSpacing: "0.04em" };

  return (
    <Modal
      size="lg"
      title={!currentId
        ? (window.L("Nouvelle définition", "New definition", "Nueva definición"))
        : (window.L("Modifier : " + code, "Edit: " + code, "Editar: " + code))}
      onClose={onClose}
      onSubmit={onSubmit}
      footer={<>
        <button type="button" className="btn sm ghost" onClick={onClose} disabled={busy}>
          {window.L("Fermer", "Close", "Cerrar")}
        </button>
        <button type="submit" className="btn sm primary" disabled={busy}>
          {busy ? "…" : (!currentId ? (window.L("Créer", "Create", "Crear")) : (window.L("Enregistrer", "Save", "Guardar")))}
        </button>
      </>}>
      <div style={{ display: "grid", gap: 12 }}>
          <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 10 }}>
            <div>
              <label style={lbl}>{window.L("Code *", "Code *", "Código *")}</label>
              <input style={inp} value={code} onChange={(e) => setCode(e.target.value)}
                placeholder="REFT-CPN-01" maxLength={64} required />
            </div>
            <div>
              <label style={lbl}>{window.L("Niveau", "Level", "Nivel")}</label>
              <select style={inp} value={level} onChange={(e) => setLevel(e.target.value)}>
                <option value="">{window.L("— Non renseigné —", "— Not set —", "— Sin especificar —")}</option>
                {LEVELS.map((l) => (
                  <option key={l.v} value={l.v}>{(lang === "es" ? (l.es != null ? l.es : l.en) : lang === "fr" ? l.fr : l.en)}</option>
                ))}
              </select>
            </div>
          </div>
          <div>
            <label style={lbl}>{window.L("Nom (FR) *", "Name (FR) *", "Nombre (FR) *")}</label>
            <input style={inp} value={nameFr} onChange={(e) => setNameFr(e.target.value)}
              placeholder={window.L("Couverture vaccinale Penta-3", "Penta-3 immunization coverage", "Cobertura de vacunación Penta-3")} required />
          </div>
          <div>
            <label style={lbl}>{window.L("Nom (EN)", "Name (EN)", "Nombre (EN)")}</label>
            <input style={inp} value={nameEn} onChange={(e) => setNameEn(e.target.value)}
              placeholder="Penta-3 immunization coverage" />
          </div>
          <div>
            <label style={lbl}>{window.L("Nom (ES)", "Name (ES)", "Nombre (ES)")}</label>
            <input style={inp} value={nameEs} onChange={(e) => setNameEs(e.target.value)}
              placeholder="Cobertura de vacunación Penta-3" />
          </div>
          <div>
            <label style={lbl}>{window.L("Secteur", "Sector", "Sector")}</label>
            <select style={inp} value={sectorId} onChange={(e) => onSectorChange(e.target.value)}>
              <option value="">{window.L("— Aucun secteur —", "— No sector —", "— Ningún sector —")}</option>
              {(liveSectors || []).map((s) => (
                <option key={s.id} value={s.id}>{lang === "en" ? s.en : s.fr}</option>
              ))}
              <option value="__NEW__">{window.L("+ Nouveau secteur…", "+ New sector…", "+ Nuevo sector…")}</option>
            </select>
            <div className="text-faint" style={{ fontSize: 10.5, marginTop: 4 }}>
              {window.L("Associer l'indicateur à un secteur facilite le filtrage et le regroupement dans les tableaux de bord.", "Linking the indicator to a sector helps filtering and grouping in dashboards.", "Vincular el indicador a un sector facilita el filtrado y la agrupación en los paneles.")}
            </div>
          </div>
          <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 10 }}>
            <div>
              <label style={lbl}>{window.L("Origine (Institution)", "Origin (Institution)", "Origen (Institución)")}</label>
              <input style={inp} value={origin} onChange={(e) => setOrigin(e.target.value)}
                placeholder={window.L("REFT Africa · OMS · USAID · …", "REFT Africa · WHO · USAID · …", "REFT Africa · OMS · USAID · …")} />
            </div>
            <div>
              <label style={lbl}>{window.L("Type de mesure", "Measurement type", "Tipo de medición")}</label>
              <select style={inp} value={valueKind} onChange={(e) => setValueKind(e.target.value)}>
                <option value="numeric">{window.L("Numérique (chiffres bruts)", "Numeric (raw figures)", "Numérico (cifras brutas)")}</option>
                <option value="score_pefa">{window.L("Score PEFA (A · B+ · B · C+ · C · D+ · D)", "PEFA score (A · B+ · B · C+ · C · D+ · D)", "Puntuación PEFA (A · B+ · B · C+ · C · D+ · D)")}</option>
              </select>
              <div className="text-faint" style={{ fontSize: 10.5, marginTop: 4 }}>
                {valueKind === "score_pefa"
                  ? (window.L("Conversion auto : A=4, B+=3.5, B=3, C+=2.5, C=2, D+=1.5, D=1, NR=non noté", "Auto-conversion: A=4, B+=3.5, B=3, C+=2.5, C=2, D+=1.5, D=1, NR=not rated", "Conversión automática: A=4, B+=3.5, B=3, C+=2.5, C=2, D+=1.5, D=1, NR=no calificado"))
                  : (window.L("Saisie numérique standard (baseline, cible, valeurs trimestrielles…)", "Standard numeric capture (baseline, target, periodic values…)", "Captura numérica estándar (línea de base, meta, valores trimestrales…)"))}
              </div>
            </div>
          </div>

          {/* PEFA components — visible only for score_pefa indicators.
              An indicator like PI-27 may have 1 to 4 sub-components (PI-27.1
              … PI-27.4), each scored separately A/B+/B/…/D/NR. The final
              indicator score stays manually entered (NO auto-averaging —
              PEFA framework is qualitative). */}
          {valueKind === "score_pefa" && (
            <PefaComponentsSection
              lang={lang}
              parentCode={code}
              components={components}
              setComponents={setComponents} />
          )}

          {/* Shared-catalogue flag — super-admin only */}
          {isSuperAdmin ? (
            <div style={{ padding: "10px 12px", background: "var(--bg-sunken)", borderRadius: 6, border: "1px solid var(--line)" }}>
              <label style={{ display: "flex", gap: 10, alignItems: "flex-start", cursor: "pointer" }}>
                <input type="checkbox" checked={isShared} onChange={(e) => setIsShared(e.target.checked)}
                  style={{ marginTop: 3 }} />
                <div>
                  <div style={{ fontSize: 13, fontWeight: 600 }}>
                    🌐 {window.L("Indicateur partagé entre toutes les organisations", "Shared across all organisations", "Indicador compartido entre todas las organizaciones")}
                  </div>
                  <div className="text-faint" style={{ fontSize: 11, marginTop: 2 }}>
                    {window.L("Cocher pour les indicateurs standards (CNUCED, ODD, PEFA, REFT…) qui doivent apparaître dans le catalogue de toutes les organisations. Seul un super-administrateur peut créer ou modifier ces indicateurs.", "Tick for standard indicators (UNCTAD, SDG, PEFA, REFT…) that should appear in every organisation's catalogue. Only super-administrators can create or edit them.", "Marque para los indicadores estándar (UNCTAD, ODS, PEFA, REFT…) que deben aparecer en el catálogo de todas las organizaciones. Solo un superadministrador puede crearlos o editarlos.")}
                  </div>
                </div>
              </label>
            </div>
          ) : existing && existing.is_shared && (
            <div style={{ padding: "8px 10px", background: "#dbeafe", color: "#1e3a8a", borderRadius: 6, fontSize: 12 }}>
              🌐 {window.L("Cet indicateur est partagé entre toutes les organisations. Seul un super-administrateur peut le modifier.", "This indicator is shared across all organisations. Only a super-administrator can edit it.", "Este indicador se comparte entre todas las organizaciones. Solo un superadministrador puede editarlo.")}
            </div>
          )}

          {err && (
            <div style={{ padding: "8px 10px", background: "#fee2e2", color: "#991b1b", borderRadius: 6, fontSize: 12.5 }}>
              {err}
            </div>
          )}

          {/* PIRS attachments — visible once the definition exists */}
          <div style={{ borderTop: "1px solid var(--line)", paddingTop: 12, marginTop: 4 }}>
            <div style={{ display: "flex", alignItems: "center", marginBottom: 8 }}>
              <div style={{ fontSize: 13, fontWeight: 600 }}>
                {window.L("Fiches PIRS (multilingues)", "PIRS sheets (multilingual)", "Fichas PIRS (multilingües)")}
              </div>
              <div style={{ flex: 1 }} />
              {!currentId && (
                <span className="text-faint" style={{ fontSize: 11 }}>
                  {window.L("Enregistrez d'abord la définition pour pouvoir attacher des fichiers.", "Save the definition first to attach files.", "Guarde primero la definición para poder adjuntar archivos.")}
                </span>
              )}
            </div>
            {currentId ? (
              <PirsAttachmentsSection definitionId={currentId} lang={lang} />
            ) : (
              <div className="text-faint" style={{ fontSize: 11.5, padding: 10, border: "1px dashed var(--line)", borderRadius: 6, textAlign: "center" }}>
                {window.L("Pas encore de fichiers — cliquer « Créer » d'abord, puis attacher les fiches PIRS dans la langue de votre choix.", "No files yet — click 'Create' first, then attach PIRS sheets in any language.", "Aún no hay archivos — haga clic en « Crear » primero, luego adjunte las fichas PIRS en el idioma que prefiera.")}
              </div>
            )}
          </div>

          {/* Désagrégation requise — Phase C-1
              Le super-admin et l'admin org cochent les axes selon lesquels
              cet indicateur devra être désagrégé à la saisie. La saisie de
              la grille (Phase D) lit cette config pour générer toutes les
              combinaisons de cellules à remplir. */}
          <div style={{ borderTop: "1px solid var(--line)", paddingTop: 12, marginTop: 4 }}>
            <div style={{ display: "flex", alignItems: "center", marginBottom: 8 }}>
              <div style={{ fontSize: 13, fontWeight: 600 }}>
                {window.L("Désagrégation requise", "Required disaggregation", "Desagregación requerida")}
              </div>
              <div style={{ flex: 1 }} />
              {!currentId && (
                <span className="text-faint" style={{ fontSize: 11 }}>
                  {window.L("Enregistrez d'abord la définition pour configurer les axes.", "Save the definition first to configure axes.", "Guarde primero la definición para configurar los ejes.")}
                </span>
              )}
            </div>
            {currentId ? (
              <DisaggregationAxesSection definitionId={currentId} lang={lang} />
            ) : (
              <div className="text-faint" style={{ fontSize: 11.5, padding: 10, border: "1px dashed var(--line)", borderRadius: 6, textAlign: "center" }}>
                {window.L("Pas encore d'axes — cliquer « Créer » d'abord, puis cocher les axes selon lesquels cet indicateur sera désagrégé.", "No axes yet — click 'Create' first, then tick the axes this indicator should be disaggregated along.", "Aún no hay ejes — haga clic en « Crear » primero, luego marque los ejes según los cuales se desagregará este indicador.")}
              </div>
            )}
          </div>

          {/* E6 · Historique des versions — visible si la définition existe.
              Permet aux auditeurs / bailleurs de retracer "qu'est-ce que
              cet indicateur était à telle date ?". */}
          {currentId && <VersionHistorySection definitionId={currentId} lang={lang} />}
        </div>
      {newSectorOpen && (
        <NewSectorModal lang={lang}
          onClose={() => setNewSectorOpen(false)}
          onCreated={(sec) => { setSectorId(sec.id); setNewSectorOpen(false); }} />
      )}
    </Modal>
  );
}

// ── Multi-PIRS attachments section ──────────────────────────────────────────
// Lists current attachments and offers an inline "+ Ajouter PIRS" form with
// language selector, optional label, and a file picker. Each row can be
// opened (signed URL) or removed.
function PirsAttachmentsSection({ definitionId, lang }) {
  const { data: attachments, loading, refresh } = window.melr.useDefinitionAttachments(definitionId);
  const [busy, setBusy]   = useStateID(null);
  const [err, setErr]     = useStateID(null);

  // Inline-add form state
  const [addLang, setAddLang]   = useStateID("");      // "" = neutral
  const [addLabel, setAddLabel] = useStateID("");
  const addFileRef = useRefID(null);
  const [uploadBusy, setUploadBusy] = useStateID(false);

  const LANGS = [
    { v: "",   l: window.L("— Langue neutre —", "— Language-neutral —", "— Idioma neutro —") },
    { v: "fr", l: "Français (FR)" },
    { v: "en", l: "English (EN)" },
    { v: "es", l: "Español (ES)" },
    { v: "pt", l: "Português (PT)" },
    { v: "ar", l: "العربية (AR)" },
  ];

  const onAdd = async (e) => {
    e.preventDefault();
    const f = addFileRef.current && addFileRef.current.files && addFileRef.current.files[0];
    if (!f) { setErr(window.L("Choisir un fichier.", "Pick a file.", "Elija un archivo.")); return; }
    if (!/\.(pdf|docx?|xlsx?|ods|odt)$/i.test(f.name)) {
      setErr(window.L("Formats acceptés : PDF, Word (.doc, .docx), Excel (.xls, .xlsx).", "Accepted formats: PDF, Word (.doc, .docx), Excel (.xls, .xlsx).", "Formatos aceptados: PDF, Word (.doc, .docx), Excel (.xls, .xlsx)."));
      return;
    }
    if (f.size > 10 * 1024 * 1024) {
      setErr(window.L("Taille max 10 Mo.", "Max size 10 MB.", "Tamaño máx. 10 MB."));
      return;
    }
    setErr(null); setUploadBusy(true);
    try {
      await window.melr.addDefinitionAttachment(definitionId, f, {
        language: addLang || null,
        label:    addLabel.trim() || null,
      });
      setAddLabel("");
      setAddLang("");
      if (addFileRef.current) addFileRef.current.value = "";
      await refresh();
    } catch (e2) { setErr(e2.message); }
    finally { setUploadBusy(false); }
  };

  const onRemove = async (a) => {
    if (!window.confirm(window.L(
      "Retirer le fichier « " + (a.filename || a.path) + " » ?",
      "Remove file '" + (a.filename || a.path) + "'?",
      "¿Quitar el archivo « " + (a.filename || a.path) + " »?"))) return;
    setBusy(a.id);
    try { await window.melr.removeDefinitionAttachment(a.id); await refresh(); }
    catch (e2) { setErr(e2.message); }
    finally { setBusy(null); }
  };

  const inp = { padding: "6px 8px", borderRadius: 5, border: "1px solid var(--line)", fontSize: 12, background: "var(--bg, white)", color: "var(--text)", boxSizing: "border-box", fontFamily: "inherit" };
  const langLabel = (code) => {
    const m = LANGS.find((l) => l.v === code);
    return m ? m.l : (code || "—");
  };

  return (
    <div>
      {loading ? (
        <div className="text-faint" style={{ padding: 8, fontSize: 12 }}>{window.L("Chargement…", "Loading…", "Cargando…")}</div>
      ) : attachments.length === 0 ? (
        <div className="text-faint" style={{ padding: 8, fontSize: 12, fontStyle: "italic" }}>
          {window.L("Aucun fichier pour le moment.", "No file yet.", "Ningún archivo por ahora.")}
        </div>
      ) : (
        <div style={{ display: "flex", flexDirection: "column", gap: 4, marginBottom: 10 }}>
          {attachments.map((a) => (
            <div key={a.id} style={{
              display: "grid", gridTemplateColumns: "90px 1fr auto auto", gap: 8, alignItems: "center",
              padding: "6px 10px", border: "1px solid var(--line)", borderRadius: 5, background: "var(--bg-elev, white)",
            }}>
              <span className="pill" style={{ fontSize: 10, padding: "2px 6px" }}>
                {a.language ? a.language.toUpperCase() : (window.L("neutre", "neutral", "neutro"))}
              </span>
              <div style={{ minWidth: 0 }}>
                <PirsLink path={a.path} filename={a.filename || "PIRS"} lang={lang} />
                {a.label && (
                  <div className="text-faint" style={{ fontSize: 10.5, marginTop: 2 }}>{a.label}</div>
                )}
              </div>
              <span className="text-faint mono" style={{ fontSize: 10.5 }}>
                {a.size_bytes ? Math.max(1, Math.round(a.size_bytes / 1024)) + " KB" : ""}
              </span>
              <button type="button" className="btn xs ghost" onClick={() => onRemove(a)} disabled={busy === a.id}
                title={window.L("Retirer", "Remove", "Quitar")}>
                <Icon.x />
              </button>
            </div>
          ))}
        </div>
      )}

      {/* Inline "+ Add" subform */}
      <div style={{ padding: 10, background: "var(--bg-sunken, #f9fafb)", borderRadius: 6 }}>
        <div className="text-faint" style={{ fontSize: 10.5, textTransform: "uppercase", letterSpacing: "0.04em", marginBottom: 6 }}>
          {window.L("Ajouter une fiche PIRS", "Add a PIRS file", "Agregar una ficha PIRS")}
        </div>
        <div style={{ display: "grid", gridTemplateColumns: "140px 1fr auto", gap: 8, alignItems: "center" }}>
          <select value={addLang} onChange={(e) => setAddLang(e.target.value)} style={inp}>
            {LANGS.map((l) => <option key={l.v || "neutral"} value={l.v}>{l.l}</option>)}
          </select>
          <input value={addLabel} onChange={(e) => setAddLabel(e.target.value)}
            placeholder={window.L("Libellé (optionnel) — ex: PIRS v2", "Label (optional) — e.g. PIRS v2", "Etiqueta (opcional) — ej.: PIRS v2")}
            style={inp} maxLength={120} />
          <span></span>
          <span style={{ gridColumn: "1 / -1" }}>
            <input ref={addFileRef} type="file" accept=".pdf,.doc,.docx,.xls,.xlsx,.ods,.odt"
              style={{ padding: 4, fontSize: 12, width: "100%" }} />
          </span>
        </div>
        <div className="text-faint" style={{ fontSize: 10.5, marginTop: 6 }}>
          {window.L("PDF, Word ou Excel · 10 Mo max.", "PDF, Word or Excel · 10 MB max.", "PDF, Word o Excel · 10 MB máx.")}
        </div>
        {err && (
          <div style={{ padding: "6px 10px", background: "#fee2e2", color: "#991b1b", borderRadius: 5, fontSize: 12, marginTop: 8 }}>
            {err}
          </div>
        )}
        <div style={{ display: "flex", justifyContent: "flex-end", marginTop: 8 }}>
          <button type="button" className="btn sm" onClick={onAdd} disabled={uploadBusy}>
            <Icon.upload /> {uploadBusy ? "…" : (window.L("Ajouter", "Add", "Agregar"))}
          </button>
        </div>
      </div>
    </div>
  );
}

// ── PEFA components section (E1) ──────────────────────────────────────────
// Only renders when value_kind = 'score_pefa'. A PEFA indicator like PI-27
// has 1 to 4 sub-components (PI-27.1 … PI-27.4), each scored separately
// A/B+/B/C+/C/D+/D/NR. This section lets the user declare those components
// with auto-suggested codes (based on parent code) and bilingual names.
//
// The final indicator score (the parent's overall rating) stays MANUALLY
// entered — there is no auto-averaging because the PEFA framework is
// qualitative and component-to-parent rules are codified in the PEFA
// handbook, not a simple arithmetic mean.
function PefaComponentsSection({ lang, parentCode, components, setComponents }) {
  // Cap at 4 — PEFA framework rarely has more. Adjust if real-world data
  // needs change.
  const MAX_COMPONENTS = 4;
  // Sanitise parentCode for the auto-suggested child codes. Strip any
  // trailing ".N" so PI-27.1 doesn't become PI-27.1.1 when the user edits.
  const baseCode = (parentCode || "").replace(/\.[0-9]+$/, "").trim();

  const addComponent = () => {
    if (components.length >= MAX_COMPONENTS) return;
    const nextIdx = components.length + 1;
    const suggested = baseCode ? `${baseCode}.${nextIdx}` : "";
    setComponents([...components, { code: suggested, name_fr: "", name_en: "" }]);
  };
  const removeComponent = (i) => {
    setComponents(components.filter((_, idx) => idx !== i));
  };
  const updateComponent = (i, patch) => {
    setComponents(components.map((c, idx) => idx === i ? { ...c, ...patch } : c));
  };
  // Move a component up/down to reorder (useful when the original PEFA
  // handbook order matters but the user added them out of sequence).
  const moveComponent = (i, dir) => {
    const j = i + dir;
    if (j < 0 || j >= components.length) return;
    const next = [...components];
    [next[i], next[j]] = [next[j], next[i]];
    setComponents(next);
  };

  const inp = { width: "100%", padding: "6px 8px", borderRadius: 5, border: "1px solid var(--line)", fontSize: 12.5, background: "var(--input-bg, var(--bg, white))", color: "var(--text)", boxSizing: "border-box", fontFamily: "inherit" };

  return (
    <div style={{ borderTop: "1px solid var(--line)", paddingTop: 12, marginTop: 4 }}>
      <div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 6 }}>
        <div style={{ fontSize: 13, fontWeight: 600 }}>
          {window.L("Composantes PEFA", "PEFA components", "Componentes PEFA")}
        </div>
        <span className="pill" style={{ fontSize: 10, padding: "1px 7px" }}>
          {components.length} / {MAX_COMPONENTS}
        </span>
        <div style={{ flex: 1 }} />
        <button type="button" className="btn xs"
          onClick={addComponent}
          disabled={components.length >= MAX_COMPONENTS}>
          + {window.L("Ajouter", "Add", "Agregar")}
        </button>
      </div>
      <div className="text-faint" style={{ fontSize: 11, marginBottom: 8 }}>
        {window.L("Un indicateur PEFA comporte 1 à 4 composantes notées séparément. Codification suggérée : ", "A PEFA indicator has 1 to 4 components scored separately. Suggested code: ", "Un indicador PEFA tiene de 1 a 4 componentes calificados por separado. Codificación sugerida: ")}
        <code style={{ fontSize: 10.5 }}>
          {baseCode ? `${baseCode}.1, ${baseCode}.2…` : (window.L("(saisir d'abord le code parent)", "(enter parent code first)", "(ingrese primero el código padre)"))}
        </code>
        . {window.L("La note finale de l'indicateur reste saisie manuellement.", "The final indicator grade stays manually entered.", "La calificación final del indicador sigue ingresándose manualmente.")}
      </div>

      {components.length === 0 ? (
        <div className="text-faint" style={{ fontSize: 11.5, padding: 10, border: "1px dashed var(--line)", borderRadius: 6, textAlign: "center" }}>
          {window.L("Aucune composante. Cliquer « + Ajouter » pour en créer une.", "No components. Click '+ Add' to create one.", "Ningún componente. Haga clic en « + Agregar » para crear uno.")}
        </div>
      ) : (
        <div style={{ display: "grid", gap: 6 }}>
          {components.map((c, i) => (
            <div key={i} style={{
              display: "grid",
              gridTemplateColumns: "auto 110px 1fr 1fr auto",
              gap: 6, alignItems: "center",
              padding: "6px 8px",
              background: "var(--bg-sunken)",
              border: "1px solid var(--line)",
              borderRadius: 6,
            }}>
              <div style={{ display: "flex", flexDirection: "column", gap: 2 }}>
                <button type="button" className="btn xs ghost"
                  onClick={() => moveComponent(i, -1)} disabled={i === 0}
                  title={window.L("Monter", "Move up", "Subir")}
                  style={{ padding: "1px 5px", fontSize: 10, lineHeight: 1 }}>↑</button>
                <button type="button" className="btn xs ghost"
                  onClick={() => moveComponent(i, +1)} disabled={i === components.length - 1}
                  title={window.L("Descendre", "Move down", "Bajar")}
                  style={{ padding: "1px 5px", fontSize: 10, lineHeight: 1 }}>↓</button>
              </div>
              <input
                style={{ ...inp, fontFamily: "var(--font-mono)", fontSize: 11.5 }}
                value={c.code || ""}
                onChange={(e) => updateComponent(i, { code: e.target.value })}
                placeholder={baseCode ? `${baseCode}.${i + 1}` : "PI-XX.N"} />
              <input
                style={inp}
                value={c.name_fr || ""}
                onChange={(e) => updateComponent(i, { name_fr: e.target.value })}
                placeholder={window.L("Nom de la composante (FR)", "Component name (FR)", "Nombre del componente (FR)")} />
              <input
                style={inp}
                value={c.name_en || ""}
                onChange={(e) => updateComponent(i, { name_en: e.target.value })}
                placeholder="Component name (EN)" />
              <button type="button" className="btn xs ghost danger"
                onClick={() => removeComponent(i)}
                title={window.L("Retirer", "Remove", "Quitar")}
                style={{ padding: "2px 7px" }}>
                ✕
              </button>
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

// ── Disaggregation axes section (Phase C-1) ───────────────────────────────
// Lists all axes visible to the user (shared globals + this org's local axes)
// and lets the admin tick a checkbox per axis. Checked = this indicator will
// be disaggregated along that axis. The saisie grid (Phase D) reads this
// config to generate the cells to fill.
//
// For each checked axis we display a preview of its values (chips with the
// short label, full description on hover) so the admin can verify they
// match the indicator's reporting requirements before committing.
//
// Save is autosaved per-toggle via definitionDisaggregationsCrud (no
// separate Save button needed — feels lighter and matches the PirsAttachments
// section pattern that also saves inline).
function DisaggregationAxesSection({ definitionId, lang }) {
  const { data: axes,    loading: axesLoading }    = window.melr.useDisaggregationAxes();
  const { data: assigned, loading: assignedLoading, refresh } = window.melr.useDefinitionDisaggregations(definitionId);
  const [busy, setBusy] = useStateID(null);       // axis_id being toggled
  const [err, setErr]   = useStateID(null);

  // Build a Set of axis_id currently assigned to this definition for O(1) lookup
  const assignedIds = React.useMemo(() => {
    const s = new Set();
    (assigned || []).forEach((row) => s.add(row.axis_id));
    return s;
  }, [assigned]);

  const toggleAxis = async (axisId, currentlyOn) => {
    setBusy(axisId);
    setErr(null);
    try {
      if (currentlyOn) {
        await window.melr.definitionDisaggregationsCrud.removeAxis(definitionId, axisId);
      } else {
        // Append at the end of the display order
        const nextOrder = (assigned || []).reduce((m, r) => Math.max(m, r.display_order || 0), -1) + 1;
        await window.melr.definitionDisaggregationsCrud.addAxis(definitionId, axisId, { is_required: true, display_order: nextOrder });
      }
      await refresh();
    } catch (e) { setErr(e.message); }
    finally { setBusy(null); }
  };

  if (axesLoading || assignedLoading) {
    return <div className="text-faint" style={{ fontSize: 11.5, padding: 8 }}>{window.L("Chargement…", "Loading…", "Cargando…")}</div>;
  }

  return (
    <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
      <div style={{ fontSize: 11, color: "var(--text-muted)" }}>
        {window.L("Cocher les axes selon lesquels cet indicateur sera désagrégé à la saisie. Chaque combinaison génère une cellule N/D à remplir.", "Tick the axes this indicator will be disaggregated along at data entry. Each combination generates one N/D cell to fill.", "Marque los ejes según los cuales se desagregará este indicador en la captura. Cada combinación genera una celda N/D por completar.")}
      </div>

      {axes.length === 0 ? (
        <div className="text-faint" style={{ fontSize: 11.5, padding: 10, border: "1px dashed var(--line)", borderRadius: 6, textAlign: "center" }}>
          {window.L("Aucun axe disponible. Les axes globaux (sex, age, population, entité) seront seedés par la migration.", "No axis available. The global axes (sex, age, population, entity) are seeded by the migration.", "Ningún eje disponible. Los ejes globales (sexo, edad, población, entidad) se sembrarán con la migración.")}
        </div>
      ) : (
        <div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
          {axes.map((a) => {
            const on = assignedIds.has(a.id);
            const isBusy = busy === a.id;
            return (
              <DisaggregationAxisRow
                key={a.id}
                axis={a}
                on={on}
                busy={isBusy}
                lang={lang}
                onToggle={() => toggleAxis(a.id, on)}
              />
            );
          })}
        </div>
      )}

      {err && (
        <div style={{ padding: "6px 10px", borderRadius: 5, fontSize: 11.5, background: "#fee2e2", color: "#991b1b" }}>
          {err}
        </div>
      )}
    </div>
  );
}

// One row in the axes panel = one axis with checkbox + label + values preview
function DisaggregationAxisRow({ axis, on, busy, lang, onToggle }) {
  // Lazy-load values when the axis is expanded (saves DB queries on first paint).
  // ⚠️ React Rules of Hooks: useAxisValues MUST be called unconditionally on
  // every render. We toggle its argument to null when not expanded — the hook
  // returns an empty list in that case (it null-checks at the top).
  const [expanded, setExpanded] = useStateID(on);   // start expanded if on
  const { data: values, loading } = window.melr.useAxisValues(expanded ? axis.id : null);

  // Whenever the toggle flips ON, auto-expand to show the values
  React.useEffect(() => { if (on) setExpanded(true); }, [on]);

  const label = (lang === "es" ? (axis.name_es != null ? axis.name_es : axis.name_en || axis.name_fr) : lang === "fr" ? axis.name_fr : axis.name_en || axis.name_fr);
  const desc  = axis.description;
  const isShared = !!axis.is_shared;

  return (
    <div style={{
      border: "1px solid " + (on ? "var(--accent-line, #c7d2fe)" : "var(--line)"),
      borderRadius: 6,
      padding: "8px 12px",
      background: on ? "var(--accent-soft, #eef2ff)" : "var(--bg, white)",
    }}>
      <div style={{ display: "flex", alignItems: "center", gap: 10 }}>
        <input
          type="checkbox"
          checked={on}
          disabled={busy}
          onChange={onToggle}
          style={{ width: 16, height: 16, cursor: busy ? "wait" : "pointer", accentColor: "var(--accent, #6366f1)" }}
        />
        <div style={{ flex: 1, minWidth: 0 }}>
          <div style={{ display: "flex", alignItems: "baseline", gap: 6 }}>
            <span style={{ fontSize: 12.5, fontWeight: 500 }}>{label}</span>
            <span style={{ fontFamily: "monospace", fontSize: 10, color: "var(--text-faint)" }}>· {axis.code}</span>
            {isShared && (
              <span style={{ fontSize: 9.5, padding: "1px 6px", borderRadius: 3, background: "oklch(0.95 0.04 195)", color: "oklch(0.40 0.10 195)", fontWeight: 600 }}>
                {window.L("PARTAGÉ", "SHARED", "COMPARTIDO")}
              </span>
            )}
            <span style={{ fontSize: 10, color: "var(--text-faint)" }}>· {axis.kind === "value-list" ? (window.L("liste de valeurs", "value list", "lista de valores")) : (window.L("référence entité", "entity reference", "referencia de entidad"))}</span>
          </div>
          {desc && <div style={{ fontSize: 11, color: "var(--text-muted)", marginTop: 2 }}>{desc}</div>}
        </div>
        <button
          type="button"
          className="btn xs ghost"
          onClick={() => setExpanded((e) => !e)}
          disabled={busy}>
          {expanded ? (window.L("Masquer", "Hide", "Ocultar")) : (window.L("Aperçu", "Preview", "Vista previa"))}
        </button>
      </div>

      {expanded && (
        <div style={{ marginTop: 8, paddingTop: 8, borderTop: "1px dashed var(--line)" }}>
          {axis.kind === "entity-ref" ? (
            <div style={{ fontSize: 11, color: "var(--text-muted)", fontStyle: "italic" }}>
              {window.L("Cet axe pointe vers la liste « Entités » (ONG partenaires, communautés, districts) — à gérer dans le drawer dédié.", "This axis points to the 'Entities' list (partner NGOs, communities, districts) — managed in the dedicated drawer.", "Este eje apunta a la lista « Entidades » (ONG socias, comunidades, distritos) — gestionada en el panel dedicado.")}
            </div>
          ) : loading ? (
            <div className="text-faint" style={{ fontSize: 11 }}>{window.L("Chargement des valeurs…", "Loading values…", "Cargando los valores…")}</div>
          ) : (values && values.length > 0) ? (
            <div style={{ display: "flex", gap: 5, flexWrap: "wrap" }}>
              {values.map((v) => {
                const lbl = (lang === "es" ? (v.label_es != null ? v.label_es : v.label_en || v.label_fr) : lang === "fr" ? v.label_fr : v.label_en || v.label_fr);
                const ttl = lang === "fr" ? (v.description_fr || v.label_fr) : (v.description_en || v.label_en || v.label_fr);
                const isOrgLocal = !!v.organization_id;
                return (
                  <span key={v.id}
                    title={ttl + (isOrgLocal ? (window.L(" (ajouté par votre org)", " (added by your org)", " (agregado por su organización)")) : "")}
                    style={{
                      display: "inline-flex", alignItems: "center", gap: 4,
                      fontSize: 11, fontWeight: 500,
                      padding: "2px 8px", borderRadius: 999,
                      background: isOrgLocal ? "oklch(0.96 0.04 75)" : "var(--bg-sunken, #f3f4f6)",
                      color:      isOrgLocal ? "oklch(0.42 0.10 75)" : "var(--text)",
                      border: "1px solid " + (isOrgLocal ? "oklch(0.82 0.10 75)" : "var(--line)"),
                    }}>
                    {lbl}
                    {isOrgLocal && <span style={{ fontSize: 9 }}>★</span>}
                  </span>
                );
              })}
            </div>
          ) : (
            <div className="text-faint" style={{ fontSize: 11, fontStyle: "italic" }}>
              {window.L("Aucune valeur définie pour cet axe.", "No values defined for this axis.", "Ningún valor definido para este eje.")}
            </div>
          )}
        </div>
      )}
    </div>
  );
}

// ── Excel import modal ──────────────────────────────────────────────────────
function ImportDefinitionsModal({ lang, onClose }) {
  const [file, setFile]   = useStateID(null);
  const [busy, setBusy]   = useStateID(false);
  const [result, setResult] = useStateID(null);
  const [err, setErr]     = useStateID(null);

  const onRun = async () => {
    if (!file) { setErr(window.L("Choisir un fichier Excel.", "Pick an Excel file.", "Elija un archivo Excel.")); return; }
    setBusy(true); setErr(null); setResult(null);
    try {
      const r = await window.melr.importIndicatorDefinitionsFromExcel(file);
      setResult(r);
    } catch (e) { setErr(e.message); }
    finally { setBusy(false); }
  };

  return (
    <Modal
      title={window.L("Importer un Excel d'indicateurs", "Import indicators from Excel", "Importar un Excel de indicadores")}
      onClose={onClose}
      footer={<>
        <button className="btn sm ghost" onClick={onClose} disabled={busy}>
          {result ? (window.L("Fermer", "Close", "Cerrar")) : (window.L("Annuler", "Cancel", "Cancelar"))}
        </button>
        <button className="btn sm primary" onClick={onRun} disabled={busy || !file}>
          {busy ? "…" : (window.L("Importer", "Import", "Importar"))}
        </button>
      </>}>
      <div>
        <div className="text-faint" style={{ fontSize: 12, marginBottom: 12 }}>
            {window.L("Colonnes attendues (insensibles à la casse, séparateurs souples) :", "Expected columns (case-insensitive, flexible separators):", "Columnas esperadas (sin distinción de mayúsculas, separadores flexibles):")}
          </div>
          <pre style={{ background: "var(--bg-sunken, #f3f4f6)", padding: 10, borderRadius: 6, fontSize: 11.5, lineHeight: 1.5 }}>
{`Code | Nom (FR) | Nom (EN) | Secteur | Origine | Niveau
REFT-001 | Couverture vaccinale | Vaccination coverage | Santé | REFT Africa | outcome
USAID-EG.3.1-1 | Foyers raccordés | Connected households | Énergie | USAID | output
PEFA-PI-1 | Dépenses réelles | Actual expenditure | Finances publiques | PEFA | outcome
…`}
          </pre>
          <div className="text-faint" style={{ fontSize: 11.5, marginBottom: 12 }}>
            {window.L("• Les codes en doublon sont mis à jour (upsert). Les lignes sans Code ou sans Nom (FR) sont ignorées.", "• Duplicate codes are upserted. Rows without Code or Name (FR) are skipped.", "• Los códigos duplicados se actualizan (upsert). Las filas sin Código o sin Nombre (FR) se ignoran.")}
          </div>
          <div className="text-faint" style={{ fontSize: 11.5, marginBottom: 12 }}>
            {window.L("• Colonne Secteur (optionnelle) : nom FR (« Santé »), nom EN (« Health »), ou identifiant (« sante »). Un secteur inconnu est signalé en erreur et la ligne est importée sans secteur — créez-le d'abord depuis « Nouveau projet ».", "• Sector column (optional): FR name ('Health'), EN name, or slug ('sante'). Unknown sectors are flagged and the row is imported without a sector — create the sector first from 'New project'.", "• Columna Sector (opcional): nombre FR (« Santé »), nombre EN (« Health ») o identificador (« sante »). Un sector desconocido se señala como error y la fila se importa sin sector — créelo primero desde « Nuevo proyecto ».")}
          </div>
          <input type="file" accept=".xlsx,.xls" onChange={(e) => setFile(e.target.files && e.target.files[0])}
            style={{ padding: 6, fontSize: 13, width: "100%" }} />

          {err && <div style={{ marginTop: 12, padding: "8px 12px", background: "#fee2e2", color: "#991b1b", borderRadius: 6, fontSize: 12.5 }}>{err}</div>}
          {result && (
            <div style={{ marginTop: 12, padding: "10px 12px", background: "#dcfce7", color: "#166534", borderRadius: 6, fontSize: 12.5 }}>
              {window.L(
                "✓ " + result.inserted + " ajouté(s) · " + result.updated + " mis à jour · " + result.skipped + " ignoré(s)",
                "✓ " + result.inserted + " added · " + result.updated + " updated · " + result.skipped + " skipped",
                "✓ " + result.inserted + " agregado(s) · " + result.updated + " actualizado(s) · " + result.skipped + " ignorado(s)")}
              {result.errors && result.errors.length > 0 && (
                <details style={{ marginTop: 6, fontSize: 11 }}>
                  <summary>{result.errors.length} {window.L("erreur(s)", "error(s)", "error(es)")}</summary>
                  <ul style={{ margin: 0, paddingLeft: 18 }}>
                    {result.errors.slice(0, 10).map((er, i) => (
                      <li key={i}>{window.L("Ligne ", "Row ", "Fila ")}{er.row}{er.code ? " (" + er.code + ")" : ""}: {er.message}</li>
                    ))}
                  </ul>
                </details>
              )}
            </div>
          )}
      </div>
    </Modal>
  );
}

// CSV export of the catalogue (for round-tripping with Excel imports).
// The "Secteur" column is exported by FR NAME (not the slug) so the file
// stays human-friendly. The importer accepts slug / FR / EN equivalently,
// so a round-trip works either way.
function exportDefinitionsCsv(rows, lang) {
  const header = ["Code", "Nom (FR)", "Nom (EN)", "Secteur", "Origine", "Niveau", "PIRS"];
  const lines = [header.join(",")];
  rows.forEach((r) => {
    const escape = (s) => {
      const v = s == null ? "" : String(s);
      return /[",\n]/.test(v) ? "\"" + v.replace(/"/g, "\"\"") + "\"" : v;
    };
    // Resolve the sector slug to a friendly FR name via the global lookup.
    // Falls back to the slug itself if for some reason the sector is gone.
    let sectorName = "";
    if (r.sector_id) {
      const s = (typeof sectorById === "function") ? sectorById(r.sector_id) : null;
      sectorName = (s && s.fr) ? s.fr : r.sector_id;
    }
    lines.push([
      escape(r.code),
      escape(r.name_fr),
      escape(r.name_en || ""),
      escape(sectorName),
      escape(r.origin_institution || ""),
      escape(r.level || ""),
      escape(r.pirs_filename || ""),
    ].join(","));
  });
  const blob = new Blob(["﻿" + lines.join("\n")], { type: "text/csv;charset=utf-8" });
  const url = URL.createObjectURL(blob);
  const a = document.createElement("a");
  a.href = url;
  a.download = "indicator-definitions-" + new Date().toISOString().slice(0, 10) + ".csv";
  document.body.appendChild(a); a.click(); document.body.removeChild(a);
  URL.revokeObjectURL(url);
}

// ============================================================================
// E4 · Politique de désagrégation au niveau organisation
// ----------------------------------------------------------------------------
// Carte rétractable affichée en haut de l'écran « Définition des indicateurs ».
// Permet à l'admin org de décréter quels axes (genre, âge, handicap, etc.)
// sont OBLIGATOIRES pour toute définition d'indicateur sociale (ou toute
// définition selon le périmètre choisi). Les définitions non conformes sont
// signalées par un chip rouge dans la liste (voir DefinitionList ci-dessus).
//
// Lecture : tout membre de l'org. Écriture : admin org + super-admin.
// ============================================================================
function DisaggregationPolicyCard({ lang, effectiveOrgId, canManage }) {
  const { data: policy, loading, refresh } = window.melr.useOrgDisaggregationPolicy(effectiveOrgId);
  const { data: axes } = window.melr.useDisaggregationAxes ? window.melr.useDisaggregationAxes() : { data: [] };
  const [open, setOpen]   = useStateID(false);
  const [editing, setEd]  = useStateID(false);
  const [draft, setDraft] = useStateID({ required: [], applies_to: "social", notes: "", enforce_on_entry: false });
  const [busy, setBusy]   = useStateID(false);
  const [err,  setErr]    = useStateID(null);

  // Synchronise draft when policy loads
  React.useEffect(() => {
    if (policy) {
      setDraft({
        required: policy.required_axis_codes || [],
        applies_to: policy.applies_to || "social",
        notes: policy.notes || "",
        enforce_on_entry: !!policy.enforce_on_entry,
      });
    }
  }, [policy && policy.organization_id, policy && policy.updated_at]);

  if (!effectiveOrgId) return null;

  // Liste des axes disponibles : on prend ceux qui sont SHARED + ceux de l'org
  // Active. On affiche uniquement code + name_fr/en.
  const visibleAxes = (axes || []).filter((a) => a.is_shared || a.organization_id === effectiveOrgId);

  const isActive = policy && policy.applies_to && policy.applies_to !== "none" && (policy.required_axis_codes || []).length > 0;

  const toggleRequired = (code) => {
    setDraft((d) => {
      const set = new Set(d.required);
      if (set.has(code)) set.delete(code); else set.add(code);
      return { ...d, required: Array.from(set) };
    });
  };

  const onSave = async () => {
    setBusy(true); setErr(null);
    try {
      await window.melr.orgDisaggregationPolicyCrud.upsert({
        organization_id: effectiveOrgId,
        required_axis_codes: draft.required,
        applies_to: draft.applies_to,
        notes: draft.notes,
        enforce_on_entry: draft.enforce_on_entry,
      });
      await refresh();
      setEd(false);
    } catch (e) {
      setErr(e.message || String(e));
    } finally {
      setBusy(false);
    }
  };

  const onDeactivate = async () => {
    if (!confirm(window.L("Désactiver la politique de désagrégation ? Les chips d'alerte disparaîtront mais l'historique reste.", "Deactivate the disaggregation policy? Warning chips will disappear but history is preserved.", "¿Desactivar la política de desagregación? Los chips de alerta desaparecerán pero el historial se conserva."))) return;
    setBusy(true); setErr(null);
    try {
      await window.melr.orgDisaggregationPolicyCrud.deactivate(effectiveOrgId);
      await refresh();
    } catch (e) {
      setErr(e.message || String(e));
    } finally {
      setBusy(false);
    }
  };

  // Affichage en mode résumé (collapsed) — visible pour tous
  if (!open) {
    return (
      <div style={{
        margin: "12px 0",
        padding: "10px 14px",
        borderRadius: 8,
        background: "linear-gradient(90deg, #fef3c7 0%, #fffbeb 100%)",
        border: "1px solid #fcd34d",
        boxShadow: "0 1px 3px rgba(217,119,6,0.15)",
        display: "flex", alignItems: "center", justifyContent: "space-between", gap: 12,
      }}>
        <div style={{ display: "flex", alignItems: "center", gap: 10 }}>
          <span style={{ fontSize: 18 }}>🎯</span>
          <div>
            <div style={{ fontWeight: 700, fontSize: 13, color: "#78350f" }}>
              {window.L("Politique de désagrégation", "Disaggregation policy", "Política de desagregación")}
              {isActive ? (
                <span style={{ marginLeft: 8, padding: "2px 8px", borderRadius: 999, background: "#d97706", color: "white", fontSize: 11, fontWeight: 700 }}>
                  {window.L("ACTIVE", "ACTIVE", "ACTIVA")}
                </span>
              ) : (
                <span style={{ marginLeft: 8, padding: "2px 8px", borderRadius: 999, background: "white", color: "#92400e", border: "1px solid #fcd34d", fontSize: 11, fontWeight: 700 }}>
                  {window.L("À DÉFINIR", "TO BE SET", "POR DEFINIR")}
                </span>
              )}
            </div>
            <div style={{ fontSize: 11.5, color: "#92400e", marginTop: 2 }}>
              {loading
                ? (window.L("Chargement…", "Loading…", "Cargando…"))
                : isActive
                  ? (window.L(
                      "Axes obligatoires : " + (policy.required_axis_codes || []).join(", ") + " · périmètre : " + (policy.applies_to === "social" ? "indicateurs sociaux" : "tous indicateurs"),
                      "Required axes: " + (policy.required_axis_codes || []).join(", ") + " · scope: " + (policy.applies_to === "social" ? "social indicators" : "all indicators"),
                      "Ejes obligatorios: " + (policy.required_axis_codes || []).join(", ") + " · perímetro: " + (policy.applies_to === "social" ? "indicadores sociales" : "todos los indicadores")))
                  : (window.L("Aucune politique active. Définissez les axes obligatoires (genre, âge, handicap…) pour vos indicateurs sociaux.", "No active policy. Set required axes (gender, age, disability…) for your social indicators.", "Ninguna política activa. Defina los ejes obligatorios (género, edad, discapacidad…) para sus indicadores sociales."))}
            </div>
          </div>
        </div>
        <button onClick={() => setOpen(true)}
          style={{ padding: "6px 12px", borderRadius: 6, border: "1px solid #d97706", background: "#d97706", color: "white", cursor: "pointer", fontSize: 12.5, fontWeight: 700 }}>
          {canManage ? (window.L("Configurer", "Configure", "Configurar")) : (window.L("Détails", "Details", "Detalles"))}
        </button>
      </div>
    );
  }

  // Mode étendu
  return (
    <div style={{
      margin: "12px 0",
      padding: "14px 16px",
      borderRadius: 8,
      background: "linear-gradient(180deg, #fef3c7 0%, #fffbeb 60px, var(--bg-elev) 60px)",
      border: "1px solid #fcd34d",
      boxShadow: "0 1px 3px rgba(217,119,6,0.15)",
    }}>
      <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 12 }}>
        <div style={{ fontWeight: 700, fontSize: 14, color: "#78350f" }}>
          🎯 {window.L("Politique de désagrégation de l'organisation", "Organization disaggregation policy", "Política de desagregación de la organización")}
        </div>
        <button onClick={() => { setOpen(false); setEd(false); }}
          style={{ padding: "4px 8px", borderRadius: 4, border: "none", background: "transparent", color: "var(--text-faint)", cursor: "pointer", fontSize: 18 }}>×</button>
      </div>
      <div style={{ fontSize: 12.5, color: "var(--text-faint)", marginBottom: 14, lineHeight: 1.5 }}>
        {window.L("Lorsque la politique est ACTIVE, MELR signale par un chip rouge dans la liste ci-dessous toute définition d'indicateur du périmètre qui n'a PAS configuré tous les axes obligatoires. Vous pouvez ajuster la désagrégation depuis « Modifier » de la définition concernée.", "When the policy is ACTIVE, MELR flags with a red chip in the list below any indicator definition in scope that does NOT have all required axes configured. Adjust the disaggregation from 'Edit' on the relevant definition.", "Cuando la política está ACTIVA, MELR señala con un chip rojo en la lista de abajo toda definición de indicador del perímetro que NO haya configurado todos los ejes obligatorios. Puede ajustar la desagregación desde « Editar » de la definición correspondiente.")}
      </div>

      {canManage && !editing && (
        <div style={{ display: "flex", gap: 8, marginBottom: 14 }}>
          <button onClick={() => setEd(true)} className="btn sm primary">
            <Icon.edit /> {policy ? (window.L("Modifier", "Edit", "Editar")) : (window.L("Définir", "Set up", "Definir"))}
          </button>
          {policy && policy.applies_to !== "none" && (
            <button onClick={onDeactivate} disabled={busy}
              style={{ padding: "6px 12px", borderRadius: 6, border: "1px solid var(--line)", background: "var(--bg)", color: "var(--text)", cursor: "pointer", fontSize: 12.5 }}>
              {window.L("Désactiver", "Deactivate", "Desactivar")}
            </button>
          )}
        </div>
      )}

      {/* Vue lecture */}
      {!editing && policy && (
        <div style={{ display: "grid", gridTemplateColumns: "max-content 1fr", gap: "8px 16px", fontSize: 13 }}>
          <div style={{ color: "var(--text-faint)", fontWeight: 600 }}>{window.L("Périmètre :", "Scope:", "Perímetro:")}</div>
          <div>{policy.applies_to === "all" ? (window.L("Tous les indicateurs", "All indicators", "Todos los indicadores"))
              : policy.applies_to === "social" ? (window.L("Indicateurs sociaux (catégorie renseignée)", "Social indicators (category set)", "Indicadores sociales (categoría especificada)"))
              : (window.L("Désactivée", "Disabled", "Desactivada"))}</div>
          <div style={{ color: "var(--text-faint)", fontWeight: 600 }}>{window.L("Axes obligatoires :", "Required axes:", "Ejes obligatorios:")}</div>
          <div>
            {(policy.required_axis_codes || []).length === 0
              ? <span style={{ color: "var(--text-faint)", fontStyle: "italic" }}>—</span>
              : (policy.required_axis_codes || []).map((code) => {
                  const ax = visibleAxes.find((a) => a.code === code);
                  return (
                    <span key={code} style={{ display: "inline-flex", gap: 4, padding: "2px 9px", marginRight: 6, marginBottom: 4, borderRadius: 999, background: "#fef3c7", color: "#92400e", border: "1px solid #fcd34d", fontSize: 11.5, fontWeight: 600 }}>
                      {ax ? (lang === "fr" ? (ax.name_fr || code) : (ax.name_en || ax.name_fr || code)) : code}
                    </span>
                  );
                })}
          </div>
          <div style={{ color: "var(--text-faint)", fontWeight: 600 }}>{window.L("Blocage saisie :", "Block entry:", "Bloqueo de captura:")}</div>
          <div>{policy.enforce_on_entry ? (window.L("Oui (saisie refusée si non conforme)", "Yes (entry refused if non-compliant)", "Sí (captura rechazada si no es conforme)")) : (window.L("Non — alertes seulement", "No — warnings only", "No — solo alertas"))}</div>
          {policy.notes && <>
            <div style={{ color: "var(--text-faint)", fontWeight: 600 }}>{window.L("Notes :", "Notes:", "Notas:")}</div>
            <div style={{ whiteSpace: "pre-wrap" }}>{policy.notes}</div>
          </>}
        </div>
      )}

      {/* Vue édition */}
      {editing && (
        <div style={{ display: "grid", gap: 12 }}>
          <div>
            <label style={{ display: "block", fontSize: 12, fontWeight: 600, marginBottom: 4 }}>{window.L("Périmètre", "Scope", "Perímetro")}</label>
            <select value={draft.applies_to} onChange={(e) => setDraft({ ...draft, applies_to: e.target.value })}
              style={{ width: "100%", padding: "8px 10px", borderRadius: 6, border: "1px solid var(--line)", background: "var(--bg)", color: "var(--text)" }}>
              <option value="social">{window.L("Indicateurs sociaux (catégorie renseignée) — recommandé", "Social indicators (category set) — recommended", "Indicadores sociales (categoría especificada) — recomendado")}</option>
              <option value="all">{window.L("Tous les indicateurs — strict", "All indicators — strict", "Todos los indicadores — estricto")}</option>
              <option value="none">{window.L("Désactivée", "Disabled", "Desactivada")}</option>
            </select>
          </div>
          <div>
            <label style={{ display: "block", fontSize: 12, fontWeight: 600, marginBottom: 6 }}>{window.L("Axes obligatoires", "Required axes", "Ejes obligatorios")}</label>
            <div style={{ display: "flex", gap: 6, flexWrap: "wrap" }}>
              {visibleAxes.length === 0
                ? <span style={{ color: "var(--text-faint)", fontSize: 12, fontStyle: "italic" }}>{window.L("Aucun axe disponible. Créez-en depuis « Définitions de base ».", "No axis available. Create some in 'Base definitions'.", "Ningún eje disponible. Cree algunos desde « Definiciones de base ».")}</span>
                : visibleAxes.map((a) => {
                    const active = draft.required.includes(a.code);
                    return (
                      <button key={a.code} type="button" onClick={() => toggleRequired(a.code)}
                        style={{
                          padding: "6px 12px", borderRadius: 999,
                          background: active ? "#d97706" : "var(--bg)",
                          color: active ? "white" : "var(--text)",
                          border: "1px solid " + (active ? "#d97706" : "var(--line)"),
                          fontSize: 12, fontWeight: 600, cursor: "pointer",
                        }}>
                        {active && <span style={{ marginRight: 4 }}>✓</span>}
                        {lang === "fr" ? (a.name_fr || a.code) : (a.name_en || a.name_fr || a.code)}
                        <span style={{ opacity: 0.6, marginLeft: 6, fontSize: 10 }}>({a.code})</span>
                      </button>
                    );
                  })}
            </div>
          </div>
          <div>
            <label style={{ display: "flex", alignItems: "center", gap: 8, fontSize: 12.5 }}>
              <input type="checkbox" checked={draft.enforce_on_entry} onChange={(e) => setDraft({ ...draft, enforce_on_entry: e.target.checked })} />
              <span>{window.L("Bloquer la saisie de valeurs sur indicateurs non conformes (à activer quand l'organisation est mûre)", "Block value entry on non-compliant indicators (enable when the organization is mature)", "Bloquear la captura de valores en indicadores no conformes (activar cuando la organización esté madura)")}</span>
            </label>
          </div>
          <div>
            <label style={{ display: "block", fontSize: 12, fontWeight: 600, marginBottom: 4 }}>{window.L("Notes (facultatives)", "Notes (optional)", "Notas (opcionales)")}</label>
            <textarea value={draft.notes} onChange={(e) => setDraft({ ...draft, notes: e.target.value })}
              rows={3}
              placeholder={window.L("Ex. : « Conforme à notre politique genre 2026, validée le 15 mai par le COMEX »", "e.g. 'Aligned with our 2026 gender policy, ratified May 15 by ExCom'", "Ej.: « Conforme a nuestra política de género 2026, validada el 15 de mayo por el COMEX »")}
              style={{ width: "100%", padding: "8px 10px", borderRadius: 6, border: "1px solid var(--line)", background: "var(--bg)", color: "var(--text)", boxSizing: "border-box", fontFamily: "inherit", fontSize: 13 }} />
          </div>
          {err && <div style={{ color: "#dc2626", fontSize: 12.5 }}>{err}</div>}
          <div style={{ display: "flex", gap: 8 }}>
            <button onClick={onSave} disabled={busy} className="btn sm primary">
              {busy ? (window.L("Enregistrement…", "Saving…", "Guardando…")) : (window.L("Enregistrer", "Save", "Guardar"))}
            </button>
            <button onClick={() => setEd(false)} disabled={busy}
              style={{ padding: "6px 12px", borderRadius: 6, border: "1px solid var(--line)", background: "var(--bg)", color: "var(--text)", cursor: "pointer", fontSize: 12.5 }}>
              {window.L("Annuler", "Cancel", "Cancelar")}
            </button>
          </div>
        </div>
      )}
    </div>
  );
}
window.DisaggregationPolicyCard = DisaggregationPolicyCard;

// ============================================================================
// E6 · Historique des versions d'une définition d'indicateur
// ----------------------------------------------------------------------------
// Affichage collapsible (par défaut fermé) dans DefinitionEditor. Liste les
// snapshots produits par le trigger trg_indicator_definitions_snapshot,
// par ordre antichronologique. Chaque ligne affiche : date, auteur, résumé
// du changement (clé(s) modifiée(s)) + un panneau dépliable montrant le
// JSON complet pour les auditeurs qui veulent voir les valeurs exactes.
//
// Important : la version COURANTE n'est PAS dans cette liste — elle vit
// dans la table indicator_definitions elle-même (et on l'a déjà à l'écran
// dans le formulaire DefinitionEditor).
// ============================================================================
function VersionHistorySection({ definitionId, lang }) {
  const { data: versions, loading } = window.melr.useIndicatorDefinitionVersions(definitionId);
  const [open, setOpen] = useStateID(false);
  const [expandedId, setExpandedId] = useStateID(null);

  if (loading) {
    return (
      <div style={{ marginTop: 18, fontSize: 11.5, color: "var(--text-faint)" }}>
        {window.L("Chargement de l'historique…", "Loading history…", "Cargando el historial…")}
      </div>
    );
  }

  const count = (versions || []).length;

  return (
    <div style={{
      marginTop: 18,
      borderRadius: 8,
      background: "var(--bg-sunken)",
      border: "1px solid var(--line)",
    }}>
      <button type="button" onClick={() => setOpen(!open)}
        style={{
          width: "100%", padding: "10px 14px",
          display: "flex", alignItems: "center", justifyContent: "space-between",
          background: "transparent", border: "none", cursor: "pointer",
          fontSize: 13, fontWeight: 700, color: "var(--text)",
        }}>
        <span>
          📚 {window.L("Historique des versions", "Version history", "Historial de versiones")}
          <span style={{ marginLeft: 8, padding: "1px 8px", borderRadius: 999, background: count > 0 ? "#dbeafe" : "var(--bg)", color: count > 0 ? "#1e40af" : "var(--text-faint)", fontSize: 11, fontWeight: 700 }}>
            {count} {count > 1 ? (window.L("versions", "versions", "versiones")) : (window.L("version", "version", "versión"))}
          </span>
        </span>
        <span style={{ color: "var(--text-faint)", fontSize: 14 }}>{open ? "▲" : "▼"}</span>
      </button>
      {open && (
        <div style={{ padding: "0 14px 14px" }}>
          <div style={{ fontSize: 11.5, color: "var(--text-faint)", marginBottom: 10, lineHeight: 1.5 }}>
            {window.L("Chaque modification de la définition est enregistrée automatiquement. La liste ci-dessous est ordonnée du plus récent au plus ancien. La version COURANTE est celle affichée dans le formulaire — elle n'apparaît pas ici.", "Every modification of the definition is auto-recorded. The list below is ordered newest-first. The CURRENT version is the one shown in the form — it does not appear here.", "Cada modificación de la definición se registra automáticamente. La lista de abajo está ordenada de la más reciente a la más antigua. La versión ACTUAL es la que se muestra en el formulario — no aparece aquí.")}
          </div>
          {count === 0 ? (
            <div style={{ textAlign: "center", padding: 16, color: "var(--text-faint)", fontSize: 12, border: "1px dashed var(--line)", borderRadius: 6 }}>
              {window.L("Aucune version antérieure. L'historique débute à la prochaine modification.", "No prior version. History starts at the next modification.", "Ninguna versión anterior. El historial comienza en la próxima modificación.")}
            </div>
          ) : (
            <div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
              {versions.map((v) => {
                const isExp = expandedId === v.id;
                const d = new Date(v.changed_at);
                const dateStr = d.toLocaleDateString(window.L("fr-FR", "en-US", "es-ES"), { day: "2-digit", month: "short", year: "numeric" })
                  + " · " + d.toLocaleTimeString(window.L("fr-FR", "en-US", "es-ES"), { hour: "2-digit", minute: "2-digit" });
                return (
                  <div key={v.id} style={{
                    border: "1px solid var(--line)", borderRadius: 6,
                    background: "var(--bg)", overflow: "hidden",
                  }}>
                    <button type="button"
                      onClick={() => setExpandedId(isExp ? null : v.id)}
                      style={{
                        width: "100%", padding: "8px 12px",
                        display: "flex", alignItems: "center", justifyContent: "space-between",
                        background: "transparent", border: "none", cursor: "pointer",
                        textAlign: "left",
                      }}>
                      <div style={{ display: "flex", flexDirection: "column", gap: 2, minWidth: 0, flex: 1 }}>
                        <div style={{ fontSize: 12.5, fontWeight: 600, color: "var(--text)" }}>{dateStr}</div>
                        <div style={{ fontSize: 11.5, color: "var(--text-faint)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>
                          {v.change_summary || (window.L("(résumé manquant)", "(summary missing)", "(resumen faltante)"))}
                        </div>
                      </div>
                      <span style={{ marginLeft: 12, color: "var(--text-faint)", fontSize: 11 }}>{isExp ? "▲" : "▼"}</span>
                    </button>
                    {isExp && (
                      <div style={{ padding: "0 12px 12px" }}>
                        <div style={{ fontSize: 11, color: "var(--text-faint)", marginBottom: 6, fontWeight: 600, textTransform: "uppercase", letterSpacing: "0.04em" }}>
                          {window.L("Snapshot complet (JSON)", "Full snapshot (JSON)", "Instantánea completa (JSON)")}
                        </div>
                        <pre style={{
                          margin: 0, padding: 10,
                          background: "var(--bg-sunken)",
                          borderRadius: 4,
                          fontSize: 10.5, lineHeight: 1.5,
                          maxHeight: 300, overflow: "auto",
                          whiteSpace: "pre-wrap", wordBreak: "break-word",
                          fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
                          color: "var(--text)",
                        }}>{JSON.stringify(v.snapshot, null, 2)}</pre>
                      </div>
                    )}
                  </div>
                );
              })}
            </div>
          )}
        </div>
      )}
    </div>
  );
}
window.VersionHistorySection = VersionHistorySection;

window.IndicatorDefinitions = IndicatorDefinitions;
