255 lines
10 KiB
Ruby
255 lines
10 KiB
Ruby
# app/controllers/dashboard_controller.rb
|
|
class DashboardController < ApplicationController
|
|
MAX_DIAS_GRAFICO = 92 # limita a granularidade diária do gráfico (≈ 1 trimestre)
|
|
|
|
def index
|
|
skip_authorization
|
|
|
|
# Motorista tem painel próprio — não enxerga o dashboard administrativo
|
|
return redirect_to(motorista_dashboard_path) if current_user.motorista?
|
|
|
|
@periodo_inicio, @periodo_fim = periodo_selecionado
|
|
carregar_dados_dashboard
|
|
end
|
|
|
|
# GET /dashboard/relatorio_financeiro?inicio=&fim=&operacoes[]=
|
|
# Relatório financeiro do período (PDF) — mesmos números da tela.
|
|
def relatorio_financeiro
|
|
skip_authorization
|
|
return redirect_to(motorista_dashboard_path) if current_user.motorista?
|
|
|
|
@periodo_inicio, @periodo_fim = periodo_selecionado
|
|
carregar_dados_dashboard
|
|
|
|
op_label = @operacao_filtro.present? ? @operacao_filtro.map { |t| Operacao.label(t) }.join(', ') : 'Todas'
|
|
|
|
dados = {
|
|
custo_total: @fin_custo_total,
|
|
ticket_medio: @fin_ticket_medio,
|
|
pago: @fin_pago,
|
|
pendente: @fin_pendente,
|
|
por_operacao: @fin_por_operacao,
|
|
por_motorista: @fin_por_motorista,
|
|
por_tipo: @fin_por_tipo,
|
|
pagamentos_feitos: @pagamentos_feitos,
|
|
pagamentos_pendentes: @pagamentos_pendentes,
|
|
pag_pago_valor: @pag_pago_valor,
|
|
pag_pago_qtd: @pag_pago_qtd,
|
|
pag_pend_valor: @pag_pend_valor,
|
|
pag_pend_qtd: @pag_pend_qtd
|
|
}
|
|
|
|
pdf = Pdf::RelatorioFinanceiroPeriodoPdf.new(
|
|
periodo_inicio: @periodo_inicio, periodo_fim: @periodo_fim,
|
|
operacao_label: op_label, dados: dados
|
|
)
|
|
|
|
send_data pdf.render,
|
|
filename: "financeiro_#{@periodo_inicio.strftime('%Y%m%d')}_#{@periodo_fim.strftime('%Y%m%d')}.pdf",
|
|
type: 'application/pdf',
|
|
disposition: 'attachment'
|
|
end
|
|
|
|
private
|
|
|
|
# Faixa de datas vinda do filtro de calendário (params inicio/fim).
|
|
# Default: do início do mês corrente até hoje. Datas inválidas caem no default
|
|
# e a ordem é normalizada caso venham invertidas.
|
|
def periodo_selecionado
|
|
inicio = parse_data(params[:inicio]) || Date.current.beginning_of_month
|
|
fim = parse_data(params[:fim]) || Date.current
|
|
inicio, fim = fim, inicio if fim < inicio
|
|
[inicio, fim]
|
|
end
|
|
|
|
# Parse tolerante: data inválida/ausente vira nil (para o default assumir).
|
|
def parse_data(str)
|
|
return nil if str.blank?
|
|
Date.parse(str)
|
|
rescue ArgumentError, TypeError
|
|
nil
|
|
end
|
|
|
|
def carregar_dados_dashboard
|
|
# Filtro de operação (multi-seleção) — aplicado a todo o dashboard.
|
|
@operacoes = Operacao.todas
|
|
@operacoes_agrupadas = Operacao.agrupadas_por_mes
|
|
@operacao_filtro = Operacao.sanitizar(params[:operacoes])
|
|
|
|
# PAGAS pela DATA REAL (checkout) — base de toda a análise financeira por data.
|
|
pagas = Entrega.da_conta_gade.pagas.no_periodo_checkout(@periodo_inicio, @periodo_fim)
|
|
pagas = pagas.da_operacoes(@operacao_filtro) if @operacao_filtro.present?
|
|
|
|
# PENDENTES (em aberto, nem concluídas nem falhadas) pela data planejada.
|
|
pendentes = Entrega.da_conta_gade.pendentes.no_periodo(@periodo_inicio, @periodo_fim)
|
|
pendentes = pendentes.da_operacoes(@operacao_filtro) if @operacao_filtro.present?
|
|
|
|
# FALHADAS (insucesso) pela data planejada — também não têm checkout.
|
|
falhadas = Entrega.da_conta_gade.falhadas.no_periodo(@periodo_inicio, @periodo_fim)
|
|
falhadas = falhadas.da_operacoes(@operacao_filtro) if @operacao_filtro.present?
|
|
|
|
# Totais gerais
|
|
@entregas_pagas = pagas.count
|
|
@entregas_pendentes = pendentes.count
|
|
@entregas_falhadas = falhadas.count
|
|
@total_entregas = @entregas_pagas + @entregas_pendentes + @entregas_falhadas
|
|
|
|
# Configurações de preço
|
|
config = Configuracao.mapa_de_precos
|
|
|
|
# Valor estimado total
|
|
@valor_estimado = (@entregas_pagas * config[:entrega]).round(2)
|
|
|
|
# Por motorista (todos)
|
|
@motoristas = pagas.group(:driver)
|
|
.count
|
|
.sort_by { |_, v| -v }
|
|
.map do |driver, qtd|
|
|
{
|
|
nome: driver,
|
|
entregas: qtd,
|
|
valor: (qtd * config[:entrega]).round(2)
|
|
}
|
|
end
|
|
|
|
# Por local de entrega (contact_name)
|
|
@por_operacao = pagas.group(:contact_name)
|
|
.count
|
|
.sort_by { |_, v| -v }
|
|
.first(6)
|
|
.to_h
|
|
|
|
# Evolução diária no período (para Chart.js) — por data real (checkout)
|
|
@grafico_diario = build_grafico_diario(pagas, config[:entrega])
|
|
|
|
# Consolidações cujo período (data_inicio..data_fim) cruza o período do dashboard
|
|
# — independe de quando foram criadas (created_at).
|
|
@consolidacoes_mes = Consolidacao.ativas
|
|
.where('data_inicio <= ? AND data_fim >= ?', @periodo_fim, @periodo_inicio)
|
|
@consolidacoes_mes = @consolidacoes_mes.com_operacoes(@operacao_filtro) if @operacao_filtro.present?
|
|
@consolidacoes_abertas = @consolidacoes_mes.where(status: :rascunho).count
|
|
@consolidacoes_fechadas = @consolidacoes_mes.where(status: :finalizada).count
|
|
|
|
# Pagamentos: pago vs pendente entre as consolidações FINALIZADAS do período
|
|
# (medido por motorista — fonte: consolidacao_motoristas).
|
|
carregar_dados_pagamentos
|
|
|
|
# Visão financeira por operação (KPIs + gráficos)
|
|
carregar_dados_financeiro
|
|
|
|
# Histórico estimado mais recente
|
|
@historico_recente = HistoricoEstimado.order(created_at: :desc).limit(5)
|
|
end
|
|
|
|
def carregar_dados_pagamentos
|
|
intervalo = @periodo_inicio.beginning_of_day..@periodo_fim.end_of_day
|
|
|
|
# PAGOS: filtrados pela DATA DO PAGAMENTO (pago_em) dentro do período do topo
|
|
# — "quanto foi pago neste período".
|
|
pagos = ConsolidacaoMotorista.ativos
|
|
.joins(:consolidacao)
|
|
.where(consolidacoes: { status: :finalizada, deleted_at: nil })
|
|
.where(pago_em: intervalo)
|
|
if @operacao_filtro.present?
|
|
pagos = pagos.where(consolidacao_id: Consolidacao.com_operacoes(@operacao_filtro).select(:id))
|
|
end
|
|
@pag_pago_qtd = pagos.count
|
|
@pag_pago_valor = pagos.sum(:valor_total)
|
|
|
|
# PENDENTES: consolidações finalizadas cujo PERÍODO cruza o filtro, ainda sem
|
|
# pagamento (pendente não tem data de pagamento para filtrar).
|
|
finalizadas_ids = @consolidacoes_mes.where(status: :finalizada).select(:id)
|
|
pendentes = ConsolidacaoMotorista.ativos.where(consolidacao_id: finalizadas_ids).pendentes
|
|
@pag_pend_qtd = pendentes.count
|
|
@pag_pend_valor = pendentes.sum(:valor_total)
|
|
|
|
@pagamentos_feitos = pagos.includes(:consolidacao, :pagador).order(pago_em: :desc)
|
|
@pagamentos_pendentes = pendentes.includes(:consolidacao).order(consolidacao_id: :desc)
|
|
end
|
|
|
|
# Visão financeira (custo) das consolidações finalizadas do período, já com o
|
|
# filtro de operação aplicado.
|
|
def carregar_dados_financeiro
|
|
intervalo = @periodo_inicio.beginning_of_day..@periodo_fim.end_of_day
|
|
|
|
base = Consolidacao.ativas.where(status: :finalizada)
|
|
base = base.com_operacoes(@operacao_filtro) if @operacao_filtro.present?
|
|
|
|
# Consolidações relevantes ao período: as que CRUZAM o período OU que tiveram
|
|
# algum pagamento feito dentro do período. Sem o 2º caso, pagar uma
|
|
# consolidação de outro mês não mexia nos gráficos (só na tabela de pagamentos).
|
|
ids_periodo = base.where('data_inicio <= ? AND data_fim >= ?', @periodo_fim, @periodo_inicio).ids
|
|
ids_pagos = ConsolidacaoMotorista.ativos.where(pago_em: intervalo)
|
|
.where(consolidacao_id: base.select(:id))
|
|
.distinct.pluck(:consolidacao_id)
|
|
|
|
consols = Consolidacao.where(id: (ids_periodo + ids_pagos).uniq).to_a
|
|
ids = consols.map(&:id)
|
|
cms = ConsolidacaoMotorista.ativos.where(consolidacao_id: ids)
|
|
ces = ConsolidacaoEntrega.where(consolidacao_id: ids)
|
|
|
|
# KPIs
|
|
@fin_custo_total = cms.sum(:valor_total)
|
|
@fin_qtd_entregas = ces.distinct.count(:tracking_id)
|
|
@fin_ticket_medio = @fin_qtd_entregas.zero? ? 0 : (@fin_custo_total / @fin_qtd_entregas)
|
|
@fin_pago = cms.pagos.sum(:valor_total)
|
|
@fin_pendente = cms.pendentes.sum(:valor_total)
|
|
|
|
# Custo por operação — rateia o valor da consolidação igualmente entre suas
|
|
# operações (mantém o total consistente; exato no caso de 1 operação).
|
|
por_op = Hash.new(0)
|
|
consols.each do |c|
|
|
ops = c.operacoes.presence || ['(sem operação)']
|
|
fatia = c.valor_total / ops.size
|
|
ops.each { |o| por_op[o == '(sem operação)' ? o : Operacao.label(o)] += fatia }
|
|
end
|
|
@fin_por_operacao = por_op.sort_by { |_, v| -v }
|
|
|
|
# Composição por tipo (normal/retirada/bônus/desconto)
|
|
soma_tipo = ces.group(:tipo).sum(:valor_aplicado)
|
|
@fin_por_tipo = ConsolidacaoEntrega::TIPO_CORES.filter_map do |tipo, cfg|
|
|
valor = soma_tipo[tipo].to_f
|
|
next if valor.zero?
|
|
{ label: cfg[:label], valor: valor }
|
|
end
|
|
|
|
# Custo por motorista (todos)
|
|
@fin_por_motorista = cms.group(:motorista_nome).sum(:valor_total)
|
|
.sort_by { |_, v| -v }
|
|
end
|
|
|
|
# `pagas` já vem filtrado por checkout no período. Agrupa pela DATA REAL
|
|
# (DATE(checkout)) — DATE() sobre timestamp não faz conversão de fuso.
|
|
def build_grafico_diario(pagas, preco_entrega)
|
|
dias = (@periodo_inicio..@periodo_fim).to_a
|
|
|
|
if dias.size <= MAX_DIAS_GRAFICO
|
|
# Granularidade diária (períodos curtos)
|
|
contagem = pagas.group("DATE(checkout)").count.transform_keys(&:to_s)
|
|
labels = dias.map { |d| d.strftime('%d/%m') }
|
|
valores = dias.map { |d| ((contagem[d.to_s] || 0) * preco_entrega).round(2) }
|
|
qtds = dias.map { |d| contagem[d.to_s] || 0 }
|
|
else
|
|
# Período longo: agrega por mês para o gráfico não ficar vazio nem poluído.
|
|
contagem = pagas.group("TO_CHAR(checkout, 'YYYY-MM')").count
|
|
meses = meses_no_periodo
|
|
labels = meses.map { |m| Date.strptime(m, '%Y-%m').strftime('%m/%Y') }
|
|
valores = meses.map { |m| ((contagem[m] || 0) * preco_entrega).round(2) }
|
|
qtds = meses.map { |m| contagem[m] || 0 }
|
|
end
|
|
|
|
{ labels: labels, valores: valores, qtds: qtds }
|
|
end
|
|
|
|
# Meses ("YYYY-MM") do início ao fim do período selecionado, inclusive.
|
|
def meses_no_periodo
|
|
meses = []
|
|
cursor = @periodo_inicio.beginning_of_month
|
|
while cursor <= @periodo_fim
|
|
meses << cursor.strftime('%Y-%m')
|
|
cursor = cursor.next_month
|
|
end
|
|
meses
|
|
end
|
|
end
|