Modificação na area do motorista
This commit is contained in:
@@ -33,8 +33,17 @@ class ConsolidacoesController < ApplicationController
|
|||||||
# Filtros
|
# Filtros
|
||||||
@consolidacoes = @consolidacoes.where(status: params[:status]) if params[:status].present?
|
@consolidacoes = @consolidacoes.where(status: params[:status]) if params[:status].present?
|
||||||
@consolidacoes = @consolidacoes.where('nome ILIKE ?', "%#{params[:nome]}%") if params[:nome].present?
|
@consolidacoes = @consolidacoes.where('nome ILIKE ?', "%#{params[:nome]}%") if params[:nome].present?
|
||||||
@consolidacoes = @consolidacoes.where('data_inicio >= ?', params[:inicio]) if params[:inicio].present?
|
# Datas: a consolidação entra se CRUZA a faixa — mesmo critério do modo "Por
|
||||||
@consolidacoes = @consolidacoes.where('data_fim <= ?', params[:fim]) if params[:fim].present?
|
# motorista" e do dashboard financeiro. Com o critério antigo (caber inteira
|
||||||
|
# dentro do filtro), um fechamento de 01/08 a 31/08 sumia da lista ao filtrar
|
||||||
|
# 01/08–26/08 e as duas abas da MESMA tela contavam coisas diferentes.
|
||||||
|
if params[:inicio].present? && params[:fim].present?
|
||||||
|
@consolidacoes = @consolidacoes.cruzando_periodo(params[:inicio], params[:fim])
|
||||||
|
elsif params[:inicio].present?
|
||||||
|
@consolidacoes = @consolidacoes.where('data_fim >= ?', params[:inicio])
|
||||||
|
elsif params[:fim].present?
|
||||||
|
@consolidacoes = @consolidacoes.where('data_inicio <= ?', params[:fim])
|
||||||
|
end
|
||||||
|
|
||||||
if params[:motorista].present?
|
if params[:motorista].present?
|
||||||
ids = ConsolidacaoMotorista.where('motorista_nome ILIKE ?', "%#{params[:motorista]}%")
|
ids = ConsolidacaoMotorista.where('motorista_nome ILIKE ?', "%#{params[:motorista]}%")
|
||||||
@@ -63,7 +72,7 @@ class ConsolidacoesController < ApplicationController
|
|||||||
totais = Analytics::TotaisPorMotorista.new(
|
totais = Analytics::TotaisPorMotorista.new(
|
||||||
inicio: inicio, fim: fim,
|
inicio: inicio, fim: fim,
|
||||||
motorista: params[:detalhe].presence,
|
motorista: params[:detalhe].presence,
|
||||||
status: params[:status].presence || 'todos',
|
status: params[:status].presence || 'finalizada',
|
||||||
busca: params[:motorista].presence
|
busca: params[:motorista].presence
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -484,24 +493,26 @@ class ConsolidacoesController < ApplicationController
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
# Modo "Por motorista": os totais saem do MESMO recorte da lista (período,
|
# Modo "Por motorista": mesmo período/busca da lista, mas o recorte de datas e
|
||||||
# status e busca por motorista), para as duas visões nunca contarem coisas
|
# o default de status são os do DASHBOARD FINANCEIRO — é com ele que o operador
|
||||||
# diferentes. `detalhe` é o motorista aberto (o clique na linha).
|
# compara o número (ver Analytics::TotaisPorMotorista). Sem status na URL vale
|
||||||
|
# "Consolidado" (só finalizadas): "Geral" (com rascunhos) mostrava um valor
|
||||||
|
# maior que o do dashboard e parecia erro de conta.
|
||||||
|
# `detalhe` é o motorista aberto (o clique na linha).
|
||||||
def montar_totais
|
def montar_totais
|
||||||
@inicio, @fim = periodo_totais
|
@inicio, @fim = periodo_totais
|
||||||
|
@status_totais = params[:status].presence || 'finalizada'
|
||||||
@totais = Analytics::TotaisPorMotorista.new(
|
@totais = Analytics::TotaisPorMotorista.new(
|
||||||
inicio: @inicio, fim: @fim,
|
inicio: @inicio, fim: @fim,
|
||||||
status: params[:status].presence || 'todos',
|
status: @status_totais,
|
||||||
busca: params[:motorista].presence
|
busca: params[:motorista].presence
|
||||||
)
|
)
|
||||||
@detalhe_motorista = params[:detalhe].presence
|
@detalhe_motorista = params[:detalhe].presence
|
||||||
@detalhe = @detalhe_motorista ? @totais.consolidacoes_de(@detalhe_motorista) : nil
|
@detalhe = @detalhe_motorista ? @totais.consolidacoes_de(@detalhe_motorista) : nil
|
||||||
end
|
end
|
||||||
|
|
||||||
# Período do relatório de totais. Default: o MÊS CORRENTE INTEIRO — o fim é o
|
# Período do relatório de totais. Default: o MÊS CORRENTE INTEIRO (o mês é a
|
||||||
# último dia do mês, não "hoje", porque o recorte exige a consolidação inteira
|
# unidade de fechamento; "até hoje" recortaria o mês pela metade sem motivo).
|
||||||
# dentro da faixa (Consolidacao.no_periodo) e um fechamento que vai até 31/08
|
|
||||||
# sumiria da tela até o dia 31 chegar.
|
|
||||||
def periodo_totais
|
def periodo_totais
|
||||||
inicio = parse_data_totais(params[:inicio]) || Date.current.beginning_of_month
|
inicio = parse_data_totais(params[:inicio]) || Date.current.beginning_of_month
|
||||||
fim = parse_data_totais(params[:fim]) || Date.current.end_of_month
|
fim = parse_data_totais(params[:fim]) || Date.current.end_of_month
|
||||||
|
|||||||
@@ -105,9 +105,8 @@ module Motorista
|
|||||||
[inicio.to_date, fim.to_date]
|
[inicio.to_date, fim.to_date]
|
||||||
end
|
end
|
||||||
|
|
||||||
# Default: mês corrente INTEIRO (fim = último dia do mês). O recorte exige a
|
# Default: mês corrente INTEIRO (fim = último dia do mês) — o mês é a unidade
|
||||||
# consolidação inteira dentro da faixa, então terminar em "hoje" esconderia
|
# de fechamento. Mesma regra do lado do admin.
|
||||||
# um fechamento que vai até o fim do mês. Mesma regra do lado do admin.
|
|
||||||
def periodo_selecionado
|
def periodo_selecionado
|
||||||
inicio = parse_data(params[:inicio]) || Date.current.beginning_of_month
|
inicio = parse_data(params[:inicio]) || Date.current.beginning_of_month
|
||||||
fim = parse_data(params[:fim]) || Date.current.end_of_month
|
fim = parse_data(params[:fim]) || Date.current.end_of_month
|
||||||
|
|||||||
68
app/javascript/controllers/carrossel_controller.js
Normal file
68
app/javascript/controllers/carrossel_controller.js
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
// app/javascript/controllers/carrossel_controller.js
|
||||||
|
// Stimulus — paginação em "carrossel" para listas longas no celular (painel do
|
||||||
|
// motorista). Os itens já vêm todos no HTML; aqui só mostramos uma PÁGINA por
|
||||||
|
// vez, com setas e o indicador "1 / 3".
|
||||||
|
//
|
||||||
|
// Por que no cliente e não com ?pg= no servidor: a lista divide a tela com os
|
||||||
|
// cards de valor e o filtro de período — recarregar a página a cada seta faria o
|
||||||
|
// motorista perder a posição e piscar a tela inteira num 3G.
|
||||||
|
//
|
||||||
|
// Sem itens suficientes para duas páginas, os controles somem sozinhos (nada de
|
||||||
|
// seta morta ocupando espaço).
|
||||||
|
|
||||||
|
import { Controller } from "@hotwired/stimulus"
|
||||||
|
|
||||||
|
export default class extends Controller {
|
||||||
|
static targets = ["item", "controles", "indicador", "anterior", "proximo"]
|
||||||
|
static values = { tamanho: { type: Number, default: 10 } }
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
this.pagina = 0
|
||||||
|
this.render()
|
||||||
|
}
|
||||||
|
|
||||||
|
proxima() { this.ir(this.pagina + 1) }
|
||||||
|
anterior() { this.ir(this.pagina - 1) }
|
||||||
|
|
||||||
|
ir(destino) {
|
||||||
|
this.pagina = Math.min(Math.max(destino, 0), this.totalPaginas - 1)
|
||||||
|
this.render()
|
||||||
|
// Volta ao topo da lista: sem isso, na 2ª página o motorista continua vendo
|
||||||
|
// o fim da anterior e parece que nada mudou.
|
||||||
|
this.element.scrollIntoView({ behavior: "smooth", block: "start" })
|
||||||
|
}
|
||||||
|
|
||||||
|
get totalPaginas() {
|
||||||
|
return Math.max(Math.ceil(this.itemTargets.length / this.tamanhoValue), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const inicio = this.pagina * this.tamanhoValue
|
||||||
|
const fim = inicio + this.tamanhoValue
|
||||||
|
|
||||||
|
this.itemTargets.forEach((item, i) => {
|
||||||
|
item.classList.toggle("hidden", i < inicio || i >= fim)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!this.hasControlesTarget) return
|
||||||
|
|
||||||
|
this.controlesTarget.classList.toggle("hidden", this.totalPaginas < 2)
|
||||||
|
if (this.hasIndicadorTarget) {
|
||||||
|
// "1–10 de 12" e não "página 1 de 2": o motorista quer saber quantos
|
||||||
|
// fechamentos existem, não quantas páginas a tela tem.
|
||||||
|
const total = this.itemTargets.length
|
||||||
|
this.indicadorTarget.textContent = `${inicio + 1}–${Math.min(fim, total)} de ${total}`
|
||||||
|
}
|
||||||
|
if (this.hasAnteriorTarget) this.alternar(this.anteriorTarget, this.pagina === 0)
|
||||||
|
if (this.hasProximoTarget) this.alternar(this.proximoTarget, this.pagina >= this.totalPaginas - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Botão de ponta: desabilitado de verdade (não só apagado), para o toque não
|
||||||
|
// "funcionar sem fazer nada".
|
||||||
|
alternar(botao, desligado) {
|
||||||
|
if (!botao) return
|
||||||
|
botao.disabled = desligado
|
||||||
|
botao.classList.toggle("opacity-30", desligado)
|
||||||
|
botao.classList.toggle("pointer-events-none", desligado)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -33,8 +33,18 @@ class Consolidacao < ApplicationRecord
|
|||||||
|
|
||||||
# ── Escopos ─────────────────────────────────────────────────
|
# ── Escopos ─────────────────────────────────────────────────
|
||||||
scope :recentes, -> { order(created_at: :desc) }
|
scope :recentes, -> { order(created_at: :desc) }
|
||||||
|
# ⚠️ `no_periodo` exige a consolidação INTEIRA dentro da faixa. Não é o recorte
|
||||||
|
# das telas (elas usam `cruzando_periodo`, abaixo) — um fechamento de 01/08 a
|
||||||
|
# 31/08 sumia ao filtrar 01/08–26/08. Só use se o "caber dentro" for mesmo o
|
||||||
|
# que se quer.
|
||||||
scope :no_periodo, ->(i, f) { where('data_inicio >= ? AND data_fim <= ?', i, f) }
|
scope :no_periodo, ->(i, f) { where('data_inicio >= ? AND data_fim <= ?', i, f) }
|
||||||
|
|
||||||
|
# Consolidações que CRUZAM o período (não precisam caber inteiras dentro dele).
|
||||||
|
# É o recorte do dashboard financeiro (DashboardController#carregar_dados_
|
||||||
|
# financeiro) e, por isso, o de qualquer tela que precise bater com ele: um
|
||||||
|
# fechamento de 01/08 a 31/08 tem que aparecer ao filtrar 01/08–26/08.
|
||||||
|
scope :cruzando_periodo, ->(i, f) { where('data_inicio <= ? AND data_fim >= ?', f, i) }
|
||||||
|
|
||||||
# ── Filtro por status de pagamento (derivado dos motoristas) ─
|
# ── Filtro por status de pagamento (derivado dos motoristas) ─
|
||||||
# pago_em NÃO-nulo conta como pago; COUNT(pago_em) ignora os NULL.
|
# pago_em NÃO-nulo conta como pago; COUNT(pago_em) ignora os NULL.
|
||||||
scope :pagamento_pago, -> {
|
scope :pagamento_pago, -> {
|
||||||
|
|||||||
@@ -7,11 +7,18 @@
|
|||||||
# lista, com drill-down no motorista clicado)
|
# lista, com drill-down no motorista clicado)
|
||||||
# • motorista→ /motorista (o painel, já recortado nele — mesma conta, mesmo PDF)
|
# • motorista→ /motorista (o painel, já recortado nele — mesma conta, mesmo PDF)
|
||||||
#
|
#
|
||||||
# RECORTE (igual ao filtro da lista de consolidações): entram as consolidações
|
# RECORTE — É O MESMO DO DASHBOARD FINANCEIRO, de propósito
|
||||||
# ATIVAS cujo período está DENTRO da faixa escolhida (data_inicio >= inicio e
|
# (DashboardController#carregar_dados_financeiro). Os dois números são lidos lado
|
||||||
# data_fim <= fim). Não é sobreposição de propósito — uma consolidação que
|
# a lado pelo operador; quando divergem, a leitura é "o sistema está errado".
|
||||||
# atravessa a borda do filtro entraria inteira e o "total do período" ficaria
|
# Entram as consolidações ATIVAS que:
|
||||||
# maior que o período.
|
# • 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
|
# Só motoristas ATIVOS (não arquivados) contam: é o mesmo conjunto que a
|
||||||
# consolidação paga (ConsolidacaoMotorista.ativos), então o total desta tela
|
# consolidação paga (ConsolidacaoMotorista.ativos), então o total desta tela
|
||||||
@@ -36,15 +43,12 @@ module Analytics
|
|||||||
# telas mostram nome/período/status dela em seguida).
|
# telas mostram nome/período/status dela em seguida).
|
||||||
def registros
|
def registros
|
||||||
@registros ||= begin
|
@registros ||= begin
|
||||||
base = Consolidacao.ativas.no_periodo(@inicio, @fim)
|
base = Consolidacao.ativas
|
||||||
# Por padrão só o que já foi FECHADO conta como total do período —
|
# `public_send` sobre o enum: quem traduz :finalizada/:rascunho é o model.
|
||||||
# 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)
|
base = base.public_send(@status) if %w[finalizada rascunho].include?(@status)
|
||||||
|
|
||||||
escopo = ConsolidacaoMotorista.ativos
|
escopo = ConsolidacaoMotorista.ativos
|
||||||
.joins(:consolidacao)
|
.where(consolidacao_id: ids_do_periodo(base))
|
||||||
.merge(base)
|
|
||||||
.includes(:consolidacao)
|
.includes(:consolidacao)
|
||||||
escopo = escopo.where(motorista_nome: @motorista) if @motorista
|
escopo = escopo.where(motorista_nome: @motorista) if @motorista
|
||||||
escopo = escopo.where('motorista_nome ILIKE ?', "%#{@busca}%") if @busca
|
escopo = escopo.where('motorista_nome ILIKE ?', "%#{@busca}%") if @busca
|
||||||
@@ -94,6 +98,19 @@ module Analytics
|
|||||||
|
|
||||||
private
|
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)
|
def soma(cms)
|
||||||
cms.sum { |cm| cm.valor_total || 0 }
|
cms.sum { |cm| cm.valor_total || 0 }
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -3,14 +3,16 @@
|
|||||||
ali mesmo (sem sair da tela), as consolidações que compõem o valor dele. %>
|
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. %>
|
<%# Link desta mesma tela preservando filtros — usado pelas linhas e pelos PDFs. %>
|
||||||
<% ctx = { nome: params[:nome], motorista: params[:motorista], status: params[:status],
|
<% ctx = { nome: params[:nome], motorista: params[:motorista], status: @status_totais,
|
||||||
inicio: @inicio.strftime('%Y-%m-%d'), fim: @fim.strftime('%Y-%m-%d'),
|
inicio: @inicio.strftime('%Y-%m-%d'), fim: @fim.strftime('%Y-%m-%d'),
|
||||||
visao: 'motoristas' }.compact_blank %>
|
visao: 'motoristas' }.compact_blank %>
|
||||||
|
|
||||||
<%# ── Resumo do período ────────────────────────────────── %>
|
<%# ── Resumo do período ────────────────────────────────── %>
|
||||||
<div class="grid grid-cols-2 xl:grid-cols-4 gap-3 mb-5">
|
<div class="grid grid-cols-2 xl:grid-cols-4 gap-3 mb-5">
|
||||||
<div class="bg-[#1a1a1a] border border-[#2a2a2a] rounded-xl p-4">
|
<div class="bg-[#1a1a1a] border border-[#2a2a2a] rounded-xl p-4">
|
||||||
<p class="text-gray-400 text-xs uppercase tracking-wide"><%= icone :dinheiro %> Total do período</p>
|
<p class="text-gray-400 text-xs uppercase tracking-wide">
|
||||||
|
<%= icone :dinheiro %> <%= @status_totais == 'finalizada' ? 'Consolidado no período' : 'Total do período' %>
|
||||||
|
</p>
|
||||||
<p class="text-orange-500 font-black text-2xl sm:text-3xl mt-1 whitespace-nowrap"><%= moeda(@totais.total_geral) %></p>
|
<p class="text-orange-500 font-black text-2xl sm:text-3xl mt-1 whitespace-nowrap"><%= moeda(@totais.total_geral) %></p>
|
||||||
<p class="text-gray-500 text-xs mt-1"><%= @totais.total_consolidacoes %> consolidação(ões) · <%= @totais.linhas.size %> motorista(s)</p>
|
<p class="text-gray-500 text-xs mt-1"><%= @totais.total_consolidacoes %> consolidação(ões) · <%= @totais.linhas.size %> motorista(s)</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -120,9 +122,18 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="text-gray-500 text-xs mt-4">
|
<p class="text-gray-500 text-xs mt-4">
|
||||||
<%= icone :dica %> Entram as consolidações
|
<%= icone :dica %>
|
||||||
<%= params[:status].presence ? "com status #{params[:status]}" : 'de qualquer status' %>
|
<% if @status_totais == 'finalizada' %>
|
||||||
cujo período está dentro de <%= l @inicio, format: :short %> → <%= l @fim, format: :short %>.
|
<span class="text-gray-400">Consolidado</span> = só consolidações <strong>finalizadas</strong> —
|
||||||
Motoristas arquivados na consolidação não entram no total.
|
é o mesmo número do card "Consolidado / Pago" do Dashboard no mesmo período.
|
||||||
|
Troque para <span class="text-gray-400">Geral</span> para somar também os rascunhos em andamento.
|
||||||
|
<% elsif @status_totais == 'rascunho' %>
|
||||||
|
Mostrando <span class="text-gray-400">só rascunhos</span> — valores ainda em aberto, que mudam até o fechamento.
|
||||||
|
<% else %>
|
||||||
|
<span class="text-gray-400">Geral</span> = finalizadas <strong>+ rascunhos</strong>, por isso fica acima do
|
||||||
|
"Consolidado / Pago" do Dashboard. Troque para <span class="text-gray-400">Consolidado</span> para comparar com ele.
|
||||||
|
<% end %>
|
||||||
|
Entram as consolidações que cruzam <%= l @inicio, format: :short %> → <%= l @fim, format: :short %> e as pagas dentro do período;
|
||||||
|
motoristas arquivados não entram no total.
|
||||||
</p>
|
</p>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -111,9 +111,20 @@
|
|||||||
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' %>
|
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 %>
|
<% end %>
|
||||||
|
|
||||||
<%= f.select :status,
|
<% if @visao == 'motoristas' %>
|
||||||
options_for_select([['Todos os status', ''], ['Rascunho', 'rascunho'], ['Finalizada', 'finalizada']], params[:status]),
|
<%# "Consolidado" x "Geral" — a pergunta que o número responde muda com isso:
|
||||||
{}, class: 'flex-1 basis-40 min-w-0 bg-[#0a0a0a] border border-[#2a2a2a] text-white rounded-lg px-3 py-2.5' %>
|
Consolidado é o valor JÁ FECHADO (o mesmo do card "Consolidado / Pago" do
|
||||||
|
Dashboard); Geral soma também os rascunhos, que ainda mudam de valor. %>
|
||||||
|
<%= f.select :status,
|
||||||
|
options_for_select([['Consolidado (fechadas)', 'finalizada'],
|
||||||
|
['Geral (com rascunhos)', 'todos'],
|
||||||
|
['Só rascunhos', 'rascunho']], @status_totais),
|
||||||
|
{}, class: 'flex-1 basis-52 min-w-0 bg-[#0a0a0a] border border-[#2a2a2a] text-white rounded-lg px-3 py-2.5' %>
|
||||||
|
<% else %>
|
||||||
|
<%= 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' %>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
<%# "Pagamento" é status DA CONSOLIDAÇÃO (todos os motoristas dela); no modo
|
<%# "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
|
Por motorista cada linha já mostra pago/a receber, então o filtro sairia
|
||||||
|
|||||||
@@ -103,35 +103,57 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<%# ── Card secundário: estimativa (ainda não fechada) ──── %>
|
<%# ── Card secundário: entregas do período ──────────────
|
||||||
<div class="bg-orange-500 rounded-2xl p-6 mb-6">
|
REGRA: valor estimado e valor fechado NUNCA aparecem juntos.
|
||||||
<div class="flex items-center justify-between gap-3">
|
Quando a empresa já fechou o período, o número certo é o de cima; mostrar
|
||||||
<p class="text-black/80 font-bold uppercase tracking-wide flex items-center gap-2"><%= icone :dinheiro, cor: nil, espaco: false %> Estimativa das entregas</p>
|
ao lado uma estimativa (entregas × preço, sem descontos/bônus e sem as
|
||||||
<span class="text-black/60 text-sm whitespace-nowrap"><%= l @estimado_inicio, format: :short %> a <%= l @estimado_fim, format: :short %></span>
|
regras de classificação) fazia o motorista ver dois valores diferentes para
|
||||||
|
o mesmo mês — e o estimado costuma ser MAIOR, o que vira cobrança.
|
||||||
|
Com fechamento: só a CONTAGEM de entregas (o trabalho, sem prometer valor).
|
||||||
|
Sem fechamento: a prévia em R$, que é a única informação que ele tem. %>
|
||||||
|
<% if @consolidacoes.any? %>
|
||||||
|
<div class="bg-[#1a1a1a] border border-[#2a2a2a] rounded-2xl p-6 mb-6">
|
||||||
|
<div class="flex items-center justify-between gap-3">
|
||||||
|
<p class="text-gray-400 font-bold uppercase tracking-wide flex items-center gap-2"><%= icone :caminhao, espaco: false %> Suas entregas no período</p>
|
||||||
|
<span class="text-gray-500 text-sm whitespace-nowrap"><%= l @estimado_inicio, format: :short %> a <%= l @estimado_fim, format: :short %></span>
|
||||||
|
</div>
|
||||||
|
<p class="text-white font-black text-4xl mt-2 leading-tight"><%= @entregas_mes %></p>
|
||||||
|
<%# "Atendidas" = concluídas + sem sucesso: a empresa paga as duas, porque
|
||||||
|
em ambas o motorista foi até o local. %>
|
||||||
|
<p class="text-gray-300 text-base mt-1">
|
||||||
|
<%= @entregas_concluidas %> entregues · <%= @entregas_falhadas %> sem sucesso (pagas também)
|
||||||
|
</p>
|
||||||
|
<p class="text-gray-500 text-sm mt-3">
|
||||||
|
<%= icone :sucesso, cor: 'text-green-500' %> O valor deste período já foi fechado pela empresa — é o do card branco acima.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-black font-black text-4xl mt-2 leading-tight whitespace-nowrap"><%= moeda(@valor_estimado) %></p>
|
<% else %>
|
||||||
<%# "Atendidas" = concluídas + sem sucesso. O rótulo NÃO pode voltar a dizer
|
<div class="bg-orange-500 rounded-2xl p-6 mb-6">
|
||||||
"feitas e confirmadas": o valor acima inclui as visitas sem sucesso, que
|
<div class="flex items-center justify-between gap-3">
|
||||||
a empresa também paga (o motorista foi ao local). Sem a segunda linha o
|
<p class="text-black/80 font-bold uppercase tracking-wide flex items-center gap-2"><%= icone :dinheiro, cor: nil, espaco: false %> Prévia (ainda não fechado)</p>
|
||||||
motorista não consegue conferir a conta. %>
|
<span class="text-black/60 text-sm whitespace-nowrap"><%= l @estimado_inicio, format: :short %> a <%= l @estimado_fim, format: :short %></span>
|
||||||
<p class="text-black/80 text-base mt-2"><%= @entregas_mes %> entregas atendidas</p>
|
</div>
|
||||||
<p class="text-black/70 text-sm mt-1">
|
<p class="text-black font-black text-4xl mt-2 leading-tight whitespace-nowrap"><%= moeda(@valor_estimado) %></p>
|
||||||
<%= @entregas_concluidas %> entregues · <%= @entregas_falhadas %> sem sucesso (pagas também)
|
<p class="text-black/80 text-base mt-2"><%= @entregas_mes %> entregas atendidas</p>
|
||||||
</p>
|
<p class="text-black/70 text-sm mt-1">
|
||||||
<p class="text-black/60 text-sm mt-3">
|
<%= @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.
|
</p>
|
||||||
</p>
|
<p class="text-black/60 text-sm mt-3">
|
||||||
</div>
|
<%= icone :alerta, cor: nil %> Conta simples (entregas × valor da entrega). O valor certo sai quando a empresa fechar o período — bônus, retiradas e descontos entram só no fechamento.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
<%# ── Composição: de onde vem o valor fechado ──────────── %>
|
<%# ── Composição: de onde vem o valor fechado ──────────── %>
|
||||||
<h2 class="text-white font-bold text-xl mb-1 flex items-center gap-2"><%= icone :composicao, espaco: false %> De onde vem esse valor</h2>
|
<h2 class="text-white font-bold text-xl mb-1 flex items-center gap-2"><%= icone :composicao, espaco: false %> De onde vem esse valor</h2>
|
||||||
<p class="text-gray-400 text-base mb-3">Cada fechamento abaixo entra na soma do card branco.</p>
|
<p class="text-gray-400 text-base mb-3">Cada fechamento abaixo entra na soma do card branco.</p>
|
||||||
|
|
||||||
<% if @consolidacoes.any? %>
|
<% if @consolidacoes.any? %>
|
||||||
|
<div data-controller="carrossel" data-carrossel-tamanho-value="10">
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<% @consolidacoes.each do |cm| %>
|
<% @consolidacoes.each do |cm| %>
|
||||||
<% c = cm.consolidacao %>
|
<% c = cm.consolidacao %>
|
||||||
<div class="bg-[#1a1a1a] border border-[#2a2a2a] rounded-2xl p-5">
|
<div data-carrossel-target="item" class="bg-[#1a1a1a] border border-[#2a2a2a] rounded-2xl p-5">
|
||||||
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||||
<div class="min-w-0">
|
<div class="min-w-0">
|
||||||
<div class="flex items-center gap-2 flex-wrap">
|
<div class="flex items-center gap-2 flex-wrap">
|
||||||
@@ -162,6 +184,23 @@
|
|||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<%# Setas: alvos grandes (56px) porque a tela é usada de luva/no trânsito.
|
||||||
|
Somem sozinhas quando há uma página só (ver carrossel_controller.js). %>
|
||||||
|
<div data-carrossel-target="controles" class="hidden flex items-center justify-between gap-3 mt-4">
|
||||||
|
<button type="button" data-action="carrossel#anterior" data-carrossel-target="anterior"
|
||||||
|
class="bg-[#1a1a1a] border border-[#2a2a2a] hover:border-orange-500 text-white rounded-2xl w-14 h-14 flex items-center justify-center"
|
||||||
|
aria-label="Fechamentos anteriores">
|
||||||
|
<%= icone :voltar, cor: nil, tamanho: 'w-6 h-6', espaco: false %>
|
||||||
|
</button>
|
||||||
|
<span data-carrossel-target="indicador" class="text-gray-300 text-base font-semibold"></span>
|
||||||
|
<button type="button" data-action="carrossel#proxima" data-carrossel-target="proximo"
|
||||||
|
class="bg-orange-500 hover:bg-orange-600 text-black rounded-2xl w-14 h-14 flex items-center justify-center"
|
||||||
|
aria-label="Próximos fechamentos">
|
||||||
|
<%= icone :avancar, cor: nil, tamanho: 'w-6 h-6', espaco: false %>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<% else %>
|
<% else %>
|
||||||
<div class="bg-[#1a1a1a] border border-[#2a2a2a] rounded-2xl p-10 text-center">
|
<div class="bg-[#1a1a1a] border border-[#2a2a2a] rounded-2xl p-10 text-center">
|
||||||
<p class="mb-3"><%= icone :vazio, tamanho: 'w-12 h-12', cor: 'text-gray-600', espaco: false %></p>
|
<p class="mb-3"><%= icone :vazio, tamanho: 'w-12 h-12', cor: 'text-gray-600', espaco: false %></p>
|
||||||
|
|||||||
@@ -32,6 +32,24 @@ RSpec.describe 'Visão por motorista', type: :request do
|
|||||||
expect(response.body).to include('R$ 120,00')
|
expect(response.body).to include('R$ 120,00')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# O default tem que ser o MESMO recorte do dashboard ("Consolidado / Pago"),
|
||||||
|
# senão o operador compara as duas telas e vê valores diferentes.
|
||||||
|
it 'sem status na URL mostra o CONSOLIDADO (rascunho fica de fora)' do
|
||||||
|
consolidacao_finalizada(motorista: 'Carlos', valor: 120.0)
|
||||||
|
rascunho = create(:consolidacao, status: :rascunho,
|
||||||
|
data_inicio: Date.new(2026, 6, 1), data_fim: Date.new(2026, 6, 30))
|
||||||
|
create(:consolidacao_entrega, consolidacao: rascunho, motorista_nome: 'Carlos', valor_aplicado: 500.0)
|
||||||
|
create(:consolidacao_motorista, consolidacao: rascunho, motorista_nome: 'Carlos')
|
||||||
|
sign_in create(:gerente)
|
||||||
|
|
||||||
|
get consolidacoes_path(periodo.merge(visao: 'motoristas'))
|
||||||
|
expect(response.body).to include('R$ 120,00')
|
||||||
|
expect(response.body).not_to include('R$ 620,00')
|
||||||
|
|
||||||
|
get consolidacoes_path(periodo.merge(visao: 'motoristas', status: 'todos'))
|
||||||
|
expect(response.body).to include('R$ 620,00')
|
||||||
|
end
|
||||||
|
|
||||||
it 'só 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: 'Carlos', valor: 120.0, nome: 'Fechamento Junho')
|
||||||
consolidacao_finalizada(motorista: 'Pedro', valor: 90.0, nome: 'Fechamento do Pedro')
|
consolidacao_finalizada(motorista: 'Pedro', valor: 90.0, nome: 'Fechamento do Pedro')
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ RSpec.describe Analytics::TotaisPorMotorista do
|
|||||||
consolidar(motorista: 'Carlos', valor: 50.0, qtd: 1) # 50
|
consolidar(motorista: 'Carlos', valor: 50.0, qtd: 1) # 50
|
||||||
consolidar(motorista: 'Pedro', valor: 30.0, qtd: 1) # 30
|
consolidar(motorista: 'Pedro', valor: 30.0, qtd: 1) # 30
|
||||||
consolidar(motorista: 'Carlos', valor: 999.0, status: :rascunho) # fora (rascunho)
|
consolidar(motorista: 'Carlos', valor: 999.0, status: :rascunho) # fora (rascunho)
|
||||||
consolidar(motorista: 'Carlos', valor: 999.0, # fora (período)
|
consolidar(motorista: 'Carlos', valor: 999.0, # fora (não cruza)
|
||||||
data_inicio: Date.new(2026, 7, 1), data_fim: Date.new(2026, 7, 31))
|
data_inicio: Date.new(2026, 7, 1), data_fim: Date.new(2026, 7, 31))
|
||||||
|
|
||||||
expect(totais.linhas.map { |l| [l[:motorista], l[:valor_total]] })
|
expect(totais.linhas.map { |l| [l[:motorista], l[:valor_total]] })
|
||||||
@@ -37,6 +37,29 @@ RSpec.describe Analytics::TotaisPorMotorista do
|
|||||||
expect(totais.total_consolidacoes).to eq(3)
|
expect(totais.total_consolidacoes).to eq(3)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# ── Recorte de datas: o MESMO do dashboard financeiro ───────────────────
|
||||||
|
# Quando estes dois casos divergiam do dashboard, o operador via dois valores
|
||||||
|
# diferentes para o mesmo motorista no mesmo período e lia como erro de conta.
|
||||||
|
it 'inclui a consolidação que CRUZA o período (não precisa caber inteira)' do
|
||||||
|
# 20/05 a 20/06: começa antes do filtro, termina dentro.
|
||||||
|
consolidar(motorista: 'Carlos', valor: 100.0,
|
||||||
|
data_inicio: Date.new(2026, 5, 20), data_fim: Date.new(2026, 6, 20))
|
||||||
|
# 20/06 a 20/07: começa dentro, termina depois.
|
||||||
|
consolidar(motorista: 'Carlos', valor: 40.0,
|
||||||
|
data_inicio: Date.new(2026, 6, 20), data_fim: Date.new(2026, 7, 20))
|
||||||
|
|
||||||
|
expect(totais.total_geral).to eq(140.0)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'inclui a consolidação de outro período PAGA dentro do período' do
|
||||||
|
_, cm = consolidar(motorista: 'Carlos', valor: 80.0,
|
||||||
|
data_inicio: Date.new(2026, 4, 1), data_fim: Date.new(2026, 4, 30))
|
||||||
|
cm.update!(pago_em: Time.zone.local(2026, 6, 10, 12, 0), forma_pagamento: 'pix')
|
||||||
|
|
||||||
|
expect(totais.total_geral).to eq(80.0)
|
||||||
|
expect(totais.total_pago).to eq(80.0)
|
||||||
|
end
|
||||||
|
|
||||||
it 'respeita o filtro de status (o mesmo select da lista)' do
|
it 'respeita o filtro de status (o mesmo select da lista)' do
|
||||||
consolidar(motorista: 'Carlos', valor: 100.0)
|
consolidar(motorista: 'Carlos', valor: 100.0)
|
||||||
consolidar(motorista: 'Carlos', valor: 40.0, status: :rascunho)
|
consolidar(motorista: 'Carlos', valor: 40.0, status: :rascunho)
|
||||||
|
|||||||
Reference in New Issue
Block a user