diff --git a/app/controllers/admin/edicao_lancamentos_controller.rb b/app/controllers/admin/edicao_lancamentos_controller.rb index 8cefe0e..1135c16 100644 --- a/app/controllers/admin/edicao_lancamentos_controller.rb +++ b/app/controllers/admin/edicao_lancamentos_controller.rb @@ -22,45 +22,34 @@ class Admin::EdicaoLancamentosController < ApplicationController @motivos = motivos_seguros 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 authorize :edicao_lancamento nf = params[:nf].to_s.strip return render_erro('Informe o número da NF.') if nf.blank? - entrega = Entrega.por_nf(nf).first - return render_erro("NF #{nf} não encontrada na base de rastreio.") if entrega.nil? + entregas = Entrega.por_nf(nf).to_a + return render_erro("NF #{nf} não encontrada na base de rastreio.") if entregas.empty? - id = client.resolver_id(entrega) - visita = client.visita(id) - # Segunda fonte de imagens (POD). Não é obrigatória: se falhar, vem {}. - detalhe = client.detalhe_visita(id) + ocorrencias = ocorrencias_da_nf(nf, entregas) + unica = ocorrencias.first if ocorrencias.one? && ocorrencias.first[:id].present? render json: { - ok: true, - visita: { - id: 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 - }, - motivos: motivos_seguros + ok: true, + ocorrencias: ocorrencias, + visita: (unica && payload_visita(unica[:id], entrega_de(entregas, unica[:tracking_id]))), + motivos: motivos_seguros } rescue SimpliRoute::NotFound => e render_erro(e.message) @@ -68,6 +57,23 @@ class Admin::EdicaoLancamentosController < ApplicationController render_erro("Erro ao consultar o SimpliRoute: #{e.message}", status: :bad_gateway) 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 # 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. @@ -146,6 +152,107 @@ class Admin::EdicaoLancamentosController < ApplicationController @client ||= SimpliRoute::Client.new 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 # mudaram em relação ao estado atual — PATCH mínimo, sem sobrescrever à toa. def mudancas(anterior) diff --git a/app/services/simpli_route/client.rb b/app/services/simpli_route/client.rb index 8936ad0..49c87a7 100644 --- a/app/services/simpli_route/client.rb +++ b/app/services/simpli_route/client.rb @@ -49,6 +49,11 @@ module SimpliRoute # 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. # 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) data = entrega.planned_date&.to_date raise NotFound, 'Entrega sem planned_date — impossível localizar na API.' if data.blank? diff --git a/app/views/admin/edicao_lancamentos/show.html.erb b/app/views/admin/edicao_lancamentos/show.html.erb index 2f4eccf..82cbbbe 100644 --- a/app/views/admin/edicao_lancamentos/show.html.erb +++ b/app/views/admin/edicao_lancamentos/show.html.erb @@ -4,6 +4,7 @@ /history/ da API). Os dados do card chegam via fetch (endpoint buscar). %>
Consultando SimpliRoute…

+ <%# 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. %> + + <%# Card do lançamento (preenchido via JS após a busca) %>