Files
Reem-Notas/app/services/analytics/totais_por_motorista.rb

119 lines
4.7 KiB
Ruby

# app/services/analytics/totais_por_motorista.rb
#
# Quanto cada motorista somou no período — e QUAIS consolidações formam esse
# total. É a fonte única das duas telas de "total por motorista":
#
# • admin → /consolidacoes?visao=motoristas (modo de visualização da própria
# lista, com drill-down no motorista clicado)
# • motorista→ /motorista (o painel, já recortado nele — mesma conta, mesmo PDF)
#
# RECORTE (igual ao filtro da lista de consolidações): entram as consolidações
# ATIVAS cujo período está DENTRO da faixa escolhida (data_inicio >= inicio e
# data_fim <= fim). Não é sobreposição de propósito — uma consolidação que
# atravessa a borda do filtro entraria inteira e o "total do período" ficaria
# maior que o período.
#
# Só motoristas ATIVOS (não arquivados) contam: é o mesmo conjunto que a
# consolidação paga (ConsolidacaoMotorista.ativos), então o total desta tela
# bate com a soma dos fechamentos.
module Analytics
class TotaisPorMotorista
# status: 'finalizada' (padrão — só o que já fechou), 'rascunho' ou
# 'todos'. É o MESMO select de status da lista de consolidações.
# motorista: recorte exato (drill-down / painel do motorista).
# busca: filtro parcial por nome, o campo de busca da lista.
def initialize(inicio:, fim:, motorista: nil, status: 'finalizada', busca: nil)
@inicio = inicio.to_date
@fim = fim.to_date
@motorista = motorista.presence
@status = status.presence || 'finalizada'
@busca = busca.presence
end
attr_reader :inicio, :fim, :motorista, :status
# ConsolidacaoMotorista do recorte, com a consolidação já carregada (as duas
# telas mostram nome/período/status dela em seguida).
def registros
@registros ||= begin
base = Consolidacao.ativas.no_periodo(@inicio, @fim)
# Por padrão só o que já foi FECHADO conta como total do período —
# rascunho ainda muda de valor. `merge` (e não where(consolidacoes:
# {status: ...})) porque status é enum: quem traduz :finalizada é o model.
base = base.public_send(@status) if %w[finalizada rascunho].include?(@status)
escopo = ConsolidacaoMotorista.ativos
.joins(:consolidacao)
.merge(base)
.includes(:consolidacao)
escopo = escopo.where(motorista_nome: @motorista) if @motorista
escopo = escopo.where('motorista_nome ILIKE ?', "%#{@busca}%") if @busca
escopo.to_a
end
end
# Uma linha por motorista, do maior total para o menor.
def linhas
@linhas ||= registros.group_by(&:motorista_nome).map do |nome, cms|
pagos = cms.select(&:pago?)
pendentes = cms.reject(&:pago?)
{
motorista: nome,
consolidacoes: cms.size,
lancamentos: lancamentos_por_motorista[nome].to_i,
valor_total: soma(cms),
valor_pago: soma(pagos),
valor_pendente: soma(pendentes),
pagas: pagos.size,
pendentes: pendentes.size
}
end.sort_by { |l| -l[:valor_total] }
end
# Consolidações que compõem o total de UM motorista (o drill-down), da mais
# recente para a mais antiga.
def consolidacoes_de(nome)
registros.select { |cm| cm.motorista_nome == nome }
.sort_by { |cm| [cm.consolidacao.data_fim, cm.consolidacao.data_inicio] }
.reverse
end
def motoristas
linhas.map { |l| l[:motorista] }
end
def total_geral = soma(registros)
def total_pago = soma(registros.select(&:pago?))
def total_pendente = soma(registros.reject(&:pago?))
def total_consolidacoes = registros.map(&:consolidacao_id).uniq.size
def vazio? = registros.empty?
def periodo_label
"#{@inicio.strftime('%d/%m/%Y')} a #{@fim.strftime('%d/%m/%Y')}"
end
private
def soma(cms)
cms.sum { |cm| cm.valor_total || 0 }
end
# Lançamentos classificados por motorista (soma da quantidade — um termo com
# quantidade 5 vale 5). Uma query só para todas as consolidações do recorte,
# em vez de uma por linha da tabela.
def lancamentos_por_motorista
@lancamentos_por_motorista ||= begin
ids = registros.map(&:consolidacao_id).uniq
if ids.empty?
{}
else
ConsolidacaoEntrega.where(consolidacao_id: ids,
motorista_nome: registros.map(&:motorista_nome).uniq)
.group(:motorista_nome)
.sum(:quantidade)
end
end
end
end
end