diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb index c1e8bd5..72ee361 100644 --- a/app/controllers/dashboard_controller.rb +++ b/app/controllers/dashboard_controller.rb @@ -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 diff --git a/app/policies/application_policy.rb b/app/policies/application_policy.rb index 32a7560..b48178e 100644 --- a/app/policies/application_policy.rb +++ b/app/policies/application_policy.rb @@ -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 diff --git a/app/policies/user_policy.rb b/app/policies/user_policy.rb index 6e29731..12552eb 100644 --- a/app/policies/user_policy.rb +++ b/app/policies/user_policy.rb @@ -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? diff --git a/app/views/dashboard/index.html.erb b/app/views/dashboard/index.html.erb index f833ac3..070695b 100644 --- a/app/views/dashboard/index.html.erb +++ b/app/views/dashboard/index.html.erb @@ -3,39 +3,27 @@
- <%# Header + filtro de mês %> + <%# Header + filtro por faixa de datas %>

Dashboard

- <%= @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') %>

- <%# Navegação de mês %> -
- <%= 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 %> - - - - <% end %> - -
- <%= @data_referencia.strftime('%b %Y').capitalize %> + <%# Filtro de calendário — seleciona uma faixa de datas %> +
+
+ 📅 +
- - <% 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 %> - - - - <% end %> - <% end %> -
+ + +
<%# CARDS GRANDES — KPIs %> @@ -81,7 +69,7 @@

🚗 Motoristas

<%= @motoristas.count %>

-

com entregas no mês

+

com entregas no período

@@ -93,7 +81,7 @@

Evolução Diária

-

Valor estimado por dia no mês

+

Valor estimado por dia no período

Valor (R$) @@ -137,7 +125,7 @@ <% else %>
🚚
-

Nenhuma entrega paga neste mês

+

Nenhuma entrega paga neste período

<% end %>
@@ -162,6 +150,41 @@
+<%# Flatpickr — date range picker (CDN, mesmo padrão do Chart.js) %> + + + + + <%# Chart.js via CDN %> diff --git a/app/views/layouts/_navbar.html.erb b/app/views/layouts/_navbar.html.erb index f3ff62d..85a7d0f 100644 --- a/app/views/layouts/_navbar.html.erb +++ b/app/views/layouts/_navbar.html.erb @@ -60,7 +60,7 @@ <%# Footer da sidebar — sair %>
- <%= 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 %> 🚪 Sair <% end %> diff --git a/spec/policies/consolidacao_policy_spec.rb b/spec/policies/consolidacao_policy_spec.rb index dafd43e..1ea611c 100644 --- a/spec/policies/consolidacao_policy_spec.rb +++ b/spec/policies/consolidacao_policy_spec.rb @@ -6,7 +6,7 @@ RSpec.describe ConsolidacaoPolicy do let(:consolidacao) { build(:consolidacao, status: :rascunho) } let(:consolidacao_finalizada) { build(:consolidacao, status: :finalizada) } - permissions :index?, :show?, :create? do + permissions :index?, :show?, :create?, :new? do it 'libera para admin, gerente e operador' do %i[admin gerente operador].each do |papel| expect(subject).to permit(build(papel), Consolidacao) diff --git a/spec/policies/user_policy_spec.rb b/spec/policies/user_policy_spec.rb new file mode 100644 index 0000000..29dc0ec --- /dev/null +++ b/spec/policies/user_policy_spec.rb @@ -0,0 +1,40 @@ +require 'rails_helper' + +RSpec.describe UserPolicy do + subject { described_class } + + let(:outro) { build(:operador) } + + permissions :index? do + it 'libera para admin e gerente, nega operador/motorista' do + expect(subject).to permit(build(:admin), User) + expect(subject).to permit(build(:gerente), User) + expect(subject).not_to permit(build(:operador), User) + expect(subject).not_to permit(build(:motorista), User) + end + end + + permissions :new?, :create? do + it 'só admin cria usuários' do + expect(subject).to permit(build(:admin), User) + expect(subject).not_to permit(build(:gerente), User) + end + end + + permissions :update? do + it 'admin edita qualquer um; cada um edita a si mesmo' do + eu = build(:operador) + expect(subject).to permit(build(:admin), outro) + expect(subject).to permit(eu, eu) + expect(subject).not_to permit(build(:operador), outro) + end + end + + permissions :destroy? do + it 'admin exclui outros, mas não a si mesmo' do + admin = build(:admin) + expect(subject).to permit(admin, outro) + expect(subject).not_to permit(admin, admin) + end + end +end