# Edição de Rota — Embed API gratuita (substituição do Maps JS) — Plano de Implementação

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Substituir o Google Maps JavaScript SDK + Directions API (pagos) pela Maps Embed API (gratuita) + Google Maps URLs (sem chave) na tela `editar_rota.php`, eliminando o custo recorrente sem perder o preview da rota nem o link rápido para o app nativo.

**Architecture:** A página deixa de baixar o SDK de ~300KB e deixa de chamar a Directions API. Em vez disso, o JS local só monta duas URLs (uma para o `<iframe src>` da Embed API, outra para o `<a href>` do botão "Abrir no Google Maps") e as atualiza com debounce a cada reordenação de parada. A interface pública `window.__rotaMapaRedesenhar` é mantida, então o módulo de drag-drop não muda.

**Tech Stack:** PHP (server-side render), Vanilla JS, Google Maps Embed API (gratuita), Google Maps URLs (sem chave).

**Spec:** [`docs/superpowers/specs/2026-05-20-edicaorota-maps-embed-design.md`](../specs/2026-05-20-edicaorota-maps-embed-design.md)

---

## File Structure

| Arquivo | Mudança |
|---|---|
| `componentes/Missoes/Logistica/sinais/Edicaorota/html/editar_rota.php` | Troca `<div id="rotaMapa">` por `<iframe>`. Adiciona `<a id="abrirNoApp">`. Reescreve o `<script>` do mapa (~200 linhas → ~80 linhas). Remove `$comMapa`. |
| `componentes/Missoes/Logistica/sinais/Edicaorota/html/_layout.php` | Remove o `<script>` que carrega o Maps JS SDK. Remove `$comMapa` do contrato. Adiciona CSS para `iframe#rotaMapa` (border 0) e `.btn-abrir-app`. |

---

## Task 1: Atualizar `editar_rota.php` — HTML + JS

**Files:**
- Modify: `componentes/Missoes/Logistica/sinais/Edicaorota/html/editar_rota.php` (linhas 1-16 cabeçalho, 19-22 HTML do mapa, 151-357 script do mapa)

- [ ] **Step 1: Remover `$comMapa` do cabeçalho PHP**

Localizar (linhas 1-16):

```php
<?php
/**
 * Tela de edição da rota — drag-and-drop + botões ↑/↓ (mobile-first) +
 * mini mapa de visualização (galpão → paradas, rota via Google Directions).
 *
 * Variáveis esperadas:
 *   $token:      string
 *   $rota_atual: array  (cada item = ['id', 'pedido_id', 'sequencia', 'latitude', 'longitude', ...])
 *   $origem:     array|null  ['nome', 'latitude', 'longitude']  (filial do romaneio)
 */
$token     = $token ?? '';
$rotaAtual = $rota_atual ?? [];
$origem    = $origem ?? null;
$comMapa   = true; // ativa CSS split + carregamento do script Google Maps no _layout
ob_start();
?>
```

Substituir por:

```php
<?php
/**
 * Tela de edição da rota — drag-and-drop + botões ↑/↓ (mobile-first) +
 * mini mapa de visualização (galpão → paradas, via Google Maps Embed API).
 *
 * Variáveis esperadas:
 *   $token:      string
 *   $rota_atual: array  (cada item = ['id', 'pedido_id', 'sequencia', 'latitude', 'longitude', ...])
 *   $origem:     array|null  ['nome', 'latitude', 'longitude']  (filial do romaneio)
 */
$token     = $token ?? '';
$rotaAtual = $rota_atual ?? [];
$origem    = $origem ?? null;
ob_start();
?>
```

- [ ] **Step 2: Trocar `<div id="rotaMapa">` por `<iframe>` + botão "Abrir no app"**

Localizar (linhas 19-22):

```html
  <aside id="rotaMapaWrap" class="card-mapa">
    <div id="rotaMapa"></div>
    <div id="rotaMapaAviso" class="msg-mapa"></div>
  </aside>
```

Substituir por:

```html
  <aside id="rotaMapaWrap" class="card-mapa">
    <iframe id="rotaMapa" loading="lazy" referrerpolicy="no-referrer-when-downgrade" allowfullscreen></iframe>
    <a id="abrirNoApp" class="btn-abrir-app" target="_blank" rel="noopener" aria-disabled="true">Abrir no Google Maps</a>
    <div id="rotaMapaAviso" class="msg-mapa"></div>
  </aside>
```

- [ ] **Step 3: Substituir o script do mapa (linhas 151-357) pela versão Embed**

Localizar o bloco inteiro que começa em `<script>` na linha ~151 e termina em `</script>` na linha ~357 (o script que define `(function() { ... var aviso = ... var mapaEl = ... })();` com toda a lógica de `gm.Map`, `Marker`, `DirectionsService`, etc).

Substituir por:

```php
<script>
(function() {
  var KEYMAPS = <?= json_encode(KEYMAPSGOOGLE) ?>;
  var aviso   = document.getElementById('rotaMapaAviso');
  var iframe  = document.getElementById('rotaMapa');
  var btnApp  = document.getElementById('abrirNoApp');
  var lista   = document.getElementById('listaParadas');
  var origem  = window.__rotaOrigem;

  if (!iframe || !lista) return;

  // Sem coordenada do galpão: mapa indisponível.
  if (!origem || origem.latitude == null || origem.longitude == null) {
    iframe.style.display = 'none';
    if (btnApp) {
      btnApp.setAttribute('aria-disabled', 'true');
      btnApp.removeAttribute('href');
    }
    if (aviso) aviso.textContent = 'Mapa indisponível — galpão sem coordenada cadastrada.';
    return;
  }

  // Maps Embed API directions: limite prático de ~20 waypoints (origin + destination + intermediários).
  var LIMITE_EMBED = 20;

  function coord(p) { return p.lat + ',' + p.lng; }

  function coletarParadas() {
    var paradas = [];
    var semCoord = 0;
    Array.prototype.forEach.call(lista.querySelectorAll('li'), function (li) {
      var lat = parseFloat(li.dataset.lat);
      var lng = parseFloat(li.dataset.lng);
      if (isNaN(lat) || isNaN(lng)) { semCoord++; return; }
      paradas.push({ lat: lat, lng: lng });
    });
    return { paradas: paradas, semCoord: semCoord };
  }

  function buildDirectionsParams(paradas) {
    var origemStr = origem.latitude + ',' + origem.longitude;
    var destino   = paradas[paradas.length - 1];
    var waypoints = paradas.slice(0, -1);
    var params = '&origin=' + encodeURIComponent(origemStr)
               + '&destination=' + encodeURIComponent(coord(destino));
    if (waypoints.length) {
      params += '&waypoints=' + encodeURIComponent(waypoints.map(coord).join('|'));
    }
    return params;
  }

  function montarUrls(paradas) {
    var origemStr = origem.latitude + ',' + origem.longitude;

    // 0 paradas com coordenada: iframe mostra só o galpão (modo place).
    if (paradas.length === 0) {
      return {
        embed: 'https://www.google.com/maps/embed/v1/place'
             + '?key=' + encodeURIComponent(KEYMAPS)
             + '&q=' + encodeURIComponent(origemStr),
        app: null,
        truncado: false
      };
    }

    var truncado     = paradas.length > LIMITE_EMBED;
    var paradasEmbed = truncado ? paradas.slice(0, LIMITE_EMBED) : paradas;

    var embed = 'https://www.google.com/maps/embed/v1/directions'
              + '?key=' + encodeURIComponent(KEYMAPS)
              + '&mode=driving'
              + buildDirectionsParams(paradasEmbed);

    var app = 'https://www.google.com/maps/dir/?api=1'
            + '&travelmode=driving'
            + buildDirectionsParams(paradas);

    return { embed: embed, app: app, truncado: truncado };
  }

  function atualizarAviso(semCoord, truncado) {
    if (!aviso) return;
    var msgs = [];
    if (semCoord > 0) {
      msgs.push(semCoord === 1
        ? '1 parada não aparece no mapa (sem coordenada cadastrada).'
        : semCoord + ' paradas não aparecem no mapa (sem coordenada cadastrada).');
    }
    if (truncado) {
      msgs.push('Mapa mostra as ' + LIMITE_EMBED + ' primeiras paradas. Use "Abrir no Google Maps" para a rota completa.');
    }
    aviso.textContent = msgs.join(' ');
  }

  function redesenhar() {
    var info = coletarParadas();
    var urls = montarUrls(info.paradas);

    if (iframe.src !== urls.embed) {
      iframe.src = urls.embed;
    }

    if (urls.app) {
      btnApp.href = urls.app;
      btnApp.setAttribute('aria-disabled', 'false');
    } else {
      btnApp.removeAttribute('href');
      btnApp.setAttribute('aria-disabled', 'true');
    }

    atualizarAviso(info.semCoord, urls.truncado);
  }

  var debounceTimer = null;
  // Mesma interface pública usada pelo módulo de drag-drop (linha ~97).
  window.__rotaMapaRedesenhar = function () {
    clearTimeout(debounceTimer);
    debounceTimer = setTimeout(redesenhar, 500);
  };

  redesenhar();
})();
</script>
```

- [ ] **Step 4: Verificar lint/sintaxe**

Run:

```bash
php -l /home/goldie/www/genesis/componentes/Missoes/Logistica/sinais/Edicaorota/html/editar_rota.php
```

Expected: `No syntax errors detected`

- [ ] **Step 5: Não commitar ainda**

A mudança só vai funcionar depois do Task 2 (CSS para `iframe` sem border + botão). Avançar pro Task 2.

---

## Task 2: Atualizar `_layout.php` — remover Maps JS, adicionar CSS de iframe e botão

**Files:**
- Modify: `componentes/Missoes/Logistica/sinais/Edicaorota/html/_layout.php` (cabeçalho linhas 1-15, CSS linhas 162-174, script linhas 221-223)

- [ ] **Step 1: Remover `$comMapa` do cabeçalho**

Localizar (linhas 1-15):

```php
<?php
/**
 * Layout base mobile-first para as views públicas de edição de rota.
 *
 * Espera as variáveis:
 *   $titulo:   string
 *   $conteudo: string (HTML já renderizado)
 *   $mensagem: array|null  (['nivel' => success|danger|warning, 'texto' => ...])
 *   $comMapa:  bool|null   (true: carrega Google Maps + ativa split layout)
 */

$titulo   = $titulo   ?? 'Edição de Rota';
$conteudo = $conteudo ?? '';
$mensagem = $mensagem ?? null;
$comMapa  = $comMapa  ?? false;
?>
```

Substituir por:

```php
<?php
/**
 * Layout base mobile-first para as views públicas de edição de rota.
 *
 * Espera as variáveis:
 *   $titulo:   string
 *   $conteudo: string (HTML já renderizado)
 *   $mensagem: array|null  (['nivel' => success|danger|warning, 'texto' => ...])
 */

$titulo   = $titulo   ?? 'Edição de Rota';
$conteudo = $conteudo ?? '';
$mensagem = $mensagem ?? null;
?>
```

- [ ] **Step 2: Atualizar CSS de `#rotaMapa` para iframe + adicionar `.btn-abrir-app`**

Localizar (linhas 162-174):

```css
  #rotaMapa {
    width: 100%;
    height: 280px;
    border-radius: 8px;
    background: var(--card-2);
  }
  .msg-mapa {
    font-size: 0.78rem;
    color: var(--muted);
    margin-top: 6px;
    min-height: 1.1em;
  }
```

Substituir por:

```css
  #rotaMapa {
    width: 100%;
    height: 280px;
    border: 0;
    border-radius: 8px;
    background: var(--card-2);
    display: block;
  }
  .btn-abrir-app {
    display: block;
    margin-top: 8px;
    padding: 12px 14px;
    background: var(--primary);
    color: #0c1929;
    border-radius: 8px;
    text-align: center;
    text-decoration: none;
    font-weight: 600;
  }
  .btn-abrir-app[aria-disabled="true"] {
    opacity: 0.5;
    pointer-events: none;
  }
  .msg-mapa {
    font-size: 0.78rem;
    color: var(--muted);
    margin-top: 6px;
    min-height: 1.1em;
  }
```

- [ ] **Step 3: Remover o `<script>` do Maps JS**

Localizar (linhas 221-223):

```php
<?php if ($comMapa && defined('KEYMAPSGOOGLE')): ?>
<script async defer src="https://maps.googleapis.com/maps/api/js?key=<?= htmlspecialchars(KEYMAPSGOOGLE) ?>&libraries=geometry"></script>
<?php endif; ?>
```

Substituir por: **(remover totalmente as 3 linhas — nada no lugar).**

O bloco `</body></html>` que vem depois fica colado em `</div>` (fechamento da `.container`), igual aos outros layouts simples do projeto.

- [ ] **Step 4: Verificar lint/sintaxe**

Run:

```bash
php -l /home/goldie/www/genesis/componentes/Missoes/Logistica/sinais/Edicaorota/html/_layout.php
```

Expected: `No syntax errors detected`

- [ ] **Step 5: Confirmar que `$comMapa` não é referenciado em nenhum outro lugar**

Run:

```bash
grep -rn "comMapa" /home/goldie/www/genesis --include="*.php" 2>/dev/null
```

Expected: nenhum resultado. Se aparecer algum, parar e investigar antes de seguir.

---

## Task 3: Verificação no browser

**Pré-requisito:** O ambiente de dev (LAMP local em `dev_gx_goldie`) precisa estar rodando. A tela `editar_rota.php` é aberta via token de motorista — caminho de acesso depende do estado dos dados.

- [ ] **Step 1: Subir o ambiente local (se ainda não tiver subido)**

Run no host que serve o Apache (ou seguir o padrão do projeto). Confirmar com:

```bash
curl -s -o /dev/null -w "%{http_code}\n" http://localhost/genesis/
```

Expected: `200` ou `302` (depende da home; qualquer um != `5xx` indica que o servidor responde).

- [ ] **Step 2: Localizar uma URL válida de `editar_rota`**

Run:

```bash
grep -rn "edicaorota\|editar_rota\|EditarRota" /home/goldie/www/genesis/componentes/Missoes/Logistica/sinais/Edicaorota/Edicaorota.php 2>/dev/null | head -20
```

Inspeção esperada: identifica a rota que serve `editar_rota.php` (provavelmente `/edicaorota/{token}/editar` ou similar). Use o token de uma rota com paradas existentes no banco `dev_gx_goldie`.

Se não houver token pronto, criar um via fluxo normal de "Edição de Rota" (motorista solicita edição → recebe link com token). Documentar a URL exata aqui antes de seguir.

- [ ] **Step 3: Smoke test — galpão com coord, 3+ paradas**

Abrir a URL no Chrome. Esperado:

- O iframe carrega o Google Maps com a rota ligando galpão → paradas → destino, traçado por ruas (mesma cara que `maps.google.com/dir/`).
- O botão "Abrir no Google Maps" está visível e clicável (não opaco).
- DevTools Network: **nenhuma** request para `maps.googleapis.com/maps/api/js` nem para `maps.googleapis.com/maps/api/directions`. A única request pro Google deve ser o iframe em `www.google.com/maps/embed/v1/directions`.

Se aparecer chamada pro `maps/api/js` ou `directions` direto, parar — significa que o código antigo ficou no ar (cache do navegador?). Hard reload (`Ctrl+Shift+R`) e re-checar.

- [ ] **Step 4: Reorder dispara atualização**

Arrastar uma parada para outra posição (ou usar as setas ▲▼). Esperar 500ms. Esperado:

- O iframe recarrega com a nova ordem das paradas.
- O `href` do botão "Abrir no Google Maps" também muda (verificar via DevTools — `document.getElementById('abrirNoApp').href`).
- A lista re-numera as posições corretamente (comportamento existente).

- [ ] **Step 5: Botão "Abrir no Google Maps" funciona**

Clicar no botão. Esperado:

- Em desktop: abre `www.google.com/maps/dir/` em nova aba com a rota.
- Em mobile (testar pelo emulador do DevTools com user-agent Android ou no celular real): abre o app nativo do Google Maps com a rota carregada e botão "Iniciar".

- [ ] **Step 6: Edge case — galpão sem coord**

Construir um cenário (ou editar `$origem` no PHP temporariamente) onde `$origem['latitude']` é `null`. Esperado:

- Iframe escondido (`display: none`).
- Botão "Abrir no Google Maps" opaco e não-clicável (cursor não muda).
- Aviso `#rotaMapaAviso` mostra "Mapa indisponível — galpão sem coordenada cadastrada."

Reverter qualquer alteração temporária depois do teste.

- [ ] **Step 7: Edge case — paradas sem coord**

Cenário com algumas paradas sem `data-lat`/`data-lng` (no banco: `latitude IS NULL`). Esperado:

- Iframe mostra só as paradas com coordenada.
- Aviso mostra "N paradas não aparecem no mapa (sem coordenada cadastrada)."
- Botão "Abrir no Google Maps" também filtra (não inclui as sem coord).

- [ ] **Step 8: Edge case — mais de 20 paradas**

Cenário com 22+ paradas com coordenada. Esperado:

- Iframe mostra rota até a 20ª parada.
- Aviso mostra "Mapa mostra as 20 primeiras paradas. Use 'Abrir no Google Maps' para a rota completa."
- Botão inclui todas as paradas no URL (até o limite prático do Google Maps URLs).

Se não houver dado real com 22+ paradas, esse step pode ser feito injetando 22 `<li>` fake via Console:

```js
var lista = document.getElementById('listaParadas');
for (var i = 0; i < 22; i++) {
  var li = document.createElement('li');
  li.dataset.lat = (-23.5 + i * 0.01).toString();
  li.dataset.lng = (-46.6 + i * 0.01).toString();
  li.innerHTML = '<span class="pos">' + (i+1) + '</span><div class="info"><div class="titulo">Teste ' + i + '</div></div>';
  lista.appendChild(li);
}
window.__rotaMapaRedesenhar();
```

Aguardar 500ms, conferir comportamento. Recarregar a página depois pra limpar o teste.

- [ ] **Step 9: Confirmar que Maps Embed API está habilitada no GCP**

A pessoa com acesso ao Google Cloud Console do projeto que detém `KEYMAPSGOOGLE` precisa confirmar:

1. Abrir Google Cloud Console → APIs & Services → Library
2. Buscar "Maps Embed API" → confirmar status "Enabled"
3. (Defesa em profundidade) Buscar "Maps JavaScript API" e "Directions API" — pode deixar habilitadas até o deploy estar em produção; depois disso, desabilitar pra evitar uso acidental futuro.

Se a Embed API estiver **desabilitada**, o iframe retorna uma página de erro do Google ao invés do mapa. Habilitar (não tem custo de ativação).

- [ ] **Step 10: Commit**

Run:

```bash
cd /home/goldie/www/genesis && \
git add componentes/Missoes/Logistica/sinais/Edicaorota/html/editar_rota.php \
        componentes/Missoes/Logistica/sinais/Edicaorota/html/_layout.php \
        docs/superpowers/specs/2026-05-20-edicaorota-maps-embed-design.md \
        docs/superpowers/plans/2026-05-20-edicaorota-maps-embed.md && \
git commit -m "$(cat <<'EOF'
feat(edicaorota): trocar Maps JS pago por Embed API gratuita

Substitui o SDK do Google Maps JavaScript e a Directions API (ambos
cobrados por uso) pelo iframe da Maps Embed API (gratuita) e link
externo via Google Maps URLs (sem chave) na tela editar_rota.php.

- HTML: <div id="rotaMapa"> vira <iframe>; adiciona botão
  "Abrir no Google Maps" que abre o app nativo no mobile.
- JS: ~200 linhas de gm.Map/Marker/Directions viram ~80 linhas de
  montagem de URL com debounce de 500ms. Mantém a interface pública
  window.__rotaMapaRedesenhar pro módulo de drag-drop.
- Layout: remove o <script> do Maps JS e a flag $comMapa (não usada
  por mais nenhuma view do sinal).

Custo direto cai a zero. Página fica ~300KB mais leve.
EOF
)"
```

Expected: commit criado, hooks (se houver) passam, `git status` limpo nos 4 arquivos acima.

---

## Notas

- **Não há teste unitário** porque a mudança é puramente UI (sem lógica de domínio nem dado persistido). A verificação é manual via browser, alinhada com a seção "Testes" da spec.
- **A interface `window.__rotaMapaRedesenhar` é o contrato**: ela é chamada por `reindexar()` em `editar_rota.php:97`. Preservar o nome e a semântica (debounced redraw) é o que garante que o drag-drop continua funcionando sem mexer nele.
- **A chave `KEYMAPSGOOGLE`** continua exposta no HTML (agora no `src` do iframe em vez de no `src` do `<script>`). A restrição por HTTP referrer no console do Google é o que protege contra reuso da chave em outros sites — mantida igual.
