diff --git a/README.md b/README.md
index e3d1a16..36b65aa 100644
--- a/README.md
+++ b/README.md
@@ -2113,3 +2113,121 @@ docker compose exec app bundle exec rspec \
```
+
+---
+
+🔢 Dashboard × Operações: por que os números não batiam — notas x visitas + painel de avulsas (24/08/2026)
+
+> ⚠️ **STATUS: implementado, ainda NÃO executado.** Não há Ruby/Bundler nem Postgres na máquina de
+> desenvolvimento — foi conferida a sintaxe de todos os `.rb` e `.erb` alterados. **A suíte e a
+> validação com dado real continuam pendentes** — roteiro no fim desta seção.
+
+### 🎯 O problema
+Mesmo período filtrado, dois números diferentes:
+
+| | Dashboard financeiro | Dashboard de Operações |
+|---|---|---|
+| Total | 4977 | 4973 |
+| Entregues / Sucesso | 4852 | 4851 |
+| Falhadas / Recusas | 124 | 122 |
+| Pendentes | 1 | 0 |
+
+Não era arredondamento: **as duas telas contam coisas diferentes**, e nada na interface dizia isso.
+
+- O **financeiro** conta **visitas** (idas ao local). É o recorte certo lá, porque é por ida que o
+ motorista recebe — `Entrega.contar_atendidas` conta linhas, e a consolidação paga em cima disso.
+- **Operações** conta **notas fiscais** (último status de cada NF). É o recorte certo aqui, porque
+ é o que o cliente paga e o que confere nos documentos físicos — o mesmo critério da aba ENTREGAS
+ da planilha entregue (`Analytics::PlanilhaEntregas`).
+
+Uma NF que falhou dia 10 e foi entregue dia 12 vale **2 no financeiro e 1 em Operações**. Correto
+nos dois — mas invisível.
+
+### 🐛 Três defeitos reais por trás disso
+
+**1. O dedup rodava sobre a tabela inteira, não sobre o período.** O `ROW_NUMBER() ... rn = 1`
+ficava numa CTE **antes** do filtro de data. Se o último checkout de uma NF era **posterior** ao fim
+do período, a visita que aconteceu **dentro** do período sumia da contagem do mês. Subcontagem
+silenciosa em todo fechamento. Agora o dedup acontece em `#linhas`, **depois** do período e do
+cross-filter.
+
+**2. O dedup escondia todo o insucesso reentregue.** NF que falhou duas vezes antes de entregar
+aparecia como 100% de sucesso, e o motivo sumia do "Índices de falha".
+
+**3. O período nem era aplicado no modo Operação.** `montar([@operacao], ...)` era chamado **sem
+`inicio:`/`fim:`** e o seletor de data só aparecia no modo Global — comparar as duas telas "no mesmo
+período" era literalmente impossível.
+
+### 🆕 O que mudou na tela
+
+**Operações — camada "Visitas ao local"** (abaixo dos 4 cards): visitas realizadas, retentativas,
+insucessos por visita e "entregues na 2ª ida ou mais". Os 4 cards de cima seguem contando **notas**.
+
+**Operações — painel "Notas fora da operação"**: NFs entregues no período que **não estão em nenhuma
+planilha `gade_entregas_*`** — os planos avulsos e de inclusão. Contavam no financeiro e o
+`INNER JOIN` com a tabela da operação as descartava aqui. Agora aparecem com NF, plano/título,
+motorista, unidade, data, resultado e motivo, com quebra por plano de origem.
+
+**Operações — período no modo Operação**: o seletor passa a valer também aqui, mas **só quando o
+operador escolhe uma faixa** (`inicio`/`fim` na URL). Sem escolha, o recorte segue sendo a operação
+inteira — senão abrir uma operação de meses atrás cairia no mês corrente e mostraria zero. Botão
+**"Operação inteira"** volta ao recorte natural.
+
+**Financeiro — linha "N notas fiscais"** no card Total Entregas, ao lado de "visitas atendidas".
+
+### ⚙️ Pontos não-óbvios
+
+**Duas camadas no mesmo objeto.** `OperacaoMetricas#visitas` = uma linha por ida; `#linhas` = uma
+linha por NF (a última visita). Todos os KPIs, o donut, os motivos, o mapa e a tabela espelho
+continuam saindo de `#linhas` — ou seja, **a tela segue batendo com a planilha do cliente**. Só a
+faixa nova lê `#visitas`.
+
+**`uniq` por `tracking_id`.** Sem a CTE, se a mesma `nota_fiscal` estiver repetida dentro de uma
+tabela de operação (ou em duas tabelas do UNION global), o `INNER JOIN` devolvia a **mesma visita**
+mais de uma vez. Agora colapsa.
+
+**Filtro de conta unificado.** `Entrega.condicao_conta_sql` nasceu para as queries cruas de
+`Analytics` usarem exatamente o mesmo recorte de `DB_EXISTING_ACCOUNT_ID` do scope
+`da_conta_gade` — antes o dashboard filtrava conta e Operações não.
+
+**`NOT IN` com `NULL` devolve zero linhas.** Cada `SELECT` da união em `NotasForaOperacao` filtra
+`nota_fiscal IS NOT NULL`; sem isso um único NULL numa planilha deixaria o painel vazio para sempre.
+Tem spec para isso.
+
+### 📂 Arquivos
+```
+app/models/entrega.rb (contas_gade + condicao_conta_sql)
+app/services/analytics/operacao_metricas.rb (visitas x linhas; dedup pós-filtro; conta)
+app/services/analytics/notas_fora_operacao.rb (NOVO — avulsas/inclusão)
+app/controllers/operacoes_dashboard_controller.rb (período no modo Operação; @fora_operacao)
+app/controllers/dashboard_controller.rb (@notas_atendidas)
+app/views/operacoes_dashboard/_painel.html.erb (faixa "Visitas ao local")
+app/views/operacoes_dashboard/_fora_operacao.html.erb (NOVO — painel de avulsas)
+app/views/operacoes_dashboard/index.html.erb (seletor de período + render do painel)
+app/views/dashboard/index.html.erb (linha "N notas fiscais")
+spec/services/analytics/operacao_metricas_spec.rb (+ NF com retentativa)
+spec/services/analytics/notas_fora_operacao_spec.rb (NOVO)
+```
+
+> **Sem migration e sem gem nova** — model, services, controllers, views e specs.
+
+### ⏳ Pendente — roteiro
+```bash
+# 1. Suíte (não pôde ser executada aqui — sem Ruby/Bundler local)
+docker compose exec app bundle exec rspec \
+ spec/services/analytics/operacao_metricas_spec.rb \
+ spec/services/analytics/notas_fora_operacao_spec.rb \
+ spec/models/entrega_spec.rb
+
+# 2. Conferir a coluna que carrega o nome do PLANO ("(Avulsa)", "INCLUSÃO").
+# NotasForaOperacao::COLUNAS_PLANO tenta title, notes, comments, route_id nessa ordem.
+# Se o painel mostrar "SEM PLANO IDENTIFICADO", a coluna certa é outra:
+docker compose exec app bin/rails runner 'puts Entrega.column_names.sort'
+
+# 3. Com dado real, no mesmo período nas duas telas:
+# financeiro "N notas fiscais" == Operações Global "Total de Entregas"
+# financeiro "Total Entregas" == Operações "Visitas ao local" + notas fora da operação
+```
+
+
+ visitas atendidas · <%= @notas_atendidas %> notas fiscais +
<%# Card 3: Consolidações %> diff --git a/app/views/operacoes_dashboard/_fora_operacao.html.erb b/app/views/operacoes_dashboard/_fora_operacao.html.erb new file mode 100644 index 0000000..b9ca8c5 --- /dev/null +++ b/app/views/operacoes_dashboard/_fora_operacao.html.erb @@ -0,0 +1,81 @@ +<%# locals: fora (Analytics::NotasForaOperacao), inicio, fim (Date) + NFs entregues no período que não constam em nenhuma planilha de operação — + os planos avulsos / de inclusão. Contam no dashboard financeiro e antes + sumiam daqui (o INNER JOIN com a tabela da operação as descartava). %> ++ <%= icone :parcial, cor: 'text-amber-400', espaco: false %> + Notas fora da operação +
+
+ Entregues em <%= inicio.strftime('%d/%m/%Y') %> – <%= fim.strftime('%d/%m/%Y') %>
+ sem constar em nenhuma planilha gade_entregas_* — planos avulsos e de inclusão.
+
+ Nenhuma nota fora das planilhas de operação neste período. +
+ <% else %> + <%# Quebra por plano de origem — é aqui que "(Avulsa)" e "INCLUSÃO" se separam. %> +| NF | +Plano / título | +Motorista | +Unidade | +Data | +Resultado | +Motivo | +
|---|---|---|---|---|---|---|
| <%= linha['reference_id'] %> | +<%= fora.plano(linha) %> | +<%= linha['driver'].presence || '—' %> | +<%= linha['contact_name'].presence || '—' %> | ++ <%= (linha['checkout'].presence || linha['planned_date'].presence)&.to_date&.strftime('%d/%m/%Y') || '—' %> + | ++ <%= sucesso ? 'Entregue' : (linha['status'].presence || '—') %> + | +<%= linha['observation'].presence || '—' %> | +
+ Mostrando as <%= Analytics::NotasForaOperacao::LIMITE %> mais recentes de <%= fora.total %>. +
+ <% end %> + <% end %> +Total de Entregas
<%= metricas.total %>
+notas fiscais da operação
<%= icone :sucesso, cor: 'text-green-500', tamanho: 'w-4 h-4' %> <%= metricas.sucesso %> · <%= icone :ponto, cor: 'text-blue-500', tamanho: 'w-4 h-4' %> <%= metricas.recusas %> · <%= icone :parcial, cor: 'text-amber-400', tamanho: 'w-4 h-4' %> <%= metricas.pendentes %>
@@ -49,6 +50,37 @@ <%= pct_total.(metricas.pendentes) %>% do totalVisitas ao local
++ O dashboard financeiro conta VISITAS (o motorista recebe por ida); esta tela conta NOTAS. + A diferença são as <%= metricas.retentativas %> retentativas acima. +
+