Melhorias gerais descritas no READ.me
This commit is contained in:
BIN
Imagens para implantação /Imagem colada (22).png
Normal file
BIN
Imagens para implantação /Imagem colada (22).png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 280 KiB |
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
23
app/javascript/controllers/adicionar_motorista_controller.js
Normal file
23
app/javascript/controllers/adicionar_motorista_controller.js
Normal 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()
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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) %>
|
||||
|
||||
@@ -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>
|
||||
<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>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user