novo pilar tipo: termo (valor 5 no enum), lançado como uma linha por lote com manual: true e uma coluna nova quantidade. O valor_aplicado guarda o total do lote (qtd × preço), então recalcular_motorista! e todas as somas existentes seguem corretas sem mudança — linhas antigas ficam com quantidade = 1.

This commit is contained in:
2026-06-26 09:32:59 -03:00
parent f42949db78
commit 09867c8d39
17 changed files with 256 additions and 17 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

View File

@@ -159,8 +159,9 @@ class ConsolidacaoEntregasController < ApplicationController
todas = @consolidacao.consolidacao_entregas.where(motorista_nome: @motorista) todas = @consolidacao.consolidacao_entregas.where(motorista_nome: @motorista)
# Agregados calculados sobre o conjunto COMPLETO (não só a página exibida) # Agregados calculados sobre o conjunto COMPLETO (não só a página exibida).
@resumo = todas.group(:tipo).count # sum(:quantidade) (default 1) conta os termos lançados em lote como N entregas.
@resumo = todas.group(:tipo).sum(:quantidade)
@total = todas.sum do |c| @total = todas.sum do |c|
c.tipo == 'desconto' ? -c.valor_aplicado : c.valor_aplicado c.tipo == 'desconto' ? -c.valor_aplicado : c.valor_aplicado
end end
@@ -255,6 +256,31 @@ class ConsolidacaoEntregasController < ApplicationController
render_apontamento_erro(e.message) render_apontamento_erro(e.message)
end 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.
def apontar_termo
authorize @consolidacao, :update?
motorista = @consolidacao.adicionar_termos!(
motorista: params[:motorista].to_s.strip,
quantidade: params[:quantidade],
user: current_user
)
auditar!(:editar, @consolidacao,
dados_novos: { entrega_termo: params[:quantidade].to_i, 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}."
end
end
rescue ArgumentError => e
render_apontamento_erro(e.message)
end
# DELETE /consolidacoes/:consolidacao_id/consolidacao_entregas/remover_apontamento # DELETE /consolidacoes/:consolidacao_id/consolidacao_entregas/remover_apontamento
# Remove um apontamento manual — por ?id= (um pilar) ou por ?tracking_id=&motorista= # Remove um apontamento manual — por ?id= (um pilar) ou por ?tracking_id=&motorista=
# (todos os pilares daquela nota). Não passa pelo guard de elegibilidade, pois o # (todos os pilares daquela nota). Não passa pelo guard de elegibilidade, pois o

View File

@@ -353,7 +353,8 @@ class ConsolidacoesController < ApplicationController
authorize @consolidacao, :show? authorize @consolidacao, :show?
@motorista = params[:motorista] @motorista = params[:motorista]
@entregas = @consolidacao.consolidacao_entregas.where(motorista_nome: @motorista) @entregas = @consolidacao.consolidacao_entregas.where(motorista_nome: @motorista)
@resumo = @entregas.group(:tipo).count # sum(:quantidade) (default 1) conta os termos lançados em lote como N entregas.
@resumo = @entregas.group(:tipo).sum(:quantidade)
@total = @entregas.sum { |e| e.desconto? ? -e.valor_aplicado : e.valor_aplicado } @total = @entregas.sum { |e| e.desconto? ? -e.valor_aplicado : e.valor_aplicado }
render layout: false # renderiza dentro do modal (Turbo Frame) render layout: false # renderiza dentro do modal (Turbo Frame)
end end

View File

@@ -0,0 +1,78 @@
// 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.
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["modal", "quantidade", "erro"]
static values = { apontarTermoUrl: String, motorista: String }
abrir() {
this.erroTarget.classList.add("hidden")
this.quantidadeTarget.value = "1"
this.modalTarget.classList.remove("hidden")
}
fechar() {
this.modalTarget.classList.add("hidden")
}
// Fecha ao clicar no fundo (fora do card)
fundo(event) {
if (event.target === this.modalTarget) this.fechar()
}
// Stepper
mais() {
this.quantidadeTarget.value = String(this.qtdAtual() + 1)
}
menos() {
this.quantidadeTarget.value = String(Math.max(1, this.qtdAtual() - 1))
}
qtdAtual() {
const n = parseInt(this.quantidadeTarget.value, 10)
return Number.isFinite(n) && n > 0 ? n : 1
}
async adicionar(event) {
event?.preventDefault()
const quantidade = this.qtdAtual()
const motorista = this.motoristaValue.trim()
if (!motorista) { this.mostrarErro("Motorista não identificado."); return }
const btn = event.currentTarget
btn.disabled = true
try {
const resp = await fetch(this.apontarTermoUrlValue, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Accept": "application/json",
"X-CSRF-Token": document.querySelector('meta[name="csrf-token"]').content
},
body: JSON.stringify({ motorista: motorista, quantidade: quantidade })
})
const data = await resp.json()
if (!resp.ok || data.ok === false) {
this.mostrarErro(data.erro || "Não foi possível adicionar as entregas de termo.")
return
}
// Recarrega para refletir o valor atualizado do motorista.
window.location.reload()
} catch (e) {
this.mostrarErro("Erro ao adicionar as entregas de termo. Tente novamente.")
} finally {
btn.disabled = false
}
}
mostrarErro(msg) {
this.erroTarget.textContent = msg
this.erroTarget.classList.remove("hidden")
}
}

View File

@@ -9,6 +9,7 @@ class Configuracao < ApplicationRecord
preco_bonus preco_bonus
preco_desconto preco_desconto
preco_extraordinaria preco_extraordinaria
preco_termo
notificacao_whatsapp notificacao_whatsapp
notificacao_email notificacao_email
empresa_nome empresa_nome
@@ -21,6 +22,7 @@ class Configuracao < ApplicationRecord
preco_bonus preco_bonus
preco_desconto preco_desconto
preco_extraordinaria preco_extraordinaria
preco_termo
].freeze ].freeze
validates :chave, presence: true, inclusion: { in: CHAVES }, uniqueness: true validates :chave, presence: true, inclusion: { in: CHAVES }, uniqueness: true
@@ -52,6 +54,10 @@ class Configuracao < ApplicationRecord
valor('preco_extraordinaria').to_f valor('preco_extraordinaria').to_f
end end
def self.preco_termo
valor('preco_termo').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
{ {
@@ -59,7 +65,8 @@ class Configuracao < ApplicationRecord
retirada: preco_retirada, retirada: preco_retirada,
bonus: preco_bonus, bonus: preco_bonus,
desconto: preco_desconto, desconto: preco_desconto,
extraordinaria: preco_extraordinaria extraordinaria: preco_extraordinaria,
termo: preco_termo
} }
end end
@@ -69,6 +76,7 @@ class Configuracao < ApplicationRecord
'preco_bonus' => 'Bônus', 'preco_bonus' => 'Bônus',
'preco_desconto' => 'Desconto', 'preco_desconto' => 'Desconto',
'preco_extraordinaria' => 'Entrega Extraordinária', 'preco_extraordinaria' => 'Entrega Extraordinária',
'preco_termo' => 'Entrega de Termo',
'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'
@@ -80,6 +88,7 @@ class Configuracao < ApplicationRecord
'preco_bonus' => '⭐', 'preco_bonus' => '⭐',
'preco_desconto' => '⚠️', 'preco_desconto' => '⚠️',
'preco_extraordinaria' => '✨', 'preco_extraordinaria' => '✨',
'preco_termo' => '📄',
'notificacao_whatsapp' => '💬', 'notificacao_whatsapp' => '💬',
'notificacao_email' => '✉️', 'notificacao_email' => '✉️',
'empresa_nome' => '🏢' 'empresa_nome' => '🏢'

View File

@@ -244,6 +244,37 @@ class Consolidacao < ApplicationRecord
nome nome
end end
# ── Entrega de termo ────────────────────────────────────────
# Lança um LOTE de N "entregas de termo" — entregas sem NF nem código de rastreio
# 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:)
nome = motorista.to_s.strip
raise ArgumentError, 'Informe o motorista do termo.' if nome.blank?
qtd = quantidade.to_i
raise ArgumentError, 'Informe uma quantidade válida de termos.' if qtd < 1
transaction do
consolidacao_motoristas.find_or_create_by!(motorista_nome: nome)
consolidacao_entregas.create!(
tracking_id: "TERMO-#{SecureRandom.uuid}", # sintético: não há Entrega/NF
tipo: 'termo',
motorista_nome: nome,
quantidade: qtd,
valor_aplicado: qtd * ConsolidacaoEntrega.valor_para('termo'), # total do lote
manual: true,
created_by: user&.id
)
recalcular_motorista!(nome)
end
nome
end
# Recalcula o valor de um motorista (soma dos pilares, desconto subtrai) e o # Recalcula o valor de um motorista (soma dos pilares, desconto subtrai) e o
# total da consolidação. Fonte única usada pelo wizard e pelos apontamentos. # total da consolidação. Fonte única usada pelo wizard e pelos apontamentos.
def recalcular_motorista!(nome) def recalcular_motorista!(nome)

View File

@@ -14,7 +14,8 @@ class ConsolidacaoEntrega < ApplicationRecord
retirada: 1, retirada: 1,
bonus: 2, bonus: 2,
desconto: 3, desconto: 3,
extraordinaria: 4 extraordinaria: 4,
termo: 5
} }
TIPO_CORES = { TIPO_CORES = {
@@ -22,7 +23,8 @@ class ConsolidacaoEntrega < ApplicationRecord
'retirada' => { bg: 'bg-orange-800', text: 'text-white', label: 'Retirada' }, 'retirada' => { bg: 'bg-orange-800', text: 'text-white', label: 'Retirada' },
'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' }
}.freeze }.freeze
# Uma entrega pode ter vários pilares (Normal + Bônus + Retirada…), # Uma entrega pode ter vários pilares (Normal + Bônus + Retirada…),
@@ -31,6 +33,7 @@ class ConsolidacaoEntrega < ApplicationRecord
validates :motorista_nome, presence: true validates :motorista_nome, presence: true
validates :tipo, presence: true validates :tipo, presence: true
validates :valor_aplicado, presence: true, numericality: { greater_than_or_equal_to: 0 } validates :valor_aplicado, presence: true, numericality: { greater_than_or_equal_to: 0 }
validates :quantidade, numericality: { greater_than: 0 }
# Busca a entrega original (leitura do banco existente) # Busca a entrega original (leitura do banco existente)
def entrega_original def entrega_original
@@ -50,6 +53,7 @@ class ConsolidacaoEntrega < ApplicationRecord
when 'bonus' then Configuracao.preco_bonus when 'bonus' then Configuracao.preco_bonus
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
else 0 else 0
end end
end end

View File

@@ -46,7 +46,9 @@ module Pdf
secao "PROVENTOS E DESCONTOS" secao "PROVENTOS E DESCONTOS"
resumo = @entregas.group(:tipo).count # `quantidade` (default 1) conta as entregas de termo lançadas em lote;
# para os demais pilares equivale ao número de linhas.
resumo = @entregas.group(:tipo).sum(:quantidade)
# Lista os quatro pilares sempre (Normal, Retirada, Bônus, Desconto), # Lista os quatro pilares sempre (Normal, Retirada, Bônus, Desconto),
# mesmo zerados, para o motorista ver o descritivo completo. # mesmo zerados, para o motorista ver o descritivo completo.
por_tipo = ConsolidacaoEntrega::TIPO_CORES.map do |tipo, cfg| por_tipo = ConsolidacaoEntrega::TIPO_CORES.map do |tipo, cfg|

View File

@@ -70,7 +70,8 @@ module Pdf
def tabela_por_tipo def tabela_por_tipo
soma = @entregas.reorder(nil).group(:tipo).sum(:valor_aplicado) soma = @entregas.reorder(nil).group(:tipo).sum(:valor_aplicado)
qtd = @entregas.reorder(nil).group(:tipo).count # `quantidade` (default 1) conta os termos lançados em lote como N entregas.
qtd = @entregas.reorder(nil).group(:tipo).sum(:quantidade)
linhas = [['Tipo', 'Quantidade', 'Valor']] linhas = [['Tipo', 'Quantidade', 'Valor']]
ConsolidacaoEntrega::TIPO_CORES.each do |tipo, cfg| ConsolidacaoEntrega::TIPO_CORES.each do |tipo, cfg|

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.tipo_cor[:label], e.termo? && 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
@@ -67,7 +67,8 @@ module Pdf
secao "RESUMO" secao "RESUMO"
# reorder(nil): remove o .order(:created_at) da relação — senão o Postgres # reorder(nil): remove o .order(:created_at) da relação — senão o Postgres
# recusa "GROUP BY tipo ORDER BY created_at" (PG::GroupingError → 500). # recusa "GROUP BY tipo ORDER BY created_at" (PG::GroupingError → 500).
resumo = @entregas.reorder(nil).group(:tipo).count # sum(:quantidade) (default 1) conta os termos lançados em lote como N entregas.
resumo = @entregas.reorder(nil).group(:tipo).sum(:quantidade)
total = @entregas.sum { |e| e.desconto? ? -e.valor_aplicado : e.valor_aplicado } total = @entregas.sum { |e| e.desconto? ? -e.valor_aplicado : e.valor_aplicado }
resumo_linhas = [['Tipo', 'Quantidade']] resumo_linhas = [['Tipo', 'Quantidade']]

View File

@@ -12,13 +12,14 @@
</div> </div>
<%# Resumo por tipo %> <%# Resumo por tipo %>
<div class="grid grid-cols-2 md:grid-cols-5 gap-3 mb-6"> <div class="grid grid-cols-2 md:grid-cols-6 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']
].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>
@@ -66,7 +67,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] %> <%= c.tipo_cor[:label] %><% if c.termo? && 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

@@ -197,6 +197,61 @@
</div> </div>
</div> </div>
</div> </div>
<%# Entrega de termo — entrega SEM nota fiscal nem código; só motorista + quantidade. %>
<div data-controller="apontamento-termo"
data-apontamento-termo-apontar-termo-url-value="<%= apontar_termo_consolidacao_consolidacao_entregas_path(@consolidacao) %>"
data-apontamento-termo-motorista-value="<%= @motorista %>"
class="mb-4">
<button type="button" data-action="apontamento-termo#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-blue-500/60 hover:border-blue-500 font-semibold px-4 py-3 rounded-lg min-h-[48px]">
Entrega de termo
<span class="text-gray-500 text-xs font-normal hidden md:inline">(sem nota/código — só quantidade)</span>
</button>
<%# Modal %>
<div data-apontamento-termo-target="modal" data-action="click->apontamento-termo#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">📄 Entrega de termo</h2>
<button type="button" data-action="apontamento-termo#fechar"
class="text-gray-500 hover:text-white text-2xl leading-none">✕</button>
</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 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 %>
<div class="flex items-center justify-between 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-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>
<%# Stepper de quantidade %>
<label class="block text-gray-400 text-sm mb-2">Quantidade de termos</label>
<div class="flex items-center justify-center gap-3 mb-5">
<button type="button" data-action="apontamento-termo#menos"
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="number" inputmode="numeric" min="1" value="1" readonly
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#mais"
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>
</div>
<button type="button" data-action="apontamento-termo#adicionar"
class="w-full bg-green-600 hover:bg-green-500 text-white font-bold py-3.5 rounded-lg min-h-[48px]">
Adicionar termos ✓
</button>
</div>
</div>
</div>
<% end %> <% end %>
<div class="grid grid-cols-1 lg:grid-cols-4 gap-4"> <div class="grid grid-cols-1 lg:grid-cols-4 gap-4">

View File

@@ -159,13 +159,14 @@
</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| %>
<% entrega = pilares.first.entrega_original %> <% eh_termo = pilares.first.termo? %>
<% 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">NF <%= nf_exibida.presence || '—' %></p> <p class="text-white font-bold"><%= eh_termo ? '📄 Entrega de termo' : "NF #{nf_exibida.presence || '—'}" %></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 %>
@@ -174,7 +175,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] %></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.quantidade > 1 %> ×<%= p.quantidade %><% end %></span>
<% end %> <% end %>
</div> </div>
</td> </td>
@@ -184,7 +185,7 @@
<%= button_to '✕ Remover', <%= button_to '✕ Remover',
remover_apontamento_consolidacao_consolidacao_entregas_path(@consolidacao, tracking_id: pilares.first.tracking_id, motorista: motorista), remover_apontamento_consolidacao_consolidacao_entregas_path(@consolidacao, tracking_id: pilares.first.tracking_id, motorista: motorista),
method: :delete, method: :delete,
form: { data: { turbo_confirm: "Remover o apontamento da NF #{nf_exibida.presence || '—'} de #{motorista}?" } }, form: { data: { turbo_confirm: eh_termo ? "Remover a entrega de termo de #{motorista}?" : "Remover o apontamento da NF #{nf_exibida.presence || '—'} de #{motorista}?" } },
class: 'text-gray-400 hover:text-red-400 border border-[#2a2a2a] hover:border-red-500/50 px-3 py-2 rounded-lg text-xs min-h-[40px]' %> class: 'text-gray-400 hover:text-red-400 border border-[#2a2a2a] hover:border-red-500/50 px-3 py-2 rounded-lg text-xs min-h-[40px]' %>
</td> </td>
<% end %> <% end %>

View File

@@ -59,6 +59,7 @@ Rails.application.routes.draw do
get :buscar_nf # JSON — busca a NF na SimpleRoute (apontamento manual) get :buscar_nf # JSON — busca a NF na SimpleRoute (apontamento manual)
post :apontar # cria apontamento manual a partir da NF post :apontar # cria apontamento manual a partir da NF
post :apontar_manual # apontamento 100% manual (sem rastreio) post :apontar_manual # apontamento 100% manual (sem rastreio)
post :apontar_termo # lança N entregas de termo (sem NF/código)
delete :remover_apontamento # remove um apontamento manual (por id) delete :remover_apontamento # remove um apontamento manual (por id)
end end
end end

View File

@@ -0,0 +1,9 @@
# Adiciona `quantidade` aos pilares da consolidação. Usado pelas "entregas de
# termo" (lote de N termos lançado numa única linha). Linhas existentes/demais
# pilares ficam com quantidade = 1 e seguem inalteradas — o valor_aplicado já
# guarda o total da linha, então as somas existentes continuam corretas.
class AddQuantidadeToConsolidacaoEntregas < ActiveRecord::Migration[7.1]
def change
add_column :consolidacao_entregas, :quantidade, :integer, null: false, default: 1
end
end

View File

@@ -0,0 +1,18 @@
# Cria a configuração de preço "Entrega de Termo" 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 AddPrecoTermoConfiguracao < ActiveRecord::Migration[7.1]
def up
return if Configuracao.exists?(chave: 'preco_termo')
Configuracao.create!(
chave: 'preco_termo',
valor: '15.00',
descricao: 'Valor pago por entrega de termo (R$)'
)
end
def down
Configuracao.where(chave: 'preco_termo').delete_all
end
end

View File

@@ -29,6 +29,7 @@ configs = [
{ chave: 'preco_bonus', valor: '10.00', descricao: 'Valor de bônus por meta/critério (R$)' }, { chave: 'preco_bonus', valor: '10.00', descricao: 'Valor de bônus por meta/critério (R$)' },
{ 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: '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' },