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:
BIN
Imagens para implantação /Imagem colada (20).png
Normal file
BIN
Imagens para implantação /Imagem colada (20).png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 42 KiB |
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
78
app/javascript/controllers/apontamento_termo_controller.js
Normal file
78
app/javascript/controllers/apontamento_termo_controller.js
Normal 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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' => '🏢'
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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|
|
||||||
|
|||||||
@@ -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|
|
||||||
|
|||||||
@@ -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']]
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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 %>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
18
db/migrate/20260626000002_add_preco_termo_configuracao.rb
Normal file
18
db/migrate/20260626000002_add_preco_termo_configuracao.rb
Normal 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
|
||||||
@@ -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' },
|
||||||
|
|||||||
Reference in New Issue
Block a user