teste #10
@@ -22,45 +22,34 @@ class Admin::EdicaoLancamentosController < ApplicationController
|
|||||||
@motivos = motivos_seguros
|
@motivos = motivos_seguros
|
||||||
end
|
end
|
||||||
|
|
||||||
# GET /admin/edicao_lancamento/buscar?nf=79774 → JSON com o estado atual.
|
# GET /admin/edicao_lancamento/buscar?nf=79774 → JSON com TODOS os lançamentos
|
||||||
|
# daquela NF.
|
||||||
|
#
|
||||||
|
# ⚠️ Uma NF pode ter mais de um lançamento: quando o plano é duplicado no
|
||||||
|
# SimpliRoute nasce uma visita nova (outro tracking_id) com a mesma NF, e a
|
||||||
|
# antiga — a que tem o motorista/checkout — continua existindo. Por isso a
|
||||||
|
# busca devolve a LISTA e o ADM escolhe qual editar; assumir "1 NF = 1
|
||||||
|
# lançamento" escondia justamente o lançamento antigo.
|
||||||
|
#
|
||||||
|
# Com um único lançamento já vem o card montado, para não custar um segundo
|
||||||
|
# round-trip no caso comum.
|
||||||
def buscar
|
def buscar
|
||||||
authorize :edicao_lancamento
|
authorize :edicao_lancamento
|
||||||
|
|
||||||
nf = params[:nf].to_s.strip
|
nf = params[:nf].to_s.strip
|
||||||
return render_erro('Informe o número da NF.') if nf.blank?
|
return render_erro('Informe o número da NF.') if nf.blank?
|
||||||
|
|
||||||
entrega = Entrega.por_nf(nf).first
|
entregas = Entrega.por_nf(nf).to_a
|
||||||
return render_erro("NF #{nf} não encontrada na base de rastreio.") if entrega.nil?
|
return render_erro("NF #{nf} não encontrada na base de rastreio.") if entregas.empty?
|
||||||
|
|
||||||
id = client.resolver_id(entrega)
|
ocorrencias = ocorrencias_da_nf(nf, entregas)
|
||||||
visita = client.visita(id)
|
unica = ocorrencias.first if ocorrencias.one? && ocorrencias.first[:id].present?
|
||||||
# Segunda fonte de imagens (POD). Não é obrigatória: se falhar, vem {}.
|
|
||||||
detalhe = client.detalhe_visita(id)
|
|
||||||
|
|
||||||
render json: {
|
render json: {
|
||||||
ok: true,
|
ok: true,
|
||||||
visita: {
|
ocorrencias: ocorrencias,
|
||||||
id: id,
|
visita: (unica && payload_visita(unica[:id], entrega_de(entregas, unica[:tracking_id]))),
|
||||||
nf: visita['reference'],
|
motivos: motivos_seguros
|
||||||
titulo: visita['title'],
|
|
||||||
endereco: visita['address'],
|
|
||||||
status: visita['status'],
|
|
||||||
checkout_observation: visita['checkout_observation'],
|
|
||||||
checkout_comment: visita['checkout_comment'],
|
|
||||||
notes: visita['notes'],
|
|
||||||
checkout_time: visita['checkout_time'],
|
|
||||||
checkout_latitude: visita['checkout_latitude'],
|
|
||||||
checkout_longitude: visita['checkout_longitude'],
|
|
||||||
planned_date: visita['planned_date'],
|
|
||||||
contato: visita['contact_name'],
|
|
||||||
telefone: visita['contact_phone'],
|
|
||||||
# Galeria unificada — TODAS as fotos do lançamento, de todas as fontes,
|
|
||||||
# cada uma etiquetada para o ADM saber o que está conferindo.
|
|
||||||
fotos: fotos_do_lancamento(entrega, visita, detalhe),
|
|
||||||
motorista: entrega.driver,
|
|
||||||
veiculo: entrega.vehicle
|
|
||||||
},
|
|
||||||
motivos: motivos_seguros
|
|
||||||
}
|
}
|
||||||
rescue SimpliRoute::NotFound => e
|
rescue SimpliRoute::NotFound => e
|
||||||
render_erro(e.message)
|
render_erro(e.message)
|
||||||
@@ -68,6 +57,23 @@ class Admin::EdicaoLancamentosController < ApplicationController
|
|||||||
render_erro("Erro ao consultar o SimpliRoute: #{e.message}", status: :bad_gateway)
|
render_erro("Erro ao consultar o SimpliRoute: #{e.message}", status: :bad_gateway)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# GET /admin/edicao_lancamento/carregar?visit_id=871407488&tracking_id=SR...
|
||||||
|
# Monta o card de UM lançamento escolhido na lista de ocorrências.
|
||||||
|
def carregar
|
||||||
|
authorize :edicao_lancamento
|
||||||
|
|
||||||
|
id = params[:visit_id].to_s.strip
|
||||||
|
return render_erro('Lançamento não identificado.') if id.blank?
|
||||||
|
|
||||||
|
entrega = Entrega.find_by(tracking_id: params[:tracking_id]) if params[:tracking_id].present?
|
||||||
|
|
||||||
|
render json: { ok: true, visita: payload_visita(id, entrega), motivos: motivos_seguros }
|
||||||
|
rescue SimpliRoute::NotFound => e
|
||||||
|
render_erro(e.message)
|
||||||
|
rescue SimpliRoute::Error => e
|
||||||
|
render_erro("Erro ao consultar o SimpliRoute: #{e.message}", status: :bad_gateway)
|
||||||
|
end
|
||||||
|
|
||||||
# PATCH /admin/edicao_lancamento/atualizar
|
# PATCH /admin/edicao_lancamento/atualizar
|
||||||
# HTML: fluxo de form clássico (fallback). JSON: salvar-por-campo do card
|
# HTML: fluxo de form clássico (fallback). JSON: salvar-por-campo do card
|
||||||
# (click-to-edit) — o fetch manda visit_id + só o campo editado.
|
# (click-to-edit) — o fetch manda visit_id + só o campo editado.
|
||||||
@@ -146,6 +152,107 @@ class Admin::EdicaoLancamentosController < ApplicationController
|
|||||||
@client ||= SimpliRoute::Client.new
|
@client ||= SimpliRoute::Client.new
|
||||||
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
|
||||||
|
trackings = entregas.map { |e| e.tracking_id.to_s }
|
||||||
|
do_dia = datas.flat_map { |data| client.visitas_da_data(data) }
|
||||||
|
|
||||||
|
visitas = do_dia.select { |v| v['reference'].to_s == nf || trackings.include?(v['tracking_id'].to_s) }
|
||||||
|
.uniq { |v| v['id'] }
|
||||||
|
|
||||||
|
achadas = visitas.map { |v| ocorrencia_da_api(v, entrega_de(entregas, v['tracking_id'])) }
|
||||||
|
|
||||||
|
# Lançamento que existe no espelho mas não apareceu na API (data sem plano,
|
||||||
|
# visita removida): entra na lista como não editável, para o ADM ver que
|
||||||
|
# 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) }
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
def entrega_de(entregas, tracking_id)
|
||||||
|
entregas.find { |e| e.tracking_id.to_s == tracking_id.to_s }
|
||||||
|
end
|
||||||
|
|
||||||
|
# Resumo de um lançamento para a lista de escolha (não carrega fotos/detalhe —
|
||||||
|
# isso só acontece quando o ADM abre um).
|
||||||
|
def ocorrencia_da_api(visita, entrega)
|
||||||
|
{
|
||||||
|
id: visita['id'],
|
||||||
|
tracking_id: visita['tracking_id'],
|
||||||
|
titulo: visita['title'],
|
||||||
|
endereco: visita['address'],
|
||||||
|
status: visita['status'],
|
||||||
|
data: visita['planned_date'],
|
||||||
|
checkout_time: visita['checkout_time'],
|
||||||
|
motorista: entrega&.driver,
|
||||||
|
veiculo: entrega&.vehicle,
|
||||||
|
editavel: true,
|
||||||
|
# Está na API mas ainda não no espelho — daí não sabermos motorista/veículo.
|
||||||
|
aviso: entrega ? nil : 'Ainda não sincronizado no painel'
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def ocorrencia_do_espelho(entrega)
|
||||||
|
{
|
||||||
|
id: nil,
|
||||||
|
tracking_id: entrega.tracking_id,
|
||||||
|
titulo: entrega.local,
|
||||||
|
endereco: entrega.address,
|
||||||
|
status: entrega.status,
|
||||||
|
data: entrega.planned_date,
|
||||||
|
checkout_time: entrega.checkout,
|
||||||
|
motorista: entrega.driver,
|
||||||
|
veiculo: entrega.vehicle,
|
||||||
|
editavel: false,
|
||||||
|
aviso: 'Não localizado na API do SimpliRoute — não é editável'
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
# Estado completo de UMA visita para o card (inclui a galeria de fotos, que
|
||||||
|
# custa uma chamada extra ao POD). `entrega` pode ser nil quando a visita
|
||||||
|
# ainda não sincronizou no espelho.
|
||||||
|
def payload_visita(id, entrega)
|
||||||
|
visita = client.visita(id)
|
||||||
|
# Segunda fonte de imagens (POD). Não é obrigatória: se falhar, vem {}.
|
||||||
|
detalhe = client.detalhe_visita(id)
|
||||||
|
|
||||||
|
{
|
||||||
|
id: id,
|
||||||
|
tracking_id: visita['tracking_id'],
|
||||||
|
nf: visita['reference'],
|
||||||
|
titulo: visita['title'],
|
||||||
|
endereco: visita['address'],
|
||||||
|
status: visita['status'],
|
||||||
|
checkout_observation: visita['checkout_observation'],
|
||||||
|
checkout_comment: visita['checkout_comment'],
|
||||||
|
notes: visita['notes'],
|
||||||
|
checkout_time: visita['checkout_time'],
|
||||||
|
checkout_latitude: visita['checkout_latitude'],
|
||||||
|
checkout_longitude: visita['checkout_longitude'],
|
||||||
|
planned_date: visita['planned_date'],
|
||||||
|
contato: visita['contact_name'],
|
||||||
|
telefone: visita['contact_phone'],
|
||||||
|
# Galeria unificada — TODAS as fotos do lançamento, de todas as fontes,
|
||||||
|
# cada uma etiquetada para o ADM saber o que está conferindo.
|
||||||
|
fotos: fotos_do_lancamento(entrega, visita, detalhe),
|
||||||
|
motorista: entrega&.driver,
|
||||||
|
veiculo: entrega&.vehicle
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
# Monta o hash de PATCH só com os campos que vieram no form E que realmente
|
# Monta o hash de PATCH só com os campos que vieram no form E que realmente
|
||||||
# mudaram em relação ao estado atual — PATCH mínimo, sem sobrescrever à toa.
|
# mudaram em relação ao estado atual — PATCH mínimo, sem sobrescrever à toa.
|
||||||
def mudancas(anterior)
|
def mudancas(anterior)
|
||||||
|
|||||||
@@ -49,6 +49,11 @@ module SimpliRoute
|
|||||||
# do espelho local, que só tem tracking_id (SR...) + reference_id (NF) +
|
# do espelho local, que só tem tracking_id (SR...) + reference_id (NF) +
|
||||||
# planned_date. Casa pelo tracking_id; se não achar, tenta pela NF.
|
# planned_date. Casa pelo tracking_id; se não achar, tenta pela NF.
|
||||||
# Levanta NotFound (nada) ou Error (ambiguidade sem tracking_id).
|
# Levanta NotFound (nada) ou Error (ambiguidade sem tracking_id).
|
||||||
|
#
|
||||||
|
# ⚠️ Ambiguidade por NF é REAL (plano duplicado gera duas visitas com a mesma
|
||||||
|
# NF). Quem precisa lidar com isso — a tela de edição — usa
|
||||||
|
# `visitas_da_data` + `casa_entrega?` e deixa o ADM escolher; este método
|
||||||
|
# continua servindo os fluxos que exigem resposta única.
|
||||||
def resolver_id(entrega)
|
def resolver_id(entrega)
|
||||||
data = entrega.planned_date&.to_date
|
data = entrega.planned_date&.to_date
|
||||||
raise NotFound, 'Entrega sem planned_date — impossível localizar na API.' if data.blank?
|
raise NotFound, 'Entrega sem planned_date — impossível localizar na API.' if data.blank?
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
/history/ da API). Os dados do card chegam via fetch (endpoint buscar). %>
|
/history/ da API). Os dados do card chegam via fetch (endpoint buscar). %>
|
||||||
<div class="space-y-6" id="edicao-app"
|
<div class="space-y-6" id="edicao-app"
|
||||||
data-buscar-url="<%= buscar_admin_edicao_lancamento_path %>"
|
data-buscar-url="<%= buscar_admin_edicao_lancamento_path %>"
|
||||||
|
data-carregar-url="<%= carregar_admin_edicao_lancamento_path %>"
|
||||||
data-atualizar-url="<%= atualizar_admin_edicao_lancamento_path %>"
|
data-atualizar-url="<%= atualizar_admin_edicao_lancamento_path %>"
|
||||||
data-historico-url="<%= historico_admin_edicao_lancamento_path %>"
|
data-historico-url="<%= historico_admin_edicao_lancamento_path %>"
|
||||||
data-csrf="<%= form_authenticity_token %>"
|
data-csrf="<%= form_authenticity_token %>"
|
||||||
@@ -43,6 +44,16 @@
|
|||||||
<p id="busca-loading" class="hidden mt-2 text-sm text-gray-400">Consultando SimpliRoute…</p>
|
<p id="busca-loading" class="hidden mt-2 text-sm text-gray-400">Consultando SimpliRoute…</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<%# Lançamentos da NF — só aparece quando há mais de um. Plano duplicado no
|
||||||
|
SimpliRoute cria uma visita nova com a MESMA NF, e a antiga (a que tem o
|
||||||
|
motorista) continua valendo: aqui o ADM vê as duas e escolhe qual editar. %>
|
||||||
|
<div id="ocorrencias" class="hidden bg-[#111] border border-white/5 rounded-2xl p-5 space-y-3">
|
||||||
|
<p class="text-sm text-gray-300">
|
||||||
|
<span id="oc-contador" class="font-semibold text-white"></span> <span id="oc-dica"></span>
|
||||||
|
</p>
|
||||||
|
<div id="oc-lista" class="space-y-2"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<%# Card do lançamento (preenchido via JS após a busca) %>
|
<%# Card do lançamento (preenchido via JS após a busca) %>
|
||||||
<div id="card" class="hidden bg-[#111] border border-white/5 rounded-2xl overflow-hidden">
|
<div id="card" class="hidden bg-[#111] border border-white/5 rounded-2xl overflow-hidden">
|
||||||
|
|
||||||
@@ -142,13 +153,16 @@
|
|||||||
const app = document.getElementById('edicao-app');
|
const app = document.getElementById('edicao-app');
|
||||||
const URLS = {
|
const URLS = {
|
||||||
buscar: app.dataset.buscarUrl,
|
buscar: app.dataset.buscarUrl,
|
||||||
|
carregar: app.dataset.carregarUrl,
|
||||||
atualizar: app.dataset.atualizarUrl,
|
atualizar: app.dataset.atualizarUrl,
|
||||||
historico: app.dataset.historicoUrl
|
historico: app.dataset.historicoUrl
|
||||||
};
|
};
|
||||||
const CSRF = app.dataset.csrf;
|
const CSRF = app.dataset.csrf;
|
||||||
const MOTIVOS = JSON.parse(app.dataset.motivos || '[]');
|
const MOTIVOS = JSON.parse(app.dataset.motivos || '[]');
|
||||||
|
|
||||||
let visita = null; // estado atual do lançamento carregado
|
let visita = null; // estado atual do lançamento carregado
|
||||||
|
let ocorrencias = []; // todos os lançamentos da NF buscada
|
||||||
|
let selecionado = null; // tracking_id do lançamento aberto no card
|
||||||
|
|
||||||
// ── Config dos campos editáveis ─────────────────────────────
|
// ── Config dos campos editáveis ─────────────────────────────
|
||||||
// Ícone do sprite (public/icons.svg). fill=currentColor -> herda a cor do badge.
|
// Ícone do sprite (public/icons.svg). fill=currentColor -> herda a cor do badge.
|
||||||
@@ -192,14 +206,19 @@
|
|||||||
|
|
||||||
loadEl.classList.remove('hidden');
|
loadEl.classList.remove('hidden');
|
||||||
cardEl.classList.add('hidden');
|
cardEl.classList.add('hidden');
|
||||||
|
ocorrencias = [];
|
||||||
|
selecionado = null;
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(URLS.buscar + '?nf=' + encodeURIComponent(nf), { headers: { 'Accept': 'application/json' } });
|
const resp = await fetch(URLS.buscar + '?nf=' + encodeURIComponent(nf), { headers: { 'Accept': 'application/json' } });
|
||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
if (!data.ok) { mostrarErroBusca(data.erro || 'Não foi possível localizar a entrega.'); return; }
|
if (!data.ok) { renderOcorrencias(); mostrarErroBusca(data.erro || 'Não foi possível localizar a entrega.'); return; }
|
||||||
visita = data.visita;
|
ocorrencias = data.ocorrencias || [];
|
||||||
renderCard();
|
// Só veio o card pronto quando a NF tem um lançamento único; com mais de
|
||||||
cardEl.classList.remove('hidden');
|
// 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.');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
renderOcorrencias();
|
||||||
mostrarErroBusca('Erro de conexão ao buscar a NF.');
|
mostrarErroBusca('Erro de conexão ao buscar a NF.');
|
||||||
} finally {
|
} finally {
|
||||||
loadEl.classList.add('hidden');
|
loadEl.classList.add('hidden');
|
||||||
@@ -210,6 +229,89 @@
|
|||||||
document.getElementById('btn-buscar').addEventListener('click', buscar);
|
document.getElementById('btn-buscar').addEventListener('click', buscar);
|
||||||
campoNf.addEventListener('keydown', e => { if (e.key === 'Enter') { e.preventDefault(); buscar(); } });
|
campoNf.addEventListener('keydown', e => { if (e.key === 'Enter') { e.preventDefault(); buscar(); } });
|
||||||
|
|
||||||
|
// ── Lista de lançamentos da NF ──────────────────────────────
|
||||||
|
const ocEl = document.getElementById('ocorrencias');
|
||||||
|
const ocLista = document.getElementById('oc-lista');
|
||||||
|
const ocCont = document.getElementById('oc-contador');
|
||||||
|
const ocDica = document.getElementById('oc-dica');
|
||||||
|
|
||||||
|
const fmtData = v => {
|
||||||
|
if (!v) return '—';
|
||||||
|
const d = new Date(v);
|
||||||
|
return isNaN(d) ? String(v).slice(0, 10) : d.toLocaleDateString('pt-BR');
|
||||||
|
};
|
||||||
|
|
||||||
|
// Com um lançamento editável só, a lista some — a tela fica igual ao que era
|
||||||
|
// antes. Fica visível se ele não for editável, para explicar o porquê.
|
||||||
|
function renderOcorrencias() {
|
||||||
|
const n = ocorrencias.length;
|
||||||
|
if (n === 0 || (n === 1 && ocorrencias[0].editavel)) {
|
||||||
|
ocEl.classList.add('hidden'); ocLista.innerHTML = ''; return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ocCont.textContent = n + (n === 1 ? ' lançamento' : ' lançamentos');
|
||||||
|
ocDica.textContent = n > 1 ? 'para esta NF — escolha qual quer editar.' : 'para esta NF.';
|
||||||
|
ocLista.innerHTML = ocorrencias.map((o, i) => {
|
||||||
|
const cfg = STATUS[o.status] || { label: o.status || '—', cls: 'bg-gray-800 text-gray-400', icone: 'question-circle' };
|
||||||
|
const ativo = selecionado != null && String(selecionado) === String(o.tracking_id);
|
||||||
|
return '' +
|
||||||
|
'<div class="flex flex-wrap items-center justify-between gap-3 p-3 rounded-xl border ' +
|
||||||
|
(ativo ? 'border-orange-500/60 bg-orange-500/10' : 'border-white/10 bg-[#1a1a1a]') + '">' +
|
||||||
|
'<div class="min-w-0">' +
|
||||||
|
'<div class="flex items-center gap-2 flex-wrap">' +
|
||||||
|
'<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-bold ' + cfg.cls + '">' +
|
||||||
|
ico(cfg.icone) + ' ' + esc(cfg.label) + '</span>' +
|
||||||
|
'<span class="text-white text-sm font-semibold">' + esc(fmtData(o.data)) + '</span>' +
|
||||||
|
'<span class="text-xs text-gray-500">Visita ' + esc(o.id ?? '—') + '</span>' +
|
||||||
|
'</div>' +
|
||||||
|
'<p class="text-xs text-gray-400 mt-1">' +
|
||||||
|
(o.motorista ? esc(o.motorista) : '<span class="text-gray-600">sem motorista</span>') + ' · ' +
|
||||||
|
(o.veiculo ? esc(o.veiculo) : '<span class="text-gray-600">sem veículo</span>') +
|
||||||
|
'</p>' +
|
||||||
|
(o.aviso ? '<p class="text-xs text-amber-400 mt-1">' + esc(o.aviso) + '</p>' : '') +
|
||||||
|
'</div>' +
|
||||||
|
(o.editavel
|
||||||
|
? '<button type="button" data-oc="' + i + '" class="px-4 py-2 text-sm font-semibold rounded-xl transition-colors ' +
|
||||||
|
(ativo ? 'bg-white/5 text-gray-400 border border-white/10' : 'bg-[#f97316] hover:bg-orange-500 text-white') + '">' +
|
||||||
|
(ativo ? 'Editando' : 'Editar') + '</button>'
|
||||||
|
: '') +
|
||||||
|
'</div>';
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
ocLista.addEventListener('click', e => {
|
||||||
|
const btn = e.target.closest('[data-oc]');
|
||||||
|
if (btn) abrirOcorrencia(ocorrencias[Number(btn.dataset.oc)]);
|
||||||
|
});
|
||||||
|
|
||||||
|
async function abrirOcorrencia(o) {
|
||||||
|
if (!o || !o.id) return;
|
||||||
|
erroEl.classList.add('hidden');
|
||||||
|
loadEl.classList.remove('hidden');
|
||||||
|
cardEl.classList.add('hidden');
|
||||||
|
try {
|
||||||
|
const resp = await fetch(URLS.carregar + '?visit_id=' + encodeURIComponent(o.id) +
|
||||||
|
(o.tracking_id ? '&tracking_id=' + encodeURIComponent(o.tracking_id) : ''),
|
||||||
|
{ headers: { 'Accept': 'application/json' } });
|
||||||
|
const data = await resp.json();
|
||||||
|
if (!data.ok) { mostrarErroBusca(data.erro || 'Não foi possível abrir este lançamento.'); return; }
|
||||||
|
abrirCard(data.visita);
|
||||||
|
cardEl.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||||
|
} catch (e) {
|
||||||
|
mostrarErroBusca('Erro de conexão ao abrir o lançamento.');
|
||||||
|
} finally {
|
||||||
|
loadEl.classList.add('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function abrirCard(v) {
|
||||||
|
visita = v;
|
||||||
|
selecionado = v.tracking_id;
|
||||||
|
renderCard();
|
||||||
|
renderOcorrencias(); // destaca na lista qual está aberto
|
||||||
|
cardEl.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
// ── Card ────────────────────────────────────────────────────
|
// ── Card ────────────────────────────────────────────────────
|
||||||
function renderCard() {
|
function renderCard() {
|
||||||
const v = visita;
|
const v = visita;
|
||||||
@@ -440,7 +542,12 @@
|
|||||||
if (data.campos && data.campos.length) Object.assign(visita, data.visita);
|
if (data.campos && data.campos.length) Object.assign(visita, data.visita);
|
||||||
pintarExibicao(div, def, data.campos && data.campos.length > 0);
|
pintarExibicao(div, def, data.campos && data.campos.length > 0);
|
||||||
div.classList.add('cursor-pointer');
|
div.classList.add('cursor-pointer');
|
||||||
if (def.campo === 'status') renderBadge();
|
if (def.campo === 'status') {
|
||||||
|
renderBadge();
|
||||||
|
// Mantém a lista coerente com o que acabou de ser gravado.
|
||||||
|
const o = ocorrencias.find(x => String(x.tracking_id) === String(visita.tracking_id));
|
||||||
|
if (o) { o.status = visita.status; renderOcorrencias(); }
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
msgErro.textContent = 'Erro de conexão ao salvar.';
|
msgErro.textContent = 'Erro de conexão ao salvar.';
|
||||||
msgErro.classList.remove('hidden');
|
msgErro.classList.remove('hidden');
|
||||||
|
|||||||
@@ -92,7 +92,8 @@ Rails.application.routes.draw do
|
|||||||
# Edição de lançamento do SimpliRoute (correção de status/motivo/comentário)
|
# Edição de lançamento do SimpliRoute (correção de status/motivo/comentário)
|
||||||
# — só admin. Grava direto na API do SimpliRoute + AuditoriaLog.
|
# — só admin. Grava direto na API do SimpliRoute + AuditoriaLog.
|
||||||
resource :edicao_lancamento, only: [:show], controller: 'edicao_lancamentos' do
|
resource :edicao_lancamento, only: [:show], controller: 'edicao_lancamentos' do
|
||||||
get :buscar # NF -> dados da visita (JSON)
|
get :buscar # NF -> lançamentos daquela NF (JSON; pode haver + de um)
|
||||||
|
get :carregar # visit_id -> card de um lançamento escolhido (JSON)
|
||||||
patch :atualizar # aplica a alteração na API
|
patch :atualizar # aplica a alteração na API
|
||||||
get :historico # histórico da visita na API (JSON, best-effort)
|
get :historico # histórico da visita na API (JSON, best-effort)
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user