/* global React, ReactDOM */
const { useState, useEffect, useMemo, useRef, useCallback } = React;

const ACCENT = { hex: "#E08B2A", soft: "#FBE7CB", ink: "#7A4A11" };

/* Obras pinadas no mapa (compartilhadas entre o card do dashboard e a tela /mapa) */
const OBRAS = [
  { nome: "Estação Brasilândia · Linha 6",  bairro: "Brasilândia",  lat: -23.467,  lng: -46.690, status: "ok",    estacas: 384 },
  { nome: "Estação Pompeia · Linha 6",       bairro: "Pompeia",      lat: -23.527,  lng: -46.682, status: "ok",    estacas: 240 },
  { nome: "Pátio Logístico Vila Leopoldina", bairro: "Vila Leopoldina", lat: -23.527, lng: -46.728, status: "warn", estacas: 168 },
  { nome: "Túnel TBM-3 · Trecho Norte",      bairro: "Santana",      lat: -23.503,  lng: -46.625, status: "ok",    estacas: 96 },
  { nome: "Bloco B7 · Tatuapé",              bairro: "Tatuapé",      lat: -23.539,  lng: -46.575, status: "warn", estacas: 72 },
  { nome: "Reservatório R-8 · Itaquera",     bairro: "Itaquera",     lat: -23.540,  lng: -46.461, status: "alert", estacas: 50 },
  { nome: "Edifício Faria Lima Tower",       bairro: "Itaim Bibi",   lat: -23.578,  lng: -46.689, status: "ok",    estacas: 124 },
  { nome: "Centro Logístico Cumbica",        bairro: "Guarulhos",    lat: -23.435,  lng: -46.480, status: "ok",    estacas: 210 },
];

// Cor do pin no mapa por status. Aceita os legados (ok/warn/alert) usados
// pela mini-map do dashboard e os status reais da MapaPage (offline /
// ocioso / perfurando / concretando / erro), derivados de gps_posicao.
const corObra = (s, accentHex) => {
  switch (s) {
    case "ok":          return "#22A06B";
    case "warn":        return accentHex;
    case "alert":       return "#D14343";
    case "ocioso":      return "#22A06B"; // verde
    case "perfurando":  return "#D14343"; // vermelho
    case "concretando": return "#3B82F6"; // azul
    case "erro":        return "#E0A02A"; // ambar
    case "offline":     return "#94A3B8"; // cinza
    default:            return accentHex || "#94A3B8";
  }
};
const rotuloObra = (s) => ({
  ok: "operando", warn: "atenção", alert: "alerta",
  perfurando: "perfurando", concretando: "concretando",
  ocioso: "ocioso", offline: "offline", erro: "erro",
})[s] || s || "—";

/* ============================================================
   GOOGLE MAPS — loader e helpers de geocoding
============================================================ */
let _googleMapsPromise = null;
function loadGoogleMaps() {
  if (_googleMapsPromise) return _googleMapsPromise;
  _googleMapsPromise = new Promise((resolve, reject) => {
    if (window.google && window.google.maps) return resolve(window.google);
    const key = window.GOOGLE_MAPS_KEY;
    if (!key) return reject(new Error("GOOGLE_MAPS_KEY ausente — confira /config.js"));
    const cb = "__gmapsCb_" + Math.random().toString(36).slice(2);
    window[cb] = () => { resolve(window.google); try { delete window[cb]; } catch (e) {} };
    const s = document.createElement("script");
    s.src = `https://maps.googleapis.com/maps/api/js?key=${encodeURIComponent(key)}&libraries=places,geocoding&language=pt-BR&region=BR&callback=${cb}`;
    s.async = true;
    s.defer = true;
    s.onerror = () => reject(new Error("Falha ao carregar o script do Google Maps"));
    document.head.appendChild(s);
  });
  return _googleMapsPromise;
}

// Preset dark do Google Maps. Aplicado via map.setOptions({styles}) quando
// document.documentElement[data-theme] === 'dark'. Em light, passamos
// styles: [] pra restaurar o estilo padrao.
const MAP_STYLE_DARK = [
  { elementType: "geometry", stylers: [{ color: "#1B2433" }] },
  { elementType: "labels.icon", stylers: [{ visibility: "off" }] },
  { elementType: "labels.text.fill", stylers: [{ color: "#8A93A6" }] },
  { elementType: "labels.text.stroke", stylers: [{ color: "#0F1827" }] },
  { featureType: "administrative", elementType: "geometry", stylers: [{ color: "#3A4660" }] },
  { featureType: "administrative.country", elementType: "labels.text.fill", stylers: [{ color: "#B8C2D6" }] },
  { featureType: "administrative.land_parcel", stylers: [{ visibility: "off" }] },
  { featureType: "administrative.locality", elementType: "labels.text.fill", stylers: [{ color: "#C5D0E5" }] },
  { featureType: "poi", elementType: "labels.text.fill", stylers: [{ color: "#8A93A6" }] },
  { featureType: "poi.park", elementType: "geometry", stylers: [{ color: "#162033" }] },
  { featureType: "poi.park", elementType: "labels.text.fill", stylers: [{ color: "#5A6478" }] },
  { featureType: "poi.park", elementType: "labels.text.stroke", stylers: [{ color: "#0F1827" }] },
  { featureType: "road", elementType: "geometry.fill", stylers: [{ color: "#2A3447" }] },
  { featureType: "road", elementType: "labels.text.fill", stylers: [{ color: "#8A93A6" }] },
  { featureType: "road.arterial", elementType: "geometry", stylers: [{ color: "#323D52" }] },
  { featureType: "road.highway", elementType: "geometry", stylers: [{ color: "#3A4660" }] },
  { featureType: "road.local", elementType: "labels.text.fill", stylers: [{ color: "#5A6478" }] },
  { featureType: "transit", elementType: "labels.text.fill", stylers: [{ color: "#5A6478" }] },
  { featureType: "water", elementType: "geometry", stylers: [{ color: "#0B131F" }] },
  { featureType: "water", elementType: "labels.text.fill", stylers: [{ color: "#3D4860" }] },
];

async function geocodeEndereco(query, extra = {}) {
  const g = await loadGoogleMaps();
  return new Promise((resolve, reject) => {
    new g.maps.Geocoder().geocode(
      { address: query, region: "br", componentRestrictions: { country: "br" }, ...extra },
      (results, status) => {
        if (status === "OK" && results && results[0]) {
          const loc = results[0].geometry.location;
          resolve({ lat: loc.lat(), lng: loc.lng(), formatted: results[0].formatted_address, raw: results[0] });
        } else {
          reject(new Error("Geocode falhou: " + status));
        }
      }
    );
  });
}

// Reverse geocoding: dado lat/lng, devolve componentes do endereco mais
// proximo. Usado pelo botao "Buscar endereço pelas coordenadas" no modal
// de edicao da maquina (preenche endereco/cidade a partir de lat/lon).
async function reverseGeocode(lat, lng) {
  const g = await loadGoogleMaps();
  return new Promise((resolve, reject) => {
    new g.maps.Geocoder().geocode({ location: { lat: Number(lat), lng: Number(lng) } }, (results, status) => {
      if (status === "OK" && results && results[0]) {
        resolve({ formatted: results[0].formatted_address, raw: results[0] });
      } else {
        reject(new Error("Reverse geocode falhou: " + status));
      }
    });
  });
}

async function geocodePorPlaceId(placeId) {
  const g = await loadGoogleMaps();
  return new Promise((resolve, reject) => {
    new g.maps.Geocoder().geocode({ placeId }, (results, status) => {
      if (status === "OK" && results && results[0]) {
        const loc = results[0].geometry.location;
        resolve({ lat: loc.lat(), lng: loc.lng(), formatted: results[0].formatted_address, raw: results[0] });
      } else {
        reject(new Error("Place details falhou: " + status));
      }
    });
  });
}

// Autocomplete de enderecos/POIs via Places API (New). Se a API nao
// estiver habilitada/liberada na chave do GCP, falha silenciosamente
// na primeira tentativa e desliga pro resto da sessao — evita console
// spam. UI continua funcional (CEP + geocode no save sao via Geocoding
// API que e' separada). `window.__autocompleteIndisponivel` e' inspecionado
// pela UI pra mostrar uma notinha discreta no modal.
let _autocompleteDesligado = false;
async function autocompleteEnderecos(input) {
  if (_autocompleteDesligado) return [];
  if (!input || !input.trim()) return [];
  const g = await loadGoogleMaps();
  try {
    const placesLib = await g.maps.importLibrary("places");
    const { AutocompleteSuggestion, AutocompleteSessionToken } = placesLib;
    const sessionToken = new AutocompleteSessionToken();
    const { suggestions } = await AutocompleteSuggestion.fetchAutocompleteSuggestions({
      input,
      sessionToken,
      language: "pt-BR",
      region: "br",
      includedRegionCodes: ["br"],
    });
    return (suggestions || [])
      .filter(s => s.placePrediction)
      .map(s => {
        const p = s.placePrediction;
        const txt = (v) => (v && typeof v === 'object' ? v.text : v) || "";
        return {
          description: txt(p.text),
          structured_formatting: {
            main_text: txt(p.mainText),
            secondary_text: txt(p.secondaryText),
          },
          place_id: p.placeId,
        };
      });
  } catch (e) {
    // Loga so a primeira vez (com instrucao) e desliga novas tentativas.
    if (!_autocompleteDesligado) {
      _autocompleteDesligado = true;
      window.__autocompleteIndisponivel = true;
      console.info(
        "[Autocomplete desligado] Places API (New) bloqueada. Habilite no GCP " +
        "e/ou ajuste API key restrictions. UI continua funcional via CEP/geocode."
      );
    }
    return [];
  }
}

function parseEnderecoGoogle(geocodeResult) {
  const comps = geocodeResult?.address_components || [];
  const get = (...types) => {
    for (const t of types) {
      const c = comps.find(x => x.types.includes(t));
      if (c) return c.long_name;
    }
    return "";
  };
  const rua = get("route");
  const bairro = get("sublocality_level_1", "sublocality", "neighborhood");
  return {
    rua: [rua, bairro].filter(Boolean).join(", ") || (geocodeResult?.formatted_address || "").split(",")[0],
    cidade: get("administrative_area_level_2", "locality") || "",
    uf: get("administrative_area_level_1") || "",
  };
}

/* ============================================================
   ICONS
============================================================ */
const I = {
  dash: (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" {...p}><path d="M3 12l9-8 9 8"/><path d="M5 10v10h14V10"/></svg>,
  pile: (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" {...p}><path d="M8 3h8M9 3v18M15 3v18M5 21h14"/><path d="M9 8h6M9 13h6M9 18h6"/></svg>,
  map:  (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" {...p}><path d="M9 4L3 6v14l6-2 6 2 6-2V4l-6 2-6-2z"/><path d="M9 4v14M15 6v14"/></svg>,
  signal:(p)=> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" {...p}><path d="M3 18a12 12 0 0118-10"/><path d="M6 18a8 8 0 0112-7"/><path d="M9 18a4 4 0 016-3"/><circle cx="12" cy="18" r="1.2" fill="currentColor"/></svg>,
  bell: (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" {...p}><path d="M6 16V11a6 6 0 1112 0v5l1.5 2h-15L6 16z"/><path d="M10 21h4"/></svg>,
  cog:  (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" {...p}><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.7 1.7 0 00.3 1.8l.1.1a2 2 0 11-2.8 2.8l-.1-.1a1.7 1.7 0 00-1.8-.3 1.7 1.7 0 00-1 1.5V21a2 2 0 11-4 0v-.1a1.7 1.7 0 00-1-1.5 1.7 1.7 0 00-1.8.3l-.1.1a2 2 0 11-2.8-2.8l.1-.1a1.7 1.7 0 00.3-1.8 1.7 1.7 0 00-1.5-1H3a2 2 0 110-4h.1a1.7 1.7 0 001.5-1 1.7 1.7 0 00-.3-1.8l-.1-.1a2 2 0 112.8-2.8l.1.1a1.7 1.7 0 001.8.3h.1a1.7 1.7 0 001-1.5V3a2 2 0 114 0v.1a1.7 1.7 0 001 1.5 1.7 1.7 0 001.8-.3l.1-.1a2 2 0 112.8 2.8l-.1.1a1.7 1.7 0 00-.3 1.8v.1a1.7 1.7 0 001.5 1H21a2 2 0 110 4h-.1a1.7 1.7 0 00-1.5 1z"/></svg>,
  search:(p)=> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" {...p}><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.3-4.3"/></svg>,
  arrow:(p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" {...p}><path d="M5 12h14M13 6l6 6-6 6"/></svg>,
  eye:  (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" {...p}><path d="M2 12s4-7 10-7 10 7 10 7-4 7-10 7S2 12 2 12z"/><circle cx="12" cy="12" r="3"/></svg>,
  up:   (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" {...p}><path d="M6 14l6-6 6 6"/></svg>,
  down: (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" {...p}><path d="M6 10l6 6 6-6"/></svg>,
  chev: (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="M9 6l6 6-6 6"/></svg>,
  plus: (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="M12 5v14M5 12h14"/></svg>,
  pencil: (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 113 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>,
  check: (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="M20 6L9 17l-5-5"/></svg>,
  undo:  (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="M3 7v6h6"/><path d="M3.51 13A9 9 0 1012 4a9 9 0 00-7.49 4.04L3 13"/></svg>,
  trash: (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="M3 6h18"/><path d="M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2"/><path d="M19 6l-1 14a2 2 0 01-2 2H8a2 2 0 01-2-2L5 6"/><path d="M10 11v6M14 11v6"/></svg>,
  user: (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" {...p}><circle cx="12" cy="8" r="4"/><path d="M4 21a8 8 0 0116 0"/></svg>,
  truck:(p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="M3 17V7h11v10M14 11h4l3 3v3h-7"/><circle cx="7.5" cy="17.5" r="2"/><circle cx="17.5" cy="17.5" r="2"/></svg>,
  clock:(p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" {...p}><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></svg>,
  sun:  (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" {...p}><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/></svg>,
  moon: (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="M21 12.79A9 9 0 1111.21 3 7 7 0 0021 12.79z"/></svg>,
  funnel:(p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="M3 5h18l-7 9v6l-4-2v-4L3 5z"/></svg>,
};

// ── Tema (light/dark) ─────────────────────────────────────────────
// Persistencia simples em localStorage. Valor padrao = "light". O
// inline script no index.html ja aplica o atributo antes do React
// montar (evita flash); o useTema mantem o estado em sincronia.
const TEMA_KEY = 'compugeo_theme';

// Quantidade de pastas mostradas inicialmente na sidebar. O usuario carrega
// mais clicando no botao no fim da lista. Valor sera configuravel pela
// tela de configuracoes (futuro) — por isso ja persiste em localStorage.
const PASTAS_PAG_KEY = 'compugeo_pastas_pagina';
const PASTAS_PAG_PADRAO = 25;
function lerPastasPagina() {
  try {
    const v = parseInt(localStorage.getItem(PASTAS_PAG_KEY), 10);
    return Number.isFinite(v) && v > 0 ? v : PASTAS_PAG_PADRAO;
  } catch (_) { return PASTAS_PAG_PADRAO; }
}

// Helper compartilhado: salva uma alteracao parcial nas preferencias do
// usuario via PUT /api/configuracoes. O backend faz merge dentro de
// `preferencias_usuario.dados`, entao mandar so o caminho alterado
// (`{ dashboard: { producaoMensal: { tipoGrafico: 'barras' } } }`)
// nao apaga as outras chaves.
function salvarPrefUsuario(parcial) {
  return fetch('/api/configuracoes', {
    method: 'PUT',
    credentials: 'same-origin',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ dados: parcial }),
  }).catch(() => {});
}
function lerTemaSalvo() {
  // Default de fabrica = dark. So volta 'light' se o usuario salvou
  // explicitamente esse valor no localStorage.
  try {
    const t = localStorage.getItem(TEMA_KEY);
    return t === 'light' ? 'light' : 'dark';
  } catch (_) { return 'dark'; }
}
function aplicarTema(tema) {
  document.documentElement.setAttribute('data-theme', tema);
  try { localStorage.setItem(TEMA_KEY, tema); } catch (_) {}
  // Sincroniza o theme-color (status-bar do iOS PWA / Chrome Android) com
  // a cor da .dash-head do tema ativo. Sem isso, ao alternar tema dentro
  // do app a tira do topo continuava na cor do tema antigo.
  try {
    let meta = document.querySelector('meta[name="theme-color"]:not([media])');
    if (!meta) {
      meta = document.createElement('meta');
      meta.setAttribute('name', 'theme-color');
      document.head.appendChild(meta);
    }
    meta.setAttribute('content', tema === 'dark' ? '#121C2B' : '#0B2545');
  } catch (_) {}
}
function useTema() {
  const [tema, setTema] = useState(() => lerTemaSalvo());
  // Salva no servidor (preferencias_usuario.dados.preferencias.tema). Em
  // paralelo continua escrevendo no localStorage via aplicarTema pra
  // garantir o "no-flash" no proximo boot (o inline script em index.html
  // le localStorage antes de qualquer fetch). Sync remoto e' fire-and-
  // forget — se falhar, o local ainda foi gravado.
  const salvarRemoto = (novo) => {
    salvarPrefUsuario({ preferencias: { tema: novo } });
  };
  const alternar = () => setTema(t => {
    const novo = t === 'dark' ? 'light' : 'dark';
    aplicarTema(novo);
    salvarRemoto(novo);
    return novo;
  });
  const definir = (novo) => {
    if (novo !== 'light' && novo !== 'dark') return;
    aplicarTema(novo);
    salvarRemoto(novo);
    setTema(novo);
  };
  return [tema, alternar, definir];
}

function TemaToggle({ tema, onAlternar }) {
  const titulo = tema === 'dark' ? 'Trocar para tema claro' : 'Trocar para tema escuro';
  return (
    <button
      className="dash-icon-btn"
      title={titulo}
      aria-label={titulo}
      onClick={onAlternar}
    >
      {tema === 'dark' ? <I.sun width="18" height="18"/> : <I.moon width="18" height="18"/>}
    </button>
  );
}

/* ============================================================
   LOGIN
============================================================ */
function LoginScreen({ accent, onLogin }) {
  const [user, setUser] = useState("");
  const [pw, setPw] = useState("");
  const [show, setShow] = useState(false);
  const [remember, setRemember] = useState(true);
  const [loading, setLoading] = useState(false);
  const [erro, setErro] = useState("");

  const onSubmit = async (e) => {
    e.preventDefault();
    setErro("");
    const email = user.trim();
    if (!email || !pw) {
      setErro("Informe e-mail e senha.");
      return;
    }
    setLoading(true);
    try {
      const resp = await fetch("/api/login", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        credentials: "same-origin",
        body: JSON.stringify({ email, senha: pw, manter: remember }),
      });
      if (!resp.ok) {
        const corpo = await resp.json().catch(() => ({}));
        setLoading(false);
        setErro(corpo.erro || "Falha no login.");
        return;
      }
      const corpo = await resp.json();
      setLoading(false);
      onLogin({
        cliente: corpo.cliente || "",
        admin: !!corpo.admin,
        papel: corpo.papel || (corpo.admin ? "admin_sistema" : "usuario"),
        nome: corpo.nome || "",
        email: corpo.email || "",
        politicaAceita: !!corpo.politicaAceita,
      });
    } catch (err) {
      setLoading(false);
      setErro("Não foi possível conectar. Tente novamente.");
    }
  };

  return (
    <div className="login-root">
      <aside className="login-brand">
        <div className="login-brand-inner">
          <div className="login-logo">
            <img
              className="login-logo-img login-logo-img-light"
              src="/img/logo-horizontal.svg"
              alt="Compugeo"
            />
            <img
              className="login-logo-img login-logo-img-dark"
              src="/img/logo-horizontal-white.svg"
              alt="Compugeo"
            />
          </div>

          <div className="login-hero">
            <div className="login-hero-eyebrow">v0.1 · 2026</div>
            <h1 className="login-hero-title">
              Estacas, fundações e instrumentação<br/>em <em style={{ color: accent.hex, fontStyle: "normal" }}>tempo real.</em>
            </h1>
            <p className="login-hero-body">
              Acompanhe a perfuração, o consumo de concreto e a integridade
              estrutural de cada elemento de fundação direto do canteiro.
            </p>
          </div>

          <div className="login-viz">
            <div className="login-viz-head">
              <span>EXEC-2154 · Estaca E-117</span>
              <span style={{ color: accent.hex }}>● ao vivo</span>
            </div>
            <svg viewBox="0 0 320 120" className="login-viz-svg">
              <defs>
                <linearGradient id="lvg" x1="0" x2="0" y1="0" y2="1">
                  <stop offset="0" stopColor={accent.hex} stopOpacity=".35"/>
                  <stop offset="1" stopColor={accent.hex} stopOpacity="0"/>
                </linearGradient>
              </defs>
              {[20,40,60,80,100].map(y => <line key={y} x1="0" x2="320" y1={y} y2={y} stroke="rgba(255,255,255,.06)"/>)}
              <path d="M0 90 L20 70 L40 75 L60 55 L80 60 L100 40 L120 50 L140 35 L160 25 L180 30 L200 20 L220 28 L240 18 L260 22 L280 14 L300 18 L320 12 L320 120 L0 120 Z" fill="url(#lvg)"/>
              <path d="M0 90 L20 70 L40 75 L60 55 L80 60 L100 40 L120 50 L140 35 L160 25 L180 30 L200 20 L220 28 L240 18 L260 22 L280 14 L300 18 L320 12" fill="none" stroke={accent.hex} strokeWidth="1.6"/>
            </svg>
            <div className="login-viz-foot">
              <div><span className="lvf-k">Profundidade</span><span className="lvf-v">18.4 m</span></div>
              <div><span className="lvf-k">Pressão</span><span className="lvf-v">42 bar</span></div>
              <div><span className="lvf-k">Concreto</span><span className="lvf-v">9.2 m³</span></div>
            </div>
          </div>

          <footer className="login-foot">
            <span>© 2026 Compugeo Tecnologia</span>
            <span>Termos · Privacidade · Suporte</span>
          </footer>
        </div>
      </aside>

      <main className="login-main">
        <div className="login-card">
          <div className="login-card-eyebrow">ENTRAR NA PLATAFORMA</div>
          <h2 className="login-card-title">Bem-vindo de volta</h2>
          <p className="login-card-sub">Acesse o painel da sua obra para continuar.</p>

          <form className="login-form" onSubmit={onSubmit}>
            <label className="login-field">
              <span className="login-field-label">E-mail</span>
              <input
                type="email"
                value={user}
                onChange={(e)=>setUser(e.target.value)}
                placeholder="seu@email.com"
                autoComplete="username"
              />
            </label>

            <label className="login-field">
              <span className="login-field-label">
                Senha
                <a className="login-link" href="#" style={{ color: accent.hex }}>Esqueci minha senha</a>
              </span>
              <div className="login-pw">
                <input
                  type={show?"text":"password"}
                  value={pw}
                  onChange={(e)=>setPw(e.target.value)}
                  autoComplete="current-password"
                />
                <button type="button" onClick={()=>setShow(s=>!s)} className="login-pw-eye" aria-label="mostrar senha">
                  <I.eye width="18" height="18"/>
                </button>
              </div>
            </label>

            <div className="login-row">
              <label className="login-check">
                <input type="checkbox" checked={remember} onChange={(e)=>setRemember(e.target.checked)} />
                <span className="login-check-box" style={{ borderColor: remember ? accent.hex : undefined, background: remember ? accent.hex : "transparent" }}>
                  {remember && <svg viewBox="0 0 16 16" width="11" height="11" fill="none" stroke="#fff" strokeWidth="2.4"><path d="M3 8.5L6.5 12 13 4.5"/></svg>}
                </span>
                Manter conectado neste dispositivo
              </label>
            </div>

            {erro && <div className="login-error" role="alert">{erro}</div>}

            <button type="submit" className="login-submit" style={{ background: accent.hex }} disabled={loading}>
              {loading ? <span className="login-spinner"/> : <>Entrar <I.arrow width="18" height="18"/></>}
            </button>
          </form>
        </div>
      </main>
    </div>
  );
}

/* ============================================================
   SELEÇÃO DE EMPRESA (admin)
============================================================ */
// Tela de aceite da politica de privacidade. Mostrada na PRIMEIRA sessao
// (politica_aceita_em IS NULL no banco). Bloqueia o acesso ao Dashboard
// ate o usuario marcar o checkbox e clicar em "Aceitar e continuar".
// Tem botao "Sair" pra quem nao quer aceitar — derruba a sessao.
// Layout responsivo: em mobile (<600px) ocupa tela cheia, sem bordas
// arredondadas; botoes empilhados. Em desktop, card centralizado.
function PoliticaScreen({ accent, onAceito, onSair }) {
  const [aceito, setAceito] = useState(false);
  const [salvando, setSalvando] = useState(false);
  const [erro, setErro] = useState(null);
  const [mobile, setMobile] = useState(() => typeof window !== "undefined" && window.innerWidth < 600);

  useEffect(() => {
    const onResize = () => setMobile(window.innerWidth < 600);
    window.addEventListener("resize", onResize);
    return () => window.removeEventListener("resize", onResize);
  }, []);

  const enviar = async () => {
    if (!aceito || salvando) return;
    setSalvando(true);
    setErro(null);
    try {
      const r = await fetch("/api/politica-aceitar", {
        method: "POST",
        credentials: "same-origin",
      });
      if (!r.ok) {
        const j = await r.json().catch(() => ({}));
        throw new Error(j.erro || `Erro ${r.status}`);
      }
      onAceito();
    } catch (e) {
      setErro(String(e.message || e));
      setSalvando(false);
    }
  };

  return (
    <div style={{
      minHeight: "100vh",
      background: "var(--bg, #fafafa)",
      display: "flex",
      alignItems: mobile ? "stretch" : "center",
      justifyContent: "center",
      padding: mobile ? 0 : 20,
    }}>
      <div style={{
        maxWidth: 720,
        width: "100%",
        background: "var(--surface, #fff)",
        borderRadius: mobile ? 0 : 12,
        boxShadow: mobile ? "none" : "0 10px 40px rgba(0,0,0,.12)",
        overflow: "hidden",
        display: "flex",
        flexDirection: "column",
        maxHeight: mobile ? "100vh" : "90vh",
        minHeight: mobile ? "100vh" : undefined,
      }}>
        <div style={{ padding: mobile ? "18px 16px 12px" : "28px 32px 16px", borderBottom: "1px solid var(--border, #e5e7eb)" }}>
          <div style={{ fontSize: 11, letterSpacing: 1.2, color: "var(--muted, #6b7280)", textTransform: "uppercase", fontWeight: 600 }}>
            Primeiro acesso
          </div>
          <h1 style={{ margin: "4px 0 0", fontSize: mobile ? 18 : 22, fontWeight: 700, color: "var(--ink, #1f2937)" }}>
            Política de Privacidade
          </h1>
          <p style={{ margin: "8px 0 0", color: "var(--ink-2, #4b5563)", fontSize: 13, lineHeight: 1.45 }}>
            Antes de continuar, leia e aceite a Política de Privacidade. Você só precisa fazer isso uma vez.
          </p>
        </div>

        <div style={{ flex: 1, overflow: "hidden", background: "var(--bg, #fafafa)" }}>
          <iframe
            src="/politica-privacidade.html"
            title="Política de Privacidade"
            style={{
              width: "100%",
              height: "100%",
              minHeight: mobile ? 240 : 360,
              border: "none",
              display: "block",
            }}
          />
        </div>

        <div style={{ padding: mobile ? "14px 16px 16px" : "16px 32px 20px", borderTop: "1px solid var(--border, #e5e7eb)" }}>
          <label style={{ display: "flex", alignItems: "flex-start", gap: 10, fontSize: 13, color: "var(--ink, #1f2937)", cursor: "pointer", userSelect: "none", lineHeight: 1.4 }}>
            <input
              type="checkbox"
              checked={aceito}
              onChange={(e) => setAceito(e.target.checked)}
              disabled={salvando}
              style={{ marginTop: 2, width: 16, height: 16, cursor: "pointer", flexShrink: 0 }}
            />
            <span>
              Li e aceito a <a href="/politica-privacidade.html" target="_blank" rel="noopener" style={{ color: accent.hex }}>Política de Privacidade</a> da Compugeo.
              Estou ciente de quais dados pessoais são coletados, para qual finalidade, e dos meus direitos como titular.
            </span>
          </label>

          {erro && (
            <div style={{ marginTop: 10, padding: "8px 10px", background: "#fef2f2", color: "#dc2626", borderRadius: 6, fontSize: 13 }}>
              {erro}
            </div>
          )}

          <div style={{
            display: "flex",
            flexDirection: mobile ? "column-reverse" : "row",
            justifyContent: mobile ? "stretch" : "space-between",
            alignItems: mobile ? "stretch" : "center",
            marginTop: 16,
            gap: mobile ? 8 : 12,
          }}>
            <button
              type="button"
              onClick={onSair}
              disabled={salvando}
              style={{
                padding: mobile ? "12px 16px" : "10px 16px",
                background: "transparent",
                color: "var(--muted, #6b7280)",
                border: "none",
                cursor: salvando ? "default" : "pointer",
                fontSize: 14,
              }}
            >
              Sair sem aceitar
            </button>
            <button
              type="button"
              onClick={enviar}
              disabled={!aceito || salvando}
              style={{
                padding: mobile ? "12px 20px" : "10px 20px",
                background: (!aceito || salvando) ? "var(--muted, #9ca3af)" : accent.hex,
                color: "#fff",
                border: "none",
                borderRadius: 6,
                cursor: (!aceito || salvando) ? "not-allowed" : "pointer",
                fontSize: 14,
                fontWeight: 600,
                opacity: (!aceito || salvando) ? 0.6 : 1,
              }}
            >
              {salvando ? "Salvando…" : "Aceitar e continuar"}
            </button>
          </div>
        </div>
      </div>
    </div>
  );
}

function EmpresaSelectScreen({ accent, onSelecionar, onSair }) {
  const [empresas, setEmpresas] = useState([]);
  const [filtro, setFiltro] = useState("");
  const [carregando, setCarregando] = useState(true);
  const [erro, setErro] = useState("");
  const [enviando, setEnviando] = useState(false);

  useEffect(() => {
    let vivo = true;
    (async () => {
      try {
        const r = await fetch("/api/empresas", { credentials: "same-origin" });
        if (!r.ok) {
          if (!vivo) return;
          setErro("Falha ao carregar empresas.");
          setCarregando(false);
          return;
        }
        const lista = await r.json();
        if (!vivo) return;
        setEmpresas(Array.isArray(lista) ? lista : []);
        setCarregando(false);
      } catch {
        if (!vivo) return;
        setErro("Não foi possível conectar.");
        setCarregando(false);
      }
    })();
    return () => { vivo = false; };
  }, []);

  const escolher = async (empresa) => {
    setErro("");
    setEnviando(true);
    try {
      const r = await fetch("/api/selecionar-empresa", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        credentials: "same-origin",
        body: JSON.stringify({ empresa }),
      });
      if (!r.ok) {
        const c = await r.json().catch(() => ({}));
        setErro(c.erro || "Falha ao selecionar empresa.");
        setEnviando(false);
        return;
      }
      const c = await r.json();
      onSelecionar(c.cliente || empresa);
    } catch {
      setErro("Não foi possível conectar.");
      setEnviando(false);
    }
  };

  const filtradas = empresas.filter(e =>
    e.toLowerCase().includes(filtro.trim().toLowerCase())
  );

  return (
    <div className="login-root">
      <main className="login-main" style={{ width: "100%" }}>
        <div className="login-card" style={{ maxWidth: 520 }}>
          <div className="login-card-eyebrow">ADMINISTRADOR</div>
          <h2 className="login-card-title">Selecione a empresa</h2>
          <p className="login-card-sub">
            Você está autenticado como administrador. Escolha a empresa que deseja acessar.
          </p>

          <label className="login-field" style={{ marginBottom: 16 }}>
            <span className="login-field-label">Buscar empresa</span>
            <input
              type="text"
              value={filtro}
              onChange={(e) => setFiltro(e.target.value)}
              placeholder="Digite para filtrar…"
              autoFocus
            />
          </label>

          {erro && <div className="login-error" role="alert">{erro}</div>}

          <div
            style={{
              display: "flex",
              flexDirection: "column",
              gap: 8,
              maxHeight: 320,
              overflowY: "auto",
              border: "1px solid var(--border, #e5e7eb)",
              borderRadius: 10,
              padding: 8,
              background: "var(--bg-soft, #fafafa)",
            }}
          >
            {carregando && <div style={{ padding: 12, color: "var(--muted)" }}>Carregando…</div>}
            {!carregando && filtradas.length === 0 && (
              <div style={{ padding: 12, color: "var(--muted)" }}>
                {empresas.length === 0 ? "Nenhuma empresa cadastrada." : "Nenhuma empresa encontrada."}
              </div>
            )}
            {!carregando && filtradas.map((e) => (
              <button
                key={e}
                type="button"
                disabled={enviando}
                onClick={() => escolher(e)}
                style={{
                  textAlign: "left",
                  padding: "12px 14px",
                  borderRadius: 8,
                  border: "1px solid var(--border, #e5e7eb)",
                  background: "#fff",
                  cursor: enviando ? "progress" : "pointer",
                  fontSize: 14,
                  fontWeight: 500,
                  color: "var(--fg, #111)",
                  display: "flex",
                  alignItems: "center",
                  justifyContent: "space-between",
                }}
              >
                <span>{e}</span>
                <span style={{ color: accent.hex, fontSize: 13 }}>Acessar →</span>
              </button>
            ))}
          </div>

          <button
            type="button"
            onClick={onSair}
            className="login-submit"
            style={{ marginTop: 20, background: "transparent", color: accent.hex, border: `1px solid ${accent.hex}` }}
            disabled={enviando}
          >
            Sair
          </button>
        </div>
      </main>
    </div>
  );
}

/* ============================================================
   DASHBOARD
============================================================ */
const SPARK_VAZIA = [0, 0, 0, 0, 0, 0, 0];
const KPI_VAZIO = { valor: 0, delta: null, sparkline: SPARK_VAZIA };
const KPI_MAQUINAS_VAZIO = { valor: "0/0", delta: null, sparkline: [], media: 0, taxa: 0, online: 0, total: 0 };
const KPIS_VAZIOS = { estacas: KPI_VAZIO, metros: KPI_VAZIO, concreto: KPI_VAZIO, maquinas: KPI_MAQUINAS_VAZIO };

const fmtInt = n => Math.round(Number(n) || 0).toLocaleString("pt-BR");

function Dashboard({ accent, cliente, papel = "usuario", nome = "", email = "", onClienteChange, onSair }) {
  // Helpers de permissao (defesa em profundidade — backend e a fonte real
  // da verdade; aqui so escondemos elementos para nao confundir o usuario).
  const podeAdministrar = papel === "admin_sistema" || papel === "admin_cliente";
  const ehAdminSistema = papel === "admin_sistema";

  const [tema, alternarTema, definirTema] = useTema();

  // Drawer da sidebar em mobile (<768px). Em desktop fica sempre visivel,
  // entao esse estado e ignorado pelo CSS.
  const [sidebarAberto, setSidebarAberto] = useState(false);
  const fecharSidebar = useCallback(() => setSidebarAberto(false), []);

  // Lista de empresas pra o dropdown de filtro do admin_sistema.
  const [empresas, setEmpresas] = useState([]);
  useEffect(() => {
    if (!ehAdminSistema) return;
    fetch("/api/empresas", { credentials: "same-origin" })
      .then(r => r.ok ? r.json() : [])
      .then(arr => setEmpresas(Array.isArray(arr) ? arr : []))
      .catch(() => {});
  }, [ehAdminSistema]);

  // Troca o cliente filtrado (ou limpa para "todos"). Re-emite o JWT.
  const trocarCliente = useCallback(async (alvo) => {
    try {
      const r = await fetch("/api/selecionar-empresa", {
        method: "POST",
        credentials: "same-origin",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ empresa: alvo || "" }),
      });
      if (!r.ok) {
        const e = await r.json().catch(() => ({}));
        throw new Error(e.erro || `Erro ${r.status}`);
      }
      const j = await r.json();
      if (typeof onClienteChange === "function") onClienteChange(j.cliente || "");
    } catch (e) {
      window.alert(`Falha ao trocar cliente: ${e.message || e}`);
    }
  }, [onClienteChange]);
  const [active, setActive] = useState("dashboard");
  // Obra atualmente em foco na tela de detalhes (clicada no FrontList).
  // null quando nao ha obra selecionada.
  const [obraSelecionada, setObraSelecionada] = useState(null);
  const [estacasOpen, setEstacasOpen] = useState(false);
  const [pastas, setPastas] = useState(null);
  const [pastasErr, setPastasErr] = useState(null);
  // Maquinas pra alimentar o mini-mapa do Dashboard (mesmos dados que
  // a MapaPage usa). Carrega 1 vez ao montar — o usuario abre a tela
  // cheia pra editar/atualizar.
  const [maquinasMapa, setMaquinasMapa] = useState([]);
  useEffect(() => {
    fetch('/api/maquinas-mapa', { credentials: 'same-origin' })
      .then(r => r.ok ? r.json() : [])
      .then(arr => setMaquinasMapa(Array.isArray(arr) ? arr : []))
      .catch(() => {});
  }, []);
  const obrasMiniMapa = React.useMemo(() => maquinasMapa
    .filter(m => {
      if (m.lat == null || m.lon == null) return false;
      const lat = Number(m.lat), lon = Number(m.lon);
      if (!Number.isFinite(lat) || !Number.isFinite(lon)) return false;
      if (lat === 0 && lon === 0) return false;
      if (lat < -90 || lat > 90 || lon < -180 || lon > 180) return false;
      return true;
    })
    .map(m => ({
      id: m.id,
      nome: m.tag || m.nome || m.num_serie,
      bairro: m.cidade || '',
      lat: Number(m.lat),
      lng: Number(m.lon),
      status: m.status,
      estacas: 0,
    })), [maquinasMapa]);
  // Paginacao da lista de pastas na sidebar. O passo de carregamento (+25
  // por padrao) virá da tela de configuracoes; aqui só lemos do storage.
  const [limitePastas, setLimitePastas] = useState(() => lerPastasPagina());
  // Reset ao recarregar a lista pra nao manter estado de "muitas paginas
  // abertas" entre cargas (ex: depois de salvar uma copia editada).
  useEffect(() => { setLimitePastas(lerPastasPagina()); }, [pastas]);
  const [pastaAberta, setPastaAberta] = useState(null);
  const [maquinaAberta, setMaquinaAberta] = useState(null);
  // Filtro especial vindo da busca livre (ex.: ">25"). Quando setado,
  // mostra a planilha PastaPlanilha alimentada por /api/lista-prof.
  const [filtroProf, setFiltroProf] = useState(null); // { operador, metros }
  const [planilha, setPlanilha] = useState(null);
  const [planilhaCarregando, setPlanilhaCarregando] = useState(false);
  const [estacaAberta, setEstacaAberta] = useState(null);
  // De onde a estaca aberta foi acessada: "dashboard" (widget "Ultimas
  // estacas executadas") ou "planilha" (pasta lateral, busca, filtro). Usado
  // pelo rotulo do botao Voltar do EstacaGrafico em mobile ("← Dashboard"
  // vs "← Planilha"). null quando nenhuma estaca esta' aberta.
  const [origemEstaca, setOrigemEstaca] = useState(null);
  // Lista de navegação ativa (origem: pasta clicada OU últimas estacas).
  // Cada item: { pasta, arquivo }. Usada pelo foot-nav e pelo ↑/↓.
  const [navLista, setNavLista] = useState([]);
  // Tipo da pasta atualmente expandida na sidebar — "contrato" ou
  // "obra-editada". Necessario porque uma obra editada pode ter o mesmo
  // nome de um contrato existente.
  const [tipoPastaAberta, setTipoPastaAberta] = useState(null);
  // Fluxo de edicao manual da estaca (duplo-clique). Etapa 1: pedir login
  // de admin; etapa 2: abrir o editor com os dados da estaca selecionada.
  const [estacaParaEditar, setEstacaParaEditar] = useState(null); // { arquivo, contrato }
  // Pos-admin reauth, se a sessao nao tinha operador, pedimos credenciais
  // dele antes de abrir o editor — admin nao pode aparecer como autor.
  const [operadorParaIdentificar, setOperadorParaIdentificar] = useState(null); // { arquivo }
  const [editorAberto, setEditorAberto] = useState(null);          // { arquivo, contrato }
  // Apos autorizar a edicao, perguntamos se o usuario quer abrir na mesma
  // aba (modal sobre o dashboard) ou em uma nova aba (editor-tab.html).
  const [escolhaTab, setEscolhaTab] = useState(null);              // { arquivo, contrato }
  // Apos autenticar admin para um contrato, libera todas as estacas dele
  // sem pedir senha novamente. Trocando de contrato (pasta), volta a pedir.
  const [contratoAutorizado, setContratoAutorizado] = useState(null);
  const [kpis, setKpis] = useState(KPIS_VAZIOS);
  const [metricaProd, setMetricaProd] = useState("estacas");
  const [granProd, setGranProd] = useState("mensal"); // "mensal" | "diaria"
  const [periodoProd, setPeriodoProd] = useState(6);  // meses (mensal) ou dias (diaria)
  const [maquinaProd, setMaquinaProd] = useState(""); // "" = todas; senao num_serie
  const [maquinasProd, setMaquinasProd] = useState([]); // [{numSerie, tag}]
  const [diametroProd, setDiametroProd] = useState(""); // "" = todos; senao mm
  const [diametrosProd, setDiametrosProd] = useState([]); // [{diametro, estacas}]
  // tipoGrafico e ocultarDiasSemProducao agora moram em preferencias_usuario
  // (sincronizam entre dispositivos do mesmo login). Os defaults aplicam ate
  // o fetch de /api/configuracoes chegar — quando chega, o useEffect abaixo
  // sobrescreve via setCfgDashboard.
  const [tipoGraficoProd, setTipoGraficoProdState] = useState('linha');
  const setTipoGraficoProd = useCallback((v) => {
    const novo = v === 'barras' ? 'barras' : 'linha';
    setTipoGraficoProdState(novo);
    salvarPrefUsuario({ dashboard: { producaoMensal: { tipoGrafico: novo } } });
  }, []);
  const [ocultarSemProd, setOcultarSemProdState] = useState(false);
  const setOcultarSemProd = useCallback((v) => {
    const novo = !!v;
    setOcultarSemProdState(novo);
    salvarPrefUsuario({ dashboard: { producaoMensal: { ocultarDiasSemProducao: novo } } });
  }, []);
  // Config do dashboard carregada de /api/configuracoes — define ordem
  // dos KPIs, tipo do grafico de producao, ocultar dias sem producao,
  // e os defaults dos selects (aplicados so na primeira vez).
  const [cfgDashboard, setCfgDashboard] = useState(null);
  const [producao, setProducao] = useState([]);
  const [toasts, setToasts] = useState([]);
  const [importandoDmp, setImportandoDmp] = useState(false);
  // Estado da importacao em lote de .cge: null quando ocioso; durante a
  // importacao guarda { fase, total, processadas, atual, resumo, erro }.
  // Ao fim, vira { fase: 'pronto', resumo: {...} } e o modal mostra o
  // sumario; usuario fecha manualmente para o estado voltar a null.
  const [importCge, setImportCge] = useState(null);
  const [importMenuAberto, setImportMenuAberto] = useState(false);
  // Modal de cadastro manual de obra (botao "Adicionar nova obra" do dash).
  const [novaObraAberta, setNovaObraAberta] = useState(false);
  // Quando preenchido, o mesmo modal abre em modo edicao com prefill.
  const [obraEditando, setObraEditando] = useState(null);
  // {nome, contrato} pra abrir o modal de criar com esses campos prefilled
  // (vem do fluxo de "obra detectada nao cadastrada").
  const [novaObraInicial, setNovaObraInicial] = useState(null);
  // Bump pra forcar o FrontList a buscar novamente apos cadastro/edicao de obra.
  const [obrasReloadKey, setObrasReloadKey] = useState(0);
  // Pares (obra, contrato) detectados em estacas novas mas sem cadastro.
  const [orfaos, setOrfaos] = useState([]);
  const [orfaosAberto, setOrfaosAberto] = useState(false);
  // Pares dispensados pelo usuario nesta sessao — nao reabre o modal pra
  // eles. Persiste em sessionStorage pra sobreviver navegacao interna.
  const dispensadosOrfaosRef = useRef(null);
  const [refreshKey, setRefreshKey] = useState(0);
  // Bump usado para forcar o iframe do EstacaGrafico a recarregar os dados
  // quando o arquivo aberto nao muda mas o conteudo mudou (ex: salvar uma
  // copia editada existente). Quando o arquivo muda (original → copia
  // nova), nao precisa do bump — a propria mudanca de prop dispara reload.
  const [chartReloadKey, setChartReloadKey] = useState(0);
  const inputDmpRef = useRef(null);
  const inputCgeRef = useRef(null);
  const inputCgePastaRef = useRef(null);
  const importMenuRef = useRef(null);
  const importEsRef = useRef(null);

  // Estado de "estaca nao lida" e' calculado pelo servidor (estaca_leitura)
  // e vem em cada item de `pastas` como `arquivosNaoLidos: [...]`.
  // Aqui mantemos apenas helpers que leem desses campos. Sem localStorage:
  // estado persiste no banco e funciona em qualquer device/browser.
  // Cache dos arquivos ja vistos no PileTable, mantido aqui (no
  // Dashboard) e nao dentro do PileTable. Razao: o PileTable e
  // desmontado toda vez que o usuario navega para outra tela
  // (pasta, maquina, grafico, mapa); se o cache morresse junto,
  // arquivos novos chegados durante a ausencia do usuario seriam
  // tratados como ja conhecidos no remount e nunca virariam toast.
  const conhecidasUltimasRef = useRef(null);
  // Limite de 2 abas de edicao simultaneas. Guarda as `Window` retornadas
  // por window.open pra contar quantas ainda estao abertas (descarta as
  // que o usuario ja fechou via `w.closed`). Reseta no remount do
  // Dashboard (troca de cliente) e no reload do browser — aceitavel.
  const abasEditorAbertasRef = useRef([]);

  const notify = useCallback((mensagem, tipo = "info") => {
    const id = Date.now() + Math.random();
    setToasts(t => [...t, { id, mensagem, tipo }]);
    setTimeout(() => setToasts(t => t.filter(x => x.id !== id)), 6000);
  }, []);

  // Inicializa o Set de pares dispensados a partir do sessionStorage.
  useEffect(() => {
    try {
      const raw = sessionStorage.getItem("compugeo_obras_orfaos_dispensadas");
      dispensadosOrfaosRef.current = new Set(raw ? JSON.parse(raw) : []);
    } catch (_) {
      dispensadosOrfaosRef.current = new Set();
    }
  }, []);

  // Detecta obras nao cadastradas a partir das estacas novas. Dedupe por
  // par (obra, contrato); ignora vazios e pares ja dispensados; intersecta
  // com /api/obras pra remover os ja cadastrados.
  const checarOrfaos = useCallback(async (novasLista) => {
    if (!dispensadosOrfaosRef.current) return;
    // Respeita a config do dashboard — usuario pode desligar o aviso.
    if (cfgDashboard && cfgDashboard.detectarObrasOrfas === false) return;
    // admin_sistema em modo "ver tudo" (cliente vazio): /api/ultimas-estacas
    // devolve estacas de TODOS os clientes e /api/obras devolve [] — a
    // intersecao abaixo trataria todo par (obra, contrato) como orfao,
    // misturando dados de clientes diferentes no modal. Pula a deteccao
    // ate o admin selecionar uma empresa. Cadastrar uma obra nesse modo
    // ja' e' bloqueado pelo servidor (POST /api/obras → 403).
    if (papel === 'admin_sistema' && !cliente) return;
    const paresMap = new Map();
    for (const e of novasLista || []) {
      const nome = String(e.obra || "").trim();
      const contrato = String(e.contrato || "").trim();
      if (!nome || !contrato) continue;
      const key = `${nome.toLowerCase()}|${contrato.toLowerCase()}`;
      if (dispensadosOrfaosRef.current.has(key)) continue;
      if (!paresMap.has(key)) paresMap.set(key, { nome, contrato, estacas: 0 });
      paresMap.get(key).estacas++;
    }
    if (paresMap.size === 0) return;

    try {
      const r = await fetch("/api/obras", { credentials: "same-origin" });
      if (!r.ok) return;
      const cadastradas = await r.json();
      const cadastradasSet = new Set(
        (Array.isArray(cadastradas) ? cadastradas : []).map(o =>
          `${String(o.nome || "").toLowerCase()}|${String(o.contrato || "").toLowerCase()}`
        )
      );
      for (const key of [...paresMap.keys()]) {
        if (cadastradasSet.has(key)) paresMap.delete(key);
      }
    } catch (_) {
      return;
    }
    if (paresMap.size === 0) return;

    // Merge com orfaos atuais (caso o modal ja esteja aberto com outros).
    setOrfaos(prev => {
      const merged = new Map(prev.map(o => [
        `${o.nome.toLowerCase()}|${o.contrato.toLowerCase()}`, { ...o },
      ]));
      for (const [key, val] of paresMap) {
        if (!merged.has(key)) merged.set(key, val);
        else merged.get(key).estacas += val.estacas;
      }
      return [...merged.values()];
    });
    setOrfaosAberto(true);
  }, [cfgDashboard, papel, cliente]);

  // Wrapper que o PileTable chama: roda o notify habitual + dispara a
  // verificacao de orfaos quando vem a lista de estacas novas.
  const handleNovasEstacas = useCallback((mensagem, novasLista) => {
    notify(mensagem);
    if (Array.isArray(novasLista) && novasLista.length > 0) {
      checarOrfaos(novasLista);
    }
    // Refaz o fetch da lista de obras pra atualizar a contagem de
    // estacas executadas (calculada via JOIN no backend, mas o front
    // so re-busca quando o reloadKey muda).
    setObrasReloadKey(k => k + 1);
  }, [notify, checarOrfaos]);

  // Carrega config de Dashboard:
  //   - sempre que o usuario entra/volta para a tela "dashboard"
  //     (pra refletir mudancas feitas em Configuracoes sem precisar F5);
  //   - os defaults dos selects (metrica/granularidade/periodo/maquina)
  //     aplicam APENAS no primeiro mount, pra nao atropelar selecoes
  //     que o usuario fez nos selects do proprio dashboard.
  const aplicouDefaultsProdRef = useRef(false);
  // Sinaliza que o primeiro fetch de /api/configuracoes terminou. Antes
  // disso, o card de Producao nao busca dados nem renderiza o chart —
  // evita o flash "linha 6 meses" seguido de "barras 90 dias" enquanto
  // os defaults reais sobem do servidor.
  const [cfgInicialCarregada, setCfgInicialCarregada] = useState(false);
  useEffect(() => {
    // active 'dashboard' eh o caso comum; tambem aceita undefined pra
    // o mount inicial onde active pode nao estar setado ainda.
    if (active && active !== 'dashboard') return;
    let vivo = true;
    fetch('/api/configuracoes', { credentials: 'same-origin' })
      .then(r => r.ok ? r.json() : null)
      .then(j => {
        if (!vivo) return;
        if (!j || !j.dados) {
          // Mesmo sem cfg valida, libera o chart pra renderizar com
          // defaults — evita ficar travado em "carregando" infinito.
          setCfgInicialCarregada(true);
          return;
        }
        // Sync de tema entre dispositivos do mesmo login. So aplicamos se
        // o servidor tiver o tema EXPLICITAMENTE salvo em
        // preferencias_usuario (j.prefsUsuario), nunca pelo default — caso
        // contrario um usuario que estava com 'light' antes de mudarmos o
        // default de fabrica pra 'dark' veria a cor flipar sem ter mexido.
        const temaSalvo = j.prefsUsuario?.preferencias?.tema;
        if ((temaSalvo === 'dark' || temaSalvo === 'light') && temaSalvo !== tema) {
          definirTema(temaSalvo);
        }
        if (!j.dados.dashboard) return;
        const d = j.dados.dashboard;
        setCfgDashboard(d);
        const pm = d.producaoMensal || {};
        // tipoGrafico e ocultarDiasSemProducao vivem em preferencias_usuario
        // e refletem mudancas remotas (ex: usuario mexeu no celular e abriu
        // o desktop) — entao sao atualizados a cada fetch da config, nao so
        // no primeiro mount.
        if (pm.tipoGrafico === 'barras' || pm.tipoGrafico === 'linha') {
          setTipoGraficoProdState(pm.tipoGrafico);
        }
        if (typeof pm.ocultarDiasSemProducao === 'boolean') {
          setOcultarSemProdState(pm.ocultarDiasSemProducao);
        }
        if (!aplicouDefaultsProdRef.current) {
          if (pm.metricaPadrao) setMetricaProd(pm.metricaPadrao);
          if (pm.granularidadePadrao) setGranProd(pm.granularidadePadrao);
          if (Number.isFinite(Number(pm.periodoPadrao)) && Number(pm.periodoPadrao) > 0) {
            setPeriodoProd(Number(pm.periodoPadrao));
          }
          if (typeof pm.maquinaPadrao === 'string') setMaquinaProd(pm.maquinaPadrao);
          aplicouDefaultsProdRef.current = true;
        }
        setCfgInicialCarregada(true);
      })
      .catch(() => { if (vivo) setCfgInicialCarregada(true); });
    return () => { vivo = false; };
  }, [active]);

  // `refreshKey` permite reexecutar os fetches de KPIs/produção apos uma
  // importacao de .DMP, sem recarregar a pagina. Alem disso, faz polling
  // a cada 60s (mesma cadencia da PileTable) e refresh ao voltar pra aba —
  // assim o delta do KPI (% vs media dos 7 dias) acompanha as estacas
  // novas que foram aparecendo durante o dia.
  useEffect(() => {
    let cancelado = false;
    const fetchKpis = () => {
      fetch(`/api/dashboard`, { credentials: "same-origin" })
        .then(r => r.json())
        .then(data => { if (!cancelado && data && !data.erro) setKpis(data); })
        .catch(() => {});
    };
    fetchKpis();
    const id = setInterval(fetchKpis, 60 * 1000);
    const onVisibility = () => {
      if (document.visibilityState === "visible") fetchKpis();
    };
    document.addEventListener("visibilitychange", onVisibility);
    return () => {
      cancelado = true;
      clearInterval(id);
      document.removeEventListener("visibilitychange", onVisibility);
    };
  }, [refreshKey]);

  useEffect(() => {
    // Espera a primeira /api/configuracoes responder antes de buscar
    // producao — sem isso o front faz UM fetch com defaults do React
    // (mensal/6) e DEPOIS outro com os defaults do usuario (diaria/90),
    // causando flash do chart "errado" na carga.
    if (!cfgInicialCarregada) return;
    let cancelado = false;
    const partes = [];
    partes.push(granProd === "diaria" ? `granularidade=diaria&dias=${periodoProd}` : `granularidade=mensal&meses=${periodoProd}`);
    if (maquinaProd) partes.push(`numSerie=${encodeURIComponent(maquinaProd)}`);
    if (diametroProd) partes.push(`diametro=${encodeURIComponent(diametroProd)}`);
    fetch(`/api/producao?${partes.join('&')}`, { credentials: "same-origin" })
      .then(r => r.json())
      .then(data => { if (!cancelado && Array.isArray(data)) setProducao(data); })
      .catch(() => {});
    return () => { cancelado = true; };
  }, [cfgInicialCarregada, granProd, periodoProd, maquinaProd, diametroProd, refreshKey]);

  // Lista de maquinas com producao pra alimentar o dropdown de filtro do
  // card de producao. Recarrega quando troca de cliente ou refresh manual.
  useEffect(() => {
    let cancelado = false;
    fetch(`/api/maquinas-producao`, { credentials: "same-origin" })
      .then(r => r.json())
      .then(data => {
        if (cancelado || !Array.isArray(data)) return;
        setMaquinasProd(data);
        // Se a maquina selecionada nao existe mais (mudou de cliente, etc.)
        // volta pra "todas" pra nao mostrar grafico vazio.
        if (maquinaProd && !data.some(m => String(m.numSerie) === String(maquinaProd))) {
          setMaquinaProd("");
        }
      })
      .catch(() => {});
    return () => { cancelado = true; };
  }, [cliente, refreshKey]); // eslint-disable-line react-hooks/exhaustive-deps

  // Lista de diametros com producao pra alimentar o filtro de diametro no
  // popover do card de producao. Mesmo padrao do fetch de maquinas.
  useEffect(() => {
    let cancelado = false;
    fetch(`/api/diametros-producao`, { credentials: "same-origin" })
      .then(r => r.json())
      .then(data => {
        if (cancelado || !Array.isArray(data)) return;
        setDiametrosProd(data);
        if (diametroProd && !data.some(d => String(d.diametro) === String(diametroProd))) {
          setDiametroProd("");
        }
      })
      .catch(() => {});
    return () => { cancelado = true; };
  }, [cliente, refreshKey]); // eslint-disable-line react-hooks/exhaustive-deps

  const importarDmp = useCallback(async (arquivo) => {
    if (!arquivo) return;
    if (!/\.dmp$/i.test(arquivo.name)) {
      notify("Selecione um arquivo .dmp", "erro");
      return;
    }
    setImportandoDmp(true);
    try {
      const fd = new FormData();
      fd.append("arquivo", arquivo);
      const resp = await fetch("/api/import-dmp", {
        method: "POST",
        credentials: "same-origin",
        body: fd,
      });
      const data = await resp.json().catch(() => ({}));
      if (!resp.ok) {
        notify(data.erro || "Falha ao importar DMP", "erro");
        return;
      }

      const {
        total = 0,
        importadas = 0,
        ignoradas = 0,
        importadasNomes = [],
        ignoradasNomes = [],
        erros = [],
      } = data;

      notify(
        `DMP processado: ${importadas} importada(s), ${ignoradas} ignorada(s) de ${total} estaca(s).`,
        importadas > 0 ? "ok" : "info"
      );

      const partes = [];
      partes.push(`Resumo da importação (${total} estaca(s) no DMP):`);
      partes.push(`• Importadas: ${importadas}`);
      partes.push(`• Ignoradas (já existiam): ${ignoradas}`);
      partes.push(`• Falhas: ${erros.length}`);

      if (importadasNomes.length > 0) {
        partes.push("");
        partes.push(`Importadas com sucesso:`);
        partes.push(importadasNomes.map(n => `• ${n}`).join("\n"));
      }
      if (ignoradasNomes.length > 0) {
        partes.push("");
        partes.push(`Ignoradas (já existiam no banco):`);
        partes.push(ignoradasNomes.map(n => `• ${n}`).join("\n"));
      }
      if (erros.length > 0) {
        partes.push("");
        partes.push(`Não foi possível importar:`);
        partes.push(erros.map(e => `• ${e.nome}: ${e.motivo}`).join("\n"));
      }

      window.alert(partes.join("\n"));

      if (importadas > 0) {
        setPastas(null);
        setRefreshKey(k => k + 1);
      }
    } catch (e) {
      notify("Erro de rede ao importar: " + (e.message || e), "erro");
    } finally {
      setImportandoDmp(false);
    }
  }, [notify]);

  // Importacao em lote de .cge — fluxo: POST start (multipart) → recebe
  // jobId → SSE de progresso ate 'fim'. Pasta ja vem filtrada pelo caller
  // (so .cge no nivel raiz). Modal de progresso fica aberto durante todo
  // o ciclo; usuario fecha manualmente apos ver o resumo.
  const importarCgeArquivos = useCallback(async (filesList) => {
    const files = Array.from(filesList || []).filter(f => /\.cge$/i.test(f.name));
    if (files.length === 0) {
      notify("Nenhum arquivo .cge encontrado.", "erro");
      return;
    }
    setImportCge({ fase: "enviando", total: files.length, processadas: 0, atual: null, resumo: null, erro: null });
    try {
      const fd = new FormData();
      for (const f of files) fd.append("arquivos", f, f.name);
      const startResp = await fetch("/api/import-cge/start", {
        method: "POST",
        credentials: "same-origin",
        body: fd,
      });
      if (!startResp.ok) {
        const erro = await startResp.json().catch(() => ({}));
        throw new Error(erro.erro || `Erro ${startResp.status}`);
      }
      const { jobId, total } = await startResp.json();
      setImportCge(prev => prev ? { ...prev, fase: "processando", total } : prev);

      await new Promise((resolve, reject) => {
        const es = new EventSource(`/api/import-cge/progress?jobId=${encodeURIComponent(jobId)}`);
        importEsRef.current = es;
        // Flag para suprimir 'onerror' espurio que alguns navegadores
        // disparam logo apos o servidor encerrar o stream com res.end()
        // — se ja recebemos 'fim', ignoramos o erro subsequente.
        let completou = false;
        es.onmessage = (ev) => {
          let d; try { d = JSON.parse(ev.data); } catch { return; }
          if (d.tipo === "iniciando") {
            setImportCge(prev => prev ? { ...prev, atual: d.arquivo, total: d.total ?? prev.total } : prev);
          } else if (d.tipo === "arquivo") {
            setImportCge(prev => prev ? { ...prev, processadas: d.indice, atual: null } : prev);
          } else if (d.tipo === "estado") {
            setImportCge(prev => prev ? { ...prev, total: d.total ?? prev.total, processadas: d.processadas ?? prev.processadas } : prev);
          } else if (d.tipo === "fim") {
            completou = true;
            es.close(); importEsRef.current = null;
            const resumo = {
              total: d.total ?? 0,
              importadas: d.importadas ?? 0,
              ignoradas: d.ignoradas ?? 0,
              importadasNomes: d.importadasNomes || [],
              ignoradasNomes: d.ignoradasNomes || [],
              erros: d.erros || [],
            };
            // Fecha o modal de progresso — o resumo vai por toast + alert
            // (mesmo padrao do DMP). Garante que o usuario sempre ve o
            // resultado, mesmo que a importacao tenha sido instantanea.
            setImportCge(null);

            const tipoToast = resumo.erros.length > 0
              ? "erro"
              : (resumo.importadas > 0 ? "ok" : "info");
            notify(
              `Importação: ${resumo.importadas} importada(s), ${resumo.ignoradas} duplicada(s)` +
                (resumo.erros.length ? `, ${resumo.erros.length} com erro` : "") +
                ` de ${resumo.total} arquivo(s).`,
              tipoToast
            );

            // Alert com detalhes — usuario confirma e fecha. Mesmo
            // formato do resumo do DMP para manter consistencia.
            const partes = [];
            partes.push(`Resumo da importação (${resumo.total} arquivo(s)):`);
            partes.push(`• Importadas: ${resumo.importadas}`);
            partes.push(`• Duplicadas (já existiam): ${resumo.ignoradas}`);
            partes.push(`• Falhas: ${resumo.erros.length}`);
            if (resumo.importadasNomes.length > 0) {
              partes.push("");
              partes.push("Importadas com sucesso:");
              partes.push(resumo.importadasNomes.map(n => `• ${n}`).join("\n"));
            }
            if (resumo.ignoradasNomes.length > 0) {
              partes.push("");
              partes.push("Já existiam (ignoradas):");
              partes.push(resumo.ignoradasNomes.map(n => `• ${n}`).join("\n"));
            }
            if (resumo.erros.length > 0) {
              partes.push("");
              partes.push("Com erro:");
              partes.push(resumo.erros.map(e => `• ${e.nome}: ${e.motivo}`).join("\n"));
            }
            window.alert(partes.join("\n"));

            if (resumo.importadas > 0) {
              setPastas(null);
              setRefreshKey(k => k + 1);
            }
            resolve();
          } else if (d.tipo === "erro") {
            completou = true;
            es.close(); importEsRef.current = null;
            reject(new Error(d.motivo || "Falha na importacao"));
          }
        };
        es.onerror = () => {
          if (completou) return; // close natural pos-fim — ignora.
          es.close(); importEsRef.current = null;
          reject(new Error("Conexão com o servidor caiu durante a importação."));
        };
      });
    } catch (e) {
      const msg = e.message || "Erro desconhecido";
      setImportCge(null);
      notify(`Falha na importação: ${msg}`, "erro");
      window.alert(`Falha na importação: ${msg}`);
    }
  }, [notify]);

  // Fecha o menu de import ao clicar fora.
  useEffect(() => {
    if (!importMenuAberto) return;
    const onDocClick = (e) => {
      const root = importMenuRef.current;
      if (!root) return;
      if (!root.contains(e.target)) setImportMenuAberto(false);
    };
    document.addEventListener("mousedown", onDocClick);
    return () => document.removeEventListener("mousedown", onDocClick);
  }, [importMenuAberto]);

  // Garante fechar SSE se o componente desmontar no meio de uma importacao.
  useEffect(() => () => {
    if (importEsRef.current) {
      try { importEsRef.current.close(); } catch (_) {}
      importEsRef.current = null;
    }
  }, []);

  // Pasta tem arquivo nao lido = backend devolveu algum item em arquivosNaoLidos.
  const pastaTemNovo = (p) => Array.isArray(p?.arquivosNaoLidos) && p.arquivosNaoLidos.length > 0;

  // Estaca individual nao lida = pertence ao array `arquivosNaoLidos` da pasta.
  const arquivoNaoLido = (p, arq) => Array.isArray(p?.arquivosNaoLidos) && p.arquivosNaoLidos.includes(arq);

  // Marca uma estaca como lida (idempotente). Atualiza estado local
  // imediatamente (otimista) e dispara POST em background.
  const marcarEstacaLida = useCallback((arquivo) => {
    if (!arquivo) return;
    setPastas(prev => {
      if (!Array.isArray(prev)) return prev;
      let mudou = false;
      const proximo = prev.map(p => {
        const ate = p.arquivosNaoLidos || [];
        if (!ate.includes(arquivo)) return p;
        mudou = true;
        return { ...p, arquivosNaoLidos: ate.filter(a => a !== arquivo) };
      });
      return mudou ? proximo : prev;
    });
    fetch("/api/estaca-leitura", {
      method: "POST",
      credentials: "same-origin",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ arquivo }),
    }).catch(() => {});
  }, []);

  // Sempre que o usuario abre uma estaca, registra como lida — cobre
  // todos os entry points (clique na sidebar, prev/next, busca, etc).
  useEffect(() => {
    if (estacaAberta?.arquivo) marcarEstacaLida(estacaAberta.arquivo);
  }, [estacaAberta?.arquivo, marcarEstacaLida]);

  useEffect(() => {
    if (pastas !== null) return;
    let cancelado = false;
    fetch(`/api/lista-estacas`, { credentials: "same-origin" })
      .then(r => r.json())
      .then(data => { if (!cancelado) setPastas(Array.isArray(data) ? data : []); })
      .catch(err => { if (!cancelado) setPastasErr(err.message || "Erro"); });
    return () => { cancelado = true; };
  }, [pastas]);

  // Polling da sidebar — pega arquivos novos que chegam pelo sistema externo
  // de monitoramento (parser em producao escreve direto no banco). Cadencia
  // de 60s alinhada com PileTable/KPIs. Refresh tambem quando a aba volta
  // a ficar visivel — pega novidades acumuladas com a aba em background.
  // So atualiza o estado se o conteudo mudou de fato, pra evitar re-render
  // sem necessidade (a comparacao por JSON e barata pro tamanho dessa lista).
  useEffect(() => {
    let cancelado = false;
    const recarregar = () => {
      fetch(`/api/lista-estacas`, { credentials: "same-origin" })
        .then(r => r.ok ? r.json() : Promise.reject(r.status))
        .then(data => {
          if (cancelado || !Array.isArray(data)) return;
          setPastas(prev => {
            if (!Array.isArray(prev)) return data;
            return JSON.stringify(prev) === JSON.stringify(data) ? prev : data;
          });
        })
        .catch(() => {});
    };
    const id = setInterval(recarregar, 60 * 1000);
    const onVisibility = () => {
      if (document.visibilityState === "visible") recarregar();
    };
    document.addEventListener("visibilitychange", onVisibility);
    return () => {
      cancelado = true;
      clearInterval(id);
      document.removeEventListener("visibilitychange", onVisibility);
    };
  }, []);

  useEffect(() => {
    if (!pastaAberta && !maquinaAberta && !filtroProf) { setPlanilha(null); return; }
    let cancelado = false;
    setPlanilhaCarregando(true);
    setPlanilha(null);
    const url = pastaAberta
      ? `/api/resumo-pasta?pasta=${encodeURIComponent(pastaAberta)}`
      : maquinaAberta
        ? `/api/resumo-maquina?serie=${encodeURIComponent(maquinaAberta)}`
        : filtroProf.operador
          ? `/api/lista-prof?op=${encodeURIComponent(filtroProf.operador)}&metros=${encodeURIComponent(filtroProf.metros)}&limite=500`
          : `/api/lista-prof?min=${encodeURIComponent(filtroProf.min)}&max=${encodeURIComponent(filtroProf.max)}&limite=500`;
    fetch(url, { credentials: "same-origin" })
      .then(r => r.json())
      .then(data => {
        if (cancelado) return;
        let linhas = Array.isArray(data) ? data : [];
        // Em pasta "obra-editada", o resumo-pasta devolve TODAS as estacas
        // do contrato (originais + editadas). Filtramos pra mostrar apenas
        // os arquivos listados na pasta editada (vindos da listagem da
        // sidebar — ja filtrados pelo backend por editado=TRUE).
        if (pastaAberta && tipoPastaAberta === "obra-editada") {
          const pastaObj = (pastas || []).find(p =>
            p.nomePasta === pastaAberta && (p.tipo || "contrato") === "obra-editada"
          );
          const arquivosEditados = new Set(pastaObj?.arquivos || []);
          linhas = linhas.filter(l => arquivosEditados.has(l.arquivo));
        }
        setPlanilha(linhas);
        setPlanilhaCarregando(false);
      })
      .catch(() => {
        if (cancelado) return;
        setPlanilha([]);
        setPlanilhaCarregando(false);
      });
    return () => { cancelado = true; };
  }, [pastaAberta, tipoPastaAberta, maquinaAberta, filtroProf, pastas]);

  const totalEstacas = useMemo(() => {
    if (!Array.isArray(pastas)) return null;
    // Contabiliza apenas pastas reais (contratos). Pastas tipo
    // "obra-editada" sao virtuais — agrupam estacas ja contadas no
    // contrato — entao nao entram na soma para nao duplicar.
    return pastas.reduce((acc, p) => acc + ((p.tipo || "contrato") === "contrato" ? (p.arquivos?.length || 0) : 0), 0);
  }, [pastas]);

  const nav = [
    { id: "dashboard", label: "Dashboard", icon: I.dash },
    { id: "estacas", label: "Estacas", icon: I.pile, badge: totalEstacas, expandable: true },
    { id: "mapa", label: "Mapa", icon: I.map },
    // "Monitoramento Remoto" ainda nao foi implementado — exposto so pro
    // admin_sistema enquanto a feature nao fica pronta.
    ...(ehAdminSistema ? [{ id: "monitoramento", label: "Monitoramento Remoto", icon: I.signal, dot: true }] : []),
    // "Usuarios" e "Maquinas" sao restritos ao admin_sistema. admin_cliente
    // gerencia os usuarios da propria empresa pela tela de Configuracoes
    // (Empresa e conta) — nao tem entrada propria na sidebar.
    ...(ehAdminSistema ? [{ id: "usuarios", label: "Usuários", icon: I.user || I.dash }] : []),
    ...(ehAdminSistema ? [{ id: "maquinas", label: "Máquinas", icon: I.truck || I.dash }] : []),
    // "Historico de acessos" — auditoria de login restrita ao admin_sistema.
    // admin_cliente e usuario comum nao veem o item na sidebar.
    ...(ehAdminSistema ? [{ id: "log-acessos", label: "Histórico de acessos", icon: I.clock || I.dash }] : []),
  ];

  const onNavClick = (n) => {
    if (n.id === "estacas") {
      setEstacasOpen(o => !o);
      setActive(n.id);
      return;
    }
    setActive(n.id);
    if (n.id === "dashboard" || n.id === "mapa" || n.id === "monitoramento" || n.id === "usuarios" || n.id === "maquinas" || n.id === "log-acessos") {
      setPastaAberta(null);
      setMaquinaAberta(null);
      setFiltroProf(null);
      setEstacaAberta(null);
      setEstacasOpen(false);
    }
    // Fecha o drawer apos navegar (sem efeito em desktop).
    setSidebarAberto(false);
  };

  const abrirContratoBusca = (contrato) => {
    setMaquinaAberta(null);
    setFiltroProf(null);
    setEstacaAberta(null);
    setActive("estacas");
    setEstacasOpen(true);
    setPastaAberta(contrato);
  };

  const abrirMaquinaBusca = (tag) => {
    setPastaAberta(null);
    setFiltroProf(null);
    setEstacaAberta(null);
    setActive("estacas");
    setMaquinaAberta(String(tag));
  };

  // Aciona a tela de planilha filtrada por profundidade. Dois formatos:
  //   { operador: '>', metros: 25 }      → comparacao
  //   { min: 20, max: 30 }               → intervalo
  const abrirFiltroProf = (payload) => {
    if (!payload) return;
    setPastaAberta(null);
    setMaquinaAberta(null);
    setEstacaAberta(null);
    setActive("estacas");
    if (payload.operador) {
      setFiltroProf({ operador: payload.operador, metros: Number(payload.metros) });
    } else if (payload.min != null && payload.max != null) {
      setFiltroProf({ min: Number(payload.min), max: Number(payload.max) });
    }
  };

  const abrirEstacaBusca = (contrato, arquivo, listaCustom = null) => {
    setMaquinaAberta(null);
    setFiltroProf(null);
    setActive("estacas");
    setEstacasOpen(true);
    if (contrato) setPastaAberta(contrato);
    setEstacaAberta({ pasta: contrato || "", arquivo });
    setOrigemEstaca("planilha");
    if (Array.isArray(listaCustom) && listaCustom.length) {
      setNavLista(listaCustom);
    } else {
      const p = (pastas || []).find(x => x.nomePasta === contrato);
      const arquivos = (p?.arquivos || [arquivo]);
      setNavLista(arquivos.map(arq => ({ pasta: contrato || "", arquivo: arq })));
    }
  };

  // Abre uma estaca diretamente do widget "Ultimas estacas executadas" do
  // Dashboard. Diferente de abrirEstacaBusca: NAO troca o `active` nem
  // setPastaAberta — assim, ao fechar o grafico, o usuario volta pro
  // Dashboard (e nao cai na view de "estacas" com a pasta aberta atras).
  const abrirEstacaDoDashboard = (contrato, arquivo, listaCustom = null) => {
    setMaquinaAberta(null);
    setFiltroProf(null);
    setEstacaAberta({ pasta: contrato || "", arquivo });
    setOrigemEstaca("dashboard");
    if (Array.isArray(listaCustom) && listaCustom.length) {
      setNavLista(listaCustom);
    } else {
      setNavLista([{ pasta: contrato || "", arquivo }]);
    }
  };

  // Atualiza o grafico aberto no painel apos uma mudanca persistida no banco
  // (save ou restore). Quando o arquivo muda (original → nova copia editada)
  // basta trocar `estacaAberta` — a propria mudanca de prop faz o iframe
  // recarregar. Quando e o mesmo arquivo (UPDATE in-place ou restore numa
  // copia ja aberta), bumpamos chartReloadKey para forcar o postMessage.
  const atualizarGraficoApos = (arquivoOrigem) => {
    if (!estacaAberta) return;
    if (arquivoOrigem && arquivoOrigem !== estacaAberta.arquivo) {
      setEstacaAberta({ pasta: estacaAberta.pasta, arquivo: arquivoOrigem });
    } else {
      setChartReloadKey(k => k + 1);
    }
  };

  const togglePasta = (nomePasta, tipoPasta = "contrato") => {
    setActive("estacas");
    setMaquinaAberta(null);
    setEstacaAberta(null);
    const ehAbrindo = !(pastaAberta === nomePasta && (tipoPastaAberta || "contrato") === tipoPasta);
    setPastaAberta(ehAbrindo ? nomePasta : null);
    setTipoPastaAberta(ehAbrindo ? tipoPasta : null);
    // Em mobile, fecha o drawer ao abrir uma pasta — o usuario ve a lista
    // de cards no main em vez do accordion expandido na sidebar. Mantem
    // o drawer aberto quando o clique fecha a pasta (ehAbrindo=false), pra
    // o usuario continuar navegando entre contratos sem reabrir o menu.
    // Em desktop o setState e inerte (sidebar fixa).
    if (ehAbrindo) setSidebarAberto(false);
  };

  // Exclui uma copia editada. O botao na sidebar so aparece em pastas
  // tipo 'obra-editada' — o backend tambem rejeita originais (editado=FALSE)
  // com 403, entao copia editada e o unico caso valido aqui.
  const excluirEstacaEditada = async (nomePasta, arquivo) => {
    const ok = window.confirm(`Excluir a estaca editada "${stripSufixoEditado(arquivo)}"?\n\nEsta ação não pode ser desfeita. O arquivo original não será afetado.`);
    if (!ok) return;
    try {
      const r = await fetch(`/api/estaca-editada?arquivo=${encodeURIComponent(arquivo)}`, {
        method: "DELETE",
        credentials: "same-origin",
      });
      const j = await r.json().catch(() => ({}));
      if (!r.ok) {
        notify(j.erro || "Falha ao excluir estaca", "erro");
        return;
      }
      // Se a estaca era a aberta no painel, fecha o grafico.
      if (estacaAberta && estacaAberta.pasta === nomePasta && estacaAberta.arquivo === arquivo) {
        setEstacaAberta(null);
      }
      // Se essa era a unica estaca da pasta editada aberta, fecha a pasta —
      // ela vai sumir no proximo render (listarContratos nao emite pastas
      // sem arquivos), entao deixar setada manteria estado fantasma.
      const pastaAtual = (pastas || []).find(p => p.nomePasta === nomePasta && (p.tipo || "contrato") === "obra-editada");
      const restantes = (pastaAtual?.arquivos || []).filter(a => a !== arquivo);
      if (restantes.length === 0 && pastaAberta === nomePasta && tipoPastaAberta === "obra-editada") {
        setPastaAberta(null);
        setTipoPastaAberta(null);
      }
      setPastas(null); // dispara o useEffect de fetch da sidebar
      notify("Estaca editada excluída.", "ok");
    } catch (err) {
      notify("Erro de rede ao excluir.", "erro");
    }
  };

  // Abre o fluxo de edicao manual: se o admin ja autorizou esse contrato
  // nessa sessao, vai direto pra escolha de aba; caso contrario pede
  // credenciais antes. Trocar de contrato invalida a autorizacao anterior.
  const pedirEdicao = (arquivo, contrato) => {
    if (contratoAutorizado && contratoAutorizado === contrato) {
      setEscolhaTab({ arquivo, contrato });
      return;
    }
    setEstacaParaEditar({ arquivo, contrato });
  };

  const abrirEstaca = (nomePasta, arquivo, tipoPasta = "contrato") => {
    setActive("estacas");
    setMaquinaAberta(null);
    setPastaAberta(nomePasta);
    setTipoPastaAberta(tipoPasta);
    setEstacaAberta({ pasta: nomePasta, arquivo });
    setOrigemEstaca("planilha");
    // Duas pastas podem dividir o mesmo nomePasta (contrato + obra-editada);
    // o tipo desempata pra que navLista seja consistente com a pasta clicada
    // — sem isso, abrir uma estaca editada pegava a lista do contrato e o
    // foot-nav nao encontrava o arquivo na navLista (sumia).
    const p = (pastas || []).find(x =>
      x.nomePasta === nomePasta && (x.tipo || "contrato") === tipoPasta
    );
    const arquivos = (p?.arquivos || [arquivo]);
    setNavLista(arquivos.map(arq => ({ pasta: nomePasta, arquivo: arq })));
    // Fecha o drawer apos abrir uma estaca — relevante em mobile, onde a
    // sidebar e off-canvas. Em desktop o setState e' inerte (sidebar fixa).
    setSidebarAberto(false);
  };

  // Navegação (←/→/↑/↓ do teclado e botões prev/next do foot-nav React)
  // percorre a navLista ativa — pasta clicada na sidebar, lista de últimas
  // estacas, planilha de máquina ou resultado de busca.
  // IMPORTANTE: usa setState funcional pra ler o estado MAIS RECENTE.
  // Sem isso, presses rapidos entre re-renders pegavam closure stale com
  // estacaAberta antiga; a 2a/3a chamada calculava o mesmo "proximo" da
  // primeira e React abortava o setState (Object.is identico) — usuario
  // perdia teclas. Como `prev` vem direto do React, sempre eh o atual.
  const navegarEstaca = useCallback((direcao) => {
    setEstacaAberta(prev => {
      if (!prev || navLista.length === 0) return prev;
      const idx = navLista.findIndex(
        x => x.pasta === prev.pasta && x.arquivo === prev.arquivo
      );
      if (idx === -1) return prev;
      let novoIdx;
      if (direcao === "ArrowDown" || direcao === "ArrowRight" || direcao === "next") {
        novoIdx = Math.min(idx + 1, navLista.length - 1);
      } else if (direcao === "ArrowUp" || direcao === "ArrowLeft" || direcao === "prev") {
        novoIdx = Math.max(idx - 1, 0);
      } else return prev;
      if (novoIdx === idx) return prev;
      return navLista[novoIdx];
    });
  }, [navLista]);

  useEffect(() => {
    if (!estacaAberta || navLista.length === 0) return;

    const abortarCargaIframe = () => {
      const iframe = document.querySelector('.dash-grafico-iframe');
      try { iframe?.contentWindow?.postMessage({ tipo: 'abort-carga' }, '*'); } catch (_) {}
    };

    const ativarKeepAliveIframe = () => {
      const iframe = document.querySelector('.dash-grafico-iframe');
      try { iframe?.contentWindow?.postMessage({ tipo: 'ativar-keepalive' }, '*'); } catch (_) {}
      // Foca o iframe — Chrome reduz throttling em iframes com foco.
      try { iframe?.contentWindow?.focus(); } catch (_) {}
    };

    const onKey = (e) => {
      if (e.key !== "ArrowUp" && e.key !== "ArrowDown" && e.key !== "ArrowLeft" && e.key !== "ArrowRight") return;
      const alvo = e.target;
      if (alvo && (alvo.tagName === "INPUT" || alvo.tagName === "TEXTAREA" || alvo.isContentEditable)) return;
      e.preventDefault();
      ativarKeepAliveIframe();
      abortarCargaIframe();
      navegarEstaca(e.key);
    };

    const onMessage = (ev) => {
      if (!ev.data || ev.data.tipo !== "estaca-nav") return;
      abortarCargaIframe();
      navegarEstaca(ev.data.direcao);
    };

    window.addEventListener("keydown", onKey);
    window.addEventListener("message", onMessage);
    return () => {
      window.removeEventListener("keydown", onKey);
      window.removeEventListener("message", onMessage);
    };
  }, [estacaAberta, navLista, navegarEstaca]);

  // Mantem a estaca ativa visivel no scroll da lista da sidebar. Quando o
  // usuario navega com setas (ou clica numa estaca fora da viewport pelo
  // resultado de busca), o item .is-active e' rolado pra dentro do
  // .dash-nav-files. `block: 'nearest'` so' rola se ja nao estiver visivel
  // — clicks dentro da janela visivel nao causam jump. rAF garante que o
  // React ja' aplicou a classe `is-active` antes do scroll.
  useEffect(() => {
    if (!estacaAberta) return;
    const id = requestAnimationFrame(() => {
      const el = document.querySelector('.dash-nav-file.is-active');
      if (el && typeof el.scrollIntoView === 'function') {
        el.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
      }
    });
    return () => cancelAnimationFrame(id);
  }, [estacaAberta]);

  return (
    <div className="dash-root" style={{ "--accent": accent.hex, "--accent-soft": accent.soft, "--accent-ink": accent.ink }}>
      <div
        className={"dash-side-backdrop" + (sidebarAberto ? " is-aberto" : "")}
        onClick={fecharSidebar}
        aria-hidden="true"
      />
      <aside className={"dash-side" + (sidebarAberto ? " is-aberto" : "")}>
        <div
          className="dash-side-brand"
          role="button"
          tabIndex={0}
          onClick={() => onNavClick({ id: "dashboard" })}
          onKeyDown={e => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); onNavClick({ id: "dashboard" }); } }}
          style={{ cursor: "pointer" }}
        >
          <img
            className="dash-side-logo"
            src="/img/logo-horizontal-white.svg"
            alt="Compugeo"
          />
        </div>

        {ehAdminSistema && (
          <>
            <div className="dash-side-section">CLIENTE</div>
            <div className="dash-side-cliente">
              <select
                className="dash-side-cliente-sel"
                value={cliente || ""}
                onChange={e => trocarCliente(e.target.value)}
                title="Filtrar por cliente"
              >
                <option value="">Todos os clientes</option>
                {empresas.map(emp => (
                  <option key={emp} value={emp}>{emp}</option>
                ))}
              </select>
            </div>
          </>
        )}

        <div className="dash-side-section">NAVEGAÇÃO</div>
        <nav className="dash-nav">
          {nav.map((n) => {
            const Icon = n.icon;
            const isActive = active === n.id;
            const isEstacas = n.id === "estacas";
            return (
              <React.Fragment key={n.id}>
                <button onClick={() => onNavClick(n)} className={`dash-nav-item ${isActive ? "is-active" : ""}`} data-nav-id={n.id}>
                  {isActive && <span className="dash-nav-bar" style={{ background: accent.hex }}/>}
                  <Icon width="18" height="18"/>
                  <span className="dash-nav-label">{n.label}</span>
                  {n.badge != null && <span className="dash-nav-badge">{n.badge}</span>}
                  {n.dot && <span className="dash-nav-dot" style={{ background: accent.hex }}/>}
                  {n.expandable && (
                    <span className={`dash-nav-chev ${estacasOpen ? "is-open" : ""}`}>
                      <I.chev width="14" height="14"/>
                    </span>
                  )}
                </button>
                {isEstacas && estacasOpen && (
                  <div className="dash-nav-sub">
                    {pastas === null && !pastasErr && (
                      <div className="dash-nav-sub-empty">Carregando…</div>
                    )}
                    {pastasErr && (
                      <div className="dash-nav-sub-empty">Falha ao carregar pastas</div>
                    )}
                    {pastas && pastas.length === 0 && (
                      <div className="dash-nav-sub-empty">Nenhuma pasta encontrada</div>
                    )}
                    {pastas && pastas.slice(0, limitePastas).map(p => {
                      const tipoPasta = p.tipo || "contrato";
                      const editada = tipoPasta === "obra-editada";
                      const aberta = pastaAberta === p.nomePasta && (tipoPastaAberta || "contrato") === tipoPasta;
                      return (
                        <React.Fragment key={`${tipoPasta}::${p.nomePasta}`}>
                          <button
                            className={`dash-nav-subitem ${aberta ? "is-open" : ""} ${editada ? "is-edited" : ""} ${pastaTemNovo(p) ? "has-novo" : ""}`}
                            onClick={() => togglePasta(p.nomePasta, tipoPasta)}
                          >
                            <span className={`dash-nav-subchev ${aberta ? "is-open" : ""}`}>
                              <I.chev width="12" height="12"/>
                            </span>
                            {pastaTemNovo(p) && (
                              <span
                                className="dash-nav-subnovo"
                                style={{ background: accent.hex, "--accent-glow": accent.hex }}
                                title="Tem estaca(s) nova(s)"
                                aria-label="Tem estacas novas"
                              />
                            )}
                            <span className="dash-nav-sublabel">{p.nomePasta}</span>
                            <span className="dash-nav-subcount">{p.arquivos?.length || 0}</span>
                          </button>
                          {aberta && (
                            <div className="dash-nav-files">
                              {(p.arquivos || []).length === 0 && (
                                <div className="dash-nav-file-empty">Sem estacas</div>
                              )}
                              {(p.arquivos || []).map(arq => {
                                const ativo = estacaAberta && estacaAberta.pasta === p.nomePasta && estacaAberta.arquivo === arq;
                                const naoLido = arquivoNaoLido(p, arq);
                                return (
                                  <div key={arq} className={`dash-nav-file-row ${editada ? "is-edited" : ""}`}>
                                    <button
                                      className={`dash-nav-file ${ativo ? "is-active" : ""} ${naoLido ? "is-nao-lido" : ""}`}
                                      onClick={() => abrirEstaca(p.nomePasta, arq, tipoPasta)}
                                      onDoubleClick={podeAdministrar
                                        ? (e) => { e.preventDefault(); pedirEdicao(arq, p.nomePasta); }
                                        : undefined}
                                      title={arq}
                                    >
                                      {naoLido ? (
                                        <span
                                          className="dash-nav-file-novo"
                                          style={{ background: accent.hex, "--accent-glow": accent.hex }}
                                          title="Estaca ainda não aberta"
                                          aria-label="Não lida"
                                        />
                                      ) : (
                                        <span className="dash-nav-file-dot"/>
                                      )}
                                      <span className="dash-nav-file-name">{stripSufixoEditado(arq)}</span>
                                    </button>
                                    {editada && podeAdministrar && (
                                      <button
                                        type="button"
                                        className="dash-nav-file-del"
                                        title="Excluir estaca editada"
                                        aria-label="Excluir estaca editada"
                                        onClick={(e) => { e.stopPropagation(); excluirEstacaEditada(p.nomePasta, arq); }}
                                      >
                                        ×
                                      </button>
                                    )}
                                  </div>
                                );
                              })}
                            </div>
                          )}
                        </React.Fragment>
                      );
                    })}
                    {pastas && pastas.length > limitePastas && (
                      <button
                        type="button"
                        className="dash-nav-mais"
                        onClick={() => setLimitePastas(l => l + lerPastasPagina())}
                        title={`Mostrar mais ${lerPastasPagina()} pastas (${pastas.length - limitePastas} restantes)`}
                        aria-label="Carregar mais pastas"
                      >
                        <span className="dash-nav-mais-circ">
                          <I.plus width="14" height="14"/>
                        </span>
                      </button>
                    )}
                  </div>
                )}
              </React.Fragment>
            );
          })}
        </nav>

        <div className="dash-side-foot">
          {/* <div className="dsf-status"><span className="dsf-dot"/> 142 sensores online</div> */}
          <div className="dsf-version">v0.0.1 · BR-SP</div>
        </div>
      </aside>

      <main className="dash-main">
        <header className="dash-head">
          <button
            type="button"
            className="dash-hamburguer"
            aria-label="Abrir menu"
            onClick={() => setSidebarAberto(true)}
          >
            <svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
              <path d="M4 6h16M4 12h16M4 18h16"/>
            </svg>
          </button>
          <BuscaGlobal
            cliente={cliente}
            onAbrirEstaca={abrirEstacaBusca}
            onAbrirContrato={abrirContratoBusca}
            onAbrirMaquina={abrirMaquinaBusca}
            onAbrirFiltroProf={abrirFiltroProf}
          />
          <div className="dash-head-tools">
            <button
              className="dash-icon-btn"
              title="Configurações"
              onClick={() => setActive("configuracoes")}
            ><I.cog width="18" height="18"/></button>
            <button className="dash-icon-btn" title="Notificações">
              <I.bell width="18" height="18"/>
              {/* Badge "3" mockado removido enquanto nao tem central de
                  notificacoes — quando habilitar, voltar com contagem real. */}
            </button>
            <div className="dash-divider"/>
            {(() => {
              // Linha 1: nome do usuario (fallback no email se nao tiver
              // nome cadastrado, e em "Conta" se nem email). Linha 2:
              // empresa do usuario, ou "—" pra admin_sistema sem empresa
              // selecionada.
              const nomeExibir = (nome && nome.trim()) || (email && email.split('@')[0]) || 'Conta';
              const empresaExibir = (cliente && cliente.trim()) || '—';
              const inicial = (nomeExibir || '?').trim().charAt(0).toUpperCase();
              return (
                <>
                  <button className="dash-user">
                    <div className="dash-user-name">{nomeExibir}</div>
                    <div className="dash-user-role">{empresaExibir}</div>
                  </button>
                  <div className="dash-avatar">{inicial}</div>
                </>
              );
            })()}
            <TemaToggle tema={tema} onAlternar={alternarTema}/>
            <button
              className="dash-icon-btn"
              title="Sair"
              onClick={() => onSair && onSair()}
              aria-label="Sair"
            >
              <svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
                <path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/>
                <polyline points="16 17 21 12 16 7"/>
                <line x1="21" y1="12" x2="9" y2="12"/>
              </svg>
            </button>
          </div>
        </header>

        {estacaAberta ? (
          <EstacaGrafico
            pasta={estacaAberta.pasta}
            arquivo={estacaAberta.arquivo}
            reloadKey={chartReloadKey}
            navLista={navLista}
            onNavegar={navegarEstaca}
            obra={(() => {
              const p = (pastas || []).find(x => x.nomePasta === estacaAberta.pasta);
              return p ? { nome: p.nomePasta, totalEstacas: (p.arquivos || []).length } : null;
            })()}
            temEditadas={(() => {
              const e = (pastas || []).find(x => x.nomePasta === estacaAberta.pasta && (x.tipo || "contrato") === "obra-editada");
              return !!(e && (e.arquivos || []).length > 0);
            })()}
            origem={origemEstaca}
            onVoltar={() => { setEstacaAberta(null); setOrigemEstaca(null); }}
            pausado={!!editorAberto}
          />
        ) : active === "configuracoes" ? (
          <ConfiguracoesView
            tema={tema}
            onSetTema={definirTema}
            papel={papel}
            notify={notify}
            accent={accent}
            onVoltar={() => setActive("dashboard")}
          />
        ) : active === "mapa" ? (
          <MapaPage accent={accent}/>
        ) : active === "frota" ? (
          <FrotaPage accent={accent} onVoltar={() => setActive("dashboard")}/>
        ) : active === "detalhes-obra" ? (
          <DetalhesObraPage obraId={obraSelecionada}/>
        ) : active === "monitoramento" ? (
          <MonitoramentoPage accent={accent}/>
        ) : active === "usuarios" && ehAdminSistema ? (
          <UsuariosPage accent={accent} notify={notify} papel={papel}/>
        ) : active === "maquinas" && ehAdminSistema ? (
          <MaquinasPage accent={accent} notify={notify}/>
        ) : active === "log-acessos" && ehAdminSistema ? (
          <LogAcessosPage accent={accent} papel={papel}/>
        ) : pastaAberta || maquinaAberta || filtroProf ? (
          <PastaPlanilha
            accent={accent}
            pasta={
              pastaAberta
                ? pastaAberta
                : maquinaAberta
                  ? `Máquina · ${maquinaAberta}`
                  : filtroProf.operador
                    ? `Profundidade ${filtroProf.operador} ${filtroProf.metros} m`
                    : `Profundidade entre ${filtroProf.min} e ${filtroProf.max} m`
            }
            /* Botao "Gerar Relatorio PDF" so faz sentido em pasta real
               (contrato), nao na visualizacao por maquina/profundidade
               (que mistura estacas de varios contratos). */
            pastaRelatorio={pastaAberta || null}
            temEditadas={(() => {
              if (!pastaAberta) return false;
              const e = (pastas || []).find(x => x.nomePasta === pastaAberta && (x.tipo || "contrato") === "obra-editada");
              return !!(e && (e.arquivos || []).length > 0);
            })()}
            eyebrow={
              pastaAberta
                ? "PLANILHA DE ESTACAS"
                : maquinaAberta
                  ? "PLANILHA POR MÁQUINA"
                  : "ESTACAS POR PROFUNDIDADE"
            }
            linhas={planilha}
            carregando={planilhaCarregando}
            onAbrirEstaca={(arq) => {
              if (pastaAberta) {
                abrirEstaca(pastaAberta, arq, tipoPastaAberta || "contrato");
              } else if (filtroProf) {
                // Cada linha pertence ao seu proprio contrato — navLista
                // mistura pastas, mas cada item leva sua propria pasta.
                const linhas = Array.isArray(planilha) ? planilha : [];
                const lista = linhas
                  .filter(l => l && l.arquivo)
                  .map(l => ({ pasta: (l.cabecalho && l.cabecalho.contrato) || "", arquivo: l.arquivo }));
                const linhaClicada = linhas.find(l => l.arquivo === arq);
                const pastaDest = (linhaClicada && linhaClicada.cabecalho && linhaClicada.cabecalho.contrato) || "";
                setEstacaAberta({ pasta: pastaDest, arquivo: arq });
                setOrigemEstaca("planilha");
                setNavLista(lista.length ? lista : [{ pasta: pastaDest, arquivo: arq }]);
              } else {
                const pastaLabel = `Máquina · ${maquinaAberta}`;
                const lista = (Array.isArray(planilha) ? planilha : [])
                  .filter(l => l && l.arquivo)
                  .map(l => ({ pasta: pastaLabel, arquivo: l.arquivo }));
                setEstacaAberta({ pasta: pastaLabel, arquivo: arq });
                setOrigemEstaca("planilha");
                setNavLista(lista.length ? lista : [{ pasta: pastaLabel, arquivo: arq }]);
              }
            }}
            onVoltar={() => { setPastaAberta(null); setMaquinaAberta(null); setFiltroProf(null); setActive("dashboard"); }}
          />
        ) : (
          <div className="dash-page">
            <div className="dash-pagehead">
              <div>
                <div className="dash-eyebrow">VISÃO GERAL</div>
                <h1 className="dash-h1">Dashboard</h1>
              </div>
              <div className="dash-actions">
                {podeAdministrar && (
                <>
                <input
                  ref={inputDmpRef}
                  type="file"
                  accept=".dmp"
                  style={{ display: "none" }}
                  onChange={e => {
                    const f = e.target.files && e.target.files[0];
                    e.target.value = "";
                    if (f) importarDmp(f);
                  }}
                />
                <input
                  ref={inputCgeRef}
                  type="file"
                  accept=".cge,.CGE"
                  multiple
                  style={{ display: "none" }}
                  onChange={e => {
                    // IMPORTANTE: e.target.files e' o FileList vivo do input.
                    // Setar value="" zera esse mesmo objeto, entao precisamos
                    // copiar pra Array antes de limpar.
                    const arquivos = Array.from(e.target.files || []);
                    e.target.value = "";
                    if (arquivos.length) importarCgeArquivos(arquivos);
                  }}
                />
                <input
                  ref={inputCgePastaRef}
                  type="file"
                  webkitdirectory=""
                  directory=""
                  multiple
                  style={{ display: "none" }}
                  onChange={e => {
                    // Mesmo cuidado do input de .cge: copia FileList -> Array
                    // ANTES de limpar o value, senao o FileList vivo zera.
                    const arquivos = Array.from(e.target.files || []);
                    e.target.value = "";
                    if (!arquivos.length) return;
                    // Pega so arquivos no NIVEL RAIZ da pasta selecionada
                    // (ignora subpastas) e so .cge.
                    const noNivel = arquivos.filter(f => {
                      const p = f.webkitRelativePath || f.name;
                      const partes = p.split("/").filter(Boolean);
                      // Top-level = exatamente 2 segmentos: <pasta>/<arquivo>
                      return partes.length === 2 && /\.cge$/i.test(f.name);
                    });
                    if (!noNivel.length) {
                      notify("Nenhum arquivo .cge encontrado no nível raiz da pasta.", "erro");
                      return;
                    }
                    importarCgeArquivos(noNivel);
                  }}
                />
                <div className="import-menu-wrap" ref={importMenuRef}>
                  <button
                    type="button"
                    className="dash-btn-ghost"
                    onClick={() => setImportMenuAberto(v => !v)}
                    disabled={importandoDmp || (importCge && importCge.fase !== "pronto")}
                  >
                    {importandoDmp || (importCge && importCge.fase !== "pronto") ? "Importando…" : "Importar"}
                    <span style={{ fontSize: 10, opacity: 0.6 }}>▾</span>
                  </button>
                  {importMenuAberto && (
                    <div className="import-menu-pop" role="menu">
                      <button
                        type="button"
                        className="import-menu-item"
                        role="menuitem"
                        onClick={() => { setImportMenuAberto(false); inputDmpRef.current?.click(); }}
                      >
                        <span className="import-menu-item__lbl">
                          <span>Arquivo .DMP</span>
                          <span className="import-menu-item__sub">Dump da memória da máquina</span>
                        </span>
                      </button>
                      <button
                        type="button"
                        className="import-menu-item"
                        role="menuitem"
                        onClick={() => { setImportMenuAberto(false); inputCgeRef.current?.click(); }}
                      >
                        <span className="import-menu-item__lbl">
                          <span>Arquivo(s) .CGE</span>
                          <span className="import-menu-item__sub">Um ou vários .cge soltos</span>
                        </span>
                      </button>
                      <button
                        type="button"
                        className="import-menu-item"
                        role="menuitem"
                        onClick={() => { setImportMenuAberto(false); inputCgePastaRef.current?.click(); }}
                      >
                        <span className="import-menu-item__lbl">
                          <span>Pasta com .CGE</span>
                          <span className="import-menu-item__sub">Apenas .cge no nível raiz; subpastas são ignoradas</span>
                        </span>
                      </button>
                    </div>
                  )}
                </div>
                </>
                )}
                <button
                  type="button"
                  className="dash-btn-primary"
                  style={{ background: accent.hex }}
                  onClick={() => setNovaObraAberta(true)}
                >
                  Adicionar nova obra <I.plus width="14" height="14"/>
                </button>
              </div>
            </div>

            <div className="dash-kpis">
              {(() => {
                // Defs dos KPIs disponiveis. Cada def sabe como gerar
                // suas props a partir do estado atual de `kpis` (vindo
                // de /api/dashboard) — assim a ordem e visibilidade
                // ficam controladas por cfgDashboard.kpis sem
                // duplicar logica de fetching.
                const defsKpi = {
                  estacas: () => ({
                    label: "Estacas executadas hoje",
                    value: fmtInt(kpis.estacas.valor),
                    delta: kpis.estacas.delta ?? 0,
                    unit: "estacas",
                    sparkline: kpis.estacas.sparkline,
                    media: kpis.estacas.media,
                    accent: accent.hex,
                    muted: false,
                    info: "Quantidade de estacas com fim de concretagem registrado hoje, até este horário. O delta compara com a média dos últimos 7 dias contando apenas estacas concluídas até o mesmo horário, e ignora dias sem produção.",
                  }),
                  metros: () => ({
                    label: "Metros perfurados hoje",
                    value: fmtInt(kpis.metros.valor),
                    delta: kpis.metros.delta ?? 0,
                    unit: "m totais",
                    sparkline: kpis.metros.sparkline,
                    media: kpis.metros.media,
                    accent: "#3B6EE0",
                    muted: true,
                    info: "Soma da profundidade total das estacas concluídas hoje (em metros), até este horário. O delta compara com a média dos últimos 7 dias até o mesmo horário, ignorando dias sem produção.",
                  }),
                  concreto: () => ({
                    label: "Volume de concreto hoje",
                    value: fmtInt(kpis.concreto.valor),
                    delta: kpis.concreto.delta ?? 0,
                    unit: "m³",
                    sparkline: kpis.concreto.sparkline,
                    media: kpis.concreto.media,
                    accent: "#7B61D6",
                    muted: true,
                    info: "Soma do volume de concreto das estacas concluídas hoje (em m³), até este horário. O delta compara com a média dos últimos 7 dias até o mesmo horário, ignorando dias sem produção.",
                  }),
                  maquinas: () => {
                    const m = kpis.maquinas || KPI_MAQUINAS_VAZIO;
                    const taxa = Number.isFinite(m.taxa) ? m.taxa : 0;
                    return {
                      label: "Maquinas online",
                      value: m.valor,
                      delta: m.delta ?? 0,
                      unit: `taxa ${taxa.toFixed(1)}%`,
                      sparkline: m.sparkline,
                      media: m.media,
                      accent: "#22A06B",
                      muted: true,
                      info: "Máquinas que reportaram sinal nos últimos 10 minutos sobre o total da frota ativa. A taxa é a média do pico diário de máquinas online (em qualquer janela de 15min) dos últimos 7 dias, sobre o total da frota.",
                      // Card clicavel → abre a tela de monitoramento da frota
                      // (lista de maquinas + relatorio por maquina).
                      onClick: () => setActive("frota"),
                    };
                  },
                };
                // Ordem da config; se config nao carregou ainda, usa
                // ordem default (estacas, metros, concreto, maquinas).
                const lista = (cfgDashboard && Array.isArray(cfgDashboard.kpis) && cfgDashboard.kpis.length > 0)
                  ? cfgDashboard.kpis
                  : [
                      { id: 'estacas',  visivel: true },
                      { id: 'metros',   visivel: true },
                      { id: 'concreto', visivel: true },
                      { id: 'maquinas', visivel: true },
                    ];
                return lista
                  .filter(k => k && k.visivel !== false && defsKpi[k.id])
                  .map(k => {
                    const props = defsKpi[k.id]();
                    return <KPI key={k.id} {...props}/>;
                  });
              })()}
            </div>

            <div className="dash-row">
              <Card
                className="dash-chart"
                title={granProd === "diaria" ? "Produção diária" : "Produção mensal"}
                right={
                  <FiltrosProducao
                    metrica={metricaProd}        setMetrica={setMetricaProd}
                    granularidade={granProd}     setGranularidade={setGranProd}
                    periodo={periodoProd}        setPeriodo={setPeriodoProd}
                    maquina={maquinaProd}        setMaquina={setMaquinaProd}    maquinas={maquinasProd}
                    diametro={diametroProd}      setDiametro={setDiametroProd}  diametros={diametrosProd}
                    tipoGrafico={tipoGraficoProd} setTipoGrafico={setTipoGraficoProd}
                    ocultarSemProducao={ocultarSemProd} setOcultarSemProducao={setOcultarSemProd}
                  />
                }
              >
                {cfgInicialCarregada ? (
                  <ProductionChart
                    accent={accent.hex}
                    dados={producao}
                    metrica={metricaProd}
                    granularidade={granProd}
                    tipoGrafico={tipoGraficoProd}
                    ocultarSemProducao={ocultarSemProd}
                  />
                ) : (
                  // Placeholder com a mesma altura do chart (.prod-chart e'
                  // height: 240px) pra evitar o pulo de layout quando ele
                  // entra com os valores reais do usuario.
                  <div style={{ height: 240 }} aria-hidden="true"/>
                )}
              </Card>

              <Card className="dash-status" title="Status das Obras">
                <FrontList
                  accent={accent.hex}
                  reloadKey={obrasReloadKey}
                  onAbrirObra={(obra) => setObraEditando(obra)}
                />
              </Card>
            </div>

            <div className="dash-row dash-row-full">
              <Card
                className="dash-table dash-table-full"
                title="Últimas estacas executadas"
                right={<InfoTip text="As 100 estacas concluídas mais recentemente, ordenadas pelo fim da concretagem. A lista é atualizada automaticamente em tempo real conforme novas estacas chegam."/>}
              >
                <PileTable accent={accent.hex} cliente={cliente} onAbrirEstaca={abrirEstacaDoDashboard} onNovasEstacas={handleNovasEstacas} conhecidasRef={conhecidasUltimasRef}/>
              </Card>
              {/* Card do mapa removido temporariamente — tabela ocupa toda a largura.
                  Pra restaurar, descomentar o bloco abaixo e remover .dash-row-full
                  + .dash-table-full do row/card acima.
              <Card
                className="dash-map"
                title="Mapa do canteiro"
                right={<button type="button" className="dash-link" onClick={() => setActive("mapa")}>Abrir mapa →</button>}
              >
                <div className="dash-map-mini">
                  <GoogleMap accentHex={accent.hex} mini obras={obrasMiniMapa} className="dash-map-mini-canvas"/>
                </div>
                <div className="site-map-legend" style={{ marginTop: 10, flexWrap: 'wrap' }}>
                  <span><i style={{ background: corObra("ocioso", accent.hex) }}/> Ocioso</span>
                  <span><i style={{ background: corObra("perfurando", accent.hex) }}/> Perfurando</span>
                  <span><i style={{ background: corObra("concretando", accent.hex) }}/> Concretando</span>
                  <span><i style={{ background: corObra("erro", accent.hex) }}/> Erro</span>
                  <span><i style={{ background: corObra("offline", accent.hex) }}/> Offline</span>
                </div>
              </Card>
              */}
            </div>
          </div>
        )}
      </main>
      <div className="toast-host" aria-live="polite" aria-atomic="true">
        {toasts.map(t => (
          <div key={t.id} className={`toast toast-${t.tipo}`} role="status">{t.mensagem}</div>
        ))}
      </div>
      {estacaParaEditar && (
        <AdminLoginModal
          accent={accent}
          onClose={() => setEstacaParaEditar(null)}
          onSucesso={() => {
            const { arquivo, contrato } = estacaParaEditar;
            // Autoriza esse contrato para o resto da sessao do dashboard
            // — proximas estacas dele dispensam senha. Trocar de contrato
            // sobrescreve a autorizacao e volta a pedir.
            setContratoAutorizado(contrato || null);
            setEstacaParaEditar(null);
            setEscolhaTab({ arquivo, contrato });
          }}
        />
      )}
      {escolhaTab && (
        <EscolhaTabModal
          accent={accent}
          onClose={() => setEscolhaTab(null)}
          onMesmaAba={() => {
            const alvo = escolhaTab;
            setEscolhaTab(null);
            setEditorAberto(alvo);
          }}
          onNovaAba={() => {
            // Filtra refs de abas que o usuario ja fechou — antes de checar
            // o limite, pra nao bloquear quando todas as anteriores ja foram
            // fechadas. `w.closed` e' true tambem se a aba foi fechada por
            // outro motivo (crash, etc.).
            abasEditorAbertasRef.current = abasEditorAbertasRef.current.filter(w => w && !w.closed);
            if (abasEditorAbertasRef.current.length >= 2) {
              notify("Limite de 2 abas de edição abertas. Feche uma para abrir outra.", "erro");
              setEscolhaTab(null);
              return;
            }
            const { arquivo, contrato } = escolhaTab;
            const url = `/editor-tab.html?arquivo=${encodeURIComponent(arquivo)}&contrato=${encodeURIComponent(contrato || "")}`;
            // Sem `noopener` pra conseguir guardar a referencia da Window
            // retornada. A aba aberta e' a propria app (/editor-tab.html),
            // entao nao ha risco de seguranca com o opener disponivel.
            const w = window.open(url, "_blank");
            if (w) abasEditorAbertasRef.current.push(w);
            setEscolhaTab(null);
          }}
        />
      )}
      {editorAberto && (
        <EditorEstacaModal
          arquivo={editorAberto.arquivo}
          arquivos={(() => {
            const contrato = editorAberto.contrato;
            if (!contrato || !Array.isArray(pastas)) return [];
            // Procura a pasta que de fato contem o arquivo aberto — usuario
            // pode estar editando tanto um original (tipo "contrato") quanto
            // uma copia editada ("obra-editada"). Prefere a pasta que casa
            // com o tipo da estaca aberta.
            const candidatas = pastas.filter(x => x.nomePasta === contrato);
            const dona = candidatas.find(p => (p.arquivos || []).includes(editorAberto.arquivo)) || candidatas[0];
            return dona?.arquivos || [];
          })()}
          onNavegar={(novoArquivo) => {
            // So troca o arquivo do editor. NAO mexe em estacaAberta aqui:
            // mudar a estaca aberta dispara reload do iframe do grafico
            // por tras (postMessage + scrollIntoView), o que aparece como
            // um "reload" da pagina de fundo durante a navegacao.
            setEditorAberto(prev => prev ? { ...prev, arquivo: novoArquivo } : prev);
          }}
          onFechar={() => {
            // Ao fechar, sincroniza a estaca aberta no painel com a ultima
            // estaca vista no editor — usuario volta pro grafico dela.
            const arquivoFinal = editorAberto.arquivo;
            setEstacaAberta(prev => prev && prev.arquivo !== arquivoFinal
              ? { ...prev, arquivo: arquivoFinal }
              : prev);
            setEditorAberto(null);
          }}
          onSalvo={({ alteracoes, arquivoOrigem } = {}) => {
            setEditorAberto(null);
            // Recarrega listagem da sidebar para refletir nova pasta de obra editada.
            setPastas(null);
            if (alteracoes) atualizarGraficoApos(arquivoOrigem);
          }}
          onRestaurado={({ arquivoOrigem } = {}) => {
            // Restaurar gravou os valores originais na copia editada. Fecha o
            // editor e volta o usuario para o grafico, que e recarregado com
            // os dados restaurados.
            setEditorAberto(null);
            setPastas(null);
            atualizarGraficoApos(arquivoOrigem);
            notify("Valores originais restaurados.", "ok");
          }}
          onListaInvalida={() => setPastas(null)}
          notify={notify}
        />
      )}
      {importCge && (
        <ImportCgeProgressModal
          estado={importCge}
          onClose={() => setImportCge(null)}
        />
      )}
      {(novaObraAberta || obraEditando || novaObraInicial) && (
        <NovaObraModal
          accent={accent}
          obra={obraEditando}
          inicial={novaObraInicial}
          onClose={() => { setNovaObraAberta(false); setObraEditando(null); setNovaObraInicial(null); }}
          onSalva={(obra, acao) => {
            setNovaObraAberta(false);
            setObraEditando(null);
            setNovaObraInicial(null);
            setObrasReloadKey(k => k + 1);
            // Tira da lista de orfaos se a obra recem-cadastrada casa com
            // um par pendente. Fecha o OrfaosModal se nao sobrar nenhum.
            setOrfaos(prev => {
              const key = `${String(obra.nome || "").toLowerCase()}|${String(obra.contrato || "").toLowerCase()}`;
              const next = prev.filter(o => `${o.nome.toLowerCase()}|${o.contrato.toLowerCase()}` !== key);
              if (next.length === 0) setOrfaosAberto(false);
              return next;
            });
            const msgs = {
              criar:     `Obra "${obra.nome}" cadastrada.`,
              salvar:    `Obra "${obra.nome}" atualizada.`,
              finalizar: `Obra "${obra.nome}" finalizada.`,
              reabrir:   `Obra "${obra.nome}" reaberta.`,
            };
            notify(msgs[acao] || msgs.salvar, "ok");
          }}
          onExcluida={(obra) => {
            setNovaObraAberta(false);
            setObraEditando(null);
            setNovaObraInicial(null);
            setObrasReloadKey(k => k + 1);
            notify(`Obra "${obra.nome}" excluída.`, "ok");
          }}
        />
      )}
      {orfaosAberto && orfaos.length > 0 && (
        <OrfaosModal
          accent={accent}
          orfaos={orfaos}
          onClose={() => setOrfaosAberto(false)}
          onIgnorar={() => {
            for (const o of orfaos) {
              dispensadosOrfaosRef.current.add(`${o.nome.toLowerCase()}|${o.contrato.toLowerCase()}`);
            }
            try {
              sessionStorage.setItem(
                "compugeo_obras_orfaos_dispensadas",
                JSON.stringify([...dispensadosOrfaosRef.current])
              );
            } catch (_) {}
            setOrfaos([]);
            setOrfaosAberto(false);
          }}
          onCadastrar={(o) => setNovaObraInicial({ nome: o.nome, contrato: o.contrato })}
        />
      )}
    </div>
  );
}

/* ---------- Modal de progresso da importacao em lote de .cge ---------- */
function ImportCgeProgressModal({ estado, onClose }) {
  const finalizado = estado.fase === "pronto";
  const proc = estado.processadas || 0;
  const total = estado.total || 0;
  const pct = total > 0 ? Math.min(100, Math.round((proc / total) * 100)) : 0;
  const r = estado.resumo;

  return (
    <div className="report-backdrop" onClick={(e) => { if (e.target === e.currentTarget && finalizado) onClose(); }}>
      <div className="report-modal" role="dialog" aria-modal="true" aria-labelledby="importTitle">
        <div className="report-head">
          <div className="report-eyebrow">{finalizado ? "RESUMO" : "IMPORTANDO"}</div>
          <h2 className="report-title" id="importTitle">
            {finalizado ? (estado.erro ? "Falha na importação" : "Importação concluída") : "Importando arquivos…"}
          </h2>
          <p className="report-sub">
            {finalizado
              ? (estado.erro
                  ? estado.erro
                  : `${r?.importadas || 0} importada(s), ${r?.ignoradas || 0} duplicada(s)${(r?.erros?.length || 0) > 0 ? `, ${r.erros.length} com erro` : ""}.`)
              : "Acompanhe o progresso abaixo. Não feche o navegador."}
          </p>
          {finalizado && (
            <button className="report-close" aria-label="Fechar" onClick={onClose}>
              <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
                <path d="M18 6L6 18M6 6l12 12"/>
              </svg>
            </button>
          )}
        </div>

        {!finalizado ? (
          <div className="report-body report-progress">
            <div className="report-progress-head">
              <div className="report-progress-title">{estado.fase === "enviando" ? "Enviando arquivos…" : "Processando estacas…"}</div>
              <div className="report-progress-count">{proc} de {total}</div>
            </div>
            <div className="report-progress-bar">
              <div className="report-progress-bar-fill" style={{ width: `${pct}%` }}/>
            </div>
            {estado.atual && (
              <div className="report-progress-current">
                Processando: <strong>{estado.atual}</strong>
              </div>
            )}
          </div>
        ) : estado.erro ? (
          <div className="report-body">
            <p style={{ color: "#B42318", margin: 0 }}>{estado.erro}</p>
          </div>
        ) : (
          <div className="report-body">
            <ul className="import-summary">
              <li><strong>{r.total}</strong> arquivo(s) processado(s)</li>
              <li className="is-ok"><strong>{r.importadas}</strong> importada(s)</li>
              <li className="is-warn"><strong>{r.ignoradas}</strong> duplicada(s) (já existiam)</li>
              {(r.erros?.length || 0) > 0 && (
                <li className="is-fail"><strong>{r.erros.length}</strong> com erro</li>
              )}
            </ul>
            {(r.erros?.length || 0) > 0 && (
              <details className="import-details">
                <summary>Ver erros</summary>
                <ul className="import-list">
                  {r.erros.map((e, i) => (
                    <li key={i}><strong>{e.nome}</strong>: {e.motivo}</li>
                  ))}
                </ul>
              </details>
            )}
            {(r.ignoradasNomes?.length || 0) > 0 && (
              <details className="import-details">
                <summary>Ver duplicadas ({r.ignoradasNomes.length})</summary>
                <ul className="import-list">
                  {r.ignoradasNomes.map((n, i) => <li key={i}>{n}</li>)}
                </ul>
              </details>
            )}
            <div style={{ marginTop: 16, display: "flex", justifyContent: "flex-end" }}>
              <button className="dash-btn-primary" style={{ background: "#0F1B2D", border: 0, color: "#fff" }} onClick={onClose}>
                Fechar
              </button>
            </div>
          </div>
        )}
      </div>
    </div>
  );
}

/* ---------- Modal de cadastro/edicao manual de obra (botao do dashboard) ----------
 * Quando `obra` vem preenchida, entra em modo edicao: prefill dos campos
 * e PATCH em vez de POST. Title e label do submit mudam. No modo edicao
 * o modal tambem oferece "Finalizar/Reabrir" e "Excluir" no rodape.
 * `inicial={{nome, contrato}}` prefilla so esses dois campos no modo
 * criar — usado pelo fluxo de "obra detectada nao cadastrada". */
function NovaObraModal({ accent, obra, inicial, onClose, onSalva, onExcluida }) {
  const editando = !!(obra && obra.id);
  // Datas vem da API como ISO completo (DATE no PG vira "AAAA-MM-DDT...Z"
  // pelo node-pg) — corta pros 10 primeiros chars que input[type=date] aceita.
  const formatDateInput = (v) => {
    if (!v) return "";
    if (v instanceof Date) return v.toISOString().slice(0, 10);
    return String(v).slice(0, 10);
  };
  const [nome, setNome]             = useState(editando ? (obra.nome || "") : (inicial?.nome || ""));
  const [contrato, setContrato]     = useState(editando ? (obra.contrato || "") : (inicial?.contrato || ""));
  const [qtEstacas, setQtEstacas]   = useState(editando ? String(obra.qt_estacas ?? "") : "");
  const [dataInicio, setDataInicio] = useState(editando ? formatDateInput(obra.data_inicio) : "");
  const [dataFim, setDataFim]       = useState(editando ? formatDateInput(obra.data_fim) : "");
  const [erro, setErro] = useState(null);
  const [enviando, setEnviando] = useState(false);
  const primeiroInputRef = useRef(null);

  useEffect(() => { primeiroInputRef.current?.focus(); }, []);
  useEffect(() => {
    const onKey = (e) => { if (e.key === "Escape") { e.preventDefault(); onClose(); } };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [onClose]);

  const jaFinalizada = !!(obra && obra.finalizada_em);

  // Submete o form. `opts.finalizada` (boolean opcional) entra junto no
  // PATCH — assim Salvar/Finalizar/Reabrir compartilham o mesmo fluxo de
  // validacao e gravam quaisquer edicoes pendentes na mesma chamada.
  const enviar = async (e, opts = {}) => {
    if (e) e.preventDefault();
    const nomeT = nome.trim();
    const contratoT = contrato.trim();
    const qt = Number(qtEstacas);
    if (!nomeT)      { setErro("Nome da obra é obrigatório."); return; }
    if (!contratoT)  { setErro("Número do contrato é obrigatório."); return; }
    if (!Number.isFinite(qt) || qt <= 0 || !Number.isInteger(qt)) {
      setErro("Quantidade de estacas deve ser um inteiro positivo."); return;
    }
    if (!dataInicio) { setErro("Informe a data de início."); return; }
    if (!dataFim)    { setErro("Informe a data de fim."); return; }
    if (dataFim < dataInicio) { setErro("Data de fim não pode ser anterior à data de início."); return; }

    setEnviando(true); setErro(null);
    try {
      const url    = editando ? `/api/obras/${obra.id}` : "/api/obras";
      const method = editando ? "PATCH" : "POST";
      const body = {
        nome: nomeT,
        contrato: contratoT,
        qt_estacas: qt,
        data_inicio: dataInicio,
        data_fim: dataFim,
      };
      if (editando && typeof opts.finalizada === "boolean") body.finalizada = opts.finalizada;
      const r = await fetch(url, {
        method,
        credentials: "same-origin",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(body),
      });
      const j = await r.json().catch(() => ({}));
      if (!r.ok) {
        setErro(j.erro || (editando ? "Falha ao salvar alterações." : "Falha ao cadastrar a obra."));
        return;
      }
      const acao = !editando
        ? "criar"
        : (opts.finalizada === true ? "finalizar"
        : opts.finalizada === false ? "reabrir"
        : "salvar");
      onSalva(j, acao);
    } catch (_) {
      setErro("Erro de rede.");
    } finally {
      setEnviando(false);
    }
  };

  const excluir = async () => {
    if (!editando) return;
    if (!window.confirm(`Excluir a obra "${obra.nome}"? Esta ação não pode ser desfeita.`)) return;
    setEnviando(true); setErro(null);
    try {
      const r = await fetch(`/api/obras/${obra.id}`, {
        method: "DELETE",
        credentials: "same-origin",
      });
      const j = await r.json().catch(() => ({}));
      if (!r.ok) { setErro(j.erro || "Falha ao excluir a obra."); return; }
      onExcluida(obra);
    } catch (_) {
      setErro("Erro de rede.");
    } finally {
      setEnviando(false);
    }
  };

  return (
    <div className="modal-backdrop is-open" onClick={(e) => { if (e.currentTarget === e.target) onClose(); }} style={{ zIndex: 1300 }}>
      <div className="modal modal-obra" style={{ maxWidth: 520 }}>
        <div className="modal-head">
          <h3>{editando ? "Editar obra" : "Nova obra"}</h3>
          <p>{editando ? "Atualize os dados da obra." : "Preencha os dados de cadastro da obra."}</p>
        </div>
        <form onSubmit={enviar}>
          <div className="modal-body">
            <div className="field full">
              <label className="field-label">Nome da obra</label>
              <input
                ref={primeiroInputRef}
                className="field-input"
                type="text"
                value={nome}
                onChange={(e) => setNome(e.target.value)}
                maxLength={200}
              />
            </div>
            <div className="field full">
              <label className="field-label">Número do contrato</label>
              <input
                className="field-input"
                type="text"
                value={contrato}
                onChange={(e) => setContrato(e.target.value)}
                maxLength={100}
              />
            </div>
            <div className="field full">
              <label className="field-label">Quantidade de estacas</label>
              <input
                className="field-input"
                type="number"
                min="1"
                step="1"
                value={qtEstacas}
                onChange={(e) => setQtEstacas(e.target.value)}
              />
            </div>
            <div className="field">
              <label className="field-label">Data de início</label>
              <input
                className="field-input"
                type="date"
                value={dataInicio}
                onChange={(e) => setDataInicio(e.target.value)}
              />
            </div>
            <div className="field">
              <label className="field-label">Data de fim</label>
              <input
                className="field-input"
                type="date"
                value={dataFim}
                onChange={(e) => setDataFim(e.target.value)}
                min={dataInicio || undefined}
              />
            </div>
            {erro && (
              <div className="full" style={{ color: "#B33A1A", fontSize: 12, fontWeight: 500 }}>{erro}</div>
            )}
          </div>
          <div className="modal-foot">
            {editando && (
              <button
                type="button"
                className="btn-ghost btn-danger"
                style={{ marginRight: "auto" }}
                onClick={excluir}
                disabled={enviando}
              >
                Excluir
              </button>
            )}
            {editando && (
              <button
                type="button"
                className="btn-ghost"
                onClick={() => enviar(null, { finalizada: !jaFinalizada })}
                disabled={enviando}
              >
                {jaFinalizada ? "Reabrir" : "Finalizar"}
              </button>
            )}
            <button type="button" className="btn-ghost" onClick={onClose} disabled={enviando}>Cancelar</button>
            <button
              type="submit"
              className="btn-primary"
              style={{ background: accent.hex }}
              disabled={enviando}
            >
              {enviando ? "Salvando…" : (editando ? "Salvar" : "Cadastrar")}
            </button>
          </div>
        </form>
      </div>
    </div>
  );
}

/* ---------- Modal de obras detectadas mas nao cadastradas ----------
 * Aparece quando o PileTable detecta arquivos novos cujo par
 * (obra, contrato) nao tem cadastro em /api/obras. Lista os pares
 * orfaos com botao Cadastrar por linha. "Agora nao" guarda os pares
 * em sessionStorage pra nao reabrir o modal pra eles. */
function OrfaosModal({ accent, orfaos, onClose, onIgnorar, onCadastrar }) {
  useEffect(() => {
    const onKey = (e) => { if (e.key === "Escape") { e.preventDefault(); onClose(); } };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [onClose]);

  return (
    <div
      className="modal-backdrop is-open"
      onClick={(e) => { if (e.currentTarget === e.target) onClose(); }}
      style={{ zIndex: 1250 }}
    >
      <div className="modal" style={{ maxWidth: 560 }}>
        <div className="modal-head">
          <h3>Obras não cadastradas</h3>
          <p>Detectamos estacas vinculadas a obras sem cadastro. Deseja cadastrar agora?</p>
        </div>
        <div className="modal-body" style={{ gridTemplateColumns: "1fr" }}>
          <ul className="orfaos-list">
            {orfaos.map(o => (
              <li key={`${o.nome}|${o.contrato}`} className="orfao-row">
                <div className="orfao-info">
                  <div className="orfao-nome">{o.nome}</div>
                  <div className="orfao-sub">
                    Contrato {o.contrato} · {o.estacas} {o.estacas === 1 ? "estaca nova" : "estacas novas"}
                  </div>
                </div>
                <button
                  type="button"
                  className="btn-primary"
                  style={{ background: accent.hex }}
                  onClick={() => onCadastrar(o)}
                >
                  Cadastrar
                </button>
              </li>
            ))}
          </ul>
        </div>
        <div className="modal-foot">
          <button type="button" className="btn-ghost" onClick={onIgnorar} style={{ marginRight: "auto" }}>
            Agora não
          </button>
          <button type="button" className="btn-ghost" onClick={onClose}>Fechar</button>
        </div>
      </div>
    </div>
  );
}

/* ---------- Modal de reautenticacao admin (duplo-clique em estaca) ---------- */
function AdminLoginModal({ accent, onClose, onSucesso }) {
  const [senha, setSenha] = useState("");
  const [erro, setErro] = useState(null);
  const [enviando, setEnviando] = useState(false);

  const enviar = async (e) => {
    if (e) e.preventDefault();
    if (!senha) { setErro("Informe a senha."); return; }
    setEnviando(true); setErro(null);
    try {
      const r = await fetch("/api/verificar-admin", {
        method: "POST",
        credentials: "same-origin",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ senha }),
      });
      const j = await r.json().catch(() => ({}));
      if (!r.ok) {
        setErro(j.erro || "Falha ao verificar a senha.");
        return;
      }
      onSucesso(j);
    } catch (err) {
      setErro("Erro de rede.");
    } finally {
      setEnviando(false);
    }
  };

  return (
    <div className="modal-backdrop is-open" onClick={(e) => { if (e.currentTarget === e.target) onClose(); }} style={{ zIndex: 1300 }}>
      <div className="modal" style={{ maxWidth: 420 }}>
        <div className="modal-head">
          <h3>Autorizar edição</h3>
          <p>Confirme sua senha para continuar.</p>
        </div>
        <form onSubmit={enviar}>
          <div className="modal-body" style={{ gridTemplateColumns: "1fr" }}>
            <div className="field full">
              <label className="field-label">Senha</label>
              <input
                className="field-input"
                type="password"
                value={senha}
                onChange={(e) => setSenha(e.target.value)}
                autoFocus
                autoComplete="current-password"
              />
            </div>
            {erro && (
              <div className="full" style={{ color: "#B33A1A", fontSize: 12, fontWeight: 500 }}>{erro}</div>
            )}
          </div>
          <div className="modal-foot">
            <button type="button" className="btn-ghost" onClick={onClose}>Cancelar</button>
            <button
              type="submit"
              className="btn-primary"
              style={{ background: accent.hex }}
              disabled={enviando}
            >
              {enviando ? "Verificando…" : "Confirmar"}
            </button>
          </div>
        </form>
      </div>
    </div>
  );
}

/* ---------- Modal "Mesma aba ou Nova aba?" (pos-autorizacao da edicao) ----------
 * Pergunta onde abrir o editor. Mesma aba = modal sobreposto ao dashboard.
 * Nova aba = abre /editor-tab.html, uma shell minimalista que reaproveita
 * o editor sem carregar o resto do dashboard. */
function EscolhaTabModal({ accent, onClose, onMesmaAba, onNovaAba }) {
  const btnMesmaRef = React.useRef(null);

  // Foca o botao "Mesma aba" assim que o modal abre — Enter aciona o
  // botao focado por padrao no navegador. Cancelar/Nova aba continuam
  // acessiveis via clique ou Tab. Esc fecha o modal (handler abaixo).
  React.useEffect(() => {
    btnMesmaRef.current?.focus();
  }, []);

  React.useEffect(() => {
    const onKey = (e) => {
      if (e.key === 'Escape') {
        e.preventDefault();
        onClose();
      }
    };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [onClose]);

  return (
    <div className="modal-backdrop is-open" onClick={(e) => { if (e.currentTarget === e.target) onClose(); }} style={{ zIndex: 1300 }}>
      <div className="modal" style={{ maxWidth: 440 }}>
        <div className="modal-head">
          <h3>Onde abrir o editor?</h3>
          <p>Você pode editar nesta mesma aba ou abrir o editor em uma nova aba do navegador. <span style={{ opacity: 0.7 }}>Enter para confirmar.</span></p>
        </div>
        <div className="modal-foot" style={{ gap: 8 }}>
          <button type="button" className="btn-ghost" onClick={onClose}>Cancelar</button>
          <button type="button" className="btn-ghost" onClick={onNovaAba}>Nova aba</button>
          <button
            ref={btnMesmaRef}
            type="button"
            className="btn-primary"
            style={{ background: accent.hex }}
            onClick={onMesmaAba}
          >
            Mesma aba
          </button>
        </div>
      </div>
    </div>
  );
}

/* ---------- Modal de identificacao do operador (apos admin reauth) ----------
 * Aparece quando a sessao foi feita direto como admin: o admin nao pode
 * ser registrado como autor da edicao, entao o operador "real" precisa
 * digitar suas credenciais antes do editor abrir. */
function OperadorLoginModal({ accent, onClose, onSucesso }) {
  const [email, setEmail] = useState("");
  const [senha, setSenha] = useState("");
  const [erro, setErro] = useState(null);
  const [enviando, setEnviando] = useState(false);

  const enviar = async (e) => {
    if (e) e.preventDefault();
    if (!email || !senha) { setErro("Informe e-mail e senha."); return; }
    setEnviando(true); setErro(null);
    try {
      const r = await fetch("/api/verificar-operador", {
        method: "POST",
        credentials: "same-origin",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ email, senha }),
      });
      const j = await r.json().catch(() => ({}));
      if (!r.ok) {
        setErro(j.erro || "Falha ao verificar credenciais.");
        return;
      }
      onSucesso(j);
    } catch (err) {
      setErro("Erro de rede.");
    } finally {
      setEnviando(false);
    }
  };

  return (
    <div className="modal-backdrop is-open" onClick={(e) => { if (e.currentTarget === e.target) onClose(); }} style={{ zIndex: 1300 }}>
      <div className="modal" style={{ maxWidth: 420 }}>
        <div className="modal-head">
          <h3>Identifique-se</h3>
          <p>Informe suas credenciais de operador. Será você o autor das edições nesta estaca.</p>
        </div>
        <form onSubmit={enviar}>
          <div className="modal-body" style={{ gridTemplateColumns: "1fr" }}>
            <div className="field full">
              <label className="field-label">E-mail</label>
              <input
                className="field-input"
                type="email"
                value={email}
                onChange={(e) => setEmail(e.target.value)}
                autoFocus
                autoComplete="username"
              />
            </div>
            <div className="field full">
              <label className="field-label">Senha</label>
              <input
                className="field-input"
                type="password"
                value={senha}
                onChange={(e) => setSenha(e.target.value)}
                autoComplete="current-password"
              />
            </div>
            {erro && (
              <div className="full" style={{ color: "#B33A1A", fontSize: 12, fontWeight: 500 }}>{erro}</div>
            )}
          </div>
          <div className="modal-foot">
            <button type="button" className="btn-ghost" onClick={onClose}>Cancelar</button>
            <button
              type="submit"
              className="btn-primary"
              style={{ background: accent.hex }}
              disabled={enviando}
            >
              {enviando ? "Verificando…" : "Continuar"}
            </button>
          </div>
        </form>
      </div>
    </div>
  );
}

/* ---------- Modal do editor de estacas (iframe + postMessage) ---------- */
function EditorEstacaModal({ arquivo, arquivos, onNavegar, onFechar, onSalvo, onRestaurado, onListaInvalida, notify }) {
  const iframeRef = React.useRef(null);
  const dadosRef = React.useRef(null);

  // Vizinhos da estaca no editor — derivados da pasta atual. Sempre que o
  // arquivo ou a lista mudar, manda pro iframe atualizar os botoes de nav.
  const vizinhos = React.useMemo(() => {
    const lista = Array.isArray(arquivos) ? arquivos : [];
    const idx = lista.indexOf(arquivo);
    if (idx === -1) return { temAnterior: false, temProximo: false, anterior: null, proximo: null };
    return {
      temAnterior: idx > 0,
      temProximo: idx < lista.length - 1,
      anterior: idx > 0 ? lista[idx - 1] : null,
      proximo: idx < lista.length - 1 ? lista[idx + 1] : null,
    };
  }, [arquivo, arquivos]);

  const enviarVizinhos = React.useCallback(() => {
    const iframe = iframeRef.current;
    if (!iframe || !iframe.contentWindow) return;
    iframe.contentWindow.postMessage({
      tipo: "editor-vizinhos",
      temAnterior: vizinhos.temAnterior,
      temProximo: vizinhos.temProximo,
      nomeAnterior: vizinhos.anterior ? stripSufixoEditado(vizinhos.anterior) : "",
      nomeProximo: vizinhos.proximo ? stripSufixoEditado(vizinhos.proximo) : "",
    }, "*");
  }, [vizinhos]);

  // Carrega dados ao montar. Refaz so quando arquivo mudar — onFechar/notify
  // sao recriados a cada render do Dashboard, mas seus efeitos colaterais
  // (fechar / notificar) sao identicos. Manter so `arquivo` em deps evita
  // refetch durante saves/refresh de sidebar, que estavam fechando o editor
  // em casos de race condition.
  React.useEffect(() => {
    let cancelado = false;
    fetch(`/api/estaca-editor?arquivo=${encodeURIComponent(arquivo)}`, { credentials: "same-origin" })
      .then(r => r.json())
      .then(j => {
        if (cancelado) return;
        if (!j || j.erro) {
          notify(j?.erro || "Falha ao carregar estaca", "erro");
          onFechar();
          return;
        }
        dadosRef.current = j;
        const iframe = iframeRef.current;
        if (iframe && iframe.contentWindow) {
          iframe.contentWindow.postMessage({ tipo: "carregar-editor", arquivo, dados: j }, "*");
          enviarVizinhos();
        }
      })
      .catch(() => {
        if (cancelado) return;
        // Erro de rede transiente — nao fecha o editor.
        notify("Erro de rede ao carregar estaca", "erro");
      });
    return () => { cancelado = true; };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [arquivo]);

  // Atualiza estado dos botoes prev/next se a lista da pasta mudar enquanto
  // o editor estiver aberto (ex: nova estaca chegou no servidor e a sidebar
  // foi recarregada). Independente do reload de dados.
  React.useEffect(() => {
    enviarVizinhos();
  }, [enviarVizinhos]);

  // Atalhos no nivel do dashboard — cobrem o caso onde o foco nao esta
  // dentro do iframe (ex: usuario abriu o modal e nao clicou dentro
  // ainda). O proprio editor tambem escuta keydown internamente para
  // quando o foco esta dentro do iframe.
  React.useEffect(() => {
    const onKey = (e) => {
      if (e.key === 'Escape') {
        e.preventDefault();
        onFechar();
        return;
      }
      if ((e.ctrlKey || e.metaKey) && (e.key === 's' || e.key === 'S')) {
        e.preventDefault();
        const iframe = iframeRef.current;
        iframe?.contentWindow?.postMessage({ tipo: 'editor-atalho-salvar' }, '*');
      }
    };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [onFechar]);

  React.useEffect(() => {
    const handler = async (ev) => {
      const m = ev.data;
      if (!m || typeof m !== "object") return;
      const iframe = iframeRef.current;
      if (m.tipo === "editor-pronto") {
        if (dadosRef.current && iframe && iframe.contentWindow) {
          iframe.contentWindow.postMessage({ tipo: "carregar-editor", arquivo, dados: dadosRef.current }, "*");
        }
        enviarVizinhos();
        return;
      }
      if (m.tipo === "editor-fechar") {
        onFechar();
        return;
      }
      if (m.tipo === "editor-nav") {
        const alvo = m.direcao === "next" ? vizinhos.proximo : m.direcao === "prev" ? vizinhos.anterior : null;
        if (alvo && typeof onNavegar === "function") onNavegar(alvo);
        return;
      }
      if (m.tipo === "editor-restaurar") {
        // Restaura: busca a estaca ORIGINAL (sem sufixo) e aplica os
        // valores dela sobre a copia editada — gravando no banco. O
        // editor recarrega com os valores originais; a copia continua
        // existindo (mesmo arquivo_origem), agora com dados restaurados.
        const original = String(arquivo).replace(/_editado_\d+$/, "");
        const ehCopia = original !== arquivo;
        const avisarErro = (msg) => {
          notify(msg, "erro");
          iframe?.contentWindow?.postMessage({ tipo: "restaurar-erro" }, "*");
        };
        try {
          const resp = await fetch(`/api/estaca-editor?arquivo=${encodeURIComponent(original)}`, { credentials: "same-origin" });
          const dadosOriginal = await resp.json().catch(() => ({}));
          if (!resp.ok || !dadosOriginal || dadosOriginal.erro) {
            avisarErro(dadosOriginal?.erro || "Falha ao buscar estaca original");
            return;
          }
          if (ehCopia) {
            const edicao = {
              cabecalho: dadosOriginal.cabecalho,
              perfuracao: dadosOriginal.perfuracao,
              concretagem: dadosOriginal.concretagem,
            };
            const upd = await fetch(`/api/estaca-editor?arquivo=${encodeURIComponent(arquivo)}&restaurar=1`, {
              method: "PUT",
              credentials: "same-origin",
              headers: { "Content-Type": "application/json" },
              body: JSON.stringify(edicao),
            });
            if (!upd.ok) {
              const j = await upd.json().catch(() => ({}));
              avisarErro(j.erro || "Falha ao restaurar valores na cópia");
              return;
            }
            // Sidebar pode ter mudado (contrato/obra restaurados ao original).
            if (typeof onListaInvalida === "function") onListaInvalida();
            // Notifica o pai pra atualizar o grafico em background (modal
            // continua aberto). Quando o usuario fechar, ja vera os dados
            // restaurados sem precisar trocar de pasta.
            if (typeof onRestaurado === "function") onRestaurado({ arquivoOrigem: arquivo });
          }
          dadosRef.current = dadosOriginal;
          iframe?.contentWindow?.postMessage({ tipo: "carregar-editor", arquivo, dados: dadosOriginal }, "*");
          iframe?.contentWindow?.postMessage({ tipo: "restaurado" }, "*");
        } catch (err) {
          avisarErro("Erro de rede ao restaurar");
        }
        return;
      }
      if (m.tipo === "editor-salvar") {
        try {
          const r = await fetch(`/api/estaca-editor?arquivo=${encodeURIComponent(arquivo)}`, {
            method: "PUT",
            credentials: "same-origin",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify(m.edicao || {}),
          });
          const j = await r.json().catch(() => ({}));
          if (!r.ok) {
            iframe?.contentWindow?.postMessage({ tipo: "salvar-resultado", ok: false, erro: j.erro || "Falha ao salvar" }, "*");
            return;
          }
          iframe?.contentWindow?.postMessage({ tipo: "salvar-resultado", ok: true, alteracoes: j.alteracoes || 0 }, "*");
          onSalvo({ alteracoes: j.alteracoes || 0, arquivoOrigem: j.arquivoOrigem || arquivo });
        } catch (err) {
          iframe?.contentWindow?.postMessage({ tipo: "salvar-resultado", ok: false, erro: "Erro de rede" }, "*");
        }
      }
    };
    window.addEventListener("message", handler);
    return () => window.removeEventListener("message", handler);
  }, [arquivo, onFechar, onSalvo, onRestaurado, onListaInvalida, notify, vizinhos, onNavegar, enviarVizinhos]);

  return (
    <div className="editor-iframe-modal">
      <iframe
        ref={iframeRef}
        src={`/editor-estacas.html?embed=1&arquivo=${encodeURIComponent(arquivo)}`}
        title="Editor de estacas"
      />
    </div>
  );
}

/* ---------- Mapa Google reutilizável ---------- */
function GoogleMap({ accentHex, mini = false, className = "mapa-container", obras = OBRAS, focar = null, mapApiRef = null }) {
  const mapRef = React.useRef(null);
  const containerRef = React.useRef(null);
  const markersRef = React.useRef([]);
  const infoRef = React.useRef(null);
  const pulseRafRef = React.useRef(null);
  const [pronto, setPronto] = useState(!!(window.google && window.google.maps));

  const obrasKey = obras.map((o, i) => `${o.id || o.nome || i}|${o.lat}|${o.lng}|${o.status}|${o.bairro}|${o.nome}`).join("§");

  React.useEffect(() => {
    let cancelado = false;
    loadGoogleMaps()
      .then(() => { if (!cancelado) setPronto(true); })
      .catch(err => console.error("Google Maps:", err));
    return () => { cancelado = true; };
  }, []);

  React.useEffect(() => {
    if (!pronto || !containerRef.current || mapRef.current) return;
    const g = window.google;
    const temaInicial = document.documentElement.getAttribute('data-theme') || 'light';
    const map = new g.maps.Map(containerRef.current, {
      center: { lat: -23.55, lng: -46.62 },
      zoom: mini ? 10 : 11,
      disableDefaultUI: mini,
      zoomControl: !mini,
      mapTypeControl: false,
      streetViewControl: false,
      fullscreenControl: false,
      gestureHandling: mini ? "none" : "greedy",
      clickableIcons: false,
      backgroundColor: temaInicial === 'dark' ? '#0B131F' : '#f6f7f9',
      styles: temaInicial === 'dark' ? MAP_STYLE_DARK : [],
    });
    mapRef.current = map;
    if (mapApiRef) mapApiRef.current = map;
    if (!mini) infoRef.current = new g.maps.InfoWindow();
    return () => {
      markersRef.current.forEach(m => m.setMap(null));
      markersRef.current = [];
      if (infoRef.current) { infoRef.current.close(); infoRef.current = null; }
      mapRef.current = null;
      if (mapApiRef) mapApiRef.current = null;
    };
  }, [pronto, mini]);

  // Reage a mudancas de tema sem recriar o mapa — observa o atributo
  // data-theme do <html> e aplica o styles correspondente.
  React.useEffect(() => {
    if (!pronto || !mapRef.current) return;
    const aplicar = () => {
      const t = document.documentElement.getAttribute('data-theme') || 'light';
      mapRef.current.setOptions({ styles: t === 'dark' ? MAP_STYLE_DARK : [] });
    };
    const obs = new MutationObserver(aplicar);
    obs.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
    return () => obs.disconnect();
  }, [pronto]);

  React.useEffect(() => {
    if (!pronto || !mapRef.current) return;
    const g = window.google;
    const map = mapRef.current;
    markersRef.current.forEach(m => m.setMap(null));
    markersRef.current = [];
    obras.forEach(o => {
      if (o.lat == null || o.lng == null) return;
      const cor = corObra(o.status, accentHex);
      const tamanho = mini ? 10 : 12;
      const baseScale = tamanho / 2;
      const marker = new g.maps.Marker({
        position: { lat: o.lat, lng: o.lng },
        map,
        title: mini ? `${o.bairro} · ${o.estacas} estacas` : o.nome,
        icon: {
          path: g.maps.SymbolPath.CIRCLE,
          fillColor: cor,
          fillOpacity: 1,
          strokeColor: "#ffffff",
          strokeWeight: 2,
          scale: baseScale,
        },
      });
      // Guardamos cor e escala base no proprio marker pra usar no loop
      // de pulsacao sem precisar de Map externa.
      marker._pulseBase = baseScale;
      marker._pulseColor = cor;
      if (!mini && infoRef.current) {
        marker.addListener("click", () => {
          infoRef.current.setContent(
            `<div class="mapa-popup">
              <div class="mapa-popup-bairro">${o.bairro}</div>
              <div class="mapa-popup-nome">${o.nome}</div>
              <div class="mapa-popup-meta">${o.estacas ? `${o.estacas} estacas · ` : ''}<span style="color:${cor}">${rotuloObra(o.status)}</span></div>
            </div>`
          );
          infoRef.current.open({ map, anchor: marker });
        });
      }
      markersRef.current.push(marker);
    });

    // Pulsacao dos pins via setIcon em senoide. Periodo ~1.4s; escala
    // varia entre base e base+2.5, opacidade entre .55 e 1. Throttle de
    // ~80ms (12.5 fps) basta pra parecer suave e mantem CPU baixo mesmo
    // com varios markers. So no mapa principal — no mini fica estatico.
    if (mini) return;
    if (pulseRafRef.current) cancelAnimationFrame(pulseRafRef.current);
    let ultimaAtualizacao = 0;
    const t0 = performance.now();
    const loop = (t) => {
      if (t - ultimaAtualizacao >= 80) {
        ultimaAtualizacao = t;
        const fase = ((t - t0) % 1400) / 1400;        // 0..1
        const onda = Math.sin(fase * Math.PI * 2) * 0.5 + 0.5; // 0..1
        markersRef.current.forEach(m => {
          const base = m._pulseBase || 6;
          m.setIcon({
            path: g.maps.SymbolPath.CIRCLE,
            fillColor: m._pulseColor,
            fillOpacity: 0.55 + onda * 0.45,
            strokeColor: "#ffffff",
            strokeWeight: 2,
            scale: base + onda * 2.5,
          });
        });
      }
      pulseRafRef.current = requestAnimationFrame(loop);
    };
    pulseRafRef.current = requestAnimationFrame(loop);

    return () => {
      if (pulseRafRef.current) cancelAnimationFrame(pulseRafRef.current);
      pulseRafRef.current = null;
    };
  }, [pronto, obrasKey, accentHex, mini]);

  React.useEffect(() => {
    if (!focar || !mapRef.current) return;
    mapRef.current.panTo({ lat: focar.lat, lng: focar.lng });
    mapRef.current.setZoom(14);
  }, [focar]);

  return <div ref={containerRef} className={className}/>;
}

/* ---------- Painel de mapa (tela cheia) ---------- */
// Cor por status da maquina. Status vem do backend (statusDaMaquina em
// repositorioMaquina.js) e e' derivado do ultimo `gps_posicao.estado`:
//   0=ocioso, 1=perfurando, 2=concretando, 4=erro. Sem ping nos ultimos
//   10min → offline.
function corStatusMaquina(status) {
  switch (status) {
    case "ocioso":      return "#22A06B"; // verde
    case "perfurando":  return "#D14343"; // vermelho
    case "concretando": return "#3B82F6"; // azul
    case "erro":        return "#E0A02A"; // ambar (estado nao especificado pelo usuario)
    case "offline":
    default:            return "#94A3B8"; // cinza
  }
}
function rotuloStatus(s) {
  return ({ offline: "Offline", ocioso: "Ocioso", perfurando: "Perfurando", concretando: "Concretando", erro: "Erro" })[s] || "—";
}

/* ============================================================
   FROTA — tela de monitoramento de maquinas, aberta pelo KPI
   "Maquinas online" do dashboard. Duas telas:
     1) FrotaPage     — lista da frota: status bar + tabela
                        (desktop) / cards (mobile). Dados de
                        /api/maquinas-mapa.
     2) FrotaRelatorio — relatorio por maquina: KPIs de tempo por
                        status + grafico dos ultimos 7 dias. Dados
                        de /api/frota-relatorio (agregacao de
                        gps_posicao — ver relatorioFrota() em
                        repositorioMaquina.js).
============================================================ */

// Tempo relativo curto ("agora", "há 12s", "há 4 min", "há 3 h",
// "há 2 dias") a partir de um timestamp.
function tempoRelativo(ts) {
  if (!ts) return "—";
  const ms = new Date(ts).getTime();
  if (!Number.isFinite(ms)) return "—";
  const seg = Math.floor(Math.max(0, Date.now() - ms) / 1000);
  if (seg < 10) return "agora";
  if (seg < 60) return `há ${seg}s`;
  const min = Math.floor(seg / 60);
  if (min < 60) return `há ${min} min`;
  const h = Math.floor(min / 60);
  if (h < 24) return `há ${h} h`;
  const d = Math.floor(h / 24);
  return `há ${d} dia${d === 1 ? "" : "s"}`;
}

// Data/hora curta "DD/MM HH:MM:SS" — exibida junto do tempo relativo.
function dataHoraCurta(ts) {
  if (!ts) return "";
  const d = new Date(ts);
  if (!Number.isFinite(d.getTime())) return "";
  const p = (n) => String(n).padStart(2, "0");
  return `${p(d.getDate())}/${p(d.getMonth() + 1)} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
}

// Classe do badge de status (cores em login-dash.css → .frota-badge--*).
function classeBadgeStatus(s) {
  return `frota-badge frota-badge--${s || "offline"}`;
}

// "5h 48min" a partir de horas decimais.
function fmtHoras(h) {
  const total = Math.max(0, Math.round((Number(h) || 0) * 60));
  return `${Math.floor(total / 60)}h ${String(total % 60).padStart(2, "0")}min`;
}

// Nome do estado a partir do codigo numerico (gps_posicao.estado).
const FROTA_ESTADO_NOME = { 0: "ocioso", 1: "perfurando", 2: "concretando" };

// "HH:MM" (24h) → minutos do dia. "09:15" → 555.
function hmParaMin(s) {
  const [h, m] = String(s || "0:0").split(":").map(Number);
  return (Number.isFinite(h) ? h : 0) * 60 + (Number.isFinite(m) ? m : 0);
}
// minutos do dia → "HH:MM" pro eixo de hora real do modo timeline.
function fmtHoraDia(min) {
  const t = ((Math.round(min) % 1440) + 1440) % 1440;
  return `${String(Math.floor(t / 60)).padStart(2, "0")}:${String(t % 60).padStart(2, "0")}`;
}

// Grafico de barras empilhadas — tempo por status nos 7 dias mais recentes
// COM producao (dias sem producao sao pulados, ver relatorioFrota()).
// `dias` = [{ d, ocioso, perfurando, concretando, segmentos }] — horas por
// status + `segmentos` (sequencia cronologica de runs do dia).
//
// Dois modos (prop `modo`):
//   - "totais"   : cada barra = total de horas por status no dia, 3
//                  segmentos ordenados (ocioso/perfurando/concretando).
//                  Altura fixa de 240px, eixo Y em horas.
//   - "timeline" : cada barra = a sequencia REAL do dia. Eixo Y = HORA REAL
//                  DO DIA (ex.: 06:00 → 18:00), nao tempo acumulado. A
//                  janela vai do 1o ao ultimo log de qualquer dia visivel,
//                  arredondada pra hora cheia. Cada run e' posicionada no
//                  horario em que aconteceu (start/fim de `s.hi`/`s.hf`);
//                  periodos sem sinal viram espaco vazio. Zoom vertical de
//                  1px/min; o wrapper rola na vertical (.is-timeline no
//                  CSS). <title> nativo mostra o intervalo de cada run.
//
// O SVG mede o container (ResizeObserver) e renderiza o viewBox na largura
// real em px — mesmo padrao do ProductionChart. Antes usava
// preserveAspectRatio="none", que esticava o SVG e distorcia texto/barras.
// Com viewBox 1:1 o vetor escala nitido.
const FROTA_PX_POR_MIN = 1; // zoom vertical do modo timeline
function FrotaChart({ dias, modo = "totais" }) {
  // Backend ja retorna so dias com producao (ate 7), entao todo `dia` aqui
  // tem barra/segmentos pra desenhar. Se a serie nao tem producao alguma,
  // mostra aviso no lugar do grafico.
  const visiveis = (dias || []);
  const wrapRef = useRef(null);
  const [W, setW] = useState(720);
  useEffect(() => {
    const el = wrapRef.current;
    if (!el || typeof ResizeObserver === "undefined") return;
    const atualizar = () => {
      const w = el.clientWidth;
      if (w > 0) setW(Math.max(320, Math.round(w)));
    };
    atualizar();
    const ro = new ResizeObserver(atualizar);
    ro.observe(el);
    return () => ro.disconnect();
  }, []);

  const ehTimeline = modo === "timeline";

  if (visiveis.length === 0) {
    return (
      <div className={`frota-chart-wrap ${ehTimeline ? "is-timeline" : ""}`} ref={wrapRef}>
        <div className="frota-chart-vazio">Sem dados no período.</div>
      </div>
    );
  }

  // Janela de relogio do modo timeline: do 1o ao ultimo log de qualquer dia
  // visivel, arredondada pra hora cheia. Default 06:00–18:00 se nao houver
  // runs (jornada tipica de obra).
  let janIni = Infinity, janFim = -Infinity;
  visiveis.forEach(d => (d.segmentos || []).forEach(s => {
    const a = hmParaMin(s.hi);
    let b = hmParaMin(s.hf);
    if (b < a) b += 1440; // run cruzando meia-noite
    if (a < janIni) janIni = a;
    if (b > janFim) janFim = b;
  }));
  if (!Number.isFinite(janIni) || janFim <= janIni) {
    janIni = 6 * 60; janFim = 18 * 60;
  } else {
    janIni = Math.floor(janIni / 60) * 60;
    janFim = Math.ceil(janFim / 60) * 60;
    if (janFim <= janIni) janFim = janIni + 60;
  }
  const janDur = janFim - janIni; // minutos

  // Geometria. Totais: altura fixa 240, eixo em horas. Timeline: altura =
  // janela de relogio * PX_POR_MIN (zoom vertical), rotulo do dia no topo.
  const ML = ehTimeline ? 54 : 44, MR = 14;
  const MT = ehTimeline ? 26 : 12;
  const MB = ehTimeline ? 14 : 30;
  const iH = ehTimeline ? Math.round(janDur * FROTA_PX_POR_MIN) : 240 - MT - MB;
  const H = MT + iH + MB;
  const iW = W - ML - MR;
  const n = visiveis.length;
  const gw = iW / n;
  const bw = Math.min(46, gw * 0.46);

  // Timeline: minuto-do-dia → y. Totais: horas → y.
  const yDeMin = (min) => MT + ((min - janIni) / janDur) * iH;
  const totalDia = (d) => d.ocioso + d.perfurando + d.concretando;
  const maxH = Math.max(12, ...visiveis.map(totalDia)); // so usado no modo totais
  const escalaY = (v) => (v / maxH) * iH;

  // Linhas de grade + rotulos do eixo Y.
  let gridLinhas;
  if (ehTimeline) {
    // Marcacoes de hora em hora (ou de 2h se a janela for muito larga).
    const gIntervalo = janDur > 900 ? 120 : 60;
    gridLinhas = [];
    for (let m = janIni; m <= janFim + 0.5; m += gIntervalo) {
      gridLinhas.push({ y: yDeMin(m), label: fmtHoraDia(m) });
    }
  } else {
    const ticks = 4;
    gridLinhas = Array.from({ length: ticks + 1 }, (_, i) => ({
      y: MT + (iH * i) / ticks,
      label: `${Math.round(maxH - (maxH * i) / ticks)}h`,
    }));
  }
  const yBase = ehTimeline ? MT : MT + iH; // baseline: topo no timeline, base no totais

  return (
    <div className={`frota-chart-wrap ${ehTimeline ? "is-timeline" : ""}`} ref={wrapRef}>
      <svg
        className="frota-chart-svg"
        viewBox={`0 0 ${W} ${H}`}
        width={W}
        height={H}
        role="img"
        aria-label={ehTimeline
          ? "Linha do tempo dos estados por dia, eixo em hora real"
          : "Tempo por status nos últimos 7 dias"}
      >
        {/* hachura cinza pros gaps offline do modo timeline */}
        <defs>
          <pattern id="frotaHachura" width="7" height="7" patternUnits="userSpaceOnUse" patternTransform="rotate(45)">
            <rect width="7" height="7" fill="rgba(148,163,184,0.10)"/>
            <line x1="3.5" y1="0" x2="3.5" y2="7" stroke="rgba(148,163,184,0.55)" strokeWidth="1.6"/>
          </pattern>
        </defs>
        {gridLinhas.map((g, i) => (
          <g key={`g${i}`}>
            <line x1={ML} x2={W - MR} y1={g.y} y2={g.y} className="frota-chart-grid"/>
            <text x={ML - 8} y={g.y + 4} textAnchor="end" className="frota-chart-axis">{g.label}</text>
          </g>
        ))}
        {visiveis.map((d, i) => {
          const x = ML + i * gw + (gw - bw) / 2;
          const cx = ML + i * gw + gw / 2;
          // Timeline: cada run posicionada pela HORA REAL (s.hi/s.hf); gaps
          // viram espaco vazio. Totais: 3 segmentos empilhados de baixo p/ cima.
          let conteudo;
          if (ehTimeline) {
            const segs = d.segmentos || [];
            // Gaps entre runs consecutivas = periodos offline / sem sinal —
            // preenchidos com hachura cinza em vez de ficarem vazios.
            const gaps = [];
            for (let g = 0; g + 1 < segs.length; g++) {
              const de = hmParaMin(segs[g].hf);
              const ate = hmParaMin(segs[g + 1].hi);
              if (ate > de + 0.01) gaps.push({ de, ate, k: `gap${g}` });
            }
            conteudo = [
              // gaps primeiro (atras), runs por cima
              ...gaps.map(gp => {
                const y0 = yDeMin(gp.de);
                const h = yDeMin(gp.ate) - y0;
                if (h <= 0) return null;
                return (
                  <rect key={gp.k} x={x} y={y0} width={bw} height={h} fill="url(#frotaHachura)">
                    <title>{`${fmtHoraDia(gp.de)}–${fmtHoraDia(gp.ate)} · Offline`}</title>
                  </rect>
                );
              }),
              ...segs.map((s, si) => {
                const a = hmParaMin(s.hi);
                let b = hmParaMin(s.hf);
                if (b < a) b += 1440;
                const y0 = yDeMin(a);
                const h = yDeMin(b) - y0;
                if (h <= 0) return null;
                const nome = FROTA_ESTADO_NOME[s.estado] || "ocioso";
                return (
                  <rect key={`seg${si}`} x={x} y={y0} width={bw} height={h} fill={corStatusMaquina(nome)}>
                    <title>{`${s.hi}–${s.hf} · ${rotuloStatus(nome)}`}</title>
                  </rect>
                );
              }),
            ];
          } else {
            let yy = MT + iH;
            conteudo = [
              { k: "ocioso", v: d.ocioso },
              { k: "perfurando", v: d.perfurando },
              { k: "concretando", v: d.concretando },
            ].map(s => {
              const h = escalaY(s.v);
              if (h <= 0) return null;
              yy -= h;
              return <rect key={s.k} x={x} y={yy} width={bw} height={h} rx="2" fill={corStatusMaquina(s.k)}/>;
            });
          }
          return (
            <g key={d.d}>
              {conteudo}
              <text
                x={cx}
                y={ehTimeline ? MT - 10 : MT + iH + 18}
                textAnchor="middle"
                className="frota-chart-axis"
              >{d.d}</text>
            </g>
          );
        })}
        <line x1={ML} x2={W - MR} y1={yBase} y2={yBase} className="frota-chart-eixo"/>
      </svg>
    </div>
  );
}

// Card de relatorio de uma maquina — exibido INLINE na FrotaPage, entre a
// status bar e a lista (nao e' uma tela separada). KPIs de tempo por
// status no periodo selecionado + grafico fixo dos ultimos 7 dias. O %
// e' sobre "tempo com sinal" (ocioso + perfurando + concretando) — estado
// de erro e periodos offline ficam de fora. Ver relatorioFrota() em
// repositorioMaquina.js.
function FrotaRelatorio({ maquina, onFechar }) {
  const [periodo, setPeriodo] = useState("7d");
  const [relatorio, setRelatorio] = useState(null); // null = 1a carga
  const [carregando, setCarregando] = useState(true);
  const [erro, setErro] = useState(null);
  // Modo do grafico: "totais" (horas somadas por status) ou "timeline"
  // (sequencia cronologica dos estados ao longo do dia). So afeta a
  // visualizacao — os dois modos usam os mesmos dados ja carregados.
  const [modoGrafico, setModoGrafico] = useState("totais");
  const cardRef = React.useRef(null);
  const PERIODOS = [
    { id: "hoje", label: "Hoje" },
    { id: "7d",   label: "7 dias" },
    { id: "mes",  label: "Mês" },
  ];

  // `reqRef` da' um id incremental a cada request; respostas de requests
  // antigos (periodo trocado no meio, ou poll concorrente) sao descartadas.
  const reqRef = React.useRef(0);

  // Busca o relatorio. `silencioso` (poll de 60s): nao pisca o "Carregando…"
  // nem esmaece os KPIs, e mantem o ultimo dado bom se a request falhar.
  // NAO faz setRelatorio(null) — manter KPIs/grafico na tela enquanto
  // recarrega evita o flash e o remount do grafico. Só os KPIs dependem do
  // periodo; o grafico e' sempre os 7 dias mais recentes com producao, entao
  // nao "recarrega" — so re-renderiza com os mesmos dados (instantaneo).
  // Obs.: como FrotaRelatorio e' montado com key={maquina.id} na FrotaPage,
  // trocar de maquina remonta o componente — `relatorio` volta a null e a
  // 1a carga mostra o "Carregando…" normalmente.
  const buscarRelatorio = React.useCallback((silencioso) => {
    const meu = ++reqRef.current;
    if (!silencioso) {
      setErro(null);
      setCarregando(true);
    }
    const serie = maquina.num_serie || "";
    fetch(`/api/frota-relatorio?serie=${encodeURIComponent(serie)}&periodo=${encodeURIComponent(periodo)}`, { credentials: "same-origin" })
      .then(r => r.ok ? r.json() : r.json().then(j => Promise.reject(j.erro || `Erro ${r.status}`)))
      .then(data => {
        if (meu !== reqRef.current) return; // resposta obsoleta
        setRelatorio(data && data.kpis ? data : { kpis: [], dias: [] });
        setCarregando(false);
        setErro(null);
      })
      .catch(e => {
        if (meu !== reqRef.current) return;
        // Poll silencioso: mantem o ultimo dado bom e ignora o erro.
        if (!silencioso) {
          setErro(String(e || "Falha ao carregar relatório"));
          setCarregando(false);
        }
      });
  }, [maquina.num_serie, periodo]);

  // Carga inicial + refetch ao trocar de periodo (com dim nos KPIs).
  useEffect(() => { buscarRelatorio(false); }, [buscarRelatorio]);

  // Polling de 60s + refresh ao voltar pra aba — silencioso (sem dim, sem
  // flash). Mesma cadencia da lista da frota e do resto do app.
  useEffect(() => {
    const id = setInterval(() => buscarRelatorio(true), 60 * 1000);
    const onVis = () => {
      if (document.visibilityState === "visible") buscarRelatorio(true);
    };
    document.addEventListener("visibilitychange", onVis);
    return () => {
      clearInterval(id);
      document.removeEventListener("visibilitychange", onVis);
    };
  }, [buscarRelatorio]);

  // Ao abrir (ou trocar de maquina) traz o card pra dentro da viewport —
  // sem isso, clicar numa linha la' embaixo da lista mostraria o card fora
  // da tela. `nearest` nao mexe no scroll se ja' estiver visivel.
  useEffect(() => {
    cardRef.current?.scrollIntoView({ behavior: "smooth", block: "nearest" });
  }, [maquina.num_serie]);

  return (
    <section className="frota-detalhe-card" ref={cardRef}>
      <header className="frota-detalhe-hd">
        <div className="frota-detalhe-hd__l">
          <div className="frota-detalhe-tag">
            {maquina.tag || maquina.nome || "—"}
            <span className={classeBadgeStatus(maquina.status)}>{rotuloStatus(maquina.status)}</span>
          </div>
          <div className="frota-detalhe-meta">
            Série {maquina.num_serie || "—"} · Obra {maquina.obra || "—"} · Última atualização {dataHoraCurta(maquina.gps_momento) || "—"}
          </div>
        </div>
        <button type="button" className="frota-detalhe-fechar" onClick={onFechar} aria-label="Fechar relatório">
          <svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
            <line x1="6" y1="6" x2="18" y2="18"/><line x1="18" y1="6" x2="6" y2="18"/>
          </svg>
        </button>
      </header>
      <div className="frota-detalhe-body">
        <div className="frota-periodo">
          {PERIODOS.map(p => (
            <button
              key={p.id}
              type="button"
              className={periodo === p.id ? "is-active" : ""}
              onClick={() => setPeriodo(p.id)}
            >{p.label}</button>
          ))}
        </div>
        {erro && <div className="usuarios-erro">{erro}</div>}
        {!erro && relatorio === null && <div className="usuarios-empty">Carregando…</div>}
        {!erro && relatorio && (
          <>
            {/* `is-recarregando` so esmaece os KPIs durante o refetch de
                periodo — o grafico abaixo nao e' afetado. */}
            <div className={`frota-kpis ${carregando ? "is-recarregando" : ""}`}>
              {relatorio.kpis.map(k => (
                <div key={k.id} className={`frota-kpi frota-kpi--${k.id}`}>
                  <span className="frota-kpi__lbl">{k.label}</span>
                  <span className="frota-kpi__val">{fmtHoras((k.segundos || 0) / 3600)}</span>
                  <span className="frota-kpi__pct">{(k.pct || 0).toFixed(1).replace(".", ",")}% do período</span>
                </div>
              ))}
            </div>
            <div className="frota-chart-head">
              <div className="frota-chart-titulo">
                {modoGrafico === "timeline" ? "Linha do tempo · últimos 7 dias" : "Total por status · últimos 7 dias"}
              </div>
              <div className="frota-chart-toggle">
                <button
                  type="button"
                  className={modoGrafico === "totais" ? "is-active" : ""}
                  onClick={() => setModoGrafico("totais")}
                >Totais</button>
                <button
                  type="button"
                  className={modoGrafico === "timeline" ? "is-active" : ""}
                  onClick={() => setModoGrafico("timeline")}
                >Linha do tempo</button>
              </div>
            </div>
            <div className="frota-chart">
              <FrotaChart dias={relatorio.dias} modo={modoGrafico}/>
            </div>
            <div className="frota-legenda">
              {[["ocioso", "Ocioso"], ["perfurando", "Perfurando"], ["concretando", "Concretando"]].map(([k, l]) => (
                <span key={k} className="frota-legenda__item">
                  <span className="frota-legenda__sw" style={{ background: corStatusMaquina(k) }}/>{l}
                </span>
              ))}
              {modoGrafico === "timeline" && (
                <span className="frota-legenda__item">
                  <span className="frota-legenda__sw frota-legenda__sw--hachura"/>Offline / sem sinal
                </span>
              )}
            </div>
          </>
        )}
      </div>
    </section>
  );
}

// Tela 1 — lista da frota. Aberta pelo KPI "Maquinas online".
function FrotaPage({ accent, onVoltar }) {
  const [maquinas, setMaquinas] = useState(null); // null = carregando
  const [erro, setErro] = useState(null);
  const [selecionada, setSelecionada] = useState(null);
  const [filtroStatus, setFiltroStatus] = useState("todas");
  const [busca, setBusca] = useState("");

  // `silencioso`: no polling de 60s nao mexe em erro nem pisca o
  // "Carregando…" — so atualiza a lista no lugar, preservando filtro,
  // busca e o card de relatorio aberto.
  const recarregar = React.useCallback((silencioso = false) => {
    if (!silencioso) setErro(null);
    fetch("/api/maquinas-mapa", { credentials: "same-origin" })
      .then(r => r.ok ? r.json() : r.json().then(j => Promise.reject(j.erro || `Erro ${r.status}`)))
      .then(arr => {
        const lista = Array.isArray(arr) ? arr : [];
        setMaquinas(lista);
        setErro(null);
        // Re-aponta a maquina selecionada pro objeto fresco (mesmo id) pra
        // o cabecalho do card de relatorio refletir status/ult. atualizacao
        // novos sem fechar e reabrir. Se sumiu da lista, mantem o anterior.
        setSelecionada(prev => (prev ? (lista.find(m => m.id === prev.id) || prev) : prev));
      })
      .catch(e => {
        // Poll silencioso: mantem a lista atual e ignora o erro transitorio.
        if (!silencioso) setErro(String(e || "Falha ao carregar máquinas"));
      });
  }, []);

  // Carga inicial + polling de 60s + refresh ao voltar pra aba (pausa
  // quando a aba nao esta visivel). Mesma cadencia do MapaPage e dos KPIs.
  useEffect(() => {
    recarregar(false);
    const id = setInterval(() => recarregar(true), 60 * 1000);
    const onVis = () => {
      if (document.visibilityState === "visible") recarregar(true);
    };
    document.addEventListener("visibilitychange", onVis);
    return () => {
      clearInterval(id);
      document.removeEventListener("visibilitychange", onVis);
    };
  }, [recarregar]);

  const contadores = React.useMemo(() => {
    const c = { todas: 0, ocioso: 0, perfurando: 0, concretando: 0, erro: 0, offline: 0 };
    (maquinas || []).forEach(m => {
      c.todas++;
      if (c[m.status] != null) c[m.status]++;
    });
    return c;
  }, [maquinas]);

  const filtradas = React.useMemo(() => {
    let lista = maquinas || [];
    if (filtroStatus !== "todas") lista = lista.filter(m => m.status === filtroStatus);
    const q = busca.trim().toLowerCase();
    if (q) {
      lista = lista.filter(m =>
        [m.tag, m.nome, m.num_serie, m.obra].some(v => String(v || "").toLowerCase().includes(q))
      );
    }
    return lista;
  }, [maquinas, filtroStatus, busca]);

  const STATUS_PILLS = [
    { id: "todas", label: "Todas" },
    { id: "ocioso", label: "Ocioso" },
    { id: "perfurando", label: "Perfurando" },
    { id: "concretando", label: "Concretando" },
    { id: "erro", label: "Erro" },
    { id: "offline", label: "Offline" },
  ];
  const online = contadores.ocioso + contadores.perfurando + contadores.concretando + contadores.erro;

  return (
    <div className="dash-page frota-page">
      <button type="button" className="frota-voltar-mob" onClick={onVoltar}>← Dashboard</button>
      <div className="dash-pagehead">
        <div>
          <div className="dash-eyebrow">MONITORAMENTO DE EQUIPAMENTOS</div>
          <h1 className="dash-h1">Máquinas</h1>
          <div className="dash-bread">
            {maquinas === null
              ? "Carregando…"
              : `${contadores.todas} equipamento${contadores.todas === 1 ? "" : "s"} · ${online} online`}
          </div>
        </div>
        <div className="dash-actions">
          <button className="dash-btn-ghost" onClick={onVoltar}>← Dashboard</button>
        </div>
      </div>

      <div className="frota-statusbar">
        {STATUS_PILLS.map(p => (
          <button
            key={p.id}
            type="button"
            className={`frota-pill ${filtroStatus === p.id ? "is-active" : ""}`}
            onClick={() => setFiltroStatus(p.id)}
          >
            <span
              className="frota-pill__dot"
              style={{ background: p.id === "todas" ? "var(--ink)" : corStatusMaquina(p.id) }}
            />
            <span className="frota-pill__txt">
              <span className="frota-pill__lbl">{p.label}</span>
              <span className="frota-pill__num">{contadores[p.id] ?? 0}</span>
            </span>
          </button>
        ))}
      </div>

      {/* Card de relatorio — aparece inline entre a status bar e a lista
          quando uma maquina e' selecionada; some ao fechar ou clicar de
          novo na mesma linha. Animacao de expansao em .frota-detalhe. */}
      {selecionada && (
        <div className="frota-detalhe">
          <FrotaRelatorio
            key={selecionada.id}
            maquina={selecionada}
            onFechar={() => setSelecionada(null)}
          />
        </div>
      )}

      <Card
        title="Frota"
        right={
          <div className="frota-card-tools">
            <span className="frota-live" title="Atualiza automaticamente a cada 60 segundos">
              ao vivo · 60s
            </span>
            <input
              className="frota-busca"
              placeholder="Buscar TAG, série, obra"
              value={busca}
              onChange={(e) => setBusca(e.target.value)}
            />
          </div>
        }
      >
        {erro && <div className="usuarios-erro">{erro}</div>}
        {!erro && maquinas === null && <div className="usuarios-empty">Carregando…</div>}
        {!erro && maquinas && filtradas.length === 0 && (
          <div className="usuarios-empty">
            {contadores.todas === 0 ? "Nenhuma máquina cadastrada." : "Nenhuma máquina para o filtro selecionado."}
          </div>
        )}
        {!erro && maquinas && filtradas.length > 0 && (
          <>
            <div className="frota-tabela-wrap">
              <table className="usuarios-tabela frota-tabela">
                <thead>
                  <tr>
                    <th>Status</th>
                    <th>TAG</th>
                    <th>Série do computador</th>
                    <th>Obra atual</th>
                    <th>Última atualização</th>
                  </tr>
                </thead>
                <tbody>
                  {filtradas.map(m => (
                    <tr
                      key={m.id}
                      className={`frota-row ${selecionada && selecionada.id === m.id ? "frota-row--sel" : ""}`}
                      onClick={() => setSelecionada(prev => (prev && prev.id === m.id ? null : m))}
                    >
                      <td><span className={classeBadgeStatus(m.status)}>{rotuloStatus(m.status)}</span></td>
                      <td className="frota-tag">{m.tag || m.nome || "—"}</td>
                      <td className="usuarios-email">{m.num_serie || "—"}</td>
                      <td>{m.obra || "—"}</td>
                      <td className="frota-update">
                        <strong>{tempoRelativo(m.gps_momento)}</strong>
                        {dataHoraCurta(m.gps_momento) && <span> · {dataHoraCurta(m.gps_momento)}</span>}
                      </td>
                    </tr>
                  ))}
                </tbody>
              </table>
            </div>
            <div className="frota-cards">
              {filtradas.map(m => (
                <button
                  key={m.id}
                  type="button"
                  className={`frota-card ${selecionada && selecionada.id === m.id ? "frota-card--sel" : ""}`}
                  onClick={() => setSelecionada(prev => (prev && prev.id === m.id ? null : m))}
                >
                  <div className="frota-card__top">
                    <span className="frota-card__tag">{m.tag || m.nome || "—"}</span>
                    <span className={classeBadgeStatus(m.status)}>{rotuloStatus(m.status)}</span>
                  </div>
                  <div className="frota-card__rows">
                    <div className="frota-card__row"><span className="lbl">Série</span><span className="val">{m.num_serie || "—"}</span></div>
                    <div className="frota-card__row"><span className="lbl">Obra</span><span className="val">{m.obra || "—"}</span></div>
                  </div>
                  <div className="frota-card__update">
                    <span className="when">{tempoRelativo(m.gps_momento)}</span>
                    <span className="ts">{dataHoraCurta(m.gps_momento) || "—"}</span>
                  </div>
                </button>
              ))}
            </div>
          </>
        )}
      </Card>
    </div>
  );
}

function MapaPage({ accent }) {
  const [maquinas, setMaquinas] = useState([]);
  const [carregando, setCarregando] = useState(true);
  const [erro, setErro] = useState(null);
  const [filtroTabela, setFiltroTabela] = useState("");
  const [filtroStatus, setFiltroStatus] = useState("all");
  const [focar, setFocar] = useState(null);
  const [editando, setEditando] = useState(null); // maquina sendo editada (objeto)
  const [salvando, setSalvando] = useState(false);
  const mapSecRef = React.useRef(null);
  const tableSecRef = React.useRef(null);
  const mapApiRef = React.useRef(null);
  // Geocodificacao reversa das maquinas com GPS automatico — ver efeito
  // abaixo. `geoCache` e' chaveado por coordenada arredondada (~100m).
  const [geoCache, setGeoCache] = useState({});
  const geoPendRef = React.useRef(new Set());

  // `silencioso` evita o flicker do "Carregando…" no polling de 60s; so o
  // primeiro mount e cliques explicitos passam false.
  const recarregar = React.useCallback((silencioso = false) => {
    if (!silencioso) {
      setCarregando(true);
      setErro(null);
    }
    fetch('/api/maquinas-mapa', { credentials: 'same-origin' })
      .then(r => r.ok ? r.json() : Promise.reject(r.status))
      .then(data => {
        setMaquinas(Array.isArray(data) ? data : []);
        if (!silencioso) setCarregando(false);
      })
      .catch(err => {
        if (!silencioso) {
          setErro(`Falha ao carregar (${err}).`);
          setCarregando(false);
        }
      });
  }, []);
  // Polling de 60s + refresh ao voltar pra aba — mesma cadencia do dashboard.
  // Garante que status (online/offline/perfurando/concretando) e "Ult.
  // Atualizacao" reflitam pings de GPS chegando sem o usuario sair da pagina.
  React.useEffect(() => {
    recarregar();
    const id = setInterval(() => recarregar(true), 60 * 1000);
    const onVis = () => {
      if (document.visibilityState === 'visible') recarregar(true);
    };
    document.addEventListener('visibilitychange', onVis);
    return () => {
      clearInterval(id);
      document.removeEventListener('visibilitychange', onVis);
    };
  }, [recarregar]);

  // Geocodificacao reversa das maquinas com GPS automatico: converte a
  // lat/lon do ultimo ping no endereco de rua exibido na tabela. Cache por
  // coordenada arredondada (~100m) — maquina parada nao re-geocodifica a
  // cada poll de 60s, e maquinas no mesmo canteiro compartilham a chamada.
  React.useEffect(() => {
    maquinas.forEach(m => {
      if (m.fonte_gps !== 'auto') return;
      const lat = Number(m.lat), lon = Number(m.lon);
      if (!Number.isFinite(lat) || !Number.isFinite(lon)) return;
      const chave = `${lat.toFixed(3)},${lon.toFixed(3)}`;
      if (geoCache[chave] || geoPendRef.current.has(chave)) return;
      geoPendRef.current.add(chave);
      reverseGeocode(lat, lon)
        .then(({ raw }) => {
          const e = parseEnderecoGoogle(raw);
          setGeoCache(c => ({ ...c, [chave]: { texto: e.rua, cidade: e.cidade } }));
        })
        .catch(() => setGeoCache(c => ({ ...c, [chave]: { erro: true } })))
        .finally(() => geoPendRef.current.delete(chave));
    });
  }, [maquinas, geoCache]);

  // Endereco geocodificado de uma maquina (ou null = ainda carregando).
  const geoDeMaquina = (m) => {
    const lat = Number(m.lat), lon = Number(m.lon);
    if (!Number.isFinite(lat) || !Number.isFinite(lon)) return null;
    return geoCache[`${lat.toFixed(3)},${lon.toFixed(3)}`] || null;
  };

  // Mapeia maquinas pra o shape que o GoogleMap espera (lng vs lon, etc.)
  // Filtra coords invalidas/null/zero — sem isso, maquinas com lat/lon
  // nulos vinham com Number(null)=0 e o pin caia em (0,0), no meio do
  // Atlantico (Null Island).
  const obrasMapa = React.useMemo(() => maquinas
    .filter(m => {
      if (m.lat == null || m.lon == null) return false;
      const lat = Number(m.lat), lon = Number(m.lon);
      if (!Number.isFinite(lat) || !Number.isFinite(lon)) return false;
      // Coordenadas exatamente em (0,0) sao quase certamente lixo (GPS
      // sem fix). Range global plausivel pra rejeitar swap lat<->lon.
      if (lat === 0 && lon === 0) return false;
      if (lat < -90 || lat > 90 || lon < -180 || lon > 180) return false;
      return true;
    })
    .map(m => ({
      id: m.id,
      nome: m.tag || m.nome || m.num_serie,
      bairro: m.cidade || '',
      lat: Number(m.lat),
      lng: Number(m.lon),
      status: m.status,
      estacas: 0,
    })), [maquinas]);

  const linhasTabela = maquinas
    .filter(m => {
      if (filtroStatus !== 'all' && m.status !== filtroStatus) return false;
      if (!filtroTabela) return true;
      const q = filtroTabela.toLowerCase();
      return ((m.tag || '') + ' ' + (m.num_serie || '') + ' ' + (m.obra || '')
              + ' ' + (m.contrato || '') + ' ' + (m.endereco || '') + ' ' + (m.cidade || ''))
             .toLowerCase().includes(q);
    })
    // Ordena por TAG em ordem natural (M1, M2, M10 — nao M1, M10, M2).
    // Maquinas sem tag vao pro fim.
    .sort((a, b) => {
      const ta = (a.tag || '').trim();
      const tb = (b.tag || '').trim();
      if (!ta && !tb) return 0;
      if (!ta) return 1;
      if (!tb) return -1;
      return ta.localeCompare(tb, 'pt-BR', { numeric: true, sensitivity: 'base' });
    });

  const irParaTabela = () => tableSecRef.current?.scrollIntoView({ behavior: 'smooth' });
  const voltarMapa = () => mapSecRef.current?.scrollIntoView({ behavior: 'smooth' });

  // Reposiciona o mapa pra mostrar todas as maquinas com coordenada valida.
  // Sem maquinas → ignorado. 1 maquina → centro fixo. 2+ → fitBounds com padding.
  const mostrarTodas = () => {
    const map = mapApiRef.current;
    const g = window.google;
    if (!map || !g || obrasMapa.length === 0) return;
    if (obrasMapa.length === 1) {
      const p = obrasMapa[0];
      map.setCenter({ lat: p.lat, lng: p.lng });
      map.setZoom(14);
      return;
    }
    const bounds = new g.maps.LatLngBounds();
    obrasMapa.forEach(p => bounds.extend({ lat: p.lat, lng: p.lng }));
    map.fitBounds(bounds, 80);
  };

  const salvarEdicao = async (payload) => {
    if (!editando) return;
    setSalvando(true);
    try {
      const r = await fetch(`/api/maquinas-mapa/${editando.id}`, {
        method: 'PUT',
        credentials: 'same-origin',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(payload),
      });
      if (!r.ok) {
        const j = await r.json().catch(() => ({}));
        alert(j.erro || `Falha ao salvar (${r.status}).`);
        return;
      }
      setEditando(null);
      recarregar();
    } catch (err) {
      alert('Erro de rede.');
    } finally {
      setSalvando(false);
    }
  };

  return (
    <div className="dash-page map-page">
      <section className="map-section" ref={mapSecRef}>
        <div className="page-head">
          <div>
            <div className="eyebrow">FROTA · CANTEIROS ATIVOS</div>
            <h1 className="h1">Mapa</h1>
          </div>
        </div>

        <div className="map-card">
          <GoogleMap
            accentHex={accent.hex}
            obras={obrasMapa}
            focar={focar}
            mapApiRef={mapApiRef}
            className="map-leaflet"
          />
          <button
            type="button"
            className="map-fit-btn"
            onClick={mostrarTodas}
            disabled={obrasMapa.length === 0}
            title={obrasMapa.length === 0 ? 'Sem máquinas com localização' : `Mostrar todas as máquinas (${obrasMapa.length})`}
          >
            <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
              <path d="M3 9V3h6"/>
              <path d="M21 9V3h-6"/>
              <path d="M3 15v6h6"/>
              <path d="M21 15v6h-6"/>
            </svg>
            Ver todas
          </button>
        </div>

        <button className="scroll-hint" onClick={irParaTabela}>
          Ver tabela de máquinas
          <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2"><path d="M6 9l6 6 6-6"/></svg>
        </button>
      </section>

      <section className="table-section" ref={tableSecRef}>
        <div className="table-section-head">
          <div>
            <h2>Máquinas</h2>
            <p>{maquinas.length} cadastrada(s) · clique numa linha pra focar no mapa · botão "Editar" pra atualizar endereço/coordenadas.</p>
          </div>
          <div className="actions">
            <button className="btn-ghost" onClick={voltarMapa}>
              <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="1.8"><path d="M6 15l6-6 6 6"/></svg>
              Voltar ao mapa
            </button>
          </div>
        </div>

        <div className="table-card">
          <div className="tc-filter">
            <div className="tc-filter-search">
              <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="1.6"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.3-4.3"/></svg>
              <input placeholder="Filtrar por TAG, NS, obra, contrato, endereço…" value={filtroTabela} onChange={(e) => setFiltroTabela(e.target.value)}/>
            </div>
            <select className="d-select" value={filtroStatus} onChange={(e) => setFiltroStatus(e.target.value)} style={{ minWidth: 140 }}>
              <option value="all">Todos status</option>
              <option value="ocioso">Ocioso</option>
              <option value="perfurando">Perfurando</option>
              <option value="concretando">Concretando</option>
              <option value="erro">Erro</option>
              <option value="offline">Offline</option>
            </select>
            <span className="tc-filter-count">{linhasTabela.length} itens</span>
          </div>

          <div className="tc-grid-header" style={{ gridTemplateColumns: "120px 100px 110px 1fr 1fr 1.4fr 1fr 130px 160px" }}>
            <span>Status</span>
            <span>TAG</span>
            <span>Nº Série</span>
            <span>Obra</span>
            <span>Contrato</span>
            <span>Endereço</span>
            <span>Cidade</span>
            <span>Ult. Atualização</span>
            <span style={{ textAlign: 'right' }}>Ações</span>
          </div>

          <div className="tc-list">
            {carregando && <div className="dash-nav-sub-empty" style={{ padding: 16 }}>Carregando…</div>}
            {erro && !carregando && <div className="dash-nav-sub-empty" style={{ padding: 16, color: '#D14343' }}>{erro}</div>}
            {!carregando && !erro && linhasTabela.length === 0 && (
              <div className="dash-nav-sub-empty" style={{ padding: 16 }}>Nenhuma máquina encontrada.</div>
            )}
            {!carregando && !erro && linhasTabela.map(m => {
              const cor = corStatusMaquina(m.status);
              const temGps = m.lat != null && m.lon != null;
              // GPS automatico (ping do equipamento): o endereco vem da
              // geocodificacao reversa das coordenadas e o botao "Editar"
              // some — editar na mao so faz sentido quando NAO ha GPS.
              const gpsAuto = m.fonte_gps === 'auto';
              const geo = gpsAuto ? geoDeMaquina(m) : null;
              const formatarDt = (ts) => {
                if (!ts) return '—';
                const d = new Date(ts);
                if (!Number.isFinite(d.getTime())) return '—';
                const p = (n) => String(n).padStart(2, '0');
                return `${p(d.getDate())}/${p(d.getMonth() + 1)}/${d.getFullYear()} ${p(d.getHours())}:${p(d.getMinutes())}`;
              };
              return (
                <div
                  key={m.id}
                  className="tc-row"
                  style={{ gridTemplateColumns: "120px 100px 110px 1fr 1fr 1.4fr 1fr 130px 160px" }}
                  onClick={() => { if (temGps) { setFocar({ lat: Number(m.lat), lng: Number(m.lon), _ts: Date.now() }); voltarMapa(); } }}
                >
                  <span className="tc-id">
                    <i className="tc-dot" style={{ background: cor }}/>
                    {rotuloStatus(m.status)}
                  </span>
                  <span className="tc-est">{m.tag || '—'}</span>
                  <span className="tc-est">{m.num_serie || '—'}</span>
                  <span className="tc-obra">{m.obra || '—'}</span>
                  <span className="tc-end">{m.contrato || '—'}</span>
                  <span className="tc-end" title={gpsAuto ? 'Endereço obtido pelo GPS do equipamento' : (m.fonte_gps === 'manual' ? 'Coordenada inserida manualmente' : '')}>
                    {!temGps && <span style={{ color: '#E0A02A', marginRight: 6 }} title="Sem localização — clique em Editar">⚠</span>}
                    {gpsAuto
                      ? (geo && geo.texto ? geo.texto
                         : geo && geo.erro ? `${Number(m.lat).toFixed(5)}, ${Number(m.lon).toFixed(5)}`
                         : 'Localizando…')
                      : (m.endereco || '—')}
                  </span>
                  <span className="tc-cidade">
                    {gpsAuto ? ((geo && geo.cidade) || '—') : (m.cidade || '—')}
                  </span>
                  <span className="tc-est">{formatarDt(m.gps_momento)}</span>
                  <span className="tc-acoes">
                    {!gpsAuto && (
                      <button className="tc-act-btn" onClick={(e) => { e.stopPropagation(); setEditando(m); }} title="Editar endereço">
                        Editar
                      </button>
                    )}
                    <button
                      className="tc-act-btn"
                      disabled={!temGps}
                      onClick={(e) => {
                        e.stopPropagation();
                        if (!temGps) return;
                        setFocar({ lat: Number(m.lat), lng: Number(m.lon), _ts: Date.now() });
                        voltarMapa();
                      }}
                      title={temGps ? 'Centralizar no mapa' : 'Sem localização'}
                    >
                      Focar
                    </button>
                  </span>
                </div>
              );
            })}
          </div>
        </div>
      </section>

      {editando && (
        <EditarMaquinaModal
          maquina={editando}
          accent={accent}
          salvando={salvando}
          onClose={() => setEditando(null)}
          onSalvar={salvarEdicao}
        />
      )}
    </div>
  );
}

/* ---------- Modal de edicao do endereco/coords da maquina ----------
 * Lat/lon nao aparecem na UI — sao derivados automaticamente do CEP, do
 * autocomplete ou geocodados na hora de salvar. O backend ainda guarda
 * lat_manual/lon_manual pra o pin do mapa funcionar. */
function EditarMaquinaModal({ maquina, accent, salvando, onClose, onSalvar }) {
  const [cep, setCep] = useState('');
  const [endereco, setEndereco] = useState(maquina.endereco || '');
  const [cidade, setCidade] = useState(maquina.cidade || '');
  const [latNum, setLatNum] = useState(maquina.fonte_gps === 'manual' && maquina.lat != null ? Number(maquina.lat) : null);
  const [lonNum, setLonNum] = useState(maquina.fonte_gps === 'manual' && maquina.lon != null ? Number(maquina.lon) : null);
  const [sugestoes, setSugestoes] = useState([]);
  const [buscandoCep, setBuscandoCep] = useState(false);
  const [erro, setErro] = useState(null);

  // Limpa coords cacheadas ao editar o endereco — forca novo geocode no
  // save (a menos que o usuario escolha do autocomplete/CEP novamente).
  const editarEndereco = (v) => { setEndereco(v); setLatNum(null); setLonNum(null); };

  // Busca CEP via Google (mesma logica que ja existia na tabela do mapa).
  const buscarCep = async (raw) => {
    const limpo = String(raw || '').replace(/\D/g, '');
    if (limpo.length !== 8) return;
    setBuscandoCep(true);
    setErro(null);
    try {
      const cepFmt = `${limpo.slice(0, 5)}-${limpo.slice(5)}`;
      const det = await geocodeEndereco(`${cepFmt}, Brasil`);
      const parsed = parseEnderecoGoogle(det.raw);
      setEndereco(parsed.rua || endereco);
      setCidade(parsed.cidade || cidade);
      setLatNum(det.lat);
      setLonNum(det.lng);
    } catch (e) {
      setErro('CEP não encontrado.');
    } finally {
      setBuscandoCep(false);
    }
  };

  // Autocomplete debounced. Tenta Places API (New); se cair em 403 (quota
  // ou billing nao configurado), `autocompleteEnderecos` retorna [] e a
  // UI segue sem dropdown — usuario usa CEP ou digita e o save geocoda.
  React.useEffect(() => {
    if (!endereco || endereco.length < 3 || window.__autocompleteIndisponivel) {
      setSugestoes([]); return;
    }
    const t = setTimeout(async () => {
      try {
        const q = `${endereco}${cidade ? ', ' + cidade : ''}`;
        const preds = await autocompleteEnderecos(q);
        setSugestoes(preds.map(p => ({
          label: p.description,
          rua: p.structured_formatting?.main_text || p.description.split(',')[0],
          cidade: (p.structured_formatting?.secondary_text || '').split(',')[0] || '',
          placeId: p.place_id,
        })));
      } catch { setSugestoes([]); }
    }, 300);
    return () => clearTimeout(t);
  }, [endereco, cidade]);

  const aplicarSugestao = async (s) => {
    setSugestoes([]);
    if (!s.placeId) {
      setEndereco(s.rua); setCidade(s.cidade || cidade);
      return;
    }
    try {
      const det = await geocodePorPlaceId(s.placeId);
      const parsed = parseEnderecoGoogle(det.raw);
      setEndereco(parsed.rua || s.rua);
      setCidade(parsed.cidade || s.cidade || cidade);
      setLatNum(det.lat);
      setLonNum(det.lng);
    } catch (e) { console.warn('Falha ao detalhar sugestao:', e); }
  };

  const submit = async (e) => {
    e?.preventDefault?.();
    setErro(null);
    let lat = latNum, lon = lonNum;
    // Se ainda nao tem coords (usuario digitou direto sem usar
    // CEP/autocomplete), tenta geocodar no momento do save.
    if ((lat == null || lon == null) && endereco.trim()) {
      try {
        const det = await geocodeEndereco(`${endereco}${cidade ? ', ' + cidade : ''}, Brasil`);
        lat = det.lat; lon = det.lng;
      } catch (_) { /* segue salvando sem coords — pin nao aparece, mas dados ficam */ }
    }
    onSalvar({
      endereco: endereco.trim() || null,
      cidade:   cidade.trim() || null,
      lat_manual: lat,
      lon_manual: lon,
    });
  };

  return (
    <div className="modal-backdrop is-open" onClick={(e) => { if (e.currentTarget === e.target && !salvando) onClose(); }} style={{ zIndex: 1300 }}>
      <div className="modal" style={{ maxWidth: 520 }}>
        <div className="modal-head">
          <h3>Editar localização — {maquina.tag || maquina.nome || maquina.num_serie}</h3>
          <p>
            {maquina.fonte_gps === 'auto'
              ? 'Esta máquina já recebe GPS automaticamente; endereço/cidade aqui são referência.'
              : 'Informe o CEP (preenche automaticamente) ou digite o endereço com auto-completar.'}
          </p>
        </div>
        <form onSubmit={submit}>
          <div className="modal-body" style={{ gridTemplateColumns: "1fr" }}>
            <div className="field full">
              <label className="field-label">CEP</label>
              <div style={{ display: 'flex', gap: 8 }}>
                <input
                  className="field-input"
                  value={cep}
                  onChange={(e) => setCep(e.target.value)}
                  onBlur={(e) => buscarCep(e.target.value)}
                  onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); buscarCep(cep); } }}
                  placeholder="01310-100"
                  maxLength={9}
                  inputMode="numeric"
                  style={{ flex: '0 0 160px' }}
                />
                <button type="button" className="btn-ghost" onClick={() => buscarCep(cep)} disabled={buscandoCep}>
                  {buscandoCep ? 'Buscando…' : 'Buscar CEP'}
                </button>
              </div>
            </div>
            <div className="field full" style={{ position: 'relative' }}>
              <label className="field-label">Endereço</label>
              <input
                className="field-input"
                value={endereco}
                onChange={(e) => editarEndereco(e.target.value)}
                placeholder="Rua, número, bairro"
                autoComplete="off"
              />
              {sugestoes.length > 0 && (
                <ul className="tc-suggest" style={{ top: '100%', left: 0, right: 0, zIndex: 1400 }}>
                  {sugestoes.map((s, i) => (
                    <li key={i} onMouseDown={(e) => { e.preventDefault(); aplicarSugestao(s); }}>
                      <span className="tc-suggest-rua">{s.rua}</span>
                      <span className="tc-suggest-cidade">{s.label}</span>
                    </li>
                  ))}
                </ul>
              )}
            </div>
            <div className="field full">
              <label className="field-label">Cidade</label>
              <input className="field-input" value={cidade} onChange={(e) => setCidade(e.target.value)} placeholder="ex: São Paulo"/>
            </div>
            {erro && (
              <div className="field full" style={{ color: '#B33A1A', fontSize: 12 }}>{erro}</div>
            )}
          </div>
          <div className="modal-foot">
            <button type="button" className="btn-ghost" onClick={onClose} disabled={salvando}>Cancelar</button>
            <button type="submit" className="btn-primary" style={{ background: accent.hex }} disabled={salvando}>
              {salvando ? 'Salvando…' : 'Salvar'}
            </button>
          </div>
        </form>
      </div>
    </div>
  );
}

/* ---------- Painel de monitoramento remoto ---------- */
const MAQ_INICIAIS = [
  { id: "MAQ-001", name: "SIMEhC-01", type: "Hélice contínua", project: "Linha 6 · Brasilândia", contract: "CC-2154", diam: 600, stake: "E-1149", status: "ok",
    pile: { volume: 9.2, pressure: 4.6, dur: "07:32", end: "—", start: "07:11", depth: 18.4 },
    g: { pressao: 4.6, consumo: -22, vel: 215, perfil: 600 } },
  { id: "MAQ-002", name: "SIMEhC-02", type: "Hélice contínua", project: "Linha 6 · Brasilândia", contract: "CC-2154", diam: 800, stake: "E-1150", status: "ok",
    pile: { volume: 4.6, pressure: 5.1, dur: "04:12", end: "—", start: "07:24", depth: 12.1 },
    g: { pressao: 5.1, consumo: -38, vel: 312, perfil: 800 } },
  { id: "MAQ-003", name: "SIMEhC-03", type: "Estaca raiz", project: "Pátio Anhanguera", contract: "CC-2160", diam: 400, stake: "R-077", status: "warn",
    pile: { volume: 2.1, pressure: 6.8, dur: "12:48", end: "—", start: "06:40", depth: 9.5 },
    g: { pressao: 6.8, consumo: -64, vel: 95, perfil: 400 } },
  { id: "MAQ-004", name: "BG-25 #4", type: "Hélice contínua", project: "Linha 6 · Brasilândia", contract: "CC-2154", diam: 1000, stake: "E-1148", status: "warn",
    pile: { volume: 10.8, pressure: 8.4, dur: "00:45", end: "07:55", start: "07:10", depth: 20.0 },
    g: { pressao: 8.4, consumo: -88, vel: 410, perfil: 1000 } },
  { id: "MAQ-005", name: "SIMEhC-05", type: "Diafragma", project: "Túnel TBM-3", contract: "CC-2178", diam: 800, stake: "D-012", status: "ok",
    pile: { volume: 14.2, pressure: 5.5, dur: "09:00", end: "—", start: "06:30", depth: 22.0 },
    g: { pressao: 5.5, consumo: -45, vel: 180, perfil: 800 } },
  { id: "MAQ-006", name: "SIMEhC-06", type: "Strauss", project: "Bloco B7 · CD", contract: "CC-2185", diam: 350, stake: "S-204", status: "ok",
    pile: { volume: 1.8, pressure: 3.2, dur: "02:14", end: "—", start: "08:18", depth: 7.5 },
    g: { pressao: 3.2, consumo: -28, vel: 140, perfil: 350 } },
  { id: "MAQ-007", name: "SIMEhC-07", type: "Hélice contínua", project: "Reservatório R-8", contract: "CC-2191", diam: 600, stake: "E-301", status: "idle",
    pile: { volume: 0, pressure: 0, dur: "00:00", end: "—", start: "—", depth: 0 },
    g: { pressao: 0, consumo: 0, vel: 0, perfil: 0 } },
  { id: "MAQ-008", name: "BG-30 #8", type: "Hélice contínua", project: "Ponte Estaiada", contract: "CC-2199", diam: 1200, stake: "E-088", status: "warn",
    pile: { volume: 18.4, pressure: 5.9, dur: "06:10", end: "—", start: "08:00", depth: 28.0 },
    g: { pressao: 5.9, consumo: -52, vel: 240, perfil: 1200 } },
  { id: "MAQ-009", name: "SIMEhC-09", type: "Estaca raiz", project: "Estação Pinheiros", contract: "CC-2204", diam: 400, stake: "R-022", status: "ok",
    pile: { volume: 3.4, pressure: 4.0, dur: "05:32", end: "—", start: "07:40", depth: 11.0 },
    g: { pressao: 4.0, consumo: -34, vel: 198, perfil: 400 } },
  { id: "MAQ-010", name: "SIMEhC-10", type: "Hélice contínua", project: "Edifício JK 2041", contract: "CC-2211", diam: 800, stake: "E-014", status: "idle",
    pile: { volume: 0, pressure: 0, dur: "00:00", end: "—", start: "—", depth: 0 },
    g: { pressao: 0, consumo: 0, vel: 0, perfil: 0 } },
];

const MON_PER_PAGE = 6;
const monClamp = (v, mn, mx) => Math.max(mn, Math.min(mx, v));

function RadialGauge({ value, min, max, accent }) {
  const v = monClamp(value, min, max);
  const pct = (v - min) / (max - min || 1);
  const cx = 90, cy = 90, r = 64;
  const polar = (deg, rad) => {
    const a = (deg - 90) * Math.PI / 180;
    return [cx + rad * Math.cos(a), cy + rad * Math.sin(a)];
  };
  const arcLen = Math.PI * r;
  const dashOffset = arcLen * (1 - pct);
  const [sx, sy] = polar(180, r);
  const [ex, ey] = polar(360, r);
  const tipAngle = (180 + pct * 180 - 90) * Math.PI / 180;
  const gid = "gh-" + accent.replace("#", "");
  return (
    <svg viewBox="0 0 180 110" className="gh-svg" preserveAspectRatio="xMidYMid meet">
      <defs>
        <linearGradient id={gid} x1="0" x2="1" y1="0" y2="0">
          <stop offset="0" stopColor={accent} stopOpacity=".55"/>
          <stop offset="1" stopColor={accent}/>
        </linearGradient>
      </defs>
      <path d={`M ${sx} ${sy} A ${r} ${r} 0 0 1 ${ex} ${ey}`} fill="none" stroke="rgba(255,255,255,.08)" strokeWidth="10" strokeLinecap="round"/>
      <path d={`M ${sx} ${sy} A ${r} ${r} 0 0 1 ${ex} ${ey}`} fill="none"
            stroke={`url(#${gid})`} strokeWidth="10" strokeLinecap="round"
            strokeDasharray={arcLen} strokeDashoffset={dashOffset}
            style={{ transition: "stroke-dashoffset .5s ease" }}/>
      <circle cx={cx + r * Math.cos(tipAngle)} cy={cy + r * Math.sin(tipAngle)} r="5" fill="#fff" stroke={accent} strokeWidth="2"/>
    </svg>
  );
}

function Sparkline({ data, accent, min, max }) {
  if (!data || data.length < 2) return <svg viewBox="0 0 100 30" className="gs-svg"/>;
  const W = 100, H = 30, pad = 2;
  const range = (max - min) || 1;
  const pts = data.map((v, i) => {
    const x = (i / (data.length - 1)) * W;
    const y = pad + (1 - (v - min) / range) * (H - pad * 2);
    return [x, y];
  });
  const line = pts.map((p, i) => (i === 0 ? "M" : "L") + p[0].toFixed(1) + " " + p[1].toFixed(1)).join(" ");
  const area = line + ` L${W} ${H} L0 ${H} Z`;
  const last = pts[pts.length - 1];
  const gid = "gs-" + accent.replace("#", "") + "-" + data.length;
  return (
    <svg viewBox="0 0 100 30" className="gs-svg" preserveAspectRatio="none">
      <defs>
        <linearGradient id={gid} x1="0" x2="0" y1="0" y2="1">
          <stop offset="0" stopColor={accent} stopOpacity=".28"/>
          <stop offset="1" stopColor={accent} stopOpacity="0"/>
        </linearGradient>
      </defs>
      <path d={area} fill={`url(#${gid})`}/>
      <path d={line} fill="none" stroke={accent} strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round"/>
      <circle cx={last[0]} cy={last[1]} r="1.8" fill={accent}/>
    </svg>
  );
}

function MachineCard({ m, history, accentHex }) {
  const stClass = m.status === "ok" ? "ok" : m.status === "warn" ? "warn" : "idle";
  const stateLabel = m.status === "idle" ? "OCIOSO" : m.status === "warn" ? "CONCRETANDO" : "PERFURANDO";
  return (
    <div className="machine">
      <div className={`m-head ${stClass}`}>
        <span className={`m-head-dot ${m.status === "idle" ? "idle" : ""}`}/>
        <span className="m-head-name">{m.name} · {stateLabel}</span>
        <span className="m-head-state">{m.id}</span>
      </div>
      <div className="m-info">
        <div className="m-info-cell"><span className="m-info-k">Data SIMEhC</span><span className="m-info-v">24/04/2026</span></div>
        <div className="m-info-cell"><span className="m-info-k">Tempo Exec.</span><span className="m-info-v">{m.pile.dur}</span></div>
        <div className="m-info-cell"><span className="m-info-k">Prof. atual</span><span className="m-info-v">{m.pile.depth.toFixed(2)} m</span></div>
      </div>
      <div className="m-meta">
        <div><span className="m-meta-k">Obra:</span><span className="m-meta-v">{m.project}</span></div>
        <div><span className="m-meta-k">Estaca:</span><span className="m-meta-v">{m.stake}</span></div>
        <div><span className="m-meta-k">Contrato:</span><span className="m-meta-v">{m.contract}</span></div>
        <div><span className="m-meta-k">Trado:</span><span className="m-meta-v">{m.diam} mm</span></div>
      </div>
      <div className="m-body">
        <div className="m-list">
          <div className="m-list-tab">CONCRETAGEM</div>
          <div className="m-list-row"><span className="m-list-k">Volume</span><span className={`m-list-v ${m.pile.volume ? "" : "dim"}`}>{m.pile.volume ? m.pile.volume.toFixed(1) + " m³" : "—"}</span></div>
          <div className="m-list-row"><span className="m-list-k">Pressão</span><span className={`m-list-v ${m.pile.pressure ? "" : "dim"}`}>{m.pile.pressure ? m.pile.pressure.toFixed(1) + " bar" : "—"}</span></div>
          <div className="m-list-row"><span className="m-list-k">Duração</span><span className="m-list-v">{m.pile.dur}</span></div>
          <div className="m-list-row"><span className="m-list-k">Início</span><span className={`m-list-v ${m.pile.start === "—" ? "dim" : ""}`}>{m.pile.start}</span></div>
          <div className="m-list-row"><span className="m-list-k">Fim</span><span className={`m-list-v ${m.pile.end === "—" ? "dim" : ""}`}>{m.pile.end}</span></div>
        </div>
        <div className="m-gauges">
          <div className="gauge-hero">
            <div className="gh-title">PRESSÃO CONCRETO</div>
            <RadialGauge value={m.g.pressao} min={0} max={10} accent={accentHex}/>
            <div className="gh-readout">
              <div className="gh-value">{m.g.pressao.toFixed(1)}</div>
              <div className="gh-unit">bar</div>
            </div>
            <div className="gh-range"><span>0</span><span>10 bar</span></div>
          </div>
          <div className="gauge-spark">
            <div className="gs-head">
              <span className="gs-title">CONSUMO</span>
              <span className="gs-value">{m.g.consumo.toFixed(0)}<small>%</small></span>
            </div>
            <Sparkline data={history.consumo} accent="#3B6EE0" min={-100} max={100}/>
          </div>
          <div className="gauge-spark">
            <div className="gs-head">
              <span className="gs-title">VEL. SUBIDA</span>
              <span className="gs-value">{m.g.vel.toFixed(0)}<small>m/h</small></span>
            </div>
            <Sparkline data={history.vel} accent="#0F8A8A" min={0} max={500}/>
          </div>
        </div>
      </div>
    </div>
  );
}

function MonitoramentoPage({ accent }) {
  const [machines, setMachines] = useState(MAQ_INICIAIS);
  const [historyMap, setHistoryMap] = useState({});
  const [page, setPage] = useState(0);
  const [modalOpen, setModalOpen] = useState(false);
  const [form, setForm] = useState({ name: "", tag: "", type: "Hélice contínua", project: "", contract: "", diam: "" });
  const [now, setNow] = useState(new Date());
  // Modo "tela cheia" (CSS fullscreen, nao API): some com header/strip/pager
  // e cards crescem pra ocupar a viewport inteira. ESC sai.
  const [expandido, setExpandido] = useState(false);
  const viewportRef = React.useRef(null);

  React.useEffect(() => {
    if (!expandido) return;
    const onKey = (e) => { if (e.key === "Escape") setExpandido(false); };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [expandido]);

  const totalPages = Math.max(1, Math.ceil((machines.length + 1) / MON_PER_PAGE));

  useEffect(() => {
    const id = setInterval(() => {
      setMachines(prev => prev.map(m => {
        if (m.status === "idle") return m;
        return {
          ...m,
          g: {
            ...m.g,
            pressao: monClamp(m.g.pressao + (Math.random() - .5) * 0.6, 0.5, 9.5),
            consumo: monClamp(m.g.consumo + (Math.random() - .5) * 8, -95, 95),
            vel: monClamp(m.g.vel + (Math.random() - .5) * 30, 50, 480),
          },
          pile: { ...m.pile, pressure: m.g.pressao },
        };
      }));
      setNow(new Date());
    }, 2000);
    return () => clearInterval(id);
  }, []);

  useEffect(() => {
    setHistoryMap(prev => {
      const next = { ...prev };
      machines.forEach(m => {
        const h = next[m.id] || { pressao: [], consumo: [], vel: [], perfil: [] };
        const novo = { pressao: [...h.pressao, m.g.pressao], consumo: [...h.consumo, m.g.consumo], vel: [...h.vel, m.g.vel], perfil: [...h.perfil, m.g.perfil] };
        ["pressao", "consumo", "vel", "perfil"].forEach(k => { if (novo[k].length > 32) novo[k].shift(); });
        next[m.id] = novo;
      });
      return next;
    });
  }, [machines]);

  useEffect(() => {
    const v = viewportRef.current;
    if (!v) return;
    v.scrollTo({ left: page * v.clientWidth, behavior: "smooth" });
  }, [page, totalPages]);

  const counts = machines.reduce((acc, m) => { acc[m.status] = (acc[m.status] || 0) + 1; return acc; }, { ok: 0, warn: 0, idle: 0 });

  const onScroll = () => {
    const v = viewportRef.current;
    if (!v) return;
    const newPage = Math.round(v.scrollLeft / v.clientWidth);
    if (newPage !== page) setPage(newPage);
  };

  const abrirModal = () => setModalOpen(true);
  const fecharModal = () => { setModalOpen(false); setForm({ name: "", tag: "", type: "Hélice contínua", project: "", contract: "", diam: "" }); };
  const salvarMaquina = () => {
    const tag = form.tag.trim() || ("MAQ-" + String(machines.length + 1).padStart(3, "0"));
    const nova = {
      id: tag,
      name: form.name.trim() || "Nova Máquina",
      type: form.type,
      project: form.project.trim() || "—",
      contract: form.contract.trim() || "—",
      diam: Number(form.diam) || 600,
      stake: "—",
      status: "idle",
      pile: { volume: 0, pressure: 0, dur: "00:00", end: "—", start: "—", depth: 0 },
      g: { pressao: 0, consumo: 0, vel: 0, perfil: 0 },
    };
    setMachines(prev => [...prev, nova]);
    fecharModal();
    setPage(Math.floor(machines.length / MON_PER_PAGE));
  };

  const hh = String(now.getHours()).padStart(2, "0");
  const mm = String(now.getMinutes()).padStart(2, "0");
  const ss = String(now.getSeconds()).padStart(2, "0");

  const paginas = [];
  for (let p = 0; p < totalPages; p++) {
    const start = p * MON_PER_PAGE;
    const end = Math.min(start + MON_PER_PAGE, machines.length);
    const cards = [];
    for (let i = start; i < end; i++) {
      const m = machines[i];
      cards.push(
        <MachineCard
          key={m.id}
          m={m}
          history={historyMap[m.id] || { pressao: [], consumo: [], vel: [], perfil: [] }}
          accentHex={accent.hex}
        />
      );
    }
    if (end - start < MON_PER_PAGE) {
      cards.push(
        <div key={`add-${p}`} className="machine is-add" onClick={abrirModal}>
          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5"><circle cx="12" cy="12" r="10"/><path d="M12 8v8M8 12h8"/></svg>
          <span>Adicionar máquina</span>
          <small>até 6 por página</small>
        </div>
      );
    }
    paginas.push(<div key={p} className="machine-page">{cards}</div>);
  }

  return (
    <div className={`dash-page mon-page${expandido ? " mon-fullscreen" : ""}`}>
      {expandido && (
        <button
          type="button"
          className="mon-fs-exit"
          onClick={() => setExpandido(false)}
          aria-label="Sair de tela cheia (ESC)"
          title="Sair de tela cheia (ESC)"
        >
          <svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
            <path d="M18 6L6 18M6 6l12 12"/>
          </svg>
        </button>
      )}
      <div className="page-head mon-page-head">
        <div>
          <div className="eyebrow"><span className="live-dot"/> TELEMETRIA · TEMPO REAL</div>
          <h1 className="h1">Monitoramento Remoto</h1>
          <div className="bread">Atualizado há 1s · 25 abr 2026 · {hh}:{mm}:{ss} BRT</div>
        </div>
        <div>
          <button
            type="button"
            className="dash-btn-ghost"
            onClick={() => setExpandido(true)}
            title="Expandir para tela cheia"
          >
            <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" style={{ marginRight: 6, verticalAlign: "-2px" }}>
              <path d="M3 8V4h4M21 8V4h-4M3 16v4h4M21 16v4h-4"/>
            </svg>
            Tela cheia
          </button>
        </div>
      </div>

      <div className="status-strip">
        <div className="strip-card">
          <div className="strip-icon s-idle">
            <svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="13" r="8"/><path d="M12 9v4l2.5 1.5"/><path d="M9 2h6"/><path d="M12 2v3"/></svg>
          </div>
          <div>
            <div className="strip-label">Ociosas</div>
            <div className="strip-value">{counts.idle}</div>
          </div>
        </div>
        <div className="strip-card">
          <div className="strip-icon s-drill">
            <svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"><path d="M12 2v18"/><path d="M7 5c2.5 0 5 1 5 2s-2.5 2-5 2"/><path d="M17 7c-2.5 0-5 1-5 2s2.5 2 5 2"/><path d="M7 11c2.5 0 5 1 5 2s-2.5 2-5 2"/><path d="M17 13c-2.5 0-5 1-5 2s2.5 2 5 2"/><path d="M9 22h6l-3-2z" fill="currentColor" stroke="none"/></svg>
          </div>
          <div>
            <div className="strip-label">Perfurando</div>
            <div className="strip-value">{counts.ok}</div>
          </div>
        </div>
        <div className="strip-card">
          <div className="strip-icon s-concrete">
            <svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"><path d="M2 17h3"/><path d="M19 17h3"/><circle cx="7" cy="17.5" r="2"/><circle cx="17" cy="17.5" r="2"/><path d="M9 17h6"/><path d="M2 9h6v6"/><path d="M8 11l4-3 7 1 1 7h-3"/><path d="M11 8.5l1.5 6.5"/><path d="M14.5 8.8l-1 6.7"/></svg>
          </div>
          <div>
            <div className="strip-label">Concretando</div>
            <div className="strip-value">{counts.warn}</div>
          </div>
        </div>
      </div>

      <div className="page-bar">
        <div className="page-bar-left">
          <b>{machines.length}</b> máquinas · <span>Página {page + 1} de {totalPages}</span>
        </div>
        <div className="pager">
          <button className="pager-btn" onClick={() => setPage(p => Math.max(0, p - 1))} disabled={page === 0}>
            <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2"><path d="M15 18l-6-6 6-6"/></svg>
          </button>
          <div className="pager-dots">
            {Array.from({ length: totalPages }).map((_, i) => (
              <button key={i} className={`pager-dot ${i === page ? "is-active" : ""}`} onClick={() => setPage(i)}/>
            ))}
          </div>
          <button className="pager-btn" onClick={() => setPage(p => Math.min(totalPages - 1, p + 1))} disabled={page >= totalPages - 1}>
            <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2"><path d="M9 6l6 6-6 6"/></svg>
          </button>
        </div>
      </div>

      <div className="machine-viewport" ref={viewportRef} onScroll={onScroll}>
        {expandido ? (
          /* Tela cheia: uma unica grade com TODAS as maquinas + o card "Adicionar".
             Colunas e linhas sao calculadas pra deixar o layout o mais quadrado
             possivel (cols = ceil(sqrt(N * aspect))). */
          (() => {
            // Layout fixo 4 colunas x 3 linhas (ate 12 maquinas por tela).
            // Acima de 12, sobram cards na proxima "pagina" — mas como
            // viewport esta com overflow hidden no fullscreen, somente os
            // 12 primeiros aparecem. Ajustar se for necessario paginar.
            return (
              <div
                className="machine-page is-flat"
                style={{
                  gridTemplateColumns: `repeat(4, minmax(0, 1fr))`,
                  gridTemplateRows: `repeat(3, minmax(0, 1fr))`,
                }}
              >
                {machines.slice(0, 12).map(m => (
                  <MachineCard
                    key={m.id}
                    m={m}
                    history={historyMap[m.id] || { pressao: [], consumo: [], vel: [], perfil: [] }}
                    accentHex={accent.hex}
                  />
                ))}
              </div>
            );
          })()
        ) : (
          <div className="machine-track">{paginas}</div>
        )}
      </div>

      {modalOpen && (
        <div className="modal-backdrop is-open" onClick={(e) => { if (e.currentTarget === e.target) fecharModal(); }}>
          <div className="modal">
            <div className="modal-head">
              <h3>Adicionar máquina</h3>
              <p>Cadastre uma máquina para monitoramento em tempo real</p>
            </div>
            <div className="modal-body">
              <div className="field full">
                <label className="field-label">Nome da máquina</label>
                <input className="field-input" value={form.name} onChange={(e) => setForm(f => ({ ...f, name: e.target.value }))} placeholder="ex: SIMEhC-09 / Hélice Bauer BG-25" autoFocus/>
              </div>
              <div className="field">
                <label className="field-label">ID / TAG</label>
                <input className="field-input" value={form.tag} onChange={(e) => setForm(f => ({ ...f, tag: e.target.value }))} placeholder="ex: MAQ-009"/>
              </div>
              <div className="field">
                <label className="field-label">Tipo</label>
                <select className="field-select" value={form.type} onChange={(e) => setForm(f => ({ ...f, type: e.target.value }))}>
                  <option>Hélice contínua</option>
                  <option>Estaca raiz</option>
                  <option>Pré-moldada</option>
                  <option>Strauss</option>
                  <option>Diafragma</option>
                </select>
              </div>
              <div className="field full">
                <label className="field-label">Obra</label>
                <input className="field-input" value={form.project} onChange={(e) => setForm(f => ({ ...f, project: e.target.value }))} placeholder="ex: Linha 6 · Estação Brasilândia"/>
              </div>
              <div className="field">
                <label className="field-label">Contrato</label>
                <input className="field-input" value={form.contract} onChange={(e) => setForm(f => ({ ...f, contract: e.target.value }))} placeholder="ex: CC-2154"/>
              </div>
              <div className="field">
                <label className="field-label">Diâmetro do trado (mm)</label>
                <input className="field-input" type="number" value={form.diam} onChange={(e) => setForm(f => ({ ...f, diam: e.target.value }))} placeholder="ex: 600"/>
              </div>
            </div>
            <div className="modal-foot">
              <button className="btn-ghost" onClick={fecharModal}>Cancelar</button>
              <button className="btn-primary" style={{ background: accent.hex }} onClick={salvarMaquina}>Adicionar</button>
            </div>
          </div>
        </div>
      )}
    </div>
  );
}

/* ---------- Tela de detalhes da obra (iframe) ---------- */

// Renderiza detalhes-obra.html dentro de um iframe em modo ?embed=1
// (esconde o sidebar interno do HTML). Os dados sao hardcoded no HTML
// por enquanto — quando o cadastro de obras estiver pronto, dados reais
// poderao ser passados via query param ou postMessage.
//
// Tema: o tema atual do SPA e' passado no src inicial via ?theme=...
// pra evitar flash de tema claro no load. Trocas posteriores enquanto
// a tela esta aberta sao notificadas via postMessage (MutationObserver
// no <html> detecta o data-theme mudar). Espelha o padrao usado pelo
// GoogleMap pra reagir a tema.
function DetalhesObraPage({ obraId }) {
  const iframeRef = React.useRef(null);
  // Tema inicial — usado so na construcao do src; depois disso, e' o
  // postMessage que sincroniza.
  const temaInicialRef = React.useRef(
    (typeof document !== 'undefined'
      ? document.documentElement.getAttribute('data-theme')
      : null) || 'light'
  );
  const src = "/detalhes-obra.html?embed=1"
    + `&theme=${encodeURIComponent(temaInicialRef.current)}`
    + (obraId ? `&id=${encodeURIComponent(obraId)}` : "");

  React.useEffect(() => {
    const enviarTema = () => {
      const tema = document.documentElement.getAttribute('data-theme') || 'light';
      const win = iframeRef.current && iframeRef.current.contentWindow;
      if (!win) return;
      try { win.postMessage({ tipo: 'set-theme', theme: tema }, '*'); } catch (_) {}
    };
    const obs = new MutationObserver(enviarTema);
    obs.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
    return () => obs.disconnect();
  }, []);

  return (
    <div className="dash-page dash-page-detalhes-obra">
      <div className="detalhes-obra-frame">
        <iframe
          ref={iframeRef}
          src={src}
          title="Detalhes da obra"
          className="detalhes-obra-iframe"
        />
      </div>
    </div>
  );
}

/* ---------- Painel do gráfico (iframe da tela de estacas) ---------- */

// Remove o sufixo _editado_<timestamp> aplicado as copias geradas pelo
// editor. So o backend usa o nome completo (precisa ser unico); na UI
// exibimos o nome original.
function stripSufixoEditado(arquivo) {
  return String(arquivo || "").replace(/_editado_\d+$/i, "");
}

// Extrai "PA2_P15" + "26/05/04" de "PA2_P15 - 240526071530.cge"
// (formato gerado por dmpToCge.js: <estaca> - yymmddHHMM.cge).
function parseEstacaArquivo(arquivo) {
  if (!arquivo) return { nome: "", data: "" };
  const limpo = stripSufixoEditado(arquivo);
  const m = limpo.match(/^(.*?)\s*-\s*(\d{2})(\d{2})(\d{2})\d{4}\.cge$/i);
  if (!m) return { nome: limpo.replace(/\.cge$/i, ""), data: "" };
  return { nome: m[1].trim(), data: `${m[2]}/${m[3]}/${m[4]}` };
}
function labelEstaca(arquivo) {
  const { nome, data } = parseEstacaArquivo(arquivo);
  return data ? `${nome} · ${data}` : nome || "—";
}

function EstacaGrafico({ pasta, arquivo, reloadKey = 0, navLista = [], onNavegar, obra = null, temEditadas = false, onVoltar, origem = "planilha", pausado = false }) {
  const iframeRef = React.useRef(null);
  const frameRef = React.useRef(null);
  const carregadoRef = React.useRef(false);
  const srcInicialRef = React.useRef(`/graficos.html?embed=1&pasta=${encodeURIComponent(pasta)}&arquivo=${encodeURIComponent(arquivo)}`);
  const [gerandoPdf, setGerandoPdf] = React.useState(false);
  const [modalRelOpen, setModalRelOpen] = React.useState(false);
  // Nome da obra vem do cabecalho dos dados, que so o iframe tem acesso
  // direto. O iframe envia { tipo: 'estaca-meta', obra } via postMessage
  // logo apos carregar — guardamos aqui pra exibir no pagehead.
  const [obraNome, setObraNome] = React.useState("");
  React.useEffect(() => { setObraNome(""); }, [arquivo]);
  React.useEffect(() => {
    const onMsg = (ev) => {
      if (ev.data?.tipo !== 'estaca-meta') return;
      if (ev.data.arquivo && ev.data.arquivo !== arquivo) return;
      setObraNome(ev.data.obra || "");
    };
    window.addEventListener('message', onMsg);
    return () => window.removeEventListener('message', onMsg);
  }, [arquivo]);

  // Mantem navLista em ref pra calcular vizinhas no postMessage sem
  // disparar o effect de carregar-estaca toda vez que a lista muda
  // (evita re-fetch desnecessario quando so o navLista mudou).
  const navListaRef = React.useRef(navLista);
  React.useEffect(() => { navListaRef.current = navLista; }, [navLista]);

  const calcularVizinhas = React.useCallback((p, a) => {
    const lista = navListaRef.current || [];
    const idx = lista.findIndex(x => x.pasta === p && x.arquivo === a);
    if (idx === -1) return [];
    const viz = [];
    // 2 antes + 2 depois — cobre navegacao rapida sem encarecer demais.
    for (let d = 1; d <= 2; d++) {
      if (idx - d >= 0) viz.push(lista[idx - d]);
      if (idx + d < lista.length) viz.push(lista[idx + d]);
    }
    return viz;
  }, []);

  // Recarrega o iframe sempre que o arquivo aberto muda OU quando o
  // pai sinaliza atualizacao via reloadKey (ex: salvar uma copia editada
  // ja aberta, em que o nome do arquivo nao muda mas o conteudo sim).
  // Quando o editor de estacas abre (pausado=true), o iframe de fundo
  // fica coberto pelo modal — entao pulamos qualquer postMessage ate o
  // editor fechar (reduz CPU/GPU). A re-execucao do efeito quando
  // `pausado` cai para false pega a estaca atual e sincroniza.
  const reloadKeyAnteriorRef = React.useRef(reloadKey);
  React.useEffect(() => {
    if (!carregadoRef.current) return; // primeira carga vai pelo src
    if (pausado) return;
    const iframe = iframeRef.current;
    if (!iframe || !iframe.contentWindow) return;
    if (reloadKey !== reloadKeyAnteriorRef.current) {
      try { iframe.contentWindow.postMessage({ tipo: 'invalidar-cache', pasta, arquivo }, '*'); } catch (_) {}
      reloadKeyAnteriorRef.current = reloadKey;
    }
    const vizinhas = calcularVizinhas(pasta, arquivo);
    iframe.contentWindow.postMessage({ tipo: "carregar-estaca", pasta, arquivo, vizinhas }, "*");
  }, [pasta, arquivo, reloadKey, pausado, calcularVizinhas]);

  // Ping periodico pra manter o iframe "acordado" — o parent nao e
  // throttled, entao esses postMessages forcam o iframe a processar
  // mensagens regularmente, impedindo que o Chrome reduza sua prioridade.
  React.useEffect(() => {
    if (pausado) return;
    const id = setInterval(() => {
      const iframe = iframeRef.current;
      try { iframe?.contentWindow?.postMessage({ tipo: 'ping' }, '*'); } catch (_) {}
    }, 500);
    return () => clearInterval(id);
  }, [pausado]);

  // Recebe status do iframe pra refletir o estado de "gerando" no botão pai
  React.useEffect(() => {
    const onMsg = (ev) => {
      if (ev.data?.tipo !== "pdf-status") return;
      setGerandoPdf(ev.data.estado === "inicio");
    };
    window.addEventListener("message", onMsg);
    return () => window.removeEventListener("message", onMsg);
  }, []);

  const gerarPdf = (graficos = null) => {
    const iframe = iframeRef.current;
    if (!iframe || !iframe.contentWindow) return;
    iframe.contentWindow.postMessage({ tipo: "gerar-pdf", graficos }, "*");
  };

  const gerarPng = (graficos = null) => {
    const iframe = iframeRef.current;
    if (!iframe || !iframe.contentWindow) return;
    iframe.contentWindow.postMessage({ tipo: "gerar-png", graficos }, "*");
  };

  // Ao entrar/trocar de estaca, posiciona o iframe ocupando toda a viewport.
  // O usuario rola pra cima pra ver o pagehead (Voltar/PDF) e pra baixo
  // pra ver o foot-nav.
  React.useEffect(() => {
    const frame = frameRef.current;
    if (!frame) return;
    const t = setTimeout(() => {
      frame.scrollIntoView({ behavior: "auto", block: "start" });
    }, 60);
    return () => clearTimeout(t);
  }, [pasta, arquivo]);

  return (
    <div className="dash-page dash-page-grafico" style={pausado ? { display: "none" } : undefined}>
      {/* Link "← Planilha" visivel so em mobile. O .dash-actions (que tem
          o botao desktop) esta hidden em mobile pra liberar espaco ao chart;
          este atalho cumpre o mesmo papel — mesmo visual do "← Dashboard"
          da FrotaPage (texto plano em azul, sem pill). */}
      <button
        type="button"
        className="grafico-voltar-mobile"
        onClick={onVoltar}
        aria-label={`Voltar para ${origem === "dashboard" ? "o Dashboard" : "a planilha"}`}
      >← {origem === "dashboard" ? "Dashboard" : "Planilha"}</button>
      <div className="dash-pagehead">
        <div>
          <div
            className="dash-eyebrow"
            style={{
              fontSize: 16,
              letterSpacing: '0.02em',
              textTransform: 'none',
              fontWeight: 600,
              color: 'var(--ink)',
              marginBottom: 8,
            }}
          >
            {obraNome || pasta}
          </div>
          <h1 className="dash-h1">{stripSufixoEditado(arquivo)}</h1>
          <div className="dash-bread">Visualização do registro de perfuração</div>
        </div>
        <div className="dash-actions">
          <button className="dash-btn-ghost" onClick={onVoltar}>← Voltar à planilha</button>
          <button
            className="dash-btn-primary"
            style={{ background: "var(--navy)" }}
            onClick={() => setModalRelOpen(true)}
            disabled={gerandoPdf}
          >
            {gerandoPdf ? "⏳ Gerando PDF…" : "⬇ Relatório PDF / XLSX"}
          </button>
        </div>
      </div>
      <div className="dash-grafico-frame" ref={frameRef}>
        <iframe
          ref={iframeRef}
          src={srcInicialRef.current}
          title={`Gráfico ${arquivo}`}
          className="dash-grafico-iframe"
          onLoad={() => {
            carregadoRef.current = true;
            // Se o editor estiver aberto, pula o sync inicial — o efeito
            // vai disparar quando o editor fechar (pausado: true→false).
            if (pausado) return;
            // Se o usuario navegou pra outra estaca enquanto o iframe ainda
            // carregava, o postMessage anterior foi descartado (carregadoRef
            // ainda era false). Aqui sincroniza com a estaca atual — sem
            // isso o iframe ficava preso na estaca inicial ate uma proxima
            // navegacao re-disparar o efeito.
            const iframe = iframeRef.current;
            if (iframe?.contentWindow) {
              const vizinhas = calcularVizinhas(pasta, arquivo);
              iframe.contentWindow.postMessage({ tipo: "carregar-estaca", pasta, arquivo, vizinhas }, "*");
            }
          }}
        />
      </div>
      <FootNav pasta={pasta} arquivo={arquivo} navLista={navLista} onNavegar={onNavegar} />

      {/* FAB de baixar PNG — visivel so em mobile (CSS controla). Substitui
          o "Relatorio PDF/XLSX" do pagehead (escondido em mobile). PNG e' mais
          pratico que PDF pra compartilhar em mensageiros do celular.
          Lista fixa de 6 graficos na ordem MT|VR|VA|Perfil|PC|VS — sem IX/IY/Vazao
          (ruidosos no relatorio mobile). A ordem do array tambem define a ordem
          visual via aplicarSelecaoGraficos (script.js:820). */}
      <button
        type="button"
        className="png-fab-mobile"
        onClick={() => gerarPng(["mt", "vr", "va", "perfil", "pc", "vs"])}
        disabled={gerandoPdf}
        aria-label={gerandoPdf ? "Gerando imagem…" : "Baixar imagem PNG"}
      >
        {gerandoPdf ? (
          <span className="png-fab-spinner" aria-hidden="true"/>
        ) : (
          /* SF Symbol "square.and.arrow.down" — icone iOS padrao de
             salvar/baixar (retangulo aberto no topo + seta pra baixo). */
          <svg
            className="png-fab-icon"
            width="20"
            height="20"
            viewBox="0 0 24 24"
            fill="none"
            stroke="currentColor"
            strokeWidth="2"
            strokeLinecap="round"
            strokeLinejoin="round"
            aria-hidden="true"
          >
            <path d="M12 3v13"/>
            <path d="m7 11 5 5 5-5"/>
            <path d="M5 14v5a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-5"/>
          </svg>
        )}
        <span className="png-fab-label">PNG</span>
      </button>

      <RelatorioModal
        open={modalRelOpen}
        onClose={() => setModalRelOpen(false)}
        nomeEstacaAtual={parseEstacaArquivo(arquivo).nome || arquivo}
        arquivoAtual={arquivo}
        obra={obra}
        pasta={pasta}
        temEditadas={temEditadas}
        onGerar={(opcao, extras) => {
          // single-1: gerado via postMessage pro iframe (gerarPdf existente).
          // Demais opcoes (all-1 / all-4 / synthesis) sao tratadas dentro do
          // proprio modal (fetch + download), nao chegam aqui.
          if (opcao === "single-1") {
            setModalRelOpen(false);
            gerarPdf(extras?.graficos || null);
          }
        }}
      />
    </div>
  );
}

/* ---------- Foot-nav (rodape de navegacao entre estacas) ---------- */
function FootNav({ pasta, arquivo, navLista, onNavegar }) {
  const total = Array.isArray(navLista) ? navLista.length : 0;
  const idx = total
    ? navLista.findIndex(x => x.pasta === pasta && x.arquivo === arquivo)
    : -1;
  if (idx < 0) return null;
  const prev = idx > 0 ? navLista[idx - 1] : null;
  const next = idx < total - 1 ? navLista[idx + 1] : null;

  return (
    <div className="estaca-foot-nav">
      <button
        type="button"
        className="estaca-foot-nav__btn estaca-foot-nav__btn--left"
        aria-label="Estaca anterior"
        disabled={!prev}
        onClick={() => prev && onNavegar && onNavegar("prev")}
      >
        <span className="estaca-foot-nav__arrow" aria-hidden="true">←</span>
        <span className="estaca-foot-nav__txt">
          <span className="estaca-foot-nav__lbl">Anterior</span>
          <span className="estaca-foot-nav__name">{prev ? labelEstaca(prev.arquivo) : "—"}</span>
        </span>
      </button>

      <div className="estaca-foot-nav__center">
        Estaca <strong>{idx + 1} de {total}</strong>
      </div>

      <button
        type="button"
        className="estaca-foot-nav__btn estaca-foot-nav__btn--right"
        aria-label="Próxima estaca"
        disabled={!next}
        onClick={() => next && onNavegar && onNavegar("next")}
      >
        <span className="estaca-foot-nav__txt">
          <span className="estaca-foot-nav__lbl">Próxima</span>
          <span className="estaca-foot-nav__name">{next ? labelEstaca(next.arquivo) : "—"}</span>
        </span>
        <span className="estaca-foot-nav__arrow" aria-hidden="true">→</span>
      </button>
    </div>
  );
}

/* ---------- Modal "Gerar Relatorio" ----------
 * Apenas a opcao "single-1" esta integrada ao back-end (= comportamento
 * antigo do botao). As outras opcoes ficam selecionaveis mas exibem
 * aviso de "em breve" no submit. */
const REL_THUMBS = {
  "single-1": '<svg viewBox="0 0 44 56" preserveAspectRatio="xMidYMid meet"><rect x="2" y="2" width="40" height="52" rx="3" fill="#fff" stroke="#D8DEE8" stroke-width="1"/><rect x="6" y="6" width="22" height="2.5" rx="1" fill="#0F1B2D"/><rect x="6" y="11" width="14" height="1.5" rx=".7" fill="#A8B0C2"/><rect x="6" y="16" width="32" height="6" rx="1" fill="#EEF2F7"/><g transform="translate(6 26)"><rect x="0" y="0" width="6" height="24" rx=".5" fill="#3B6EE0" opacity=".7"/><rect x="8" y="2" width="6" height="22" rx=".5" fill="#22A06B" opacity=".7"/><rect x="16" y="4" width="6" height="20" rx=".5" fill="#D14343" opacity=".65"/><rect x="24" y="1" width="6" height="23" rx=".5" fill="#7C3AED" opacity=".65"/></g></svg>',
  "all-1": '<svg viewBox="0 0 44 56" preserveAspectRatio="xMidYMid meet"><rect x="9" y="3" width="32" height="44" rx="3" fill="#fff" stroke="#D8DEE8" stroke-width=".8"/><rect x="6" y="6" width="32" height="44" rx="3" fill="#fff" stroke="#D8DEE8" stroke-width=".8"/><rect x="3" y="9" width="32" height="44" rx="3" fill="#fff" stroke="#A8B0C2" stroke-width="1"/><rect x="7" y="14" width="18" height="2" rx="1" fill="#0F1B2D"/><rect x="7" y="19" width="10" height="1.2" fill="#A8B0C2"/><g transform="translate(7 24)"><rect x="0" y="0" width="4" height="24" fill="#3B6EE0" opacity=".7"/><rect x="6" y="3" width="4" height="21" fill="#22A06B" opacity=".7"/><rect x="12" y="5" width="4" height="19" fill="#D14343" opacity=".6"/><rect x="18" y="2" width="4" height="22" fill="#7C3AED" opacity=".6"/></g></svg>',
  "all-4": '<svg viewBox="0 0 44 56" preserveAspectRatio="xMidYMid meet"><rect x="9" y="3" width="32" height="44" rx="3" fill="#fff" stroke="#D8DEE8" stroke-width=".8"/><rect x="6" y="6" width="32" height="44" rx="3" fill="#fff" stroke="#D8DEE8" stroke-width=".8"/><rect x="3" y="9" width="32" height="44" rx="3" fill="#fff" stroke="#A8B0C2" stroke-width="1"/><rect x="7" y="14" width="14" height="2" rx="1" fill="#0F1B2D"/><g transform="translate(7 20)"><rect x="0" y="0" width="11" height="13" rx="1" fill="#EEF2F7"/><rect x="2" y="3" width="1.5" height="8" fill="#3B6EE0" opacity=".7"/><rect x="4.5" y="5" width="1.5" height="6" fill="#3B6EE0" opacity=".7"/><rect x="7" y="2" width="1.5" height="9" fill="#3B6EE0" opacity=".7"/><rect x="13" y="0" width="11" height="13" rx="1" fill="#EEF2F7"/><rect x="15" y="4" width="1.5" height="7" fill="#22A06B" opacity=".7"/><rect x="17.5" y="2" width="1.5" height="9" fill="#22A06B" opacity=".7"/><rect x="20" y="5" width="1.5" height="6" fill="#22A06B" opacity=".7"/><rect x="0" y="15" width="11" height="13" rx="1" fill="#EEF2F7"/><rect x="2" y="18" width="1.5" height="8" fill="#D14343" opacity=".6"/><rect x="4.5" y="20" width="1.5" height="6" fill="#D14343" opacity=".6"/><rect x="7" y="17" width="1.5" height="9" fill="#D14343" opacity=".6"/><rect x="13" y="15" width="11" height="13" rx="1" fill="#EEF2F7"/><rect x="15" y="19" width="1.5" height="7" fill="#7C3AED" opacity=".6"/><rect x="17.5" y="17" width="1.5" height="9" fill="#7C3AED" opacity=".6"/><rect x="20" y="20" width="1.5" height="6" fill="#7C3AED" opacity=".6"/></g></svg>',
  "synthesis": '<svg viewBox="0 0 44 56" preserveAspectRatio="xMidYMid meet"><rect x="2" y="2" width="40" height="52" rx="2" fill="#fff" stroke="#C8D0DC" stroke-width="1"/><rect x="2" y="2" width="40" height="6" rx="2" fill="#1F7A4D"/><rect x="2" y="6" width="40" height="2" fill="#1F7A4D"/><text x="6" y="6.5" font-family="Inter, sans-serif" font-size="3.2" fill="#fff" font-weight="700">XLS</text><rect x="2" y="9" width="40" height="3" fill="#F2F4F7"/><g font-family="Inter, sans-serif" font-size="2.4" fill="#5A6478" font-weight="600"><text x="7" y="11.4">A</text><text x="13" y="11.4">B</text><text x="19" y="11.4">C</text><text x="25" y="11.4">D</text><text x="31" y="11.4">E</text><text x="37" y="11.4">F</text></g><rect x="2" y="12" width="3" height="42" fill="#F2F4F7"/><g stroke="#E1E5EC" stroke-width=".4"><line x1="11" y1="12" x2="11" y2="54"/><line x1="17" y1="12" x2="17" y2="54"/><line x1="23" y1="12" x2="23" y2="54"/><line x1="29" y1="12" x2="29" y2="54"/><line x1="35" y1="12" x2="35" y2="54"/><line x1="2" y1="16" x2="42" y2="16"/><line x1="2" y1="20" x2="42" y2="20"/><line x1="2" y1="24" x2="42" y2="24"/><line x1="2" y1="28" x2="42" y2="28"/><line x1="2" y1="32" x2="42" y2="32"/><line x1="2" y1="36" x2="42" y2="36"/><line x1="2" y1="40" x2="42" y2="40"/><line x1="2" y1="44" x2="42" y2="44"/><line x1="2" y1="48" x2="42" y2="48"/></g><rect x="5" y="12" width="37" height="4" fill="#E8EFF7"/></svg>',
};

// Selecao de graficos default: os 6 atualmente exibidos no PDF
// (mantemos para compatibilidade com a versao antiga, antes do picker).
const GRAFICOS_DEFAULT = ["mt", "vr", "va", "pc", "perfil", "vs"];
const GRAFICOS_OPCOES = [
  { id: "mt",     label: "MT",     desc: "Momento de torção" },
  { id: "vr",     label: "VR",     desc: "Velocidade de rotação" },
  { id: "va",     label: "VA",     desc: "Velocidade de avanço" },
  { id: "pc",     label: "PC",     desc: "Pressão de concreto" },
  { id: "perfil", label: "PERFIL", desc: "Perfil da estaca" },
  { id: "vs",     label: "VS",     desc: "Velocidade de subida" },
  { id: "ix",     label: "IX",     desc: "Inclinação X" },
  { id: "iy",     label: "IY",     desc: "Inclinação Y" },
  { id: "vazao",  label: "Vazão",  desc: "Vazão de concreto" },
];
// Capacidade do grid (slots) varia por modo. Perfil sempre pesa 2 (largura
// dupla), demais 1.
//   single-1 / all-1: 7 slots → ate 6 com perfil, 7 sem perfil
//   all-4 (4 estacas/pag): 5 slots → ate 4 com perfil (perfil + 3), 5 sem
const GRAFICOS_SLOTS_PADRAO = 7;
const GRAFICOS_SLOTS_4PP = 5;
const slotsPorModo = (modo) => (modo === "all-4" ? GRAFICOS_SLOTS_4PP : GRAFICOS_SLOTS_PADRAO);
const pesoGrafico = (id) => (id === "perfil" ? 2 : 1);
const slotsUsados = (lista) => lista.reduce((s, id) => s + pesoGrafico(id), 0);
const limiteGraficos = (lista, slots) => (lista.includes("perfil") ? slots - 1 : slots);

// Persistencia da selecao de graficos (incluindo a ordem) entre sessoes.
// Salva no localStorage do browser do usuario; ao reabrir o modal, a lista
// volta com os mesmos graficos na mesma ordem da ultima vez.
const LS_GRAFICOS_KEY = "compugeo_pdf_graficos";
const GRAFICOS_VALID_IDS = new Set(GRAFICOS_OPCOES.map(g => g.id));
// Minimo de graficos exigido no PDF (single-1 / all-1 / all-4) — abaixo
// disso a leitura do relatorio fica pobre. Vale para ambos os modos.
const GRAFICOS_MIN = 3;
function carregarGraficosSalvos() {
  try {
    const raw = localStorage.getItem(LS_GRAFICOS_KEY);
    if (!raw) return GRAFICOS_DEFAULT;
    const lista = JSON.parse(raw);
    if (!Array.isArray(lista)) return GRAFICOS_DEFAULT;
    // Filtra ids que nao existem mais (caso a lista de opcoes mude no futuro).
    const validos = lista.filter(id => typeof id === "string" && GRAFICOS_VALID_IDS.has(id));
    // Se uma sessao antiga salvou menos do que o minimo atual, volta pro default.
    return validos.length >= GRAFICOS_MIN ? validos : GRAFICOS_DEFAULT;
  } catch (_) {
    return GRAFICOS_DEFAULT;
  }
}
function salvarGraficosSalvos(lista) {
  try { localStorage.setItem(LS_GRAFICOS_KEY, JSON.stringify(lista)); } catch (_) {}
}

const LS_SINTESE_FMT_KEY = "compugeo_sintese_formato";
function carregarFormatoSintese() {
  try {
    const v = localStorage.getItem(LS_SINTESE_FMT_KEY);
    return v === "xlsx" ? "xlsx" : "pdf";
  } catch (_) { return "pdf"; }
}

function RelatorioModal({ open, onClose, nomeEstacaAtual, arquivoAtual, obra, pasta, onGerar, ocultarEstacaAtual = false, temEditadas = false }) {
  // Quando aberto da planilha (sem estaca atual), oculta o grupo "Estaca
  // atual" e ja comeca com "1 Estaca por pagina (todas)" pre-selecionado.
  const selInicial = ocultarEstacaAtual ? "all-1" : "single-1";
  const [sel, setSel] = React.useState(selInicial);
  const [gerando, setGerando] = React.useState(false);
  const [graficosSel, setGraficosSel] = React.useState(carregarGraficosSalvos);
  // Versao das estacas a serem usadas no PDF de pasta (all-1 / all-4):
  //   'originais': sempre o arquivo original (default).
  //   'editadas': substitui pelo arquivo da copia editada quando existir,
  //               mantendo o original na posicao quando nao houver.
  // Toggle so eh exibido nos modos all-1/all-4 e fica desabilitado quando
  // a pasta nao tem nenhuma copia editada.
  const [versao, setVersao] = React.useState("originais");
  // Filtro de periodo opcional. Quando `usarPeriodo` esta ligado e ao menos
  // uma das datas esta preenchida, a geracao envia dataIni/dataFim pros
  // endpoints (all-1, all-4, sintese) que filtram por inicio_perf.
  // `periodoMinMax` guarda o range REAL do contrato (min/max de inicio_perf)
  // — usado pra pre-preencher os inputs e bloquear datas fora desse range.
  const [usarPeriodo, setUsarPeriodo] = React.useState(false);
  const [dataIni, setDataIni] = React.useState("");
  const [dataFim, setDataFim] = React.useState("");
  const [periodoMinMax, setPeriodoMinMax] = React.useState({ dataIni: null, dataFim: null });
  // Formato do Relatorio de Sintese: 'pdf' ou 'xlsx'. PDF abre no preview;
  // XLSX baixa direto (browsers nao renderizam XLSX em iframe).
  const [formatoSintese, setFormatoSintese] = React.useState(carregarFormatoSintese);
  // Progresso de "all-1": { total, atual, concluidos: [{arquivo, ok, motivo}] }
  const [progresso, setProgresso] = React.useState(null);
  // Preview do PDF: { url (blob URL), nome }. Quando setado, modal mostra
  // o PDF embutido com botoes "Voltar" e "Baixar".
  const [preview, setPreview] = React.useState(null);
  const esRef = React.useRef(null);
  const concluidosRef = React.useRef(null);

  const limparPreview = () => {
    setPreview(prev => {
      if (prev?.url) try { URL.revokeObjectURL(prev.url); } catch (_) {}
      return null;
    });
  };

  const resetar = () => {
    if (esRef.current) { try { esRef.current.close(); } catch(_) {} esRef.current = null; }
    setGerando(false);
    setProgresso(null);
    limparPreview();
  };

  React.useEffect(() => {
    if (open) {
      setSel(selInicial);
      // Recarrega do localStorage a cada abertura — pega edicoes feitas
      // em outras abas / sessoes anteriores.
      setGraficosSel(carregarGraficosSalvos());
      setVersao("originais");
      setUsarPeriodo(false);
      setDataIni("");
      setDataFim("");
      setPeriodoMinMax({ dataIni: null, dataFim: null });
      resetar();
    }
  }, [open, selInicial]);

  // Busca o range real de datas do contrato (min/max de inicio_perf) na
  // abertura do modal, pra ja vir pre-preenchido quando o usuario ligar
  // "Filtrar por data". Aborta se o modal fechar antes da resposta.
  React.useEffect(() => {
    if (!open || !pasta) return;
    let cancelado = false;
    (async () => {
      try {
        const resp = await fetch(`/api/contrato/periodo?pasta=${encodeURIComponent(pasta)}`, { credentials: "same-origin" });
        if (!resp.ok) return;
        const data = await resp.json();
        if (cancelado) return;
        setPeriodoMinMax({ dataIni: data.dataIni || null, dataFim: data.dataFim || null });
        // Pre-preenche os campos com o range completo do contrato.
        if (data.dataIni) setDataIni(data.dataIni);
        if (data.dataFim) setDataFim(data.dataFim);
      } catch (_) { /* silencioso — filtro continua funcionando sem pre-fill */ }
    })();
    return () => { cancelado = true; };
  }, [open, pasta]);

  // Persiste a selecao (ids + ordem) toda vez que muda. Inclui mudancas
  // feitas pelo trim de slots e drag-and-drop da strip de ordem.
  React.useEffect(() => {
    salvarGraficosSalvos(graficosSel);
  }, [graficosSel]);

  // Persiste o formato preferido da sintese (PDF ou XLSX).
  React.useEffect(() => {
    try { localStorage.setItem(LS_SINTESE_FMT_KEY, formatoSintese); } catch (_) {}
  }, [formatoSintese]);

  // Voltar ao seletor a partir do preview (mantem selecao do usuario).
  const voltarDoPreview = () => limparPreview();

  // Baixa o blob ja gerado (sem novo round-trip ao server).
  const baixarPreview = () => {
    if (!preview) return;
    const a = document.createElement("a");
    a.href = preview.url;
    a.download = preview.nome;
    document.body.appendChild(a);
    a.click();
    a.remove();
  };

  const slotsAtuais = slotsPorModo(sel);

  // graficosSel guarda a ordem desejada no PDF: novos itens entram no fim
  // (ordem de selecao). O usuario reordena depois via drag-and-drop na strip
  // "Ordem no PDF" — por isso evitamos sort canonico aqui.
  const toggleGrafico = (id) => {
    setGraficosSel(prev => {
      if (prev.includes(id)) {
        // Bloqueia desmarcar abaixo do minimo (UX: checkbox vira disabled
        // antes do clique, mas mantemos a guarda aqui por seguranca).
        if (prev.length <= GRAFICOS_MIN) return prev;
        return prev.filter(x => x !== id);
      }
      // Capacidade depende do modo (5 ou 7 slots). Perfil pesa 2; demais, 1.
      if (slotsUsados(prev) + pesoGrafico(id) > slotsAtuais) return prev;
      return [...prev, id];
    });
  };

  // Ao trocar de modo (single-1 → all-4 etc), trim a selecao se passar do
  // limite do novo modo. Mantem a ordem; corta os do final.
  React.useEffect(() => {
    setGraficosSel(prev => {
      if (slotsUsados(prev) <= slotsAtuais) return prev;
      const out = [];
      let usados = 0;
      for (const id of prev) {
        if (usados + pesoGrafico(id) > slotsAtuais) break;
        out.push(id);
        usados += pesoGrafico(id);
      }
      return out;
    });
  }, [slotsAtuais]);

  // Drag-and-drop da strip de ordem. dragIdx eh o indice do item sendo
  // arrastado; o drop faz splice (remove + insere) na nova posicao.
  const dragIdxRef = React.useRef(null);
  const [dragOverIdx, setDragOverIdx] = React.useState(null);
  const onChipDragStart = (idx) => (e) => {
    dragIdxRef.current = idx;
    e.dataTransfer.effectAllowed = "move";
    try { e.dataTransfer.setData("text/plain", String(idx)); } catch (_) {}
  };
  const onChipDragOver = (idx) => (e) => {
    e.preventDefault();
    e.dataTransfer.dropEffect = "move";
    if (dragOverIdx !== idx) setDragOverIdx(idx);
  };
  const onChipDragLeave = () => setDragOverIdx(null);
  const onChipDrop = (idx) => (e) => {
    e.preventDefault();
    const from = dragIdxRef.current;
    dragIdxRef.current = null;
    setDragOverIdx(null);
    if (from === null || from === idx) return;
    setGraficosSel(prev => {
      const arr = [...prev];
      const [moved] = arr.splice(from, 1);
      arr.splice(idx, 0, moved);
      return arr;
    });
  };
  const onChipDragEnd = () => { dragIdxRef.current = null; setDragOverIdx(null); };

  const mostrarPicker = sel !== "synthesis";
  React.useEffect(() => () => resetar(), []);

  // Esc fecha (a menos que esteja gerando — evita interromper geracao)
  React.useEffect(() => {
    if (!open) return;
    const onKey = (e) => { if (e.key === "Escape" && !gerando) onClose && onClose(); };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [open, onClose, gerando]);

  // Trava scroll do body enquanto o modal estiver aberto: senao a pagina
  // de fundo rola junto quando o conteudo do modal supera a viewport.
  React.useEffect(() => {
    if (!open) return;
    const prev = document.body.style.overflow;
    document.body.style.overflow = "hidden";
    return () => { document.body.style.overflow = prev; };
  }, [open]);

  // Auto-scroll na lista de concluidos sempre que adiciona novo item
  React.useEffect(() => {
    const el = concluidosRef.current;
    if (el) el.scrollTop = el.scrollHeight;
  }, [progresso?.concluidos?.length]);

  if (!open) return null;

  // Quando o usuario liga "Filtrar por data", devolve { dataIni, dataFim }
  // (apenas as preenchidas). Caso contrario, devolve {} pra deixar os
  // payloads/querystrings limpos. Aceita range aberto de um lado.
  const periodoFiltro = () => {
    if (!usarPeriodo) return {};
    const out = {};
    if (dataIni) out.dataIni = dataIni;
    if (dataFim) out.dataFim = dataFim;
    return out;
  };

  // Download em batch (all-1 e all-4): fluxo de 3 etapas
  //   1) POST <startEndpoint> → recebe jobId
  //   2) EventSource /api/pdf-pasta/progress?jobId — mostra progresso
  //   3) Ao receber 'fim', baixa /api/pdf-pasta/download?jobId
  // O 4pp reusa os mesmos endpoints de progress/download — so o start muda.
  const gerarPastaPdf = async (startEndpoint = "/api/pdf-pasta/start") => {
    if (!pasta) { window.alert("Pasta nao identificada."); return; }
    setGerando(true);
    setProgresso({ total: obra?.totalEstacas || 0, atual: null, concluidos: [] });
    try {
      const periodo = periodoFiltro();
      const startResp = await fetch(startEndpoint, {
        method: "POST",
        credentials: "same-origin",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          pasta,
          graficos: graficosSel,
          versao: temEditadas ? versao : "originais",
          ...periodo,
        }),
      });
      if (!startResp.ok) {
        const erro = await startResp.json().catch(() => ({}));
        throw new Error(erro.erro || `Erro ${startResp.status}`);
      }
      const { jobId, total } = await startResp.json();
      setProgresso(p => ({ ...p, total }));

      await new Promise((resolve, reject) => {
        const es = new EventSource(`/api/pdf-pasta/progress?jobId=${encodeURIComponent(jobId)}`);
        esRef.current = es;
        es.onmessage = (ev) => {
          let d; try { d = JSON.parse(ev.data); } catch { return; }
          if (d.tipo === "iniciando") {
            setProgresso(p => ({ ...p, atual: d.arquivo, total: d.total ?? p.total }));
          } else if (d.tipo === "arquivo") {
            setProgresso(p => ({
              ...p,
              concluidos: [...p.concluidos, { arquivo: d.arquivo, ok: d.ok, motivo: d.motivo }],
              atual: null,
            }));
          } else if (d.tipo === "estado") {
            setProgresso(p => ({ ...p, total: d.total ?? p.total, atual: d.atual }));
          } else if (d.tipo === "fim") {
            es.close(); esRef.current = null; resolve();
          } else if (d.tipo === "erro") {
            es.close(); esRef.current = null; reject(new Error(d.motivo || "Falha na geracao"));
          }
        };
        es.onerror = () => {
          es.close(); esRef.current = null;
          reject(new Error("Conexao com o servidor caiu durante a geracao."));
        };
      });

      // Pega o blob e mostra no preview (substitui download direto).
      const dlResp = await fetch(`/api/pdf-pasta/download?jobId=${encodeURIComponent(jobId)}`, { credentials: "same-origin" });
      if (!dlResp.ok) {
        const erro = await dlResp.json().catch(() => ({}));
        throw new Error(erro.erro || `Falha no download (${dlResp.status})`);
      }
      const blob = await dlResp.blob();
      const url = URL.createObjectURL(blob);
      const nome = `${(pasta || "obra").replace(/[^\w.\-]/g, "_")}.pdf`;
      // Limpa progresso/loader sem zerar o preview que acabamos de criar.
      if (esRef.current) { try { esRef.current.close(); } catch(_) {} esRef.current = null; }
      setProgresso(null);
      setGerando(false);
      setPreview({ url, nome });
    } catch (err) {
      window.alert(`Falha ao gerar PDF da obra: ${err.message}`);
      resetar();
    }
  };

  // single-1: gera o PDF da estaca atual e mostra no preview.
  const gerarSinglePdfPreview = async () => {
    if (!pasta || !arquivoAtual) {
      window.alert("Estaca nao identificada.");
      return;
    }
    setGerando(true);
    try {
      const g = graficosSel.length
        ? `&graficos=${encodeURIComponent(graficosSel.join(","))}`
        : "";
      const resp = await fetch(
        `/api/pdf?pasta=${encodeURIComponent(pasta)}&arquivo=${encodeURIComponent(arquivoAtual)}${g}`,
        { credentials: "same-origin" }
      );
      if (!resp.ok) {
        const erro = await resp.json().catch(() => ({}));
        throw new Error(erro.erro || `Erro ${resp.status}`);
      }
      const blob = await resp.blob();
      const url = URL.createObjectURL(blob);
      const nome = `${arquivoAtual.replace(/\.cge$/i, "")}.pdf`;
      setPreview({ url, nome });
    } catch (err) {
      window.alert(`Falha ao gerar PDF: ${err.message}`);
    } finally {
      setGerando(false);
    }
  };

  // Sintese: query unica + render server-side, sem progresso (rapido).
  // PDF: mesmo fluxo de preview dos outros modos (iframe + Baixar).
  // XLSX: download direto (iframe nao renderiza planilha) e fecha o modal.
  const gerarSintese = async () => {
    if (!pasta) { window.alert("Pasta nao identificada."); return; }
    setGerando(true);
    try {
      const ehXlsx = formatoSintese === "xlsx";
      const endpoint = ehXlsx ? "/api/sintese-xlsx" : "/api/pdf-sintese";
      const periodo = periodoFiltro();
      const qs = new URLSearchParams({ pasta });
      if (periodo.dataIni) qs.set("dataIni", periodo.dataIni);
      if (periodo.dataFim) qs.set("dataFim", periodo.dataFim);
      const resp = await fetch(`${endpoint}?${qs.toString()}`, { credentials: "same-origin" });
      if (!resp.ok) {
        const erro = await resp.json().catch(() => ({}));
        throw new Error(erro.erro || `Erro ${resp.status}`);
      }
      const blob = await resp.blob();
      const nomeBase = (pasta || "obra").replace(/[^\w.\-]/g, "_");
      const nome = `sintese_${nomeBase}.${ehXlsx ? "xlsx" : "pdf"}`;

      if (ehXlsx) {
        const url = URL.createObjectURL(blob);
        const a = document.createElement("a");
        a.href = url; a.download = nome;
        document.body.appendChild(a);
        a.click();
        a.remove();
        URL.revokeObjectURL(url);
        onClose && onClose();
      } else {
        const url = URL.createObjectURL(blob);
        setPreview({ url, nome });
      }
    } catch (err) {
      window.alert(`Falha ao gerar Relatório de Síntese: ${err.message}`);
    } finally {
      setGerando(false);
    }
  };

  const onSubmit = () => {
    if (gerando) return;
    if (mostrarPicker && graficosSel.length < GRAFICOS_MIN) {
      window.alert(`Selecione no mínimo ${GRAFICOS_MIN} gráficos.`);
      return;
    }
    if (sel === "all-1") gerarPastaPdf("/api/pdf-pasta/start");
    else if (sel === "all-4") gerarPastaPdf("/api/pdf-4pp/start");
    else if (sel === "synthesis") gerarSintese();
    else if (sel === "single-1") gerarSinglePdfPreview();
    else onGerar && onGerar(sel, { graficos: graficosSel });
  };


  const totalObra = obra?.totalEstacas ?? 0;
  const obraNome = obra?.nome || "—";
  const pagsTodas4 = totalObra > 0 ? Math.ceil(totalObra / 4) : 0;
  const sinteseHint = formatoSintese === "xlsx"
    ? "XLSX · planilha Excel · todas as estacas"
    : "PDF · A4 paisagem · resumo de todas as estacas";
  const HINTS = {
    "single-1":  "PDF · A4 retrato · 1 página",
    "all-1":     `PDF · A4 retrato · ~${totalObra || "?"} páginas`,
    "all-4":     `PDF · A4 retrato · ~${pagsTodas4 || "?"} páginas`,
    "synthesis": sinteseHint,
  };

  const Opt = ({ value, title, badge, desc, synthesis = false }) => (
    <label
      className={`report-opt${synthesis ? " is-synthesis" : ""}${sel === value ? " is-selected" : ""}${gerando ? " is-disabled" : ""}`}
      data-value={value}
      onClick={(e) => { e.preventDefault(); if (!gerando) setSel(value); }}
    >
      <input type="radio" name="report" value={value} checked={sel === value} readOnly disabled={gerando} />
      <div className="report-thumb" dangerouslySetInnerHTML={{ __html: REL_THUMBS[value] }} />
      <div className="report-opt-text">
        <div className="report-opt-title">
          {title}
          {badge && <span className={`report-badge${synthesis ? " is-xls" : ""}`}>{badge}</span>}
        </div>
        <div className="report-opt-desc">{desc}</div>
      </div>
      <div className="report-radio"></div>
    </label>
  );

  return (
    <div className="report-backdrop" onClick={(e) => { if (e.target === e.currentTarget) onClose(); }}>
      <div className={`report-modal${preview ? " is-preview" : ""}`} role="dialog" aria-modal="true" aria-labelledby="reportTitle">
        <div className="report-head">
          <div className="report-eyebrow">{preview ? "PREVIEW" : "EXPORTAR"}</div>
          <h2 className="report-title" id="reportTitle">{preview ? "Pré-visualização" : "Gerar Relatório"}</h2>
          <p className="report-sub">
            {preview
              ? "Confira o layout antes de baixar. Volte se precisar ajustar a seleção."
              : "Escolha o tipo e o formato de exibição do relatório."}
          </p>
          <button className="report-close" aria-label="Fechar" onClick={onClose}>
            <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
              <path d="M18 6L6 18M6 6l12 12"/>
            </svg>
          </button>
        </div>

        {preview ? (
          <div className="report-body report-preview-body">
            <iframe
              src={preview.url}
              className="report-preview-frame"
              title="Pré-visualização do PDF"
            />
          </div>
        ) : gerando && progresso ? (
          <div className="report-body report-progress">
            <div className="report-progress-head">
              <div className="report-progress-title">Gerando relatório…</div>
              <div className="report-progress-count">
                {progresso.concluidos.length} de {progresso.total || "?"}
              </div>
            </div>
            <div className="report-progress-bar">
              <div
                className="report-progress-bar-fill"
                style={{ width: progresso.total ? `${(progresso.concluidos.length / progresso.total) * 100}%` : "0%" }}
              />
            </div>
            {progresso.atual && (
              <div className="report-progress-current">
                Processando: <strong>{labelEstaca(progresso.atual)}</strong>
              </div>
            )}
            <ul className="report-progress-list" ref={concluidosRef}>
              {progresso.concluidos.map((c, i) => (
                <li key={`${c.arquivo}-${i}`} className={c.ok ? "is-ok" : "is-fail"}>
                  <span className="report-progress-icon" aria-hidden="true">{c.ok ? "✓" : "✗"}</span>
                  <span className="report-progress-name">{labelEstaca(c.arquivo)}</span>
                  {!c.ok && c.motivo && <span className="report-progress-motivo">{c.motivo}</span>}
                </li>
              ))}
              {progresso.concluidos.length === 0 && (
                <li className="report-progress-empty">Aguardando primeira estaca…</li>
              )}
            </ul>
          </div>
        ) : (
        <div className="report-body">
          {!ocultarEstacaAtual && (
          <div className="report-group">
            <div className="report-group-label">
              Estaca atual
              <span className="report-pill">{nomeEstacaAtual || "—"}</span>
            </div>
            <div className="report-options is-full">
              <Opt value="single-1" title="1 Estaca por página" badge="A4" desc="Detalhamento completo da estaca selecionada." />
            </div>
          </div>
          )}

          <div className="report-group">
            <div className="report-group-label">
              Todas as estacas da obra
              <span className="report-pill is-info">
                {obra ? `${obraNome} · ${totalObra} ${totalObra === 1 ? "estaca" : "estacas"}` : "Obra não identificada"}
              </span>
            </div>
            <div className="report-options">
              <Opt value="all-1" title="1 Estaca por página" badge={`${totalObra || "?"} pág.`} desc="Lote completo, uma estaca por folha." />
              <Opt value="all-4" title="4 Estacas por página" badge={`~${pagsTodas4 || "?"} pág.`} desc="Lote completo, 4 estacas por folha." />
            </div>
            {(sel === "all-1" || sel === "all-4") && (
              <div
                className="report-formato"
                title={!temEditadas ? "Esta pasta não tem estacas editadas." : ""}
              >
                <span className="report-formato-label">Versão:</span>
                <div className="report-formato-toggle">
                  <button
                    type="button"
                    className={`report-formato-btn${versao === "originais" ? " is-on" : ""}`}
                    onClick={() => setVersao("originais")}
                    disabled={gerando}
                  >Originais</button>
                  <button
                    type="button"
                    className={`report-formato-btn${versao === "editadas" ? " is-on" : ""}`}
                    onClick={() => temEditadas && setVersao("editadas")}
                    disabled={gerando || !temEditadas}
                  >Editadas + Originais</button>
                </div>
              </div>
            )}
          </div>

          <div className="report-group">
            <div className="report-group-label">Resumo executivo</div>
            <div className="report-options is-full">
              <Opt value="synthesis" title="Relatório de Síntese" desc="Resumo consolidado dos dados de todas as estacas da obra." synthesis />
            </div>
            {sel === "synthesis" && (
              <div className="report-formato">
                <span className="report-formato-label">Formato:</span>
                <div className="report-formato-toggle">
                  <button
                    type="button"
                    className={`report-formato-btn${formatoSintese === "pdf" ? " is-on" : ""}`}
                    onClick={() => setFormatoSintese("pdf")}
                    disabled={gerando}
                  >PDF</button>
                  <button
                    type="button"
                    className={`report-formato-btn${formatoSintese === "xlsx" ? " is-on" : ""}`}
                    onClick={() => setFormatoSintese("xlsx")}
                    disabled={gerando}
                  >XLSX</button>
                </div>
              </div>
            )}
          </div>

          {(sel === "all-1" || sel === "all-4" || sel === "synthesis") && (
            <div className="report-group">
              <div className="report-group-label">
                Período
                <label className="report-periodo-toggle" title="Restringe ao intervalo de datas de início da perfuração">
                  <input
                    type="checkbox"
                    checked={usarPeriodo}
                    onChange={(e) => setUsarPeriodo(e.target.checked)}
                    disabled={gerando}
                  />
                  <span>Filtrar por data</span>
                </label>
              </div>
              {usarPeriodo && (
                <div className="report-periodo">
                  <div className="report-periodo-row">
                    <label className="report-periodo-field">
                      <span className="report-periodo-lbl">De</span>
                      <input
                        type="date"
                        value={dataIni}
                        min={periodoMinMax.dataIni || undefined}
                        max={dataFim || periodoMinMax.dataFim || undefined}
                        onChange={(e) => setDataIni(e.target.value)}
                        disabled={gerando}
                      />
                    </label>
                    <label className="report-periodo-field">
                      <span className="report-periodo-lbl">Até</span>
                      <input
                        type="date"
                        value={dataFim}
                        min={dataIni || periodoMinMax.dataIni || undefined}
                        max={periodoMinMax.dataFim || undefined}
                        onChange={(e) => setDataFim(e.target.value)}
                        disabled={gerando}
                      />
                    </label>
                    {(dataIni || dataFim) && (
                      <button
                        type="button"
                        className="report-periodo-clear"
                        onClick={() => { setDataIni(""); setDataFim(""); }}
                        disabled={gerando}
                        title="Limpar datas"
                      >Limpar</button>
                    )}
                  </div>
                  <div className="report-periodo-presets">
                    {[
                      ...(periodoMinMax.dataIni && periodoMinMax.dataFim
                        ? [{ id: "tudo", label: "Período completo" }]
                        : []),
                      { id: "hoje",   label: "Hoje" },
                      { id: "7d",     label: "Últimos 7 dias" },
                      { id: "30d",    label: "Últimos 30 dias" },
                      { id: "mes",    label: "Mês atual" },
                    ].map(p => (
                      <button
                        key={p.id}
                        type="button"
                        className="report-periodo-chip"
                        disabled={gerando}
                        onClick={() => {
                          const hoje = new Date();
                          const fmt = (d) => {
                            const y = d.getFullYear();
                            const m = String(d.getMonth() + 1).padStart(2, "0");
                            const dd = String(d.getDate()).padStart(2, "0");
                            return `${y}-${m}-${dd}`;
                          };
                          let ini, fim;
                          if (p.id === "tudo") {
                            ini = periodoMinMax.dataIni;
                            fim = periodoMinMax.dataFim;
                          } else if (p.id === "hoje") {
                            ini = fim = fmt(hoje);
                          } else if (p.id === "7d") {
                            const d = new Date(hoje); d.setDate(d.getDate() - 6);
                            ini = fmt(d); fim = fmt(hoje);
                          } else if (p.id === "30d") {
                            const d = new Date(hoje); d.setDate(d.getDate() - 29);
                            ini = fmt(d); fim = fmt(hoje);
                          } else {
                            const d = new Date(hoje.getFullYear(), hoje.getMonth(), 1);
                            ini = fmt(d); fim = fmt(hoje);
                          }
                          setDataIni(ini);
                          setDataFim(fim);
                        }}
                      >{p.label}</button>
                    ))}
                  </div>
                </div>
              )}
            </div>
          )}

          {mostrarPicker && (
            <div className="report-group">
              <div className="report-group-label">
                Gráficos no PDF
                <span className="report-pill">
                  {graficosSel.length}/{limiteGraficos(graficosSel, slotsAtuais)}
                </span>
              </div>
              <div className="report-graficos-grid">
                {GRAFICOS_OPCOES.map(g => {
                  const checked = graficosSel.includes(g.id);
                  // Slot insuficiente pra adicionar este grafico (perfil = 2 slots).
                  const limite = !checked &&
                    slotsUsados(graficosSel) + pesoGrafico(g.id) > slotsAtuais;
                  // Bloqueia desmarcar se cair abaixo do minimo do PDF.
                  const minimo = checked && graficosSel.length <= GRAFICOS_MIN;
                  const tituloLimite = g.id === "perfil"
                    ? "Sem espaço — desmarque 2 gráficos para incluir o Perfil"
                    : "Sem espaço — desmarque um gráfico ou remova o Perfil";
                  const tituloMinimo = `Mínimo de ${GRAFICOS_MIN} gráficos no PDF`;
                  return (
                    <label
                      key={g.id}
                      className={`report-grafico-opt${checked ? " is-on" : ""}${limite || minimo ? " is-disabled" : ""}`}
                      title={limite ? tituloLimite : (minimo ? tituloMinimo : g.desc)}
                    >
                      <input
                        type="checkbox"
                        checked={checked}
                        disabled={limite || minimo || gerando}
                        onChange={() => toggleGrafico(g.id)}
                      />
                      <span className="report-grafico-label">{g.label}</span>
                      <span className="report-grafico-desc">{g.desc}</span>
                    </label>
                  );
                })}
              </div>

              {graficosSel.length > 1 && (
                <div className="report-ordem">
                  <div className="report-ordem-titulo">
                    Ordem no PDF
                    <span className="report-ordem-hint">arraste para reordenar</span>
                  </div>
                  <div
                    className="report-ordem-strip"
                    onDragLeave={onChipDragLeave}
                  >
                    {graficosSel.map((id, idx) => {
                      const opt = GRAFICOS_OPCOES.find(o => o.id === id);
                      return (
                        <div
                          key={id}
                          className={`report-ordem-chip${dragOverIdx === idx ? " is-drop-target" : ""}`}
                          draggable={!gerando}
                          onDragStart={onChipDragStart(idx)}
                          onDragOver={onChipDragOver(idx)}
                          onDrop={onChipDrop(idx)}
                          onDragEnd={onChipDragEnd}
                          title={opt?.desc || id}
                        >
                          <span className="report-ordem-num">{idx + 1}</span>
                          <span className="report-ordem-handle" aria-hidden="true">⋮⋮</span>
                          <span className="report-ordem-label">{opt?.label || id}</span>
                        </div>
                      );
                    })}
                  </div>
                </div>
              )}
            </div>
          )}
        </div>
        )}

        <div className="report-foot">
          <div className="report-foot-info">
            <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2">
              <circle cx="12" cy="12" r="9"/><path d="M12 8v5M12 16h.01"/>
            </svg>
            <span>
              {preview
                ? preview.nome
                : gerando && progresso
                ? `${progresso.concluidos.length} de ${progresso.total || "?"} estacas processadas`
                : HINTS[sel]}
            </span>
          </div>
          <div className="report-foot-actions">
            {preview ? (
              <>
                <button className="btn btn-ghost" onClick={voltarDoPreview} disabled={gerando}>← Voltar</button>
                <button className="btn btn-primary" onClick={baixarPreview} disabled={gerando}>
                  <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
                    <path d="M12 3v12M7 10l5 5 5-5M5 21h14"/>
                  </svg>
                  Baixar PDF
                </button>
              </>
            ) : (
            <>
            <button className="btn btn-ghost" onClick={onClose} disabled={gerando}>Cancelar</button>
            <button className="btn btn-primary" onClick={onSubmit} disabled={gerando}>
              {gerando ? (
                <>
                  <svg className="report-spinner" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
                    <path d="M21 12a9 9 0 1 1-6.2-8.55"/>
                  </svg>
                  Gerando…
                </>
              ) : sel === "synthesis" && formatoSintese === "xlsx" ? (
                /* XLSX baixa direto (sem preview), entao o CTA muda. */
                <>
                  <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
                    <path d="M12 3v12M7 10l5 5 5-5M5 21h14"/>
                  </svg>
                  Baixar
                </>
              ) : (
                <>
                  <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
                    <path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7-11-7-11-7z"/>
                    <circle cx="12" cy="12" r="3"/>
                  </svg>
                  Visualizar
                </>
              )}
            </button>
            </>
            )}
          </div>
        </div>
      </div>
    </div>
  );
}

/* ---------- Painel da pasta (planilha de estacas) ---------- */
function PastaPlanilha({ accent, pasta, eyebrow = "PLANILHA DE ESTACAS", linhas, carregando, onAbrirEstaca, onVoltar, pastaRelatorio = null, temEditadas = false }) {
  const [modalRelOpen, setModalRelOpen] = React.useState(false);
  // Info de obra pra alimentar o RelatorioModal (nome + total de estacas).
  // Tira do primeiro cabecalho disponivel; total vem do tamanho da lista.
  const obraInfo = React.useMemo(() => {
    const arr = Array.isArray(linhas) ? linhas : [];
    const primeira = arr.find(l => l && l.cabecalho)?.cabecalho;
    return { nome: (primeira && primeira.obra) || pastaRelatorio || pasta, totalEstacas: arr.length };
  }, [linhas, pasta, pastaRelatorio]);
  const z = (n) => String(n ?? 0).padStart(2, "0");
  const dataStr = (d, m, a) => (d || m || a) ? `${z(d)}/${z(m)}/${a}` : "";
  const horaStr = (h, m) => (h !== undefined && h !== null) ? `${z(h)}:${z(m)}:00` : "";
  const durStr  = (m, s) => `00:${z(m)}:${z(s)}`;

  const cols = [
    "Estaca","Contrato","Obra","Data início perf.","Hora início perf.",
    "Data fim perf.","Hora fim perf.","Tempo perf.","Diâmetro trado","Passo",
    "Inclinação X","Inclinação Y","Profundidade","Volume","Superconsumo"
  ];

  return (
    <div className="dash-page">
      <div className="dash-pagehead">
        <div>
          <div className="dash-eyebrow">{eyebrow}</div>
          <h1 className="dash-h1">{pasta}</h1>
          <div className="dash-bread">
            {Array.isArray(linhas) ? `${linhas.length} estaca${linhas.length === 1 ? "" : "s"} carregada${linhas.length === 1 ? "" : "s"}` : "Carregando…"}
          </div>
        </div>
        <div className="dash-actions">
          <button className="dash-btn-ghost" onClick={onVoltar}>← Voltar ao Dashboard</button>
          {pastaRelatorio && (
            <button
              className="dash-btn-primary"
              style={{ background: "var(--navy)" }}
              onClick={() => setModalRelOpen(true)}
              disabled={carregando || !Array.isArray(linhas) || linhas.length === 0}
            >
              ⬇ Relatório PDF / XLSX
            </button>
          )}
        </div>
      </div>

      {pastaRelatorio && (
        <RelatorioModal
          open={modalRelOpen}
          onClose={() => setModalRelOpen(false)}
          ocultarEstacaAtual={true}
          obra={obraInfo}
          pasta={pastaRelatorio}
          temEditadas={temEditadas}
        />
      )}

      <Card title={`Estacas em ${pasta}`} right={<span className="dash-bread" style={{ margin: 0 }}>Clique em uma linha para abrir o gráfico</span>}>
        <div className="pasta-planilha-scroll">
          <table className="pasta-planilha-tabela">
            <thead>
              <tr>{cols.map(c => <th key={c}>{c}</th>)}</tr>
            </thead>
            <tbody>
              {carregando && (
                <tr><td colSpan={cols.length} className="pasta-planilha-empty">Carregando…</td></tr>
              )}
              {!carregando && Array.isArray(linhas) && linhas.length === 0 && (
                <tr><td colSpan={cols.length} className="pasta-planilha-empty">Nenhuma estaca nesta pasta.</td></tr>
              )}
              {!carregando && Array.isArray(linhas) && linhas.map(({ arquivo, cabecalho: c, erro }) => {
                if (erro || !c) {
                  return (
                    <tr key={arquivo} className="linha-erro">
                      <td data-label="Estaca">{arquivo}</td>
                      <td colSpan={cols.length - 1}>Falha ao ler arquivo</td>
                    </tr>
                  );
                }
                const cels = [
                  c.nomeEstaca || arquivo,
                  c.contrato || "",
                  c.obra || "",
                  dataStr(c.dIniPerf, c.mesIniPerf, c.aIniPerf),
                  horaStr(c.hIniPerf, c.mIniPerf),
                  dataStr(c.dFimPerf, c.mesFimPerf, c.aFimPerf),
                  horaStr(c.hFimPerf, c.mFimPerf),
                  durStr(c.tPerfM, c.tPerfS),
                  `${c.diametroTrado} mm`,
                  `${c.passo} cm`,
                  `${Number(c.incX).toFixed(2)}º`,
                  `${Number(c.incY).toFixed(2)}º`,
                  `${c.profTotal / 100} m`,
                  `${c.volTotal} L`,
                  `${c.superconsumo}%`,
                ];
                return (
                  <tr key={arquivo} onClick={() => onAbrirEstaca(arquivo)}>
                    {cels.map((v, i) => (
                      <td
                        key={i}
                        data-label={cols[i]}
                        className={i === 0 ? "pp-id" : ""}
                      >
                        {v}
                      </td>
                    ))}
                  </tr>
                );
              })}
            </tbody>
          </table>
        </div>
      </Card>
    </div>
  );
}

/* ---------- Dashboard sub-components ---------- */
function Card({ title, right, children, className="" }) {
  return (
    <section className={`d-card ${className}`}>
      <header className="d-card-head">
        <h3>{title}</h3>
        <div>{right}</div>
      </header>
      <div className="d-card-body">{children}</div>
    </section>
  );
}

// Botao "i" com tooltip on hover/focus. Compartilha o estilo do .kpi-info,
// mas com classe propria pra posicionamento dentro do header de um Card.
function InfoTip({ text }) {
  return (
    <div className="kpi-info" tabIndex={0} aria-label={text}>
      <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
        <circle cx="12" cy="12" r="10"/>
        <line x1="12" y1="16" x2="12" y2="12"/>
        <line x1="12" y1="8" x2="12.01" y2="8"/>
      </svg>
      <div className="kpi-info-tip" role="tooltip">{text}</div>
    </div>
  );
}

function DropdownPill({ label }) {
  return <button className="d-pill">{label} <I.down width="14" height="14"/></button>;
}

// Botao + popover unico que agrupa todos os filtros do card de Producao
// (metrica, granularidade, periodo, maquina, diametro). Substitui a fileira
// de selects no header pra evitar sobrecarga visual conforme novos filtros
// sao adicionados. Trigger mostra um badge com a contagem de filtros ativos
// (maquina != "" e/ou diametro != "" — os outros sao sempre escolhidos).
function FiltrosProducao({
  metrica, setMetrica,
  granularidade, setGranularidade,
  periodo, setPeriodo,
  maquina, setMaquina, maquinas,
  diametro, setDiametro, diametros,
  tipoGrafico, setTipoGrafico,
  ocultarSemProducao, setOcultarSemProducao,
}) {
  const [aberto, setAberto] = useState(false);
  const rootRef = useRef(null);

  useEffect(() => {
    if (!aberto) return;
    const onDoc = (e) => {
      if (!rootRef.current) return;
      if (!rootRef.current.contains(e.target)) setAberto(false);
    };
    const onKey = (e) => { if (e.key === "Escape") setAberto(false); };
    document.addEventListener("mousedown", onDoc);
    document.addEventListener("keydown", onKey);
    return () => {
      document.removeEventListener("mousedown", onDoc);
      document.removeEventListener("keydown", onKey);
    };
  }, [aberto]);

  const ativos = (maquina ? 1 : 0) + (diametro ? 1 : 0);
  const limpar = () => { setMaquina(""); setDiametro(""); };
  const trocarGranularidade = (novo) => {
    setGranularidade(novo);
    setPeriodo(novo === "diaria" ? 30 : 6);
  };

  const opcoesPeriodo = granularidade === "diaria"
    ? [{v:7,l:"7 dias"},{v:14,l:"14 dias"},{v:30,l:"30 dias"},{v:60,l:"60 dias"},{v:90,l:"90 dias"}]
    : [{v:3,l:"3 meses"},{v:6,l:"6 meses"},{v:9,l:"9 meses"},{v:12,l:"12 meses"},{v:24,l:"24 meses"},{v:36,l:"36 meses"},{v:48,l:"48 meses"}];

  return (
    <div className="prod-filtros-wrap" ref={rootRef}>
      <button
        type="button"
        className={"prod-filtros-trigger" + (aberto ? " is-open" : "")}
        onClick={() => setAberto(v => !v)}
        aria-expanded={aberto}
        aria-haspopup="dialog"
      >
        <I.funnel width="14" height="14"/>
        <span>Filtros</span>
        {ativos > 0 && <span className="prod-filtros-badge">{ativos}</span>}
      </button>
      {aberto && (
        <div className="prod-filtros-pop" role="dialog" aria-label="Filtros do gráfico de produção">
          <div className="prod-filtros-grupo">
            <div className="prod-filtros-rotulo">Métrica</div>
            <div className="prod-filtros-seg">
              {[
                { v: "estacas",  l: "Estacas" },
                { v: "metros",   l: "Metros" },
                { v: "concreto", l: "Concreto" },
              ].map(o => (
                <button
                  key={o.v}
                  type="button"
                  className={"prod-filtros-seg-btn" + (metrica === o.v ? " is-active" : "")}
                  onClick={() => setMetrica(o.v)}
                >{o.l}</button>
              ))}
            </div>
          </div>

          <div className="prod-filtros-grupo">
            <div className="prod-filtros-rotulo">Granularidade</div>
            <div className="prod-filtros-seg">
              {[
                { v: "mensal", l: "Mensal" },
                { v: "diaria", l: "Diária" },
              ].map(o => (
                <button
                  key={o.v}
                  type="button"
                  className={"prod-filtros-seg-btn" + (granularidade === o.v ? " is-active" : "")}
                  onClick={() => trocarGranularidade(o.v)}
                >{o.l}</button>
              ))}
            </div>
          </div>

          <div className="prod-filtros-grupo">
            <div className="prod-filtros-rotulo">Tipo de gráfico</div>
            <div className="prod-filtros-seg">
              {[
                { v: "linha",  l: "Linha" },
                { v: "barras", l: "Barras" },
              ].map(o => (
                <button
                  key={o.v}
                  type="button"
                  className={"prod-filtros-seg-btn" + (tipoGrafico === o.v ? " is-active" : "")}
                  onClick={() => setTipoGrafico(o.v)}
                >{o.l}</button>
              ))}
            </div>
          </div>

          <div className="prod-filtros-grupo">
            <div className="prod-filtros-rotulo">Período</div>
            <select
              className="d-select prod-filtros-select"
              value={periodo}
              onChange={e => setPeriodo(Number(e.target.value))}
            >
              {opcoesPeriodo.map(o => (
                <option key={o.v} value={o.v}>Últimos {o.l}</option>
              ))}
            </select>
          </div>

          <div className="prod-filtros-grupo">
            <div className="prod-filtros-rotulo">Máquina</div>
            <select
              className="d-select prod-filtros-select"
              value={maquina}
              onChange={e => setMaquina(e.target.value)}
            >
              <option value="">Todas as máquinas</option>
              {maquinas.map(m => (
                <option key={m.numSerie} value={m.numSerie}>
                  {m.tag ? `${m.tag} / ${m.numSerie}` : `${m.numSerie}`}
                </option>
              ))}
            </select>
          </div>

          <div className="prod-filtros-grupo">
            <div className="prod-filtros-rotulo">Diâmetro do trado</div>
            <select
              className="d-select prod-filtros-select"
              value={diametro}
              onChange={e => setDiametro(e.target.value)}
            >
              <option value="">Todos os diâmetros</option>
              {diametros.map(d => (
                <option key={d.diametro} value={d.diametro}>
                  {`${d.diametro} mm`}
                </option>
              ))}
            </select>
          </div>

          <label className="prod-filtros-switch">
            <span>
              <span className="prod-filtros-switch-label">Ocultar períodos sem produção</span>
              <span className="prod-filtros-switch-desc">Esconde do gráfico os meses ou dias com zero estacas.</span>
            </span>
            <span className="prod-filtros-toggle">
              <input
                type="checkbox"
                checked={!!ocultarSemProducao}
                onChange={(e) => setOcultarSemProducao(e.target.checked)}
              />
              <span className="prod-filtros-toggle-track"/>
            </span>
          </label>

          <div className="prod-filtros-rodape">
            {ativos > 0 ? (
              <button type="button" className="prod-filtros-limpar" onClick={limpar}>
                Limpar filtros
              </button>
            ) : <span/>}
            <button type="button" className="prod-filtros-ok" onClick={() => setAberto(false)}>
              OK
            </button>
          </div>
        </div>
      )}
    </div>
  );
}

function BuscaGlobal({ cliente, onAbrirEstaca, onAbrirContrato, onAbrirMaquina, onAbrirFiltroProf }) {
  const [q, setQ] = useState("");
  const [resultados, setResultados] = useState({ estacas: [], contratos: [], maquinas: [] });
  const [aberto, setAberto] = useState(false);
  const [idx, setIdx] = useState(0);
  const inputRef = React.useRef(null);
  const containerRef = React.useRef(null);

  // ⌘K / Ctrl+K foca o input. Esc fecha o popover.
  useEffect(() => {
    const onKey = (e) => {
      const ehAtalho = (e.metaKey || e.ctrlKey) && (e.key === "k" || e.key === "K");
      if (ehAtalho) {
        e.preventDefault();
        inputRef.current?.focus();
        inputRef.current?.select?.();
      }
      if (e.key === "Escape") setAberto(false);
    };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, []);

  // Click fora fecha.
  useEffect(() => {
    const onDocClick = (e) => {
      if (containerRef.current && !containerRef.current.contains(e.target)) setAberto(false);
    };
    window.addEventListener("mousedown", onDocClick);
    return () => window.removeEventListener("mousedown", onDocClick);
  }, []);

  // Debounce de 250ms entre digitação e fetch.
  useEffect(() => {
    const termo = q.trim();
    if (!termo) {
      setResultados({ estacas: [], contratos: [], maquinas: [] });
      return;
    }
    let cancelado = false;
    const t = setTimeout(() => {
      fetch(`/api/busca?q=${encodeURIComponent(termo)}`, { credentials: "same-origin" })
        .then(r => r.json())
        .then(data => {
          if (cancelado || !data || data.erro) return;
          setResultados(data);
          setIdx(0);
        })
        .catch(() => {});
    }, 250);
    return () => { cancelado = true; clearTimeout(t); };
  }, [q]);

  // Lista plana ordenada (estacas → contratos → máquinas) para navegação por teclado.
  const plana = useMemo(() => [
    ...(resultados.estacas   || []).map(r => ({ ...r, grupo: "Estacas"   })),
    ...(resultados.contratos || []).map(r => ({ ...r, grupo: "Contratos" })),
    ...(resultados.maquinas  || []).map(r => ({ ...r, grupo: "Máquinas"  })),
  ], [resultados]);

  const escolher = (item) => {
    if (!item) return;
    setAberto(false);
    setQ("");
    if (item.tipo === "estaca")    onAbrirEstaca(item.payload?.contrato, item.payload?.arquivo);
    else if (item.tipo === "contrato")    onAbrirContrato(item.payload?.contrato);
    else if (item.tipo === "maquina")     onAbrirMaquina(item.payload?.tag);
    else if (item.tipo === "filtro-prof") onAbrirFiltroProf && onAbrirFiltroProf(item.payload);
  };

  const onKeyDown = (e) => {
    if (e.key === "ArrowDown") { e.preventDefault(); setAberto(true); setIdx(i => Math.min(i + 1, Math.max(plana.length - 1, 0))); }
    else if (e.key === "ArrowUp") { e.preventDefault(); setIdx(i => Math.max(i - 1, 0)); }
    else if (e.key === "Enter")   { e.preventDefault(); escolher(plana[idx]); }
  };

  const grupos = ["Estacas", "Contratos", "Máquinas"];
  const termoTrim = q.trim();

  return (
    <div className="dash-search" ref={containerRef}>
      <I.search width="16" height="16"/>
      <input
        ref={inputRef}
        placeholder="Buscar estacas, obras, máquinas"
        value={q}
        onChange={e => { setQ(e.target.value); setAberto(true); setIdx(0); }}
        onFocus={() => { if (q.trim()) setAberto(true); }}
        onKeyDown={onKeyDown}
      />
      {aberto && termoTrim && (
        <div className="dash-search-popover" role="listbox">
          {plana.length === 0 ? (
            <div className="dash-search-vazio">Nenhum resultado para "{termoTrim}"</div>
          ) : grupos.map(grupo => {
            const itens = plana.filter(p => p.grupo === grupo);
            if (itens.length === 0) return null;
            return (
              <div key={grupo} className="dash-search-grupo">
                <div className="dash-search-grupo-titulo">{grupo}</div>
                {itens.map(item => {
                  const i = plana.indexOf(item);
                  return (
                    <button
                      type="button"
                      key={`${item.tipo}-${i}`}
                      className={`dash-search-item ${i === idx ? "is-active" : ""}`}
                      onMouseDown={(e) => { e.preventDefault(); escolher(item); }}
                      onMouseEnter={() => setIdx(i)}
                    >
                      <div className="dash-search-titulo">{item.titulo}</div>
                      {item.subtitulo && <div className="dash-search-subtitulo">{item.subtitulo}</div>}
                    </button>
                  );
                })}
              </div>
            );
          })}
        </div>
      )}
    </div>
  );
}

// Escurece um hex (#rrggbb) multiplicando cada canal por (1 - fator).
function escurecerHex(hex, fator = 0.35) {
  if (typeof hex !== "string" || !/^#?[0-9a-f]{6}$/i.test(hex)) return hex;
  const m = hex.replace("#", "");
  const ch = (i) => Math.max(0, Math.min(255, Math.round(parseInt(m.slice(i, i + 2), 16) * (1 - fator))));
  const px = (n) => n.toString(16).padStart(2, "0");
  return `#${px(ch(0))}${px(ch(2))}${px(ch(4))}`;
}

function KPI({ label, value, delta, unit, sparkline, media, accent, info, onClick }) {
  const positive = delta >= 0;
  // KPIs com `onClick` viram um "card-botao" — abre uma tela ao clicar
  // (ex.: "Maquinas online" → FrotaPage). O resto do KPI continua igual.
  const clicavel = typeof onClick === "function";
  const [hover, setHover] = useState(null); // { idx, mx, my } ou null
  // Sparkline pode chegar vazio ou com 1 ponto (dias zerados sao filtrados
  // no backend). Tratamos esses casos pra nao gerar NaN no path SVG.
  const pontos = Array.isArray(sparkline) ? sparkline.filter(v => Number.isFinite(v)) : [];
  // Os limites do grafico consideram a media junto com os pontos — assim a
  // linha da media nao sai do enquadramento quando hoje for um pico/vale.
  const mediaValida = Number.isFinite(media) && media > 0;
  const limites = mediaValida ? [...pontos, media] : pontos;
  const max = limites.length ? Math.max(...limites) : 0;
  const min = limites.length ? Math.min(...limites) : 0;
  const xAt = i => pontos.length <= 1 ? 60 : (i / (pontos.length - 1)) * 120;
  const yAt = v => 36 - ((v - min) / (max - min || 1)) * 30;
  const path = pontos.length === 0
    ? "M0 20 L120 20"  // placeholder valido — sem isso o <path> com fill abaixo gera SVG invalido
    : pontos.length === 1
      ? `M0 20 L120 20`  // linha reta no meio quando so tem 1 ponto
      : pontos.map((v, i) => `${i === 0 ? "M" : "L"}${xAt(i).toFixed(1)} ${yAt(v).toFixed(1)}`).join(" ");
  const yMedia = mediaValida ? yAt(media) : null;
  // ID do gradient precisa ser url(#...) valido — `label` tem espacos/acento,
  // entao normalizamos pra alfanumerico. Sem isso o fill caia pro preto default.
  const gradId = `kpi-grad-${label.replace(/[^a-zA-Z0-9]/g, "_")}`;
  const accentEscuro = escurecerHex(accent);

  const onMouseMoveSpark = (e) => {
    if (pontos.length === 0) return;
    const svg = e.currentTarget;
    const rect = svg.getBoundingClientRect();
    if (rect.width === 0) return;
    const mxView = ((e.clientX - rect.left) / rect.width) * 120;
    let idx = 0;
    let melhor = Infinity;
    for (let i = 0; i < pontos.length; i++) {
      const d = Math.abs(xAt(i) - mxView);
      if (d < melhor) { melhor = d; idx = i; }
    }
    setHover({ idx, mx: e.clientX - rect.left, my: e.clientY - rect.top });
  };
  return (
    <div
      className={`kpi ${clicavel ? "kpi-clicavel" : ""}`}
      {...(clicavel ? {
        role: "button",
        tabIndex: 0,
        onClick,
        onKeyDown: (e) => {
          if (e.key === "Enter" || e.key === " ") { e.preventDefault(); onClick(); }
        },
      } : {})}
    >
      {clicavel && (
        <span className="kpi-seta" aria-hidden="true">
          <I.arrow width="16" height="16"/>
        </span>
      )}
      {info && (
        <div className="kpi-info" tabIndex={0} aria-label={info}>
          <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
            <circle cx="12" cy="12" r="10"/>
            <line x1="12" y1="16" x2="12" y2="12"/>
            <line x1="12" y1="8" x2="12.01" y2="8"/>
          </svg>
          <div className="kpi-info-tip" role="tooltip">{info}</div>
        </div>
      )}
      <div className="kpi-label">{label}</div>
      <div className="kpi-row">
        <div>
          <div className="kpi-value">{value}</div>
          <div className={`kpi-delta ${positive ? "up" : "down"}`}>
            {positive ? <I.up width="12" height="12"/> : <I.down width="12" height="12"/>}
            {Math.abs(delta).toFixed(1)}%
            <span className="kpi-unit">{unit}</span>
          </div>
        </div>
        <div className="kpi-spark-wrap" style={{ position: "relative" }}>
          <svg
            viewBox="0 0 120 40"
            className="kpi-spark"
            width="120"
            height="40"
            preserveAspectRatio="none"
            onMouseMove={onMouseMoveSpark}
            onMouseLeave={() => setHover(null)}
          >
            <defs>
              <linearGradient id={gradId} x1="0" x2="0" y1="0" y2="1">
                <stop offset="0" stopColor={accentEscuro} stopOpacity=".55"/>
                <stop offset="1" stopColor={accentEscuro} stopOpacity="0"/>
              </linearGradient>
            </defs>
            <path d={`${path} L120 40 L0 40 Z`} fill={`url(#${gradId})`}/>
            <path d={path} fill="none" stroke={accentEscuro} strokeWidth="1.8"/>
            {yMedia !== null && (
              <line
                x1="0" x2="120"
                y1={yMedia.toFixed(1)} y2={yMedia.toFixed(1)}
                style={{ stroke: 'var(--ink)' }}
                strokeOpacity="0.35"
                strokeWidth="1"
                strokeDasharray="3 3"
              />
            )}
            {hover && pontos.length > 0 && (
              <g pointerEvents="none">
                <line
                  x1={xAt(hover.idx)} x2={xAt(hover.idx)}
                  y1={2} y2={38}
                  style={{ stroke: 'var(--ink)' }} strokeOpacity="0.20" strokeDasharray="2 2"
                />
                <circle
                  cx={xAt(hover.idx)} cy={yAt(pontos[hover.idx])}
                  r="2.6" fill={accentEscuro} stroke="#fff" strokeWidth="1.4"
                />
              </g>
            )}
          </svg>
          {hover && pontos.length > 0 && (
            <div
              className="kpi-tooltip"
              style={{
                left: Math.min(Math.max(hover.mx - 30, -10), 80),
                top: -36,
              }}
            >
              <strong>{fmtNumPt(pontos[hover.idx])}</strong>
              <span className="kpi-tooltip-unit">{unit}</span>
            </div>
          )}
        </div>
      </div>
    </div>
  );
}

const MESES_ABREV = ["Jan","Fev","Mar","Abr","Mai","Jun","Jul","Ago","Set","Out","Nov","Dez"];
const METRICA_UNIT = { estacas: "estacas", metros: "m", concreto: "m³" };

function rotuloMes(mesIso, comAno = false) {
  const [a, m] = String(mesIso).split("-");
  const idx = (Number(m) || 1) - 1;
  const nome = MESES_ABREV[idx] || mesIso;
  return comAno ? `${nome}/${String(a).slice(-2)}` : nome;
}

// "2026-05-23" → "23/05" (curto) ou "23/05/26" (com ano).
function rotuloDia(diaIso, comAno = false) {
  const [a, m, d] = String(diaIso).split("-");
  const dd = (d || "").padStart(2, "0");
  const mm = (m || "").padStart(2, "0");
  return comAno ? `${dd}/${mm}/${String(a || "").slice(-2)}` : `${dd}/${mm}`;
}

// Escolhe um teto "redondo" para o eixo Y a partir do valor máximo da série,
// para garantir 4 linhas de grade espaçadas e legíveis.
function tetoEixoY(maxValor) {
  if (maxValor <= 0) return 4;
  const exp = Math.pow(10, Math.floor(Math.log10(maxValor)));
  const candidatos = [1, 1.25, 1.5, 2, 2.5, 3, 4, 5, 7.5, 10];
  for (const c of candidatos) {
    const teto = c * exp;
    if (teto >= maxValor) return teto;
  }
  return 10 * exp;
}

const fmtNumPt = n => Math.round(Number(n) || 0).toLocaleString("pt-BR");

function ProductionChart({ accent, dados, metrica, granularidade = "mensal", tipoGrafico = "linha", ocultarSemProducao = false }) {
  // SVG mede o container e cresce em largura — antes era fixo em 720 e
  // virava letterbox quando o card do dashboard ficava largo, deixando um
  // sobre-padding visivel nas laterais.
  const wrapRef = useRef(null);
  const [W, setW] = useState(720);
  const H = 240, pad = { l: 40, r: 12, t: 12, b: 28 };
  useEffect(() => {
    const el = wrapRef.current;
    if (!el || typeof ResizeObserver === "undefined") return;
    const atualizar = () => {
      const w = el.clientWidth;
      if (w > 0) setW(Math.max(360, Math.round(w)));
    };
    atualizar();
    const ro = new ResizeObserver(atualizar);
    ro.observe(el);
    return () => ro.disconnect();
  }, []);
  const unit = METRICA_UNIT[metrica] || "";
  const ehDiaria = granularidade === "diaria";
  const chaveTempo = ehDiaria ? "dia" : "mes";
  const [hover, setHover] = useState(null); // { idx, mx, my } ou null

  // Filtra dias/meses sem producao (valor 0) quando configurado.
  // Faz isso ANTES de validar se array esta vazio pra mostrar mensagem
  // adequada quando filtro elimina tudo.
  const dadosBase = Array.isArray(dados) ? dados : [];
  const dadosVis = ocultarSemProducao
    ? dadosBase.filter(d => Number(d[metrica]) > 0)
    : dadosBase;

  if (!dadosVis.length) {
    return (
      <div className="prod-chart-wrap" ref={wrapRef}>
        <svg viewBox={`0 0 ${W} ${H}`} className="prod-chart">
          <text x={W/2} y={H/2} textAnchor="middle" fontSize="12" fill="#8A93A6">
            {dadosBase.length === 0 ? "Sem dados para o período selecionado." : "Nenhum período com produção no recorte selecionado."}
          </text>
        </svg>
      </div>
    );
  }

  const dadosUsados = dadosVis;
  const valores = dadosUsados.map(d => Number(d[metrica]) || 0);
  // Media ignora dias zerados (fim de semana, parada, etc) — mesma logica
  // dos KPIs do topo do dashboard. Calculada sobre o conjunto visivel:
  // se "ocultarSemProducao" estiver ligado, dadosVis ja nao tem zeros, mas
  // o filter mantem o comportamento correto nos dois casos.
  const valoresNaoZero = valores.filter(v => v > 0);
  const media = valoresNaoZero.length > 0
    ? valoresNaoZero.reduce((a, b) => a + b, 0) / valoresNaoZero.length
    : 0;
  const teto = tetoEixoY(Math.max(...valores, 0));
  const linhasY = [0, teto * 0.25, teto * 0.5, teto * 0.75, teto];

  const ehBarras = tipoGrafico === 'barras';
  // Em barras: o eixo X mapeia o CENTRO de cada barra. Largura util e'
  // a mesma da linha. Em linha: i mapeia diretamente posicao 0..n-1.
  const x = i => pad.l + (dadosUsados.length === 1 ? (W - pad.l - pad.r) / 2 : (i / (dadosUsados.length - 1)) * (W - pad.l - pad.r));
  const y = v => pad.t + (1 - v / teto) * (H - pad.t - pad.b);

  // Largura/posicao das barras quando tipoGrafico === 'barras'.
  const barLarguraTotal = (W - pad.l - pad.r) / Math.max(1, dadosUsados.length);
  const barLargura = Math.max(2, barLarguraTotal * 0.65);
  const barX = i => pad.l + barLarguraTotal * i + (barLarguraTotal - barLargura) / 2;

  const linha = valores.map((v, i) => `${i ? "L" : "M"}${x(i)} ${y(v)}`).join(" ");
  const area = `${linha} L${x(valores.length - 1)} ${y(0)} L${x(0)} ${y(0)} Z`;

  // Detecta viewport estreito (mobile). Como W e' clampado a min 360,
  // diferenciamos por largura real do container via wrapRef.
  const larguraReal = wrapRef.current ? wrapRef.current.clientWidth : W;
  const ehMobile = larguraReal > 0 && larguraReal < 480;

  // Step de labels do eixo X — adapta a quantidade de pontos pra nao ficar
  // poluido. Para diaria a densidade e' maior, entao paasos maiores.
  // Em mobile multiplicamos o passo pra deixar ~5 labels visiveis.
  const passoBase = ehDiaria
    ? (dadosUsados.length <= 7 ? 1
       : dadosUsados.length <= 14 ? 2
       : dadosUsados.length <= 30 ? 5
       : dadosUsados.length <= 60 ? 7
       : 10)
    : (dadosUsados.length <= 12 ? 1
       : dadosUsados.length <= 24 ? 2
       : dadosUsados.length <= 36 ? 3
       : 4);
  const passo = ehMobile
    ? Math.max(passoBase, Math.ceil(dadosUsados.length / 5))
    : passoBase;
  const formatarRotulo = (d, comAno) => ehDiaria ? rotuloDia(d[chaveTempo], comAno) : rotuloMes(d[chaveTempo], comAno);
  // Em mobile, ano sempre omitido (espaco premium).
  const comAno = ehMobile ? false
    : (ehDiaria ? dadosUsados.length > 60 : dadosUsados.length > 12);

  const onMouseMove = (e) => {
    const svg = e.currentTarget;
    const rect = svg.getBoundingClientRect();
    if (rect.width === 0) return;
    // Converte coords do mouse (tela) pra userspace do SVG via CTM inverso.
    // Mapeamento ingenuo (mouseX/rectWidth * W) erra quando o aspect ratio
    // do container nao bate com o do viewBox (720x240): preserveAspectRatio
    // "meet" centraliza com letterbox lateral, deslocando o ponto exato em
    // que viewBox X=0 cai dentro do rect — daí a hover-bolinha "fora" da
    // bolinha real do dado.
    const ctm = svg.getScreenCTM();
    let mxView;
    if (ctm) {
      const pt = svg.createSVGPoint();
      pt.x = e.clientX; pt.y = e.clientY;
      mxView = pt.matrixTransform(ctm.inverse()).x;
    } else {
      mxView = ((e.clientX - rect.left) / rect.width) * W;
    }
    let idx = 0;
    let melhor = Infinity;
    for (let i = 0; i < dadosUsados.length; i++) {
      const refX = ehBarras ? (barX(i) + barLargura / 2) : x(i);
      const d = Math.abs(refX - mxView);
      if (d < melhor) { melhor = d; idx = i; }
    }
    setHover({ idx, mx: e.clientX - rect.left, my: e.clientY - rect.top });
  };

  return (
    <div className="prod-chart-wrap" style={{ position: 'relative' }} ref={wrapRef}>
      <svg
        viewBox={`0 0 ${W} ${H}`}
        className="prod-chart"
        onMouseMove={onMouseMove}
        onMouseLeave={() => setHover(null)}
      >
        <defs>
          <linearGradient id="gExec" x1="0" x2="0" y1="0" y2="1">
            <stop offset="0" stopColor={accent} stopOpacity=".35"/>
            <stop offset="1" stopColor={accent} stopOpacity="0"/>
          </linearGradient>
        </defs>
        {linhasY.map((v) => (
          <g key={v}>
            <line x1={pad.l} x2={W - pad.r} y1={y(v)} y2={y(v)} style={{ stroke: 'var(--line)' }}/>
            <text x={pad.l - 8} y={y(v) + 4} textAnchor="end" fontSize="10" fill="#8A93A6" fontFamily="JetBrains Mono, monospace">
              {fmtNumPt(v)}
            </text>
          </g>
        ))}
        {ehBarras ? (
          valores.map((v, i) => (
            <rect
              key={i}
              x={barX(i)}
              y={y(v)}
              width={barLargura}
              height={Math.max(0, y(0) - y(v))}
              fill={accent}
              opacity={hover && hover.idx === i ? 1 : 0.85}
              rx={2}
            />
          ))
        ) : (
          <>
            <path d={area} fill="url(#gExec)"/>
            <path d={linha} fill="none" stroke={accent} strokeWidth="2.2"/>
            {valores.map((v, i) => (
              <circle key={i} cx={x(i)} cy={y(v)} r={dadosUsados.length > 24 ? 2.6 : 3.6} fill="#fff" stroke={accent} strokeWidth="2"/>
            ))}
          </>
        )}
        {media > 0 && (
          <g pointerEvents="none">
            <line
              x1={pad.l} x2={W - pad.r}
              y1={y(media)} y2={y(media)}
              style={{ stroke: 'var(--ink)' }}
              strokeOpacity="0.4"
              strokeWidth="1"
              strokeDasharray="4 3"
            />
            <text
              x={W - pad.r - 4}
              y={y(media) - 4}
              textAnchor="end"
              fontSize="10"
              fontFamily="JetBrains Mono, monospace"
              fill="var(--ink)"
              fillOpacity="0.6"
            >
              média {fmtNumPt(media)}
            </text>
          </g>
        )}
        {dadosUsados.map((d, i) => {
          if (i % passo !== 0 && i !== dadosUsados.length - 1) return null;
          const tx = ehBarras ? (barX(i) + barLargura / 2) : x(i);
          // Key composta: granularidade muda antes do fetch retornar
          // (race entre granProd e producao no parent), entao `d[chaveTempo]`
          // fica undefined pra todas as linhas durante a transicao — e React
          // reclama de keys duplicadas. Indice fallback garante unicidade.
          return (
            <text key={d.dia ?? d.mes ?? i} x={tx} y={H - 8} textAnchor="middle" fontSize="11" fill="#8A93A6">
              {formatarRotulo(d, comAno)}
            </text>
          );
        })}
        {hover && (
          <g pointerEvents="none">
            {ehBarras ? null : (
              <>
                <line
                  x1={x(hover.idx)} x2={x(hover.idx)}
                  y1={pad.t} y2={H - pad.b}
                  style={{ stroke: 'var(--ink)' }} strokeOpacity="0.20" strokeDasharray="3 3"
                />
                <circle
                  cx={x(hover.idx)} cy={y(valores[hover.idx])}
                  r="5" fill={accent} stroke="#fff" strokeWidth="2"
                />
              </>
            )}
          </g>
        )}
      </svg>
      {hover && (
        <div
          className="prod-tooltip"
          style={{
            left: Math.min(hover.mx + 12, 520),
            top: Math.max(0, hover.my - 50),
          }}
        >
          <div className="prod-tooltip-label">{formatarRotulo(dadosUsados[hover.idx], true)}</div>
          <div className="prod-tooltip-valor">
            <strong>{fmtNumPt(valores[hover.idx])}</strong>
            <span className="prod-tooltip-unit">{unit}</span>
          </div>
        </div>
      )}
    </div>
  );
}

function FrontList({ accent, onAbrirObra, reloadKey }) {
  const [obras, setObras] = useState(null);
  const [erro, setErro] = useState(null);

  useEffect(() => {
    let cancelado = false;
    setErro(null);
    fetch("/api/obras", { credentials: "same-origin" })
      .then(r => r.ok ? r.json() : Promise.reject(new Error(`HTTP ${r.status}`)))
      .then(j => { if (!cancelado) setObras(Array.isArray(j) ? j : []); })
      .catch(() => { if (!cancelado) { setObras([]); setErro("Falha ao carregar obras."); } });
    return () => { cancelado = true; };
  }, [reloadKey]);

  const colorFor = (s) => s === "ok" ? "#22A06B" : s === "warn" ? accent : "#D14343";
  // Status derivado do progresso, espelhando o esquema visual do mock
  // anterior: <25% = alerta, <70% = atencao, >=70% = ok.
  const statusFor = (pct) => pct >= 70 ? "ok" : pct >= 25 ? "warn" : "alert";

  if (obras === null) {
    return <div className="front-list-empty" style={{ padding: 16, fontSize: 13, color: "var(--muted)" }}>Carregando obras…</div>;
  }
  if (obras.length === 0) {
    return (
      <div className="front-list-empty" style={{ padding: 16, fontSize: 13, color: "var(--muted)" }}>
        {erro ? erro : "Nenhuma obra cadastrada. Clique em “Adicionar nova obra” para começar."}
      </div>
    );
  }
  return (
    <ul className="front-list">
      {obras.map(o => {
        const total = Number(o.qt_estacas) || 0;
        const exec = Number(o.executadas) || 0;
        const pct = total > 0 ? Math.min(100, Math.round((exec / total) * 100)) : 0;
        const finalizada = !!o.finalizada_em;
        // Obra finalizada usa cor neutra independente do progresso —
        // visual deixa claro que a obra nao esta mais em andamento.
        const status = finalizada ? "done" : statusFor(pct);
        const barColor = finalizada ? "var(--muted)" : colorFor(status);
        return (
        <li
          key={o.id}
          className={finalizada ? "is-finalizada" : undefined}
          onClick={onAbrirObra ? () => onAbrirObra(o) : undefined}
          style={onAbrirObra ? { cursor: "pointer" } : undefined}
          role={onAbrirObra ? "button" : undefined}
          tabIndex={onAbrirObra ? 0 : undefined}
          onKeyDown={onAbrirObra ? (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); onAbrirObra(o); } } : undefined}
        >
          <div className="fl-row">
            <div className="fl-status" style={{ background: barColor }}/>
            <div className="fl-info">
              <div className="fl-name">
                {o.nome}
                {finalizada && <span className="fl-badge">Finalizada</span>}
              </div>
              <div className="fl-crew">Contrato {o.contrato} · {exec}/{total} estacas</div>
              {Array.isArray(o.tags) && o.tags.length > 0 && (
                <div className="fl-tags">{o.tags.join(" | ")}</div>
              )}
            </div>
            <div className="fl-pct">{pct}%</div>
          </div>
          <div className="fl-bar"><span style={{ width: `${pct}%`, background: barColor }}/></div>
        </li>
        );
      })}
    </ul>
  );
}

function PileTable({ accent, cliente, onAbrirEstaca, onNovasEstacas, conhecidasRef: conhecidasRefProp }) {
  const [rows, setRows] = useState([]);
  const [loading, setLoading] = useState(true);
  const [erro, setErro] = useState(null);
  // Configuracao da tabela — vem de /api/configuracoes (que ja inclui
  // os alertas, colunas visiveis e demais preferencias). Default: tudo
  // visivel ate a config carregar, pra nao piscar a tabela vazia.
  const [colunasAlerta, setColunasAlerta] = useState([]);
  const [colunasVisiveis, setColunasVisiveis] = useState(null); // null = ainda nao carregou, mostra tudo
  // Faixas de classificacao do superconsumo (usadas pela badge da
  // coluna Superconsumo). 4 niveis com 3 limites.
  const [faixasSuper, setFaixasSuper] = useState({ minAtencao: 15, minOk: 25, maxOk: 50 });
  // Limite de linhas exibidas na tabela. Vem de cfg.tabelaEstacas.limiteLinhas;
  // backend faz clamp em 1..500. Default 100.
  const [limiteLinhas, setLimiteLinhas] = useState(100);
  // Usa o ref do pai quando recebido (pra que o cache "arquivos ja
  // vistos" sobreviva ao desmonte deste componente). Mantem fallback
  // local para permitir uso isolado.
  const localConhecidasRef = useRef(null);
  const conhecidasRef = conhecidasRefProp || localConhecidasRef;

  useEffect(() => {
    let cancelado = false;
    fetch(`/api/configuracoes`, { credentials: "same-origin" })
      .then(r => r.ok ? r.json() : null)
      .then(j => {
        if (cancelado || !j || !j.dados || !j.dados.tabelaEstacas) return;
        const t = j.dados.tabelaEstacas;
        if (Array.isArray(t.alertas?.colunas)) {
          // A coluna "Consumo" do alerta foi descontinuada — agora a
          // classificacao de superconsumo vai pra propria coluna
          // Superconsumo (badge colorida). Filtra fora pra nao
          // duplicar.
          setColunasAlerta(t.alertas.colunas.filter(c => c.id !== 'consumo'));
        }
        if (t.colunasVisiveis && typeof t.colunasVisiveis === 'object') setColunasVisiveis(t.colunasVisiveis);
        if (t.faixasSuperConsumo && typeof t.faixasSuperConsumo === 'object') {
          setFaixasSuper({
            minAtencao: Number(t.faixasSuperConsumo.minAtencao) || 15,
            minOk:      Number(t.faixasSuperConsumo.minOk)      || 25,
            maxOk:      Number(t.faixasSuperConsumo.maxOk)      || 50,
          });
        }
        if (Number.isFinite(Number(t.limiteLinhas)) && Number(t.limiteLinhas) > 0) {
          // Clamp local 1..500 espelha o backend.
          setLimiteLinhas(Math.min(500, Math.max(1, Math.floor(Number(t.limiteLinhas)))));
        }
      })
      .catch(() => {});
    return () => { cancelado = true; };
  }, []);

  // Classifica um valor de superconsumo em ok / atencao / alerta a
  // partir das faixas configuradas. Retorna a cor (hex) e o estado
  // (string) — usados pela badge da coluna Superconsumo.
  const classificarSuper = (valor) => {
    const v = Number(valor);
    if (!Number.isFinite(v)) return { cor: '#8A93A6', estado: 'sem dado' };
    if (v < faixasSuper.minAtencao) return { cor: '#D14343', estado: 'alerta' };
    if (v < faixasSuper.minOk)      return { cor: '#E08B2A', estado: 'atencao' };
    if (v <= faixasSuper.maxOk)     return { cor: '#22A06B', estado: 'ideal' };
    return { cor: '#D14343', estado: 'alerta' };
  };

  // Extrai o numero a exibir na badge de uma coluna de alerta. Cada
  // tipo de coluna tem sua propria fonte de dado:
  //   faixas          -> linha[col.campo]
  //   inclinacao_xy   -> max(|X|, |Y|)
  //   perfil_vs_raio  -> minPerfil
  const valorDeColunaAlerta = (col, linha) => {
    if (!col || !linha) return null;
    if (col.tipo === 'faixas') return linha[col.campo];
    if (col.tipo === 'inclinacao_xy') {
      const x = Math.abs(Number(linha.maxAbsInclX));
      const y = Math.abs(Number(linha.maxAbsInclY));
      const ax = Number.isFinite(x) ? x : -Infinity;
      const ay = Number.isFinite(y) ? y : -Infinity;
      const m = Math.max(ax, ay);
      return Number.isFinite(m) && m > -Infinity ? m : null;
    }
    // Perfil: o dado bruto e' um raio (comparado com diametroTrado/2 na
    // logica de cor). A coluna exibe em DIAMETRO pra ficar comparavel
    // com a coluna Diametro da tabela — entao multiplica por 2.
    if (col.tipo === 'perfil_vs_raio') {
      const v = Number(linha.minPerfil);
      return Number.isFinite(v) ? v * 2 : null;
    }
    return null;
  };

  // Formata um numero pt-BR com N casas decimais.
  const fmtN = (n, casas) => Number(n).toLocaleString('pt-BR', {
    minimumFractionDigits: casas, maximumFractionDigits: casas,
  });

  // Formata o conteudo da badge por contexto da coluna. Cada id tem sua
  // regra de casas decimais. Inclinacao X/Y mostra os DOIS valores
  // separados por "|" (em vez do max), com 1 casa cada.
  const formatarValorAlerta = (col, valor, linha) => {
    const id = col && col.id;
    if (id === 'inclinacao') {
      const x = Number(linha && linha.maxAbsInclX);
      const y = Number(linha && linha.maxAbsInclY);
      if (!Number.isFinite(x) && !Number.isFinite(y)) return '—';
      const sx = Number.isFinite(x) ? fmtN(x, 1) : '—';
      const sy = Number.isFinite(y) ? fmtN(y, 1) : '—';
      return `${sx} | ${sy}`;
    }
    if (valor == null) return '—';
    const n = Number(valor);
    if (!Number.isFinite(n)) return '—';
    if (id === 'torque' || id === 'rotacao') return fmtN(n, 0);
    if (id === 'concreto') return fmtN(n, 1);
    if (id === 'perfil')   return fmtN(n, 0);
    return fmtN(n, 2);
  };

  // Helper: coluna esta visivel? Se a config nao carregou ainda
  // (colunasVisiveis === null), mostra tudo. Se carregou e o nome nao
  // esta no objeto, mostra (default). Se esta e e' false, esconde.
  const isVisivel = (nome) => {
    if (colunasVisiveis == null) return true;
    return colunasVisiveis[nome] !== false;
  };
  // Filtra colunas dinamicas (alertas) pelo titulo configuravel
  const alertasVisiveis = colunasAlerta.filter(c => isVisivel(c.titulo));

  useEffect(() => {
    let cancelado = false;

    const carregar = (eMontagemInicial) => {
      if (eMontagemInicial) { setLoading(true); setErro(null); }
      fetch(`/api/ultimas-estacas?limite=${limiteLinhas}`, { credentials: "same-origin" })
        .then(r => r.ok ? r.json() : Promise.reject(r.status))
        .then(data => {
          if (cancelado) return;
          const lista = Array.isArray(data) ? data : [];
          // Detecta novidades sempre que ja existir cache previo —
          // mesmo na "primeira" carga deste mount, contanto que o cache
          // do pai tenha sobrevivido a navegacoes anteriores.
          if (conhecidasRef.current) {
            const novasLista = lista.filter(r => r.arquivo && !conhecidasRef.current.has(r.arquivo));
            const novas = novasLista.length;
            if (novas > 0 && typeof onNovasEstacas === "function") {
              // Passa lista crua das estacas novas (com obra+contrato) como
              // 2o arg pro App fazer deteccao de obras nao cadastradas.
              // Handlers antigos que so esperam string continuam funcionando.
              onNovasEstacas(
                `${novas} ${novas === 1 ? "arquivo novo" : "arquivos novos"}`,
                novasLista.map(r => ({
                  arquivo: r.arquivo,
                  obra: r.obra || "",
                  contrato: r.contrato || "",
                }))
              );
            }
          }
          conhecidasRef.current = new Set(lista.map(r => r.arquivo).filter(Boolean));
          setRows(lista);
        })
        .catch(() => { if (!cancelado && eMontagemInicial) setErro("Falha ao carregar."); })
        .finally(() => { if (!cancelado && eMontagemInicial) setLoading(false); });
    };

    carregar(true);
    // Polling de 60s. Antes era 5min — usuario raramente ficava parado
    // tempo suficiente para ver o toast.
    const id = setInterval(() => carregar(false), 60 * 1000);

    // Quando a aba volta a ficar visivel, recarrega na hora — pega
    // novas estacas que apareceram enquanto a aba estava em background
    // (onde o setInterval e throttle pelo navegador ate ~1x/min).
    const onVisibility = () => {
      if (document.visibilityState === "visible") carregar(false);
    };
    document.addEventListener("visibilitychange", onVisibility);

    return () => {
      cancelado = true;
      clearInterval(id);
      document.removeEventListener("visibilitychange", onVisibility);
    };
  }, [onNovasEstacas, conhecidasRef, limiteLinhas]);


  // Avalia uma coluna do config-alertas pra uma linha. Retorna sempre um
  // objeto { tipo, color } compativel com o renderizador de icone.
  const COR_ESTADO = { ok: "#22A06B", atencao: "#E0A02A", alerta: "#D14343" };
  const TIPO_ESTADO = { ok: "ok", atencao: "warn", alerta: "alert" };
  const _estado = (e) => ({ tipo: TIPO_ESTADO[e] || "alert", color: COR_ESTADO[e] || "#D14343" });
  const avaliarColunaAlerta = (col, linha) => {
    if (!col) return _estado("alerta");
    if (col.tipo === "perfil_vs_raio") {
      const raio = Number(linha.diametroTrado) / 2;
      const minP = linha.minPerfil;
      if (minP == null || !Number.isFinite(raio)) return _estado("alerta");
      return Number(minP) < raio ? _estado("alerta") : _estado("ok");
    }
    if (col.tipo === "inclinacao_xy") {
      const lim = Number(col.limite ?? 0.5);
      const x = linha.maxAbsInclX;
      const y = linha.maxAbsInclY;
      if (x == null && y == null) return _estado("alerta");
      const fora = (Number(x) > lim) || (Number(y) > lim);
      return fora ? _estado("alerta") : _estado("ok");
    }
    if (col.tipo === "faixas") {
      const v = linha[col.campo];
      if (v == null || !Number.isFinite(Number(v))) return _estado(col.padrao || "alerta");
      const n = Number(v);
      for (const f of (col.faixas || [])) {
        const okMin = f.min == null || n >= Number(f.min);
        const okMax = f.max == null || n <= Number(f.max);
        if (okMin && okMax) return _estado(f.estado || col.padrao || "alerta");
      }
      return _estado(col.padrao || "alerta");
    }
    return _estado("alerta");
  };

  const fmtNum = (n, casas = 2) => {
    const v = Number(n);
    if (!isFinite(v)) return "—";
    return v.toLocaleString("pt-BR", { minimumFractionDigits: casas, maximumFractionDigits: casas });
  };

  const fmtData = (iso) => {
    if (!iso) return "—";
    const d = new Date(iso);
    if (isNaN(d.getTime())) return "—";
    const dd = String(d.getDate()).padStart(2, "0");
    const mm = String(d.getMonth() + 1).padStart(2, "0");
    const yy = String(d.getFullYear()).slice(-2);
    const hh = String(d.getHours()).padStart(2, "0");
    const mi = String(d.getMinutes()).padStart(2, "0");
    return `${dd}/${mm}/${yy} ${hh}:${mi}`;
  };

  // Numero total de colunas que vao aparecer (pra dimensionar o colSpan
  // dos estados "carregando/erro/vazio"). Conta as fixas + as dinamicas
  // visiveis, segundo a config atual.
  const fixasVisiveis = ["Fim concretagem","Tag","Obra","Estaca","Diâmetro","Profundidade","Volume","Superconsumo"]
    .filter(isVisivel).length;
  const totalColunas = fixasVisiveis + alertasVisiveis.length;

  return (
    <div className="pile-table-wrap">
      <table className="pile-table">
        <thead>
          <tr>
            {isVisivel("Fim concretagem") && <th>Fim Concretagem</th>}
            {isVisivel("Tag") && <th>Tag</th>}
            {isVisivel("Obra") && <th>Obra</th>}
            {isVisivel("Estaca") && <th>Estaca</th>}
            {isVisivel("Diâmetro") && <th>Diâmetro</th>}
            {isVisivel("Profundidade") && <th>Profundidade</th>}
            {isVisivel("Volume") && <th>Volume Concreto</th>}
            {isVisivel("Superconsumo") && <th>Superconsumo</th>}
            {alertasVisiveis.map(col => (
              <th key={col.id} style={{ textAlign: "center" }}>
                <span style={{ display: "inline-flex", alignItems: "center", gap: 6 }}>
                  {col.titulo}
                  {col.tooltip ? <InfoTip text={col.tooltip}/> : null}
                </span>
              </th>
            ))}
          </tr>
        </thead>
        <tbody>
          {loading && (
            <tr><td colSpan={totalColunas} className="pt-empty">Carregando…</td></tr>
          )}
          {!loading && erro && (
            <tr><td colSpan={totalColunas} className="pt-empty">{erro}</td></tr>
          )}
          {!loading && !erro && rows.length === 0 && (
            <tr><td colSpan={totalColunas} className="pt-empty">Nenhuma estaca encontrada.</td></tr>
          )}
          {!loading && !erro && (() => {
            // Lista de navegacao = todas as linhas clicaveis exibidas. Passada
            // ao parent para que prev/next andem entre as ultimas estacas.
            const navListaUltimas = rows
              .filter(rr => !!rr.arquivo)
              .map(rr => ({ pasta: rr.contrato || "", arquivo: rr.arquivo }));
            return rows.map((r, i) => {
            const podeAbrir = !!r.arquivo && typeof onAbrirEstaca === "function";
            return (
              <tr
                key={`${r.arquivo || r.nomeEstaca}-${i}`}
                className={podeAbrir ? "pt-row pt-row-clicavel" : "pt-row"}
                onClick={podeAbrir ? () => onAbrirEstaca(r.contrato || "", r.arquivo, navListaUltimas) : undefined}
                tabIndex={podeAbrir ? 0 : undefined}
                onKeyDown={podeAbrir ? (e) => {
                  if (e.key === "Enter" || e.key === " ") {
                    e.preventDefault();
                    onAbrirEstaca(r.contrato || "", r.arquivo, navListaUltimas);
                  }
                } : undefined}
                role={podeAbrir ? "button" : undefined}
              >
                {isVisivel("Fim concretagem") && <td className="pt-num" data-label="Fim concretagem">{fmtData(r.fimConc)}</td>}
                {isVisivel("Tag") && <td data-label="Tag">{r.tag || "—"}</td>}
                {isVisivel("Obra") && <td data-label="Obra">{r.obra || "—"}</td>}
                {isVisivel("Estaca") && <td className="pt-id" data-label="Estaca">{r.nomeEstaca || "—"}</td>}
                {isVisivel("Diâmetro") && <td className="pt-num" data-label="Diâmetro">{r.diametroTrado != null ? `${r.diametroTrado} mm` : "—"}</td>}
                {isVisivel("Profundidade") && <td className="pt-num" data-label="Profundidade">{r.profTotal != null ? `${fmtNum(r.profTotal / 100, 2)} m` : "—"}</td>}
                {isVisivel("Volume") && <td className="pt-num" data-label="Volume concreto">{r.volTotal != null ? `${fmtNum(r.volTotal / 1000, 2)} m³` : "—"}</td>}
                {isVisivel("Superconsumo") && (
                  <td className="pt-num" data-label="Superconsumo">
                    {r.superconsumo == null ? "—" : (() => {
                      const cls = classificarSuper(r.superconsumo);
                      return (
                        <span style={{
                          display: 'inline-flex', alignItems: 'center', gap: 6,
                          padding: '3px 10px', borderRadius: 999,
                          background: cls.cor + '1F',
                          color: cls.cor,
                          fontWeight: 600, fontSize: 12,
                          fontFamily: "'JetBrains Mono', monospace",
                        }} title={`Classificação: ${cls.estado}`}>
                          {Math.round(Number(r.superconsumo))}%
                        </span>
                      );
                    })()}
                  </td>
                )}
                {alertasVisiveis.map(col => {
                  const a = avaliarColunaAlerta(col, r);
                  const valor = valorDeColunaAlerta(col, r);
                  const texto = formatarValorAlerta(col, valor, r);
                  // X/Y nao usa "valor" — ela monta o texto a partir das
                  // duas chaves de incl. So mostra "—" se ambas faltarem,
                  // o que ja vem refletido no texto retornado.
                  const semDado = col.tipo === 'inclinacao_xy' ? texto === '—' : valor == null;
                  return (
                    <td key={col.id} className="pt-num" data-label={col.titulo} style={{ textAlign: 'center' }}>
                      {semDado ? (
                        <span style={{ color: 'var(--muted-2, #8A93A6)' }}>—</span>
                      ) : (
                        <span style={{
                          display: 'inline-flex', alignItems: 'baseline', gap: 4,
                          padding: '3px 10px', borderRadius: 999,
                          background: a.color + '1F',
                          color: a.color,
                          fontWeight: 600, fontSize: 12,
                          fontFamily: "'JetBrains Mono', monospace",
                        }} title={`${col.titulo}: ${a.tipo}`}>
                          {texto}
                        </span>
                      )}
                    </td>
                  );
                })}
              </tr>
            );
            });
          })()}
        </tbody>
      </table>
    </div>
  );
}

function SiteMap({ accent }) {
  const pins = [
    { x: 22, y: 38, status: "ok" },
    { x: 36, y: 30, status: "ok" },
    { x: 48, y: 44, status: "warn" },
    { x: 62, y: 32, status: "ok" },
    { x: 74, y: 56, status: "alert" },
    { x: 30, y: 62, status: "ok" },
    { x: 56, y: 70, status: "ok" },
    { x: 82, y: 38, status: "warn" },
  ];
  const colorFor = (s) => s === "ok" ? "#22A06B" : s === "warn" ? accent : "#D14343";
  return (
    <div className="site-map">
      <svg viewBox="0 0 320 220" className="site-map-svg">
        <defs>
          <pattern id="grid" width="20" height="20" patternUnits="userSpaceOnUse">
            <path d="M20 0H0V20" fill="none" stroke="#E6EAF1" strokeWidth="1"/>
          </pattern>
        </defs>
        <rect width="320" height="220" fill="#F6F8FB"/>
        <rect width="320" height="220" fill="url(#grid)"/>
        <path d="M20 50 L120 30 L240 60 L300 100 L280 180 L160 200 L60 170 L30 120 Z" fill="#fff" stroke="#CFD6E1" strokeWidth="1.5"/>
        <path d="M20 120 L300 100" stroke="#CFD6E1" strokeWidth="6" fill="none" opacity=".6"/>
        <path d="M160 30 L160 200" stroke="#CFD6E1" strokeWidth="4" fill="none" opacity=".5"/>
        {pins.map((p, i) => (
          <g key={i} transform={`translate(${(p.x/100)*320},${(p.y/100)*220})`}>
            <circle r="9" fill={colorFor(p.status)} opacity=".18"/>
            <circle r="4" fill={colorFor(p.status)}/>
          </g>
        ))}
      </svg>
      <div className="site-map-legend">
        <span><i style={{ background: "#22A06B" }}/> Operando</span>
        <span><i style={{ background: accent }}/> Atenção</span>
        <span><i style={{ background: "#D14343" }}/> Alerta</span>
      </div>
    </div>
  );
}

/* ============================================================
   USUÁRIOS (admin_sistema)
============================================================ */
function UsuariosPage({ accent, notify, papel }) {
  const [usuarios, setUsuarios] = useState(null); // null = carregando
  const [erro, setErro] = useState(null);
  const [refresh, setRefresh] = useState(0);
  // null = fechado, "novo" = criando, { ...usuario } = editando aquele.
  const [modal, setModal] = useState(null);
  // Admin_cliente tem permissoes reduzidas: nao pode criar, nao pode
  // excluir, e nao ve coluna razao social/cnpj (que so faz sentido pra
  // admin_sistema que navega entre empresas).
  const ehAdminSistema = papel === "admin_sistema";
  const ehAdminCliente = papel === "admin_cliente";
  const podeCriar = ehAdminSistema;
  const podeExcluir = ehAdminSistema;
  const mostrarColEmpresa = ehAdminSistema;

  useEffect(() => {
    let cancelado = false;
    setErro(null);
    fetch("/api/usuarios", { credentials: "same-origin" })
      .then(r => r.ok ? r.json() : r.json().then(j => Promise.reject(j.erro || `Erro ${r.status}`)))
      .then(arr => { if (!cancelado) setUsuarios(Array.isArray(arr) ? arr : []); })
      .catch(e => { if (!cancelado) setErro(String(e || "Falha ao carregar usuários")); });
    return () => { cancelado = true; };
  }, [refresh]);

  const toggleAtivo = async (u) => {
    const acao = u.ativo ? "desativar" : "reativar";
    if (!window.confirm(`Confirma ${acao} o usuário ${u.email}?`)) return;
    try {
      const r = await fetch(`/api/usuarios/${u.id}`, {
        method: "PATCH",
        credentials: "same-origin",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ ativo: !u.ativo }),
      });
      if (!r.ok) {
        const j = await r.json().catch(() => ({}));
        throw new Error(j.erro || `Erro ${r.status}`);
      }
      notify(`Usuário ${acao === "desativar" ? "desativado" : "reativado"}.`, "ok");
      setRefresh(k => k + 1);
    } catch (e) {
      notify(`Falha ao ${acao}: ${e.message || e}`, "erro");
    }
  };

  const excluirUsuario = async (u) => {
    if (!window.confirm(
      `Excluir definitivamente o usuário ${u.email}?\n\n` +
      `Esta ação não pode ser desfeita. Considere desativar em vez de excluir.`
    )) return;
    try {
      const r = await fetch(`/api/usuarios/${u.id}`, {
        method: "DELETE",
        credentials: "same-origin",
      });
      const j = await r.json().catch(() => ({}));
      if (!r.ok) throw new Error(j.erro || `Erro ${r.status}`);
      notify("Usuário excluído.", "ok");
      setRefresh(k => k + 1);
    } catch (e) {
      notify(`Falha ao excluir: ${e.message || e}`, "erro");
    }
  };

  // Limpa todas as sessoes ativas de um usuario. Util quando ele fica
  // trancado pelo limite de 2 sessoes e nao consegue logar (fechou
  // navegador sem deslogar, perdeu cookie, etc.). Restrito a admin_sistema.
  const limparSessoes = async (u) => {
    if (!window.confirm(
      `Limpar todas as sessões ativas de ${u.email}?\n\n` +
      `O usuário será desconectado de qualquer dispositivo em que esteja logado e ` +
      `o limite de 2 sessões simultâneas será resetado.`
    )) return;
    try {
      const r = await fetch(`/api/usuarios/${u.id}/sessoes`, {
        method: "DELETE",
        credentials: "same-origin",
      });
      const j = await r.json().catch(() => ({}));
      if (!r.ok) throw new Error(j.erro || `Erro ${r.status}`);
      notify(`Sessões limpas (${j.removidas || 0} removida${j.removidas === 1 ? '' : 's'}).`, "ok");
    } catch (e) {
      notify(`Falha ao limpar sessões: ${e.message || e}`, "erro");
    }
  };

  const fmtDataHora = (v) => {
    if (!v) return "—";
    try {
      const d = new Date(v);
      const z = (n) => String(n).padStart(2, "0");
      return `${z(d.getDate())}/${z(d.getMonth() + 1)}/${d.getFullYear()} ${z(d.getHours())}:${z(d.getMinutes())}`;
    } catch { return "—"; }
  };
  const labelPapel = (p) => p === "admin_sistema" ? "Admin Sistema"
                         : p === "admin_cliente" ? "Admin Cliente"
                         : "Usuário";

  return (
    <div className="dash-page">
      <div className="dash-pagehead">
        <div>
          <div className="dash-eyebrow">USUÁRIOS</div>
          <h1 className="dash-h1">Gerenciar usuários</h1>
          <div className="dash-bread">
            {usuarios === null ? "Carregando…" : `${usuarios.length} usuário${usuarios.length === 1 ? "" : "s"}`}
          </div>
        </div>
        <div className="dash-actions">
          {podeCriar && (
            <button
              className="dash-btn-primary"
              style={{ background: accent.hex }}
              onClick={() => setModal("novo")}
            >
              Adicionar usuário <I.plus width="14" height="14"/>
            </button>
          )}
        </div>
      </div>

      <Card title="Usuários cadastrados">
        <div className="usuarios-tabela-wrap">
          {erro && <div className="usuarios-erro">{erro}</div>}
          {!erro && (() => {
            // Numero de colunas calculado dinamicamente pra dimensionar
            // os colSpans dos estados loading/vazio sem desencaixe.
            const totalCols = 6 + (mostrarColEmpresa ? 3 : 1) + 1; // base 6 + (cliente+razao+cnpj) ou só cliente + acoes
            return (
            <table className="usuarios-tabela">
              <thead>
                <tr>
                  <th>E-mail</th>
                  <th>Nome</th>
                  <th>Papel</th>
                  <th>Cliente</th>
                  {mostrarColEmpresa && <th>Razão social</th>}
                  {mostrarColEmpresa && <th>CNPJ</th>}
                  <th>Último login</th>
                  <th>Status</th>
                  <th></th>
                </tr>
              </thead>
              <tbody>
                {usuarios === null && (
                  <tr><td colSpan={totalCols} className="usuarios-empty">Carregando…</td></tr>
                )}
                {usuarios && usuarios.length === 0 && (
                  <tr><td colSpan={totalCols} className="usuarios-empty">Nenhum usuário cadastrado.</td></tr>
                )}
                {usuarios && usuarios.map(u => (
                  <tr key={u.id} className={u.ativo ? "" : "is-inativo"}>
                    <td className="usuarios-email">{u.email}</td>
                    <td>{u.nome || "—"}</td>
                    <td>
                      <span className={`usuarios-papel papel-${u.papel}`}>{labelPapel(u.papel)}</span>
                    </td>
                    <td>{u.cliente || (u.papel === "admin_sistema" ? "—" : "")}</td>
                    {mostrarColEmpresa && <td>{u.razao_social || "—"}</td>}
                    {mostrarColEmpresa && <td>{u.cnpj || "—"}</td>}
                    <td className="usuarios-data">{fmtDataHora(u.ultimo_login)}</td>
                    <td>
                      <span className={`usuarios-status ${u.ativo ? "is-on" : "is-off"}`}>
                        {u.ativo ? "Ativo" : "Inativo"}
                      </span>
                    </td>
                    <td className="usuarios-acoes">
                      <button
                        type="button"
                        className="usuarios-btn-acao"
                        onClick={() => setModal(u)}
                        title="Editar dados do usuário"
                      >
                        Editar
                      </button>
                      <button
                        type="button"
                        className="usuarios-btn-acao"
                        onClick={() => toggleAtivo(u)}
                        title={u.ativo ? "Desativar usuário" : "Reativar usuário"}
                      >
                        {u.ativo ? "Desativar" : "Reativar"}
                      </button>
                      {ehAdminSistema && (
                        <button
                          type="button"
                          className="usuarios-btn-acao"
                          onClick={() => limparSessoes(u)}
                          title="Limpar todas as sessões ativas (libera o limite de 2 conexões)"
                        >
                          Limpar sessões
                        </button>
                      )}
                      {podeExcluir && (
                        <button
                          type="button"
                          className="usuarios-btn-acao is-danger"
                          onClick={() => excluirUsuario(u)}
                          title="Excluir definitivamente"
                        >
                          Excluir
                        </button>
                      )}
                    </td>
                  </tr>
                ))}
              </tbody>
            </table>
            );
          })()}
        </div>
      </Card>

      {modal && (
        <UsuarioFormModal
          accent={accent}
          notify={notify}
          usuario={modal === "novo" ? null : modal}
          papelDoLogado={papel}
          onClose={() => setModal(null)}
          onSalvo={() => { setModal(null); setRefresh(k => k + 1); }}
        />
      )}
    </div>
  );
}

// Gera uma senha aleatoria (12 chars) com pelo menos 1 maiuscula, 1
// minuscula e 1 digito. Caracteres ambiguos (0/O/I/l/1) excluidos para
// nao confundir o usuario. Usa crypto.getRandomValues para entropia.
function gerarSenhaAleatoria(len = 12) {
  const upper = "ABCDEFGHJKLMNPQRSTUVWXYZ";
  const lower = "abcdefghjkmnpqrstuvwxyz";
  const digit = "23456789";
  const all = upper + lower + digit;
  const tam = Math.max(8, len);
  const buf = new Uint32Array(tam + 3);
  crypto.getRandomValues(buf);
  const arr = [
    upper[buf[0] % upper.length],
    lower[buf[1] % lower.length],
    digit[buf[2] % digit.length],
  ];
  for (let i = 3; i < tam; i++) arr.push(all[buf[i] % all.length]);
  // Fisher–Yates shuffle com bytes aleatorios pra desambiguar a ordem.
  for (let i = arr.length - 1; i > 0; i--) {
    const j = buf[(i + tam) % buf.length] % (i + 1);
    [arr[i], arr[j]] = [arr[j], arr[i]];
  }
  return arr.join("");
}

function UsuarioFormModal({ accent, notify, usuario = null, onClose, onSalvo, papelDoLogado }) {
  const editando = !!usuario;
  const [email, setEmail] = useState(usuario?.email || "");
  const [senha, setSenha] = useState("");
  const [mostrarSenha, setMostrarSenha] = useState(false);
  const [nome, setNome] = useState(usuario?.nome || "");
  const [papel, setPapel] = useState(usuario?.papel || "usuario");
  const [cliente, setCliente] = useState(usuario?.cliente || "");
  const [empresas, setEmpresas] = useState([]);
  const [salvando, setSalvando] = useState(false);
  const [erro, setErro] = useState(null);
  // Restricoes de admin_cliente: nao pode mudar empresa do usuario nem
  // promover ninguem pra admin_sistema. Backend ja valida; UI espelha.
  const ehAdminClienteLogado = papelDoLogado === "admin_cliente";
  const ehAdminSistemaLogado = papelDoLogado === "admin_sistema";

  const aoGerarSenha = () => {
    const nova = gerarSenhaAleatoria(12);
    setSenha(nova);
    setMostrarSenha(true);
  };
  const aoCopiarSenha = async () => {
    if (!senha) return;
    try {
      await navigator.clipboard.writeText(senha);
      notify("Senha copiada para a área de transferência.", "ok");
    } catch {
      notify("Não foi possível copiar — selecione e copie manualmente.", "erro");
    }
  };

  useEffect(() => {
    fetch("/api/empresas", { credentials: "same-origin" })
      .then(r => r.ok ? r.json() : [])
      .then(arr => setEmpresas(Array.isArray(arr) ? arr : []))
      .catch(() => {});
  }, []);

  // Esc fecha quando nao esta salvando
  useEffect(() => {
    const onKey = (e) => { if (e.key === "Escape" && !salvando) onClose(); };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [onClose, salvando]);

  const exigeCliente = papel !== "admin_sistema";

  const submit = async (e) => {
    if (e) e.preventDefault();
    setErro(null);
    if (!email.trim()) return setErro("E-mail é obrigatório.");
    if (!editando && !senha) return setErro("Senha é obrigatória.");
    if (senha && senha.length < 6) return setErro("A senha deve ter pelo menos 6 caracteres.");
    if (exigeCliente && !cliente.trim()) return setErro("Cliente é obrigatório para este papel.");
    setSalvando(true);
    try {
      const corpo = {
        email: email.trim(),
        nome: nome.trim() || null,
      };
      // admin_cliente nao pode mudar `cliente` nem `papel` — backend
      // rejeita mesmo se vierem inalterados. So inclui esses campos no
      // payload quando o solicitante e' admin_sistema (ou na criacao,
      // que e' restrita a admin_sistema de qualquer jeito).
      if (!ehAdminClienteLogado) {
        corpo.papel = papel;
        corpo.cliente = exigeCliente ? cliente.trim() : null;
      }
      // Senha so vai no payload se preenchida (em edit, em branco = nao troca).
      if (senha) corpo.senha = senha;

      let r;
      if (editando) {
        r = await fetch(`/api/usuarios/${usuario.id}`, {
          method: "PATCH",
          credentials: "same-origin",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify(corpo),
        });
      } else {
        r = await fetch("/api/usuarios", {
          method: "POST",
          credentials: "same-origin",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify(corpo),
        });
      }
      const j = await r.json().catch(() => ({}));
      if (!r.ok) throw new Error(j.erro || `Erro ${r.status}`);
      notify(editando ? `Usuário ${j.email} atualizado.` : `Usuário ${j.email} criado.`, "ok");
      onSalvo();
    } catch (err) {
      setErro(err.message || "Falha ao salvar usuário.");
    } finally {
      setSalvando(false);
    }
  };

  return (
    <div className="report-backdrop" onClick={(e) => { if (e.target === e.currentTarget && !salvando) onClose(); }}>
      <div className="report-modal" role="dialog" aria-modal="true">
        <div className="report-head">
          <div className="report-eyebrow">{editando ? "EDITAR USUÁRIO" : "NOVO USUÁRIO"}</div>
          <h2 className="report-title">{editando ? "Editar usuário" : "Adicionar usuário"}</h2>
          <p className="report-sub">
            {editando
              ? "Atualize os dados. Deixe a senha em branco para mantê-la."
              : "Preencha os dados."}
          </p>
          <button className="report-close" aria-label="Fechar" onClick={onClose}>
            <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
              <path d="M18 6L6 18M6 6l12 12"/>
            </svg>
          </button>
        </div>
        <form className="report-body usuario-form" onSubmit={submit}>
          <label className="usuario-form-row">
            <span className="usuario-form-lbl">E-mail</span>
            <input type="email" className="usuario-form-inp" autoFocus required
              value={email} onChange={e => setEmail(e.target.value)}/>
          </label>
          <label className="usuario-form-row">
            <span className="usuario-form-lbl">
              {editando ? "Nova senha (deixe em branco para manter)" : "Senha"}
            </span>
            <div className="usuario-form-senha">
              <input
                type={mostrarSenha ? "text" : "password"}
                className="usuario-form-inp"
                required={!editando}
                minLength={editando ? undefined : 6}
                value={senha}
                onChange={e => setSenha(e.target.value)}
                placeholder={editando ? "•••••••• (não alterada)" : ""}
                autoComplete="new-password"
              />
              <button
                type="button"
                className="usuario-form-senha-btn"
                onClick={() => setMostrarSenha(v => !v)}
                title={mostrarSenha ? "Ocultar senha" : "Mostrar senha"}
                aria-label={mostrarSenha ? "Ocultar senha" : "Mostrar senha"}
              >
                {mostrarSenha ? "Ocultar" : "Mostrar"}
              </button>
              <button
                type="button"
                className="usuario-form-senha-btn"
                onClick={aoGerarSenha}
                title="Gerar senha aleatória"
              >
                Gerar
              </button>
              <button
                type="button"
                className="usuario-form-senha-btn"
                onClick={aoCopiarSenha}
                disabled={!senha}
                title="Copiar senha para a área de transferência"
              >
                Copiar
              </button>
            </div>
          </label>
          <label className="usuario-form-row">
            <span className="usuario-form-lbl">Nome (opcional)</span>
            <input type="text" className="usuario-form-inp"
              value={nome} onChange={e => setNome(e.target.value)}/>
          </label>
          {/* admin_cliente nao escolhe papel — nem na criacao (que ele
              nem acessa) nem na edicao. Mostra so um chip informativo
              com o papel atual pra ele saber qual nivel o usuario tem. */}
          {ehAdminClienteLogado ? (
            <div className="usuario-form-row">
              <span className="usuario-form-lbl">Papel</span>
              <div className="usuario-form-papeis-readonly" style={{ color: 'var(--muted)', fontSize: 13 }}>
                {papel === 'admin_cliente' ? 'Admin Cliente' : 'Usuário'}
                <span style={{ marginLeft: 8, fontSize: 12 }}>
                  (somente o administrador do sistema pode alterar o papel)
                </span>
              </div>
            </div>
          ) : (
            <div className="usuario-form-row">
              <span className="usuario-form-lbl">Papel</span>
              <div className="usuario-form-papeis">
                {[
                  { id: "usuario", lbl: "Usuário", desc: "Acesso de leitura. Sem importar/editar." },
                  { id: "admin_cliente", lbl: "Admin Cliente", desc: "Administra dentro de uma empresa." },
                  // admin_sistema so aparece pra quem ja e' admin_sistema.
                  ...(ehAdminSistemaLogado ? [
                    { id: "admin_sistema", lbl: "Admin Sistema", desc: "Acesso global. Sem cliente fixo." },
                  ] : []),
                ].map(p => (
                  <label key={p.id} className={`usuario-papel-opt ${papel === p.id ? "is-on" : ""}`}>
                    <input type="radio" name="papel" value={p.id} checked={papel === p.id}
                      onChange={e => setPapel(e.target.value)}/>
                    <div>
                      <div className="usuario-papel-titulo">{p.lbl}</div>
                      <div className="usuario-papel-desc">{p.desc}</div>
                    </div>
                  </label>
                ))}
              </div>
            </div>
          )}
          {exigeCliente && (
            <label className="usuario-form-row">
              <span className="usuario-form-lbl">Cliente</span>
              <input
                type="text"
                className="usuario-form-inp"
                required
                disabled={ehAdminClienteLogado}
                list="usuario-empresas-existentes"
                placeholder="Nome do cliente (ex: Construtora X)"
                value={cliente}
                onChange={e => setCliente(e.target.value)}
                title={ehAdminClienteLogado ? 'Admin de empresa não pode mudar a empresa do usuário.' : undefined}
              />
              <datalist id="usuario-empresas-existentes">
                {empresas.map(e => <option key={e} value={e}/>)}
              </datalist>
            </label>
          )}
          {erro && <div className="usuario-form-erro">{erro}</div>}
          <div className="usuario-form-acoes">
            <button type="button" className="dash-btn-ghost" onClick={onClose} disabled={salvando}>
              Cancelar
            </button>
            <button type="submit" className="dash-btn-primary" style={{ background: accent.hex }} disabled={salvando}>
              {salvando ? "Salvando…" : (editando ? "Salvar alterações" : "Criar usuário")}
            </button>
          </div>
        </form>
      </div>
    </div>
  );
}

/* ============================================================
   MÁQUINAS (admin_sistema)
============================================================ */
function MaquinasPage({ accent, notify }) {
  const [maquinas, setMaquinas] = useState(null);
  const [erro, setErro] = useState(null);
  const [refresh, setRefresh] = useState(0);
  // null = fechado, "novo" = criando, { ...maquina } = editando.
  const [modal, setModal] = useState(null);
  // ids cuja senha esta sendo exibida na tabela (toggle por linha).
  const [senhasVisiveis, setSenhasVisiveis] = useState({});
  // Filtro por cliente (header da lista). "all" = mostra tudo.
  const [filtroCliente, setFiltroCliente] = useState("all");
  const [sincronizando, setSincronizando] = useState(false);
  // Ordenacao opcional por header (so Tag e Num. serie sao clicaveis).
  // `campo: null` = ordem default da API (ativo DESC, cliente, nome).
  const [sort, setSort] = useState({ campo: null, direcao: "asc" });
  const aoOrdenarPor = (campo) => {
    setSort(s => {
      if (s.campo !== campo) return { campo, direcao: "asc" };
      if (s.direcao === "asc") return { campo, direcao: "desc" };
      return { campo: null, direcao: "asc" };
    });
  };

  // Lista distinct de clientes existentes nas maquinas carregadas — popula
  // o dropdown de filtro. Ordena alfabeticamente case-insensitive.
  const clientesUnicos = React.useMemo(() => {
    if (!Array.isArray(maquinas)) return [];
    const set = new Set();
    maquinas.forEach(m => { if (m.cliente) set.add(m.cliente); });
    return [...set].sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
  }, [maquinas]);

  const maquinasFiltradas = React.useMemo(() => {
    if (!Array.isArray(maquinas)) return [];
    if (filtroCliente === "all") return maquinas;
    return maquinas.filter(m => m.cliente === filtroCliente);
  }, [maquinas, filtroCliente]);

  // Aplica a ordenacao por header. num_serie usa comparador numerico quando
  // ambos os lados parsearem (sao codigos como "1234", "20000"); tag usa
  // localeCompare com modo numerico ("M2" < "M10" em ordem natural).
  const maquinasOrdenadas = React.useMemo(() => {
    if (!sort.campo) return maquinasFiltradas;
    const cmp = sort.campo === "num_serie"
      ? (a, b) => {
          const va = String(a.num_serie || "");
          const vb = String(b.num_serie || "");
          const na = parseFloat(va);
          const nb = parseFloat(vb);
          if (Number.isFinite(na) && Number.isFinite(nb) && na !== nb) return na - nb;
          return va.localeCompare(vb, undefined, { numeric: true, sensitivity: "base" });
        }
      : (a, b) => {
          const va = String(a.tag || "");
          const vb = String(b.tag || "");
          // vazios (tag null) caem pro fim em asc; localeCompare ja' garante
          // base case-insensitive + ordenacao natural ("M2" < "M10").
          if (!va && vb) return 1;
          if (va && !vb) return -1;
          return va.localeCompare(vb, undefined, { numeric: true, sensitivity: "base" });
        };
    const sorted = [...maquinasFiltradas].sort(cmp);
    return sort.direcao === "desc" ? sorted.reverse() : sorted;
  }, [maquinasFiltradas, sort]);

  const formatarDataHora = (ts) => {
    if (!ts) return "—";
    const d = new Date(ts);
    if (!Number.isFinite(d.getTime())) return "—";
    const pad = (n) => String(n).padStart(2, "0");
    return `${pad(d.getDate())}/${pad(d.getMonth() + 1)}/${d.getFullYear()} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
  };

  useEffect(() => {
    let cancelado = false;
    setErro(null);
    fetch("/api/maquinas", { credentials: "same-origin" })
      .then(r => r.ok ? r.json() : r.json().then(j => Promise.reject(j.erro || `Erro ${r.status}`)))
      .then(arr => { if (!cancelado) setMaquinas(Array.isArray(arr) ? arr : []); })
      .catch(e => { if (!cancelado) setErro(String(e || "Falha ao carregar máquinas")); });
    return () => { cancelado = true; };
  }, [refresh]);

  const toggleAtivo = async (m) => {
    const acao = m.ativo ? "desativar" : "reativar";
    if (!window.confirm(`Confirma ${acao} a máquina ${m.nome}?`)) return;
    try {
      const r = await fetch(`/api/maquinas/${m.id}`, {
        method: "PATCH",
        credentials: "same-origin",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ ativo: !m.ativo }),
      });
      if (!r.ok) {
        const j = await r.json().catch(() => ({}));
        throw new Error(j.erro || `Erro ${r.status}`);
      }
      notify(`Máquina ${acao === "desativar" ? "desativada" : "reativada"}.`, "ok");
      setRefresh(k => k + 1);
    } catch (e) {
      notify(`Falha ao ${acao}: ${e.message || e}`, "erro");
    }
  };

  const sincronizar = async () => {
    if (sincronizando) return;
    setSincronizando(true);
    try {
      const r = await fetch('/api/maquinas/sincronizar', {
        method: 'POST',
        credentials: 'same-origin',
      });
      const j = await r.json().catch(() => ({}));
      if (!r.ok) throw new Error(j.erro || `Erro ${r.status}`);
      const partes = [];
      if (j.atualizadas) partes.push(`${j.atualizadas} atualizada${j.atualizadas === 1 ? '' : 's'}`);
      if (j.criadas)     partes.push(`${j.criadas} criada${j.criadas === 1 ? '' : 's'}`);
      notify(partes.length ? `Sincronização concluída: ${partes.join(', ')}.` : 'Sincronização concluída — nada novo.', 'ok');
      setRefresh(k => k + 1);
    } catch (e) {
      notify(`Falha ao sincronizar: ${e.message || e}`, 'erro');
    } finally {
      setSincronizando(false);
    }
  };

  const excluir = async (m) => {
    if (!window.confirm(
      `Excluir definitivamente a máquina ${m.nome} (${m.num_serie})?\n\n` +
      `Esta ação não pode ser desfeita. Considere desativar em vez de excluir.`
    )) return;
    try {
      const r = await fetch(`/api/maquinas/${m.id}`, {
        method: "DELETE",
        credentials: "same-origin",
      });
      const j = await r.json().catch(() => ({}));
      if (!r.ok) throw new Error(j.erro || `Erro ${r.status}`);
      notify("Máquina excluída.", "ok");
      setRefresh(k => k + 1);
    } catch (e) {
      notify(`Falha ao excluir: ${e.message || e}`, "erro");
    }
  };

  return (
    <div className="dash-page">
      <div className="dash-pagehead">
        <div>
          <div className="dash-eyebrow">MÁQUINAS</div>
          <h1 className="dash-h1">Gerenciar máquinas</h1>
          <div className="dash-bread">
            {maquinas === null ? "Carregando…" : `${maquinas.length} máquina${maquinas.length === 1 ? "" : "s"}`}
          </div>
        </div>
        <div className="dash-actions">
          <button
            className="dash-btn-ghost"
            onClick={sincronizar}
            disabled={sincronizando}
            title="Atualiza a tag de cada num_serie pela última vista em produção e cria cadastros para num_series inéditos."
          >
            {sincronizando ? 'Sincronizando…' : 'Sincronizar'}
          </button>
          <button
            className="dash-btn-primary"
            style={{ background: accent.hex }}
            onClick={() => setModal("novo")}
          >
            Adicionar máquina <I.plus width="14" height="14"/>
          </button>
        </div>
      </div>

      <Card
        title="Máquinas cadastradas"
        right={
          <select
            className="d-select"
            value={filtroCliente}
            onChange={(e) => setFiltroCliente(e.target.value)}
            style={{ minWidth: 180 }}
            title="Filtrar por cliente"
          >
            <option value="all">Todos os clientes</option>
            {clientesUnicos.map(c => <option key={c} value={c}>{c}</option>)}
          </select>
        }
      >
        <div className="usuarios-tabela-wrap">
          {erro && <div className="usuarios-erro">{erro}</div>}
          {!erro && (
            <table className="usuarios-tabela">
              <thead>
                <tr>
                  <th
                    className="th-ordenavel"
                    onClick={() => aoOrdenarPor("tag")}
                    aria-sort={sort.campo === "tag" ? (sort.direcao === "asc" ? "ascending" : "descending") : "none"}
                  >
                    Tag
                    <span className="th-ordem-ind" aria-hidden="true">
                      {sort.campo === "tag" ? (sort.direcao === "asc" ? "▲" : "▼") : "↕"}
                    </span>
                  </th>
                  <th>Cliente</th>
                  <th
                    className="th-ordenavel"
                    onClick={() => aoOrdenarPor("num_serie")}
                    aria-sort={sort.campo === "num_serie" ? (sort.direcao === "asc" ? "ascending" : "descending") : "none"}
                  >
                    Núm. série
                    <span className="th-ordem-ind" aria-hidden="true">
                      {sort.campo === "num_serie" ? (sort.direcao === "asc" ? "▲" : "▼") : "↕"}
                    </span>
                  </th>
                  <th>SSN</th>
                  <th>Endereço</th>
                  <th>Cidade</th>
                  <th>Última edição</th>
                  <th>Senha</th>
                  <th>Status</th>
                  <th></th>
                </tr>
              </thead>
              <tbody>
                {maquinas === null && (
                  <tr><td colSpan={10} className="usuarios-empty">Carregando…</td></tr>
                )}
                {maquinas && maquinasFiltradas.length === 0 && (
                  <tr><td colSpan={10} className="usuarios-empty">
                    {maquinas.length === 0 ? "Nenhuma máquina cadastrada." : "Nenhuma máquina para o cliente selecionado."}
                  </td></tr>
                )}
                {maquinas && maquinasOrdenadas.map(m => (
                  <tr key={m.id} className={m.ativo ? "" : "is-inativo"}>
                    <td>{m.tag || "—"}</td>
                    <td>{m.cliente}</td>
                    <td className="usuarios-email">{m.num_serie}</td>
                    <td className="usuarios-email">{m.ssn || "—"}</td>
                    <td>{m.endereco || "—"}</td>
                    <td>{m.cidade || "—"}</td>
                    <td className="usuarios-data">{formatarDataHora(m.editado_endereco_em)}</td>
                    <td>
                      <span className="maquinas-senha-cell">
                        <code>{senhasVisiveis[m.id] ? (m.senha || "—") : (m.senha ? "••••••" : "—")}</code>
                        {m.senha && (
                          <button
                            type="button"
                            className="maquinas-senha-toggle"
                            onClick={() => setSenhasVisiveis(s => ({ ...s, [m.id]: !s[m.id] }))}
                            title={senhasVisiveis[m.id] ? "Ocultar" : "Mostrar"}
                          >
                            {senhasVisiveis[m.id] ? "Ocultar" : "Mostrar"}
                          </button>
                        )}
                      </span>
                    </td>
                    <td>
                      <span className={`usuarios-status ${m.ativo ? "is-on" : "is-off"}`}>
                        {m.ativo ? "Ativa" : "Inativa"}
                      </span>
                    </td>
                    <td className="usuarios-acoes">
                      <button type="button" className="usuarios-btn-acao" onClick={() => setModal(m)}>Editar</button>
                      <button type="button" className="usuarios-btn-acao" onClick={() => toggleAtivo(m)}>
                        {m.ativo ? "Desativar" : "Reativar"}
                      </button>
                      <button type="button" className="usuarios-btn-acao is-danger" onClick={() => excluir(m)}>Excluir</button>
                    </td>
                  </tr>
                ))}
              </tbody>
            </table>
          )}
        </div>
      </Card>

      {modal && (
        <MaquinaFormModal
          accent={accent}
          notify={notify}
          maquina={modal === "novo" ? null : modal}
          onClose={() => setModal(null)}
          onSalvo={() => { setModal(null); setRefresh(k => k + 1); }}
        />
      )}
    </div>
  );
}

// ─────────────────────────────────────────────
// LogAcessosPage — auditoria de login (sucesso/falha).
// admin_sistema ve todos os clientes (com coluna Cliente); admin_cliente
// ve so o proprio (coluna escondida). Operador comum nao tem acesso —
// validacao tambem no backend (requireAdminCliente).
// Retencao de 90 dias e gerenciada pelo job no boot do server.
// ─────────────────────────────────────────────
function LogAcessosPage({ accent, papel }) {
  const ehAdminSistema = papel === "admin_sistema";
  const [registros, setRegistros] = useState(null); // null = carregando
  const [total, setTotal] = useState(0);
  const [erro, setErro] = useState(null);
  const [pagina, setPagina] = useState(0); // 0-based
  const LIMITE = 50;

  // Filtros
  const [fEmail, setFEmail] = useState("");
  const [fSucesso, setFSucesso] = useState(""); // "" | "true" | "false"
  const [fTipo, setFTipo] = useState(""); // "" | "login" | "retorno"
  const [fDe, setFDe] = useState("");
  const [fAte, setFAte] = useState("");
  // refresh agrega tudo num único trigger pra dispara o fetch.
  const [refresh, setRefresh] = useState(0);

  useEffect(() => {
    let cancelado = false;
    setRegistros(null);
    setErro(null);
    const q = new URLSearchParams();
    if (fEmail) q.set("email", fEmail);
    if (fSucesso) q.set("sucesso", fSucesso);
    if (fTipo) q.set("tipo", fTipo);
    if (fDe) q.set("de", fDe);
    if (fAte) q.set("ate", fAte);
    q.set("limite", String(LIMITE));
    q.set("offset", String(pagina * LIMITE));
    fetch(`/api/log-acessos?${q.toString()}`, { credentials: "same-origin" })
      .then(r => r.ok ? r.json() : r.json().then(j => Promise.reject(j.erro || `Erro ${r.status}`)))
      .then(d => {
        if (cancelado) return;
        setRegistros(Array.isArray(d.registros) ? d.registros : []);
        setTotal(Number(d.total) || 0);
      })
      .catch(e => { if (!cancelado) setErro(String(e || "Falha ao carregar acessos.")); });
    return () => { cancelado = true; };
  }, [refresh, pagina]);

  const aplicarFiltros = () => {
    setPagina(0);
    setRefresh(k => k + 1);
  };
  const limparFiltros = () => {
    setFEmail(""); setFSucesso(""); setFTipo(""); setFDe(""); setFAte("");
    setPagina(0);
    setRefresh(k => k + 1);
  };

  const fmtDataHora = (v) => {
    if (!v) return "—";
    try {
      const d = new Date(v);
      const z = (n) => String(n).padStart(2, "0");
      return `${z(d.getDate())}/${z(d.getMonth() + 1)}/${d.getFullYear()} ${z(d.getHours())}:${z(d.getMinutes())}:${z(d.getSeconds())}`;
    } catch { return "—"; }
  };
  const labelMotivo = (m) => {
    if (!m) return "";
    const map = {
      usuario_nao_encontrado: "E-mail não cadastrado",
      usuario_inativo: "Conta inativa",
      senha_incorreta: "Senha incorreta",
      sem_empresa: "Conta sem empresa",
    };
    return map[m] || m;
  };

  const totalPaginas = Math.max(1, Math.ceil(total / LIMITE));

  return (
    <div className="dash-page">
      <div className="dash-pagehead">
        <div>
          <div className="dash-eyebrow">SEGURANÇA</div>
          <h1 className="dash-h1">Histórico de acessos</h1>
          <div className="dash-bread">
            {registros === null ? "Carregando…" : `${total} registro${total === 1 ? "" : "s"} · retenção de 90 dias`}
          </div>
        </div>
      </div>

      <Card>
        <div style={{ padding: "12px 16px", display: "flex", flexWrap: "wrap", gap: 10, alignItems: "flex-end" }}>
          <label style={{ display: "flex", flexDirection: "column", gap: 4, fontSize: 12 }}>
            <span style={{ color: "var(--muted)" }}>E-mail contém</span>
            <input
              type="text"
              value={fEmail}
              onChange={e => setFEmail(e.target.value)}
              onKeyDown={e => { if (e.key === "Enter") aplicarFiltros(); }}
              placeholder="ex: joao@"
              style={{ padding: "6px 10px", borderRadius: 6, border: "1px solid var(--border)", minWidth: 200 }}
            />
          </label>
          <label style={{ display: "flex", flexDirection: "column", gap: 4, fontSize: 12 }}>
            <span style={{ color: "var(--muted)" }}>Status</span>
            <select
              value={fSucesso}
              onChange={e => setFSucesso(e.target.value)}
              style={{ padding: "6px 10px", borderRadius: 6, border: "1px solid var(--border)" }}
            >
              <option value="">Todos</option>
              <option value="true">Só sucessos</option>
              <option value="false">Só falhas</option>
            </select>
          </label>
          <label style={{ display: "flex", flexDirection: "column", gap: 4, fontSize: 12 }}>
            <span style={{ color: "var(--muted)" }}>Tipo</span>
            <select
              value={fTipo}
              onChange={e => setFTipo(e.target.value)}
              style={{ padding: "6px 10px", borderRadius: 6, border: "1px solid var(--border)" }}
            >
              <option value="">Todos</option>
              <option value="login">Login</option>
              <option value="retorno">Retorno</option>
            </select>
          </label>
          <label style={{ display: "flex", flexDirection: "column", gap: 4, fontSize: 12 }}>
            <span style={{ color: "var(--muted)" }}>De</span>
            <input
              type="date"
              value={fDe}
              onChange={e => setFDe(e.target.value)}
              style={{ padding: "6px 10px", borderRadius: 6, border: "1px solid var(--border)" }}
            />
          </label>
          <label style={{ display: "flex", flexDirection: "column", gap: 4, fontSize: 12 }}>
            <span style={{ color: "var(--muted)" }}>Até</span>
            <input
              type="date"
              value={fAte}
              onChange={e => setFAte(e.target.value)}
              style={{ padding: "6px 10px", borderRadius: 6, border: "1px solid var(--border)" }}
            />
          </label>
          <button
            className="dash-btn-primary"
            style={{ background: accent.hex, padding: "8px 14px" }}
            onClick={aplicarFiltros}
          >
            Filtrar
          </button>
          <button
            className="dash-btn-ghost"
            style={{ padding: "8px 14px" }}
            onClick={limparFiltros}
          >
            Limpar
          </button>
        </div>

        <div style={{ padding: "0 16px 16px" }}>
          {erro ? (
            <div style={{ padding: 16, color: "#dc2626" }}>Falha: {erro}</div>
          ) : registros === null ? (
            <div style={{ padding: 16, color: "var(--muted)" }}>Carregando…</div>
          ) : registros.length === 0 ? (
            <div style={{ padding: 16, color: "var(--muted)" }}>Nenhum acesso registrado com esses filtros.</div>
          ) : (
            <div style={{ overflowX: "auto" }}>
              <table className="usuarios-tabela" style={{ width: "100%", fontSize: 13 }}>
                <thead>
                  <tr>
                    <th style={{ textAlign: "left" }}>Data / hora</th>
                    <th style={{ textAlign: "left" }}>Tipo</th>
                    <th style={{ textAlign: "left" }}>E-mail</th>
                    {ehAdminSistema && <th style={{ textAlign: "left" }}>Cliente</th>}
                    <th style={{ textAlign: "left" }}>Resultado</th>
                    <th style={{ textAlign: "left" }}>IP</th>
                    <th style={{ textAlign: "left" }}>Navegador</th>
                  </tr>
                </thead>
                <tbody>
                  {registros.map(r => (
                    <tr key={r.id}>
                      <td style={{ whiteSpace: "nowrap" }}>{fmtDataHora(r.criado_em)}</td>
                      <td style={{ whiteSpace: "nowrap" }}>
                        {r.tipo === "retorno" ? (
                          <span style={{ color: "var(--muted)", fontSize: 12 }}>Retorno</span>
                        ) : (
                          <span style={{ fontSize: 12 }}>Login</span>
                        )}
                      </td>
                      <td>{r.email}</td>
                      {ehAdminSistema && <td>{r.cliente || "—"}</td>}
                      <td>
                        {r.sucesso ? (
                          <span style={{ color: "#16a34a", fontWeight: 500 }}>Sucesso</span>
                        ) : (
                          <span style={{ color: "#dc2626" }}>
                            Falha{r.motivo_falha ? ` · ${labelMotivo(r.motivo_falha)}` : ""}
                          </span>
                        )}
                      </td>
                      <td style={{ fontFamily: "monospace", fontSize: 12 }}>{r.ip || "—"}</td>
                      <td style={{ maxWidth: 320, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }} title={r.user_agent || ""}>
                        {r.user_agent || "—"}
                      </td>
                    </tr>
                  ))}
                </tbody>
              </table>
            </div>
          )}
        </div>

        {total > LIMITE && (
          <div style={{ padding: "8px 16px 16px", display: "flex", justifyContent: "space-between", alignItems: "center" }}>
            <span style={{ fontSize: 12, color: "var(--muted)" }}>
              Página {pagina + 1} de {totalPaginas}
            </span>
            <div style={{ display: "flex", gap: 8 }}>
              <button
                className="dash-btn-ghost"
                disabled={pagina === 0}
                onClick={() => setPagina(p => Math.max(0, p - 1))}
                style={{ padding: "6px 12px", opacity: pagina === 0 ? 0.5 : 1 }}
              >
                Anterior
              </button>
              <button
                className="dash-btn-ghost"
                disabled={pagina + 1 >= totalPaginas}
                onClick={() => setPagina(p => p + 1)}
                style={{ padding: "6px 12px", opacity: pagina + 1 >= totalPaginas ? 0.5 : 1 }}
              >
                Próxima
              </button>
            </div>
          </div>
        )}
      </Card>
    </div>
  );
}

function MaquinaFormModal({ accent, notify, maquina = null, onClose, onSalvo }) {
  const editando = !!maquina;
  // Coluna `nome` ainda existe no banco (NOT NULL), mas nao tem mais
  // input dedicado: derivamos automaticamente a partir do tag, ou caimos
  // no num_serie como fallback. Em edicao, preserva o que ja estava.
  const nomeOriginal = maquina?.nome || "";
  const [tag, setTag] = useState(maquina?.tag || "");
  const [cliente, setCliente] = useState(maquina?.cliente || "");
  const [numSerie, setNumSerie] = useState(maquina?.num_serie || "");
  const [ssn, setSsn] = useState(maquina?.ssn || "");
  const [senha, setSenha] = useState(maquina?.senha || "");
  const [mostrarSenha, setMostrarSenha] = useState(false);
  const [empresas, setEmpresas] = useState([]);
  const [numSeries, setNumSeries] = useState([]);
  const [tags, setTags] = useState([]);
  const [salvando, setSalvando] = useState(false);
  const [erro, setErro] = useState(null);

  // Lista de clientes (vem das estacas + usuarios via /api/empresas)
  useEffect(() => {
    fetch("/api/empresas", { credentials: "same-origin" })
      .then(r => r.ok ? r.json() : [])
      .then(arr => setEmpresas(Array.isArray(arr) ? arr : []))
      .catch(() => {});
  }, []);

  // Quando o cliente muda, recarrega num_series do cliente. Em modo de
  // criacao tambem zera num_serie e tag pra forcar nova selecao. Em modo
  // de edicao, preserva o valor atual (pode nao estar na lista se a
  // maquina foi cadastrada manualmente antes do dropdown).
  useEffect(() => {
    if (!cliente) { setNumSeries([]); return; }
    fetch(`/api/estaca-num-series?cliente=${encodeURIComponent(cliente)}`, { credentials: "same-origin" })
      .then(r => r.ok ? r.json() : [])
      .then(arr => setNumSeries(Array.isArray(arr) ? arr : []))
      .catch(() => setNumSeries([]));
  }, [cliente]);

  // Quando num_serie muda, recarrega as tags daquele cliente+serie.
  useEffect(() => {
    if (!cliente || !numSerie) { setTags([]); return; }
    fetch(`/api/estaca-tags?cliente=${encodeURIComponent(cliente)}&num_serie=${encodeURIComponent(numSerie)}`, { credentials: "same-origin" })
      .then(r => r.ok ? r.json() : [])
      .then(arr => setTags(Array.isArray(arr) ? arr : []))
      .catch(() => setTags([]));
  }, [cliente, numSerie]);

  useEffect(() => {
    const onKey = (e) => { if (e.key === "Escape" && !salvando) onClose(); };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [onClose, salvando]);

  const aoCopiarSenha = async () => {
    if (!senha) return;
    try {
      await navigator.clipboard.writeText(senha);
      notify("Senha copiada.", "ok");
    } catch {
      notify("Não foi possível copiar — selecione manualmente.", "erro");
    }
  };

  const submit = async (e) => {
    if (e) e.preventDefault();
    setErro(null);
    if (!cliente.trim()) return setErro("Cliente é obrigatório.");
    if (!numSerie.trim()) return setErro("Número de série é obrigatório.");
    setSalvando(true);
    try {
      // Banco ainda exige nome NOT NULL. Em edicao preserva o original;
      // em criacao deriva de tag (ou num_serie como fallback).
      const nomeFinal = (nomeOriginal || tag.trim() || numSerie.trim()).trim();
      const corpo = {
        nome: nomeFinal,
        tag: tag.trim() || null,
        cliente: cliente.trim(),
        num_serie: numSerie.trim(),
        ssn: ssn.trim() || null,
        senha: senha || null,
      };
      const r = await fetch(
        editando ? `/api/maquinas/${maquina.id}` : "/api/maquinas",
        {
          method: editando ? "PATCH" : "POST",
          credentials: "same-origin",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify(corpo),
        }
      );
      const j = await r.json().catch(() => ({}));
      if (!r.ok) throw new Error(j.erro || `Erro ${r.status}`);
      notify(editando ? `Máquina ${j.nome} atualizada.` : `Máquina ${j.nome} criada.`, "ok");
      onSalvo();
    } catch (err) {
      setErro(err.message || "Falha ao salvar máquina.");
    } finally {
      setSalvando(false);
    }
  };

  return (
    <div className="report-backdrop" onClick={(e) => { if (e.target === e.currentTarget && !salvando) onClose(); }}>
      <div className="report-modal" role="dialog" aria-modal="true">
        <div className="report-head">
          <div className="report-eyebrow">{editando ? "EDITAR MÁQUINA" : "NOVA MÁQUINA"}</div>
          <h2 className="report-title">{editando ? "Editar máquina" : "Adicionar máquina"}</h2>
          <p className="report-sub">A senha é usada pelo sistema para monitoramento remoto.</p>
          <button className="report-close" aria-label="Fechar" onClick={onClose}>
            <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
              <path d="M18 6L6 18M6 6l12 12"/>
            </svg>
          </button>
        </div>
        <form className="report-body usuario-form" onSubmit={submit}>
          <label className="usuario-form-row">
            <span className="usuario-form-lbl">Cliente</span>
            <select
              className="usuario-form-inp"
              required
              autoFocus
              value={cliente}
              onChange={(e) => {
                const c = e.target.value;
                setCliente(c);
                // Limpa num_serie/tag em modo criacao pra forcar nova
                // selecao da cascata. Em edicao preserva.
                if (!editando) { setNumSerie(""); setTag(""); }
              }}
            >
              <option value="">Selecione um cliente…</option>
              {/* Em edicao, se o cliente atual nao estiver na lista (raro),
                  inclui no topo pra nao perder o valor. */}
              {editando && cliente && !empresas.includes(cliente) && (
                <option value={cliente}>{cliente}</option>
              )}
              {empresas.map(e => <option key={e} value={e}>{e}</option>)}
            </select>
          </label>
          <label className="usuario-form-row">
            <span className="usuario-form-lbl">Número de série</span>
            {/* input + datalist (em vez de <select>): a lista sugere as
                series ja vistas em producao para o cliente, mas permite
                digitar uma serie inedita — caso da maquina cadastrada
                antes de enviar o primeiro arquivo de producao. */}
            <input
              type="text"
              className="usuario-form-inp"
              required
              list="maquina-num-series"
              value={numSerie}
              disabled={!cliente}
              placeholder={cliente ? "Selecione ou digite um número de série…" : "Escolha o cliente primeiro"}
              onChange={(e) => {
                setNumSerie(e.target.value);
                if (!editando) setTag("");
              }}
            />
            <datalist id="maquina-num-series">
              {numSeries.map(ns => <option key={ns} value={ns} />)}
            </datalist>
          </label>
          <label className="usuario-form-row">
            <span className="usuario-form-lbl">Tag</span>
            {/* idem num_serie: serie inedita nao tem tags em producao,
                entao o campo precisa aceitar texto livre. */}
            <input
              type="text"
              className="usuario-form-inp"
              list="maquina-tags"
              value={tag}
              disabled={!numSerie}
              placeholder={numSerie ? "Selecione ou digite uma tag (opcional)…" : "Escolha o número de série primeiro"}
              onChange={(e) => setTag(e.target.value)}
            />
            <datalist id="maquina-tags">
              {tags.map(t => <option key={t} value={t} />)}
            </datalist>
          </label>
          <label className="usuario-form-row">
            <span className="usuario-form-lbl">SSN (opcional)</span>
            <input type="text" className="usuario-form-inp"
              value={ssn} onChange={e => setSsn(e.target.value)}/>
          </label>
          <label className="usuario-form-row">
            <span className="usuario-form-lbl">Senha (monitoramento remoto)</span>
            <div className="usuario-form-senha">
              <input
                type={mostrarSenha ? "text" : "password"}
                className="usuario-form-inp"
                value={senha}
                onChange={e => setSenha(e.target.value)}
                autoComplete="off"
              />
              <button type="button" className="usuario-form-senha-btn"
                onClick={() => setMostrarSenha(v => !v)}>
                {mostrarSenha ? "Ocultar" : "Mostrar"}
              </button>
              <button type="button" className="usuario-form-senha-btn"
                onClick={aoCopiarSenha} disabled={!senha}>
                Copiar
              </button>
            </div>
          </label>
          {erro && <div className="usuario-form-erro">{erro}</div>}
          <div className="usuario-form-acoes">
            <button type="button" className="dash-btn-ghost" onClick={onClose} disabled={salvando}>
              Cancelar
            </button>
            <button type="submit" className="dash-btn-primary" style={{ background: accent.hex }} disabled={salvando}>
              {salvando ? "Salvando…" : (editando ? "Salvar alterações" : "Criar máquina")}
            </button>
          </div>
        </form>
      </div>
    </div>
  );
}

/* ============================================================
   ROOT
============================================================ */
function App() {
  const [logged, setLogged] = useState(false);
  const [cliente, setCliente] = useState("");
  const [admin, setAdmin] = useState(false);
  const [papel, setPapel] = useState("usuario");
  const [nome, setNome] = useState("");
  const [email, setEmail] = useState("");
  // politicaAceita: tri-state. null = ainda nao sabe (carregando), true =
  // aceito (segue pro app), false = mostra tela de aceite bloqueante.
  const [politicaAceita, setPoliticaAceita] = useState(null);
  const [carregando, setCarregando] = useState(true);

  useEffect(() => {
    // Tema ja foi aplicado pelo inline script em index.html (le do
    // localStorage; default = light). O toggle no header do dashboard
    // gerencia trocas via useTema/aplicarTema.
    fetch("/api/me", { credentials: "same-origin" })
      .then(r => r.ok ? r.json() : null)
      .then(j => {
        if (j && j.cliente !== undefined) {
          setCliente(j.cliente || "");
          setAdmin(!!j.admin);
          setPapel(j.papel || (j.admin ? "admin_sistema" : "usuario"));
          setNome(j.nome || "");
          setEmail(j.email || "");
          setPoliticaAceita(!!j.politicaAceita);
          setLogged(true);
        }
      })
      .catch(() => {})
      .finally(() => setCarregando(false));
  }, []);

  // Tela de login nao foi desenhada pra dark — sempre forca light enquanto
  // o usuario nao esta autenticado. Ao logar, restaura a preferencia salva.
  useEffect(() => {
    if (logged) {
      document.documentElement.setAttribute('data-theme', lerTemaSalvo());
    } else {
      document.documentElement.setAttribute('data-theme', 'light');
    }
  }, [logged]);

  const sair = async () => {
    try { await fetch("/api/logout", { method: "POST", credentials: "same-origin" }); } catch {}
    setLogged(false);
    setCliente("");
    setAdmin(false);
    setPapel("usuario");
    setNome("");
    setEmail("");
    setPoliticaAceita(null);
  };

  if (carregando) return null;
  if (!logged) {
    return (
      <LoginScreen
        accent={ACCENT}
        onLogin={({ cliente, admin, papel: novoPapel, nome: novoNome, email: novoEmail, politicaAceita: pAceita }) => {
          setCliente(cliente || "");
          setAdmin(!!admin);
          setPapel(novoPapel || (admin ? "admin_sistema" : "usuario"));
          setNome(novoNome || "");
          setEmail(novoEmail || "");
          setPoliticaAceita(!!pAceita);
          setLogged(true);
        }}
      />
    );
  }
  // Antes de liberar o app: usuario precisa ter aceitado a politica de
  // privacidade (LGPD). Mostra tela bloqueante na primeira sessao apos
  // o cadastro (e tambem se a politica for atualizada no futuro — basta
  // dar UPDATE usuarios SET politica_aceita_em = NULL).
  if (politicaAceita === false) {
    return (
      <PoliticaScreen
        accent={ACCENT}
        onAceito={() => setPoliticaAceita(true)}
        onSair={sair}
      />
    );
  }
  // admin_sistema cai direto no Dashboard com cliente="" (ve dados de
  // todos os clientes) e usa o dropdown na sidebar pra filtrar por cliente.
  // `key` forca remount ao trocar de cliente — limpa estado local
  // (pastaAberta, planilha, etc) sem ter que zerar manualmente cada um.
  return (
    <Dashboard
      key={`cli:${cliente}`}
      accent={ACCENT}
      cliente={cliente}
      papel={papel}
      nome={nome}
      email={email}
      onClienteChange={(c) => setCliente(c || "")}
      onSair={sair}
    />
  );
}

// Expoe o modal de edicao/criacao de usuario para a tela de Configuracoes
// (configuracoes.jsx, carregada antes deste arquivo). A referencia e' lida
// no momento do render, entao o load order nao causa problema.
window.UsuarioFormModal = UsuarioFormModal;

ReactDOM.createRoot(document.getElementById("root")).render(<App />);
