372 lines
15 KiB
Ruby
372 lines
15 KiB
Ruby
# app/controllers/consolidacao_entregas_controller.rb
|
|
class ConsolidacaoEntregasController < ApplicationController
|
|
include Auditavel
|
|
|
|
before_action :set_consolidacao
|
|
before_action :bloquear_se_finalizada, except: %i[validar revisar]
|
|
|
|
# GET /consolidacoes/:consolidacao_id/consolidacao_entregas/validar?motorista=X
|
|
# WIZARD PASSO 2 — tela de validação com toggles
|
|
def validar
|
|
authorize @consolidacao, :show?
|
|
@motorista = params[:motorista]
|
|
|
|
# Entregas atendidas do período no banco existente (read-only): sucesso +
|
|
# insucesso, pois o motorista foi ao local e a entrega precisa constar.
|
|
# Período pela DATA REAL (checkout), não pela planejada.
|
|
@entregas = Entrega.atendidas
|
|
.da_conta_gade
|
|
.no_periodo_checkout(@consolidacao.data_inicio, @consolidacao.data_fim)
|
|
.do_motorista(@motorista)
|
|
@entregas = @entregas.da_veiculo(@consolidacao.vehicle_ids) if @consolidacao.vehicle_ids.present?
|
|
@entregas = @entregas.da_operacoes(@consolidacao.operacoes) if @consolidacao.operacoes.present?
|
|
@entregas = @entregas.order(:checkout)
|
|
|
|
# Classificações já feitas — agrupadas por tracking_id (uma entrega pode ter
|
|
# vários pilares). { tracking_id => [ConsolidacaoEntrega, ...] }
|
|
@classificacoes = @consolidacao.consolidacao_entregas
|
|
.where(motorista_nome: @motorista)
|
|
.group_by(&:tracking_id)
|
|
|
|
@total_entregas = @consolidacao.entregas_elegiveis_count(@motorista)
|
|
@classificadas = @consolidacao.classificadas_count(@motorista)
|
|
|
|
# Nomes dos motoristas ATIVOS — alimenta o seletor do apontamento 100% manual.
|
|
@motoristas_consolidacao = @consolidacao.consolidacao_motoristas.ativos.pluck(:motorista_nome)
|
|
|
|
# Filtro "apenas não classificadas"
|
|
if params[:apenas_pendentes] == '1'
|
|
@entregas = @entregas.reject { |e| @classificacoes.key?(e.tracking_id) }
|
|
end
|
|
|
|
@precos = {
|
|
entrega_normal: Configuracao.preco_entrega,
|
|
retirada: Configuracao.preco_retirada,
|
|
bonus: Configuracao.preco_bonus,
|
|
desconto: Configuracao.preco_desconto,
|
|
extraordinaria: Configuracao.preco_extraordinaria
|
|
}
|
|
end
|
|
|
|
# POST /consolidacoes/:consolidacao_id/consolidacao_entregas/classificar
|
|
# Classifica UMA entrega (chamado pelo toggle via Turbo/fetch)
|
|
def classificar
|
|
authorize @consolidacao, :update?
|
|
|
|
return render_nao_elegivel unless entrega_elegivel?(params[:motorista], params[:tracking_id])
|
|
|
|
# Toggle do pilar: se já existe, remove; senão, adiciona (permite combinar
|
|
# vários pilares na mesma entrega).
|
|
ce = @consolidacao.consolidacao_entregas
|
|
.find_by(tracking_id: params[:tracking_id], tipo: params[:tipo])
|
|
if ce
|
|
ce.destroy
|
|
ativo = false
|
|
else
|
|
@consolidacao.consolidacao_entregas.create!(
|
|
tracking_id: params[:tracking_id],
|
|
motorista_nome: params[:motorista],
|
|
tipo: params[:tipo],
|
|
valor_aplicado: valor_classificacao(params[:tipo]),
|
|
created_by: current_user.id
|
|
)
|
|
ativo = true
|
|
end
|
|
|
|
recalcular_motorista(params[:motorista])
|
|
|
|
respond_to do |format|
|
|
format.json do
|
|
render json: resumo_json(params[:motorista]).merge(
|
|
ativo: ativo, tracking_id: params[:tracking_id], tipo: params[:tipo]
|
|
)
|
|
end
|
|
format.html { redirect_back fallback_location: wizard_consolidacao_path(@consolidacao) }
|
|
end
|
|
end
|
|
|
|
# POST /consolidacoes/:consolidacao_id/consolidacao_entregas/classificar_em_massa
|
|
# Ações em massa: marcar todos / marcar selecionados
|
|
def classificar_em_massa
|
|
authorize @consolidacao, :update?
|
|
|
|
tipo = params[:tipo]
|
|
motorista = params[:motorista]
|
|
tracking_ids = Array(params[:tracking_ids]).reject(&:blank?)
|
|
|
|
return render_nao_elegivel unless motorista_na_consolidacao?(motorista)
|
|
|
|
elegiveis = tracking_ids_elegiveis(motorista)
|
|
# Sem tracking_ids = todas as elegíveis do período (botão "todos").
|
|
# Com tracking_ids → mantém só as que de fato são elegíveis para o motorista.
|
|
tracking_ids = tracking_ids.empty? ? elegiveis : (tracking_ids & elegiveis)
|
|
|
|
remover = params[:acao].to_s == 'remover'
|
|
|
|
ActiveRecord::Base.transaction do
|
|
if remover
|
|
# Desmarca o pilar das entregas selecionadas (mantém os demais pilares).
|
|
@consolidacao.consolidacao_entregas
|
|
.where(motorista_nome: motorista, tipo: tipo, tracking_id: tracking_ids)
|
|
.destroy_all
|
|
else
|
|
# ADICIONA o pilar às entregas (não remove os demais já marcados).
|
|
valor = valor_classificacao(tipo)
|
|
tracking_ids.each do |tid|
|
|
ce = @consolidacao.consolidacao_entregas.find_or_initialize_by(tracking_id: tid, tipo: tipo)
|
|
ce.assign_attributes(motorista_nome: motorista,
|
|
valor_aplicado: valor, created_by: current_user.id)
|
|
ce.save!
|
|
end
|
|
end
|
|
end
|
|
|
|
recalcular_motorista(motorista)
|
|
auditar!(:editar, @consolidacao,
|
|
dados_novos: { acao_massa: tipo, modo: (remover ? 'remover' : 'adicionar'),
|
|
qtd: tracking_ids.size, motorista: motorista })
|
|
|
|
respond_to do |format|
|
|
format.json { render json: resumo_json(motorista) }
|
|
format.html do
|
|
verbo = remover ? 'desmarcadas de' : 'marcadas como'
|
|
redirect_to validar_consolidacao_consolidacao_entregas_path(@consolidacao, motorista: motorista),
|
|
notice: "#{tracking_ids.size} entregas #{verbo} #{tipo.humanize}."
|
|
end
|
|
end
|
|
end
|
|
|
|
# GET /consolidacoes/:consolidacao_id/consolidacao_entregas/revisar?motorista=X
|
|
# WIZARD PASSO 3 — revisão do motorista
|
|
def revisar
|
|
authorize @consolidacao, :show?
|
|
@motorista = params[:motorista]
|
|
|
|
todas = @consolidacao.consolidacao_entregas.where(motorista_nome: @motorista)
|
|
|
|
# Agregados calculados sobre o conjunto COMPLETO (não só a página exibida)
|
|
@resumo = todas.group(:tipo).count
|
|
@total = todas.sum do |c|
|
|
c.tipo == 'desconto' ? -c.valor_aplicado : c.valor_aplicado
|
|
end
|
|
@total_pilares = todas.count
|
|
@total_classificadas = todas.distinct.count(:tracking_id)
|
|
|
|
# Paginação só da tabela exibida — 20 padrão, com seletor 20/50/100
|
|
@por_pagina = [20, 50, 100].include?(params[:por_pagina].to_i) ? params[:por_pagina].to_i : 20
|
|
begin
|
|
@pagy, @classificacoes = pagy(todas.order(:created_at), limit: @por_pagina)
|
|
rescue Pagy::OverflowError
|
|
@pagy, @classificacoes = pagy(todas.order(:created_at), limit: @por_pagina, page: 1)
|
|
end
|
|
|
|
# Próximo motorista pendente (para botão "Próximo Motorista") — só ativos
|
|
nomes_completos = @consolidacao.consolidacao_motoristas.ativos.map(&:motorista_nome)
|
|
@proximo = nomes_completos.find do |nome|
|
|
next false if nome == @motorista
|
|
total = @consolidacao.entregas_elegiveis_count(nome)
|
|
@consolidacao.classificadas_count(nome) < total
|
|
end
|
|
end
|
|
|
|
# GET /consolidacoes/:consolidacao_id/consolidacao_entregas/buscar_nf?nf=123
|
|
# APONTAMENTO MANUAL — busca a NF na SimpleRoute (sem filtro de período).
|
|
def buscar_nf
|
|
authorize @consolidacao, :update?
|
|
|
|
nf = params[:nf].to_s.strip
|
|
return render(json: { ok: false, erro: 'Informe o número da nota.' }, status: :unprocessable_entity) if nf.blank?
|
|
|
|
entregas = Entrega.da_conta_gade.por_nf(nf).order(:checkout)
|
|
|
|
if entregas.empty?
|
|
render json: { ok: false, erro: "Nota #{nf} não encontrada na base de rastreio." }
|
|
else
|
|
render json: { ok: true, nf: nf, entregas: entregas.map(&:resumo_apontamento) }
|
|
end
|
|
end
|
|
|
|
# POST /consolidacoes/:consolidacao_id/consolidacao_entregas/apontar
|
|
# Cria um apontamento manual a partir de um tracking_id encontrado pela NF.
|
|
# Aceita um ou vários pilares (tipos[]) na mesma entrega.
|
|
def apontar
|
|
authorize @consolidacao, :update?
|
|
|
|
entrega = Entrega.da_conta_gade.find_by(tracking_id: params[:tracking_id])
|
|
return render_apontamento_erro('Entrega não encontrada na base de rastreio.') unless entrega
|
|
|
|
motorista = @consolidacao.adicionar_apontamento!(entrega, tipos_param, current_user)
|
|
auditar!(:editar, @consolidacao,
|
|
dados_novos: { apontamento_manual: entrega.numero_nf, tipos: tipos_param, motorista: motorista })
|
|
|
|
respond_to do |format|
|
|
format.json { render json: resumo_json(motorista).merge(motorista: motorista) }
|
|
format.html do
|
|
redirect_to validar_consolidacao_consolidacao_entregas_path(@consolidacao, motorista: motorista),
|
|
notice: "Apontamento da nota #{entrega.numero_nf} adicionado para #{motorista}."
|
|
end
|
|
end
|
|
rescue ArgumentError => e
|
|
render_apontamento_erro(e.message)
|
|
end
|
|
|
|
# POST /consolidacoes/:consolidacao_id/consolidacao_entregas/apontar_manual
|
|
# Apontamento 100% MANUAL — não consulta a base de rastreio. O admin informa o
|
|
# motorista (pode ser outro que não o da tela), a NF (texto livre), uma
|
|
# observação/local e os pilares. Usado quando o motorista foi ao local mas a
|
|
# NF ficou registrada com outro motorista no rastreio.
|
|
def apontar_manual
|
|
authorize @consolidacao, :update?
|
|
|
|
motorista = @consolidacao.adicionar_apontamento_manual!(
|
|
motorista: params[:motorista].to_s.strip,
|
|
tipos: tipos_param,
|
|
nf: params[:nf],
|
|
obs: params[:obs],
|
|
valor_extra: valor_extraordinaria_param,
|
|
user: current_user
|
|
)
|
|
auditar!(:editar, @consolidacao,
|
|
dados_novos: { apontamento_100_manual: params[:nf], motorista: motorista, tipos: tipos_param })
|
|
|
|
respond_to do |format|
|
|
format.json { render json: resumo_json(motorista).merge(motorista: motorista) }
|
|
format.html do
|
|
redirect_to validar_consolidacao_consolidacao_entregas_path(@consolidacao, motorista: motorista),
|
|
notice: "Apontamento manual adicionado para #{motorista}."
|
|
end
|
|
end
|
|
rescue ArgumentError => e
|
|
render_apontamento_erro(e.message)
|
|
end
|
|
|
|
# DELETE /consolidacoes/:consolidacao_id/consolidacao_entregas/remover_apontamento
|
|
# Remove um apontamento manual — por ?id= (um pilar) ou por ?tracking_id=&motorista=
|
|
# (todos os pilares daquela nota). Não passa pelo guard de elegibilidade, pois o
|
|
# tracking está fora do período (por isso não reusa o toggle classificar).
|
|
def remover_apontamento
|
|
authorize @consolidacao, :update?
|
|
|
|
scope = @consolidacao.consolidacao_entregas.apontamentos
|
|
registros = if params[:id].present?
|
|
Array(scope.find_by(id: params[:id]))
|
|
else
|
|
scope.where(tracking_id: params[:tracking_id], motorista_nome: params[:motorista]).to_a
|
|
end
|
|
return render_apontamento_erro('Apontamento não encontrado.') if registros.empty?
|
|
|
|
motorista = registros.first.motorista_nome
|
|
tracking = registros.first.tracking_id
|
|
registros.each(&:destroy)
|
|
@consolidacao.recalcular_motorista!(motorista)
|
|
auditar!(:editar, @consolidacao,
|
|
dados_novos: { remover_apontamento_manual: tracking, motorista: motorista })
|
|
|
|
respond_to do |format|
|
|
format.json { render json: resumo_json(motorista).merge(motorista: motorista) }
|
|
format.html { redirect_back fallback_location: consolidacao_path(@consolidacao), notice: 'Apontamento removido.' }
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
def set_consolidacao
|
|
@consolidacao = Consolidacao.find(params[:consolidacao_id])
|
|
end
|
|
|
|
def bloquear_se_finalizada
|
|
return unless @consolidacao.finalizada? && current_user.operador?
|
|
redirect_to @consolidacao, alert: 'Consolidação finalizada — apenas admin/gerente podem editar.'
|
|
end
|
|
|
|
# Motorista realmente faz parte desta consolidação? (ignora arquivados)
|
|
def motorista_na_consolidacao?(nome)
|
|
nome.present? && @consolidacao.consolidacao_motoristas.ativos.exists?(motorista_nome: nome)
|
|
end
|
|
|
|
# tracking_ids atendidos/elegíveis para o motorista no período (respeita veículos).
|
|
# Atendidas = sucesso + insucesso com checkout (o motorista foi ao local).
|
|
def tracking_ids_elegiveis(motorista)
|
|
return [] unless motorista.present?
|
|
|
|
rel = Entrega.atendidas.da_conta_gade
|
|
.no_periodo_checkout(@consolidacao.data_inicio, @consolidacao.data_fim)
|
|
.do_motorista(motorista)
|
|
rel = rel.da_veiculo(@consolidacao.vehicle_ids) if @consolidacao.vehicle_ids.present?
|
|
rel = rel.da_operacoes(@consolidacao.operacoes) if @consolidacao.operacoes.present?
|
|
rel.pluck(:tracking_id)
|
|
end
|
|
|
|
# Valida que a entrega pertence ao motorista informado e ao período/veículo da
|
|
# consolidação — impede classificar tracking_ids arbitrários via requisição.
|
|
def entrega_elegivel?(motorista, tracking_id)
|
|
motorista_na_consolidacao?(motorista) &&
|
|
tracking_id.present? &&
|
|
tracking_ids_elegiveis(motorista).include?(tracking_id)
|
|
end
|
|
|
|
# Pilares (tipos) selecionados no modal — aceita tipos[] (vários) ou tipo (um).
|
|
def tipos_param
|
|
tipos = Array(params[:tipos]).presence || Array(params[:tipo])
|
|
tipos.reject(&:blank?)
|
|
end
|
|
|
|
def render_apontamento_erro(msg)
|
|
respond_to do |format|
|
|
format.json { render json: { ok: false, erro: msg }, status: :unprocessable_entity }
|
|
format.html { redirect_to wizard_consolidacao_path(@consolidacao), alert: msg }
|
|
end
|
|
end
|
|
|
|
def render_nao_elegivel
|
|
respond_to do |format|
|
|
format.json { render json: { ok: false, erro: 'Entrega não elegível para este motorista.' }, status: :unprocessable_entity }
|
|
format.html do
|
|
redirect_to validar_consolidacao_consolidacao_entregas_path(@consolidacao, motorista: params[:motorista]),
|
|
alert: 'Entrega não elegível para este motorista.'
|
|
end
|
|
end
|
|
end
|
|
|
|
def valor_para_tipo(tipo)
|
|
ConsolidacaoEntrega.valor_para(tipo)
|
|
end
|
|
|
|
# Valor a aplicar na classificação. Para o pilar "extraordinária" o admin pode
|
|
# informar um valor customizado (params[:valor]) por ser uma exceção; se vier
|
|
# ausente/inválido, cai no preço padrão de Configurações. Demais pilares usam
|
|
# sempre o preço configurado.
|
|
def valor_classificacao(tipo)
|
|
return valor_para_tipo(tipo) unless tipo.to_s == 'extraordinaria'
|
|
|
|
valor = params[:valor].to_s.strip.tr(',', '.')
|
|
valor = BigDecimal(valor) rescue nil
|
|
valor.present? && valor >= 0 ? valor : Configuracao.preco_extraordinaria
|
|
end
|
|
|
|
# Valor customizado da Extraordinária vindo do formulário (params[:valor]),
|
|
# aceitando vírgula decimal. Retorna nil se ausente/inválido — aí o model usa
|
|
# o preço padrão de Configurações.
|
|
def valor_extraordinaria_param
|
|
bruto = params[:valor].to_s.strip.tr(',', '.')
|
|
return nil if bruto.blank?
|
|
|
|
valor = BigDecimal(bruto) rescue nil
|
|
valor if valor && valor >= 0
|
|
end
|
|
|
|
def recalcular_motorista(nome)
|
|
@consolidacao.recalcular_motorista!(nome)
|
|
end
|
|
|
|
def resumo_json(motorista)
|
|
classificacoes = @consolidacao.consolidacao_entregas.where(motorista_nome: motorista)
|
|
{
|
|
ok: true,
|
|
classificadas: classificacoes.distinct.count(:tracking_id),
|
|
por_tipo: classificacoes.group(:tipo).count,
|
|
valor_total: classificacoes.sum { |e| e.tipo == 'desconto' ? -e.valor_aplicado : e.valor_aplicado }.to_f
|
|
}
|
|
end
|
|
end
|