Criação da visão geral da consolidação por motorista
This commit is contained in:
111
app/services/analytics/totais_por_motorista.rb
Normal file
111
app/services/analytics/totais_por_motorista.rb
Normal file
@@ -0,0 +1,111 @@
|
||||
# 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/totais (todos os motoristas + drill-down)
|
||||
# • motorista→ /motorista/totais (só ele, 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
|
||||
def initialize(inicio:, fim:, motorista: nil, incluir_rascunhos: false)
|
||||
@inicio = inicio.to_date
|
||||
@fim = fim.to_date
|
||||
@motorista = motorista.presence
|
||||
@incluir_rascunhos = incluir_rascunhos
|
||||
end
|
||||
|
||||
attr_reader :inicio, :fim, :motorista
|
||||
|
||||
# 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)
|
||||
# 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 sabe traduzir :finalizada é o próprio model.
|
||||
base = base.finalizada unless @incluir_rascunhos
|
||||
|
||||
escopo = ConsolidacaoMotorista.ativos
|
||||
.joins(:consolidacao)
|
||||
.merge(base)
|
||||
.includes(:consolidacao)
|
||||
escopo = escopo.where(motorista_nome: @motorista) if @motorista
|
||||
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
|
||||
130
app/services/pdf/totais_motorista_pdf.rb
Normal file
130
app/services/pdf/totais_motorista_pdf.rb
Normal file
@@ -0,0 +1,130 @@
|
||||
# app/services/pdf/totais_motorista_pdf.rb
|
||||
#
|
||||
# Relatório "total por motorista no período" — o mesmo documento serve às duas
|
||||
# telas, porque os números vêm do mesmo Analytics::TotaisPorMotorista:
|
||||
#
|
||||
# • admin (todos os motoristas): resumo por motorista + as consolidações que
|
||||
# compõem o total de cada um;
|
||||
# • motorista (um só): o total fechado dele no período + as consolidações que
|
||||
# formam esse valor, com espaço de assinatura para servir de comprovante.
|
||||
#
|
||||
module Pdf
|
||||
class TotaisMotoristaPdf < BasePdf
|
||||
VERDE = '22C55E'
|
||||
AMBAR = 'D97706'
|
||||
|
||||
# totais: Analytics::TotaisPorMotorista já montado (ele conhece o recorte).
|
||||
# detalhar: inclui, para cada motorista, a lista de consolidações. Sempre
|
||||
# ligado quando o relatório é de um motorista só.
|
||||
def initialize(totais:, detalhar: true, titulo: nil)
|
||||
super()
|
||||
@t = totais
|
||||
@detalhar = detalhar || @t.motorista.present?
|
||||
@titulo = titulo
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def corpo
|
||||
secao(@titulo || (@t.motorista ? 'MEU TOTAL NO PERÍODO' : 'TOTAL POR MOTORISTA'))
|
||||
|
||||
@pdf.text "Período: #{@t.periodo_label}", size: 11, style: :bold
|
||||
@pdf.fill_color CINZA
|
||||
@pdf.text(@t.motorista ? "Motorista: #{@t.motorista}" : "Motoristas: #{@t.linhas.size}", size: 10)
|
||||
@pdf.text "Consolidações no período: #{@t.total_consolidacoes}", size: 10
|
||||
@pdf.fill_color '000000'
|
||||
|
||||
secao 'TOTAIS'
|
||||
kpis
|
||||
|
||||
if @t.vazio?
|
||||
@pdf.fill_color CINZA
|
||||
@pdf.text 'Nenhuma consolidação fechada neste período.', size: 10
|
||||
@pdf.fill_color '000000'
|
||||
return
|
||||
end
|
||||
|
||||
unless @t.motorista
|
||||
secao 'RESUMO POR MOTORISTA'
|
||||
tabela_resumo
|
||||
end
|
||||
|
||||
return unless @detalhar
|
||||
|
||||
@t.linhas.each do |linha|
|
||||
@pdf.start_new_page if @pdf.cursor < 160
|
||||
secao "COMPOSIÇÃO — #{linha[:motorista].upcase}"
|
||||
tabela_composicao(@t.consolidacoes_de(linha[:motorista]))
|
||||
end
|
||||
|
||||
assinaturas(motorista: @t.motorista) if @t.motorista
|
||||
end
|
||||
|
||||
def kpis
|
||||
linhas = [
|
||||
['Total do período', moeda(@t.total_geral)],
|
||||
['Pago', moeda(@t.total_pago)],
|
||||
['A receber', moeda(@t.total_pendente)]
|
||||
]
|
||||
@pdf.table(linhas, width: 300, cell_style: { size: 11, padding: [6, 10] }) do |t|
|
||||
t.columns(0).font_style = :bold
|
||||
t.columns(1).align = :right
|
||||
t.row(1).text_color = VERDE
|
||||
t.row(2).text_color = AMBAR
|
||||
t.cells.borders = [:bottom]
|
||||
t.cells.border_color = 'DDDDDD'
|
||||
end
|
||||
end
|
||||
|
||||
def tabela_resumo
|
||||
linhas = [['Motorista', 'Consol.', 'Lanç.', 'Pago', 'A receber', 'Total']]
|
||||
@t.linhas.each do |l|
|
||||
linhas << [l[:motorista], l[:consolidacoes].to_s, l[:lancamentos].to_s,
|
||||
moeda(l[:valor_pago]), moeda(l[:valor_pendente]), moeda(l[:valor_total])]
|
||||
end
|
||||
linhas << ['TOTAL', @t.total_consolidacoes.to_s, '',
|
||||
moeda(@t.total_pago), moeda(@t.total_pendente), moeda(@t.total_geral)]
|
||||
|
||||
@pdf.table(linhas, header: true, width: @pdf.bounds.width,
|
||||
column_widths: { 1 => 50, 2 => 50, 3 => 80, 4 => 80, 5 => 85 },
|
||||
cell_style: { size: 8, padding: [4, 6] }) do |t|
|
||||
t.row(0).background_color = PRETO
|
||||
t.row(0).text_color = 'FFFFFF'
|
||||
t.row(0).font_style = :bold
|
||||
t.columns(1..5).align = :right
|
||||
t.row(-1).font_style = :bold
|
||||
t.row(1..-1).borders = [:bottom]
|
||||
t.row(1..-1).border_color = 'DDDDDD'
|
||||
end
|
||||
end
|
||||
|
||||
# As consolidações que compõem o valor — é o "clicar no motorista" da tela,
|
||||
# em papel.
|
||||
def tabela_composicao(cms)
|
||||
linhas = [['Consolidação', 'Período', 'Situação', 'Pago em', 'Valor']]
|
||||
cms.each do |cm|
|
||||
c = cm.consolidacao
|
||||
linhas << [
|
||||
c.nome,
|
||||
"#{c.data_inicio.strftime('%d/%m/%y')}–#{c.data_fim.strftime('%d/%m/%y')}",
|
||||
cm.pago? ? 'Pago' : 'A receber',
|
||||
cm.pago? ? cm.pago_em.strftime('%d/%m/%Y') : '—',
|
||||
moeda(cm.valor_total)
|
||||
]
|
||||
end
|
||||
linhas << ['TOTAL', '', '', '', moeda(cms.sum { |cm| cm.valor_total || 0 })]
|
||||
|
||||
@pdf.table(linhas, header: true, width: @pdf.bounds.width,
|
||||
column_widths: { 1 => 90, 2 => 70, 3 => 70, 4 => 80 },
|
||||
cell_style: { size: 8, padding: [4, 6] }) do |t|
|
||||
t.row(0).background_color = PRETO
|
||||
t.row(0).text_color = 'FFFFFF'
|
||||
t.row(0).font_style = :bold
|
||||
t.columns(4).align = :right
|
||||
t.row(-1).font_style = :bold
|
||||
t.row(1..-1).borders = [:bottom]
|
||||
t.row(1..-1).border_color = 'DDDDDD'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user