# 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'] }, 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 def atualizar authorize :edicao_lancamento id = params[:visit_id].to_s.strip return redirect_to(admin_edicao_lancamento_path, alert: 'Visita não identificada.') if id.blank? anterior = client.visita(id) attrs = mudancas(anterior) if attrs.empty? return redirect_to(admin_edicao_lancamento_path, alert: 'Nenhuma alteração informada.') end if attrs['status'].present? && STATUS_VALIDOS.exclude?(attrs['status']) return redirect_to(admin_edicao_lancamento_path, alert: '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 ) redirect_to admin_edicao_lancamento_path, notice: "Lançamento da NF #{atualizada['reference']} atualizado no SimpliRoute. " \ 'O painel refletirá na próxima sincronização.' rescue SimpliRoute::Error => e redirect_to admin_edicao_lancamento_path, alert: "Não foi possível atualizar: #{e.message}" end # GET /admin/edicao_lancamento/historico?visit_id=123 → JSON (best-effort). def historico authorize :edicao_lancamento render json: { ok: true, historico: client.historico(params[:visit_id]) } 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 # 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