# Edicaorota — redesign do painel do gestor — Implementation Plan

> **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:** Reescrever a tela `/missao/logistica/edicaorota` (painel do gestor de edições de rota) seguindo padrão Genesis Metronic, enriquecendo os cards com diff visual + tempo aguardando + romaneio/filial, substituindo `prompt()` por modal, e adicionando tabs (Pendentes/Aprovadas/Rejeitadas) + busca client-side.

**Architecture:** A view atual (`html/edicaorota.php`) é reescrita do zero com layout `app-toolbar` + `app-content > card`. A `EdicaoRotaSonda` ganha hidratação extra (`hidratarParaPainelGestor`) que pré-calcula o diff de posições, conta paradas alteradas, computa tempo aguardando e resolve número/filial do romaneio via uma nova interface opcional (`EdicaoRotaReferenciaDescritorInterface`) — mantendo o Sinal genérico. O Satélite `EdicoesRotaAprovacao` passa a aceitar `status` opcional pra alimentar as tabs.

**Tech Stack:** PHP 8+ namespaced, Genesis V2 (ModelInteligencia/Sondas), Metronic theme (Bootstrap 5), Tailwind utilities (`d-flex`, `gap-*`), CSS vars do tema (`var(--bg-card)`), i18n via `show()` lendo `linguagens/<lang>.json`.

---

## File Structure

| Arquivo | Tipo | Responsabilidade |
| --- | --- | --- |
| `componentes/Missoes/Logistica/sinais/Edicaorota/Callbacks/EdicaoRotaReferenciaDescritorInterface.php` | Criar | Interface opcional que callbacks implementam pra expor número/filial da referência (não toca em `EdicaoRotaCallbackInterface`). |
| `componentes/Missoes/Logistica/sinais/Edicaorota/Adapters/Romaneio/Callbacks/RomaneioVeiculoEdicaoRotaCallback.php` | Modificar | Adiciona `implements EdicaoRotaReferenciaDescritorInterface` + método `descreverReferencia(int $id): ?array`. |
| `componentes/Missoes/Logistica/sinais/Edicaorota/Sondas/EdicaoRotaSonda.php` | Modificar | `listarPorStatus` aceita `string\|array`. Novo método público `hidratarParaPainelGestor` (cliente+endereço+diff+aguardando_minutos+romaneio). |
| `componentes/Missoes/Logistica/sinais/Edicaorota/Satelites/EdicoesRotaAprovacao.php` | Modificar | `listarPendentes` aceita `?string $status` (default `pendente_aprovacao`). |
| `componentes/Missoes/Logistica/sinais/Edicaorota/Edicaorota.php` | Modificar | `telaGestorPendentes` usa `hidratarParaPainelGestor` em vez do `hidratarLista` antigo. |
| `componentes/Missoes/Logistica/sinais/Edicaorota/html/edicaorota.php` | Reescrever | View Metronic completa com toolbar + tabs + cards diff. |
| `componentes/Missoes/Logistica/sinais/Edicaorota/html/edicaorota.js` | Criar | Lógica de tabs (chama satélite), busca client-side, abrir modal de rejeitar, dispatch de aprovar/rejeitar. |
| `componentes/Missoes/Logistica/sinais/Edicaorota/html/_modal_rejeitar.php` | Criar | Partial do modal Bootstrap (textarea de motivo + botões). |
| `linguagens/pt-br.json` | Modificar | Adicionar ~20 chaves com prefixo `edicaorota_`. |
| `linguagens/en.json` | Modificar | Espelho EN. |
| `linguagens/es.json` | Modificar | Espelho ES. |
| `componentes/Missoes/Logistica/sinais/Edicaorota/Testes/Sondas/HidratarParaPainelGestorTddTest.php` | Criar | Teste unit do cálculo de diff (entrada array → saída esperada). |
| `componentes/Missoes/Logistica/sinais/Edicaorota/Testes/Sondas/ListarPorStatusArrayTddTest.php` | Criar | Teste que `listarPorStatus` aceita array (mockando find via subclasse). |
| `componentes/Missoes/Logistica/sinais/Edicaorota/Testes/Callbacks/EdicaoRotaReferenciaDescritorTddTest.php` | Criar | Teste que callback Romaneio implementa a nova interface. |

---

## Task 1: Criar `EdicaoRotaReferenciaDescritorInterface`

**Files:**
- Create: `componentes/Missoes/Logistica/sinais/Edicaorota/Callbacks/EdicaoRotaReferenciaDescritorInterface.php`
- Test: `componentes/Missoes/Logistica/sinais/Edicaorota/Testes/Callbacks/EdicaoRotaReferenciaDescritorTddTest.php`

- [ ] **Step 1: Escrever o teste falhando**

Conteúdo do arquivo `Testes/Callbacks/EdicaoRotaReferenciaDescritorTddTest.php`:

```php
<?php

require_once __DIR__ . '/../../Callbacks/EdicaoRotaReferenciaDescritorInterface.php';

use GalaxiaMissoes\Logistica\sinais\Edicaorota\Callbacks\EdicaoRotaReferenciaDescritorInterface;

class AssertDescritor
{
    public static int $passou = 0;
    public static int $falhas = 0;

    public static function true(bool $cond, string $msg): void
    {
        if ($cond) { self::$passou++; echo "✅ [PASS] {$msg}\n"; }
        else       { self::$falhas++; echo "❌ [FAIL] {$msg}\n"; }
    }
}

echo "====================================================\n";
echo "TDD — EdicaoRotaReferenciaDescritorInterface\n";
echo "====================================================\n\n";

// A interface deve existir e exigir descreverReferencia(int): ?array
$reflection = new ReflectionClass(EdicaoRotaReferenciaDescritorInterface::class);
AssertDescritor::true($reflection->isInterface(), 'EdicaoRotaReferenciaDescritorInterface é uma interface');
AssertDescritor::true($reflection->hasMethod('descreverReferencia'), 'tem método descreverReferencia');

$metodo = $reflection->getMethod('descreverReferencia');
$params = $metodo->getParameters();
AssertDescritor::true(count($params) === 1, 'descreverReferencia recebe 1 parâmetro');
AssertDescritor::true(
    (string) $params[0]->getType() === 'int',
    'parâmetro é int'
);
AssertDescritor::true(
    (string) $metodo->getReturnType() === '?array',
    'retorno é ?array'
);

echo "\nResumo: {AssertDescritor::$passou} PASS / {AssertDescritor::$falhas} FAIL\n";
exit(AssertDescritor::$falhas > 0 ? 1 : 0);
```

- [ ] **Step 2: Rodar teste pra confirmar que falha**

Run: `cd /home/goldie/www/genesis && php componentes/Missoes/Logistica/sinais/Edicaorota/Testes/Callbacks/EdicaoRotaReferenciaDescritorTddTest.php`
Expected: erro `require_once` falha — arquivo não existe.

- [ ] **Step 3: Criar a interface**

Conteúdo de `Callbacks/EdicaoRotaReferenciaDescritorInterface.php`:

```php
<?php

namespace GalaxiaMissoes\Logistica\sinais\Edicaorota\Callbacks;

/**
 * Contrato opcional para callbacks que conseguem descrever a referência
 * (número + filial + qualquer dado humano-legível) para o painel do gestor.
 *
 * Implementação é opcional: callbacks que não implementam fazem a Sonda
 * cair em fallback ("Romaneio #N" sem filial).
 *
 * @return array{numero?: string, filial?: string, ...}|null
 *   null quando a referência não pôde ser resolvida (id inválido, registro removido, etc.).
 */
interface EdicaoRotaReferenciaDescritorInterface
{
    public function descreverReferencia(int $referenciaId): ?array;
}
```

- [ ] **Step 4: Rodar teste pra confirmar que passa**

Run: `cd /home/goldie/www/genesis && php componentes/Missoes/Logistica/sinais/Edicaorota/Testes/Callbacks/EdicaoRotaReferenciaDescritorTddTest.php`
Expected: `5 PASS / 0 FAIL`.

- [ ] **Step 5: Commit**

```bash
git add componentes/Missoes/Logistica/sinais/Edicaorota/Callbacks/EdicaoRotaReferenciaDescritorInterface.php \
        componentes/Missoes/Logistica/sinais/Edicaorota/Testes/Callbacks/EdicaoRotaReferenciaDescritorTddTest.php
git commit -m "feat(edicaorota): add EdicaoRotaReferenciaDescritorInterface for painel do gestor"
```

---

## Task 2: Implementar `descreverReferencia` no `RomaneioVeiculoEdicaoRotaCallback`

**Files:**
- Modify: `componentes/Missoes/Logistica/sinais/Edicaorota/Adapters/Romaneio/Callbacks/RomaneioVeiculoEdicaoRotaCallback.php`
- Test: integração (precisa DB) — vamos cobrir com smoke test que só checa que implementa a interface.

- [ ] **Step 1: Adicionar asserção ao teste existente da Task 1**

Edite `Testes/Callbacks/EdicaoRotaReferenciaDescritorTddTest.php` adicionando antes do "Resumo":

```php
require_once __DIR__ . '/../../Adapters/Romaneio/Callbacks/RomaneioVeiculoEdicaoRotaCallback.php';

use GalaxiaMissoes\Logistica\sinais\Edicaorota\Adapters\Romaneio\Callbacks\RomaneioVeiculoEdicaoRotaCallback;

AssertDescritor::true(
    is_subclass_of(
        RomaneioVeiculoEdicaoRotaCallback::class,
        EdicaoRotaReferenciaDescritorInterface::class
    ),
    'RomaneioVeiculoEdicaoRotaCallback implementa EdicaoRotaReferenciaDescritorInterface'
);
```

- [ ] **Step 2: Rodar teste pra confirmar que falha**

Run: `cd /home/goldie/www/genesis && php componentes/Missoes/Logistica/sinais/Edicaorota/Testes/Callbacks/EdicaoRotaReferenciaDescritorTddTest.php`
Expected: última asserção FAIL (callback não implementa).

- [ ] **Step 3: Implementar `descreverReferencia` no callback**

Em `Adapters/Romaneio/Callbacks/RomaneioVeiculoEdicaoRotaCallback.php`:

1. No `use` adicionar: `use GalaxiaMissoes\Logistica\sinais\Edicaorota\Callbacks\EdicaoRotaReferenciaDescritorInterface;`
2. Trocar a linha `class RomaneioVeiculoEdicaoRotaCallback implements EdicaoRotaCallbackInterface, EdicaoRotaDiaEfetivoResolverInterface` por:

```php
class RomaneioVeiculoEdicaoRotaCallback implements
    EdicaoRotaCallbackInterface,
    EdicaoRotaDiaEfetivoResolverInterface,
    EdicaoRotaReferenciaDescritorInterface
```

3. Adicionar método (logo após `resolverDiaEfetivo`):

```php
/**
 * Resolve número do romaneio + nome da filial para o painel do gestor.
 * Caminho: romaneios_veiculos.id → romaneio_id → romaneios.numero / filial_nome.
 *
 * Retorna null se a referência não existir mais (registro removido).
 * Retorna array com chaves: 'numero' (string), 'filial' (string|null), 'veiculo_id' (int).
 */
public function descreverReferencia(int $referenciaId): ?array
{
    if ($referenciaId <= 0) {
        return null;
    }

    $veic = (new RomaneioVeiculo())->findById($referenciaId);
    $veic = is_array($veic) ? ($veic[0] ?? null) : $veic;
    if (empty($veic) || empty($veic->romaneio_id)) {
        return null;
    }

    $romaneio = (new Romaneio())->findById((int) $veic->romaneio_id);
    $romaneio = is_array($romaneio) ? ($romaneio[0] ?? null) : $romaneio;
    if (empty($romaneio)) {
        return null;
    }

    return [
        'numero'     => (string) ($romaneio->numero ?? $romaneio->id ?? ''),
        'filial'     => $romaneio->filial_nome ?? null,
        'veiculo_id' => $referenciaId,
    ];
}
```

- [ ] **Step 4: Rodar teste pra confirmar que passa**

Run: `cd /home/goldie/www/genesis && php componentes/Missoes/Logistica/sinais/Edicaorota/Testes/Callbacks/EdicaoRotaReferenciaDescritorTddTest.php`
Expected: `6 PASS / 0 FAIL`.

- [ ] **Step 5: Verificar que o teste de aprovação existente continua passando**

Run: `cd /home/goldie/www/genesis && php componentes/Missoes/Logistica/sinais/Edicaorota/Testes/Laboratorios/CriarEdicaoRotaLaboratorioTddTest.php`
Expected: continua passando (não tocamos em `aoAprovar`/`aoRejeitar`).

- [ ] **Step 6: Commit**

```bash
git add componentes/Missoes/Logistica/sinais/Edicaorota/Adapters/Romaneio/Callbacks/RomaneioVeiculoEdicaoRotaCallback.php \
        componentes/Missoes/Logistica/sinais/Edicaorota/Testes/Callbacks/EdicaoRotaReferenciaDescritorTddTest.php
git commit -m "feat(edicaorota): RomaneioVeiculoEdicaoRotaCallback implementa descreverReferencia"
```

---

## Task 3: `EdicaoRotaSonda::listarPorStatus` aceita `string|array`

**Files:**
- Modify: `componentes/Missoes/Logistica/sinais/Edicaorota/Sondas/EdicaoRotaSonda.php:87-91`
- Test: `componentes/Missoes/Logistica/sinais/Edicaorota/Testes/Sondas/ListarPorStatusArrayTddTest.php`

- [ ] **Step 1: Escrever o teste falhando**

Conteúdo do arquivo `Testes/Sondas/ListarPorStatusArrayTddTest.php`:

```php
<?php

require_once __DIR__ . '/../../Sondas/EdicaoRotaSonda.php';

use GalaxiaMissoes\Logistica\sinais\Edicaorota\Sondas\EdicaoRotaSonda;

class AssertListagem
{
    public static int $passou = 0;
    public static int $falhas = 0;

    public static function true(bool $cond, string $msg): void
    {
        if ($cond) { self::$passou++; echo "✅ [PASS] {$msg}\n"; }
        else       { self::$falhas++; echo "❌ [FAIL] {$msg}\n"; }
    }
}

echo "====================================================\n";
echo "TDD — EdicaoRotaSonda::listarPorStatus aceita string|array\n";
echo "====================================================\n\n";

$reflection = new ReflectionMethod(EdicaoRotaSonda::class, 'listarPorStatus');
$params     = $reflection->getParameters();
$tipo       = (string) ($params[0]->getType() ?? '');

AssertListagem::true(
    $tipo === 'string|array' || $tipo === 'array|string',
    "tipo do parâmetro \$status é union string|array (atual: '{$tipo}')"
);
AssertListagem::true(
    (string) $reflection->getReturnType() === 'array',
    'retorno continua array'
);

echo "\nResumo: " . AssertListagem::$passou . " PASS / " . AssertListagem::$falhas . " FAIL\n";
exit(AssertListagem::$falhas > 0 ? 1 : 0);
```

- [ ] **Step 2: Rodar teste pra confirmar que falha**

Run: `cd /home/goldie/www/genesis && php componentes/Missoes/Logistica/sinais/Edicaorota/Testes/Sondas/ListarPorStatusArrayTddTest.php`
Expected: FAIL — tipo atual é `string`.

- [ ] **Step 3: Modificar a assinatura na Sonda**

Em `Sondas/EdicaoRotaSonda.php` substituir todo o método `listarPorStatus` (linhas ~87-91):

```php
/**
 * Lista edições pelo status (string única) ou por um conjunto de status (array).
 *
 * @param  string|string[] $status
 * @return array
 */
public function listarPorStatus(string|array $status): array
{
    if (is_array($status)) {
        $status = array_values(array_filter(array_map('strval', $status), fn($s) => $s !== ''));
        if (empty($status)) {
            return [];
        }
        // ModelInteligencia::find usa placeholders nomeados (st1, st2, ...).
        $placeholders = [];
        $params       = [];
        foreach ($status as $i => $valor) {
            $key            = "st{$i}";
            $placeholders[] = ":{$key}";
            $params[]       = "{$key}={$valor}";
        }
        $where  = 'status IN (' . implode(',', $placeholders) . ') ORDER BY data_cadastro ASC';
        $bind   = implode('&', $params);
        $resultado = $this->find($where, $bind)->fetch(true);
        return is_array($resultado) ? $resultado : [];
    }

    $resultado = $this->find("status = :st ORDER BY data_cadastro ASC", "st={$status}")->fetch(true);
    return is_array($resultado) ? $resultado : [];
}
```

- [ ] **Step 4: Rodar teste pra confirmar que passa**

Run: `cd /home/goldie/www/genesis && php componentes/Missoes/Logistica/sinais/Edicaorota/Testes/Sondas/ListarPorStatusArrayTddTest.php`
Expected: `2 PASS / 0 FAIL`.

- [ ] **Step 5: Commit**

```bash
git add componentes/Missoes/Logistica/sinais/Edicaorota/Sondas/EdicaoRotaSonda.php \
        componentes/Missoes/Logistica/sinais/Edicaorota/Testes/Sondas/ListarPorStatusArrayTddTest.php
git commit -m "feat(edicaorota): listarPorStatus aceita string|array para filtros do painel"
```

---

## Task 4: `EdicaoRotaSonda::hidratarParaPainelGestor` (cálculo de diff)

A lógica de cálculo de diff é PHP puro — testável sem DB.

**Files:**
- Modify: `componentes/Missoes/Logistica/sinais/Edicaorota/Sondas/EdicaoRotaSonda.php` (adicionar método)
- Test: `componentes/Missoes/Logistica/sinais/Edicaorota/Testes/Sondas/HidratarParaPainelGestorTddTest.php`

- [ ] **Step 1: Escrever o teste falhando**

Conteúdo do arquivo `Testes/Sondas/HidratarParaPainelGestorTddTest.php`:

```php
<?php

require_once __DIR__ . '/../../Sondas/EdicaoRotaSonda.php';

use GalaxiaMissoes\Logistica\sinais\Edicaorota\Sondas\EdicaoRotaSonda;

class AssertHidrat
{
    public static int $passou = 0;
    public static int $falhas = 0;

    public static function eq($esperado, $atual, string $msg): void
    {
        if ($esperado === $atual) {
            self::$passou++; echo "✅ [PASS] {$msg}\n";
        } else {
            self::$falhas++;
            echo "❌ [FAIL] {$msg}\n   esperado: " . json_encode($esperado) . "\n   atual:    " . json_encode($atual) . "\n";
        }
    }
}

echo "====================================================\n";
echo "TDD — EdicaoRotaSonda::calcularDiff (PHP puro)\n";
echo "====================================================\n\n";

// Subclasse de teste que isola só a função pura de diff (sem hidratação que
// puxa info de cliente via DB). O método público hidratarParaPainelGestor
// delega o cálculo de diff para calcularDiff (testado aqui).
class SondaTeste extends EdicaoRotaSonda
{
    public function diffPublico(array $atual, array $proposta): array
    {
        return $this->calcularDiff($atual, $proposta);
    }
}

$sonda = new SondaTeste();

// --- Caso 1: rota inalterada (todas pos iguais) ---
$atual    = [['pedido_id' => 10], ['pedido_id' => 20], ['pedido_id' => 30]];
$proposta = [['pedido_id' => 10], ['pedido_id' => 20], ['pedido_id' => 30]];
$diff     = $sonda->diffPublico($atual, $proposta);

AssertHidrat::eq(3, count($diff['paradas']), 'caso 1: retorna 3 paradas');
AssertHidrat::eq(0, $diff['paradas_alteradas'], 'caso 1: 0 paradas alteradas');
AssertHidrat::eq(0, $diff['paradas'][0]['delta'], 'caso 1: parada 1 delta=0');
AssertHidrat::eq(1, $diff['paradas'][0]['pos_atual'], 'caso 1: parada 1 pos_atual=1');
AssertHidrat::eq(1, $diff['paradas'][0]['pos_nova'],  'caso 1: parada 1 pos_nova=1');

// --- Caso 2: reordenou 3 paradas, 1 ficou no lugar ---
$atual    = [['pedido_id' => 10], ['pedido_id' => 20], ['pedido_id' => 30], ['pedido_id' => 40]];
$proposta = [['pedido_id' => 30], ['pedido_id' => 10], ['pedido_id' => 20], ['pedido_id' => 40]];
$diff     = $sonda->diffPublico($atual, $proposta);

AssertHidrat::eq(4, count($diff['paradas']), 'caso 2: 4 paradas');
AssertHidrat::eq(3, $diff['paradas_alteradas'], 'caso 2: 3 paradas alteradas (40 ficou)');

// Ordem do retorno = ordem da proposta. pedido 30 estava em pos 3, agora em 1 → delta -2.
AssertHidrat::eq(30, $diff['paradas'][0]['pedido_id'], 'caso 2: 1ª parada da proposta = pedido 30');
AssertHidrat::eq(3,  $diff['paradas'][0]['pos_atual'], 'caso 2: pedido 30 pos_atual=3');
AssertHidrat::eq(1,  $diff['paradas'][0]['pos_nova'],  'caso 2: pedido 30 pos_nova=1');
AssertHidrat::eq(-2, $diff['paradas'][0]['delta'],     'caso 2: pedido 30 delta=-2 (subiu)');

// pedido 40: pos 4 → 4 → delta 0
AssertHidrat::eq(0,  $diff['paradas'][3]['delta'],     'caso 2: pedido 40 delta=0 (ficou)');

// --- Caso 3: proposta vazia ---
$diff = $sonda->diffPublico([['pedido_id' => 1]], []);
AssertHidrat::eq(0, count($diff['paradas']),        'caso 3: proposta vazia → 0 paradas');
AssertHidrat::eq(0, $diff['paradas_alteradas'],     'caso 3: paradas_alteradas=0');

// --- Caso 4: parada nova na proposta (não existia em rota_atual) ---
$atual    = [['pedido_id' => 10]];
$proposta = [['pedido_id' => 10], ['pedido_id' => 99]];
$diff     = $sonda->diffPublico($atual, $proposta);

AssertHidrat::eq(2,    count($diff['paradas']), 'caso 4: 2 paradas');
AssertHidrat::eq(null, $diff['paradas'][1]['pos_atual'], 'caso 4: pedido novo pos_atual=null');
AssertHidrat::eq(2,    $diff['paradas'][1]['pos_nova'],  'caso 4: pedido novo pos_nova=2');
AssertHidrat::eq(null, $diff['paradas'][1]['delta'],     'caso 4: pedido novo delta=null (sem comparação)');

echo "\nResumo: " . AssertHidrat::$passou . " PASS / " . AssertHidrat::$falhas . " FAIL\n";
exit(AssertHidrat::$falhas > 0 ? 1 : 0);
```

- [ ] **Step 2: Rodar teste pra confirmar que falha**

Run: `cd /home/goldie/www/genesis && php componentes/Missoes/Logistica/sinais/Edicaorota/Testes/Sondas/HidratarParaPainelGestorTddTest.php`
Expected: erro fatal — método `calcularDiff` não existe.

- [ ] **Step 3: Adicionar `calcularDiff` (protected) e `hidratarParaPainelGestor` (public) na Sonda**

Em `Sondas/EdicaoRotaSonda.php`, adicione antes do método `save()` (logo após `listarPorStatus`):

```php
/**
 * Hidrata um registro do listagem para o painel do gestor com tudo que a view precisa:
 *   - rota_atual / rota_proposta decodificadas
 *   - paradas da proposta enriquecidas com cliente+endereço (via buscarInfoParadas)
 *   - diff de posições por parada (pos_atual, pos_nova, delta)
 *   - paradas_alteradas (count)
 *   - aguardando_minutos (now - data_cadastro)
 *   - status_label / status_css (via EdicaoRotaStatus enum)
 *   - referencia (numero/filial via descritor opcional do callback)
 *
 * @param  object|array $registro  uma linha de edicoes_rota
 * @return array
 */
public function hidratarParaPainelGestor($registro): array
{
    $r = (array) $registro;
    $rotaAtual    = json_decode($r['rota_atual']    ?? '[]', true) ?: [];
    $rotaProposta = json_decode($r['rota_proposta'] ?? '[]', true) ?: [];

    $diff = $this->calcularDiff($rotaAtual, $rotaProposta);

    // Enriquece paradas do diff com cliente/endereço
    $pedidoIds = array_filter(array_map(
        fn($p) => (int) ($p['pedido_id'] ?? 0),
        $diff['paradas']
    ));
    if (!empty($pedidoIds)) {
        $infoMap = $this->buscarInfoParadas(array_values($pedidoIds));
        $diff['paradas'] = array_map(function ($p) use ($infoMap) {
            $pid = (int) ($p['pedido_id'] ?? 0);
            if ($pid && isset($infoMap[$pid])) {
                return array_merge($infoMap[$pid], $p);
            }
            return $p;
        }, $diff['paradas']);
    }

    $r['rota_atual']        = $rotaAtual;
    $r['rota_proposta']     = $rotaProposta;
    $r['paradas']           = $diff['paradas'];
    $r['paradas_alteradas'] = $diff['paradas_alteradas'];
    $r['aguardando_minutos'] = $this->calcularAguardandoMinutos($r['data_cadastro'] ?? null);

    $statusEnum = \GalaxiaMissoes\Logistica\sinais\Edicaorota\Enums\EdicaoRotaStatus::tryFrom($r['status'] ?? '');
    $r['status_label'] = $statusEnum?->label() ?? ($r['status'] ?? '');
    $r['status_css']   = $statusEnum?->cssClass() ?? 'draft';

    $r['referencia'] = $this->descreverReferenciaSeDisponivel(
        (string) ($r['callback_modulo'] ?? ''),
        (int)    ($r['callback_referencia_id'] ?? 0)
    );

    return $r;
}

/**
 * Calcula diff posicional entre rota_atual e rota_proposta.
 * Saída: ['paradas' => [...], 'paradas_alteradas' => int].
 * Cada item em paradas: ['pedido_id', 'pos_atual'|null, 'pos_nova', 'delta'|null].
 */
protected function calcularDiff(array $rotaAtual, array $rotaProposta): array
{
    // Indexa pedido_id → posição (1-based) na rota_atual.
    $mapaAtual = [];
    foreach ($rotaAtual as $idx => $p) {
        $pid = (int) ($p['pedido_id'] ?? 0);
        if ($pid > 0) {
            $mapaAtual[$pid] = $idx + 1;
        }
    }

    $paradas    = [];
    $alteradas  = 0;
    foreach ($rotaProposta as $idx => $p) {
        $pid     = (int) ($p['pedido_id'] ?? 0);
        $posNova = $idx + 1;
        $posAtual = $mapaAtual[$pid] ?? null;
        $delta    = $posAtual === null ? null : ($posNova - $posAtual);

        if ($delta !== null && $delta !== 0) {
            $alteradas++;
        }

        $paradas[] = [
            'pedido_id' => $pid,
            'pos_atual' => $posAtual,
            'pos_nova'  => $posNova,
            'delta'     => $delta,
        ];
    }

    return ['paradas' => $paradas, 'paradas_alteradas' => $alteradas];
}

/**
 * Diferença em minutos entre agora e o timestamp informado.
 * Retorna 0 se inválido.
 */
protected function calcularAguardandoMinutos(?string $dataCadastro): int
{
    if (empty($dataCadastro)) {
        return 0;
    }
    try {
        $ts = (new \DateTime($dataCadastro))->getTimestamp();
        $diff = time() - $ts;
        return $diff > 0 ? (int) floor($diff / 60) : 0;
    } catch (\Throwable $e) {
        return 0;
    }
}

/**
 * Resolve número/filial via callback registrado (se implementar a interface descritora).
 * Retorna null quando não há descritor — a view cai em fallback.
 */
protected function descreverReferenciaSeDisponivel(string $modulo, int $referenciaId): ?array
{
    if ($modulo === '' || $referenciaId <= 0) {
        return null;
    }
    if (!\GalaxiaMissoes\Logistica\sinais\Edicaorota\Callbacks\EdicaoRotaCallbackRegistry::temRegistro($modulo)) {
        return null;
    }
    $callback = \GalaxiaMissoes\Logistica\sinais\Edicaorota\Callbacks\EdicaoRotaCallbackRegistry::resolver($modulo);
    if (!($callback instanceof \GalaxiaMissoes\Logistica\sinais\Edicaorota\Callbacks\EdicaoRotaReferenciaDescritorInterface)) {
        return null;
    }
    return $callback->descreverReferencia($referenciaId);
}
```

- [ ] **Step 4: Rodar teste pra confirmar que passa**

Run: `cd /home/goldie/www/genesis && php componentes/Missoes/Logistica/sinais/Edicaorota/Testes/Sondas/HidratarParaPainelGestorTddTest.php`
Expected: todos PASS (12 asserções).

- [ ] **Step 5: Commit**

```bash
git add componentes/Missoes/Logistica/sinais/Edicaorota/Sondas/EdicaoRotaSonda.php \
        componentes/Missoes/Logistica/sinais/Edicaorota/Testes/Sondas/HidratarParaPainelGestorTddTest.php
git commit -m "feat(edicaorota): Sonda::hidratarParaPainelGestor com diff/aguardando/referencia"
```

---

## Task 5: Satélite usa `hidratarParaPainelGestor` (sem mudança de assinatura)

**Files:**
- Modify: `componentes/Missoes/Logistica/sinais/Edicaorota/Satelites/EdicoesRotaAprovacao.php`

As tabs do painel são links `<a href=?status=>` que disparam reload SSR (Task 6 hydrata `$_GET['status']`). Portanto o satélite **não precisa** aceitar `status` opcional — basta usar a nova hidratação no `listarPendentes` e `detalhar`.

- [ ] **Step 1: Reescrever `listarPendentes` (mesma assinatura, hidratação nova)**

Trocar o método `listarPendentes` inteiro por:

```php
private function listarPendentes(): array
{
    $sonda = new EdicaoRotaSonda();
    $lista = $sonda->listarPorStatus(EdicaoRotaStatus::PENDENTE_APROVACAO->value);

    return [
        'notification_class' => 'success',
        'pendentes'          => array_map(
            fn($r) => $sonda->hidratarParaPainelGestor($r),
            $lista
        ),
    ];
}
```

- [ ] **Step 2: Atualizar `detalhar` e remover `hidratarEdicao` (obsoleto)**

Em `detalhar()` trocar:

```php
'edicao' => $this->hidratarEdicao($registro),
```

por:

```php
'edicao' => (new EdicaoRotaSonda())->hidratarParaPainelGestor($registro),
```

Apagar o método `hidratarEdicao` (linhas ~79-87). Verificar com grep:

Run: `cd /home/goldie/www/genesis && grep -rn 'hidratarEdicao' componentes/Missoes/Logistica/sinais/Edicaorota/`
Expected: zero ocorrências.

- [ ] **Step 3: Verificar PHP syntax**

Run: `php -l componentes/Missoes/Logistica/sinais/Edicaorota/Satelites/EdicoesRotaAprovacao.php`
Expected: `No syntax errors detected`.

- [ ] **Step 4: Commit**

```bash
git add componentes/Missoes/Logistica/sinais/Edicaorota/Satelites/EdicoesRotaAprovacao.php
git commit -m "refactor(edicaorota): satelite usa hidratarParaPainelGestor"
```

---

## Task 6: `Edicaorota::telaGestorPendentes` usa hidratação nova

**Files:**
- Modify: `componentes/Missoes/Logistica/sinais/Edicaorota/Edicaorota.php:157-169` e `:379-387`

- [ ] **Step 1: Substituir `telaGestorPendentes`**

Trocar o método `telaGestorPendentes` (linhas ~157-169) por:

```php
private function telaGestorPendentes(array $data): self
{
    // Status filtrado via ?status= (allowlist). Default = pendente_aprovacao.
    $statusPermitidos = [
        EdicaoRotaStatus::PENDENTE_APROVACAO->value,
        EdicaoRotaStatus::APROVADA->value,
        EdicaoRotaStatus::REJEITADA->value,
    ];
    $statusReq = (string) ($_GET['status'] ?? '');
    $statusAtivo = in_array($statusReq, $statusPermitidos, true)
        ? $statusReq
        : EdicaoRotaStatus::PENDENTE_APROVACAO->value;

    $sonda = new EdicaoRotaSonda();
    $lista = $sonda->listarPorStatus($statusAtivo);

    $pendentes = array_map(
        fn($r) => $sonda->hidratarParaPainelGestor($r),
        $lista
    );

    $resposta['html'] = $this->galaxiaRoute->visual('html/edicaorota', [
        'pendentes'    => $pendentes,
        'satelite'     => 'EdicoesRotaAprovacao',
        'status_ativo' => $statusAtivo,
    ], []);

    $this->galaxiaRoute->addResponse($this->routeId, $resposta);
    $this->response = $this->galaxiaRoute->getResponse($this->routeId);
    return $this;
}
```

Confirma que os valores do enum existem: o Enum `EdicaoRotaStatus` deve ter casos `APROVADA` e `REJEITADA` (além de `PENDENTE_APROVACAO`). Caso algum não exista, ajustar a allowlist para usar os valores corretos.

Run: `cd /home/goldie/www/genesis && grep -E 'case [A-Z]+' componentes/Missoes/Logistica/sinais/Edicaorota/Enums/EdicaoRotaStatus.php`
Expected: ver os casos do enum; ajustar a allowlist se nome diferir.

- [ ] **Step 2: Remover o método `hidratarLista` (obsoleto)**

Apagar o método `hidratarLista` em `Edicaorota.php` (linhas ~379-387). Verificar com grep que ninguém mais usa:

Run: `cd /home/goldie/www/genesis && grep -rn 'hidratarLista' componentes/Missoes/Logistica/sinais/Edicaorota/`
Expected: zero ocorrências após remoção.

- [ ] **Step 3: Verificar PHP syntax**

Run: `php -l componentes/Missoes/Logistica/sinais/Edicaorota/Edicaorota.php`
Expected: `No syntax errors detected`.

- [ ] **Step 4: Commit**

```bash
git add componentes/Missoes/Logistica/sinais/Edicaorota/Edicaorota.php
git commit -m "refactor(edicaorota): telaGestorPendentes usa hidratarParaPainelGestor"
```

---

## Task 7: Adicionar chaves i18n nos 3 idiomas

**Files:**
- Modify: `linguagens/pt-br.json`
- Modify: `linguagens/en.json`
- Modify: `linguagens/es.json`

- [ ] **Step 1: Conferir formato dos arquivos de idioma**

Run: `head -5 linguagens/pt-br.json`
Expected: JSON com chave/valor (será inserido no objeto raiz).

- [ ] **Step 2: Adicionar chaves em `pt-br.json`**

Inserir antes do `}` final do objeto raiz (deixar vírgula correta na chave anterior):

```json
  "edicaorota_titulo": "Edições de rota",
  "edicaorota_breadcrumb_inicio": "Início",
  "edicaorota_breadcrumb_missao": "Logística",
  "edicaorota_tab_pendentes": "Pendentes",
  "edicaorota_tab_aprovadas": "Aprovadas",
  "edicaorota_tab_rejeitadas": "Rejeitadas",
  "edicaorota_busca_placeholder": "Buscar motorista ou romaneio…",
  "edicaorota_edicao_numero": "Edição #",
  "edicaorota_solicitado_ha": "há",
  "edicaorota_solicitado_minutos": "min",
  "edicaorota_solicitado_horas": "h",
  "edicaorota_romaneio_label": "Romaneio",
  "edicaorota_filial_label": "Filial",
  "edicaorota_paradas_mudaram_singular": "1 de {total} paradas mudou",
  "edicaorota_paradas_mudaram_plural": "{n} de {total} paradas mudaram",
  "edicaorota_ver_inalteradas": "Ver paradas inalteradas",
  "edicaorota_ocultar_inalteradas": "Ocultar inalteradas",
  "edicaorota_aprovar": "Aprovar",
  "edicaorota_rejeitar": "Rejeitar",
  "edicaorota_modal_rejeitar_titulo": "Rejeitar edição",
  "edicaorota_modal_motivo_label": "Motivo da rejeição (enviado ao motorista)",
  "edicaorota_modal_motivo_invalido": "Informe pelo menos 3 caracteres.",
  "edicaorota_modal_cancelar": "Cancelar",
  "edicaorota_modal_confirmar": "Rejeitar",
  "edicaorota_empty": "Nenhuma edição encontrada."
```

- [ ] **Step 3: Adicionar chaves em `en.json`**

Mesma estrutura, traduzido:

```json
  "edicaorota_titulo": "Route edits",
  "edicaorota_breadcrumb_inicio": "Home",
  "edicaorota_breadcrumb_missao": "Logistics",
  "edicaorota_tab_pendentes": "Pending",
  "edicaorota_tab_aprovadas": "Approved",
  "edicaorota_tab_rejeitadas": "Rejected",
  "edicaorota_busca_placeholder": "Search driver or manifest…",
  "edicaorota_edicao_numero": "Edit #",
  "edicaorota_solicitado_ha": "ago",
  "edicaorota_solicitado_minutos": "min",
  "edicaorota_solicitado_horas": "h",
  "edicaorota_romaneio_label": "Manifest",
  "edicaorota_filial_label": "Branch",
  "edicaorota_paradas_mudaram_singular": "1 of {total} stops changed",
  "edicaorota_paradas_mudaram_plural": "{n} of {total} stops changed",
  "edicaorota_ver_inalteradas": "Show unchanged stops",
  "edicaorota_ocultar_inalteradas": "Hide unchanged",
  "edicaorota_aprovar": "Approve",
  "edicaorota_rejeitar": "Reject",
  "edicaorota_modal_rejeitar_titulo": "Reject edit",
  "edicaorota_modal_motivo_label": "Rejection reason (sent to driver)",
  "edicaorota_modal_motivo_invalido": "Enter at least 3 characters.",
  "edicaorota_modal_cancelar": "Cancel",
  "edicaorota_modal_confirmar": "Reject",
  "edicaorota_empty": "No edits found."
```

- [ ] **Step 4: Adicionar chaves em `es.json`**

```json
  "edicaorota_titulo": "Ediciones de ruta",
  "edicaorota_breadcrumb_inicio": "Inicio",
  "edicaorota_breadcrumb_missao": "Logística",
  "edicaorota_tab_pendentes": "Pendientes",
  "edicaorota_tab_aprovadas": "Aprobadas",
  "edicaorota_tab_rejeitadas": "Rechazadas",
  "edicaorota_busca_placeholder": "Buscar conductor o manifiesto…",
  "edicaorota_edicao_numero": "Edición #",
  "edicaorota_solicitado_ha": "hace",
  "edicaorota_solicitado_minutos": "min",
  "edicaorota_solicitado_horas": "h",
  "edicaorota_romaneio_label": "Manifiesto",
  "edicaorota_filial_label": "Sucursal",
  "edicaorota_paradas_mudaram_singular": "1 de {total} paradas cambió",
  "edicaorota_paradas_mudaram_plural": "{n} de {total} paradas cambiaron",
  "edicaorota_ver_inalteradas": "Ver paradas sin cambios",
  "edicaorota_ocultar_inalteradas": "Ocultar sin cambios",
  "edicaorota_aprovar": "Aprobar",
  "edicaorota_rejeitar": "Rechazar",
  "edicaorota_modal_rejeitar_titulo": "Rechazar edición",
  "edicaorota_modal_motivo_label": "Motivo del rechazo (enviado al conductor)",
  "edicaorota_modal_motivo_invalido": "Ingrese al menos 3 caracteres.",
  "edicaorota_modal_cancelar": "Cancelar",
  "edicaorota_modal_confirmar": "Rechazar",
  "edicaorota_empty": "No se encontraron ediciones."
```

- [ ] **Step 5: Validar JSON dos 3 arquivos**

Run: `for f in linguagens/pt-br.json linguagens/en.json linguagens/es.json; do echo "--- $f"; php -r "json_decode(file_get_contents('$f'), true, 512, JSON_THROW_ON_ERROR); echo 'OK';" || echo "BROKEN: $f"; done`
Expected: três `OK`.

- [ ] **Step 6: Commit**

```bash
git add linguagens/pt-br.json linguagens/en.json linguagens/es.json
git commit -m "i18n(edicaorota): chaves do painel do gestor em pt-br/en/es"
```

---

## Task 8: Criar `html/_modal_rejeitar.php`

**Files:**
- Create: `componentes/Missoes/Logistica/sinais/Edicaorota/html/_modal_rejeitar.php`

- [ ] **Step 1: Criar o partial**

Conteúdo de `html/_modal_rejeitar.php`:

```php
<?php
/**
 * Modal único de rejeição de edição de rota.
 * Renderizado uma vez por carga da view do painel.
 * JS pareado injeta `data-edicao-id` ao abrir.
 */
?>
<div class="modal fade" id="edicaorota_modal_rejeitar" tabindex="-1" aria-hidden="true">
  <div class="modal-dialog modal-dialog-centered">
    <div class="modal-content" style="background: var(--bg-card, #1a1a2e); color: var(--text-primary, #e5e7eb);">
      <div class="modal-header">
        <h5 class="modal-title">
          <?= show('edicaorota_modal_rejeitar_titulo') ?>
          <span id="edicaorota_modal_numero" class="text-muted ms-1"></span>
        </h5>
        <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
      </div>
      <div class="modal-body">
        <label for="edicaorota_motivo" class="form-label fw-semibold">
          <?= show('edicaorota_modal_motivo_label') ?>
        </label>
        <textarea
          id="edicaorota_motivo"
          class="form-control"
          rows="4"
          minlength="3"
          required
          style="background: var(--bg-primary, #05050f); color: var(--text-primary, #e5e7eb);"></textarea>
        <div id="edicaorota_motivo_erro" class="invalid-feedback d-none">
          <?= show('edicaorota_modal_motivo_invalido') ?>
        </div>
      </div>
      <div class="modal-footer">
        <button type="button" class="btn btn-light" data-bs-dismiss="modal">
          <?= show('edicaorota_modal_cancelar') ?>
        </button>
        <button type="button" id="edicaorota_btn_confirmar_rejeitar" class="btn btn-danger">
          <?= show('edicaorota_modal_confirmar') ?>
        </button>
      </div>
    </div>
  </div>
</div>
```

- [ ] **Step 2: Verificar PHP syntax**

Run: `php -l componentes/Missoes/Logistica/sinais/Edicaorota/html/_modal_rejeitar.php`
Expected: `No syntax errors detected`.

- [ ] **Step 3: Commit**

```bash
git add componentes/Missoes/Logistica/sinais/Edicaorota/html/_modal_rejeitar.php
git commit -m "feat(edicaorota): modal de rejeição substitui prompt() do browser"
```

---

## Task 9: Reescrever `html/edicaorota.php` com layout Metronic + diff visual

**Files:**
- Modify (rewrite): `componentes/Missoes/Logistica/sinais/Edicaorota/html/edicaorota.php`

- [ ] **Step 1: Reescrever o arquivo completo**

Substituir o conteúdo inteiro de `html/edicaorota.php` por:

```php
<?php
/**
 * Painel do gestor — lista de edições de rota com diff visual.
 *
 * Espera $pendentes (hidratados via EdicaoRotaSonda::hidratarParaPainelGestor),
 * $satelite (default: 'EdicoesRotaAprovacao') e $status_ativo (default 'pendente_aprovacao').
 */
$pendentes    = $pendentes ?? [];
$satelite     = $satelite  ?? 'EdicoesRotaAprovacao';
$statusAtivo  = $status_ativo ?? 'pendente_aprovacao';
$tabs = [
    'pendente_aprovacao' => 'edicaorota_tab_pendentes',
    'aprovada'           => 'edicaorota_tab_aprovadas',
    'rejeitada'          => 'edicaorota_tab_rejeitadas',
];

/**
 * Helper: monta string "há X min" / "há Xh Ym" sem expor strings cruas.
 */
$formatarTempoAguardando = function (int $minutos): string {
    if ($minutos <= 0) return '0 ' . show('edicaorota_solicitado_minutos');
    if ($minutos < 60) return $minutos . ' ' . show('edicaorota_solicitado_minutos');
    $h = intdiv($minutos, 60);
    $m = $minutos % 60;
    if ($m === 0) return $h . ' ' . show('edicaorota_solicitado_horas');
    return $h . ' ' . show('edicaorota_solicitado_horas') . ' ' . $m . ' ' . show('edicaorota_solicitado_minutos');
};
?>
<div class="d-flex flex-column flex-column-fluid px-0">

  <!--begin::Toolbar-->
  <div id="kt_app_toolbar" class="app-toolbar py-3 py-lg-6">
    <div class="app-container container-xxl d-flex flex-stack px-0">
      <div class="page-title d-flex flex-column justify-content-center me-3">
        <h1 class="page-heading text-gray-900 fw-bold fs-1 my-0">
          <?= show('edicaorota_titulo') ?>
        </h1>
        <ul class="breadcrumb breadcrumb-separatorless fw-semibold fs-7 my-0 pt-1">
          <li class="breadcrumb-item text-muted">
            <a href="<?= goldie_url(); ?>" class="text-muted text-hover-primary">
              <?= show('edicaorota_breadcrumb_inicio') ?>
            </a>
          </li>
          <li class="breadcrumb-item"><span class="bullet bg-gray-500 w-5px h-2px"></span></li>
          <li class="breadcrumb-item text-muted"><?= show('edicaorota_breadcrumb_missao') ?></li>
          <li class="breadcrumb-item"><span class="bullet bg-gray-500 w-5px h-2px"></span></li>
          <li class="breadcrumb-item text-dark"><?= show('edicaorota_titulo') ?></li>
        </ul>
      </div>
    </div>
  </div>
  <!--end::Toolbar-->

  <!--begin::Content-->
  <div id="kt_app_content" class="app-content flex-column-fluid px-0">
    <div class="app-container container-xxl px-0">
      <div class="card" style="background: var(--bg-card, #1a1a2e); color: var(--text-primary, #e5e7eb);">
        <div class="card-header border-0 pt-6 flex-wrap gap-3">
          <ul class="nav nav-pills" id="edicaorota_tabs">
            <?php foreach ($tabs as $statusValor => $chaveLabel):
              $ativo = $statusValor === $statusAtivo;
            ?>
              <li class="nav-item">
                <a href="?status=<?= htmlspecialchars($statusValor) ?>"
                   class="nav-link fw-semibold<?= $ativo ? ' active' : '' ?>"
                   data-status="<?= htmlspecialchars($statusValor) ?>">
                  <?= show($chaveLabel) ?>
                  <?php if ($ativo): ?>
                    <span class="badge badge-light-warning ms-2"><?= count($pendentes) ?></span>
                  <?php endif; ?>
                </a>
              </li>
            <?php endforeach; ?>
          </ul>
          <div class="d-flex align-items-center">
            <span class="position-relative me-md-2">
              <i class="ki-outline ki-magnifier fs-3 position-absolute top-50 translate-middle ms-6"></i>
              <input
                type="text"
                id="edicaorota_busca"
                class="form-control form-control-solid w-250px ps-10"
                placeholder="<?= show('edicaorota_busca_placeholder') ?>">
            </span>
          </div>
        </div>

        <div class="card-body pt-3" id="edicaorota_lista" data-satelite="<?= htmlspecialchars($satelite) ?>">
          <?php if (empty($pendentes)): ?>
            <div id="edicaorota_empty" class="text-center py-10 text-muted">
              <i class="ki-outline ki-information-5 fs-2x mb-3 d-block"></i>
              <?= show('edicaorota_empty') ?>
            </div>
          <?php endif; ?>

          <?php foreach ($pendentes as $edicao):
            $edicaoId    = (int) ($edicao['id'] ?? 0);
            $motorista   = (string) ($edicao['identificador_motorista'] ?? '');
            $statusCss   = (string) ($edicao['status_css'] ?? 'warning');
            $statusLabel = (string) ($edicao['status_label'] ?? '');
            $minutos     = (int) ($edicao['aguardando_minutos'] ?? 0);
            $referencia  = $edicao['referencia'] ?? null;
            $paradas     = $edicao['paradas'] ?? [];
            $totalParadas    = count($paradas);
            $paradasAlteradas = (int) ($edicao['paradas_alteradas'] ?? 0);
            $alteradasLabel = $paradasAlteradas === 1
                ? str_replace('{total}', (string) $totalParadas, show('edicaorota_paradas_mudaram_singular'))
                : str_replace(['{n}','{total}'], [(string) $paradasAlteradas, (string) $totalParadas], show('edicaorota_paradas_mudaram_plural'));
          ?>
            <div class="border border-secondary rounded p-4 mb-4 edicaorota-card"
                 data-edicao-id="<?= $edicaoId ?>"
                 data-motorista="<?= htmlspecialchars(mb_strtolower($motorista)) ?>"
                 data-romaneio="<?= htmlspecialchars(mb_strtolower((string) ($referencia['numero'] ?? ''))) ?>"
                 style="background: var(--bg-card-2, #232342);">

              <div class="d-flex justify-content-between align-items-start flex-wrap gap-2 mb-2">
                <div class="d-flex align-items-center gap-2 flex-wrap">
                  <span class="badge badge-light-<?= htmlspecialchars($statusCss) ?>"><?= htmlspecialchars($statusLabel) ?></span>
                  <strong class="text-light"><?= show('edicaorota_edicao_numero') ?><?= $edicaoId ?></strong>
                  <span class="text-muted small">·</span>
                  <span class="text-muted small"><?= show('edicaorota_solicitado_ha') ?> <?= htmlspecialchars($formatarTempoAguardando($minutos)) ?></span>
                </div>
              </div>

              <div class="text-muted small mb-3 d-flex flex-wrap gap-2">
                <span><?= htmlspecialchars($motorista) ?: '—' ?></span>
                <?php if ($referencia && !empty($referencia['numero'])): ?>
                  <span>·</span>
                  <span><?= show('edicaorota_romaneio_label') ?> #<?= htmlspecialchars((string) $referencia['numero']) ?></span>
                <?php endif; ?>
                <?php if ($referencia && !empty($referencia['filial'])): ?>
                  <span>·</span>
                  <span><?= show('edicaorota_filial_label') ?>: <?= htmlspecialchars((string) $referencia['filial']) ?></span>
                <?php endif; ?>
              </div>

              <?php if ($paradasAlteradas > 0): ?>
                <div class="fw-semibold mb-2 text-warning"><?= htmlspecialchars($alteradasLabel) ?></div>
              <?php endif; ?>

              <ul class="list-unstyled mb-3 edicaorota-paradas">
                <?php foreach ($paradas as $p):
                  $delta    = $p['delta'] ?? null;
                  $posAtual = $p['pos_atual'] ?? null;
                  $posNova  = (int) ($p['pos_nova'] ?? 0);
                  $cliente  = (string) ($p['cliente'] ?? ('Pedido #' . (int) ($p['pedido_id'] ?? 0)));
                  $endereco = (string) ($p['endereco'] ?? '');
                  if ($delta === null) {
                    $icone = '＋'; $cor = 'info';      $classeExtra = 'edicaorota-parada-nova';
                  } elseif ($delta < 0) {
                    $icone = '▲'; $cor = 'success';   $classeExtra = 'edicaorota-parada-subiu';
                  } elseif ($delta > 0) {
                    $icone = '▼'; $cor = 'warning';   $classeExtra = 'edicaorota-parada-desceu';
                  } else {
                    $icone = '='; $cor = 'secondary'; $classeExtra = 'edicaorota-parada-igual';
                  }
                  $oculta = ($delta === 0) ? ' d-none' : '';
                ?>
                  <li class="d-flex align-items-center gap-2 py-1 border-start border-3 ps-2 mb-1 <?= $classeExtra . $oculta ?>"
                      style="border-color: var(--bs-<?= $cor ?>) !important;">
                    <span class="badge badge-light-<?= $cor ?>" style="min-width: 1.6rem;"><?= $icone ?></span>
                    <span class="flex-grow-1">
                      <strong class="text-light"><?= htmlspecialchars($cliente) ?></strong>
                      <?php if ($endereco !== ''): ?>
                        <span class="text-muted small d-block"><?= htmlspecialchars($endereco) ?></span>
                      <?php endif; ?>
                    </span>
                    <span class="small text-muted">
                      <?= $posAtual !== null ? ((int) $posAtual . 'ª') : '—' ?>
                      →
                      <?= $posNova ?>ª
                    </span>
                  </li>
                <?php endforeach; ?>
              </ul>

              <?php
                $contemIguais = false;
                foreach ($paradas as $p) {
                  if (($p['delta'] ?? null) === 0) { $contemIguais = true; break; }
                }
              ?>
              <?php if ($contemIguais): ?>
                <button type="button" class="btn btn-sm btn-light edicaorota-toggle-iguais mb-3">
                  <span class="ver"><?= show('edicaorota_ver_inalteradas') ?></span>
                  <span class="ocultar d-none"><?= show('edicaorota_ocultar_inalteradas') ?></span>
                </button>
              <?php endif; ?>

              <div class="d-flex gap-2">
                <button type="button" class="btn btn-light-danger flex-fill edicaorota-btn-rejeitar"
                        data-edicao-id="<?= $edicaoId ?>">
                  <?= show('edicaorota_rejeitar') ?>
                </button>
                <button type="button" class="btn btn-success flex-fill edicaorota-btn-aprovar"
                        data-edicao-id="<?= $edicaoId ?>">
                  <?= show('edicaorota_aprovar') ?>
                </button>
              </div>
            </div>
          <?php endforeach; ?>
        </div>
      </div>
    </div>
  </div>
  <!--end::Content-->
</div>

<?php include __DIR__ . '/_modal_rejeitar.php'; ?>
```

- [ ] **Step 2: Verificar PHP syntax**

Run: `php -l componentes/Missoes/Logistica/sinais/Edicaorota/html/edicaorota.php`
Expected: `No syntax errors detected`.

- [ ] **Step 3: Commit**

```bash
git add componentes/Missoes/Logistica/sinais/Edicaorota/html/edicaorota.php
git commit -m "feat(edicaorota): redesign painel do gestor com layout Metronic e diff visual"
```

---

## Task 10: Criar `html/edicaorota.js` (tabs + busca + modal)

**Files:**
- Create: `componentes/Missoes/Logistica/sinais/Edicaorota/html/edicaorota.js`

- [ ] **Step 1: Criar o JS pareado**

Conteúdo de `html/edicaorota.js`:

```javascript
/* Painel do gestor — edicaorota
   Responsabilidades:
     1. Tabs de status: ao clicar, chama satélite::listar_pendentes com status
     2. Busca client-side por motorista/romaneio (filtra cards no DOM)
     3. Botão "Aprovar": chama satélite::aprovar
     4. Botão "Rejeitar": abre modal; submit do modal chama satélite::rejeitar
     5. Toggle de paradas inalteradas
*/
(function () {
  var lista = document.getElementById('edicaorota_lista');
  if (!lista) return;

  var satelite = lista.dataset.satelite || 'EdicoesRotaAprovacao';

  // -------- Tabs --------
  // Tabs são `<a href="?status=...">`, então a navegação acontece naturalmente.
  // Não interceptamos com JS — o SSR re-renderiza com o status correto.
  // (Mantemos sem listener pra evitar piscar UI e ter URL compartilhável.)

  // -------- Busca client-side --------
  var inputBusca = document.getElementById('edicaorota_busca');
  if (inputBusca) {
    inputBusca.addEventListener('input', function () {
      var q = (inputBusca.value || '').toLowerCase().trim();
      var cards = lista.querySelectorAll('.edicaorota-card');
      var algum = false;
      cards.forEach(function (card) {
        var motorista = card.dataset.motorista || '';
        var romaneio  = card.dataset.romaneio  || '';
        var match = !q || motorista.indexOf(q) >= 0 || romaneio.indexOf(q) >= 0;
        card.style.display = match ? '' : 'none';
        if (match) algum = true;
      });
      var empty = document.getElementById('edicaorota_empty');
      if (empty) empty.classList.toggle('d-none', algum);
    });
  }

  // -------- Aprovar --------
  lista.addEventListener('click', function (ev) {
    var btn = ev.target.closest('.edicaorota-btn-aprovar');
    if (!btn) return;
    var id = parseInt(btn.dataset.edicaoId, 10);
    if (!id || !window.galaxia) return;
    btn.disabled = true;
    galaxia.satelite(satelite, 'aprovar', { edicao_id: id }, function (resp) {
      if (resp && (resp.notification_class === 'success')) {
        var card = btn.closest('.edicaorota-card');
        if (card) card.remove();
      } else {
        btn.disabled = false;
      }
    });
  });

  // -------- Rejeitar --------
  var modalEl   = document.getElementById('edicaorota_modal_rejeitar');
  var inputMotivo = document.getElementById('edicaorota_motivo');
  var btnConfirm  = document.getElementById('edicaorota_btn_confirmar_rejeitar');
  var spanNumero  = document.getElementById('edicaorota_modal_numero');
  var modalErro   = document.getElementById('edicaorota_motivo_erro');
  var edicaoIdAtual = null;
  var bsModal = null;

  if (modalEl && window.bootstrap) {
    bsModal = window.bootstrap.Modal.getOrCreateInstance(modalEl);
  }

  lista.addEventListener('click', function (ev) {
    var btn = ev.target.closest('.edicaorota-btn-rejeitar');
    if (!btn) return;
    edicaoIdAtual = parseInt(btn.dataset.edicaoId, 10);
    if (!edicaoIdAtual) return;
    if (inputMotivo) {
      inputMotivo.value = '';
      inputMotivo.classList.remove('is-invalid');
    }
    if (modalErro) modalErro.classList.add('d-none');
    if (spanNumero) spanNumero.textContent = '#' + edicaoIdAtual;
    if (bsModal) bsModal.show();
  });

  if (btnConfirm) {
    btnConfirm.addEventListener('click', function () {
      var motivo = (inputMotivo && inputMotivo.value || '').trim();
      if (motivo.length < 3) {
        if (inputMotivo) inputMotivo.classList.add('is-invalid');
        if (modalErro)   modalErro.classList.remove('d-none');
        return;
      }
      if (!edicaoIdAtual || !window.galaxia) return;
      btnConfirm.disabled = true;
      galaxia.satelite(satelite, 'rejeitar', {
        edicao_id: edicaoIdAtual,
        motivo_rejeicao: motivo
      }, function (resp) {
        btnConfirm.disabled = false;
        if (resp && resp.notification_class === 'success') {
          var card = lista.querySelector('.edicaorota-card[data-edicao-id="' + edicaoIdAtual + '"]');
          if (card) card.remove();
          if (bsModal) bsModal.hide();
        }
      });
    });
  }

  // -------- Toggle paradas iguais --------
  lista.addEventListener('click', function (ev) {
    var btn = ev.target.closest('.edicaorota-toggle-iguais');
    if (!btn) return;
    var card = btn.closest('.edicaorota-card');
    if (!card) return;
    var iguais = card.querySelectorAll('.edicaorota-parada-igual');
    var verLabel    = btn.querySelector('.ver');
    var ocultarLabel = btn.querySelector('.ocultar');
    var aberto = !iguais[0] || !iguais[0].classList.contains('d-none');
    iguais.forEach(function (li) { li.classList.toggle('d-none', aberto); });
    if (verLabel)     verLabel.classList.toggle('d-none', !aberto);
    if (ocultarLabel) ocultarLabel.classList.toggle('d-none', aberto);
  });
})();
```

- [ ] **Step 2: Verificar sintaxe JS (Node.js está disponível no projeto?)**

Run: `node --check componentes/Missoes/Logistica/sinais/Edicaorota/html/edicaorota.js 2>&1 | head -5 || echo "node não disponível — pular check"`
Expected: vazio (sem erros) ou mensagem "node não disponível".

- [ ] **Step 3: Commit**

```bash
git add componentes/Missoes/Logistica/sinais/Edicaorota/html/edicaorota.js
git commit -m "feat(edicaorota): JS pareado com tabs, busca, aprovar/rejeitar e modal"
```

---

## Task 11: Verificação manual no browser

Sem testes automatizados do front — verificação visual obrigatória.

- [ ] **Step 1: Levantar o ambiente**

Run: `cd /home/goldie/www/genesis && (php -S 0.0.0.0:8000 -t . > /tmp/genesis-server.log 2>&1 &)` (ou usar o ambiente já configurado em `dev-galpaotms.goldieapp.com.br`).

- [ ] **Step 2: Login como gestor e abrir `/missao/logistica/edicaorota`**

Confirmar visualmente:
- Toolbar com título "Edições de rota" e breadcrumb funcionam
- Card principal segue o estilo dark do tema (não tem fundo branco do Bootstrap padrão)
- Tabs "Pendentes (X) / Aprovadas / Rejeitadas" estão visíveis
- Busca à direita das tabs
- Cards de edição mostram badge de status, ID, "há X min", motorista, romaneio, filial
- Resumo "X de Y paradas mudaram" aparece se há diff
- Paradas iguais começam ocultas; botão "Ver inalteradas" expande
- Paradas que subiram têm ícone ▲ verde; desceram têm ▼ amarelo
- Botões Aprovar (verde) e Rejeitar (vermelho) no rodapé

- [ ] **Step 3: Testar fluxo aprovar**

Clicar Aprovar em uma edição. Card deve sumir; status no banco passa pra `aprovada`.

- [ ] **Step 4: Testar fluxo rejeitar**

Clicar Rejeitar. Modal abre. Submeter sem motivo → mensagem de erro inline. Submeter com motivo válido → modal fecha, card some.

- [ ] **Step 5: Testar tabs**

Clicar "Aprovadas" → URL atualiza (reload), página recarrega com lista vazia ou aprovadas. Mesma coisa pra "Rejeitadas".

- [ ] **Step 6: Testar busca**

Digitar parte do nome do motorista → outros cards somem.

- [ ] **Step 7: Testar mobile (≤768px)**

DevTools → modo mobile (iPhone). Cards empilham bem; modal abre full-screen ou centralizado; tabs + busca quebram em duas linhas.

- [ ] **Step 8: Testar i18n**

Trocar idioma (mecanismo do projeto) e validar EN e ES. Strings sem chave de tradução não devem aparecer.

- [ ] **Step 9: Reportar verificação no commit final**

```bash
git commit --allow-empty -m "verify(edicaorota): redesign do painel do gestor validado em browser"
```

---

## Self-Review Checklist (rodar após implementação)

- [ ] Tela `/missao/logistica/edicaorota` segue layout Metronic (toolbar + breadcrumb + card)
- [ ] Nenhum `style=""` inline na view (cores via vars CSS)
- [ ] Diff visual mostra ▲/▼/= por parada
- [ ] Modal de rejeição substitui `prompt()`
- [ ] Tabs/busca funcionam
- [ ] Todas as strings via `show()`; chaves em pt-br/en/es
- [ ] Testes TDD (Tasks 1-4) passam: `php Testes/Callbacks/EdicaoRotaReferenciaDescritorTddTest.php`, `php Testes/Sondas/ListarPorStatusArrayTddTest.php`, `php Testes/Sondas/HidratarParaPainelGestorTddTest.php`
- [ ] Testes existentes do sinal não regridem: `php Testes/Laboratorios/CriarEdicaoRotaLaboratorioTddTest.php`, `php Testes/Eventos/EdicaoRotaOtpEventoTddTest.php`
