# 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 — É O MESMO DO DASHBOARD FINANCEIRO, de propósito # (DashboardController#carregar_dados_financeiro). Os dois números são lidos lado # a lado pelo operador; quando divergem, a leitura é "o sistema está errado". # Entram as consolidações ATIVAS que: # • CRUZAM o período (data_inicio <= fim AND data_fim >= inicio) — um # fechamento de 01/08 a 31/08 aparece ao filtrar 01/08–26/08; OU # • tiveram algum PAGAMENTO dentro do período (pagar em agosto um fechamento # de julho tem que aparecer em agosto). # # STATUS: por padrão só as FINALIZADAS — é o "Consolidado" do dashboard, o valor # que já fechou. 'todos' inclui os rascunhos em andamento (a visão "Geral"), que # ainda mudam de valor. # # 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 # `public_send` sobre o enum: quem traduz :finalizada/:rascunho é o model. base = base.public_send(@status) if %w[finalizada rascunho].include?(@status) escopo = ConsolidacaoMotorista.ativos .where(consolidacao_id: ids_do_periodo(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 # As consolidações do período: as que cruzam a faixa + as que receberam # pagamento dentro dela. Mesmo par de conjuntos do dashboard financeiro. def ids_do_periodo(base) intervalo = @inicio.beginning_of_day..@fim.end_of_day cruzam = base.cruzando_periodo(@inicio, @fim).ids pagas = ConsolidacaoMotorista.ativos .where(pago_em: intervalo) .where(consolidacao_id: base.select(:id)) .distinct.pluck(:consolidacao_id) (cruzam + pagas).uniq end 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 # O EXISTS espelha o `ConsolidacaoMotorista.ativos` de `registros`, e por # PAR (consolidação, motorista) — filtrar só por nome não basta: o # mesmo motorista pode estar ativo numa consolidação do período e # arquivado em outra, e aí os lançamentos da arquivada entravam nesta # contagem enquanto o R$ da mesma linha os excluía. Arquivar motorista # é reversível e NÃO apaga consolidacao_entregas (só o excluir apaga). ConsolidacaoEntrega.where(consolidacao_id: ids, motorista_nome: registros.map(&:motorista_nome).uniq) .where(<<~SQL.squish) EXISTS (SELECT 1 FROM consolidacao_motoristas cm WHERE cm.consolidacao_id = consolidacao_entregas.consolidacao_id AND cm.motorista_nome = consolidacao_entregas.motorista_nome AND cm.arquivado_em IS NULL) SQL .group(:motorista_nome) .sum(:quantidade) end end end end end