Compare commits

...

2 Commits

21 changed files with 99 additions and 50 deletions

View File

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 80 KiB

View File

Before

Width:  |  Height:  |  Size: 189 KiB

After

Width:  |  Height:  |  Size: 189 KiB

View File

Before

Width:  |  Height:  |  Size: 138 KiB

After

Width:  |  Height:  |  Size: 138 KiB

View File

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View File

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 53 KiB

View File

Before

Width:  |  Height:  |  Size: 292 KiB

After

Width:  |  Height:  |  Size: 292 KiB

View File

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 65 KiB

View File

Before

Width:  |  Height:  |  Size: 122 KiB

After

Width:  |  Height:  |  Size: 122 KiB

View File

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 56 KiB

View File

Before

Width:  |  Height:  |  Size: 147 KiB

After

Width:  |  Height:  |  Size: 147 KiB

View File

Before

Width:  |  Height:  |  Size: 161 KiB

After

Width:  |  Height:  |  Size: 161 KiB

View File

Before

Width:  |  Height:  |  Size: 94 KiB

After

Width:  |  Height:  |  Size: 94 KiB

View File

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

View File

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 112 KiB

View File

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 70 KiB

View File

Before

Width:  |  Height:  |  Size: 160 KiB

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 140 KiB

After

Width:  |  Height:  |  Size: 75 KiB

View File

@@ -13,7 +13,7 @@ class ConsolidacaoEntregasController < ApplicationController
def validar def validar
authorize @consolidacao, :show? authorize @consolidacao, :show?
@motorista = params[:motorista] @motorista = params[:motorista]
@veiculo = params[:vehicle].presence # filtro "fechar por carro" (opcional) @veiculos_sel = veiculos_param(params[:vehicle]) # filtro por veículo(s) — multi-seleção
# Entregas atendidas do período no banco existente (read-only): sucesso + # Entregas atendidas do período no banco existente (read-only): sucesso +
# insucesso, pois o motorista foi ao local e a entrega precisa constar. # insucesso, pois o motorista foi ao local e a entrega precisa constar.
@@ -24,7 +24,7 @@ class ConsolidacaoEntregasController < ApplicationController
.do_motorista(@motorista) .do_motorista(@motorista)
@entregas = @entregas.da_veiculo(@consolidacao.vehicle_ids) if @consolidacao.vehicle_ids.present? @entregas = @entregas.da_veiculo(@consolidacao.vehicle_ids) if @consolidacao.vehicle_ids.present?
@entregas = @entregas.da_operacoes(@consolidacao.operacoes) if @consolidacao.operacoes.present? @entregas = @entregas.da_operacoes(@consolidacao.operacoes) if @consolidacao.operacoes.present?
@entregas = filtrar_por_veiculo(@entregas, @veiculo) if @veiculo @entregas = filtrar_por_veiculo(@entregas, @veiculos_sel) if @veiculos_sel.any?
@entregas = @entregas.order(:checkout) @entregas = @entregas.order(:checkout)
# Classificações já feitas — agrupadas por tracking_id (uma entrega pode ter # Classificações já feitas — agrupadas por tracking_id (uma entrega pode ter
@@ -36,10 +36,10 @@ class ConsolidacaoEntregasController < ApplicationController
# Status "fechado" por veículo (filtro + acompanhamento) e totais com escopo: # Status "fechado" por veículo (filtro + acompanhamento) e totais com escopo:
# quando um carro está selecionado, a barra reflete só aquele carro. # quando um carro está selecionado, a barra reflete só aquele carro.
@veiculos_status = veiculos_status(@motorista) @veiculos_status = veiculos_status(@motorista)
if @veiculo if @veiculos_sel.any?
atual = @veiculos_status.find { |v| v[:vehicle] == @veiculo } sel = @veiculos_status.select { |v| @veiculos_sel.include?(v[:vehicle]) }
@total_entregas = atual ? atual[:eligible] : 0 @total_entregas = sel.sum { |v| v[:eligible] }
@classificadas = atual ? atual[:classificadas] : 0 @classificadas = sel.sum { |v| v[:classificadas] }
else else
@total_entregas = @consolidacao.entregas_elegiveis_count(@motorista) @total_entregas = @consolidacao.entregas_elegiveis_count(@motorista)
@classificadas = @consolidacao.classificadas_count(@motorista) @classificadas = @consolidacao.classificadas_count(@motorista)
@@ -97,7 +97,7 @@ class ConsolidacaoEntregasController < ApplicationController
respond_to do |format| respond_to do |format|
format.json do format.json do
render json: resumo_json(params[:motorista], vehicle: params[:vehicle].presence).merge( render json: resumo_json(params[:motorista], vehicles: params[:vehicle]).merge(
ativo: ativo, tracking_id: params[:tracking_id], tipo: params[:tipo] ativo: ativo, tracking_id: params[:tracking_id], tipo: params[:tipo]
) )
end end
@@ -116,8 +116,8 @@ class ConsolidacaoEntregasController < ApplicationController
return render_nao_elegivel unless motorista_na_consolidacao?(motorista) return render_nao_elegivel unless motorista_na_consolidacao?(motorista)
# Recorte por veículo (fechar um carro de cada vez). Sem veículo = todos. # Recorte por veículo(s) selecionado(s). Sem veículo = todos.
elegiveis = tracking_ids_elegiveis(motorista, params[:vehicle].presence) elegiveis = tracking_ids_elegiveis(motorista, params[:vehicle])
# Sem tracking_ids = todas as elegíveis do período/veículo (botão "todos"). # 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. # 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) tracking_ids = tracking_ids.empty? ? elegiveis : (tracking_ids & elegiveis)
@@ -148,10 +148,10 @@ class ConsolidacaoEntregasController < ApplicationController
qtd: tracking_ids.size, motorista: motorista }) qtd: tracking_ids.size, motorista: motorista })
respond_to do |format| respond_to do |format|
format.json { render json: resumo_json(motorista, vehicle: params[:vehicle].presence) } format.json { render json: resumo_json(motorista, vehicles: params[:vehicle]) }
format.html do format.html do
verbo = remover ? 'desmarcadas de' : 'marcadas como' verbo = remover ? 'desmarcadas de' : 'marcadas como'
redirect_to validar_consolidacao_consolidacao_entregas_path(@consolidacao, motorista: motorista, vehicle: params[:vehicle].presence), redirect_to validar_consolidacao_consolidacao_entregas_path(@consolidacao, motorista: motorista, vehicle: veiculos_param(params[:vehicle]).presence),
notice: "#{tracking_ids.size} entregas #{verbo} #{tipo.humanize}." notice: "#{tracking_ids.size} entregas #{verbo} #{tipo.humanize}."
end end
end end
@@ -347,9 +347,20 @@ class ConsolidacaoEntregasController < ApplicationController
rel.pluck(:tracking_id) rel.pluck(:tracking_id)
end end
# Recorte por um único veículo (trata o grupo "sem veículo"). # Normaliza o parâmetro de veículo (string única OU array vindo de vehicle[])
def filtrar_por_veiculo(rel, veiculo) # numa lista limpa de selecionados.
veiculo == SEM_VEICULO ? rel.where(vehicle: [nil, '']) : rel.da_veiculo(veiculo) def veiculos_param(raw)
Array(raw).reject(&:blank?)
end
# Recorte por um OU MAIS veículos (trata o grupo "sem veículo"). Lista vazia
# = sem recorte (todos os veículos).
def filtrar_por_veiculo(rel, veiculos)
lista = veiculos_param(veiculos)
return rel if lista.empty?
valores = lista.reject { |v| v == SEM_VEICULO }
valores += [nil, ''] if lista.include?(SEM_VEICULO)
rel.where(vehicle: valores)
end end
# Status "fechado" por veículo do motorista: lista de # Status "fechado" por veículo do motorista: lista de
@@ -447,21 +458,28 @@ class ConsolidacaoEntregasController < ApplicationController
rel_manual.where.not(tipo: ConsolidacaoEntrega.tipos[:termo]).distinct.count(:tracking_id) rel_manual.where.not(tipo: ConsolidacaoEntrega.tipos[:termo]).distinct.count(:tracking_id)
end end
# `vehicle` (opcional): quando presente, `classificadas_escopo` reflete só aquele # `vehicles` (opcional, string ou array): quando presente, TODO o resumo
# carro (para a barra de progresso por veículo). `veiculos_status` acompanha o # (barra de progresso, contagem por pilar e valor total) reflete apenas o(s)
# payload para o front atualizar os chips sem recarregar. # veículo(s) selecionado(s). `veiculos_status` acompanha o payload para o front
def resumo_json(motorista, vehicle: nil) # atualizar os chips sem recarregar.
def resumo_json(motorista, vehicles: nil)
lista = veiculos_param(vehicles)
classificacoes = @consolidacao.consolidacao_entregas.where(motorista_nome: motorista) classificacoes = @consolidacao.consolidacao_entregas.where(motorista_nome: motorista)
status = veiculos_status(motorista) status = veiculos_status(motorista)
classificadas = classificacoes.distinct.count(:tracking_id) classificadas_motorista = classificacoes.distinct.count(:tracking_id)
escopo = if vehicle.present?
(status.find { |v| v[:vehicle] == vehicle } || {})[:classificadas] || 0 if lista.any?
else # Recorta as classificações às NFs dos veículos selecionados.
classificadas ids = tracking_ids_elegiveis(motorista, lista)
end classificacoes = classificacoes.where(tracking_id: ids)
escopo = status.select { |v| lista.include?(v[:vehicle]) }.sum { |v| v[:classificadas] }
else
escopo = classificadas_motorista
end
{ {
ok: true, ok: true,
classificadas: classificadas, classificadas: classificadas_motorista,
classificadas_escopo: escopo, classificadas_escopo: escopo,
manuais: contar_manuais(classificacoes.where(manual: true)), manuais: contar_manuais(classificacoes.where(manual: true)),
por_tipo: classificacoes.group(:tipo).sum(:quantidade), por_tipo: classificacoes.group(:tipo).sum(:quantidade),

View File

@@ -6,7 +6,7 @@ import { Controller } from "@hotwired/stimulus"
export default class extends Controller { export default class extends Controller {
static targets = [ static targets = [
"linha", "checkbox", "selecionarTodos", "linha", "checkbox", "selecionarTodos", "busca", "buscaVazia",
"toggles", "barra", "barraManuais", "percentual", "chipVeiculo", "toggles", "barra", "barraManuais", "percentual", "chipVeiculo",
"contadorClassificadas", "contadorTotal", "contadorManuais", "manuaisInfo", "contadorClassificadas", "contadorTotal", "contadorManuais", "manuaisInfo",
"qtdNormal", "qtdRetirada", "qtdBonus", "qtdDesconto", "qtdExtraordinaria", "qtdNormal", "qtdRetirada", "qtdBonus", "qtdDesconto", "qtdExtraordinaria",
@@ -18,7 +18,7 @@ export default class extends Controller {
massaUrl: String, // POST classificar_em_massa massaUrl: String, // POST classificar_em_massa
motorista: String, motorista: String,
total: Number, total: Number,
veiculo: String, // veículo selecionado (filtro "fechar por carro") veiculos: Array, // veículo(s) selecionado(s) — filtro multi-seleção
precoExtraordinaria: Number // preço padrão sugerido no prompt precoExtraordinaria: Number // preço padrão sugerido no prompt
} }
@@ -51,7 +51,7 @@ export default class extends Controller {
tracking_id: tracking, tracking_id: tracking,
tipo: tipo, tipo: tipo,
motorista: this.motoristaValue, motorista: this.motoristaValue,
vehicle: this.veiculoValue, vehicle: this.veiculosValue,
valor: valor valor: valor
}) })
// resp.ativo indica se o pilar ficou ligado ou desligado (toggle) // resp.ativo indica se o pilar ficou ligado ou desligado (toggle)
@@ -64,10 +64,27 @@ export default class extends Controller {
} }
} }
// ── Selecionar / desmarcar todos os checkboxes ───────────── // ── Busca na listagem de notas (filtra os cards por NF/local/endereço/veículo) ──
filtrarBusca(event) {
const q = (event.currentTarget.value || "").trim().toLowerCase()
let visiveis = 0
this.linhaTargets.forEach(l => {
const ok = q === "" || (l.dataset.busca || "").includes(q)
l.classList.toggle("hidden", !ok)
if (ok) visiveis++
})
if (this.hasBuscaVaziaTarget)
this.buscaVaziaTarget.classList.toggle("hidden", q === "" || visiveis > 0)
}
// ── Selecionar / desmarcar todos os checkboxes (só os visíveis na busca) ──
selecionarTodos(event) { selecionarTodos(event) {
const marcar = event.currentTarget.checked const marcar = event.currentTarget.checked
this.checkboxTargets.forEach(c => { c.checked = marcar }) this.checkboxTargets.forEach(c => {
const linha = c.closest("[data-busca]")
if (linha && linha.classList.contains("hidden")) return
c.checked = marcar
})
} }
// O pilar está ativo (ring-2) na entrega indicada? // O pilar está ativo (ring-2) na entrega indicada?
@@ -101,7 +118,7 @@ export default class extends Controller {
const resp = await this.post(this.massaUrlValue, { const resp = await this.post(this.massaUrlValue, {
tipo: tipo, tipo: tipo,
motorista: this.motoristaValue, motorista: this.motoristaValue,
vehicle: this.veiculoValue, vehicle: this.veiculosValue,
tracking_ids: ids, tracking_ids: ids,
acao: remover ? "remover" : "adicionar", acao: remover ? "remover" : "adicionar",
valor: valor valor: valor
@@ -212,12 +229,13 @@ export default class extends Controller {
if (total) total.textContent = dados.eligible if (total) total.textContent = dados.eligible
if (check) check.textContent = dados.fechado ? "✓" : "" if (check) check.textContent = dados.fechado ? "✓" : ""
// Estado de cor determinístico: verde (fechado) > laranja (selecionado) > neutro. // Estado de cor determinístico: laranja (selecionado) > verde (fechado) > neutro.
// Seleção tem prioridade para o usuário sempre enxergar quais carros somam.
chip.classList.remove(...VERDE, ...LARANJA, ...NEUTRO) chip.classList.remove(...VERDE, ...LARANJA, ...NEUTRO)
if (dados.fechado) { if (this.veiculosValue.includes(chip.dataset.vehicle)) {
chip.classList.add(...VERDE)
} else if (chip.dataset.vehicle === this.veiculoValue) {
chip.classList.add(...LARANJA) chip.classList.add(...LARANJA)
} else if (dados.fechado) {
chip.classList.add(...VERDE)
} else { } else {
chip.classList.add(...NEUTRO) chip.classList.add(...NEUTRO)
} }

View File

@@ -6,14 +6,14 @@
data-validacao-massa-url-value="<%= classificar_em_massa_consolidacao_consolidacao_entregas_path(@consolidacao) %>" data-validacao-massa-url-value="<%= classificar_em_massa_consolidacao_consolidacao_entregas_path(@consolidacao) %>"
data-validacao-motorista-value="<%= @motorista %>" data-validacao-motorista-value="<%= @motorista %>"
data-validacao-total-value="<%= @total_entregas %>" data-validacao-total-value="<%= @total_entregas %>"
data-validacao-veiculo-value="<%= @veiculo %>" data-validacao-veiculos-value="<%= @veiculos_sel.to_json %>"
data-validacao-preco-extraordinaria-value="<%= @precos[:extraordinaria] %>"> data-validacao-preco-extraordinaria-value="<%= @precos[:extraordinaria] %>">
<%# Cabeçalho + controles fixos no topo (no desktop); só a lista de NFs rola. %> <%# Cabeçalho + controles fixos no topo (no desktop); só a lista de NFs rola. %>
<div class="lg:shrink-0"> <div class="lg:shrink-0">
<%# Header %> <%# Header %>
<div class="mb-6"> <div class="mb-3">
<%= link_to '← Voltar (Passo 1)', wizard_consolidacao_path(@consolidacao), <%= link_to '← Voltar (Passo 1)', wizard_consolidacao_path(@consolidacao),
class: 'inline-flex items-center gap-1 bg-[#1a1a1a] hover:bg-[#2a2a2a] text-white border border-[#2a2a2a] hover:border-orange-500 font-semibold px-4 py-2.5 rounded-lg text-sm min-h-[44px]' %> class: 'inline-flex items-center gap-1 bg-[#1a1a1a] hover:bg-[#2a2a2a] text-white border border-[#2a2a2a] hover:border-orange-500 font-semibold px-4 py-2.5 rounded-lg text-sm min-h-[44px]' %>
<div class="flex items-center justify-between mt-2"> <div class="flex items-center justify-between mt-2">
@@ -28,10 +28,10 @@
</div> </div>
<%# Barra de progresso (reflete o veículo selecionado, se houver) %> <%# 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="bg-[#1a1a1a] border border-[#2a2a2a] rounded-xl p-3 mb-3">
<div class="flex justify-between text-sm mb-2"> <div class="flex justify-between text-sm mb-2">
<span class="text-white font-semibold"> <span class="text-white font-semibold">
<% if @veiculo.present? %>🚚 <%= @veiculo %> — <% end %> <% if @veiculos_sel.any? %>🚚 <%= @veiculos_sel.join(', ') %> — <% end %>
<span data-validacao-target="contadorClassificadas"><%= @classificadas %></span> <span data-validacao-target="contadorClassificadas"><%= @classificadas %></span>
de <span data-validacao-target="contadorTotal"><%= @total_entregas %></span> entregas classificadas de <span data-validacao-target="contadorTotal"><%= @total_entregas %></span> entregas classificadas
<span data-validacao-target="manuaisInfo" class="<%= 'hidden' unless @manuais.positive? %> text-blue-400 font-normal">· <span data-validacao-target="manuaisInfo" class="<%= 'hidden' unless @manuais.positive? %> text-blue-400 font-normal">·
@@ -49,25 +49,27 @@
<%# Fechar por veículo — filtra a tela por carro e mostra quais já estão fechados. <%# 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. %> Só aparece quando o motorista usou mais de um veículo no período. %>
<% if @veiculos_status.size > 1 %> <% if @veiculos_status.size > 1 %>
<div class="bg-[#1a1a1a] border border-[#2a2a2a] rounded-xl p-4 mb-4"> <div class="bg-[#1a1a1a] border border-[#2a2a2a] rounded-xl p-3 mb-3">
<div class="flex items-center gap-2 mb-2"> <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-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> <span class="text-gray-600 text-xs">clique para filtrar/somar um ou vários carros (clique de novo p/ tirar)</span>
</div> </div>
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">
<%# Chip "Todos" %> <%# Chip "Todos" — limpa a seleção (mostra todos os carros) %>
<%= link_to validar_consolidacao_consolidacao_entregas_path(@consolidacao, motorista: @motorista), <%= 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 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 %> #{@veiculos_sel.empty? ? 'border-orange-500 text-orange-400 bg-orange-500/10' : 'border-[#2a2a2a] text-gray-300 hover:border-orange-500'}" do %>
Todos Todos
<% end %> <% end %>
<% @veiculos_status.each do |v| %> <% @veiculos_status.each do |v| %>
<% selecionado = (@veiculo == v[:vehicle]) %> <% selecionado = @veiculos_sel.include?(v[:vehicle]) %>
<%= link_to validar_consolidacao_consolidacao_entregas_path(@consolidacao, motorista: @motorista, vehicle: v[:vehicle]), <%# Alterna este veículo dentro da seleção atual (multi-seleção) %>
<% nova_sel = selecionado ? (@veiculos_sel - [v[:vehicle]]) : (@veiculos_sel + [v[:vehicle]]) %>
<%= link_to validar_consolidacao_consolidacao_entregas_path(@consolidacao, motorista: @motorista, vehicle: nova_sel.presence),
data: { 'validacao-target': 'chipVeiculo', vehicle: v[:vehicle], eligible: v[:eligible] }, 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 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 %> #{selecionado ? 'border-orange-500 text-orange-400 bg-orange-500/10' : (v[:fechado] ? 'border-green-500 text-green-400 bg-green-500/10' : 'border-[#2a2a2a] text-gray-300 hover:border-orange-500')}" do %>
<span><%= v[:vehicle] %></span> <span><%= v[:vehicle] %></span>
<span class="text-xs <%= v[:fechado] ? 'text-green-400' : 'text-gray-500' %>"> <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-feito><%= v[:classificadas] %></span>/<span data-chip-total><%= v[:eligible] %></span>
@@ -80,7 +82,7 @@
<% end %> <% end %>
<%# Ações em massa + filtro %> <%# 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"> <div class="bg-[#1a1a1a] border border-[#2a2a2a] rounded-xl p-3 mb-3 flex flex-wrap items-center gap-3">
<label class="flex items-center gap-2 text-gray-300 text-sm cursor-pointer select-none"> <label class="flex items-center gap-2 text-gray-300 text-sm cursor-pointer select-none">
<input type="checkbox" data-validacao-target="selecionarTodos" <input type="checkbox" data-validacao-target="selecionarTodos"
data-action="change->validacao#selecionarTodos" data-action="change->validacao#selecionarTodos"
@@ -104,7 +106,7 @@
<div class="ml-auto"> <div class="ml-auto">
<%= link_to params[:apenas_pendentes] == '1' ? 'Mostrar todas' : 'Apenas não classificadas', <%= link_to params[:apenas_pendentes] == '1' ? 'Mostrar todas' : 'Apenas não classificadas',
validar_consolidacao_consolidacao_entregas_path(@consolidacao, motorista: @motorista, vehicle: @veiculo, validar_consolidacao_consolidacao_entregas_path(@consolidacao, motorista: @motorista, vehicle: @veiculos_sel.presence,
apenas_pendentes: params[:apenas_pendentes] == '1' ? nil : '1'), 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' %> class: 'text-orange-400 hover:text-orange-300 text-sm border border-orange-500/40 px-3 py-2 rounded-lg' %>
</div> </div>
@@ -114,7 +116,7 @@
entrega e entrega de termo) num único botão com menu suspenso. Os modais e entrega e entrega de termo) num único botão com menu suspenso. Os modais e
controllers existentes são reaproveitados; só o gatilho mudou. %> controllers existentes são reaproveitados; só o gatilho mudou. %>
<% unless @consolidacao.finalizada? && current_user.operador? %> <% unless @consolidacao.finalizada? && current_user.operador? %>
<div class="relative mb-4" <div class="relative mb-3"
data-controller="ferramentas apontamento-manual apontamento-termo" data-controller="ferramentas apontamento-manual apontamento-termo"
data-apontamento-manual-apontar-manual-url-value="<%= apontar_manual_consolidacao_consolidacao_entregas_path(@consolidacao) %>" data-apontamento-manual-apontar-manual-url-value="<%= apontar_manual_consolidacao_consolidacao_entregas_path(@consolidacao) %>"
data-apontamento-termo-apontar-termo-url-value="<%= apontar_termo_consolidacao_consolidacao_entregas_path(@consolidacao) %>" data-apontamento-termo-apontar-termo-url-value="<%= apontar_termo_consolidacao_consolidacao_entregas_path(@consolidacao) %>"
@@ -257,16 +259,27 @@
</div> </div>
<%# Fim do cabeçalho fixo — abaixo só a lista de NFs rola. %> <%# Fim do cabeçalho fixo — abaixo só a lista de NFs rola. %>
<div class="grid grid-cols-1 lg:grid-cols-4 gap-4 lg:flex-1 lg:min-h-0"> <div class="grid grid-cols-1 lg:grid-cols-4 gap-3 lg:flex-1 lg:min-h-0">
<%# ── Lista de entregas (3 colunas) — área rolável ──── %> <%# ── Lista de entregas (3 colunas) — área rolável ──── %>
<div class="lg:col-span-3 space-y-2 lg:overflow-y-auto lg:pr-1"> <div class="lg:col-span-3 space-y-2 lg:overflow-y-auto lg:pr-1">
<%# 🔍 Busca na listagem — filtra os cards por NF, local, endereço ou veículo %>
<div class="sticky top-0 z-20 bg-[#0a0a0a] pb-2">
<div class="relative">
<span class="absolute left-3 top-1/2 -translate-y-1/2 text-gray-500 pointer-events-none">🔍</span>
<input type="search" data-validacao-target="busca" data-action="input->validacao#filtrarBusca"
placeholder="Buscar NF, local, endereço ou veículo…" autocomplete="off"
class="w-full bg-[#1a1a1a] border border-[#2a2a2a] rounded-lg pl-10 pr-3 py-2.5 text-sm text-gray-200 placeholder-gray-500 focus:border-orange-500 focus:outline-none">
</div>
<p data-validacao-target="buscaVazia" class="hidden text-gray-500 text-xs mt-2 px-1">Nenhuma nota encontrada para a busca.</p>
</div>
<% if @entregas.any? %> <% if @entregas.any? %>
<% @entregas.each do |e| %> <% @entregas.each do |e| %>
<% tipos_ativos = (@classificacoes[e.tracking_id] || []).map(&:tipo) %> <% 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 lg:flex-row lg:items-center gap-3" <div class="bg-[#1a1a1a] border <%= e.falhada? ? 'border-red-500/50' : 'border-[#2a2a2a]' %> rounded-xl p-4 flex flex-col lg:flex-row lg:items-center gap-3"
data-validacao-target="linha" data-tracking="<%= e.tracking_id %>" data-validacao-target="linha" data-tracking="<%= e.tracking_id %>"
data-vehicle="<%= e.vehicle.presence || '(sem veículo)' %>" data-vehicle="<%= e.vehicle.presence || '(sem veículo)' %>"
data-busca="<%= [e.numero_nf, e.local, e.address, e.vehicle].compact.join(' ').downcase %>"
data-classificada="<%= tipos_ativos.any? ? '1' : '0' %>"> data-classificada="<%= tipos_ativos.any? ? '1' : '0' %>">
<%# Checkbox seleção em massa %> <%# Checkbox seleção em massa %>