Correção Dash board e implantação Relaório de ADM

This commit is contained in:
2026-06-22 17:11:53 -03:00
parent e23ca0f10d
commit 94d8420bbd
16 changed files with 453 additions and 39 deletions

View File

@@ -39,7 +39,8 @@ class ConsolidacaoEntregasController < ApplicationController
entrega_normal: Configuracao.preco_entrega, entrega_normal: Configuracao.preco_entrega,
retirada: Configuracao.preco_retirada, retirada: Configuracao.preco_retirada,
bonus: Configuracao.preco_bonus, bonus: Configuracao.preco_bonus,
desconto: Configuracao.preco_desconto desconto: Configuracao.preco_desconto,
extraordinaria: Configuracao.preco_extraordinaria
} }
end end
@@ -62,7 +63,7 @@ class ConsolidacaoEntregasController < ApplicationController
tracking_id: params[:tracking_id], tracking_id: params[:tracking_id],
motorista_nome: params[:motorista], motorista_nome: params[:motorista],
tipo: params[:tipo], tipo: params[:tipo],
valor_aplicado: valor_para_tipo(params[:tipo]), valor_aplicado: valor_classificacao(params[:tipo]),
created_by: current_user.id created_by: current_user.id
) )
ativo = true ativo = true
@@ -106,7 +107,7 @@ class ConsolidacaoEntregasController < ApplicationController
.destroy_all .destroy_all
else else
# ADICIONA o pilar às entregas (não remove os demais já marcados). # ADICIONA o pilar às entregas (não remove os demais já marcados).
valor = valor_para_tipo(tipo) valor = valor_classificacao(tipo)
tracking_ids.each do |tid| tracking_ids.each do |tid|
ce = @consolidacao.consolidacao_entregas.find_or_initialize_by(tracking_id: tid, tipo: tipo) ce = @consolidacao.consolidacao_entregas.find_or_initialize_by(tracking_id: tid, tipo: tipo)
ce.assign_attributes(motorista_nome: motorista, ce.assign_attributes(motorista_nome: motorista,
@@ -296,6 +297,18 @@ class ConsolidacaoEntregasController < ApplicationController
ConsolidacaoEntrega.valor_para(tipo) ConsolidacaoEntrega.valor_para(tipo)
end end
# Valor a aplicar na classificação. Para o pilar "extraordinária" o admin pode
# informar um valor customizado (params[:valor]) por ser uma exceção; se vier
# ausente/inválido, cai no preço padrão de Configurações. Demais pilares usam
# sempre o preço configurado.
def valor_classificacao(tipo)
return valor_para_tipo(tipo) unless tipo.to_s == 'extraordinaria'
valor = params[:valor].to_s.strip.tr(',', '.')
valor = BigDecimal(valor) rescue nil
valor.present? && valor >= 0 ? valor : Configuracao.preco_extraordinaria
end
def recalcular_motorista(nome) def recalcular_motorista(nome)
@consolidacao.recalcular_motorista!(nome) @consolidacao.recalcular_motorista!(nome)
end end

View File

@@ -5,7 +5,8 @@ class ConsolidacoesController < ApplicationController
before_action :set_consolidacao, before_action :set_consolidacao,
only: %i[show edit update destroy finalizar arquivar reativar wizard only: %i[show edit update destroy finalizar arquivar reativar wizard
registrar_pagamento cancelar_pagamento registrar_pagamento cancelar_pagamento
preview_holerite gerar_pdf_relatorio gerar_pdf_holerite] preview_holerite gerar_pdf_relatorio gerar_pdf_holerite
gerar_pdf_financeiro]
# Formas de pagamento aceitas (o select da UI usa esta mesma lista). # Formas de pagamento aceitas (o select da UI usa esta mesma lista).
FORMAS_PAGAMENTO = %w[pix transferencia dinheiro].freeze FORMAS_PAGAMENTO = %w[pix transferencia dinheiro].freeze
@@ -271,6 +272,19 @@ class ConsolidacoesController < ApplicationController
disposition: 'attachment' disposition: 'attachment'
end end
# GET /consolidacoes/:id/gerar_pdf_financeiro — relatório financeiro da consolidação
def gerar_pdf_financeiro
authorize @consolidacao, :show?
pdf = Pdf::RelatorioFinanceiroConsolidacaoPdf.new(consolidacao: @consolidacao)
auditar!(:editar, @consolidacao, dados_novos: { pdf: 'financeiro' })
send_data pdf.render,
filename: "financeiro_#{@consolidacao.nome.parameterize}_#{@consolidacao.id}.pdf",
type: 'application/pdf',
disposition: 'attachment'
end
# GET /consolidacoes/:id/gerar_pdf_holerite?motorista=X # GET /consolidacoes/:id/gerar_pdf_holerite?motorista=X
def gerar_pdf_holerite def gerar_pdf_holerite
autorizar_holerite!(params[:motorista]) autorizar_holerite!(params[:motorista])

View File

@@ -12,6 +12,44 @@ class DashboardController < ApplicationController
carregar_dados_dashboard carregar_dados_dashboard
end end
# GET /dashboard/relatorio_financeiro?inicio=&fim=&operacoes[]=
# Relatório financeiro do período (PDF) — mesmos números da tela.
def relatorio_financeiro
skip_authorization
return redirect_to(motorista_dashboard_path) if current_user.motorista?
@periodo_inicio, @periodo_fim = periodo_selecionado
carregar_dados_dashboard
op_label = @operacao_filtro.present? ? @operacao_filtro.map { |t| Operacao.label(t) }.join(', ') : 'Todas'
dados = {
custo_total: @fin_custo_total,
ticket_medio: @fin_ticket_medio,
pago: @fin_pago,
pendente: @fin_pendente,
por_operacao: @fin_por_operacao,
por_motorista: @fin_por_motorista,
por_tipo: @fin_por_tipo,
pagamentos_feitos: @pagamentos_feitos,
pagamentos_pendentes: @pagamentos_pendentes,
pag_pago_valor: @pag_pago_valor,
pag_pago_qtd: @pag_pago_qtd,
pag_pend_valor: @pag_pend_valor,
pag_pend_qtd: @pag_pend_qtd
}
pdf = Pdf::RelatorioFinanceiroPeriodoPdf.new(
periodo_inicio: @periodo_inicio, periodo_fim: @periodo_fim,
operacao_label: op_label, dados: dados
)
send_data pdf.render,
filename: "financeiro_#{@periodo_inicio.strftime('%Y%m%d')}_#{@periodo_fim.strftime('%Y%m%d')}.pdf",
type: 'application/pdf',
disposition: 'attachment'
end
private private
# Faixa de datas vinda do filtro de calendário (params inicio/fim). # Faixa de datas vinda do filtro de calendário (params inicio/fim).

View File

@@ -9,14 +9,15 @@ export default class extends Controller {
"linha", "checkbox", "selecionarTodos", "modoRemover", "rotuloAcao", "linha", "checkbox", "selecionarTodos", "modoRemover", "rotuloAcao",
"toggles", "barra", "percentual", "toggles", "barra", "percentual",
"contadorClassificadas", "contadorTotal", "contadorClassificadas", "contadorTotal",
"qtdNormal", "qtdRetirada", "qtdBonus", "qtdDesconto", "valorTotal" "qtdNormal", "qtdRetirada", "qtdBonus", "qtdDesconto", "qtdExtraordinaria", "valorTotal"
] ]
static values = { static values = {
url: String, // POST classificar url: String, // POST classificar
massaUrl: String, // POST classificar_em_massa massaUrl: String, // POST classificar_em_massa
motorista: String, motorista: String,
total: Number total: Number,
precoExtraordinaria: Number // preço padrão sugerido no prompt
} }
connect() { connect() {
@@ -34,12 +35,21 @@ export default class extends Controller {
const tracking = btn.dataset.tracking const tracking = btn.dataset.tracking
const tipo = btn.dataset.tipo const tipo = btn.dataset.tipo
// Pilar extraordinária: ao ATIVAR (botão ainda sem ring-2), pergunta o valor.
const ativando = !btn.classList.contains("ring-2")
let valor = null
if (tipo === "extraordinaria" && ativando) {
valor = this.pedirValor()
if (valor === null) return // admin cancelou
}
btn.disabled = true btn.disabled = true
try { try {
const resp = await this.post(this.urlValue, { const resp = await this.post(this.urlValue, {
tracking_id: tracking, tracking_id: tracking,
tipo: tipo, tipo: tipo,
motorista: this.motoristaValue motorista: this.motoristaValue,
valor: valor
}) })
// resp.ativo indica se o pilar ficou ligado ou desligado (toggle) // resp.ativo indica se o pilar ficou ligado ou desligado (toggle)
this.setToggle(tracking, tipo, resp.ativo) this.setToggle(tracking, tipo, resp.ativo)
@@ -58,11 +68,18 @@ export default class extends Controller {
const verbo = remover ? "REMOVER de TODAS as entregas" : "Marcar TODAS as entregas como" const verbo = remover ? "REMOVER de TODAS as entregas" : "Marcar TODAS as entregas como"
if (!confirm(`${verbo} ${this.labelTipo(tipo)}?`)) return if (!confirm(`${verbo} ${this.labelTipo(tipo)}?`)) return
let valor = null
if (tipo === "extraordinaria" && !remover) {
valor = this.pedirValor()
if (valor === null) return
}
const resp = await this.post(this.massaUrlValue, { const resp = await this.post(this.massaUrlValue, {
tipo: tipo, tipo: tipo,
motorista: this.motoristaValue, motorista: this.motoristaValue,
tracking_ids: [], // vazio = todas tracking_ids: [], // vazio = todas
acao: remover ? "remover" : "adicionar" acao: remover ? "remover" : "adicionar",
valor: valor
}) })
this.linhaTargets.forEach(l => this.setToggle(l.dataset.tracking, tipo, !remover)) this.linhaTargets.forEach(l => this.setToggle(l.dataset.tracking, tipo, !remover))
this.atualizarResumo(resp) this.atualizarResumo(resp)
@@ -97,11 +114,19 @@ export default class extends Controller {
} }
const remover = this.modoRemover const remover = this.modoRemover
let valor = null
if (tipo === "extraordinaria" && !remover) {
valor = this.pedirValor()
if (valor === null) return
}
const resp = await this.post(this.massaUrlValue, { const resp = await this.post(this.massaUrlValue, {
tipo: tipo, tipo: tipo,
motorista: this.motoristaValue, motorista: this.motoristaValue,
tracking_ids: ids, tracking_ids: ids,
acao: remover ? "remover" : "adicionar" acao: remover ? "remover" : "adicionar",
valor: valor
}) })
ids.forEach(id => this.setToggle(id, tipo, !remover)) ids.forEach(id => this.setToggle(id, tipo, !remover))
this.checkboxTargets.forEach(c => c.checked = false) this.checkboxTargets.forEach(c => c.checked = false)
@@ -150,6 +175,8 @@ export default class extends Controller {
this.qtdRetiradaTarget.textContent = porTipo["retirada"] || 0 this.qtdRetiradaTarget.textContent = porTipo["retirada"] || 0
this.qtdBonusTarget.textContent = porTipo["bonus"] || 0 this.qtdBonusTarget.textContent = porTipo["bonus"] || 0
this.qtdDescontoTarget.textContent = porTipo["desconto"] || 0 this.qtdDescontoTarget.textContent = porTipo["desconto"] || 0
if (this.hasQtdExtraordinariaTarget)
this.qtdExtraordinariaTarget.textContent = porTipo["extraordinaria"] || 0
const valor = dados.valor_total || 0 const valor = dados.valor_total || 0
this.valorTotalTarget.textContent = this.valorTotalTarget.textContent =
@@ -171,6 +198,21 @@ export default class extends Controller {
labelTipo(tipo) { labelTipo(tipo) {
return { entrega_normal: "Normal", retirada: "Retirada", return { entrega_normal: "Normal", retirada: "Retirada",
bonus: "Bônus", desconto: "Desconto" }[tipo] || tipo bonus: "Bônus", desconto: "Desconto",
extraordinaria: "Extraordinária" }[tipo] || tipo
}
// Pergunta o valor da entrega extraordinária (pré-preenchido com o padrão).
// Retorna o número (>= 0) ou null se o admin cancelar.
pedirValor() {
const padrao = (this.precoExtraordinariaValue || 0).toFixed(2).replace(".", ",")
const entrada = prompt("Valor desta entrega extraordinária (R$):", padrao)
if (entrada === null) return null
const num = parseFloat(entrada.replace(",", "."))
if (isNaN(num) || num < 0) {
alert("Valor inválido. Informe um número maior ou igual a zero.")
return null
}
return num
} }
} }

View File

@@ -8,6 +8,7 @@ class Configuracao < ApplicationRecord
preco_retirada preco_retirada
preco_bonus preco_bonus
preco_desconto preco_desconto
preco_extraordinaria
notificacao_whatsapp notificacao_whatsapp
notificacao_email notificacao_email
empresa_nome empresa_nome
@@ -19,6 +20,7 @@ class Configuracao < ApplicationRecord
preco_retirada preco_retirada
preco_bonus preco_bonus
preco_desconto preco_desconto
preco_extraordinaria
].freeze ].freeze
validates :chave, presence: true, inclusion: { in: CHAVES }, uniqueness: true validates :chave, presence: true, inclusion: { in: CHAVES }, uniqueness: true
@@ -46,13 +48,18 @@ class Configuracao < ApplicationRecord
valor('preco_desconto').to_f valor('preco_desconto').to_f
end end
def self.preco_extraordinaria
valor('preco_extraordinaria').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
{ {
entrega: preco_entrega, entrega: preco_entrega,
retirada: preco_retirada, retirada: preco_retirada,
bonus: preco_bonus, bonus: preco_bonus,
desconto: preco_desconto desconto: preco_desconto,
extraordinaria: preco_extraordinaria
} }
end end
@@ -61,6 +68,7 @@ class Configuracao < ApplicationRecord
'preco_retirada' => 'Retirada', 'preco_retirada' => 'Retirada',
'preco_bonus' => 'Bônus', 'preco_bonus' => 'Bônus',
'preco_desconto' => 'Desconto', 'preco_desconto' => 'Desconto',
'preco_extraordinaria' => 'Entrega Extraordinária',
'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'
@@ -71,6 +79,7 @@ class Configuracao < ApplicationRecord
'preco_retirada' => '📦', 'preco_retirada' => '📦',
'preco_bonus' => '⭐', 'preco_bonus' => '⭐',
'preco_desconto' => '⚠️', 'preco_desconto' => '⚠️',
'preco_extraordinaria' => '✨',
'notificacao_whatsapp' => '💬', 'notificacao_whatsapp' => '💬',
'notificacao_email' => '✉️', 'notificacao_email' => '✉️',
'empresa_nome' => '🏢' 'empresa_nome' => '🏢'

View File

@@ -13,14 +13,16 @@ class ConsolidacaoEntrega < ApplicationRecord
entrega_normal: 0, entrega_normal: 0,
retirada: 1, retirada: 1,
bonus: 2, bonus: 2,
desconto: 3 desconto: 3,
extraordinaria: 4
} }
TIPO_CORES = { TIPO_CORES = {
'entrega_normal' => { bg: 'bg-orange-500', text: 'text-white', label: 'Entrega Normal' }, 'entrega_normal' => { bg: 'bg-orange-500', text: 'text-white', label: 'Entrega Normal' },
'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' }
}.freeze }.freeze
# Uma entrega pode ter vários pilares (Normal + Bônus + Retirada…), # Uma entrega pode ter vários pilares (Normal + Bônus + Retirada…),
@@ -47,6 +49,7 @@ class ConsolidacaoEntrega < ApplicationRecord
when 'retirada' then Configuracao.preco_retirada when 'retirada' then Configuracao.preco_retirada
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
else 0 else 0
end end
end end

View File

@@ -0,0 +1,103 @@
# app/services/pdf/relatorio_financeiro_consolidacao_pdf.rb
#
# Relatório Financeiro de uma consolidação: resumo por motorista (pago x
# pendente), composição por tipo de lançamento e totais. Para o admin imprimir
# para conferência e histórico.
#
module Pdf
class RelatorioFinanceiroConsolidacaoPdf < BasePdf
VERDE = '22C55E'
def initialize(consolidacao:)
super()
@consolidacao = consolidacao
@motoristas = consolidacao.consolidacao_motoristas.order(:motorista_nome)
@entregas = consolidacao.consolidacao_entregas
end
private
def corpo
secao "RELATÓRIO FINANCEIRO — CONSOLIDAÇÃO"
@pdf.text "Consolidação: #{@consolidacao.nome}", size: 11, style: :bold
@pdf.text "Período: #{@consolidacao.data_inicio.strftime('%d/%m/%Y')} a #{@consolidacao.data_fim.strftime('%d/%m/%Y')}", size: 10
@pdf.fill_color CINZA
@pdf.text "Status: #{@consolidacao.status.humanize} · Pagamento: #{@consolidacao.status_pagamento.to_s.humanize}", size: 10
@pdf.fill_color '000000'
secao "POR MOTORISTA (#{@motoristas.size})"
tabela_motoristas
secao "COMPOSIÇÃO POR TIPO"
tabela_por_tipo
@pdf.start_new_page if @pdf.cursor < 140
secao "TOTAIS"
totais
end
def tabela_motoristas
if @motoristas.any?
linhas = [['#', 'Motorista', 'Pagamento', 'Forma', 'Valor']]
@motoristas.each_with_index do |cm, i|
pagamento = cm.pago? ? "Pago em #{cm.pago_em.strftime('%d/%m/%Y')}" : 'Pendente'
linhas << [
(i + 1).to_s,
cm.motorista_nome,
pagamento,
cm.forma_pagamento&.humanize || '—',
moeda(cm.valor_total)
]
end
@pdf.table(linhas, header: true, width: @pdf.bounds.width,
column_widths: { 0 => 22, 4 => 80 },
cell_style: { size: 8, padding: [4, 6] }) do |t|
t.row(0).background_color = PRETO
t.row(0).text_color = 'FFFFFF'
t.row(0).font_style = :bold
t.columns(4).align = :right
t.row(1..-1).borders = [:bottom]
t.row(1..-1).border_color = 'DDDDDD'
end
else
@pdf.fill_color CINZA
@pdf.text 'Nenhum motorista nesta consolidação.', size: 10
@pdf.fill_color '000000'
end
end
def tabela_por_tipo
soma = @entregas.reorder(nil).group(:tipo).sum(:valor_aplicado)
qtd = @entregas.reorder(nil).group(:tipo).count
linhas = [['Tipo', 'Quantidade', 'Valor']]
ConsolidacaoEntrega::TIPO_CORES.each do |tipo, cfg|
valor = soma[tipo].to_f
sinal = tipo == 'desconto' ? '-' : ''
linhas << [cfg[:label], (qtd[tipo] || 0).to_s, "#{sinal}#{moeda(valor)}"]
end
@pdf.table(linhas, width: 360, cell_style: { size: 9, padding: [4, 8] }) do |t|
t.row(0).background_color = LARANJA
t.row(0).text_color = '000000'
t.row(0).font_style = :bold
t.columns(2).align = :right
end
end
def totais
linhas = [
['Custo total', moeda(@consolidacao.valor_total)],
['Pago', moeda(@consolidacao.valor_pago)],
['A pagar', moeda(@consolidacao.valor_pendente)]
]
@pdf.table(linhas, width: 300, cell_style: { size: 11, padding: [6, 10] }) do |t|
t.columns(0).font_style = :bold
t.columns(1).align = :right
t.row(1).text_color = VERDE
end
end
end
end

View File

@@ -0,0 +1,154 @@
# app/services/pdf/relatorio_financeiro_periodo_pdf.rb
#
# Relatório Financeiro consolidado de um período: espelha a visão financeira do
# dashboard (KPIs, custo por operação/motorista/tipo e pagamentos) num PDF para
# o admin imprimir para conferência e histórico. Recebe os agregados já
# calculados pelo DashboardController (mesma fonte de números da tela).
#
module Pdf
class RelatorioFinanceiroPeriodoPdf < BasePdf
VERDE = '22C55E'
def initialize(periodo_inicio:, periodo_fim:, operacao_label:, dados:)
super()
@inicio = periodo_inicio
@fim = periodo_fim
@op = operacao_label
@d = dados
end
private
def corpo
secao "RELATÓRIO FINANCEIRO — PERÍODO"
@pdf.text "Período: #{@inicio.strftime('%d/%m/%Y')} a #{@fim.strftime('%d/%m/%Y')}", size: 11, style: :bold
@pdf.fill_color CINZA
@pdf.text "Operação: #{@op}", size: 10
@pdf.fill_color '000000'
secao "INDICADORES"
kpis
secao "CUSTO POR OPERAÇÃO"
tabela_pares(@d[:por_operacao], 'Operação')
secao "CUSTO POR MOTORISTA"
tabela_pares(@d[:por_motorista], 'Motorista')
secao "COMPOSIÇÃO POR TIPO"
tabela_por_tipo
@pdf.start_new_page if @pdf.cursor < 160
secao "PAGAMENTOS REALIZADOS NO PERÍODO"
tabela_pagamentos_feitos
secao "PAGAMENTOS PENDENTES"
tabela_pagamentos_pendentes
end
def kpis
linhas = [
['Custo total', moeda(@d[:custo_total])],
['Ticket médio', moeda(@d[:ticket_medio])],
['Pago', moeda(@d[:pago])],
['A pagar', moeda(@d[:pendente])]
]
@pdf.table(linhas, width: 300, cell_style: { size: 11, padding: [6, 10] }) do |t|
t.columns(0).font_style = :bold
t.columns(1).align = :right
t.row(2).text_color = VERDE
end
end
# pares: Array de [label, valor]
def tabela_pares(pares, titulo_col)
if pares.present?
linhas = [[titulo_col, 'Valor']]
pares.each { |label, valor| linhas << [label.to_s, moeda(valor)] }
@pdf.table(linhas, header: true, width: @pdf.bounds.width,
cell_style: { size: 9, padding: [4, 8] }) do |t|
t.row(0).background_color = LARANJA
t.row(0).text_color = '000000'
t.row(0).font_style = :bold
t.columns(1).align = :right
t.row(1..-1).borders = [:bottom]
t.row(1..-1).border_color = 'DDDDDD'
end
else
sem_dados
end
end
def tabela_por_tipo
if @d[:por_tipo].present?
linhas = [['Tipo', 'Valor']]
@d[:por_tipo].each { |h| linhas << [h[:label], moeda(h[:valor])] }
@pdf.table(linhas, header: true, width: 360, cell_style: { size: 9, padding: [4, 8] }) do |t|
t.row(0).background_color = LARANJA
t.row(0).text_color = '000000'
t.row(0).font_style = :bold
t.columns(1).align = :right
end
else
sem_dados
end
end
def tabela_pagamentos_feitos
registros = @d[:pagamentos_feitos]
if registros.present?
linhas = [['Pago em', 'Motorista', 'Consolidação', 'Forma', 'Valor']]
registros.each do |cm|
linhas << [
cm.pago_em.strftime('%d/%m/%Y'),
cm.motorista_nome,
cm.consolidacao.nome,
cm.forma_pagamento&.humanize || '—',
moeda(cm.valor_total)
]
end
tabela_listagem(linhas)
@pdf.move_down 6
@pdf.fill_color VERDE
@pdf.text "Total pago no período: #{moeda(@d[:pag_pago_valor])} · #{@d[:pag_pago_qtd]} pagamento(s)", size: 10, style: :bold
@pdf.fill_color '000000'
else
sem_dados('Nenhum pagamento realizado neste período.')
end
end
def tabela_pagamentos_pendentes
registros = @d[:pagamentos_pendentes]
if registros.present?
linhas = [['Motorista', 'Consolidação', 'Valor']]
registros.each do |cm|
linhas << [cm.motorista_nome, cm.consolidacao.nome, moeda(cm.valor_total)]
end
tabela_listagem(linhas)
@pdf.move_down 6
@pdf.text "Total a pagar: #{moeda(@d[:pag_pend_valor])} · #{@d[:pag_pend_qtd]} motorista(s)", size: 10, style: :bold
else
sem_dados('Nenhuma pendência no período.')
end
end
def tabela_listagem(linhas)
@pdf.table(linhas, header: true, width: @pdf.bounds.width,
cell_style: { size: 8, padding: [4, 6] }) do |t|
t.row(0).background_color = PRETO
t.row(0).text_color = 'FFFFFF'
t.row(0).font_style = :bold
t.columns(-1).align = :right
t.row(1..-1).borders = [:bottom]
t.row(1..-1).border_color = 'DDDDDD'
end
end
def sem_dados(msg = 'Sem dados no período.')
@pdf.fill_color CINZA
@pdf.text msg, size: 10
@pdf.fill_color '000000'
end
end
end

View File

@@ -12,12 +12,13 @@
</div> </div>
<%# Resumo por tipo %> <%# Resumo por tipo %>
<div class="grid grid-cols-2 md:grid-cols-4 gap-3 mb-6"> <div class="grid grid-cols-2 md:grid-cols-5 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']
].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>

View File

@@ -5,7 +5,8 @@
data-validacao-url-value="<%= classificar_consolidacao_consolidacao_entregas_path(@consolidacao) %>" data-validacao-url-value="<%= classificar_consolidacao_consolidacao_entregas_path(@consolidacao) %>"
data-validacao-massa-url-value="<%= classificar_em_massa_consolidacao_consolidacao_entregas_path(@consolidacao) %>" data-validacao-massa-url-value="<%= classificar_em_massa_consolidacao_consolidacao_entregas_path(@consolidacao) %>"
data-validacao-motorista-value="<%= @motorista %>" data-validacao-motorista-value="<%= @motorista %>"
data-validacao-total-value="<%= @total_entregas %>"> data-validacao-total-value="<%= @total_entregas %>"
data-validacao-preco-extraordinaria-value="<%= @precos[:extraordinaria] %>">
<%# Header %> <%# Header %>
<div class="mb-6"> <div class="mb-6">
@@ -68,6 +69,8 @@
class="bg-white text-black font-bold px-3 py-2 rounded-lg text-xs border border-orange-500 min-h-[44px]">Bônus</button> class="bg-white text-black font-bold px-3 py-2 rounded-lg text-xs border border-orange-500 min-h-[44px]">Bônus</button>
<button data-action="click->validacao#marcarSelecionados" data-tipo="desconto" <button data-action="click->validacao#marcarSelecionados" data-tipo="desconto"
class="bg-black text-white font-bold px-3 py-2 rounded-lg text-xs border border-gray-700 min-h-[44px]">Desconto</button> class="bg-black text-white font-bold px-3 py-2 rounded-lg text-xs border border-gray-700 min-h-[44px]">Desconto</button>
<button data-action="click->validacao#marcarSelecionados" data-tipo="extraordinaria"
class="bg-purple-600 text-white font-bold px-3 py-2 rounded-lg text-xs min-h-[44px]">Extra</button>
</div> </div>
<div class="ml-auto"> <div class="ml-auto">
@@ -111,7 +114,8 @@
['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 border border-orange-500'], ['bonus', 'Bônus', 'bg-white text-black border border-orange-500'],
['desconto', 'Desc.', 'bg-black text-white border border-gray-700'] ['desconto', 'Desc.', 'bg-black text-white border border-gray-700'],
['extraordinaria', 'Extra', 'bg-purple-600 text-white']
].each do |tipo, label, cores| %> ].each do |tipo, label, cores| %>
<button data-action="click->validacao#classificar" <button data-action="click->validacao#classificar"
data-tracking="<%= e.tracking_id %>" data-tipo="<%= tipo %>" data-tracking="<%= e.tracking_id %>" data-tipo="<%= tipo %>"
@@ -153,6 +157,10 @@
<span class="flex items-center gap-2"><span class="w-3 h-3 bg-black border border-gray-600 rounded-full"></span> Desconto</span> <span class="flex items-center gap-2"><span class="w-3 h-3 bg-black border border-gray-600 rounded-full"></span> Desconto</span>
<span class="text-white font-bold" data-validacao-target="qtdDesconto">0</span> <span class="text-white font-bold" data-validacao-target="qtdDesconto">0</span>
</div> </div>
<div class="flex justify-between items-center">
<span class="flex items-center gap-2"><span class="w-3 h-3 bg-purple-600 rounded-full"></span> Extraordinária</span>
<span class="text-white font-bold" data-validacao-target="qtdExtraordinaria">0</span>
</div>
</div> </div>
<div class="border-t border-[#2a2a2a] pt-4"> <div class="border-t border-[#2a2a2a] pt-4">
@@ -165,6 +173,7 @@
<p>📦 Retirada: <%= moeda(@precos[:retirada]) %></p> <p>📦 Retirada: <%= moeda(@precos[:retirada]) %></p>
<p>⭐ Bônus: <%= moeda(@precos[:bonus]) %></p> <p>⭐ Bônus: <%= moeda(@precos[:bonus]) %></p>
<p>⚠️ Desconto: -<%= moeda(@precos[:desconto]) %></p> <p>⚠️ Desconto: -<%= moeda(@precos[:desconto]) %></p>
<p>✨ Extraordinária: <%= moeda(@precos[:extraordinaria]) %> (valor por entrega)</p>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -53,7 +53,8 @@
<% [['entrega_normal', '🚚 Entrega Normal', Configuracao.preco_entrega], <% [['entrega_normal', '🚚 Entrega Normal', Configuracao.preco_entrega],
['retirada', '📦 Retirada', Configuracao.preco_retirada], ['retirada', '📦 Retirada', Configuracao.preco_retirada],
['bonus', '⭐ Bônus', Configuracao.preco_bonus], ['bonus', '⭐ Bônus', Configuracao.preco_bonus],
['desconto', '⚠️ Desconto', Configuracao.preco_desconto]].each do |tipo, label, preco| %> ['desconto', '⚠️ Desconto', Configuracao.preco_desconto],
['extraordinaria', '✨ Entrega Extraordinária', Configuracao.preco_extraordinaria]].each do |tipo, label, preco| %>
<label class="flex items-center gap-2 bg-[#0a0a0a] border border-[#2a2a2a] rounded-lg px-3 py-3 cursor-pointer hover:border-orange-500 text-sm text-white"> <label class="flex items-center gap-2 bg-[#0a0a0a] border border-[#2a2a2a] rounded-lg px-3 py-3 cursor-pointer hover:border-orange-500 text-sm text-white">
<%= check_box_tag 'tipos[]', tipo, tipo == 'entrega_normal', id: nil, class: 'accent-orange-500' %> <%= check_box_tag 'tipos[]', tipo, tipo == 'entrega_normal', id: nil, class: 'accent-orange-500' %>
<span><%= label %><br><span class="text-gray-500 text-xs"><%= moeda(preco) %></span></span> <span><%= label %><br><span class="text-gray-500 text-xs"><%= moeda(preco) %></span></span>

View File

@@ -35,6 +35,9 @@
<%= link_to '✏️ Continuar validação', wizard_consolidacao_path(@consolidacao), <%= link_to '✏️ Continuar validação', wizard_consolidacao_path(@consolidacao),
class: 'bg-orange-500 hover:bg-orange-600 text-black font-bold px-5 py-3 rounded-lg min-h-[48px] flex items-center' %> class: 'bg-orange-500 hover:bg-orange-600 text-black font-bold px-5 py-3 rounded-lg min-h-[48px] flex items-center' %>
<% end %> <% end %>
<%= link_to '📊 Relatório financeiro', gerar_pdf_financeiro_consolidacao_path(@consolidacao),
data: { turbo: false },
class: 'bg-[#1a1a1a] hover:bg-[#2a2a2a] text-white border border-orange-500 font-semibold px-4 py-3 rounded-lg min-h-[48px] flex items-center' %>
<% if policy(@consolidacao).destroy? && !@consolidacao.arquivada? %> <% if policy(@consolidacao).destroy? && !@consolidacao.arquivada? %>
<%= button_to '📁 Arquivar', arquivar_consolidacao_path(@consolidacao), method: :post, <%= button_to '📁 Arquivar', arquivar_consolidacao_path(@consolidacao), method: :post,
data: { turbo_confirm: 'Arquivar esta consolidação? Ela não será excluída, apenas movida para o arquivo.' }, data: { turbo_confirm: 'Arquivar esta consolidação? Ela não será excluída, apenas movida para o arquivo.' },

View File

@@ -75,7 +75,8 @@
<% [['entrega_normal', '🚚 Entrega Normal', Configuracao.preco_entrega], <% [['entrega_normal', '🚚 Entrega Normal', Configuracao.preco_entrega],
['retirada', '📦 Retirada', Configuracao.preco_retirada], ['retirada', '📦 Retirada', Configuracao.preco_retirada],
['bonus', '⭐ Bônus', Configuracao.preco_bonus], ['bonus', '⭐ Bônus', Configuracao.preco_bonus],
['desconto', '⚠️ Desconto', Configuracao.preco_desconto]].each do |tipo, label, preco| %> ['desconto', '⚠️ Desconto', Configuracao.preco_desconto],
['extraordinaria', '✨ Entrega Extraordinária', Configuracao.preco_extraordinaria]].each do |tipo, label, preco| %>
<label class="flex items-center gap-2 bg-[#0a0a0a] border border-[#2a2a2a] rounded-lg px-3 py-3 cursor-pointer hover:border-orange-500 text-sm text-white"> <label class="flex items-center gap-2 bg-[#0a0a0a] border border-[#2a2a2a] rounded-lg px-3 py-3 cursor-pointer hover:border-orange-500 text-sm text-white">
<input type="checkbox" value="<%= tipo %>" data-apontamento-target="tipoCheck" <input type="checkbox" value="<%= tipo %>" data-apontamento-target="tipoCheck"
<%= 'checked' if tipo == 'entrega_normal' %> <%= 'checked' if tipo == 'entrega_normal' %>

View File

@@ -61,11 +61,20 @@
</div> </div>
</details> </details>
<% end %> <% end %>
<%# Imprimir relatório financeiro do período/operação atuais %>
<%= link_to '🖨️ Relatório',
dashboard_relatorio_financeiro_path(
inicio: @periodo_inicio.strftime('%Y-%m-%d'),
fim: @periodo_fim.strftime('%Y-%m-%d'),
operacoes: @operacao_filtro),
data: { turbo: false },
class: 'pl-4 pr-4 py-2.5 bg-orange-500 hover:bg-orange-600 text-black font-bold rounded-xl text-sm whitespace-nowrap' %>
</form> </form>
</div> </div>
<%# CARDS GRANDES — KPIs %> <%# CARDS GRANDES — KPIs %>
<div class="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 gap-4"> <div class="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-5 gap-4">
<%# Card 1: Valor estimado %> <%# Card 1: Valor estimado %>
<div class="sm:col-span-2 xl:col-span-1 bg-[#f97316] rounded-2xl p-6 relative overflow-hidden"> <div class="sm:col-span-2 xl:col-span-1 bg-[#f97316] rounded-2xl p-6 relative overflow-hidden">
@@ -76,17 +85,28 @@
<%= moeda(@valor_estimado) %> <%= moeda(@valor_estimado) %>
</p> </p>
<p class="text-orange-200 text-sm"> <p class="text-orange-200 text-sm">
<%= @entregas_pagas %> entregas pagas <%= @entregas_pagas %> entregas concluídas
</p> </p>
</div> </div>
</div> </div>
<%# Card destacado: total consolidado e pago (consolidações finalizadas do período) %>
<div class="sm:col-span-2 xl:col-span-1 bg-[#1a1a1a] rounded-2xl border border-green-500/30 p-6">
<p class="text-gray-400 text-sm font-medium mb-2">💼 Consolidado / Pago</p>
<p class="text-3xl font-black text-white mb-1"><%= moeda(@fin_custo_total) %></p>
<p class="text-gray-400 text-xs mb-2">consolidado no período</p>
<div class="flex items-center gap-2 text-sm border-t border-white/5 pt-2">
<span class="text-green-400 font-semibold"><%= moeda(@fin_pago) %></span>
<span class="text-gray-400">pago</span>
</div>
</div>
<%# Card 2: Total de entregas %> <%# Card 2: Total de entregas %>
<div class="bg-[#1a1a1a] rounded-2xl border border-white/5 p-6"> <div class="bg-[#1a1a1a] rounded-2xl border border-white/5 p-6">
<p class="text-gray-400 text-sm font-medium mb-2">📦 Total Entregas</p> <p class="text-gray-400 text-sm font-medium mb-2">📦 Total Entregas</p>
<p class="text-4xl font-black text-white mb-1"><%= @total_entregas %></p> <p class="text-4xl font-black text-white mb-1"><%= @total_entregas %></p>
<div class="flex items-center gap-2 text-sm"> <div class="flex items-center gap-2 text-sm">
<span class="text-green-400"><%= @entregas_pagas %> pagas</span> <span class="text-green-400"><%= @entregas_pagas %> entregues</span>
<span class="text-gray-400">·</span> <span class="text-gray-400">·</span>
<span class="text-yellow-500"><%= @entregas_pendentes %> pendentes</span> <span class="text-yellow-500"><%= @entregas_pendentes %> pendentes</span>
</div> </div>
@@ -187,7 +207,7 @@
<% else %> <% else %>
<div class="text-center py-8 text-gray-400"> <div class="text-center py-8 text-gray-400">
<div class="text-3xl mb-2">🚚</div> <div class="text-3xl mb-2">🚚</div>
<p class="text-sm">Nenhuma entrega paga neste período</p> <p class="text-sm">Nenhuma entrega concluída neste período</p>
</div> </div>
<% end %> <% end %>
</div> </div>
@@ -577,7 +597,7 @@
Chart.getChart(ctx)?.destroy(); Chart.getChart(ctx)?.destroy();
const labels = <%= raw @fin_por_tipo.map { |h| h[:label] }.to_json %>; const labels = <%= raw @fin_por_tipo.map { |h| h[:label] }.to_json %>;
const valores = <%= raw @fin_por_tipo.map { |h| h[:valor] }.to_json %>; const valores = <%= raw @fin_por_tipo.map { |h| h[:valor] }.to_json %>;
const cores = ['#f97316', '#9a3412', '#fed7aa', '#374151']; const cores = ['#f97316', '#9a3412', '#fed7aa', '#374151', '#9333ea'];
new Chart(ctx, { new Chart(ctx, {
type: 'doughnut', type: 'doughnut',
data: { labels: labels, datasets: [{ data: valores, backgroundColor: cores, borderColor: '#1a1a1a', borderWidth: 3 }] }, data: { labels: labels, datasets: [{ data: valores, backgroundColor: cores, borderColor: '#1a1a1a', borderWidth: 3 }] },

View File

@@ -14,6 +14,7 @@ Rails.application.routes.draw do
# Dashboard principal (admin/gerente/operador) # Dashboard principal (admin/gerente/operador)
get '/dashboard', to: 'dashboard#index', as: :dashboard get '/dashboard', to: 'dashboard#index', as: :dashboard
get '/dashboard/relatorio_financeiro', to: 'dashboard#relatorio_financeiro', as: :dashboard_relatorio_financeiro
# Painel motorista # Painel motorista
get '/motorista', to: 'motorista/dashboard#index', as: :motorista_dashboard get '/motorista', to: 'motorista/dashboard#index', as: :motorista_dashboard
@@ -44,6 +45,7 @@ Rails.application.routes.draw do
get :preview_holerite get :preview_holerite
get :gerar_pdf_relatorio get :gerar_pdf_relatorio
get :gerar_pdf_holerite get :gerar_pdf_holerite
get :gerar_pdf_financeiro # relatório financeiro da consolidação
end end
resources :consolidacao_entregas, only: [] do resources :consolidacao_entregas, only: [] do

View File

@@ -28,6 +28,7 @@ configs = [
{ chave: 'preco_retirada', valor: '20.00', descricao: 'Valor pago por retirada de equipamento (R$)' }, { chave: 'preco_retirada', valor: '20.00', descricao: 'Valor pago por retirada de equipamento (R$)' },
{ 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: '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' },