Melhorias gerais descritas no READ.me

This commit is contained in:
2026-06-26 11:30:35 -03:00
parent 1ca5fc67d7
commit 1ad1d4c143
8 changed files with 198 additions and 23 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 280 KiB

View File

@@ -48,6 +48,12 @@ class ConsolidacaoEntregasController < ApplicationController
# Nomes dos motoristas ATIVOS — alimenta o seletor do apontamento 100% manual.
@motoristas_consolidacao = @consolidacao.consolidacao_motoristas.ativos.pluck(:motorista_nome)
# Lançamentos manuais do motorista (apontamentos + termo) — feedback no resumo
# e na barra de progresso. Ficam fora do conjunto elegível do período.
@manuais = @consolidacao.consolidacao_entregas
.where(motorista_nome: @motorista, manual: true)
.distinct.count(:tracking_id)
# Filtro "apenas não classificadas"
if params[:apenas_pendentes] == '1'
@entregas = @entregas.reject { |e| @classificacoes.key?(e.tracking_id) }
@@ -58,7 +64,8 @@ class ConsolidacaoEntregasController < ApplicationController
retirada: Configuracao.preco_retirada,
bonus: Configuracao.preco_bonus,
desconto: Configuracao.preco_desconto,
extraordinaria: Configuracao.preco_extraordinaria
extraordinaria: Configuracao.preco_extraordinaria,
termo: Configuracao.preco_termo
}
end
@@ -450,6 +457,7 @@ class ConsolidacaoEntregasController < ApplicationController
ok: true,
classificadas: classificadas,
classificadas_escopo: escopo,
manuais: classificacoes.where(manual: true).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,
veiculos_status: status

View File

@@ -165,6 +165,8 @@ class ConsolidacoesController < ApplicationController
authorize @consolidacao, :update?
@arquivados = @consolidacao.consolidacao_motoristas.arquivados.order(:motorista_nome)
# Alimenta o datalist do "Adicionar motorista" (mesma fonte da criação).
@motoristas_disponiveis = Entrega.motoristas_ativos
@motoristas = @consolidacao.consolidacao_motoristas.ativos.order(:motorista_nome).map do |cm|
total = @consolidacao.entregas_elegiveis_count(cm.motorista_nome)
classificadas = @consolidacao.classificadas_count(cm.motorista_nome)
@@ -299,6 +301,59 @@ class ConsolidacoesController < ApplicationController
notice: "#{cm.motorista_nome} restaurado para a consolidação."
end
# DELETE /consolidacoes/:id/excluir_motorista — apaga de vez 1 motorista desta
# consolidação (só admin/gerente). Caminho destrutivo (o reversível é arquivar):
# como consolidacao_entregas se liga ao motorista por STRING (motorista_nome,
# sem FK/cascade), apagamos os lançamentos dele à mão antes de remover o registro.
def excluir_motorista
authorize @consolidacao, :destroy?
cm = @consolidacao.consolidacao_motoristas.find(params[:consolidacao_motorista_id])
if cm.pago?
return redirect_to wizard_consolidacao_path(@consolidacao),
alert: "#{cm.motorista_nome} já está pago — estorne o pagamento antes de excluir."
end
nome = cm.motorista_nome
ActiveRecord::Base.transaction do
@consolidacao.consolidacao_entregas.where(motorista_nome: nome).delete_all
cm.destroy!
end
# cm já não existe — atualiza o total da consolidação direto (soma dos ativos).
@consolidacao.update_column(:valor_total, @consolidacao.consolidacao_motoristas.ativos.sum(:valor_total))
auditar!(:editar, @consolidacao, dados_novos: { excluir_motorista: nome })
redirect_to wizard_consolidacao_path(@consolidacao),
notice: "#{nome} excluído desta consolidação."
end
# POST /consolidacoes/:id/adicionar_motorista — inclui um motorista nesta
# consolidação (ou restaura, se estava arquivado). Útil para refazer rápido
# depois de excluir/arquivar por erro de precificação.
def adicionar_motorista
authorize @consolidacao, :update?
nome = params[:motorista_nome].to_s.strip
if nome.blank?
return redirect_to wizard_consolidacao_path(@consolidacao), alert: 'Informe o nome do motorista.'
end
cm = @consolidacao.consolidacao_motoristas.find_or_initialize_by(motorista_nome: nome)
aviso =
if cm.persisted? && cm.arquivado?
cm.desarquivar!(current_user)
"#{nome} restaurado para a consolidação."
elsif cm.persisted?
"#{nome} já está nesta consolidação."
else
cm.save!
"#{nome} adicionado à consolidação."
end
@consolidacao.recalcular_motorista!(nome)
auditar!(:editar, @consolidacao, dados_novos: { adicionar_motorista: nome })
redirect_to wizard_consolidacao_path(@consolidacao), notice: aviso
end
# ── FASE 7 — PDFs ────────────────────────────────────────────
# GET /consolidacoes/:id/gerar_pdf_relatorio?motorista=X

View File

@@ -0,0 +1,23 @@
// app/javascript/controllers/adicionar_motorista_controller.js
// Stimulus — modal "Adicionar motorista" no Passo 1. Só abre/fecha o modal; o
// formulário em si submete normalmente (Turbo), redirecionando de volta ao wizard.
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["modal", "input"]
abrir() {
this.modalTarget.classList.remove("hidden")
if (this.hasInputTarget) this.inputTarget.focus()
}
fechar() {
this.modalTarget.classList.add("hidden")
}
// Fecha ao clicar no fundo (fora do card)
fundo(event) {
if (event.target === this.modalTarget) this.fechar()
}
}

View File

@@ -7,9 +7,10 @@ import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = [
"linha", "checkbox", "selecionarTodos", "modoRemover", "rotuloAcao",
"toggles", "barra", "percentual", "chipVeiculo",
"contadorClassificadas", "contadorTotal",
"qtdNormal", "qtdRetirada", "qtdBonus", "qtdDesconto", "qtdExtraordinaria", "valorTotal"
"toggles", "barra", "barraManuais", "percentual", "chipVeiculo",
"contadorClassificadas", "contadorTotal", "contadorManuais", "manuaisInfo",
"qtdNormal", "qtdRetirada", "qtdBonus", "qtdDesconto", "qtdExtraordinaria",
"qtdTermo", "qtdManuais", "valorTotal"
]
static values = {
@@ -181,20 +182,36 @@ export default class extends Controller {
this.qtdDescontoTarget.textContent = porTipo["desconto"] || 0
if (this.hasQtdExtraordinariaTarget)
this.qtdExtraordinariaTarget.textContent = porTipo["extraordinaria"] || 0
if (this.hasQtdTermoTarget)
this.qtdTermoTarget.textContent = porTipo["termo"] || 0
const valor = dados.valor_total || 0
this.valorTotalTarget.textContent =
"R$ " + valor.toLocaleString("pt-BR", { minimumFractionDigits: 2, maximumFractionDigits: 2 })
// Lançamentos manuais do motorista (apontamentos + termo): feedback visual.
// Ficam fora do denominador de elegíveis (o gate de finalização não muda).
const manuais = dados.manuais || 0
if (this.hasQtdManuaisTarget) this.qtdManuaisTarget.textContent = manuais
if (this.hasContadorManuaisTarget) this.contadorManuaisTarget.textContent = manuais
if (this.hasManuaisInfoTarget) this.manuaisInfoTarget.classList.toggle("hidden", manuais <= 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)
const pct = total > 0 ? Math.round((classificadas / total) * 100) : 0
this.barraTarget.style.width = `${Math.min(pct, 100)}%`
this.percentualTarget.textContent = `${Math.min(pct, 100)}%`
// Barra com 2 segmentos, proporcionais ao total geral (elegíveis + manuais):
// laranja/verde = elegíveis classificadas · azul = lançamentos manuais.
const denom = total + manuais
this.barraTarget.style.width = denom > 0 ? `${(classificadas / denom) * 100}%` : "0%"
if (this.hasBarraManuaisTarget)
this.barraManuaisTarget.style.width = denom > 0 ? `${(manuais / denom) * 100}%` : "0%"
// Cor do segmento elegível: verde quando todas as elegíveis estão classificadas.
this.barraTarget.classList.toggle("bg-green-500", pct >= 100)
this.barraTarget.classList.toggle("bg-orange-500", pct < 100)

View File

@@ -31,11 +31,15 @@
<% if @veiculo.present? %>🚚 <%= @veiculo %> — <% end %>
<span data-validacao-target="contadorClassificadas"><%= @classificadas %></span>
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="contadorManuais"><%= @manuais %></span> manuais</span>
</span>
<span class="text-orange-500 font-bold" data-validacao-target="percentual"></span>
</div>
<div class="w-full bg-[#0a0a0a] rounded-full h-3">
<div data-validacao-target="barra" class="h-3 rounded-full bg-orange-500 transition-all duration-300" style="width: 0%"></div>
<%# Barra com 2 segmentos: laranja/verde = elegíveis classificadas · azul = lançamentos manuais %>
<div class="w-full bg-[#0a0a0a] rounded-full h-3 flex overflow-hidden">
<div data-validacao-target="barra" class="h-3 bg-orange-500 transition-all duration-300" style="width: 0%"></div>
<div data-validacao-target="barraManuais" class="h-3 bg-blue-500 transition-all duration-300" style="width: 0%"></div>
</div>
</div>
@@ -347,6 +351,15 @@
<span class="flex items-center gap-2"><span class="w-3 h-3 bg-purple-600 rounded-full"></span> Extraordinária</span>
<span class="text-white font-bold" data-validacao-target="qtdExtraordinaria">0</span>
</div>
<div class="flex justify-between items-center">
<span class="flex items-center gap-2"><span class="w-3 h-3 bg-blue-600 rounded-full"></span> Termo</span>
<span class="text-white font-bold" data-validacao-target="qtdTermo">0</span>
</div>
</div>
<div class="border-t border-[#2a2a2a] pt-4 flex justify-between items-center text-sm">
<span class="flex items-center gap-2 text-blue-400"><span class="w-3 h-3 bg-blue-500 rounded-full"></span> Apontamentos manuais</span>
<span class="text-blue-400 font-bold" data-validacao-target="qtdManuais"><%= @manuais %></span>
</div>
<div class="border-t border-[#2a2a2a] pt-4">
@@ -360,6 +373,7 @@
<p>⭐ Bônus: <%= moeda(@precos[:bonus]) %></p>
<p>⚠️ Desconto: -<%= moeda(@precos[:desconto]) %></p>
<p>✨ Extraordinária: <%= moeda(@precos[:extraordinaria]) %> (valor por entrega)</p>
<p>📄 Termo: <%= moeda(@precos[:termo]) %></p>
</div>
</div>
</div>
@@ -371,6 +385,7 @@
<% _todas = @classificacoes.values.flatten %>
<%= raw({
classificadas: @classificadas,
manuais: @manuais,
por_tipo: _todas.group_by(&:tipo).transform_values(&:size),
valor_total: _todas.sum { |c| c.tipo == 'desconto' ? -c.valor_aplicado : c.valor_aplicado }.to_f
}.to_json) %>

View File

@@ -5,7 +5,7 @@
<%# Header do wizard %>
<div class="mb-6">
<%= link_to '← Voltar para consolidações', consolidacoes_path, class: 'text-gray-500 hover:text-orange-500 text-sm' %>
<%= link_to '← Voltar para o resumo', consolidacao_path(@consolidacao), class: 'text-gray-500 hover:text-orange-500 text-sm' %>
<h1 class="text-2xl font-bold text-white mt-2"><%= @consolidacao.nome %></h1>
<p class="text-gray-400 text-sm">
📅 <%= l @consolidacao.data_inicio, format: :short %> → <%= l @consolidacao.data_fim, format: :short %>
@@ -31,19 +31,36 @@
</div>
</div>
<%# Apontamento manual — adiciona entrega por fora (busca pela NF na rastreio) %>
<%# Caixa de ferramentas do Passo 1 — concentra "Apontamento manual" (busca pela
NF) e "Adicionar motorista" num único botão com menu suspenso. %>
<% unless @consolidacao.finalizada? && current_user.operador? %>
<div data-controller="apontamento"
<div class="relative mb-4"
data-controller="ferramentas apontamento adicionar-motorista"
data-apontamento-buscar-url-value="<%= buscar_nf_consolidacao_consolidacao_entregas_path(@consolidacao) %>"
data-apontamento-apontar-url-value="<%= apontar_consolidacao_consolidacao_entregas_path(@consolidacao) %>"
class="mb-4">
<button type="button" data-action="apontamento#abrir"
class="w-full md:w-auto inline-flex items-center justify-center gap-2 bg-[#1a1a1a] hover:bg-[#2a2a2a] text-white border border-dashed border-orange-500/60 hover:border-orange-500 font-semibold px-4 py-3 rounded-lg min-h-[48px]">
Apontamento manual
<span class="text-gray-500 text-xs font-normal hidden md:inline">(nota que chegou por fora)</span>
data-apontamento-apontar-url-value="<%= apontar_consolidacao_consolidacao_entregas_path(@consolidacao) %>">
<%# Botão único + menu suspenso %>
<button type="button" data-action="ferramentas#toggle"
class="w-full md:w-auto inline-flex items-center justify-center gap-2 bg-[#1a1a1a] hover:bg-[#2a2a2a] text-white border border-dashed border-[#3a3a3a] hover:border-orange-500 font-semibold px-4 py-3 rounded-lg min-h-[48px]">
Adicionar / Ferramentas
<span class="text-gray-500">▾</span>
</button>
<%# Modal %>
<div data-ferramentas-target="menu"
class="hidden absolute z-40 mt-2 w-72 bg-[#1a1a1a] border border-[#2a2a2a] rounded-xl shadow-xl overflow-hidden">
<button type="button" data-action="apontamento#abrir ferramentas#fechar"
class="w-full text-left px-4 py-3 hover:bg-[#2a2a2a] border-l-2 border-transparent hover:border-orange-500">
<span class="block text-white font-semibold">📝 Apontamento manual</span>
<span class="block text-gray-500 text-xs">nota que chegou por fora (busca pela NF)</span>
</button>
<button type="button" data-action="adicionar-motorista#abrir ferramentas#fechar"
class="w-full text-left px-4 py-3 hover:bg-[#2a2a2a] border-t border-[#2a2a2a] border-l-2 border-l-transparent hover:border-l-green-500">
<span class="block text-white font-semibold">🧑‍✈️ Adicionar motorista</span>
<span class="block text-gray-500 text-xs">incluir/restaurar um motorista nesta consolidação</span>
</button>
</div>
<%# Modal — apontamento manual (busca NF) %>
<div data-apontamento-target="modal" data-action="click->apontamento#fundo"
class="hidden fixed inset-0 z-50 bg-black/70 flex items-center justify-center p-4">
<div class="bg-[#1a1a1a] border border-[#2a2a2a] rounded-2xl w-full max-w-lg p-6 max-h-[90vh] overflow-y-auto">
@@ -92,6 +109,35 @@
</div>
</div>
</div>
<%# Modal — adicionar motorista %>
<div data-adicionar-motorista-target="modal" data-action="click->adicionar-motorista#fundo"
class="hidden fixed inset-0 z-50 bg-black/70 flex items-center justify-center p-4">
<div class="bg-[#1a1a1a] border border-[#2a2a2a] rounded-2xl w-full max-w-md p-6">
<div class="flex items-center justify-between mb-1">
<h2 class="text-xl font-bold text-white">🧑‍✈️ Adicionar motorista</h2>
<button type="button" data-action="adicionar-motorista#fechar"
class="text-gray-500 hover:text-white text-2xl leading-none">✕</button>
</div>
<p class="text-gray-400 text-sm mb-4">Inclua um motorista nesta consolidação. Se ele estava arquivado, será restaurado.</p>
<%= form_with url: adicionar_motorista_consolidacao_path(@consolidacao), method: :post do |f| %>
<label class="block text-gray-400 text-sm mb-1">Motorista</label>
<input data-adicionar-motorista-target="input" type="text" name="motorista_nome"
list="motoristas-disponiveis" placeholder="Nome do motorista" autocomplete="off"
class="w-full bg-[#0a0a0a] border border-[#2a2a2a] text-white rounded-lg px-3 py-3 text-base focus:border-orange-500 focus:outline-none mb-4">
<datalist id="motoristas-disponiveis">
<% Array(@motoristas_disponiveis).each do |nome| %>
<option value="<%= nome %>"></option>
<% end %>
</datalist>
<button type="submit"
class="w-full bg-green-600 hover:bg-green-500 text-white font-bold py-3.5 rounded-lg min-h-[48px]">
Adicionar ✓
</button>
<% end %>
</div>
</div>
</div>
<% end %>
@@ -162,12 +208,21 @@
<p class="text-gray-300 font-semibold"><%= cm.motorista_nome %></p>
<p class="text-gray-600 text-xs">Arquivado · não conta no total</p>
</div>
<% if pode_arquivar_motorista %>
<%= button_to '♻️ Restaurar',
desarquivar_motorista_consolidacao_path(@consolidacao, consolidacao_motorista_id: cm.id),
method: :post,
class: 'text-green-400 hover:text-green-300 border border-[#2a2a2a] hover:border-green-500/50 px-3 py-2 rounded-lg text-xs min-h-[40px]' %>
<% end %>
<div class="flex items-center gap-2">
<% if pode_arquivar_motorista %>
<%= button_to '♻️ Restaurar',
desarquivar_motorista_consolidacao_path(@consolidacao, consolidacao_motorista_id: cm.id),
method: :post,
class: 'text-green-400 hover:text-green-300 border border-[#2a2a2a] hover:border-green-500/50 px-3 py-2 rounded-lg text-xs min-h-[40px]' %>
<% end %>
<% if policy(@consolidacao).destroy? %>
<%= button_to '🗑️ Excluir',
excluir_motorista_consolidacao_path(@consolidacao, consolidacao_motorista_id: cm.id),
method: :delete,
form: { data: { turbo_confirm: "Excluir DEFINITIVAMENTE #{cm.motorista_nome} desta consolidação? Os lançamentos dele serão apagados e isso não pode ser desfeito." } },
class: 'text-red-400 hover:text-red-300 border border-[#2a2a2a] hover:border-red-500/50 px-3 py-2 rounded-lg text-xs min-h-[40px]' %>
<% end %>
</div>
</div>
<% end %>
</div>

View File

@@ -44,6 +44,8 @@ Rails.application.routes.draw do
post :cancelar_pagamento # Estorna o pagamento de 1 motorista (ou todos)
post :arquivar_motorista # Arquiva 1 motorista da consolidação (reversível)
post :desarquivar_motorista # Restaura um motorista arquivado
delete :excluir_motorista # Exclui de vez 1 motorista (admin/gerente)
post :adicionar_motorista # Adiciona/restaura 1 motorista nesta consolidação
get :preview_extrato
get :gerar_pdf_relatorio
get :gerar_pdf_extrato