620 lines
26 KiB
Ruby
620 lines
26 KiB
Ruby
# 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/08–26/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, :exportar_pdf?
|
||
|
||
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, :finalizar?
|
||
|
||
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, :finalizar_parcial?
|
||
|
||
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, :arquivar?
|
||
@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, :reativar?
|
||
@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, :gerir_motoristas?
|
||
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, :gerir_motoristas?
|
||
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, :excluir_motorista?
|
||
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, :gerir_motoristas?
|
||
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, :exportar_pdf?
|
||
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, :exportar_pdf?
|
||
|
||
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
|