diff --git a/Imagens para implantação /Imagem colada (20).png b/Imagens para implantação /Imagem colada (20).png new file mode 100644 index 0000000..a1078bd Binary files /dev/null and b/Imagens para implantação /Imagem colada (20).png differ diff --git a/app/controllers/consolidacao_entregas_controller.rb b/app/controllers/consolidacao_entregas_controller.rb index 9197556..976d5be 100644 --- a/app/controllers/consolidacao_entregas_controller.rb +++ b/app/controllers/consolidacao_entregas_controller.rb @@ -159,8 +159,9 @@ class ConsolidacaoEntregasController < ApplicationController todas = @consolidacao.consolidacao_entregas.where(motorista_nome: @motorista) - # Agregados calculados sobre o conjunto COMPLETO (não só a página exibida) - @resumo = todas.group(:tipo).count + # Agregados calculados sobre o conjunto COMPLETO (não só a página exibida). + # 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| c.tipo == 'desconto' ? -c.valor_aplicado : c.valor_aplicado end @@ -255,6 +256,31 @@ class ConsolidacaoEntregasController < ApplicationController render_apontamento_erro(e.message) 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 # 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 diff --git a/app/controllers/consolidacoes_controller.rb b/app/controllers/consolidacoes_controller.rb index c640bcd..e215600 100644 --- a/app/controllers/consolidacoes_controller.rb +++ b/app/controllers/consolidacoes_controller.rb @@ -353,7 +353,8 @@ class ConsolidacoesController < ApplicationController authorize @consolidacao, :show? @motorista = params[: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 } render layout: false # renderiza dentro do modal (Turbo Frame) end diff --git a/app/javascript/controllers/apontamento_termo_controller.js b/app/javascript/controllers/apontamento_termo_controller.js new file mode 100644 index 0000000..aea439d --- /dev/null +++ b/app/javascript/controllers/apontamento_termo_controller.js @@ -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") + } +} diff --git a/app/models/configuracao.rb b/app/models/configuracao.rb index 22d1e51..56ae369 100644 --- a/app/models/configuracao.rb +++ b/app/models/configuracao.rb @@ -9,6 +9,7 @@ class Configuracao < ApplicationRecord preco_bonus preco_desconto preco_extraordinaria + preco_termo notificacao_whatsapp notificacao_email empresa_nome @@ -21,6 +22,7 @@ class Configuracao < ApplicationRecord preco_bonus preco_desconto preco_extraordinaria + preco_termo ].freeze validates :chave, presence: true, inclusion: { in: CHAVES }, uniqueness: true @@ -52,6 +54,10 @@ class Configuracao < ApplicationRecord valor('preco_extraordinaria').to_f end + def self.preco_termo + valor('preco_termo').to_f + end + # Mapa usado pelo DashboardController (Fase 3) def self.mapa_de_precos { @@ -59,7 +65,8 @@ class Configuracao < ApplicationRecord retirada: preco_retirada, bonus: preco_bonus, desconto: preco_desconto, - extraordinaria: preco_extraordinaria + extraordinaria: preco_extraordinaria, + termo: preco_termo } end @@ -69,6 +76,7 @@ class Configuracao < ApplicationRecord 'preco_bonus' => 'Bônus', 'preco_desconto' => 'Desconto', 'preco_extraordinaria' => 'Entrega Extraordinária', + 'preco_termo' => 'Entrega de Termo', 'notificacao_whatsapp' => 'Notificação WhatsApp', 'notificacao_email' => 'Notificação E-mail', 'empresa_nome' => 'Nome da Empresa' @@ -80,6 +88,7 @@ class Configuracao < ApplicationRecord 'preco_bonus' => '⭐', 'preco_desconto' => '⚠️', 'preco_extraordinaria' => '✨', + 'preco_termo' => '📄', 'notificacao_whatsapp' => '💬', 'notificacao_email' => '✉️', 'empresa_nome' => '🏢' diff --git a/app/models/consolidacao.rb b/app/models/consolidacao.rb index f80dbbf..45b3a15 100644 --- a/app/models/consolidacao.rb +++ b/app/models/consolidacao.rb @@ -244,6 +244,37 @@ class Consolidacao < ApplicationRecord nome 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 # total da consolidação. Fonte única usada pelo wizard e pelos apontamentos. def recalcular_motorista!(nome) diff --git a/app/models/consolidacao_entrega.rb b/app/models/consolidacao_entrega.rb index 980e58e..df93357 100644 --- a/app/models/consolidacao_entrega.rb +++ b/app/models/consolidacao_entrega.rb @@ -14,7 +14,8 @@ class ConsolidacaoEntrega < ApplicationRecord retirada: 1, bonus: 2, desconto: 3, - extraordinaria: 4 + extraordinaria: 4, + termo: 5 } TIPO_CORES = { @@ -22,7 +23,8 @@ class ConsolidacaoEntrega < ApplicationRecord 'retirada' => { bg: 'bg-orange-800', text: 'text-white', label: 'Retirada' }, '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' } + '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 # Uma entrega pode ter vários pilares (Normal + Bônus + Retirada…), @@ -31,6 +33,7 @@ class ConsolidacaoEntrega < ApplicationRecord validates :motorista_nome, presence: true validates :tipo, presence: true 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) def entrega_original @@ -50,6 +53,7 @@ class ConsolidacaoEntrega < ApplicationRecord when 'bonus' then Configuracao.preco_bonus when 'desconto' then Configuracao.preco_desconto when 'extraordinaria' then Configuracao.preco_extraordinaria + when 'termo' then Configuracao.preco_termo else 0 end end diff --git a/app/services/pdf/extrato_pdf.rb b/app/services/pdf/extrato_pdf.rb index 54a7d07..0bbee3d 100644 --- a/app/services/pdf/extrato_pdf.rb +++ b/app/services/pdf/extrato_pdf.rb @@ -46,7 +46,9 @@ module Pdf 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), # mesmo zerados, para o motorista ver o descritivo completo. por_tipo = ConsolidacaoEntrega::TIPO_CORES.map do |tipo, cfg| diff --git a/app/services/pdf/relatorio_financeiro_consolidacao_pdf.rb b/app/services/pdf/relatorio_financeiro_consolidacao_pdf.rb index 60a8f47..69428e2 100644 --- a/app/services/pdf/relatorio_financeiro_consolidacao_pdf.rb +++ b/app/services/pdf/relatorio_financeiro_consolidacao_pdf.rb @@ -70,7 +70,8 @@ module Pdf def tabela_por_tipo 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']] ConsolidacaoEntrega::TIPO_CORES.each do |tipo, cfg| diff --git a/app/services/pdf/relatorio_motorista_pdf.rb b/app/services/pdf/relatorio_motorista_pdf.rb index 9708d32..afd5b71 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.tipo_cor[:label], + e.termo? && e.quantidade > 1 ? "#{e.tipo_cor[:label]} ×#{e.quantidade}" : e.tipo_cor[:label], (e.desconto? ? '-' : '') + moeda(e.valor_aplicado) ] end @@ -67,7 +67,8 @@ module Pdf secao "RESUMO" # 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). - 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 } resumo_linhas = [['Tipo', 'Quantidade']] diff --git a/app/views/consolidacao_entregas/revisar.html.erb b/app/views/consolidacao_entregas/revisar.html.erb index 52bd067..fd3161a 100644 --- a/app/views/consolidacao_entregas/revisar.html.erb +++ b/app/views/consolidacao_entregas/revisar.html.erb @@ -12,13 +12,14 @@ <%# Resumo por tipo %> -
<%= @resumo[tipo] || 0 %>
@@ -66,7 +67,7 @@Entrega sem nota fiscal nem código de rastreio. Informe quantos termos foram entregues.
+ + + + <%# Motorista (contexto da tela) + preço unitário %> +Motorista
+🚚 <%= @motorista %>
+Preço por termo
+<%= moeda(Configuracao.preco_termo) %>
+NF <%= nf_exibida.presence || '—' %>
+<%= eh_termo ? '📄 Entrega de termo' : "NF #{nf_exibida.presence || '—'}" %>
<%= motorista %><% if entrega&.vehicle.present? %> · <%= entrega.vehicle %><% end %> <% if entrega.nil? && obs_manual.present? %> · <%= obs_manual %><% end %> @@ -174,7 +175,7 @@