# Dashboard Operacional — Plano Técnico

## Revisão R3 — Sondas existentes (REUSAR, não criar)

**Feedback do Goldie (R3)**: "Não devemos criar a sonda MissaoLogisticaSonda pois já existe em missões logísticas. KpisSonda me parece mais um Laboratório (UseCase/Regras de Negócio) do que algo de model/entidade — caso queira seguir nesse padrão renomeie para KpisSonda mesmo (intent KPI)."

A R2 falhou em duas frentes:

1. Listou `MissaoLogisticaSonda`, `MissaoLogisticaItemSonda` como **Sondas a criar**, mas elas **já existem em disco** desde commits anteriores deste branch.
2. Listou `MissaoLogisticaUsuarioSonda` como Sonda nova, mas o uso real (ranking/contagem) é cálculo agregado → cabe em Lab.

### Sondagem realizada (R3 — agressiva)

```bash
$ find componentes -type d -name Sondas | xargs -I{} ls {}/*.php 2>/dev/null
```

Resultado completo das **Sondas V2** que cobrem o domínio Logística/MissaoLogistica:

| Caminho | Entity (tabela) | Decisão R3 |
|---|---|---|
| `componentes/Missoes/Logistica/sinais/Dashboard/Sondas/MissaoLogisticaSonda.php` | `missao_logistica` | **EXISTE — REUSAR** (não recriar) |
| `componentes/Missoes/Logistica/sinais/Dashboard/Sondas/MissaoLogisticaItemSonda.php` | `missao_logistica_itens` | **EXISTE — REUSAR** (não recriar) |
| `componentes/Missoes/Logistica/sinais/Dashboard/Sondas/KpisSonda.php` | `missao_logistica` (mas método é `agregar()` calculando 6 KPIs + sparklines) | **MOVER → `KpisLaboratorio`** (regra de negócio, não model) |
| `componentes/Missoes/Logistica/sinais/Dashboard/Sondas/FluxoDiarioSonda.php` | `missao_logistica` (método é `serieDiaria()` agregando por dia/semana com switch RN16) | **MOVER → `FluxoLaboratorio`** (cálculo, não model) |
| `componentes/Constelacoes/Logistica/MissaoLogistica/Sondas/PedidoErpSonda.php` | `pedido_erp` | Escopo distinto (não usar) |
| `componentes/Constelacoes/Logistica/MissaoLogistica/Sondas/RecomecarItemSonda.php` | `missao_logistica_itens` (parcial — métodos pra recomeçar item) | Escopo distinto (não confundir com `MissaoLogisticaItemSonda` do Dashboard) |
| `componentes/Missoes/Logistica/sinais/Central/Sondas/RotaCarregamento.php` | `rotas_carregamento` | **REUSAR** quando precisar de detalhe de rota |
| `componentes/Missoes/Logistica/sinais/Edicaorota/Sondas/EdicaoRotaSonda.php` | `edicao_rota` | Escopo distinto |

`MissaoLogisticaSonda` e `MissaoLogisticaItemSonda` já estão **exatamente no padrão V2 fino** (entity 1:1, `customerControl: true`, métodos crus de leitura: `listarPorJanela`, `contarPorTipoNaJanela`, `agruparPorDia`, `listarOvernight`, `serieDiariaPorTipo`, `somarQuantidadeBipada`, `somarCubagem`, `agruparPorConcluidoPor`, `listarItensComDivergencia`, `serieDiariaItens`). **Cobrem 100% das necessidades dos Labs novos.** O dev na ordem de implementação **não escreve essas Sondas — só consome.**

### Aplicação da regra Sonda vs Lab (R3, definitiva)

| Item | Antes (R2 dizia) | Agora (R3) | Razão |
|---|---|---|---|
| `MissaoLogisticaSonda` | "criar nova" | **JÁ EXISTE — REUSAR** | Tabela + Sonda V2 fina já em disco e correta |
| `MissaoLogisticaItemSonda` | "criar nova" | **JÁ EXISTE — REUSAR** | Idem |
| `MissaoLogisticaUsuarioSonda` | "criar nova" | **NÃO CRIAR — vira método em `RankingSeparadoresLaboratorio`** (consome `MissaoLogisticaSonda` + `MissaoLogisticaItemSonda` via JOIN ou agregação no Lab). A V1 `Inteligencias/MissaoLogisticaUsuario.php` continua atendendo a parte de escrita (sincronizarEquipe) em outros sinais; o Dashboard só lê via agregação. | Uso é ranking/contagem (cálculo). Sem leitura crua útil isolada. Regra: cálculo = Lab. |
| `KpisSonda` (em disco) | "vira `KpisLaboratorio`" (R2) | **CONFIRMADO — mover para `Laboratorios/KpisLaboratorio.php`** e deletar arquivo antigo | É agregação (6 KPIs + sparklines) — Lab. Goldie autoriza manter o nome `KpisSonda` apenas se representar uma view/agregação materializada (não é o caso aqui — é cálculo on-the-fly). |
| `FluxoDiarioSonda` (em disco) | "vira `FluxoLaboratorio`" (R2) | **CONFIRMADO — mover para `Laboratorios/FluxoLaboratorio.php`** e deletar arquivo antigo | Idem |
| `ErrosMotoristaSonda` | "criar nova" se tabela inexistir | **MANTER como Sonda nova** condicional. Única Sonda V2 nova legítima desta feature: tabela é nova (criada na migration), sem Sonda V2 prévia, sem Inteligencia V1. 1:1 puro. | Tabela nova → Sonda V2 nova é a forma correta. |

### Saldo R3

- **Sondas V2 a CRIAR nesta feature**: **0 ou 1** (`ErrosMotoristaSonda` apenas se Risco 1 confirmar tabela inexistente).
- **Sondas V2 a REUSAR**: 2 (`MissaoLogisticaSonda`, `MissaoLogisticaItemSonda`) + 1 condicional (`Central/Sondas/RotaCarregamento` se algum Lab precisar de detalhe de rota).
- **Sondas V2 a DELETAR após migração de conteúdo**: 2 (`KpisSonda`, `FluxoDiarioSonda` — viram Labs).
- **Laboratórios a criar**: 9 (KpisLab, FluxoLab, RankingSeparadoresLab, RankingConferentesLab, ErrosSeparacaoLab, ErrosCarregamentoLab, RetrabalhoLab, ComparativoMensalLab, ScoreCalculator).

---

## Revisão arquitetural — Sondas vs Labs (R2)

**Feedback do Goldie**: "KpisSonda — o ideal seria uma Sonda ou um Laboratório? Mais me parece regra de negócio do que uma model (sendo que já existe a missao_logistica)."

Ele tem razão. A V1 do techplan listava 11 "Sondas" temáticas (KpisSonda, FluxoDiarioSonda, RankingSeparadoresSonda, ErrosSeparacaoSonda, ScoreCalculator-style etc). Aplicando a regra V2 corretamente:

- **Sonda** = Model V2 (extends `ModelInteligencia`), 1:1 com **uma tabela**. Existe só se a tabela ainda não tem Sonda V2.
- **Laboratório** = use-case que **agrega/transforma/calcula** sobre Sondas. KPIs, séries diárias, rankings, scores ponderados, comparativos mensais, sparklines, classificação de tipo de erro = **Lab**.

> **Nota R3**: a sondagem da R2 era incompleta — afirmava que as 3 tabelas operacionais centrais "só têm Inteligencia V1, nenhuma Sonda V2". Isso era falso para `missao_logistica` e `missao_logistica_itens` (Sondas V2 já existiam neste sinal). A seção R3 acima corrige.

### Reclassificação

| Item da V1 do plano | Reclassificação R2 | Destino |
|---|---|---|
| `KpisSonda` | Lab — agrega 6 KPIs | `KpisLaboratorio` (mover do arquivo `Sondas/KpisSonda.php` existente) |
| `FluxoDiarioSonda` | Lab — série diária | `FluxoLaboratorio` (mover do arquivo `Sondas/FluxoDiarioSonda.php` existente) |
| `OvernightSonda` | Lab — recorte de fluxo | dentro de `FluxoLaboratorio` |
| `RankingSeparadoresSonda` | Lab — ranking + score | `RankingSeparadoresLaboratorio` |
| `RankingConferentesSonda` | Lab — ranking + score | `RankingConferentesLaboratorio` |
| `ErrosSeparacaoSonda` | Lab — classifica e agrega tipos | `ErrosSeparacaoLaboratorio` |
| `ErrosCarregamentoSonda` | Lab — agrega 4 tipos | `ErrosCarregamentoLaboratorio` |
| `TempoMedioItemSonda` | Lab — derivado de itens | método em `RankingSeparadoresLaboratorio` |
| `TempoMedioCarregamentoSonda` | Lab — derivado de carregamentos | método em `RankingConferentesLaboratorio` |
| `RetrabalhoSonda` | Lab — série + índice | `RetrabalhoLaboratorio` |
| `ComparativoMensalSonda` | Lab — comparativo MoM | `ComparativoMensalLaboratorio` |
| ~~Sonda V2 para `missao_logistica`~~ | **Já existe — reusar (R3)** | `Sondas/MissaoLogisticaSonda.php` (em disco) |
| ~~Sonda V2 para `missao_logistica_itens`~~ | **Já existe — reusar (R3)** | `Sondas/MissaoLogisticaItemSonda.php` (em disco) |
| ~~Sonda V2 para `missao_logistica_usuarios`~~ | **Não criar (R3)** — uso é agregação | método em `RankingSeparadoresLaboratorio` consumindo as 2 Sondas existentes via JOIN/SQL |
| `ErrosMotoristaSonda` (tabela nova condicional) | **Sonda nova** (única) | `ErrosMotoristaSonda` |
| `rotas_carregamento` | Reusar V2 existente | `Central/Sondas/RotaCarregamento.php` |

**Saldo R3 (atualizado)**: 0 a 1 Sonda nova (`ErrosMotoristaSonda` condicional), 2 Sondas existentes reusadas (`MissaoLogisticaSonda`, `MissaoLogisticaItemSonda`), 2 arquivos `Sondas/*.php` movidos para `Laboratorios/` (KpisSonda, FluxoDiarioSonda) e **9 Laboratórios** novos.

### Onde colocar a Sonda nova condicional

`ErrosMotoristaSonda` (se criada): **dentro do sinal `Dashboard/Sondas/`** — primeiro consumidor é o Dashboard. Promover para `Constelacoes/` em PR de refactor puro quando 2º consumidor aparecer (mesma política das duas Sondas já existentes neste sinal).

---

## Resumo de decisão arquitetural

Reusa o sinal existente `componentes/Missoes/Logistica/sinais/Dashboard/` (já em padrão V2 com dispatcher fino + Laboratorios + Validators + Testes). **Não cria sinal novo, não cria Adapter, não cria Registry.** A feature é leitura-pesada com 1 fluxo (UI → handler → Laboratório → Sonda): introduzir Adapter/Strategy seria padrão prematuro.

A maior alteração é **substituir** o conteúdo de `Laboratorios/DashboardLaboratorio.php` (482 linhas voltadas para kanban) por um conjunto de Laboratórios temáticos finos, **mover para Laboratórios** os arquivos `Sondas/KpisSonda.php` e `Sondas/FluxoDiarioSonda.php` (que são cálculo, não model — feedback Goldie R3), **reusar** as Sondas V2 finas `Sondas/MissaoLogisticaSonda.php` e `Sondas/MissaoLogisticaItemSonda.php` que já existem, e refatorar `Dashboard.php` para deixar de fazer SSR de 10 blocos no `start()`. Os 12 handlers atuais em `componentes/Missoes/Plataforma/sinais/Api/Handlers/Dashboard/` são **podados quase todos** (decisão na seção "Auditoria dos 12 handlers atuais").

Stack frontend: **ApexCharts** (já presente em `node_modules/apexcharts`) + **flowbite-datepicker** com plugin `DateRangePicker` (já em `node_modules/flowbite-datepicker`). Zero nova dependência npm.

## Sinal de referência

- **Imitar (controller + Laboratorio + Validator + Handler V2)**: `componentes/Missoes/Logistica/sinais/Dashboard/` (próprio, recém-mergeado em `dev_team`) e `componentes/Missoes/Plataforma/sinais/Api/Handlers/Dashboard/VisaoGeralDiaHandler.php` — padrão exato de dispatcher fino + Validator dedicado + Lab finalístico.
- **Imitar (Sonda V2 fina, 1:1 com tabela)**: `componentes/Missoes/Logistica/sinais/Dashboard/Sondas/MissaoLogisticaSonda.php` e `componentes/Missoes/Logistica/sinais/Dashboard/Sondas/MissaoLogisticaItemSonda.php` (já no padrão correto, dentro deste mesmo sinal). Também `componentes/Missoes/Logistica/sinais/Edicaorota/Sondas/EdicaoRotaSonda.php` e `componentes/Missoes/Logistica/sinais/Central/Sondas/RotaCarregamento.php` (referências canônicas — `feedback_referencias_v2.md`, `feedback_sondas_v2.md`).
- **Imitar (uso ApexCharts)**: `componentes/Constelacoes/Dashboards/Graficos/Satelites/GraficoDados.php` — só para entender a forma da série esperada pelo Apex; ignorar a parte de Inteligencias (V1).
- **Não imitar**: `componentes/Constelacoes/Logistica/MissaoLogistica/Inteligencias/MissaoLogistica.php`, `MissaoLogisticaItem.php`, `MissaoLogisticaUsuario.php` (V1 — usa `ModelInteligencia` mas em padrão antigo, contém escrita). Toda leitura nova passa pelas Sondas V2 do sinal Dashboard.
- **Não imitar**: `componentes/Constelacoes/Logistica/Board/Inteligencias/` e `Central/Inteligencias/` (legado V1).

## Estrutura proposta

### Estado atual em disco (sob `componentes/Missoes/Logistica/sinais/Dashboard/`)

```
Sondas/
├── MissaoLogisticaSonda.php          ← REUSAR (V2 fina, 1:1 com missao_logistica) ✅
├── MissaoLogisticaItemSonda.php      ← REUSAR (V2 fina, 1:1 com missao_logistica_itens) ✅
├── KpisSonda.php                     ← MOVER conteúdo para Laboratorios/KpisLaboratorio.php e DELETAR
└── FluxoDiarioSonda.php              ← MOVER conteúdo para Laboratorios/FluxoLaboratorio.php e DELETAR

Laboratorios/
├── DashboardLaboratorio.php          ← REMOVER (substituído pelos Labs temáticos)
└── ScoreCalculator.php               ← MANTER (já criado, funções puras)

Migrations/
├── 2026_05_05_add_dashboard_operacional_indexes.sql   ← MANTER
└── 2026_05_05_create_erros_motorista.sql              ← CONDICIONAL (Risco 1)
```

### Novos arquivos (sob `componentes/Missoes/Logistica/sinais/Dashboard/`)

```
Sondas/                                       # apenas 1 Sonda nova condicional
└── ErrosMotoristaSonda.php                   # entity: erros_motorista (CONDICIONAL — Risco 1)
                                              # única Sonda V2 nova legítima:
                                              # tabela nova, 1:1, sem Inteligencia V1 prévia.

Laboratorios/                                 # toda agregação/cálculo/score vive aqui
├── KpisLaboratorio.php                       # 6 KPIs + sparklines
│                                             #   ORIGEM: conteúdo do Sondas/KpisSonda.php é movido para cá;
│                                             #   muda hierarquia (não estende ModelInteligencia);
│                                             #   recebe MissaoLogisticaSonda + MissaoLogisticaItemSonda
│                                             #   no construtor; chama métodos das Sondas existentes.
├── FluxoLaboratorio.php                      # série diária + overnight
│                                             #   ORIGEM: conteúdo do Sondas/FluxoDiarioSonda.php é movido para cá;
│                                             #   chama MissaoLogisticaSonda::serieDiariaPorTipo() e ::listarOvernight().
├── RankingSeparadoresLaboratorio.php         # ranking + score + tempo médio item
│                                             #   inclui métodos de agregação por usuário (substituindo a
│                                             #   "MissaoLogisticaUsuarioSonda" que a R2 propunha — R3 elimina).
├── RankingConferentesLaboratorio.php         # ranking + score + tempo médio carga
├── ErrosSeparacaoLaboratorio.php             # classificação trocado/falta/sobra + breakdown por pessoa
├── ErrosCarregamentoLaboratorio.php          # 4 tipos + breakdown por conferente
├── RetrabalhoLaboratorio.php                 # série + índice
└── ComparativoMensalLaboratorio.php          # genérico — separação e carregamento (atual vs N meses atrás)

Validators/
├── FiltroPeriodoValidator.php                # from/to (Y-m-d), from<=to, range<=365d
├── PaginacaoValidator.php                    # page/perPage para rankings
└── ComparativoValidator.php                  # mesesAtras (1..12) para comparativos

Testes/Laboratorios/
├── ScoreCalculatorTddTest.php                # unit puro: fórmulas §9 spec
├── KpisLaboratorioTddTest.php                # com Sondas stubadas
├── RankingSeparadoresLaboratorioTddTest.php  # inclui agregação por usuário (sem Sonda nova)
└── ErrosSeparacaoLaboratorioTddTest.php      # classificação trocado/falta/sobra

Testes/Sondas/                                # somente para a Sonda nova (se criada)
└── ErrosMotoristaSondaTddTest.php            # CONDICIONAL — só se Risco 1 confirmar criação

Testes/Integracao/                            # rodam só em dev_gx_goldie
├── KpisIntegrationTest.php
├── FluxoDiarioIntegrationTest.php
├── RankingSeparadoresIntegrationTest.php
└── ErrosCarregamentoIntegrationTest.php
```

> **Observação R3**: removidos os testes `MissaoLogisticaSondaTddTest`, `MissaoLogisticaItemSondaTddTest` e `MissaoLogisticaUsuarioSondaTddTest` da lista — as duas primeiras Sondas já existem (smoke do contrato V2 pode ser feito ad hoc, mas não é trabalho desta feature) e a terceira Sonda não vai ser criada. Se o dev quiser cobrir as 2 existentes, é bem-vindo — não é bloqueante.

### Arquivos a refatorar/remover dentro do sinal

- `Sondas/KpisSonda.php` → **mover conteúdo** para `Laboratorios/KpisLaboratorio.php` e DELETAR. O método `agregar()` permanece; muda só a hierarquia (não estende `ModelInteligencia`; recebe `MissaoLogisticaSonda` e `MissaoLogisticaItemSonda` injetadas via construtor). Toda a lógica SQL crua hoje dentro desse pseudo-Sonda já tem equivalente nos métodos das 2 Sondas finas existentes (`MissaoLogisticaSonda::contarPorTipoNaJanela`, `::listarOvernight`, `MissaoLogisticaItemSonda::somarQuantidadeBipada`, `::somarCubagem`, `::serieDiariaItens`) — o Lab passa a chamá-los em vez de re-fazer SQL.
- `Sondas/FluxoDiarioSonda.php` → **mover conteúdo** para `Laboratorios/FluxoLaboratorio.php` e DELETAR. Mesma estratégia: chama `MissaoLogisticaSonda::serieDiariaPorTipo()`.
- `Laboratorios/DashboardLaboratorio.php` → **remover** (substituído pelos Labs temáticos).
- `Validators/AcaoMissaoValidator.php`, `DetalhesMissaoValidator.php`, `FiltroDashboardValidator.php` → **remover** (handlers correlatos serão deletados).

### Novos arquivos (sob `componentes/Missoes/Plataforma/sinais/Api/Handlers/Dashboard/`)

Inalterado vs V1 do plano (a quantidade de handlers não mudou — handler é orquestrador HTTP, não depende da divisão Sonda/Lab):

```
KpisHandler.php                    # GET /api/v1/dashboard/kpis
FluxoDiarioHandler.php             # GET /api/v1/dashboard/fluxo-diario
OvernightSerieHandler.php          # GET /api/v1/dashboard/overnight-serie
RankingSeparadoresHandler.php      # GET /api/v1/dashboard/ranking-separadores
RankingConferentesHandler.php      # GET /api/v1/dashboard/ranking-conferentes
ErrosSeparacaoHandler.php          # GET /api/v1/dashboard/erros-separacao
ErrosSeparacaoComparativoHandler.php
ErrosSeparacaoPorPessoaHandler.php
ErrosCarregamentoHandler.php
ErrosCarregamentoComparativoHandler.php
ErrosPorConferenteHandler.php
TempoMedioItemHandler.php
TempoMedioCarregamentoHandler.php
RetrabalhoHandler.php
```

### Arquivos alterados

- `componentes/Missoes/Logistica/sinais/Dashboard/Dashboard.php` — `renderShell()` deixa de chamar 10 métodos do Lab e passa apenas `defaultRange` (últimos 30 dias úteis) + `defaultTab` (querystring `?tab=` ou `'visao-geral'`) à view. Remover Luz_MissaoCard / Luz_AlertaItem (não usados pela nova UI).
- `componentes/Missoes/Logistica/sinais/Dashboard/html/dashboard.php` — substituído integralmente por **shell** novo: header com FiltroPeriodo + nav de 3 abas + grade de 6 KPI cards (placeholders) + 3 `<section data-aba>` vazias com `data-loaded="false"`.
- `componentes/Missoes/Logistica/sinais/Dashboard/html/dashboard.js` — reescrito do zero (~600 linhas estimadas).
- `componentes/Missoes/Logistica/sinais/Dashboard/html/dashboard.css` (novo) — tokens semafóricos.
- `componentes/Missoes/Plataforma/sinais/Api/Routes.php` — remove 11 rotas obsoletas, mantém `tipos-missao`, adiciona 14 novas.
- `componentes/Missoes/Plataforma/sinais/Api/Api.php` — métodos correspondentes.
- `linguagens/{pt-br,en,es}.json` — chaves novas (lista na seção i18n). **Já tem alterações pendentes não-commitadas** (estado git status); merge cuidadoso.

### Decisão sobre `frontend-documentacao.md`

Mover de `componentes/Missoes/Logistica/sinais/Dashboard/frontend-documentacao.md` (untracked) para `PRD/dashboard-operacional/99-design-reference.md` ao final da feature.

## Sondas e schema

### Mapeamento entidades-do-domínio (spec) → tabelas Genesis (R3 atualizado)

| Entidade da spec | Tabela real | Sonda V2 |
|---|---|---|
| `Pessoa` (separador/conferente) | `usuarios` (Galaxia) + `missao_logistica_usuarios` | Galaxia já cobre `usuarios`; **agregação de papel/participação fica em `RankingSeparadoresLaboratorio`** consumindo as 2 Sondas existentes via JOIN. Sem Sonda nova. |
| `RotaSeparacao` | `missao_logistica` com `tipo_missao_id=2` | **REUSA `MissaoLogisticaSonda`** (já existe) |
| `ParticipacaoSeparacao` | `missao_logistica_usuarios` + `missao_logistica_itens` | combinação no Lab — `RankingSeparadoresLaboratorio` faz a agregação por usuário (SQL agregado consumindo `MissaoLogisticaItemSonda::agruparPorConcluidoPor` + JOIN com `missao_logistica_usuarios` direto, ou via método novo na Sonda existente) |
| `Carregamento` | `missao_logistica` com `tipo_missao_id=3` | **REUSA `MissaoLogisticaSonda`** (filtro por tipo) + `RotaCarregamento` (Central) quando precisar de detalhe de rota |
| `Erro de separação (trocado/falta/sobra)` | derivado de `missao_logistica_itens.quantidade_esperada/quantidade_bipada/conferido` | classificação fica em `ErrosSeparacaoLaboratorio::classificarTipo($item)`; leitura via `MissaoLogisticaItemSonda::listarItensComDivergencia` (já existe) |
| `ErrosMotorista` (4 tipos) | tabela `erros_motorista` (criação condicional — Risco 1) | **`ErrosMotoristaSonda`** se confirmada inexistência (única Sonda nova) |
| `pendenteOvernight` | derivar de timestamps em `missao_logistica` | método em `FluxoLaboratorio` lendo `MissaoLogisticaSonda::listarOvernight` (já existe) |

### Sondas V2 finas — contrato (estado em disco — REUSAR)

Padrão único: cada Sonda extends `ModelInteligencia`, expõe métodos de leitura **focados em filtros típicos do domínio** (não agrega / não calcula score / não monta sparkline — isso é Lab). Contratos **já implementados**:

**`MissaoLogisticaSonda`** — `componentes/Missoes/Logistica/sinais/Dashboard/Sondas/MissaoLogisticaSonda.php` (entity: `missao_logistica`, db: `gx_goldie`, `customerControl: true`)
```
- listarPorJanela(int $tipoMissaoId, string $from, string $to): array
- contarPorTipoNaJanela(int $tipoMissaoId, string $from, string $to): int
- agruparPorDia(int $tipoMissaoId, string $from, string $to): array          // [{data, qtd}]
- listarOvernight(string $from, string $to): array                           // SEPARACAO sem CARREGAMENTO no mesmo dia
- serieDiariaPorTipo(string $from, string $to): array                        // [{data, separacoes, carregamentos}]
```

**`MissaoLogisticaItemSonda`** — `componentes/Missoes/Logistica/sinais/Dashboard/Sondas/MissaoLogisticaItemSonda.php` (entity: `missao_logistica_itens`, `customerControl: true`)
```
- listarPorMissoesNaJanela(array $tipoMissaoIds, string $from, string $to): array
- somarQuantidadeBipada(int $tipoMissaoId, string $from, string $to): int
- somarCubagem(int $tipoMissaoId, string $from, string $to): float           // usa dados_adicionais->>'cubagem'
- agruparPorConcluidoPor(string $from, string $to): array                    // [{usuario_id, itens, cubagem, tempo_seg_medio}]
- listarItensComDivergencia(string $from, string $to): array                 // bipada<>esperada (insumo do ErrosSeparacaoLab)
- serieDiariaItens(string $from, string $to): array                          // [{data, itens, cubagem}]
```

> **Se algum Lab precisar de método novo na Sonda** (ex.: `RankingSeparadoresLaboratorio` querer um `agruparPorConcluidoPorComLideranca()` que faça JOIN com `missao_logistica_usuarios`), o dev pode **adicionar método na Sonda existente** — desde que continue 1:1 com a tabela e a query seja crua/agregação SQL pura. **Não criar Sonda nova**. O dev decide caso a caso, mas a default é manter o JOIN no Lab via SQL ad-hoc lendo da Sonda principal.

### Sonda V2 nova (CONDICIONAL — única)

**`ErrosMotoristaSonda`** (entity: `erros_motorista`, **CONDICIONAL** — só se Risco 1 confirmar tabela inexistente, `customerControl: true`)
```
- contarPorTipo(string $from, string $to): array                             // [tipo => qtd]
- agruparPorConferente(string $from, string $to): array                      // [{conferente_id, total, por_tipo}]
- listarPorJanela(string $from, string $to): array                           // SELECT cru
```

> **Comparativo mensal** (mesma janela meses atrás) **não vai na Sonda** — vai no `ComparativoMensalLaboratorio` que chama `ErrosMotoristaSonda::contarPorTipo()` 2× com janelas diferentes. Sonda continua fina.

Sondas **nunca** retornam estrutura específica do Dashboard (sparklines, scores, faixas semafóricas) — isso é responsabilidade dos Labs.

### Migrations

(Inalteradas vs V1 do plano — listadas para completude. Já existem em `Migrations/`.)

#### `2026_05_05_add_dashboard_operacional_indexes.sql`
```sql
BEGIN;
CREATE INDEX IF NOT EXISTS idx_missao_tipo_concluido
    ON missao_logistica (tipo_missao_id, finalizado_em)
    WHERE finalizado_em IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_mlu_usuario_papel
    ON missao_logistica_usuarios (usuario_id, papel);
CREATE INDEX IF NOT EXISTS idx_itens_concluido_por_em
    ON missao_logistica_itens (concluido_por, concluido_em)
    WHERE concluido_em IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_itens_quantidades
    ON missao_logistica_itens (missao_logistica_id)
    WHERE quantidade_bipada <> quantidade_esperada;
COMMIT;
```

#### `2026_05_05_create_erros_motorista.sql` — **CONDICIONAL** (ver Risco 1)
```sql
BEGIN;
CREATE TABLE IF NOT EXISTS erros_motorista (
    id SERIAL PRIMARY KEY,
    missao_logistica_id INTEGER NOT NULL REFERENCES missao_logistica(id) ON DELETE CASCADE,
    conferente_usuario_id INTEGER,
    motorista_usuario_id INTEGER,
    tipo VARCHAR(40) NOT NULL,
    descricao TEXT,
    pedido_id INTEGER,
    romaneio_id INTEGER,
    block_empresa_id INTEGER NOT NULL,
    block_filial_id  INTEGER,
    block_usuario_id INTEGER NOT NULL,
    data_cadastro TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW(),
    data_atualizacao TIMESTAMP WITHOUT TIME ZONE,
    usuario_cadastro VARCHAR(255),
    usuario_atualizacao VARCHAR(255)
);
CREATE INDEX idx_erros_motorista_missao ON erros_motorista (missao_logistica_id);
CREATE INDEX idx_erros_motorista_conferente_data ON erros_motorista (conferente_usuario_id, data_cadastro);
CREATE INDEX idx_erros_motorista_tipo_data ON erros_motorista (tipo, data_cadastro);
CREATE INDEX idx_erros_motorista_empresa ON erros_motorista (block_empresa_id);
COMMIT;
```

`propeties` da `ErrosMotoristaSonda`: `customerControl: true, companyControl: false, userControl: false`.

## Endpoints / fluxo

Todas as rotas são `GET`, gateadas pelo middleware JWT existente em `/api/v1` (sessão Galaxia same-origin). Querystring obrigatória: `from`, `to` (Y-m-d).

| Método | Rota | Handler | Lab → Sondas | Validator |
|---|---|---|---|---|
| GET | /api/v1/dashboard/kpis | KpisHandler | KpisLab → MissaoLogisticaSonda + MissaoLogisticaItemSonda | FiltroPeriodoValidator |
| GET | /api/v1/dashboard/fluxo-diario | FluxoDiarioHandler | FluxoLab → MissaoLogisticaSonda | FiltroPeriodoValidator |
| GET | /api/v1/dashboard/overnight-serie | OvernightSerieHandler | FluxoLab → MissaoLogisticaSonda::listarOvernight | FiltroPeriodoValidator |
| GET | /api/v1/dashboard/ranking-separadores | RankingSeparadoresHandler | RankingSeparadoresLab → MissaoLogisticaSonda + MissaoLogisticaItemSonda + JOIN ad-hoc com missao_logistica_usuarios | FiltroPeriodoValidator + PaginacaoValidator |
| GET | /api/v1/dashboard/ranking-conferentes | RankingConferentesHandler | RankingConferentesLab → MissaoLogisticaSonda + MissaoLogisticaItemSonda + JOIN ad-hoc | FiltroPeriodoValidator + PaginacaoValidator |
| GET | /api/v1/dashboard/erros-separacao | ErrosSeparacaoHandler | ErrosSeparacaoLab → MissaoLogisticaItemSonda | FiltroPeriodoValidator |
| GET | /api/v1/dashboard/erros-separacao/comparativo | ErrosSeparacaoComparativoHandler | ErrosSeparacaoLab + ComparativoMensalLab | FiltroPeriodoValidator + ComparativoValidator |
| GET | /api/v1/dashboard/erros-separacao/por-pessoa | ErrosSeparacaoPorPessoaHandler | ErrosSeparacaoLab paginado | FiltroPeriodoValidator + PaginacaoValidator |
| GET | /api/v1/dashboard/erros-carregamento | ErrosCarregamentoHandler | ErrosCarregamentoLab → ErrosMotoristaSonda | FiltroPeriodoValidator |
| GET | /api/v1/dashboard/erros-carregamento/comparativo | ErrosCarregamentoComparativoHandler | ErrosCarregamentoLab + ComparativoMensalLab | FiltroPeriodoValidator + ComparativoValidator |
| GET | /api/v1/dashboard/erros-por-conferente | ErrosPorConferenteHandler | ErrosCarregamentoLab paginado | FiltroPeriodoValidator + PaginacaoValidator |
| GET | /api/v1/dashboard/tempo-medio-item | TempoMedioItemHandler | RankingSeparadoresLab::tempoMedioGeral | FiltroPeriodoValidator |
| GET | /api/v1/dashboard/tempo-medio-carregamento | TempoMedioCarregamentoHandler | RankingConferentesLab::tempoMedioGeral | FiltroPeriodoValidator |
| GET | /api/v1/dashboard/retrabalho | RetrabalhoHandler | RetrabalhoLab → MissaoLogisticaItemSonda | FiltroPeriodoValidator |
| GET | /api/v1/dashboard/tipos-missao | (mantido) TiposMissaoHandler | (mantido) | (mantido) |

### Auditoria dos 12 handlers atuais

| Handler atual | Decisão | Razão |
|---|---|---|
| `ResumoOperacionalHandler` | **REMOVER** | era contadores do dia (kanban); substituído por `KpisHandler` com janela. |
| `ListarMissoesHandler` | **REMOVER** | nova UI não tem listagem de missões. |
| `DesempenhoHandler` | **REMOVER** | era ranking diário; substituído por `RankingSeparadoresHandler` + `RankingConferentesHandler` com janela. |
| `AlertasHandler` | **REMOVER** | nova UI não tem cards de alerta. |
| `TiposMissaoHandler` | **MANTER** | usado por filtros e selects. |
| `ExtratoHandler` | **REMOVER** | nova UI não tem timeline. |
| `VisaoGeralDiaHandler` | **REMOVER** | substituído por `KpisHandler`. |
| `VeiculosStatusHandler` | **REMOVER** | nova UI não mostra status de veículos. |
| `RotasDoDiaHandler` | **REMOVER** | substituído por `FluxoDiarioHandler`. |
| `FiltrarHandler` | **REMOVER** | sem uso novo. |
| `DetalhesMissaoHandler` | **REMOVER** | nova UI não tem drill-down. |
| `AcaoMissaoHandler` (POST) | **REMOVER** | nova UI é leitura pura. |

Sobrevive: **1 handler** (`TiposMissaoHandler`).

## Frontend Genesis-stack

(Inalterado vs V1 do plano.)

### Bibliotecas escolhidas

| Função | Lib | Justificativa |
|---|---|---|
| Charts | **ApexCharts** | já em `node_modules/apexcharts`; já usado em `Constelacoes/Dashboards/Graficos`. |
| DateRange picker | **flowbite-datepicker `DateRangePicker`** | já em `node_modules/flowbite-datepicker`; i18n PT/EN/ES nativos. |
| Tabela paginada | **vanilla JS** com `<template>` + paginação client-side | rankings paginados pelo handler. |

### Estrutura `dashboard.js`

```
DashboardBoot                  (injetado pelo PHP — defaultRange, defaultTab, csrf, i18n)
DashboardApi.get(path, qs)
DashboardState                 ({ from, to, abaAtiva })
DashboardRouter                (querystring; troca de aba sem reload)
DashboardFiltro                (DateRangePicker)
DashboardKpis                  (6 cards + sparklines)
DashboardAbas.visaoGeral       (FluxoDiario AreaChart + Overnight BarChart + Top3)
DashboardAbas.separacao        (cards + Pie erros + Ranking + Comparativo)
DashboardAbas.carregamento     (cards + Retrabalho line + Ranking + ErrosPorConferente)
```

### Persistência de estado

Querystring (`?from=...&to=...&tab=separacao`). F5-safe, link compartilhável, zero lib.

### Dispatcher fino em `Dashboard.php`

```php
private function renderShell(array $dados): self {
    $hoje = date('Y-m-d');
    $from = $_GET['from'] ?? date('Y-m-d', strtotime('-30 weekdays'));
    $to   = $_GET['to']   ?? $hoje;
    $tab  = in_array($_GET['tab'] ?? '', ['visao-geral','separacao','carregamento'])
        ? $_GET['tab'] : 'visao-geral';
    $dados['boot'] = ['from'=>$from, 'to'=>$to, 'tab'=>$tab, 'i18n'=>$this->i18nKeys()];
    $resposta['html'] = $this->galaxiaRoute->visual('html/dashboard', $dados, []);
}
```

Sem chamadas a Lab no boot (AC8.5).

## Padrões de extensibilidade

**N/A.** 1 fluxo único de leitura: UI → handler → Lab → Sondas. Nenhum ponto de extensão polimórfica é exigido pela spec. `ScoreCalculator` é classe utilitária pura, não padrão arquitetural.

## Performance / cache

- Índices novos cobrem queries quentes: `tipo_missao_id + finalizado_em`, `concluido_por + concluido_em`, `usuario_id + papel`.
- **Cache na rodada 1: NÃO.** Critério de upgrade: p95 >500ms em `dev_gx_goldie` para 30 dias → cache curto (5 min) por `(customer_id, from, to, tipo)`.
- Agregação no banco (Sondas usam `GROUP BY date_trunc(...)`), nunca em PHP.
- `FiltroPeriodoValidator` rejeita range > 365 dias (RN17).
- Agregação semanal automática quando range > 60 dias (RN16) — feita no SQL.

## Validators dedicados

(Inalterados vs V1.)

- `FiltroPeriodoValidator` — `from`, `to` (Y-m-d), `from <= to`, range ≤ 365.
- `PaginacaoValidator` — `page` (≥1, default 1), `perPage` (1..100, default 20).
- `ComparativoValidator` — `mesesAtras` (1..12, default 1).

## i18n (chaves novas — pt-br/en/es)

```
dashboard_op_titulo
dashboard_op_aba_visao_geral / separacao / carregamento
dashboard_op_kpi_separacoes / carregamentos / itens / cubagem / eficiencia / overnight
dashboard_op_filtro_periodo / aplicar / range_invalido / range_max_365
dashboard_op_fluxo_titulo / overnight_titulo
dashboard_op_top3_separadores / top3_conferentes
dashboard_op_tempo_medio_item / pie_tipos_erro
dashboard_op_erro_trocado / erro_falta / erro_sobra
dashboard_op_ranking_geral / posicao / nome / itens / cubagem / tempo / assertividade / rotas / lider / score / retrabalhos
dashboard_op_erros_por_mes / comparativo_mensal / variacao
dashboard_op_separadores_mais_erros / erros_por_conferente
dashboard_op_erro_item_faltando / item_trocado / avaria_transporte / endereco_errado
dashboard_op_indice_retrabalho
dashboard_op_carregando / sem_dados / sem_dados_periodo
```

## Ordem de implementação (sequencial — atualizada R3)

1. **Sondagem schema** em `psql dev_gx_goldie`: confirmar (a) `codigo` de SEPARACAO em `tipos_missao_logistica`, (b) existência da tabela `erros_motorista`, (c) derivação de overnight via timestamps. **Destrava ou bloqueia tudo.**
2. Migration de índices → `EXPLAIN` em query de exemplo.
3. (Condicional) Migration `erros_motorista` + seed mínimo + criar `Sondas/ErrosMotoristaSonda.php` (única Sonda nova) com `Testes/Sondas/ErrosMotoristaSondaTddTest.php` cobrindo o contrato V2.
4. ~~Sondas V2 finas (`MissaoLogisticaSonda`, `MissaoLogisticaItemSonda`, `MissaoLogisticaUsuarioSonda`)~~ — **R3 elimina esta etapa**: as 2 primeiras Sondas já existem em disco, a terceira não será criada (vira agregação no Lab).
5. `ScoreCalculator` puro + `ScoreCalculatorTddTest` (TDD, 100% offline). **Já existe em `Laboratorios/ScoreCalculator.php` — verificar e completar.**
6. `FiltroPeriodoValidator` + `PaginacaoValidator` + `ComparativoValidator` + testes unit.
7. `KpisLaboratorio` — **mover** conteúdo de `Sondas/KpisSonda.php` para `Laboratorios/KpisLaboratorio.php`, trocar hierarquia (não estende ModelInteligencia), receber `MissaoLogisticaSonda` e `MissaoLogisticaItemSonda` no construtor, substituir SQL inline por chamadas aos métodos das Sondas existentes. Deletar `Sondas/KpisSonda.php`. + `KpisHandler` + integration test → primeiro endpoint funcional ponta-a-ponta.
8. `FluxoLaboratorio` — **mover** conteúdo de `Sondas/FluxoDiarioSonda.php` para `Laboratorios/FluxoLaboratorio.php`. Mesma estratégia. Deletar `Sondas/FluxoDiarioSonda.php`. + handlers `FluxoDiarioHandler` e `OvernightSerieHandler`.
9. `RankingSeparadoresLaboratorio` (inclui agregação por usuário/papel via SQL agregado consumindo `MissaoLogisticaItemSonda` + JOIN com `missao_logistica_usuarios`) + handler + `TempoMedioItemHandler`.
10. `RankingConferentesLaboratorio` + handler + `TempoMedioCarregamentoHandler`.
11. `ErrosSeparacaoLaboratorio` + `ErrosCarregamentoLaboratorio` + handlers (incluindo comparativo e por-pessoa/por-conferente).
12. `RetrabalhoLaboratorio` + handler.
13. `ComparativoMensalLaboratorio` (consumido pelos 2 endpoints de comparativo).
14. **Refactor de Routes.php + Api.php**: remover 11 handlers obsoletos, adicionar 14 novos. Rodar suíte de integration.
15. Refactor `Dashboard.php` (renderShell mínimo) + novo `dashboard.php` (shell HTML).
16. `dashboard.js` reescrito do zero — começar por DashboardKpis + Filtro, depois cada aba.
17. `dashboard.css` (cores semafóricas) + i18n nas 3 línguas.
18. Smoke test manual em `/missao/logistica/dashboard?from=...&to=...`.
19. **Confirmar deleção** de `Sondas/KpisSonda.php` e `Sondas/FluxoDiarioSonda.php` após migrar conteúdo. `grep -r "KpisSonda\|FluxoDiarioSonda" componentes/` deve retornar zero ocorrências.
20. Mover `frontend-documentacao.md` para `PRD/dashboard-operacional/99-design-reference.md`.

## Riscos e decisões não-óbvias

1. **Schema não tem `erros_motorista` explícita.** Sondagem em `dev_gx_goldie` na etapa 1 é mandatória; se confirmado inexistente, criar tabela e Sonda — mas dados históricos ficam vazios até o sistema começar a registrar. Gate explícito ao PO antes do dev começar.
2. **Definição de tipo de erro de separação (trocado/falta/sobra)** pode não ser determinística a partir de `quantidade_esperada`/`quantidade_bipada`/`conferido` apenas. **Mitigação:** isolar regra em `ErrosSeparacaoLaboratorio::classificarTipo($item): string` (Lab, não Sonda — é regra de negócio). Teste unit cobrindo cada caso.
3. **Multi-tenant em queries agregadas com JOIN**: `missao_logistica`, `missao_logistica_usuarios` e `missao_logistica_itens` têm `block_empresa_id`/`block_filial_id` — cuidado para não duplicar contagens nem perder rows. **Mitigação:** filtrar tenancy SEMPRE na missão raiz e propagar via JOIN. As Sondas V2 finas existentes (`MissaoLogisticaSonda`, `MissaoLogisticaItemSonda`) já garantem isso via `customerControl: true` e via filtro explícito `block_empresa_id = :empresa` no SQL cru. Cobrir com `TenancyDashboardIntegrationTest`.
4. **Sondas V2 existentes ficam dentro do sinal `Dashboard/Sondas/`, não em `Constelacoes/`.** Convivem com `Inteligencias/` V1 das mesmas tabelas — risco de devs futuros confundirem qual usar. **Mitigação:** o docblock no topo de cada Sonda já documenta a coexistência (verificado nos arquivos em disco). Quando outro sinal adotar essas Sondas, promover para `Constelacoes/Logistica/MissaoLogistica/Sondas/` em PR de refactor puro.
5. **`RankingSeparadoresLaboratorio` precisa fazer JOIN com `missao_logistica_usuarios` sem ter Sonda V2 dessa tabela.** Duas opções: (a) Lab faz SQL ad-hoc com JOIN consumindo `$this->conn` injetado; (b) adicionar método `agruparPorConcluidoPorComLideranca()` na Sonda `MissaoLogisticaItemSonda` existente fazendo o JOIN ali. **Decisão**: opção (b) é menos limpa (Sonda passa a conhecer outra tabela). Opção (a) é a recomendada — Lab pode ter helper `rawSql()` próprio similar ao das Sondas (já há padrão estabelecido no `KpisSonda.php` atual). Se o dev preferir (b), aceitável desde que o método novo continue sendo só leitura/agregação SQL.

## Breaking changes

- **API**: 11 endpoints `/api/v1/dashboard/*` removidos. API é interna (`project_api_interna.md`) — único consumidor era o `dashboard.js` legado, reescrito junto. Antes do merge: `grep -r "fetch('/api/v1/dashboard"` em todo o repo.
- **Rota visual**: `/missao/logistica/dashboard` muda de kanban para painel operacional. Substituição in-place.
- **Schema**: índices novos são aditivos. Tabela `erros_motorista`, se criada, é nova. Nenhum DROP/ALTER.
- **Símbolos PHP**: classes `KpisSonda` e `FluxoDiarioSonda` deixam de existir (renomeadas para Labs). Como ambas são novas e ainda não foram referenciadas por nada fora do próprio sinal, blast radius zero. Confirmar via grep antes de deletar.

## Plano de teste

### Unit (PHP, standalone, sem DB)
- `ScoreCalculatorTddTest.php` — fórmulas §9 da spec.
- `FiltroPeriodoValidatorTddTest.php`, `PaginacaoValidatorTddTest.php`, `ComparativoValidatorTddTest.php`.
- `KpisLaboratorioTddTest.php` — Lab com Sondas stubadas (eficiência, divisão por zero RN3).
- `RankingSeparadoresLaboratorioTddTest.php` — assertividade com `itens=0` retorna null/"—" (RN5); cobre agregação por usuário (substitui o teste de `MissaoLogisticaUsuarioSonda` que não existirá).
- `ErrosSeparacaoLaboratorioTddTest.php` — `classificarTipo($item)` para cada combinação (esperada/bipada/conferido).
- `ErrosMotoristaSondaTddTest.php` — **CONDICIONAL** (só se Sonda for criada): smoke do contrato V2 (entity correta, propeties, métodos públicos retornam tipos esperados).

### Integration (rodam só em `dev_gx_goldie`)
- `KpisIntegrationTest.php` — 30 dias contra dados reais; cada KPI vs `SELECT` cru de referência.
- `FluxoDiarioIntegrationTest.php` — soma das séries diárias = total de KPIs.
- `RankingSeparadoresIntegrationTest.php` — top-1 por score bate com cálculo manual.
- `ErrosCarregamentoIntegrationTest.php` — totais por tipo somam ao total geral.
- `TenancyDashboardIntegrationTest.php` — usuário de outro `customer` não vê dados alheios em nenhum endpoint.
- `PerformanceDashboardIntegrationTest.php` — cada handler de KPI/série <500ms para 30 dias.

### Manual / smoke
- Abrir `/missao/logistica/dashboard` sem querystring → últimos 30 dias úteis em "Visão Geral".
- Mudar período → KPIs e gráficos atualizam, skeletons aparecem.
- Trocar aba → URL atualiza, KPIs do topo continuam visíveis (AC3.4).
- F5 com `?from=...&to=...&tab=separacao` → estado restaurado.
- Trocar idioma no Galaxia → todas as labels mudam (AC7.2).
- Tentar período de 400 dias → mensagem de erro (AC2.6).

## Out of scope (confirmando spec)

- React/Vite/Tailwind/shadcn/Recharts no repo.
- Criar novo sinal (reusa `Logistica/sinais/Dashboard/`).
- Mexer em outros sinais de Logística (Edicaorota, Board, Central, Armazem, Expedicao).
- Abrir `/api/v1` publicamente.
- Exportações CSV/PDF.
- Auth/authz nova.
- Alterar schema do banco para colunas existentes; **exceção controlada**: criar `erros_motorista` se sondagem confirmar inexistência (Risco 1).
- Mock determinístico (PRNG).
- Manter o kanban antigo em paralelo.

**Acréscimos técnicos out of scope:**

- Cache server-side de handlers (rodada 2 condicionada a baseline de performance).
- **Migrar `Inteligencias/MissaoLogistica.php`, `MissaoLogisticaItem.php`, `MissaoLogisticaUsuario.php` (V1) para Sondas V2 globais em `Constelacoes/`** — fora do escopo. As Sondas V2 desta feature ficam dentro do sinal `Dashboard/Sondas/` para minimizar blast radius. Promoção para `Constelacoes/` é PR futuro de refactor puro quando 2º consumidor aparecer.
- **Criar Sonda V2 para `missao_logistica_usuarios`** — R3 elimina. A leitura agregada para ranking/contagem de papéis fica no `RankingSeparadoresLaboratorio` via SQL ad-hoc. Quando outro sinal precisar de leitura crua dessa tabela, criar Sonda V2 em PR separado.
- WebSocket / live update.
- Drill-down de uma rota/missão a partir do dashboard.
