/* global React, Icon, Modal */
const { useState: useStateA, useEffect: useEffectA, useMemo: useMemoA } = React;

// NOTE: no class-based ErrorBoundary here. An earlier attempt to add one
// (with `class extends React.Component`) appeared to break Babel standalone
// parsing of this whole file in some environments, leaving window.Activities
// undefined. We rely on defensive coding + console logging instead.

// ============================================================================
// ACTIVITIES SCREEN (E3) · gestion des activités projet
// ----------------------------------------------------------------------------
// 1 activité = 1 projet (FK obligatoire — donc le champ « PROJET » est toujours
// connu et apparaît systématiquement en premier dans le modal de saisie).
//
// Cette page permet de :
//   • lister les activités (filtrables par projet, statut, type, dates)
//   • créer / éditer / supprimer une activité avec tous les champs métier
//   • saisir GPS (latitude/longitude), partenaires, indicateurs, pièces jointes
//   • cocher PRESSE / TDRs / Rapport
//   • ajouter à la volée un nouveau type d'activité, partenaire ou composante
//     projet (sans quitter le modal)
// ============================================================================

// Status options shared by filter + modal. Order = workflow order.
const ACTIVITY_STATUSES = [
  // not_started : couleur assez foncée pour rester lisible avec un texte
  // blanc (l'ancienne 0.78 était trop claire → la pastille paraissait vide,
  // d'où l'impression de statut « non renseigné »).
  { v: "not_started", fr: "Non démarrée",   en: "Not started", es: "No iniciada",  color: "oklch(0.52 0.04 250)" },
  { v: "ongoing",     fr: "En cours",       en: "Ongoing", es: "En curso",      color: "oklch(0.55 0.16 230)" },
  { v: "completed",   fr: "Terminée",       en: "Completed", es: "Finalizada",    color: "oklch(0.55 0.16 145)" },
  { v: "postponed",   fr: "Reportée",       en: "Postponed", es: "Aplazada",    color: "oklch(0.65 0.16 70)"  },
  { v: "cancelled",   fr: "Annulée",        en: "Cancelled", es: "Cancelada",    color: "oklch(0.55 0.16 12)"  },
];
function statusLabel(slug, lang) {
  const s = ACTIVITY_STATUSES.find((x) => x.v === slug);
  if (s) return (lang === "es" ? (s.es != null ? s.es : s.en) : lang === "fr" ? s.fr : s.en);
  // Valeur absente → afficher le statut par défaut « Non démarrée » plutôt
  // qu'une pastille vide. Valeur inconnue (non vide) → l'afficher telle quelle.
  if (!slug) { const d = ACTIVITY_STATUSES[0]; return (lang === "es" ? (d.es != null ? d.es : d.en) : lang === "fr" ? d.fr : d.en); }
  return slug;
}
function statusColor(slug) {
  const s = ACTIVITY_STATUSES.find((x) => x.v === slug);
  return s ? s.color : ACTIVITY_STATUSES[0].color;
}

function Activities({ t, lang, isSuperAdmin, effectiveOrgId, isAdmin, hasPerm, lockedProjectUuid, embedded }) {
  // Mode « projet verrouillé » (onglet Activités dans le détail Projet) :
  // lockedProjectUuid restreint la liste/la saisie à ce seul projet et masque
  // le filtre Projet. `embedded` enlève le grand titre d'écran (on est dans
  // un onglet). L'UI reste sinon strictement identique au module du menu.
  // ── Data ────────────────────────────────────────────────────────────────
  const { projects, loading: projectsLoading } = window.melr.useProjects();
  const { data: activities, loading } = window.melr.useActivities(lockedProjectUuid || null);
  const { data: types }      = window.melr.useActivityTypes();
  const { data: partners }   = window.melr.usePartners();

  // Visible projects in the org context. mapProjectRow exposes the row as
  // { uuid, id (= legacy code), nameFr, nameEn, organizationId, ... } — NOT
  // the raw snake_case from Supabase. We key everything off `uuid` since
  // activities.project_id holds the UUID FK.
  // For super-admin acting-as-org we additionally narrow client-side.
  const visibleProjects = useMemoA(() => {
    const list = projects || [];
    if (!effectiveOrgId || isSuperAdmin) return list;
    return list.filter((p) => !p.organizationId || p.organizationId === effectiveOrgId);
  }, [projects, effectiveOrgId, isSuperAdmin]);

  // En mode verrouillé, l'éditeur ne propose que le projet courant (champ
  // Projet figé). Sinon, tous les projets visibles.
  const editorProjects = useMemoA(
    () => (lockedProjectUuid ? visibleProjects.filter((p) => p.uuid === lockedProjectUuid) : visibleProjects),
    [visibleProjects, lockedProjectUuid],
  );

  // ── Filters ─────────────────────────────────────────────────────────────
  const [fProject,  setFProject]  = useStateA(lockedProjectUuid || "");
  const [fStatus,   setFStatus]   = useStateA("");
  const [fType,     setFType]     = useStateA("");
  const [fSearch,   setFSearch]   = useStateA("");
  const [editing,   setEditing]   = useStateA(null);   // null | "new" | row.id
  const [busy,      setBusy]      = useStateA(null);
  const [toast,     setToast]     = useStateA(null);

  const visibleActivities = useMemoA(() => {
    // a.project_id is a UUID — match against p.uuid (the actual Supabase id),
    // NOT p.id (which is the legacy code like "P-241").
    const orgProjectUuids = new Set(visibleProjects.map((p) => p.uuid));
    return (activities || []).filter((a) => {
      if (orgProjectUuids.size && !orgProjectUuids.has(a.project_id) && !isSuperAdmin) return false;
      if (fProject && a.project_id !== fProject) return false;
      if (fStatus  && a.status     !== fStatus)  return false;
      if (fType    && a.activity_type_id !== fType) return false;
      if (fSearch) {
        const q = fSearch.toLowerCase();
        const hay = `${a.title || ""} ${a.description || ""} ${a.location_name || ""} ${a.code || ""}`.toLowerCase();
        if (!hay.includes(q)) return false;
      }
      return true;
    });
  }, [activities, visibleProjects, fProject, fStatus, fType, fSearch, isSuperAdmin]);

  // Keyed by p.uuid so we can look up project metadata from activity.project_id
  const projectsByUuid = useMemoA(() => Object.fromEntries(visibleProjects.map((p) => [p.uuid, p])), [visibleProjects]);
  const typesById      = useMemoA(() => Object.fromEntries((types || []).map((t) => [t.id, t])),     [types]);

  const onDelete = async (a) => {
    if (!window.confirm(window.L(`Supprimer l'activité « ${a.title} » ? Cette action est irréversible.`, `Delete activity "${a.title}"? This action is irreversible.`, `¿Eliminar la actividad «${a.title}»? Esta acción es irreversible.`))) return;
    setBusy(a.id);
    try {
      await window.melr.activitiesCrud.remove(a.id);
      setToast({ ok: true, msg: window.L("Activité supprimée.", "Activity deleted.", "Actividad eliminada.") });
    } catch (e) {
      setToast({ ok: false, msg: e.message });
    } finally {
      setBusy(null);
      setTimeout(() => setToast(null), 3500);
    }
  };

  // ── Export (Excel / Word / PDF) de la sélection courante (filtres appliqués) ──
  const ACTIVITY_EXPORT_COLS = [
    { key: "projet",       label: window.L("Projet", "Project", "Proyecto") },
    { key: "code",         label: "Code" },
    { key: "titre",        label: window.L("Titre", "Title", "Título") },
    { key: "type",         label: "Type" },
    { key: "composante",   label: window.L("Composante", "Component", "Componente") },
    { key: "responsable",  label: window.L("Responsable", "Lead", "Responsable") },
    { key: "pmo",          label: "PMO" },
    { key: "beneficiaires", label: window.L("Bénéficiaires", "Beneficiaries", "Beneficiarios") },
    { key: "lieu",         label: window.L("Lieu", "Location", "Lugar") },
    { key: "debut",        label: window.L("Début", "Start", "Inicio") },
    { key: "fin",          label: window.L("Fin", "End", "Fin") },
    { key: "statut",       label: window.L("Statut", "Status", "Estado") },
  ];
  const buildActivityRows = () => visibleActivities.map((a) => {
    const proj = projectsByUuid[a.project_id];
    const type = typesById[a.activity_type_id];
    return {
      projet:        proj ? proj.id : "",
      code:          a.code || "",
      titre:         a.title || "",
      type:          type ? ((lang === "es" ? (type.name_es != null ? type.name_es : type.name_en || type.name_fr) : lang === "fr" ? type.name_fr : type.name_en || type.name_fr)) : "",
      composante:    a.component ? ((lang === "es" ? (a.component.name_es != null ? a.component.name_es : a.component.name_en || a.component.name_fr) : lang === "fr" ? a.component.name_fr : a.component.name_en || a.component.name_fr)) : "",
      responsable:   a.responsible ? a.responsible.full_name : "",
      pmo:           a.pmo_stk ? (a.pmo_stk.name + ((a.pmo_stakeholder_ids && a.pmo_stakeholder_ids.length > 1) ? " +" + (a.pmo_stakeholder_ids.length - 1) : "")) : "",
      beneficiaires: a.beneficiaries_text || "",
      lieu:          a.location_name || "",
      debut:         a.start_date || "",
      fin:           a.end_date || "",
      statut:        statusLabel(a.status, lang),
    };
  });
  const exportActivities = (format) => {
    const rows = buildActivityRows();
    if (!rows.length) { setToast({ ok: false, msg: window.L("Aucune activité à exporter.", "No activity to export.", "Ninguna actividad para exportar.") }); setTimeout(() => setToast(null), 3000); return; }
    const date  = new Date().toISOString().slice(0, 10);
    const pid   = lockedProjectUuid || fProject;
    const scope = pid ? ((projectsByUuid[pid] || {}).id || "projet") : (window.L("toutes", "all", "todas"));
    const base  = "activites-" + scope + "-" + date;
    const title = (window.L("Activités · ", "Activities · ", "Actividades · ")) + scope;
    if (format === "excel")     window.melr.exportRowsExcel(base + ".xlsx", rows, ACTIVITY_EXPORT_COLS, "Activités");
    else if (format === "word") window.melr.exportRowsWord(base + ".docx", title, rows, ACTIVITY_EXPORT_COLS);
    else if (format === "pdf")  window.melr.exportRowsPdf(base + ".pdf", title, rows, ACTIVITY_EXPORT_COLS);
  };

  const editingRow = editing && editing !== "new"
    ? (activities || []).find((a) => a.id === editing)
    : null;

  // ── Render ─────────────────────────────────────────────────────────────
  return (
    <div className={embedded ? "" : "screen"} data-screen-label="Activities">
      <div className="row" style={{ alignItems: "center", marginBottom: 12 }}>
        {embedded ? (
          <div className="text-faint" style={{ fontSize: 12 }}>
            {window.L("Activités de mise en œuvre de ce projet (formations, visites, ateliers, etc.)", "Implementation activities for this project (training, visits, workshops, etc.)", "Actividades de ejecución de este proyecto (formaciones, visitas, talleres, etc.)")}
          </div>
        ) : (
          <div>
            <div className="screen-title">{window.L("Activités", "Activities", "Actividades")}</div>
            <div className="text-faint" style={{ fontSize: 12, marginTop: 2 }}>
              {window.L("Suivi des activités de mise en œuvre par projet (formations, visites, ateliers, etc.)", "Tracking implementation activities by project (training, visits, workshops, etc.)", "Seguimiento de las actividades de ejecución por proyecto (formaciones, visitas, talleres, etc.)")}
            </div>
          </div>
        )}
        <div style={{ flex: 1 }} />
        {visibleActivities.length > 0 && (() => {
          // Couleurs harmonisées avec les autres exports de la plateforme :
          // Excel = vert, Word = bleu, PDF = rouge (cf. Reporting).
          const expStyle = (color) => ({
            padding: "6px 12px", borderRadius: 6, border: "1px solid " + color,
            background: color, color: "white", fontSize: 12.5, fontWeight: 600,
            whiteSpace: "nowrap", cursor: "pointer",
          });
          return (
            <div className="row gap-xs" style={{ marginRight: 8 }}>
              <button style={expStyle("#15803d")} onClick={() => exportActivities("excel")} title={window.L("Exporter en Excel", "Export to Excel", "Exportar a Excel")}>📈 Excel</button>
              <button style={expStyle("#2563eb")} onClick={() => exportActivities("word")} title={window.L("Exporter en Word", "Export to Word", "Exportar a Word")}>📄 Word</button>
              <button style={expStyle("#dc2626")} onClick={() => exportActivities("pdf")} title={window.L("Exporter en PDF", "Export to PDF", "Exportar a PDF")}>📕 PDF</button>
            </div>
          );
        })()}
        <button className="btn sm primary" onClick={() => setEditing("new")}>
          <Icon.plus /> {window.L("Nouvelle activité", "New activity", "Nueva actividad")}
        </button>
      </div>

      {/* Filter bar */}
      <div className="card" style={{ marginBottom: 12 }}>
        <div className="card-body" style={{ display: "grid", gridTemplateColumns: lockedProjectUuid ? "1fr 1fr 2fr" : "1.5fr 1fr 1fr 2fr", gap: 8 }}>
          {!lockedProjectUuid && (
            <select className="input" value={fProject} onChange={(e) => setFProject(e.target.value)}>
              <option value="">{window.L("— Tous les projets —", "— All projects —", "— Todos los proyectos —")}</option>
              {visibleProjects.map((p) => (
                <option key={p.uuid} value={p.uuid}>{p.id} · {lang === "fr" ? (p.nameFr || p.nameEn) : (p.nameEn || p.nameFr)}</option>
              ))}
            </select>
          )}
          <select className="input" value={fStatus} onChange={(e) => setFStatus(e.target.value)}>
            <option value="">{window.L("— Tous statuts —", "— All statuses —", "— Todos los estados —")}</option>
            {ACTIVITY_STATUSES.map((s) => (
              <option key={s.v} value={s.v}>{(lang === "es" ? (s.es != null ? s.es : s.en) : lang === "fr" ? s.fr : s.en)}</option>
            ))}
          </select>
          <select className="input" value={fType} onChange={(e) => setFType(e.target.value)}>
            <option value="">{window.L("— Tous types —", "— All types —", "— Todos los tipos —")}</option>
            {(types || []).map((tt) => (
              <option key={tt.id} value={tt.id}>{(lang === "es" ? (tt.name_es != null ? tt.name_es : tt.name_en || tt.name_fr) : lang === "fr" ? tt.name_fr : tt.name_en || tt.name_fr)}</option>
            ))}
          </select>
          <input className="input" type="search" placeholder={window.L("Recherche (titre, description, lieu…)", "Search (title, description, location…)", "Búsqueda (título, descripción, lugar…)")}
            value={fSearch} onChange={(e) => setFSearch(e.target.value)} />
        </div>
      </div>

      {toast && (
        <div style={{
          marginBottom: 8, padding: "8px 12px", borderRadius: 6, fontSize: 12.5,
          background: toast.ok ? "#dcfce7" : "#fee2e2",
          color:      toast.ok ? "#166534" : "#991b1b",
        }}>{toast.msg}</div>
      )}

      <div className="card">
        <div className="card-body flush" style={{
          overflowX: "auto", overflowY: "auto",
          maxHeight: "calc(100vh - 280px)",
        }}>
          {loading || projectsLoading ? (
            <div className="text-faint" style={{ padding: 24, textAlign: "center" }}>{window.L("Chargement…", "Loading…", "Cargando…")}</div>
          ) : visibleActivities.length === 0 ? (
            <div className="text-faint" style={{ padding: 24, textAlign: "center" }}>
              {window.L("Aucune activité. Cliquez sur « Nouvelle activité » pour en créer une.", "No activities yet. Click 'New activity' to create one.", "Ninguna actividad todavía. Haga clic en «Nueva actividad» para crear una.")}
            </div>
          ) : (
            <table className="tbl" style={{ minWidth: "100%" }}>
              <thead>
                <tr>
                  <th>{window.L("Projet", "Project", "Proyecto")}</th>
                  <th>{window.L("Titre", "Title", "Título")}</th>
                  <th>{window.L("Type", "Type", "Tipo")}</th>
                  <th>{window.L("Lieu", "Location", "Lugar")}</th>
                  <th>{window.L("Début", "Start", "Inicio")}</th>
                  <th>{window.L("Fin", "End", "Fin")}</th>
                  <th>{window.L("Statut", "Status", "Estado")}</th>
                  <th style={{
                    position: "sticky", right: 0, zIndex: 2,
                    minWidth: 140, background: "var(--bg-sunken)",
                    boxShadow: "-4px 0 6px -4px rgba(0,0,0,0.08)",
                  }}>{window.L("Actions", "Actions", "Acciones")}</th>
                </tr>
              </thead>
              <tbody>
                {visibleActivities.map((a) => {
                  const proj = projectsByUuid[a.project_id];
                  const type = typesById[a.activity_type_id];
                  return (
                    <tr key={a.id}>
                      <td className="mono" style={{ fontSize: 11.5 }}>
                        {/* p.id IS the legacy code (project code like "P-241"). */}
                        {proj ? proj.id : <span className="text-faint">—</span>}
                      </td>
                      <td>
                        <div style={{ fontWeight: 500 }}>{a.title}</div>
                        {a.code && <div className="text-faint mono" style={{ fontSize: 10.5 }}>{a.code}</div>}
                      </td>
                      <td>{type
                        ? <span className="pill" style={{ fontSize: 10.5 }}>{(lang === "es" ? (type.name_es != null ? type.name_es : type.name_en || type.name_fr) : lang === "fr" ? type.name_fr : type.name_en || type.name_fr)}</span>
                        : <span className="text-faint">—</span>}</td>
                      <td className="text-faint" style={{ fontSize: 11 }}>
                        {(() => {
                          if (a.location_name) return a.location_name;
                          // Coerce defensively in case PostgREST returns numeric as string.
                          const la = Number(a.latitude), lo = Number(a.longitude);
                          if (Number.isFinite(la) && Number.isFinite(lo)) {
                            return `${la.toFixed(3)}, ${lo.toFixed(3)}`;
                          }
                          return "—";
                        })()}
                      </td>
                      <td className="text-faint" style={{ fontSize: 11 }}>{a.start_date || "—"}</td>
                      <td className="text-faint" style={{ fontSize: 11 }}>{a.end_date   || "—"}</td>
                      <td>
                        <span className="pill" style={{ fontSize: 10.5, background: statusColor(a.status), color: "white" }}>
                          {statusLabel(a.status, lang)}
                        </span>
                      </td>
                      <td style={{
                        position: "sticky", right: 0,
                        background: "var(--bg-elev, white)",
                        boxShadow: "-4px 0 6px -4px rgba(0,0,0,0.08)",
                        minWidth: 140,
                      }}>
                        <div className="row gap-xs" style={{ justifyContent: "flex-end" }}>
                          <button className="btn sm primary" onClick={() => setEditing(a.id)}
                            style={{ fontSize: 11.5, padding: "3px 10px" }}>
                            <Icon.edit /> {window.L("Modifier", "Edit", "Editar")}
                          </button>
                          <button className="btn xs ghost" onClick={() => onDelete(a)} disabled={busy === a.id}
                            title={window.L("Supprimer", "Delete", "Eliminar")}>
                            <Icon.x />
                          </button>
                        </div>
                      </td>
                    </tr>
                  );
                })}
              </tbody>
            </table>
          )}
        </div>
      </div>

      {editing && (
        <ActivityEditorModal
          key={editing}
          lang={lang}
          existing={editingRow}
          visibleProjects={editorProjects}
          isAdmin={isAdmin || isSuperAdmin}
          onClose={() => setEditing(null)}
          onSaved={() => setEditing(null)}
        />
      )}
    </div>
  );
}

// ============================================================================
// ACTIVITY EDITOR MODAL · all the metadata + multi-pickers + attachments
// ============================================================================
function ActivityEditorModal({ lang, existing, visibleProjects, isAdmin, onClose, onSaved }) {
  const isNew = !existing;
  // Stateful current id so the attachments section becomes available
  // immediately after first save without closing/reopening the modal.
  const [currentId, setCurrentId] = useStateA(existing ? existing.id : null);

  // ── Core fields ────────────────────────────────────────────────────────
  // PROJET is always the first field — required by spec ("le champ PROJET en
  // première ligne du formulaire toujours"). Empty by default for new
  // activities; pre-filled when editing.
  const [projectId,  setProjectId]  = useStateA(existing ? existing.project_id : "");
  const [code,       setCode]       = useStateA(existing ? (existing.code || "")        : "");
  const [title,      setTitle]      = useStateA(existing ? (existing.title || "")       : "");
  const [description,setDescription]= useStateA(existing ? (existing.description || "") : "");
  const [typeId,     setTypeId]     = useStateA(existing ? (existing.activity_type_id || "") : "");
  const [componentId,setComponentId]= useStateA(existing ? (existing.project_component_id || "") : "");
  const [locationName,setLocationName] = useStateA(existing ? (existing.location_name || "") : "");
  const [lat,        setLat]        = useStateA(existing && existing.latitude  != null ? String(existing.latitude)  : "");
  const [lng,        setLng]        = useStateA(existing && existing.longitude != null ? String(existing.longitude) : "");
  // Attache geographique fine (Pays > Region > Dept > Arr > Commune > Quartier)
  // En edition, on hydrate avec les FKs existantes (commune_id/quartier_id).
  // Le pays par defaut est SN ; les niveaux superieurs seront resolus a
  // la volee depuis commune_id si manquants — mais en pratique l'utilisateur
  // les remplit cascade depuis le pays.
  const [geo, setGeo] = useStateA(() => existing
    ? {
        country_iso2: "SN",                          // a remplir manuellement si l'historique en avait un autre
        commune_id:  existing.commune_id  || null,
        quartier_id: existing.quartier_id || null,
      }
    : { country_iso2: "SN" });
  const [startDate,  setStartDate]  = useStateA(existing ? (existing.start_date || "") : "");
  const [endDate,    setEndDate]    = useStateA(existing ? (existing.end_date   || "") : "");
  const [status,     setStatus]     = useStateA(existing ? (existing.status || "not_started") : "not_started");
  const [genderSensitive, setGenderSensitive] = useStateA(existing ? !!existing.gender_sensitive : false);
  const [budgetPlanned, setBudgetPlanned] = useStateA(existing && existing.budget_planned != null ? String(existing.budget_planned) : "");
  const [costActual,    setCostActual]    = useStateA(existing && existing.cost_actual    != null ? String(existing.cost_actual)    : "");
  const [currency,   setCurrency]   = useStateA(existing ? (existing.currency || "XOF") : "XOF");
  const [hasPress,   setHasPress]   = useStateA(existing ? !!existing.has_press  : false);
  const [hasTdrs,    setHasTdrs]    = useStateA(existing ? !!existing.has_tdrs   : false);
  const [hasReport,  setHasReport]  = useStateA(existing ? !!existing.has_report : false);
  const [notes,      setNotes]      = useStateA(existing ? (existing.notes || "") : "");
  // Nouveaux champs : Responsable (membre équipe), PMO (partenaire), Bénéficiaires (texte).
  const [responsibleId, setResponsibleId] = useStateA(existing ? (existing.responsible_user_id || "") : "");
  // PMO + partenaires de mise en œuvre = parties prenantes du projet.
  const [pmoStakeholderIds, setPmoStakeholderIds] = useStateA(
    existing && Array.isArray(existing.pmo_stakeholder_ids) && existing.pmo_stakeholder_ids.length
      ? existing.pmo_stakeholder_ids
      : (existing && existing.pmo_stakeholder_id ? [existing.pmo_stakeholder_id] : [])
  );
  const [implPartnerIds, setImplPartnerIds] = useStateA(existing && Array.isArray(existing.implementing_stakeholder_ids) ? existing.implementing_stakeholder_ids : []);
  const [beneficiariesText, setBeneficiariesText] = useStateA(existing ? (existing.beneficiaries_text || "") : "");

  // ── Linked entities (loaded on edit, empty on create) ──────────────────
  // hydrated tracks "did we already sync once from the server for THIS
  // currentId?" — without this guard the effect re-fires whenever links
  // arrive late and overwrites the user's mid-edit selections with the
  // stale empty array from the previous activityId. The Map remembers,
  // per currentId, whether we've already done the initial fetch+sync.
  const { partners: linkedPartners, indicatorIds: linkedIndicators, loading: linksLoading } =
    window.melr.useActivityLinks(currentId);
  const [partnerIds, setPartnerIds] = useStateA([]);
  const [indicatorIds, setIndicatorIds] = useStateA([]);
  const [hydratedFor, setHydratedFor] = useStateA(null);
  useEffectA(() => {
    if (!currentId) { setHydratedFor(null); return; }
    if (linksLoading) return;
    if (hydratedFor === currentId) return;  // already synced for this row
    setPartnerIds((linkedPartners || []).map((p) => p.partner_id));
    setIndicatorIds(linkedIndicators || []);
    setHydratedFor(currentId);
  }, [linksLoading, currentId, linkedPartners, linkedIndicators, hydratedFor]);

  const [busy, setBusy] = useStateA(false);
  const [err, setErr]   = useStateA(null);

  const onSubmit = async (e) => {
    e.preventDefault();
    e.stopPropagation && e.stopPropagation();
    if (!projectId)     { setErr(window.L("Veuillez choisir un projet.", "Please pick a project.", "Por favor, elija un proyecto.")); return; }
    if (!title.trim())  { setErr(window.L("Le titre est requis.", "Title is required.", "El título es obligatorio.")); return; }
    setBusy(true); setErr(null);
    const payload = {
      project_id:            projectId,
      code:                  code.trim() || null,
      title:                 title.trim(),
      description:           description.trim() || null,
      activity_type_id:      typeId || null,
      project_component_id:  componentId || null,
      location_name:         locationName.trim() || null,
      latitude:              lat,
      longitude:             lng,
      // FKs vers le referentiel geo
      commune_id:            geo.commune_id  || null,
      quartier_id:           geo.quartier_id || null,
      start_date:            startDate || null,
      end_date:              endDate || null,
      status,
      gender_sensitive:      genderSensitive,
      budget_planned:        budgetPlanned,
      cost_actual:           costActual,
      currency,
      has_press:             hasPress,
      has_tdrs:              hasTdrs,
      has_report:            hasReport,
      notes:                 notes.trim() || null,
      responsible_user_id:   responsibleId || null,
      pmo_stakeholder_ids:   pmoStakeholderIds,
      pmo_stakeholder_id:    pmoStakeholderIds[0] || null,
      implementing_stakeholder_ids: implPartnerIds,
      beneficiaries_text:    beneficiariesText.trim() || null,
      indicator_ids:         indicatorIds,
    };
    // eslint-disable-next-line no-console
    console.log("[Activities] submit payload:", payload);
    try {
      let saved;
      if (!currentId) {
        saved = await window.melr.activitiesCrud.create(payload);
        // eslint-disable-next-line no-console
        console.log("[Activities] created:", saved);
        setCurrentId(saved.id);
      } else {
        saved = await window.melr.activitiesCrud.update(currentId, payload);
        // eslint-disable-next-line no-console
        console.log("[Activities] updated:", saved);
      }
      // Keep modal open after first create so user can immediately attach
      // files (TDRs, photos, etc.) without re-navigating.
      if (currentId && onSaved) onSaved();
    } catch (e2) {
      // eslint-disable-next-line no-console
      console.error("[Activities] submit failed:", e2);
      setErr(e2.message || String(e2));
    } finally { setBusy(false); }
  };

  return (
    <Modal
      size="lg"
      title={isNew
        ? (window.L("Nouvelle activité", "New activity", "Nueva actividad"))
        : (window.L("Modifier l'activité", "Edit activity", "Editar la actividad"))}
      onClose={onClose}
      onSubmit={onSubmit}
      footer={<>
        <button type="button" className="btn sm ghost" onClick={onClose} disabled={busy}>
          {window.L("Fermer", "Close", "Cerrar")}
        </button>
        <button type="submit" className="btn sm primary" disabled={busy}>
          {busy ? "…" : (currentId ? (window.L("Enregistrer", "Save", "Guardar")) : (window.L("Créer", "Create", "Crear")))}
        </button>
      </>}>
      <ActivityFormBody
        lang={lang}
        currentId={currentId}
        projectId={projectId}  setProjectId={setProjectId}
        visibleProjects={visibleProjects}
        code={code} setCode={setCode}
        title={title} setTitle={setTitle}
        description={description} setDescription={setDescription}
        typeId={typeId} setTypeId={setTypeId}
        componentId={componentId} setComponentId={setComponentId}
        locationName={locationName} setLocationName={setLocationName}
        lat={lat} setLat={setLat} lng={lng} setLng={setLng}
        geo={geo} setGeo={setGeo}
        startDate={startDate} setStartDate={setStartDate}
        endDate={endDate} setEndDate={setEndDate}
        status={status} setStatus={setStatus}
        genderSensitive={genderSensitive} setGenderSensitive={setGenderSensitive}
        budgetPlanned={budgetPlanned} setBudgetPlanned={setBudgetPlanned}
        costActual={costActual} setCostActual={setCostActual}
        currency={currency} setCurrency={setCurrency}
        hasPress={hasPress} setHasPress={setHasPress}
        hasTdrs={hasTdrs}   setHasTdrs={setHasTdrs}
        hasReport={hasReport} setHasReport={setHasReport}
        notes={notes} setNotes={setNotes}
        responsibleId={responsibleId} setResponsibleId={setResponsibleId}
        pmoStakeholderIds={pmoStakeholderIds} setPmoStakeholderIds={setPmoStakeholderIds}
        implPartnerIds={implPartnerIds} setImplPartnerIds={setImplPartnerIds}
        beneficiariesText={beneficiariesText} setBeneficiariesText={setBeneficiariesText}
        partnerIds={partnerIds} setPartnerIds={setPartnerIds}
        indicatorIds={indicatorIds} setIndicatorIds={setIndicatorIds}
        isAdmin={isAdmin}
      />
      {err && (
        <div style={{ padding: "8px 10px", background: "#fee2e2", color: "#991b1b", borderRadius: 6, fontSize: 12.5, marginTop: 10 }}>
          {err}
        </div>
      )}
    </Modal>
  );
}

// Extracted body so the modal stays readable. All state lives in the parent
// (ActivityEditorModal); this is a pure render component.
function ActivityFormBody(props) {
  const {
    lang, currentId,
    projectId, setProjectId, visibleProjects,
    code, setCode, title, setTitle, description, setDescription,
    typeId, setTypeId, componentId, setComponentId,
    locationName, setLocationName, lat, setLat, lng, setLng,
    geo, setGeo,
    startDate, setStartDate, endDate, setEndDate,
    status, setStatus, genderSensitive, setGenderSensitive,
    budgetPlanned, setBudgetPlanned, costActual, setCostActual, currency, setCurrency,
    hasPress, setHasPress, hasTdrs, setHasTdrs, hasReport, setHasReport,
    notes, setNotes,
    responsibleId, setResponsibleId,
    pmoStakeholderIds, setPmoStakeholderIds, implPartnerIds, setImplPartnerIds,
    beneficiariesText, setBeneficiariesText,
    partnerIds, setPartnerIds, indicatorIds, setIndicatorIds,
    isAdmin,
  } = props;

  // Responsable = membre de l'équipe du projet ; PMO = partenaire.
  const { data: teamMembers } = (window.melr && window.melr.useProjectTeam) ? window.melr.useProjectTeam(projectId) : { data: [] };
  // PMO + Partenaires de mise en œuvre : tirés de la liste « Projet | Parties
  // prenantes » (public.stakeholders). Source unique, project-scoped — la même
  // liste alimente le sélecteur PMO et le multi-choix des partenaires.
  const { data: stakeholderRows } = (window.melr && window.melr.useStakeholders) ? window.melr.useStakeholders(projectId) : { data: [] };
  const stakeholders = (stakeholderRows || []).filter(Boolean);
  const pmoSel = Array.isArray(pmoStakeholderIds) ? pmoStakeholderIds : [];
  const togglePmo = (id) => {
    setPmoStakeholderIds(pmoSel.includes(id) ? pmoSel.filter((x) => x !== id) : [...pmoSel, id]);
  };
  const implSel = Array.isArray(implPartnerIds) ? implPartnerIds : [];
  const toggleImplPartner = (id) => {
    setImplPartnerIds(implSel.includes(id) ? implSel.filter((x) => x !== id) : [...implSel, id]);
  };

  const inp = { width: "100%", padding: "8px 10px", borderRadius: 6, border: "1px solid var(--line)", fontSize: 13, background: "var(--input-bg, 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" };

  // Use device geolocation to fill lat/lng. Optional — many environments
  // block geolocation; show an error pill if it fails.
  const [geoErr, setGeoErr] = useStateA(null);
  const useDeviceLocation = () => {
    if (!navigator.geolocation) { setGeoErr(window.L("Géolocalisation non disponible.", "Geolocation not available.", "Geolocalización no disponible.")); return; }
    setGeoErr(null);
    navigator.geolocation.getCurrentPosition(
      (pos) => {
        setLat(pos.coords.latitude.toFixed(6));
        setLng(pos.coords.longitude.toFixed(6));
      },
      (e) => { setGeoErr(e.message); },
      { enableHighAccuracy: true, timeout: 10000 }
    );
  };

  return (
    <div style={{ display: "grid", gap: 12 }}>
      {/* 1. PROJET (toujours en première ligne) */}
      {/* mapProjectRow shape : p.uuid = real Supabase UUID (FK target),
          p.id = legacy code, p.nameFr/p.nameEn = display name. */}
      <div>
        <label style={lbl}>{window.L("Projet *", "Project *", "Proyecto *")}</label>
        <select required style={inp} value={projectId} onChange={(e) => setProjectId(e.target.value)}>
          <option value="">
            {(visibleProjects && visibleProjects.length)
              ? (window.L("— Choisir un projet —", "— Pick a project —", "— Elegir un proyecto —"))
              : (window.L("— Aucun projet visible —", "— No project visible —", "— Ningún proyecto visible —"))}
          </option>
          {visibleProjects.map((p) => (
            <option key={p.uuid} value={p.uuid}>
              {p.id} · {lang === "fr" ? (p.nameFr || p.nameEn) : (p.nameEn || p.nameFr)}
            </option>
          ))}
        </select>
      </div>

      {/* 2. Titre + code */}
      <div style={{ display: "grid", gridTemplateColumns: "2fr 1fr", gap: 10 }}>
        <div>
          <label style={lbl}>{window.L("Titre *", "Title *", "Título *")}</label>
          <input required style={inp} value={title} onChange={(e) => setTitle(e.target.value)}
            placeholder={window.L("Atelier de formation sur…", "Training workshop on…", "Taller de formación sobre…")} />
        </div>
        <div>
          <label style={lbl}>{window.L("Code (optionnel)", "Code (optional)", "Código (opcional)")}</label>
          <input style={inp} value={code} onChange={(e) => setCode(e.target.value)} placeholder="ACT-001" />
        </div>
      </div>

      {/* 3. Type + composante */}
      <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 10 }}>
        <ActivityTypePicker lang={lang} value={typeId} onChange={setTypeId} isAdmin={isAdmin} inp={inp} lbl={lbl} />
        <ProjectComponentPicker lang={lang} projectId={projectId} value={componentId} onChange={setComponentId} isAdmin={isAdmin} inp={inp} lbl={lbl} />
      </div>

      {/* 3b. Responsable (membre de l'équipe du projet) */}
      <div>
        <label style={lbl}>{window.L("Responsable de l'activité", "Activity lead", "Responsable de la actividad")}</label>
        <select style={inp} value={responsibleId} onChange={(e) => setResponsibleId(e.target.value)}>
          <option value="">{window.L("— Aucun —", "— None —", "— Ninguno —")}</option>
          {(teamMembers || []).map((m) => (
            <option key={m.user_id} value={m.user_id}>
              {(m.profile && (m.profile.full_name || m.profile.email)) || m.user_id}
            </option>
          ))}
        </select>
        {projectId && (teamMembers || []).length === 0 && (
          <div className="text-faint" style={{ fontSize: 10.5, marginTop: 3 }}>
            {window.L("Aucun membre sur ce projet — ajoutez-en via l'onglet Équipe.", "No member on this project — add via the Team tab.", "Ningún miembro en este proyecto — añada uno desde la pestaña Equipo.")}
          </div>
        )}
      </div>

      {/* 3b-bis. PMO — plusieurs parties prenantes possibles (même liste que
          « Projet | Parties prenantes »). */}
      <div>
        <label style={lbl}>PMO{pmoSel.length ? " (" + pmoSel.length + ")" : ""}</label>
        {!projectId ? (
          <div className="text-faint" style={{ fontSize: 11 }}>
            {window.L("Choisissez d'abord un projet.", "Pick a project first.", "Elija primero un proyecto.")}
          </div>
        ) : stakeholders.length === 0 ? (
          <div className="text-faint" style={{ fontSize: 10.5 }}>
            {window.L("Ajoutez d'abord des parties prenantes via Projet | Parties prenantes.", "First add stakeholders via Project | Stakeholders.", "Añada primero partes interesadas desde Proyecto | Partes interesadas.")}
          </div>
        ) : (
          <div style={{ display: "flex", flexWrap: "wrap", gap: 8, padding: "8px 10px", background: "var(--bg-sunken)", borderRadius: 6, border: "1px solid var(--line)" }}>
            {stakeholders.map((s) => {
              const on = pmoSel.includes(s.id);
              return (
                <label key={s.id} style={{ display: "inline-flex", gap: 6, alignItems: "center", fontSize: 12.5, cursor: "pointer", padding: "3px 8px", borderRadius: 999, border: "1px solid var(--line)", background: on ? "var(--accent-soft, #e0e7ff)" : "transparent" }}>
                  <input type="checkbox" checked={on} onChange={() => togglePmo(s.id)} />
                  {s.name}
                </label>
              );
            })}
          </div>
        )}
      </div>

      {/* 3c. Bénéficiaires (texte libre) */}
      <div>
        <label style={lbl}>{window.L("Bénéficiaires", "Beneficiaries", "Beneficiarios")}</label>
        <textarea style={{ ...inp, minHeight: 52, resize: "vertical" }} value={beneficiariesText}
          onChange={(e) => setBeneficiariesText(e.target.value)}
          placeholder={window.L("Ex. 120 femmes maraîchères, 30 jeunes…", "e.g. 120 women farmers, 30 youth…", "Ej.: 120 mujeres hortelanas, 30 jóvenes…")} />
      </div>

      {/* 4. Description */}
      <div>
        <label style={lbl}>{window.L("Description", "Description", "Descripción")}</label>
        <textarea style={{ ...inp, minHeight: 72, resize: "vertical" }} value={description}
          onChange={(e) => setDescription(e.target.value)}
          placeholder={window.L("Objet, méthodologie, public ciblé…", "Purpose, methodology, audience…", "Objeto, metodología, público objetivo…")} />
      </div>

      {/* 5. Lieu + GPS */}
      <div>
        <label style={lbl}>{window.L("Lieu de l'activité", "Activity location", "Lugar de la actividad")}</label>
        <input style={inp} value={locationName} onChange={(e) => setLocationName(e.target.value)}
          placeholder={window.L("Dakar, Hôtel Pullman — Salle A", "Dakar, Pullman Hotel — Room A", "Dakar, Hotel Pullman — Sala A")} />
      </div>
      <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr auto", gap: 10, alignItems: "end" }}>
        <div>
          <label style={lbl}>{window.L("Latitude", "Latitude", "Latitud")}</label>
          <input style={inp} type="number" step="any" value={lat} onChange={(e) => setLat(e.target.value)} placeholder="14.6928" />
        </div>
        <div>
          <label style={lbl}>{window.L("Longitude", "Longitude", "Longitud")}</label>
          <input style={inp} type="number" step="any" value={lng} onChange={(e) => setLng(e.target.value)} placeholder="-17.4467" />
        </div>
        <button type="button" className="btn sm ghost" onClick={useDeviceLocation}
          title={window.L("Utiliser la position actuelle de l'appareil", "Use device GPS", "Usar la posición actual del dispositivo")}>
          <Icon.pin /> {window.L("GPS", "GPS", "GPS")}
        </button>
      </div>
      {geoErr && <div className="text-faint" style={{ fontSize: 11, color: "#b91c1c" }}>⚠ {geoErr}</div>}

      {/* 5b. Attache administrative (Pays > Region > Dept > Arr > Commune > Quartier) */}
      <div style={{ padding: "10px 12px", background: "var(--bg-sunken)", borderRadius: 6, border: "1px dashed var(--line)" }}>
        <div style={{ ...lbl, marginBottom: 8 }}>
          {window.L("Localisation administrative (optionnel)", "Administrative location (optional)", "Localización administrativa (opcional)")}
        </div>
        {window.GeoPicker
          ? <window.GeoPicker value={geo} onChange={setGeo} lang={lang}
              showQuartier={true} allowQuartierCreate={true}
              defaultCountry="SN" showLabels={true} />
          : <div className="text-faint" style={{ fontSize: 12 }}>
              {window.L("Module géographique en cours de chargement…", "Loading geo module…", "Cargando módulo geográfico…")}
            </div>}
      </div>

      {/* 6. Dates */}
      <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: 10 }}>
        <div>
          <label style={lbl}>{window.L("Début", "Start", "Inicio")}</label>
          <input style={inp} type="date" value={startDate} onChange={(e) => setStartDate(e.target.value)} />
        </div>
        <div>
          <label style={lbl}>{window.L("Fin", "End", "Fin")}</label>
          <input style={inp} type="date" value={endDate} onChange={(e) => setEndDate(e.target.value)} />
        </div>
        <div>
          <label style={lbl}>{window.L("Statut", "Status", "Estado")}</label>
          <select style={inp} value={status} onChange={(e) => setStatus(e.target.value)}>
            {ACTIVITY_STATUSES.map((s) => (
              <option key={s.v} value={s.v}>{(lang === "es" ? (s.es != null ? s.es : s.en) : lang === "fr" ? s.fr : s.en)}</option>
            ))}
          </select>
        </div>
      </div>

      {/* 7. Gender sensitive */}
      <label style={{ display: "flex", gap: 8, alignItems: "center", padding: "8px 10px", background: "var(--bg-sunken)", borderRadius: 6, cursor: "pointer" }}>
        <input type="checkbox" checked={genderSensitive} onChange={(e) => setGenderSensitive(e.target.checked)} />
        <span style={{ fontSize: 12.5 }}>
          {window.L("Activité sensible au genre", "Gender-sensitive activity", "Actividad sensible al género")}
        </span>
      </label>

      {/* 8. Partenaires de mise en œuvre — choisis parmi les PARTIES PRENANTES
             du projet (même liste que le PMO ci-dessus). */}
      <div>
        <label style={lbl}>{window.L("Partenaires de mise en œuvre", "Implementing partners", "Socios de ejecución")}</label>
        {!projectId ? (
          <div className="text-faint" style={{ fontSize: 11 }}>
            {window.L("Choisissez d'abord un projet.", "Pick a project first.", "Elija primero un proyecto.")}
          </div>
        ) : stakeholders.length === 0 ? (
          <div className="text-faint" style={{ fontSize: 10.5 }}>
            {window.L("Aucune partie prenante — ajoutez-en via Projet | Parties prenantes.", "No stakeholder — add via Project | Stakeholders.", "Ninguna parte interesada — añada una desde Proyecto | Partes interesadas.")}
          </div>
        ) : (
          <div style={{ display: "flex", flexWrap: "wrap", gap: 8, padding: "8px 10px", background: "var(--bg-sunken)", borderRadius: 6, border: "1px solid var(--line)" }}>
            {stakeholders.map((s) => {
              const on = implSel.includes(s.id);
              return (
                <label key={s.id} style={{ display: "inline-flex", gap: 6, alignItems: "center", fontSize: 12.5, cursor: "pointer", padding: "3px 8px", borderRadius: 999, border: "1px solid var(--line)", background: on ? "var(--accent-soft, #e0e7ff)" : "transparent" }}>
                  <input type="checkbox" checked={on} onChange={() => toggleImplPartner(s.id)} />
                  {s.name}
                </label>
              );
            })}
          </div>
        )}
      </div>

      {/* 9. Indicateurs (multi, scoped to project) */}
      <IndicatorsMultiPicker lang={lang} projectId={projectId} value={indicatorIds} onChange={setIndicatorIds} inp={inp} lbl={lbl} />

      {/* 10. Budget + coût */}
      <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 100px", gap: 10 }}>
        <div>
          <label style={lbl}>{window.L("Budget prévu", "Planned budget", "Presupuesto previsto")}</label>
          <input style={inp} type="number" step="any" value={budgetPlanned} onChange={(e) => setBudgetPlanned(e.target.value)} placeholder="0" />
        </div>
        <div>
          <label style={lbl}>{window.L("Coût réel", "Actual cost", "Coste real")}</label>
          <input style={inp} type="number" step="any" value={costActual} onChange={(e) => setCostActual(e.target.value)} placeholder="0" />
        </div>
        <div>
          <label style={lbl}>{window.L("Devise", "Currency", "Moneda")}</label>
          <input style={inp} value={currency} onChange={(e) => setCurrency(e.target.value)} maxLength={6} />
        </div>
      </div>

      {/* 11. Cases PRESSE / TDRs / Rapport */}
      <div>
        <div style={lbl}>{window.L("Livrables confirmés", "Confirmed deliverables", "Entregables confirmados")}</div>
        <div style={{ display: "flex", gap: 14, flexWrap: "wrap", padding: "8px 10px", background: "var(--bg-sunken)", borderRadius: 6 }}>
          <label style={{ display: "inline-flex", gap: 6, alignItems: "center", fontSize: 12.5, cursor: "pointer" }}>
            <input type="checkbox" checked={hasPress} onChange={(e) => setHasPress(e.target.checked)} /> PRESSE
          </label>
          <label style={{ display: "inline-flex", gap: 6, alignItems: "center", fontSize: 12.5, cursor: "pointer" }}>
            <input type="checkbox" checked={hasTdrs} onChange={(e) => setHasTdrs(e.target.checked)} /> TDRs
          </label>
          <label style={{ display: "inline-flex", gap: 6, alignItems: "center", fontSize: 12.5, cursor: "pointer" }}>
            <input type="checkbox" checked={hasReport} onChange={(e) => setHasReport(e.target.checked)} /> {window.L("Rapport", "Report", "Informe")}
          </label>
        </div>
      </div>

      {/* 12. Notes libres */}
      <div>
        <label style={lbl}>{window.L("Notes", "Notes", "Notas")}</label>
        <textarea style={{ ...inp, minHeight: 48, resize: "vertical" }} value={notes}
          onChange={(e) => setNotes(e.target.value)}
          placeholder={window.L("Observations, points d'attention…", "Observations, points of attention…", "Observaciones, puntos de atención…")} />
      </div>

      {/* 13. Valeurs des indicateurs — Cohérence saisie · Phase 5
          Section qui apparaît une fois l'activité enregistrée. Pour chaque
          indicateur lié, un mini-formulaire de saisie qui crée une ligne
          dans indicator_values avec activity_id pré-rempli (FK ON DELETE
          SET NULL + trigger d'intégrité same-project). Cela évite à
          l'utilisateur d'avoir à passer ensuite par Suivi des indicateurs
          → Saisir la valeur pour chaque indicateur séparément. */}
      <div style={{ borderTop: "1px solid var(--line)", paddingTop: 12, marginTop: 4 }}>
        <div style={{ fontSize: 13, fontWeight: 600, marginBottom: 6 }}>
          {window.L("Valeurs des indicateurs", "Indicator values", "Valores de los indicadores")}
          <span className="text-faint" style={{ fontSize: 10.5, fontWeight: 400, marginLeft: 8 }}>
            {window.L("Saisir directement depuis l'activité", "Enter directly from the activity", "Introducir directamente desde la actividad")}
          </span>
        </div>
        {currentId ? (
          (indicatorIds && indicatorIds.length > 0) ? (
            <ActivityInlineIndicatorValues
              lang={lang}
              activityId={currentId}
              indicatorIds={indicatorIds}
              startDate={startDate}
              endDate={endDate} />
          ) : (
            <div className="text-faint" style={{ fontSize: 11.5, padding: 10, border: "1px dashed var(--line)", borderRadius: 6, textAlign: "center" }}>
              {window.L("Aucun indicateur lié à cette activité. Cocher des indicateurs ci-dessus pour activer la saisie de valeurs.", "No indicator linked to this activity. Tick indicators above to enable value entry.", "Ningún indicador vinculado a esta actividad. Marque indicadores arriba para habilitar la introducción de valores.")}
            </div>
          )
        ) : (
          <div className="text-faint" style={{ fontSize: 11.5, padding: 10, border: "1px dashed var(--line)", borderRadius: 6, textAlign: "center" }}>
            {window.L("Enregistrez d'abord l'activité, puis saisissez les valeurs ici.", "Save the activity first, then enter values here.", "Guarde primero la actividad y luego introduzca los valores aquí.")}
          </div>
        )}
      </div>

      {/* 14. Pièces jointes — disponible une fois l'activité créée */}
      <div style={{ borderTop: "1px solid var(--line)", paddingTop: 12, marginTop: 4 }}>
        <div style={{ fontSize: 13, fontWeight: 600, marginBottom: 6 }}>
          {window.L("Pièces jointes", "Attachments", "Adjuntos")}
          <span className="text-faint" style={{ fontSize: 10.5, fontWeight: 400, marginLeft: 8 }}>
            {window.L("TDRs, présences, photos, rapports…", "TDRs, attendance, photos, reports…", "TDR, asistencias, fotos, informes…")}
          </span>
        </div>
        {currentId ? (
          <ActivityAttachmentsSection activityId={currentId} lang={lang} />
        ) : (
          <div className="text-faint" style={{ fontSize: 11.5, padding: 10, border: "1px dashed var(--line)", borderRadius: 6, textAlign: "center" }}>
            {window.L("Enregistrez d'abord l'activité (« Créer »), puis attachez des fichiers ici.", "Save the activity first ('Create'), then attach files here.", "Guarde primero la actividad («Crear») y luego adjunte archivos aquí.")}
          </div>
        )}
      </div>
    </div>
  );
}

// ============================================================================
// ACTIVITY INLINE INDICATOR VALUES (Phase 5)
// ============================================================================
// Renders one mini-form per linked indicator. Each form lets the agent
// enter the measurement directly from the activity context — period_start
// and period_end are pre-filled from the activity's dates, activity_id is
// passed to indicator_values for traceability.
//
// PEFA indicators get the alphabetic dropdown; numeric ones get a number
// input. Disaggregation grid is intentionally OUT OF SCOPE here (too much
// surface for an inline form) — for disaggregated saisie the user still
// goes through Suivi des indicateurs → Saisir la valeur.
function ActivityInlineIndicatorValues({ lang, activityId, indicatorIds, startDate, endDate }) {
  // Fetch the indicators by their UUIDs. We pass null to useIndicators for
  // "all visible" and filter client-side; the alternative (no filter) keeps
  // the hook simple and works since indicatorIds is small.
  const { data: allIndicators } = window.melr.useIndicators ? window.melr.useIndicators() : { data: [] };
  const indicators = (allIndicators || []).filter((i) => indicatorIds.includes(i.id));
  const pefaLabels = (window.melr.scoreLabels && window.melr.scoreLabels("score_pefa")) || [];

  if (!indicators.length) {
    return (
      <div className="text-faint" style={{ fontSize: 11.5, padding: 10, border: "1px dashed var(--line)", borderRadius: 6, textAlign: "center" }}>
        {window.L("Chargement des indicateurs liés…", "Loading linked indicators…", "Cargando indicadores vinculados…")}
      </div>
    );
  }

  return (
    <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
      {indicators.map((ind) => (
        <ActivityIndicatorValueRow key={ind.id}
          lang={lang}
          activityId={activityId}
          indicator={ind}
          startDate={startDate}
          endDate={endDate}
          pefaLabels={pefaLabels} />
      ))}
    </div>
  );
}

function ActivityIndicatorValueRow({ lang, activityId, indicator, startDate, endDate, pefaLabels }) {
  const isPefa = indicator.value_kind === "score_pefa";
  const [value, setValue] = useStateA("");
  const [valueText, setValueText] = useStateA("");
  const [comment, setComment] = useStateA("");
  const [busy, setBusy] = useStateA(false);
  const [done, setDone] = useStateA(false);  // green confirmation after save
  const [err, setErr] = useStateA(null);

  const submit = async () => {
    if (!startDate || !endDate) {
      setErr(window.L("Renseignez d'abord les dates de début et de fin de l'activité.", "Set the activity start and end dates first.", "Indique primero las fechas de inicio y fin de la actividad."));
      return;
    }
    if (!isPefa && (value === "" || value == null)) {
      setErr(window.L("Saisir une valeur.", "Enter a value.", "Introducir un valor."));
      return;
    }
    if (isPefa && !valueText) {
      setErr(window.L("Choisir un score PEFA.", "Pick a PEFA score.", "Elegir una puntuación PEFA."));
      return;
    }
    setErr(null); setBusy(true);
    try {
      await window.melr.createIndicatorValue(indicator.id, {
        period_start: startDate,
        period_end:   endDate,
        value_kind:   indicator.value_kind || "numeric",
        value:        isPefa ? null : Number(value),
        value_text:   isPefa ? valueText : null,
        activity_id:  activityId,
        status:       "pending",
        comment:      comment || null,
      });
      setDone(true);
      setTimeout(() => setDone(false), 4000);
      setValue(""); setValueText(""); setComment("");
    } catch (e) { setErr(e.message); }
    finally { setBusy(false); }
  };

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

  return (
    <div style={{
      padding: "8px 10px",
      borderRadius: 6,
      background: done ? "color-mix(in oklch, oklch(0.55 0.16 145) 8%, var(--bg-elev))" : "var(--bg-sunken)",
      border: "1px solid " + (done ? "color-mix(in oklch, oklch(0.55 0.16 145) 40%, var(--line))" : "var(--line)"),
      transition: "background 0.3s",
    }}>
      <div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 6 }}>
        <span className="mono" style={{ fontSize: 11, color: "var(--text-faint)", minWidth: 60 }}>{indicator.code}</span>
        <span style={{ flex: 1, fontSize: 12.5, fontWeight: 500 }}>
          {(lang === "es" ? (indicator.name_es != null ? indicator.name_es : indicator.name_en || indicator.name_fr) : lang === "fr" ? indicator.name_fr : indicator.name_en || indicator.name_fr)}
        </span>
        {indicator.unit && !isPefa && (
          <span className="text-faint" style={{ fontSize: 10.5 }}>{indicator.unit}</span>
        )}
        {isPefa && (
          <span className="pill accent" style={{ fontSize: 10 }}>PEFA</span>
        )}
      </div>
      <div style={{ display: "grid", gridTemplateColumns: "1fr 2fr auto", gap: 6, alignItems: "center" }}>
        {isPefa ? (
          <select value={valueText} onChange={(e) => setValueText(e.target.value)} style={inp}>
            <option value="">{window.L("— Score —", "— Score —", "— Puntuación —")}</option>
            {(pefaLabels || []).map((l) => <option key={l} value={l}>{l}</option>)}
          </select>
        ) : (
          <input type="number" step="any" value={value} onChange={(e) => setValue(e.target.value)} style={inp}
            placeholder={window.L("Valeur", "Value", "Valor")} />
        )}
        <input type="text" value={comment} onChange={(e) => setComment(e.target.value)} style={inp}
          placeholder={window.L("Commentaire (optionnel)", "Comment (optional)", "Comentario (opcional)")} />
        <button type="button" onClick={submit} disabled={busy}
          className={"btn xs" + (done ? " " : " primary")}
          style={{ minWidth: 80 }}>
          {done
            ? (window.L("✓ Enregistré", "✓ Saved", "✓ Guardado"))
            : busy
              ? "…"
              : (window.L("Enregistrer", "Save", "Guardar"))}
        </button>
      </div>
      {err && (
        <div style={{ marginTop: 4, fontSize: 11, color: "#b91c1c" }}>{err}</div>
      )}
    </div>
  );
}

// ============================================================================
// LOOKUPS PICKERS · dropdowns with inline "+ Nouveau" sub-modals
// ============================================================================

function ActivityTypePicker({ lang, value, onChange, isAdmin, inp, lbl }) {
  // We grab refresh() so we can force a re-fetch immediately after an
  // inline create — realtime can be unsubscribed (or slow), and we don't
  // want the picker to display the just-created id with no matching option.
  const { data: types, refresh } = window.melr.useActivityTypes();
  const [newOpen, setNewOpen] = useStateA(false);
  return (
    <div>
      <label style={lbl}>{window.L("Type d'activité", "Activity type", "Tipo de actividad")}</label>
      <select style={inp} value={value} onChange={(e) => {
        if (e.target.value === "__NEW__") setNewOpen(true);
        else onChange(e.target.value);
      }}>
        <option value="">{window.L("— Aucun —", "— None —", "— Ninguno —")}</option>
        {(types || []).map((t) => (
          <option key={t.id} value={t.id}>{(lang === "es" ? (t.name_es != null ? t.name_es : t.name_en || t.name_fr) : lang === "fr" ? t.name_fr : t.name_en || t.name_fr)}</option>
        ))}
        {isAdmin && <option value="__NEW__">{window.L("+ Nouveau type…", "+ New type…", "+ Nuevo tipo…")}</option>}
      </select>
      {newOpen && (
        <NewActivityTypeModal lang={lang}
          onClose={() => setNewOpen(false)}
          onCreated={async (t) => {
            // Refresh first so the new id is a valid <option>, then select.
            await refresh();
            onChange(t.id);
            setNewOpen(false);
          }} />
      )}
    </div>
  );
}

function ProjectComponentPicker({ lang, projectId, value, onChange, isAdmin, inp, lbl }) {
  const { data: components, refresh } = window.melr.useProjectComponents(projectId);
  const [newOpen, setNewOpen] = useStateA(false);
  return (
    <div>
      <label style={lbl}>{window.L("Composante du projet", "Project component", "Componente del proyecto")}</label>
      <select style={inp} value={value} disabled={!projectId} onChange={(e) => {
        if (e.target.value === "__NEW__") setNewOpen(true);
        else onChange(e.target.value);
      }}>
        <option value="">
          {projectId
            ? (window.L("— Aucune —", "— None —", "— Ninguno —"))
            : (window.L("— Choisir un projet d'abord —", "— Pick a project first —", "— Elija primero un proyecto —"))}
        </option>
        {(components || []).map((c) => (
          <option key={c.id} value={c.id}>{(lang === "es" ? (c.name_es != null ? c.name_es : c.name_en || c.name_fr) : lang === "fr" ? c.name_fr : c.name_en || c.name_fr)}</option>
        ))}
        {isAdmin && projectId && <option value="__NEW__">{window.L("+ Nouvelle composante…", "+ New component…", "+ Nuevo componente…")}</option>}
      </select>
      {newOpen && (
        <NewProjectComponentModal lang={lang} projectId={projectId}
          onClose={() => setNewOpen(false)}
          onCreated={async (c) => {
            // Refresh so the new id matches a <option> BEFORE we select it
            // (without this, the select value points to nothing and the
            // dropdown visually resets to "— Aucune —").
            await refresh();
            onChange(c.id);
            setNewOpen(false);
          }} />
      )}
    </div>
  );
}

function PartnersMultiPicker({ lang, value, onChange, isAdmin, inp, lbl }) {
  const { data: partners, refresh } = window.melr.usePartners();
  const [newOpen, setNewOpen] = useStateA(false);
  const selectedSet = new Set(value || []);
  const toggle = (id) => {
    if (selectedSet.has(id)) onChange(value.filter((x) => x !== id));
    else                     onChange([...(value || []), id]);
  };
  const removeOne = (id) => onChange((value || []).filter((x) => x !== id));
  const partnerLabel = (p) => ((lang === "es" ? (p.name_es != null ? p.name_es : p.name_en || p.name_fr) : lang === "fr" ? p.name_fr : p.name_en || p.name_fr));

  // Pour la zone "selectionnes" : on resout chaque id du `value` vers
  // l'objet partenaire correspondant. Si un id n'est pas trouve (cas rare
  // de partenaire supprime entre-temps), on l'affiche en grise.
  const partnersById = (partners || []).reduce((m, p) => { m[p.id] = p; return m; }, {});
  const selectedItems = (value || []).map((id) => ({ id, p: partnersById[id] }));

  return (
    <div>
      <div style={{ display: "flex", alignItems: "center", marginBottom: 4 }}>
        <label style={{ ...lbl, marginBottom: 0 }}>{window.L("Partenaires de mise en œuvre", "Implementation partners", "Socios de ejecución")}</label>
        <span className="text-faint" style={{ fontSize: 10.5, fontWeight: 400, marginLeft: 6 }}>
          {selectedSet.size} {window.L("sélectionné(s)", "selected", "seleccionado(s)")}
        </span>
        <div style={{ flex: 1 }} />
        {isAdmin && (
          <button type="button" className="btn xs ghost" onClick={() => setNewOpen(true)}
            style={{ fontSize: 10.5, padding: "2px 8px" }}>
            + {window.L("Nouveau", "New", "Nuevo")}
          </button>
        )}
      </div>

      {/* Zone "Selectionnes" — chips avec X explicite pour retirer un par un */}
      {selectedItems.length > 0 && (
        <div style={{
          display: "flex", flexWrap: "wrap", gap: 4,
          marginBottom: 6, padding: 6,
          background: "var(--bg-elev, white)",
          border: "1px solid var(--line)", borderRadius: 6,
        }}>
          {selectedItems.map(({ id, p }) => (
            <span key={id} className="pill accent" style={{
              display: "inline-flex", alignItems: "center", gap: 4,
              background: "var(--accent, #4f46e5)", color: "white",
              fontSize: 11.5, padding: "3px 4px 3px 10px", borderRadius: 12,
            }}>
              {p ? partnerLabel(p) : <span style={{ fontStyle: "italic" }}>(?)</span>}
              <button type="button" onClick={() => removeOne(id)}
                title={window.L("Retirer ce partenaire", "Remove this partner", "Quitar este socio")}
                style={{
                  display: "inline-flex", alignItems: "center", justifyContent: "center",
                  width: 18, height: 18, border: 0, padding: 0,
                  background: "rgba(255,255,255,0.2)", color: "white",
                  borderRadius: "50%", cursor: "pointer", fontSize: 14, lineHeight: 1,
                }}>×</button>
            </span>
          ))}
        </div>
      )}

      {/* Zone "Tous les partenaires" — clic pour basculer la selection */}
      <div style={{ maxHeight: 140, overflowY: "auto", border: "1px solid var(--line)", borderRadius: 6, padding: 6, background: "var(--bg-sunken)" }}>
        {(partners || []).length === 0 ? (
          <div className="text-faint" style={{ fontSize: 11, padding: 6, textAlign: "center" }}>
            {window.L("Aucun partenaire enregistré. Cliquer « + Nouveau » pour en ajouter.", "No partners yet. Click '+ New' to add one.", "No hay socios registrados. Haga clic en «+ Nuevo» para añadir uno.")}
          </div>
        ) : (
          <div style={{ display: "flex", flexWrap: "wrap", gap: 4 }}>
            {(partners || []).map((p) => {
              const on = selectedSet.has(p.id);
              return (
                <button key={p.id} type="button" onClick={() => toggle(p.id)}
                  className={"pill" + (on ? " accent" : "")}
                  title={on
                    ? (window.L("Cliquer pour retirer", "Click to remove", "Haga clic para quitar"))
                    : (window.L("Cliquer pour ajouter", "Click to add", "Haga clic para añadir"))}
                  style={{
                    cursor: "pointer", fontSize: 11.5,
                    background: on ? "var(--accent, #4f46e5)" : "white",
                    color:      on ? "white" : "var(--text)",
                    border: "1px solid " + (on ? "var(--accent, #4f46e5)" : "var(--line)"),
                  }}>
                  {on && "✓ "}{partnerLabel(p)}
                </button>
              );
            })}
          </div>
        )}
      </div>
      {newOpen && (
        <NewPartnerModal lang={lang}
          onClose={() => setNewOpen(false)}
          onCreated={async (p) => {
            // Refresh BEFORE updating the selection so the new partner pill
            // is rendered (otherwise the id is "selected" but no chip shows
            // until realtime fires, and the user thinks the entry was lost).
            await refresh();
            onChange([...(value || []), p.id]);
            setNewOpen(false);
          }} />
      )}
    </div>
  );
}

function IndicatorsMultiPicker({ lang, projectId, value, onChange, inp, lbl }) {
  // Pull refresh() so the user can force a re-fetch without closing the
  // modal — useful when an indicator is added in "Suivi des indicateurs"
  // in a parallel tab or when realtime isn't published for `indicators`.
  const { data: indicators, refresh: refreshIndicators } = window.melr.useIndicators(projectId);
  const selectedSet = new Set(value || []);
  const toggle = (id) => {
    if (selectedSet.has(id)) onChange(value.filter((x) => x !== id));
    else                     onChange([...(value || []), id]);
  };
  // Local search + "only selected" toggle. Useful when a project has dozens
  // of indicators — the user can narrow the visible list by code/name or
  // restrict to what they've already ticked to review their picks.
  const [search, setSearch] = useStateA("");
  const [onlySelected, setOnlySelected] = useStateA(false);
  const filtered = (indicators || []).filter((i) => {
    if (onlySelected && !selectedSet.has(i.id)) return false;
    if (!search.trim()) return true;
    const q = search.toLowerCase();
    const hay = `${i.code || ""} ${i.name_fr || ""} ${i.name_en || ""}`.toLowerCase();
    return hay.includes(q);
  });
  return (
    <div>
      <div style={{ display: "flex", alignItems: "center", gap: 6, marginBottom: 4 }}>
        <label style={{ ...lbl, marginBottom: 0 }}>{window.L("Indicateurs liés", "Linked indicators", "Indicadores vinculados")}</label>
        <span className="text-faint" style={{ fontSize: 10.5, fontWeight: 400 }}>
          {selectedSet.size} / {(indicators || []).length} {window.L("sélectionné(s)", "selected", "seleccionado(s)")}
        </span>
      </div>
      {projectId && (indicators || []).length > 0 && (
        <div style={{ display: "flex", gap: 6, marginBottom: 4 }}>
          <input type="search" value={search} onChange={(e) => setSearch(e.target.value)}
            placeholder={window.L("Rechercher par code ou nom…", "Search by code or name…", "Buscar por código o nombre…")}
            style={{ ...inp, fontSize: 12, padding: "5px 8px" }} />
          <button type="button"
            className={"btn xs" + (onlySelected ? " primary" : " ghost")}
            onClick={() => setOnlySelected((v) => !v)}
            style={{ whiteSpace: "nowrap" }}
            title={window.L("N'afficher que les indicateurs sélectionnés", "Show only selected indicators", "Mostrar solo los indicadores seleccionados")}>
            {onlySelected
              ? (window.L("✓ Sélectionnés", "✓ Selected", "✓ Seleccionados"))
              : (window.L("Sélectionnés", "Selected", "Seleccionados"))}
          </button>
          <button type="button" className="btn xs ghost"
            onClick={() => refreshIndicators && refreshIndicators()}
            title={window.L("Recharger la liste depuis le serveur (si tu viens d'ajouter un indicateur dans Suivi des indicateurs)", "Reload the list from the server (if you just added an indicator in Indicator tracking)", "Recargar la lista desde el servidor (si acabas de añadir un indicador en Seguimiento de indicadores)")}>
            🔄
          </button>
        </div>
      )}
      <div style={{ maxHeight: 200, overflowY: "auto", border: "1px solid var(--line)", borderRadius: 6, padding: 6, background: "var(--bg-sunken)" }}>
        {!projectId ? (
          <div className="text-faint" style={{ fontSize: 11, padding: 6, textAlign: "center" }}>
            {window.L("Choisir un projet d'abord pour voir ses indicateurs.", "Pick a project first to see its indicators.", "Elija primero un proyecto para ver sus indicadores.")}
          </div>
        ) : (indicators || []).length === 0 ? (
          <div className="text-faint" style={{ fontSize: 11, padding: 6, textAlign: "center" }}>
            {window.L("Aucun indicateur pour ce projet.", "No indicators for this project yet.", "No hay indicadores para este proyecto.")}
          </div>
        ) : filtered.length === 0 ? (
          <div className="text-faint" style={{ fontSize: 11, padding: 6, textAlign: "center" }}>
            {window.L(`Aucun indicateur ne correspond à « ${search} ».`, `No indicators match "${search}".`, `Ningún indicador coincide con «${search}».`)}
          </div>
        ) : (
          <div style={{ display: "flex", flexDirection: "column", gap: 3 }}>
            {filtered.map((i) => {
              const on = selectedSet.has(i.id);
              return (
                <label key={i.id} style={{ display: "flex", gap: 6, alignItems: "center", padding: "3px 6px", cursor: "pointer", borderRadius: 4, background: on ? "color-mix(in oklch, var(--accent) 12%, transparent)" : "transparent" }}>
                  <input type="checkbox" checked={on} onChange={() => toggle(i.id)} />
                  <span className="mono" style={{ fontSize: 10.5, color: "var(--text-faint)", minWidth: 60 }}>{i.code}</span>
                  <span style={{ fontSize: 12 }}>{(lang === "es" ? (i.name_es != null ? i.name_es : i.name_en || i.name_fr) : lang === "fr" ? i.name_fr : i.name_en || i.name_fr)}</span>
                </label>
              );
            })}
          </div>
        )}
      </div>
    </div>
  );
}

// ============================================================================
// ATTACHMENTS SECTION · upload + list per activity
// ============================================================================
function ActivityAttachmentsSection({ activityId, lang }) {
  const { data: attachments, refresh } = window.melr.useActivityAttachments(activityId);
  const { profile } = window.melr.useCurrentProfile();
  const [uploadBusy, setUploadBusy] = useStateA(false);
  const [kind, setKind] = useStateA("");
  const [err, setErr] = useStateA(null);

  const onPick = async (e) => {
    const files = Array.from(e.target.files || []);
    if (files.length === 0) return;
    setUploadBusy(true); setErr(null);
    try {
      const orgId = profile && profile.organization_id;
      if (!orgId) throw new Error("No org context");
      for (const f of files) {
        await window.melr.uploadActivityAttachment(activityId, orgId, f, kind || null);
      }
      await refresh();
      e.target.value = "";
    } catch (e2) { setErr(e2.message); }
    finally { setUploadBusy(false); }
  };

  const onRemove = async (id) => {
    if (!window.confirm(window.L("Supprimer ce fichier ?", "Delete this file?", "¿Eliminar este archivo?"))) return;
    try {
      await window.melr.removeActivityAttachment(id);
      await refresh();
    } catch (e) { setErr(e.message); }
  };

  const openFile = async (path) => {
    const url = await window.melr.getActivityAttachmentUrl(path);
    if (url) window.open(url, "_blank");
  };

  return (
    <div>
      <div style={{ display: "flex", gap: 8, alignItems: "center", marginBottom: 6 }}>
        <select value={kind} onChange={(e) => setKind(e.target.value)}
          style={{ padding: "5px 8px", borderRadius: 5, border: "1px solid var(--line)", fontSize: 12, background: "var(--input-bg, var(--bg, white))" }}>
          <option value="">{window.L("— Type (optionnel) —", "— Kind (optional) —", "— Tipo (opcional) —")}</option>
          <option value="tdrs">TDRs</option>
          <option value="attendance">{window.L("Feuille de présence", "Attendance sheet", "Hoja de asistencia")}</option>
          <option value="photo">{window.L("Photo", "Photo", "Foto")}</option>
          <option value="report">{window.L("Rapport", "Report", "Informe")}</option>
          <option value="press">{window.L("Presse", "Press", "Prensa")}</option>
          <option value="other">{window.L("Autre", "Other", "Otro")}</option>
        </select>
        <label className="btn sm" style={{ cursor: uploadBusy ? "wait" : "pointer" }}>
          <Icon.upload /> {uploadBusy ? "…" : (window.L("Téléverser…", "Upload…", "Subir…"))}
          <input type="file" multiple style={{ display: "none" }} onChange={onPick} disabled={uploadBusy} />
        </label>
      </div>
      {err && <div style={{ padding: "6px 10px", background: "#fee2e2", color: "#991b1b", borderRadius: 5, fontSize: 11.5, marginBottom: 6 }}>{err}</div>}
      {(attachments || []).length === 0 ? (
        <div className="text-faint" style={{ fontSize: 11, padding: 10, textAlign: "center", background: "var(--bg-sunken)", borderRadius: 6 }}>
          {window.L("Aucune pièce jointe.", "No attachments.", "No hay adjuntos.")}
        </div>
      ) : (
        <div style={{ display: "grid", gap: 4 }}>
          {(attachments || []).map((a) => (
            <div key={a.id} style={{ display: "flex", gap: 8, alignItems: "center", padding: "6px 10px", background: "var(--bg-sunken)", borderRadius: 5, fontSize: 12 }}>
              <button type="button" onClick={() => openFile(a.file_path)} className="link"
                style={{ background: "none", border: 0, padding: 0, color: "var(--accent, #4f46e5)", cursor: "pointer", fontSize: 12, textAlign: "left", flex: 1 }}>
                🗎 {a.file_name}
              </button>
              {a.kind && <span className="pill" style={{ fontSize: 10 }}>{a.kind}</span>}
              <span className="text-faint" style={{ fontSize: 10.5 }}>
                {a.size_bytes ? (a.size_bytes < 1024 * 1024 ? `${Math.round(a.size_bytes / 1024)} KB` : `${(a.size_bytes / 1024 / 1024).toFixed(1)} MB`) : ""}
              </span>
              <button type="button" className="btn xs ghost" onClick={() => onRemove(a.id)}
                title={window.L("Supprimer", "Delete", "Eliminar")}>
                <Icon.x />
              </button>
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

// ============================================================================
// INLINE CREATE-NEW MODALS for each lookup
// ============================================================================

function NewActivityTypeModal({ lang, onClose, onCreated }) {
  const [code, setCode]       = useStateA("");
  const [nameFr, setNameFr]   = useStateA("");
  const [nameEn, setNameEn]   = useStateA("");
  const [busy, setBusy]       = useStateA(false);
  const [err, setErr]         = useStateA(null);
  const submit = async (e) => {
    e.preventDefault();
    setBusy(true); setErr(null);
    try {
      const created = await window.melr.activityTypesCrud.create({
        code: code.trim() || nameFr.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").slice(0, 32),
        name_fr: nameFr.trim(),
        name_en: nameEn.trim() || null,
      });
      onCreated(created);
    } catch (e2) { setErr(e2.message); }
    finally { setBusy(false); }
  };
  return <SmallLookupModal lang={lang} title={window.L("Nouveau type d'activité", "New activity type", "Nuevo tipo de actividad")}
    onClose={onClose} onSubmit={submit} busy={busy} err={err}
    fields={[
      { label: "Code", value: code,   setValue: setCode,   placeholder: "formation_avancee" },
      { label: window.L("Nom (FR) *", "Name (FR) *", "Nombre (FR) *"), value: nameFr, setValue: setNameFr, required: true, placeholder: window.L("Formation avancée", "Advanced training", "Formación avanzada") },
      { label: window.L("Nom (EN)", "Name (EN)", "Nombre (EN)"),     value: nameEn, setValue: setNameEn, placeholder: "Advanced training" },
    ]} />;
}

function NewPartnerModal({ lang, onClose, onCreated }) {
  const [nameFr, setNameFr]   = useStateA("");
  const [nameEn, setNameEn]   = useStateA("");
  const [code, setCode]       = useStateA("");
  const [busy, setBusy]       = useStateA(false);
  const [err, setErr]         = useStateA(null);
  const submit = async (e) => {
    e.preventDefault();
    setBusy(true); setErr(null);
    try {
      const created = await window.melr.partnersCrud.create({
        code: code.trim() || null,
        name_fr: nameFr.trim(),
        name_en: nameEn.trim() || null,
      });
      onCreated(created);
    } catch (e2) { setErr(e2.message); }
    finally { setBusy(false); }
  };
  return <SmallLookupModal lang={lang} title={window.L("Nouveau partenaire", "New partner", "Nuevo socio")}
    onClose={onClose} onSubmit={submit} busy={busy} err={err}
    fields={[
      { label: window.L("Nom (FR) *", "Name (FR) *", "Nombre (FR) *"), value: nameFr, setValue: setNameFr, required: true },
      { label: window.L("Nom (EN)", "Name (EN)", "Nombre (EN)"),     value: nameEn, setValue: setNameEn },
      { label: "Code", value: code, setValue: setCode, placeholder: "UNICEF, USAID…" },
    ]} />;
}

function NewProjectComponentModal({ lang, projectId, onClose, onCreated }) {
  const [code, setCode]       = useStateA("");
  const [nameFr, setNameFr]   = useStateA("");
  const [nameEn, setNameEn]   = useStateA("");
  const [busy, setBusy]       = useStateA(false);
  const [err, setErr]         = useStateA(null);
  const submit = async (e) => {
    e.preventDefault();
    setBusy(true); setErr(null);
    try {
      const created = await window.melr.projectComponentsCrud.create({
        project_id: projectId,
        code: code.trim() || null,
        name_fr: nameFr.trim(),
        name_en: nameEn.trim() || null,
      });
      onCreated(created);
    } catch (e2) { setErr(e2.message); }
    finally { setBusy(false); }
  };
  return <SmallLookupModal lang={lang} title={window.L("Nouvelle composante projet", "New project component", "Nuevo componente de proyecto")}
    onClose={onClose} onSubmit={submit} busy={busy} err={err}
    fields={[
      { label: window.L("Nom (FR) *", "Name (FR) *", "Nombre (FR) *"), value: nameFr, setValue: setNameFr, required: true,
        placeholder: window.L("Composante Santé", "Health component", "Componente Salud") },
      { label: window.L("Nom (EN)", "Name (EN)", "Nombre (EN)"),     value: nameEn, setValue: setNameEn },
      { label: "Code", value: code, setValue: setCode, placeholder: "C1, C2…" },
    ]} />;
}

// Tiny shared modal shell to avoid copy-pasting the same wrapping JSX 3 times.
// Note: this modal is OPENED FROM INSIDE the ActivityEditorModal's <form>.
// We MUST NOT render another <form> inside it (HTML forbids nesting forms,
// and the inner submit otherwise bubbles to the parent form → double-submit
// of the parent activity with stale values → page blanche).
// Solution: pass onSubmit=null to Modal so its wrapper stays a <div>, and
// trigger the create via onClick on the "Créer" button.
function SmallLookupModal({ lang, title, onClose, onSubmit, busy, err, fields }) {
  const inp = { width: "100%", padding: "8px 10px", borderRadius: 6, border: "1px solid var(--line)", fontSize: 13, background: "var(--input-bg, 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" };
  // Enter-to-submit on the last input is preserved by listening at the
  // container level — we synthesise a "submit-like" event for the caller
  // who still expects { preventDefault() }.
  const fakeSubmit = () => onSubmit({ preventDefault: () => {} });
  const onKeyDown = (e) => {
    if (e.key === "Enter" && !busy && !e.shiftKey) {
      // Only intercept Enter inside <input> — keep <textarea> linebreaks
      // working as expected.
      if (e.target && e.target.tagName === "INPUT") {
        e.preventDefault();
        e.stopPropagation();
        fakeSubmit();
      }
    }
  };
  return (
    <Modal size="sm" title={title} onClose={onClose}
      footer={<>
        <button type="button" className="btn sm ghost" onClick={onClose} disabled={busy}>
          {window.L("Annuler", "Cancel", "Cancelar")}
        </button>
        <button type="button" className="btn sm primary" disabled={busy} onClick={fakeSubmit}>
          {busy ? "…" : (window.L("Créer", "Create", "Crear"))}
        </button>
      </>}>
      <div style={{ display: "grid", gap: 10 }} onKeyDown={onKeyDown}>
        {fields.map((f, i) => (
          <div key={i}>
            <label style={lbl}>{f.label}</label>
            <input style={inp} value={f.value} onChange={(e) => f.setValue(e.target.value)}
              placeholder={f.placeholder || ""} required={!!f.required} />
          </div>
        ))}
        {err && (
          <div style={{ padding: "6px 10px", background: "#fee2e2", color: "#991b1b", borderRadius: 5, fontSize: 12 }}>{err}</div>
        )}
      </div>
    </Modal>
  );
}

// Expose for app.jsx route dispatch.
window.Activities = Activities;
// Exposé pour permettre l'édition d'une activité depuis le Gantt du projet
// (clic sur une barre d'activité ouvre ce même éditeur).
window.ActivityEditorModal = ActivityEditorModal;
// Couleurs de statut d'activité réutilisables (Gantt) — clé = slug enum.
window.ACTIVITY_STATUS_COLORS = ACTIVITY_STATUSES.reduce((m, s) => { m[s.v] = s.color; return m; }, {});
// Référentiel complet { v, fr, en, color } — utilisé par la légende du Gantt.
window.ACTIVITY_STATUSES = ACTIVITY_STATUSES;
