Implantação da função de controle de consolidaçoes pagas e pendentes
This commit is contained in:
@@ -4,8 +4,12 @@ class ConsolidacoesController < ApplicationController
|
||||
|
||||
before_action :set_consolidacao,
|
||||
only: %i[show edit update destroy finalizar arquivar reativar wizard
|
||||
registrar_pagamento cancelar_pagamento
|
||||
preview_holerite gerar_pdf_relatorio gerar_pdf_holerite]
|
||||
|
||||
# Formas de pagamento aceitas (o select da UI usa esta mesma lista).
|
||||
FORMAS_PAGAMENTO = %w[pix transferencia dinheiro].freeze
|
||||
|
||||
# GET /consolidacoes — lista com filtros
|
||||
def index
|
||||
authorize Consolidacao
|
||||
@@ -27,6 +31,10 @@ class ConsolidacoesController < ApplicationController
|
||||
if params[:vehicle].present?
|
||||
@consolidacoes = @consolidacoes.where('vehicle_ids @> ?', [params[:vehicle]].to_json)
|
||||
end
|
||||
|
||||
if %w[pago pendente parcial].include?(params[:pagamento])
|
||||
@consolidacoes = @consolidacoes.public_send("pagamento_#{params[:pagamento]}")
|
||||
end
|
||||
end
|
||||
|
||||
# GET /consolidacoes/new
|
||||
@@ -144,6 +152,37 @@ class ConsolidacoesController < ApplicationController
|
||||
redirect_to arquivadas_consolidacoes_path, notice: 'Consolidação excluída permanentemente.'
|
||||
end
|
||||
|
||||
# POST /consolidacoes/:id/registrar_pagamento
|
||||
# Marca 1 motorista (param consolidacao_motorista_id) ou TODOS os pendentes.
|
||||
def registrar_pagamento
|
||||
authorize @consolidacao, :registrar_pagamento?
|
||||
return pagamento_indisponivel unless @consolidacao.finalizada?
|
||||
|
||||
forma = params[:forma_pagamento].presence_in(FORMAS_PAGAMENTO) || 'pix'
|
||||
alvos = motoristas_alvo.select { |cm| !cm.pago? }
|
||||
|
||||
alvos.each do |cm|
|
||||
cm.marcar_pago!(current_user, forma: forma)
|
||||
NotificacaoService.notificar_pagamento(@consolidacao, cm)
|
||||
end
|
||||
|
||||
auditar!(:registrar_pagamento, @consolidacao,
|
||||
dados_novos: { motoristas: alvos.map(&:motorista_nome), forma: forma })
|
||||
redirect_to @consolidacao, notice: pagamento_notice(alvos.size, :pago)
|
||||
end
|
||||
|
||||
# POST /consolidacoes/:id/cancelar_pagamento — estorna 1 motorista ou TODOS.
|
||||
def cancelar_pagamento
|
||||
authorize @consolidacao, :cancelar_pagamento?
|
||||
|
||||
alvos = motoristas_alvo.select(&:pago?)
|
||||
alvos.each { |cm| cm.cancelar_pagamento!(current_user) }
|
||||
|
||||
auditar!(:cancelar_pagamento, @consolidacao,
|
||||
dados_novos: { motoristas: alvos.map(&:motorista_nome) })
|
||||
redirect_to @consolidacao, notice: pagamento_notice(alvos.size, :estornado)
|
||||
end
|
||||
|
||||
# ── FASE 7 — PDFs ────────────────────────────────────────────
|
||||
|
||||
# GET /consolidacoes/:id/gerar_pdf_relatorio?motorista=X
|
||||
@@ -166,9 +205,11 @@ class ConsolidacoesController < ApplicationController
|
||||
motorista = params[:motorista]
|
||||
user_mot = User.motorista.find_by('LOWER(nome) = ?', motorista.to_s.downcase)
|
||||
user_mot&.regenerar_login_token! if user_mot&.login_token.blank?
|
||||
cm = @consolidacao.consolidacao_motoristas.find_by(motorista_nome: motorista)
|
||||
|
||||
pdf = Pdf::HoleritePdf.new(consolidacao: @consolidacao, motorista: motorista,
|
||||
user_motorista: user_mot, app_host: ENV['APP_HOST'])
|
||||
user_motorista: user_mot, app_host: ENV['APP_HOST'],
|
||||
pago_em: cm&.pago_em, forma_pagamento: cm&.forma_pagamento)
|
||||
auditar!(:editar, @consolidacao, dados_novos: { pdf: 'holerite', motorista: motorista })
|
||||
|
||||
send_data pdf.render,
|
||||
@@ -204,6 +245,26 @@ class ConsolidacoesController < ApplicationController
|
||||
@consolidacao = Consolidacao.find(params[:id])
|
||||
end
|
||||
|
||||
# Um motorista específico (consolidacao_motorista_id) ou todos da consolidação.
|
||||
def motoristas_alvo
|
||||
if params[:consolidacao_motorista_id].present?
|
||||
[@consolidacao.consolidacao_motoristas.find(params[:consolidacao_motorista_id])]
|
||||
else
|
||||
@consolidacao.consolidacao_motoristas.to_a
|
||||
end
|
||||
end
|
||||
|
||||
def pagamento_indisponivel
|
||||
redirect_to @consolidacao, alert: 'Só é possível registrar pagamento de uma consolidação finalizada.'
|
||||
end
|
||||
|
||||
def pagamento_notice(qtd, acao)
|
||||
return 'Nenhuma alteração: motoristas já estavam no estado solicitado.' if qtd.zero?
|
||||
|
||||
verbo = acao == :pago ? 'marcado(s) como pago(s)' : 'com pagamento estornado'
|
||||
"#{qtd} motorista(s) #{verbo}."
|
||||
end
|
||||
|
||||
def preparar_form_new
|
||||
@motoristas = Entrega.motoristas_ativos
|
||||
@veiculos = Entrega.veiculos_unicos
|
||||
|
||||
@@ -79,10 +79,28 @@ class DashboardController < ApplicationController
|
||||
@consolidacoes_abertas = @consolidacoes_mes.where(status: :rascunho).count
|
||||
@consolidacoes_fechadas = @consolidacoes_mes.where(status: :finalizada).count
|
||||
|
||||
# Pagamentos: pago vs pendente entre as consolidações FINALIZADAS do período
|
||||
# (medido por motorista — fonte: consolidacao_motoristas).
|
||||
carregar_dados_pagamentos
|
||||
|
||||
# Histórico estimado mais recente
|
||||
@historico_recente = HistoricoEstimado.order(created_at: :desc).limit(5)
|
||||
end
|
||||
|
||||
def carregar_dados_pagamentos
|
||||
finalizadas = @consolidacoes_mes.where(status: :finalizada)
|
||||
cms = ConsolidacaoMotorista.where(consolidacao_id: finalizadas.select(:id))
|
||||
|
||||
@pag_pago_qtd = cms.pagos.count
|
||||
@pag_pend_qtd = cms.pendentes.count
|
||||
@pag_pago_valor = cms.pagos.sum(:valor_total)
|
||||
@pag_pend_valor = cms.pendentes.sum(:valor_total)
|
||||
|
||||
@tabela_pagamentos = finalizadas.recentes.map do |c|
|
||||
{ consolidacao: c, status: c.status_pagamento, pago: c.valor_pago, pendente: c.valor_pendente }
|
||||
end
|
||||
end
|
||||
|
||||
def build_grafico_diario(entregas, preco_entrega)
|
||||
dias = (@periodo_inicio..@periodo_fim).to_a
|
||||
|
||||
|
||||
@@ -27,6 +27,17 @@ module Motorista
|
||||
.where(motorista_nome: nome,
|
||||
consolidacoes: { status: :finalizada, deleted_at: nil })
|
||||
.sum(:valor_total)
|
||||
|
||||
# Aviso in-app: pagamentos confirmados nos últimos 7 dias (notificação na
|
||||
# própria página do motorista, além de WhatsApp/email).
|
||||
@pagamentos_recentes = ConsolidacaoMotorista
|
||||
.joins(:consolidacao)
|
||||
.where(motorista_nome: nome,
|
||||
consolidacoes: { status: :finalizada, deleted_at: nil })
|
||||
.where.not(pago_em: nil)
|
||||
.where('pago_em >= ?', 7.days.ago)
|
||||
.order(pago_em: :desc)
|
||||
.includes(:consolidacao)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -36,6 +36,20 @@ module ApplicationHelper
|
||||
end
|
||||
end
|
||||
|
||||
# Badge de status de pagamento de uma consolidação (derivado dos motoristas)
|
||||
def badge_pagamento(status)
|
||||
cfg = case status.to_s
|
||||
when 'pago' then { cor: 'bg-green-600 text-white', icone: '💰', label: 'Pago' }
|
||||
when 'parcial' then { cor: 'bg-yellow-500 text-black', icone: '⏳', label: 'Parcial' }
|
||||
when 'pendente' then { cor: 'bg-gray-700 text-gray-300', icone: '🕗', label: 'Pendente' }
|
||||
else { cor: 'bg-gray-800 text-gray-400', icone: '❓', label: status.to_s.humanize }
|
||||
end
|
||||
|
||||
tag.span class: "inline-flex items-center gap-1 px-2.5 py-1 rounded-full text-xs font-bold #{cfg[:cor]}" do
|
||||
"#{cfg[:icone]} #{cfg[:label]}"
|
||||
end
|
||||
end
|
||||
|
||||
# Badge de papel do usuário (admin/gerente/operador/motorista)
|
||||
def badge_role(role)
|
||||
cfg = case role.to_s
|
||||
|
||||
@@ -11,4 +11,15 @@ class ConsolidacaoMailer < ApplicationMailer
|
||||
mail to: user.email,
|
||||
subject: "💰 Seu pagamento foi fechado — #{consolidacao.nome}"
|
||||
end
|
||||
|
||||
def pagamento_efetuado(user, consolidacao, consolidacao_motorista)
|
||||
@user = user
|
||||
@consolidacao = consolidacao
|
||||
@valor = consolidacao_motorista.valor_total
|
||||
@forma = consolidacao_motorista.forma_pagamento
|
||||
@link_painel = "https://#{ENV.fetch('APP_HOST', 'localhost:3000')}/motorista"
|
||||
|
||||
mail to: user.email,
|
||||
subject: "✅ Seu pagamento foi efetuado — #{consolidacao.nome}"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -30,6 +30,24 @@ class Consolidacao < ApplicationRecord
|
||||
scope :recentes, -> { order(created_at: :desc) }
|
||||
scope :no_periodo, ->(i, f) { where('data_inicio >= ? AND data_fim <= ?', i, f) }
|
||||
|
||||
# ── Filtro por status de pagamento (derivado dos motoristas) ─
|
||||
# pago_em NÃO-nulo conta como pago; COUNT(pago_em) ignora os NULL.
|
||||
scope :pagamento_pago, -> {
|
||||
where(id: ConsolidacaoMotorista.group(:consolidacao_id)
|
||||
.having('COUNT(*) > 0 AND COUNT(*) = COUNT(pago_em)')
|
||||
.select(:consolidacao_id))
|
||||
}
|
||||
scope :pagamento_pendente, -> {
|
||||
where(id: ConsolidacaoMotorista.group(:consolidacao_id)
|
||||
.having('COUNT(pago_em) = 0')
|
||||
.select(:consolidacao_id))
|
||||
}
|
||||
scope :pagamento_parcial, -> {
|
||||
where(id: ConsolidacaoMotorista.group(:consolidacao_id)
|
||||
.having('COUNT(pago_em) > 0 AND COUNT(pago_em) < COUNT(*)')
|
||||
.select(:consolidacao_id))
|
||||
}
|
||||
|
||||
# ── Métodos ─────────────────────────────────────────────────
|
||||
|
||||
def arquivar!(user)
|
||||
@@ -53,6 +71,26 @@ class Consolidacao < ApplicationRecord
|
||||
)
|
||||
end
|
||||
|
||||
# ── Pagamento (status derivado dos motoristas da consolidação) ──
|
||||
# :pendente (nenhum pago) · :pago (todos pagos) · :parcial (alguns).
|
||||
def status_pagamento
|
||||
total = consolidacao_motoristas.count
|
||||
return :pendente if total.zero?
|
||||
|
||||
pagos = consolidacao_motoristas.pagos.count
|
||||
return :pendente if pagos.zero?
|
||||
|
||||
pagos >= total ? :pago : :parcial
|
||||
end
|
||||
|
||||
def valor_pago
|
||||
consolidacao_motoristas.pagos.sum(:valor_total)
|
||||
end
|
||||
|
||||
def valor_pendente
|
||||
consolidacao_motoristas.pendentes.sum(:valor_total)
|
||||
end
|
||||
|
||||
def finalizar!(user)
|
||||
return false unless todas_entregas_classificadas?
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ class ConsolidacaoMotorista < ApplicationRecord
|
||||
self.table_name = 'consolidacao_motoristas'
|
||||
|
||||
belongs_to :consolidacao
|
||||
belongs_to :pagador, class_name: 'User', foreign_key: :pago_por, optional: true
|
||||
has_many :consolidacao_entregas,
|
||||
->(cm) { where(motorista_nome: cm.motorista_nome) },
|
||||
foreign_key: :consolidacao_id,
|
||||
@@ -15,6 +16,22 @@ class ConsolidacaoMotorista < ApplicationRecord
|
||||
|
||||
before_save :recalcular_valor
|
||||
|
||||
# ── Pagamento ───────────────────────────────────────────────
|
||||
scope :pagos, -> { where.not(pago_em: nil) }
|
||||
scope :pendentes, -> { where(pago_em: nil) }
|
||||
|
||||
def pago?
|
||||
pago_em.present?
|
||||
end
|
||||
|
||||
def marcar_pago!(user, forma:)
|
||||
update!(pago_em: Time.current, pago_por: user&.id, forma_pagamento: forma)
|
||||
end
|
||||
|
||||
def cancelar_pagamento!(_user)
|
||||
update!(pago_em: nil, pago_por: nil, forma_pagamento: nil)
|
||||
end
|
||||
|
||||
def recalcular_valor
|
||||
# Fonte única de verdade: o valor_aplicado capturado no momento da
|
||||
# classificação (igual ao ConsolidacaoEntregasController#recalcular_motorista).
|
||||
|
||||
@@ -7,6 +7,10 @@ class ConsolidacaoPolicy < ApplicationPolicy
|
||||
def update? = user.pode_consolidar? && !record_finalizada_para_operador?
|
||||
def destroy? = user.admin? || user.gerente?
|
||||
|
||||
# Registrar/estornar pagamento: mesma régua de arquivar/excluir.
|
||||
def registrar_pagamento? = user.admin? || user.gerente?
|
||||
def cancelar_pagamento? = registrar_pagamento?
|
||||
|
||||
private
|
||||
|
||||
# Operador não mexe em consolidação finalizada
|
||||
|
||||
@@ -9,6 +9,11 @@ class NotificacaoService
|
||||
new(consolidacao).notificar_todos
|
||||
end
|
||||
|
||||
# Avisa UM motorista de que o pagamento dele foi efetuado.
|
||||
def self.notificar_pagamento(consolidacao, consolidacao_motorista)
|
||||
new(consolidacao).enviar_aviso_pagamento(consolidacao_motorista)
|
||||
end
|
||||
|
||||
def initialize(consolidacao)
|
||||
@consolidacao = consolidacao
|
||||
end
|
||||
@@ -23,6 +28,14 @@ class NotificacaoService
|
||||
end
|
||||
end
|
||||
|
||||
def enviar_aviso_pagamento(cm)
|
||||
user = User.motorista.ativos.find_by('LOWER(nome) = ?', cm.motorista_nome.downcase)
|
||||
return unless user
|
||||
|
||||
enviar_whatsapp_pagamento(user, cm) if whatsapp_ativo?
|
||||
enviar_email_pagamento(user, cm) if email_ativo? && user.email.present?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def whatsapp_ativo?
|
||||
@@ -65,4 +78,37 @@ class NotificacaoService
|
||||
rescue => e
|
||||
Rails.logger.error("[Notificacao] Falha email #{user.email}: #{e.message}")
|
||||
end
|
||||
|
||||
# ── Aviso de pagamento efetuado ─────────────────────────────
|
||||
def mensagem_pagamento(user, cm)
|
||||
valor = "R$ #{format('%.2f', cm.valor_total)}".gsub('.', ',')
|
||||
"Olá #{user.nome.split.first}! ✅\n" \
|
||||
"Seu pagamento foi efetuado.\n" \
|
||||
"📋 #{@consolidacao.nome}\n" \
|
||||
"💰 Valor: #{valor}\n" \
|
||||
"Acesse seu painel para conferir: " \
|
||||
"https://#{ENV.fetch('APP_HOST', 'localhost:3000')}/motorista"
|
||||
end
|
||||
|
||||
def enviar_whatsapp_pagamento(user, cm)
|
||||
return if user.telefone.blank?
|
||||
|
||||
require 'twilio-ruby'
|
||||
cliente = Twilio::REST::Client.new(ENV['TWILIO_ACCOUNT_SID'], ENV['TWILIO_AUTH_TOKEN'])
|
||||
cliente.messages.create(
|
||||
from: ENV['TWILIO_WHATSAPP_FROM'],
|
||||
to: "whatsapp:#{user.telefone}",
|
||||
body: mensagem_pagamento(user, cm)
|
||||
)
|
||||
Rails.logger.info("[Notificacao] WhatsApp pagamento enviado para #{user.nome}")
|
||||
rescue => e
|
||||
Rails.logger.error("[Notificacao] Falha WhatsApp pagamento #{user.nome}: #{e.message}")
|
||||
end
|
||||
|
||||
def enviar_email_pagamento(user, cm)
|
||||
ConsolidacaoMailer.pagamento_efetuado(user, @consolidacao, cm).deliver_later
|
||||
Rails.logger.info("[Notificacao] Email pagamento agendado para #{user.email}")
|
||||
rescue => e
|
||||
Rails.logger.error("[Notificacao] Falha email pagamento #{user.email}: #{e.message}")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -7,13 +7,19 @@ require 'rqrcode'
|
||||
|
||||
module Pdf
|
||||
class HoleritePdf < BasePdf
|
||||
def initialize(consolidacao:, motorista:, user_motorista: nil, app_host: nil)
|
||||
VERDE = '22C55E'
|
||||
CINZA_BG = 'E5E7EB'
|
||||
|
||||
def initialize(consolidacao:, motorista:, user_motorista: nil, app_host: nil,
|
||||
pago_em: nil, forma_pagamento: nil)
|
||||
super()
|
||||
@consolidacao = consolidacao
|
||||
@motorista = motorista
|
||||
@user_motorista = user_motorista
|
||||
@app_host = app_host || ENV.fetch('APP_HOST', 'localhost:3000')
|
||||
@entregas = consolidacao.consolidacao_entregas.where(motorista_nome: motorista)
|
||||
@consolidacao = consolidacao
|
||||
@motorista = motorista
|
||||
@user_motorista = user_motorista
|
||||
@app_host = app_host || ENV.fetch('APP_HOST', 'localhost:3000')
|
||||
@pago_em = pago_em
|
||||
@forma_pagamento = forma_pagamento
|
||||
@entregas = consolidacao.consolidacao_entregas.where(motorista_nome: motorista)
|
||||
end
|
||||
|
||||
private
|
||||
@@ -34,6 +40,8 @@ module Pdf
|
||||
t.columns(0).width = 120
|
||||
end
|
||||
|
||||
desenhar_selo_pagamento
|
||||
|
||||
secao "PROVENTOS E DESCONTOS"
|
||||
|
||||
resumo = @entregas.group(:tipo).count
|
||||
@@ -84,6 +92,26 @@ module Pdf
|
||||
assinaturas(motorista: @motorista)
|
||||
end
|
||||
|
||||
# Selo de pagamento: faixa verde "PAGO" (com data/forma) ou cinza "PENDENTE".
|
||||
def desenhar_selo_pagamento
|
||||
@pdf.move_down 10
|
||||
pago = @pago_em.present?
|
||||
cor = pago ? VERDE : CINZA_BG
|
||||
texto = if pago
|
||||
via = @forma_pagamento.present? ? " · #{@forma_pagamento.to_s.capitalize}" : ''
|
||||
"✓ PAGO em #{@pago_em.strftime('%d/%m/%Y')}#{via}"
|
||||
else
|
||||
"PAGAMENTO PENDENTE"
|
||||
end
|
||||
|
||||
@pdf.fill_color cor
|
||||
@pdf.fill_rounded_rectangle [0, @pdf.cursor], 250, 26, 5
|
||||
@pdf.fill_color(pago ? 'FFFFFF' : '374151')
|
||||
@pdf.text_box texto, at: [12, @pdf.cursor - 8], width: 238, size: 10, style: :bold
|
||||
@pdf.fill_color '000000'
|
||||
@pdf.move_down 34
|
||||
end
|
||||
|
||||
# Monta a URL respeitando o esquema do APP_HOST. Se vier sem http(s)://
|
||||
# (ex.: "100.75.222.23:3000") assume http — produção em IP:porta normalmente
|
||||
# não tem TLS. Defina APP_HOST com "https://" se o servidor usar HTTPS.
|
||||
|
||||
47
app/views/consolidacao_mailer/pagamento_efetuado.html.erb
Normal file
47
app/views/consolidacao_mailer/pagamento_efetuado.html.erb
Normal file
@@ -0,0 +1,47 @@
|
||||
<%# app/views/consolidacao_mailer/pagamento_efetuado.html.erb %>
|
||||
<div style="font-family: Arial, sans-serif; max-width: 520px; margin: 0 auto; background: #0a0a0a; border-radius: 12px; overflow: hidden;">
|
||||
|
||||
<div style="background: #0a0a0a; padding: 24px; border-left: 6px solid #f97316;">
|
||||
<h1 style="color: #ffffff; margin: 0; font-size: 20px;">REEM TRANSPORTE</h1>
|
||||
<p style="color: #f97316; margin: 4px 0 0; font-size: 13px;">Sistema de Logística</p>
|
||||
</div>
|
||||
|
||||
<div style="background: #ffffff; padding: 28px;">
|
||||
<h2 style="color: #0a0a0a; margin-top: 0;">Olá, <%= @user.nome.split.first %>! ✅</h2>
|
||||
|
||||
<p style="color: #374151; font-size: 15px; line-height: 1.6;">
|
||||
Seu pagamento de entregas foi <strong>efetuado</strong> pela empresa.
|
||||
</p>
|
||||
|
||||
<table style="width: 100%; margin: 20px 0; border-collapse: collapse;">
|
||||
<tr>
|
||||
<td style="background: #f3f4f6; padding: 10px 14px; font-weight: bold; color: #374151;">Período</td>
|
||||
<td style="padding: 10px 14px; color: #0a0a0a;"><%= @consolidacao.nome %></td>
|
||||
</tr>
|
||||
<% if @forma.present? %>
|
||||
<tr>
|
||||
<td style="background: #f3f4f6; padding: 10px 14px; font-weight: bold; color: #374151;">Forma</td>
|
||||
<td style="padding: 10px 14px; color: #0a0a0a;"><%= @forma.to_s.humanize %></td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</table>
|
||||
|
||||
<div style="background: #22c55e; border-radius: 10px; padding: 20px; text-align: center; margin: 20px 0;">
|
||||
<p style="color: rgba(0,0,0,0.6); margin: 0; font-size: 12px; font-weight: bold;">VALOR PAGO</p>
|
||||
<p style="color: #0a0a0a; margin: 6px 0 0; font-size: 32px; font-weight: 900;">
|
||||
R$ <%= format('%.2f', @valor).gsub('.', ',') %>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p style="text-align: center; margin: 24px 0;">
|
||||
<a href="<%= @link_painel %>"
|
||||
style="background: #0a0a0a; color: #f97316; text-decoration: none; padding: 14px 28px; border-radius: 8px; font-weight: bold; display: inline-block;">
|
||||
Ver meu painel e baixar holerite →
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p style="color: #9ca3af; font-size: 12px; text-align: center;">
|
||||
Entre com seu PIN de 4 números ou escaneie o QR Code do seu holerite.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
<%# ── Filtros ──────────────────────────────────────────── %>
|
||||
<%= form_with url: consolidacoes_path, method: :get,
|
||||
class: 'bg-[#1a1a1a] border border-[#2a2a2a] rounded-xl p-4 mb-6 grid grid-cols-1 md:grid-cols-5 gap-3' do |f| %>
|
||||
class: 'bg-[#1a1a1a] border border-[#2a2a2a] rounded-xl p-4 mb-6 grid grid-cols-1 md:grid-cols-6 gap-3' do |f| %>
|
||||
<%= f.text_field :nome, value: params[:nome], placeholder: 'Buscar por nome…',
|
||||
class: 'bg-[#0a0a0a] border border-[#2a2a2a] text-white rounded-lg px-3 py-2.5 focus:border-orange-500 focus:outline-none' %>
|
||||
|
||||
@@ -21,6 +21,10 @@
|
||||
options_for_select([['Todos os status', ''], ['📝 Rascunho', 'rascunho'], ['✅ Finalizada', 'finalizada']], params[:status]),
|
||||
{}, class: 'bg-[#0a0a0a] border border-[#2a2a2a] text-white rounded-lg px-3 py-2.5' %>
|
||||
|
||||
<%= f.select :pagamento,
|
||||
options_for_select([['💵 Pagamento (todos)', ''], ['💰 Pagas', 'pago'], ['⏳ Parciais', 'parcial'], ['🕗 Pendentes', 'pendente']], params[:pagamento]),
|
||||
{}, class: 'bg-[#0a0a0a] border border-[#2a2a2a] text-white rounded-lg px-3 py-2.5' %>
|
||||
|
||||
<%= f.date_field :inicio, value: params[:inicio],
|
||||
class: 'bg-[#0a0a0a] border border-[#2a2a2a] text-white rounded-lg px-3 py-2.5' %>
|
||||
|
||||
@@ -44,6 +48,7 @@
|
||||
<div class="flex items-center gap-3 mb-1">
|
||||
<h3 class="text-white font-bold text-lg"><%= c.nome %></h3>
|
||||
<%= badge_status(c.status) %>
|
||||
<% if c.finalizada? %><%= badge_pagamento(c.status_pagamento) %><% end %>
|
||||
</div>
|
||||
<p class="text-gray-400 text-sm">
|
||||
📅 <%= l c.data_inicio, format: :short %> → <%= l c.data_fim, format: :short %>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<%# app/views/consolidacoes/show.html.erb %>
|
||||
<% content_for :title, @consolidacao.nome %>
|
||||
<% formas_pagamento = [['PIX', 'pix'], ['Transferência', 'transferencia'], ['Dinheiro', 'dinheiro']] %>
|
||||
<% pode_pagar = @consolidacao.finalizada? && policy(@consolidacao).registrar_pagamento? %>
|
||||
|
||||
<div class="max-w-4xl mx-auto">
|
||||
|
||||
@@ -7,9 +9,10 @@
|
||||
<%= link_to '← Consolidações', consolidacoes_path, class: 'text-gray-500 hover:text-orange-500 text-sm' %>
|
||||
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-3 mt-2">
|
||||
<div>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex items-center gap-3 flex-wrap">
|
||||
<h1 class="text-2xl font-bold text-white"><%= @consolidacao.nome %></h1>
|
||||
<%= badge_status(@consolidacao.status) %>
|
||||
<% if @consolidacao.finalizada? %><%= badge_pagamento(@consolidacao.status_pagamento) %><% end %>
|
||||
</div>
|
||||
<p class="text-gray-400 text-sm mt-1">
|
||||
📅 <%= l @consolidacao.data_inicio, format: :short %> → <%= l @consolidacao.data_fim, format: :short %>
|
||||
@@ -31,6 +34,28 @@
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%# ── Pagamento da operação inteira ─────────────────────── %>
|
||||
<% if pode_pagar %>
|
||||
<div class="flex flex-col sm:flex-row sm:items-center gap-2 mt-4 bg-[#1a1a1a] border border-[#2a2a2a] rounded-xl p-4">
|
||||
<span class="text-gray-400 text-sm font-medium mr-1">💵 Operação inteira:</span>
|
||||
<% if @consolidacao.status_pagamento != :pago %>
|
||||
<%= form_with url: registrar_pagamento_consolidacao_path(@consolidacao), method: :post,
|
||||
data: { turbo_confirm: 'Marcar TODOS os motoristas pendentes desta consolidação como PAGOS?' },
|
||||
class: 'flex items-center gap-2' do |f| %>
|
||||
<%= f.select :forma_pagamento, options_for_select(formas_pagamento, 'pix'),
|
||||
{}, class: 'bg-[#0a0a0a] border border-[#2a2a2a] text-white rounded-lg px-3 py-2.5 text-sm' %>
|
||||
<%= f.submit '💰 Marcar tudo como pago',
|
||||
class: 'bg-green-600 hover:bg-green-700 text-white font-bold px-4 py-2.5 rounded-lg text-sm cursor-pointer min-h-[44px]' %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% if @consolidacao.status_pagamento != :pendente %>
|
||||
<%= button_to '↩ Estornar tudo', cancelar_pagamento_consolidacao_path(@consolidacao), method: :post,
|
||||
data: { turbo_confirm: 'Estornar o pagamento de TODOS os motoristas?' },
|
||||
class: 'text-gray-400 hover:text-white border border-[#2a2a2a] px-4 py-2.5 rounded-lg text-sm min-h-[44px]' %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<%# Total geral %>
|
||||
@@ -46,8 +71,39 @@
|
||||
<div class="bg-[#1a1a1a] border border-[#2a2a2a] rounded-xl p-5">
|
||||
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||
<div>
|
||||
<p class="text-white font-bold text-lg"><%= cm.motorista_nome %></p>
|
||||
<div class="flex items-center gap-2 flex-wrap">
|
||||
<p class="text-white font-bold text-lg"><%= cm.motorista_nome %></p>
|
||||
<% if @consolidacao.finalizada? %>
|
||||
<%= badge_pagamento(cm.pago? ? :pago : :pendente) %>
|
||||
<% end %>
|
||||
</div>
|
||||
<p class="text-orange-500 font-black text-xl"><%= moeda(cm.valor_total) %></p>
|
||||
<% if cm.pago? %>
|
||||
<p class="text-green-400 text-xs mt-1">
|
||||
💰 Pago em <%= l cm.pago_em, format: :short %>
|
||||
<% if cm.forma_pagamento.present? %>via <%= cm.forma_pagamento.humanize %><% end %>
|
||||
<% if cm.pagador.present? %>por <%= cm.pagador.nome_display %><% end %>
|
||||
</p>
|
||||
<% end %>
|
||||
|
||||
<% if pode_pagar %>
|
||||
<div class="mt-2">
|
||||
<% if cm.pago? %>
|
||||
<%= button_to '↩ Estornar pagamento', cancelar_pagamento_consolidacao_path(@consolidacao, consolidacao_motorista_id: cm.id), method: :post,
|
||||
data: { turbo_confirm: "Estornar o pagamento de #{cm.motorista_nome}?" },
|
||||
class: 'text-gray-400 hover:text-white border border-[#2a2a2a] px-3 py-2 rounded-lg text-xs min-h-[40px]' %>
|
||||
<% else %>
|
||||
<%= form_with url: registrar_pagamento_consolidacao_path(@consolidacao, consolidacao_motorista_id: cm.id), method: :post,
|
||||
data: { turbo_confirm: "Marcar #{cm.motorista_nome} como pago?" },
|
||||
class: 'flex items-center gap-2' do |f| %>
|
||||
<%= f.select :forma_pagamento, options_for_select(formas_pagamento, 'pix'),
|
||||
{}, class: 'bg-[#0a0a0a] border border-[#2a2a2a] text-white rounded-lg px-2 py-2 text-xs' %>
|
||||
<%= f.submit '💰 Marcar pago',
|
||||
class: 'bg-green-600 hover:bg-green-700 text-white font-bold px-3 py-2 rounded-lg text-xs cursor-pointer min-h-[40px]' %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<%# Preview abre modal %>
|
||||
|
||||
@@ -138,6 +138,79 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%# ── PAGAMENTOS (pago x pendente) ──────────────────────── %>
|
||||
<div class="grid grid-cols-1 xl:grid-cols-3 gap-4">
|
||||
|
||||
<%# Rosca pago vs a pagar (por valor) %>
|
||||
<div class="bg-[#1a1a1a] rounded-2xl border border-white/5 p-6">
|
||||
<h2 class="text-lg font-semibold text-white mb-1">💵 Pagamentos</h2>
|
||||
<p class="text-gray-400 text-sm mb-4">Consolidações finalizadas no período</p>
|
||||
|
||||
<% if (@pag_pago_qtd + @pag_pend_qtd).zero? %>
|
||||
<div class="text-center py-8 text-gray-500">
|
||||
<div class="text-3xl mb-2">🧾</div>
|
||||
<p class="text-sm">Nenhuma consolidação finalizada no período</p>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="relative h-44 mb-4">
|
||||
<canvas id="grafico-pagamentos"></canvas>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<span class="text-green-400">● Pago</span>
|
||||
<span class="text-white font-semibold">
|
||||
<%= moeda(@pag_pago_valor) %> · <%= @pag_pago_qtd %> motorista(s)
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<span class="text-gray-400">● A pagar</span>
|
||||
<span class="text-white font-semibold">
|
||||
<%= moeda(@pag_pend_valor) %> · <%= @pag_pend_qtd %> motorista(s)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<%# Tabela de consolidações com status de pagamento %>
|
||||
<div class="xl:col-span-2 bg-[#1a1a1a] rounded-2xl border border-white/5 p-6">
|
||||
<h2 class="text-lg font-semibold text-white mb-4">📋 Status de pagamento por consolidação</h2>
|
||||
|
||||
<% if @tabela_pagamentos.any? %>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr class="text-gray-500 text-left border-b border-white/5">
|
||||
<th class="py-2 pr-3 font-medium">Consolidação</th>
|
||||
<th class="py-2 px-3 font-medium">Status</th>
|
||||
<th class="py-2 px-3 font-medium text-right">Pago</th>
|
||||
<th class="py-2 pl-3 font-medium text-right">A pagar</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% @tabela_pagamentos.each do |linha| %>
|
||||
<tr class="border-b border-white/5">
|
||||
<td class="py-2.5 pr-3">
|
||||
<%= link_to linha[:consolidacao].nome, consolidacao_path(linha[:consolidacao]),
|
||||
class: 'text-white hover:text-orange-500 font-medium' %>
|
||||
</td>
|
||||
<td class="py-2.5 px-3"><%= badge_pagamento(linha[:status]) %></td>
|
||||
<td class="py-2.5 px-3 text-right text-green-400"><%= moeda(linha[:pago]) %></td>
|
||||
<td class="py-2.5 pl-3 text-right text-gray-400"><%= moeda(linha[:pendente]) %></td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="text-center py-8 text-gray-500">
|
||||
<div class="text-3xl mb-2">📋</div>
|
||||
<p class="text-sm">Nenhuma consolidação finalizada no período</p>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%# Por operação %>
|
||||
<% if @por_operacao.any? %>
|
||||
<div class="bg-[#1a1a1a] rounded-2xl border border-white/5 p-6">
|
||||
@@ -271,3 +344,47 @@
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
<%# Rosca de pagamentos (pago x a pagar, por valor) %>
|
||||
<script>
|
||||
(function () {
|
||||
const ctx = document.getElementById('grafico-pagamentos');
|
||||
if (!ctx || typeof Chart === 'undefined') return;
|
||||
Chart.getChart(ctx)?.destroy();
|
||||
|
||||
const pago = <%= @pag_pago_valor.to_f %>;
|
||||
const aPagar = <%= @pag_pend_valor.to_f %>;
|
||||
|
||||
new Chart(ctx, {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: ['Pago', 'A pagar'],
|
||||
datasets: [{
|
||||
data: [pago, aPagar],
|
||||
backgroundColor: ['#22c55e', '#374151'],
|
||||
borderColor: '#1a1a1a',
|
||||
borderWidth: 3
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
cutout: '62%',
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
tooltip: {
|
||||
backgroundColor: '#0a0a0a',
|
||||
borderColor: '#22c55e',
|
||||
borderWidth: 1,
|
||||
titleColor: '#9ca3af',
|
||||
bodyColor: '#ffffff',
|
||||
padding: 12,
|
||||
callbacks: {
|
||||
label: (c) => ` ${c.label}: R$ ${c.parsed.toFixed(2).replace('.', ',')}`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
@@ -6,6 +6,27 @@
|
||||
<h1 class="text-2xl font-bold text-white mb-1">Olá, <%= current_user.nome.split.first %>! 👋</h1>
|
||||
<p class="text-gray-400 mb-6">Aqui estão seus valores de entregas.</p>
|
||||
|
||||
<%# ── Aviso de pagamento recém-confirmado ───────────────── %>
|
||||
<% if @pagamentos_recentes.any? %>
|
||||
<div class="bg-green-600/15 border border-green-600/40 rounded-2xl p-5 mb-6">
|
||||
<p class="text-green-400 font-bold text-sm uppercase tracking-wide mb-2">✅ Pagamento confirmado!</p>
|
||||
<div class="space-y-2">
|
||||
<% @pagamentos_recentes.each do |cm| %>
|
||||
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-1">
|
||||
<p class="text-white text-sm">
|
||||
Você recebeu <span class="font-black text-green-400"><%= moeda(cm.valor_total) %></span>
|
||||
pela consolidação <span class="font-semibold"><%= cm.consolidacao.nome %></span>
|
||||
</p>
|
||||
<p class="text-gray-400 text-xs">
|
||||
💰 <%= l cm.pago_em.to_date, format: :short %>
|
||||
<% if cm.forma_pagamento.present? %>· <%= cm.forma_pagamento.humanize %><% end %>
|
||||
</p>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%# ── 2 Cards grandes ─────────────────────────────────── %>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-8">
|
||||
|
||||
@@ -41,14 +62,23 @@
|
||||
<div class="bg-[#1a1a1a] border border-[#2a2a2a] rounded-xl p-5">
|
||||
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-3">
|
||||
<div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex items-center gap-2 flex-wrap">
|
||||
<p class="text-white font-bold"><%= c.nome %></p>
|
||||
<%= badge_status(c.status) %>
|
||||
<% if c.finalizada? %>
|
||||
<%= badge_pagamento(cm&.pago? ? :pago : :pendente) %>
|
||||
<% end %>
|
||||
</div>
|
||||
<p class="text-gray-400 text-sm mt-1">
|
||||
📅 <%= l c.data_inicio, format: :short %> a <%= l c.data_fim, format: :short %>
|
||||
</p>
|
||||
<p class="text-orange-500 font-black text-xl mt-1"><%= moeda(cm&.valor_total || 0) %></p>
|
||||
<% if cm&.pago? %>
|
||||
<p class="text-green-400 text-xs mt-1">
|
||||
💰 Pago em <%= l cm.pago_em.to_date, format: :short %>
|
||||
<% if cm.forma_pagamento.present? %>via <%= cm.forma_pagamento.humanize %><% end %>
|
||||
</p>
|
||||
<% end %>
|
||||
</div>
|
||||
<% if c.finalizada? %>
|
||||
<%= link_to '⬇️ Baixar meu holerite',
|
||||
|
||||
@@ -34,7 +34,9 @@ Rails.application.routes.draw do
|
||||
get :wizard # Passo 1 — selecionar motorista
|
||||
post :finalizar
|
||||
post :arquivar
|
||||
post :reativar # Restaura uma consolidação arquivada
|
||||
post :reativar # Restaura uma consolidação arquivada
|
||||
post :registrar_pagamento # Marca 1 motorista (ou todos) como pago
|
||||
post :cancelar_pagamento # Estorna o pagamento de 1 motorista (ou todos)
|
||||
get :preview_holerite
|
||||
get :gerar_pdf_relatorio
|
||||
get :gerar_pdf_holerite
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
# Registra o pagamento de cada motorista dentro de uma consolidação:
|
||||
# QUEM pagou (pago_por), QUANDO (pago_em, NULL = pendente) e a FORMA (PIX etc.).
|
||||
class AddPagamentoToConsolidacaoMotoristas < ActiveRecord::Migration[7.1]
|
||||
def change
|
||||
add_column :consolidacao_motoristas, :pago_em, :datetime unless column_exists?(:consolidacao_motoristas, :pago_em)
|
||||
add_column :consolidacao_motoristas, :pago_por, :integer unless column_exists?(:consolidacao_motoristas, :pago_por)
|
||||
add_column :consolidacao_motoristas, :forma_pagamento, :string unless column_exists?(:consolidacao_motoristas, :forma_pagamento)
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user