diff --git a/Imagens para correção /Imagem colada (3).png b/Imagens para correção /Imagem colada (3).png new file mode 100644 index 0000000..f965c88 Binary files /dev/null and b/Imagens para correção /Imagem colada (3).png differ diff --git a/Imagens para correção /Imagem colada (4).png b/Imagens para correção /Imagem colada (4).png new file mode 100644 index 0000000..7943b09 Binary files /dev/null and b/Imagens para correção /Imagem colada (4).png differ diff --git a/app/controllers/consolidacao_entregas_controller.rb b/app/controllers/consolidacao_entregas_controller.rb index fa19d7d..a0092d0 100644 --- a/app/controllers/consolidacao_entregas_controller.rb +++ b/app/controllers/consolidacao_entregas_controller.rb @@ -464,36 +464,9 @@ class ConsolidacaoEntregasController < ApplicationController # { 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). - # Status por veículo do motorista: { vehicle, eligible, classificadas, completo, - # fechado, arquivado }. `completo` = 100% classificado (verde, pode consolidar); - # `fechado` = veículo consolidado (cadeado, vem de consolidacao_veiculos); - # `arquivado` = fora do gate. Une os veículos com entregas elegíveis (rastreio) - # com os que só existem como linha de consolidacao_veiculos (arquivados/manuais). + # Status por veículo do motorista (fonte única no model: Consolidacao#veiculos_do_motorista). 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 - estados = @consolidacao.consolidacao_veiculos.where(motorista_nome: motorista).index_by(&:vehicle) - - veiculos = (por_veiculo.keys + estados.keys).uniq - veiculos.map do |veiculo| - ids = (por_veiculo[veiculo] || []).map(&:first) - feito = ids.count { |id| classificadas.include?(id) } - cv = estados[veiculo] - { vehicle: veiculo, eligible: ids.size, classificadas: feito, - completo: ids.size.positive? && feito >= ids.size, - fechado: cv&.fechado? || false, - arquivado: cv&.arquivado? || false } - end.sort_by { |v| v[:vehicle] } + @consolidacao.veiculos_do_motorista(motorista) end # Valida que a entrega pertence ao motorista informado e ao período/veículo da diff --git a/app/controllers/consolidacoes_controller.rb b/app/controllers/consolidacoes_controller.rb index 40e3899..b8fe1cd 100644 --- a/app/controllers/consolidacoes_controller.rb +++ b/app/controllers/consolidacoes_controller.rb @@ -3,7 +3,7 @@ class ConsolidacoesController < ApplicationController include Auditavel before_action :set_consolidacao, - only: %i[show edit update destroy finalizar arquivar reativar wizard + only: %i[show edit update destroy finalizar finalizar_parcial arquivar reativar wizard registrar_pagamento cancelar_pagamento arquivar_motorista desarquivar_motorista excluir_motorista adicionar_motorista @@ -181,20 +181,20 @@ class ConsolidacoesController < ApplicationController .apontamentos .where(motorista_nome: cm.motorista_nome) .distinct.count(:tracking_id) - # Status por veículo (para o breakdown do card) e estado de fechamento. - veiculos = @consolidacao.consolidacao_veiculos.ativos - .where(motorista_nome: cm.motorista_nome) - .order(:vehicle) + # Status por veículo (breakdown do card): total usados, consolidados (🔒), + # pendentes (não dá p/ consolidar ainda) e prontos (100%, falta consolidar). + vinfo = @consolidacao.veiculos_do_motorista(cm.motorista_nome).reject { |v| v[:arquivado] } { - registro: cm, - total: total, - classificadas: classificadas, - apontamentos: apontamentos, - veiculos: veiculos, - veiculos_fechados: veiculos.count(&:fechado?), - # Espelha o gate do servidor (todas_entregas_classificadas?): exige tudo - # classificado E todos os veículos ativos fechados (consolidados). - completo: @consolidacao.motorista_completo?(cm.motorista_nome) + registro: cm, + total: total, + classificadas: classificadas, + apontamentos: apontamentos, + veiculos_total: vinfo.size, + veiculos_fechados: vinfo.count { |v| v[:fechado] }, + veiculos_prontos: vinfo.count { |v| !v[:fechado] && v[:completo] }, + veiculos_pendentes: vinfo.select { |v| !v[:fechado] && !v[:completo] && v[:eligible].positive? } + .map { |v| v[:vehicle] }, + completo: @consolidacao.motorista_completo?(cm.motorista_nome) } end end @@ -213,6 +213,25 @@ class ConsolidacoesController < ApplicationController end end + # POST /consolidacoes/:id/finalizar_parcial + # Atalho "finalizar só os veículos consolidados": arquiva os veículos que ainda + # não dá para consolidar (não 100% classificados) e finaliza a consolidação. Os + # veículos consolidados/prontos seguem no fechamento; os pendentes ficam de fora. + def finalizar_parcial + authorize @consolidacao, :update? + + arquivados = @consolidacao.arquivar_veiculos_pendentes!(current_user) + if @consolidacao.finalizar!(current_user) + auditar!(:finalizar, @consolidacao, dados_novos: { finalizar_parcial: true, veiculos_arquivados: arquivados }) + NotificacaoService.notificar_finalizacao(@consolidacao) + redirect_to @consolidacao, + notice: "Consolidação finalizada. #{arquivados.size} veículo(s) pendente(s) ficaram de fora. Motoristas notificados." + else + redirect_to wizard_consolidacao_path(@consolidacao), + alert: 'Não foi possível finalizar: ainda há entregas pendentes fora dos veículos arquivados.' + end + end + # POST /consolidacoes/:id/arquivar def arquivar authorize @consolidacao, :destroy? diff --git a/app/models/consolidacao.rb b/app/models/consolidacao.rb index 3c9a9a3..8bb25d9 100644 --- a/app/models/consolidacao.rb +++ b/app/models/consolidacao.rb @@ -208,6 +208,54 @@ class Consolidacao < ApplicationRecord .distinct.count(:tracking_id) end + # Status por veículo do motorista: array de { vehicle, eligible, classificadas, + # completo, fechado, arquivado }. `completo` = 100% classificado (pode consolidar); + # `fechado` = veículo consolidado (🔒); `arquivado` = fora do gate. Une os veículos + # com entregas elegíveis (rastreio) com os que só têm linha em consolidacao_veiculos + # (arquivados / buckets manuais). Fonte única do filtro de carros e do wizard. + def veiculos_do_motorista(motorista) + return [] unless motorista.present? + + rel = Entrega.atendidas.da_conta_gade + .no_periodo_checkout(data_inicio, data_fim) + .do_motorista(motorista) + rel = rel.da_veiculo(vehicle_ids) if vehicle_ids.present? + rel = rel.da_operacoes(operacoes) if operacoes.present? + + por_veiculo = rel.pluck(:tracking_id, :vehicle).group_by { |(_id, v)| v.presence || SEM_VEICULO } + classificadas = consolidacao_entregas.where(motorista_nome: motorista, manual: false) + .distinct.pluck(:tracking_id).to_set + estados = consolidacao_veiculos.where(motorista_nome: motorista).index_by(&:vehicle) + + (por_veiculo.keys + estados.keys).uniq.map do |veiculo| + ids = (por_veiculo[veiculo] || []).map(&:first) + feito = ids.count { |id| classificadas.include?(id) } + cv = estados[veiculo] + { vehicle: veiculo, eligible: ids.size, classificadas: feito, + completo: ids.size.positive? && feito >= ids.size, + fechado: cv&.fechado? || false, + arquivado: cv&.arquivado? || false } + end.sort_by { |v| v[:vehicle] } + end + + # Arquiva os veículos PENDENTES (com entregas elegíveis mas não 100% classificadas) + # de todos os motoristas ativos, deixando-os de fora da finalização. Usado pelo + # atalho "arquivar pendentes e finalizar" — os prontos/consolidados seguem no todo. + # Retorna a lista "Motorista · Veículo" arquivada. + def arquivar_veiculos_pendentes!(user) + arquivados = [] + consolidacao_motoristas.ativos.each do |cm| + veiculos_do_motorista(cm.motorista_nome).each do |v| + next if v[:fechado] || v[:arquivado] || v[:completo] || v[:eligible].zero? + + cv = consolidacao_veiculos.find_or_create_by!(motorista_nome: cm.motorista_nome, vehicle: v[:vehicle]) + cv.arquivar!(user) + arquivados << "#{cm.motorista_nome} · #{v[:vehicle]}" + end + end + arquivados + end + # ── Apontamento manual ────────────────────────────────────── # Adiciona uma entrega "por fora" (NF que chegou depois) com um ou mais pilares # (tipos). O motorista vem da própria entrega e é incluído na consolidação se diff --git a/app/views/consolidacoes/wizard.html.erb b/app/views/consolidacoes/wizard.html.erb index d6d5913..0fba29b 100644 --- a/app/views/consolidacoes/wizard.html.erb +++ b/app/views/consolidacoes/wizard.html.erb @@ -187,11 +187,14 @@ · ➕ <%= m[:apontamentos] %> apontamento<%= 's' if m[:apontamentos] > 1 %> <% end %>

- <% if m[:veiculos].any? %> + <% if m[:veiculos_total] > 0 %>

- - 🚗 <%= m[:veiculos_fechados] %>/<%= m[:veiculos].size %> veículo<%= 's' if m[:veiculos].size > 1 %> consolidado<%= 's' if m[:veiculos_fechados] != 1 %> + + 🚗 <%= m[:veiculos_fechados] %>/<%= m[:veiculos_total] %> veículo<%= 's' if m[:veiculos_total] > 1 %> consolidado<%= 's' if m[:veiculos_fechados] != 1 %> + <% if m[:veiculos_pendentes].any? %> + · pendente<%= 's' if m[:veiculos_pendentes].size > 1 %>: <%= m[:veiculos_pendentes].join(', ') %> + <% end %>

<% end %> @@ -255,23 +258,39 @@ <% end %> - <%# Finalizar — só habilita se tudo classificado %> - <% tudo_completo = @motoristas.all? { |m| m[:completo] } && @motoristas.any? %> -
-
-

Total da consolidação

-

<%= moeda(@consolidacao.valor_total) %>

+ <%# Finalizar — habilita quando tudo classificado. Se sobram veículos pendentes + mas já há veículos consolidados, oferece "arquivar pendentes e finalizar". %> + <% tudo_completo = @motoristas.all? { |m| m[:completo] } && @motoristas.any? %> + <% tem_consolidados = @motoristas.any? { |m| m[:veiculos_fechados] > 0 || m[:veiculos_prontos] > 0 } %> + <% tem_pendentes = @motoristas.any? { |m| m[:veiculos_pendentes].any? } %> +
+
+
+

Total da consolidação

+

<%= moeda(@consolidacao.valor_total) %>

+
+ <% if @consolidacao.rascunho? %> +
+ <% if !tudo_completo && tem_consolidados && tem_pendentes %> + <%= button_to '🔒 Arquivar pendentes e finalizar', + finalizar_parcial_consolidacao_path(@consolidacao), + method: :post, + data: { turbo_confirm: 'Arquivar TODOS os veículos ainda não classificados 100% e finalizar? Eles ficam de fora desta consolidação (você resolve depois). Os veículos consolidados são pagos normalmente.' }, + class: 'bg-orange-500 hover:bg-orange-600 text-black font-bold px-5 py-3.5 rounded-lg min-h-[48px] flex items-center justify-center text-center' %> + <% end %> + <%= button_to 'Finalizar consolidação ✓', finalizar_consolidacao_path(@consolidacao), + method: :post, + disabled: !tudo_completo, + data: { turbo_confirm: 'Finalizar? Após finalizada, a consolidação não poderá ser editada por operadores.' }, + class: "font-bold px-6 py-3.5 rounded-lg min-h-[48px] transition-colors + #{tudo_completo ? 'bg-green-600 hover:bg-green-500 text-white cursor-pointer' : 'bg-[#2a2a2a] text-gray-600 cursor-not-allowed'}" %> +
+ <% end %>
- <% if @consolidacao.rascunho? %> - <%= button_to 'Finalizar consolidação ✓', finalizar_consolidacao_path(@consolidacao), - method: :post, - disabled: !tudo_completo, - data: { turbo_confirm: 'Finalizar? Após finalizada, a consolidação não poderá ser editada por operadores.' }, - class: "font-bold px-6 py-3.5 rounded-lg min-h-[48px] transition-colors - #{tudo_completo ? 'bg-green-600 hover:bg-green-500 text-white cursor-pointer' : 'bg-[#2a2a2a] text-gray-600 cursor-not-allowed'}" %> - <% end %>
<% unless tudo_completo %> -

⚠️ Classifique todas as entregas de todos os motoristas para poder finalizar.

+

+ ⚠️ Classifique todas as entregas de todos os motoristas para finalizar tudo<% if tem_consolidados && tem_pendentes %> — ou use “Arquivar pendentes e finalizar” para fechar só os veículos já consolidados.<% end %> +

<% end %>
diff --git a/config/routes.rb b/config/routes.rb index bc7d017..299ba8a 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -41,6 +41,7 @@ Rails.application.routes.draw do member do get :wizard # Passo 1 — selecionar motorista post :finalizar + post :finalizar_parcial # arquiva veículos pendentes e finaliza (só os consolidados) post :arquivar post :reativar # Restaura uma consolidação arquivada post :registrar_pagamento # Marca 1 motorista (ou todos) como pago diff --git a/spec/models/consolidacao_spec.rb b/spec/models/consolidacao_spec.rb index 7f90f14..cb9a616 100644 --- a/spec/models/consolidacao_spec.rb +++ b/spec/models/consolidacao_spec.rb @@ -293,6 +293,29 @@ RSpec.describe Consolidacao, type: :model do end end + describe '#arquivar_veiculos_pendentes!' do + let(:consolidacao) { create(:consolidacao) } + let(:gerente) { create(:gerente) } + + before { create(:consolidacao_motorista, consolidacao: consolidacao, motorista_nome: 'A') } + + it 'arquiva só os veículos pendentes (com elegíveis e < 100%)' do + allow(consolidacao).to receive(:veiculos_do_motorista).with('A').and_return([ + { vehicle: 'GADE_1', eligible: 5, classificadas: 5, completo: true, fechado: true, arquivado: false }, + { vehicle: 'GADE_2', eligible: 3, classificadas: 1, completo: false, fechado: false, arquivado: false }, + { vehicle: '(sem veículo)', eligible: 0, classificadas: 0, completo: false, fechado: false, arquivado: false } + ]) + + arquivados = consolidacao.arquivar_veiculos_pendentes!(gerente) + + expect(arquivados).to eq(['A · GADE_2']) + expect(consolidacao.consolidacao_veiculos.find_by(motorista_nome: 'A', vehicle: 'GADE_2')).to be_arquivado + # não mexe no consolidado (GADE_1) nem no bucket sem elegíveis + expect(consolidacao.consolidacao_veiculos.where(vehicle: 'GADE_1')).not_to exist + expect(consolidacao.consolidacao_veiculos.where(vehicle: '(sem veículo)')).not_to exist + end + end + describe '#finalizar! fecha os veículos ainda abertos' do let(:consolidacao) { create(:consolidacao) } let(:gerente) { create(:gerente) }