/* global React */
const { useState: useStateM, useEffect: useEffectM } = React;

// ==================== MOBILE — FIELD COLLECTION ====================

const FORMS_LIB = [
  { id: "CPN-01", fr: "Visite consultation prénatale", en: "Antenatal care visit", color: "var(--accent)", icon: "heart", fields: 14, est: "4 min", proj: "P-241" },
  { id: "VAC-02", fr: "Séance vaccination PEV", en: "EPI vaccination session", color: "var(--green)", icon: "syringe", fields: 9, est: "3 min", proj: "P-188" },
  { id: "PAL-03", fr: "Cas paludisme suspecté", en: "Suspected malaria case", color: "var(--red)", icon: "alert", fields: 11, est: "5 min", proj: "P-156" },
  { id: "STK-04", fr: "Inventaire chaîne du froid", en: "Cold chain stock check", color: "var(--violet)", icon: "thermometer", fields: 8, est: "6 min", proj: "P-188" },
  { id: "NUT-05", fr: "Dépistage nutritionnel enfant", en: "Child nutrition screening", color: "var(--amber)", icon: "scale", fields: 10, est: "4 min", proj: "P-098" },
  { id: "SUP-06", fr: "Supervision agent communautaire", en: "Community worker supervision", color: "var(--text-muted)", icon: "users", fields: 12, est: "8 min", proj: "P-241" },
];

const QUEUE = [
  { id: "S-2841", form: "CPN-01", site: "CSCom Sévaré", agent: "Aminata Coulibaly", when: "il y a 4 min", ago_en: "4 min ago", status: "queued", size: "11 KB" },
  { id: "S-2840", form: "VAC-02", site: "Poste Banamba", agent: "Ousmane Traoré", when: "il y a 18 min", ago_en: "18 min ago", status: "queued", size: "7 KB" },
  { id: "S-2839", form: "PAL-03", site: "CSCom Niono", agent: "Aïssata Diallo", when: "il y a 32 min", ago_en: "32 min ago", status: "queued", size: "14 KB" },
  { id: "S-2838", form: "CPN-01", site: "CSCom Sévaré", agent: "Aminata Coulibaly", when: "il y a 41 min", ago_en: "41 min ago", status: "queued", size: "10 KB" },
  { id: "S-2837", form: "STK-04", site: "Dépôt Mopti", agent: "Modibo Keïta", when: "il y a 1 h", ago_en: "1 h ago", status: "syncing", size: "5 KB" },
  { id: "S-2836", form: "VAC-02", site: "Poste Banamba", agent: "Ousmane Traoré", when: "il y a 1 h", ago_en: "1 h ago", status: "synced", size: "8 KB" },
  { id: "S-2835", form: "PAL-03", site: "CSCom Niono", agent: "Aïssata Diallo", when: "il y a 2 h", ago_en: "2 h ago", status: "synced", size: "12 KB" },
  { id: "S-2834", form: "NUT-05", site: "École Bla-2", agent: "Halima Issoufou", when: "il y a 3 h", ago_en: "3 h ago", status: "review", size: "9 KB" },
];

const FIELD_AGENTS = [
  { n: "Aminata Coulibaly", site: "CSCom Sévaré · Mali", sub: "12 saisies aujourd'hui", sub_en: "12 entries today", on: true, q: 4 },
  { n: "Ousmane Traoré", site: "Poste Banamba · Mali", sub: "8 saisies · Wi-Fi", sub_en: "8 entries · Wi-Fi", on: true, q: 1 },
  { n: "Aïssata Diallo", site: "CSCom Niono · Mali", sub: "Hors-ligne · 2G faible", sub_en: "Offline · weak 2G", on: false, q: 9 },
  { n: "Modibo Keïta", site: "Dépôt Mopti · Mali", sub: "Synchronisé", sub_en: "Synced", on: true, q: 0 },
  { n: "Halima Issoufou", site: "École Bla-2 · Niger", sub: "Synchronisé · Wi-Fi", sub_en: "Synced · Wi-Fi", on: true, q: 0 },
  { n: "Mahamat Saleh", site: "Centre N'Djaména", sub: "Hors-ligne 4 h", sub_en: "Offline 4 h", on: false, q: 6 },
];

// MobileField is an offline-first simulator: it renders a mock phone
// frame with an in-memory queue meant to demonstrate sync behavior, not
// to be a real data entry app. window.melr.useForms() returns the live
// form definitions (seed-batch-5.sql adds CPN-01, VAX-01, STOCK-01 for
// P-001) — wire them in if/when the simulator becomes a real form
// runner. For now the fixture is kept.
function MobileField({ t, lang }) {
  const { data: liveForms } = window.melr.useForms();
  const { data: liveSubs, realtime: subRealtime } = window.melr.useFormSubmissions();
  const { data: liveSites } = window.melr.useSites();
  const localQueueSize = window.melr.useLocalQueueSize();
  const LiveBadge = window.melr.LiveBadge;
  const [screen, setScreen] = useStateM("home"); // home | form | review
  const [airplane, setAirplane] = useStateM(false);
  const [syncBusy, setSyncBusy] = useStateM(false);
  const [syncMsg, setSyncMsg]   = useStateM(null);

  // ─── Simulator state: real form submission flow ────────────────────
  const [activeFormId, setActiveFormId] = useStateM(null);
  const [entryData, setEntryData]       = useStateM({});
  const [entrySite, setEntrySite]       = useStateM("");
  const [entryGeo, setEntryGeo]         = useStateM(null);  // { lat, lng } | null
  const [submitting, setSubmitting]     = useStateM(false);
  const [submitToast, setSubmitToast]   = useStateM(null);

  const activeForm = (liveForms || []).find((f) => f.id === activeFormId) || null;

  const startForm = (formId) => {
    setActiveFormId(formId);
    setEntryData({});
    setEntrySite("");
    setEntryGeo(null);
    setScreen("form");
    // Best-effort GPS capture in the background (some browsers prompt)
    if (navigator.geolocation) {
      navigator.geolocation.getCurrentPosition(
        (pos) => setEntryGeo({ lat: pos.coords.latitude, lng: pos.coords.longitude, accuracy: pos.coords.accuracy }),
        () => {},
        { enableHighAccuracy: false, timeout: 4000 }
      );
    }
  };
  const setField = (key, value) => setEntryData((d) => ({ ...d, [key]: value }));
  const resetEntry = () => {
    setActiveFormId(null); setEntryData({}); setEntrySite(""); setEntryGeo(null);
    setScreen("home");
  };

  const onSubmitEntry = async () => {
    if (!activeForm) return;
    setSubmitting(true);
    try {
      const dataPayload = { ...entryData };
      if (entryGeo) dataPayload._geo = entryGeo;
      const result = await window.melr.submitForm({
        form_id: activeForm.id,
        site_id: entrySite || null,
        data: dataPayload,
        captured_at: new Date().toISOString(),
        device_id: typeof navigator !== "undefined" ? (navigator.userAgent || "").slice(0, 80) : null,
      });
      setSubmitToast({
        ok: true,
        local: !!result.local,
        msg: result.local
          ? (lang === "fr" ? "📥 Saisie mise en file locale — sera synchronisée au retour en ligne" : "📥 Saved locally — will sync when back online")
          : (lang === "fr" ? "✓ Saisie envoyée et enregistrée" : "✓ Submission saved"),
      });
      setTimeout(() => setSubmitToast(null), 4000);
      resetEntry();
    } catch (e) {
      setSubmitToast({ ok: false, msg: e.message });
      setTimeout(() => setSubmitToast(null), 5000);
    } finally { setSubmitting(false); }
  };

  // Detect real device online status (independent from the "airplane mode"
  // toggle that drives the phone simulator).
  const [online, setOnline] = useStateM(typeof navigator !== "undefined" ? navigator.onLine : true);
  useEffectM(() => {
    const on  = () => setOnline(true);
    const off = () => setOnline(false);
    window.addEventListener("online", on);
    window.addEventListener("offline", off);
    return () => { window.removeEventListener("online", on); window.removeEventListener("offline", off); };
  }, []);

  // Auto-flush the local offline queue when we come back online.
  useEffectM(() => {
    if (online && localQueueSize > 0) {
      window.melr.flushLocalQueue().catch(() => {});
    }
  }, [online, localQueueSize]);

  // PWA install: show the button on Android/Chrome when beforeinstallprompt
  // fired, OR on iOS Safari (which uses Share → Add to Home Screen).
  // Hidden if the app is already running as installed PWA.
  const [installAvailable, setInstallAvailable] = useStateM(false);
  useEffectM(() => {
    const check = () => {
      const installed = window.melr.isStandalonePwa && window.melr.isStandalonePwa();
      if (installed) { setInstallAvailable(false); return; }
      const can = (window.melr.canInstallPwa && window.melr.canInstallPwa())
              || (window.melr.isIos && window.melr.isIos());
      setInstallAvailable(can);
    };
    check();
    window.addEventListener("appinstalled", check);
    // beforeinstallprompt may fire after first render
    const interval = setInterval(check, 2000);
    return () => { window.removeEventListener("appinstalled", check); clearInterval(interval); };
  }, []);
  const onInstallApp = async () => {
    if (!window.melr.promptInstallPwa) return;
    const r = await window.melr.promptInstallPwa();
    if (r && r.outcome === "accepted") setInstallAvailable(false);
  };

  // Decide which dataset drives the right-side cards.
  const useLive = liveSubs && liveSubs.length > 0;
  // Map live submissions to the legacy row shape used by the queue table.
  const liveMapped = (liveSubs || []).map((s) => ({
    id: "S-" + String(s.id).slice(0, 6).toUpperCase(),
    form: (s.forms && s.forms.code) || "—",
    site: (s.sites && (s.sites.code + " " + s.sites.name)) || "—",
    agent: (s.agent && (s.agent.full_name || s.agent.email)) || "—",
    when: new Date(s.uploaded_at).toLocaleString(lang === "fr" ? "fr-FR" : "en-US"),
    ago_en: new Date(s.uploaded_at).toLocaleString("en-US"),
    status: s.state,
    size: s.size_bytes ? Math.max(1, Math.round(s.size_bytes / 1024)) + " KB" : "—",
    raw: s,
  }));
  const QUEUE_VIEW = useLive ? liveMapped : QUEUE;
  const queued  = QUEUE_VIEW.filter((q) => q.status === "queued").length + localQueueSize;
  const syncing = QUEUE_VIEW.filter((q) => q.status === "syncing").length;
  const synced  = QUEUE_VIEW.filter((q) => q.status === "synced").length;

  const onForceSync = async () => {
    setSyncBusy(true); setSyncMsg(null);
    try {
      const r = await window.melr.flushLocalQueue();
      const errs = r.errors || [];
      let msg;
      if (errs.length > 0) {
        msg = lang === "fr"
          ? (r.synced + " envoyée(s) · " + r.remaining + " bloquée(s) : " + errs[0].message)
          : (r.synced + " sent · " + r.remaining + " blocked: " + errs[0].message);
      } else {
        msg = lang === "fr"
          ? (r.synced + " soumission(s) envoyée(s) · " + r.remaining + " en attente.")
          : (r.synced + " submission(s) synced · " + r.remaining + " pending.");
      }
      setSyncMsg(msg);
      setTimeout(() => setSyncMsg(null), 6000);
    } catch (e) { setSyncMsg(e.message); }
    finally { setSyncBusy(false); }
  };
  // Live forms banner (compact). The Android simulator below stays on
  // the rich fixture FORMS_LIB until a real form runner is built.
  //
  // STICKY: the user scrolls through the long mobile-app preview below;
  // this banner pins to the top so the active form list stays visible
  // while inspecting the simulator / sync queue / agent list. Same
  // sticky pattern as the OrgRoles "acting-as-org" indicator —
  // backdrop blur + drop shadow so it reads cleanly over scrolled
  // content. z-index 20 sits above the page header (z-index 4).
  const liveBanner = liveForms && liveForms.length > 0 ? (
    <div className="card" style={{
      marginBottom: 14,
      background: "rgba(236, 253, 245, 0.96)",
      borderColor: "#10b981",
      position: "sticky",
      top: 0,
      zIndex: 20,
      boxShadow: "0 6px 16px rgba(6, 95, 70, 0.12), 0 2px 4px rgba(6, 95, 70, 0.08)",
      backdropFilter: "saturate(180%) blur(10px)",
      WebkitBackdropFilter: "saturate(180%) blur(10px)",
    }}>
      <div className="card-head" style={{ alignItems: "center", gap: 8 }}>
        <span style={{ width: 8, height: 8, borderRadius: "50%", background: "#10b981" }}></span>
        <div className="card-title" style={{ color: "#065f46" }}>
          {lang === "fr" ? "Formulaires actifs en base" : "Live forms"}
        </div>
        <span className="tag-mono" style={{ marginLeft: "auto" }}>{liveForms.length}</span>
      </div>
      <div className="card-body" style={{ display: "flex", gap: 8, flexWrap: "wrap", paddingTop: 4 }}>
        {liveForms.map((f) => (
          <span key={f.id} className="pill" style={{ background: "white", borderColor: "#10b981", color: "#065f46" }}>
            <span className="tag-mono">{f.code}</span>
            <span style={{ marginLeft: 6 }}>{lang === "en" ? (f.name_en || f.name_fr) : f.name_fr}</span>
            {f.projects && f.projects.code && (
              <span className="tag-mono" style={{ marginLeft: 6, fontSize: 10, opacity: 0.7 }}>{f.projects.code}</span>
            )}
          </span>
        ))}
      </div>
    </div>
  ) : null;

  return (
    <div className="page">
      <div className="page-header">
        <div className="page-eyebrow">{lang === "fr" ? "COLLECTE DE DONNÉES · ANDROID & IOS" : "DATA COLLECTION · ANDROID & IOS"}</div>
        <div className="page-header-row">
          <div>
            <h1 className="page-title">
              {lang === "fr" ? "Application mobile MELR" : "MELR mobile app"}
              {" "}<LiveBadge on={subRealtime} lang={lang} />
            </h1>
            <div className="page-sub">
              {lang === "fr"
                ? "Saisie hors-ligne · synchronisation différée · installable sur Android & iOS (PWA)."
                : "Offline data entry · deferred sync · installable on Android & iOS (PWA)."}
            </div>
          </div>
          <div className="page-header-actions">
            <span className={"pill " + (online ? "green dot" : "red dot")} title={online ? "" : (lang === "fr" ? "Mode hors-ligne" : "Offline mode")}
              style={{ marginRight: 6 }}>
              {online
                ? (lang === "fr" ? "En ligne" : "Online")
                : (lang === "fr" ? "Hors-ligne" : "Offline")}
            </span>
            {installAvailable && (
              <button className="btn sm" onClick={onInstallApp}
                title={window.melr.isIos && window.melr.isIos()
                  ? (lang === "fr" ? "Voir les instructions pour iOS" : "Show iOS instructions")
                  : (lang === "fr" ? "Installer l'application sur cet appareil" : "Install the app on this device")}>
                <Icon.download /> {lang === "fr" ? "Installer l'app" : "Install app"}
              </button>
            )}
            <button className="btn sm" onClick={onForceSync} disabled={syncBusy || !online}>
              <Icon.refresh /> {syncBusy ? "…" : (lang === "fr" ? "Synchroniser" : "Sync now")}
              {localQueueSize > 0 && <span className="tag-mono" style={{ marginLeft: 4 }}>{localQueueSize}</span>}
            </button>
            <button className="btn sm primary"
              onClick={() => {
                // Clear the "prefer desktop" flag so future loads on this device
                // route to mobile, and navigate to the mobile surface now.
                if (window.melr && window.melr.preferMobile) window.melr.preferMobile();
                const url = new URL(window.location.href);
                url.searchParams.set("mobile", "1");
                window.location.assign(url.toString());
              }}
              title={lang === "fr" ? "Basculer sur la surface de saisie plein écran" : "Switch to the full-screen entry surface"}>
              <Icon.smartphone /> {lang === "fr" ? "Mode mobile" : "Mobile mode"}
            </button>
          </div>
        </div>
        {syncMsg && (
          <div style={{ marginTop: 8, padding: "6px 10px", background: "#dbeafe", color: "#1e40af", borderRadius: 4, fontSize: 12.5 }}>
            {syncMsg}
          </div>
        )}
        {!online && localQueueSize > 0 && (
          <div style={{ marginTop: 8, padding: "6px 10px", background: "#fef3c7", color: "#92400e", borderRadius: 4, fontSize: 12.5 }}>
            {lang === "fr"
              ? "🌐 Mode hors-ligne · " + localQueueSize + " soumission(s) en attente. La synchronisation reprendra au retour en ligne."
              : "🌐 Offline mode · " + localQueueSize + " submission(s) queued locally. Sync resumes when back online."}
          </div>
        )}
      </div>

      {liveBanner}

      <div className="mobile-kpis">
        {(() => {
          // Today's submissions (live)
          const today = new Date(); today.setHours(0, 0, 0, 0);
          const todayCount = (liveSubs || []).filter((s) => new Date(s.uploaded_at) >= today).length;
          // Unique agents who submitted in the last 7 days
          const sevenDays = new Date(Date.now() - 7 * 86400000);
          const recentAgents = new Set((liveSubs || []).filter((s) => new Date(s.uploaded_at) >= sevenDays && s.agent_id).map((s) => s.agent_id));
          return [
            { l: lang === "fr" ? "Agents actifs (7j)" : "Active agents (7d)", v: String(recentAgents.size || FIELD_AGENTS.length),
              s: lang === "fr" ? "ayant soumis cette semaine" : "submitted this week" },
            { l: lang === "fr" ? "Saisies aujourd'hui" : "Entries today",
              v: useLive ? String(todayCount) : "284",
              s: useLive ? (lang === "fr" ? "depuis la base de données" : "from the database") : ("+18% " + (lang === "fr" ? "vs hier" : "vs yesterday")) },
            { l: lang === "fr" ? "File de synchro" : "Sync queue",
              v: String(queued + syncing),
              s: localQueueSize > 0
                  ? (localQueueSize + " " + (lang === "fr" ? "en local · le reste sur le serveur" : "local · rest on server"))
                  : (lang === "fr" ? "soumissions en attente" : "submissions pending") },
            { l: lang === "fr" ? "Formulaires actifs" : "Active forms",
              v: String((liveForms && liveForms.length) || FORMS_LIB.length),
              s: lang === "fr" ? "publiés sur mobile" : "published on mobile" },
          ];
        })().map((k, i) => (
          <div key={i} className="kpi">
            <div className="kpi-label">{k.l}</div>
            <div className="kpi-value">{k.v}</div>
            <div className="kpi-sub">{k.s}</div>
          </div>
        ))}
      </div>

      <div className="mobile-layout">
        {/* LEFT — phone preview */}
        <div className="mobile-preview-col">
          <div className="mobile-preview-tools">
            <div className="seg sm">
              <button className={"seg-btn" + (screen === "home" ? " active" : "")} onClick={() => setScreen("home")}>
                <Icon.home /> {lang === "fr" ? "Accueil agent" : "Agent home"}
              </button>
              <button className={"seg-btn" + (screen === "form" ? " active" : "")} onClick={() => setScreen("form")}>
                <Icon.fileText /> {lang === "fr" ? "Formulaire" : "Form"}
              </button>
              <button className={"seg-btn" + (screen === "review" ? " active" : "")} onClick={() => setScreen("review")}>
                <Icon.check /> {lang === "fr" ? "Récap" : "Review"}
              </button>
              <button className={"seg-btn" + (screen === "submissions" ? " active" : "")} onClick={() => setScreen("submissions")}>
                <Icon.fileText /> {lang === "fr" ? "Mes saisies" : "My submissions"}
              </button>
            </div>
            <button className={"btn sm" + (airplane ? " accent" : "")} onClick={() => setAirplane(!airplane)}>
              <Icon.plane /> {airplane ? (lang === "fr" ? "Hors-ligne" : "Offline") : (lang === "fr" ? "En ligne" : "Online")}
            </button>
          </div>

          <div className="mobile-stage">
            <AndroidDevice width={368} height={760}>
              {screen === "home" && (
                <PhoneHome lang={lang} airplane={airplane}
                  forms={liveForms || []} fixtureForms={FORMS_LIB}
                  todayCount={(liveSubs || []).filter((s) => {
                    const t = new Date(); t.setHours(0, 0, 0, 0);
                    return new Date(s.uploaded_at) >= t;
                  }).length}
                  submissionsCount={(liveSubs || []).length + localQueueSize}
                  onOpenSubmissions={() => setScreen("submissions")}
                  onPickForm={startForm} />
              )}
              {screen === "form" && (
                <PhoneForm lang={lang} airplane={airplane}
                  form={activeForm} sites={liveSites || []}
                  entryData={entryData} setField={setField}
                  entrySite={entrySite} setEntrySite={setEntrySite}
                  entryGeo={entryGeo}
                  onBack={() => setScreen("home")}
                  onContinue={() => setScreen("review")} />
              )}
              {screen === "submissions" && (
                <PhoneSubmissions lang={lang}
                  serverSubs={liveSubs || []}
                  localQueue={window.melr.readLocalQueue ? window.melr.readLocalQueue() : []}
                  forms={liveForms || []}
                  sites={liveSites || []}
                  onBack={() => setScreen("home")} />
              )}
              {screen === "review" && (
                <PhoneReview lang={lang} airplane={airplane}
                  form={activeForm} sites={liveSites || []}
                  entryData={entryData} entrySite={entrySite} entryGeo={entryGeo}
                  submitting={submitting}
                  onBack={() => setScreen("form")}
                  onSubmit={onSubmitEntry} />
              )}
            </AndroidDevice>
            {submitToast && (
              <div style={{
                position: "absolute", left: "50%", bottom: 32,
                transform: "translateX(-50%)",
                padding: "10px 18px", borderRadius: 8,
                background: submitToast.ok ? (submitToast.local ? "#fef3c7" : "#dcfce7") : "#fee2e2",
                color: submitToast.ok ? (submitToast.local ? "#92400e" : "#166534") : "#991b1b",
                border: "1px solid " + (submitToast.ok ? (submitToast.local ? "#fcd34d" : "#86efac") : "#fca5a5"),
                fontSize: 13, fontWeight: 500,
                boxShadow: "0 6px 18px rgba(0,0,0,.18)", zIndex: 50,
              }}>{submitToast.msg}</div>
            )}
            <div className="mobile-caption">
              <Icon.smartphone />
              <span>{lang === "fr" ? "Android 11+ · iOS 14+ · 18 Mo" : "Android 11+ · iOS 14+ · 18 MB"}</span>
            </div>
          </div>
        </div>

        {/* RIGHT — context panels */}
        <div className="mobile-side-col">
          <div className="card">
            <div className="card-head">
              <div className="card-title">{lang === "fr" ? "File de synchronisation" : "Sync queue"}</div>
              <span className="pill amber dot">{queued} {lang === "fr" ? "en attente" : "pending"}</span>
              <span className="text-faint" style={{ marginLeft: 8, fontSize: 11 }}>
                {synced} {lang === "fr" ? "synchronisés" : "synced"}
                {!useLive && " · " + (lang === "fr" ? "données démo" : "demo data")}
              </span>
              <button className="btn sm" onClick={onForceSync} disabled={syncBusy || !online}
                style={{ marginLeft: "auto" }}>
                <Icon.refresh /> {lang === "fr" ? "Forcer la synchro" : "Force sync"}
              </button>
            </div>
            <div className="card-body flush">
              <table className="tbl">
                <thead>
                  <tr>
                    <th>ID</th>
                    <th>{lang === "fr" ? "Formulaire" : "Form"}</th>
                    <th>{lang === "fr" ? "Site / Agent" : "Site / Agent"}</th>
                    <th>{lang === "fr" ? "Capté" : "Captured"}</th>
                    <th className="num">{lang === "fr" ? "Taille" : "Size"}</th>
                    <th>{lang === "fr" ? "État" : "State"}</th>
                  </tr>
                </thead>
                <tbody>
                  {QUEUE_VIEW.length === 0 && (
                    <tr><td colSpan={6} style={{ padding: 22, textAlign: "center", color: "var(--text-faint)" }}>
                      {lang === "fr"
                        ? "Aucune soumission. Les saisies des agents apparaîtront ici en temps réel."
                        : "No submission yet. Field-agent entries will show up here in real time."}
                    </td></tr>
                  )}
                  {QUEUE_VIEW.map((q) => (
                    <tr key={q.id}>
                      <td className="mono text-faint">{q.id}</td>
                      <td className="mono">{q.form}</td>
                      <td><div className="strong">{q.site}</div><div className="text-faint" style={{ fontSize: 11 }}>{q.agent}</div></td>
                      <td className="muted">{lang === "fr" ? q.when : q.ago_en}</td>
                      <td className="num mono">{q.size}</td>
                      <td>
                        {q.status === "queued"  && <span className="pill amber dot">{lang === "fr" ? "En file" : "Queued"}</span>}
                        {q.status === "syncing" && <span className="pill accent dot syncing">{lang === "fr" ? "Synchro…" : "Syncing…"}</span>}
                        {q.status === "synced"  && <span className="pill green dot">{lang === "fr" ? "Synchronisé" : "Synced"}</span>}
                        {q.status === "review"  && <span className="pill violet dot">{lang === "fr" ? "À valider" : "Review"}</span>}
                      </td>
                    </tr>
                  ))}
                </tbody>
              </table>
            </div>
          </div>

          <div className="grid cols-2" style={{ marginTop: 16 }}>
            <div className="card">
              <div className="card-head">
                <div className="card-title">{lang === "fr" ? "Formulaires publiés" : "Published forms"}</div>
                <span className="tag-mono">{FORMS_LIB.length}</span>
              </div>
              <div className="card-body flush">
                {FORMS_LIB.map((f) => (
                  <div key={f.id} className="form-row">
                    <div className="form-mark" style={{ background: f.color }}>{f.id.slice(0, 3)}</div>
                    <div style={{ flex: 1 }}>
                      <div className="strong">{lang === "fr" ? f.fr : f.en}</div>
                      <div className="text-faint" style={{ fontSize: 11 }}>
                        <span className="mono">{f.id}</span> · {f.fields} {lang === "fr" ? "champs" : "fields"} · ≈ {f.est} · {f.proj}
                      </div>
                    </div>
                    <Icon.chevronRight className="sm muted" />
                  </div>
                ))}
              </div>
            </div>

            <div className="card">
              <div className="card-head">
                <div className="card-title">{lang === "fr" ? "Agents terrain" : "Field agents"}</div>
                <span className="tag-mono">{FIELD_AGENTS.length}</span>
              </div>
              <div className="card-body flush">
                {FIELD_AGENTS.map((a, i) => (
                  <div key={i} className="agent-row">
                    <div className="agent-av" style={{ background: avColor(a.n) }}>
                      {initials(a.n)}
                      <span className={"presence " + (a.on ? "on" : "off")}></span>
                    </div>
                    <div style={{ flex: 1, minWidth: 0 }}>
                      <div className="strong">{a.n}</div>
                      <div className="text-faint" style={{ fontSize: 11 }}>{a.site}</div>
                      <div className="text-faint" style={{ fontSize: 11 }}>{lang === "fr" ? a.sub : a.sub_en}</div>
                    </div>
                    {a.q > 0 && <span className="pill amber dot">{a.q}</span>}
                  </div>
                ))}
              </div>
            </div>
          </div>

          <div className="card" style={{ marginTop: 16 }}>
            <div className="card-head">
              <div className="card-title">{lang === "fr" ? "Capacités hors-ligne" : "Offline capabilities"}</div>
            </div>
            <div className="card-body" style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 14 }}>
              {[
                { i: "wifiOff", t: lang === "fr" ? "Saisie 100% hors-ligne" : "100% offline capture", s: lang === "fr" ? "SQLite chiffré local · 28 jours d'autonomie" : "Encrypted local SQLite · 28 days autonomy" },
                { i: "refresh", t: lang === "fr" ? "Synchronisation différentielle" : "Differential sync", s: lang === "fr" ? "Reprend là où ça s'est arrêté · compression gzip" : "Resumable · gzip compression" },
                { i: "shield", t: lang === "fr" ? "Sécurité" : "Security", s: lang === "fr" ? "Chiffrement AES-256 · code PIN · effacement à distance" : "AES-256 · PIN · remote wipe" },
                { i: "map", t: lang === "fr" ? "Géolocalisation" : "Geolocation", s: lang === "fr" ? "GPS rattaché au site · cartes hors-ligne" : "GPS attached to site · offline maps" },
                { i: "camera", t: lang === "fr" ? "Photo & signature" : "Photo & signature", s: lang === "fr" ? "Compression intelligente · stockage diff\u00e9r\u00e9" : "Smart compression · deferred upload" },
                { i: "globe", t: lang === "fr" ? "Multilingue" : "Multilingual", s: lang === "fr" ? "FR · EN · Bambara · Wolof · Haoussa" : "FR · EN · Bambara · Wolof · Hausa" },
              ].map((c, i) => {
                const Ic = Icon[c.i] || Icon.check;
                return (
                  <div key={i} className="capability">
                    <div className="cap-icon"><Ic /></div>
                    <div>
                      <div className="strong">{c.t}</div>
                      <div className="text-faint" style={{ fontSize: 11.5 }}>{c.s}</div>
                    </div>
                  </div>
                );
              })}
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

// ==================== PHONE SCREENS ====================

function PhoneStatus({ lang, airplane }) {
  return (
    <div className="ph-status">
      <div className="ph-status-row">
        {airplane ? <Icon.wifiOff /> : <Icon.wifi />}
        <span>{airplane ? (lang === "fr" ? "Hors-ligne" : "Offline") : (lang === "fr" ? "Wi-Fi · synchronisé" : "Wi-Fi · synced")}</span>
        <span className="ph-status-sep">·</span>
        <Icon.battery />
        <span>78%</span>
      </div>
      {airplane && (
        <div className="ph-offline-banner">
          <Icon.cloudOff />
          <div style={{ flex: 1 }}>
            <div className="strong">{lang === "fr" ? "Mode hors-ligne" : "Offline mode"}</div>
            <div className="ph-sub">{lang === "fr" ? "4 saisies seront envoyées au retour du réseau" : "4 entries will sync when network resumes"}</div>
          </div>
          <span className="ph-pill amber">4</span>
        </div>
      )}
    </div>
  );
}

function PhoneHome({ lang, airplane, forms, fixtureForms, todayCount, onPickForm, onOpenSubmissions, submissionsCount }) {
  // Current user — shown under "Bonjour" so the agent can confirm at a
  // glance which account is signed in (useful when several team members
  // share a device).
  const { profile: meProfile } = window.melr.useCurrentProfile();
  // Prefer real forms; if none, show the fixture set as a visual demo
  // but clicking them still works (creates a synthetic submission with
  // the fixture as fake form_id — we just disable that case here).
  const useLive = (forms || []).length > 0;
  const list = useLive ? forms : [];
  const fxList = !useLive ? fixtureForms.slice(0, 4) : [];

  // Build the project filter from the forms the user can actually see (RLS
  // already scoped them by project_agents). Persist the last pick locally
  // so re-entering the home screen keeps the agent on their project.
  const PROJ_FILTER_KEY = "melr.mobile.project-filter";
  const projectsFromForms = React.useMemo(() => {
    const map = new Map();
    list.forEach((f) => {
      if (f.projects && f.projects.code) map.set(f.projects.code, f.projects);
    });
    return Array.from(map.values()).sort((a, b) => (a.code || "").localeCompare(b.code || ""));
  }, [list]);
  const [projectFilter, setProjectFilter] = useStateM(() => {
    try { return localStorage.getItem(PROJ_FILTER_KEY) || ""; } catch (_) { return ""; }
  });
  React.useEffect(() => {
    // If only one project is available, auto-select it. Otherwise reset
    // the filter when the persisted project is no longer in the visible list.
    if (projectsFromForms.length === 1) {
      const only = projectsFromForms[0].code;
      if (projectFilter !== only) setProjectFilter(only);
    } else if (projectFilter && !projectsFromForms.some((p) => p.code === projectFilter)) {
      setProjectFilter("");
    }
  }, [projectsFromForms]);
  React.useEffect(() => {
    try { localStorage.setItem(PROJ_FILTER_KEY, projectFilter || ""); } catch (_) {}
  }, [projectFilter]);

  const filteredList = projectFilter
    ? list.filter((f) => f.projects && f.projects.code === projectFilter)
    : list;
  const showFilter = projectsFromForms.length > 1;

  return (
    <div className="phone">
      <div className="ph-greeting">
        <div className="ph-av" style={{ background: avColor("MELR") }}>ME</div>
        <div style={{ flex: 1, minWidth: 0 }}>
          <div className="ph-hello">{lang === "fr" ? "Bonjour" : "Hello"}</div>
          {meProfile && (meProfile.full_name || meProfile.email) && (
            <div className="ph-sub" style={{ fontSize: 11.5, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}
              title={(meProfile.full_name || meProfile.email) + (meProfile.email ? " — " + meProfile.email : "")}>
              <span className="strong" style={{ color: "var(--text)" }}>{meProfile.full_name || meProfile.email}</span>
              {meProfile.full_name && meProfile.email && (
                <span style={{ opacity: 0.7 }}> ({meProfile.email})</span>
              )}
            </div>
          )}
          <div className="ph-sub">{new Date().toLocaleDateString(lang === "fr" ? "fr-FR" : "en-US", { weekday: "long", month: "short", day: "numeric" })}</div>
        </div>
        <button className="ph-iconbtn" onClick={onOpenSubmissions}
          title={lang === "fr" ? "Mes saisies" : "My submissions"}
          style={{ position: "relative" }}>
          <Icon.fileText />
          {submissionsCount > 0 && (
            <span style={{
              position: "absolute", top: -3, right: -3,
              background: "var(--accent)", color: "white",
              fontSize: 9, fontWeight: 700, borderRadius: 999,
              padding: "1px 5px", minWidth: 14, textAlign: "center", lineHeight: 1.2,
            }}>{submissionsCount > 99 ? "99+" : submissionsCount}</span>
          )}
        </button>
      </div>

      <PhoneStatus lang={lang} airplane={airplane} />

      <div className="ph-section-h">{lang === "fr" ? "Aujourd'hui" : "Today"}</div>
      <div className="ph-stats">
        <div className="ph-stat"><div className="ph-stat-v">{todayCount}</div><div className="ph-stat-l">{lang === "fr" ? "saisies" : "entries"}</div></div>
        <div className="ph-stat"><div className="ph-stat-v">{filteredList.length || fxList.length}</div><div className="ph-stat-l">{lang === "fr" ? "form." : "forms"}</div></div>
      </div>

      {showFilter && (
        <div style={{ padding: "0 4px 4px" }}>
          <div className="ph-section-h" style={{ marginBottom: 4 }}>
            {lang === "fr" ? "Projet" : "Project"}
          </div>
          <select value={projectFilter} onChange={(e) => setProjectFilter(e.target.value)}
            style={{ width: "100%", padding: "8px 10px", border: "1px solid var(--border)", borderRadius: 6, background: "white", fontSize: 13 }}>
            <option value="">{lang === "fr" ? "Tous les projets" : "All projects"} ({list.length})</option>
            {projectsFromForms.map((p) => {
              const count = list.filter((f) => f.projects && f.projects.code === p.code).length;
              const name = lang === "en" ? (p.name_en || p.name_fr || "") : (p.name_fr || "");
              const label = name && name !== p.code ? (p.code + " · " + name) : p.code;
              return <option key={p.code} value={p.code}>{label} ({count})</option>;
            })}
          </select>
        </div>
      )}

      <div className="ph-section-h">{lang === "fr" ? "Formulaires disponibles" : "Available forms"}</div>
      {!useLive && (
        <div style={{ padding: "8px 10px", background: "#fef3c7", color: "#92400e", borderRadius: 6, fontSize: 11.5, marginBottom: 6 }}>
          {lang === "fr"
            ? "Aucun formulaire actif en base de données. Liste démo ci-dessous."
            : "No active form in the database. Demo list below."}
        </div>
      )}
      {useLive && filteredList.length === 0 && (
        <div style={{ padding: "8px 10px", background: "#fef3c7", color: "#92400e", borderRadius: 6, fontSize: 11.5, marginBottom: 6 }}>
          {lang === "fr"
            ? "Aucun formulaire pour ce projet. Choisissez « Tous les projets » ou contactez votre admin pour être affecté."
            : "No form for this project. Pick 'All projects' or ask your admin to assign you."}
        </div>
      )}
      {useLive && filteredList.map((f) => (
        <button key={f.id} className="ph-form" onClick={() => onPickForm && onPickForm(f.id)}>
          <div className="ph-form-mark" style={{ background: "var(--accent)" }}>{(f.code || "").slice(0, 3) || "F"}</div>
          <div style={{ flex: 1, textAlign: "left" }}>
            <div className="strong" style={{ fontSize: 14 }}>{lang === "en" ? (f.name_en || f.name_fr) : f.name_fr}</div>
            <div className="ph-sub mono">{f.code}{f.version ? " · " + f.version : ""}{f.projects && f.projects.code ? " · " + f.projects.code : ""}</div>
          </div>
          <Icon.chevronRight className="sm muted" />
        </button>
      ))}
      {!useLive && fxList.map((f) => (
        <button key={f.id} className="ph-form" disabled style={{ opacity: 0.6 }}
          title={lang === "fr" ? "Formulaire démo (non opérationnel)" : "Demo form (not operational)"}>
          <div className="ph-form-mark" style={{ background: f.color }}>{f.id.slice(0, 3)}</div>
          <div style={{ flex: 1, textAlign: "left" }}>
            <div className="strong" style={{ fontSize: 14 }}>{lang === "fr" ? f.fr : f.en}</div>
            <div className="ph-sub">{f.fields} {lang === "fr" ? "champs · ≈" : "fields · ≈"} {f.est} · {lang === "fr" ? "démo" : "demo"}</div>
          </div>
        </button>
      ))}
    </div>
  );
}

// ─── Photo field — direct camera capture with thumbnail preview ──────────
function PhotoField({ lang, value, onChange }) {
  const fileInputRef = React.useRef(null);
  const onFile = (e) => {
    const file = e.target.files && e.target.files[0];
    if (!file) return;
    const reader = new FileReader();
    reader.onload = () => {
      onChange({
        _pendingUpload: true,
        dataUrl: reader.result,
        name: file.name,
        mime: file.type,
        size: file.size,
      });
    };
    reader.readAsDataURL(file);
  };
  const clear = () => onChange(null);
  const preview = value && value.dataUrl ? value.dataUrl : null;
  return (
    <div>
      <input ref={fileInputRef} type="file" accept="image/*" capture="environment"
        style={{ display: "none" }} onChange={onFile} />
      {preview ? (
        <div style={{ position: "relative", borderRadius: 8, overflow: "hidden", border: "1px solid var(--border)" }}>
          <img src={preview} alt="" style={{ display: "block", width: "100%", maxHeight: 220, objectFit: "cover" }} />
          <div style={{ display: "flex", gap: 6, padding: 6, background: "rgba(0,0,0,.04)" }}>
            <button className="ph-btn ghost" style={{ flex: 1, fontSize: 12, padding: "6px 8px" }}
              onClick={() => fileInputRef.current && fileInputRef.current.click()}>
              <Icon.camera /> {lang === "fr" ? "Reprendre" : "Retake"}
            </button>
            <button className="ph-btn ghost" style={{ flex: 1, fontSize: 12, padding: "6px 8px" }} onClick={clear}>
              <Icon.x /> {lang === "fr" ? "Effacer" : "Clear"}
            </button>
          </div>
          <div style={{ padding: "2px 8px 6px", fontSize: 10, color: "var(--text-faint)" }}>
            {value.name} · {value.size ? Math.round(value.size / 1024) + " KB" : ""}
          </div>
        </div>
      ) : (
        <button className="ph-btn ghost" style={{ width: "100%", justifyContent: "center", padding: 12 }}
          onClick={() => fileInputRef.current && fileInputRef.current.click()}>
          <Icon.camera /> {lang === "fr" ? "Prendre une photo" : "Take a photo"}
        </button>
      )}
    </div>
  );
}

// ─── Signature pad — drawable canvas, exports to PNG data URL ────────────
function SignaturePad({ lang, value, onChange }) {
  const canvasRef = React.useRef(null);
  const drawingRef = React.useRef(false);
  const lastRef = React.useRef(null);
  // Lazy-init the canvas on mount; load existing signature if present.
  React.useEffect(() => {
    const c = canvasRef.current;
    if (!c) return;
    const dpr = window.devicePixelRatio || 1;
    const rect = c.getBoundingClientRect();
    c.width  = Math.round(rect.width  * dpr);
    c.height = Math.round(rect.height * dpr);
    const ctx = c.getContext("2d");
    ctx.scale(dpr, dpr);
    ctx.lineCap = "round"; ctx.lineJoin = "round"; ctx.lineWidth = 2; ctx.strokeStyle = "#0f172a";
    // Restore preview if a signature was already saved
    if (value && value.dataUrl) {
      const img = new Image();
      img.onload = () => ctx.drawImage(img, 0, 0, rect.width, rect.height);
      img.src = value.dataUrl;
    }
  }, []);
  const ptFromEvent = (e) => {
    const c = canvasRef.current;
    const rect = c.getBoundingClientRect();
    const t = e.touches ? e.touches[0] : e;
    return { x: t.clientX - rect.left, y: t.clientY - rect.top };
  };
  const start = (e) => {
    e.preventDefault();
    drawingRef.current = true;
    lastRef.current = ptFromEvent(e);
  };
  const move = (e) => {
    if (!drawingRef.current) return;
    e.preventDefault();
    const c = canvasRef.current;
    const ctx = c.getContext("2d");
    const p = ptFromEvent(e);
    ctx.beginPath();
    ctx.moveTo(lastRef.current.x, lastRef.current.y);
    ctx.lineTo(p.x, p.y);
    ctx.stroke();
    lastRef.current = p;
  };
  const end = () => {
    if (!drawingRef.current) return;
    drawingRef.current = false;
    const c = canvasRef.current;
    const dataUrl = c.toDataURL("image/png");
    onChange({ _pendingUpload: true, dataUrl, name: "signature.png", mime: "image/png" });
  };
  const clear = () => {
    const c = canvasRef.current;
    const ctx = c.getContext("2d");
    ctx.clearRect(0, 0, c.width, c.height);
    onChange(null);
  };
  return (
    <div>
      <div style={{ border: "1px dashed var(--border)", borderRadius: 8, background: "white", touchAction: "none" }}>
        <canvas ref={canvasRef}
          style={{ display: "block", width: "100%", height: 140, borderRadius: 8 }}
          onMouseDown={start} onMouseMove={move} onMouseUp={end} onMouseLeave={end}
          onTouchStart={start} onTouchMove={move} onTouchEnd={end}
        />
      </div>
      <div style={{ display: "flex", gap: 6, marginTop: 6 }}>
        <button className="ph-btn ghost" style={{ flex: 1, fontSize: 12, padding: "6px 8px" }} onClick={clear}>
          <Icon.x /> {lang === "fr" ? "Effacer" : "Clear"}
        </button>
        {value && value.dataUrl && (
          <span className="ph-hint" style={{ flex: 2, alignSelf: "center", margin: 0 }}>
            <Icon.check /> {lang === "fr" ? "Signature capturée" : "Signature captured"}
          </span>
        )}
      </div>
    </div>
  );
}

// Schema-driven form runner. Reads `form.schema.fields[]` from the database
// and binds each input to the parent entryData state. Falls back to a
// minimal site/notes pair if the form has no schema, so the simulator
// stays usable even with seed data that hasn't been backfilled.
function PhoneForm({ lang, airplane, form, sites, entryData, setField, entrySite, setEntrySite, entryGeo, onBack, onContinue }) {
  if (!form) {
    return (
      <div className="phone">
        <div className="ph-form-head">
          <button className="ph-iconbtn" onClick={onBack}><Icon.chevronLeft /></button>
          <div style={{ flex: 1 }}>
            <div className="ph-form-title">{lang === "fr" ? "Aucun formulaire" : "No form"}</div>
            <div className="ph-sub">{lang === "fr" ? "Retour à l'accueil" : "Back to home"}</div>
          </div>
        </div>
        <PhoneStatus lang={lang} airplane={airplane} />
        <div style={{ padding: 16, color: "var(--text-faint)", fontSize: 12.5 }}>
          {lang === "fr"
            ? "Choisissez un formulaire depuis l'écran d'accueil."
            : "Pick a form from the home screen first."}
        </div>
      </div>
    );
  }

  const schemaFields = (form.schema && Array.isArray(form.schema.fields)) ? form.schema.fields : [];
  // Always show a site selector + a free-form note, regardless of schema.
  const hasSiteInSchema = schemaFields.some((f) => f.k === "site");
  const fieldCount = schemaFields.length || 2; // site + notes fallback
  const filled = schemaFields.filter((f) => {
    if (f.k === "site") return !!entrySite;
    const v = entryData[f.k];
    return v !== undefined && v !== "" && v !== null;
  }).length + (entrySite ? (hasSiteInSchema ? 0 : 1) : 0);
  const pct = Math.min(100, Math.round((filled / Math.max(1, fieldCount)) * 100));

  // ── E2-bis (data for new field types) ─────────────────────────────────
  // Hooks are ALWAYS called (Rules of Hooks) regardless of whether the
  // schema uses these field types — useIndicators/useActivities already
  // return early when projectId is null, and useProjects/usePartners
  // fetches are tiny anyway. The cost is negligible for forms that don't
  // use these types.
  //
  // Convention: the FIRST field of type `project` in the schema is "the"
  // project for the form. Activity/Indicators pickers filter by it.
  const projectField    = schemaFields.find((f) => f.type === "project");
  const pickedProjectId = projectField ? entryData[projectField.k] : null;
  const { projects: mobileProjects = [] } = window.melr.useProjects();
  const { data: mobileActivities  = [] } = window.melr.useActivities(pickedProjectId || null);
  const { data: mobilePartners    = [] } = window.melr.usePartners();
  const { data: mobileIndicators  = [] } = window.melr.useIndicators(pickedProjectId || null);

  const renderField = (f) => {
    if (f.k === "site") {
      return (
        <div key={f.k} className="ph-field">
          <label>{f.l || (lang === "fr" ? "Site" : "Site")}</label>
          <select className="ph-input" value={entrySite} onChange={(e) => setEntrySite(e.target.value)}
            style={{ width: "100%", padding: "8px 10px", border: "1px solid var(--border)", borderRadius: 6, background: "white", fontSize: 13 }}>
            <option value="">{lang === "fr" ? "— Choisir un site —" : "— Pick a site —"}</option>
            {(sites || []).map((s) => (
              <option key={s.id} value={s.id}>{s.code ? s.code + " · " : ""}{s.name}</option>
            ))}
          </select>
          {entryGeo && (
            <div className="ph-hint">
              <Icon.map /> {entryGeo.lat.toFixed(4)}°, {entryGeo.lng.toFixed(4)}°
              {entryGeo.accuracy ? " · ±" + Math.round(entryGeo.accuracy) + "m" : ""}
            </div>
          )}
        </div>
      );
    }
    const v = entryData[f.k] != null ? entryData[f.k] : "";
    if (f.type === "bool") {
      return (
        <div key={f.k} className="ph-field">
          <label>{f.l || f.k}</label>
          <div className="ph-checks">
            <button className={"ph-check" + (v === true ? " on" : "")} onClick={() => setField(f.k, true)}>
              <span className="ph-check-box">{v === true && <Icon.check />}</span>
              {lang === "fr" ? "Oui" : "Yes"}
            </button>
            <button className={"ph-check" + (v === false ? " on" : "")} onClick={() => setField(f.k, false)}>
              <span className="ph-check-box">{v === false && <Icon.check />}</span>
              {lang === "fr" ? "Non" : "No"}
            </button>
          </div>
        </div>
      );
    }
    if (f.type === "number") {
      return (
        <div key={f.k} className="ph-field">
          <label>{f.l || f.k}</label>
          <input type="number" className="ph-input" value={v}
            onChange={(e) => setField(f.k, e.target.value === "" ? "" : Number(e.target.value))}
            style={{ width: "100%", padding: "8px 10px", border: "1px solid var(--border)", borderRadius: 6, background: "white", fontSize: 13 }}
          />
        </div>
      );
    }
    if (f.type === "date") {
      return (
        <div key={f.k} className="ph-field">
          <label>{f.l || f.k}</label>
          <input type="date" className="ph-input" value={v}
            onChange={(e) => setField(f.k, e.target.value)}
            style={{ width: "100%", padding: "8px 10px", border: "1px solid var(--border)", borderRadius: 6, background: "white", fontSize: 13 }}
          />
        </div>
      );
    }
    if (f.type === "select" && Array.isArray(f.options)) {
      return (
        <div key={f.k} className="ph-field">
          <label>{f.l || f.k}</label>
          <select className="ph-input" value={v} onChange={(e) => setField(f.k, e.target.value)}
            style={{ width: "100%", padding: "8px 10px", border: "1px solid var(--border)", borderRadius: 6, background: "white", fontSize: 13 }}>
            <option value="">{lang === "fr" ? "— Choisir —" : "— Pick —"}</option>
            {f.options.map((o) => (
              <option key={o.value || o} value={o.value || o}>{o.label || o.value || o}</option>
            ))}
          </select>
        </div>
      );
    }
    if (f.type === "matrix") {
      // Matrix is too rich to render in the simulator — fall back to JSON-ish text
      return (
        <div key={f.k} className="ph-field">
          <label>{f.l || f.k}</label>
          <textarea className="ph-input" placeholder={lang === "fr" ? "Lignes (article: qté)" : "Rows (item: qty)"}
            value={typeof v === "string" ? v : ""}
            onChange={(e) => setField(f.k, e.target.value)}
            rows={3}
            style={{ width: "100%", padding: "8px 10px", border: "1px solid var(--border)", borderRadius: 6, background: "white", fontSize: 13, resize: "vertical" }}
          />
          <div className="ph-hint">{lang === "fr" ? "Saisissez une ligne par article (démo simplifiée)." : "One item per line (simplified demo)."}</div>
        </div>
      );
    }
    if (f.type === "photo") {
      return (
        <div key={f.k} className="ph-field">
          <label>{f.l || (lang === "fr" ? "Photo" : "Photo")}</label>
          <PhotoField lang={lang} value={entryData[f.k]} onChange={(val) => setField(f.k, val)} />
          <div className="ph-hint">{lang === "fr" ? "Ouvre l'appareil photo si disponible." : "Opens the camera if available."}</div>
        </div>
      );
    }
    if (f.type === "signature") {
      return (
        <div key={f.k} className="ph-field">
          <label>{f.l || (lang === "fr" ? "Signature" : "Signature")}</label>
          <SignaturePad lang={lang} value={entryData[f.k]} onChange={(val) => setField(f.k, val)} />
        </div>
      );
    }
    // ── E2-bis · new field types from FormBuilder ─────────────────────
    if (f.type === "project") {
      return (
        <div key={f.k} className="ph-field">
          <label>{f.l || (lang === "fr" ? "Projet" : "Project")}</label>
          <select className="ph-input" value={v} onChange={(e) => setField(f.k, e.target.value)}
            style={{ width: "100%", padding: "8px 10px", border: "1px solid var(--border)", borderRadius: 6, background: "white", fontSize: 13 }}>
            <option value="">{lang === "fr" ? "— Choisir un projet —" : "— Pick a project —"}</option>
            {(mobileProjects || []).map((p) => (
              <option key={p.uuid} value={p.uuid}>{p.id} · {lang === "fr" ? (p.nameFr || p.nameEn) : (p.nameEn || p.nameFr)}</option>
            ))}
          </select>
        </div>
      );
    }
    if (f.type === "activity") {
      const disabled = !pickedProjectId;
      return (
        <div key={f.k} className="ph-field">
          <label>{f.l || (lang === "fr" ? "Activité" : "Activity")}</label>
          <select className="ph-input" value={v} disabled={disabled}
            onChange={(e) => setField(f.k, e.target.value)}
            style={{ width: "100%", padding: "8px 10px", border: "1px solid var(--border)", borderRadius: 6, background: "white", fontSize: 13, opacity: disabled ? 0.6 : 1 }}>
            <option value="">
              {disabled
                ? (lang === "fr" ? "— Choisir d'abord un projet —" : "— Pick a project first —")
                : (lang === "fr" ? "— Choisir une activité —" : "— Pick an activity —")}
            </option>
            {(mobileActivities || []).map((a) => (
              <option key={a.id} value={a.id}>{a.code ? a.code + " · " : ""}{a.title}</option>
            ))}
          </select>
        </div>
      );
    }
    if (f.type === "gps") {
      // v is expected to be { lat, lng } or null. Tolerate strings if the
      // user typed manually.
      const gpsObj = (v && typeof v === "object") ? v : { lat: "", lng: "" };
      const setCoord = (k, val) => setField(f.k, { ...gpsObj, [k]: val });
      const useDeviceGps = () => {
        if (!navigator.geolocation) {
          alert(lang === "fr" ? "Géolocalisation non disponible sur cet appareil." : "Geolocation unavailable.");
          return;
        }
        navigator.geolocation.getCurrentPosition(
          (pos) => setField(f.k, { lat: pos.coords.latitude.toFixed(6), lng: pos.coords.longitude.toFixed(6) }),
          (err) => alert((lang === "fr" ? "Erreur GPS : " : "GPS error: ") + err.message),
          { enableHighAccuracy: true, timeout: 10000 }
        );
      };
      return (
        <div key={f.k} className="ph-field">
          <label>{f.l || (lang === "fr" ? "Coordonnées GPS" : "GPS coordinates")}</label>
          <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr auto", gap: 4 }}>
            <input className="ph-input" type="number" step="any" placeholder="Lat"
              value={gpsObj.lat || ""} onChange={(e) => setCoord("lat", e.target.value)}
              style={{ width: "100%", padding: "8px 10px", border: "1px solid var(--border)", borderRadius: 6, background: "white", fontSize: 13 }} />
            <input className="ph-input" type="number" step="any" placeholder="Lng"
              value={gpsObj.lng || ""} onChange={(e) => setCoord("lng", e.target.value)}
              style={{ width: "100%", padding: "8px 10px", border: "1px solid var(--border)", borderRadius: 6, background: "white", fontSize: 13 }} />
            <button className="ph-iconbtn" onClick={useDeviceGps} title="GPS"
              style={{ padding: "6px 10px", fontSize: 12 }}>
              <Icon.pin /> GPS
            </button>
          </div>
          {gpsObj.lat && gpsObj.lng && (
            <div className="ph-hint">
              <Icon.map /> {Number(gpsObj.lat).toFixed(4)}°, {Number(gpsObj.lng).toFixed(4)}°
            </div>
          )}
        </div>
      );
    }
    if (f.type === "partners_multi") {
      const selected = Array.isArray(v) ? v : [];
      const toggle = (id) => {
        if (selected.includes(id)) setField(f.k, selected.filter((x) => x !== id));
        else                        setField(f.k, [...selected, id]);
      };
      return (
        <div key={f.k} className="ph-field">
          <label>
            {f.l || (lang === "fr" ? "Partenaires" : "Partners")}
            <span className="ph-hint" style={{ marginLeft: 6 }}>· {selected.length} {lang === "fr" ? "sélectionné(s)" : "selected"}</span>
          </label>
          <div style={{ display: "flex", flexWrap: "wrap", gap: 4, padding: 6, background: "var(--bg-sunken, #f3f4f6)", borderRadius: 6, border: "1px solid var(--border)" }}>
            {(mobilePartners || []).length === 0 ? (
              <div className="ph-hint" style={{ padding: 4 }}>
                {lang === "fr" ? "Aucun partenaire disponible." : "No partners available."}
              </div>
            ) : (mobilePartners || []).map((p) => {
              const on = selected.includes(p.id);
              return (
                <button key={p.id} type="button" onClick={() => toggle(p.id)}
                  style={{
                    fontSize: 11.5, padding: "2px 8px", borderRadius: 999,
                    background: on ? "var(--accent, #4f46e5)" : "white",
                    color: on ? "white" : "var(--text, #111)",
                    border: "1px solid " + (on ? "var(--accent, #4f46e5)" : "var(--border, #ddd)"),
                    cursor: "pointer",
                  }}>
                  {on && "✓ "}{lang === "fr" ? p.name_fr : (p.name_en || p.name_fr)}
                </button>
              );
            })}
          </div>
        </div>
      );
    }
    if (f.type === "indicators_multi") {
      const selected = Array.isArray(v) ? v : [];
      const disabled = !pickedProjectId;
      const toggle = (id) => {
        if (selected.includes(id)) setField(f.k, selected.filter((x) => x !== id));
        else                        setField(f.k, [...selected, id]);
      };
      return (
        <div key={f.k} className="ph-field">
          <label>
            {f.l || (lang === "fr" ? "Indicateurs" : "Indicators")}
            <span className="ph-hint" style={{ marginLeft: 6 }}>· {selected.length} {lang === "fr" ? "sélectionné(s)" : "selected"}</span>
          </label>
          <div style={{ display: "flex", flexDirection: "column", gap: 3, padding: 6, background: "var(--bg-sunken, #f3f4f6)", borderRadius: 6, border: "1px solid var(--border)", maxHeight: 160, overflowY: "auto" }}>
            {disabled ? (
              <div className="ph-hint" style={{ padding: 4 }}>
                {lang === "fr" ? "Sélectionner d'abord un projet pour voir ses indicateurs." : "Pick a project first to see its indicators."}
              </div>
            ) : (mobileIndicators || []).length === 0 ? (
              <div className="ph-hint" style={{ padding: 4 }}>
                {lang === "fr" ? "Aucun indicateur pour ce projet." : "No indicators for this project."}
              </div>
            ) : (mobileIndicators || []).map((i) => {
              const on = selected.includes(i.id);
              return (
                <label key={i.id} style={{ display: "flex", gap: 6, alignItems: "center", padding: "3px 4px", cursor: "pointer", fontSize: 12 }}>
                  <input type="checkbox" checked={on} onChange={() => toggle(i.id)} />
                  <span style={{ fontFamily: "monospace", fontSize: 10.5, color: "var(--text-faint)", minWidth: 50 }}>{i.code}</span>
                  <span>{lang === "fr" ? i.name_fr : (i.name_en || i.name_fr)}</span>
                </label>
              );
            })}
          </div>
        </div>
      );
    }
    if (f.type === "attachments") {
      // Local-only file list (kept in entryData as an array of {name, size, type}).
      // Actual upload happens on form submit via submitForm() — keeping the
      // mobile path offline-first. The agent sees file metadata immediately.
      const files = Array.isArray(v) ? v : [];
      const onPick = (e) => {
        const picked = Array.from(e.target.files || []);
        if (picked.length === 0) return;
        const meta = picked.map((file) => ({ name: file.name, size: file.size, type: file.type, _file: file }));
        setField(f.k, [...files, ...meta]);
        e.target.value = "";
      };
      const removeAt = (idx) => setField(f.k, files.filter((_, i) => i !== idx));
      return (
        <div key={f.k} className="ph-field">
          <label>{f.l || (lang === "fr" ? "Pièces jointes" : "Attachments")}</label>
          <label className="ph-iconbtn" style={{ display: "inline-flex", padding: "6px 10px", fontSize: 12, cursor: "pointer" }}>
            <Icon.upload /> {lang === "fr" ? "Ajouter des fichiers" : "Add files"}
            <input type="file" multiple style={{ display: "none" }} onChange={onPick} />
          </label>
          {files.length > 0 && (
            <div style={{ display: "flex", flexDirection: "column", gap: 3, marginTop: 6 }}>
              {files.map((file, idx) => (
                <div key={idx} style={{ display: "flex", gap: 6, alignItems: "center", padding: "4px 8px", background: "var(--bg-sunken, #f3f4f6)", borderRadius: 5, fontSize: 11.5 }}>
                  <span style={{ flex: 1, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>🗎 {file.name}</span>
                  <span className="ph-hint">{file.size ? (file.size < 1024*1024 ? Math.round(file.size/1024)+" KB" : (file.size/1024/1024).toFixed(1)+" MB") : ""}</span>
                  <button type="button" onClick={() => removeAt(idx)} style={{ background: "none", border: "none", cursor: "pointer", fontSize: 12, color: "var(--text-faint)" }}>✕</button>
                </div>
              ))}
            </div>
          )}
        </div>
      );
    }
    if (f.type === "activity_status") {
      const STATUSES = [
        { v: "not_started", fr: "Non démarrée",  en: "Not started" },
        { v: "ongoing",     fr: "En cours",      en: "Ongoing" },
        { v: "completed",   fr: "Terminée",      en: "Completed" },
        { v: "postponed",   fr: "Reportée",      en: "Postponed" },
        { v: "cancelled",   fr: "Annulée",       en: "Cancelled" },
      ];
      return (
        <div key={f.k} className="ph-field">
          <label>{f.l || (lang === "fr" ? "Statut" : "Status")}</label>
          <select className="ph-input" value={v} onChange={(e) => setField(f.k, e.target.value)}
            style={{ width: "100%", padding: "8px 10px", border: "1px solid var(--border)", borderRadius: 6, background: "white", fontSize: 13 }}>
            <option value="">{lang === "fr" ? "— Choisir —" : "— Pick —"}</option>
            {STATUSES.map((s) => (
              <option key={s.v} value={s.v}>{lang === "fr" ? s.fr : s.en}</option>
            ))}
          </select>
        </div>
      );
    }
    // Default = text
    return (
      <div key={f.k} className="ph-field">
        <label>{f.l || f.k}</label>
        <input type="text" className="ph-input" value={v}
          onChange={(e) => setField(f.k, e.target.value)}
          style={{ width: "100%", padding: "8px 10px", border: "1px solid var(--border)", borderRadius: 6, background: "white", fontSize: 13 }}
        />
      </div>
    );
  };

  // Build the render list: ensure a site selector exists even if schema doesn't have one.
  const fieldsToRender = hasSiteInSchema ? schemaFields : [{ k: "site", l: lang === "fr" ? "Site" : "Site", type: "select" }, ...schemaFields];

  const formName = lang === "en" ? (form.name_en || form.name_fr) : form.name_fr;
  const canContinue = !!entrySite; // need at least a site

  return (
    <div className="phone">
      <div className="ph-form-head">
        <button className="ph-iconbtn" onClick={onBack}><Icon.chevronLeft /></button>
        <div style={{ flex: 1 }}>
          <div className="ph-form-title">{formName}</div>
          <div className="ph-sub mono">
            {form.code}{form.version ? " · " + form.version : ""}
            {form.projects && form.projects.code ? " · " + form.projects.code : ""}
          </div>
        </div>
        <span className="ph-pill accent">{filled}/{fieldCount}</span>
      </div>

      <div className="ph-progress"><div className="ph-progress-fill" style={{ width: pct + "%" }}></div></div>

      <PhoneStatus lang={lang} airplane={airplane} />

      <div className="ph-section-h">{lang === "fr" ? "Saisie" : "Capture"}</div>

      {fieldsToRender.length === 0 ? (
        <div className="ph-field">
          <label>{lang === "fr" ? "Notes libres" : "Free notes"}</label>
          <textarea className="ph-input" value={entryData.notes || ""}
            onChange={(e) => setField("notes", e.target.value)}
            rows={4}
            style={{ width: "100%", padding: "8px 10px", border: "1px solid var(--border)", borderRadius: 6, background: "white", fontSize: 13, resize: "vertical" }}
          />
          <div className="ph-hint">{lang === "fr" ? "Ce formulaire n'a pas de schéma défini." : "This form has no schema defined."}</div>
        </div>
      ) : fieldsToRender.map(renderField)}

      <div className="ph-actions">
        <button className="ph-btn ghost" onClick={onBack}><Icon.chevronLeft /> {lang === "fr" ? "Retour" : "Back"}</button>
        <button className="ph-btn primary" onClick={onContinue} disabled={!canContinue}
          style={!canContinue ? { opacity: 0.5, cursor: "not-allowed" } : null}>
          {lang === "fr" ? "Continuer" : "Continue"} <Icon.chevronRight />
        </button>
      </div>
    </div>
  );
}

// Recap of entered data + real submit. The submit handler is in the
// parent (onSubmitEntry) and calls window.melr.submitForm(), so this
// screen only displays state and triggers the parent action.
function PhoneReview({ lang, airplane, form, sites, entryData, entrySite, entryGeo, submitting, onBack, onSubmit }) {
  if (!form) {
    return (
      <div className="phone">
        <div className="ph-form-head">
          <button className="ph-iconbtn" onClick={onBack}><Icon.chevronLeft /></button>
          <div style={{ flex: 1 }}>
            <div className="ph-form-title">{lang === "fr" ? "Aucune saisie" : "Nothing to review"}</div>
          </div>
        </div>
        <PhoneStatus lang={lang} airplane={airplane} />
        <div style={{ padding: 16, color: "var(--text-faint)", fontSize: 12.5 }}>
          {lang === "fr"
            ? "Démarrez une saisie depuis l'accueil."
            : "Start a capture from the home screen."}
        </div>
      </div>
    );
  }

  const formName = lang === "en" ? (form.name_en || form.name_fr) : form.name_fr;
  const siteRow = (sites || []).find((s) => s.id === entrySite);
  const schemaFields = (form.schema && Array.isArray(form.schema.fields)) ? form.schema.fields : [];

  const formatValue = (f, v) => {
    if (v === undefined || v === "" || v === null) return "—";
    if (typeof v === "boolean") return v ? (lang === "fr" ? "Oui" : "Yes") : (lang === "fr" ? "Non" : "No");
    if (f && f.type === "date") return v;
    // Attachment markers — render as a small thumbnail / chip
    if (typeof v === "object" && (v._pendingUpload || v._attachment)) {
      const sizeKb = v.size ? Math.round(v.size / 1024) + " KB" : "";
      if (v.dataUrl) {
        return (
          <div style={{ display: "flex", alignItems: "center", gap: 6 }}>
            <img src={v.dataUrl} alt="" style={{ width: 46, height: 46, objectFit: "cover", borderRadius: 4, border: "1px solid var(--border)" }} />
            <span style={{ fontSize: 11 }}>{v.name || "file"}{sizeKb ? " · " + sizeKb : ""}</span>
          </div>
        );
      }
      return (
        <span style={{ fontSize: 11 }}>
          <Icon.fileText /> {v.name || "file"}{sizeKb ? " · " + sizeKb : ""}
        </span>
      );
    }
    return String(v);
  };

  const rows = [];
  // Site row first
  rows.push({
    l: lang === "fr" ? "Site" : "Site",
    v: siteRow ? ((siteRow.code ? siteRow.code + " · " : "") + siteRow.name) : "—",
    warn: !siteRow,
  });
  // Schema-defined fields
  schemaFields.forEach((f) => {
    if (f.k === "site") return; // already shown
    rows.push({ l: f.l || f.k, v: formatValue(f, entryData[f.k]) });
  });
  // Free notes if no schema
  if (schemaFields.length === 0 && entryData.notes) {
    rows.push({ l: lang === "fr" ? "Notes" : "Notes", v: entryData.notes });
  }
  // GPS
  if (entryGeo) {
    rows.push({
      l: lang === "fr" ? "Géolocalisation" : "Geolocation",
      v: entryGeo.lat.toFixed(4) + "°, " + entryGeo.lng.toFixed(4) + "°"
         + (entryGeo.accuracy ? " · ±" + Math.round(entryGeo.accuracy) + "m" : ""),
    });
  }

  const filledCount = rows.filter((r) => r.v !== "—").length;
  const isComplete  = !!siteRow;
  const submitLabel = !navigator.onLine || airplane
    ? (lang === "fr" ? "Mettre en file" : "Queue locally")
    : (lang === "fr" ? "Envoyer" : "Submit");

  return (
    <div className="phone">
      <div className="ph-form-head">
        <button className="ph-iconbtn" onClick={onBack}><Icon.chevronLeft /></button>
        <div style={{ flex: 1 }}>
          <div className="ph-form-title">{lang === "fr" ? "Récapitulatif" : "Review"}</div>
          <div className="ph-sub mono">{form.code} · {filledCount}/{rows.length}</div>
        </div>
        <span className={"ph-pill " + (isComplete ? "green" : "amber")}>
          {isComplete ? (lang === "fr" ? "Prêt" : "Ready") : (lang === "fr" ? "Site manquant" : "Site missing")}
        </span>
      </div>

      <PhoneStatus lang={lang} airplane={airplane} />

      <div className="ph-section-h">{formName}</div>

      {!isComplete && (
        <div className="ph-alert">
          <Icon.alert />
          <div>
            <div className="strong">{lang === "fr" ? "Site manquant" : "Site missing"}</div>
            <div className="ph-sub">{lang === "fr" ? "Retour pour sélectionner un site." : "Go back to pick a site."}</div>
          </div>
        </div>
      )}

      <div className="ph-review">
        {rows.map((r, i) => (
          <div key={i} className={"ph-rev-row" + (r.warn ? " warn" : "")}>
            <div className="ph-rev-l">{r.l}</div>
            <div className="ph-rev-v">{r.v}</div>
          </div>
        ))}
      </div>

      <div className="ph-actions">
        <button className="ph-btn ghost" onClick={onBack} disabled={submitting}>
          <Icon.edit /> {lang === "fr" ? "Modifier" : "Edit"}
        </button>
        <button className="ph-btn primary" onClick={onSubmit} disabled={submitting || !isComplete}
          style={(submitting || !isComplete) ? { opacity: 0.6, cursor: "not-allowed" } : null}>
          {submitting ? (lang === "fr" ? "Envoi…" : "Sending…") : submitLabel}
          <Icon.send />
        </button>
      </div>
    </div>
  );
}

// ─── My submissions — own recent entries, server + local queue ───────────
function PhoneSubmissions({ lang, serverSubs, localQueue, forms, sites, onBack }) {
  const [expandedId, setExpandedId] = useStateM(null);

  // Index forms + sites for fast lookup of pretty names
  const formById = React.useMemo(() => {
    const m = new Map();
    (forms || []).forEach((f) => m.set(f.id, f));
    return m;
  }, [forms]);
  const siteById = React.useMemo(() => {
    const m = new Map();
    (sites || []).forEach((s) => m.set(s.id, s));
    return m;
  }, [sites]);

  // Normalise both sources into a single row shape so the list rendering
  // doesn't have to care where the row came from.
  const rows = React.useMemo(() => {
    const out = [];
    (localQueue || []).forEach((q) => {
      out.push({
        id:         q.id,
        kind:       "local",
        form_id:    q.form_id,
        site_id:    q.site_id,
        data:       q.data || {},
        captured_at: q.captured_at,
        when:       q._localQueuedAt || (q.captured_at ? new Date(q.captured_at).getTime() : 0),
      });
    });
    (serverSubs || []).forEach((s) => {
      out.push({
        id:         s.id,
        kind:       "server",
        state:      s.state,
        form_id:    s.form_id,
        site_id:    s.site_id,
        data:       s.data || {},
        captured_at: s.captured_at,
        uploaded_at: s.uploaded_at,
        size_bytes: s.size_bytes,
        when:       s.uploaded_at ? new Date(s.uploaded_at).getTime() : 0,
      });
    });
    // Newest first
    out.sort((a, b) => (b.when || 0) - (a.when || 0));
    return out;
  }, [serverSubs, localQueue]);

  const formatRelative = (ms) => {
    if (!ms) return "—";
    const diff = Date.now() - ms;
    const sec = Math.round(diff / 1000);
    if (sec < 60)  return lang === "fr" ? "à l'instant" : "just now";
    const min = Math.round(sec / 60);
    if (min < 60)  return lang === "fr" ? ("il y a " + min + " min") : (min + " min ago");
    const hr = Math.round(min / 60);
    if (hr < 24)   return lang === "fr" ? ("il y a " + hr + " h")    : (hr + " h ago");
    const day = Math.round(hr / 24);
    if (day < 7)   return lang === "fr" ? ("il y a " + day + " j")   : (day + " d ago");
    return new Date(ms).toLocaleDateString(lang === "fr" ? "fr-FR" : "en-US", { day: "numeric", month: "short" });
  };

  const stateBadge = (row) => {
    if (row.kind === "local") {
      return <span className="ph-pill amber">{lang === "fr" ? "📥 En file" : "📥 Queued"}</span>;
    }
    if (row.state === "queued")  return <span className="ph-pill amber">{lang === "fr" ? "En attente" : "Pending"}</span>;
    if (row.state === "syncing") return <span className="ph-pill accent">{lang === "fr" ? "Synchro…" : "Syncing…"}</span>;
    if (row.state === "synced")  return <span className="ph-pill green">{lang === "fr" ? "✓ Envoyé" : "✓ Sent"}</span>;
    if (row.state === "review")  return <span className="ph-pill violet">{lang === "fr" ? "À valider" : "Review"}</span>;
    return <span className="ph-pill green">{lang === "fr" ? "✓" : "✓"}</span>;
  };

  return (
    <div className="phone">
      <div className="ph-form-head">
        <button className="ph-iconbtn" onClick={onBack}><Icon.chevronLeft /></button>
        <div style={{ flex: 1 }}>
          <div className="ph-form-title">{lang === "fr" ? "Mes saisies" : "My submissions"}</div>
          <div className="ph-sub mono">
            {rows.length}{" "}
            {rows.length === 1
              ? (lang === "fr" ? "saisie" : "submission")
              : (lang === "fr" ? "saisies" : "submissions")}
          </div>
        </div>
      </div>

      {rows.length === 0 ? (
        <div className="text-faint" style={{ padding: 28, textAlign: "center", fontSize: 13 }}>
          {lang === "fr"
            ? "Aucune saisie pour le moment. Lancez un formulaire depuis l'accueil pour commencer."
            : "No submission yet. Start a form from the home screen."}
        </div>
      ) : (
        <div style={{ display: "flex", flexDirection: "column", gap: 6, padding: "4px 0" }}>
          {rows.slice(0, 40).map((r) => {
            const form = formById.get(r.form_id);
            const site = r.site_id ? siteById.get(r.site_id) : null;
            const formName = form
              ? (lang === "en" ? (form.name_en || form.name_fr) : form.name_fr)
              : (lang === "fr" ? "Formulaire inconnu" : "Unknown form");
            const formCode = form ? form.code : (r.form_id ? r.form_id.slice(0, 6) : "—");
            const siteName = site ? ((site.code ? site.code + " · " : "") + site.name) : "—";
            const isOpen = expandedId === r.id;
            return (
              <div key={r.id} style={{ border: "1px solid var(--border, #e5e7eb)", borderRadius: 6, background: "white" }}>
                <button
                  onClick={() => setExpandedId(isOpen ? null : r.id)}
                  style={{
                    width: "100%", display: "grid", gridTemplateColumns: "auto 1fr auto",
                    gap: 8, alignItems: "center", padding: "10px 12px",
                    background: "transparent", border: 0, cursor: "pointer", textAlign: "left",
                  }}>
                  <div className="ph-form-mark" style={{ background: "var(--accent)", width: 32, height: 32, fontSize: 11 }}>
                    {(formCode || "F").slice(0, 3)}
                  </div>
                  <div style={{ minWidth: 0 }}>
                    <div className="strong" style={{ fontSize: 13, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{formName}</div>
                    <div className="ph-sub" style={{ fontSize: 11, marginTop: 1 }}>
                      {siteName} · {formatRelative(r.when)}
                    </div>
                  </div>
                  <div>{stateBadge(r)}</div>
                </button>
                {isOpen && (
                  <div style={{ padding: "8px 12px 12px", borderTop: "1px solid var(--border, #e5e7eb)", background: "#f8fafc" }}>
                    <div style={{ fontSize: 10.5, color: "var(--text-faint)", marginBottom: 4 }}>
                      <span className="mono">{formCode}</span>
                      {r.captured_at && (
                        <> · {lang === "fr" ? "saisi le" : "captured"} {new Date(r.captured_at).toLocaleString(lang === "fr" ? "fr-FR" : "en-US")}</>
                      )}
                      {r.size_bytes && <> · {Math.max(1, Math.round(r.size_bytes / 1024))} KB</>}
                    </div>
                    {/* Field values */}
                    <div style={{ display: "flex", flexDirection: "column", gap: 3 }}>
                      {Object.entries(r.data || {})
                        .filter(([k]) => k !== "_geo")
                        .map(([k, v]) => (
                          <div key={k} style={{ display: "grid", gridTemplateColumns: "100px 1fr", gap: 6, fontSize: 11.5 }}>
                            <div className="text-faint" style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{k}</div>
                            <div style={{ wordBreak: "break-word" }}>
                              {v === null || v === undefined || v === ""
                                ? <span className="text-faint">—</span>
                                : typeof v === "boolean"
                                ? (v ? (lang === "fr" ? "Oui" : "Yes") : (lang === "fr" ? "Non" : "No"))
                                : typeof v === "object"
                                ? (v._pendingUpload || v._attachment
                                    ? <span><Icon.fileText className="sm" /> {v.name || "fichier"}{v.size ? " · " + Math.round(v.size / 1024) + " KB" : ""}</span>
                                    : <span className="mono text-faint" style={{ fontSize: 10.5 }}>{JSON.stringify(v)}</span>)
                                : String(v)}
                            </div>
                          </div>
                        ))}
                    </div>
                    {r.data && r.data._geo && (
                      <div className="ph-hint" style={{ marginTop: 6 }}>
                        <Icon.map /> {r.data._geo.lat.toFixed(4)}°, {r.data._geo.lng.toFixed(4)}°
                      </div>
                    )}
                  </div>
                )}
              </div>
            );
          })}
          {rows.length > 40 && (
            <div className="text-faint" style={{ textAlign: "center", padding: 10, fontSize: 11 }}>
              {lang === "fr"
                ? "Affichage des 40 saisies les plus récentes."
                : "Showing the 40 most recent submissions."}
            </div>
          )}
        </div>
      )}
    </div>
  );
}

window.MobileField = MobileField;

// ============================================================================
// MOBILE APP — fullscreen route (no sidebar, no Android frame)
// ----------------------------------------------------------------------------
// Reached via /MELR.html?mobile=1 or /MELR.html#mobile. The router in
// app.jsx short-circuits the full app shell and renders this component
// instead, so installed PWAs can launch directly into a phone-optimized
// data-entry surface.
// ============================================================================

function MobileApp({ lang, setLang, onExit }) {
  const { data: liveForms } = window.melr.useForms();
  const { data: liveSubs }  = window.melr.useFormSubmissions();
  const { data: liveSites } = window.melr.useSites();
  const localQueueSize = window.melr.useLocalQueueSize();
  const localQueue     = window.melr.useLocalQueue();
  const { has: hasPerm } = window.melr.useCurrentUserPermissions();
  const isAdmin = hasPerm && hasPerm("users.manage");

  const [screen, setScreen]           = useStateM("home");
  const [activeFormId, setActiveFormId] = useStateM(null);
  const [entryData, setEntryData]     = useStateM({});
  const [entrySite, setEntrySite]     = useStateM("");
  const [entryGeo, setEntryGeo]       = useStateM(null);
  const [submitting, setSubmitting]   = useStateM(false);
  const [submitToast, setSubmitToast] = useStateM(null);

  const activeForm = (liveForms || []).find((f) => f.id === activeFormId) || null;

  const startForm = (formId) => {
    setActiveFormId(formId);
    setEntryData({}); setEntrySite(""); setEntryGeo(null);
    setScreen("form");
    if (navigator.geolocation) {
      navigator.geolocation.getCurrentPosition(
        (pos) => setEntryGeo({ lat: pos.coords.latitude, lng: pos.coords.longitude, accuracy: pos.coords.accuracy }),
        () => {},
        { enableHighAccuracy: false, timeout: 4000 }
      );
    }
  };
  const setField   = (k, v) => setEntryData((d) => ({ ...d, [k]: v }));
  const resetEntry = () => {
    setActiveFormId(null); setEntryData({}); setEntrySite(""); setEntryGeo(null);
    setScreen("home");
  };
  const onSubmitEntry = async () => {
    if (!activeForm) return;
    setSubmitting(true);
    try {
      const dataPayload = { ...entryData };
      if (entryGeo) dataPayload._geo = entryGeo;
      const result = await window.melr.submitForm({
        form_id: activeForm.id,
        site_id: entrySite || null,
        data: dataPayload,
        captured_at: new Date().toISOString(),
        device_id: typeof navigator !== "undefined" ? (navigator.userAgent || "").slice(0, 80) : null,
      });
      setSubmitToast({
        ok: true,
        local: !!result.local,
        msg: result.local
          ? (lang === "fr" ? "📥 Saisie mise en file locale" : "📥 Saved locally")
          : (lang === "fr" ? "✓ Saisie envoyée" : "✓ Submission saved"),
      });
      setTimeout(() => setSubmitToast(null), 4000);
      resetEntry();
    } catch (e) {
      setSubmitToast({ ok: false, msg: e.message });
      setTimeout(() => setSubmitToast(null), 5000);
    } finally { setSubmitting(false); }
  };

  // Online detection + auto-flush
  const [online, setOnline] = useStateM(typeof navigator !== "undefined" ? navigator.onLine : true);
  useEffectM(() => {
    const on  = () => setOnline(true);
    const off = () => setOnline(false);
    window.addEventListener("online", on);
    window.addEventListener("offline", off);
    return () => { window.removeEventListener("online", on); window.removeEventListener("offline", off); };
  }, []);
  const [syncBusy, setSyncBusy] = useStateM(false);
  const [syncMsg,  setSyncMsg]  = useStateM(null);
  // Auto-flush + manual force-sync share the same handler so behaviour is
  // identical. Surfaces flush.errors so the agent sees WHY a row stays in
  // the queue (RLS denial vs network).
  const runFlush = async () => {
    setSyncBusy(true); setSyncMsg(null);
    try {
      const r = await window.melr.flushLocalQueue();
      const errs = r.errors || [];
      if (r.synced === 0 && r.remaining === 0) {
        setSyncMsg({ kind: "info", msg: lang === "fr" ? "Aucune saisie en attente." : "Nothing to sync." });
      } else if (errs.length === 0) {
        setSyncMsg({ kind: "ok", msg: (lang === "fr"
          ? (r.synced + " saisie(s) envoyée(s)" + (r.remaining > 0 ? " · " + r.remaining + " en attente" : ""))
          : (r.synced + " submission(s) sent"  + (r.remaining > 0 ? " · " + r.remaining + " pending" : ""))) });
      } else {
        // Surface the first concrete error so the user sees the cause.
        setSyncMsg({ kind: "warn", msg: (lang === "fr"
          ? (r.synced + " envoyée(s) · " + r.remaining + " bloquée(s) : " + errs[0].message)
          : (r.synced + " sent · " + r.remaining + " blocked: " + errs[0].message)) });
      }
      setTimeout(() => setSyncMsg(null), 6000);
    } catch (e) { setSyncMsg({ kind: "warn", msg: e.message }); }
    finally { setSyncBusy(false); }
  };
  useEffectM(() => {
    if (online && localQueueSize > 0) {
      // Auto-flush silently — only surface the message on manual click
      window.melr.flushLocalQueue().catch(() => {});
    }
  }, [online, localQueueSize]);

  const todayCount = (liveSubs || []).filter((s) => {
    const t = new Date(); t.setHours(0, 0, 0, 0);
    return new Date(s.uploaded_at) >= t;
  }).length;

  return (
    <div className="mobile-app">
      <div className="mobile-app-bar">
        <div className="mobile-app-brand">
          <img src="/abas-group.png" alt="MELR" style={{ width: 22, height: 22, borderRadius: 4 }} />
          <div>
            <div style={{ fontWeight: 600, fontSize: 13 }}>MELR</div>
            <div style={{ fontSize: 10, opacity: 0.7 }}>
              {lang === "fr" ? "Collecte de données" : "Data Collection"}
            </div>
          </div>
        </div>
        <div style={{ flex: 1 }}></div>
        {isAdmin && (
          <span className="pill violet" style={{ fontSize: 10 }}
            title={lang === "fr"
              ? "Vous êtes admin (permission users.manage) — vous voyez TOUS les formulaires de l'org."
              : "You are admin (users.manage) — you see ALL forms in the org."}>
            ADMIN
          </span>
        )}
        <span className={"pill " + (online ? "green dot" : "red dot")} style={{ fontSize: 10 }}>
          {online ? (lang === "fr" ? "En ligne" : "Online") : (lang === "fr" ? "Hors-ligne" : "Offline")}
          {localQueueSize > 0 && <span className="tag-mono" style={{ marginLeft: 4 }}>{localQueueSize}</span>}
        </span>
        {(localQueueSize > 0 || syncBusy) && (
          <button className="btn sm" onClick={runFlush} disabled={syncBusy}
            title={lang === "fr" ? "Forcer la synchronisation maintenant" : "Force sync now"}
            style={{ padding: "4px 8px", fontSize: 11 }}>
            <Icon.refresh /> {syncBusy ? "…" : (lang === "fr" ? "Synchro" : "Sync")}
          </button>
        )}
        <button className="btn sm ghost" onClick={() => setLang(lang === "fr" ? "en" : "fr")}
          title={lang === "fr" ? "Switch to English" : "Passer en français"}
          style={{ padding: "4px 8px", fontSize: 11 }}>
          {lang === "fr" ? "EN" : "FR"}
        </button>
        {onExit && (
          <button className="btn sm" onClick={onExit} title={lang === "fr" ? "Mode bureau" : "Desktop mode"}
            style={{ padding: "4px 8px", fontSize: 11 }}>
            {lang === "fr" ? "Bureau" : "Desktop"}
          </button>
        )}
      </div>

      {syncMsg && (
        <div style={{
          padding: "6px 12px", fontSize: 12.5,
          background: syncMsg.kind === "warn" ? "#fef3c7" : (syncMsg.kind === "ok" ? "#dcfce7" : "#dbeafe"),
          color:      syncMsg.kind === "warn" ? "#92400e" : (syncMsg.kind === "ok" ? "#166534" : "#1e40af"),
          borderBottom: "1px solid var(--line)",
        }}>{syncMsg.msg}</div>
      )}

      <div className="mobile-app-stage">
        {screen === "home" && (
          <PhoneHome lang={lang} airplane={!online}
            forms={liveForms || []} fixtureForms={FORMS_LIB}
            todayCount={todayCount}
            submissionsCount={(liveSubs || []).length + (localQueue || []).length}
            onOpenSubmissions={() => setScreen("submissions")}
            onPickForm={startForm} />
        )}
        {screen === "form" && (
          <PhoneForm lang={lang} airplane={!online}
            form={activeForm} sites={liveSites || []}
            entryData={entryData} setField={setField}
            entrySite={entrySite} setEntrySite={setEntrySite}
            entryGeo={entryGeo}
            onBack={() => setScreen("home")}
            onContinue={() => setScreen("review")} />
        )}
        {screen === "review" && (
          <PhoneReview lang={lang} airplane={!online}
            form={activeForm} sites={liveSites || []}
            entryData={entryData} entrySite={entrySite} entryGeo={entryGeo}
            submitting={submitting}
            onBack={() => setScreen("form")}
            onSubmit={onSubmitEntry} />
        )}
        {screen === "submissions" && (
          <PhoneSubmissions lang={lang}
            serverSubs={liveSubs || []}
            localQueue={localQueue || []}
            forms={liveForms || []}
            sites={liveSites || []}
            onBack={() => setScreen("home")} />
        )}
      </div>

      {submitToast && (
        <div className="mobile-app-toast" style={{
          background: submitToast.ok ? (submitToast.local ? "#fef3c7" : "#dcfce7") : "#fee2e2",
          color:      submitToast.ok ? (submitToast.local ? "#92400e" : "#166534") : "#991b1b",
          border: "1px solid " + (submitToast.ok ? (submitToast.local ? "#fcd34d" : "#86efac") : "#fca5a5"),
        }}>{submitToast.msg}</div>
      )}
    </div>
  );
}
window.MobileApp = MobileApp;
