Correção do função de editar lançamento para quer consiga listar todas

This commit is contained in:
2026-07-21 16:23:25 -03:00
parent 2b0a3b52ef
commit faa5226e95
4 changed files with 390 additions and 19 deletions

View File

@@ -33,6 +33,9 @@ class Admin::EdicaoLancamentosController < ApplicationController
#
# Com um único lançamento já vem o card montado, para não custar um segundo
# round-trip no caso comum.
#
# Aceita ?de=&ate= para varrer um período (a API só filtra por UM dia, então
# varremos dia a dia). Sem período, usa as datas que o espelho conhece.
def buscar
authorize :edicao_lancamento
@@ -40,15 +43,21 @@ class Admin::EdicaoLancamentosController < ApplicationController
return render_erro('Informe o número da NF.') if nf.blank?
entregas = Entrega.por_nf(nf).to_a
return render_erro("NF #{nf} não encontrada na base de rastreio.") if entregas.empty?
datas = datas_de_busca(entregas)
return render_erro(sem_datas_msg(nf, entregas)) if datas.empty?
ocorrencias = ocorrencias_da_nf(nf, entregas)
falhas = []
ocorrencias = ocorrencias_da_nf(nf, entregas, datas, falhas)
unica = ocorrencias.first if ocorrencias.one? && ocorrencias.first[:id].present?
render json: {
ok: true,
ocorrencias: ocorrencias,
visita: (unica && payload_visita(unica[:id], entrega_de(entregas, unica[:tracking_id]))),
visita: (unica && payload_seguro(unica, entregas, falhas)),
periodo: { de: datas.first, ate: datas.last, dias: datas.size },
# Dias que a API não respondeu. Vão para a tela: uma busca que devolve
# menos do que existe TEM que dizer que devolveu menos.
falhas: falhas,
motivos: motivos_seguros
}
rescue SimpliRoute::NotFound => e
@@ -152,18 +161,53 @@ class Admin::EdicaoLancamentosController < ApplicationController
@client ||= SimpliRoute::Client.new
end
# Máximo de dias varridos numa busca por período. A API só filtra por UM dia,
# então cada dia é uma chamada — com `search` cada uma custa ~0,6 s e elas vão
# em lotes paralelos, o que torna dois meses viável; o teto existe para a tela
# não virar uma varredura sem fim.
MAX_DIAS_VARREDURA = 62
# Quais dias consultar na API:
# • com ?de=/?ate= — o período pedido (o SimpliRoute duplica plano para
# outra data, e aí o espelho não tem como saber qual é);
# • sem período — as datas que o espelho conhece (barato: 1 ou 2 chamadas).
def datas_de_busca(entregas)
de = data_param(:de)
ate = data_param(:ate)
if de || ate
inicio, fim = [de || ate, ate || de].minmax
(inicio..fim).first(MAX_DIAS_VARREDURA)
else
entregas.filter_map { |e| e.planned_date&.to_date }.uniq.sort
end
end
def data_param(chave)
valor = params[chave].to_s.strip
return nil if valor.blank?
Date.parse(valor)
rescue Date::Error
nil
end
# Sem datas não há o que consultar — e o motivo muda a saída para o ADM.
def sem_datas_msg(nf, entregas)
if entregas.empty?
"NF #{nf} não encontrada na base de rastreio. Informe um período para procurar direto no SimpliRoute."
else
"NF #{nf} está na base de rastreio mas sem data planejada. Informe um período para procurar na API."
end
end
# Todos os lançamentos da NF, cruzando as DUAS fontes:
# • espelho de rastreio (Entrega) — traz motorista/veículo;
# • API do SimpliRoute, nas datas desses lançamentos — pega a visita
# duplicada que a sincronização ainda não trouxe para o espelho.
#
# A API não tem busca global por NF (só por data), então as datas vêm do
# espelho: 1 chamada por data distinta (plano duplicado costuma cair no mesmo
# dia, e aí é 1 chamada só).
def ocorrencias_da_nf(nf, entregas)
datas = entregas.filter_map { |e| e.planned_date&.to_date }.uniq
# • API do SimpliRoute, nas datas varridas — pega a visita duplicada que a
# sincronização ainda não trouxe para o espelho.
def ocorrencias_da_nf(nf, entregas, datas, falhas)
trackings = entregas.map { |e| e.tracking_id.to_s }
do_dia = datas.flat_map { |data| client.visitas_da_data(data) }
do_dia = visitas_dos_dias(datas, nf, falhas)
visitas = do_dia.select { |v| v['reference'].to_s == nf || trackings.include?(v['tracking_id'].to_s) }
.uniq { |v| v['id'] }
@@ -176,13 +220,48 @@ class Admin::EdicaoLancamentosController < ApplicationController
# ele existe em vez de sumir silenciosamente — que é o bug que estamos
# corrigindo.
vistos = visitas.map { |v| v['tracking_id'].to_s }
orfas = entregas.reject { |e| vistos.include?(e.tracking_id.to_s) }.map { |e| ocorrencia_do_espelho(e) }
orfas = entregas.reject { |e| vistos.include?(e.tracking_id.to_s) }
.map { |e| ocorrencia_do_espelho(e, datas.include?(e.planned_date&.to_date)) }
# Mais recente primeiro (a data vem como texto ISO da API e como timestamp
# do espelho — os 10 primeiros caracteres normalizam as duas).
(achadas + orfas).sort_by { |o| [o[:data].to_s[0, 10], o[:id].to_i] }.reverse
end
# Dias consultados de uma vez. Cada chamada abre a própria conexão HTTP (ver
# SimpliRoute::Client#requisicao) e não toca o banco, então dá para
# paralelizar em lotes; o lote pequeno evita martelar a API do SimpliRoute.
MAX_PARALELO_VARREDURA = 6
# Passa a NF como `search` — sem isso cada dia baixaria ~3,8 MB (o dia inteiro)
# e um mês seria inviável. Ver a nota em SimpliRoute::Client#visitas_da_data.
def visitas_dos_dias(datas, nf, falhas)
mutex = Mutex.new
datas.each_slice(MAX_PARALELO_VARREDURA).flat_map do |lote|
lote.map { |data| Thread.new { visitas_do_dia(data, nf, falhas, mutex) } }.flat_map(&:value)
end
end
# Um dia que a API não responde NÃO pode derrubar a busca inteira: registra a
# falha e segue com os outros dias. (Foi assim que uma NF com visitas em duas
# datas passou a não devolver nada em vez de devolver o que deu certo.)
def visitas_do_dia(data, nf, falhas, mutex)
client.visitas_da_data(data, busca: nf)
rescue SimpliRoute::Error => e
mutex.synchronize { falhas << { data: data.strftime('%d/%m/%Y'), erro: e.message } }
[]
end
# Abrir o card do lançamento único é conveniência: se falhar, a lista ainda
# tem que aparecer.
def payload_seguro(ocorrencia, entregas, falhas)
payload_visita(ocorrencia[:id], entrega_de(entregas, ocorrencia[:tracking_id]))
rescue SimpliRoute::Error => e
falhas << { data: ocorrencia[:data].to_s[0, 10], erro: "não foi possível abrir o lançamento: #{e.message}" }
nil
end
def entrega_de(entregas, tracking_id)
return nil if tracking_id.blank?
@@ -233,7 +312,7 @@ class Admin::EdicaoLancamentosController < ApplicationController
}
end
def ocorrencia_do_espelho(entrega)
def ocorrencia_do_espelho(entrega, data_varrida)
{
id: nil,
tracking_id: entrega.tracking_id,
@@ -246,7 +325,11 @@ class Admin::EdicaoLancamentosController < ApplicationController
veiculo: entrega.vehicle,
editavel: false,
no_painel: true,
aviso: 'Só no painel — não está entre as visitas desta data no SimpliRoute, então não dá para editar'
aviso: (if data_varrida
'Só no painel — não está entre as visitas desta data no SimpliRoute, então não dá para editar'
else
'Fora do período consultado — inclua a data deste lançamento no período para poder editá-lo'
end)
}
end

View File

@@ -41,8 +41,27 @@ module SimpliRoute
end
# Todas as visitas de uma data (Array de hashes).
def visitas_da_data(data)
Array(get("/v1/routes/visits/?planned_date=#{data.to_date.iso8601}"))
#
# `busca` vira `&search=` — parâmetro NÃO documentado, mas verificado contra
# a API real em 21/07/2026: filtra de verdade e derruba a resposta de um dia
# de ~3,8 MB / ~9 s (1879 visitas) para ~1 KB / ~0,6 s. Sem ele, varrer um
# mês é inviável.
#
# ⚠️ NÃO confiar só nele: `reference`, `reference_id`, `q` e `title` são
# ignorados em SILÊNCIO pela API (devolvem 200 com o dia inteiro). Se o
# `search` mudar de nome, cai no mesmo comportamento — por isso quem chama
# continua filtrando por `reference`, e a perda é só de desempenho.
#
# ⚠️ SEM `planned_date` a API responde um conjunto padrão (~2469 visitas,
# medido) que NÃO cobre o histórico: a NF 82891 voltava só com a visita de
# 21/07, escondendo a de 17/07. Todo filtro de intervalo testado
# (planned_date_from/to, since/until, __gte/__lte, date_from/to) é ignorado.
# Por isso a data continua obrigatória e a varredura é dia a dia.
def visitas_da_data(data, busca: nil)
caminho = "/v1/routes/visits/?planned_date=#{data.to_date.iso8601}"
caminho += "&search=#{URI.encode_www_form_component(busca.to_s)}" if busca.present?
Array(get(caminho))
end
# Resolve o `id` numérico (usado na URL de escrita) a partir de uma Entrega

View File

@@ -40,8 +40,40 @@
Buscar
</button>
</div>
<%# Período — a API do SimpliRoute só filtra visita por UM dia, então achar
uma NF exige saber em quais dias procurar. Sem período, procuramos nas
datas que o painel já conhece; quando o plano foi duplicado para outra
data, o painel não sabe dela e é preciso ampliar aqui. %>
<div class="flex flex-wrap items-end gap-2 mt-3">
<div>
<label class="block text-xs text-gray-500 mb-1" for="campo-de">De</label>
<input id="campo-de" type="date"
class="bg-[#1a1a1a] border border-white/10 rounded-xl px-3 py-2 text-white text-sm
focus:outline-none focus:border-orange-500 min-h-[42px]">
</div>
<div>
<label class="block text-xs text-gray-500 mb-1" for="campo-ate">Até</label>
<input id="campo-ate" type="date"
class="bg-[#1a1a1a] border border-white/10 rounded-xl px-3 py-2 text-white text-sm
focus:outline-none focus:border-orange-500 min-h-[42px]">
</div>
<button id="btn-30dias" type="button"
class="px-4 py-2 bg-[#1a1a1a] hover:bg-[#252525] border border-white/10 text-gray-200
text-sm rounded-xl transition-colors min-h-[42px] whitespace-nowrap">
Últimos 30 dias
</button>
<button id="btn-limpar-periodo" type="button"
class="px-3 py-2 text-gray-500 hover:text-gray-300 text-sm min-h-[42px]">Limpar</button>
</div>
<p class="mt-2 text-xs text-gray-500">
Sem período, procuramos só nas datas que o painel conhece. Se o plano foi duplicado para outra
data, amplie o período — a API do SimpliRoute não busca NF por intervalo, então varremos dia a
dia (máx. 62 dias).
</p>
<p id="busca-erro" class="hidden mt-2 text-sm text-red-400"></p>
<p id="busca-loading" class="hidden mt-2 text-sm text-gray-400">Consultando SimpliRoute…</p>
<p id="busca-periodo" class="hidden mt-2 text-xs text-gray-500"></p>
<div id="busca-falhas" class="hidden mt-2 p-3 rounded-xl bg-red-900/20 border border-red-500/40 text-xs text-red-200 space-y-1"></div>
</div>
<%# Lançamentos da NF — só aparece quando há mais de um. Plano duplicado no
@@ -199,9 +231,16 @@
const loadEl = document.getElementById('busca-loading');
const cardEl = document.getElementById('card');
const campoDe = document.getElementById('campo-de');
const campoAte = document.getElementById('campo-ate');
const periodoEl = document.getElementById('busca-periodo');
const falhasEl = document.getElementById('busca-falhas');
async function buscar() {
const nf = campoNf.value.trim();
erroEl.classList.add('hidden');
periodoEl.classList.add('hidden');
falhasEl.classList.add('hidden');
if (!nf) { mostrarErroBusca('Informe o número da NF.'); return; }
loadEl.classList.remove('hidden');
@@ -209,14 +248,18 @@
ocorrencias = [];
selecionado = null;
try {
const resp = await fetch(URLS.buscar + '?nf=' + encodeURIComponent(nf), { headers: { 'Accept': 'application/json' } });
let url = URLS.buscar + '?nf=' + encodeURIComponent(nf);
if (campoDe.value) url += '&de=' + encodeURIComponent(campoDe.value);
if (campoAte.value) url += '&ate=' + encodeURIComponent(campoAte.value);
const resp = await fetch(url, { headers: { 'Accept': 'application/json' } });
const data = await resp.json();
if (!data.ok) { renderOcorrencias(); mostrarErroBusca(data.erro || 'Não foi possível localizar a entrega.'); return; }
ocorrencias = data.ocorrencias || [];
renderPeriodo(data.periodo, data.falhas);
// Só veio o card pronto quando a NF tem um lançamento único; com mais de
// um, o ADM escolhe na lista antes de abrir qualquer coisa.
if (data.visita) { abrirCard(data.visita); } else { visita = null; renderOcorrencias(); }
if (!data.visita && !ocorrencias.length) mostrarErroBusca('Nenhum lançamento localizado para esta NF.');
if (!data.visita && !ocorrencias.length) mostrarErroBusca('Nenhum lançamento localizado para esta NF no período consultado.');
} catch (e) {
renderOcorrencias();
mostrarErroBusca('Erro de conexão ao buscar a NF.');
@@ -226,9 +269,37 @@
}
function mostrarErroBusca(msg) { erroEl.textContent = msg; erroEl.classList.remove('hidden'); }
// Dizer o que foi consultado e o que falhou. Uma busca que devolve menos do
// que existe precisa admitir isso — senão parece que a NF não existe.
function renderPeriodo(periodo, falhas) {
if (periodo && periodo.dias) {
periodoEl.textContent = 'Consultado: ' + fmtData(periodo.de) +
(periodo.dias > 1 ? ' a ' + fmtData(periodo.ate) + ' (' + periodo.dias + ' dias)' : '') + '.';
periodoEl.classList.remove('hidden');
}
if (falhas && falhas.length) {
falhasEl.innerHTML = '<p class="font-semibold">A API do SimpliRoute falhou em ' + falhas.length +
' dia(s) — pode haver lançamento faltando na lista:</p>' +
falhas.map(f => '<p>' + esc(f.data) + ' — ' + esc(f.erro) + '</p>').join('');
falhasEl.classList.remove('hidden');
}
}
document.getElementById('btn-buscar').addEventListener('click', buscar);
campoNf.addEventListener('keydown', e => { if (e.key === 'Enter') { e.preventDefault(); buscar(); } });
document.getElementById('btn-30dias').addEventListener('click', () => {
const hoje = new Date();
const trinta = new Date(hoje.getTime() - 30 * 86400000);
const iso = d => d.toISOString().slice(0, 10);
campoDe.value = iso(trinta);
campoAte.value = iso(hoje);
if (campoNf.value.trim()) buscar();
});
document.getElementById('btn-limpar-periodo').addEventListener('click', () => {
campoDe.value = ''; campoAte.value = '';
});
// ── Lista de lançamentos da NF ──────────────────────────────
const ocEl = document.getElementById('ocorrencias');
const ocLista = document.getElementById('oc-lista');