teste #7

Merged
victor merged 15 commits from teste into main 2026-07-15 19:30:26 -03:00
13 changed files with 158 additions and 62 deletions
Showing only changes of commit 5af51534be - Show all commits

View File

@@ -64,7 +64,8 @@ class ConsolidacaoEntregasController < ApplicationController
bonus: Configuracao.preco_bonus, bonus: Configuracao.preco_bonus,
desconto: Configuracao.preco_desconto, desconto: Configuracao.preco_desconto,
extraordinaria: Configuracao.preco_extraordinaria, extraordinaria: Configuracao.preco_extraordinaria,
termo: Configuracao.preco_termo termo: Configuracao.preco_termo,
termo_especial: Configuracao.preco_termo_especial
} }
end end
@@ -267,25 +268,38 @@ class ConsolidacaoEntregasController < ApplicationController
end end
# POST /consolidacoes/:consolidacao_id/consolidacao_entregas/apontar_termo # 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 # ENTREGA DE TERMO — lança lotes de termos para o motorista. Não há NF nem
# código de rastreio: só a quantidade, a um preço fixo configurável. # código de rastreio: só as quantidades (normal e especial), cada tipo a um
# preço fixo configurável.
def apontar_termo def apontar_termo
authorize @consolidacao, :update? authorize @consolidacao, :update?
motorista = @consolidacao.adicionar_termos!( qtd_normal = params[:quantidade].to_i
motorista: params[:motorista].to_s.strip, qtd_especial = params[:quantidade_especial].to_i
quantidade: params[:quantidade], raise ArgumentError, 'Informe ao menos um termo (normal ou especial).' if qtd_normal < 1 && qtd_especial < 1
vehicle: params[:vehicle], # veículo escolhido no lançamento (Q3)
user: current_user 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, 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| respond_to do |format|
format.json { render json: resumo_json(motorista).merge(motorista: motorista) } format.json { render json: resumo_json(motorista).merge(motorista: motorista) }
format.html do format.html do
redirect_to validar_consolidacao_consolidacao_entregas_path(@consolidacao, motorista: motorista), 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
end end
rescue ArgumentError => e rescue ArgumentError => e
@@ -534,8 +548,9 @@ class ConsolidacaoEntregasController < ApplicationController
# Conta lançamentos manuais como ENTREGAS: cada termo vale `quantidade` (lote # Conta lançamentos manuais como ENTREGAS: cada termo vale `quantidade` (lote
# numa só linha); os demais apontamentos manuais valem 1 por nota (tracking). # numa só linha); os demais apontamentos manuais valem 1 por nota (tracking).
def contar_manuais(rel_manual) def contar_manuais(rel_manual)
rel_manual.termo.sum(:quantidade) + tipos_termo = ConsolidacaoEntrega.tipos.values_at(:termo, :termo_especial)
rel_manual.where.not(tipo: ConsolidacaoEntrega.tipos[:termo]).distinct.count(:tracking_id) rel_manual.where(tipo: tipos_termo).sum(:quantidade) +
rel_manual.where.not(tipo: tipos_termo).distinct.count(:tracking_id)
end end
# `vehicles` (opcional, string ou array): quando presente, TODO o resumo # `vehicles` (opcional, string ou array): quando presente, TODO o resumo

View File

@@ -1,17 +1,18 @@
// app/javascript/controllers/apontamento_termo_controller.js // app/javascript/controllers/apontamento_termo_controller.js
// Stimulus — Entrega de termo. Lança um lote de N termos para o motorista da tela. // Stimulus — Entrega de termo. Lança lotes de termos (normal e/ou especial) para
// Entrega sem NF nem código de rastreio: só a quantidade (stepper N +), a um // o motorista da tela. Entrega sem NF nem código de rastreio: só as quantidades
// preço fixo configurável. // (um stepper N + por tipo), cada tipo a um preço fixo configurável.
import { Controller } from "@hotwired/stimulus" import { Controller } from "@hotwired/stimulus"
export default class extends Controller { export default class extends Controller {
static targets = ["modal", "quantidade", "erro", "veiculo"] static targets = ["modal", "quantidade", "quantidadeEspecial", "erro", "veiculo"]
static values = { apontarTermoUrl: String, motorista: String } static values = { apontarTermoUrl: String, motorista: String }
abrir() { abrir() {
this.erroTarget.classList.add("hidden") this.erroTarget.classList.add("hidden")
this.quantidadeTarget.value = "1" this.quantidadeTarget.value = "1"
this.quantidadeEspecialTarget.value = "0"
this.modalTarget.classList.remove("hidden") this.modalTarget.classList.remove("hidden")
} }
@@ -24,26 +25,39 @@ export default class extends Controller {
if (event.target === this.modalTarget) this.fechar() if (event.target === this.modalTarget) this.fechar()
} }
// Stepper // Steppers (mínimo 0 em ambos; o envio exige ao menos 1 no total)
mais() { mais() {
this.quantidadeTarget.value = String(this.qtdAtual() + 1) this.quantidadeTarget.value = String(this.qtdAtual(this.quantidadeTarget) + 1)
} }
menos() { menos() {
this.quantidadeTarget.value = String(Math.max(1, this.qtdAtual() - 1)) this.quantidadeTarget.value = String(Math.max(0, this.qtdAtual(this.quantidadeTarget) - 1))
} }
qtdAtual() { maisEspecial() {
const n = parseInt(this.quantidadeTarget.value, 10) this.quantidadeEspecialTarget.value = String(this.qtdAtual(this.quantidadeEspecialTarget) + 1)
return Number.isFinite(n) && n > 0 ? n : 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) { async adicionar(event) {
event?.preventDefault() event?.preventDefault()
const quantidade = this.qtdAtual() const quantidade = this.qtdAtual(this.quantidadeTarget)
const motorista = this.motoristaValue.trim() const quantidadeEspecial = this.qtdAtual(this.quantidadeEspecialTarget)
const motorista = this.motoristaValue.trim()
if (!motorista) { this.mostrarErro("Motorista não identificado."); return } 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 const btn = event.currentTarget
btn.disabled = true btn.disabled = true
@@ -56,9 +70,10 @@ export default class extends Controller {
"X-CSRF-Token": document.querySelector('meta[name="csrf-token"]').content "X-CSRF-Token": document.querySelector('meta[name="csrf-token"]').content
}, },
body: JSON.stringify({ body: JSON.stringify({
motorista: motorista, motorista: motorista,
quantidade: quantidade, quantidade: quantidade,
vehicle: this.hasVeiculoTarget ? this.veiculoTarget.value : "" quantidade_especial: quantidadeEspecial,
vehicle: this.hasVeiculoTarget ? this.veiculoTarget.value : ""
}) })
}) })
const data = await resp.json() const data = await resp.json()

View File

@@ -12,7 +12,7 @@ export default class extends Controller {
"consolidarBtn", "consolidarHint", "consolidarBtn", "consolidarHint",
"contadorClassificadas", "contadorTotal", "contadorManuais", "manuaisInfo", "contadorClassificadas", "contadorTotal", "contadorManuais", "manuaisInfo",
"qtdNormal", "qtdRetirada", "qtdBonus", "qtdDesconto", "qtdExtraordinaria", "qtdNormal", "qtdRetirada", "qtdBonus", "qtdDesconto", "qtdExtraordinaria",
"qtdTermo", "qtdManuais", "valorTotal" "qtdTermo", "qtdTermoEspecial", "qtdManuais", "valorTotal"
] ]
static values = { static values = {
@@ -187,6 +187,8 @@ export default class extends Controller {
this.qtdExtraordinariaTarget.textContent = porTipo["extraordinaria"] || 0 this.qtdExtraordinariaTarget.textContent = porTipo["extraordinaria"] || 0
if (this.hasQtdTermoTarget) if (this.hasQtdTermoTarget)
this.qtdTermoTarget.textContent = porTipo["termo"] || 0 this.qtdTermoTarget.textContent = porTipo["termo"] || 0
if (this.hasQtdTermoEspecialTarget)
this.qtdTermoEspecialTarget.textContent = porTipo["termo_especial"] || 0
const valor = dados.valor_total || 0 const valor = dados.valor_total || 0
this.valorTotalTarget.textContent = this.valorTotalTarget.textContent =

View File

@@ -10,6 +10,7 @@ class Configuracao < ApplicationRecord
preco_desconto preco_desconto
preco_extraordinaria preco_extraordinaria
preco_termo preco_termo
preco_termo_especial
notificacao_whatsapp notificacao_whatsapp
notificacao_email notificacao_email
empresa_nome empresa_nome
@@ -23,6 +24,7 @@ class Configuracao < ApplicationRecord
preco_desconto preco_desconto
preco_extraordinaria preco_extraordinaria
preco_termo preco_termo
preco_termo_especial
].freeze ].freeze
validates :chave, presence: true, inclusion: { in: CHAVES }, uniqueness: true validates :chave, presence: true, inclusion: { in: CHAVES }, uniqueness: true
@@ -58,6 +60,10 @@ class Configuracao < ApplicationRecord
valor('preco_termo').to_f valor('preco_termo').to_f
end end
def self.preco_termo_especial
valor('preco_termo_especial').to_f
end
# Mapa usado pelo DashboardController (Fase 3) # Mapa usado pelo DashboardController (Fase 3)
def self.mapa_de_precos def self.mapa_de_precos
{ {
@@ -66,7 +72,8 @@ class Configuracao < ApplicationRecord
bonus: preco_bonus, bonus: preco_bonus,
desconto: preco_desconto, desconto: preco_desconto,
extraordinaria: preco_extraordinaria, extraordinaria: preco_extraordinaria,
termo: preco_termo termo: preco_termo,
termo_especial: preco_termo_especial
} }
end end
@@ -77,6 +84,7 @@ class Configuracao < ApplicationRecord
'preco_desconto' => 'Desconto', 'preco_desconto' => 'Desconto',
'preco_extraordinaria' => 'Entrega Extraordinária', 'preco_extraordinaria' => 'Entrega Extraordinária',
'preco_termo' => 'Entrega de Termo', 'preco_termo' => 'Entrega de Termo',
'preco_termo_especial' => 'Entrega de Termo Especial',
'notificacao_whatsapp' => 'Notificação WhatsApp', 'notificacao_whatsapp' => 'Notificação WhatsApp',
'notificacao_email' => 'Notificação E-mail', 'notificacao_email' => 'Notificação E-mail',
'empresa_nome' => 'Nome da Empresa' 'empresa_nome' => 'Nome da Empresa'
@@ -89,6 +97,7 @@ class Configuracao < ApplicationRecord
'preco_desconto' => '⚠️', 'preco_desconto' => '⚠️',
'preco_extraordinaria' => '✨', 'preco_extraordinaria' => '✨',
'preco_termo' => '📄', 'preco_termo' => '📄',
'preco_termo_especial' => '📋',
'notificacao_whatsapp' => '💬', 'notificacao_whatsapp' => '💬',
'notificacao_email' => '✉️', 'notificacao_email' => '✉️',
'empresa_nome' => '🏢' 'empresa_nome' => '🏢'

View File

@@ -339,10 +339,15 @@ class Consolidacao < ApplicationRecord
# para atrelar (só motorista + quantidade, a um preço fixo configurável). Fica # 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 # 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. # 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 nome = motorista.to_s.strip
raise ArgumentError, 'Informe o motorista do termo.' if nome.blank? 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 qtd = quantidade.to_i
raise ArgumentError, 'Informe uma quantidade válida de termos.' if qtd < 1 raise ArgumentError, 'Informe uma quantidade válida de termos.' if qtd < 1
@@ -351,10 +356,10 @@ class Consolidacao < ApplicationRecord
consolidacao_entregas.create!( consolidacao_entregas.create!(
tracking_id: "TERMO-#{SecureRandom.uuid}", # sintético: não há Entrega/NF tracking_id: "TERMO-#{SecureRandom.uuid}", # sintético: não há Entrega/NF
tipo: 'termo', tipo: tipo,
motorista_nome: nome, motorista_nome: nome,
quantidade: qtd, quantidade: qtd,
valor_aplicado: qtd * ConsolidacaoEntrega.valor_para('termo'), # total do lote valor_aplicado: qtd * ConsolidacaoEntrega.valor_para(tipo), # total do lote
manual: true, manual: true,
vehicle: vehicle.presence, # veículo escolhido no lançamento (Q3) vehicle: vehicle.presence, # veículo escolhido no lançamento (Q3)
created_by: user&.id created_by: user&.id

View File

@@ -15,7 +15,8 @@ class ConsolidacaoEntrega < ApplicationRecord
bonus: 2, bonus: 2,
desconto: 3, desconto: 3,
extraordinaria: 4, extraordinaria: 4,
termo: 5 termo: 5,
termo_especial: 6
} }
TIPO_CORES = { TIPO_CORES = {
@@ -24,7 +25,8 @@ class ConsolidacaoEntrega < ApplicationRecord
'bonus' => { bg: 'bg-white border border-orange-500', text: 'text-black', label: 'Bônus' }, 'bonus' => { bg: 'bg-white border border-orange-500', text: 'text-black', label: 'Bônus' },
'desconto' => { bg: 'bg-gray-900', text: 'text-white', label: 'Desconto' }, 'desconto' => { bg: 'bg-gray-900', text: 'text-white', label: 'Desconto' },
'extraordinaria' => { bg: 'bg-purple-600', text: 'text-white', label: 'Entrega Extraordinária' }, '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 }.freeze
# Uma entrega pode ter vários pilares (Normal + Bônus + Retirada…), # 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 'desconto' then Configuracao.preco_desconto
when 'extraordinaria' then Configuracao.preco_extraordinaria when 'extraordinaria' then Configuracao.preco_extraordinaria
when 'termo' then Configuracao.preco_termo when 'termo' then Configuracao.preco_termo
when 'termo_especial' then Configuracao.preco_termo_especial
else 0 else 0
end end
end end

View File

@@ -37,7 +37,7 @@ module Pdf
original&.vehicle.presence || (e.manual? ? 'manual' : '—'), original&.vehicle.presence || (e.manual? ? 'manual' : '—'),
original ? "NF #{original.numero_nf}" : (e.nf_manual.present? ? "NF #{e.nf_manual}" : '—'), original ? "NF #{original.numero_nf}" : (e.nf_manual.present? ? "NF #{e.nf_manual}" : '—'),
original ? original.address.to_s : (e.obs_manual.presence || '—'), 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) (e.desconto? ? '-' : '') + moeda(e.valor_aplicado)
] ]
end end

View File

@@ -12,14 +12,15 @@
</div> </div>
<%# Resumo por tipo %> <%# Resumo por tipo %>
<div class="grid grid-cols-2 md:grid-cols-6 gap-3 mb-6"> <div class="grid grid-cols-2 md:grid-cols-7 gap-3 mb-6">
<% [ <% [
['entrega_normal', 'Normal', 'bg-orange-500 text-black'], ['entrega_normal', 'Normal', 'bg-orange-500 text-black'],
['retirada', 'Retirada', 'bg-orange-800 text-white'], ['retirada', 'Retirada', 'bg-orange-800 text-white'],
['bonus', 'Bônus', 'bg-white text-black'], ['bonus', 'Bônus', 'bg-white text-black'],
['desconto', 'Desconto', 'bg-black text-white border border-gray-700'], ['desconto', 'Desconto', 'bg-black text-white border border-gray-700'],
['extraordinaria', 'Extraordinária', 'bg-purple-600 text-white'], ['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| %> ].each do |tipo, label, cores| %>
<div class="<%= cores %> rounded-xl p-4 text-center"> <div class="<%= cores %> rounded-xl p-4 text-center">
<p class="font-black text-3xl"><%= @resumo[tipo] || 0 %></p> <p class="font-black text-3xl"><%= @resumo[tipo] || 0 %></p>
@@ -67,7 +68,7 @@
<td class="px-4 py-3 text-gray-300 text-xs"><%= c.entrega_original&.address.presence || endereco_manual.presence || '—' %></td> <td class="px-4 py-3 text-gray-300 text-xs"><%= c.entrega_original&.address.presence || endereco_manual.presence || '—' %></td>
<td class="px-4 py-3"> <td class="px-4 py-3">
<span class="<%= c.tipo_cor[:bg] %> <%= c.tipo_cor[:text] %> px-2 py-1 rounded text-xs font-bold"> <span class="<%= c.tipo_cor[:bg] %> <%= c.tipo_cor[:text] %> px-2 py-1 rounded text-xs font-bold">
<%= 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 %>
</span> </span>
<% if c.manual? %> <% if c.manual? %>
<span class="ml-1 bg-orange-500/15 text-orange-400 border border-orange-500/40 px-2 py-1 rounded text-xs font-bold"> Apontamento</span> <span class="ml-1 bg-orange-500/15 text-orange-400 border border-orange-500/40 px-2 py-1 rounded text-xs font-bold"> Apontamento</span>

View File

@@ -288,20 +288,14 @@
<button type="button" data-action="apontamento-termo#fechar" <button type="button" data-action="apontamento-termo#fechar"
class="text-gray-500 hover:text-white text-2xl leading-none">✕</button> class="text-gray-500 hover:text-white text-2xl leading-none">✕</button>
</div> </div>
<p class="text-gray-400 text-sm mb-4">Entrega sem nota fiscal nem código de rastreio. Informe quantos termos foram entregues.</p> <p class="text-gray-400 text-sm mb-4">Entrega sem nota fiscal nem código de rastreio. Informe quantos termos de cada tipo foram entregues.</p>
<p data-apontamento-termo-target="erro" class="hidden bg-red-500/10 text-red-400 border border-red-500/30 rounded-lg px-3 py-2 text-sm mb-3"></p> <p data-apontamento-termo-target="erro" class="hidden bg-red-500/10 text-red-400 border border-red-500/30 rounded-lg px-3 py-2 text-sm mb-3"></p>
<%# Motorista (contexto da tela) + preço unitário %> <%# Motorista (contexto da tela) %>
<div class="flex items-center justify-between bg-[#0a0a0a] border border-[#2a2a2a] rounded-lg px-4 py-3 mb-4"> <div class="bg-[#0a0a0a] border border-[#2a2a2a] rounded-lg px-4 py-3 mb-4">
<div> <p class="text-gray-500 text-xs">Motorista</p>
<p class="text-gray-500 text-xs">Motorista</p> <p class="text-white font-semibold">🚚 <%= @motorista %></p>
<p class="text-white font-semibold">🚚 <%= @motorista %></p>
</div>
<div class="text-right">
<p class="text-gray-500 text-xs">Preço por termo</p>
<p class="text-blue-400 font-semibold"><%= moeda(Configuracao.preco_termo) %></p>
</div>
</div> </div>
<%# Veículo (Q3) — a qual carro estes termos pertencem %> <%# Veículo (Q3) — a qual carro estes termos pertencem %>
@@ -314,15 +308,32 @@
<% end %> <% end %>
</select> </select>
<%# Stepper de quantidade %> <%# Steppers de quantidade — termo normal e termo especial %>
<label class="block text-gray-400 text-sm mb-2">Quantidade de termos</label> <div class="grid grid-cols-2 gap-3 mb-5">
<div class="flex items-center justify-center gap-3 mb-5"> <div class="bg-[#0a0a0a] border border-[#2a2a2a] rounded-lg p-3">
<button type="button" data-action="apontamento-termo#menos" <p class="text-white text-sm font-semibold text-center">📄 Termo Normal</p>
class="w-12 h-12 rounded-lg bg-[#0a0a0a] border border-[#2a2a2a] hover:border-blue-500 text-white text-2xl font-bold leading-none"></button> <p class="text-blue-400 text-xs text-center mb-2"><%= moeda(@precos[:termo]) %> cada</p>
<input data-apontamento-termo-target="quantidade" type="text" inputmode="numeric" value="1" readonly <div class="flex items-center justify-center gap-2">
class="w-24 text-center bg-[#0a0a0a] border border-[#2a2a2a] text-white text-2xl font-bold rounded-lg py-2.5 focus:outline-none"> <button type="button" data-action="apontamento-termo#menos"
<button type="button" data-action="apontamento-termo#mais" class="w-10 h-10 rounded-lg bg-[#1a1a1a] border border-[#2a2a2a] hover:border-blue-500 text-white text-xl font-bold leading-none"></button>
class="w-12 h-12 rounded-lg bg-[#0a0a0a] border border-[#2a2a2a] hover:border-blue-500 text-white text-2xl font-bold leading-none">+</button> <input data-apontamento-termo-target="quantidade" type="text" inputmode="numeric" value="1" readonly
class="w-14 text-center bg-[#1a1a1a] border border-[#2a2a2a] text-white text-xl font-bold rounded-lg py-2 focus:outline-none">
<button type="button" data-action="apontamento-termo#mais"
class="w-10 h-10 rounded-lg bg-[#1a1a1a] border border-[#2a2a2a] hover:border-blue-500 text-white text-xl font-bold leading-none">+</button>
</div>
</div>
<div class="bg-[#0a0a0a] border border-[#2a2a2a] rounded-lg p-3">
<p class="text-white text-sm font-semibold text-center">📋 Termo Especial</p>
<p class="text-cyan-400 text-xs text-center mb-2"><%= moeda(@precos[:termo_especial]) %> cada</p>
<div class="flex items-center justify-center gap-2">
<button type="button" data-action="apontamento-termo#menosEspecial"
class="w-10 h-10 rounded-lg bg-[#1a1a1a] border border-[#2a2a2a] hover:border-cyan-500 text-white text-xl font-bold leading-none"></button>
<input data-apontamento-termo-target="quantidadeEspecial" type="text" inputmode="numeric" value="0" readonly
class="w-14 text-center bg-[#1a1a1a] border border-[#2a2a2a] text-white text-xl font-bold rounded-lg py-2 focus:outline-none">
<button type="button" data-action="apontamento-termo#maisEspecial"
class="w-10 h-10 rounded-lg bg-[#1a1a1a] border border-[#2a2a2a] hover:border-cyan-500 text-white text-xl font-bold leading-none">+</button>
</div>
</div>
</div> </div>
<button type="button" data-action="apontamento-termo#adicionar" <button type="button" data-action="apontamento-termo#adicionar"
@@ -437,6 +448,10 @@
<span class="flex items-center gap-2"><span class="w-3 h-3 bg-blue-600 rounded-full"></span> Termo</span> <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> <span class="text-white font-bold" data-validacao-target="qtdTermo">0</span>
</div> </div>
<div class="flex justify-between items-center">
<span class="flex items-center gap-2"><span class="w-3 h-3 bg-cyan-600 rounded-full"></span> Termo Especial</span>
<span class="text-white font-bold" data-validacao-target="qtdTermoEspecial">0</span>
</div>
</div> </div>
<div class="border-t border-[#2a2a2a] pt-4 flex justify-between items-center text-sm"> <div class="border-t border-[#2a2a2a] pt-4 flex justify-between items-center text-sm">
@@ -456,6 +471,7 @@
<p>⚠️ Desconto: -<%= moeda(@precos[:desconto]) %></p> <p>⚠️ Desconto: -<%= moeda(@precos[:desconto]) %></p>
<p>✨ Extraordinária: <%= moeda(@precos[:extraordinaria]) %> (valor por entrega)</p> <p>✨ Extraordinária: <%= moeda(@precos[:extraordinaria]) %> (valor por entrega)</p>
<p>📄 Termo: <%= moeda(@precos[:termo]) %></p> <p>📄 Termo: <%= moeda(@precos[:termo]) %></p>
<p>📋 Termo Especial: <%= moeda(@precos[:termo_especial]) %></p>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -171,14 +171,20 @@
</thead> </thead>
<tbody class="divide-y divide-[#2a2a2a]"> <tbody class="divide-y divide-[#2a2a2a]">
<% @apontamentos.each do |(motorista, _tracking), pilares| %> <% @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 %> <% entrega = eh_termo ? nil : pilares.first.entrega_original %>
<% nf_exibida = entrega&.numero_nf || pilares.first.nf_manual %> <% nf_exibida = entrega&.numero_nf || pilares.first.nf_manual %>
<% obs_manual = pilares.first.obs_manual %> <% obs_manual = pilares.first.obs_manual %>
<% total = pilares.sum { |p| p.desconto? ? -p.valor_aplicado : p.valor_aplicado } %> <% total = pilares.sum { |p| p.desconto? ? -p.valor_aplicado : p.valor_aplicado } %>
<tr> <tr>
<td class="px-4 py-3"> <td class="px-4 py-3">
<p class="text-white font-bold"><%= eh_termo ? '📄 Entrega de termo' : "NF #{nf_exibida.presence || '—'}" %></p> <p class="text-white font-bold"><%= if pilares.first.termo?
'📄 Entrega de termo'
elsif pilares.first.termo_especial?
'📋 Entrega de termo especial'
else
"NF #{nf_exibida.presence || '—'}"
end %></p>
<p class="text-gray-400 text-xs"> <p class="text-gray-400 text-xs">
<%= motorista %><% if entrega&.vehicle.present? %> · <%= entrega.vehicle %><% end %> <%= motorista %><% if entrega&.vehicle.present? %> · <%= entrega.vehicle %><% end %>
<% if entrega.nil? && obs_manual.present? %> · <%= obs_manual %><% end %> <% if entrega.nil? && obs_manual.present? %> · <%= obs_manual %><% end %>
@@ -187,7 +193,7 @@
<td class="px-4 py-3"> <td class="px-4 py-3">
<div class="flex flex-wrap gap-1"> <div class="flex flex-wrap gap-1">
<% pilares.each do |p| %> <% pilares.each do |p| %>
<span class="<%= p.tipo_cor[:bg] %> <%= p.tipo_cor[:text] %> px-2 py-1 rounded text-xs font-bold"><%= p.tipo_cor[:label] %><% if p.termo? && p.quantidade > 1 %> ×<%= p.quantidade %><% end %></span> <span class="<%= p.tipo_cor[:bg] %> <%= p.tipo_cor[:text] %> px-2 py-1 rounded text-xs font-bold"><%= p.tipo_cor[:label] %><% if (p.termo? || p.termo_especial?) && p.quantidade > 1 %> ×<%= p.quantidade %><% end %></span>
<% end %> <% end %>
</div> </div>
</td> </td>

View File

@@ -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

View File

@@ -30,6 +30,7 @@ configs = [
{ chave: 'preco_desconto', valor: '15.00', descricao: 'Valor descontado por problema (R$)' }, { 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_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', 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_whatsapp', valor: 'false', descricao: 'Notificações via WhatsApp (true/false)' },
{ chave: 'notificacao_email', valor: 'false', descricao: 'Notificações via e-mail (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' }, { chave: 'empresa_nome', valor: 'Reem Transporte', descricao: 'Nome da empresa exibido no sistema' },

View File

@@ -30,6 +30,11 @@ RSpec.describe Configuracao, type: :model do
it 'retorna 0.0 quando a chave não existe' do it 'retorna 0.0 quando a chave não existe' do
expect(Configuracao.preco_bonus).to eq(0.0) expect(Configuracao.preco_bonus).to eq(0.0)
end 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 end
describe '#valor_formatado' do describe '#valor_formatado' do