diff --git a/Imagens para implantação /Imagem colada (19).png b/Imagens para implantação /Imagem colada (19).png new file mode 100644 index 0000000..c301943 Binary files /dev/null and b/Imagens para implantação /Imagem colada (19).png differ diff --git a/README.md b/README.md index bab0009..f0e4e1a 100644 --- a/README.md +++ b/README.md @@ -840,7 +840,7 @@ app/views/consolidacao_entregas/validar.html.erb # selo de status por entrega --- -
+
🆕 Atualização 25/06/2026 — Apontamento 100% manual + Arquivar motorista individualmente Dois recursos na consolidação: @@ -902,3 +902,43 @@ docker-compose exec app bundle exec rspec
+--- + +
+🆕 Atualização 25/06/2026 — Fechar por veículo na tela de Validar + +Um motorista pode usar **vários veículos** no período. A tela de Validar (Passo 2) mostrava tudo +junto e o **"Marcar todos como Normal"** marcava **todas** as entregas do motorista. Agora dá +para **fechar cada carro individualmente**. + +### 🆕 Funcionalidades +- **Filtro por veículo** (chips "🚗 Fechar por veículo") na tela de Validar — só aparece quando o + motorista usou **mais de um veículo**. Clicar num carro filtra a lista para aquele veículo; a + **barra de progresso** e o **"Marcar todos"/seleção** passam a valer **só para o carro + selecionado**. Há um chip **"Todos"** para a visão completa. +- **Status "fechado" por carro**: cada chip mostra `classificadas/elegíveis` e fica **verde com ✓** + quando o carro está 100% classificado — atualiza em tempo real (sem recarregar) ao classificar. +- A **finalização continua por motorista** (quando todos os carros de todos os motoristas estiverem + prontos) — o gate não mudou. + +### ⚙️ Como funciona (técnico) +- `ConsolidacaoEntregasController#veiculos_status(motorista)` → `[{ vehicle, eligible, + classificadas, fechado }]` (2 queries: elegíveis com a coluna do veículo + classificadas + não-manuais). Reusa o scope `Entrega.da_veiculo`. Grupo "sem veículo" via sentinela `SEM_VEICULO`. +- `validar` aceita `params[:vehicle]`: recorta `@entregas` e, quando há carro selecionado, os totais + da barra (`@total_entregas`/`@classificadas`) vêm do `veiculos_status` daquele carro. +- `tracking_ids_elegiveis(motorista, vehicle = nil)` e `classificar_em_massa` passam o veículo — + é o que faz **"Marcar todos" fechar só o carro**. `resumo_json(motorista, vehicle:)` devolve + `classificadas_escopo` (barra por carro) + `veiculos_status` (refresh dos chips). +- `validacao_controller.js`: value `veiculo`, target `chipVeiculo`, `vehicle` nos POSTs, + `atualizarChips()` (verde/✓ + contadores). Sem migration — só leitura + UI. + +### 📂 Arquivos +``` +app/controllers/consolidacao_entregas_controller.rb # veiculos_status, validar, tracking_ids_elegiveis, classificar_em_massa, resumo_json +app/views/consolidacao_entregas/validar.html.erb # chips por veículo + barra com escopo +app/javascript/controllers/validacao_controller.js # value veiculo, chips, classificadas_escopo +``` + +
+ diff --git a/app/controllers/consolidacao_entregas_controller.rb b/app/controllers/consolidacao_entregas_controller.rb index 8b25153..9197556 100644 --- a/app/controllers/consolidacao_entregas_controller.rb +++ b/app/controllers/consolidacao_entregas_controller.rb @@ -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 diff --git a/app/javascript/controllers/validacao_controller.js b/app/javascript/controllers/validacao_controller.js index 1984ae6..5b74e2f 100644 --- a/app/javascript/controllers/validacao_controller.js +++ b/app/javascript/controllers/validacao_controller.js @@ -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) { diff --git a/app/views/consolidacao_entregas/validar.html.erb b/app/views/consolidacao_entregas/validar.html.erb index ddaa9a4..2cfe31a 100644 --- a/app/views/consolidacao_entregas/validar.html.erb +++ b/app/views/consolidacao_entregas/validar.html.erb @@ -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 @@ - <%# Barra de progresso %> + <%# Barra de progresso (reflete o veículo selecionado, se houver) %>
+ <% if @veiculo.present? %>🚚 <%= @veiculo %> — <% end %> <%= @classificadas %> de <%= @total_entregas %> entregas classificadas @@ -37,6 +39,39 @@
+ <%# 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 %> +
+
+ 🚗 Fechar por veículo: + clique para filtrar e classificar um carro de cada vez +
+
+ <%# 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 %> + <%= v[:vehicle] %> + + <%= v[:classificadas] %>/<%= v[:eligible] %> + <%= '✓' if v[:fechado] %> + + <% end %> + <% end %> +
+
+ <% end %> + <%# Ações em massa + filtro %>
Ações em massa: @@ -75,7 +110,7 @@
<%= 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' %>
@@ -173,6 +208,7 @@ <% tipos_ativos = (@classificacoes[e.tracking_id] || []).map(&:tipo) %>
<%# Checkbox seleção em massa %>