Correção Dash board e implantação Relaório de ADM
This commit is contained in:
@@ -39,7 +39,8 @@ class ConsolidacaoEntregasController < ApplicationController
|
||||
entrega_normal: Configuracao.preco_entrega,
|
||||
retirada: Configuracao.preco_retirada,
|
||||
bonus: Configuracao.preco_bonus,
|
||||
desconto: Configuracao.preco_desconto
|
||||
desconto: Configuracao.preco_desconto,
|
||||
extraordinaria: Configuracao.preco_extraordinaria
|
||||
}
|
||||
end
|
||||
|
||||
@@ -62,7 +63,7 @@ class ConsolidacaoEntregasController < ApplicationController
|
||||
tracking_id: params[:tracking_id],
|
||||
motorista_nome: params[:motorista],
|
||||
tipo: params[:tipo],
|
||||
valor_aplicado: valor_para_tipo(params[:tipo]),
|
||||
valor_aplicado: valor_classificacao(params[:tipo]),
|
||||
created_by: current_user.id
|
||||
)
|
||||
ativo = true
|
||||
@@ -106,7 +107,7 @@ class ConsolidacaoEntregasController < ApplicationController
|
||||
.destroy_all
|
||||
else
|
||||
# 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|
|
||||
ce = @consolidacao.consolidacao_entregas.find_or_initialize_by(tracking_id: tid, tipo: tipo)
|
||||
ce.assign_attributes(motorista_nome: motorista,
|
||||
@@ -296,6 +297,18 @@ class ConsolidacaoEntregasController < ApplicationController
|
||||
ConsolidacaoEntrega.valor_para(tipo)
|
||||
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)
|
||||
@consolidacao.recalcular_motorista!(nome)
|
||||
end
|
||||
|
||||
@@ -5,7 +5,8 @@ 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]
|
||||
preview_holerite gerar_pdf_relatorio gerar_pdf_holerite
|
||||
gerar_pdf_financeiro]
|
||||
|
||||
# Formas de pagamento aceitas (o select da UI usa esta mesma lista).
|
||||
FORMAS_PAGAMENTO = %w[pix transferencia dinheiro].freeze
|
||||
@@ -271,6 +272,19 @@ class ConsolidacoesController < ApplicationController
|
||||
disposition: 'attachment'
|
||||
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
|
||||
def gerar_pdf_holerite
|
||||
autorizar_holerite!(params[:motorista])
|
||||
|
||||
@@ -12,6 +12,44 @@ class DashboardController < ApplicationController
|
||||
carregar_dados_dashboard
|
||||
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
|
||||
|
||||
# Faixa de datas vinda do filtro de calendário (params inicio/fim).
|
||||
|
||||
@@ -9,14 +9,15 @@ export default class extends Controller {
|
||||
"linha", "checkbox", "selecionarTodos", "modoRemover", "rotuloAcao",
|
||||
"toggles", "barra", "percentual",
|
||||
"contadorClassificadas", "contadorTotal",
|
||||
"qtdNormal", "qtdRetirada", "qtdBonus", "qtdDesconto", "valorTotal"
|
||||
"qtdNormal", "qtdRetirada", "qtdBonus", "qtdDesconto", "qtdExtraordinaria", "valorTotal"
|
||||
]
|
||||
|
||||
static values = {
|
||||
url: String, // POST classificar
|
||||
massaUrl: String, // POST classificar_em_massa
|
||||
motorista: String,
|
||||
total: Number
|
||||
url: String, // POST classificar
|
||||
massaUrl: String, // POST classificar_em_massa
|
||||
motorista: String,
|
||||
total: Number,
|
||||
precoExtraordinaria: Number // preço padrão sugerido no prompt
|
||||
}
|
||||
|
||||
connect() {
|
||||
@@ -34,12 +35,21 @@ export default class extends Controller {
|
||||
const tracking = btn.dataset.tracking
|
||||
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
|
||||
try {
|
||||
const resp = await this.post(this.urlValue, {
|
||||
tracking_id: tracking,
|
||||
tipo: tipo,
|
||||
motorista: this.motoristaValue
|
||||
motorista: this.motoristaValue,
|
||||
valor: valor
|
||||
})
|
||||
// resp.ativo indica se o pilar ficou ligado ou desligado (toggle)
|
||||
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"
|
||||
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, {
|
||||
tipo: tipo,
|
||||
motorista: this.motoristaValue,
|
||||
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.atualizarResumo(resp)
|
||||
@@ -97,11 +114,19 @@ export default class extends Controller {
|
||||
}
|
||||
|
||||
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, {
|
||||
tipo: tipo,
|
||||
motorista: this.motoristaValue,
|
||||
tracking_ids: ids,
|
||||
acao: remover ? "remover" : "adicionar"
|
||||
acao: remover ? "remover" : "adicionar",
|
||||
valor: valor
|
||||
})
|
||||
ids.forEach(id => this.setToggle(id, tipo, !remover))
|
||||
this.checkboxTargets.forEach(c => c.checked = false)
|
||||
@@ -150,6 +175,8 @@ export default class extends Controller {
|
||||
this.qtdRetiradaTarget.textContent = porTipo["retirada"] || 0
|
||||
this.qtdBonusTarget.textContent = porTipo["bonus"] || 0
|
||||
this.qtdDescontoTarget.textContent = porTipo["desconto"] || 0
|
||||
if (this.hasQtdExtraordinariaTarget)
|
||||
this.qtdExtraordinariaTarget.textContent = porTipo["extraordinaria"] || 0
|
||||
|
||||
const valor = dados.valor_total || 0
|
||||
this.valorTotalTarget.textContent =
|
||||
@@ -171,6 +198,21 @@ export default class extends Controller {
|
||||
|
||||
labelTipo(tipo) {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ class Configuracao < ApplicationRecord
|
||||
preco_retirada
|
||||
preco_bonus
|
||||
preco_desconto
|
||||
preco_extraordinaria
|
||||
notificacao_whatsapp
|
||||
notificacao_email
|
||||
empresa_nome
|
||||
@@ -19,6 +20,7 @@ class Configuracao < ApplicationRecord
|
||||
preco_retirada
|
||||
preco_bonus
|
||||
preco_desconto
|
||||
preco_extraordinaria
|
||||
].freeze
|
||||
|
||||
validates :chave, presence: true, inclusion: { in: CHAVES }, uniqueness: true
|
||||
@@ -46,13 +48,18 @@ class Configuracao < ApplicationRecord
|
||||
valor('preco_desconto').to_f
|
||||
end
|
||||
|
||||
def self.preco_extraordinaria
|
||||
valor('preco_extraordinaria').to_f
|
||||
end
|
||||
|
||||
# Mapa usado pelo DashboardController (Fase 3)
|
||||
def self.mapa_de_precos
|
||||
{
|
||||
entrega: preco_entrega,
|
||||
retirada: preco_retirada,
|
||||
bonus: preco_bonus,
|
||||
desconto: preco_desconto
|
||||
entrega: preco_entrega,
|
||||
retirada: preco_retirada,
|
||||
bonus: preco_bonus,
|
||||
desconto: preco_desconto,
|
||||
extraordinaria: preco_extraordinaria
|
||||
}
|
||||
end
|
||||
|
||||
@@ -61,6 +68,7 @@ class Configuracao < ApplicationRecord
|
||||
'preco_retirada' => 'Retirada',
|
||||
'preco_bonus' => 'Bônus',
|
||||
'preco_desconto' => 'Desconto',
|
||||
'preco_extraordinaria' => 'Entrega Extraordinária',
|
||||
'notificacao_whatsapp' => 'Notificação WhatsApp',
|
||||
'notificacao_email' => 'Notificação E-mail',
|
||||
'empresa_nome' => 'Nome da Empresa'
|
||||
@@ -71,6 +79,7 @@ class Configuracao < ApplicationRecord
|
||||
'preco_retirada' => '📦',
|
||||
'preco_bonus' => '⭐',
|
||||
'preco_desconto' => '⚠️',
|
||||
'preco_extraordinaria' => '✨',
|
||||
'notificacao_whatsapp' => '💬',
|
||||
'notificacao_email' => '✉️',
|
||||
'empresa_nome' => '🏢'
|
||||
|
||||
@@ -13,14 +13,16 @@ class ConsolidacaoEntrega < ApplicationRecord
|
||||
entrega_normal: 0,
|
||||
retirada: 1,
|
||||
bonus: 2,
|
||||
desconto: 3
|
||||
desconto: 3,
|
||||
extraordinaria: 4
|
||||
}
|
||||
|
||||
TIPO_CORES = {
|
||||
'entrega_normal' => { bg: 'bg-orange-500', text: 'text-white', label: 'Entrega Normal' },
|
||||
'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' }
|
||||
'desconto' => { bg: 'bg-gray-900', text: 'text-white', label: 'Desconto' },
|
||||
'extraordinaria' => { bg: 'bg-purple-600', text: 'text-white', label: 'Entrega Extraordinária' }
|
||||
}.freeze
|
||||
|
||||
# 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 'bonus' then Configuracao.preco_bonus
|
||||
when 'desconto' then Configuracao.preco_desconto
|
||||
when 'extraordinaria' then Configuracao.preco_extraordinaria
|
||||
else 0
|
||||
end
|
||||
end
|
||||
|
||||
103
app/services/pdf/relatorio_financeiro_consolidacao_pdf.rb
Normal file
103
app/services/pdf/relatorio_financeiro_consolidacao_pdf.rb
Normal 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
|
||||
154
app/services/pdf/relatorio_financeiro_periodo_pdf.rb
Normal file
154
app/services/pdf/relatorio_financeiro_periodo_pdf.rb
Normal 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
|
||||
@@ -12,12 +12,13 @@
|
||||
</div>
|
||||
|
||||
<%# 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'],
|
||||
['retirada', 'Retirada', 'bg-orange-800 text-white'],
|
||||
['bonus', 'Bônus', 'bg-white text-black'],
|
||||
['desconto', 'Desconto', 'bg-black text-white border border-gray-700']
|
||||
['entrega_normal', 'Normal', 'bg-orange-500 text-black'],
|
||||
['retirada', 'Retirada', 'bg-orange-800 text-white'],
|
||||
['bonus', 'Bônus', 'bg-white text-black'],
|
||||
['desconto', 'Desconto', 'bg-black text-white border border-gray-700'],
|
||||
['extraordinaria', 'Extraordinária', 'bg-purple-600 text-white']
|
||||
].each do |tipo, label, cores| %>
|
||||
<div class="<%= cores %> rounded-xl p-4 text-center">
|
||||
<p class="font-black text-3xl"><%= @resumo[tipo] || 0 %></p>
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
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-motorista-value="<%= @motorista %>"
|
||||
data-validacao-total-value="<%= @total_entregas %>">
|
||||
data-validacao-total-value="<%= @total_entregas %>"
|
||||
data-validacao-preco-extraordinaria-value="<%= @precos[:extraordinaria] %>">
|
||||
|
||||
<%# Header %>
|
||||
<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>
|
||||
<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>
|
||||
<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 class="ml-auto">
|
||||
@@ -111,7 +114,8 @@
|
||||
['entrega_normal', 'Normal', 'bg-orange-500 text-black'],
|
||||
['retirada', 'Retirada', 'bg-orange-800 text-white'],
|
||||
['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| %>
|
||||
<button data-action="click->validacao#classificar"
|
||||
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="text-white font-bold" data-validacao-target="qtdDesconto">0</span>
|
||||
</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 class="border-t border-[#2a2a2a] pt-4">
|
||||
@@ -165,6 +173,7 @@
|
||||
<p>📦 Retirada: <%= moeda(@precos[:retirada]) %></p>
|
||||
<p>⭐ Bônus: <%= moeda(@precos[:bonus]) %></p>
|
||||
<p>⚠️ Desconto: -<%= moeda(@precos[:desconto]) %></p>
|
||||
<p>✨ Extraordinária: <%= moeda(@precos[:extraordinaria]) %> (valor por entrega)</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -50,10 +50,11 @@
|
||||
<div data-nota-avulsa-target="confirmar" class="hidden border-t border-[#2a2a2a] pt-4">
|
||||
<label class="block text-gray-400 text-sm mb-2">Classificar como <span class="text-gray-600">(pode marcar mais de uma)</span></label>
|
||||
<div class="grid grid-cols-2 gap-2 mb-4">
|
||||
<% [['entrega_normal', '🚚 Entrega Normal', Configuracao.preco_entrega],
|
||||
['retirada', '📦 Retirada', Configuracao.preco_retirada],
|
||||
['bonus', '⭐ Bônus', Configuracao.preco_bonus],
|
||||
['desconto', '⚠️ Desconto', Configuracao.preco_desconto]].each do |tipo, label, preco| %>
|
||||
<% [['entrega_normal', '🚚 Entrega Normal', Configuracao.preco_entrega],
|
||||
['retirada', '📦 Retirada', Configuracao.preco_retirada],
|
||||
['bonus', '⭐ Bônus', Configuracao.preco_bonus],
|
||||
['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">
|
||||
<%= 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>
|
||||
|
||||
@@ -35,6 +35,9 @@
|
||||
<%= 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' %>
|
||||
<% 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? %>
|
||||
<%= 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.' },
|
||||
|
||||
@@ -72,10 +72,11 @@
|
||||
<div data-apontamento-target="confirmar" class="hidden border-t border-[#2a2a2a] pt-4">
|
||||
<label class="block text-gray-400 text-sm mb-2">Classificar como <span class="text-gray-600">(pode marcar mais de uma)</span></label>
|
||||
<div class="grid grid-cols-2 gap-2 mb-4">
|
||||
<% [['entrega_normal', '🚚 Entrega Normal', Configuracao.preco_entrega],
|
||||
['retirada', '📦 Retirada', Configuracao.preco_retirada],
|
||||
['bonus', '⭐ Bônus', Configuracao.preco_bonus],
|
||||
['desconto', '⚠️ Desconto', Configuracao.preco_desconto]].each do |tipo, label, preco| %>
|
||||
<% [['entrega_normal', '🚚 Entrega Normal', Configuracao.preco_entrega],
|
||||
['retirada', '📦 Retirada', Configuracao.preco_retirada],
|
||||
['bonus', '⭐ Bônus', Configuracao.preco_bonus],
|
||||
['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">
|
||||
<input type="checkbox" value="<%= tipo %>" data-apontamento-target="tipoCheck"
|
||||
<%= 'checked' if tipo == 'entrega_normal' %>
|
||||
|
||||
@@ -61,11 +61,20 @@
|
||||
</div>
|
||||
</details>
|
||||
<% 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>
|
||||
</div>
|
||||
|
||||
<%# 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 %>
|
||||
<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) %>
|
||||
</p>
|
||||
<p class="text-orange-200 text-sm">
|
||||
<%= @entregas_pagas %> entregas pagas
|
||||
<%= @entregas_pagas %> entregas concluídas
|
||||
</p>
|
||||
</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 %>
|
||||
<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-4xl font-black text-white mb-1"><%= @total_entregas %></p>
|
||||
<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-yellow-500"><%= @entregas_pendentes %> pendentes</span>
|
||||
</div>
|
||||
@@ -187,7 +207,7 @@
|
||||
<% else %>
|
||||
<div class="text-center py-8 text-gray-400">
|
||||
<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>
|
||||
<% end %>
|
||||
</div>
|
||||
@@ -577,7 +597,7 @@
|
||||
Chart.getChart(ctx)?.destroy();
|
||||
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 cores = ['#f97316', '#9a3412', '#fed7aa', '#374151'];
|
||||
const cores = ['#f97316', '#9a3412', '#fed7aa', '#374151', '#9333ea'];
|
||||
new Chart(ctx, {
|
||||
type: 'doughnut',
|
||||
data: { labels: labels, datasets: [{ data: valores, backgroundColor: cores, borderColor: '#1a1a1a', borderWidth: 3 }] },
|
||||
|
||||
@@ -14,6 +14,7 @@ Rails.application.routes.draw do
|
||||
|
||||
# Dashboard principal (admin/gerente/operador)
|
||||
get '/dashboard', to: 'dashboard#index', as: :dashboard
|
||||
get '/dashboard/relatorio_financeiro', to: 'dashboard#relatorio_financeiro', as: :dashboard_relatorio_financeiro
|
||||
|
||||
# Painel motorista
|
||||
get '/motorista', to: 'motorista/dashboard#index', as: :motorista_dashboard
|
||||
@@ -44,6 +45,7 @@ Rails.application.routes.draw do
|
||||
get :preview_holerite
|
||||
get :gerar_pdf_relatorio
|
||||
get :gerar_pdf_holerite
|
||||
get :gerar_pdf_financeiro # relatório financeiro da consolidação
|
||||
end
|
||||
|
||||
resources :consolidacao_entregas, only: [] do
|
||||
|
||||
@@ -28,6 +28,7 @@ configs = [
|
||||
{ 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_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_email', valor: 'false', descricao: 'Notificações via e-mail (true/false)' },
|
||||
{ chave: 'empresa_nome', valor: 'Reem Transporte', descricao: 'Nome da empresa exibido no sistema' },
|
||||
|
||||
Reference in New Issue
Block a user