From 5af51534beda77a8f26b2d5c50a45ef53ebe8dfbae0e25a34a1141aa7c814a16 Mon Sep 17 00:00:00 2001 From: victor Date: Wed, 15 Jul 2026 16:27:20 -0300 Subject: [PATCH] =?UTF-8?q?Adi=C3=A7=C3=A3o=20de=20mais=20um=20pre=C3=A7o?= =?UTF-8?q?=20'Termo=20especial'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../consolidacao_entregas_controller.rb | 41 +++++++++----- .../apontamento_termo_controller.js | 45 ++++++++++----- .../controllers/validacao_controller.js | 4 +- app/models/configuracao.rb | 11 +++- app/models/consolidacao.rb | 11 +++- app/models/consolidacao_entrega.rb | 7 ++- app/services/pdf/relatorio_motorista_pdf.rb | 2 +- .../consolidacao_entregas/revisar.html.erb | 7 ++- .../consolidacao_entregas/validar.html.erb | 56 ++++++++++++------- app/views/consolidacoes/show.html.erb | 12 +++- ...1_add_preco_termo_especial_configuracao.rb | 18 ++++++ db/seeds.rb | 1 + spec/models/configuracao_spec.rb | 5 ++ 13 files changed, 158 insertions(+), 62 deletions(-) create mode 100644 db/migrate/20260715000001_add_preco_termo_especial_configuracao.rb diff --git a/app/controllers/consolidacao_entregas_controller.rb b/app/controllers/consolidacao_entregas_controller.rb index a0092d0..df04cc8 100644 --- a/app/controllers/consolidacao_entregas_controller.rb +++ b/app/controllers/consolidacao_entregas_controller.rb @@ -64,7 +64,8 @@ class ConsolidacaoEntregasController < ApplicationController bonus: Configuracao.preco_bonus, desconto: Configuracao.preco_desconto, extraordinaria: Configuracao.preco_extraordinaria, - termo: Configuracao.preco_termo + termo: Configuracao.preco_termo, + termo_especial: Configuracao.preco_termo_especial } end @@ -267,25 +268,38 @@ class ConsolidacaoEntregasController < ApplicationController end # POST /consolidacoes/:consolidacao_id/consolidacao_entregas/apontar_termo - # ENTREGA DE TERMO — lança um lote de N termos para o motorista. Não há NF nem - # código de rastreio: só a quantidade, a um preço fixo configurável. + # ENTREGA DE TERMO — lança lotes de termos para o motorista. Não há NF nem + # código de rastreio: só as quantidades (normal e especial), cada tipo a um + # preço fixo configurável. def apontar_termo authorize @consolidacao, :update? - motorista = @consolidacao.adicionar_termos!( - motorista: params[:motorista].to_s.strip, - quantidade: params[:quantidade], - vehicle: params[:vehicle], # veículo escolhido no lançamento (Q3) - user: current_user - ) + qtd_normal = params[:quantidade].to_i + qtd_especial = params[:quantidade_especial].to_i + raise ArgumentError, 'Informe ao menos um termo (normal ou especial).' if qtd_normal < 1 && qtd_especial < 1 + + motorista = nil + ActiveRecord::Base.transaction do + { 'termo' => qtd_normal, 'termo_especial' => qtd_especial }.each do |tipo, qtd| + next if qtd < 1 + + motorista = @consolidacao.adicionar_termos!( + motorista: params[:motorista].to_s.strip, + quantidade: qtd, + tipo: tipo, + vehicle: params[:vehicle], # veículo escolhido no lançamento (Q3) + user: current_user + ) + end + end auditar!(:editar, @consolidacao, - dados_novos: { entrega_termo: params[:quantidade].to_i, motorista: motorista }) + dados_novos: { entrega_termo: qtd_normal, entrega_termo_especial: qtd_especial, motorista: motorista }) respond_to do |format| format.json { render json: resumo_json(motorista).merge(motorista: motorista) } format.html do redirect_to validar_consolidacao_consolidacao_entregas_path(@consolidacao, motorista: motorista), - notice: "#{params[:quantidade].to_i} entrega(s) de termo adicionada(s) para #{motorista}." + notice: "#{qtd_normal + qtd_especial} entrega(s) de termo adicionada(s) para #{motorista}." end end rescue ArgumentError => e @@ -534,8 +548,9 @@ class ConsolidacaoEntregasController < ApplicationController # Conta lançamentos manuais como ENTREGAS: cada termo vale `quantidade` (lote # numa só linha); os demais apontamentos manuais valem 1 por nota (tracking). def contar_manuais(rel_manual) - rel_manual.termo.sum(:quantidade) + - rel_manual.where.not(tipo: ConsolidacaoEntrega.tipos[:termo]).distinct.count(:tracking_id) + tipos_termo = ConsolidacaoEntrega.tipos.values_at(:termo, :termo_especial) + rel_manual.where(tipo: tipos_termo).sum(:quantidade) + + rel_manual.where.not(tipo: tipos_termo).distinct.count(:tracking_id) end # `vehicles` (opcional, string ou array): quando presente, TODO o resumo diff --git a/app/javascript/controllers/apontamento_termo_controller.js b/app/javascript/controllers/apontamento_termo_controller.js index 260bd55..5068d0e 100644 --- a/app/javascript/controllers/apontamento_termo_controller.js +++ b/app/javascript/controllers/apontamento_termo_controller.js @@ -1,17 +1,18 @@ // app/javascript/controllers/apontamento_termo_controller.js -// Stimulus — Entrega de termo. Lança um lote de N termos para o motorista da tela. -// Entrega sem NF nem código de rastreio: só a quantidade (stepper − N +), a um -// preço fixo configurável. +// Stimulus — Entrega de termo. Lança lotes de termos (normal e/ou especial) para +// o motorista da tela. Entrega sem NF nem código de rastreio: só as quantidades +// (um stepper − N + por tipo), cada tipo a um preço fixo configurável. import { Controller } from "@hotwired/stimulus" export default class extends Controller { - static targets = ["modal", "quantidade", "erro", "veiculo"] + static targets = ["modal", "quantidade", "quantidadeEspecial", "erro", "veiculo"] static values = { apontarTermoUrl: String, motorista: String } abrir() { this.erroTarget.classList.add("hidden") this.quantidadeTarget.value = "1" + this.quantidadeEspecialTarget.value = "0" this.modalTarget.classList.remove("hidden") } @@ -24,26 +25,39 @@ export default class extends Controller { if (event.target === this.modalTarget) this.fechar() } - // Stepper + // Steppers (mínimo 0 em ambos; o envio exige ao menos 1 no total) mais() { - this.quantidadeTarget.value = String(this.qtdAtual() + 1) + this.quantidadeTarget.value = String(this.qtdAtual(this.quantidadeTarget) + 1) } menos() { - this.quantidadeTarget.value = String(Math.max(1, this.qtdAtual() - 1)) + this.quantidadeTarget.value = String(Math.max(0, this.qtdAtual(this.quantidadeTarget) - 1)) } - qtdAtual() { - const n = parseInt(this.quantidadeTarget.value, 10) - return Number.isFinite(n) && n > 0 ? n : 1 + maisEspecial() { + this.quantidadeEspecialTarget.value = String(this.qtdAtual(this.quantidadeEspecialTarget) + 1) + } + + menosEspecial() { + this.quantidadeEspecialTarget.value = String(Math.max(0, this.qtdAtual(this.quantidadeEspecialTarget) - 1)) + } + + qtdAtual(alvo) { + const n = parseInt(alvo.value, 10) + return Number.isFinite(n) && n > 0 ? n : 0 } async adicionar(event) { event?.preventDefault() - const quantidade = this.qtdAtual() - const motorista = this.motoristaValue.trim() + const quantidade = this.qtdAtual(this.quantidadeTarget) + const quantidadeEspecial = this.qtdAtual(this.quantidadeEspecialTarget) + const motorista = this.motoristaValue.trim() if (!motorista) { this.mostrarErro("Motorista não identificado."); return } + if (quantidade + quantidadeEspecial < 1) { + this.mostrarErro("Informe ao menos um termo (normal ou especial).") + return + } const btn = event.currentTarget btn.disabled = true @@ -56,9 +70,10 @@ export default class extends Controller { "X-CSRF-Token": document.querySelector('meta[name="csrf-token"]').content }, body: JSON.stringify({ - motorista: motorista, - quantidade: quantidade, - vehicle: this.hasVeiculoTarget ? this.veiculoTarget.value : "" + motorista: motorista, + quantidade: quantidade, + quantidade_especial: quantidadeEspecial, + vehicle: this.hasVeiculoTarget ? this.veiculoTarget.value : "" }) }) const data = await resp.json() diff --git a/app/javascript/controllers/validacao_controller.js b/app/javascript/controllers/validacao_controller.js index a8c3c79..e75c130 100644 --- a/app/javascript/controllers/validacao_controller.js +++ b/app/javascript/controllers/validacao_controller.js @@ -12,7 +12,7 @@ export default class extends Controller { "consolidarBtn", "consolidarHint", "contadorClassificadas", "contadorTotal", "contadorManuais", "manuaisInfo", "qtdNormal", "qtdRetirada", "qtdBonus", "qtdDesconto", "qtdExtraordinaria", - "qtdTermo", "qtdManuais", "valorTotal" + "qtdTermo", "qtdTermoEspecial", "qtdManuais", "valorTotal" ] static values = { @@ -187,6 +187,8 @@ export default class extends Controller { this.qtdExtraordinariaTarget.textContent = porTipo["extraordinaria"] || 0 if (this.hasQtdTermoTarget) this.qtdTermoTarget.textContent = porTipo["termo"] || 0 + if (this.hasQtdTermoEspecialTarget) + this.qtdTermoEspecialTarget.textContent = porTipo["termo_especial"] || 0 const valor = dados.valor_total || 0 this.valorTotalTarget.textContent = diff --git a/app/models/configuracao.rb b/app/models/configuracao.rb index 56ae369..d42932a 100644 --- a/app/models/configuracao.rb +++ b/app/models/configuracao.rb @@ -10,6 +10,7 @@ class Configuracao < ApplicationRecord preco_desconto preco_extraordinaria preco_termo + preco_termo_especial notificacao_whatsapp notificacao_email empresa_nome @@ -23,6 +24,7 @@ class Configuracao < ApplicationRecord preco_desconto preco_extraordinaria preco_termo + preco_termo_especial ].freeze validates :chave, presence: true, inclusion: { in: CHAVES }, uniqueness: true @@ -58,6 +60,10 @@ class Configuracao < ApplicationRecord valor('preco_termo').to_f end + def self.preco_termo_especial + valor('preco_termo_especial').to_f + end + # Mapa usado pelo DashboardController (Fase 3) def self.mapa_de_precos { @@ -66,7 +72,8 @@ class Configuracao < ApplicationRecord bonus: preco_bonus, desconto: preco_desconto, extraordinaria: preco_extraordinaria, - termo: preco_termo + termo: preco_termo, + termo_especial: preco_termo_especial } end @@ -77,6 +84,7 @@ class Configuracao < ApplicationRecord 'preco_desconto' => 'Desconto', 'preco_extraordinaria' => 'Entrega Extraordinária', 'preco_termo' => 'Entrega de Termo', + 'preco_termo_especial' => 'Entrega de Termo Especial', 'notificacao_whatsapp' => 'Notificação WhatsApp', 'notificacao_email' => 'Notificação E-mail', 'empresa_nome' => 'Nome da Empresa' @@ -89,6 +97,7 @@ class Configuracao < ApplicationRecord 'preco_desconto' => '⚠️', 'preco_extraordinaria' => '✨', 'preco_termo' => '📄', + 'preco_termo_especial' => '📋', 'notificacao_whatsapp' => '💬', 'notificacao_email' => '✉️', 'empresa_nome' => '🏢' diff --git a/app/models/consolidacao.rb b/app/models/consolidacao.rb index 8bb25d9..7d14d9e 100644 --- a/app/models/consolidacao.rb +++ b/app/models/consolidacao.rb @@ -339,10 +339,15 @@ class Consolidacao < ApplicationRecord # para atrelar (só motorista + quantidade, a um preço fixo configurável). Fica # numa única linha (manual: true): quantidade guarda o N e valor_aplicado guarda # o total do lote, então recalcular_motorista! e demais somas seguem inalteradas. - def adicionar_termos!(motorista:, quantidade:, user:, vehicle: nil) + # `tipo` distingue termo normal ('termo') de termo especial ('termo_especial'), + # cada um com seu preço configurável. + def adicionar_termos!(motorista:, quantidade:, user:, vehicle: nil, tipo: 'termo') nome = motorista.to_s.strip raise ArgumentError, 'Informe o motorista do termo.' if nome.blank? + tipo = tipo.to_s + raise ArgumentError, 'Tipo de termo inválido.' unless %w[termo termo_especial].include?(tipo) + qtd = quantidade.to_i raise ArgumentError, 'Informe uma quantidade válida de termos.' if qtd < 1 @@ -351,10 +356,10 @@ class Consolidacao < ApplicationRecord consolidacao_entregas.create!( tracking_id: "TERMO-#{SecureRandom.uuid}", # sintético: não há Entrega/NF - tipo: 'termo', + tipo: tipo, motorista_nome: nome, quantidade: qtd, - valor_aplicado: qtd * ConsolidacaoEntrega.valor_para('termo'), # total do lote + valor_aplicado: qtd * ConsolidacaoEntrega.valor_para(tipo), # total do lote manual: true, vehicle: vehicle.presence, # veículo escolhido no lançamento (Q3) created_by: user&.id diff --git a/app/models/consolidacao_entrega.rb b/app/models/consolidacao_entrega.rb index df93357..2ce0894 100644 --- a/app/models/consolidacao_entrega.rb +++ b/app/models/consolidacao_entrega.rb @@ -15,7 +15,8 @@ class ConsolidacaoEntrega < ApplicationRecord bonus: 2, desconto: 3, extraordinaria: 4, - termo: 5 + termo: 5, + termo_especial: 6 } TIPO_CORES = { @@ -24,7 +25,8 @@ class ConsolidacaoEntrega < ApplicationRecord 'bonus' => { bg: 'bg-white border border-orange-500', text: 'text-black', label: 'Bônus' }, 'desconto' => { bg: 'bg-gray-900', text: 'text-white', label: 'Desconto' }, 'extraordinaria' => { bg: 'bg-purple-600', text: 'text-white', label: 'Entrega Extraordinária' }, - 'termo' => { bg: 'bg-blue-600', text: 'text-white', label: 'Entrega de Termo' } + 'termo' => { bg: 'bg-blue-600', text: 'text-white', label: 'Entrega de Termo' }, + 'termo_especial' => { bg: 'bg-cyan-600', text: 'text-white', label: 'Entrega de Termo Especial' } }.freeze # Uma entrega pode ter vários pilares (Normal + Bônus + Retirada…), @@ -54,6 +56,7 @@ class ConsolidacaoEntrega < ApplicationRecord when 'desconto' then Configuracao.preco_desconto when 'extraordinaria' then Configuracao.preco_extraordinaria when 'termo' then Configuracao.preco_termo + when 'termo_especial' then Configuracao.preco_termo_especial else 0 end end diff --git a/app/services/pdf/relatorio_motorista_pdf.rb b/app/services/pdf/relatorio_motorista_pdf.rb index afd5b71..bb1f08c 100644 --- a/app/services/pdf/relatorio_motorista_pdf.rb +++ b/app/services/pdf/relatorio_motorista_pdf.rb @@ -37,7 +37,7 @@ module Pdf original&.vehicle.presence || (e.manual? ? 'manual' : '—'), original ? "NF #{original.numero_nf}" : (e.nf_manual.present? ? "NF #{e.nf_manual}" : '—'), original ? original.address.to_s : (e.obs_manual.presence || '—'), - e.termo? && e.quantidade > 1 ? "#{e.tipo_cor[:label]} ×#{e.quantidade}" : e.tipo_cor[:label], + (e.termo? || e.termo_especial?) && e.quantidade > 1 ? "#{e.tipo_cor[:label]} ×#{e.quantidade}" : e.tipo_cor[:label], (e.desconto? ? '-' : '') + moeda(e.valor_aplicado) ] end diff --git a/app/views/consolidacao_entregas/revisar.html.erb b/app/views/consolidacao_entregas/revisar.html.erb index fd3161a..c5dc270 100644 --- a/app/views/consolidacao_entregas/revisar.html.erb +++ b/app/views/consolidacao_entregas/revisar.html.erb @@ -12,14 +12,15 @@ <%# Resumo por tipo %> -
+
<% [ ['entrega_normal', 'Normal', 'bg-orange-500 text-black'], ['retirada', 'Retirada', 'bg-orange-800 text-white'], ['bonus', 'Bônus', 'bg-white text-black'], ['desconto', 'Desconto', 'bg-black text-white border border-gray-700'], ['extraordinaria', 'Extraordinária', 'bg-purple-600 text-white'], - ['termo', 'Termo', 'bg-blue-600 text-white'] + ['termo', 'Termo', 'bg-blue-600 text-white'], + ['termo_especial', 'Termo Especial', 'bg-cyan-600 text-white'] ].each do |tipo, label, cores| %>

<%= @resumo[tipo] || 0 %>

@@ -67,7 +68,7 @@ <%= c.entrega_original&.address.presence || endereco_manual.presence || '—' %> - <%= c.tipo_cor[:label] %><% if c.termo? && c.quantidade > 1 %> ×<%= c.quantidade %><% end %> + <%= c.tipo_cor[:label] %><% if (c.termo? || c.termo_especial?) && c.quantidade > 1 %> ×<%= c.quantidade %><% end %> <% if c.manual? %> ➕ Apontamento diff --git a/app/views/consolidacao_entregas/validar.html.erb b/app/views/consolidacao_entregas/validar.html.erb index 1ad2c76..1bff40e 100644 --- a/app/views/consolidacao_entregas/validar.html.erb +++ b/app/views/consolidacao_entregas/validar.html.erb @@ -288,20 +288,14 @@
-

Entrega sem nota fiscal nem código de rastreio. Informe quantos termos foram entregues.

+

Entrega sem nota fiscal nem código de rastreio. Informe quantos termos de cada tipo foram entregues.

- <%# Motorista (contexto da tela) + preço unitário %> -
-
-

Motorista

-

🚚 <%= @motorista %>

-
-
-

Preço por termo

-

<%= moeda(Configuracao.preco_termo) %>

-
+ <%# Motorista (contexto da tela) %> +
+

Motorista

+

🚚 <%= @motorista %>

<%# Veículo (Q3) — a qual carro estes termos pertencem %> @@ -314,15 +308,32 @@ <% end %> - <%# Stepper de quantidade %> - -
- - - + <%# Steppers de quantidade — termo normal e termo especial %> +
+
+

📄 Termo Normal

+

<%= moeda(@precos[:termo]) %> cada

+
+ + + +
+
+
+

📋 Termo Especial

+

<%= moeda(@precos[:termo_especial]) %> cada

+
+ + + +
+
+
+ Termo Especial + 0 +
@@ -456,6 +471,7 @@

⚠️ Desconto: -<%= moeda(@precos[:desconto]) %>

✨ Extraordinária: <%= moeda(@precos[:extraordinaria]) %> (valor por entrega)

📄 Termo: <%= moeda(@precos[:termo]) %>

+

📋 Termo Especial: <%= moeda(@precos[:termo_especial]) %>

diff --git a/app/views/consolidacoes/show.html.erb b/app/views/consolidacoes/show.html.erb index bcf131d..27cdb67 100644 --- a/app/views/consolidacoes/show.html.erb +++ b/app/views/consolidacoes/show.html.erb @@ -171,14 +171,20 @@ <% @apontamentos.each do |(motorista, _tracking), pilares| %> - <% eh_termo = pilares.first.termo? %> + <% eh_termo = pilares.first.termo? || pilares.first.termo_especial? %> <% entrega = eh_termo ? nil : pilares.first.entrega_original %> <% nf_exibida = entrega&.numero_nf || pilares.first.nf_manual %> <% obs_manual = pilares.first.obs_manual %> <% total = pilares.sum { |p| p.desconto? ? -p.valor_aplicado : p.valor_aplicado } %> -

<%= eh_termo ? '📄 Entrega de termo' : "NF #{nf_exibida.presence || '—'}" %>

+

<%= if pilares.first.termo? + '📄 Entrega de termo' + elsif pilares.first.termo_especial? + '📋 Entrega de termo especial' + else + "NF #{nf_exibida.presence || '—'}" + end %>

<%= motorista %><% if entrega&.vehicle.present? %> · <%= entrega.vehicle %><% end %> <% if entrega.nil? && obs_manual.present? %> · <%= obs_manual %><% end %> @@ -187,7 +193,7 @@

<% pilares.each do |p| %> - <%= p.tipo_cor[:label] %><% if p.termo? && p.quantidade > 1 %> ×<%= p.quantidade %><% end %> + <%= p.tipo_cor[:label] %><% if (p.termo? || p.termo_especial?) && p.quantidade > 1 %> ×<%= p.quantidade %><% end %> <% end %>
diff --git a/db/migrate/20260715000001_add_preco_termo_especial_configuracao.rb b/db/migrate/20260715000001_add_preco_termo_especial_configuracao.rb new file mode 100644 index 0000000..3112576 --- /dev/null +++ b/db/migrate/20260715000001_add_preco_termo_especial_configuracao.rb @@ -0,0 +1,18 @@ +# Cria a configuração de preço "Entrega de Termo Especial" em bancos já +# existentes, para que apareça na tela de Configurações. Idempotente — não +# sobrescreve se o admin já tiver definido um valor. +class AddPrecoTermoEspecialConfiguracao < ActiveRecord::Migration[7.1] + def up + return if Configuracao.exists?(chave: 'preco_termo_especial') + + Configuracao.create!( + chave: 'preco_termo_especial', + valor: '0.00', + descricao: 'Valor pago por entrega de termo especial (R$)' + ) + end + + def down + Configuracao.where(chave: 'preco_termo_especial').delete_all + end +end diff --git a/db/seeds.rb b/db/seeds.rb index d0d1e9a..0466e38 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -30,6 +30,7 @@ configs = [ { chave: 'preco_desconto', valor: '15.00', descricao: 'Valor descontado por problema (R$)' }, { chave: 'preco_extraordinaria', valor: '25.00', descricao: 'Valor coringa para entregas fora do planejamento (R$)' }, { chave: 'preco_termo', valor: '15.00', descricao: 'Valor pago por entrega de termo (R$)' }, + { chave: 'preco_termo_especial', valor: '0.00', descricao: 'Valor pago por entrega de termo especial (R$)' }, { chave: 'notificacao_whatsapp', valor: 'false', descricao: 'Notificações via WhatsApp (true/false)' }, { chave: 'notificacao_email', valor: 'false', descricao: 'Notificações via e-mail (true/false)' }, { chave: 'empresa_nome', valor: 'Reem Transporte', descricao: 'Nome da empresa exibido no sistema' }, diff --git a/spec/models/configuracao_spec.rb b/spec/models/configuracao_spec.rb index 2938f86..d6f874e 100644 --- a/spec/models/configuracao_spec.rb +++ b/spec/models/configuracao_spec.rb @@ -30,6 +30,11 @@ RSpec.describe Configuracao, type: :model do it 'retorna 0.0 quando a chave não existe' do expect(Configuracao.preco_bonus).to eq(0.0) end + + it 'aceita a chave preco_termo_especial e converte em float' do + create(:configuracao, chave: 'preco_termo_especial', valor: '25.00') + expect(Configuracao.preco_termo_especial).to eq(25.0) + end end describe '#valor_formatado' do