Files
Reem-Notas/app/controllers/admin/edicao_lancamentos_controller.rb

232 lines
8.1 KiB
Ruby

# 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 <img>.
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 <img src> 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