Corrige 3 erros de runtime + filtro de faixa de datas no dashboard
Erros corrigidos (reportados em prints): - Logout dava "No route matches [GET] /auth/logout": link usava method: :delete (rails-ujs), que o Turbo ignora -> agora data-turbo-method - /admin/usuarios estourava NoMethodError 'admin_ou_gerente?'/'admin?' na UserPolicy -> adicionados helpers que delegam para o User - Nova consolidação estourava NoMethodError 'new?' na ConsolidacaoPolicy -> ApplicationPolicy passa a definir new? (= create?) e edit? (= update?) Dashboard: - Substitui a navegação por mês por um filtro de calendário com faixa de datas (Flatpickr, range mode) que auto-submete inicio/fim - Controller passa a consultar por período (Entrega.no_periodo) em vez de do_mes; gráfico diário limitado a ~1 trimestre Specs: UserPolicy + new? na ConsolidacaoPolicy. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,35 +1,44 @@
|
||||
# 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?
|
||||
|
||||
@data_referencia = parse_data(params[:data])
|
||||
@mes_inicio = @data_referencia.beginning_of_month
|
||||
@mes_fim = @data_referencia.end_of_month
|
||||
|
||||
@periodo_inicio, @periodo_fim = periodo_selecionado
|
||||
carregar_dados_dashboard
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Parse tolerante: data inválida/ausente cai no dia atual em vez de estourar 500.
|
||||
# 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 Date.current if str.blank?
|
||||
return nil if str.blank?
|
||||
Date.parse(str)
|
||||
rescue ArgumentError, TypeError
|
||||
Date.current
|
||||
nil
|
||||
end
|
||||
|
||||
def carregar_dados_dashboard
|
||||
# Entregas do mês atual (da tabela read-only) — restritas à conta Gade
|
||||
entregas_mes = Entrega.da_conta_gade.do_mes(@data_referencia)
|
||||
# Entregas do período (tabela read-only) — restritas à conta Gade
|
||||
entregas = Entrega.da_conta_gade.no_periodo(@periodo_inicio, @periodo_fim)
|
||||
|
||||
# Totais gerais
|
||||
@total_entregas = entregas_mes.count
|
||||
@entregas_pagas = entregas_mes.pagas.count
|
||||
@total_entregas = entregas.count
|
||||
@entregas_pagas = entregas.pagas.count
|
||||
@entregas_pendentes = @total_entregas - @entregas_pagas
|
||||
|
||||
# Configurações de preço
|
||||
@@ -39,12 +48,12 @@ class DashboardController < ApplicationController
|
||||
@valor_estimado = (@entregas_pagas * config[:entrega]).round(2)
|
||||
|
||||
# Por motorista (top 10)
|
||||
@motoristas = entregas_mes.pagas
|
||||
.group(:driver)
|
||||
.count
|
||||
.sort_by { |_, v| -v }
|
||||
.first(10)
|
||||
.map do |driver, qtd|
|
||||
@motoristas = entregas.pagas
|
||||
.group(:driver)
|
||||
.count
|
||||
.sort_by { |_, v| -v }
|
||||
.first(10)
|
||||
.map do |driver, qtd|
|
||||
{
|
||||
nome: driver,
|
||||
entregas: qtd,
|
||||
@@ -53,18 +62,18 @@ class DashboardController < ApplicationController
|
||||
end
|
||||
|
||||
# Por operação (route_id → contact_name)
|
||||
@por_operacao = entregas_mes.pagas
|
||||
.group(:contact_name)
|
||||
.count
|
||||
.sort_by { |_, v| -v }
|
||||
.first(6)
|
||||
.to_h
|
||||
@por_operacao = entregas.pagas
|
||||
.group(:contact_name)
|
||||
.count
|
||||
.sort_by { |_, v| -v }
|
||||
.first(6)
|
||||
.to_h
|
||||
|
||||
# Evolução diária do mês (para Chart.js)
|
||||
@grafico_diario = build_grafico_diario(entregas_mes, config[:entrega])
|
||||
# Evolução diária no período (para Chart.js)
|
||||
@grafico_diario = build_grafico_diario(entregas, config[:entrega])
|
||||
|
||||
# Consolidações do mês
|
||||
@consolidacoes_mes = Consolidacao.ativas.where(created_at: @mes_inicio.beginning_of_day..@mes_fim.end_of_day)
|
||||
# Consolidações criadas no período
|
||||
@consolidacoes_mes = Consolidacao.ativas.where(created_at: @periodo_inicio.beginning_of_day..@periodo_fim.end_of_day)
|
||||
@consolidacoes_abertas = @consolidacoes_mes.where(status: :rascunho).count
|
||||
@consolidacoes_fechadas = @consolidacoes_mes.where(status: :finalizada).count
|
||||
|
||||
@@ -73,16 +82,18 @@ class DashboardController < ApplicationController
|
||||
end
|
||||
|
||||
def build_grafico_diario(entregas, preco_entrega)
|
||||
dias = ((@mes_inicio)..[@mes_fim, Date.today].min).map(&:to_s)
|
||||
# Em períodos muito longos, agrupa o gráfico de forma a não gerar centenas de pontos.
|
||||
dias = (@periodo_inicio..@periodo_fim).to_a
|
||||
return { labels: [], valores: [], qtds: [] } if dias.size > MAX_DIAS_GRAFICO
|
||||
|
||||
contagem = entregas.pagas
|
||||
.group("DATE(planned_date)")
|
||||
.count
|
||||
.transform_keys { |k| k.to_s }
|
||||
.transform_keys(&:to_s)
|
||||
|
||||
labels = dias.map { |d| Date.parse(d).strftime('%d/%m') }
|
||||
valores = dias.map { |d| ((contagem[d] || 0) * preco_entrega).round(2) }
|
||||
qtds = dias.map { |d| contagem[d] || 0 }
|
||||
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 }
|
||||
|
||||
{ labels: labels, valores: valores, qtds: qtds }
|
||||
end
|
||||
|
||||
@@ -10,7 +10,9 @@ class ApplicationPolicy
|
||||
def index? = user.pode_consolidar? || user.admin?
|
||||
def show? = user.pode_consolidar? || user.admin?
|
||||
def create? = user.pode_consolidar? || user.admin?
|
||||
def new? = create?
|
||||
def update? = user.pode_consolidar? || user.admin?
|
||||
def edit? = update?
|
||||
def destroy? = user.admin?
|
||||
|
||||
class Scope
|
||||
|
||||
@@ -32,6 +32,19 @@ class UserPolicy < ApplicationPolicy
|
||||
admin?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Helpers de papel usados pelas permissões acima (delegam para o User)
|
||||
def admin?
|
||||
user.admin?
|
||||
end
|
||||
|
||||
def admin_ou_gerente?
|
||||
user.admin? || user.gerente?
|
||||
end
|
||||
|
||||
public
|
||||
|
||||
class Scope < Scope
|
||||
def resolve
|
||||
if user.admin?
|
||||
|
||||
@@ -3,39 +3,27 @@
|
||||
|
||||
<div class="space-y-6">
|
||||
|
||||
<%# Header + filtro de mês %>
|
||||
<%# Header + filtro por faixa de datas %>
|
||||
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-white">Dashboard</h1>
|
||||
<p class="text-gray-400 text-sm mt-0.5">
|
||||
<%= @data_referencia.strftime('%B de %Y').capitalize %> · Atualizado às <%= Time.now.strftime('%H:%M') %>
|
||||
<%= @periodo_inicio.strftime('%d/%m/%Y') %> – <%= @periodo_fim.strftime('%d/%m/%Y') %>
|
||||
· Atualizado às <%= Time.now.strftime('%H:%M') %>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<%# Navegação de mês %>
|
||||
<div class="flex items-center gap-2">
|
||||
<%= link_to dashboard_path(data: (@data_referencia - 1.month).to_s),
|
||||
class: 'p-2.5 bg-[#1a1a1a] hover:bg-white/10 border border-white/10 rounded-xl
|
||||
text-gray-400 hover:text-white transition-colors' do %>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
|
||||
</svg>
|
||||
<% end %>
|
||||
|
||||
<div class="px-4 py-2.5 bg-[#1a1a1a] border border-white/10 rounded-xl text-white font-medium min-w-[140px] text-center">
|
||||
<%= @data_referencia.strftime('%b %Y').capitalize %>
|
||||
<%# Filtro de calendário — seleciona uma faixa de datas %>
|
||||
<form id="dashboard-filtro-form" method="get" action="<%= dashboard_path %>" class="flex items-center gap-2">
|
||||
<div class="relative">
|
||||
<span class="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 text-orange-500">📅</span>
|
||||
<input type="text" id="periodo-range" readonly placeholder="Selecione o período"
|
||||
class="cursor-pointer pl-4 pr-10 py-2.5 bg-[#1a1a1a] border border-white/10 rounded-xl
|
||||
text-white text-sm w-[240px] focus:outline-none focus:border-orange-500 placeholder-gray-500">
|
||||
</div>
|
||||
|
||||
<% unless @data_referencia >= Date.today.beginning_of_month %>
|
||||
<%= link_to dashboard_path(data: (@data_referencia + 1.month).to_s),
|
||||
class: 'p-2.5 bg-[#1a1a1a] hover:bg-white/10 border border-white/10 rounded-xl
|
||||
text-gray-400 hover:text-white transition-colors' do %>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
||||
</svg>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
<input type="hidden" name="inicio" id="inicio-hidden" value="<%= @periodo_inicio.strftime('%Y-%m-%d') %>">
|
||||
<input type="hidden" name="fim" id="fim-hidden" value="<%= @periodo_fim.strftime('%Y-%m-%d') %>">
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<%# CARDS GRANDES — KPIs %>
|
||||
@@ -81,7 +69,7 @@
|
||||
<div class="bg-[#1a1a1a] rounded-2xl border border-white/5 p-6">
|
||||
<p class="text-gray-400 text-sm font-medium mb-2">🚗 Motoristas</p>
|
||||
<p class="text-4xl font-black text-white mb-1"><%= @motoristas.count %></p>
|
||||
<p class="text-gray-500 text-sm">com entregas no mês</p>
|
||||
<p class="text-gray-500 text-sm">com entregas no período</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -93,7 +81,7 @@
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold text-white">Evolução Diária</h2>
|
||||
<p class="text-gray-400 text-sm">Valor estimado por dia no mês</p>
|
||||
<p class="text-gray-400 text-sm">Valor estimado por dia no período</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-xs text-gray-500">
|
||||
<span class="inline-block w-3 h-0.5 bg-[#f97316] rounded"></span> Valor (R$)
|
||||
@@ -137,7 +125,7 @@
|
||||
<% else %>
|
||||
<div class="text-center py-8 text-gray-500">
|
||||
<div class="text-3xl mb-2">🚚</div>
|
||||
<p class="text-sm">Nenhuma entrega paga neste mês</p>
|
||||
<p class="text-sm">Nenhuma entrega paga neste período</p>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
@@ -162,6 +150,41 @@
|
||||
|
||||
</div>
|
||||
|
||||
<%# Flatpickr — date range picker (CDN, mesmo padrão do Chart.js) %>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr@4.6.13/dist/flatpickr.min.css">
|
||||
<script src="https://cdn.jsdelivr.net/npm/flatpickr@4.6.13/dist/flatpickr.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/flatpickr@4.6.13/dist/l10n/pt.js"></script>
|
||||
<script>
|
||||
(function () {
|
||||
const input = document.getElementById('periodo-range');
|
||||
if (!input || typeof flatpickr === 'undefined') return;
|
||||
|
||||
const inicio = document.getElementById('inicio-hidden').value;
|
||||
const fim = document.getElementById('fim-hidden').value;
|
||||
|
||||
// formata Date local → YYYY-MM-DD (evita deslocamento de fuso do toISOString)
|
||||
const toISO = (d) =>
|
||||
d.getFullYear() + '-' +
|
||||
String(d.getMonth() + 1).padStart(2, '0') + '-' +
|
||||
String(d.getDate()).padStart(2, '0');
|
||||
|
||||
flatpickr(input, {
|
||||
mode: 'range',
|
||||
locale: 'pt',
|
||||
dateFormat: 'd/m/Y',
|
||||
defaultDate: [inicio, fim],
|
||||
maxDate: 'today',
|
||||
onClose: function (selectedDates) {
|
||||
if (selectedDates.length === 2) {
|
||||
document.getElementById('inicio-hidden').value = toISO(selectedDates[0]);
|
||||
document.getElementById('fim-hidden').value = toISO(selectedDates[1]);
|
||||
document.getElementById('dashboard-filtro-form').requestSubmit();
|
||||
}
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
<%# Chart.js via CDN %>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
|
||||
<%# Footer da sidebar — sair %>
|
||||
<div class="px-6 py-4 border-t border-[#2a2a2a]">
|
||||
<%= link_to destroy_user_session_path, method: :delete,
|
||||
<%= link_to destroy_user_session_path, data: { turbo_method: :delete, turbo_confirm: 'Deseja sair?' },
|
||||
class: "flex items-center gap-2 text-gray-400 hover:text-red-400 text-sm transition-colors" do %>
|
||||
<span>🚪</span> Sair
|
||||
<% end %>
|
||||
|
||||
Reference in New Issue
Block a user