/* global React, ReactDOM, Icon, Modal, SECTORS, PROJECTS, sectorById, sectorLabel, projectName, projectCountries, projectPhase, getPlanFor, generatePlanFor, PDPlanning, ProgrammesPage, ProgrammeDetail, Dashboard, Baseline, Indicators, AuditSystem, AuditData, Reporting, Learning, ExAnte, MobileField, AndroidDevice, Workflow, ProjectDetail, Notifications, Help, ProfileScreen, TweaksPanel, useTweaks, TweakSection, TweakColor, TweakRadio, TweakToggle */
const { useState, useEffect, useMemo } = React;

// ==================== PROJECTS PORTFOLIO ====================
function Projects({ t, lang, onOpen, isSuperAdmin: isSuperAdminProp, actingOrgId, effectiveOrgId, myOrgId }) {
  // All hooks MUST be called unconditionally, before any early return
  // (rules of hooks). The previous version had useState(filter) AFTER
  // a `if (loading) return …`, which made the number of hooks vary
  // between renders → "Rendered more hooks than during the previous
  // render" crash.
  const { projects: PROJECTS, loading, error, refresh, realtime } = window.melr.useProjects();
  const { programmes: PROGRAMMES_LIST } = window.melr.usePrograms();
  const { currency } = window.melr.useCurrency();
  // Per-project format: respects the project's native currency
  const fmtP = (v, src) => window.melr.formatAmount(v, src || "EUR", currency, lang);
  // Aggregate: convert each to EUR then to display currency
  const fmtAgg = (vals) => {
    const inEur = vals.reduce((s, x) => s + window.melr.convertToEur(x.amount || 0, x.ccy || "EUR"), 0);
    return window.melr.formatMoney(inEur, currency, lang);
  };
  const LiveBadge = window.melr.LiveBadge;
  const [createOpen, setCreateOpen] = useState(false);
  const [transferProject, setTransferProject] = useState(null);
  const [leadProject, setLeadProject] = useState(null);  // project for the lead picker
  const [filter, setFilter] = useState("all");
  const [sectorFilter, setSectorFilter] = useState("all");
  // Programme filter: "all" | "none" (orphan projects with no programme) |
  // programme UUID. Hidden from the UI when no programme exists yet.
  const [programmeFilter, setProgrammeFilter] = useState("all");
  // Super-admin context — pulled from props (set at App level) so the
  // page reacts to the global acting-as-org pill in the topbar.
  // Fallback to local hook for backward compat when used outside the
  // router (e.g. embedded tests).
  const { has: hasPerm } = window.melr.useCurrentUserPermissions();
  const isSuperAdmin = (isSuperAdminProp !== undefined)
    ? isSuperAdminProp
    : (hasPerm && hasPerm("orgs.manage"));
  // "Nouveau projet" is an admin action — creating projects requires the
  // users.manage permission (= admin role per is_admin_user) or super-
  // admin. Reviewer / data-entry users shouldn't see the button.
  const canCreateProject = isSuperAdmin || (hasPerm && hasPerm("users.manage"));
  const { data: allOrgs } = window.melr.useAllOrganizations();
  if (loading) return <div className="page"><div className="page-body" style={{ padding: 40 }}>{lang === "fr" ? "Chargement des projets…" : "Loading projects…"}</div></div>;
  if (error)   return <div className="page"><div className="page-body" style={{ padding: 40, color: "#b91c1c" }}>{(lang === "fr" ? "Erreur base de données : " : "Database error: ") + error}</div></div>;
  const projects = PROJECTS.map((p) => ({
    id: p.id, uuid: p.uuid, organizationId: p.organizationId,
    name: projectName(p, lang), country: projectCountries(p, lang), lead: p.lead,
    status: p.status, progress: p.progress, budget: p.budget, disbursed: p.disbursed,
    indic: p.indic, sites: p.sites, phase: projectPhase(p, lang), risk: p.risk, sector: p.sector,
    nativeCurrency: p.nativeCurrency || "EUR",
    programmeId:   p.programmeId   || null,
    programmeCode: p.programmeCode || null,
    programmeName: p.programmeName || null,
  }));
  // When super-admin is acting on a specific (non-home) org, scope the list
  // to that org. RLS lets the super-admin SELECT every project; this filter
  // narrows the UI to match the "Acting on Org X" pill.
  const orgScopedProjects = (isSuperAdmin && actingOrgId && actingOrgId !== myOrgId)
    ? projects.filter((p) => p.organizationId === actingOrgId)
    : projects;
  const filtered = orgScopedProjects
    .filter((p) => filter === "all" || p.status === filter)
    .filter((p) => sectorFilter === "all" || p.sector === sectorFilter)
    .filter((p) => programmeFilter === "all"
      || (programmeFilter === "none" && !p.programmeId)
      || p.programmeId === programmeFilter);
  const selectedProgramme = (PROGRAMMES_LIST || []).find((pg) => pg.id === programmeFilter);
  const orphanCount = projects.filter((p) => !p.programmeId).length;
  // Sum across mixed-currency projects: convert each to EUR first
  const totalBudgetEur = projects.reduce((s, p) => s + window.melr.convertToEur(p.budget    || 0, p.nativeCurrency), 0);
  const totalDisbEur   = projects.reduce((s, p) => s + window.melr.convertToEur(p.disbursed || 0, p.nativeCurrency), 0);
  const activeSectors = [...new Set(projects.map((p) => p.sector))].length;
  const totalSites = projects.reduce((s, p) => s + p.sites, 0);

  return (
    <div className="page">
      <div className="page-header">
        <div className="page-eyebrow">{lang === "fr" ? "PORTEFEUILLE" : "PORTFOLIO"}</div>
        <div className="page-header-row">
          <div>
            <h1 className="page-title">{t("nav.projects")} <LiveBadge on={realtime} lang={lang} /></h1>
            <div className="page-sub">{projects.length} {lang === "fr" ? "projets · 247 sites · 1.84M bénéficiaires" : "projects · 247 sites · 1.84M beneficiaries"}</div>
          </div>
          <div className="page-header-actions">
            <button className="btn sm"><Icon.upload /> {t("c.import")}</button>
            <button className="btn sm" onClick={() => {
              const date = new Date().toISOString().slice(0, 10);
              window.melr.exportCSV(`projects-${date}.csv`, projects, [
                { key: "id",      label: "Code" },
                { key: "name",    label: lang === "fr" ? "Nom" : "Name" },
                { key: "sector",  label: lang === "fr" ? "Secteur" : "Sector" },
                { key: "country", label: lang === "fr" ? "Pays" : "Country" },
                { key: "lead",    label: lang === "fr" ? "Responsable" : "Lead" },
                { key: "status",  label: lang === "fr" ? "Statut" : "Status" },
                { key: "phase",   label: lang === "fr" ? "Phase" : "Phase" },
                { key: "progress",label: lang === "fr" ? "Avancement %" : "Progress %" },
                { key: "budget",  label: lang === "fr" ? "Budget M€" : "Budget M€",   value: (p) => (p.budget    != null ? Number(p.budget).toFixed(2)    : "") },
                { key: "disbursed",label: lang === "fr" ? "Décaissé M€" : "Disbursed M€", value: (p) => (p.disbursed != null ? Number(p.disbursed).toFixed(2) : "") },
                { key: "risk",    label: lang === "fr" ? "Risque" : "Risk" },
              ]);
            }}><Icon.download /> {t("c.export")}</button>
            {canCreateProject && (
              <button className="btn sm primary" onClick={() => setCreateOpen(true)}><Icon.plus /> {lang === "fr" ? "Nouveau projet" : "New project"}</button>
            )}
          </div>
        </div>
      </div>

      <div className="grid cols-4" style={{ marginBottom: 16 }}>
        {[
          { l: lang === "fr" ? "Budget total" : "Total budget", v: window.melr.formatMoney(totalBudgetEur, currency, lang), s: (lang === "fr" ? projects.length + " projets · " + activeSectors + " secteurs" : projects.length + " projects · " + activeSectors + " sectors") },
          { l: lang === "fr" ? "Décaissé" : "Disbursed", v: window.melr.formatMoney(totalDisbEur, currency, lang), s: totalBudgetEur > 0 ? Math.round(totalDisbEur / totalBudgetEur * 100) + "%" : "—" },
          { l: lang === "fr" ? "Indicateurs actifs" : "Active indicators", v: "219", s: lang === "fr" ? "dont 38 en alerte" : "incl. 38 alerting" },
          { l: lang === "fr" ? "Sites suivis" : "Tracked sites", v: totalSites, s: lang === "fr" ? "sur 12 pays" : "across 12 countries" },
        ].map((k, i) => (
          <div key={i} className="kpi">
            <div className="kpi-label">{k.l}</div>
            <div className="kpi-value">{k.v}</div>
            <div className="kpi-sub">{k.s}</div>
          </div>
        ))}
      </div>

      {/* Sector filter row */}
      <div className="sector-filter-row">
        <button className={"sector-chip-btn" + (sectorFilter === "all" ? " active" : "")} onClick={() => setSectorFilter("all")}>
          <span className="sector-dot" style={{ background: "var(--text)" }}></span>
          {lang === "fr" ? "Tous les secteurs" : "All sectors"}
          <span className="seg-count">{projects.length}</span>
        </button>
        {SECTORS.map((s) => {
          const count = projects.filter((p) => p.sector === s.id).length;
          if (count === 0) return null;
          return (
            <button key={s.id} className={"sector-chip-btn" + (sectorFilter === s.id ? " active" : "")}
              style={sectorFilter === s.id ? { background: s.bg, borderColor: s.color, color: s.color } : {}}
              onClick={() => setSectorFilter(s.id)}>
              <span className="sector-dot" style={{ background: s.color }}></span>
              {sectorLabel(s, lang)}
              <span className="seg-count">{count}</span>
            </button>
          );
        })}
      </div>

      <div className="filter-bar">
        <div className="seg">
          {[
            { k: "all", l: lang === "fr" ? "Tous" : "All", c: projects.length },
            { k: "active", l: lang === "fr" ? "Actifs" : "Active", c: projects.filter((p) => p.status === "active").length },
            { k: "appraisal", l: lang === "fr" ? "Évaluation" : "Appraisal", c: projects.filter((p) => p.status === "appraisal").length },
            { k: "closing", l: lang === "fr" ? "Clôture" : "Closing", c: projects.filter((p) => p.status === "closing").length },
            { k: "paused", l: lang === "fr" ? "Suspendus" : "Paused", c: projects.filter((p) => p.status === "paused").length },
          ].map((s) => (
            <button key={s.k} className={"seg-btn" + (filter === s.k ? " active" : "")} onClick={() => setFilter(s.k)}>
              {s.l} <span className="seg-count">{s.c}</span>
            </button>
          ))}
        </div>
        <div className="row gap-sm" style={{ marginLeft: "auto" }}>
          {/* Programme filter — always visible so the feature is discoverable,
              even before any programme is created. */}
          <label className="row gap-xs" style={{ fontSize: 11.5 }}>
            <span className="text-faint">◇ {lang === "fr" ? "Programme" : "Programme"}</span>
            <select value={programmeFilter} onChange={(e) => setProgrammeFilter(e.target.value)}
              style={{ fontSize: 12, padding: "4px 8px", borderRadius: 6, border: "1px solid var(--line)", background: "var(--bg)", color: "var(--text)" }}>
              <option value="all">{lang === "fr" ? "Tous" : "All"} ({projects.length})</option>
              {orphanCount > 0 && (
                <option value="none">— {lang === "fr" ? "Sans programme" : "No programme"} — ({orphanCount})</option>
              )}
              {(PROGRAMMES_LIST || []).length === 0 && (
                <option disabled value="__empty">
                  {lang === "fr" ? "(aucun programme créé)" : "(no programme yet)"}
                </option>
              )}
              {(PROGRAMMES_LIST || []).map((pg) => {
                const c = projects.filter((p) => p.programmeId === pg.id).length;
                return (
                  <option key={pg.id} value={pg.id}>
                    {pg.code} — {(lang === "en" ? (pg.name_en || pg.name_fr) : pg.name_fr)} ({c})
                  </option>
                );
              })}
            </select>
          </label>
          <button className="btn sm"><Icon.filter /> {t("c.filter")}</button>
          <div className="seg sm">
            <button className="seg-btn active"><Icon.layout /></button>
            <button className="seg-btn"><Icon.spreadsheet /></button>
            <button className="seg-btn"><Icon.map /></button>
          </div>
        </div>
      </div>

      {programmeFilter !== "all" && (
        <div className="row gap-sm" style={{
          padding: "8px 12px", marginBottom: 12, borderRadius: 6,
          background: "var(--bg-sunken)", fontSize: 12,
          border: "1px solid var(--line-faint)",
        }}>
          <span className="text-faint">
            {lang === "fr" ? "Vue filtrée :" : "Filtered view:"}
          </span>
          <span className="strong">
            {programmeFilter === "none"
              ? (lang === "fr" ? "◇ Projets sans programme" : "◇ Projects without programme")
              : (selectedProgramme
                  ? <>◇ {selectedProgramme.code} — {lang === "en" ? (selectedProgramme.name_en || selectedProgramme.name_fr) : selectedProgramme.name_fr}</>
                  : null)}
          </span>
          <span className="pill" style={{ marginLeft: 4 }}>{filtered.length}</span>
          {programmeFilter !== "none" && selectedProgramme && (
            <button className="btn xs ghost" style={{ marginLeft: 4 }}
              onClick={() => onOpen && onOpen("prog:" + selectedProgramme.id)}>
              {lang === "fr" ? "Ouvrir le programme →" : "Open programme →"}
            </button>
          )}
          <button className="btn xs ghost" style={{ marginLeft: "auto" }}
            onClick={() => setProgrammeFilter("all")}>
            {lang === "fr" ? "Effacer le filtre" : "Clear filter"}
          </button>
        </div>
      )}

      <div className="card">
        <div className="card-body flush">
          <table className="tbl">
            <thead>
              <tr>
                <th style={{ width: 70 }}>ID</th>
                <th>{lang === "fr" ? "Projet" : "Project"}</th>
                <th>{lang === "fr" ? "Secteur" : "Sector"}</th>
                <th>{t("c.country")}</th>
                <th>{t("c.owner")}</th>
                <th>{lang === "fr" ? "Phase" : "Phase"}</th>
                <th>{t("c.progress")}</th>
                <th className="num">{lang === "fr" ? "Budget" : "Budget"}</th>
                <th className="num">{lang === "fr" ? "Décaissé" : "Disbursed"}</th>
                <th className="num">Sites</th>
                <th>{lang === "fr" ? "Risque" : "Risk"}</th>
              </tr>
            </thead>
            <tbody>
              {filtered.map((p) => {
                const s = sectorById(p.sector);
                return (
                <tr key={p.id} className="row-link" onClick={() => onOpen && onOpen(p.id)}>
                  <td className="mono text-faint">{p.id}</td>
                  <td className="strong">
                    {p.name}
                    {p.programmeCode && (
                      <span className="tag-mono"
                        style={{ marginLeft: 6, fontSize: 9.5, padding: "1px 5px", borderRadius: 4, background: "var(--bg-sunken)", color: "var(--text-faint)" }}
                        title={p.programmeName || ""}>
                        ◇ {p.programmeCode}
                      </span>
                    )}
                  </td>
                  <td><span className="sector-chip" style={{ background: s.bg, color: s.color, borderColor: s.color }}>{sectorLabel(s, lang)}</span></td>
                  <td className="muted">{p.country}</td>
                  <td>
                    <span className="row gap-sm"><span className="avatar xs" style={{ background: avColor(p.lead) }}>{initials(p.lead)}</span>{p.lead}</span>
                  </td>
                  <td className="muted">{p.phase}</td>
                  <td>
                    <div className="row gap-sm">
                      <div className="bar"><div className="bar-fill" style={{ width: p.progress + "%" }}></div></div>
                      <span className="mono num-sm" style={{ minWidth: 30 }}>{p.progress}%</span>
                    </div>
                  </td>
                  <td className="num mono">{fmtP(p.budget, p.nativeCurrency)} <span className="text-faint" style={{ fontSize: 9, marginLeft: 4 }}>{p.nativeCurrency !== currency ? p.nativeCurrency : ""}</span></td>
                  <td className="num mono">{fmtP(p.disbursed, p.nativeCurrency)}</td>
                  <td className="num mono">{p.sites}</td>
                  <td>
                    {p.risk === "ok" && <span className="pill green dot">{lang === "fr" ? "OK" : "OK"}</span>}
                    {p.risk === "warn" && <span className="pill amber dot">{lang === "fr" ? "Vigilance" : "Watch"}</span>}
                    {p.risk === "bad" && <span className="pill red dot">{lang === "fr" ? "Élevé" : "High"}</span>}
                  </td>
                  {isSuperAdmin && (
                    <td onClick={(e) => e.stopPropagation()}>
                      <div className="row gap-xs">
                        <button className="btn xs ghost"
                          onClick={() => setLeadProject(p)}
                          title={lang === "fr" ? "Définir le responsable de projet" : "Set project lead"}
                          style={{ fontSize: 10 }}>
                          👤 {lang === "fr" ? "Resp." : "Lead"}
                        </button>
                        <button className="btn xs ghost"
                          onClick={() => setTransferProject(p)}
                          title={lang === "fr" ? "Transférer vers une autre organisation" : "Transfer to another organization"}
                          style={{ fontSize: 10 }}>
                          🔁 {lang === "fr" ? "Transférer" : "Transfer"}
                        </button>
                      </div>
                    </td>
                  )}
                </tr>
              );})}
            </tbody>
          </table>
        </div>
      </div>
      {createOpen && (
        <CreateProjectModal lang={lang} onClose={() => setCreateOpen(false)} onCreated={refresh}
          isSuperAdmin={isSuperAdmin} allOrgs={allOrgs} />
      )}
      {transferProject && (
        <TransferProjectModal lang={lang} project={transferProject}
          allOrgs={allOrgs}
          onClose={() => setTransferProject(null)}
          onTransferred={refresh} />
      )}
      {leadProject && (
        <SetProjectLeadModal lang={lang} project={leadProject}
          allOrgs={allOrgs}
          onClose={() => setLeadProject(null)}
          onSaved={refresh} />
      )}
    </div>
  );
}

// ── Set / change a project's lead user (responsable) ─────────────────────────
// Lists the project's org members and lets the user pick one. Super-admin
// can call this on any project (RLS allows). For a regular admin, the
// project must be in their own org.
function SetProjectLeadModal({ lang, project, allOrgs, onClose, onSaved }) {
  // useOrgMembers(orgId) — pull members of the project's org explicitly,
  // not the caller's (super-admin can be in another org). For a regular
  // admin, project.organizationId IS their org so this is a no-op.
  const { members } = window.melr.useOrgMembers(project.organizationId);
  const targetOrg = (allOrgs || []).find((o) => o.id === project.organizationId);
  const [pickedId, setPickedId] = useState("");
  const [busy, setBusy] = useState(false);
  const [err, setErr]   = useState(null);

  const onConfirm = async () => {
    setBusy(true); setErr(null);
    try {
      // pickedId may be empty string = clear the lead
      await window.melr.updateProject(project.uuid, { lead_user_id: pickedId || null });
      if (onSaved) await onSaved();
      onClose();
    } catch (e) { setErr(e.message); }
    finally { setBusy(false); }
  };

  return (
    <Modal
      title={<>👤 {lang === "fr" ? "Définir le responsable du projet" : "Set project lead"}</>}
      onClose={onClose}
      footer={<>
        <button type="button" className="btn sm ghost" onClick={onClose} disabled={busy}>
          {lang === "fr" ? "Annuler" : "Cancel"}
        </button>
        <button type="button" className="btn sm primary" onClick={onConfirm} disabled={busy || !members || members.length === 0}>
          {busy ? "…" : (lang === "fr" ? "Enregistrer" : "Save")}
        </button>
      </>}>
      <div style={{ display: "grid", gap: 12 }}>
        <div>
          <div className="text-faint" style={{ fontSize: 11, textTransform: "uppercase", letterSpacing: "0.04em" }}>
            {lang === "fr" ? "Projet" : "Project"}
          </div>
          <div className="strong" style={{ fontSize: 14 }}>
            <span className="mono">{project.id}</span> · {project.name}
          </div>
          {targetOrg && (
            <div className="text-faint" style={{ fontSize: 11, marginTop: 2 }}>
              {lang === "fr" ? "Organisation : " : "Organization: "}<b>{targetOrg.name}</b>
            </div>
          )}
        </div>
        <div className="text-faint" style={{ fontSize: 12, lineHeight: 1.55 }}>
          {lang === "fr"
            ? "Choisir un membre de l'organisation du projet comme responsable hiérarchique. Le responsable est distinct des agents de saisie (gérés via Org & rôles → Affectations aux projets)."
            : "Pick a member of the project's organization as the project lead. The lead is distinct from field agents (managed via Org & roles → Project assignments)."}
        </div>
        <div>
          <label style={{ display: "block", fontSize: 11, color: "var(--text-faint)", marginBottom: 4, textTransform: "uppercase", letterSpacing: "0.04em" }}>
            {lang === "fr" ? "Responsable" : "Lead"}
          </label>
          {!members || members.length === 0 ? (
            <div className="text-faint" style={{ padding: 8, fontSize: 12.5, border: "1px dashed var(--line)", borderRadius: 6 }}>
              {lang === "fr"
                ? "Aucun membre dans cette organisation. Affecter d'abord un utilisateur à l'organisation via Org & rôles."
                : "No member in this organization. Assign a user to the organization first via Org & roles."}
            </div>
          ) : (
            <select value={pickedId} onChange={(e) => setPickedId(e.target.value)}
              style={{ width: "100%", padding: "8px 10px", borderRadius: 6, border: "1px solid var(--line)", fontSize: 13 }}>
              <option value="">{lang === "fr" ? "— Aucun responsable —" : "— No lead —"}</option>
              {members.map((m) => (
                <option key={m.id} value={m.id}>
                  {m.full_name || m.email}{m.email && m.full_name ? " (" + m.email + ")" : ""}
                </option>
              ))}
            </select>
          )}
        </div>
        {err && (
          <div style={{ padding: "8px 10px", background: "#fee2e2", color: "#991b1b", borderRadius: 6, fontSize: 12.5 }}>{err}</div>
        )}
      </div>
    </Modal>
  );
}

// ── Transfer a project to another organization (super-admin only) ──────────
// Detaches from current programme + clears lead. Cascade on indicators,
// sites, etc. stays — they're project-scoped and follow the project.
function TransferProjectModal({ lang, project, allOrgs, onClose, onTransferred }) {
  const [targetOrgId, setTargetOrgId] = useState("");
  const [busy, setBusy] = useState(false);
  const [err, setErr]   = useState(null);

  const candidates = (allOrgs || [])
    .filter((o) => !o.archived_at && o.id !== project.organizationId);

  const onConfirm = async () => {
    if (!targetOrgId) {
      setErr(lang === "fr" ? "Choisir une organisation cible." : "Pick a target organization.");
      return;
    }
    setBusy(true); setErr(null);
    try {
      await window.melr.transferProjectToOrg(project.uuid, targetOrgId);
      if (onTransferred) await onTransferred();
      onClose();
    } catch (e) { setErr(e.message); }
    finally { setBusy(false); }
  };

  const target = candidates.find((o) => o.id === targetOrgId);

  return (
    <Modal
      title={<>🔁 {lang === "fr" ? "Transférer le projet" : "Transfer project"}</>}
      onClose={onClose}
      footer={<>
        <button type="button" className="btn sm ghost" onClick={onClose} disabled={busy}>
          {lang === "fr" ? "Annuler" : "Cancel"}
        </button>
        <button type="button" onClick={onConfirm} disabled={busy || !targetOrgId}
          style={{
            padding: "8px 14px", borderRadius: 6, border: 0,
            background: targetOrgId ? "#d97706" : "#9ca3af",
            color: "white", cursor: targetOrgId ? "pointer" : "not-allowed", fontWeight: 600,
          }}>
          {busy ? "…" : (lang === "fr"
            ? "Transférer vers " + (target ? target.name : "…")
            : "Transfer to " + (target ? target.name : "…"))}
        </button>
      </>}>
      <div style={{ display: "grid", gap: 12 }}>
        <div>
          <div className="text-faint" style={{ fontSize: 11, textTransform: "uppercase", letterSpacing: "0.04em" }}>
            {lang === "fr" ? "Projet" : "Project"}
          </div>
          <div className="strong" style={{ fontSize: 14 }}>
            <span className="mono">{project.id}</span> · {project.name}
          </div>
        </div>
        <div className="text-faint" style={{ fontSize: 12.5, lineHeight: 1.55 }}>
          {lang === "fr"
            ? "Le projet va changer d'organisation. Conséquences : (1) son lien à un programme parent est rompu (les programmes sont scopés par org) ; (2) le responsable de projet est remis à NULL — l'admin de la nouvelle org en désignera un parmi ses membres ; (3) les affectations agents → projet existantes sont effacées (les agents de l'ancienne org perdraient l'accès via RLS de toute façon). Les indicateurs, sites, saisies et audits du projet suivent."
            : "The project will change organization. Consequences: (1) the parent-programme link is dropped (programmes are org-scoped); (2) the lead is reset to NULL — the new org's admin will pick one of its members; (3) existing agent → project assignments are wiped (agents of the old org would lose access via RLS anyway). Indicators, sites, submissions and audits follow the project."}
        </div>
        <div>
          <label style={{ display: "block", fontSize: 11, color: "var(--text-faint)", marginBottom: 4, textTransform: "uppercase", letterSpacing: "0.04em" }}>
            {lang === "fr" ? "Organisation cible *" : "Target organization *"}
          </label>
          <select required value={targetOrgId} onChange={(e) => setTargetOrgId(e.target.value)}
            style={{ width: "100%", padding: "8px 10px", borderRadius: 6, border: "1px solid var(--line)", fontSize: 13 }}>
            <option value="">{lang === "fr" ? "— Choisir —" : "— Pick —"}</option>
            {candidates.map((o) => (
              <option key={o.id} value={o.id}>{o.name} ({o.slug})</option>
            ))}
          </select>
        </div>
        {err && (
          <div style={{ padding: "8px 10px", background: "#fee2e2", color: "#991b1b", borderRadius: 6, fontSize: 12.5 }}>{err}</div>
        )}
      </div>
    </Modal>
  );
}

// ==================== CREATE PROJECT MODAL ====================
function CreateProjectModal({ lang, onClose, onCreated, isSuperAdmin, allOrgs }) {
  const [code, setCode]       = useState("");
  const [nameFr, setNameFr]   = useState("");
  const [sectorId, setSectorId] = useState("sante");
  const [budget, setBudget]   = useState("");
  const [projectCurrency, setProjectCurrency] = useState("EUR");
  const [programmeId, setProgrammeId] = useState("");
  const [programmesList, setProgrammesList] = useState([]);
  // Live sectors from the DB. window.SECTORS mirror keeps legacy code in sync.
  const { data: liveSectors } = window.melr.useSectors();
  const [newSectorOpen, setNewSectorOpen] = useState(false);
  // Super-admin: target organization for the new project. NULL = caller's
  // own org (default). Hidden for regular admins.
  const { profile: meProfile } = window.melr.useCurrentProfile();
  const [targetOrgId, setTargetOrgId] = useState("");
  useEffect(() => {
    // Default the picker to the caller's own org once profile arrives
    if (isSuperAdmin && !targetOrgId && meProfile && meProfile.organization_id) {
      setTargetOrgId(meProfile.organization_id);
    }
  }, [isSuperAdmin, meProfile]);
  const [submitting, setSubmitting] = useState(false);
  const [err, setErr]         = useState(null);
  const allRates = window.melr.getMergedRates();

  // Load programmes for the parent dropdown.
  useEffect(() => {
    window.melr.fetchProgrammes().then((rows) => setProgrammesList(rows)).catch(() => {});
  }, []);

  // Use live sectors first; fall back to the static SECTORS constant if the
  // DB load is still in flight (data-sectors.jsx pre-loads the legacy 12).
  const SECTOR_CHOICES = (liveSectors && liveSectors.length > 0)
    ? liveSectors
    : ((typeof SECTORS !== "undefined" && SECTORS) || [
        { id: "sante", fr: "Santé", en: "Health" },
        { id: "port",  fr: "Gestion portuaire", en: "Port management" },
      ]);

  // Sentinel value used by the sector <select> to open the NewSectorModal.
  // When the user picks "__NEW__", we open the modal instead of setting the
  // state. After creation, the realtime channel re-fires useSectors() and the
  // new row appears in liveSectors automatically.
  const onSectorChange = (val) => {
    if (val === "__NEW__") setNewSectorOpen(true);
    else                   setSectorId(val);
  };

  const submit = async (e) => {
    e.preventDefault();
    setErr(null); setSubmitting(true);
    try {
      await window.melr.createProject({
        code: code.trim(),
        name_fr: nameFr.trim(),
        sector_id: sectorId,
        budget_native: budget || 0,
        currency: projectCurrency,
        programme_id: programmeId || null,
        // Super-admin override: target org. When null/empty, createProject
        // falls back to the caller's profile.organization_id.
        organization_id: (isSuperAdmin && targetOrgId) ? targetOrgId : null,
      });
      if (onCreated) await onCreated();
      onClose();
    } catch (e) {
      setErr(e.message);
    } finally {
      setSubmitting(false);
    }
  };

  return (
    <Modal
      size="sm"
      title={lang === "fr" ? "Nouveau projet" : "New project"}
      onClose={onClose}
      onSubmit={submit}
      footer={<>
        <button type="button" onClick={onClose} disabled={submitting} className="btn sm ghost">
          {lang === "fr" ? "Annuler" : "Cancel"}
        </button>
        <button type="submit" disabled={submitting} className="btn sm primary">
          {submitting ? "…" : (lang === "fr" ? "Créer" : "Create")}
        </button>
      </>}>
      <div style={{ display: "grid", gap: 10 }}>
        {/* Super-admin: target organization (visible only with orgs.manage).
            Defaults to the caller's own org. Regular admins don't see this. */}
        {isSuperAdmin && (allOrgs || []).length > 1 && (
          <label style={{ display: "grid", gap: 4 }}>
            <span style={{ fontSize: 12, opacity: .75 }}>
              🛡 {lang === "fr" ? "Organisation cible (super-admin)" : "Target organization (super-admin)"}
            </span>
            <select value={targetOrgId} onChange={(e) => setTargetOrgId(e.target.value)}
              style={{ padding: "8px 10px", borderRadius: 6, border: "1px solid var(--line)", background: "#eef2ff" }}>
              {(allOrgs || []).filter((o) => !o.archived_at).map((o) => (
                <option key={o.id} value={o.id}>
                  {o.name}{o.slug ? " · " + o.slug : ""}
                  {meProfile && o.id === meProfile.organization_id ? " (la vôtre)" : ""}
                </option>
              ))}
            </select>
          </label>
        )}
        <label style={{ display: "grid", gap: 4 }}>
          <span style={{ fontSize: 12, opacity: .75 }}>{lang === "fr" ? "Code (ex: P-100)" : "Code (e.g. P-100)"}</span>
          <input required value={code} onChange={(e) => setCode(e.target.value)} placeholder="P-100"
            style={{ padding: "8px 10px", borderRadius: 6, border: "1px solid var(--line)" }} />
        </label>
        <label style={{ display: "grid", gap: 4 }}>
          <span style={{ fontSize: 12, opacity: .75 }}>{lang === "fr" ? "Nom" : "Name"}</span>
          <input required value={nameFr} onChange={(e) => setNameFr(e.target.value)}
            placeholder={lang === "fr" ? "Nom du projet" : "Project name"}
            style={{ padding: "8px 10px", borderRadius: 6, border: "1px solid var(--line)" }} />
        </label>
        <label style={{ display: "grid", gap: 4 }}>
          <span style={{ fontSize: 12, opacity: .75 }}>{lang === "fr" ? "Secteur" : "Sector"}</span>
          <select value={sectorId} onChange={(e) => onSectorChange(e.target.value)}
            style={{ padding: "8px 10px", borderRadius: 6, border: "1px solid var(--line)" }}>
            {SECTOR_CHOICES.map((s) => (
              <option key={s.id} value={s.id}>{lang === "en" ? s.en : s.fr}</option>
            ))}
            <option value="__NEW__">
              {lang === "fr" ? "+ Nouveau secteur…" : "+ New sector…"}
            </option>
          </select>
        </label>
        <label style={{ display: "grid", gap: 4 }}>
          <span style={{ fontSize: 12, opacity: .75 }}>
            {lang === "fr" ? "Programme parent (optionnel)" : "Parent programme (optional)"}
          </span>
          <select value={programmeId} onChange={(e) => setProgrammeId(e.target.value)}
            style={{ padding: "8px 10px", borderRadius: 6, border: "1px solid var(--line)" }}>
            <option value="">— {lang === "fr" ? "aucun programme" : "no programme"} —</option>
            {programmesList.map((p) => (
              <option key={p.id} value={p.id}>
                {p.code} — {lang === "en" ? (p.name_en || p.name_fr) : p.name_fr}
              </option>
            ))}
          </select>
        </label>
        <div style={{ display: "grid", gridTemplateColumns: "2fr 1fr", gap: 10 }}>
          <label style={{ display: "grid", gap: 4 }}>
            <span style={{ fontSize: 12, opacity: .75 }}>{lang === "fr" ? "Budget (en millions)" : "Budget (in millions)"}</span>
            <input type="number" step="0.1" value={budget} onChange={(e) => setBudget(e.target.value)} placeholder="2.5"
              style={{ padding: "8px 10px", borderRadius: 6, border: "1px solid var(--line)" }} />
          </label>
          <label style={{ display: "grid", gap: 4 }}>
            <span style={{ fontSize: 12, opacity: .75 }}>{lang === "fr" ? "Devise native" : "Native currency"}</span>
            <select value={projectCurrency} onChange={(e) => setProjectCurrency(e.target.value)}
              style={{ padding: "8px 10px", borderRadius: 6, border: "1px solid var(--line)" }}>
              {Object.keys(allRates).map((code) => (
                <option key={code} value={code}>{code}</option>
              ))}
            </select>
          </label>
        </div>
        <div className="text-faint" style={{ fontSize: 11 }}>
          {lang === "fr"
            ? `Le budget sera stocké en ${projectCurrency}. La conversion se fera automatiquement selon la devise d'affichage.`
            : `Budget will be stored in ${projectCurrency}. Conversion happens automatically based on display currency.`}
        </div>
        {err && <div style={{ color: "#b91c1c", fontSize: 12 }}>{err}</div>}
      </div>
      {newSectorOpen && (
        <NewSectorModal lang={lang}
          onClose={() => setNewSectorOpen(false)}
          onCreated={(sec) => { setSectorId(sec.id); setNewSectorOpen(false); }} />
      )}
    </Modal>
  );
}

// Expose CreateProjectModal globally so other screens (Dashboard) can mount
// it without duplicating the form. Module load order in MELR.html puts
// app.jsx last, so by the time the Dashboard renders the modal is on window.
window.CreateProjectModal = CreateProjectModal;

function initials(name) { return name.split(/\s+/).slice(0, 2).map((s) => s[0]).join("").toUpperCase(); }
function avColor(name) {
  let h = 0; for (let i = 0; i < name.length; i++) h = (h * 31 + name.charCodeAt(i)) | 0;
  return `oklch(0.78 0.08 ${Math.abs(h) % 360})`;
}

// ==================== APP SHELL ====================
// =====================================================================
// Module catalog — single source of truth for the optional / core
// distinction. core:true modules cannot be disabled by a super-admin
// (they're essential to the platform; disabling them would brick the
// shell). Anything not flagged core is opt-in / opt-out per org.
//
// Mapping is "module_code" → list of nav-item IDs that get hidden
// when the module is disabled, plus the routes that get the disabled
// placeholder instead of the screen.
//
// Codes here MUST match the strings stored in organization_modules.
// The DB doesn't enforce this — we keep the catalog client-side so a
// new module addition doesn't require an SQL migration.
// =====================================================================
const MODULE_CATALOG = [
  // ── Core (cannot be disabled) ──────────────────────────────────────
  { code: "dashboard",     fr: "Tableau de bord",       en: "Dashboard",        core: true,  navIds: ["dashboard"],     routes: ["dashboard"] },
  { code: "projects",      fr: "Projets",               en: "Projects",         core: true,  navIds: ["projects"],      routes: ["projects", "project"] },
  { code: "programmes",    fr: "Programmes",            en: "Programmes",       core: true,  navIds: ["programmes"],    routes: ["programmes", "programme"] },
  { code: "indicators",    fr: "Indicateurs",           en: "Indicators",       core: true,  navIds: ["indicators_group", "indicators", "indicator_defs"], routes: ["indicators", "indicator_defs"] },
  { code: "workflow",      fr: "Validation",            en: "Validation",       core: true,  navIds: ["workflow"],      routes: ["workflow"] },
  { code: "notifications", fr: "Notifications",         en: "Notifications",    core: true,  navIds: ["notifications"], routes: ["notifications"] },
  { code: "reporting",     fr: "Reporting",             en: "Reporting",        core: true,  navIds: ["reporting"],     routes: ["reporting"] },
  { code: "org",           fr: "Organisation & rôles",  en: "Organization & roles", core: true, navIds: ["org"],        routes: ["org"] },
  { code: "settings",      fr: "Paramètres",            en: "Settings",         core: true,  navIds: ["settings"],      routes: ["settings"] },
  { code: "help",          fr: "Aide",                  en: "Help",             core: true,  navIds: ["help"],          routes: ["help"] },
  // ── Optional (super-admin can disable per org) ─────────────────────
  { code: "exante",        fr: "Évaluation ex-ante",    en: "Ex-ante appraisal", core: false, navIds: ["exante"],       routes: ["exante"] },
  { code: "baseline",      fr: "Baseline",              en: "Baseline",         core: false, navIds: ["baseline"],      routes: ["baseline"] },
  { code: "mobile",        fr: "Collecte mobile",       en: "Mobile collection", core: false, navIds: ["mobile"],       routes: ["mobile"] },
  { code: "audit",         fr: "Audit SAT/DVT",         en: "SAT/DVT audit",    core: false, navIds: ["audit_system", "audit_data", "audit_dashboard"], routes: ["audit_system", "audit_data", "audit_dashboard"] },
  { code: "learning",      fr: "Apprentissage",         en: "Learning",         core: false, navIds: ["learning"],      routes: ["learning"] },
  { code: "form_builder",  fr: "Constructeur de formulaires", en: "Form builder", core: false, navIds: ["form_builder"], routes: ["form_builder"] },
];

// Helpers compiled once for fast lookup at render-time.
const NAV_ID_TO_MODULE   = (() => {
  const m = {};
  for (const cat of MODULE_CATALOG) for (const id of cat.navIds) m[id] = cat.code;
  return m;
})();
const ROUTE_TO_MODULE = (() => {
  const m = {};
  for (const cat of MODULE_CATALOG) for (const r of cat.routes) m[r] = cat.code;
  return m;
})();
const MODULE_BY_CODE = (() => {
  const m = {};
  for (const cat of MODULE_CATALOG) m[cat.code] = cat;
  return m;
})();

// Helper used by the route-render block to swap a screen for a friendly
// "module disabled" panel when the org's super-admin has turned it off.
function isModuleDisabledForRoute(route, disabledSet) {
  if (!disabledSet || disabledSet.size === 0) return false;
  // Find the module code for the route (or for the route prefix, e.g.
  // "exante:capex" → "exante").
  let code = ROUTE_TO_MODULE[route];
  if (!code && typeof route === "string") {
    const colon = route.indexOf(":");
    if (colon > 0) code = ROUTE_TO_MODULE[route.slice(0, colon)];
  }
  if (!code) return false;
  return disabledSet.has(code);
}

function ModuleDisabledPanel({ lang, moduleCode }) {
  const mod = MODULE_BY_CODE[moduleCode];
  const label = mod ? (lang === "fr" ? mod.fr : mod.en) : moduleCode;
  return (
    <div className="page">
      <div className="page-header">
        <div className="page-eyebrow">{lang === "fr" ? "MODULE DÉSACTIVÉ" : "MODULE DISABLED"}</div>
        <h1 className="page-title">{label}</h1>
      </div>
      <div className="card" style={{ marginTop: 14 }}>
        <div className="card-body" style={{ padding: 28, textAlign: "center" }}>
          <div style={{ fontSize: 14, fontWeight: 600, marginBottom: 8 }}>
            {lang === "fr"
              ? "Ce module n'est pas activé pour votre organisation."
              : "This module is not enabled for your organization."}
          </div>
          <div className="muted" style={{ fontSize: 12.5, maxWidth: 540, margin: "0 auto" }}>
            {lang === "fr"
              ? "Le module a été désactivé par l'opérateur de la plateforme. Si vous pensez en avoir besoin, contactez votre administrateur ou votre interlocuteur REFT Africa pour qu'il l'active."
              : "The platform operator has disabled this module. If you need it, please contact your administrator or REFT Africa to have it enabled."}
          </div>
        </div>
      </div>
    </div>
  );
}

const NAV = [
  { group: "nav.work", items: [
    { id: "dashboard",     icon: "home",       key: "nav.dashboard",     hue: 230 },
    { id: "programmes",    icon: "layers",     key: "nav.programmes",    hue: 270 },
    // Sectors management (rename, recolor, add). Restricted to platform
    // super-admin via superAdminOnly: the Sidebar filters out items with
    // this flag when isSuperAdmin is false. The placement (right under
    // Programmes) keeps catalogue-management items grouped together
    // visually while still hiding from regular admins.
    { id: "sectors",       icon: "target",     key: "nav.sectors",       hue: 50,  superAdminOnly: true },
    { id: "projects",      icon: "folder",     key: "nav.projects",      hue: 290 },
    { id: "activities",    icon: "calendar",   key: "nav.activities",    hue: 165 },
    { id: "workflow",      icon: "badgeCheck", key: "nav.workflow",      badge: "8", hue: 155 },
    { id: "notifications", icon: "bell",       key: "nav.notifications", badge: "3", hue: 75  },
  ]},
  // Ex-ante appraisal is its own group: a collapsible parent with one
  // sub-item per CRUD card plus a top "Vue d'ensemble" anchor and a
  // "Modèle de rapport" anchor that lands on the legacy demo document
  // (kept as an example of a complete ex-ante report).
  { group: "nav.exante_group", items: [
    { id: "exante", icon: "scale", key: "nav.exante", expandable: true, hue: 200, children: [
      { id: "exante",                   key: "nav.exante.overview",         anchor: null },
      { id: "exante:decision",          key: "nav.exante.decision",         anchor: "card-decision" },
      { id: "exante:demo",              key: "nav.exante.demo",             anchor: "exante-demo-doc" },
      { divider: true, id: "div-found", key: "nav.exante.section.foundations" },
      { id: "exante:identification",    key: "nav.exante.identification",   anchor: "card-identification" },
      { id: "exante:inputs",            key: "nav.exante.inputs",           anchor: "card-inputs" },
      { id: "exante:scenarios",         key: "nav.exante.scenarios",        anchor: "card-scenarios" },
      { id: "exante:calendar",          key: "nav.exante.calendar",         anchor: "card-calendar" },
      { divider: true, id: "div-fin",   key: "nav.exante.section.financial" },
      { id: "exante:capex",             key: "nav.exante.capex",            anchor: "card-capex" },
      { id: "exante:opex",              key: "nav.exante.opex",             anchor: "card-opex" },
      { id: "exante:revenue",           key: "nav.exante.revenue",          anchor: "card-revenue" },
      { id: "exante:financing",         key: "nav.exante.financing",        anchor: "card-financing" },
      { id: "exante:findicators",       key: "nav.exante.findicators",      anchor: "card-financial-indicators" },
      { id: "exante:debt",              key: "nav.exante.debt",             anchor: "card-debt" },
      { id: "exante:pl",                key: "nav.exante.pl",               anchor: "card-pl" },
      { id: "exante:cashflow",          key: "nav.exante.cashflow",         anchor: "card-cashflow" },
      { id: "exante:stakeholders",      key: "nav.exante.stakeholders",     anchor: "card-stakeholders" },
      { divider: true, id: "div-eco",   key: "nav.exante.section.economic" },
      { id: "exante:public_finance",    key: "nav.exante.public_finance",   anchor: "card-public-finance" },
      { id: "exante:mpr_p1",            key: "nav.exante.mpr_p1",           anchor: "card-mpr-p1" },
      { id: "exante:mpr_p2",            key: "nav.exante.mpr_p2",           anchor: "card-mpr-p2" },
      { id: "exante:mpr_p3",            key: "nav.exante.mpr_p3",           anchor: "card-mpr-p3" },
      { id: "exante:eindicators",       key: "nav.exante.eindicators",      anchor: "card-economic-indicators" },
      { divider: true, id: "div-dec",   key: "nav.exante.section.decision" },
      { id: "exante:quality",           key: "nav.exante.quality",          anchor: "card-quality" },
      { id: "exante:multicriteria",     key: "nav.exante.multicriteria",    anchor: "card-multicriteria" },
      { id: "exante:institutional",     key: "nav.exante.institutional",    anchor: "card-institutional" },
      { id: "exante:sensitivity",       key: "nav.exante.sensitivity",      anchor: "card-sensitivity" },
    ]},
  ]},
  { group: "nav.modules", items: [
    { id: "indicators_group", icon: "trending", key: "nav.indicators_group", expandable: true, hue: 320, children: [
      { id: "indicator_defs", key: "nav.indicator_defs" },
      { id: "indicators",     key: "nav.indicators" },
    ]},
    { id: "baseline",     icon: "map",         key: "nav.baseline",     hue: 195 },
    { id: "mobile",       icon: "smartphone",  key: "nav.mobile",       badge: "47", hue: 145 },
    { id: "audit_system",    icon: "shieldCheck", key: "nav.audit_system",    hue: 25  },
    { id: "audit_data",      icon: "database",    key: "nav.audit_data",      badge: "3",  hue: 350 },
    { id: "audit_dashboard", icon: "trending",    key: "nav.audit_dashboard", hue: 40  },
    { id: "reporting",    icon: "fileText",    key: "nav.reporting",    hue: 250 },
    { id: "learning",     icon: "brain",       key: "nav.learning",     hue: 285 },
  ]},
  { group: "nav.admin", items: [
    { id: "organizations", icon: "layers",   key: "nav.organizations", hue: 195 },
    // Organisation & rôles expanded into 6 sub-routes. Same convention
    // as ex-ante: parent id "org" renders the legacy all-in-one view;
    // children "org:matrix", "org:members", … render only that section.
    { id: "org", icon: "users", key: "nav.org", expandable: true, hue: 215, children: [
      { id: "org",                  key: "nav.org.overview" },
      { id: "org:matrix",           key: "nav.org.matrix" },
      { id: "org:members",          key: "nav.org.members" },
      { id: "org:project_agents",   key: "nav.org.project_agents" },
      { id: "org:pending",          key: "nav.org.pending" },
      { id: "org:invitations",      key: "nav.org.invitations" },
      { id: "org:modules",          key: "nav.org.modules",  superAdminOnly: true },
    ]},
    { id: "form_builder",  icon: "fileText", key: "nav.form_builder",  hue: 200 },
    { id: "settings",      icon: "settings", key: "nav.settings",      hue: 240 },
    { id: "help",          icon: "info",     key: "nav.help",          hue: 175 },
  ]},
];

const ROLES = [
  { id: "admin", fr: "Administrateur", en: "Administrator", scope: "global", color: "var(--violet)" },
  { id: "lead", fr: "Chef de projet", en: "Project lead", scope: "P-241", color: "var(--accent)" },
  { id: "me", fr: "Responsable S&E", en: "M&E officer", scope: "P-241, P-188", color: "var(--green)" },
  { id: "data", fr: "Saisie terrain", en: "Field data entry", scope: "11 sites", color: "var(--amber)" },
  { id: "donor", fr: "Bailleur (lecture)", en: "Donor (read-only)", scope: "AFD portfolio", color: "var(--text-faint)" },
];

// Mobile-only route detection. Anything in (?mobile=1) or (#mobile)
// short-circuits the desktop shell so installed PWAs and phones land
// straight on the data-entry surface. Re-checked on hashchange / popstate
// so navigation works in both directions without a reload.
function isMobileRoute() {
  if (typeof window === "undefined") return false;
  try {
    const params = new URLSearchParams(window.location.search);
    if (params.get("mobile") === "1") return true;
  } catch (_) {}
  return window.location.hash === "#mobile" || window.location.hash === "#/mobile";
}

// Top-level router: decides which surface to mount. Lives above App and
// MobileApp so each child has a stable hook order regardless of route.
function AppRouter() {
  const [mobileRoute, setMobileRoute] = useState(isMobileRoute());
  useEffect(() => {
    const onNav = () => setMobileRoute(isMobileRoute());
    window.addEventListener("hashchange", onNav);
    window.addEventListener("popstate", onNav);
    return () => {
      window.removeEventListener("hashchange", onNav);
      window.removeEventListener("popstate", onNav);
    };
  }, []);
  const [mobileLang, setMobileLang] = useState(() => {
    try { return localStorage.getItem("melr.lang") || "fr"; } catch (_) { return "fr"; }
  });
  useEffect(() => { try { localStorage.setItem("melr.lang", mobileLang); } catch (_) {} }, [mobileLang]);
  const exitMobile = () => {
    try {
      const url = new URL(window.location.href);
      url.searchParams.delete("mobile");
      url.hash = "";
      window.history.replaceState(null, "", url.toString());
    } catch (_) {}
    // Remember the user's choice so the auto-redirect in main.jsx doesn't
    // immediately bounce them back to mobile on the next page load.
    if (window.melr && window.melr.preferDesktop) window.melr.preferDesktop();
    setMobileRoute(false);
  };
  if (mobileRoute) {
    return <window.MobileApp lang={mobileLang} setLang={setMobileLang} onExit={exitMobile} />;
  }
  return <App />;
}

function App() {
  const [tweaks, setTweak] = useTweaks(/*EDITMODE-BEGIN*/{
    "lang": "fr",
    "accent": "indigo",
    "density": "default",
    "sidebar": "expanded",
    "dark": false
  }/*EDITMODE-END*/);
  const [route, setRouteRaw] = useState("dashboard");
  const [selectedProject, setSelectedProject] = useState("P-241");
  const [selectedProgramme, setSelectedProgramme] = useState(null);
  const [exanteSection, setExanteSection] = useState(null);
  const [role, setRole] = useState(ROLES[1]);
  const [roleOpen, setRoleOpen] = useState(false);
  const [cmdOpen, setCmdOpen] = useState(false);

  // Intercept ex-ante sub-routes ("exante:<sub>"). When clicked, we
  // resolve the sub-id to a DOM element id (anchor) and switch to the
  // unified "exante" screen with exanteSection set so the ExAnte
  // component scrolls to that element.
  const EXANTE_SUB_MAP = {
    // Top-level shortcuts
    "exante:decision":          "card-decision",
    "exante:demo":              "exante-demo-doc",
    // CRUD card anchors
    "exante:identification":    "card-identification",
    "exante:inputs":            "card-inputs",
    "exante:scenarios":         "card-scenarios",
    "exante:calendar":          "card-calendar",
    "exante:capex":             "card-capex",
    "exante:opex":              "card-opex",
    "exante:revenue":           "card-revenue",
    "exante:financing":         "card-financing",
    "exante:findicators":       "card-financial-indicators",
    "exante:debt":              "card-debt",
    "exante:pl":                "card-pl",
    "exante:cashflow":          "card-cashflow",
    "exante:stakeholders":      "card-stakeholders",
    "exante:public_finance":    "card-public-finance",
    "exante:mpr_p1":            "card-mpr-p1",
    "exante:mpr_p2":            "card-mpr-p2",
    "exante:mpr_p3":            "card-mpr-p3",
    "exante:eindicators":       "card-economic-indicators",
    "exante:quality":           "card-quality",
    "exante:multicriteria":     "card-multicriteria",
    "exante:institutional":     "card-institutional",
    "exante:sensitivity":       "card-sensitivity",
  };
  const setRoute = (r) => {
    if (typeof r === "string" && r.startsWith("exante:")) {
      setExanteSection(EXANTE_SUB_MAP[r] || null);
      setRouteRaw(r); // keep the sub-id so the sidebar highlights it
      return;
    }
    // Plain ex-ante = overview (default to first sub-section "ii")
    if (r === "exante") {
      setExanteSection(null);
    }
    setRouteRaw(r);
  };

  const t = useMemo(() => window.makeT(tweaks.lang), [tweaks.lang]);
  window.__navigate = setRoute;

  // Apply tweaks to root
  useEffect(() => {
    const root = document.documentElement;
    const accents = {
      indigo: { h: 256, c: 0.16, l: 0.51 },
      teal:   { h: 195, c: 0.13, l: 0.48 },
      slate:  { h: 250, c: 0.04, l: 0.32 },
      rose:   { h: 12,  c: 0.16, l: 0.55 },
    };
    const a = accents[tweaks.accent] || accents.indigo;
    root.style.setProperty("--accent-h", a.h);
    root.style.setProperty("--accent-c", a.c);
    root.style.setProperty("--accent-l", a.l);
    root.style.setProperty("--density", tweaks.density === "compact" ? 0.88 : tweaks.density === "comfortable" ? 1.12 : 1);
    // Dark theme: set BOTH the legacy `.dark` class (a handful of CSS
    // rules in styles.css still target it for nav-item tinting) AND the
    // [data-theme="dark"] attribute (the bulk of dark-mode overrides —
    // --bg, --text, --accent-soft, --text-on-soft, etc. — live there).
    // Setting only the class silently produced unreadable pills/inputs.
    root.classList.toggle("dark", !!tweaks.dark);
    if (tweaks.dark) root.setAttribute("data-theme", "dark");
    else             root.removeAttribute("data-theme");
    root.classList.toggle("sidebar-rail", tweaks.sidebar === "rail");
  }, [tweaks]);

  useEffect(() => {
    const onKey = (e) => {
      if ((e.metaKey || e.ctrlKey) && e.key === "k") { e.preventDefault(); setCmdOpen((v) => !v); }
      if (e.key === "Escape") setCmdOpen(false);
    };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, []);

  const lang = tweaks.lang;

  // ─── Global acting-as-org context (super-admin only) ───────────────────
  // Stored at App level + persisted in localStorage so it survives reloads
  // and propagates to every screen via screenProps. NULL = user's home org
  // (default behaviour for everyone, no-op for non-super-admins).
  const { has: hasPerm } = window.melr.useCurrentUserPermissions();
  const isSuperAdmin = hasPerm && hasPerm("orgs.manage");
  const { profile: meProfile } = window.melr.useCurrentProfile();
  const [actingOrgId, setActingOrgIdRaw] = useState(() => {
    try { return localStorage.getItem("melr.acting-org") || null; } catch (_) { return null; }
  });
  const setActingOrgId = (v) => {
    setActingOrgIdRaw(v || null);
    try {
      if (v) localStorage.setItem("melr.acting-org", v);
      else   localStorage.removeItem("melr.acting-org");
    } catch (_) {}
  };
  // Non-super-admins are always implicitly on their home org. We clear the
  // stored value defensively so a former super-admin who lost the perm
  // doesn't carry around a stale acting context.
  useEffect(() => {
    if (!isSuperAdmin && actingOrgId) setActingOrgId(null);
  }, [isSuperAdmin]);

  // ─── Multi-org membership: 'Active org' (persistent user choice) ───────
  // Distinct from actingOrgId (super-admin temporary cross-org admin).
  // A user with > 1 membership can pick which one is currently active;
  // their choice is stored in localStorage and applies to every screen.
  // For mono-org users (the vast majority), this is a no-op — activeOrgId
  // falls back to the home org from profile.
  const { data: myMemberships } = window.melr.useUserMemberships();
  const [activeOrgIdRaw, setActiveOrgIdState] = window.melr.useActiveOrgId();
  // Active org defaults to the home org when no explicit choice or when
  // the stored choice is no longer a valid membership.
  const activeOrgId = useMemo(() => {
    const home = meProfile && meProfile.organization_id;
    if (!activeOrgIdRaw) return home;
    const stillMember = (myMemberships || []).some((m) => m.organization_id === activeOrgIdRaw);
    return stillMember ? activeOrgIdRaw : home;
  }, [activeOrgIdRaw, myMemberships, meProfile]);
  // Defensive cleanup: if stored choice is stale, clear it.
  useEffect(() => {
    if (activeOrgIdRaw && activeOrgId !== activeOrgIdRaw) setActiveOrgIdState(null);
  }, [activeOrgIdRaw, activeOrgId]);

  // Effective org for screens: super-admin acting > user-chosen active >
  // profile home org. Most code paths just need this one number.
  const effectiveOrgId = (isSuperAdmin && actingOrgId)
    ? actingOrgId
    : (activeOrgId || (meProfile && meProfile.organization_id));

  // Per-org module flags. The Set holds the codes of modules the
  // super-admin has explicitly DISABLED for the effective org —
  // default-ON convention means absence of a row = module enabled.
  // Realtime subscription inside the hook means a toggle from the
  // admin UI propagates here (and to the Sidebar / route gate) in a
  // few hundred ms without a reload.
  const { disabledSet: disabledModules } = window.melr.useDisabledModules(effectiveOrgId);

  // Convenience derived flags that every screen can read from screenProps,
  // so individual components don't have to wire useCurrentUserPermissions
  // themselves. isAdmin = has users.manage permission OR super-admin.
  // canEditIndicators is a finer-grained flag for the indicators screen.
  // Both are FALSE when permissions are still loading — fail closed.
  const isAdmin            = isSuperAdmin || (hasPerm && hasPerm("users.manage"));
  const canEditIndicators  = isSuperAdmin || (hasPerm && (hasPerm("users.manage") || hasPerm("indicators.edit")));

  const screenProps = {
    t, lang,
    onOpen: (id) => {
      if (id === "projects-list")   { setRoute("projects"); return; }
      if (id === "programmes-list") { setRoute("programmes"); return; }
      // Programme drill-down uses the prefix "prog:<uuid>"
      if (typeof id === "string" && id.startsWith("prog:")) {
        setSelectedProgramme(id.slice(5));
        setRoute("programme");
        return;
      }
      setSelectedProject(id);
      setRoute("project");
    },
    selectedProject,
    selectedProgramme,
    // Super-admin context — every screen can opt in to scope by it
    isSuperAdmin,
    hasPerm,
    isAdmin,
    canEditIndicators,
    actingOrgId,
    setActingOrgId,
    // Multi-org membership — every screen reads activeOrgId via effectiveOrgId
    activeOrgId,
    setActiveOrgId: setActiveOrgIdState,
    myMemberships,
    effectiveOrgId,
    myOrgId: meProfile && meProfile.organization_id,
  };

  return (
    <div className="app" data-screen-label={"App / " + route}>
      <Sidebar route={route} setRoute={setRoute} t={t} lang={lang} role={role} isSuperAdmin={isSuperAdmin} disabledModules={disabledModules}
        collapsed={tweaks.sidebar === "rail"}
        onToggleCollapsed={() => setTweak("sidebar", tweaks.sidebar === "rail" ? "expanded" : "rail")} />
      <div className="main">
        <Topbar t={t} lang={lang} setLang={(l) => setTweak("lang", l)} role={role} setRole={setRole} roleOpen={roleOpen} setRoleOpen={setRoleOpen} setCmdOpen={setCmdOpen} route={route} setRoute={setRoute} dark={tweaks.dark} setDark={(v) => setTweak("dark", v)}
          isSuperAdmin={isSuperAdmin} actingOrgId={actingOrgId} setActingOrgId={setActingOrgId}
          myOrgId={meProfile && meProfile.organization_id}
          activeOrgId={activeOrgId} setActiveOrgId={setActiveOrgIdState}
          myMemberships={myMemberships} />
          <div className="scroll">{(() => {
            // Module gate: if the route maps to a disabled module AND
            // the user is not a super-admin, swap the whole screen for
            // a friendly placeholder. Super-admin always sees the real
            // screen so they can act / audit / re-enable from anywhere.
            if (!isSuperAdmin && isModuleDisabledForRoute(route, disabledModules)) {
              let code = ROUTE_TO_MODULE[route];
              if (!code && typeof route === "string") {
                const i = route.indexOf(":");
                if (i > 0) code = ROUTE_TO_MODULE[route.slice(0, i)];
              }
              return <ModuleDisabledPanel lang={lang} moduleCode={code} />;
            }
            // Otherwise: the regular per-route render.
            return (<>
              {route === "dashboard" && <Dashboard {...screenProps} />}
              {route === "projects" && <Projects {...screenProps} />}
              {route === "programmes" && <ProgrammesPage {...screenProps} />}
              {route === "programme" && <ProgrammeDetail {...screenProps} onBack={() => setRoute("programmes")} />}
              {route === "sectors" && <window.SectorsManagement {...screenProps} />}
              {route === "project" && <ProjectDetail {...screenProps} onBack={() => setRoute("projects")} />}
              {route === "indicators" && <Indicators {...screenProps} setRoute={setRoute} />}
              {route === "indicator_defs" && <window.IndicatorDefinitions {...screenProps} />}
              {route === "activities" && (
                window.Activities
                  ? <window.Activities {...screenProps} />
                  : <div style={{ padding: 24 }}>
                      <div className="card" style={{ borderColor: "oklch(0.75 0.18 25)" }}>
                        <div className="card-body">
                          <div style={{ fontWeight: 600, marginBottom: 6 }}>
                            ⚠ {lang === "fr" ? "Écran Activités indisponible" : "Activities screen unavailable"}
                          </div>
                          <div style={{ fontSize: 12.5, color: "var(--text-muted)" }}>
                            {lang === "fr"
                              ? "Le module screens-activities.jsx n'a pas pu se charger. Vérifier la console (F12) pour le message d'erreur Babel/JSX."
                              : "screens-activities.jsx failed to load. Check the console (F12) for the Babel/JSX error message."}
                          </div>
                        </div>
                      </div>
                    </div>
              )}
              {route === "baseline" && <Baseline {...screenProps} />}
              {route === "mobile" && <MobileField {...screenProps} />}
              {(route === "exante" || (typeof route === "string" && route.startsWith("exante:"))) && <ExAnte {...screenProps} activeSection={exanteSection} />}
              {route === "workflow" && <Workflow {...screenProps} />}
              {route === "audit_system" && <AuditSystem {...screenProps} />}
              {route === "audit_data" && <AuditData {...screenProps} />}
              {route === "audit_dashboard" && <AuditDashboard {...screenProps} setRoute={setRoute} />}
              {route === "reporting" && <Reporting {...screenProps} />}
              {route === "notifications" && <Notifications {...screenProps} setRoute={setRoute} />}
              {route === "learning" && <Learning {...screenProps} />}
              {(route === "org" || (typeof route === "string" && route.startsWith("org:"))) && (
                <OrgRoles {...screenProps}
                  orgSubRoute={typeof route === "string" && route.startsWith("org:") ? route.slice(4) : null} />
              )}
              {route === "organizations" && <window.Organizations {...screenProps} />}
              {route === "form_builder" && <window.FormBuilder {...screenProps} />}
              {route === "settings" && <SettingsScreen {...screenProps} setRoute={setRoute} tweaks={tweaks} setTweak={setTweak} />}
              {route === "help" && <Help {...screenProps} />}
              {route === "profile" && <ProfileScreen {...screenProps} setLang={(l) => setTweak("lang", l)} />}
            </>);
          })()}</div>
      </div>
      {cmdOpen && <CommandPalette t={t} lang={lang} onClose={() => setCmdOpen(false)} setRoute={(r) => { setRoute(r); setCmdOpen(false); }} />}
      <TweaksPanel title="Tweaks">
        <TweakSection title={lang === "fr" ? "Apparence" : "Appearance"}>
          <TweakColor label={lang === "fr" ? "Accent" : "Accent"} value={tweaks.accent} onChange={(v) => setTweak("accent", v)}
            options={[
              { value: "indigo", color: "oklch(0.51 0.16 256)", label: "Indigo" },
              { value: "teal", color: "oklch(0.48 0.13 195)", label: "Teal" },
              { value: "slate", color: "oklch(0.32 0.04 250)", label: "Slate" },
              { value: "rose", color: "oklch(0.55 0.16 12)", label: "Rose" },
            ]} />
          <TweakToggle label={lang === "fr" ? "Mode sombre" : "Dark mode"} value={tweaks.dark} onChange={(v) => setTweak("dark", v)} />
        </TweakSection>
        <TweakSection title={lang === "fr" ? "Densité" : "Density"}>
          <TweakRadio label={lang === "fr" ? "Espacement" : "Spacing"} value={tweaks.density} onChange={(v) => setTweak("density", v)}
            options={[
              { value: "compact", label: lang === "fr" ? "Compact" : "Compact" },
              { value: "default", label: lang === "fr" ? "Normal" : "Normal" },
              { value: "comfortable", label: lang === "fr" ? "Aéré" : "Comfortable" },
            ]} />
        </TweakSection>
        <TweakSection title={lang === "fr" ? "Mise en page" : "Layout"}>
          <TweakRadio label={lang === "fr" ? "Barre latérale" : "Sidebar"} value={tweaks.sidebar} onChange={(v) => setTweak("sidebar", v)}
            options={[
              { value: "expanded", label: lang === "fr" ? "Développée" : "Expanded" },
              { value: "rail", label: lang === "fr" ? "Compacte" : "Rail" },
            ]} />
          <TweakRadio label={lang === "fr" ? "Langue" : "Language"} value={tweaks.lang} onChange={(v) => setTweak("lang", v)}
            options={[{ value: "fr", label: "Français" }, { value: "en", label: "English" }]} />
        </TweakSection>
      </TweaksPanel>
    </div>
  );
}

function Sidebar({ route, setRoute, t, lang, role, isSuperAdmin, disabledModules, collapsed, onToggleCollapsed }) {
  // Per-group expanded state for items that have children (currently
  // ex-ante). Default: expanded when the current route belongs to one of
  // the children, collapsed otherwise.
  const [expanded, setExpanded] = useState(() => ({}));
  const toggleExpand = (id) => setExpanded((s) => ({ ...s, [id]: !s[id] }));
  // Permission-gate the NAV: items flagged `superAdminOnly` only render
  // when the caller is a platform super-admin. Module-gate: items whose
  // nav id maps to a disabled module (via NAV_ID_TO_MODULE) are also
  // hidden — except for the super-admin themselves, who should always
  // see every nav entry so they can audit / re-enable from anywhere.
  const visibleItems = (items) => (items || []).filter((it) => {
    if (it.superAdminOnly && !isSuperAdmin) return false;
    if (disabledModules && !isSuperAdmin) {
      const code = NAV_ID_TO_MODULE[it.id];
      if (code && disabledModules.has(code)) return false;
    }
    return true;
  });
  // Recompute "is on a sub-item of this expandable" so we can auto-expand.
  const isSubRoute = (it) => {
    if (!it.children) return false;
    // Match by id prefix: route starts with "exante:" or equals "exante".
    return it.children.some((c) => c.id === route);
  };

  return (
    <aside className="sidebar" data-collapsed={collapsed ? "true" : "false"}>
      <div className="brand">
        <div className="brand-logo" style={{ background: "white", overflow: "hidden", padding: 2 }}>
          <img src="/abas-group.png" alt="ABAS Group" style={{ width: "100%", height: "100%", objectFit: "contain", display: "block" }} />
        </div>
        <div className="brand-text">
          <div className="brand-name">MELR</div>
          <div className="brand-sub">{lang === "fr" ? "S&E multi-secteurs" : "Multi-sector M&E"}</div>
        </div>
        <div className="brand-version">v3.4</div>
        <button className="sidebar-toggle"
          onClick={onToggleCollapsed}
          aria-label={collapsed
            ? (lang === "fr" ? "Développer la barre latérale" : "Expand sidebar")
            : (lang === "fr" ? "Réduire la barre latérale" : "Collapse sidebar")}
          title={collapsed
            ? (lang === "fr" ? "Développer la barre latérale" : "Expand sidebar")
            : (lang === "fr" ? "Réduire la barre latérale" : "Collapse sidebar")}>
          <Icon.chevronDown className="sm" style={{
            transition: "transform .2s",
            transform: collapsed ? "rotate(-90deg)" : "rotate(90deg)",
          }} />
        </button>
      </div>

      <div className="sidebar-scroll">
      {NAV.map((g) => (
        <div key={g.group} className="nav-group">
          <div className="nav-group-h">{t(g.group)}</div>
          {visibleItems(g.items).map((it) => {
            const Ic = Icon[it.icon];
            // Expandable parent (e.g. Ex-ante)
            if (it.expandable && it.children) {
              const open = expanded[it.id] !== undefined ? expanded[it.id] : isSubRoute(it);
              const parentActive = route === it.id || isSubRoute(it);
              const hueStyle = it.hue != null ? { "--nav-hue": it.hue } : null;
              return (
                <React.Fragment key={it.id}>
                  <button className={"nav-item" + (parentActive ? " active" : "")}
                    data-hue={it.hue != null ? it.hue : undefined}
                    style={hueStyle}
                    title={collapsed ? t(it.key) : undefined}
                    onClick={() => {
                      // In collapsed mode, navigating to the parent route is
                      // more useful than just toggling the children (which are
                      // hidden anyway). For Ex-ante that means open the overview.
                      if (collapsed) { setRoute(it.id); return; }
                      toggleExpand(it.id);
                    }}>
                    <Ic />
                    <span className="nav-label">{t(it.key)}</span>
                    {it.badge && <span className="nav-badge">{it.badge}</span>}
                    <Icon.chevronDown className="sm" style={{
                      marginLeft: it.badge ? 4 : "auto",
                      transition: "transform .15s",
                      transform: open ? "rotate(0deg)" : "rotate(-90deg)",
                      opacity: 0.6,
                    }} />
                  </button>
                  {open && !collapsed && (
                    <div style={{ display: "grid", paddingLeft: 8, marginTop: -2 }}>
                      {it.children.filter((c) => !c.superAdminOnly || isSuperAdmin).map((c) => {
                        if (c.divider) {
                          return (
                            <div key={c.id} style={{
                              paddingLeft: 30, paddingTop: 8, paddingBottom: 4,
                              fontSize: 10, fontWeight: 600,
                              color: "var(--text-faint)",
                              textTransform: "uppercase", letterSpacing: "0.04em",
                            }}>
                              {t(c.key)}
                            </div>
                          );
                        }
                        return (
                          <button key={c.id + (c.anchor || "")}
                            className={"nav-item nav-sub" + (route === c.id ? " active" : "")}
                            data-hue={it.hue != null ? it.hue : undefined}
                            onClick={() => setRoute(c.id)}
                            style={{
                              "--nav-hue": it.hue,
                              paddingLeft: 30, fontSize: 12,
                              opacity: route === c.id ? 1 : 0.78,
                            }}>
                            <span className="nav-label" style={{ textAlign: "left" }}>
                              {t(c.key)}
                            </span>
                          </button>
                        );
                      })}
                    </div>
                  )}
                </React.Fragment>
              );
            }
            // Plain item
            const hueStyle = it.hue != null ? { "--nav-hue": it.hue } : null;
            return (
              <button key={it.id}
                className={"nav-item" + (route === it.id ? " active" : "")}
                data-hue={it.hue != null ? it.hue : undefined}
                style={hueStyle}
                title={collapsed ? t(it.key) : undefined}
                onClick={() => setRoute(it.id)}>
                <Ic />
                <span className="nav-label">{t(it.key)}</span>
                {it.badge && <span className="nav-badge">{it.badge}</span>}
              </button>
            );
          })}
        </div>
      ))}
      </div>

      <div className="sidebar-footer">
        <div className="role-card">
          <div className="row gap-sm">
            <div className="role-dot" style={{ background: role.color }}></div>
            <div style={{ flex: 1, minWidth: 0 }}>
              <div className="role-name">{lang === "fr" ? role.fr : role.en}</div>
              <div className="role-scope">{role.scope}</div>
            </div>
          </div>
        </div>
      </div>
    </aside>
  );
}

// Small violet pill shown in the Topbar when a super-admin is actively
// "acting as" another organization. Clicking the ✕ clears the context
// and returns to the user's home org.
function SuperAdminActingPill({ lang, actingOrgId, onClear }) {
  const { data: allOrgs } = window.melr.useAllOrganizations();
  const org = (allOrgs || []).find((o) => o.id === actingOrgId);
  if (!org) return null;
  return (
    <div style={{
      display: "inline-flex", alignItems: "center", gap: 6,
      padding: "4px 10px", borderRadius: 999, fontSize: 11.5, fontWeight: 600,
      background: "#eef2ff", color: "#3730a3", border: "1px solid #a5b4fc",
      marginLeft: 12,
    }} title={lang === "fr" ? "Mode super-admin · contexte actif" : "Super-admin mode · active context"}>
      <span>🛡 {lang === "fr" ? "Agit sur" : "Acting on"} : <b>{org.name}</b></span>
      <button onClick={onClear}
        title={lang === "fr" ? "Revenir à mon organisation" : "Back to my organization"}
        style={{ background: "transparent", border: 0, color: "#3730a3", cursor: "pointer", padding: 0, fontSize: 13, lineHeight: 1 }}>
        ✕
      </button>
    </div>
  );
}

// Compact pill+dropdown shown in the Topbar. Lets users with multiple
// organization_memberships switch the active org; for super-admins with a
// single membership, it ALSO offers a 'Join another org' action so they can
// self-grant a membership without going through the violet 'Acting on' pill
// + 'Add existing member' flow. Distinct from the super-admin acting-as
// pill: this represents permanent memberships, persisted in localStorage.
function ActiveOrgSwitcher({ lang, myMemberships, activeOrgId, setActiveOrgId, myOrgId, isSuperAdmin }) {
  const [open, setOpen] = useState(false);
  const [joinOpen, setJoinOpen] = useState(false);
  // Filter out archived orgs from the picker.
  const visible = (myMemberships || []).filter((m) => !m.orgArchived);
  // Show the pill when:
  //   • user has > 1 membership (the switcher is useful), OR
  //   • user is super-admin (so we can offer the 'Join another org' action).
  // Hidden for regular single-org users — they'd see a static pill with no
  // useful action, which adds visual noise.
  if (visible.length <= 1 && !isSuperAdmin) return null;
  const current = visible.find((m) => m.organization_id === activeOrgId) || visible.find((m) => m.isHome) || visible[0];
  return (
    <div style={{ position: "relative", marginLeft: 12 }}>
      <button onClick={() => setOpen((v) => !v)}
        title={lang === "fr" ? "Changer d'organisation active" : "Switch active organization"}
        style={{
          display: "inline-flex", alignItems: "center", gap: 6,
          padding: "4px 10px", borderRadius: 999, fontSize: 11.5, fontWeight: 600,
          background: "#ecfeff", color: "#155e75", border: "1px solid #67e8f9",
          cursor: "pointer",
        }}>
        <span>🏢 <b>{current ? current.orgName : "—"}</b></span>
        {current && current.isHome && (
          <span style={{ fontSize: 9.5, opacity: 0.7, fontWeight: 400 }}>
            ({lang === "fr" ? "home" : "home"})
          </span>
        )}
        <Icon.chevronDown className="sm" />
      </button>
      {open && (
        <>
          <div className="overlay-bg" onClick={() => setOpen(false)}></div>
          <div style={{
            position: "absolute", top: "calc(100% + 6px)", right: 0, zIndex: 50,
            minWidth: 280, background: "var(--bg, white)", border: "1px solid var(--line)",
            borderRadius: 8, boxShadow: "0 10px 25px rgba(0,0,0,0.08)", padding: 6,
          }}>
            <div style={{ padding: "8px 10px 4px 10px", fontSize: 10.5, fontWeight: 600, color: "var(--text-faint)", textTransform: "uppercase", letterSpacing: "0.04em" }}>
              {lang === "fr" ? "Organisation active" : "Active organization"}
            </div>
            {visible.map((m) => {
              const isActive = current && m.organization_id === current.organization_id;
              return (
                <button key={m.id}
                  onClick={() => {
                    // Picking the home org clears the explicit choice so the
                    // default (home) behaviour resumes naturally.
                    if (m.organization_id === myOrgId) setActiveOrgId(null);
                    else                                setActiveOrgId(m.organization_id);
                    setOpen(false);
                  }}
                  style={{
                    display: "flex", alignItems: "center", gap: 8, width: "100%",
                    padding: "8px 10px", border: 0, borderRadius: 6,
                    background: isActive ? "#ecfeff" : "transparent",
                    color: "var(--text)", cursor: "pointer", textAlign: "left",
                  }}>
                  <span style={{ flex: 1, fontSize: 13, fontWeight: isActive ? 600 : 500 }}>{m.orgName}</span>
                  {m.isHome && (
                    <span style={{ fontSize: 10, color: "#0e7490", background: "#cffafe", padding: "1px 6px", borderRadius: 4 }}>
                      {lang === "fr" ? "home" : "home"}
                    </span>
                  )}
                  {isActive && <Icon.check className="sm" />}
                </button>
              );
            })}
            {/* Explainer when the user only has 1 membership: */}
            {visible.length === 1 && (
              <div style={{ padding: "8px 10px", fontSize: 10.5, color: "var(--text-faint)", lineHeight: 1.5, background: "var(--bg-sunken, #f8fafc)", borderRadius: 4, marginTop: 4 }}>
                {lang === "fr"
                  ? "Vous n'avez qu'une seule appartenance. Pour basculer sur une autre org, ajoutez-vous comme membre ci-dessous ou utilisez le pill violet super-admin dans Org & rôles."
                  : "You have a single membership. To switch to another org, add yourself as a member below or use the violet super-admin pill in Org & roles."}
              </div>
            )}
            {/* Super-admin self-join shortcut: */}
            {isSuperAdmin && (
              <button onClick={() => { setOpen(false); setJoinOpen(true); }}
                style={{
                  display: "flex", alignItems: "center", gap: 8, width: "100%",
                  padding: "8px 10px", border: 0, borderRadius: 6,
                  marginTop: 4,
                  background: "transparent", color: "#0891b2", cursor: "pointer", textAlign: "left",
                  fontWeight: 600, fontSize: 12.5,
                  borderTop: "1px solid var(--line-faint)",
                }}>
                <Icon.plus className="sm" />
                <span>{lang === "fr" ? "Rejoindre une autre organisation" : "Join another organization"}</span>
              </button>
            )}
            <div style={{ padding: "6px 10px 4px 10px", fontSize: 10.5, color: "var(--text-faint)", borderTop: "1px solid var(--line-faint)", marginTop: 4 }}>
              {lang === "fr"
                ? "Votre choix est mémorisé dans ce navigateur."
                : "Your choice is remembered in this browser."}
            </div>
          </div>
        </>
      )}
      {joinOpen && (
        <JoinAnotherOrgModal lang={lang}
          excludeIds={visible.map((m) => m.organization_id)}
          onClose={() => setJoinOpen(false)}
          onJoined={(newMembership) => {
            // Switch the active org to the freshly-joined one so the user
            // immediately sees that org's data across the app.
            if (newMembership && newMembership.organization_id) {
              setActiveOrgId(newMembership.organization_id);
            }
            setJoinOpen(false);
          }} />
      )}
    </div>
  );
}

// Modal launched from the cyan Active-org pill's '+ Join another org'
// action (super-admin only). Lists every org the user isn't already a
// member of and inserts a manual membership row via membershipsCrud.joinSelf.
function JoinAnotherOrgModal({ lang, excludeIds, onClose, onJoined }) {
  const { data: allOrgs } = window.melr.useAllOrganizations();
  const [pickedId, setPickedId] = useState("");
  const [busy, setBusy] = useState(false);
  const [err, setErr]   = useState(null);
  const candidates = (allOrgs || []).filter((o) =>
    !o.archived_at && !(excludeIds || []).includes(o.id)
  );

  const onConfirm = async () => {
    if (!pickedId) {
      setErr(lang === "fr" ? "Choisir une organisation." : "Pick an organization.");
      return;
    }
    setBusy(true); setErr(null);
    try {
      const m = await window.melr.membershipsCrud.joinSelf(pickedId, null);
      if (onJoined) onJoined(m);
    } catch (e) { setErr(e.message); }
    finally { setBusy(false); }
  };

  const target = candidates.find((o) => o.id === pickedId);

  return (
    <Modal
      title={<>🏢 {lang === "fr" ? "Rejoindre une autre organisation" : "Join another organization"}</>}
      onClose={onClose}
      footer={<>
        <button type="button" className="btn sm ghost" onClick={onClose} disabled={busy}>
          {lang === "fr" ? "Annuler" : "Cancel"}
        </button>
        <button type="button" onClick={onConfirm} disabled={busy || !pickedId}
          style={{
            padding: "8px 14px", borderRadius: 6, border: 0,
            background: pickedId ? "#0891b2" : "#9ca3af",
            color: "white", cursor: pickedId ? "pointer" : "not-allowed", fontWeight: 600,
          }}>
          {busy ? "…" : (lang === "fr"
            ? "Rejoindre " + (target ? target.name : "…")
            : "Join " + (target ? target.name : "…"))}
        </button>
      </>}>
      <div style={{ display: "grid", gap: 12 }}>
        <div className="text-faint" style={{ fontSize: 12.5, lineHeight: 1.55 }}>
          {lang === "fr"
            ? "Action super-admin uniquement. Vous serez ajouté(e) comme membre secondaire de l'organisation choisie. Votre organisation d'origine (home) reste inchangée. Vous pourrez ensuite basculer entre vos orgs via le sélecteur cyan en haut de la page."
            : "Super-admin only. You'll be added as a secondary member of the chosen organization. Your home org stays unchanged. You can then switch between your orgs via the cyan selector in the topbar."}
        </div>
        <div>
          <label style={{ display: "block", fontSize: 11, color: "var(--text-faint)", marginBottom: 4, textTransform: "uppercase", letterSpacing: "0.04em" }}>
            {lang === "fr" ? "Organisation cible *" : "Target organization *"}
          </label>
          <select required value={pickedId} onChange={(e) => setPickedId(e.target.value)}
            style={{ width: "100%", padding: "8px 10px", borderRadius: 6, border: "1px solid var(--line)", fontSize: 13, background: "var(--bg, white)", color: "var(--text)" }}>
            <option value="">{lang === "fr" ? "— Choisir —" : "— Pick —"}</option>
            {candidates.map((o) => (
              <option key={o.id} value={o.id}>{o.name}{o.slug ? " (" + o.slug + ")" : ""}</option>
            ))}
          </select>
          {candidates.length === 0 && (
            <div className="text-faint" style={{ fontSize: 11, marginTop: 6 }}>
              {lang === "fr"
                ? "Vous êtes déjà membre de toutes les organisations actives."
                : "You're already a member of every active organization."}
            </div>
          )}
        </div>
        {err && (
          <div style={{ padding: "8px 10px", background: "#fee2e2", color: "#991b1b", borderRadius: 6, fontSize: 12.5 }}>{err}</div>
        )}
      </div>
    </Modal>
  );
}

function Topbar({ t, lang, setLang, role, setRole, roleOpen, setRoleOpen, setCmdOpen, route, setRoute, dark, setDark, isSuperAdmin, actingOrgId, setActingOrgId, myOrgId, activeOrgId, setActiveOrgId, myMemberships }) {
  const [userMenuOpen, setUserMenuOpen] = useState(false);
  const [meEmail, setMeEmail] = useState("");
  useEffect(() => {
    window.melr.currentProfile().then((p) => { if (p) setMeEmail(p.email || ""); });
    const refresh = () => window.melr.currentProfile().then((p) => setMeEmail(p && p.email || ""));
    window.addEventListener("melr:auth", refresh);
    return () => window.removeEventListener("melr:auth", refresh);
  }, []);
  const meInitials = (meEmail || "??").split(/[@.\-_]/).filter(Boolean).slice(0, 2).map((s) => s[0].toUpperCase()).join("");
  const breadcrumb = useMemo(() => {
    const map = {
      dashboard: [t("nav.work"), t("nav.dashboard")],
      projects: [t("nav.work"), t("nav.projects")],
      programmes: [t("nav.work"), t("nav.programmes")],
      workflow: [t("nav.work"), t("nav.workflow")],
      notifications: [t("nav.work"), t("nav.notifications")],
      exante: [t("nav.exante_group"), t("nav.exante"), "P-241"],
      indicators:     [t("nav.modules"), t("nav.indicators_group"), t("nav.indicators")],
      indicator_defs: [t("nav.modules"), t("nav.indicators_group"), t("nav.indicator_defs")],
      baseline: [t("nav.modules"), t("nav.baseline")],
      mobile: [t("nav.modules"), t("nav.mobile")],
      audit_system: [t("nav.modules"), t("nav.audit_system"), "P-241"],
      audit_data: [t("nav.modules"), t("nav.audit_data"), "P-241"],
      reporting: [t("nav.modules"), t("nav.reporting")],
      project: [t("nav.work"), t("nav.projects"), "P-241"],
      programme: [t("nav.work"), t("nav.programmes"), lang === "fr" ? "Détail" : "Detail"],
      learning: [t("nav.modules"), t("nav.learning")],
      org: [t("nav.admin"), t("nav.org")],
      organizations: [t("nav.admin"), t("nav.organizations")],
      form_builder: [t("nav.admin"), t("nav.form_builder")],
      settings: [t("nav.admin"), t("nav.settings")],
    };
    // Ex-ante sub-routes share the parent crumb chain but with the
    // section label at the end. E.g. exante:context →
    // "Évaluation ex ante › Évaluation ex ante › II. Contexte…"
    if (typeof route === "string" && route.startsWith("exante:")) {
      const subKey = "nav." + route.replace(":", ".");
      return [t("nav.exante_group"), t("nav.exante"), t(subKey)];
    }
    // Org sub-routes: Admin › Organisation & rôles › <section>
    if (typeof route === "string" && route.startsWith("org:")) {
      const subKey = "nav." + route.replace(":", ".");
      return [t("nav.admin"), t("nav.org"), t(subKey)];
    }
    return map[route] || [];
  }, [route, lang]);

  return (
    <header className="topbar">
      <div className="breadcrumb">
        {breadcrumb.map((b, i) => (
          <React.Fragment key={i}>
            <span className={i === breadcrumb.length - 1 ? "crumb cur" : "crumb"}>{b}</span>
            {i < breadcrumb.length - 1 && <Icon.chevronRight className="sm muted" />}
          </React.Fragment>
        ))}
      </div>

      {/* Multi-org membership: persistent active-org switcher. Shows for
          users with > 1 membership AND for super-admins (single-membership
          super-admins get a 'Join another org' shortcut). Distinct from
          the violet super-admin pill below (which is a temporary cross-org
          admin override that does not require membership). */}
      <ActiveOrgSwitcher lang={lang} myMemberships={myMemberships}
        activeOrgId={activeOrgId} setActiveOrgId={setActiveOrgId}
        myOrgId={myOrgId} isSuperAdmin={isSuperAdmin} />

      {/* Super-admin: global acting-as-org indicator. Visible only when
          actively acting on a non-home org. Click to switch back, or use
          the dedicated picker in Org & rôles to pick a different org. */}
      {isSuperAdmin && actingOrgId && actingOrgId !== myOrgId && (
        <SuperAdminActingPill lang={lang} actingOrgId={actingOrgId}
          onClear={() => setActingOrgId(null)} />
      )}

      <button className="search-trigger" onClick={() => setCmdOpen(true)}>
        <Icon.search />
        <span>{t("top.search")}</span>
        <span className="kbd">⌘K</span>
      </button>

      <div className="topbar-actions">
        <CurrencySwitcher lang={lang} />

        <div className="lang-switch">
          <button className={"lang-btn" + (lang === "fr" ? " active" : "")} onClick={() => setLang("fr")}>FR</button>
          <button className={"lang-btn" + (lang === "en" ? " active" : "")} onClick={() => setLang("en")}>EN</button>
        </div>

        <button className="iconbtn" onClick={() => setDark(!dark)} title={lang === "fr" ? (dark ? "Mode clair" : "Mode sombre") : (dark ? "Light mode" : "Dark mode")}>
          {dark ? <Icon.sun /> : <Icon.moon />}
        </button>
        <button className="iconbtn"><Icon.bell /><span className="dot-red"></span></button>
        <button className="iconbtn"><Icon.message /></button>

        <div className="role-switcher">
          <button className="role-trigger" onClick={() => setRoleOpen(!roleOpen)}>
            <span className="role-dot" style={{ background: role.color }}></span>
            <div style={{ textAlign: "left", lineHeight: 1.15 }}>
              <div className="role-name-mini">{lang === "fr" ? role.fr : role.en}</div>
              <div className="role-scope-mini">{role.scope}</div>
            </div>
            <Icon.chevronDown className="sm" />
          </button>
          {roleOpen && (
            <>
              <div className="overlay-bg" onClick={() => setRoleOpen(false)}></div>
              <div className="role-menu">
                <div className="role-menu-h">{lang === "fr" ? "Changer de rôle" : "Switch role"}</div>
                {ROLES.map((r) => (
                  <button key={r.id} className={"role-opt" + (r.id === role.id ? " active" : "")} onClick={() => { setRole(r); setRoleOpen(false); }}>
                    <span className="role-dot" style={{ background: r.color }}></span>
                    <div style={{ flex: 1 }}>
                      <div className="strong">{lang === "fr" ? r.fr : r.en}</div>
                      <div className="text-faint" style={{ fontSize: 11 }}>{r.scope}</div>
                    </div>
                    {r.id === role.id && <Icon.check className="sm" />}
                  </button>
                ))}
                <div className="role-menu-foot">
                  <Icon.shield className="sm muted" />
                  <span style={{ fontSize: 11.5, color: "var(--text-muted)" }}>{lang === "fr" ? "Permissions héritées de l'organisation" : "Permissions inherited from organization"}</span>
                </div>
              </div>
            </>
          )}
        </div>

        <div style={{ position: "relative" }}>
          <button
            className="avatar"
            style={{ background: avColor(meEmail || "U"), border: 0, cursor: "pointer", padding: 0, color: "white", fontWeight: 600 }}
            onClick={() => setUserMenuOpen(!userMenuOpen)}
            title={meEmail || (lang === "fr" ? "Mon compte" : "My account")}>
            {meInitials || "U"}
          </button>
          {userMenuOpen && (
            <>
              <div className="overlay-bg" onClick={() => setUserMenuOpen(false)}></div>
              <div className="role-menu" style={{ right: 0, left: "auto", minWidth: 220 }}>
                <div className="role-menu-h" style={{ flexDirection: "column", alignItems: "flex-start", gap: 2 }}>
                  <span style={{ fontWeight: 600 }}>{lang === "fr" ? "Mon compte" : "My account"}</span>
                  <span className="text-faint" style={{ fontSize: 11, fontWeight: 400 }}>{meEmail || "—"}</span>
                </div>
                <button className="role-opt" onClick={() => { setUserMenuOpen(false); setRoute && setRoute("profile"); }}>
                  <Icon.user className="sm" />
                  <span style={{ flex: 1 }}>{lang === "fr" ? "Mon profil" : "My profile"}</span>
                </button>
                <button className="role-opt" onClick={() => { setUserMenuOpen(false); setRoute && setRoute("help"); }}>
                  <Icon.info className="sm" />
                  <span style={{ flex: 1 }}>{lang === "fr" ? "Aide" : "Help"}</span>
                </button>
                <button className="role-opt" style={{ color: "#b91c1c" }}
                  onClick={async () => { setUserMenuOpen(false); await window.melr.supabase.auth.signOut(); }}>
                  <Icon.logout className="sm" />
                  <span style={{ flex: 1 }}>{lang === "fr" ? "Se déconnecter" : "Sign out"}</span>
                </button>
              </div>
            </>
          )}
        </div>
      </div>
    </header>
  );
}

// ==================== CURRENCY SWITCHER ====================
function CurrencySwitcher({ lang }) {
  const { currency, setCurrency, rates } = window.melr.useCurrency();
  const [open, setOpen] = useState(false);
  const [customOpen, setCustomOpen] = useState(false);

  // Top 3 quick choices (XOF / USD / EUR), the rest goes in the modal.
  const QUICK = ["XOF", "USD", "EUR"];

  return (
    <div className="role-switcher" style={{ position: "relative" }}>
      <button className="role-trigger" onClick={() => setOpen(!open)} style={{ padding: "6px 10px" }}>
        <Icon.euro className="sm" />
        <span style={{ marginLeft: 4, fontWeight: 600 }}>{currency}</span>
        <Icon.chevronDown className="sm" />
      </button>
      {open && (
        <>
          <div className="overlay-bg" onClick={() => setOpen(false)}></div>
          <div className="role-menu" style={{ minWidth: 220 }}>
            <div className="role-menu-h">{lang === "fr" ? "Devise d'affichage" : "Display currency"}</div>
            {Object.entries(rates).map(([code, meta]) => (
              <button key={code}
                className={"role-opt" + (code === currency ? " active" : "")}
                onClick={() => { setCurrency(code); setOpen(false); }}>
                <span className="mono" style={{ width: 42, textAlign: "left", fontWeight: 600 }}>{code}</span>
                <div style={{ flex: 1 }}>
                  <div className="strong">{meta.name}</div>
                  <div className="text-faint" style={{ fontSize: 11 }}>
                    1 EUR = {meta.rate} {code} · {meta.symbol}
                  </div>
                </div>
                {code === currency && <Icon.check className="sm" />}
              </button>
            ))}
            <div className="role-menu-foot" style={{ flexDirection: "column", gap: 6, alignItems: "stretch" }}>
              <button className="btn xs" onClick={() => { setOpen(false); setCustomOpen(true); }} style={{ width: "100%" }}>
                <Icon.plus /> {lang === "fr" ? "Autre devise / taux personnalisé" : "Other currency / custom rate"}
              </button>
              <div className="text-faint" style={{ fontSize: 10.5 }}>
                {lang === "fr" ? "XOF/EUR = 655.957 (taux fixe BCEAO). USD ajustable." : "XOF/EUR = 655.957 (fixed BCEAO peg). USD adjustable."}
              </div>
            </div>
          </div>
        </>
      )}
      {customOpen && <CustomRateModal lang={lang} onClose={() => setCustomOpen(false)} onSaved={() => setCustomOpen(false)} />}
    </div>
  );
}

function CustomRateModal({ lang, onClose, onSaved }) {
  const [code, setCode]     = useState("");
  const [rate, setRate]     = useState("");
  const [symbol, setSymbol] = useState("");
  const [name, setName]     = useState("");
  const [decimals, setDecimals] = useState("2");
  const [err, setErr]       = useState(null);

  const submit = (e) => {
    e.preventDefault();
    setErr(null);
    try {
      window.melr.setCustomRate(code.toUpperCase(), rate, {
        symbol: symbol || code.toUpperCase(),
        name:   name   || code.toUpperCase(),
        decimals: Number(decimals),
      });
      onSaved();
    } catch (e2) { setErr(e2.message); }
  };
  const inp = { padding: "8px 10px", borderRadius: 6, border: "1px solid var(--line)", fontSize: 13 };

  return (
    <div onClick={onClose} style={{
      position: "fixed", inset: 0, background: "rgba(0,0,0,.45)", zIndex: 9999,
      display: "flex", alignItems: "center", justifyContent: "center",
    }}>
      <form onClick={(e) => e.stopPropagation()} onSubmit={submit} style={{
        background: "var(--bg, white)", color: "var(--text, #111)",
        padding: 22, borderRadius: 10, width: 460, maxWidth: "92vw",
        boxShadow: "0 10px 30px rgba(0,0,0,.25)", display: "grid", gap: 10,
      }}>
        <div style={{ fontSize: 18, fontWeight: 600 }}>
          {lang === "fr" ? "Ajouter une devise" : "Add a currency"}
        </div>
        <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 10 }}>
          <label style={{ display: "grid", gap: 4 }}>
            <span style={{ fontSize: 11, opacity: 0.75 }}>{lang === "fr" ? "Code (3 lettres, ex: NGN)" : "Code (3 letters, e.g. NGN)"}</span>
            <input required value={code} onChange={(e) => setCode(e.target.value.toUpperCase().slice(0, 4))}
              placeholder="NGN" style={inp} />
          </label>
          <label style={{ display: "grid", gap: 4 }}>
            <span style={{ fontSize: 11, opacity: 0.75 }}>{lang === "fr" ? "Taux (1 EUR = ? code)" : "Rate (1 EUR = ? code)"}</span>
            <input required type="number" step="any" value={rate} onChange={(e) => setRate(e.target.value)}
              placeholder="1750" style={inp} />
          </label>
        </div>
        <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: 10 }}>
          <label style={{ display: "grid", gap: 4 }}>
            <span style={{ fontSize: 11, opacity: 0.75 }}>{lang === "fr" ? "Symbole" : "Symbol"}</span>
            <input value={symbol} onChange={(e) => setSymbol(e.target.value)} placeholder="₦" style={inp} />
          </label>
          <label style={{ display: "grid", gap: 4 }}>
            <span style={{ fontSize: 11, opacity: 0.75 }}>{lang === "fr" ? "Nom" : "Name"}</span>
            <input value={name} onChange={(e) => setName(e.target.value)} placeholder={lang === "fr" ? "Naira nigérian" : "Nigerian Naira"} style={inp} />
          </label>
          <label style={{ display: "grid", gap: 4 }}>
            <span style={{ fontSize: 11, opacity: 0.75 }}>{lang === "fr" ? "Décimales" : "Decimals"}</span>
            <select value={decimals} onChange={(e) => setDecimals(e.target.value)} style={inp}>
              <option value="0">0</option>
              <option value="2">2</option>
              <option value="3">3</option>
            </select>
          </label>
        </div>
        {err && <div style={{ color: "#b91c1c", fontSize: 12 }}>{err}</div>}
        <div style={{ display: "flex", gap: 8, justifyContent: "flex-end", marginTop: 4 }}>
          <button type="button" onClick={onClose}
            style={{ padding: "8px 14px", borderRadius: 6, border: "1px solid var(--line)", background: "transparent", cursor: "pointer" }}>
            {lang === "fr" ? "Annuler" : "Cancel"}
          </button>
          <button type="submit"
            style={{ padding: "8px 14px", borderRadius: 6, border: 0, background: "#2563eb", color: "white", cursor: "pointer", fontWeight: 600 }}>
            {lang === "fr" ? "Enregistrer" : "Save"}
          </button>
        </div>
      </form>
    </div>
  );
}

// =====================================================================
// ModulesPanel — super-admin only. Per-organization grid of checkboxes
// that toggle entries in organization_modules. Core modules are shown
// but locked (read-only); optional modules are interactive.
//
// Reads all flags at once via useAllOrganizationModules() so the grid
// renders without N round-trips. Writes go through organizationModulesCrud.
// Both sides are realtime: a toggle from any super-admin session
// updates every open instance within a few hundred ms.
// =====================================================================
function ModulesPanel({ lang, allOrgs }) {
  const { data: allFlags, loading, refresh } = window.melr.useAllOrganizationModules();
  const [busy, setBusy] = useState(null); // "orgId:code" while a toggle is in flight

  // Build a quick lookup: (orgId × code) → enabled. Missing key = enabled
  // (default-ON convention).
  const isEnabled = (orgId, code) => {
    const row = (allFlags || []).find((r) => r.organization_id === orgId && r.module_code === code);
    return !row || row.enabled !== false;
  };

  const toggle = async (orgId, code, currentlyEnabled) => {
    const key = orgId + ":" + code;
    setBusy(key);
    try {
      await window.melr.organizationModulesCrud.setEnabled(orgId, code, !currentlyEnabled);
      await refresh();
    } catch (e) {
      alert((lang === "fr" ? "Erreur : " : "Error: ") + e.message);
    } finally { setBusy(null); }
  };

  const optionalModules = MODULE_CATALOG.filter((m) => !m.core);

  // Sort orgs so that demo / test orgs land last alphabetically.
  const orgs = (allOrgs || []).slice().sort((a, b) =>
    (a.name_fr || a.slug || "").localeCompare(b.name_fr || b.slug || "")
  );

  return (
    <div className="card" style={{ marginTop: 16 }}>
      <div className="card-head">
        <div className="card-title">
          {lang === "fr" ? "Modules par organisation" : "Modules per organization"}
          <span className="pill" style={{ marginLeft: 8, fontSize: 10, background: "var(--brand-tint, #eff6ff)", color: "var(--brand, #1d4ed8)" }}>
            {lang === "fr" ? "Super-admin" : "Super-admin"}
          </span>
        </div>
        <button className="btn xs ghost" onClick={refresh} disabled={loading}>
          <Icon.refresh /> {lang === "fr" ? "Rafraîchir" : "Refresh"}
        </button>
      </div>
      <div className="card-body" style={{ paddingTop: 0 }}>
        <div style={{ fontSize: 12, color: "var(--text-faint)", marginBottom: 10 }}>
          {lang === "fr"
            ? "Modules optionnels — décochez pour masquer le module aux utilisateurs de l'organisation. Les modules essentiels (cœur du produit) sont verrouillés et ne peuvent pas être désactivés."
            : "Optional modules — uncheck to hide a module from the organization's users. Core modules are locked and cannot be disabled."}
        </div>
      </div>
      <div className="card-body flush">
        {loading && (
          <div style={{ padding: 18, textAlign: "center", color: "var(--text-faint)", fontSize: 12.5 }}>
            {lang === "fr" ? "Chargement…" : "Loading…"}
          </div>
        )}
        {!loading && orgs.length === 0 && (
          <div style={{ padding: 22, textAlign: "center", color: "var(--text-faint)", fontSize: 12.5 }}>
            {lang === "fr" ? "Aucune organisation." : "No organization."}
          </div>
        )}
        {!loading && orgs.length > 0 && (
          <div style={{ overflowX: "auto" }}>
            <table className="tbl" style={{ minWidth: 720 }}>
              <thead>
                <tr>
                  <th style={{ position: "sticky", left: 0, background: "var(--bg, white)", zIndex: 1 }}>
                    {lang === "fr" ? "Organisation" : "Organization"}
                  </th>
                  {optionalModules.map((m) => (
                    <th key={m.code} className="num" style={{ minWidth: 110, fontSize: 11 }}
                        title={m.code}>
                      {lang === "fr" ? m.fr : m.en}
                    </th>
                  ))}
                </tr>
              </thead>
              <tbody>
                {orgs.map((o) => (
                  <tr key={o.id}>
                    <td className="strong" style={{ position: "sticky", left: 0, background: "var(--bg, white)" }}>
                      <div>{lang === "fr" ? (o.name_fr || o.slug) : (o.name_en || o.name_fr || o.slug)}</div>
                      <div className="text-faint mono" style={{ fontSize: 10.5 }}>{o.slug}</div>
                    </td>
                    {optionalModules.map((m) => {
                      const enabled = isEnabled(o.id, m.code);
                      const key = o.id + ":" + m.code;
                      const isBusy = busy === key;
                      return (
                        <td key={m.code} className="num">
                          <label style={{ display: "inline-flex", alignItems: "center", justifyContent: "center", cursor: isBusy ? "wait" : "pointer", opacity: isBusy ? 0.5 : 1 }}>
                            <input
                              type="checkbox"
                              checked={enabled}
                              disabled={isBusy}
                              onChange={() => toggle(o.id, m.code, enabled)}
                              style={{ width: 16, height: 16 }}
                              aria-label={m.code + " — " + (o.slug || o.id)}
                            />
                          </label>
                        </td>
                      );
                    })}
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
        )}
      </div>
      <div className="card-body" style={{ paddingTop: 0 }}>
        <div style={{ fontSize: 11, color: "var(--text-faint)" }}>
          <span className="strong">
            {lang === "fr" ? "Modules cœur (verrouillés) : " : "Core modules (locked): "}
          </span>
          {MODULE_CATALOG.filter((m) => m.core).map((m) => (lang === "fr" ? m.fr : m.en)).join(", ")}
        </div>
      </div>
    </div>
  );
}

function CommandPalette({ t, lang, onClose, setRoute }) {
  const [q, setQ] = useState("");
  const cmds = [
    { t: "Dashboard", s: lang === "fr" ? "Vue d'ensemble" : "Overview", r: "dashboard", i: "home" },
    { t: lang === "fr" ? "Programmes" : "Programmes", s: lang === "fr" ? "hiérarchie & Gantt consolidé" : "hierarchy & consolidated Gantt", r: "programmes", i: "layers" },
    { t: lang === "fr" ? "Projets" : "Projects", s: "9 " + (lang === "fr" ? "actifs" : "active"), r: "projects", i: "folder" },
    { t: lang === "fr" ? "Indicateurs" : "Indicators", s: "170 " + (lang === "fr" ? "suivis" : "tracked"), r: "indicators", i: "trending" },
    { t: lang === "fr" ? "Baseline & cartographie" : "Baseline & mapping", s: "247 sites", r: "baseline", i: "map" },
    { t: lang === "fr" ? "Évaluation ex ante" : "Ex-ante appraisal", s: "P-241 · " + (lang === "fr" ? "12 sections" : "12 sections"), r: "exante", i: "scale" },
    { t: lang === "fr" ? "Audit système" : "System audit", s: "P-241", r: "audit_system", i: "shieldCheck" },
    { t: lang === "fr" ? "Audit des données" : "Data audit", s: "3 " + (lang === "fr" ? "alertes" : "alerts"), r: "audit_data", i: "database" },
    { t: "Reporting", s: lang === "fr" ? "Rapports trimestriels" : "Quarterly reports", r: "reporting", i: "fileText" },
    { t: lang === "fr" ? "Apprentissage" : "Learning", s: lang === "fr" ? "Bibliothèque & RETEX" : "Library & lessons", r: "learning", i: "brain" },
  ];
  const filtered = q ? cmds.filter((c) => (c.t + " " + c.s).toLowerCase().includes(q.toLowerCase())) : cmds;
  return (
    <>
      <div className="overlay-bg" onClick={onClose}></div>
      <div className="palette">
        <div className="palette-input">
          <Icon.search />
          <input autoFocus placeholder={t("top.search")} value={q} onChange={(e) => setQ(e.target.value)} />
          <span className="kbd">ESC</span>
        </div>
        <div className="palette-section-h">{lang === "fr" ? "Naviguer" : "Navigate"}</div>
        <div className="palette-list">
          {filtered.map((c, i) => {
            const Ic = Icon[c.i];
            return (
              <button key={i} className="palette-item" onClick={() => setRoute(c.r)}>
                <Ic />
                <div style={{ flex: 1, textAlign: "left" }}>
                  <div className="strong">{c.t}</div>
                  <div className="text-faint" style={{ fontSize: 11 }}>{c.s}</div>
                </div>
                <Icon.chevronRight className="sm muted" />
              </button>
            );
          })}
        </div>
        <div className="palette-foot">
          <span className="row gap-xs"><span className="kbd">↑</span><span className="kbd">↓</span> {lang === "fr" ? "naviguer" : "navigate"}</span>
          <span className="row gap-xs"><span className="kbd">↵</span> {lang === "fr" ? "ouvrir" : "open"}</span>
        </div>
      </div>
    </>
  );
}

// ==================== ORG / ROLES ====================
function OrgRoles({ t, lang, isSuperAdmin, actingOrgId, setActingOrgId, effectiveOrgId, myOrgId, hasPerm, isAdmin, orgSubRoute }) {
  // orgSubRoute: when set, render ONLY the matching section. Values:
  //   "matrix"         — Matrice rôles × permissions
  //   "members"        — Membres & rôles
  //   "project_agents" — Affectations aux projets
  //   "pending"        — Inscrits en attente
  //   "invitations"    — Invitations par lien
  //   "modules"        — Modules par organisation (super-admin)
  // null (route === "org") shows everything (legacy view).
  const showAll        = !orgSubRoute;
  const showMatrix     = showAll || orgSubRoute === "matrix";
  const showMembers    = showAll || orgSubRoute === "members";
  const showProjAgents = showAll || orgSubRoute === "project_agents";
  const showPending    = showAll || orgSubRoute === "pending";
  const showInvitations= showAll || orgSubRoute === "invitations";
  const showModules    = showAll || orgSubRoute === "modules";
  // The global super-admin acting-as-org context is now passed in via
  // screenProps; OrgRoles used to own this state locally — moved to App
  // level so the picker in the Topbar pill and the dropdown here stay
  // synchronized.
  // Org list — empty for non-super-admins (RLS).
  const { data: allOrgs } = window.melr.useAllOrganizations();
  // For backward-compat with existing render code that referenced meProfile.
  const { profile: meProfile } = window.melr.useCurrentProfile();

  const { members, refresh: refreshMembers, realtime } = window.melr.useOrgMembers(effectiveOrgId);
  const { data: liveRoles, refresh: refreshRoles, realtime: rolesRealtime } = window.melr.useRoles(effectiveOrgId);
  const { data: liveAssignments, refresh: refreshAssignments } = window.melr.useUserRoles();
  // Per-pending-user picked org slug. Empty string = caller's own org.
  const [assignTargets, setAssignTargets] = useState({});
  const LiveBadge = window.melr.LiveBadge;
  const [pending, setPending]   = useState([]);
  const [pendingLoading, setPL] = useState(true);
  const [busy, setBusy]         = useState(false);
  const [seedMsg, setSeedMsg]   = useState(null);
  const [newRoleOpen, setNewRoleOpen] = useState(false);
  // Phase 4 multi-org: 'Add existing user as member' modal
  const [addMemberOpen, setAddMemberOpen] = useState(false);
  // List of memberships of the effective org (used to show 'also in: X' badges
  // next to each member in the table — handy for super-admin auditing).
  const { data: orgMemberships } = window.melr.useOrgMembershipsList(effectiveOrgId);

  const refreshPending = async () => {
    setPL(true);
    try {
      const rows = await window.melr.fetchPendingUsers();
      setPending(rows);
    } catch (e) { console.error("[MELR] fetchPendingUsers:", e); }
    finally    { setPL(false); }
  };
  useEffect(() => { refreshPending(); }, []);

  const onAssign = async (email, targetOrgSlug) => {
    // Resolve target org's display name for the confirmation prompt
    let orgLabel = lang === "fr" ? "votre organisation" : "your organization";
    if (targetOrgSlug) {
      const o = (allOrgs || []).find((x) => x.slug === targetOrgSlug);
      if (o) orgLabel = o.name + " (" + o.slug + ")";
      else   orgLabel = targetOrgSlug;
    }
    const promptMsg = lang === "fr"
      ? ("Ajouter " + email + " à " + orgLabel + " ?")
      : ("Add " + email + " to " + orgLabel + "?");
    if (!window.confirm(promptMsg)) return;
    try {
      await window.melr.assignUserToOrg(email, targetOrgSlug || null);
      await refreshMembers();
      await refreshPending();
    } catch (e) { window.alert((lang === "fr" ? "Erreur : " : "Error: ") + e.message); }
  };
  const onRevoke = async (email) => {
    if (!window.confirm((lang === "fr" ? "Retirer l'accès de " : "Revoke access for ") + email + (lang === "fr" ? " ? Le compte est conservé." : " ? The account is kept."))) return;
    try {
      await window.melr.revokeUserFromOrg(email);
      await refreshMembers();
      await refreshPending();
    } catch (e) { window.alert((lang === "fr" ? "Erreur : " : "Error: ") + e.message); }
  };

  // ── Roles & permissions ───────────────────────────────────────────
  const CANONICAL_PERMS = [
    { k: "dashboards.view",    fr: "Voir tableaux de bord",        en: "View dashboards" },
    { k: "field_data.enter",   fr: "Saisir données terrain",       en: "Enter field data" },
    { k: "indicators.edit",    fr: "Modifier indicateurs",         en: "Edit indicators" },
    { k: "reports.validate",   fr: "Valider rapports",             en: "Validate reports" },
    { k: "dqa.launch",         fr: "Lancer audit DQA",             en: "Launch DQA audit" },
    { k: "exante.submit",      fr: "Soumettre ex ante",            en: "Submit ex-ante" },
    { k: "workflow.approve",   fr: "Approuver workflow",           en: "Approve workflow" },
    { k: "workflow.delegate",  fr: "Déléguer workflow",            en: "Delegate workflow" },
    { k: "users.manage",       fr: "Gérer utilisateurs",           en: "Manage users" },
    { k: "org.configure",      fr: "Configurer organisation",      en: "Configure org" },
    { k: "orgs.manage",        fr: "Super-admin · gérer toutes les organisations", en: "Super-admin · manage all organizations" },
  ];
  const onSeedDefaults = async () => {
    setBusy(true); setSeedMsg(null);
    try {
      const added = await window.melr.seedDefaultRoles(effectiveOrgId);
      await refreshRoles();
      setSeedMsg(lang === "fr"
        ? (added + " rôle(s) ajouté(s).")
        : (added + " role(s) added."));
    } catch (e) { setSeedMsg(e.message); }
    finally { setBusy(false); }
  };
  const onTogglePerm = async (roleId, perm, enabled) => {
    try { await window.melr.rolesCrud.togglePermission(roleId, perm, enabled); }
    catch (e) { window.alert((lang === "fr" ? "Erreur : " : "Error: ") + e.message); }
  };
  const onDeleteRole = async (role) => {
    if (!window.confirm(lang === "fr"
      ? "Supprimer le rôle « " + (role.name_fr || role.code) + " » ? Toutes les attributions de ce rôle seront retirées."
      : "Delete role '" + (role.name_en || role.code) + "'? All assignments will be removed.")) return;
    try { await window.melr.rolesCrud.remove(role.id); await refreshRoles(); }
    catch (e) { window.alert((lang === "fr" ? "Erreur : " : "Error: ") + e.message); }
  };

  // Pending users: super-admin acting on a non-default org filters to
  // requests targeting that org (plus null-requested for legacy). In
  // default mode they see ALL pending so cross-org dispatch still works.
  const ownOrgId = meProfile && meProfile.organization_id;
  const visiblePending = (isSuperAdmin && effectiveOrgId && effectiveOrgId !== ownOrgId)
    ? pending.filter((u) => !u.requested_organization_id || u.requested_organization_id === effectiveOrgId)
    : pending;

  // Assignments grouped by user_id for the member list
  const assignmentsByUser = new Map();
  (liveAssignments || []).forEach((a) => {
    if (!assignmentsByUser.has(a.user_id)) assignmentsByUser.set(a.user_id, []);
    assignmentsByUser.get(a.user_id).push(a);
  });

  const onAssignRole = async (userId, roleId) => {
    if (!roleId) return;
    try {
      await window.melr.userRolesCrud.assign(userId, roleId, "organization", null);
      await refreshAssignments();
    } catch (e) { window.alert((lang === "fr" ? "Erreur : " : "Error: ") + e.message); }
  };
  const onUnassignRole = async (assignmentId) => {
    try { await window.melr.userRolesCrud.remove(assignmentId); await refreshAssignments(); }
    catch (e) { window.alert((lang === "fr" ? "Erreur : " : "Error: ") + e.message); }
  };

  const roleLabel = (r) => (lang === "fr" ? (r.name_fr || r.code) : (r.name_en || r.code));

  // Whole-screen gate: every panel below (matrix, members, project agents,
  // pending sign-ups, invitations, modules) is admin/super-admin territory.
  // A reviewer / data-entry user has nothing to act on here. Show a single
  // "restricted access" panel instead of leaking individual admin tools.
  // canManage is true when:
  //   • super-admin, OR
  //   • the user has users.manage permission (= "admin" role per is_admin_user).
  // hasPerm may still be loading on first render — that returns undefined
  // → fails closed (no admin access until perms confirmed).
  const canManageOrg = !!isSuperAdmin || !!isAdmin
    || (typeof hasPerm === "function" && hasPerm("users.manage"));

  if (!canManageOrg) {
    return (
      <div className="page">
        <div className="page-header">
          <div className="page-eyebrow">{lang === "fr" ? "ADMINISTRATION" : "ADMINISTRATION"}</div>
          <h1 className="page-title">{t("nav.org")}</h1>
        </div>
        <div className="card" style={{ marginTop: 14 }}>
          <div className="card-body" style={{ padding: 28, textAlign: "center" }}>
            <div style={{ fontSize: 14, fontWeight: 600, marginBottom: 8 }}>
              {lang === "fr"
                ? "Accès restreint aux administrateurs"
                : "Restricted to administrators"}
            </div>
            <div className="muted" style={{ fontSize: 12.5, maxWidth: 540, margin: "0 auto" }}>
              {lang === "fr"
                ? "Cette page permet de gérer les membres, les rôles, les invitations et les modules de l'organisation. Elle est réservée aux comptes ayant la permission « Gérer utilisateurs » (rôle administrateur ou super-admin). Contactez votre administrateur si vous pensez en avoir besoin."
                : "This page lets you manage members, roles, invitations and modules for the organization. It's restricted to accounts with the 'Manage users' permission (administrator or super-admin role). Contact your administrator if you need access."}
            </div>
          </div>
        </div>
      </div>
    );
  }

  return (
    <div className="page">
      <div className="page-header">
        <div className="page-eyebrow">{lang === "fr" ? "ADMINISTRATION" : "ADMINISTRATION"}</div>
        <div className="page-header-row">
          <div>
            <h1 className="page-title">{t("nav.org")} <LiveBadge on={realtime || rolesRealtime} lang={lang} /></h1>
            <div className="page-sub">{lang === "fr"
              ? `${members.length} membre${members.length > 1 ? "s" : ""} actif${members.length > 1 ? "s" : ""} · ${liveRoles.length} rôle${liveRoles.length > 1 ? "s" : ""}${pending.length > 0 ? ` · ${pending.length} en attente` : ""}`
              : `${members.length} active member${members.length > 1 ? "s" : ""} · ${liveRoles.length} role${liveRoles.length > 1 ? "s" : ""}${pending.length > 0 ? ` · ${pending.length} pending` : ""}`}</div>
          </div>
        </div>
      </div>

      {/* Super-admin: "Acting as org X" selector. Switches the context of
          every sub-panel (members, roles, project assignments, pending,
          invitations) to the chosen org.
          - Hidden for non-super-admins (no orgs.manage permission).
          - When super-admin but org list comes back empty, we render a
            diagnostic placeholder so the user understands WHY the
            selector isn't there (instead of silently dropping it).
          - STICKY top: when scrolling through the long Org & rôles page
            (matrix + members + project assignments + invitations …),
            the org picker stays pinned so the admin always sees which
            org context they're operating on. z-index 20 covers the
            page-header (z-index 4) when both reach top:0. */}
      {isSuperAdmin && (allOrgs || []).length > 0 ? (
        <div className="card" style={{
          marginBottom: 16, padding: "10px 14px",
          background: "rgba(238, 242, 255, 0.96)", borderColor: "#a5b4fc",
          display: "flex", alignItems: "center", gap: 12, flexWrap: "wrap",
          position: "sticky", top: 0, zIndex: 20,
          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)",
        }}>
          <span style={{ fontSize: 12, color: "#3730a3", fontWeight: 600 }}>
            {lang === "fr" ? "🛡 Mode super-admin · Agir en tant qu'admin de :" : "🛡 Super-admin mode · Acting as admin of:"}
          </span>
          <select value={effectiveOrgId || ""}
            onChange={(e) => {
              const v = e.target.value || null;
              // Picking the user's home org clears the acting context
              // (state becomes null) so the Topbar pill disappears.
              setActingOrgId(v && v === myOrgId ? null : v);
            }}
            style={{ padding: "5px 10px", fontSize: 12.5, borderRadius: 6, border: "1px solid #a5b4fc", background: "white", color: "var(--text)" }}>
            {(allOrgs || []).filter((o) => !o.archived_at).map((o) => (
              <option key={o.id} value={o.id}>
                {o.name}{o.slug ? " · " + o.slug : ""}
                {o.id === myOrgId ? " (la vôtre)" : ""}
              </option>
            ))}
          </select>
          <span className="text-faint" style={{ fontSize: 11 }}>
            {lang === "fr"
              ? "Tous les panneaux ci-dessous opèrent sur cette org."
              : "Every panel below acts on this org."}
          </span>
        </div>
      ) : isSuperAdmin && (allOrgs || []).length === 0 ? (
        <div className="card" style={{
          marginBottom: 16, padding: "10px 14px",
          background: "#fef3c7", borderColor: "#fcd34d",
        }}>
          <div style={{ fontSize: 12, color: "#92400e", fontWeight: 600, marginBottom: 4 }}>
            {lang === "fr"
              ? "🛡 Mode super-admin actif, mais la liste des organisations est vide."
              : "🛡 Super-admin mode active, but the organizations list is empty."}
          </div>
          <div style={{ fontSize: 11, color: "#92400e", lineHeight: 1.5 }}>
            {lang === "fr"
              ? "Causes habituelles : (1) le chargement de la liste a échoué (ouvrez la console F12 → onglet Console pour les messages [MELR]) ; (2) vous venez d'obtenir la permission orgs.manage et votre session ne l'a pas encore prise en compte → déconnectez-vous puis reconnectez-vous."
              : "Common causes: (1) the list failed to load (open the F12 console for [MELR] messages); (2) you just gained the orgs.manage permission and your session hasn't picked it up yet → sign out and back in."}
          </div>
        </div>
      ) : null}

      {/* Matrix roles × permissions */}
      {showMatrix && (
      <div className="card" style={{ marginBottom: 16 }}>
        <div className="card-head">
          <div className="card-title">{lang === "fr" ? "Matrice rôles × permissions" : "Roles × permissions matrix"}</div>
          <span className="tag-mono" style={{ marginLeft: 8 }}>{liveRoles.length}</span>
          <div style={{ flex: 1 }} />
          {liveRoles.length === 0 && (
            <button className="btn sm primary" onClick={onSeedDefaults} disabled={busy}>
              <Icon.zap /> {lang === "fr" ? "Initialiser 5 rôles par défaut" : "Seed 5 default roles"}
            </button>
          )}
          {liveRoles.length > 0 && (
            <button className="btn sm" onClick={() => setNewRoleOpen(true)}>
              <Icon.plus /> {lang === "fr" ? "Nouveau rôle" : "New role"}
            </button>
          )}
        </div>
        {seedMsg && (
          <div style={{ padding: "8px 14px", fontSize: 12, color: "var(--text-muted)", background: "var(--bg-sunken)" }}>
            {seedMsg}
          </div>
        )}
        <div className="card-body flush" style={{ overflowX: "auto" }}>
          {liveRoles.length === 0 ? (
            <div className="text-faint" style={{ padding: 32, textAlign: "center", fontSize: 13 }}>
              {lang === "fr"
                ? "Aucun rôle pour votre organisation. Cliquez « Initialiser » pour créer 5 rôles par défaut (Admin, Chef de projet, S&E, Saisie, Bailleur)."
                : "No role for your organization. Click 'Seed' to create 5 starter roles (Admin, Project lead, M&E, Field entry, Donor)."}
            </div>
          ) : (
            <table className="tbl matrix" style={{ minWidth: 600 }}>
              <thead>
                <tr>
                  <th>{lang === "fr" ? "Capacité" : "Capability"}</th>
                  {liveRoles.map((r) => (
                    <th key={r.id} style={{ textAlign: "center" }}>
                      <div style={{ fontSize: 11.5, fontWeight: 600 }}>{roleLabel(r)}</div>
                      <div className="text-faint" style={{ fontSize: 9.5, fontWeight: 400 }}>{r.code}</div>
                      <button className="btn xs ghost"
                        onClick={() => onDeleteRole(r)}
                        title={lang === "fr" ? "Supprimer ce rôle" : "Delete this role"}
                        style={{ padding: 0, marginTop: 2 }}>
                        <Icon.x />
                      </button>
                    </th>
                  ))}
                </tr>
              </thead>
              <tbody>
                {CANONICAL_PERMS.map((p) => (
                  <tr key={p.k}>
                    <td>
                      <span style={{ fontSize: 12.5 }}>{lang === "fr" ? p.fr : p.en}</span>
                      <span className="tag-mono text-faint" style={{ marginLeft: 6, fontSize: 9.5 }}>{p.k}</span>
                    </td>
                    {liveRoles.map((r) => {
                      const enabled = (r.permissions || []).includes(p.k);
                      return (
                        <td key={r.id} style={{ textAlign: "center" }}>
                          <input type="checkbox" checked={enabled}
                            onChange={(e) => onTogglePerm(r.id, p.k, e.target.checked)}
                            style={{ cursor: "pointer", accentColor: "var(--accent)" }} />
                        </td>
                      );
                    })}
                  </tr>
                ))}
              </tbody>
            </table>
          )}
        </div>
      </div>
      )}

      {/* Members + assignments */}
      {showMembers && (
      <div className="card" style={{ marginBottom: 16 }}>
        <div className="card-head">
          <div className="card-title">{lang === "fr" ? "Membres de l'organisation & rôles" : "Members & their roles"}</div>
          <span className="tag-mono" style={{ marginLeft: 8 }}>{members.length}</span>
          <div style={{ flex: 1 }} />
          <button className="btn xs primary" onClick={() => setAddMemberOpen(true)}
            title={lang === "fr"
              ? "Ajouter un utilisateur déjà inscrit comme membre de cette organisation (multi-org)."
              : "Add an already-signed-up user as a member of this organization (multi-org)."}>
            <Icon.plus /> {lang === "fr" ? "Membre existant" : "Existing member"}
          </button>
        </div>
        <div className="card-body flush">
          {members.length === 0 ? (
            <div className="text-faint" style={{ padding: 20, textAlign: "center" }}>
              {lang === "fr" ? "Aucun membre dans votre organisation pour le moment." : "No members in your organization yet."}
            </div>
          ) : (
            members.map((u) => {
              const name = u.full_name || u.email;
              const assigned = assignmentsByUser.get(u.id) || [];
              const assignedRoleIds = new Set(assigned.map((a) => a.role_id));
              const availableToAdd = liveRoles.filter((r) => !assignedRoleIds.has(r.id));
              return (
                <div key={u.id} className="user-row" style={{ flexWrap: "wrap" }}>
                  <div className="avatar sm" style={{ background: avColor(name) }}>{initials(name)}</div>
                  <div style={{ flex: 1, minWidth: 200 }}>
                    <div className="strong">{name}</div>
                    <div className="text-faint" style={{ fontSize: 11 }}>{u.email}{u.locale ? " · " + u.locale.toUpperCase() : ""}</div>
                  </div>
                  <div className="row gap-xs" style={{ flexWrap: "wrap", justifyContent: "flex-end" }}>
                    {assigned.map((a) => (
                      <span key={a.id} className="pill accent" style={{ fontSize: 10.5, paddingRight: 2 }}>
                        {a.roles ? (lang === "fr" ? a.roles.name_fr : a.roles.name_en) : "—"}
                        <button className="btn xs ghost" onClick={() => onUnassignRole(a.id)}
                          title={lang === "fr" ? "Retirer ce rôle" : "Remove this role"}
                          style={{ padding: 0, marginLeft: 2 }}>
                          <Icon.x className="sm" />
                        </button>
                      </span>
                    ))}
                    {availableToAdd.length > 0 && (
                      <select
                        defaultValue=""
                        onChange={(e) => { if (e.target.value) { onAssignRole(u.id, e.target.value); e.target.value = ""; } }}
                        style={{ padding: "3px 6px", fontSize: 11, borderRadius: 4, border: "1px solid var(--line)", background: "var(--bg, white)", color: "var(--text)" }}
                        title={lang === "fr" ? "Attribuer un rôle" : "Assign a role"}>
                        <option value="">+ {lang === "fr" ? "rôle" : "role"}</option>
                        {availableToAdd.map((r) => (
                          <option key={r.id} value={r.id}>{roleLabel(r)}</option>
                        ))}
                      </select>
                    )}
                    <button className="btn xs ghost" onClick={() => onRevoke(u.email)} title={lang === "fr" ? "Retirer l'accès" : "Revoke access"}>
                      <Icon.x />
                    </button>
                  </div>
                </div>
              );
            })
          )}
          {/* Secondary memberships: users whose HOME org is elsewhere but
              who have a membership row in this org (Phase 4 multi-org).
              They don't appear in `members` (which lists home-org users)
              so we render them below as a separate list with a 🔗 badge. */}
          {(() => {
            const homeEmails = new Set((members || []).map((u) => u.email));
            const secondary = (orgMemberships || []).filter((m) =>
              !m.isHomeForUser && m.email && !homeEmails.has(m.email)
            );
            if (secondary.length === 0) return null;
            return (
              <>
                <div style={{ padding: "10px 14px 4px 14px", fontSize: 11, fontWeight: 600, color: "var(--text-faint)", textTransform: "uppercase", letterSpacing: "0.04em", borderTop: "1px solid var(--line-faint)" }}>
                  {lang === "fr" ? "Membres rattachés (org d'origine différente)" : "Attached members (different home org)"}
                  <span className="tag-mono" style={{ marginLeft: 8 }}>{secondary.length}</span>
                </div>
                {secondary.map((m) => {
                  const name = m.fullName || m.email;
                  return (
                    <div key={m.id} className="user-row" style={{ flexWrap: "wrap", opacity: 0.95 }}>
                      <div className="avatar sm" style={{ background: avColor(name) }}>{initials(name)}</div>
                      <div style={{ flex: 1, minWidth: 200 }}>
                        <div className="strong">{name}</div>
                        <div className="text-faint" style={{ fontSize: 11 }}>
                          {m.email}
                          <span className="pill" style={{ marginLeft: 8, background: "#ecfeff", borderColor: "#67e8f9", color: "#155e75", fontSize: 9.5 }}>
                            🔗 {lang === "fr" ? "membre rattaché" : "attached member"}
                          </span>
                        </div>
                      </div>
                      <div className="row gap-xs" style={{ flexWrap: "wrap", justifyContent: "flex-end" }}>
                        <span className="text-faint" style={{ fontSize: 10.5 }}>
                          {lang === "fr" ? "ajouté " : "added "}
                          {new Date(m.created_at).toLocaleDateString(lang === "fr" ? "fr-FR" : "en-US")}
                        </span>
                        <button className="btn xs ghost" title={lang === "fr" ? "Retirer la membership (l'utilisateur reste membre de son org d'origine)" : "Remove this membership (the user stays in their home org)"}
                          onClick={async () => {
                            if (!window.confirm(lang === "fr"
                              ? "Retirer " + name + " de cette organisation ? L'utilisateur restera membre de son organisation d'origine."
                              : "Remove " + name + " from this organization? They will remain a member of their home org.")) return;
                            try { await window.melr.membershipsCrud.remove(m.id); }
                            catch (e) { window.alert((lang === "fr" ? "Erreur : " : "Error: ") + e.message); }
                          }}>
                          <Icon.x />
                        </button>
                      </div>
                    </div>
                  );
                })}
              </>
            );
          })()}
        </div>
      </div>
      )}

      {newRoleOpen && <NewRoleModal lang={lang} onClose={() => setNewRoleOpen(false)} onSaved={refreshRoles} />}

      {/* Project assignments — controls which projects' forms a field agent sees on mobile */}
      {showProjAgents && (
        <ProjectAgentsPanel lang={lang} members={members} effectiveOrgId={effectiveOrgId} />
      )}

      {/* Pending users awaiting admin assignment */}
      {showPending && (
      <div className="card" style={{ marginBottom: 16 }}>
        <div className="card-head">
          <div className="card-title">{lang === "fr" ? "Inscrits en attente d'attribution" : "Pending sign-ups"}</div>
          <span className="tag-mono" style={{ marginLeft: "auto" }}>{pending.length}</span>
          <button className="btn xs ghost" onClick={refreshPending} title={lang === "fr" ? "Rafraîchir" : "Refresh"} style={{ marginLeft: 6 }}>
            <Icon.refresh />
          </button>
        </div>
        <div className="card-body flush">
          {pendingLoading ? (
            <div className="text-faint" style={{ padding: 20, textAlign: "center" }}>{lang === "fr" ? "Chargement…" : "Loading…"}</div>
          ) : visiblePending.length === 0 ? (
            <div className="text-faint" style={{ padding: 20, textAlign: "center" }}>
              {lang === "fr"
                ? "Aucun nouvel inscrit en attente. Les nouveaux comptes apparaîtront ici après leur inscription."
                : "No new sign-ups waiting. New accounts will appear here after they sign up."}
            </div>
          ) : (
            visiblePending.map((u) => {
              const name = u.full_name || u.email;
              // Super-admins target a specific org via a per-row dropdown;
              // for super-admins we pre-fill it with the user's requested
              // org (Approach B) so the common case is one-click.
              const initialTarget = u.requested_organization_slug || "";
              const targetSlug = (u.id in assignTargets) ? assignTargets[u.id] : initialTarget;
              return (
                <div key={u.id} className="user-row" style={{ flexWrap: "wrap" }}>
                  <div className="avatar sm" style={{ background: "#fbbf24" }}>{initials(name)}</div>
                  <div style={{ flex: 1, minWidth: 200 }}>
                    <div className="strong">{name}</div>
                    <div className="text-faint" style={{ fontSize: 11 }}>
                      {u.email} · {lang === "fr" ? "inscrit le" : "signed up"} {new Date(u.created_at).toLocaleDateString(lang === "fr" ? "fr-FR" : "en-US")}
                    </div>
                    {/* Requested-org chip (Approach B) — appears when the user chose an org at signup */}
                    {u.requested_organization_name && (
                      <div style={{ marginTop: 4 }}>
                        <span className="pill" style={{ background: "#dbeafe", color: "#1e3a8a", fontSize: 10 }}>
                          {lang === "fr" ? "Demande : " : "Wants to join: "}{u.requested_organization_name}
                        </span>
                      </div>
                    )}
                  </div>
                  {isSuperAdmin && (allOrgs || []).length > 0 && (
                    <select value={targetSlug}
                      onChange={(e) => setAssignTargets((m) => ({ ...m, [u.id]: e.target.value }))}
                      title={lang === "fr" ? "Affecter à cette organisation" : "Assign to this organization"}
                      style={{ padding: "4px 8px", fontSize: 12, borderRadius: 4, border: "1px solid var(--line)", background: "var(--bg, white)", color: "var(--text)", maxWidth: 220 }}>
                      <option value="">
                        {lang === "fr" ? "— Mon organisation (par défaut) —" : "— My organization (default) —"}
                      </option>
                      {(allOrgs || []).filter((o) => !o.archived_at).map((o) => (
                        <option key={o.id} value={o.slug}>
                          {o.name}{o.slug ? " · " + o.slug : ""}
                          {o.slug === u.requested_organization_slug ? " ✓" : ""}
                        </option>
                      ))}
                    </select>
                  )}
                  <button className="btn xs primary" onClick={() => onAssign(u.email, targetSlug || null)}>
                    <Icon.plus /> {lang === "fr" ? "Ajouter à l'org" : "Add to org"}
                  </button>
                </div>
              );
            })
          )}
        </div>
      </div>
      )}

      {/* Invitations — token-based onboarding (Approach C).
          Both gated on the section-level showInvitations AND on
          users.manage (the whole screen is already behind canManageOrg
          but the sub-route gate makes navigation predictable). */}
      {showInvitations && (isSuperAdmin || (hasPerm && hasPerm("users.manage"))) && (
        <InvitationsPanel lang={lang} allOrgs={allOrgs} isSuperAdmin={isSuperAdmin}
          roles={liveRoles} effectiveOrgId={effectiveOrgId} />
      )}

      {/* Module flags — super-admin only. Per-tenant module on/off
          toggle. Hidden from regular admins by design (avoid self-lockout). */}
      {showModules && isSuperAdmin && (
        <ModulesPanel lang={lang} allOrgs={allOrgs} />
      )}

      {addMemberOpen && (
        <AddExistingMemberModal lang={lang}
          targetOrgId={effectiveOrgId}
          roles={liveRoles}
          onClose={() => setAddMemberOpen(false)}
          onAdded={async () => {
            // Re-fetch the members list. useOrgMembers reads from profiles
            // joined by organization_id (home org), so a new membership row
            // for an external user won't show there yet — that's expected,
            // they remain a member of their own home org. The "membership
            // badges" pull from useOrgMembershipsList which DOES include
            // them.
            await refreshMembers();
            setAddMemberOpen(false);
          }} />
      )}
    </div>
  );
}

// ─── Add existing user as member modal ───────────────────────────────────
// Lets an admin add an already-registered user as a member of their org.
// The user's home org (profile.organization_id) is unchanged — this is a
// SECONDARY membership. The user picks which org is active in their
// topbar switcher.
function AddExistingMemberModal({ lang, targetOrgId, roles, onClose, onAdded }) {
  const [email, setEmail]   = useState("");
  const [roleId, setRoleId] = useState("");
  const [busy, setBusy]     = useState(false);
  const [err, setErr]       = useState(null);
  const [msg, setMsg]       = useState(null);

  const onSubmit = async (e) => {
    e.preventDefault();
    if (!email.trim()) { setErr(lang === "fr" ? "Email requis." : "Email required."); return; }
    setBusy(true); setErr(null);
    try {
      const m = await window.melr.membershipsCrud.addByEmail(
        targetOrgId, email.trim().toLowerCase(), roleId || null
      );
      const name = m.profiles && (m.profiles.full_name || m.profiles.email);
      setMsg(lang === "fr"
        ? "✓ " + (name || email) + " ajouté(e) comme membre."
        : "✓ " + (name || email) + " added as a member.");
      setEmail(""); setRoleId("");
      if (onAdded) setTimeout(() => onAdded(), 1500);
    } 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="sm"
      title={lang === "fr" ? "Ajouter un membre existant" : "Add an existing member"}
      onClose={onClose}
      onSubmit={onSubmit}
      footer={<>
        <button type="button" className="btn sm ghost" onClick={onClose} disabled={busy}>
          {lang === "fr" ? "Fermer" : "Close"}
        </button>
        <button type="submit" className="btn sm primary" disabled={busy || !email.trim()}>
          {busy ? "…" : (lang === "fr" ? "Ajouter" : "Add")}
        </button>
      </>}>
      <div style={{ display: "grid", gap: 12 }}>
        <div className="text-faint" style={{ fontSize: 12, lineHeight: 1.55 }}>
          {lang === "fr"
            ? "Ajoute un utilisateur déjà inscrit sur la plateforme comme membre de cette organisation. Son organisation d'origine (« home ») reste inchangée — il pourra basculer entre ses orgs via le sélecteur en haut. Pour inviter un NOUVEL utilisateur (qui n'a pas encore de compte), utilisez la carte « Invitations par lien »."
            : "Add a user who is already registered on the platform as a member of this organization. Their original (home) org stays unchanged — they'll switch between their orgs via the topbar selector. To invite a NEW user (no account yet), use the 'Invitation links' card instead."}
        </div>
        <div>
          <label style={lbl}>{lang === "fr" ? "Email de l'utilisateur *" : "User email *"}</label>
          <input type="email" required style={inp} value={email} onChange={(e) => setEmail(e.target.value)}
            placeholder="marie@org.com" autoFocus />
          <div className="text-faint" style={{ fontSize: 10.5, marginTop: 4 }}>
            {lang === "fr"
              ? "L'utilisateur doit déjà avoir un compte sur la plateforme."
              : "The user must already have an account on the platform."}
          </div>
        </div>
        {(roles || []).length > 0 && (
          <div>
            <label style={lbl}>{lang === "fr" ? "Rôle (optionnel)" : "Role (optional)"}</label>
            <select style={inp} value={roleId} onChange={(e) => setRoleId(e.target.value)}>
              <option value="">{lang === "fr" ? "— Aucun rôle attribué —" : "— No role assigned —"}</option>
              {(roles || []).map((r) => (
                <option key={r.id} value={r.id}>
                  {lang === "fr" ? (r.name_fr || r.code) : (r.name_en || r.code)}
                </option>
              ))}
            </select>
            <div className="text-faint" style={{ fontSize: 10.5, marginTop: 4 }}>
              {lang === "fr"
                ? "Le rôle est attaché à la membership (peut être modifié ensuite via la matrice rôles × membres)."
                : "The role is attached to the membership (can be changed later via the roles × members matrix)."}
            </div>
          </div>
        )}
        {err && (
          <div style={{ padding: "8px 10px", background: "#fee2e2", color: "#991b1b", borderRadius: 6, fontSize: 12.5 }}>{err}</div>
        )}
        {msg && (
          <div style={{ padding: "8px 10px", background: "#dcfce7", color: "#166534", borderRadius: 6, fontSize: 12.5 }}>{msg}</div>
        )}
      </div>
    </Modal>
  );
}

// ─── Invitations panel ────────────────────────────────────────────────────
// Lists pending / used / expired invitations for the caller's org (or every
// org when super-admin), lets admins generate a new invitation link, and
// copies the share URL to the clipboard on creation.
function InvitationsPanel({ lang, allOrgs, isSuperAdmin, roles, effectiveOrgId }) {
  // Scope invitations to the effective org. For super-admin acting as
  // Org X, this shows Org X's invitations only; for regular admin it
  // also matches RLS naturally.
  const { data: invitations, loading, refresh } = window.melr.useInvitations(effectiveOrgId);
  const [createOpen, setCreateOpen] = useState(false);
  const [toast, setToast] = useState(null);
  const [busy, setBusy]   = useState(null);

  const showToast = (ok, msg, ttl) => {
    setToast({ ok, msg });
    setTimeout(() => setToast(null), ttl || 4500);
  };
  const onCopy = (url) => {
    if (!url) return;
    try {
      navigator.clipboard.writeText(url);
      showToast(true, lang === "fr" ? "✓ Lien copié dans le presse-papier" : "✓ Link copied to clipboard", 3500);
    } catch (_) {
      // Fallback: alert with the URL
      window.prompt(lang === "fr" ? "Copier le lien :" : "Copy link:", url);
    }
  };
  // After an invitation row is created, try to email the recipient via
  // the send-invitation-email Edge Function. Always copy the link as a
  // belt-and-suspenders fallback so the admin can still share it even
  // if the function isn't configured / the email bounces / etc.
  const onInvitationCreated = async (inv, ctx) => {
    refresh();
    const url = window.melr.invitationsCrud.inviteUrl(inv.token);
    // No email on the invitation row → copy-link as before.
    if (!ctx || !ctx.email) {
      onCopy(url);
      return;
    }
    // Email was provided. Copy link first so the admin can fall back to
    // manual share if the send errors.
    try { navigator.clipboard.writeText(url); } catch (_) {}
    try {
      await window.melr.sendInvitationEmail({
        token:     inv.token,
        email:     ctx.email,
        org_name:  ctx.org_name,
        role_name: ctx.role_name,
        lang,
        invite_url: url,
      });
      showToast(true, lang === "fr"
        ? "✉ Invitation envoyée à " + ctx.email + " (lien aussi copié)"
        : "✉ Invitation sent to " + ctx.email + " (link also copied)", 5500);
    } catch (e) {
      // Surface the provider error but keep the flow alive: the link is
      // copied + visible in the table so the admin can share it manually.
      showToast(false, (lang === "fr"
        ? "Envoi automatique impossible (" + (e.message || "erreur") + "). Lien copié — partagez-le manuellement."
        : "Auto-send failed (" + (e.message || "error") + "). Link copied — share it manually."), 9000);
    }
  };

  const onRevoke = async (inv) => {
    if (!window.confirm(lang === "fr"
      ? "Révoquer cette invitation ? Le lien ne pourra plus être utilisé."
      : "Revoke this invitation? The link will no longer work.")) return;
    setBusy(inv.id);
    try { await window.melr.invitationsCrud.revoke(inv.id); await refresh(); }
    catch (e) { window.alert((lang === "fr" ? "Erreur : " : "Error: ") + e.message); }
    finally { setBusy(null); }
  };

  return (
    <div className="card" style={{ marginBottom: 16, marginTop: 16 }}>
      <div className="card-head">
        <div className="card-title">
          {lang === "fr" ? "Invitations par lien" : "Invitation links"}
        </div>
        <span className="tag-mono" style={{ marginLeft: 6 }}>{invitations.length}</span>
        <div style={{ flex: 1 }} />
        <div className="text-faint" style={{ fontSize: 11, marginRight: 6 }}>
          {lang === "fr"
            ? "Le membre clique → s'inscrit → rattaché directement (pas de file d'attente)"
            : "Member clicks → signs up → auto-attached (no pending queue)"}
        </div>
        <button className="btn sm primary" onClick={() => setCreateOpen(true)}>
          <Icon.plus /> {lang === "fr" ? "Inviter" : "Invite"}
        </button>
      </div>
      <div className="card-body flush">
        {loading ? (
          <div className="text-faint" style={{ padding: 16, textAlign: "center" }}>{lang === "fr" ? "Chargement…" : "Loading…"}</div>
        ) : invitations.length === 0 ? (
          <div className="text-faint" style={{ padding: 16, textAlign: "center" }}>
            {lang === "fr"
              ? "Aucune invitation pour le moment. Cliquez « Inviter » pour générer un lien."
              : "No invitation yet. Click 'Invite' to generate a link."}
          </div>
        ) : (
          <table className="tbl">
            <thead>
              <tr>
                <th>{lang === "fr" ? "Email" : "Email"}</th>
                {isSuperAdmin && <th>{lang === "fr" ? "Organisation" : "Organization"}</th>}
                <th>{lang === "fr" ? "Expire" : "Expires"}</th>
                <th>{lang === "fr" ? "Statut" : "Status"}</th>
                <th></th>
              </tr>
            </thead>
            <tbody>
              {invitations.map((inv) => {
                const url = window.melr.invitationsCrud.inviteUrl(inv.token);
                // Multi-use display helpers. max_uses === null means unlimited;
                // max_uses === 1 is the legacy single-use case. For anything
                // else we render a progress chip ("3 / 50") next to the
                // status pill so the admin can see how saturated the link is.
                const isMulti = inv.max_uses === null || (typeof inv.max_uses === "number" && inv.max_uses > 1);
                const useCount = typeof inv.use_count === "number" ? inv.use_count : 0;
                const progressLabel = inv.max_uses === null
                  ? (useCount + " / ∞")
                  : (useCount + " / " + inv.max_uses);
                return (
                  <tr key={inv.id} style={{ opacity: inv.status !== "pending" ? 0.55 : 1 }}>
                    <td>{inv.email || <span className="text-faint">{lang === "fr" ? "tout email" : "any email"}</span>}</td>
                    {isSuperAdmin && (
                      <td className="mono text-faint">
                        {inv.organizations ? (inv.organizations.slug || "—") : "—"}
                      </td>
                    )}
                    <td className="text-faint" style={{ fontSize: 11 }}>
                      {new Date(inv.expires_at).toLocaleString(lang === "fr" ? "fr-FR" : "en-US")}
                    </td>
                    <td>
                      <div className="row gap-xs" style={{ flexWrap: "wrap" }}>
                        {inv.status === "pending"  && <span className="pill green dot" style={{ fontSize: 10 }}>{lang === "fr" ? "En attente" : "Pending"}</span>}
                        {inv.status === "used"     && <span className="pill" style={{ background: "var(--bg-sunken)", fontSize: 10 }}>{lang === "fr" ? "Utilisée" : "Used"}</span>}
                        {inv.status === "expired"  && <span className="pill" style={{ background: "#fef3c7", color: "#92400e", fontSize: 10 }}>{lang === "fr" ? "Expirée" : "Expired"}</span>}
                        {isMulti && (
                          <span className="pill mono" style={{ background: "var(--bg-sunken)", fontSize: 10 }}
                                title={lang === "fr" ? "Utilisations / Limite" : "Uses / Limit"}>
                            {progressLabel}
                          </span>
                        )}
                      </div>
                    </td>
                    <td>
                      <div className="row gap-xs" style={{ justifyContent: "flex-end" }}>
                        {inv.status === "pending" && url && (
                          <button className="btn xs" onClick={() => onCopy(url)}
                            title={lang === "fr" ? "Copier le lien" : "Copy link"}>
                            <Icon.copy /> {lang === "fr" ? "Lien" : "Link"}
                          </button>
                        )}
                        <button className="btn xs ghost" onClick={() => onRevoke(inv)} disabled={busy === inv.id}
                          title={lang === "fr" ? "Révoquer" : "Revoke"}>
                          <Icon.x />
                        </button>
                      </div>
                    </td>
                  </tr>
                );
              })}
            </tbody>
          </table>
        )}
      </div>
      {toast && (
        <div style={{
          margin: 12, padding: "8px 12px", borderRadius: 6, fontSize: 12.5,
          background: toast.ok ? "#dcfce7" : "#fee2e2",
          color:      toast.ok ? "#166534" : "#991b1b",
        }}>{toast.msg}</div>
      )}
      {createOpen && (
        <CreateInvitationModal lang={lang}
          allOrgs={allOrgs} isSuperAdmin={isSuperAdmin}
          roles={roles}
          defaultOrgId={effectiveOrgId}
          onClose={() => setCreateOpen(false)}
          onCreated={onInvitationCreated} />
      )}
    </div>
  );
}

// ── Create-invitation modal ────────────────────────────────────────────────
function CreateInvitationModal({ lang, allOrgs, isSuperAdmin, roles, defaultOrgId, onClose, onCreated }) {
  const { profile: meProfile } = window.melr.useCurrentProfile();
  const [orgId, setOrgId]     = useState(defaultOrgId || (meProfile ? meProfile.organization_id : ""));
  const [email, setEmail]     = useState("");
  const [roleId, setRoleId]   = useState("");
  const [validityH, setValidityH] = useState(168); // 7 days
  // maxUses: 1 (single-use) by default · '5'/'10'/'50' for cohort onboarding ·
  // '' (empty string) means unlimited (NULL server-side). Stored as string in
  // the select; we coerce to int (or null) at submit time.
  const [maxUses, setMaxUses] = useState("1");
  const [busy, setBusy]       = useState(false);
  const [err, setErr]         = useState(null);
  // Email-restricted invitations are forced single-use server-side. We
  // mirror that constraint in the UI so the choice is consistent.
  const emailIsSet = !!(email && email.trim());

  // Sync orgId when defaults arrive
  useEffect(() => {
    if (defaultOrgId) setOrgId(defaultOrgId);
    else if (!orgId && meProfile && meProfile.organization_id) setOrgId(meProfile.organization_id);
  }, [defaultOrgId, meProfile]);

  const onSubmit = async (e) => {
    e.preventDefault();
    if (!orgId) { setErr(lang === "fr" ? "Organisation requise." : "Organization required."); return; }
    setBusy(true); setErr(null);
    try {
      // Resolve max_uses: forced to 1 when an email is set; otherwise use
      // the dropdown choice ("" means unlimited → null on the wire).
      const resolvedMaxUses = emailIsSet
        ? 1
        : (maxUses === "" ? null : Number(maxUses));
      const inv = await window.melr.invitationsCrud.create({
        organization_id: orgId,
        email:           email || null,
        role_id:         roleId || null,
        expires_in_hours: validityH,
        max_uses:        resolvedMaxUses,
      });
      // Resolve labels for the email template / toast. We do this here
      // because the modal already has allOrgs + roles in props; the
      // panel just gets the resolved strings.
      const targetOrg = (allOrgs || []).find((o) => o.id === orgId);
      const targetRole = roleId ? (roles || []).find((r) => r.id === roleId) : null;
      const ctx = {
        email:     email || null,
        org_name:  targetOrg ? targetOrg.name : null,
        role_name: targetRole
          ? (lang === "fr" ? (targetRole.name_fr || targetRole.code) : (targetRole.name_en || targetRole.code))
          : null,
      };
      if (onCreated) await onCreated(inv, ctx);
      onClose();
    } 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
      title={lang === "fr" ? "Nouvelle invitation" : "New invitation"}
      onClose={onClose}
      onSubmit={onSubmit}
      footer={<>
        <button type="button" className="btn sm ghost" onClick={onClose} disabled={busy}>{lang === "fr" ? "Annuler" : "Cancel"}</button>
        <button type="submit" className="btn sm primary" disabled={busy}>
          {busy ? "…" : (lang === "fr" ? "Générer le lien" : "Generate link")}
        </button>
      </>}>
      <div style={{ display: "grid", gap: 12 }}>
        <div className="text-faint" style={{ fontSize: 12 }}>
            {lang === "fr"
              ? "Génère un lien d'invitation (usage unique ou multi-usage). Si vous renseignez un email, l'invitation est envoyée automatiquement au destinataire et le lien devient nominatif ; sinon, le lien est juste copié dans le presse-papier et peut être partagé selon la limite d'utilisations choisie."
              : "Generates an invitation link (single- or multi-use). If you set an email, the invitation is auto-sent to the recipient and the link is bound to that account; otherwise the link is just copied to the clipboard and can be shared up to the chosen usage cap."}
          </div>
          {isSuperAdmin && (allOrgs || []).length > 0 && (
            <div>
              <label style={lbl}>{lang === "fr" ? "Organisation *" : "Organization *"}</label>
              <select required style={inp} value={orgId} onChange={(e) => setOrgId(e.target.value)}>
                <option value="">{lang === "fr" ? "— Choisir —" : "— Pick —"}</option>
                {(allOrgs || []).filter((o) => !o.archived_at).map((o) => (
                  <option key={o.id} value={o.id}>{o.name} ({o.slug})</option>
                ))}
              </select>
            </div>
          )}
          <div>
            <label style={lbl}>{lang === "fr" ? "Email (envoi automatique)" : "Email (auto-send)"}</label>
            <input type="email" style={inp} value={email} onChange={(e) => setEmail(e.target.value)}
              placeholder={lang === "fr" ? "marie@org.com (laisser vide = pas d'envoi auto)" : "marie@org.com (leave empty = no auto-send)"} />
            <div className="text-faint" style={{ fontSize: 10.5, marginTop: 4 }}>
              {lang === "fr"
                ? "Si renseigné : (1) seul ce compte pourra consommer le lien ; (2) l'invitation est envoyée automatiquement à cette adresse. Sinon, n'importe qui ayant le lien peut l'utiliser et aucun email n'est envoyé."
                : "If set: (1) only that account can redeem the link; (2) the invitation is auto-sent to that address. Otherwise anyone with the link can use it and no email is sent."}
            </div>
          </div>
          {(roles || []).length > 0 && (
            <div>
              <label style={lbl}>{lang === "fr" ? "Rôle attribué automatiquement (optionnel)" : "Auto-assigned role (optional)"}</label>
              <select style={inp} value={roleId} onChange={(e) => setRoleId(e.target.value)}>
                <option value="">{lang === "fr" ? "— Aucun —" : "— None —"}</option>
                {(roles || []).map((r) => (
                  <option key={r.id} value={r.id}>{lang === "fr" ? (r.name_fr || r.code) : (r.name_en || r.code)}</option>
                ))}
              </select>
            </div>
          )}
          <div>
            <label style={lbl}>{lang === "fr" ? "Validité" : "Validity"}</label>
            <select style={inp} value={validityH} onChange={(e) => setValidityH(Number(e.target.value))}>
              <option value={24}>{lang === "fr" ? "24 heures" : "24 hours"}</option>
              <option value={72}>{lang === "fr" ? "3 jours" : "3 days"}</option>
              <option value={168}>{lang === "fr" ? "7 jours (recommandé)" : "7 days (recommended)"}</option>
              <option value={720}>{lang === "fr" ? "30 jours" : "30 days"}</option>
            </select>
          </div>
          <div>
            <label style={lbl}>{lang === "fr" ? "Nombre d'utilisations" : "Number of uses"}</label>
            <select style={{ ...inp, opacity: emailIsSet ? 0.5 : 1 }}
                    value={emailIsSet ? "1" : maxUses}
                    disabled={emailIsSet}
                    onChange={(e) => setMaxUses(e.target.value)}>
              <option value="1">{lang === "fr" ? "1 — Usage unique (recommandé pour une personne nommée)" : "1 — Single-use (recommended for a named person)"}</option>
              <option value="5">{lang === "fr" ? "5 — Petit groupe" : "5 — Small group"}</option>
              <option value="10">{lang === "fr" ? "10 — Cohorte moyenne" : "10 — Medium cohort"}</option>
              <option value="50">{lang === "fr" ? "50 — Grande cohorte" : "50 — Large cohort"}</option>
              <option value="">{lang === "fr" ? "Illimité (jusqu'à expiration)" : "Unlimited (until expiry)"}</option>
            </select>
            <div className="text-faint" style={{ fontSize: 10.5, marginTop: 4 }}>
              {emailIsSet
                ? (lang === "fr"
                    ? "Forcé à 1 car un email est renseigné : le lien est nominatif."
                    : "Forced to 1 because an email is set: the link is bound to that account.")
                : (lang === "fr"
                    ? "Au-delà d'1, plusieurs personnes différentes peuvent s'inscrire avec le MÊME lien jusqu'à atteindre la limite ou expiration. Idéal pour un enrôlement de cohorte."
                    : "Above 1, several different people can sign up with the SAME link until the limit or expiry is reached. Ideal for cohort onboarding.")}
            </div>
          </div>
        {err && (
          <div style={{ padding: "8px 10px", background: "#fee2e2", color: "#991b1b", borderRadius: 6, fontSize: 12.5 }}>{err}</div>
        )}
      </div>
    </Modal>
  );
}

// ─── Project assignments — admin panel ───────────────────────────────────
// Shows every member of the org with the projects they're affected to.
// Admins can add / remove project assignments per member. The mobile UI
// uses these rows to scope what forms each agent sees.
//
// useProjects() returns mapped objects shaped { uuid, id (=code), nameFr,
// nameEn, ... } — NOT the raw database row. project_agents stores the
// project UUID, so we always join on `p.uuid`, never on `p.id`.
function ProjectAgentsPanel({ lang, members, effectiveOrgId }) {
  // Note: useProjects() returns { projects, loading, error, refresh, realtime }
  // (not the { data } shape used by the newer hooks).
  const { projects: allProjects, loading: projectsLoading } = window.melr.useProjects();
  // When acting on a specific org (super-admin case), filter projects to
  // that org. For a regular admin, useProjects() already returns only
  // their own org's projects via RLS, so this filter is a no-op.
  const projects = effectiveOrgId
    ? (allProjects || []).filter((p) => p.organizationId === effectiveOrgId)
    : (allProjects || []);
  const { data: assignments, refresh } = window.melr.useProjectAgents();
  const [busyKey, setBusyKey] = useState(null);

  // Group assignments by agent_id (assignment.project_id is the UUID)
  const byAgent = new Map();
  (assignments || []).forEach((a) => {
    if (!byAgent.has(a.agent_id)) byAgent.set(a.agent_id, []);
    byAgent.get(a.agent_id).push(a);
  });

  // mapProjectRow exposes the project code as `id` (legacy convention) and
  // the database UUID as `uuid`. Use `id` here.
  const projLabel = (p) => p.id + " · " + (lang === "en" ? (p.nameEn || p.nameFr) : p.nameFr);

  const onAdd = async (agentId, projectUuid) => {
    if (!projectUuid) return;
    const key = agentId + ":" + projectUuid;
    setBusyKey(key);
    try {
      await window.melr.projectAgentsCrud.assign(projectUuid, agentId, null);
      await refresh();
    } catch (e) { window.alert((lang === "fr" ? "Erreur : " : "Error: ") + e.message); }
    finally { setBusyKey(null); }
  };
  const onRemove = async (agentId, projectUuid) => {
    const key = agentId + ":" + projectUuid;
    setBusyKey(key);
    try {
      await window.melr.projectAgentsCrud.remove(projectUuid, agentId);
      await refresh();
    } catch (e) { window.alert((lang === "fr" ? "Erreur : " : "Error: ") + e.message); }
    finally { setBusyKey(null); }
  };

  return (
    <div className="card" style={{ marginBottom: 16 }}>
      <div className="card-head">
        <div className="card-title">
          {lang === "fr" ? "Affectations aux projets" : "Project assignments"}
        </div>
        <span className="tag-mono" style={{ marginLeft: 8 }}>{assignments.length}</span>
        <div style={{ flex: 1 }} />
        <div className="text-faint" style={{ fontSize: 11, marginRight: 6 }}>
          {lang === "fr"
            ? "Limite la liste des formulaires visibles sur mobile · les admins voient tout"
            : "Scopes the form list visible on mobile · admins still see everything"}
        </div>
      </div>
      <div className="card-body flush">
        {projectsLoading ? (
          <div className="text-faint" style={{ padding: 20, textAlign: "center", fontSize: 13 }}>
            {lang === "fr" ? "Chargement des projets…" : "Loading projects…"}
          </div>
        ) : (projects || []).length === 0 ? (
          <div className="text-faint" style={{ padding: 20, textAlign: "center", fontSize: 13 }}>
            {lang === "fr"
              ? "Aucun projet dans votre organisation. Créez un projet d'abord pour pouvoir y affecter des agents."
              : "No project in your organization. Create a project first to assign agents."}
          </div>
        ) : members.length === 0 ? (
          <div className="text-faint" style={{ padding: 20, textAlign: "center", fontSize: 13 }}>
            {lang === "fr" ? "Aucun membre actif." : "No active members."}
          </div>
        ) : (
          members.map((u) => {
            const name = u.full_name || u.email;
            const userAssignments = byAgent.get(u.id) || [];
            const assignedUuids = new Set(userAssignments.map((a) => a.project_id));
            const availableProjects = (projects || []).filter((p) => !assignedUuids.has(p.uuid));
            return (
              <div key={u.id} className="user-row" style={{ flexWrap: "wrap" }}>
                <div className="avatar sm" style={{ background: avColor(name) }}>{initials(name)}</div>
                <div style={{ flex: 1, minWidth: 200 }}>
                  <div className="strong">{name}</div>
                  <div className="text-faint" style={{ fontSize: 11 }}>{u.email}</div>
                </div>
                <div className="row gap-xs" style={{ flexWrap: "wrap", justifyContent: "flex-end" }}>
                  {userAssignments.length === 0 && (
                    <span className="text-faint" style={{ fontSize: 11, marginRight: 6 }}>
                      {lang === "fr" ? "Aucun projet" : "No project"}
                    </span>
                  )}
                  {userAssignments.map((a) => {
                    const proj = (projects || []).find((p) => p.uuid === a.project_id);
                    let label;
                    if (proj) {
                      label = projLabel(proj);
                    } else if (a.projects) {
                      const nm = lang === "en" ? (a.projects.name_en || a.projects.name_fr) : a.projects.name_fr;
                      label = nm ? (a.projects.code + " · " + nm) : a.projects.code;
                    } else {
                      label = "—";
                    }
                    const key = u.id + ":" + a.project_id;
                    return (
                      <span key={a.project_id} className="pill green" style={{ fontSize: 10.5, paddingRight: 2 }}>
                        {label}
                        <button className="btn xs ghost"
                          onClick={() => onRemove(u.id, a.project_id)}
                          disabled={busyKey === key}
                          title={lang === "fr" ? "Retirer cette affectation" : "Remove this assignment"}
                          style={{ padding: 0, marginLeft: 2 }}>
                          <Icon.x className="sm" />
                        </button>
                      </span>
                    );
                  })}
                  {availableProjects.length > 0 && (
                    <select
                      defaultValue=""
                      onChange={(e) => { if (e.target.value) { onAdd(u.id, e.target.value); e.target.value = ""; } }}
                      style={{ padding: "3px 6px", fontSize: 11, borderRadius: 4, border: "1px solid var(--line)", background: "var(--bg, white)", color: "var(--text)" }}
                      title={lang === "fr" ? "Affecter un projet" : "Assign a project"}>
                      <option value="">+ {lang === "fr" ? "projet" : "project"}</option>
                      {availableProjects.map((p) => (
                        <option key={p.uuid} value={p.uuid}>{projLabel(p)}</option>
                      ))}
                    </select>
                  )}
                </div>
              </div>
            );
          })
        )}
      </div>
    </div>
  );
}

// Modal for creating a custom role (code + bilingual names + scope + description)
function NewRoleModal({ lang, onClose, onSaved }) {
  const [code, setCode]         = useState("");
  const [nameFr, setNameFr]     = useState("");
  const [nameEn, setNameEn]     = useState("");
  const [description, setDesc]  = useState("");
  const [scope, setScope]       = useState("organization");
  const [busy, setBusy]         = useState(false);
  const [err, setErr]           = useState(null);

  const onSubmit = async (e) => {
    e.preventDefault();
    if (!code.trim() || !nameFr.trim() || !nameEn.trim()) {
      setErr(lang === "fr" ? "Code et noms requis." : "Code and names required.");
      return;
    }
    setBusy(true); setErr(null);
    try {
      await window.melr.rolesCrud.create({
        code: code.trim().toLowerCase().replace(/[^a-z0-9_]/g, "_"),
        name_fr: nameFr.trim(),
        name_en: nameEn.trim(),
        description: description.trim() || null,
        scope,
      });
      if (onSaved) await onSaved();
      onClose();
    } catch (e) { setErr(e.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" };
  const lbl = { display: "block", fontSize: 11, color: "var(--text-faint)", marginBottom: 4, textTransform: "uppercase", letterSpacing: "0.04em", marginTop: 10 };

  return (
    <div onClick={() => !busy && onClose()} style={{
      position: "fixed", inset: 0, background: "rgba(0,0,0,.5)", zIndex: 9999,
      display: "flex", alignItems: "center", justifyContent: "center",
    }}>
      <form onClick={(e) => e.stopPropagation()} onSubmit={onSubmit} style={{
        background: "var(--bg, white)", color: "var(--text, #111)",
        padding: 22, borderRadius: 10, width: 480, maxWidth: "92vw",
        boxShadow: "0 10px 30px rgba(0,0,0,.25)",
      }}>
        <div style={{ fontSize: 17, fontWeight: 600, marginBottom: 4 }}>
          {lang === "fr" ? "Nouveau rôle" : "New role"}
        </div>

        <label style={lbl}>{lang === "fr" ? "Code (identifiant technique)" : "Code (technical id)"} *</label>
        <input autoFocus required style={inp} value={code} onChange={(e) => setCode(e.target.value)}
          placeholder="data_steward, regional_lead, …" />

        <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 10 }}>
          <div>
            <label style={lbl}>{lang === "fr" ? "Nom (FR)" : "Name (FR)"} *</label>
            <input required style={inp} value={nameFr} onChange={(e) => setNameFr(e.target.value)} />
          </div>
          <div>
            <label style={lbl}>{lang === "fr" ? "Nom (EN)" : "Name (EN)"} *</label>
            <input required style={inp} value={nameEn} onChange={(e) => setNameEn(e.target.value)} />
          </div>
        </div>

        <label style={lbl}>{lang === "fr" ? "Portée" : "Scope"}</label>
        <select style={inp} value={scope} onChange={(e) => setScope(e.target.value)}>
          <option value="organization">{lang === "fr" ? "Organisation" : "Organization"}</option>
          <option value="programme">{lang === "fr" ? "Programme" : "Programme"}</option>
          <option value="project">{lang === "fr" ? "Projet" : "Project"}</option>
          <option value="site">{lang === "fr" ? "Site" : "Site"}</option>
          <option value="global">{lang === "fr" ? "Global" : "Global"}</option>
        </select>

        <label style={lbl}>{lang === "fr" ? "Description" : "Description"}</label>
        <textarea style={{ ...inp, minHeight: 60, resize: "vertical", fontFamily: "inherit" }}
          value={description} onChange={(e) => setDesc(e.target.value)} />

        {err && <div style={{ color: "#b91c1c", fontSize: 12, marginTop: 10 }}>{err}</div>}

        <div style={{ display: "flex", gap: 8, justifyContent: "flex-end", marginTop: 16 }}>
          <button type="button" onClick={onClose} disabled={busy}
            style={{ padding: "8px 14px", borderRadius: 6, border: "1px solid var(--line)", background: "transparent", cursor: "pointer" }}>
            {lang === "fr" ? "Annuler" : "Cancel"}
          </button>
          <button type="submit" disabled={busy}
            style={{ padding: "8px 14px", borderRadius: 6, border: 0, background: "#2563eb", color: "white", cursor: "pointer", fontWeight: 600 }}>
            {busy ? "…" : (lang === "fr" ? "Créer" : "Create")}
          </button>
        </div>

        <div className="text-faint" style={{ fontSize: 11, marginTop: 10 }}>
          {lang === "fr"
            ? "Astuce : les permissions s'attribuent dans la matrice après création."
            : "Tip: permissions are toggled in the matrix after creation."}
        </div>
      </form>
    </div>
  );
}

function SettingsScreen({ t, lang, setRoute, tweaks, setTweak }) {
  const { org, refresh: refreshOrg, loading: orgLoading } = window.melr.useCurrentOrganization();
  const { members } = window.melr.useOrgMembers();
  const [profile, setProfile] = useState(null);
  const [orgForm, setOrgForm] = useState(null);
  const [profForm, setProfForm] = useState(null);
  const [orgBusy, setOrgBusy] = useState(false);
  const [profBusy, setProfBusy] = useState(false);
  const [orgMsg, setOrgMsg] = useState(null);
  const [profMsg, setProfMsg] = useState(null);

  useEffect(() => {
    let cancelled = false;
    window.melr.currentProfile().then((p) => { if (!cancelled && p) { setProfile(p); setProfForm({ full_name: p.full_name || "", locale: p.locale || "fr" }); } });
    return () => { cancelled = true; };
  }, []);
  useEffect(() => {
    if (org) setOrgForm({ name: org.name || "", slug: org.slug || "", logo_url: org.logo_url || "", default_locale: org.default_locale || "fr" });
  }, [org && org.id]);

  const onSaveOrg = async () => {
    if (!orgForm || !orgForm.name.trim()) { setOrgMsg(lang === "fr" ? "Nom requis." : "Name required."); return; }
    setOrgBusy(true); setOrgMsg(null);
    try {
      await window.melr.updateOrganization({
        name: orgForm.name.trim(),
        slug: orgForm.slug.trim() || null,
        logo_url: orgForm.logo_url.trim() || null,
        default_locale: orgForm.default_locale || "fr",
      });
      await refreshOrg();
      setOrgMsg(lang === "fr" ? "✓ Organisation enregistrée." : "✓ Organization saved.");
      setTimeout(() => setOrgMsg(null), 3000);
    } catch (e) { setOrgMsg(e.message); }
    finally { setOrgBusy(false); }
  };

  const onSaveProfile = async () => {
    if (!profForm || !profForm.full_name.trim()) { setProfMsg(lang === "fr" ? "Nom requis." : "Name required."); return; }
    setProfBusy(true); setProfMsg(null);
    try {
      const updated = await window.melr.updateProfile({
        full_name: profForm.full_name.trim(),
        locale: profForm.locale || "fr",
      });
      setProfile(updated);
      // Propagate locale change to the app-level tweak so the UI flips language
      if (setTweak && updated && updated.locale) setTweak("lang", updated.locale);
      setProfMsg(lang === "fr" ? "✓ Profil enregistré." : "✓ Profile saved.");
      setTimeout(() => setProfMsg(null), 3000);
    } catch (e) { setProfMsg(e.message); }
    finally { setProfBusy(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" };
  const lbl = { display: "block", fontSize: 11, color: "var(--text-faint)", marginBottom: 4, textTransform: "uppercase", letterSpacing: "0.04em", marginTop: 10 };

  return (
    <div className="page">
      <div className="page-header">
        <div className="page-eyebrow">{lang === "fr" ? "ADMINISTRATION / PARAMÈTRES" : "ADMINISTRATION / SETTINGS"}</div>
        <h1 className="page-title">{t("nav.settings")}</h1>
        <div className="page-sub">
          {lang === "fr"
            ? "Organisation · profil personnel · apparence · raccourcis vers les autres modules admin"
            : "Organization · personal profile · appearance · shortcuts to other admin modules"}
        </div>
      </div>

      <div className="page-body">
        <div className="grid cols-2" style={{ gap: 16 }}>
          {/* Organisation */}
          <div className="card">
            <div className="card-head">
              <div className="card-title">{lang === "fr" ? "Organisation" : "Organization"}</div>
              <span className="tag-mono" style={{ marginLeft: 8 }}>{members.length} {lang === "fr" ? "membres" : "members"}</span>
            </div>
            <div className="card-body">
              {orgLoading || !orgForm ? (
                <div className="text-faint" style={{ padding: 12, textAlign: "center", fontSize: 12 }}>
                  {lang === "fr" ? "Chargement…" : "Loading…"}
                </div>
              ) : (
                <>
                  <label style={lbl}>{lang === "fr" ? "Nom de l'organisation" : "Organization name"} *</label>
                  <input style={inp} value={orgForm.name} onChange={(e) => setOrgForm({ ...orgForm, name: e.target.value })} />

                  <label style={lbl}>{lang === "fr" ? "Identifiant court (slug)" : "Short identifier (slug)"}</label>
                  <input style={inp} value={orgForm.slug} onChange={(e) => setOrgForm({ ...orgForm, slug: e.target.value })}
                    placeholder="reft-africa, melr-sahel…" />

                  <label style={lbl}>{lang === "fr" ? "URL du logo" : "Logo URL"}</label>
                  <input style={inp} value={orgForm.logo_url} onChange={(e) => setOrgForm({ ...orgForm, logo_url: e.target.value })}
                    placeholder="https://…/logo.png" />

                  <label style={lbl}>{lang === "fr" ? "Langue par défaut" : "Default language"}</label>
                  <select style={inp} value={orgForm.default_locale} onChange={(e) => setOrgForm({ ...orgForm, default_locale: e.target.value })}>
                    <option value="fr">Français</option>
                    <option value="en">English</option>
                  </select>

                  {orgMsg && <div style={{ marginTop: 10, fontSize: 12, color: orgMsg.startsWith("✓") ? "#15803d" : "#b91c1c" }}>{orgMsg}</div>}

                  <div style={{ display: "flex", gap: 8, justifyContent: "flex-end", marginTop: 14 }}>
                    <button className="btn sm primary" onClick={onSaveOrg} disabled={orgBusy}>
                      {orgBusy ? "…" : (lang === "fr" ? "Enregistrer" : "Save")}
                    </button>
                  </div>
                </>
              )}
            </div>
          </div>

          {/* Mon profil */}
          <div className="card">
            <div className="card-head">
              <div className="card-title">{lang === "fr" ? "Mon profil" : "My profile"}</div>
              {profile && <span className="text-faint" style={{ marginLeft: 8, fontSize: 11 }}>{profile.email}</span>}
            </div>
            <div className="card-body">
              {!profForm ? (
                <div className="text-faint" style={{ padding: 12, textAlign: "center", fontSize: 12 }}>
                  {lang === "fr" ? "Chargement…" : "Loading…"}
                </div>
              ) : (
                <>
                  <label style={lbl}>{lang === "fr" ? "Nom complet" : "Full name"} *</label>
                  <input style={inp} value={profForm.full_name} onChange={(e) => setProfForm({ ...profForm, full_name: e.target.value })} />

                  <label style={lbl}>{lang === "fr" ? "Langue d'interface" : "Interface language"}</label>
                  <select style={inp} value={profForm.locale} onChange={(e) => setProfForm({ ...profForm, locale: e.target.value })}>
                    <option value="fr">Français</option>
                    <option value="en">English</option>
                  </select>

                  {profMsg && <div style={{ marginTop: 10, fontSize: 12, color: profMsg.startsWith("✓") ? "#15803d" : "#b91c1c" }}>{profMsg}</div>}

                  <div style={{ display: "flex", gap: 8, justifyContent: "space-between", marginTop: 14, alignItems: "center" }}>
                    <button className="btn sm" onClick={() => setRoute && setRoute("profile")}
                      title={lang === "fr" ? "Mot de passe, avatar, etc." : "Password, avatar, etc."}>
                      {lang === "fr" ? "Profil complet →" : "Full profile →"}
                    </button>
                    <button className="btn sm primary" onClick={onSaveProfile} disabled={profBusy}>
                      {profBusy ? "…" : (lang === "fr" ? "Enregistrer" : "Save")}
                    </button>
                  </div>
                </>
              )}
            </div>
          </div>

          {/* Apparence */}
          <div className="card">
            <div className="card-head">
              <div className="card-title">{lang === "fr" ? "Apparence" : "Appearance"}</div>
              <span className="text-faint" style={{ marginLeft: 8, fontSize: 11 }}>{lang === "fr" ? "préférences locales" : "local preferences"}</span>
            </div>
            <div className="card-body">
              <label style={lbl}>{lang === "fr" ? "Mode" : "Mode"}</label>
              <div className="row gap-sm" style={{ marginTop: 2 }}>
                <button className={"btn sm " + (!tweaks.dark ? "primary" : "")}
                  onClick={() => setTweak && setTweak("dark", false)}>
                  ☀ {lang === "fr" ? "Clair" : "Light"}
                </button>
                <button className={"btn sm " + (tweaks.dark ? "primary" : "")}
                  onClick={() => setTweak && setTweak("dark", true)}>
                  ☾ {lang === "fr" ? "Sombre" : "Dark"}
                </button>
              </div>

              <label style={lbl}>{lang === "fr" ? "Couleur d'accent" : "Accent colour"}</label>
              <div className="row gap-sm" style={{ marginTop: 2 }}>
                {["indigo", "blue", "violet", "emerald", "rose"].map((c) => (
                  <button key={c} className={"btn sm " + (tweaks.accent === c ? "primary" : "")}
                    onClick={() => setTweak && setTweak("accent", c)}
                    style={{ textTransform: "capitalize" }}>
                    {c}
                  </button>
                ))}
              </div>

              <label style={lbl}>{lang === "fr" ? "Densité" : "Density"}</label>
              <div className="row gap-sm" style={{ marginTop: 2 }}>
                {["compact", "default", "spacious"].map((d) => (
                  <button key={d} className={"btn sm " + (tweaks.density === d ? "primary" : "")}
                    onClick={() => setTweak && setTweak("density", d)}
                    style={{ textTransform: "capitalize" }}>
                    {d === "compact" ? (lang === "fr" ? "Compact" : "Compact")
                     : d === "default" ? (lang === "fr" ? "Standard" : "Default")
                     : (lang === "fr" ? "Aéré" : "Spacious")}
                  </button>
                ))}
              </div>

              <div className="text-faint" style={{ fontSize: 11, marginTop: 12 }}>
                {lang === "fr"
                  ? "Ces préférences sont stockées localement dans votre navigateur — synchronisation cross-device à venir."
                  : "Stored locally in your browser — cross-device sync coming later."}
              </div>
            </div>
          </div>

          {/* Liens d'administration */}
          <div className="card">
            <div className="card-head">
              <div className="card-title">{lang === "fr" ? "Liens d'administration" : "Admin shortcuts"}</div>
            </div>
            <div className="card-body" style={{ display: "flex", flexDirection: "column", gap: 8 }}>
              <SettingsLink lang={lang} setRoute={setRoute} target="org"
                icon="users"
                titleFr="Organisation & rôles" titleEn="Organization & roles"
                subFr="Membres, rôles, matrice de permissions" subEn="Members, roles, permissions matrix" />
              <SettingsLink lang={lang} setRoute={setRoute} target="workflow"
                icon="badgeCheck"
                titleFr="Configurer les flux d'approbation" titleEn="Configure approval flows"
                subFr="Modèles SAT / DVT / rapport · SLA et étapes" subEn="SAT / DVT / report templates · SLA + steps" />
              <SettingsLink lang={lang} setRoute={setRoute} target="notifications"
                icon="bell"
                titleFr="Notifications & règles d'alerte" titleEn="Notifications & alert rules"
                subFr="Canaux, fréquence, mentions, audit" subEn="Channels, frequency, mentions, audit" />
              <SettingsLink lang={lang} setRoute={setRoute} target="help"
                icon="info"
                titleFr="Aide & documentation" titleEn="Help & docs"
                subFr="Démarrage, modules, raccourcis" subEn="Quickstart, modules, shortcuts" />
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

function SettingsLink({ lang, setRoute, target, icon, titleFr, titleEn, subFr, subEn }) {
  const Ic = Icon[icon] || Icon.info;
  return (
    <button onClick={() => setRoute && setRoute(target)}
      style={{
        display: "grid", gridTemplateColumns: "auto 1fr auto", gap: 10, alignItems: "center",
        padding: "10px 12px", borderRadius: 6, border: "1px solid var(--line)",
        background: "var(--bg-elev, white)", color: "var(--text)", cursor: "pointer",
        textAlign: "left", width: "100%",
      }}
      onMouseEnter={(e) => e.currentTarget.style.background = "var(--bg-hover, #f3f4f6)"}
      onMouseLeave={(e) => e.currentTarget.style.background = "var(--bg-elev, white)"}>
      <Ic />
      <div>
        <div style={{ fontWeight: 600, fontSize: 13 }}>{lang === "fr" ? titleFr : titleEn}</div>
        <div className="text-faint" style={{ fontSize: 11, marginTop: 2 }}>{lang === "fr" ? subFr : subEn}</div>
      </div>
      <Icon.chevronRight />
    </button>
  );
}

// ==================== MOUNT ====================
const root = ReactDOM.createRoot(document.getElementById("app"));
root.render(<AppRouter />);
