# Edição de Rota — redesign do painel do gestor

**Data:** 2026-05-21
**Sinal:** `componentes/Missoes/Logistica/sinais/Edicaorota`
**Tela afetada:** `html/edicaorota.php` (painel do gestor — `/missao/logistica/edicaorota`)

## Contexto

A view atual do painel do gestor (`html/edicaorota.php`) foi escrita como protótipo e tem dívidas visuais e funcionais:

- **Sem layout Genesis** — usa `<h2>` solto + `<div>` com `style=""` inline; falta `app-toolbar`, breadcrumb e estrutura `app-content > card`.
- **Inline-style em todos os elementos** — não usa classes Metronic nem CSS vars (`var(--bg-card)`).
- **Sem i18n** — strings em PT hardcoded (`"Aprovar"`, `"Rejeitar"`, `"Edições de rota pendentes"`).
- **Cards pobres de informação** — mostram só `Edição #X — Motorista` e duas colunas de títulos (`<ol>` ordem atual vs proposta). O gestor não vê quanto tempo a edição está aguardando, qual romaneio é, nem **o que mudou** (só pode comparar visualmente item a item).
- **`prompt()` do browser pra capturar motivo de rejeição** — UX ruim, sem validação, sem padronização visual.
- **Sem filtros / busca / empty-state estilizado** — tudo carregado de uma vez (`PENDENTE_APROVACAO` apenas), sem possibilidade de revisar histórico de Aprovadas/Rejeitadas pelo mesmo painel.

Diferentes pontos do app reportam que a tela "parece jogada".

## Objetivo

Reescrever `html/edicaorota.php` (+ JS pareado + modal partial + i18n) seguindo o padrão V2 Genesis (Metronic + Tailwind híbrido, vars CSS, i18n em 3 línguas) e enriquecer os cards com contexto suficiente para o gestor decidir aprovar/rejeitar **sem precisar comparar listas item-a-item**.

Fora do escopo:
- Views públicas do motorista (`editar_rota.php`, `validar_codigo.php`, `status.php`, `_layout.php`).
- Mudança de fluxo de aprovação/rejeição (continua via satélite `EdicoesRotaAprovacao::aprovar/rejeitar`).
- Paginação (lista de pendentes raramente fica grande na operação real).
- Nova coluna de motivo do motorista (não existe; criar seria outra spec).

## Decisão arquitetural

1. **View renovada** em `html/edicaorota.php` com layout Metronic padrão (`app-toolbar` + breadcrumb + `card`) — espelha o que `Regioesagrupamentos/html/estrutura.php` já faz no Logística.
2. **JS pareado** `html/edicaorota.js` com lógica de tabs de status, busca, sort e abertura do modal de rejeição.
3. **Modal de rejeição** como partial separado em `html/_modal_rejeitar.php` — substitui o `prompt()` do browser por modal Bootstrap nativo (já carregado pelo tema Metronic).
4. **Sonda enriquecida** — `EdicaoRotaSonda::listarPorStatus()` passa a aceitar múltiplos status e a hidratação na Sonda já produz:
   - paradas com cliente + endereço (reusa `buscarInfoParadas`)
   - diff de posições pré-calculado (cada parada da `rota_proposta` ganha `pos_atual`, `pos_nova`, `delta`)
   - contagem `paradas_alteradas`
   - tempo aguardando (computado do `data_cadastro` no PHP)
5. **i18n** completo nas 3 línguas (`pt-br`, `en`, `es`) com prefixo `edicaorota_`.

## Estrutura nova

```
sinais/Edicaorota/
├── Edicaorota.php           # (sem mudança — método telaGestorPendentes
│                            #   continua chamando $galaxia->visual('html/edicaorota'))
├── Sondas/
│   └── EdicaoRotaSonda.php  # listarPorStatus aceita array; hidratação
│                            #   adiciona dados de cliente/endereço/diff
├── Satelites/
│   └── EdicoesRotaAprovacao.php  # (sem mudança — já tem aprovar/rejeitar/listar_pendentes)
└── html/
    ├── edicaorota.php       # NOVA view com layout Metronic + cards diff
    ├── edicaorota.js        # NOVO — tabs, busca, sort, abrir modal
    └── _modal_rejeitar.php  # NOVO — partial do modal
```

Hidratação adicional na Sonda (não em SQL cru — usa `find()` da Sonda + `buscarInfoParadas` que já existe):

```php
// Cada item de $registro['rota_proposta'] (após hidratação) terá:
[
  'pedido_id'   => 1234,
  'cliente'     => 'Cliente Y',
  'endereco'    => 'Rua X, 100',
  'pos_atual'   => 3,   // posição na rota_atual (1-based)
  'pos_nova'    => 1,   // posição na rota_proposta
  'delta'       => -2,  // pos_nova - pos_atual; <0 subiu, >0 desceu, 0 igual
]

// E no nível da edição:
[
  'data_cadastro'      => '2026-05-21 09:15:00',
  'aguardando_minutos' => 23,   // computado no PHP
  'paradas_alteradas'  => 3,
  // ... resto
]
```

## Layout final (Metronic)

```
┌─ app-toolbar ────────────────────────────────────────────┐
│  Edições de rota                                         │
│  Início › Logística › Edições de rota                    │
└──────────────────────────────────────────────────────────┘
┌─ app-content > card ─────────────────────────────────────┐
│  ┌─ card-header ─────────────────────────────────────┐  │
│  │ [Pendentes (3)] [Aprovadas] [Rejeitadas]   🔍 busca│  │
│  └────────────────────────────────────────────────────┘  │
│  ┌─ card-body ────────────────────────────────────────┐  │
│  │  ┌─ card-edicao ───────────────────────────────┐   │  │
│  │  │ [Pendente]  Edição #42 · há 23 min          │   │  │
│  │  │ João Silva · Romaneio #128 · Filial Centro  │   │  │
│  │  │                                              │   │  │
│  │  │ 3 de 8 paradas mudaram                       │   │  │
│  │  │ ┌─ paradas (apenas as alteradas, depois ─┐   │   │  │
│  │  │ │ inalteradas com display:none + "ver    │   │  │
│  │  │ │ todas") ─────────────────────────────  │   │  │  │
│  │  │ │ ▲ Cliente C · Rua Z 50    3ª → 1ª (−2) │   │  │  │
│  │  │ │ ▼ Cliente B · Rua Y 200   2ª → 4ª (+2) │   │  │  │
│  │  │ │ ▲ Cliente D · Av W 800    4ª → 3ª (−1) │   │  │  │
│  │  │ │ [ver paradas inalteradas (5)]          │   │  │  │
│  │  │ └────────────────────────────────────────┘   │   │  │
│  │  │                                              │   │  │
│  │  │           [✕ Rejeitar]   [✓ Aprovar]        │   │  │
│  │  └──────────────────────────────────────────────┘   │  │
│  │  ... (próximas edições)                              │  │
│  │                                                       │  │
│  │  [empty-state estilizado se vazio]                   │  │
│  └───────────────────────────────────────────────────┘  │
└──────────────────────────────────────────────────────────┘
```

### Card de edição — anatomia

| Bloco          | Conteúdo                                                                          |
| -------------- | --------------------------------------------------------------------------------- |
| Header         | Badge de status + `Edição #ID` + tempo aguardando (`há Xh / X min`)               |
| Subheader      | Motorista (`identificador_motorista`) · `Romaneio #N` · `Filial X`                |
| Resumo do diff | "X de Y paradas mudaram" (link "ver inalteradas" expande as iguais)               |
| Lista de diff  | Linha por parada: ícone (▲/▼/=) + cliente + endereço + `pos_atual → pos_nova`     |
| Ações          | Botões Aprovar (`btn btn-success`) e Rejeitar (`btn btn-light-danger`)            |

Cores das linhas de diff (vars CSS / classes Tailwind do projeto):
- **▲ subiu** (delta < 0): borda esquerda verde, ícone verde, badge verde claro
- **▼ desceu** (delta > 0): borda esquerda laranja, ícone laranja, badge laranja claro
- **= igual** (delta = 0): cinza, recolhido por default

### Modal de rejeição

Partial `html/_modal_rejeitar.php` — único modal na página, reutilizado para qualquer edição (recebe `edicao_id` via `data-*`).

```html
<div class="modal fade" id="edicaorota_modal_rejeitar" tabindex="-1">
  <div class="modal-dialog">
    <div class="modal-content">
      <div class="modal-header">
        <h5 class="modal-title">{show edicaorota_modal_rejeitar_titulo}</h5>
        <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
      </div>
      <div class="modal-body">
        <label class="form-label">{show edicaorota_modal_motivo_label}</label>
        <textarea id="edicaorota_motivo" class="form-control" rows="4" minlength="3" required></textarea>
        <div class="invalid-feedback">{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>
```

JS pareado: ao clicar "Rejeitar" num card, abre o modal com `data-edicao-id` injetado. Submit do modal valida `minlength ≥ 3` e chama `galaxia.satelite('EdicoesRotaAprovacao','rejeitar', { edicao_id, motivo_rejeicao })`.

## Tabs + busca + sort (client-side)

A view sempre carrega **só os pendentes** no SSR (mesmo comportamento atual). O JS pareado oferece:

- **Tabs**: ao clicar em "Aprovadas" ou "Rejeitadas", chama `galaxia.satelite('EdicoesRotaAprovacao','listar_pendentes', {status: 'aprovada'})` — o satélite passa a aceitar `status` opcional no payload (default mantém `PENDENTE_APROVACAO`). Renderiza no client substituindo o `card-body`.
- **Busca**: filtra cards visíveis no DOM por `identificador_motorista` ou `romaneio_numero` (toLowerCase + indexOf).
- **Sort**: default = mais antigo primeiro (gestor prioriza o que está esperando há mais tempo).

Sem paginação. Se a lista crescer muito a ponto de precisar, vira nova spec.

## Backend — mudanças mínimas

### `Sondas/EdicaoRotaSonda.php`

1. `listarPorStatus($status)` passa a aceitar **string OU array**:
   ```php
   public function listarPorStatus(string|array $status): array
   ```
   - String: comportamento atual (`status = :st`).
   - Array: usa `IN (...)` na query (`find()` aceita WHERE custom).

2. Adicionar método `hidratarParaPainelGestor(array $registro): array` que:
   - decodifica `rota_atual` e `rota_proposta`
   - chama `buscarInfoParadas` com a união dos `pedido_id`
   - calcula `pos_atual` (de `rota_atual`) e `pos_nova` (de `rota_proposta`) para cada parada da proposta
   - calcula `delta` e `paradas_alteradas`
   - calcula `aguardando_minutos` (via `data_cadastro`)
   - retorna registro enriquecido

### `Satelites/EdicoesRotaAprovacao.php`

1. `listarPendentes()` aceita `?string $status = null` (default: `PENDENTE_APROVACAO`) e usa a hidratação nova:
   ```php
   private function listarPendentes(?string $status = null): array
   ```
2. `__construct` passa `data['status'] ?? null` para `listarPendentes`.

### `Edicaorota.php`

`telaGestorPendentes()` reusa `EdicaoRotaSonda::hidratarParaPainelGestor` em vez de só `json_decode`. Sem outras mudanças.

## i18n

Chaves novas em `linguagens/{pt-br,en,es}.json`:

```
edicaorota_titulo                       = "Edições de rota" / "Route edits" / "Ediciones de ruta"
edicaorota_breadcrumb_inicio            = "Início" / "Home" / "Inicio"
edicaorota_breadcrumb_missao            = "Logística" / "Logistics" / "Logística"
edicaorota_tab_pendentes                = "Pendentes" / "Pending" / "Pendientes"
edicaorota_tab_aprovadas                = "Aprovadas" / "Approved" / "Aprobadas"
edicaorota_tab_rejeitadas               = "Rejeitadas" / "Rejected" / "Rechazadas"
edicaorota_busca_placeholder            = "Buscar motorista ou romaneio…" / ... / ...
edicaorota_edicao_numero                = "Edição #" / "Edit #" / "Edición #"
edicaorota_solicitado_ha                = "há" / "ago" / "hace"   (composto com tempo)
edicaorota_solicitado_minutos           = "min"
edicaorota_solicitado_horas             = "h"
edicaorota_romaneio_label               = "Romaneio" / "Manifest" / "Manifiesto"
edicaorota_filial_label                 = "Filial" / "Branch" / "Sucursal"
edicaorota_paradas_mudaram              = "{n} de {total} paradas mudaram" (interpolar no PHP)
edicaorota_ver_inalteradas              = "Ver paradas inalteradas ({n})"
edicaorota_ocultar_inalteradas          = "Ocultar inalteradas"
edicaorota_pos_label                    = "pos."
edicaorota_aprovar                      = "Aprovar" / "Approve" / "Aprobar"
edicaorota_rejeitar                     = "Rejeitar" / "Reject" / "Rechazar"
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."
```

## Critérios de aceite

1. Tela `/missao/logistica/edicaorota` renderiza com `app-toolbar` (título + breadcrumb) e `app-content > card` igual aos outros painéis Genesis (Regioesagrupamentos serve de referência visual).
2. Nenhum `style=""` inline na view (só vars CSS via `<style>` se necessário, ou classes utilitárias do tema).
3. Cada card mostra status, ID, tempo aguardando, motorista, romaneio, filial, contador de paradas alteradas e diff visual ▲/▼/=.
4. Paradas inalteradas ficam ocultas por default; link "ver inalteradas" expande/recolhe.
5. Clicar Rejeitar abre modal com textarea obrigatório (min 3 chars); sem `prompt()` do browser.
6. Tabs "Pendentes / Aprovadas / Rejeitadas" filtram a lista chamando o satélite.
7. Busca filtra cards visíveis sem nova request.
8. Empty-state quando lista vazia tem aparência consistente com o resto do app.
9. Todas as strings visíveis passam por `show()`. Chaves existem nos 3 arquivos de idioma.
10. Mobile (≤768px) — cards empilham, header do card quebra em 2 linhas, modal ocupa largura cheia.

## Riscos

- **`data_cadastro` como proxy de "solicitado em"** — o status muda para `pendente_aprovacao` quando o motorista submete (`SubmeterRotaPropostaLaboratorio`), mas a tabela não tem timestamp dedicado dessa transição. Usar `data_cadastro` é aproximação (geralmente a diferença é minutos). Aceitável; documentar como "tempo desde a criação da edição".
- **Romaneio número/filial** — vem do `callback_referencia_id` via `RomaneioVeiculoEdicaoRotaCallback`. Precisa expor método `descreverReferencia(int $id): array` no callback (ou via join na Sonda). Verificar se o adapter `Adapters/Romaneio` já tem esse helper antes de implementar.
- **Mexer na Sonda** — o método `listarPorStatus` é usado também por `telaGestorPendentes` (Edicaorota.php) e por `listar_pendentes` do satélite. Mudança é retrocompatível (string continua funcionando).
