Adição do filtro de veiculon na parte da consolidação

This commit is contained in:
2026-06-25 12:33:17 -03:00
parent 0c5b5bcbe6
commit f42949db78
5 changed files with 197 additions and 22 deletions

View File

@@ -5,11 +5,15 @@ class ConsolidacaoEntregasController < ApplicationController
before_action :set_consolidacao
before_action :bloquear_se_finalizada, except: %i[validar revisar]
# Rótulo/sentinela do grupo de entregas sem veículo no filtro "fechar por carro".
SEM_VEICULO = '(sem veículo)'.freeze
# GET /consolidacoes/:consolidacao_id/consolidacao_entregas/validar?motorista=X
# WIZARD PASSO 2 — tela de validação com toggles
def validar
authorize @consolidacao, :show?
@motorista = params[:motorista]
@veiculo = params[:vehicle].presence # filtro "fechar por carro" (opcional)
# Entregas atendidas do período no banco existente (read-only): sucesso +
# insucesso, pois o motorista foi ao local e a entrega precisa constar.
@@ -20,6 +24,7 @@ class ConsolidacaoEntregasController < ApplicationController
.do_motorista(@motorista)
@entregas = @entregas.da_veiculo(@consolidacao.vehicle_ids) if @consolidacao.vehicle_ids.present?
@entregas = @entregas.da_operacoes(@consolidacao.operacoes) if @consolidacao.operacoes.present?
@entregas = filtrar_por_veiculo(@entregas, @veiculo) if @veiculo
@entregas = @entregas.order(:checkout)
# Classificações já feitas — agrupadas por tracking_id (uma entrega pode ter
@@ -28,8 +33,17 @@ class ConsolidacaoEntregasController < ApplicationController
.where(motorista_nome: @motorista)
.group_by(&:tracking_id)
@total_entregas = @consolidacao.entregas_elegiveis_count(@motorista)
@classificadas = @consolidacao.classificadas_count(@motorista)
# Status "fechado" por veículo (filtro + acompanhamento) e totais com escopo:
# quando um carro está selecionado, a barra reflete só aquele carro.
@veiculos_status = veiculos_status(@motorista)
if @veiculo
atual = @veiculos_status.find { |v| v[:vehicle] == @veiculo }
@total_entregas = atual ? atual[:eligible] : 0
@classificadas = atual ? atual[:classificadas] : 0
else
@total_entregas = @consolidacao.entregas_elegiveis_count(@motorista)
@classificadas = @consolidacao.classificadas_count(@motorista)
end
# Nomes dos motoristas ATIVOS — alimenta o seletor do apontamento 100% manual.
@motoristas_consolidacao = @consolidacao.consolidacao_motoristas.ativos.pluck(:motorista_nome)
@@ -77,7 +91,7 @@ class ConsolidacaoEntregasController < ApplicationController
respond_to do |format|
format.json do
render json: resumo_json(params[:motorista]).merge(
render json: resumo_json(params[:motorista], vehicle: params[:vehicle].presence).merge(
ativo: ativo, tracking_id: params[:tracking_id], tipo: params[:tipo]
)
end
@@ -96,8 +110,9 @@ class ConsolidacaoEntregasController < ApplicationController
return render_nao_elegivel unless motorista_na_consolidacao?(motorista)
elegiveis = tracking_ids_elegiveis(motorista)
# Sem tracking_ids = todas as elegíveis do período (botão "todos").
# Recorte por veículo (fechar um carro de cada vez). Sem veículo = todos.
elegiveis = tracking_ids_elegiveis(motorista, params[:vehicle].presence)
# Sem tracking_ids = todas as elegíveis do período/veículo (botão "todos").
# Com tracking_ids → mantém só as que de fato são elegíveis para o motorista.
tracking_ids = tracking_ids.empty? ? elegiveis : (tracking_ids & elegiveis)
@@ -127,10 +142,10 @@ class ConsolidacaoEntregasController < ApplicationController
qtd: tracking_ids.size, motorista: motorista })
respond_to do |format|
format.json { render json: resumo_json(motorista) }
format.json { render json: resumo_json(motorista, vehicle: params[:vehicle].presence) }
format.html do
verbo = remover ? 'desmarcadas de' : 'marcadas como'
redirect_to validar_consolidacao_consolidacao_entregas_path(@consolidacao, motorista: motorista),
redirect_to validar_consolidacao_consolidacao_entregas_path(@consolidacao, motorista: motorista, vehicle: params[:vehicle].presence),
notice: "#{tracking_ids.size} entregas #{verbo} #{tipo.humanize}."
end
end
@@ -286,7 +301,9 @@ class ConsolidacaoEntregasController < ApplicationController
# tracking_ids atendidos/elegíveis para o motorista no período (respeita veículos).
# Atendidas = sucesso + insucesso com checkout (o motorista foi ao local).
def tracking_ids_elegiveis(motorista)
# `vehicle` (opcional) recorta para UM carro — é o que faz "Marcar todos" fechar
# só o veículo selecionado.
def tracking_ids_elegiveis(motorista, vehicle = nil)
return [] unless motorista.present?
rel = Entrega.atendidas.da_conta_gade
@@ -294,9 +311,41 @@ class ConsolidacaoEntregasController < ApplicationController
.do_motorista(motorista)
rel = rel.da_veiculo(@consolidacao.vehicle_ids) if @consolidacao.vehicle_ids.present?
rel = rel.da_operacoes(@consolidacao.operacoes) if @consolidacao.operacoes.present?
rel = filtrar_por_veiculo(rel, vehicle) if vehicle.present?
rel.pluck(:tracking_id)
end
# Recorte por um único veículo (trata o grupo "sem veículo").
def filtrar_por_veiculo(rel, veiculo)
veiculo == SEM_VEICULO ? rel.where(vehicle: [nil, '']) : rel.da_veiculo(veiculo)
end
# Status "fechado" por veículo do motorista: lista de
# { vehicle:, eligible:, classificadas:, fechado: }. Fonte única do filtro de
# carros e do acompanhamento (chips). 2 queries: elegíveis (com a coluna do
# veículo) e as já classificadas (não-manuais).
def veiculos_status(motorista)
return [] unless motorista.present?
rel = Entrega.atendidas.da_conta_gade
.no_periodo_checkout(@consolidacao.data_inicio, @consolidacao.data_fim)
.do_motorista(motorista)
rel = rel.da_veiculo(@consolidacao.vehicle_ids) if @consolidacao.vehicle_ids.present?
rel = rel.da_operacoes(@consolidacao.operacoes) if @consolidacao.operacoes.present?
por_veiculo = rel.pluck(:tracking_id, :vehicle).group_by { |(_id, v)| v.presence || SEM_VEICULO }
classificadas = @consolidacao.consolidacao_entregas
.where(motorista_nome: motorista, manual: false)
.distinct.pluck(:tracking_id).to_set
por_veiculo.map do |veiculo, linhas|
ids = linhas.map(&:first)
feito = ids.count { |id| classificadas.include?(id) }
{ vehicle: veiculo, eligible: ids.size, classificadas: feito,
fechado: ids.size.positive? && feito >= ids.size }
end.sort_by { |v| v[:vehicle] }
end
# Valida que a entrega pertence ao motorista informado e ao período/veículo da
# consolidação — impede classificar tracking_ids arbitrários via requisição.
def entrega_elegivel?(motorista, tracking_id)
@@ -359,13 +408,25 @@ class ConsolidacaoEntregasController < ApplicationController
@consolidacao.recalcular_motorista!(nome)
end
def resumo_json(motorista)
# `vehicle` (opcional): quando presente, `classificadas_escopo` reflete só aquele
# carro (para a barra de progresso por veículo). `veiculos_status` acompanha o
# payload para o front atualizar os chips sem recarregar.
def resumo_json(motorista, vehicle: nil)
classificacoes = @consolidacao.consolidacao_entregas.where(motorista_nome: motorista)
status = veiculos_status(motorista)
classificadas = classificacoes.distinct.count(:tracking_id)
escopo = if vehicle.present?
(status.find { |v| v[:vehicle] == vehicle } || {})[:classificadas] || 0
else
classificadas
end
{
ok: true,
classificadas: classificacoes.distinct.count(:tracking_id),
por_tipo: classificacoes.group(:tipo).count,
valor_total: classificacoes.sum { |e| e.tipo == 'desconto' ? -e.valor_aplicado : e.valor_aplicado }.to_f
classificadas: classificadas,
classificadas_escopo: escopo,
por_tipo: classificacoes.group(:tipo).count,
valor_total: classificacoes.sum { |e| e.tipo == 'desconto' ? -e.valor_aplicado : e.valor_aplicado }.to_f,
veiculos_status: status
}
end
end

View File

@@ -7,7 +7,7 @@ import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = [
"linha", "checkbox", "selecionarTodos", "modoRemover", "rotuloAcao",
"toggles", "barra", "percentual",
"toggles", "barra", "percentual", "chipVeiculo",
"contadorClassificadas", "contadorTotal",
"qtdNormal", "qtdRetirada", "qtdBonus", "qtdDesconto", "qtdExtraordinaria", "valorTotal"
]
@@ -17,6 +17,7 @@ export default class extends Controller {
massaUrl: String, // POST classificar_em_massa
motorista: String,
total: Number,
veiculo: String, // veículo selecionado (filtro "fechar por carro")
precoExtraordinaria: Number // preço padrão sugerido no prompt
}
@@ -49,6 +50,7 @@ export default class extends Controller {
tracking_id: tracking,
tipo: tipo,
motorista: this.motoristaValue,
vehicle: this.veiculoValue,
valor: valor
})
// resp.ativo indica se o pilar ficou ligado ou desligado (toggle)
@@ -77,7 +79,8 @@ export default class extends Controller {
const resp = await this.post(this.massaUrlValue, {
tipo: tipo,
motorista: this.motoristaValue,
tracking_ids: [], // vazio = todas
vehicle: this.veiculoValue,
tracking_ids: [], // vazio = todas (do veículo selecionado, se houver)
acao: remover ? "remover" : "adicionar",
valor: valor
})
@@ -124,6 +127,7 @@ export default class extends Controller {
const resp = await this.post(this.massaUrlValue, {
tipo: tipo,
motorista: this.motoristaValue,
vehicle: this.veiculoValue,
tracking_ids: ids,
acao: remover ? "remover" : "adicionar",
valor: valor
@@ -182,7 +186,8 @@ export default class extends Controller {
this.valorTotalTarget.textContent =
"R$ " + valor.toLocaleString("pt-BR", { minimumFractionDigits: 2, maximumFractionDigits: 2 })
const classificadas = dados.classificadas || 0
// Barra/contador refletem o ESCOPO atual (veículo selecionado, se houver).
const classificadas = (dados.classificadas_escopo ?? dados.classificadas) || 0
this.contadorClassificadasTarget.textContent = classificadas
const total = this.totalValue || parseInt(this.contadorTotalTarget.textContent)
@@ -190,10 +195,43 @@ export default class extends Controller {
this.barraTarget.style.width = `${Math.min(pct, 100)}%`
this.percentualTarget.textContent = `${Math.min(pct, 100)}%`
if (pct >= 100) {
this.barraTarget.classList.remove("bg-orange-500")
this.barraTarget.classList.add("bg-green-500")
}
this.barraTarget.classList.toggle("bg-green-500", pct >= 100)
this.barraTarget.classList.toggle("bg-orange-500", pct < 100)
if (dados.veiculos_status) this.atualizarChips(dados.veiculos_status)
}
// Atualiza os chips "fechar por veículo" (contador + estado fechado) a partir
// do status devolvido pelo servidor.
atualizarChips(status) {
if (!this.hasChipVeiculoTarget) return
const porVeiculo = {}
status.forEach(v => { porVeiculo[v.vehicle] = v })
const VERDE = ["border-green-500", "text-green-400", "bg-green-500/10"]
const LARANJA = ["border-orange-500", "text-orange-400", "bg-orange-500/10"]
const NEUTRO = ["border-[#2a2a2a]", "text-gray-300"]
this.chipVeiculoTargets.forEach(chip => {
const dados = porVeiculo[chip.dataset.vehicle]
if (!dados) return
const feito = chip.querySelector("[data-chip-feito]")
const total = chip.querySelector("[data-chip-total]")
const check = chip.querySelector("[data-chip-check]")
if (feito) feito.textContent = dados.classificadas
if (total) total.textContent = dados.eligible
if (check) check.textContent = dados.fechado ? "✓" : ""
// Estado de cor determinístico: verde (fechado) > laranja (selecionado) > neutro.
chip.classList.remove(...VERDE, ...LARANJA, ...NEUTRO)
if (dados.fechado) {
chip.classList.add(...VERDE)
} else if (chip.dataset.vehicle === this.veiculoValue) {
chip.classList.add(...LARANJA)
} else {
chip.classList.add(...NEUTRO)
}
})
}
labelTipo(tipo) {

View File

@@ -6,6 +6,7 @@
data-validacao-massa-url-value="<%= classificar_em_massa_consolidacao_consolidacao_entregas_path(@consolidacao) %>"
data-validacao-motorista-value="<%= @motorista %>"
data-validacao-total-value="<%= @total_entregas %>"
data-validacao-veiculo-value="<%= @veiculo %>"
data-validacao-preco-extraordinaria-value="<%= @precos[:extraordinaria] %>">
<%# Header %>
@@ -23,10 +24,11 @@
</div>
</div>
<%# Barra de progresso %>
<%# Barra de progresso (reflete o veículo selecionado, se houver) %>
<div class="bg-[#1a1a1a] border border-[#2a2a2a] rounded-xl p-4 mb-4">
<div class="flex justify-between text-sm mb-2">
<span class="text-white font-semibold">
<% if @veiculo.present? %>🚚 <%= @veiculo %> — <% end %>
<span data-validacao-target="contadorClassificadas"><%= @classificadas %></span>
de <span data-validacao-target="contadorTotal"><%= @total_entregas %></span> entregas classificadas
</span>
@@ -37,6 +39,39 @@
</div>
</div>
<%# Fechar por veículo — filtra a tela por carro e mostra quais já estão fechados.
Só aparece quando o motorista usou mais de um veículo no período. %>
<% if @veiculos_status.size > 1 %>
<div class="bg-[#1a1a1a] border border-[#2a2a2a] rounded-xl p-4 mb-4">
<div class="flex items-center gap-2 mb-2">
<span class="text-gray-400 text-sm font-semibold">🚗 Fechar por veículo:</span>
<span class="text-gray-600 text-xs">clique para filtrar e classificar um carro de cada vez</span>
</div>
<div class="flex flex-wrap gap-2">
<%# Chip "Todos" %>
<%= link_to validar_consolidacao_consolidacao_entregas_path(@consolidacao, motorista: @motorista),
class: "px-3 py-2 rounded-lg text-sm font-semibold border min-h-[40px] flex items-center
#{@veiculo.blank? ? 'border-orange-500 text-orange-400 bg-orange-500/10' : 'border-[#2a2a2a] text-gray-300 hover:border-orange-500'}" do %>
Todos
<% end %>
<% @veiculos_status.each do |v| %>
<% selecionado = (@veiculo == v[:vehicle]) %>
<%= link_to validar_consolidacao_consolidacao_entregas_path(@consolidacao, motorista: @motorista, vehicle: v[:vehicle]),
data: { 'validacao-target': 'chipVeiculo', vehicle: v[:vehicle], eligible: v[:eligible] },
class: "px-3 py-2 rounded-lg text-sm font-semibold border min-h-[40px] flex items-center gap-2 transition-colors
#{v[:fechado] ? 'border-green-500 text-green-400 bg-green-500/10' : (selecionado ? 'border-orange-500 text-orange-400 bg-orange-500/10' : 'border-[#2a2a2a] text-gray-300 hover:border-orange-500')}" do %>
<span><%= v[:vehicle] %></span>
<span class="text-xs <%= v[:fechado] ? 'text-green-400' : 'text-gray-500' %>">
<span data-chip-feito><%= v[:classificadas] %></span>/<span data-chip-total><%= v[:eligible] %></span>
<span data-chip-check><%= '✓' if v[:fechado] %></span>
</span>
<% end %>
<% end %>
</div>
</div>
<% end %>
<%# Ações em massa + filtro %>
<div class="bg-[#1a1a1a] border border-[#2a2a2a] rounded-xl p-4 mb-4 flex flex-wrap items-center gap-3">
<span class="text-gray-400 text-sm font-semibold">Ações em massa:</span>
@@ -75,7 +110,7 @@
<div class="ml-auto">
<%= link_to params[:apenas_pendentes] == '1' ? 'Mostrar todas' : 'Apenas não classificadas',
validar_consolidacao_consolidacao_entregas_path(@consolidacao, motorista: @motorista,
validar_consolidacao_consolidacao_entregas_path(@consolidacao, motorista: @motorista, vehicle: @veiculo,
apenas_pendentes: params[:apenas_pendentes] == '1' ? nil : '1'),
class: 'text-orange-400 hover:text-orange-300 text-sm border border-orange-500/40 px-3 py-2 rounded-lg' %>
</div>
@@ -173,6 +208,7 @@
<% tipos_ativos = (@classificacoes[e.tracking_id] || []).map(&:tipo) %>
<div class="bg-[#1a1a1a] border <%= e.falhada? ? 'border-red-500/50' : 'border-[#2a2a2a]' %> rounded-xl p-4 flex flex-col md:flex-row md:items-center gap-3"
data-validacao-target="linha" data-tracking="<%= e.tracking_id %>"
data-vehicle="<%= e.vehicle.presence || '(sem veículo)' %>"
data-classificada="<%= tipos_ativos.any? ? '1' : '0' %>">
<%# Checkbox seleção em massa %>