, Correçoes na busca da API para ver os ultimos planos
This commit is contained in:
49
README.md
49
README.md
@@ -3329,17 +3329,46 @@ silenciosa que acerta é invisível, mas a que erra é indefensável.
|
|||||||
|
|
||||||
## Sobre buscar o plano por nome na API
|
## Sobre buscar o plano por nome na API
|
||||||
|
|
||||||
**Não é possível.** A documentação
|
> ### ✅ CORRIGIDO EM 28/08/2026 — o "não é possível" abaixo estava ERRADO
|
||||||
(https://documentation.simpliroute.com) não expõe endpoint que **liste** planos —
|
>
|
||||||
só `POST /v1/plans/create-plan/` (onde o plano tem `name`),
|
> **`GET /v1/routes/plans/` existe** e devolve os planos com o `name` que a
|
||||||
`GET /v1/plans/{planned_date}/vehicles/` e `GET /v1/plans/routes/{PLAN_ID}/visits/`.
|
> operação usa. Verificado contra a API real (`bin/sondar_planos`): **138 planos,
|
||||||
A lista de planos que aparece no site deles é da interface web. Por isso o nome é
|
> 185 KB, 0,86 s**, nomes como `EMAD SETEMBRO 2026` e `UBS OESTE AGOSTO 2026`.
|
||||||
**colado pelo operador** e casado localmente.
|
> Não é documentado. O endpoint até estava na lista de candidatos do
|
||||||
|
> `bin/sondar_plano_do_dia` — **o que faltou foi rodar o script**; a conclusão
|
||||||
|
> "não é possível" foi tirada só da documentação.
|
||||||
|
>
|
||||||
|
> A cadeia inteira fecha, sem adivinhar data nenhuma:
|
||||||
|
>
|
||||||
|
> | chamada | devolve |
|
||||||
|
> |---|---|
|
||||||
|
> | `GET /v1/routes/plans/` | `name`, `start_date`, `end_date`, `created`, `routes[]` |
|
||||||
|
> | `GET /v1/routes/routes/{uuid}/` | `vehicle` (id), `planned_date`, `total_visits`, `plan` |
|
||||||
|
> | `GET /v1/plans/routes/{uuid}/visits/` | `order`, `vehicle_id`, `reference` (NF), `title` |
|
||||||
|
> | `GET /v1/routes/vehicles/` | traduz `634185` → `GADE_057` |
|
||||||
|
>
|
||||||
|
> **Nenhum filtro funciona** nessa rota: `ordering`, `limit`, `page`, `page_size`,
|
||||||
|
> `search`, `name`, `planned_date`, `planned_date__gte`, `created_at__gte` e
|
||||||
|
> `status` devolveram os mesmos 138 itens do controle sem parâmetro. Então "os 5
|
||||||
|
> mais recentes" é corte em Ruby depois de baixar tudo — `SimpliRoute::Client#planos`.
|
||||||
|
> Ordena por `created`, e não por `start_date`, porque a janela do plano é um
|
||||||
|
> INTERVALO (o EMAD SETEMBRO 2026 vai de 31/08 a 08/09) — era exatamente isso que
|
||||||
|
> tornava a data impossível de adivinhar.
|
||||||
|
>
|
||||||
|
> Pela NF também fecha: a visita traz `route` (uuid) e a rota traz `plan` (uuid).
|
||||||
|
>
|
||||||
|
> **Lição:** conclusão tirada de documentação não é conclusão. A própria memória
|
||||||
|
> desta integração já dizia que a API ignora parâmetro desconhecido em silêncio —
|
||||||
|
> ela também não anuncia as rotas que tem.
|
||||||
|
|
||||||
> `GET /v1/plans/{planned_date}/vehicles/` devolve veículos + UUIDs de rota por
|
O texto original, mantido para contexto de como se chegou à conclusão errada:
|
||||||
> data — é o degrau (a) que o `Romaneios::PlanoDoDia` hoje tenta adivinhar. Vale
|
|
||||||
> checar com `bin/sondar_plano_do_dia`, que ganhou uma seção `[4]` procurando
|
> **Não é possível.** A documentação
|
||||||
> endpoint de planos com nome.
|
> (https://documentation.simpliroute.com) não expõe endpoint que **liste** planos —
|
||||||
|
> só `POST /v1/plans/create-plan/` (onde o plano tem `name`),
|
||||||
|
> `GET /v1/plans/{planned_date}/vehicles/` e `GET /v1/plans/routes/{PLAN_ID}/visits/`.
|
||||||
|
> A lista de planos que aparece no site deles é da interface web. Por isso o nome é
|
||||||
|
> **colado pelo operador** e casado localmente.
|
||||||
|
|
||||||
## 📂 Arquivos
|
## 📂 Arquivos
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,11 @@ class Admin::RomaneiosController < ApplicationController
|
|||||||
@romaneios = Romaneio.recentes.includes(:romaneio_veiculos).limit(50)
|
@romaneios = Romaneio.recentes.includes(:romaneio_veiculos).limit(50)
|
||||||
@operacoes_agrupadas = Operacao.agrupadas_por_mes
|
@operacoes_agrupadas = Operacao.agrupadas_por_mes
|
||||||
@data_padrao = Date.current
|
@data_padrao = Date.current
|
||||||
|
|
||||||
|
# Os 5 planos mais recentes, para o operador ESCOLHER em vez de adivinhar a
|
||||||
|
# data. Best-effort de propósito: API fora do ar devolve [] e a tela cai no
|
||||||
|
# campo de data de sempre — a importação nunca fica bloqueada por isso.
|
||||||
|
@planos = SimpliRoute::Client.new.planos(limite: 5)
|
||||||
# Nome ORIGINAL do logo escolhido (a descricao), para a tela mostrar o mesmo
|
# Nome ORIGINAL do logo escolhido (a descricao), para a tela mostrar o mesmo
|
||||||
# que o operador enviou; nil = ainda usando o logo da GADE do repositório.
|
# que o operador enviou; nil = ainda usando o logo da GADE do repositório.
|
||||||
@logo_atual = Configuracao.find_by(chave: 'romaneio_logo')&.descricao
|
@logo_atual = Configuracao.find_by(chave: 'romaneio_logo')&.descricao
|
||||||
@@ -44,7 +49,12 @@ class Admin::RomaneiosController < ApplicationController
|
|||||||
@romaneio = preparar_romaneio
|
@romaneio = preparar_romaneio
|
||||||
return if performed?
|
return if performed?
|
||||||
|
|
||||||
plano = Romaneios::PlanoDoDia.new(data: @romaneio.planned_date)
|
# `@plano_escolhido` vem de #preparar_romaneio quando o operador clicou num
|
||||||
|
# plano da lista; sem ele o caminho continua sendo por data.
|
||||||
|
# Os DOIS: a data já foi resolvida em #preparar_romaneio (não paga a chamada
|
||||||
|
# de novo) e ainda destrava a queda para o espelho local se a API cair no
|
||||||
|
# meio; o plano é quem delimita as rotas.
|
||||||
|
plano = Romaneios::PlanoDoDia.new(data: @romaneio.planned_date, plano: @plano_escolhido)
|
||||||
importar(plano.linhas, origem: plano.origem)
|
importar(plano.linhas, origem: plano.origem)
|
||||||
rescue SimpliRoute::Error => e
|
rescue SimpliRoute::Error => e
|
||||||
# NotFound herda de Error: a mensagem dele já diz para enviar o .xlsx.
|
# NotFound herda de Error: a mensagem dele já diz para enviar o .xlsx.
|
||||||
@@ -262,9 +272,39 @@ class Admin::RomaneiosController < ApplicationController
|
|||||||
# "importar de novo o mesmo dia" cair na reconciliação em vez de criar um segundo
|
# "importar de novo o mesmo dia" cair na reconciliação em vez de criar um segundo
|
||||||
# romaneio — a chave única (planned_date, operacao_tabela) é a garantia disso.
|
# romaneio — a chave única (planned_date, operacao_tabela) é a garantia disso.
|
||||||
def preparar_romaneio
|
def preparar_romaneio
|
||||||
|
# ── Plano escolhido na lista (caminho normal desde 28/08/2026) ──────────
|
||||||
|
# O plano resolve de uma vez os três campos que antes eram digitados: a
|
||||||
|
# DATA (perguntada à API, porque a janela do plano não a revela), o NOME do
|
||||||
|
# plano (que ia colado à mão e é o que descobre a operação do mês) e as
|
||||||
|
# rotas, que delimitam o romaneio quando dois planos caem no mesmo dia.
|
||||||
|
@plano_escolhido = nil
|
||||||
data = params[:planned_date].presence
|
data = params[:planned_date].presence
|
||||||
|
|
||||||
|
if params[:plano_id].present?
|
||||||
|
@plano_escolhido = SimpliRoute::Client.new.planos(limite: 20)
|
||||||
|
.find { |pl| pl['id'].to_s == params[:plano_id].to_s }
|
||||||
|
if @plano_escolhido.nil?
|
||||||
|
redirect_to admin_romaneios_path,
|
||||||
|
alert: 'Plano não encontrado no SimpliRoute — recarregue a página e escolha de novo.'
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
# A data REAL do plano, não a janela dele. Sem isso o romaneio nasceria
|
||||||
|
# com `planned_date` errado e o "Reimportar" (que é por data) buscaria o
|
||||||
|
# dia errado depois.
|
||||||
|
resolvida = Romaneios::PlanoDoDia.new(plano: @plano_escolhido).data_resolvida
|
||||||
|
if resolvida.nil?
|
||||||
|
redirect_to admin_romaneios_path,
|
||||||
|
alert: "Não consegui descobrir o dia do plano \"#{@plano_escolhido['name']}\". " \
|
||||||
|
'Informe a data ou envie a planilha do plano (.xlsx).'
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
data = resolvida.to_s
|
||||||
|
params[:rotulo_plano] = @plano_escolhido['name'] if params[:rotulo_plano].blank?
|
||||||
|
end
|
||||||
|
|
||||||
if data.blank?
|
if data.blank?
|
||||||
redirect_to admin_romaneios_path, alert: 'Informe a data do plano.'
|
redirect_to admin_romaneios_path, alert: 'Escolha um plano ou informe a data.'
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -36,8 +36,23 @@ module Romaneios
|
|||||||
|
|
||||||
attr_reader :origem
|
attr_reader :origem
|
||||||
|
|
||||||
def initialize(data:, client: nil)
|
# Duas formas de pedir o plano, e a primeira é a boa:
|
||||||
@data = data.to_date
|
#
|
||||||
|
# PlanoDoDia.new(plano: {...}) -> o PLANO escolhido na lista (o hash que
|
||||||
|
# SimpliRoute::Client#planos devolve)
|
||||||
|
# PlanoDoDia.new(data: ...) -> o jeito antigo, por data
|
||||||
|
#
|
||||||
|
# POR QUE O PLANO VENCE: o operador pensa por operação ("lançou a UBS
|
||||||
|
# Sudeste"), não por data — e a data era mesmo impossível de acertar, porque
|
||||||
|
# o plano tem uma JANELA e o dia das rotas ora é o começo dela, ora o fim
|
||||||
|
# (ver SimpliRoute::Client#rota). Com o plano em mãos a data deixa de ser
|
||||||
|
# chute: pergunta-se à API.
|
||||||
|
#
|
||||||
|
# A forma por data continua porque o "Reimportar" de um romaneio antigo só
|
||||||
|
# tem a data, e porque ela é o caminho para o espelho local quando a API cai.
|
||||||
|
def initialize(data: nil, plano: nil, client: nil)
|
||||||
|
@plano = plano
|
||||||
|
@data = data&.to_date
|
||||||
@client = client
|
@client = client
|
||||||
@origem = nil
|
@origem = nil
|
||||||
end
|
end
|
||||||
@@ -46,25 +61,90 @@ module Romaneios
|
|||||||
@linhas ||= carregar
|
@linhas ||= carregar
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# O dia REAL do plano — o que o Romaneio grava em `planned_date`. Uma
|
||||||
|
# chamada: todas as rotas de um plano dividem o mesmo dia.
|
||||||
|
def data_resolvida
|
||||||
|
@data ||= begin
|
||||||
|
rota = cliente.rota(Array(@plano && @plano['routes']).first)
|
||||||
|
rota && rota['planned_date'] ? Date.parse(rota['planned_date']) : nil
|
||||||
|
rescue Date::Error, SimpliRoute::Error
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def cliente
|
||||||
|
@cliente ||= @client || SimpliRoute::Client.new
|
||||||
|
end
|
||||||
|
|
||||||
def carregar
|
def carregar
|
||||||
linhas = da_api
|
linhas = @plano ? da_api_por_plano : da_api
|
||||||
return linhas if linhas.any?
|
return linhas if linhas.any?
|
||||||
|
|
||||||
linhas = do_espelho
|
# O espelho é por DATA. Vindo de um plano, ele só entra depois que a data
|
||||||
return linhas if linhas.any?
|
# foi resolvida — sem ela não há o que consultar, e chamar `do_espelho`
|
||||||
|
# com @data nil traria o período inteiro.
|
||||||
|
linhas = do_espelho if @data
|
||||||
|
return linhas if linhas&.any?
|
||||||
|
|
||||||
raise SimpliRoute::NotFound,
|
raise SimpliRoute::NotFound, mensagem_de_ausencia
|
||||||
"O plano de #{@data.strftime('%d/%m/%Y')} não está disponível no SimpliRoute " \
|
end
|
||||||
'nem no espelho de rastreio. Envie a planilha do plano (.xlsx).'
|
|
||||||
|
# A mensagem diz o que a pessoa escolheu, não um dado interno: quem clicou
|
||||||
|
# num plano na lista não reconhece a data resolvida, e quem digitou a data
|
||||||
|
# não sabe de plano nenhum.
|
||||||
|
def mensagem_de_ausencia
|
||||||
|
if @plano && @data.nil?
|
||||||
|
"Não consegui descobrir o dia do plano \"#{@plano['name']}\" no SimpliRoute. " \
|
||||||
|
'Escolha outro plano ou envie a planilha do plano (.xlsx).'
|
||||||
|
elsif @plano
|
||||||
|
"O plano \"#{@plano['name']}\" (#{@data.strftime('%d/%m/%Y')}) não tem paradas no " \
|
||||||
|
'SimpliRoute nem no espelho de rastreio. Envie a planilha do plano (.xlsx).'
|
||||||
|
else
|
||||||
|
"O plano de #{@data.strftime('%d/%m/%Y')} não está disponível no SimpliRoute " \
|
||||||
|
'nem no espelho de rastreio. Envie a planilha do plano (.xlsx).'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# ── Caminho do PLANO ESCOLHIDO (o normal desde 28/08/2026) ───────────────
|
||||||
|
# Três chamadas, todas verificadas contra a API real:
|
||||||
|
#
|
||||||
|
# 1. a 1ª rota do plano -> o DIA real (Client#rota)
|
||||||
|
# 2. visitas do dia -> 2047 visitas, 3 MB, ~9 s (Client#visitas_da_data)
|
||||||
|
# 3. veículos da conta -> traduz 634185 em GADE_057 (dentro de por_visitas)
|
||||||
|
#
|
||||||
|
# O FILTRO PELAS ROTAS DO PLANO É O PONTO DA COISA, não uma otimização: dois
|
||||||
|
# planos podem cair no MESMO dia (uma operação normal e uma de avulsas, por
|
||||||
|
# exemplo), e sem ele o romaneio de um sairia com as paradas do outro. A
|
||||||
|
# visita traz `route`, e o plano traz a lista de uuids de rota — o cruzamento
|
||||||
|
# é exato. Medido: 2047 visitas do dia, 73 rotas, todas com `route`,
|
||||||
|
# `vehicle` e `order` preenchidos (2047/2047).
|
||||||
|
#
|
||||||
|
# Depois do filtro é o mesmo `por_visitas` de sempre: ele já agrupa por
|
||||||
|
# veículo, resolve o nome (GADE_057) e ordena por `order`.
|
||||||
|
def da_api_por_plano
|
||||||
|
return [] unless SimpliRoute.configurado?
|
||||||
|
|
||||||
|
data = data_resolvida
|
||||||
|
return [] if data.nil?
|
||||||
|
|
||||||
|
do_plano = Array(@plano['routes']).map(&:to_s).to_set
|
||||||
|
visitas = cliente.visitas_da_data(data).select { |v| do_plano.include?(v['route'].to_s) }
|
||||||
|
return [] if visitas.empty?
|
||||||
|
|
||||||
|
linhas = por_visitas(visitas, cliente)
|
||||||
|
@origem = 'api' if linhas.any?
|
||||||
|
linhas
|
||||||
|
rescue SimpliRoute::Error
|
||||||
|
[]
|
||||||
end
|
end
|
||||||
|
|
||||||
# ── (a) e (b) ────────────────────────────────────────────────────────────
|
# ── (a) e (b) ────────────────────────────────────────────────────────────
|
||||||
def da_api
|
def da_api
|
||||||
return [] unless SimpliRoute.configurado?
|
return [] unless SimpliRoute.configurado?
|
||||||
|
|
||||||
cliente = @client || SimpliRoute::Client.new
|
cliente = self.cliente
|
||||||
visitas = cliente.visitas_da_data(@data)
|
visitas = cliente.visitas_da_data(@data)
|
||||||
return [] if visitas.empty?
|
return [] if visitas.empty?
|
||||||
|
|
||||||
|
|||||||
@@ -64,22 +64,64 @@ module SimpliRoute
|
|||||||
Array(get(caminho))
|
Array(get(caminho))
|
||||||
end
|
end
|
||||||
|
|
||||||
# Rotas PLANEJADAS da data (Array de hashes). É aqui que costuma morar o par
|
# UMA rota pelo uuid. Serve para descobrir o DIA REAL de um plano: o registro
|
||||||
# veículo/motorista, que a visita talvez não carregue — e o romaneio precisa do
|
# do plano só traz a JANELA (`start_date`/`end_date`), e o dia das rotas nem
|
||||||
# veículo para saber quantas folhas imprimir.
|
# sempre é o começo dela — medido em 28/08/2026: EMAD SETEMBRO 2026 (31/08 a
|
||||||
|
# 08/09) tem rotas em 31/08 (= início), mas UBS OESTE AGOSTO 2026 (18/08 a
|
||||||
|
# 20/08) e UBS NORTE AGOSTO 2026 (17/08 a 20/08) têm rotas em 20/08 (= FIM).
|
||||||
|
# Era isto que tornava a data impossível de adivinhar de fora; agora se
|
||||||
|
# pergunta à API. Todas as rotas de um plano dividem o mesmo dia (amostra de
|
||||||
|
# 8 em 3 planos), então basta a primeira.
|
||||||
|
def rota(id)
|
||||||
|
get("/v1/routes/routes/#{id}/")
|
||||||
|
rescue Error
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
# Rotas PLANEJADAS da data (Array de hashes). É aqui que mora o par
|
||||||
|
# veículo/motorista, e o romaneio precisa do veículo para saber quantas folhas
|
||||||
|
# imprimir.
|
||||||
#
|
#
|
||||||
# ⚠️ NÃO VERIFICADO contra a API real: não há token na máquina de
|
# ✔ VERIFICADO em 28/08/2026 contra a API real: `?planned_date=2026-08-31`
|
||||||
# desenvolvimento. Por isso é best-effort (endpoint inexistente devolve [], não
|
# devolve 73 rotas (83 KB, 0,59 s), cada uma com `vehicle` (id numérico),
|
||||||
# explode) e por isso Romaneios::PlanoDoDia tem uma escada de queda até o
|
# `plan` (uuid) e `total_visits`. ⚠️ O parâmetro é OBRIGATÓRIO: sem ele a rota
|
||||||
# espelho local. Rode `bin/sondar_plano_do_dia` no servidor antes de confiar
|
# responde 200 com **lista vazia**, não com tudo — então "veio vazio" aqui
|
||||||
# nesta chamada — e anote o resultado no cabeçalho daquele script, como foi
|
# pode ser data errada, nunca "a conta não tem rotas". A rota NÃO traz array
|
||||||
# feito em bin/sondar_busca_nf.
|
# de visitas (`visits` não existe); quem liga visita→rota é o campo `route`
|
||||||
|
# da própria visita.
|
||||||
def rotas_da_data(data)
|
def rotas_da_data(data)
|
||||||
Array(get("/v1/routes/routes/?planned_date=#{data.to_date.iso8601}"))
|
Array(get("/v1/routes/routes/?planned_date=#{data.to_date.iso8601}"))
|
||||||
rescue Error
|
rescue Error
|
||||||
[]
|
[]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Os N planos mais recentes da conta — nome, período e as rotas de cada um.
|
||||||
|
#
|
||||||
|
# ⚠️ A CONCLUSÃO ANTIGA ("a API não expõe listagem de planos, só dá para
|
||||||
|
# buscar por data") ESTAVA ERRADA. `/v1/routes/plans/` existe, não é
|
||||||
|
# documentado, e devolve TUDO com o nome que a operação usa — "EMAD SETEMBRO
|
||||||
|
# 2026", "UBS OESTE AGOSTO 2026". Verificado em 28/08/2026 contra a API real:
|
||||||
|
# 138 planos, 185 KB, 0,86 s. Ver bin/sondar_planos.
|
||||||
|
#
|
||||||
|
# POR QUE ORDENAR E CORTAR AQUI, e não na URL: a API IGNORA todo filtro nesta
|
||||||
|
# rota. `ordering`, `limit`, `page`, `page_size`, `search`, `name`,
|
||||||
|
# `start_date__gte` — os dez candidatos testados devolveram os mesmos 138
|
||||||
|
# itens do controle sem parâmetro. Então não existe "pedir 5": pede-se tudo
|
||||||
|
# (uma chamada, menos de 1 s) e corta-se em Ruby.
|
||||||
|
#
|
||||||
|
# `created` é o critério: `start_date` de um plano é o PRIMEIRO dia da janela
|
||||||
|
# (o EMAD SETEMBRO 2026 vai de 31/08 a 08/09), e é justamente essa janela que
|
||||||
|
# tornava impossível adivinhar "a data do plano". Quem publicou por último é
|
||||||
|
# quem o operador quer — ele abre a ferramenta logo depois de lançar.
|
||||||
|
def planos(limite: 5)
|
||||||
|
Array(get('/v1/routes/plans/'))
|
||||||
|
.sort_by { |p| p['created'].to_s }
|
||||||
|
.reverse
|
||||||
|
.first(limite)
|
||||||
|
rescue Error
|
||||||
|
[]
|
||||||
|
end
|
||||||
|
|
||||||
# Veículos da conta, para traduzir o id numérico que vem nas rotas
|
# Veículos da conta, para traduzir o id numérico que vem nas rotas
|
||||||
# (`vehicle: 630011`) no nome que a operação usa (`GADE_038`).
|
# (`vehicle: 630011`) no nome que a operação usa (`GADE_038`).
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -70,10 +70,51 @@
|
|||||||
authenticity_token: form_authenticity_token,
|
authenticity_token: form_authenticity_token,
|
||||||
data: { turbo: false }, class: 'space-y-4' do %>
|
data: { turbo: false }, class: 'space-y-4' do %>
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
<div class="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
||||||
|
<%# ── Plano ────────────────────────────────────────────────────────
|
||||||
|
Antes aqui se pedia a DATA do plano, e era o passo que mais errava:
|
||||||
|
o operador pensa por operação ("lançou a UBS Sudeste"), e a data
|
||||||
|
nem estava ao alcance dele — o plano tem uma JANELA e o dia das
|
||||||
|
rotas ora é o começo dela, ora o fim (EMAD SETEMBRO 2026 vai de
|
||||||
|
31/08 a 08/09 e roda em 31/08; UBS OESTE AGOSTO vai de 18 a 20/08 e
|
||||||
|
roda em 20/08). Agora ele escolhe pelo NOME e quem descobre o dia é
|
||||||
|
a API.
|
||||||
|
|
||||||
|
Cinco planos porque a ferramenta é usada logo depois de lançar uma
|
||||||
|
operação — o que ele quer está sempre entre os últimos.
|
||||||
|
|
||||||
|
A lista vazia (API fora, token vencido) NÃO tranca a tela: cai no
|
||||||
|
campo de data, que é o comportamento antigo inteiro. %>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-300 mb-1.5">Data do plano</label>
|
<label class="block text-sm font-medium text-gray-300 mb-1.5">Plano</label>
|
||||||
<input type="date" name="planned_date" required value="<%= @data_padrao %>"
|
|
||||||
class="w-full bg-[#1a1a1a] border border-white/10 rounded-xl px-3 py-3 text-white
|
<% if @planos.present? %>
|
||||||
|
<select name="plano_id"
|
||||||
|
class="w-full bg-[#1a1a1a] border border-white/10 rounded-xl px-3 py-3 text-white
|
||||||
|
focus:outline-none focus:border-orange-500 min-h-[48px]">
|
||||||
|
<% @planos.each_with_index do |pl, i| %>
|
||||||
|
<option value="<%= pl['id'] %>" <%= 'selected' if i.zero? %>>
|
||||||
|
<%= pl['name'] %> · <%= Array(pl['routes']).size %> veículos
|
||||||
|
</option>
|
||||||
|
<% end %>
|
||||||
|
<%# Escape para plano mais antigo que os 5, ou para refazer um
|
||||||
|
romaneio de um dia específico: valor vazio faz o controller
|
||||||
|
usar o campo de data abaixo, como sempre fez. %>
|
||||||
|
<option value="">— usar uma data específica —</option>
|
||||||
|
</select>
|
||||||
|
<p class="text-xs text-gray-500 mt-1.5">
|
||||||
|
Os 5 planos mais recentes do SimpliRoute. O dia é descoberto pelo plano.
|
||||||
|
</p>
|
||||||
|
<% else %>
|
||||||
|
<p class="text-xs text-orange-400/80 mb-1.5">
|
||||||
|
Não consegui listar os planos do SimpliRoute agora — informe a data.
|
||||||
|
</p>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<%# Continua existindo sempre: é o caminho do "— usar uma data
|
||||||
|
específica —" e a rede quando a API não responde. %>
|
||||||
|
<input type="date" name="planned_date" value="<%= @data_padrao %>"
|
||||||
|
<%= 'required' unless @planos.present? %>
|
||||||
|
class="w-full mt-2 bg-[#1a1a1a] border border-white/10 rounded-xl px-3 py-3 text-white
|
||||||
focus:outline-none focus:border-orange-500 min-h-[48px]">
|
focus:outline-none focus:border-orange-500 min-h-[48px]">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -103,12 +144,18 @@
|
|||||||
<label class="block text-sm font-medium text-gray-300 mb-1.5">
|
<label class="block text-sm font-medium text-gray-300 mb-1.5">
|
||||||
PLANO <span class="text-gray-500 font-normal">(canto direito do PDF)</span>
|
PLANO <span class="text-gray-500 font-normal">(canto direito do PDF)</span>
|
||||||
</label>
|
</label>
|
||||||
<input type="text" name="rotulo_plano" placeholder="Ex.: ENTREGAS EMAD 09.2026"
|
<input type="text" name="rotulo_plano" placeholder="<%= @planos.present? ? 'Deixe em branco — vem do plano' : 'Ex.: ENTREGAS EMAD 09.2026' %>"
|
||||||
class="w-full bg-[#1a1a1a] border border-white/10 rounded-xl px-3 py-3 text-white
|
class="w-full bg-[#1a1a1a] border border-white/10 rounded-xl px-3 py-3 text-white
|
||||||
placeholder-gray-600 focus:outline-none focus:border-orange-500 min-h-[48px]">
|
placeholder-gray-600 focus:outline-none focus:border-orange-500 min-h-[48px]">
|
||||||
<p class="text-xs text-gray-500 mt-1.5">
|
<p class="text-xs text-gray-500 mt-1.5">
|
||||||
Cole o nome do plano como está no SimpliRoute — é por ele que a
|
<% if @planos.present? %>
|
||||||
operação do mês (quem é <strong>NOVO</strong>) é encontrada.
|
Em branco, usa o nome do plano escolhido — é por ele que a operação
|
||||||
|
do mês (quem é <strong>NOVO</strong>) é encontrada. Preencha só para
|
||||||
|
imprimir um texto diferente no canto do PDF.
|
||||||
|
<% else %>
|
||||||
|
Cole o nome do plano como está no SimpliRoute — é por ele que a
|
||||||
|
operação do mês (quem é <strong>NOVO</strong>) é encontrada.
|
||||||
|
<% end %>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
329
bin/sondar_planos
Executable file
329
bin/sondar_planos
Executable file
@@ -0,0 +1,329 @@
|
|||||||
|
#!/usr/bin/env ruby
|
||||||
|
# frozen_string_literal: true
|
||||||
|
#
|
||||||
|
# Procura um jeito de ACHAR O PLANO sem adivinhar a data.
|
||||||
|
#
|
||||||
|
# ── O PROBLEMA ──────────────────────────────────────────────────────────────
|
||||||
|
# Hoje Romaneios::PlanoDoDia pergunta "qual plano existe NESTA data?", então
|
||||||
|
# alguém tem que acertar o dia em que o plano foi publicado. Quem usa a
|
||||||
|
# ferramenta pensa por OPERAÇÃO ("lançou a UBS Sudeste"), não por data — e a
|
||||||
|
# data do lançamento raramente é a que a pessoa lembra.
|
||||||
|
#
|
||||||
|
# O que queremos, na ordem de preferência:
|
||||||
|
# A) listar os ÚLTIMOS N PLANOS (nome + data + id) e deixar o operador
|
||||||
|
# escolher numa lista — é o que resolve de verdade;
|
||||||
|
# B) na falta disso, ir da NF ao plano: o operador já tem uma nota do
|
||||||
|
# romaneio, e `?search=<NF>` em /v1/routes/visits/ já funciona. Se o objeto
|
||||||
|
# da visita carregar uma FK de rota/plano, a cadeia
|
||||||
|
# NF -> visita -> rota -> plano resolve o mesmo problema sem endpoint novo.
|
||||||
|
#
|
||||||
|
# ── RESULTADO DA RODADA DE 28/08/2026 (contra a API real) ───────────────────
|
||||||
|
# ✔ `GET /v1/routes/plans/` EXISTE e resolve o problema: 138 planos, 185 KB,
|
||||||
|
# 0,86 s, cada um com `name` — "EMAD SETEMBRO 2026", "UBS OESTE AGOSTO
|
||||||
|
# 2026" — que é exatamente como o operador pensa. NÃO é documentado.
|
||||||
|
# ⇒ A conclusão anterior, registrada no README ("não é possível buscar o
|
||||||
|
# plano por nome"), ESTAVA ERRADA. O endpoint estava na lista de
|
||||||
|
# candidatos do bin/sondar_plano_do_dia; o que faltou foi RODAR.
|
||||||
|
# ✔ Campos do plano: id (uuid), name, start_date, end_date, created, modified,
|
||||||
|
# status, is_cluster, fleet, routes[] (uuids), start_time, end_time.
|
||||||
|
# ✔ CADEIA COMPLETA, verificada ponta a ponta:
|
||||||
|
# /v1/routes/plans/ -> plano: name + routes[]
|
||||||
|
# /v1/routes/routes/{uuid}/ -> vehicle (id), planned_date, total_visits
|
||||||
|
# /v1/plans/routes/{uuid}/visits/ -> order, vehicle_id, reference (NF), title
|
||||||
|
# /v1/routes/vehicles/ -> 634185 = GADE_057 (199 na frota)
|
||||||
|
# É tudo o que o romaneio precisa, sem adivinhar data nenhuma.
|
||||||
|
# ✔ Caminho pela NF também funciona: a visita traz `route` (uuid), e a rota
|
||||||
|
# traz `plan` (uuid). NF -> visita -> rota -> plano fecha.
|
||||||
|
# ✘ NENHUM filtro funciona em /v1/routes/plans/: `ordering`, `limit`, `page`,
|
||||||
|
# `page_size`, `search`, `name`, `planned_date`, `planned_date__gte`,
|
||||||
|
# `created_at__gte`, `status` — os dez devolveram os MESMOS 138 itens do
|
||||||
|
# controle. ⇒ "os 5 mais recentes" se faz em Ruby, depois de baixar tudo
|
||||||
|
# (SimpliRoute::Client#planos). Uma chamada de menos de 1 s, é barato.
|
||||||
|
# ✘ `/v1/plans/`, `/v1/plans/list/`, `/v1/plans/plans/`, `/v1/plans/routes/`
|
||||||
|
# -> 404. O prefixo é `/v1/routes/`, não `/v1/plans/` (que só serve
|
||||||
|
# `/v1/plans/routes/{uuid}/visits/`).
|
||||||
|
# ⚠️ `OPTIONS /v1/routes/visits/` devolve 500 — não insista, não é seu token.
|
||||||
|
#
|
||||||
|
# Rode de novo se desconfiar que a API mudou.
|
||||||
|
#
|
||||||
|
# ── MÉTODO (as três regras que este script segue) ───────────────────────────
|
||||||
|
# 1. PARÂMETRO QUE NÃO EXISTE É IGNORADO EM SILÊNCIO. A API é Django REST:
|
||||||
|
# filtro desconhecido devolve 200 + a lista inteira. "Veio 200 com
|
||||||
|
# resultados" NÃO prova nada — já custou caro aqui (ver bin/sondar_busca_nf).
|
||||||
|
# Por isso todo filtro é comparado com um CONTROLE sem o parâmetro.
|
||||||
|
# 2. VALOR INVÁLIDO COMO ORÁCULO. É mais rápido e mais conclusivo que a
|
||||||
|
# contagem: filtro que EXISTE reclama do valor (400 nomeando o campo);
|
||||||
|
# filtro que não existe ignora e devolve 200. Uma chamada por candidato.
|
||||||
|
# 3. OPTIONS ANTES DE CHUTAR. O DRF responde OPTIONS com os métodos permitidos
|
||||||
|
# e, quando a rota é navegável, com o schema dos campos — é a forma mais
|
||||||
|
# barata de saber o formato de um "plano" sem inventar nome de campo.
|
||||||
|
#
|
||||||
|
# ⚠️ SÓ LEITURA. Este script faz apenas GET e OPTIONS — não existe POST/PATCH/
|
||||||
|
# PUT/DELETE aqui, de propósito. A API é a de PRODUÇÃO do cliente.
|
||||||
|
#
|
||||||
|
# Uso (o token vive no ambiente, NUNCA no git):
|
||||||
|
#
|
||||||
|
# SIMPLIROUTE_TOKEN=xxx bin/sondar_planos --data 2026-08-31
|
||||||
|
# SIMPLIROUTE_TOKEN=xxx bin/sondar_planos --data 2026-08-31 --nf 90158
|
||||||
|
# SIMPLIROUTE_TOKEN=xxx bin/sondar_planos --data 2026-08-31 --limite 5
|
||||||
|
#
|
||||||
|
# --data um dia em que EXISTE plano publicado (é a âncora de tudo).
|
||||||
|
# --nf opcional: testa o caminho (B) partindo de uma nota conhecida.
|
||||||
|
# --limite quantos planos queremos na listagem final (padrão 5).
|
||||||
|
|
||||||
|
require 'net/http'
|
||||||
|
require 'json'
|
||||||
|
require 'uri'
|
||||||
|
require 'date'
|
||||||
|
|
||||||
|
BASE = ENV.fetch('SIMPLIROUTE_BASE_URL', 'https://api.simpliroute.com')
|
||||||
|
TOKEN = ENV['SIMPLIROUTE_TOKEN'].to_s
|
||||||
|
|
||||||
|
def sair(msg)
|
||||||
|
warn msg
|
||||||
|
exit 1
|
||||||
|
end
|
||||||
|
|
||||||
|
sair('Defina SIMPLIROUTE_TOKEN no ambiente.') if TOKEN.empty?
|
||||||
|
|
||||||
|
data = nil
|
||||||
|
nf = nil
|
||||||
|
limite = 5
|
||||||
|
ARGV.each_with_index do |a, i|
|
||||||
|
data = ARGV[i + 1] if a == '--data'
|
||||||
|
nf = ARGV[i + 1] if a == '--nf'
|
||||||
|
limite = ARGV[i + 1].to_i if a == '--limite'
|
||||||
|
end
|
||||||
|
sair('Uso: bin/sondar_planos --data <YYYY-MM-DD> [--nf <numero>] [--limite 5]') if data.to_s.empty?
|
||||||
|
limite = 5 if limite <= 0
|
||||||
|
|
||||||
|
# ── HTTP (GET e OPTIONS, só) ────────────────────────────────────────────────
|
||||||
|
def requisicao(caminho, metodo: :get, aceita: 'application/json')
|
||||||
|
uri = URI.join(BASE, caminho)
|
||||||
|
req = metodo == :options ? Net::HTTP::Options.new(uri) : Net::HTTP::Get.new(uri)
|
||||||
|
req['Authorization'] = "Token #{TOKEN}"
|
||||||
|
req['Accept'] = aceita
|
||||||
|
|
||||||
|
t0 = Time.now
|
||||||
|
res = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https',
|
||||||
|
open_timeout: 10, read_timeout: 120) { |h| h.request(req) }
|
||||||
|
corpo = begin
|
||||||
|
res.body.to_s.empty? ? nil : JSON.parse(res.body)
|
||||||
|
rescue JSON::ParserError
|
||||||
|
res.body.to_s[0, 300]
|
||||||
|
end
|
||||||
|
[res.code.to_i, corpo, res.body.to_s.bytesize, Time.now - t0, res]
|
||||||
|
rescue StandardError => e
|
||||||
|
[0, "ERRO: #{e.class}: #{e.message}", 0, Time.now - t0, nil]
|
||||||
|
end
|
||||||
|
|
||||||
|
# A resposta pode ser lista crua OU paginada ({count, next, results}).
|
||||||
|
def itens(corpo)
|
||||||
|
return corpo if corpo.is_a?(Array)
|
||||||
|
return Array(corpo['results']) if corpo.is_a?(Hash) && corpo.key?('results')
|
||||||
|
|
||||||
|
[]
|
||||||
|
end
|
||||||
|
|
||||||
|
def total(corpo, lista)
|
||||||
|
corpo.is_a?(Hash) && corpo['count'] ? corpo['count'] : lista.size
|
||||||
|
end
|
||||||
|
|
||||||
|
def titulo(t)
|
||||||
|
puts "\n#{'─' * 74}\n#{t}\n#{'─' * 74}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def linha(caminho, codigo, bytes, seg, extra = nil)
|
||||||
|
puts format(' %-58s HTTP %-3s %7s %5.2fs%s',
|
||||||
|
caminho[0, 58], codigo, "#{bytes}B", seg, extra ? " #{extra}" : '')
|
||||||
|
end
|
||||||
|
|
||||||
|
CHAVES_NOME = /name|title|label|descri/i.freeze
|
||||||
|
CHAVES_ELO = /^(route|plan|plan_id|route_id|plan_uuid)$/i.freeze
|
||||||
|
|
||||||
|
puts "SimpliRoute · sondagem de PLANOS base=#{BASE} data-âncora=#{data}"
|
||||||
|
puts 'Somente GET/OPTIONS. Nada é alterado.'
|
||||||
|
|
||||||
|
# ════════════════════════════════════════════════════════════════════════════
|
||||||
|
# [1] A VISITA APONTA PARA O PLANO? (caminho B — o mais promissor)
|
||||||
|
# ════════════════════════════════════════════════════════════════════════════
|
||||||
|
# Se a visita carregar uma FK de rota/plano, o operador nunca mais precisa saber
|
||||||
|
# a data: ele cola uma NF do romaneio e o sistema descobre o plano.
|
||||||
|
titulo('[1] a VISITA carrega elo para rota/plano?')
|
||||||
|
|
||||||
|
caminho_visitas = "/v1/routes/visits/?planned_date=#{data}"
|
||||||
|
caminho_visitas += "&search=#{nf}" if nf
|
||||||
|
cod, corpo, bytes, seg = requisicao(caminho_visitas)
|
||||||
|
linha(caminho_visitas, cod, bytes, seg)
|
||||||
|
|
||||||
|
visita = itens(corpo).first
|
||||||
|
elos = {}
|
||||||
|
if visita.is_a?(Hash)
|
||||||
|
puts " chaves da visita: #{visita.keys.sort.join(', ')}"
|
||||||
|
elos = visita.select { |k, v| k.to_s.match?(CHAVES_ELO) && !v.nil? && v.to_s.strip != '' }
|
||||||
|
if elos.empty?
|
||||||
|
puts ' => NENHUM campo route/plan preenchido. O caminho (B) morre aqui;'
|
||||||
|
puts ' o que vale é a listagem de planos do bloco [3].'
|
||||||
|
else
|
||||||
|
puts ' => ELOS ENCONTRADOS (é por aqui que se chega ao plano):'
|
||||||
|
elos.each { |k, v| puts " #{k} = #{v.inspect[0, 90]}" }
|
||||||
|
end
|
||||||
|
else
|
||||||
|
puts ' => nenhuma visita nessa data/NF — escolha uma --data com plano publicado.'
|
||||||
|
end
|
||||||
|
|
||||||
|
# ── [1b] segue cada elo ─────────────────────────────────────────────────────
|
||||||
|
# Não sabemos o prefixo da rota do recurso, então testamos os candidatos e
|
||||||
|
# deixamos o HTTP responder. Quem devolver 200 com campo de nome resolve tudo.
|
||||||
|
unless elos.empty?
|
||||||
|
titulo('[1b] seguindo os elos — quem devolve nome/data do plano?')
|
||||||
|
elos.each_value do |id|
|
||||||
|
["/v1/routes/routes/#{id}/", "/v1/plans/#{id}/", "/v1/plans/routes/#{id}/",
|
||||||
|
"/v1/routes/#{id}/", "/v1/plans/routes/#{id}/visits/"].each do |c|
|
||||||
|
cod, corpo, bytes, seg = requisicao(c)
|
||||||
|
obj = corpo.is_a?(Hash) ? corpo : itens(corpo).first
|
||||||
|
nomes = obj.is_a?(Hash) ? obj.select { |k, v| k.to_s.match?(CHAVES_NOME) && v.to_s.strip != '' } : {}
|
||||||
|
linha(c, cod, bytes, seg, nomes.empty? ? nil : "nome: #{nomes.first.inspect[0, 60]}")
|
||||||
|
next unless cod == 200 && obj.is_a?(Hash)
|
||||||
|
|
||||||
|
puts " chaves: #{obj.keys.sort.join(', ')}" if nomes.any?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# ════════════════════════════════════════════════════════════════════════════
|
||||||
|
# [2] OPTIONS — o que cada rota admite, sem chutar campo
|
||||||
|
# ════════════════════════════════════════════════════════════════════════════
|
||||||
|
titulo('[2] OPTIONS nos candidatos (métodos + schema, quando o DRF entrega)')
|
||||||
|
|
||||||
|
%w[
|
||||||
|
/v1/plans/
|
||||||
|
/v1/plans/routes/
|
||||||
|
/v1/routes/plans/
|
||||||
|
/v1/routes/routes/
|
||||||
|
/v1/routes/visits/
|
||||||
|
].each do |c|
|
||||||
|
cod, corpo, bytes, seg, res = requisicao(c, metodo: :options)
|
||||||
|
linha(c, cod, bytes, seg, res && res['Allow'] ? "Allow: #{res['Allow']}" : nil)
|
||||||
|
next unless corpo.is_a?(Hash)
|
||||||
|
|
||||||
|
campos = corpo.dig('actions', 'GET') || corpo.dig('actions', 'POST')
|
||||||
|
puts " campos: #{campos.keys.sort.join(', ')}" if campos.is_a?(Hash)
|
||||||
|
puts " filtros: #{corpo['filters'].inspect[0, 200]}" if corpo['filters']
|
||||||
|
end
|
||||||
|
|
||||||
|
# ════════════════════════════════════════════════════════════════════════════
|
||||||
|
# [3] EXISTE LISTAGEM DE PLANOS? (caminho A — o que o operador quer)
|
||||||
|
# ════════════════════════════════════════════════════════════════════════════
|
||||||
|
titulo('[3] procurando uma LISTAGEM de planos')
|
||||||
|
|
||||||
|
candidatas = [
|
||||||
|
'/v1/plans/',
|
||||||
|
'/v1/plans/list/',
|
||||||
|
'/v1/plans/plans/',
|
||||||
|
'/v1/routes/plans/',
|
||||||
|
'/v1/plans/routes/',
|
||||||
|
"/v1/plans/?planned_date=#{data}",
|
||||||
|
"/v1/routes/plans/?planned_date=#{data}"
|
||||||
|
]
|
||||||
|
|
||||||
|
listagens = []
|
||||||
|
candidatas.each do |c|
|
||||||
|
cod, corpo, bytes, seg = requisicao(c)
|
||||||
|
lista = itens(corpo)
|
||||||
|
linha(c, cod, bytes, seg, lista.empty? ? nil : "#{total(corpo, lista)} itens")
|
||||||
|
next if lista.empty?
|
||||||
|
|
||||||
|
item = lista.first
|
||||||
|
next unless item.is_a?(Hash)
|
||||||
|
|
||||||
|
listagens << [c, corpo, lista]
|
||||||
|
puts " chaves: #{item.keys.sort.join(', ')}"
|
||||||
|
nomes = item.select { |k, v| k.to_s.match?(CHAVES_NOME) && v.to_s.strip != '' }
|
||||||
|
if nomes.empty?
|
||||||
|
puts ' => nenhum campo de nome PREENCHIDO (chave existir não basta)'
|
||||||
|
else
|
||||||
|
puts " => nomes: #{lista.first(5).filter_map { |i| i.is_a?(Hash) ? i[nomes.keys.first] : nil }.inspect}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# ════════════════════════════════════════════════════════════════════════════
|
||||||
|
# [4] ORÁCULO DO VALOR INVÁLIDO — quais filtros são REAIS
|
||||||
|
# ════════════════════════════════════════════════════════════════════════════
|
||||||
|
# Filtro que existe reclama do valor (400 e costuma nomear o campo). Filtro que
|
||||||
|
# não existe é ignorado: 200 e a mesma lista do controle. Uma chamada por
|
||||||
|
# candidato — e o controle serve de régua para os que voltarem 200.
|
||||||
|
if listagens.any?
|
||||||
|
alvo = listagens.first[0].split('?').first
|
||||||
|
titulo("[4] filtros REAIS em #{alvo} (oráculo do valor inválido + controle)")
|
||||||
|
|
||||||
|
cod, corpo, bytes, seg = requisicao(alvo)
|
||||||
|
controle = total(corpo, itens(corpo))
|
||||||
|
linha("#{alvo} (CONTROLE)", cod, bytes, seg, "#{controle} itens")
|
||||||
|
|
||||||
|
%w[ordering limit page_size page planned_date planned_date__gte name search
|
||||||
|
created_at__gte status].each do |param|
|
||||||
|
c = "#{alvo}?#{param}=%25%25%25zz"
|
||||||
|
cod, corpo, bytes, seg = requisicao(c)
|
||||||
|
qtd = total(corpo, itens(corpo))
|
||||||
|
veredito = if cod == 400 then 'FILTRO EXISTE (reclamou do valor)'
|
||||||
|
elsif cod == 200 && qtd == controle then 'ignorado (igual ao controle)'
|
||||||
|
elsif cod == 200 then "mudou a contagem (#{qtd}) — investigar"
|
||||||
|
else "HTTP #{cod}"
|
||||||
|
end
|
||||||
|
linha("?#{param}=<inválido>", cod, bytes, seg, veredito)
|
||||||
|
end
|
||||||
|
|
||||||
|
# ── [4b] os ÚLTIMOS N PLANOS numa chamada só ─────────────────────────────
|
||||||
|
# É o formato que a tela precisa: o operador usa a ferramenta quando lançou
|
||||||
|
# uma operação, então o plano que ele quer está entre os últimos — uma lista
|
||||||
|
# curta de nomes vence qualquer campo de data.
|
||||||
|
titulo("[4b] dá para pedir os #{limite} planos mais recentes de uma vez?")
|
||||||
|
[
|
||||||
|
"#{alvo}?ordering=-planned_date&limit=#{limite}",
|
||||||
|
"#{alvo}?ordering=-created_at&limit=#{limite}",
|
||||||
|
"#{alvo}?ordering=-id&page_size=#{limite}"
|
||||||
|
].each do |c|
|
||||||
|
cod, corpo, bytes, seg = requisicao(c)
|
||||||
|
lista = itens(corpo)
|
||||||
|
resumo = lista.first(limite).filter_map do |i|
|
||||||
|
next unless i.is_a?(Hash)
|
||||||
|
|
||||||
|
nome = i.values_at(*i.keys.select { |k| k.to_s.match?(CHAVES_NOME) }).compact.first
|
||||||
|
"#{nome || i['id']} (#{i['planned_date'] || i['created_at']})"
|
||||||
|
end
|
||||||
|
linha(c, cod, bytes, seg, "#{total(corpo, lista)} itens")
|
||||||
|
puts " #{resumo.join(' | ')}" if resumo.any?
|
||||||
|
end
|
||||||
|
else
|
||||||
|
titulo('[4] sem listagem de planos — nada para filtrar')
|
||||||
|
puts ' Se o bloco [1] achou elo na visita, o caminho é a cadeia NF -> visita'
|
||||||
|
puts ' -> rota -> plano. Se não achou nenhum dos dois, a conclusão antiga'
|
||||||
|
puts ' ("não há como listar planos") se confirma e a saída é a planilha.'
|
||||||
|
end
|
||||||
|
|
||||||
|
# ════════════════════════════════════════════════════════════════════════════
|
||||||
|
# [5] API NAVEGÁVEL — o DRF às vezes desenha o formulário de filtros
|
||||||
|
# ════════════════════════════════════════════════════════════════════════════
|
||||||
|
# Quando a rota é navegável, o HTML traz os nomes de filtro válidos escritos na
|
||||||
|
# página. Só olhamos se aparece algo com cara de filtro; não baixamos a página.
|
||||||
|
titulo('[5] API navegável (Accept: text/html) — pistas de filtro no HTML')
|
||||||
|
['/v1/plans/', '/v1/routes/visits/'].each do |c|
|
||||||
|
cod, corpo, bytes, seg = requisicao(c, aceita: 'text/html')
|
||||||
|
html = corpo.is_a?(String) ? corpo : corpo.to_s
|
||||||
|
pistas = html.scan(/name="([a-z_]{3,30})"/i).flatten.uniq.first(12)
|
||||||
|
linha(c, cod, bytes, seg, pistas.empty? ? 'sem formulário' : "campos: #{pistas.join(', ')}")
|
||||||
|
end
|
||||||
|
|
||||||
|
titulo('COMO LER ISTO')
|
||||||
|
puts <<~FIM
|
||||||
|
• Bloco [3] com itens e nome preenchido -> caminho (A): a tela vira uma lista
|
||||||
|
dos #{limite} planos mais recentes, e a data some do formulário.
|
||||||
|
• Bloco [1] com elo (route/plan) na visita -> caminho (B): o operador cola uma
|
||||||
|
NF do romaneio e o sistema acha o plano. Resolve sem endpoint de listagem.
|
||||||
|
• Os dois vazios -> a conclusão anterior está certa: não há como buscar plano
|
||||||
|
por nome com token de conta, e o .xlsx continua sendo o plano B.
|
||||||
|
|
||||||
|
NÃO altere Romaneios::PlanoDoDia antes de ter esta saída: a escada de queda de
|
||||||
|
lá foi escrita justamente por não haver este dado.
|
||||||
|
FIM
|
||||||
Reference in New Issue
Block a user