# app/controllers/admin/edicao_lancamentos_controller.rb # # Correção de lançamentos do SimpliRoute pelo ADM. Busca a entrega pela NF (na # base de rastreio local), resolve o id numérico na API e grava a alteração # DIRETO na API do SimpliRoute — registrando tudo no AuditoriaLog. # # ⚠️ A base de rastreio local (Entrega) é só leitura e sincroniza depois: a # alteração só reflete no painel na próxima sincronização. class Admin::EdicaoLancamentosController < ApplicationController before_action :garantir_configurado # Campos que o ADM pode editar (fase 1). checkout_observation = UUID do motivo. CAMPOS_EDITAVEIS = %w[ status checkout_observation checkout_comment notes checkout_time checkout_latitude checkout_longitude ].freeze STATUS_VALIDOS = %w[pending partial completed failed canceled].freeze def show authorize :edicao_lancamento @motivos = motivos_seguros end # GET /admin/edicao_lancamento/buscar?nf=79774 → JSON com o estado atual. 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? id = client.resolver_id(entrega) visita = client.visita(id) 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'], # Fotos do card: fachada vem do rastreio (mesma foto do mapa de operações); # pictures/assinatura vêm da API. Só URLs http(s) chegam ao . foto_fachada: url_imagem(entrega.try(:foto_da_fachada)), pictures: Array(visita['pictures']).filter_map { |u| url_imagem(u) }, signature: url_imagem(visita['signature']), motorista: entrega.driver, veiculo: entrega.vehicle }, 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. def atualizar authorize :edicao_lancamento id = params[:visit_id].to_s.strip return responder_erro('Visita não identificada.') if id.blank? anterior = client.visita(id) attrs = mudancas(anterior) # Confirmar um valor idêntico não é erro no inline edit — só não há o que fazer. if attrs.empty? return respond_to do |format| format.json { render json: { ok: true, campos: [], visita: {} } } format.html { redirect_to admin_edicao_lancamento_path, alert: 'Nenhuma alteração informada.' } end end if attrs['status'].present? && STATUS_VALIDOS.exclude?(attrs['status']) return responder_erro('Status inválido.') end atualizada = client.atualizar_visita(id, attrs) AuditoriaLog.registrar( user: current_user, acao: 'editar', entidade: 'SimpliRoute::Visita', entidade_id: id, dados_anteriores: anterior.slice(*attrs.keys), dados_novos: atualizada.slice(*attrs.keys), request: request ) respond_to do |format| format.json do render json: { ok: true, campos: attrs.keys, visita: atualizada.slice(*CAMPOS_EDITAVEIS) } end format.html do redirect_to admin_edicao_lancamento_path, notice: "Lançamento da NF #{atualizada['reference']} atualizado no SimpliRoute. " \ 'O painel refletirá na próxima sincronização.' end end rescue SimpliRoute::Error => e responder_erro("Não foi possível atualizar: #{e.message}", status: :bad_gateway) end # GET /admin/edicao_lancamento/historico?visit_id=123 → JSON com as DUAS fontes: # o histórico da API do SimpliRoute (best-effort, costuma vir vazio) e a nossa # trilha do AuditoriaLog (quem editou, quando, de→para). def historico authorize :edicao_lancamento id = params[:visit_id].to_s.strip interno = AuditoriaLog.por_entidade('SimpliRoute::Visita') .where(entidade_id: id) .includes(:user).recentes.limit(50) .map do |log| { usuario: log.user&.nome_display || '—', acao: log.acao, quando: log.created_at.in_time_zone.strftime('%d/%m/%Y %H:%M'), de: log.dados_anteriores, para: log.dados_novos } end render json: { ok: true, api: client.historico(id), interno: interno } end private def client @client ||= SimpliRoute::Client.new 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) CAMPOS_EDITAVEIS.each_with_object({}) do |campo, memo| next unless params.key?(campo) novo = normalizar(campo, params[campo]) next if equivalente?(campo, novo, anterior[campo]) memo[campo] = novo end end # Evita PATCH falso: nil/"" são equivalentes; datas comparadas por instante; # lat/long por valor numérico (o reparse difere do texto original da API). def equivalente?(campo, novo, atual) return true if novo.to_s == atual.to_s case campo when 'checkout_time' a = (Time.zone.parse(novo.to_s) rescue nil) b = (Time.zone.parse(atual.to_s) rescue nil) a.present? && b.present? && a.to_i == b.to_i when 'checkout_latitude', 'checkout_longitude' novo.is_a?(Float) && atual.present? && (novo - atual.to_f).abs < 1e-9 else false end end # Normaliza valores por campo. Campos em branco viram nil (permite LIMPAR o # motivo/comentário). checkout_time aceita datetime-local e vira ISO8601. def normalizar(campo, valor) valor = valor.to_s.strip return nil if valor.blank? case campo when 'checkout_latitude', 'checkout_longitude' Float(valor) rescue valor when 'checkout_time' (Time.zone.parse(valor)&.iso8601 rescue nil) || valor else valor end end # Lista de motivos; nunca quebra a tela se a API estiver fora. def motivos_seguros client.observations rescue SimpliRoute::Error [] end def render_erro(msg, status: :unprocessable_entity) render json: { ok: false, erro: msg }, status: status end # Erro do atualizar nos dois formatos (JSON p/ inline edit, HTML p/ fallback). def responder_erro(msg, status: :unprocessable_entity) respond_to do |format| format.json { render json: { ok: false, erro: msg }, status: status } format.html { redirect_to admin_edicao_lancamento_path, alert: msg } end end # Só aceita URL http(s) — evita injetar lixo no do card (mesmo # filtro do foto_url do OperacaoMetricas). def url_imagem(valor) url = valor.to_s.strip url.match?(%r{\Ahttps?://}i) ? url : nil end # Sem token não há o que fazer — avisa e volta. def garantir_configurado return if SimpliRoute.configurado? msg = 'Integração com o SimpliRoute não configurada (defina SIMPLIROUTE_TOKEN no servidor).' respond_to do |format| format.html { redirect_to dashboard_path, alert: msg } format.json { render json: { ok: false, erro: msg }, status: :service_unavailable } end end end