From 7a8ec1bc165e01e03e99ca3cdb08eaf1d116f8e8c9df2b96b3c795ca38906cac Mon Sep 17 00:00:00 2001 From: victor Date: Wed, 26 Aug 2026 15:15:44 -0300 Subject: [PATCH] =?UTF-8?q?Cria=C3=A7=C3=A3o=20da=20vis=C3=A3o=20geral=20d?= =?UTF-8?q?a=20consolida=C3=A7=C3=A3o=20por=20motorista=20e=20Estiliza?= =?UTF-8?q?=C3=A7=C3=A3o?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/controllers/consolidacoes_controller.rb | 62 +++--- .../motorista/dashboard_controller.rb | 130 ++++++------ .../analytics/totais_por_motorista.rb | 31 +-- app/views/consolidacoes/_lista.html.erb | 38 ++++ .../consolidacoes/_por_motorista.html.erb | 128 ++++++++++++ app/views/consolidacoes/index.html.erb | 92 ++++----- app/views/consolidacoes/totais.html.erb | 191 ------------------ app/views/layouts/_navbar.html.erb | 2 - app/views/motorista/dashboard/index.html.erb | 174 ++++++++++------ app/views/motorista/dashboard/totais.html.erb | 95 --------- config/routes.rb | 12 +- spec/requests/motorista_dashboard_spec.rb | 6 +- spec/requests/totais_por_motorista_spec.rb | 52 +++-- .../analytics/totais_por_motorista_spec.rb | 20 +- 14 files changed, 508 insertions(+), 525 deletions(-) create mode 100644 app/views/consolidacoes/_lista.html.erb create mode 100644 app/views/consolidacoes/_por_motorista.html.erb delete mode 100644 app/views/consolidacoes/totais.html.erb delete mode 100644 app/views/motorista/dashboard/totais.html.erb diff --git a/app/controllers/consolidacoes_controller.rb b/app/controllers/consolidacoes_controller.rb index 30a09fa..c988e79 100644 --- a/app/controllers/consolidacoes_controller.rb +++ b/app/controllers/consolidacoes_controller.rb @@ -13,10 +13,21 @@ class ConsolidacoesController < ApplicationController # Formas de pagamento aceitas (o select da UI usa esta mesma lista). FORMAS_PAGAMENTO = %w[pix transferencia dinheiro].freeze - # GET /consolidacoes — lista com filtros + # GET /consolidacoes?visao=lista|motoristas — a MESMA tela em dois modos de + # visualização, com os mesmos filtros: + # + # lista → um card por CONSOLIDAÇÃO (o de sempre); + # motoristas → um card por MOTORISTA, com o total dele no período e, ao + # clicar, as consolidações que compõem aquele valor. + # + # São duas leituras do mesmo recorte: o trabalho é organizado por consolidação, + # mas o pagamento é por motorista — antes, para saber quanto um motorista tinha + # fechado no mês, era preciso abrir consolidação por consolidação e somar à mão. def index authorize Consolidacao + @visao = params[:visao] == 'motoristas' ? 'motoristas' : 'lista' + @consolidacoes = Consolidacao.ativas.recentes.includes(:criador) # Filtros @@ -38,42 +49,25 @@ class ConsolidacoesController < ApplicationController if %w[pago pendente parcial].include?(params[:pagamento]) @consolidacoes = @consolidacoes.public_send("pagamento_#{params[:pagamento]}") end + + montar_totais if @visao == 'motoristas' end - # GET /consolidacoes/totais?inicio=&fim=&motorista=&rascunhos=1 - # - # "Quanto cada motorista somou no período". A lista de consolidações mostra um - # card por CONSOLIDAÇÃO, mas o pagamento é por MOTORISTA: para saber quanto um - # motorista fechou no mês era preciso abrir consolidação por consolidação e - # somar à mão. Aqui o total vem pronto e, clicando no motorista (?motorista=), - # a tela abre as consolidações que compõem aquele valor. - def totais - authorize Consolidacao, :index? - - @inicio, @fim = periodo_totais - @incluir_rascunhos = params[:rascunhos] == '1' - @motorista = params[:motorista].presence - @totais = Analytics::TotaisPorMotorista.new( - inicio: @inicio, fim: @fim, incluir_rascunhos: @incluir_rascunhos - ) - # Drill-down: as consolidações do motorista clicado (a tabela geral continua - # na tela, então o service é montado sem recorte de motorista). - @detalhe = @motorista ? @totais.consolidacoes_de(@motorista) : nil - end - - # GET /consolidacoes/totais_pdf — o mesmo relatório em PDF. Com ?motorista= - # sai só o extrato daquele motorista (com espaço de assinatura). + # GET /consolidacoes/totais_pdf — o modo "Por motorista" em PDF, com os mesmos + # filtros da tela. Com ?detalhe=NOME sai só o extrato daquele motorista (com + # espaço de assinatura). def totais_pdf authorize Consolidacao, :index? inicio, fim = periodo_totais totais = Analytics::TotaisPorMotorista.new( inicio: inicio, fim: fim, - motorista: params[:motorista].presence, - incluir_rascunhos: params[:rascunhos] == '1' + motorista: params[:detalhe].presence, + status: params[:status].presence || 'todos', + busca: params[:motorista].presence ) - nome = params[:motorista].presence&.parameterize(separator: '_') + nome = params[:detalhe].presence&.parameterize(separator: '_') arquivo = ['totais', nome, inicio.strftime('%Y%m%d'), fim.strftime('%Y%m%d')].compact.join('_') send_data Pdf::TotaisMotoristaPdf.new(totais: totais, detalhar: params[:detalhar] != '0').render, filename: "#{arquivo}.pdf", @@ -490,6 +484,20 @@ class ConsolidacoesController < ApplicationController private + # Modo "Por motorista": os totais saem do MESMO recorte da lista (período, + # status e busca por motorista), para as duas visões nunca contarem coisas + # diferentes. `detalhe` é o motorista aberto (o clique na linha). + def montar_totais + @inicio, @fim = periodo_totais + @totais = Analytics::TotaisPorMotorista.new( + inicio: @inicio, fim: @fim, + status: params[:status].presence || 'todos', + busca: params[:motorista].presence + ) + @detalhe_motorista = params[:detalhe].presence + @detalhe = @detalhe_motorista ? @totais.consolidacoes_de(@detalhe_motorista) : nil + end + # Período do relatório de totais. Default: o MÊS CORRENTE INTEIRO — o fim é o # último dia do mês, não "hoje", porque o recorte exige a consolidação inteira # dentro da faixa (Consolidacao.no_periodo) e um fechamento que vai até 31/08 diff --git a/app/controllers/motorista/dashboard_controller.rb b/app/controllers/motorista/dashboard_controller.rb index 86bb26c..828a024 100644 --- a/app/controllers/motorista/dashboard_controller.rb +++ b/app/controllers/motorista/dashboard_controller.rb @@ -3,47 +3,24 @@ module Motorista class DashboardController < ApplicationController before_action :garantir_motorista - # GET /motorista + # GET /motorista?inicio=&fim= + # + # UMA tela só: o motorista escolhe o período e vê, para aquele recorte, o que + # já foi FECHADO pela empresa, o que ainda é ESTIMATIVA e de onde vem cada + # centavo (os fechamentos que compõem o total). Antes o total ficava numa + # tela e a composição em outra — e nenhuma das duas tinha período. def index nome = current_user.nome - # Card 1 — Valor ESTIMADO (mês atual): entregas ATENDIDAS × preco_entrega. - # - # Atendida = o motorista FOI ao local e fechou a visita, com sucesso - # ('completed') OU insucesso ('failed') — no insucesso o deslocamento - # aconteceu e teve custo, então a empresa paga os dois. É o MESMO recorte do - # dashboard administrativo e da consolidação (ver Entrega::STATUS_ATENDIDO e - # Entrega.contar_atendidas): usava `pagas` (só concluídas) e o motorista via - # um valor MENOR que o do fechamento, sem entender de onde vinha a diferença. - # - # ⚠️ O eixo é o CHECKOUT (data real da visita), não planned_date: falhada TEM - # checkout, e uma entrega planejada em 31/07 mas fechada em 01/08 pertence a - # agosto — igual ao que a consolidação considera elegível. - @estimado_inicio = Date.current.beginning_of_month - @estimado_fim = Date.current - atendidas = Entrega.atendidas.da_conta_gade - .do_motorista(nome) - .no_periodo_checkout(@estimado_inicio, @estimado_fim) - @entregas_concluidas = atendidas.concluidas.count - @entregas_falhadas = atendidas.falhadas.count - @entregas_mes = @entregas_concluidas + @entregas_falhadas - @valor_estimado = @entregas_mes * Configuracao.preco_entrega + @inicio, @fim = periodo_selecionado + @faixa_total = faixa_completa(nome) # para o atalho "Tudo" - # Card 2 — Valor CONSOLIDADO: soma das consolidações finalizadas - @minhas_consolidacoes = Consolidacao.ativas - .joins(:consolidacao_motoristas) - .where(consolidacao_motoristas: { motorista_nome: nome, arquivado_em: nil }) - .recentes - .distinct - - @valor_consolidado = ConsolidacaoMotorista.ativos - .joins(:consolidacao) - .where(motorista_nome: nome, - consolidacoes: { status: :finalizada, deleted_at: nil }) - .sum(:valor_total) + carregar_estimado(nome) + carregar_fechado(nome) # Aviso in-app: pagamentos confirmados nos últimos 7 dias (notificação na - # própria página do motorista, além de WhatsApp/email). + # própria página do motorista, além de WhatsApp/email). Independe do + # período escolhido — é um aviso, não um relatório. @pagamentos_recentes = ConsolidacaoMotorista .joins(:consolidacao) .where(motorista_nome: nome, @@ -54,23 +31,11 @@ module Motorista .includes(:consolidacao) end - # GET /motorista/totais?inicio=&fim= - # - # "Quanto eu fechei neste período e de onde vem esse valor". O card do painel - # mostra só o total acumulado; aqui o motorista escolhe o período, vê o total - # FECHADO (consolidações finalizadas) e a lista de consolidações que compõem - # o valor — a mesma conta que o admin vê em /consolidacoes/totais, montada - # pelo mesmo service para os dois números não poderem divergir. - def totais - @inicio, @fim = periodo_totais - @totais = totais_do_periodo - @consolidacoes = @totais.consolidacoes_de(current_user.nome) - end - - # GET /motorista/totais/pdf — o extrato do período em PDF (com assinatura), - # para o motorista guardar ou mandar por WhatsApp. + # GET /motorista/totais/pdf?inicio=&fim= — o extrato do período (total, + # composição e assinaturas), para guardar ou mandar por WhatsApp. Mesmo + # service da tela, então o papel e o painel nunca divergem. def totais_pdf - inicio, fim = periodo_totais + inicio, fim = periodo_selecionado totais = Analytics::TotaisPorMotorista.new( inicio: inicio, fim: fim, motorista: current_user.nome ) @@ -85,16 +50,65 @@ module Motorista private - def totais_do_periodo - Analytics::TotaisPorMotorista.new( - inicio: @inicio, fim: @fim, motorista: current_user.nome - ) + # Card ESTIMADO: entregas ATENDIDAS × preco_entrega dentro do período. + # + # Atendida = o motorista FOI ao local e fechou a visita, com sucesso + # ('completed') OU insucesso ('failed') — no insucesso o deslocamento + # aconteceu e teve custo, então a empresa paga os dois. É o MESMO recorte do + # dashboard administrativo e da consolidação (ver Entrega::STATUS_ATENDIDO e + # Entrega.contar_atendidas): usava `pagas` (só concluídas) e o motorista via + # um valor MENOR que o do fechamento, sem entender de onde vinha a diferença. + # + # ⚠️ O eixo é o CHECKOUT (data real da visita), não planned_date: falhada TEM + # checkout, e uma entrega planejada em 31/07 mas fechada em 01/08 pertence a + # agosto — igual ao que a consolidação considera elegível. + # + # O fim é limitado a HOJE: estimar entregas de dias que ainda não + # aconteceram não faz sentido (o período pode ir até o fim do mês). + def carregar_estimado(nome) + @estimado_inicio = @inicio + @estimado_fim = [@fim, Date.current].min + + if @estimado_fim < @estimado_inicio + @entregas_concluidas = @entregas_falhadas = @entregas_mes = 0 + @valor_estimado = 0 + return + end + + atendidas = Entrega.atendidas.da_conta_gade + .do_motorista(nome) + .no_periodo_checkout(@estimado_inicio, @estimado_fim) + @entregas_concluidas = atendidas.concluidas.count + @entregas_falhadas = atendidas.falhadas.count + @entregas_mes = @entregas_concluidas + @entregas_falhadas + @valor_estimado = @entregas_mes * Configuracao.preco_entrega end - # Default: mês corrente INTEIRO (fim = último dia do mês). O recorte pede a + # Card FECHADO + composição: o mesmo service do modo "Por motorista" do + # admin, recortado neste motorista — os dois lados mostram o mesmo número. + def carregar_fechado(nome) + @totais = Analytics::TotaisPorMotorista.new(inicio: @inicio, fim: @fim, motorista: nome) + @consolidacoes = @totais.consolidacoes_de(nome) + @valor_consolidado = @totais.total_geral + end + + # Primeira e última data de fechamento do motorista — alimenta o atalho + # "Tudo" com datas reais em vez de uma faixa inventada. + def faixa_completa(nome) + escopo = Consolidacao.ativas.finalizada + .joins(:consolidacao_motoristas) + .where(consolidacao_motoristas: { motorista_nome: nome, arquivado_em: nil }) + inicio = escopo.minimum(:data_inicio) + fim = escopo.maximum(:data_fim) + return nil if inicio.nil? || fim.nil? + + [inicio.to_date, fim.to_date] + end + + # Default: mês corrente INTEIRO (fim = último dia do mês). O recorte exige a # consolidação inteira dentro da faixa, então terminar em "hoje" esconderia - # um fechamento que vai até o fim do mês. Mesma regra do relatório do admin. - def periodo_totais + # um fechamento que vai até o fim do mês. Mesma regra do lado do admin. + def periodo_selecionado inicio = parse_data(params[:inicio]) || Date.current.beginning_of_month fim = parse_data(params[:fim]) || Date.current.end_of_month inicio, fim = fim, inicio if fim < inicio diff --git a/app/services/analytics/totais_por_motorista.rb b/app/services/analytics/totais_por_motorista.rb index 3112401..dc1b1ec 100644 --- a/app/services/analytics/totais_por_motorista.rb +++ b/app/services/analytics/totais_por_motorista.rb @@ -3,8 +3,9 @@ # 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) +# • 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 (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 @@ -17,30 +18,36 @@ # 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 + # 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 + 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.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 + # Por padrão 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 traduz :finalizada é o model. + base = base.public_send(@status) if %w[finalizada rascunho].include?(@status) escopo = ConsolidacaoMotorista.ativos .joins(:consolidacao) .merge(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 diff --git a/app/views/consolidacoes/_lista.html.erb b/app/views/consolidacoes/_lista.html.erb new file mode 100644 index 0000000..a4c4307 --- /dev/null +++ b/app/views/consolidacoes/_lista.html.erb @@ -0,0 +1,38 @@ +<%# app/views/consolidacoes/_lista.html.erb — visão por CONSOLIDAÇÃO %> +<% if @consolidacoes.any? %> +
+ <% @consolidacoes.each do |c| %> + <%= link_to consolidacao_path(c), + class: 'block bg-[#1a1a1a] border border-[#2a2a2a] hover:border-orange-500 rounded-xl p-5 transition-colors' do %> +
+
+
+

<%= c.nome %>

+ <%= badge_status(c.status) %> + <% if c.finalizada? %><%= badge_pagamento(c.status_pagamento) %><% end %> +
+

+ <%= icone :calendario %> <%= l c.data_inicio, format: :short %> → <%= l c.data_fim, format: :short %> + · <%= icone :pessoa %> <%= c.consolidacao_motoristas.count %> motorista(s) + · Criada por <%= c.criador.nome_display %> +

+ <% if c.operacoes.present? %> +

<%= icone :pasta %> <%= c.operacoes_label.join(' · ') %>

+ <% end %> +
+
+

<%= moeda(c.valor_total) %>

+

<%= c.consolidacao_entregas.count %> entregas classificadas

+
+
+ <% end %> + <% end %> +
+<% else %> +
+

<%= icone :vazio, tamanho: 'w-12 h-12', cor: 'text-gray-600', espaco: false %>

+

Nenhuma consolidação encontrada.

+ <%= link_to 'Criar primeira consolidação', new_consolidacao_path, + class: 'inline-block bg-orange-500 hover:bg-orange-600 text-black font-bold px-5 py-3 rounded-lg' %> +
+<% end %> diff --git a/app/views/consolidacoes/_por_motorista.html.erb b/app/views/consolidacoes/_por_motorista.html.erb new file mode 100644 index 0000000..27408cd --- /dev/null +++ b/app/views/consolidacoes/_por_motorista.html.erb @@ -0,0 +1,128 @@ +<%# app/views/consolidacoes/_por_motorista.html.erb — visão por MOTORISTA + Mesmo recorte da lista, somado por quem recebe. Clicar num motorista abre, + ali mesmo (sem sair da tela), as consolidações que compõem o valor dele. %> + +<%# Link desta mesma tela preservando filtros — usado pelas linhas e pelos PDFs. %> +<% ctx = { nome: params[:nome], motorista: params[:motorista], status: params[:status], + inicio: @inicio.strftime('%Y-%m-%d'), fim: @fim.strftime('%Y-%m-%d'), + visao: 'motoristas' }.compact_blank %> + +<%# ── Resumo do período ────────────────────────────────── %> +
+
+

<%= icone :dinheiro %> Total do período

+

<%= moeda(@totais.total_geral) %>

+

<%= @totais.total_consolidacoes %> consolidação(ões) · <%= @totais.linhas.size %> motorista(s)

+
+
+

<%= icone :sucesso, cor: 'text-green-400' %> Pago

+

<%= moeda(@totais.total_pago) %>

+
+
+

<%= icone :parcial, cor: 'text-amber-400' %> A pagar

+

<%= moeda(@totais.total_pendente) %>

+
+
+

<%= icone :documento %> Relatório

+
+ <%= link_to rotulo(:baixar, 'PDF completo', cor: nil), + totais_pdf_consolidacoes_path(ctx.except(:visao)), + data: { turbo: false }, + class: 'bg-orange-500 hover:bg-orange-600 text-black font-bold text-sm px-3 py-2 rounded-lg flex items-center justify-center' %> + <%= link_to 'Só o resumo', + totais_pdf_consolidacoes_path(ctx.except(:visao).merge(detalhar: '0')), + data: { turbo: false }, + class: 'text-gray-400 hover:text-orange-500 text-xs underline text-center' %> +
+
+
+ +<% if @totais.vazio? %> +
+

<%= icone :vazio, tamanho: 'w-12 h-12', cor: 'text-gray-600', espaco: false %>

+

Nenhuma consolidação com período dentro de <%= l @inicio, format: :short %> → <%= l @fim, format: :short %>.

+

Ajuste as datas acima — o recorte pega a consolidação inteira dentro da faixa.

+
+<% else %> +
+ <% @totais.linhas.each do |linha| %> + <% aberto = @detalhe_motorista == linha[:motorista] %> +
+ + <%# Cabeçalho do motorista — o clique abre/fecha a composição %> + <%= link_to consolidacoes_path(ctx.merge(detalhe: (aberto ? nil : linha[:motorista])).compact_blank), + class: 'block p-5' do %> +
+
+
+ <%= icone(aberto ? :expandir : :avancar, cor: 'text-gray-500', espaco: false) %> +

<%= linha[:motorista] %>

+
+

+ <%= icone :consolidacoes %> <%= linha[:consolidacoes] %> consolidação(ões) + · <%= icone :caminhao %> <%= linha[:lancamentos] %> lançamentos + <% if linha[:pagas].positive? %>· <%= linha[:pagas] %> paga(s)<% end %> + <% if linha[:pendentes].positive? %>· <%= linha[:pendentes] %> a pagar<% end %> +

+
+
+

<%= moeda(linha[:valor_total]) %>

+

+ <%= moeda(linha[:valor_pago]) %> pago + · + <%= moeda(linha[:valor_pendente]) %> a pagar +

+
+
+ <% end %> + + <%# Composição — o que forma o valor daquele motorista %> + <% if aberto %> +
+
+

+ <%= icone :composicao, espaco: false %> O que forma esse valor +

+ <%= link_to rotulo(:baixar, 'PDF deste motorista', cor: nil), + totais_pdf_consolidacoes_path(ctx.except(:visao).merge(detalhe: linha[:motorista])), + data: { turbo: false }, + class: 'text-gray-300 hover:text-orange-500 text-xs border border-[#2a2a2a] hover:border-orange-500 rounded-lg px-3 py-2 flex items-center' %> +
+ +
+ <% @detalhe.each do |cm| %> + <% c = cm.consolidacao %> + <%= link_to consolidacao_path(c), + class: 'flex flex-col md:flex-row md:items-center md:justify-between gap-2 bg-[#1a1a1a] border border-[#2a2a2a] hover:border-orange-500 rounded-lg px-4 py-3 transition-colors' do %> +
+
+ <%= c.nome %> + <%= badge_status(c.status) %> + <%= badge_pagamento(cm.pago? ? :pago : :pendente) %> +
+

+ <%= icone :calendario %> <%= l c.data_inicio, format: :short %> → <%= l c.data_fim, format: :short %> + <% if cm.pago? %> + · <%= icone :dinheiro, cor: 'text-green-400' %> Pago em <%= l cm.pago_em.to_date, format: :short %> + <% if cm.forma_pagamento.present? %>via <%= cm.forma_pagamento.humanize %><% end %> + <% if cm.nota_fiscal.present? %>· NF <%= cm.nota_fiscal %><% end %> + <% end %> +

+
+

<%= moeda(cm.valor_total) %>

+ <% end %> + <% end %> +
+
+ <% end %> +
+ <% end %> +
+ +

+ <%= icone :dica %> Entram as consolidações + <%= params[:status].presence ? "com status #{params[:status]}" : 'de qualquer status' %> + cujo período está dentro de <%= l @inicio, format: :short %> → <%= l @fim, format: :short %>. + Motoristas arquivados na consolidação não entram no total. +

+<% end %> diff --git a/app/views/consolidacoes/index.html.erb b/app/views/consolidacoes/index.html.erb index e8eaf82..0dfb198 100644 --- a/app/views/consolidacoes/index.html.erb +++ b/app/views/consolidacoes/index.html.erb @@ -6,8 +6,6 @@

<%= icone :consolidacoes, espaco: false %> Consolidações

- <%= link_to rotulo(:motorista, 'Totais por motorista', cor: nil), totais_consolidacoes_path, - class: 'text-gray-400 hover:text-white border border-[#2a2a2a] px-4 py-3 rounded-lg min-h-[48px] flex items-center transition-colors' %> <%= link_to rotulo(:arquivadas, 'Arquivadas', cor: nil), arquivadas_consolidacoes_path, class: 'text-gray-400 hover:text-white border border-[#2a2a2a] px-4 py-3 rounded-lg min-h-[48px] flex items-center transition-colors' %> <% if policy(Consolidacao).create? %> @@ -72,6 +70,25 @@ <% end %>
+<%# ── Modo de visualização ────────────────────────────── + A mesma tela lida de dois jeitos: por CONSOLIDAÇÃO (como o trabalho é + organizado) e por MOTORISTA (como o pagamento sai). Os filtros abaixo valem + para os dois — por isso eles viajam junto no link da aba. %> +<% ctx_filtros = { nome: params[:nome], motorista: params[:motorista], status: params[:status], + pagamento: params[:pagamento], vehicle: params[:vehicle], + inicio: params[:inicio], fim: params[:fim] }.compact_blank %> +
+ <% [['lista', :consolidacoes, 'Consolidações'], ['motoristas', :motorista, 'Por motorista']].each do |modo, ic, label| %> + <% ativo = @visao == modo %> + <%= link_to rotulo(ic, label, cor: (ativo ? nil : 'text-brand-laranja')), + consolidacoes_path(ctx_filtros.merge(visao: modo)), + class: "px-4 py-2.5 rounded-xl text-sm font-semibold whitespace-nowrap #{ativo ? 'bg-orange-500 text-black' : 'bg-[#1a1a1a] text-gray-300 border border-[#2a2a2a] hover:border-orange-500'}" %> + <% end %> + +
+ <%# ── Filtros ──────────────────────────────────────────── %> <%# Filtros em flex-wrap, NÃO em grade de 6 colunas fixas. Motivo: os breakpoints do Tailwind (md:, lg:…) enxergam a JANELA, mas o @@ -82,66 +99,49 @@ tamanho legível e a linha quebra sozinha conforme o espaço REAL. %> <%= form_with url: consolidacoes_path, method: :get, class: 'bg-[#1a1a1a] border border-[#2a2a2a] rounded-xl p-4 mb-6 flex flex-wrap items-center gap-3' do |f| %> - <%= f.text_field :nome, value: params[:nome], placeholder: 'Buscar por nome…', - class: 'flex-1 basis-48 min-w-0 bg-[#0a0a0a] border border-[#2a2a2a] text-white rounded-lg px-3 py-2.5 focus:border-orange-500 focus:outline-none' %> + <%# O modo de visualização é mantido ao filtrar (senão filtrar jogava o + operador de volta para a lista). %> + <%= hidden_field_tag :visao, @visao %> + + <% if @visao == 'motoristas' %> + <%= f.text_field :motorista, value: params[:motorista], placeholder: 'Buscar motorista…', + class: 'flex-1 basis-48 min-w-0 bg-[#0a0a0a] border border-[#2a2a2a] text-white rounded-lg px-3 py-2.5 focus:border-orange-500 focus:outline-none' %> + <% else %> + <%= f.text_field :nome, value: params[:nome], placeholder: 'Buscar por nome…', + class: 'flex-1 basis-48 min-w-0 bg-[#0a0a0a] border border-[#2a2a2a] text-white rounded-lg px-3 py-2.5 focus:border-orange-500 focus:outline-none' %> + <% end %> <%= f.select :status, options_for_select([['Todos os status', ''], ['Rascunho', 'rascunho'], ['Finalizada', 'finalizada']], params[:status]), {}, class: 'flex-1 basis-40 min-w-0 bg-[#0a0a0a] border border-[#2a2a2a] text-white rounded-lg px-3 py-2.5' %> - <%= f.select :pagamento, - options_for_select([['Pagamento (todos)', ''], ['Pagas', 'pago'], ['Parciais', 'parcial'], ['Pendentes', 'pendente']], params[:pagamento]), - {}, class: 'flex-1 basis-44 min-w-0 bg-[#0a0a0a] border border-[#2a2a2a] text-white rounded-lg px-3 py-2.5' %> + <%# "Pagamento" é status DA CONSOLIDAÇÃO (todos os motoristas dela); no modo + Por motorista cada linha já mostra pago/a receber, então o filtro sairia + confuso — some. %> + <% if @visao != 'motoristas' %> + <%= f.select :pagamento, + options_for_select([['Pagamento (todos)', ''], ['Pagas', 'pago'], ['Parciais', 'parcial'], ['Pendentes', 'pendente']], params[:pagamento]), + {}, class: 'flex-1 basis-44 min-w-0 bg-[#0a0a0a] border border-[#2a2a2a] text-white rounded-lg px-3 py-2.5' %> + <% end %> - <%= f.date_field :inicio, value: params[:inicio], + <%# No modo Por motorista o período SEMPRE vale (default: mês corrente + inteiro), então os campos já vêm preenchidos com o que está sendo somado. %> + <%= f.date_field :inicio, value: (@visao == 'motoristas' ? @inicio.strftime('%Y-%m-%d') : params[:inicio]), class: 'flex-1 basis-36 max-w-[13rem] min-w-0 bg-[#0a0a0a] border border-[#2a2a2a] text-white rounded-lg px-3 py-2.5' %> - <%= f.date_field :fim, value: params[:fim], + <%= f.date_field :fim, value: (@visao == 'motoristas' ? @fim.strftime('%Y-%m-%d') : params[:fim]), class: 'flex-1 basis-36 max-w-[13rem] min-w-0 bg-[#0a0a0a] border border-[#2a2a2a] text-white rounded-lg px-3 py-2.5' %> <%# shrink-0 + whitespace-nowrap: os botões nunca são espremidos a ponto de cortar o texto — se faltar espaço, a linha quebra e eles descem inteiros. %>
<%= f.submit 'Filtrar', class: 'bg-orange-500 hover:bg-orange-600 text-black font-bold rounded-lg cursor-pointer px-5 py-2.5 whitespace-nowrap' %> - <%= link_to 'Limpar', consolidacoes_path, class: 'px-4 py-2.5 text-gray-400 hover:text-white border border-[#2a2a2a] rounded-lg whitespace-nowrap' %> + <%= link_to 'Limpar', consolidacoes_path(visao: @visao), class: 'px-4 py-2.5 text-gray-400 hover:text-white border border-[#2a2a2a] rounded-lg whitespace-nowrap' %>
<% end %> -<%# ── Lista ────────────────────────────────────────────── %> -<% if @consolidacoes.any? %> -
- <% @consolidacoes.each do |c| %> - <%= link_to consolidacao_path(c), - class: 'block bg-[#1a1a1a] border border-[#2a2a2a] hover:border-orange-500 rounded-xl p-5 transition-colors' do %> -
-
-
-

<%= c.nome %>

- <%= badge_status(c.status) %> - <% if c.finalizada? %><%= badge_pagamento(c.status_pagamento) %><% end %> -
-

- <%= icone :calendario %> <%= l c.data_inicio, format: :short %> → <%= l c.data_fim, format: :short %> - · <%= icone :pessoa %> <%= c.consolidacao_motoristas.count %> motorista(s) - · Criada por <%= c.criador.nome_display %> -

- <% if c.operacoes.present? %> -

<%= icone :pasta %> <%= c.operacoes_label.join(' · ') %>

- <% end %> -
-
-

<%= moeda(c.valor_total) %>

-

<%= c.consolidacao_entregas.count %> entregas classificadas

-
-
- <% end %> - <% end %> -
+<% if @visao == 'motoristas' %> + <%= render 'por_motorista' %> <% else %> -
-

<%= icone :vazio, tamanho: 'w-12 h-12', cor: 'text-gray-600', espaco: false %>

-

Nenhuma consolidação encontrada.

- <%= link_to 'Criar primeira consolidação', new_consolidacao_path, - class: 'inline-block bg-orange-500 hover:bg-orange-600 text-black font-bold px-5 py-3 rounded-lg' %> -
+ <%= render 'lista' %> <% end %> diff --git a/app/views/consolidacoes/totais.html.erb b/app/views/consolidacoes/totais.html.erb deleted file mode 100644 index 2e5ad16..0000000 --- a/app/views/consolidacoes/totais.html.erb +++ /dev/null @@ -1,191 +0,0 @@ -<%# app/views/consolidacoes/totais.html.erb — Total por motorista no período %> -<% content_for :title, 'Totais por motorista' %> - -
-
-

<%= icone :motorista, espaco: false %> Totais por motorista

-

- Consolidações <%= @incluir_rascunhos ? 'de qualquer status' : 'finalizadas' %> - com período dentro de <%= l @inicio, format: :short %> → <%= l @fim, format: :short %> -

-
-
- <%= link_to rotulo(:consolidacoes, 'Consolidações', cor: nil), consolidacoes_path, - class: 'text-gray-400 hover:text-white border border-[#2a2a2a] px-4 py-3 rounded-lg min-h-[48px] flex items-center transition-colors' %> - <%= link_to rotulo(:baixar, 'Baixar relatório (PDF)', cor: nil), - totais_pdf_consolidacoes_path(inicio: @inicio, fim: @fim, rascunhos: (@incluir_rascunhos ? '1' : nil), motorista: @motorista), - data: { turbo: false }, - class: 'bg-orange-500 hover:bg-orange-600 text-black font-bold px-5 py-3 rounded-lg min-h-[48px] flex items-center transition-colors' %> -
-
- -<%# ── Filtros ──────────────────────────────────────────── %> -<%= form_with url: totais_consolidacoes_path, method: :get, - class: 'bg-[#1a1a1a] border border-[#2a2a2a] rounded-xl p-4 mb-6 flex flex-wrap items-center gap-3' do |f| %> - <%# O motorista clicado viaja junto para o filtro não fechar o drill-down. %> - <%= hidden_field_tag :motorista, @motorista if @motorista %> - - <%= f.date_field :inicio, value: @inicio.strftime('%Y-%m-%d'), - class: 'flex-1 basis-36 max-w-[13rem] min-w-0 bg-[#0a0a0a] border border-[#2a2a2a] text-white rounded-lg px-3 py-2.5' %> - <%= f.date_field :fim, value: @fim.strftime('%Y-%m-%d'), - class: 'flex-1 basis-36 max-w-[13rem] min-w-0 bg-[#0a0a0a] border border-[#2a2a2a] text-white rounded-lg px-3 py-2.5' %> - - - -
- <%= f.submit 'Filtrar', class: 'bg-orange-500 hover:bg-orange-600 text-black font-bold rounded-lg cursor-pointer px-5 py-2.5 whitespace-nowrap' %> - <%= link_to 'Limpar', totais_consolidacoes_path, class: 'px-4 py-2.5 text-gray-400 hover:text-white border border-[#2a2a2a] rounded-lg whitespace-nowrap' %> -
-<% end %> - -<%# ── KPIs do período ──────────────────────────────────── %> -
-
-

<%= icone :dinheiro %> Total do período

-

<%= moeda(@totais.total_geral) %>

-

<%= @totais.total_consolidacoes %> consolidação(ões) · <%= @totais.linhas.size %> motorista(s)

-
-
-

<%= icone :sucesso, cor: 'text-green-400' %> Pago

-

<%= moeda(@totais.total_pago) %>

-
-
-

<%= icone :parcial, cor: 'text-amber-400' %> A pagar

-

<%= moeda(@totais.total_pendente) %>

-
-
-

<%= icone :ticket %> Média por motorista

-

- <%= moeda(@totais.linhas.any? ? @totais.total_geral / @totais.linhas.size : 0) %> -

-
-
- -<% if @totais.vazio? %> -
-

<%= icone :vazio, tamanho: 'w-12 h-12', cor: 'text-gray-600', espaco: false %>

-

Nenhuma consolidação <%= @incluir_rascunhos ? '' : 'finalizada ' %>com período dentro dessa faixa.

-

Amplie o período ou marque "Incluir rascunhos".

-
-<% else %> - <%# ── Tabela: total por motorista (clique abre a composição) ── %> -
-
- - - - - - - - - - - - - <% @totais.linhas.each do |l| %> - <% aberto = @motorista == l[:motorista] %> - - <%# O nome é um LINK de verdade (e não um ): abre/fecha - a composição daquele motorista e continua acessível pelo teclado. %> - - - - - - - - <% end %> - - - - - - - - - - - -
MotoristaConsolidaçõesLançamentosPagoA pagarTotal
- <%= link_to totais_consolidacoes_path(inicio: @inicio, fim: @fim, - rascunhos: (@incluir_rascunhos ? '1' : nil), - motorista: (aberto ? nil : l[:motorista])), - class: 'text-white font-semibold hover:text-orange-500 flex items-center gap-1' do %> - <%= icone(aberto ? :expandir : :avancar, cor: 'text-gray-500', espaco: false) %><%= l[:motorista] %> - <% end %> - <%= l[:consolidacoes] %><%= l[:lancamentos] %><%= moeda(l[:valor_pago]) %><%= moeda(l[:valor_pendente]) %><%= moeda(l[:valor_total]) %>
TOTAL<%= @totais.total_consolidacoes %><%= moeda(@totais.total_pago) %><%= moeda(@totais.total_pendente) %><%= moeda(@totais.total_geral) %>
-
-
-

- <%= icone :dica %> Clique num motorista para ver as consolidações que compõem o total dele. -

- <%# PDF sem a composição de cada motorista — para períodos com muita gente, - quando só o resumo interessa. %> - <%= link_to 'Baixar só o resumo (PDF)', - totais_pdf_consolidacoes_path(inicio: @inicio, fim: @fim, rascunhos: (@incluir_rascunhos ? '1' : nil), detalhar: '0'), - data: { turbo: false }, - class: 'text-gray-400 hover:text-orange-500 text-xs underline whitespace-nowrap' %> -
-
- - <%# ── Drill-down: as consolidações que compõem o total ── %> - <% if @detalhe %> -
-
-
-

- <%= icone :composicao, espaco: false %> Composição de <%= @motorista %> -

-

- <%= @detalhe.size %> consolidação(ões) no período · - <%= moeda(@detalhe.sum { |cm| cm.valor_total || 0 }) %> -

-
-
- <%= link_to rotulo(:baixar, 'PDF deste motorista', cor: nil), - totais_pdf_consolidacoes_path(inicio: @inicio, fim: @fim, rascunhos: (@incluir_rascunhos ? '1' : nil), motorista: @motorista), - data: { turbo: false }, - class: 'bg-orange-500 hover:bg-orange-600 text-black font-bold px-4 py-2.5 rounded-lg flex items-center' %> - <%= link_to rotulo(:fechar, 'Fechar', cor: nil), - totais_consolidacoes_path(inicio: @inicio, fim: @fim, rascunhos: (@incluir_rascunhos ? '1' : nil)), - class: 'text-gray-400 hover:text-white border border-[#2a2a2a] px-4 py-2.5 rounded-lg flex items-center' %> -
-
- - <% if @detalhe.empty? %> -

Nenhuma consolidação desse motorista neste período.

- <% end %> - -
- <% @detalhe.each do |cm| %> - <% c = cm.consolidacao %> - <%= link_to consolidacao_path(c), - class: 'block bg-[#0a0a0a] border border-[#2a2a2a] hover:border-orange-500 rounded-xl p-4 transition-colors' do %> -
-
-
- <%= c.nome %> - <%= badge_status(c.status) %> - <%= badge_pagamento(cm.pago? ? :pago : :pendente) %> -
-

- <%= icone :calendario %> <%= l c.data_inicio, format: :short %> → <%= l c.data_fim, format: :short %> - <% if cm.pago? %> - · <%= icone :dinheiro, cor: 'text-green-400' %> Pago em <%= l cm.pago_em.to_date, format: :short %> - <% if cm.forma_pagamento.present? %>via <%= cm.forma_pagamento.humanize %><% end %> - <% if cm.nota_fiscal.present? %>· NF <%= cm.nota_fiscal %><% end %> - <% end %> -

-
-

<%= moeda(cm.valor_total) %>

-
- <% end %> - <% end %> -
-
- <% end %> -<% end %> diff --git a/app/views/layouts/_navbar.html.erb b/app/views/layouts/_navbar.html.erb index 3a2e6f3..5e6e8f7 100644 --- a/app/views/layouts/_navbar.html.erb +++ b/app/views/layouts/_navbar.html.erb @@ -43,14 +43,12 @@ <%# Consolidações — só quem pode consolidar (exclui o externo) %> <% if current_user.pode_consolidar? %> <%= nav_link_to 'Consolidações', consolidacoes_path, icon: :consolidacoes %> - <%= nav_link_to 'Totais por motorista', totais_consolidacoes_path, icon: :motorista %> <%= nav_link_to 'Arquivadas', arquivadas_consolidacoes_path, icon: :arquivadas %> <% end %> <%# Motorista %> <% if current_user.motorista? %> <%= nav_link_to 'Meu Painel', motorista_dashboard_path, icon: :painel %> - <%= nav_link_to 'Meu total do período', motorista_totais_path, icon: :dinheiro %> <% end %> <%# Admin/Gerente — Configurações %> diff --git a/app/views/motorista/dashboard/index.html.erb b/app/views/motorista/dashboard/index.html.erb index bdde49c..5aae57c 100644 --- a/app/views/motorista/dashboard/index.html.erb +++ b/app/views/motorista/dashboard/index.html.erb @@ -1,14 +1,23 @@ -<%# app/views/motorista/dashboard/index.html.erb — Painel do Motorista %> +<%# app/views/motorista/dashboard/index.html.erb — Painel do Motorista + Uma tela só: período → quanto já está FECHADO, quanto ainda é ESTIMATIVA e + de onde vem cada valor. Feita para celular: alvos grandes, número em + destaque, sem tabela que precise de zoom. %> <% content_for :title, 'Meu Painel' %> +<% mes = Date.current.beginning_of_month..Date.current.end_of_month %> +<% mes_ant = (Date.current - 1.month).beginning_of_month..(Date.current - 1.month).end_of_month %> +<% tres_meses = (Date.current - 2.months).beginning_of_month..Date.current.end_of_month %> +<% atalhos = { 'Este mês' => mes, 'Mês passado' => mes_ant, '3 meses' => tres_meses } %> +<% atalhos['Tudo'] = (@faixa_total.first..@faixa_total.last) if @faixa_total %> +

Olá, <%= current_user.nome.split.first %>!

-

Aqui estão seus valores de entregas.

+

Escolha o período e veja quanto você tem a receber.

<%# ── Aviso de pagamento recém-confirmado ───────────────── %> <% if @pagamentos_recentes.any? %> -
+

<%= icone :sucesso, cor: nil, espaco: false %> Pagamento confirmado!

<% @pagamentos_recentes.each do |cm| %> @@ -28,65 +37,112 @@
<% end %> - <%# ── 2 Cards grandes ─────────────────────────────────── %> -
- - <%# Card 1 — Valor Estimado (LARANJA) %> -
-

<%= icone :dinheiro, cor: nil, espaco: false %> Valor estimado deste mês

-

<%= icone :calendario, cor: nil %> <%= l @estimado_inicio, format: :short %> a <%= l @estimado_fim, format: :short %>

-

<%= moeda(@valor_estimado) %>

- <%# "Atendidas" = concluídas + sem sucesso. O rótulo NÃO pode voltar a dizer - "feitas e confirmadas": o valor acima inclui as visitas sem sucesso, que - a empresa também paga (o motorista foi ao local). Sem a segunda linha o - motorista não consegue conferir a conta. %> -

- <%= @entregas_mes %> entregas atendidas -

-

- <%= @entregas_concluidas %> entregues · <%= @entregas_falhadas %> sem sucesso (pagas também) -

-

- <%= icone :alerta, cor: nil %> Valor aproximado. O valor certo sai quando a empresa fechar o pagamento. -

+ <%# ── Período ─────────────────────────────────────────── + Chips grandes para o caso comum e um "outro período" escondido no +
(sem JS) para a data exata. %> +
+
+ <% atalhos.each do |label, faixa| %> + <% ativo = @inicio == faixa.first && @fim == faixa.last %> + <%= link_to label, + motorista_dashboard_path(inicio: faixa.first.strftime('%Y-%m-%d'), fim: faixa.last.strftime('%Y-%m-%d')), + class: "px-4 py-3 rounded-xl text-base font-bold min-h-[48px] flex items-center #{ativo ? 'bg-orange-500 text-black' : 'bg-[#0a0a0a] text-gray-300 border border-[#2a2a2a]'}" %> + <% end %>
- <%# Card 2 — Valor Consolidado (BRANCO). Clicável: leva ao total POR PERÍODO, - com as consolidações que compõem o valor e o PDF do período. %> - <%= link_to motorista_totais_path, class: 'block bg-white rounded-2xl p-6 hover:ring-2 hover:ring-orange-500 transition' do %> -

<%= icone :sucesso, cor: 'text-green-600', espaco: false %> Valor fechado para pagamento

-

<%= moeda(@valor_consolidado) %>

-

- Soma dos pagamentos já confirmados pela empresa -

-

- <%= icone :avancar, cor: 'text-orange-600' %> Ver por período e baixar o PDF +

> + <%= icone :calendario %> Outro período + <%= form_with url: motorista_dashboard_path, method: :get, class: 'flex flex-wrap items-end gap-3 mt-2' do |f| %> +
+ + <%= f.date_field :inicio, value: @inicio.strftime('%Y-%m-%d'), + class: 'w-full bg-[#0a0a0a] border border-[#2a2a2a] text-white rounded-xl px-3 py-3 text-base' %> +
+
+ + <%= f.date_field :fim, value: @fim.strftime('%Y-%m-%d'), + class: 'w-full bg-[#0a0a0a] border border-[#2a2a2a] text-white rounded-xl px-3 py-3 text-base' %> +
+ <%= f.submit 'Ver', class: 'bg-orange-500 hover:bg-orange-600 text-black font-bold rounded-xl cursor-pointer px-6 py-3 min-h-[48px]' %> + <% end %> +
+
+ + <%# ── Card principal: o que já está FECHADO ────────────── %> +
+
+

+ <%= icone :sucesso, cor: 'text-green-600', espaco: false %> Fechado pela empresa

+ <%= l @inicio, format: :short %> a <%= l @fim, format: :short %> +
+ +

<%= moeda(@valor_consolidado) %>

+ + <%# As duas metades do total: o que já caiu e o que ainda vem. %> +
+
+

<%= icone :dinheiro, cor: 'text-green-700' %> Já recebido

+

<%= moeda(@totais.total_pago) %>

+
+
+

<%= icone :parcial, cor: 'text-amber-700' %> A receber

+

<%= moeda(@totais.total_pendente) %>

+
+
+ +

+ <%= @consolidacoes.size %> fechamento(s) no período. +

+ + <% if @consolidacoes.any? %> + <%= link_to rotulo(:baixar, 'Baixar meu relatório do período (PDF)', cor: nil), + motorista_totais_pdf_path(inicio: @inicio, fim: @fim), + data: { turbo: false }, + class: 'w-full bg-orange-500 hover:bg-orange-600 text-black font-bold text-base px-6 py-4 rounded-xl min-h-[52px] flex items-center justify-center text-center mt-4' %> <% end %>
- <%# ── Lista de consolidações ──────────────────────────── %> -

<%= icone :planilha, espaco: false %> Meus pagamentos

+ <%# ── Card secundário: estimativa (ainda não fechada) ──── %> +
+
+

<%= icone :dinheiro, cor: nil, espaco: false %> Estimativa das entregas

+ <%= l @estimado_inicio, format: :short %> a <%= l @estimado_fim, format: :short %> +
+

<%= moeda(@valor_estimado) %>

+ <%# "Atendidas" = concluídas + sem sucesso. O rótulo NÃO pode voltar a dizer + "feitas e confirmadas": o valor acima inclui as visitas sem sucesso, que + a empresa também paga (o motorista foi ao local). Sem a segunda linha o + motorista não consegue conferir a conta. %> +

<%= @entregas_mes %> entregas atendidas

+

+ <%= @entregas_concluidas %> entregues · <%= @entregas_falhadas %> sem sucesso (pagas também) +

+

+ <%= icone :alerta, cor: nil %> Valor aproximado. O valor certo é o de cima, quando a empresa fecha o pagamento. +

+
- <% if @minhas_consolidacoes.any? %> + <%# ── Composição: de onde vem o valor fechado ──────────── %> +

<%= icone :composicao, espaco: false %> De onde vem esse valor

+

Cada fechamento abaixo entra na soma do card branco.

+ + <% if @consolidacoes.any? %>
- <% @minhas_consolidacoes.each do |c| %> - <% cm = c.consolidacao_motoristas.find_by(motorista_nome: current_user.nome) %> + <% @consolidacoes.each do |cm| %> + <% c = cm.consolidacao %>
-
+

<%= c.nome %>

- <%= badge_status(c.status) %> - <% if c.finalizada? %> - <%= badge_pagamento(cm&.pago? ? :pago : :pendente) %> - <% end %> + <%= badge_pagamento(cm.pago? ? :pago : :pendente) %>

<%= icone :calendario %> <%= l c.data_inicio, format: :short %> a <%= l c.data_fim, format: :short %>

-

<%= moeda(cm&.valor_total || 0) %>

- <% if cm&.pago? %> +

<%= moeda(cm.valor_total) %>

+ <% if cm.pago? %>

<%= icone :dinheiro, cor: 'text-green-400' %> Pago em <%= l cm.pago_em.to_date, format: :short %> <% if cm.forma_pagamento.present? %>via <%= cm.forma_pagamento.humanize %><% end %> @@ -94,18 +150,14 @@ <% if cm.nota_fiscal.present? %>

<%= icone :nota_fiscal %> Nota Fiscal: <%= cm.nota_fiscal %>

<% end %> + <% else %> +

<%= icone :parcial, cor: 'text-amber-400' %> Ainda não pago

<% end %>
- <% if c.finalizada? %> - <%= link_to rotulo(:baixar, 'Baixar meu extrato', cor: nil), - gerar_pdf_extrato_consolidacao_path(c, motorista: current_user.nome), - data: { turbo: false }, - class: 'w-full md:w-auto bg-orange-500 hover:bg-orange-600 text-black font-bold text-base px-6 py-4 rounded-xl min-h-[52px] flex items-center justify-center text-center' %> - <% else %> - - <%= icone :parcial, cor: 'text-amber-400' %> Aguardando fechamento - - <% end %> + <%= link_to rotulo(:baixar, 'Extrato deste fechamento', cor: nil), + gerar_pdf_extrato_consolidacao_path(c, motorista: current_user.nome), + data: { turbo: false }, + class: 'w-full md:w-auto bg-[#0a0a0a] border border-[#2a2a2a] hover:border-orange-500 text-white font-bold text-base px-5 py-4 rounded-xl min-h-[52px] flex items-center justify-center text-center' %>
<% end %> @@ -113,8 +165,14 @@ <% else %>

<%= icone :vazio, tamanho: 'w-12 h-12', cor: 'text-gray-600', espaco: false %>

-

Você ainda não aparece em nenhum pagamento.

-

Quando a empresa fechar um período com suas entregas, ele aparece aqui.

+

Nenhum fechamento neste período.

+

+ <% if @faixa_total %> + Toque em Tudo para ver todos os seus fechamentos. + <% else %> + Quando a empresa fechar um período com suas entregas, ele aparece aqui. + <% end %> +

<% end %>
diff --git a/app/views/motorista/dashboard/totais.html.erb b/app/views/motorista/dashboard/totais.html.erb deleted file mode 100644 index c161ca1..0000000 --- a/app/views/motorista/dashboard/totais.html.erb +++ /dev/null @@ -1,95 +0,0 @@ -<%# app/views/motorista/dashboard/totais.html.erb — Meu total no período %> -<% content_for :title, 'Meu total no período' %> - -
- -
-

Meu total no período

- <%= link_to rotulo(:voltar, 'Voltar', cor: nil), motorista_dashboard_path, - class: 'text-gray-300 hover:text-white border border-[#2a2a2a] px-4 py-3 rounded-xl min-h-[48px] flex items-center' %> -
- - <%# ── Período ──────────────────────────────────────────── %> - <%= form_with url: motorista_totais_path, method: :get, - class: 'bg-[#1a1a1a] border border-[#2a2a2a] rounded-2xl p-4 mb-5 flex flex-wrap items-end gap-3' do |f| %> -
- - <%= f.date_field :inicio, value: @inicio.strftime('%Y-%m-%d'), - class: 'w-full bg-[#0a0a0a] border border-[#2a2a2a] text-white rounded-xl px-3 py-3 text-base' %> -
-
- - <%= f.date_field :fim, value: @fim.strftime('%Y-%m-%d'), - class: 'w-full bg-[#0a0a0a] border border-[#2a2a2a] text-white rounded-xl px-3 py-3 text-base' %> -
- <%= f.submit 'Ver', class: 'bg-orange-500 hover:bg-orange-600 text-black font-bold rounded-xl cursor-pointer px-6 py-3 min-h-[48px]' %> - <% end %> - - <%# ── Total fechado ────────────────────────────────────── %> -
-

- <%= icone :sucesso, cor: 'text-green-600', espaco: false %> Total fechado no período -

-

<%= icone :calendario, cor: 'text-gray-500' %> <%= l @inicio, format: :short %> a <%= l @fim, format: :short %>

-

<%= moeda(@totais.total_geral) %>

-
- <%= icone :dinheiro, cor: 'text-green-700' %> Já pago: <%= moeda(@totais.total_pago) %> - <%= icone :parcial, cor: 'text-amber-700' %> A receber: <%= moeda(@totais.total_pendente) %> -
-

- Soma de <%= @consolidacoes.size %> fechamento(s) da empresa neste período. -

-
- - <%= link_to rotulo(:baixar, 'Baixar relatório em PDF', cor: nil), - motorista_totais_pdf_path(inicio: @inicio, fim: @fim), - data: { turbo: false }, - class: 'w-full bg-orange-500 hover:bg-orange-600 text-black font-bold text-base px-6 py-4 rounded-xl min-h-[52px] flex items-center justify-center mb-6' %> - - <%# ── O que compõe o valor ─────────────────────────────── %> -

<%= icone :composicao, espaco: false %> O que forma esse valor

-

Cada fechamento abaixo entra na soma acima.

- - <% if @consolidacoes.any? %> -
- <% @consolidacoes.each do |cm| %> - <% c = cm.consolidacao %> -
-
-
-
-

<%= c.nome %>

- <%= badge_pagamento(cm.pago? ? :pago : :pendente) %> -
-

- <%= icone :calendario %> <%= l c.data_inicio, format: :short %> a <%= l c.data_fim, format: :short %> -

-

<%= moeda(cm.valor_total) %>

- <% if cm.pago? %> -

- <%= icone :dinheiro, cor: 'text-green-400' %> Pago em <%= l cm.pago_em.to_date, format: :short %> - <% if cm.forma_pagamento.present? %>via <%= cm.forma_pagamento.humanize %><% end %> -

- <% if cm.nota_fiscal.present? %> -

<%= icone :nota_fiscal %> Nota Fiscal: <%= cm.nota_fiscal %>

- <% end %> - <% else %> -

<%= icone :parcial, cor: 'text-amber-400' %> Ainda não pago

- <% end %> -
- <%= link_to rotulo(:baixar, 'Extrato deste fechamento', cor: nil), - gerar_pdf_extrato_consolidacao_path(c, motorista: current_user.nome), - data: { turbo: false }, - class: 'w-full md:w-auto bg-[#0a0a0a] border border-[#2a2a2a] hover:border-orange-500 text-white font-bold text-base px-5 py-4 rounded-xl min-h-[52px] flex items-center justify-center text-center' %> -
-
- <% end %> -
- <% else %> -
-

<%= icone :vazio, tamanho: 'w-12 h-12', cor: 'text-gray-600', espaco: false %>

-

Nenhum fechamento neste período.

-

Escolha outro período acima — o fechamento aparece aqui quando a empresa finaliza a consolidação.

-
- <% end %> -
diff --git a/config/routes.rb b/config/routes.rb index 4c90e09..3f28895 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -27,9 +27,8 @@ Rails.application.routes.draw do # Painel motorista get '/motorista', to: 'motorista/dashboard#index', as: :motorista_dashboard - # Total fechado do motorista num período + as consolidações que compõem o - # valor (mesma conta do relatório do admin) e o PDF do extrato do período. - get '/motorista/totais', to: 'motorista/dashboard#totais', as: :motorista_totais + # Extrato do período do motorista em PDF (o painel já mostra o total e as + # consolidações que compõem o valor na própria tela). get '/motorista/totais/pdf', to: 'motorista/dashboard#totais_pdf', as: :motorista_totais_pdf # Login motorista — PIN de 4 dígitos + acesso via QR Code do extrato @@ -47,10 +46,9 @@ Rails.application.routes.draw do post :avulsa # cria consolidação avulsa a partir de uma NF get :arquivadas # Histórico — consolidações arquivadas - # Relatório "total por motorista no período": soma o que cada motorista - # fechou na faixa e, ao clicar num deles (?motorista=), mostra as - # consolidações que compõem o valor. `totais_pdf` é o mesmo relatório em PDF. - get :totais + # PDF do modo de visualização "Por motorista" (/consolidacoes?visao= + # motoristas): o total de cada um no período e as consolidações que + # compõem o valor. get :totais_pdf end diff --git a/spec/requests/motorista_dashboard_spec.rb b/spec/requests/motorista_dashboard_spec.rb index 7f1cce2..2970419 100644 --- a/spec/requests/motorista_dashboard_spec.rb +++ b/spec/requests/motorista_dashboard_spec.rb @@ -14,9 +14,9 @@ RSpec.describe 'Painel do motorista — contabilização das entregas sem sucess include ActiveSupport::Testing::TimeHelpers include_context 'espelho de rastreio' - # O painel não aceita filtro de período (é sempre "do dia 1º até hoje"), então - # congelamos a data: sem isso o spec quebraria sozinho ao rodar no dia 1º, com - # as datas semeadas caindo fora do mês corrente. + # O painel abre no MÊS CORRENTE (a estimativa vai até hoje), então congelamos a + # data: sem isso o spec quebraria sozinho ao rodar no dia 1º, com as datas + # semeadas caindo fora do mês corrente. let(:hoje) { Date.new(2026, 8, 20) } let(:preco) { 10.0 } let(:nome) { 'JOSE DA SILVA' } diff --git a/spec/requests/totais_por_motorista_spec.rb b/spec/requests/totais_por_motorista_spec.rb index 0f9b151..ed5d5be 100644 --- a/spec/requests/totais_por_motorista_spec.rb +++ b/spec/requests/totais_por_motorista_spec.rb @@ -1,8 +1,9 @@ require 'rails_helper' -# Rotas do relatório "total por motorista": quem entra, o que a tela mostra e o -# PDF. O drill-down (?motorista=) é o que responde "de onde vem esse valor". -RSpec.describe 'Totais por motorista', type: :request do +# Modo de visualização "Por motorista" (/consolidacoes?visao=motoristas) e o +# painel do motorista: quem entra, o que a tela soma e o drill-down que responde +# "de onde vem esse valor". +RSpec.describe 'Visão por motorista', type: :request do def consolidacao_finalizada(motorista:, valor:, nome: 'Fechamento Junho') c = create(:consolidacao, nome: nome, status: :finalizada, data_inicio: Date.new(2026, 6, 1), data_fim: Date.new(2026, 6, 30)) @@ -13,39 +14,49 @@ RSpec.describe 'Totais por motorista', type: :request do let(:periodo) { { inicio: '2026-06-01', fim: '2026-06-30' } } - describe 'admin — /consolidacoes/totais' do - it 'exige login' do - get totais_consolidacoes_path - expect(response).to redirect_to(new_user_session_path) - end - + describe 'admin — /consolidacoes?visao=motoristas' do it 'bloqueia motorista (Pundit)' do sign_in create(:motorista) - get totais_consolidacoes_path + get consolidacoes_path(visao: 'motoristas') expect(response).to redirect_to(root_path) end - it 'mostra o total do motorista no período' do + it 'soma o total do motorista no período' do consolidacao_finalizada(motorista: 'Carlos', valor: 120.0) sign_in create(:gerente) - get totais_consolidacoes_path(periodo) + get consolidacoes_path(periodo.merge(visao: 'motoristas')) expect(response).to have_http_status(:ok) expect(response.body).to include('Carlos') + expect(response.body).to include('R$ 120,00') end - it 'abre a composição do motorista clicado' do + it 'só abre a composição do motorista clicado' do + consolidacao_finalizada(motorista: 'Carlos', valor: 120.0, nome: 'Fechamento Junho') + consolidacao_finalizada(motorista: 'Pedro', valor: 90.0, nome: 'Fechamento do Pedro') + sign_in create(:gerente) + + get consolidacoes_path(periodo.merge(visao: 'motoristas')) + expect(response.body).not_to include('O que forma esse valor') + + get consolidacoes_path(periodo.merge(visao: 'motoristas', detalhe: 'Carlos')) + expect(response.body).to include('O que forma esse valor') + expect(response.body).to include('Fechamento Junho') + expect(response.body).not_to include('Fechamento do Pedro') + end + + it 'a visão lista continua sendo o padrão' do consolidacao_finalizada(motorista: 'Carlos', valor: 120.0, nome: 'Fechamento Junho') sign_in create(:gerente) - get totais_consolidacoes_path(periodo.merge(motorista: 'Carlos')) + get consolidacoes_path - expect(response.body).to include('Composição de Carlos') expect(response.body).to include('Fechamento Junho') + expect(response.body).not_to include('Total do período') end - it 'gera o PDF' do + it 'gera o PDF do período' do consolidacao_finalizada(motorista: 'Carlos', valor: 120.0) sign_in create(:gerente) @@ -56,17 +67,18 @@ RSpec.describe 'Totais por motorista', type: :request do end end - describe 'motorista — /motorista/totais' do - it 'mostra só o total dele e as consolidações que o compõem' do + describe 'motorista — painel' do + it 'mostra o total fechado do período e os fechamentos que o compõem' do motorista = create(:motorista, nome: 'Carlos') consolidacao_finalizada(motorista: 'Carlos', valor: 120.0, nome: 'Fechamento Junho') consolidacao_finalizada(motorista: 'Pedro', valor: 999.0, nome: 'Fechamento do Pedro') sign_in motorista - get motorista_totais_path(periodo) + get motorista_dashboard_path(periodo) expect(response).to have_http_status(:ok) expect(response.body).to include('Fechamento Junho') + expect(response.body).to include('R$ 120,00') expect(response.body).not_to include('Fechamento do Pedro') end @@ -83,7 +95,7 @@ RSpec.describe 'Totais por motorista', type: :request do it 'não é acessível por quem não é motorista' do sign_in create(:gerente) - get motorista_totais_path + get motorista_totais_pdf_path(periodo) expect(response).to redirect_to(dashboard_path) end end diff --git a/spec/services/analytics/totais_por_motorista_spec.rb b/spec/services/analytics/totais_por_motorista_spec.rb index 7b2e245..155a2b0 100644 --- a/spec/services/analytics/totais_por_motorista_spec.rb +++ b/spec/services/analytics/totais_por_motorista_spec.rb @@ -1,7 +1,7 @@ require 'rails_helper' -# Trava a conta do relatório "total por motorista" — a mesma que alimenta -# /consolidacoes/totais (admin) e /motorista/totais (motorista). As duas telas +# Trava a conta do modo de visualização "Por motorista" — a mesma que alimenta +# /consolidacoes?visao=motoristas (admin) e o painel do motorista. As duas telas # usam ESTE service justamente para não poderem divergir. RSpec.describe Analytics::TotaisPorMotorista do let(:inicio) { Date.new(2026, 6, 1) } @@ -37,13 +37,21 @@ RSpec.describe Analytics::TotaisPorMotorista do expect(totais.total_consolidacoes).to eq(3) end - it 'inclui rascunhos quando pedido' do + it 'respeita o filtro de status (o mesmo select da lista)' do consolidar(motorista: 'Carlos', valor: 100.0) consolidar(motorista: 'Carlos', valor: 40.0, status: :rascunho) - com_rascunho = described_class.new(inicio: inicio, fim: fim, incluir_rascunhos: true) - expect(totais.total_geral).to eq(100.0) - expect(com_rascunho.total_geral).to eq(140.0) + expect(totais.total_geral).to eq(100.0) # default: só finalizadas + expect(described_class.new(inicio: inicio, fim: fim, status: 'todos').total_geral).to eq(140.0) + expect(described_class.new(inicio: inicio, fim: fim, status: 'rascunho').total_geral).to eq(40.0) + end + + it 'busca parcial por nome do motorista (campo de busca da tela)' do + consolidar(motorista: 'Carlos Matheus', valor: 100.0) + consolidar(motorista: 'Pedro Martins', valor: 30.0) + + achou = described_class.new(inicio: inicio, fim: fim, busca: 'matheus') + expect(achou.linhas.map { |l| l[:motorista] }).to eq(['Carlos Matheus']) end it 'separa pago de a receber' do