# Dashboard Expedicao

Painel agregado de expedição/logística por cliente.
Acessível em `https://<cliente>.goldieapp.com.br/missao/dashboards/Expedicao`.

Arquitetura **cache-first**: o dashboard nunca calcula em tempo real — lê de uma
tabela pré-agregada (`dashboard_expedicao_cache`) populada por cron diário.

---

## Estrutura de arquivos

```
componentes/Missoes/Dashboards/sinais/Expedicao/
├── Expedicao.php                      # Sinal: monta tela e textarea com JSON inicial
├── Laboratorios/
│   ├── Filtros.php                    # opção 'filtrar': lê cache → shape do front
│   └── AtualizarCacheExpedicaoLaboratorio.php   # UPSERT idempotente
├── Sondas/
│   ├── DashboardExpedicaoCacheSonda.php  # model V2 da tabela de cache
│   └── Filtro.php                     # (legado, usado pelo CRUD do filtro)
├── forms/formulario_filtros.php       # form do header (período + agrupamento)
├── html/estrutura.php                 # view + JS (Chart.js + Flatpickr)
├── sql/dashboard_expedicao_cache.sql  # migration da tabela
├── Testes/Laboratorios/
│   └── FiltrosLaboratorioIntegrationTest.php
└── README.md (este arquivo)

crons/dashboard_expedicao_diario.php   # entrypoint CLI do cron
```

---

## Fluxo de dados

```
                                 ┌──────────────────────────────────┐
                                 │   romaneios + romaneios_pedidos  │
                                 │   + romaneios_veiculos + pedidos │
                                 │   + pedidos_produtos             │
                                 └──────────────┬───────────────────┘
                                                │
                                                ▼
                       ┌──────────────────────────────────────────────┐
                       │  AtualizarCacheExpedicaoLaboratorio          │
                       │  - JOIN com grupos_regioes (block escopado)  │
                       │  - distribui métricas do veículo entre       │
                       │    pedidos (evita dupla contagem com p_p)    │
                       │  - GROUP BY (dia_efetivo, grupo_regiao_id)   │
                       │  - DELETE + INSERT no range (idempotente)    │
                       └──────────────┬───────────────────────────────┘
                                      │
                                      ▼
                       ┌──────────────────────────────────────────────┐
                       │  dashboard_expedicao_cache                   │
                       │  PK (block, dia_efetivo, grupo_regiao_id)    │
                       │  ~5–8 linhas/dia/cliente                     │
                       └──────────────┬───────────────────────────────┘
                                      │
                                      ▼
                       ┌──────────────────────────────────────────────┐
                       │  Filtros::filtrar (opcao=filtrar)            │
                       │  SELECT do cache pelo período pedido         │
                       │  Agrega em "todos" + 1 bucket por grupo      │
                       │  Devolve shape esperado por html/estrutura   │
                       └──────────────┬───────────────────────────────┘
                                      │
                                      ▼
                              html/estrutura.php
                              (Chart.js + jQuery)
```

### Mapeamento pedido → grupo

`pedidos.regiao_id (FK regioes_administrativas.id)` é casado contra
`grupos_regioes.regioes_ids` (jsonb array) com escopo de `block`:

```sql
gr.regioes_ids @> to_jsonb(p.regiao_id) AND gr.block = p.block
```

Pedidos cuja `regiao_id` não está em nenhum grupo do cliente caem no
bucket especial **`grupo_regiao_id = 0` / nome `"Outros"`**.

---

## Tabela de cache

Schema em `sql/dashboard_expedicao_cache.sql`. 48 colunas, granularidade
1 linha por `(block, dia_efetivo, grupo_regiao_id)`:

- **identificação**: `block`, `dia_efetivo`, `grupo_regiao_id`, `grupo_regiao_nome`
- **faturamento**: total / proprios / terceiros
- **km**: total / ida / volta / pedidos / proprios / terceiros (vêm de `plan_*` enriquecidos)
- **cubagem**: utilizada × disponível, total × proprios × terceiros
- **quantidades distintas**: pedidos, clientes, cidades, segmentos, produtos, romaneios, romaneios_veiculos, veiculos (total/proprios/terceiros)
- **volume e peso**: produtos, peso_bruto, peso_liquido
- **tempo**: criacao_minutos_total + (numerador/denominador para média ponderada de tempo_medio_entre_pedidos e distancia_media_entre_pedidos na leitura)

UNIQUE em `(block, dia_efetivo, grupo_regiao_id)` — base da idempotência.

---

## O cron

`crons/dashboard_expedicao_diario.php`. CLI puro, idempotente.

### Defaults seguros (cache-only)

```bash
php crons/dashboard_expedicao_diario.php
```

Sem flags: recalcula o cache para **todos os blocks com `grupos_regioes` cadastrados**,
janela dos **últimos 7 dias**. Uso recomendado em produção via crontab.

### Flags

| Flag | Default | Efeito |
|---|---|---|
| `--block=N` | todos | Roda só pra esse block |
| `--dias=N` | `7` | Janela = últimos N dias até hoje |
| `--inicio=YYYY-MM-DD` | — | Início explícito (anula `--dias`) |
| `--fim=YYYY-MM-DD` | — | Fim explícito (default = `--inicio`) |
| `--com-enriquecimento` | **off** | Roda também `Analises::romaneios` (ver aviso abaixo) |

Exemplos:

```bash
# Recalcular dezembro/2025 só pro Galpão TMS
php crons/dashboard_expedicao_diario.php --block=22 --inicio=2025-12-01 --fim=2025-12-31

# Apenas hoje
php crons/dashboard_expedicao_diario.php --inicio=$(date +%Y-%m-%d)

# Backfill 30 dias
php crons/dashboard_expedicao_diario.php --dias=30
```

### Idempotência

O laboratório faz `DELETE + INSERT` dentro de transação para o range
`(block, dataInicio..dataFim)`. Rodar 3× seguidas devolve o mesmo cache.

### Recomendação de schedule (prod)

```cron
# Diário às 03:00 — recalcula cache + enriquecimento Google (max 500 chamadas/run)
# Production-only: o cron aborta se HTTP_HOST contém 'dev-'.
0 3 * * * /usr/bin/flock -n /tmp/dashboard_expedicao_diario.lock -c 'cd /home/goldie/www/genesis && CRON_HOST=genesis.goldieapp.com.br /usr/bin/php crons/dashboard_expedicao_diario.php --com-enriquecimento --max-google-calls=500 >> /home/goldie/www/genesis/logs/dashboard_expedicao_diario.log 2>&1'
```

`CRON_HOST=genesis.goldieapp.com.br` força ambiente prod. Sem essa variável o
cron detecta o default `dev-genesis...` e pula sem fazer nada. Pra testar
manualmente em dev passe `--ambiente-dev`.

A janela de 7 dias cobre romaneios fechados retroativamente. Aumente
`--dias` se houver backfill maior.

---

## `--com-enriquecimento` e Google Maps API

`Analises::romaneios` (em `componentes/Constelacoes/Pedidos/Analises/Satelites/Analises.php`)
é o satélite que preenche os campos `plan_km_total`, `plan_total_km_ida`,
`plan_total_km_volta`, `plan_pedidos_tempo_medio`, etc nos
`romaneios_veiculos`. **Sem enriquecimento, o dashboard mostra KM e
tempo médio zerados** — todo o resto (faturamento, pedidos, cubagem,
veículos) funciona normalmente, pois vem direto das tabelas-fonte.

Hoje a API Google Directions está **religada** com mitigações de custo:

1. **Timeout** de 5s no `file_get_contents` (configurável via `google_timeout_seg`)
2. **Cache in-memory por hash da sequência de coords** — dedupe N+1 dentro do mesmo run
3. **Rate-limit por run**: flag `--max-google-calls=N` no cron
4. **Dry-run**: flag `--dry-run-google` no cron — conta chamadas sem custo
5. **Não marca `atualizado_metricas_planejadas=1`** se alguma chamada Google falhou
   (cai em fallback Haversine pra não zerar dashboard, mas mantém o flag em 0
   pra próxima rodada tentar de novo)

Caminhos de invocação:

```sh
# Mede tráfego sem chamar Google (dev/staging)
php crons/dashboard_expedicao_diario.php --com-enriquecimento --dry-run-google

# Liga com teto duro de 200 chamadas (prod, primeira passada)
php crons/dashboard_expedicao_diario.php --com-enriquecimento --max-google-calls=200

# Liga sem rate-limit (depois de validar custos com --max-google-calls)
php crons/dashboard_expedicao_diario.php --com-enriquecimento
```

A função `latLng()` (geocoding) continua com kill-switch pois não tem callers
ativos e a chave está hardcoded — será tratada quando aparecer um caller.

**Pendente (próxima fase)**: mover `KEYMAPSGOOGLE` e a chave hardcoded de
`latLng()` pra env var. Hoje a chave segue versionada em `MainBoth.php` /
no source de `Analises.php`.

O modo simulação Haversine × 1.3 ainda é o fallback automático quando
a API real falha (timeout, quota, erro de rede). Aceitável pra dashboard
agregado — não pra auditoria fiscal.

---

## Adicionar um novo cliente ao dashboard

1. **Cadastrar `grupos_regioes`** pro block do cliente (via Admin ou
   diretamente no banco — `nome`, `regioes_ids` jsonb array com
   `regioes_administrativas.id`, `block`).
2. Pronto. Na próxima execução do cron, o laboratório descobre o block
   automaticamente:
   ```sql
   SELECT DISTINCT block FROM grupos_regioes WHERE block IS NOT NULL AND block > 0
   ```

Pedidos com `regiao_id` fora dos grupos cadastrados ficam no bucket
"Outros". Cobertura típica em prod ~97%.

---

## Frontend

`html/estrutura.php` recebe um `<textarea id="dashboard-data">` com o JSON
agregado e renderiza:

- **Big numbers**: faturamento, qtd_pedidos, qtd_clientes, etc.
- **KM**: total/ida/volta/casa/terceiros/entre_pedidos
- **Cubagem**: utilizada (m³) e eficiência (%) total/próprios/terceiros
- **Faturamento e cubagem por veículo** (Chart.js doughnut)
- **Faturamento por região** (Chart.js bar) — labels e cores dinâmicos
  a partir das chaves do JSON

**Botões de agrupamento são renderizados dinamicamente** em
`renderBotoesAgrupamento()` a partir das chaves do JSON. O frontend
não conhece a lista de grupos em compile-time.

### Filtros de período

- **Hoje**, **Semana Atual**, **Mês Atual**: calculados em JS, formatos
  `dd-mm-yyyy`, `dd/mm/yyyy to dd/mm/yyyy`, `mm-yyyy` respectivamente.
- **Personalizado** (Flatpickr): monta o range diretamente dos `Date`
  objects (`dd/mm/yyyy to dd/mm/yyyy`) — não depende do `rangeSeparator`
  do locale do flatpickr, que varia ("até" em pt, "to" em en, etc).

### Shape esperado pelo front

```js
{
  "todos": {
    "label": "Todos",
    "faturamento": "R$ 30.950,77",
    "calculo_km": { total_km, somente_ida, carros_casa, somente_volta, carros_terc, entre_pedidos },
    "big_numbers": { qtd_produtos, qtd_pedidos, qtd_clientes, volume_prod, cidades_atend, segm_atend, peso_bruto, peso_liquido, tempo_medio_entre_pedidos, distancia_media_entre_pedidos },
    "dados_cubagem": { total, carros_alper, carros_terc },
    "eficiencia_cubagem": [ {label: "Total", value}, {label: "Veículos Próprios", value}, {label: "Veículos Fretados", value} ],
    "faturamento_veiculo": { carros_alper, carros_terc },
    "volume_veiculos": { alper, terceiro },     // percentuais
    "quantidade_veiculos": { total, proprios, fretados },
    "agrupamentos_labels": { chave: "Nome Original", ... }
  },
  "<chave_slug_do_grupo>": { ...mesmo shape, sem agrupamentos_labels... },
  ...
}
```

A chave `agrupamentos_labels` no `todos` é a fonte de verdade para
renderizar os botões.

---

## Multi-tenant / Block isolation

- Toda query usa `block = :block` parametrizado, vindo de
  `getSession('galaxiaUserInfo')->customerData->id`
- O cron itera por blocks descobertos em `grupos_regioes`
- A tabela `dashboard_expedicao_cache` tem coluna `block` em todos os reads
- Teste cobre o caso de um customer fictício não enxergar dados de outro

---

## Testes

```bash
php componentes/Missoes/Dashboards/sinais/Expedicao/Testes/Laboratorios/FiltrosLaboratorioIntegrationTest.php
```

Roda contra `dev_gx_goldie` (fixo). Exige `$_SESSION['galaxiaUserInfo']`
em **dois** lugares (raiz + grupo `dashboard`) — o test bootstrap já
cuida disso. 60 asserts cobrindo:

- Pipeline cache (upsert idempotente)
- Shape do response em todas as chaves
- Soma de pedidos por grupo == "todos"
- Multi-tenant isolation
- Cache-only fallback (sem cache → "todos" zerado)
- Validações de input (período vazio, sem customerData, datas inválidas)

---

## Cache vazio: o que esperar

Se o cron **não rodou** ainda para um cliente:
- `Filtros::filtrar` retorna `success` com **apenas o bucket "todos"
  zerado** — não dispara fallback ao SQL bruto
- O dashboard mostra todos os números como `--`

Se o cron **rodou mas não há romaneios** no período:
- Cache fica com 0 linhas para aquele range
- Mesmo comportamento acima

Esse é o contrato — sem fallback "on-the-fly" para evitar a query pesada
voltar à vida silenciosamente. Se números estão zerados, é sinal de que
ou o cron precisa rodar, ou o cliente realmente não teve operação.
