Correção do função de editar lançamento para quer consiga listar todas
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user