/* global React, Icon, Modal */
const { useState: useStateFB, useEffect: useEffectFB, useMemo: useMemoFB } = React;

// ============================================================================
// FORM BUILDER — admin tool to create / edit forms in the database without SQL
// ----------------------------------------------------------------------------
// Two surfaces:
//   • List view (default)   — every form in the org with code / project /
//                             version / active toggle / restrictions count.
//   • Editor (slide-over)   — title metadata + drag-orderable schema fields
//                             + optional restricted-agent multi-select.
//
// Admin-only — the page is hidden in the sidebar nav unless the user has
// the users.manage permission. The underlying RLS in form-agents.sql
// rejects writes from non-admins anyway.
// ============================================================================

// Each entry can carry a short help line shown under the row to document what
// the field becomes at saisie time. Helps the form designer pick the right type.
const FIELD_TYPES = [
  { v: "text",      fr: "Texte libre",                 en: "Free text", es: "Texto libre",
    helpFr: "Champ texte libre (single line).", helpEn: "Free single-line text input." },
  { v: "number",    fr: "Nombre",                      en: "Number", es: "Número",
    helpFr: "Saisie numérique (entier ou décimal).", helpEn: "Numeric input." },
  { v: "date",      fr: "Date",                        en: "Date", es: "Fecha",
    helpFr: "Sélecteur de date.", helpEn: "Date picker." },
  { v: "bool",      fr: "Oui / Non",                   en: "Yes / No", es: "Sí / No",
    helpFr: "Case à cocher.", helpEn: "Checkbox." },
  { v: "select",    fr: "Liste déroulante",            en: "Dropdown", es: "Lista desplegable",
    helpFr: "Dropdown avec options personnalisées.", helpEn: "Dropdown with custom options." },
  { v: "matrix",    fr: "Matrice (texte)",             en: "Matrix (text)", es: "Matriz (texto)",
    helpFr: "Tableau de saisie texte (lignes × colonnes).", helpEn: "Text-input grid (rows × columns)." },
  { v: "photo",     fr: "Photo (appareil)",            en: "Photo (camera)", es: "Foto (cámara)",
    helpFr: "Capture photo depuis l'appareil terrain.", helpEn: "Take a photo on device." },
  { v: "signature", fr: "Signature manuscrite",        en: "Hand signature", es: "Firma manuscrita",
    helpFr: "Zone de dessin pour signature.", helpEn: "Hand-drawn signature pad." },
  // ── Indicator picker (single) — picks ONE indicator and expands it
  //    into a disaggregation grid at data-entry time (Pass 2 - 2026-05).
  { v: "indicator", fr: "Indicateur (1 avec désagrégation)", en: "Indicator (1 with disaggregation)", es: "Indicador (1 con desagregación)",
    helpFr: "Un seul indicateur. À la saisie, expand en grille N/D selon les axes de désagrégation.",
    helpEn: "Single indicator. At entry time, expands to N/D grid per axis." },
  // ── E2 (2026-05) · standard activity-form building blocks ─────────────
  { v: "project",         fr: "Projet (sélecteur)",      en: "Project (picker)", es: "Proyecto (selector)",
    helpFr: "Dropdown des projets de l'organisation. À placer en première ligne.",
    helpEn: "Dropdown of org projects. Should be the first field." },
  { v: "activity",        fr: "Activité (sélecteur)",    en: "Activity (picker)", es: "Actividad (selector)",
    helpFr: "Dropdown des activités du projet sélectionné dans le formulaire.",
    helpEn: "Dropdown of activities for the project picked in the form." },
  { v: "gps",             fr: "Coordonnées GPS",         en: "GPS coordinates", es: "Coordenadas GPS",
    helpFr: "Latitude + longitude + bouton de géolocalisation appareil.",
    helpEn: "Latitude + longitude + device-GPS button." },
  { v: "partners_multi",  fr: "Partenaires (multi-sélect)", en: "Partners (multi-select)", es: "Socios (selección múltiple)",
    helpFr: "Liste de partenaires de mise en œuvre, sélection multiple.",
    helpEn: "Implementation partners, multi-select." },
  { v: "indicators_multi",fr: "Indicateurs (multi-sélect)", en: "Indicators (multi-select)", es: "Indicadores (selección múltiple)",
    helpFr: "Plusieurs indicateurs du projet — cases à cocher avec recherche.",
    helpEn: "Multiple indicators of the project — searchable checklist." },
  { v: "attachments",     fr: "Pièces jointes (multiple)", en: "Attachments (multiple)", es: "Archivos adjuntos (múltiples)",
    helpFr: "Téléversement de plusieurs fichiers (TDRs, présences, photos, rapports…).",
    helpEn: "Upload multiple files (TDRs, attendance, photos, reports…)." },
  { v: "activity_status", fr: "Statut activité",         en: "Activity status", es: "Estado de la actividad",
    helpFr: "Dropdown standardisé : Non démarrée · En cours · Terminée · Reportée · Annulée.",
    helpEn: "Standard dropdown: Not started · Ongoing · Completed · Postponed · Cancelled." },
  // Date range : début + fin sur la même ligne (un seul champ logique stocke
  // un objet { start, end } dans entryData).
  { v: "date_range",      fr: "Période (début / fin)",    en: "Date range (start / end)", es: "Periodo (inicio / fin)",
    helpFr: "Deux dates côte à côte (début et fin de l'activité).",
    helpEn: "Two side-by-side dates (start and end of the activity)." },
  // ── Phase 6 (2026-05) · saisie directe d'une valeur d'indicateur ─────
  // Lie le champ à UN indicateur spécifique. À la saisie, l'agent entre
  // un nombre (ou un score PEFA selon value_kind). À la soumission, le
  // système crée AUTOMATIQUEMENT un indicator_values lié à l'activité
  // (Phase 7). Diffère du type `indicator` qui ouvre une grille de
  // désagrégation pour les indicateurs N/D désagrégés.
  { v: "indicator_value", fr: "Valeur d'un indicateur",   en: "Indicator value (single)", es: "Valor de un indicador",
    helpFr: "Saisie d'une valeur pour UN indicateur. La valeur sera enregistrée comme indicator_value lié à l'activité (auto à la soumission).",
    helpEn: "Single-value entry for ONE indicator. Saved as an indicator_value linked to the activity at submit time." },
];

// Suggested "Activity header" template — when the user clicks "Modèle activité",
// these 5 fields are inserted (or merged into) the schema in this order.
// PROJECT must always come first per the E2 spec.
// E2-bis Pass 2 (2026-05): swap single `date` for `date_range` (start+end
// on same row) per user request « date début + date fin alignées ».
const ACTIVITY_HEADER_TEMPLATE = [
  { k: "project",       l: "Projet",                  type: "project"     },
  { k: "activity",      l: "Activité",                type: "activity"    },
  { k: "period",        l: "Période de l'activité",   type: "date_range"  },
  { k: "gps_coords",    l: "Coordonnées GPS",         type: "gps"         },
  { k: "description",   l: "Description",             type: "text"        },
];

// Labels that the auto-label-suggestion (FieldRow type dropdown) considers
// as "still a default" and therefore safe to overwrite when the user changes
// a field's type. Includes the canonical FIELD_TYPES names AND the
// template-provided labels (FR + EN). Without this, a user who changes a
// "Description" field's type to partners_multi would see the label stay
// "Description" — confusing for them and for the agent at saisie time.
const KNOWN_DEFAULT_LABELS = new Set([
  // Template labels (FR)
  "Projet", "Activité", "Activite",
  "Période de l'activité", "Periode de l'activite",
  "Période (début / fin)", "Periode (debut / fin)",
  "Coordonnées GPS", "Coordonnees GPS",
  "Description", "Notes",
  "Date", "Date début", "Date Début", "Date fin", "Date Fin",
  // Template labels (EN)
  "Project", "Activity",
  "Activity period", "Date range (start / end)",
  "GPS coordinates", "Description", "Notes",
  "Date", "Start date", "End date",
]);

function FormBuilder({ t, lang }) {
  const { has, loading: permsLoading } = window.melr.useCurrentUserPermissions();
  const { data: forms, loading: formsLoading, realtime } = window.melr.useForms();
  const { projects, loading: projectsLoading } = window.melr.useProjects();
  const LiveBadge = window.melr.LiveBadge;

  // Restriction counts per form_id — one query for the org's form_agents
  // (RLS limits admins to their org via form_org()). Polled when forms
  // change so the badge stays fresh after the editor closes.
  const [restrictionsByForm, setRestrictionsByForm] = useStateFB({});
  useEffectFB(() => {
    let alive = true;
    (async () => {
      const sb = window.melr.supabase;
      if (!sb) return;
      const r = await sb.from("form_agents").select("form_id");
      if (!alive || r.error) return;
      const counts = {};
      (r.data || []).forEach((row) => { counts[row.form_id] = (counts[row.form_id] || 0) + 1; });
      setRestrictionsByForm(counts);
    })();
    return () => { alive = false; };
  }, [forms]);
  const [editingId, setEditingId] = useStateFB(null);     // form UUID or "new"
  const [filter, setFilter]       = useStateFB("");        // project code or ""
  const [busyId, setBusyId]       = useStateFB(null);
  const [exportingForm, setExportingForm] = useStateFB(null); // form row being exported

  if (permsLoading) {
    return <div className="page"><div style={{ padding: 28 }}>{window.L("Chargement…", "Loading…", "Cargando…")}</div></div>;
  }
  if (!has("users.manage")) {
    return (
      <div className="page">
        <div className="page-header">
          <div className="page-eyebrow">{window.L("ADMINISTRATION", "ADMINISTRATION", "ADMINISTRACIÓN")}</div>
          <h1 className="page-title">{window.L("Constructeur de formulaires", "Form builder", "Constructor de formularios")}</h1>
        </div>
        <div className="card" style={{ marginTop: 16, padding: 24 }}>
          <div className="text-faint" style={{ textAlign: "center" }}>
            {window.L("Accès réservé aux administrateurs (permission users.manage).", "Admins only (users.manage permission required).", "Acceso reservado a los administradores (permiso users.manage).")}
          </div>
        </div>
      </div>
    );
  }

  const onToggleActive = async (form) => {
    setBusyId(form.id);
    try { await window.melr.formsCrud.setActive(form.id, !form.active); }
    catch (e) { window.alert((window.L("Erreur : ", "Error: ", "Error: ")) + e.message); }
    finally { setBusyId(null); }
  };
  const onDelete = async (form) => {
    if (!window.confirm(window.L("Supprimer le formulaire « " + (form.name_fr || form.code) + " » ? Les soumissions existantes seront conservées mais orphelines.", "Delete form '" + (form.name_en || form.name_fr || form.code) + "'? Existing submissions are kept but orphaned.", "¿Eliminar el formulario « " + (form.name_en || form.name_fr || form.code) + " »? Los envíos existentes se conservarán pero quedarán huérfanos."))) return;
    setBusyId(form.id);
    try { await window.melr.formsCrud.remove(form.id); }
    catch (e) { window.alert((window.L("Erreur : ", "Error: ", "Error: ")) + e.message); }
    finally { setBusyId(null); }
  };

  const visible = (forms || []).filter((f) => !filter || (f.projects && f.projects.code === filter));
  const projectsFromForms = [...new Set((forms || []).map((f) => f.projects && f.projects.code).filter(Boolean))].sort();

  return (
    <div className="page">
      <div className="page-header">
        <div className="page-eyebrow">{window.L("ADMINISTRATION", "ADMINISTRATION", "ADMINISTRACIÓN")}</div>
        <div className="page-header-row">
          <div>
            <h1 className="page-title">
              {window.L("Constructeur de formulaires", "Form builder", "Constructor de formularios")}{" "}
              <LiveBadge on={realtime} lang={lang} />
            </h1>
            <div className="page-sub">
              {window.L("Créer, modifier, désactiver les formulaires utilisés par la collecte de données mobile.", "Create, edit, disable the forms used by mobile data collection.", "Crear, editar y desactivar los formularios usados por la recogida de datos móvil.")}
            </div>
          </div>
          <div className="page-header-actions">
            <button className="btn sm primary" onClick={() => setEditingId("new")}>
              <Icon.plus /> {window.L("Nouveau formulaire", "New form", "Nuevo formulario")}
            </button>
          </div>
        </div>
      </div>

      <div className="card">
        <div className="card-head">
          <div className="card-title">
            {window.L("Formulaires", "Forms", "Formularios")}{" "}
            <span className="tag-mono" style={{ marginLeft: 6 }}>{visible.length} / {(forms || []).length}</span>
          </div>
          <div style={{ flex: 1 }} />
          {projectsFromForms.length > 1 && (
            <select value={filter} onChange={(e) => setFilter(e.target.value)}
              style={{ padding: "4px 8px", fontSize: 12, borderRadius: 4, border: "1px solid var(--line)", background: "var(--bg, white)", color: "var(--text)" }}>
              <option value="">{window.L("Tous les projets", "All projects", "Todos los proyectos")}</option>
              {projectsFromForms.map((c) => <option key={c} value={c}>{c}</option>)}
            </select>
          )}
        </div>
        <div className="card-body flush">
          {formsLoading ? (
            <div className="text-faint" style={{ padding: 24, textAlign: "center" }}>
              {window.L("Chargement…", "Loading…", "Cargando…")}
            </div>
          ) : visible.length === 0 ? (
            <div className="text-faint" style={{ padding: 24, textAlign: "center" }}>
              {window.L("Aucun formulaire pour le moment. Cliquez « Nouveau formulaire » pour démarrer.", "No form yet. Click 'New form' to start.", "Aún no hay ningún formulario. Haga clic en «Nuevo formulario» para empezar.")}
            </div>
          ) : (
            <table className="tbl">
              <thead>
                <tr>
                  <th>{window.L("Code", "Code", "Código")}</th>
                  <th>{window.L("Nom", "Name", "Nombre")}</th>
                  <th>{window.L("Projet", "Project", "Proyecto")}</th>
                  <th>{window.L("Version", "Version", "Versión")}</th>
                  <th>{window.L("Champs", "Fields", "Campos")}</th>
                  <th>{window.L("Restriction", "Restriction", "Restricción")}</th>
                  <th>{window.L("Statut", "State", "Estado")}</th>
                  <th></th>
                </tr>
              </thead>
              <tbody>
                {visible.map((f) => {
                  const fieldCount = (f.schema && Array.isArray(f.schema.fields)) ? f.schema.fields.length : 0;
                  const restrictionCount = restrictionsByForm[f.id] || 0;
                  return (
                    <tr key={f.id}>
                      <td className="mono">{f.code}</td>
                      <td>{lang === "en" ? (f.name_en || f.name_fr) : f.name_fr}</td>
                      <td className="mono text-faint">{f.projects ? f.projects.code : "—"}</td>
                      <td className="mono text-faint">{f.version || "—"}</td>
                      <td className="num mono">{fieldCount}</td>
                      <td>
                        {restrictionCount > 0 ? (
                          <span className="pill green dot" style={{ fontSize: 10 }}
                            title={window.L((restrictionCount + " agent(s) autorisé(s)"), (restrictionCount + " allowed agent(s)"), (restrictionCount + " agente(s) autorizado(s)"))}>
                            🔒 {restrictionCount}
                          </span>
                        ) : (
                          <span className="text-faint" style={{ fontSize: 10.5 }}
                            title={window.L("Aucune restriction — tous les agents du projet", "No restriction — all project agents", "Sin restricción — todos los agentes del proyecto")}>
                            {window.L("Tous", "All", "Todos")}
                          </span>
                        )}
                      </td>
                      <td>
                        <button className={"pill " + (f.active ? "green dot" : "")}
                          style={{ cursor: "pointer", border: 0 }}
                          disabled={busyId === f.id}
                          onClick={() => onToggleActive(f)}
                          title={window.L("Basculer actif/inactif", "Toggle active/inactive", "Alternar activo/inactivo")}>
                          {f.active ? (window.L("Actif", "Active", "Activo")) : (window.L("Inactif", "Inactive", "Inactivo"))}
                        </button>
                      </td>
                      <td>
                        <div className="row gap-xs" style={{ justifyContent: "flex-end" }}>
                          <button className="btn xs" onClick={() => setExportingForm(f)}
                            title={window.L("Exporter les saisies en Excel", "Export submissions to Excel", "Exportar las entradas a Excel")}>
                            <Icon.download /> {window.L("Exporter", "Export", "Exportar")}
                          </button>
                          <button className="btn xs" onClick={() => setEditingId(f.id)}>
                            <Icon.edit /> {window.L("Modifier", "Edit", "Editar")}
                          </button>
                          <button className="btn xs ghost" onClick={() => onDelete(f)} disabled={busyId === f.id}
                            title={window.L("Supprimer", "Delete", "Eliminar")}>
                            <Icon.x />
                          </button>
                        </div>
                      </td>
                    </tr>
                  );
                })}
              </tbody>
            </table>
          )}
        </div>
      </div>

      {editingId && (
        <FormEditor
          lang={lang}
          formId={editingId === "new" ? null : editingId}
          existing={editingId === "new" ? null : (forms || []).find((f) => f.id === editingId)}
          projects={projects || []}
          projectsLoading={projectsLoading}
          onClose={() => setEditingId(null)}
        />
      )}

      {exportingForm && (
        <ExportModal
          lang={lang}
          form={exportingForm}
          onClose={() => setExportingForm(null)}
        />
      )}
    </div>
  );
}

// ── Export modal — date range + attachment URLs toggle, then writes .xlsx ──
function ExportModal({ lang, form, onClose }) {
  const [startDate, setStartDate] = useStateFB("");
  const [endDate, setEndDate]     = useStateFB("");
  const [includeAttachments, setIncludeAttachments] = useStateFB(true);
  const [busy, setBusy]           = useStateFB(false);
  const [done, setDone]           = useStateFB(null);
  const [err, setErr]             = useStateFB(null);

  const onExport = async () => {
    setBusy(true); setErr(null); setDone(null);
    try {
      const r = await window.melr.exportFormSubmissions(form.id, {
        startDate: startDate || null,
        endDate:   endDate || null,
        includeAttachments,
      });
      setDone(r);
    } catch (e) {
      setErr(e.message);
    } finally {
      setBusy(false);
    }
  };

  const inp = { width: "100%", padding: "7px 9px", 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" };

  return (
    <Modal
      size="sm"
      title={<>{window.L("Exporter les saisies", "Export submissions", "Exportar las entradas")}
        <span className="text-faint mono" style={{ marginLeft: 6, fontSize: 12 }}>{form.code}</span>
      </>}
      onClose={onClose}
      footer={<>
        <button className="btn sm ghost" onClick={onClose} disabled={busy}>
          {done ? (window.L("Fermer", "Close", "Cerrar")) : (window.L("Annuler", "Cancel", "Cancelar"))}
        </button>
        <button className="btn sm primary" onClick={onExport} disabled={busy}>
          {busy ? "…" : (window.L("Exporter", "Export", "Exportar"))}
        </button>
      </>}>
      <div>
        <div className="text-faint" style={{ fontSize: 12, marginBottom: 14 }}>
          {window.L("Génère un fichier Excel avec une colonne par champ du schéma. Une feuille « Pièces jointes » contient les URLs signées (1h) pour les photos et signatures.", "Produces an Excel file with one column per schema field. The 'Attachments' sheet has signed URLs (1h) for photos and signatures.", "Genera un archivo Excel con una columna por campo del esquema. Una hoja «Adjuntos» contiene las URL firmadas (1 h) para las fotos y firmas.")}
        </div>

        <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 10 }}>
          <div>
            <label style={lbl}>{window.L("Du", "From", "Desde")}</label>
            <input type="date" style={inp} value={startDate} onChange={(e) => setStartDate(e.target.value)} />
          </div>
          <div>
            <label style={lbl}>{window.L("Au", "To", "Hasta")}</label>
            <input type="date" style={inp} value={endDate} onChange={(e) => setEndDate(e.target.value)} />
          </div>
        </div>
        <div className="text-faint" style={{ fontSize: 10.5, marginTop: 4 }}>
          {window.L("Laisser vide pour exporter toutes les saisies.", "Leave blank to export all submissions.", "Dejar vacío para exportar todas las entradas.")}
        </div>

        <label style={{ display: "flex", alignItems: "center", gap: 8, marginTop: 14, fontSize: 13, cursor: "pointer" }}>
          <input type="checkbox" checked={includeAttachments} onChange={(e) => setIncludeAttachments(e.target.checked)} />
          <span>
            {window.L("Inclure les URLs des pièces jointes (1h)", "Include attachment URLs (1h)", "Incluir las URL de los adjuntos (1 h)")}
          </span>
        </label>

        {done && (
          <div style={{ marginTop: 14, padding: "10px 12px", background: "#dcfce7", color: "#166534", borderRadius: 6, fontSize: 12.5 }}>
            {window.L("✓ Fichier généré : " + done.filename + " · " + done.count + " saisie(s)" + (done.attachments ? ", " + done.attachments + " pièce(s) jointe(s)" : ""), "✓ File generated: " + done.filename + " · " + done.count + " submission(s)" + (done.attachments ? ", " + done.attachments + " attachment(s)" : ""), "✓ Archivo generado: " + done.filename + " · " + done.count + " entrada(s)" + (done.attachments ? ", " + done.attachments + " adjunto(s)" : ""))}
          </div>
        )}
        {err && (
          <div style={{ marginTop: 14, padding: "10px 12px", background: "#fee2e2", color: "#991b1b", borderRadius: 6, fontSize: 12.5 }}>
            {err}
          </div>
        )}
      </div>
    </Modal>
  );
}

// ── Editor ───────────────────────────────────────────────────────────────────

function FormEditor({ lang, formId, existing, projects, projectsLoading, onClose }) {
  const isNew = !formId;
  const [code, setCode]       = useStateFB(existing ? existing.code     : "");
  const [nameFr, setNameFr]   = useStateFB(existing ? existing.name_fr  : "");
  const [nameEn, setNameEn]   = useStateFB(existing ? (existing.name_en || "") : "");
  const [version, setVersion] = useStateFB(existing ? (existing.version || "v1") : "v1");
  const [active, setActive]   = useStateFB(existing ? existing.active   : true);
  const [projectId, setProjectId] = useStateFB(existing ? (existing.project_id || "") : "");
  const [fields, setFields]   = useStateFB(() => {
    if (existing && existing.schema && Array.isArray(existing.schema.fields)) return existing.schema.fields.map((f) => ({ ...f }));
    return [];
  });
  const [busy, setBusy] = useStateFB(false);
  const [err, setErr]   = useStateFB(null);

  // Per-form agent restriction (form_agents). Empty list → no restriction
  // (all project agents). Non-empty → only these agents can use the form.
  const { members } = window.melr.useOrgMembers();
  const { data: formAgents, refresh: refreshFormAgents } = window.melr.useFormAgents(formId);
  const [restrictedAgentIds, setRestrictedAgentIds] = useStateFB([]);
  useEffectFB(() => {
    setRestrictedAgentIds((formAgents || []).map((fa) => fa.agent_id));
  }, [formAgents]);

  const onAddField = () => {
    setFields((arr) => [
      ...arr,
      { k: "field_" + (arr.length + 1), l: "", type: "text" },
    ]);
  };
  const onRemoveField = (idx) => setFields((arr) => arr.filter((_, i) => i !== idx));
  const onMoveField   = (idx, delta) => {
    setFields((arr) => {
      const next = arr.slice();
      const newIdx = idx + delta;
      if (newIdx < 0 || newIdx >= next.length) return next;
      const [item] = next.splice(idx, 1);
      next.splice(newIdx, 0, item);
      return next;
    });
  };
  const onPatchField = (idx, patch) => {
    setFields((arr) => arr.map((f, i) => i === idx ? { ...f, ...patch } : f));
  };

  const validate = () => {
    if (!code.trim())   return window.L("Le code est requis.", "Code is required.", "El código es obligatorio.");
    if (!nameFr.trim()) return window.L("Le nom (français) est requis.", "Name (French) is required.", "El nombre (francés) es obligatorio.");
    // Field keys must be unique and snake_case-friendly
    const keys = new Set();
    for (const f of fields) {
      if (!f.k || !f.k.trim()) return window.L("Chaque champ doit avoir une clé.", "Every field needs a key.", "Cada campo debe tener una clave.");
      if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(f.k)) {
        return (window.L("Clé invalide : « ", "Invalid key: '", "Clave no válida: «")) + f.k + (window.L(" » (lettres, chiffres, underscore)", "' (letters, digits, underscore)", "» (letras, dígitos, guion bajo)"));
      }
      if (keys.has(f.k)) return (window.L("Clé en double : ", "Duplicate key: ", "Clave duplicada: ")) + f.k;
      keys.add(f.k);
      if (f.type === "select") {
        if (!Array.isArray(f.options) || f.options.length === 0) {
          return (window.L("Le champ « ", "Field '", "El campo «")) + f.k + (window.L(" » (select) doit avoir au moins une option.", "' (select) needs at least one option.", "» (select) debe tener al menos una opción."));
        }
      }
    }
    return null;
  };

  const onSave = async () => {
    const v = validate();
    if (v) { setErr(v); return; }
    setBusy(true); setErr(null);
    try {
      const payload = {
        project_id: projectId || null,
        code: code.trim(),
        name_fr: nameFr.trim(),
        name_en: nameEn.trim() || null,
        version: version.trim() || "v1",
        schema: { fields },
        active: !!active,
      };
      let saved;
      if (isNew) {
        saved = await window.melr.formsCrud.create(payload);
      } else {
        saved = await window.melr.formsCrud.update(formId, payload);
      }
      // Save the agent restriction list against the (now persisted) form id
      await window.melr.formAgentsCrud.setAll(saved.id, restrictedAgentIds);
      await refreshFormAgents();
      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", fontFamily: "inherit" };
  const lbl = { display: "block", fontSize: 11, color: "var(--text-faint)", marginBottom: 4, textTransform: "uppercase", letterSpacing: "0.04em" };

  return (
    <Modal
      size="lg"
      title={isNew
        ? (window.L("Nouveau formulaire", "New form", "Nuevo formulario"))
        : ((lang === "en" ? (existing && existing.name_en) || (existing && existing.name_fr) : (existing && existing.name_fr)) || (window.L("Modifier", "Edit", "Editar")))}
      onClose={onClose}
      footer={<>
        <button className="btn sm ghost" onClick={onClose} disabled={busy}>
          {window.L("Annuler", "Cancel", "Cancelar")}
        </button>
        <button className="btn sm primary" onClick={onSave} disabled={busy}>
          {busy ? "…" : (isNew ? (window.L("Créer", "Create", "Crear")) : (window.L("Enregistrer", "Save", "Guardar")))}
        </button>
      </>}>
      <div>
          {/* Metadata */}
          <div style={{ display: "grid", gridTemplateColumns: "120px 1fr 1fr 80px", gap: 10, alignItems: "end" }}>
            <div>
              <label style={lbl}>{window.L("Code *", "Code *", "Código *")}</label>
              <input style={inp} value={code} onChange={(e) => setCode(e.target.value)} placeholder="VISITE-01" maxLength={32} />
            </div>
            <div>
              <label style={lbl}>{window.L("Nom français *", "French name *", "Nombre francés *")}</label>
              <input style={inp} value={nameFr} onChange={(e) => setNameFr(e.target.value)} placeholder={window.L("Visite mensuelle", "Monthly visit", "Visita mensual")} />
            </div>
            <div>
              <label style={lbl}>{window.L("Nom anglais", "English name", "Nombre inglés")}</label>
              <input style={inp} value={nameEn} onChange={(e) => setNameEn(e.target.value)} placeholder="Monthly visit" />
            </div>
            <div>
              <label style={lbl}>{window.L("Version", "Version", "Versión")}</label>
              <input style={inp} value={version} onChange={(e) => setVersion(e.target.value)} placeholder="v1" />
            </div>
          </div>

          <div style={{ display: "grid", gridTemplateColumns: "1fr auto", gap: 10, alignItems: "end", marginTop: 10 }}>
            <div>
              <label style={lbl}>{window.L("Projet", "Project", "Proyecto")}</label>
              <select style={inp} value={projectId} onChange={(e) => setProjectId(e.target.value)} disabled={projectsLoading}>
                <option value="">{window.L("— Aucun projet (formulaire org-wide) —", "— No project (org-wide form) —", "— Sin proyecto (formulario de toda la org) —")}</option>
                {(projects || []).map((p) => (
                  <option key={p.uuid} value={p.uuid}>{p.id} · {lang === "en" ? (p.nameEn || p.nameFr) : p.nameFr}</option>
                ))}
              </select>
            </div>
            <div>
              <label style={lbl}>{window.L("Actif", "Active", "Activo")}</label>
              <label style={{ display: "flex", alignItems: "center", gap: 6, padding: "8px 0" }}>
                <input type="checkbox" checked={active} onChange={(e) => setActive(e.target.checked)} />
                <span style={{ fontSize: 12 }}>{active ? (window.L("Visible sur mobile", "Visible on mobile", "Visible en móvil")) : (window.L("Masqué", "Hidden", "Oculto"))}</span>
              </label>
            </div>
          </div>

          {/* Schema fields */}
          <div className="card-head" style={{ marginTop: 18, marginBottom: 8, padding: 0, borderBottom: "1px solid var(--line)", paddingBottom: 6 }}>
            <div className="card-title" style={{ fontSize: 13 }}>{window.L("Champs du formulaire", "Form fields", "Campos del formulario")}</div>
            <span className="tag-mono" style={{ marginLeft: 6 }}>{fields.length}</span>
            <div style={{ flex: 1 }} />
            {/* "Modèle activité" shortcut — adds the standard 5-field activity
                header (Projet + Activité + Période + GPS + Description). Smart:
                - Skips template fields whose key already exists
                - REMOVES legacy fields that the template superseded (ex. a
                  field with key "date" type "date" is replaced by the new
                  "period" type "date_range"). Without this cleanup, users
                  who clicked the template before our schema rev end up with
                  BOTH the old field and the new one.
                Prepends so PROJECT lands at position 0. */}
            <button className="btn sm ghost"
              onClick={() => {
                // Legacy keys that have been superseded by the current template.
                // Each entry maps an old key/type pair to the new template key
                // that replaces it. If we see the old AND the new is in the
                // template, drop the old one before merging.
                const LEGACY_SUPERSEDED = [
                  { oldKey: "date",       oldType: "date",        newKey: "period" },
                  // future migrations would go here
                ];
                const existingKeys = new Set(fields.map((f) => f.k));
                // Drop legacy fields whose replacement is in the template AND
                // the legacy field's key doesn't already match the new one.
                const cleaned = fields.filter((f) => {
                  for (const m of LEGACY_SUPERSEDED) {
                    if (f.k === m.oldKey && (!f.type || f.type === m.oldType)
                        && ACTIVITY_HEADER_TEMPLATE.some((t) => t.k === m.newKey)) {
                      return false;
                    }
                  }
                  return true;
                });
                const cleanedKeys = new Set(cleaned.map((f) => f.k));
                const toAdd = ACTIVITY_HEADER_TEMPLATE.filter((t) => !cleanedKeys.has(t.k));
                if (toAdd.length === 0 && cleaned.length === fields.length) return;
                setFields([...toAdd, ...cleaned]);
              }}
              title={window.L("Insère l'en-tête standard : Projet + Activité + Période + GPS + Description. Remplace automatiquement l'ancien champ « Date » par « Période » si présent.", "Insert the standard header: Project + Activity + Period + GPS + Description. Auto-replaces any legacy 'Date' field with the new 'Period' one.", "Inserta la cabecera estándar: Proyecto + Actividad + Periodo + GPS + Descripción. Sustituye automáticamente el antiguo campo «Fecha» por «Periodo» si está presente.")}>
              🚀 {window.L("Modèle activité", "Activity template", "Plantilla de actividad")}
            </button>
            <button className="btn sm" onClick={onAddField}><Icon.plus /> {window.L("Ajouter un champ", "Add field", "Añadir un campo")}</button>
          </div>

          {fields.length === 0 && (
            <div className="text-faint" style={{ padding: 14, textAlign: "center", fontSize: 12.5, border: "1px dashed var(--line)", borderRadius: 6 }}>
              {window.L("Aucun champ. Cliquer « 🚀 Modèle activité » pour un en-tête standard, ou « Ajouter un champ » pour partir de zéro.", "No field yet. Click '🚀 Activity template' for a standard header, or 'Add field' to start from scratch.", "Ningún campo. Haga clic en «🚀 Plantilla de actividad» para una cabecera estándar, o en «Añadir un campo» para empezar desde cero.")}
            </div>
          )}

          <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
            {fields.map((f, idx) => <FieldRow key={idx} idx={idx} field={f} lang={lang}
              onPatch={(p) => onPatchField(idx, p)} onRemove={() => onRemoveField(idx)}
              onUp={() => onMoveField(idx, -1)} onDown={() => onMoveField(idx, +1)}
              first={idx === 0} last={idx === fields.length - 1}
            />)}
          </div>

          {/* Agent restriction */}
          {!isNew && (
            <>
              <div className="card-head" style={{ marginTop: 18, marginBottom: 8, padding: 0, borderBottom: "1px solid var(--line)", paddingBottom: 6 }}>
                <div className="card-title" style={{ fontSize: 13 }}>
                  {window.L("Restriction par agent (optionnel)", "Restrict to specific agents (optional)", "Restricción por agente (opcional)")}
                </div>
                <span className="tag-mono" style={{ marginLeft: 6 }}>{restrictedAgentIds.length}</span>
              </div>
              <div style={{
                padding: "8px 10px", borderRadius: 6, fontSize: 11.5, marginBottom: 8,
                background: restrictedAgentIds.length === 0 ? "#fef3c7" : "#dcfce7",
                color:      restrictedAgentIds.length === 0 ? "#92400e" : "#166534",
                border: "1px solid " + (restrictedAgentIds.length === 0 ? "#fcd34d" : "#86efac"),
              }}>
                {restrictedAgentIds.length === 0 ? (
                  window.L("⚠ Aucune restriction active. Tous les agents affectés au projet de ce formulaire pourront le voir et le remplir sur mobile.", "⚠ No restriction active. Every agent assigned to this form's project will see and submit it on mobile.", "⚠ Ninguna restricción activa. Todos los agentes asignados al proyecto de este formulario podrán verlo y rellenarlo en móvil.")
                ) : (
                  window.L("🔒 Restriction active : seuls les " + restrictedAgentIds.length + " agent(s) sélectionné(s) ci-dessous (et les admins) verront ce formulaire sur mobile.", "🔒 Restriction active: only the " + restrictedAgentIds.length + " selected agent(s) below (and admins) will see this form on mobile.", "🔒 Restricción activa: solo los " + restrictedAgentIds.length + " agente(s) seleccionado(s) a continuación (y los administradores) verán este formulario en móvil.")
                )}
              </div>
              <div className="text-faint" style={{ fontSize: 11, marginBottom: 8 }}>
                {window.L("Cliquez sur un agent pour l'ajouter / le retirer de la liste, puis « Enregistrer ».", "Click an agent to toggle them in / out of the list, then 'Save'.", "Haga clic en un agente para añadirlo o quitarlo de la lista, y luego en «Guardar».")}
              </div>
              <div style={{ display: "flex", flexWrap: "wrap", gap: 6 }}>
                {(members || []).map((m) => {
                  const on = restrictedAgentIds.includes(m.id);
                  return (
                    <button key={m.id}
                      onClick={() => {
                        setRestrictedAgentIds((arr) => on ? arr.filter((x) => x !== m.id) : [...arr, m.id]);
                      }}
                      className={"pill " + (on ? "green dot" : "")}
                      style={{ cursor: "pointer", border: on ? 0 : "1px solid var(--line)", background: on ? "" : "var(--bg, white)" }}>
                      {m.full_name || m.email}
                    </button>
                  );
                })}
                {(members || []).length === 0 && (
                  <div className="text-faint" style={{ fontSize: 12 }}>
                    {window.L("Aucun membre dans l'org.", "No member in the org.", "Ningún miembro en la org.")}
                  </div>
                )}
              </div>
            </>
          )}

          {isNew && (
            <div className="text-faint" style={{ fontSize: 11.5, marginTop: 14 }}>
              {window.L("💡 La restriction par agent sera disponible après la création du formulaire.", "💡 Per-agent restriction becomes available after the form is created.", "💡 La restricción por agente estará disponible tras la creación del formulario.")}
            </div>
          )}

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

// One row in the field-editor list
function FieldRow({ idx, field, lang, onPatch, onRemove, onUp, onDown, first, last }) {
  const inp = { width: "100%", padding: "5px 8px", borderRadius: 5, border: "1px solid var(--line)", fontSize: 12, background: "var(--bg, white)", color: "var(--text)", boxSizing: "border-box", fontFamily: "inherit" };
  // Type metadata for the help line (and any future type-specific hints)
  const typeMeta = FIELD_TYPES.find((tt) => tt.v === (field.type || "text"));
  const help = typeMeta && ((lang === "es" ? (typeMeta.helpEs != null ? typeMeta.helpEs : typeMeta.helpEn) : lang === "fr" ? typeMeta.helpFr : typeMeta.helpEn));
  return (
    <div style={{ border: "1px solid var(--line)", borderRadius: 6, padding: 10, background: "var(--bg-elev, white)" }}>
      <div style={{ display: "grid", gridTemplateColumns: "30px 1fr 1fr 1fr auto", gap: 8, alignItems: "center" }}>
        <div className="mono text-faint" style={{ fontSize: 11, textAlign: "center" }}>#{idx + 1}</div>
        <div>
          <input style={inp} value={field.k || ""} onChange={(e) => onPatch({ k: e.target.value })}
            placeholder={window.L("clé (ex: date_visit)", "key (e.g. date_visit)", "clave (ej.: date_visit)")} />
        </div>
        <div>
          <input style={inp} value={field.l || ""} onChange={(e) => onPatch({ l: e.target.value })}
            placeholder={window.L("Libellé affiché", "Display label", "Etiqueta mostrada")} />
        </div>
        <div>
          <select style={inp} value={field.type || "text"} onChange={(e) => {
            const newType = e.target.value;
            // Auto-suggest a label when the user picks a "typed" field
            // (project, activity, partners_multi, etc.) and hasn't typed a
            // custom label yet. We overwrite the label if it's empty OR
            // matches a "known default" (a FIELD_TYPES canonical name OR a
            // template-provided label like "Description"). Custom labels
            // the user actually typed are preserved intact.
            const newMeta = FIELD_TYPES.find((tt) => tt.v === newType);
            // Build the full default-labels set : every FIELD_TYPES.fr/en
            // PLUS the KNOWN_DEFAULT_LABELS set (template labels).
            const allDefaults = new Set(KNOWN_DEFAULT_LABELS);
            FIELD_TYPES.forEach((tt) => { if (tt.fr) allDefaults.add(tt.fr); if (tt.en) allDefaults.add(tt.en); });
            const currentLabel = (field.l || "").trim();
            const labelIsDefault = currentLabel === "" || allDefaults.has(currentLabel);
            const patch = { type: newType };
            if (labelIsDefault && newMeta) {
              patch.l = (lang === "es" ? (newMeta.es != null ? newMeta.es : newMeta.en) : lang === "fr" ? newMeta.fr : newMeta.en);
            }
            onPatch(patch);
          }}>
            {FIELD_TYPES.map((t) => <option key={t.v} value={t.v}>{(lang === "es" ? (t.es != null ? t.es : t.en) : lang === "fr" ? t.fr : t.en)}</option>)}
          </select>
        </div>
        <div className="row gap-xs">
          <button className="btn xs ghost" onClick={onUp} disabled={first} title={window.L("Monter", "Move up", "Subir")}>↑</button>
          <button className="btn xs ghost" onClick={onDown} disabled={last} title={window.L("Descendre", "Move down", "Bajar")}>↓</button>
          <button className="btn xs ghost" onClick={onRemove} title={window.L("Supprimer", "Remove", "Eliminar")}><Icon.x /></button>
        </div>
      </div>
      {/* One-liner describing what this field type becomes at saisie time —
          documentation for the form designer, not the agent. */}
      {help && (
        <div className="text-faint" style={{ fontSize: 10.5, marginTop: 4, paddingLeft: 38 }}>
          {help}
        </div>
      )}
      {field.type === "select" && (
        <OptionsEditor
          lang={lang}
          options={Array.isArray(field.options) ? field.options : []}
          onChange={(opts) => onPatch({ options: opts })}
        />
      )}
      {field.type === "indicator" && (
        <IndicatorFieldEditor
          lang={lang}
          indicatorId={field.indicator_id || ""}
          onChange={(id) => onPatch({ indicator_id: id })}
        />
      )}
      {/* Phase 6 (2026-05) : reuse the same picker for indicator_value.
          The picker's disaggregation preview is irrelevant for this type
          (single-value entry, no grid), but seeing the indicator's
          metadata helps the form designer pick the right one. */}
      {field.type === "indicator_value" && (
        <IndicatorFieldEditor
          lang={lang}
          indicatorId={field.indicator_id || ""}
          onChange={(id) => onPatch({ indicator_id: id })}
        />
      )}
      {/* The new E2 field types (project, activity, gps, partners_multi,
          indicators_multi, attachments, activity_status) don't need any
          extra per-field config — they're rendered with sensible defaults
          at saisie time. Just the help line above is enough. */}
    </div>
  );
}

// ── Indicator picker for the 'indicator' field type ────────────────────────
// Lets the form designer choose ONE indicator from the catalogue.  Below the
// picker we preview the disaggregation axes that will apply at saisie time
// so the designer can confirm the form will generate the right grid.
function IndicatorFieldEditor({ lang, indicatorId, onChange }) {
  // Live indicators across all projects the caller can see.
  const { data: indicators } = window.melr.useIndicators ? window.melr.useIndicators() : { data: [] };
  // Once an indicator is picked, fetch its definition + disaggregation
  // assignments so we can show the preview chips.
  const picked = (indicators || []).find((i) => i.id === indicatorId);
  const defCode = picked && (picked.definition_code || picked.defCode);
  const { data: definition } = window.melr.useDefinitionByCode(defCode);
  const { data: assigned, loading: axesLoading } = window.melr.useDefinitionDisaggregations(definition && definition.id);

  const sel = {
    width: "100%", 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", fontFamily: "inherit",
  };

  return (
    <div style={{ marginTop: 8, padding: 10, borderRadius: 6, background: "var(--bg-sunken, #f8fafc)", border: "1px dashed var(--line)" }}>
      <div style={{ fontSize: 10.5, color: "var(--text-faint)", textTransform: "uppercase", letterSpacing: "0.04em", fontWeight: 600, marginBottom: 5 }}>
        {window.L("Indicateur lie a ce champ", "Indicator linked to this field", "Indicador vinculado a este campo")}
      </div>
      <select value={indicatorId} onChange={(e) => onChange(e.target.value)} style={sel}>
        <option value="">{window.L("-- Choisir un indicateur --", "-- Pick an indicator --", "-- Elegir un indicador --")}</option>
        {(indicators || []).map((ind) => (
          <option key={ind.id} value={ind.id}>
            {ind.code} - {lang === "en" ? (ind.name_en || ind.name_fr) : ind.name_fr}
          </option>
        ))}
      </select>

      {/* Disaggregation preview */}
      {indicatorId && (
        <div style={{ marginTop: 8 }}>
          {axesLoading ? (
            <div className="text-faint" style={{ fontSize: 11 }}>
              {window.L("Chargement de la desagregation...", "Loading disaggregation...", "Cargando la desagregación...")}
            </div>
          ) : !assigned || assigned.length === 0 ? (
            <div className="text-faint" style={{ fontSize: 11, fontStyle: "italic" }}>
              {window.L("Aucune desagregation configuree pour cet indicateur. Le champ collectera une seule valeur.", "No disaggregation configured for this indicator. The field will collect a single value.", "No hay ninguna desagregación configurada para este indicador. El campo recogerá un único valor.")}
            </div>
          ) : (
            <div>
              <div style={{ fontSize: 11, color: "var(--text-muted)", marginBottom: 4 }}>
                {window.L("Grille de saisie generee :", "Generated entry grid:", "Cuadrícula de entrada generada:")}
              </div>
              <div style={{ display: "flex", gap: 5, flexWrap: "wrap" }}>
                {assigned.map((row, i) => {
                  const lbl = lang === "fr"
                    ? (row.axis && (row.axis.name_fr || row.axis.code))
                    : (row.axis && (row.axis.name_en || row.axis.name_fr || row.axis.code));
                  return (
                    <span key={i} style={{
                      fontSize: 11, fontWeight: 500,
                      padding: "2px 8px", borderRadius: 999,
                      background: "white", color: "oklch(0.36 0.10 195)",
                      border: "1px solid oklch(0.75 0.10 195)",
                    }}>{lbl}</span>
                  );
                })}
              </div>
              <div style={{ fontSize: 10.5, color: "var(--text-faint)", marginTop: 4, fontStyle: "italic" }}>
                {window.L("Lors de la saisie du formulaire, l'agent verra une grille N/D croisant ces axes.", "When the form is filled, the agent will see an N/D grid crossing these axes.", "Al rellenar el formulario, el agente verá una cuadrícula N/D que cruza estos ejes.")}
              </div>
            </div>
          )}
        </div>
      )}
    </div>
  );
}

// ─── Visual options editor for select fields ────────────────────────────
// Row-based: each option has its own value + label inputs, reorder arrows
// and delete. "Auto" mode: if the user only fills the label, the value is
// auto-slugified from it so the saved JSON is clean.
function OptionsEditor({ lang, options, onChange }) {
  const slug = (s) => (s || "").toString().toLowerCase()
    .normalize("NFD").replace(/[̀-ͯ]/g, "")
    .replace(/[^a-z0-9_]+/g, "_")
    .replace(/^_+|_+$/g, "").slice(0, 32);

  const setAt = (idx, patch) => {
    const next = options.map((o, i) => i === idx ? { ...o, ...patch } : o);
    onChange(next);
  };
  const onLabelChange = (idx, label) => {
    const cur = options[idx] || {};
    // If the value was empty OR was the auto-slug of the previous label,
    // keep it in sync with the new label. Otherwise leave the value alone.
    const prevAuto = slug(cur.label);
    const valueIsAuto = !cur.value || cur.value === prevAuto;
    setAt(idx, { label, value: valueIsAuto ? slug(label) : cur.value });
  };
  const onValueChange = (idx, value) => setAt(idx, { value });
  const onAdd = () => onChange([...options, { value: "", label: "" }]);
  const onRemove = (idx) => onChange(options.filter((_, i) => i !== idx));
  const onMove = (idx, delta) => {
    const next = options.slice();
    const newIdx = idx + delta;
    if (newIdx < 0 || newIdx >= next.length) return;
    const [item] = next.splice(idx, 1);
    next.splice(newIdx, 0, item);
    onChange(next);
  };

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

  return (
    <div style={{ marginTop: 8, padding: 8, background: "var(--bg-sunken, #f9fafb)", borderRadius: 5 }}>
      <div className="row" style={{ marginBottom: 6, alignItems: "center" }}>
        <span className="text-faint" style={{ fontSize: 10.5, textTransform: "uppercase", letterSpacing: "0.04em" }}>
          {window.L("Options du menu déroulant", "Dropdown options", "Opciones del menú desplegable")}
        </span>
        <span className="tag-mono" style={{ marginLeft: 6, fontSize: 10 }}>{options.length}</span>
        <div style={{ flex: 1 }} />
        <button className="btn xs" onClick={onAdd}><Icon.plus /> {window.L("Option", "Option", "Opción")}</button>
      </div>
      {options.length === 0 ? (
        <div className="text-faint" style={{ fontSize: 11, fontStyle: "italic", padding: "6px 4px" }}>
          {window.L("Aucune option. Cliquez « + Option » pour en ajouter une.", "No option yet. Click '+ Option' to add one.", "Ninguna opción. Haga clic en «+ Opción» para añadir una.")}
        </div>
      ) : (
        <div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
          {/* Column headers */}
          <div className="text-faint" style={{ display: "grid", gridTemplateColumns: "24px 1fr 1fr auto", gap: 6, fontSize: 9.5, textTransform: "uppercase", letterSpacing: "0.04em", padding: "0 2px" }}>
            <div></div>
            <div>{window.L("Libellé affiché", "Display label", "Etiqueta mostrada")}</div>
            <div>{window.L("Valeur stockée", "Stored value", "Valor almacenado")}</div>
            <div></div>
          </div>
          {options.map((o, idx) => (
            <div key={idx} style={{ display: "grid", gridTemplateColumns: "24px 1fr 1fr auto", gap: 6, alignItems: "center" }}>
              <div className="mono text-faint" style={{ fontSize: 10, textAlign: "center" }}>{idx + 1}</div>
              <input style={inp} value={o.label || ""} onChange={(e) => onLabelChange(idx, e.target.value)}
                placeholder={window.L("Oui, Non, Partiellement…", "Yes, No, Partial…", "Sí, No, Parcialmente…")} />
              <input style={{ ...inp, fontFamily: "ui-monospace, monospace", color: "var(--text-faint)" }}
                value={o.value || ""} onChange={(e) => onValueChange(idx, e.target.value)}
                placeholder="auto" />
              <div className="row gap-xs">
                <button className="btn xs ghost" onClick={() => onMove(idx, -1)} disabled={idx === 0}
                  title={window.L("Monter", "Move up", "Subir")} style={{ padding: "2px 4px" }}>↑</button>
                <button className="btn xs ghost" onClick={() => onMove(idx, +1)} disabled={idx === options.length - 1}
                  title={window.L("Descendre", "Move down", "Bajar")} style={{ padding: "2px 4px" }}>↓</button>
                <button className="btn xs ghost" onClick={() => onRemove(idx)}
                  title={window.L("Supprimer cette option", "Delete option", "Eliminar esta opción")} style={{ padding: "2px 4px" }}>
                  <Icon.x />
                </button>
              </div>
            </div>
          ))}
        </div>
      )}
      <div className="text-faint" style={{ fontSize: 10.5, marginTop: 6, lineHeight: 1.4 }}>
        {window.L("💡 La valeur stockée est générée automatiquement depuis le libellé. Modifiez-la manuellement uniquement si vous avez besoin d'un code précis (ex. clé d'export Excel).", "💡 The stored value is auto-derived from the label. Override it only when you need a specific code (e.g. Excel export key).", "💡 El valor almacenado se genera automáticamente a partir de la etiqueta. Modifíquelo manualmente solo si necesita un código concreto (ej. clave de exportación a Excel).")}
      </div>
    </div>
  );
}

window.FormBuilder = FormBuilder;
