Files
Reem-Notas/app/controllers/consolidacoes_controller.rb

620 lines
26 KiB
Ruby
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# app/controllers/consolidacoes_controller.rb
class ConsolidacoesController < ApplicationController
include Auditavel
before_action :set_consolidacao,
only: %i[show edit update destroy finalizar finalizar_parcial arquivar reativar wizard
registrar_pagamento cancelar_pagamento
arquivar_motorista desarquivar_motorista
excluir_motorista adicionar_motorista
preview_extrato gerar_pdf_relatorio gerar_pdf_extrato
gerar_pdf_financeiro]
# Formas de pagamento aceitas (o select da UI usa esta mesma lista).
FORMAS_PAGAMENTO = %w[pix transferencia dinheiro].freeze
# GET /consolidacoes?visao=lista|motoristas — a MESMA tela em dois modos de
# visualização, com os mesmos filtros:
#
# lista → um card por CONSOLIDAÇÃO (o de sempre);
# motoristas → um card por MOTORISTA, com o total dele no período e, ao
# clicar, as consolidações que compõem aquele valor.
#
# São duas leituras do mesmo recorte: o trabalho é organizado por consolidação,
# mas o pagamento é por motorista — antes, para saber quanto um motorista tinha
# fechado no mês, era preciso abrir consolidação por consolidação e somar à mão.
def index
authorize Consolidacao
@visao = params[:visao] == 'motoristas' ? 'motoristas' : 'lista'
@consolidacoes = Consolidacao.ativas.recentes.includes(:criador)
# Filtros
@consolidacoes = @consolidacoes.where(status: params[:status]) if params[:status].present?
@consolidacoes = @consolidacoes.where('nome ILIKE ?', "%#{params[:nome]}%") if params[:nome].present?
# Datas: a consolidação entra se CRUZA a faixa — mesmo critério do modo "Por
# motorista" e do dashboard financeiro. Com o critério antigo (caber inteira
# dentro do filtro), um fechamento de 01/08 a 31/08 sumia da lista ao filtrar
# 01/0826/08 e as duas abas da MESMA tela contavam coisas diferentes.
if params[:inicio].present? && params[:fim].present?
@consolidacoes = @consolidacoes.cruzando_periodo(params[:inicio], params[:fim])
elsif params[:inicio].present?
@consolidacoes = @consolidacoes.where('data_fim >= ?', params[:inicio])
elsif params[:fim].present?
@consolidacoes = @consolidacoes.where('data_inicio <= ?', params[:fim])
end
if params[:motorista].present?
ids = ConsolidacaoMotorista.where('motorista_nome ILIKE ?', "%#{params[:motorista]}%")
.pluck(:consolidacao_id)
@consolidacoes = @consolidacoes.where(id: ids)
end
if params[:vehicle].present?
@consolidacoes = @consolidacoes.where('vehicle_ids @> ?', [params[:vehicle]].to_json)
end
if %w[pago pendente parcial].include?(params[:pagamento])
@consolidacoes = @consolidacoes.public_send("pagamento_#{params[:pagamento]}")
end
montar_totais if @visao == 'motoristas'
end
# GET /consolidacoes/totais_pdf — o modo "Por motorista" em PDF, com os mesmos
# filtros da tela. Com ?detalhe=NOME sai só o extrato daquele motorista (com
# espaço de assinatura).
def totais_pdf
authorize Consolidacao, :index?
inicio, fim = periodo_totais
totais = Analytics::TotaisPorMotorista.new(
inicio: inicio, fim: fim,
motorista: params[:detalhe].presence,
status: params[:status].presence || 'finalizada',
busca: params[:motorista].presence
)
nome = params[:detalhe].presence&.parameterize(separator: '_')
arquivo = ['totais', nome, inicio.strftime('%Y%m%d'), fim.strftime('%Y%m%d')].compact.join('_')
send_data Pdf::TotaisMotoristaPdf.new(totais: totais, detalhar: params[:detalhar] != '0').render,
filename: "#{arquivo}.pdf",
type: 'application/pdf',
disposition: 'attachment'
end
# GET /consolidacoes/new
def new
authorize Consolidacao
@consolidacao = Consolidacao.new(
data_inicio: Date.current.beginning_of_month,
data_fim: Date.current
)
@motoristas = Entrega.motoristas_ativos
@veiculos = Entrega.veiculos_unicos
@operacoes = Operacao.todas
end
# POST /consolidacoes
def create
authorize Consolidacao
@consolidacao = Consolidacao.new(consolidacao_params.merge(created_by: current_user.id))
@consolidacao.operacoes = Operacao.sanitizar(@consolidacao.operacoes)
motoristas = Array(params[:motoristas]).reject(&:blank?)
if motoristas.empty?
@consolidacao.errors.add(:base, 'Selecione ao menos um motorista')
preparar_form_new
return render :new, status: :unprocessable_entity
end
ActiveRecord::Base.transaction do
@consolidacao.save!
motoristas.each do |nome|
@consolidacao.consolidacao_motoristas.create!(motorista_nome: nome)
end
end
auditar!(:criar, @consolidacao, dados_novos: @consolidacao.attributes)
redirect_to wizard_consolidacao_path(@consolidacao), notice: 'Consolidação criada! Selecione o motorista para validar.'
rescue ActiveRecord::RecordInvalid
preparar_form_new
render :new, status: :unprocessable_entity
end
# GET /consolidacoes/veiculos_disponiveis?motoristas[]=X&inicio=&fim=
# Lista (JSON) os veículos usados pelos motoristas selecionados no período —
# alimenta o filtro dinâmico de veículos no formulário de nova consolidação.
def veiculos_disponiveis
authorize Consolidacao, :new?
veiculos = Entrega.veiculos_de_motoristas(
params[:motoristas], inicio: params[:inicio], fim: params[:fim]
)
render json: { veiculos: veiculos }
end
# GET /consolidacoes/operacoes_disponiveis — lista (JSON) as tabelas de operação.
def operacoes_disponiveis
authorize Consolidacao, :new?
render json: { operacoes: Operacao.todas }
end
# GET /consolidacoes/dados_da_operacao?operacoes[]=X — motoristas/veículos/período
# derivados das operações selecionadas, para pré-preencher o formulário.
def dados_da_operacao
authorize Consolidacao, :new?
render json: Operacao.dados(params[:operacoes])
end
# GET /consolidacoes/buscar_nf?nf=123 — busca a NF na SimpleRoute (nota avulsa).
def buscar_nf
authorize Consolidacao, :create?
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/avulsa — cria uma consolidação JÁ a partir de uma NF
# avulsa (entrega que chegou por fora) e adiciona o apontamento ao motorista.
def avulsa
authorize Consolidacao, :create?
entrega = Entrega.da_conta_gade.find_by(tracking_id: params[:tracking_id])
return redirect_to(consolidacoes_path, alert: 'Entrega não encontrada na base de rastreio.') unless entrega
data_ref = (entrega.checkout || entrega.planned_date)&.to_date || Date.current
consolidacao = nil
ActiveRecord::Base.transaction do
consolidacao = Consolidacao.create!(
nome: "Avulsa nf#{entrega.numero_nf}",
data_inicio: data_ref,
data_fim: data_ref.end_of_month,
created_by: current_user.id
)
consolidacao.adicionar_apontamento!(entrega, Array(params[:tipos]).reject(&:blank?), current_user)
end
auditar!(:criar, consolidacao,
dados_novos: { avulsa_nf: entrega.numero_nf, motorista: entrega.driver, tipos: params[:tipos] })
redirect_to consolidacao_path(consolidacao),
notice: "Consolidação avulsa criada para #{entrega.driver} (NF #{entrega.numero_nf})."
rescue ArgumentError => e
redirect_to consolidacoes_path, alert: e.message
rescue ActiveRecord::RecordInvalid => e
redirect_to consolidacoes_path, alert: "Não foi possível criar a consolidação avulsa: #{e.message}"
end
# GET /consolidacoes/:id — visão geral
def show
authorize @consolidacao
# A gestão de arquivamento de motorista fica no wizard; aqui listamos só os ativos.
@motoristas = @consolidacao.consolidacao_motoristas.ativos.order(:motorista_nome)
# Apontamentos manuais agrupados por nota (tracking) p/ exibição e remoção.
@apontamentos = @consolidacao.consolidacao_entregas.apontamentos
.order(:motorista_nome, :tracking_id)
.group_by { |ce| [ce.motorista_nome, ce.tracking_id] }
# Veículos consolidados por motorista (breakdown informativo no card).
@veiculos_por_motorista = @consolidacao.consolidacao_veiculos.ativos
.order(:vehicle)
.group_by(&:motorista_nome)
end
# GET /consolidacoes/:id/wizard — PASSO 1: selecionar motorista
def wizard
authorize @consolidacao, :update?
@arquivados = @consolidacao.consolidacao_motoristas.arquivados.order(:motorista_nome)
# Alimenta o datalist do "Adicionar motorista": motoristas PREVISTOS no período
# da consolidação (atendidas, respeitando filtros de veículo/operação) e os
# veículos que cada um usou — para o usuário escolher com contexto.
@motoristas_periodo = motoristas_previstos_no_periodo
@motoristas = @consolidacao.consolidacao_motoristas.ativos.order(:motorista_nome).map do |cm|
total = @consolidacao.entregas_elegiveis_count(cm.motorista_nome)
classificadas = @consolidacao.classificadas_count(cm.motorista_nome)
apontamentos = @consolidacao.consolidacao_entregas
.apontamentos
.where(motorista_nome: cm.motorista_nome)
.distinct.count(:tracking_id)
# Status por veículo (breakdown do card): total usados, consolidados (🔒),
# pendentes (não dá p/ consolidar ainda) e prontos (100%, falta consolidar).
vinfo = @consolidacao.veiculos_do_motorista(cm.motorista_nome).reject { |v| v[:arquivado] }
{
registro: cm,
total: total,
classificadas: classificadas,
apontamentos: apontamentos,
veiculos_total: vinfo.size,
veiculos_fechados: vinfo.count { |v| v[:fechado] },
veiculos_prontos: vinfo.count { |v| !v[:fechado] && v[:completo] },
veiculos_pendentes: vinfo.select { |v| !v[:fechado] && !v[:completo] && v[:eligible].positive? }
.map { |v| v[:vehicle] },
completo: @consolidacao.motorista_completo?(cm.motorista_nome)
}
end
end
# POST /consolidacoes/:id/finalizar
def finalizar
authorize @consolidacao, :update?
if @consolidacao.finalizar!(current_user)
auditar!(:finalizar, @consolidacao)
NotificacaoService.notificar_finalizacao(@consolidacao) # WhatsApp/email aos motoristas
redirect_to @consolidacao, notice: 'Consolidação finalizada! Motoristas notificados.'
else
redirect_to wizard_consolidacao_path(@consolidacao),
alert: 'Não é possível finalizar: ainda há entregas não classificadas. Classifique-as ou arquive o veículo pendente.'
end
end
# POST /consolidacoes/:id/finalizar_parcial
# Atalho "finalizar só os veículos consolidados": arquiva os veículos que ainda
# não dá para consolidar (não 100% classificados) e finaliza a consolidação. Os
# veículos consolidados/prontos seguem no fechamento; os pendentes ficam de fora.
def finalizar_parcial
authorize @consolidacao, :update?
arquivados = @consolidacao.arquivar_veiculos_pendentes!(current_user)
if @consolidacao.finalizar!(current_user)
auditar!(:finalizar, @consolidacao, dados_novos: { finalizar_parcial: true, veiculos_arquivados: arquivados })
NotificacaoService.notificar_finalizacao(@consolidacao)
redirect_to @consolidacao,
notice: "Consolidação finalizada. #{arquivados.size} veículo(s) pendente(s) ficaram de fora. Motoristas notificados."
else
redirect_to wizard_consolidacao_path(@consolidacao),
alert: 'Não foi possível finalizar: ainda há entregas pendentes fora dos veículos arquivados.'
end
end
# POST /consolidacoes/:id/arquivar
def arquivar
authorize @consolidacao, :destroy?
@consolidacao.arquivar!(current_user)
auditar!(:arquivar, @consolidacao)
redirect_to consolidacoes_path, notice: 'Consolidação arquivada.'
end
# GET /consolidacoes/arquivadas — histórico de consolidações arquivadas
def arquivadas
authorize Consolidacao
@consolidacoes = Consolidacao.arquivadas.recentes.includes(:criador, :arquivador)
end
# POST /consolidacoes/:id/reativar — restaura uma consolidação arquivada
def reativar
authorize @consolidacao, :destroy?
@consolidacao.reativar!(current_user)
auditar!(:reativar, @consolidacao)
redirect_to consolidacoes_path, notice: 'Consolidação reativada.'
end
# DELETE /consolidacoes/:id — exclui PERMANENTEMENTE (apenas arquivadas)
def destroy
authorize @consolidacao, :destroy?
@consolidacao.destroy!
auditar!(:excluir, @consolidacao)
redirect_to arquivadas_consolidacoes_path, notice: 'Consolidação excluída permanentemente.'
end
# POST /consolidacoes/:id/registrar_pagamento
# Marca 1 motorista (param consolidacao_motorista_id) ou TODOS os pendentes.
def registrar_pagamento
authorize @consolidacao, :registrar_pagamento?
return pagamento_indisponivel unless @consolidacao.finalizada?
forma = params[:forma_pagamento].presence_in(FORMAS_PAGAMENTO) || 'pix'
alvos = motoristas_alvo.select { |cm| !cm.pago? }
# NF obrigatória por motorista. Se faltar para qualquer alvo, aborta sem
# marcar nenhum (a NF vai pro painel do motorista e pro Extrato).
notas = nf_por_motorista(alvos)
if notas.nil?
return redirect_to @consolidacao,
alert: 'Informe o número da Nota Fiscal de cada motorista antes de marcar como pago.'
end
alvos.each do |cm|
cm.marcar_pago!(current_user, forma: forma, nota_fiscal: notas[cm.id])
NotificacaoService.notificar_pagamento(@consolidacao, cm)
end
auditar!(:registrar_pagamento, @consolidacao,
dados_novos: { motoristas: alvos.map(&:motorista_nome), forma: forma,
notas_fiscais: alvos.map { |cm| [cm.motorista_nome, notas[cm.id]] }.to_h })
redirect_to @consolidacao, notice: pagamento_notice(alvos.size, :pago)
end
# POST /consolidacoes/:id/cancelar_pagamento — estorna 1 motorista ou TODOS.
def cancelar_pagamento
authorize @consolidacao, :cancelar_pagamento?
alvos = motoristas_alvo.select(&:pago?)
alvos.each { |cm| cm.cancelar_pagamento!(current_user) }
auditar!(:cancelar_pagamento, @consolidacao,
dados_novos: { motoristas: alvos.map(&:motorista_nome) })
redirect_to @consolidacao, notice: pagamento_notice(alvos.size, :estornado)
end
# POST /consolidacoes/:id/arquivar_motorista — arquiva 1 motorista da
# consolidação (reversível). Ele sai dos totais/pagamento/gate, mas os dados
# ficam guardados e podem ser restaurados. Não arquiva motorista já pago.
def arquivar_motorista
authorize @consolidacao, :update?
cm = @consolidacao.consolidacao_motoristas.find(params[:consolidacao_motorista_id])
if cm.pago?
return redirect_to wizard_consolidacao_path(@consolidacao),
alert: "#{cm.motorista_nome} já está pago — estorne o pagamento antes de arquivar."
end
cm.arquivar!(current_user)
@consolidacao.recalcular_motorista!(cm.motorista_nome) # tira o valor do total
auditar!(:editar, @consolidacao, dados_novos: { arquivar_motorista: cm.motorista_nome })
redirect_to wizard_consolidacao_path(@consolidacao),
notice: "#{cm.motorista_nome} arquivado. Você pode restaurar quando quiser."
end
# POST /consolidacoes/:id/desarquivar_motorista — restaura um motorista arquivado.
def desarquivar_motorista
authorize @consolidacao, :update?
cm = @consolidacao.consolidacao_motoristas.find(params[:consolidacao_motorista_id])
cm.desarquivar!(current_user)
@consolidacao.recalcular_motorista!(cm.motorista_nome) # devolve o valor ao total
auditar!(:editar, @consolidacao, dados_novos: { desarquivar_motorista: cm.motorista_nome })
redirect_to wizard_consolidacao_path(@consolidacao),
notice: "#{cm.motorista_nome} restaurado para a consolidação."
end
# DELETE /consolidacoes/:id/excluir_motorista — apaga de vez 1 motorista desta
# consolidação (só admin/gerente). Caminho destrutivo (o reversível é arquivar):
# como consolidacao_entregas se liga ao motorista por STRING (motorista_nome,
# sem FK/cascade), apagamos os lançamentos dele à mão antes de remover o registro.
def excluir_motorista
authorize @consolidacao, :destroy?
cm = @consolidacao.consolidacao_motoristas.find(params[:consolidacao_motorista_id])
if cm.pago?
return redirect_to wizard_consolidacao_path(@consolidacao),
alert: "#{cm.motorista_nome} já está pago — estorne o pagamento antes de excluir."
end
nome = cm.motorista_nome
ActiveRecord::Base.transaction do
@consolidacao.consolidacao_entregas.where(motorista_nome: nome).delete_all
@consolidacao.consolidacao_veiculos.where(motorista_nome: nome).delete_all
cm.destroy!
end
# cm já não existe — atualiza o total da consolidação direto (soma dos ativos).
@consolidacao.update_column(:valor_total, @consolidacao.consolidacao_motoristas.ativos.sum(:valor_total))
auditar!(:editar, @consolidacao, dados_novos: { excluir_motorista: nome })
redirect_to wizard_consolidacao_path(@consolidacao),
notice: "#{nome} excluído desta consolidação."
end
# POST /consolidacoes/:id/adicionar_motorista — inclui um motorista nesta
# consolidação (ou restaura, se estava arquivado). Útil para refazer rápido
# depois de excluir/arquivar por erro de precificação.
def adicionar_motorista
authorize @consolidacao, :update?
nome = params[:motorista_nome].to_s.strip
if nome.blank?
return redirect_to wizard_consolidacao_path(@consolidacao), alert: 'Informe o nome do motorista.'
end
cm = @consolidacao.consolidacao_motoristas.find_or_initialize_by(motorista_nome: nome)
aviso =
if cm.persisted? && cm.arquivado?
cm.desarquivar!(current_user)
"#{nome} restaurado para a consolidação."
elsif cm.persisted?
"#{nome} já está nesta consolidação."
else
cm.save!
"#{nome} adicionado à consolidação."
end
@consolidacao.recalcular_motorista!(nome)
auditar!(:editar, @consolidacao, dados_novos: { adicionar_motorista: nome })
redirect_to wizard_consolidacao_path(@consolidacao), notice: aviso
end
# ── FASE 7 — PDFs ────────────────────────────────────────────
# GET /consolidacoes/:id/gerar_pdf_relatorio?motorista=X
def gerar_pdf_relatorio
authorize @consolidacao, :show?
motorista = params[:motorista]
pdf = Pdf::RelatorioMotoristaPdf.new(consolidacao: @consolidacao, motorista: motorista)
auditar!(:editar, @consolidacao, dados_novos: { pdf: 'relatorio', motorista: motorista })
send_data pdf.render,
filename: "relatorio_#{motorista.parameterize}_#{@consolidacao.id}.pdf",
type: 'application/pdf',
disposition: 'attachment'
end
# GET /consolidacoes/:id/gerar_pdf_financeiro — relatório financeiro da consolidação
def gerar_pdf_financeiro
authorize @consolidacao, :show?
pdf = Pdf::RelatorioFinanceiroConsolidacaoPdf.new(consolidacao: @consolidacao)
auditar!(:editar, @consolidacao, dados_novos: { pdf: 'financeiro' })
send_data pdf.render,
filename: "financeiro_#{@consolidacao.nome.parameterize}_#{@consolidacao.id}.pdf",
type: 'application/pdf',
disposition: 'attachment'
end
# GET /consolidacoes/:id/gerar_pdf_extrato?motorista=X
def gerar_pdf_extrato
autorizar_extrato!(params[:motorista])
motorista = params[:motorista]
user_mot = buscar_user_motorista(motorista)
user_mot&.regenerar_login_token! if user_mot&.login_token.blank?
cm = @consolidacao.consolidacao_motoristas.find_by(motorista_nome: motorista)
pdf = Pdf::ExtratoPdf.new(consolidacao: @consolidacao, motorista: motorista,
user_motorista: user_mot, app_host: ENV['APP_HOST'],
pago_em: cm&.pago_em, forma_pagamento: cm&.forma_pagamento,
nota_fiscal: cm&.nota_fiscal)
auditar!(:editar, @consolidacao, dados_novos: { pdf: 'extrato', motorista: motorista })
send_data pdf.render,
filename: "extrato_#{motorista.parameterize}_#{@consolidacao.id}.pdf",
type: 'application/pdf',
disposition: 'attachment'
end
# GET /consolidacoes/:id/preview_extrato?motorista=X — preview HTML antes do PDF
def preview_extrato
authorize @consolidacao, :show?
@motorista = params[:motorista]
@entregas = @consolidacao.consolidacao_entregas.where(motorista_nome: @motorista)
# sum(:quantidade) (default 1) conta os termos lançados em lote como N entregas.
@resumo = @entregas.group(:tipo).sum(:quantidade)
@total = @entregas.sum { |e| e.desconto? ? -e.valor_aplicado : e.valor_aplicado }
render layout: false # renderiza dentro do modal (Turbo Frame)
end
private
# Modo "Por motorista": mesmo período/busca da lista, mas o recorte de datas e
# o default de status são os do DASHBOARD FINANCEIRO — é com ele que o operador
# compara o número (ver Analytics::TotaisPorMotorista). Sem status na URL vale
# "Consolidado" (só finalizadas): "Geral" (com rascunhos) mostrava um valor
# maior que o do dashboard e parecia erro de conta.
# `detalhe` é o motorista aberto (o clique na linha).
def montar_totais
@inicio, @fim = periodo_totais
@status_totais = params[:status].presence || 'finalizada'
@totais = Analytics::TotaisPorMotorista.new(
inicio: @inicio, fim: @fim,
status: @status_totais,
busca: params[:motorista].presence
)
@detalhe_motorista = params[:detalhe].presence
@detalhe = @detalhe_motorista ? @totais.consolidacoes_de(@detalhe_motorista) : nil
end
# Período do relatório de totais. Default: o MÊS CORRENTE INTEIRO (o mês é a
# unidade de fechamento; "até hoje" recortaria o mês pela metade sem motivo).
def periodo_totais
inicio = parse_data_totais(params[:inicio]) || Date.current.beginning_of_month
fim = parse_data_totais(params[:fim]) || Date.current.end_of_month
inicio, fim = fim, inicio if fim < inicio
[inicio, fim]
end
def parse_data_totais(str)
return nil if str.blank?
Date.parse(str)
rescue ArgumentError, TypeError
nil
end
# Acha o User motorista pelo nome para gerar o QR Code de acesso. O
# motorista_nome vem dos dados externos de entrega e o users.nome é digitado
# no admin — então toleramos diferenças de caixa e de espaços (extras/duplos).
# Sem isso, qualquer divergência deixava user_mot nil e o QR sumia do extrato.
def buscar_user_motorista(nome)
exato = User.motorista.find_by('LOWER(nome) = ?', nome.to_s.downcase.strip)
return exato if exato
alvo = normalizar_nome(nome)
User.motorista.find { |u| normalizar_nome(u.nome) == alvo }
end
def normalizar_nome(nome)
nome.to_s.strip.downcase.gsub(/\s+/, ' ')
end
# Staff (show?) baixa qualquer extrato. O motorista baixa só o PRÓPRIO — o PDF
# já é filtrado pelo nome dele, então não há vazamento de dados de terceiros.
def autorizar_extrato!(motorista_nome)
if current_user.motorista?
raise Pundit::NotAuthorizedError unless motorista_nome.to_s.casecmp?(current_user.nome.to_s)
skip_authorization
else
authorize @consolidacao, :show?
end
end
def set_consolidacao
@consolidacao = Consolidacao.find(params[:id])
end
# Motoristas com entregas atendidas no período da consolidação (respeitando os
# filtros de veículo/operação dela), com os veículos que cada um usou. Alimenta
# o datalist de "Adicionar motorista". Retorna [{ nome:, veiculos: [..] }, ...].
def motoristas_previstos_no_periodo
rel = Entrega.atendidas.da_conta_gade
.no_periodo_checkout(@consolidacao.data_inicio, @consolidacao.data_fim)
rel = rel.da_veiculo(@consolidacao.vehicle_ids) if @consolidacao.vehicle_ids.present?
rel = rel.da_operacoes(@consolidacao.operacoes) if @consolidacao.operacoes.present?
rel.pluck(:driver, :vehicle)
.group_by(&:first)
.map { |nome, linhas| { nome: nome, veiculos: linhas.filter_map { |(_, v)| v.presence }.uniq.sort } }
.reject { |m| m[:nome].blank? }
.sort_by { |m| m[:nome] }
end
# Um motorista específico (consolidacao_motorista_id) ou todos da consolidação.
# "Todos" cobre apenas os ATIVOS — motorista arquivado não recebe pagamento.
def motoristas_alvo
if params[:consolidacao_motorista_id].present?
[@consolidacao.consolidacao_motoristas.find(params[:consolidacao_motorista_id])]
else
@consolidacao.consolidacao_motoristas.ativos.to_a
end
end
# Mapeia { cm.id => "numero da NF" } a partir de params[:notas_fiscais].
# Retorna nil se a NF de QUALQUER alvo estiver em branco (pagamento bloqueado).
def nf_por_motorista(alvos)
informadas = params[:notas_fiscais] || {}
alvos.each_with_object({}) do |cm, mapa|
nf = informadas[cm.id.to_s].to_s.strip
return nil if nf.blank?
mapa[cm.id] = nf
end
end
def pagamento_indisponivel
redirect_to @consolidacao, alert: 'Só é possível registrar pagamento de uma consolidação finalizada.'
end
def pagamento_notice(qtd, acao)
return 'Nenhuma alteração: motoristas já estavam no estado solicitado.' if qtd.zero?
verbo = acao == :pago ? 'marcado(s) como pago(s)' : 'com pagamento estornado'
"#{qtd} motorista(s) #{verbo}."
end
def preparar_form_new
@motoristas = Entrega.motoristas_ativos
@veiculos = Entrega.veiculos_unicos
@operacoes = Operacao.todas
end
def consolidacao_params
params.require(:consolidacao).permit(:nome, :data_inicio, :data_fim, vehicle_ids: [], operacoes: [])
end
end