, Correçoes UX consolidaçoes e de backa-end
This commit is contained in:
@@ -284,6 +284,37 @@ class ConsolidacoesController < ApplicationController
|
||||
redirect_to consolidacoes_path, notice: 'Consolidação arquivada.'
|
||||
end
|
||||
|
||||
# POST /consolidacoes/arquivar_em_lote — arquiva as consolidações marcadas na
|
||||
# lista. Mesma regra do arquivar de uma só (nenhuma trava extra): arquivar é
|
||||
# REVERSÍVEL (Arquivadas → Reativar), então o lote não precisa de confirmação
|
||||
# mais dura do que a de uma — o que ele precisa é dizer QUANTAS foram.
|
||||
#
|
||||
# Ignora em silêncio id que não está mais ativo (alguém arquivou pela outra
|
||||
# aba enquanto a lista estava aberta): o resultado que o operador queria — "essas
|
||||
# saem da lista" — já é o que está na tela, e erro aqui só assustaria à toa.
|
||||
def arquivar_em_lote
|
||||
authorize Consolidacao, :arquivar?
|
||||
|
||||
ids = Array(params[:ids]).map(&:to_i).reject(&:zero?)
|
||||
if ids.empty?
|
||||
return redirect_back fallback_location: consolidacoes_path,
|
||||
alert: 'Marque ao menos uma consolidação para arquivar.'
|
||||
end
|
||||
|
||||
alvos = Consolidacao.ativas.where(id: ids).to_a
|
||||
alvos.each do |c|
|
||||
c.arquivar!(current_user)
|
||||
auditar!(:arquivar, c, dados_novos: { em_lote: true, total_do_lote: alvos.size })
|
||||
end
|
||||
|
||||
aviso = case alvos.size
|
||||
when 0 then 'Nenhuma das consolidações marcadas continuava ativa.'
|
||||
when 1 then "1 consolidação arquivada. Você pode restaurar em \"Arquivadas\"."
|
||||
else "#{alvos.size} consolidações arquivadas. Você pode restaurar em \"Arquivadas\"."
|
||||
end
|
||||
redirect_back fallback_location: consolidacoes_path, notice: aviso
|
||||
end
|
||||
|
||||
# GET /consolidacoes/arquivadas — histórico de consolidações arquivadas
|
||||
def arquivadas
|
||||
authorize Consolidacao
|
||||
|
||||
@@ -2,6 +2,26 @@
|
||||
class DashboardController < ApplicationController
|
||||
MAX_DIAS_GRAFICO = 92 # limita a granularidade diária do gráfico (≈ 1 trimestre)
|
||||
|
||||
# Tipos lançados EM LOTE: uma linha só de consolidacao_entregas representa N
|
||||
# entregas (quantidade = N, valor_aplicado = N × preço — ver
|
||||
# Consolidacao#adicionar_termos!). Contá-los por tracking_id valeria 1.
|
||||
TIPOS_EM_LOTE = %w[termo termo_especial].freeze
|
||||
|
||||
# Cor de cada tipo no donut "Composição do custo". Fica AQUI, junto de quem
|
||||
# monta @fin_por_tipo, para label, valor e cor viajarem no MESMO hash: a view
|
||||
# tinha uma lista fixa de 5 cores para os 7 tipos de
|
||||
# ConsolidacaoEntrega::TIPO_CORES e o Chart.js reciclava o array — "Entrega de
|
||||
# Termo" saía com a mesma laranja de "Entrega Normal", no mesmo gráfico.
|
||||
# `desconto` NÃO entra: ver o comentário em #carregar_dados_financeiro.
|
||||
CORES_TIPO = {
|
||||
'entrega_normal' => '#f97316',
|
||||
'retirada' => '#9a3412',
|
||||
'bonus' => '#fed7aa',
|
||||
'extraordinaria' => '#9333ea',
|
||||
'termo' => '#2563eb',
|
||||
'termo_especial' => '#06b6d4'
|
||||
}.freeze
|
||||
|
||||
def index
|
||||
# Motorista tem painel próprio — não enxerga o dashboard administrativo
|
||||
return redirect_to(motorista_dashboard_path) if current_user.motorista?
|
||||
@@ -247,11 +267,26 @@ class DashboardController < ApplicationController
|
||||
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)
|
||||
|
||||
# ⚠️ O MESMO RECORTE DE `cms`, NO NÍVEL DA LINHA. Motorista arquivado sai do
|
||||
# dinheiro (ConsolidacaoMotorista.ativos), mas as linhas dele CONTINUAM em
|
||||
# consolidacao_entregas: `arquivar_motorista` é reversível e preserva tudo —
|
||||
# quem apaga é o `excluir_motorista`. Sem este EXISTS, o card "Custo total"
|
||||
# mostrava R$ de um conjunto e "N entregas classificadas" de outro, o ticket
|
||||
# médio dividia um pelo outro e o donut de composição não fechava com o total.
|
||||
#
|
||||
# O par é (consolidacao_id, motorista_nome), não só o nome: o mesmo motorista
|
||||
# pode estar ativo numa consolidação e arquivado em outra, ambas no período.
|
||||
ces = ConsolidacaoEntrega.where(consolidacao_id: ids).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
|
||||
|
||||
# KPIs
|
||||
@fin_custo_total = cms.sum(:valor_total)
|
||||
@fin_qtd_entregas = ces.distinct.count(:tracking_id)
|
||||
@fin_qtd_entregas = contar_entregas(ces)
|
||||
@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)
|
||||
@@ -266,12 +301,21 @@ class DashboardController < ApplicationController
|
||||
end
|
||||
@fin_por_operacao = por_op.sort_by { |_, v| -v }
|
||||
|
||||
# Composição por tipo (normal/retirada/bônus/desconto)
|
||||
# Composição por tipo (normal/retirada/bônus/…)
|
||||
#
|
||||
# ⚠️ DESCONTO NÃO É FATIA. No dinheiro ele SUBTRAI (ConsolidacaoMotorista
|
||||
# #recalcular_valor e Consolidacao#recalcular_motorista! usam
|
||||
# `-valor_aplicado`). Como fatia positiva de um DONUT — que se lê como
|
||||
# "partes de um todo" — a soma das fatias ficava maior que o Custo total
|
||||
# mostrado logo acima, e a diferença era exatamente o desconto. Ele sai do
|
||||
# gráfico e vira a linha "já abatidos" embaixo: a divergência é legítima,
|
||||
# então é explicada no lugar onde ela aparece (diretriz 1).
|
||||
soma_tipo = ces.group(:tipo).sum(:valor_aplicado)
|
||||
@fin_por_tipo = ConsolidacaoEntrega::TIPO_CORES.filter_map do |tipo, cfg|
|
||||
@fin_descontos = soma_tipo['desconto'].to_f
|
||||
@fin_por_tipo = CORES_TIPO.filter_map do |tipo, cor|
|
||||
valor = soma_tipo[tipo].to_f
|
||||
next if valor.zero?
|
||||
{ label: cfg[:label], valor: valor }
|
||||
{ label: ConsolidacaoEntrega::TIPO_CORES.dig(tipo, :label) || tipo, valor: valor, cor: cor }
|
||||
end
|
||||
|
||||
# Custo por motorista (todos)
|
||||
@@ -288,13 +332,37 @@ class DashboardController < ApplicationController
|
||||
# Partimos de @fin_por_motorista (que vem de `cms`, já .ativos) e só buscamos
|
||||
# a quantidade: assim a aba lista exatamente os motoristas do fechamento —
|
||||
# arquivado não aparece — e a soma bate com o KPI "Custo total" do topo.
|
||||
qtd_por_motorista = ces.group(:motorista_nome).distinct.count(:tracking_id)
|
||||
qtd_por_motorista = contar_entregas_por_motorista(ces)
|
||||
|
||||
@ranking_consolidado = @fin_por_motorista.map do |nome, valor|
|
||||
{ nome: nome, valor: valor, entregas: qtd_por_motorista[nome].to_i }
|
||||
end
|
||||
end
|
||||
|
||||
# Quantas ENTREGAS existem num conjunto de linhas de consolidacao_entregas.
|
||||
# São DUAS correções na mesma conta, e as duas precisam conviver:
|
||||
#
|
||||
# • DISTINCT tracking_id — a MESMA entrega vira várias linhas quando tem mais
|
||||
# de um pilar (Normal + Bônus + Retirada = 3 linhas). Contar linhas inflava
|
||||
# o número de entregas. Esta parte já estava certa.
|
||||
# • termo soma `quantidade` — um lote de termo é UMA linha com quantidade N
|
||||
# (Consolidacao#adicionar_termos!), então por tracking_id um lote de 20
|
||||
# valia 1: o custo entrava inteiro no numerador e 1 no denominador, e o
|
||||
# ticket médio saía 20× maior. É a mesma unidade que
|
||||
# Analytics::TotaisPorMotorista já usa ("um termo com quantidade 5 vale 5").
|
||||
def contar_entregas(escopo)
|
||||
escopo.where.not(tipo: TIPOS_EM_LOTE).distinct.count(:tracking_id) +
|
||||
escopo.where(tipo: TIPOS_EM_LOTE).sum(:quantidade)
|
||||
end
|
||||
|
||||
# Mesma conta de #contar_entregas, quebrada por motorista — o ranking e o KPI
|
||||
# do topo têm que somar a mesma coisa, senão viram dois números na mesma tela.
|
||||
def contar_entregas_por_motorista(escopo)
|
||||
normais = escopo.where.not(tipo: TIPOS_EM_LOTE).group(:motorista_nome).distinct.count(:tracking_id)
|
||||
lotes = escopo.where(tipo: TIPOS_EM_LOTE).group(:motorista_nome).sum(:quantidade)
|
||||
normais.merge(lotes) { |_nome, a, b| a + b }
|
||||
end
|
||||
|
||||
# `atendidas` já vem filtrado por checkout no período (concluídas + falhadas).
|
||||
# Agrupa pela DATA REAL (DATE(checkout)) — DATE() sobre timestamp não faz
|
||||
# conversão de fuso.
|
||||
|
||||
Reference in New Issue
Block a user