, Correçoes na busca da API para ver os ultimos planos
This commit is contained in:
@@ -17,6 +17,11 @@ class Admin::RomaneiosController < ApplicationController
|
||||
@romaneios = Romaneio.recentes.includes(:romaneio_veiculos).limit(50)
|
||||
@operacoes_agrupadas = Operacao.agrupadas_por_mes
|
||||
@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
|
||||
# que o operador enviou; nil = ainda usando o logo da GADE do repositório.
|
||||
@logo_atual = Configuracao.find_by(chave: 'romaneio_logo')&.descricao
|
||||
@@ -44,7 +49,12 @@ class Admin::RomaneiosController < ApplicationController
|
||||
@romaneio = preparar_romaneio
|
||||
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)
|
||||
rescue SimpliRoute::Error => e
|
||||
# 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
|
||||
# romaneio — a chave única (planned_date, operacao_tabela) é a garantia disso.
|
||||
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
|
||||
|
||||
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?
|
||||
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
|
||||
end
|
||||
|
||||
|
||||
@@ -36,8 +36,23 @@ module Romaneios
|
||||
|
||||
attr_reader :origem
|
||||
|
||||
def initialize(data:, client: nil)
|
||||
@data = data.to_date
|
||||
# Duas formas de pedir o plano, e a primeira é a boa:
|
||||
#
|
||||
# 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
|
||||
@origem = nil
|
||||
end
|
||||
@@ -46,25 +61,90 @@ module Romaneios
|
||||
@linhas ||= carregar
|
||||
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
|
||||
|
||||
def cliente
|
||||
@cliente ||= @client || SimpliRoute::Client.new
|
||||
end
|
||||
|
||||
def carregar
|
||||
linhas = da_api
|
||||
linhas = @plano ? da_api_por_plano : da_api
|
||||
return linhas if linhas.any?
|
||||
|
||||
linhas = do_espelho
|
||||
return linhas if linhas.any?
|
||||
# O espelho é por DATA. Vindo de um plano, ele só entra depois que a data
|
||||
# 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,
|
||||
"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).'
|
||||
raise SimpliRoute::NotFound, mensagem_de_ausencia
|
||||
end
|
||||
|
||||
# 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
|
||||
|
||||
# ── (a) e (b) ────────────────────────────────────────────────────────────
|
||||
def da_api
|
||||
return [] unless SimpliRoute.configurado?
|
||||
|
||||
cliente = @client || SimpliRoute::Client.new
|
||||
cliente = self.cliente
|
||||
visitas = cliente.visitas_da_data(@data)
|
||||
return [] if visitas.empty?
|
||||
|
||||
|
||||
@@ -64,22 +64,64 @@ module SimpliRoute
|
||||
Array(get(caminho))
|
||||
end
|
||||
|
||||
# Rotas PLANEJADAS da data (Array de hashes). É aqui que costuma morar o par
|
||||
# veículo/motorista, que a visita talvez não carregue — e o romaneio precisa do
|
||||
# veículo para saber quantas folhas imprimir.
|
||||
# UMA rota pelo uuid. Serve para descobrir o DIA REAL de um plano: o registro
|
||||
# do plano só traz a JANELA (`start_date`/`end_date`), e o dia das rotas nem
|
||||
# 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
|
||||
# desenvolvimento. Por isso é best-effort (endpoint inexistente devolve [], não
|
||||
# explode) e por isso Romaneios::PlanoDoDia tem uma escada de queda até o
|
||||
# espelho local. Rode `bin/sondar_plano_do_dia` no servidor antes de confiar
|
||||
# nesta chamada — e anote o resultado no cabeçalho daquele script, como foi
|
||||
# feito em bin/sondar_busca_nf.
|
||||
# ✔ VERIFICADO em 28/08/2026 contra a API real: `?planned_date=2026-08-31`
|
||||
# devolve 73 rotas (83 KB, 0,59 s), cada uma com `vehicle` (id numérico),
|
||||
# `plan` (uuid) e `total_visits`. ⚠️ O parâmetro é OBRIGATÓRIO: sem ele a rota
|
||||
# responde 200 com **lista vazia**, não com tudo — então "veio vazio" aqui
|
||||
# pode ser data errada, nunca "a conta não tem rotas". A rota NÃO traz array
|
||||
# de visitas (`visits` não existe); quem liga visita→rota é o campo `route`
|
||||
# da própria visita.
|
||||
def rotas_da_data(data)
|
||||
Array(get("/v1/routes/routes/?planned_date=#{data.to_date.iso8601}"))
|
||||
rescue Error
|
||||
[]
|
||||
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
|
||||
# (`vehicle: 630011`) no nome que a operação usa (`GADE_038`).
|
||||
#
|
||||
|
||||
@@ -70,10 +70,51 @@
|
||||
authenticity_token: form_authenticity_token,
|
||||
data: { turbo: false }, class: 'space-y-4' do %>
|
||||
<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>
|
||||
<label class="block text-sm font-medium text-gray-300 mb-1.5">Data do 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
|
||||
<label class="block text-sm font-medium text-gray-300 mb-1.5">Plano</label>
|
||||
|
||||
<% 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]">
|
||||
</div>
|
||||
|
||||
@@ -103,12 +144,18 @@
|
||||
<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>
|
||||
</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
|
||||
placeholder-gray-600 focus:outline-none focus:border-orange-500 min-h-[48px]">
|
||||
<p class="text-xs text-gray-500 mt-1.5">
|
||||
Cole o nome do plano como está no SimpliRoute — é por ele que a
|
||||
operação do mês (quem é <strong>NOVO</strong>) é encontrada.
|
||||
<% if @planos.present? %>
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user