Initial commit - Fase 1: Setup Rails + Docker

This commit is contained in:
2026-06-10 17:40:07 -03:00
commit eb812868e5
30 changed files with 1390 additions and 0 deletions

View File

@@ -0,0 +1,29 @@
# app/models/auditoria_log.rb
class AuditoriaLog < ApplicationRecord
belongs_to :user, optional: true # opcional caso user seja deletado
ACOES = %w[criar editar finalizar arquivar excluir login logout].freeze
validates :acao, presence: true
validates :entidade, presence: true
scope :recentes, -> { order(created_at: :desc) }
scope :por_user, ->(user_id) { where(user_id: user_id) }
scope :por_entidade, ->(entidade) { where(entidade: entidade) }
def self.registrar(user:, acao:, entidade:, entidade_id: nil,
dados_anteriores: {}, dados_novos: {}, request: nil)
create!(
user_id: user&.id,
acao: acao.to_s,
entidade: entidade.to_s,
entidade_id: entidade_id,
dados_anteriores: dados_anteriores,
dados_novos: dados_novos,
ip_address: request&.remote_ip,
user_agent: request&.user_agent
)
rescue => e
Rails.logger.error("[AuditoriaLog] Erro ao registrar: #{e.message}")
end
end

View File

@@ -0,0 +1,55 @@
# app/models/configuracao.rb
class Configuracao < ApplicationRecord
# Chaves válidas do sistema
CHAVES = %w[
preco_entrega
preco_retirada
preco_bonus
preco_desconto
notificacao_whatsapp
notificacao_email
empresa_nome
empresa_logo
].freeze
CHAVES_MOEDA = %w[
preco_entrega
preco_retirada
preco_bonus
preco_desconto
].freeze
validates :chave, presence: true, inclusion: { in: CHAVES }, uniqueness: true
validates :valor, presence: true
# ── Acesso rápido ───────────────────────────────────────────
def self.valor(chave)
find_by(chave: chave)&.valor
end
def self.preco_entrega
valor('preco_entrega').to_f
end
def self.preco_retirada
valor('preco_retirada').to_f
end
def self.preco_bonus
valor('preco_bonus').to_f
end
def self.preco_desconto
valor('preco_desconto').to_f
end
def moeda?
CHAVES_MOEDA.include?(chave)
end
def valor_formatado
return "R$ #{format('%.2f', valor.to_f).gsub('.', ',')}" if moeda?
valor
end
end

View File

@@ -0,0 +1,75 @@
# app/models/consolidacao.rb
class Consolidacao < ApplicationRecord
belongs_to :criador, class_name: 'User', foreign_key: :created_by
belongs_to :finalizador, class_name: 'User', foreign_key: :finalizado_por, optional: true
has_many :consolidacao_motoristas, dependent: :destroy
has_many :consolidacao_entregas, dependent: :destroy
# ── Enums ───────────────────────────────────────────────────
enum status: { rascunho: 0, finalizada: 1, arquivada: 2 }
# ── Soft delete ─────────────────────────────────────────────
scope :ativas, -> { where(deleted_at: nil) }
scope :arquivadas, -> { where.not(deleted_at: nil) }
# ── Validações ──────────────────────────────────────────────
validates :nome, presence: true
validates :data_inicio, presence: true
validates :data_fim, presence: true
validates :created_by, presence: true
validate :periodo_valido
# ── Callbacks ───────────────────────────────────────────────
before_save :recalcular_valor_total
# ── Escopos ─────────────────────────────────────────────────
scope :recentes, -> { order(created_at: :desc) }
scope :no_periodo, ->(i, f) { where('data_inicio >= ? AND data_fim <= ?', i, f) }
# ── Métodos ─────────────────────────────────────────────────
def arquivar!(user)
update!(deleted_at: Time.current, status: :arquivada)
end
def finalizar!(user)
return false unless todas_entregas_classificadas?
update!(
status: :finalizada,
finalizado_por: user.id,
finalizado_em: Time.current
)
end
def todas_entregas_classificadas?
total_entregas = ConsolidacaoMotorista
.where(consolidacao_id: id)
.sum { |cm| Entrega.contar_pagas(inicio: data_inicio, fim: data_fim, motorista: cm.motorista_nome) }
consolidacao_entregas.count >= total_entregas
end
def percentual_classificado
return 0 if total_entregas_estimadas.zero?
((consolidacao_entregas.count.to_f / total_entregas_estimadas) * 100).round
end
def total_entregas_estimadas
@total_entregas_estimadas ||= consolidacao_motoristas.sum do |cm|
Entrega.contar_pagas(inicio: data_inicio, fim: data_fim, motorista: cm.motorista_nome)
end
end
private
def periodo_valido
return unless data_inicio && data_fim
errors.add(:data_fim, 'deve ser após a data de início') if data_fim < data_inicio
end
def recalcular_valor_total
self.valor_total = consolidacao_motoristas.sum(:valor_total)
end
end

View File

@@ -0,0 +1,32 @@
# app/models/consolidacao_entrega.rb
class ConsolidacaoEntrega < ApplicationRecord
belongs_to :consolidacao
enum tipo: {
entrega_normal: 0,
retirada: 1,
bonus: 2,
desconto: 3
}
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' }
}.freeze
validates :tracking_id, presence: true, uniqueness: { scope: :consolidacao_id }
validates :motorista_nome, presence: true
validates :tipo, presence: true
validates :valor_aplicado, presence: true, numericality: { greater_than_or_equal_to: 0 }
# Busca a entrega original (leitura do banco existente)
def entrega_original
@entrega_original ||= Entrega.find_by(tracking_id: tracking_id)
end
def tipo_cor
TIPO_CORES[tipo] || TIPO_CORES['entrega_normal']
end
end

View File

@@ -0,0 +1,29 @@
# app/models/consolidacao_motorista.rb
class ConsolidacaoMotorista < ApplicationRecord
belongs_to :consolidacao
has_many :consolidacao_entregas,
->(cm) { where(motorista_nome: cm.motorista_nome) },
foreign_key: :consolidacao_id,
primary_key: :consolidacao_id
validates :motorista_nome, presence: true
validates :motorista_nome, uniqueness: { scope: :consolidacao_id }
before_save :recalcular_valor
def recalcular_valor
cfg = Configuracao
self.valor_total = consolidacao_entregas
.includes(:consolidacao)
.where(motorista_nome: motorista_nome)
.sum do |e|
case e.tipo
when 'entrega_normal' then cfg.preco_entrega
when 'retirada' then cfg.preco_retirada
when 'bonus' then cfg.preco_bonus
when 'desconto' then -cfg.preco_desconto
else 0
end
end
end
end

88
app/models/entrega.rb Normal file
View File

@@ -0,0 +1,88 @@
# app/models/entrega.rb
#
# Model de LEITURA para a tabela existente da Gade Hospitalar.
# NUNCA criar migration para esta tabela.
# NUNCA executar INSERT, UPDATE, DELETE ou DROP nesta tabela.
#
class Entrega < ApplicationRecord
self.table_name = 'db_reem_simplerout_2026'
self.primary_key = 'tracking_id'
# Apenas leitura — segurança contra mutações acidentais
def readonly?
true
end
# ── Scopes ──────────────────────────────────────────────────
scope :concluidas, -> { where(status: 'completed') }
scope :com_checkin, -> { where.not(checkin: nil) }
scope :pagas, -> { concluidas.com_checkin }
scope :pendentes, -> { where.not(status: 'completed') }
scope :no_periodo, ->(inicio, fim) {
where(planned_date: inicio.to_date..fim.to_date)
}
scope :do_motorista, ->(nome) {
where(driver: nome)
}
scope :da_rota, ->(route_id) {
where(route_id: route_id)
}
scope :da_conta_gade, -> {
where(account_id: ENV.fetch('DB_EXISTING_ACCOUNT_ID', 95907).to_i)
}
# ── Métodos de classe ────────────────────────────────────────
# Lista motoristas únicos (para selects, consolidações)
def self.motoristas_ativos(inicio: nil, fim: nil)
base = da_conta_gade
base = base.no_periodo(inicio, fim) if inicio && fim
base.distinct.order(:driver).pluck(:driver).compact.reject(&:empty?)
end
# Lista rotas únicas (route_id → descrição)
def self.rotas_unicas(inicio: nil, fim: nil)
base = da_conta_gade
base = base.no_periodo(inicio, fim) if inicio && fim
base.distinct.pluck(:route_id).compact
end
# Contagem de entregas pagas para cálculo estimado
def self.contar_pagas(inicio:, fim:, motorista: nil, route_id: nil)
base = pagas.da_conta_gade.no_periodo(inicio, fim)
base = base.do_motorista(motorista) if motorista.present?
base = base.da_rota(route_id) if route_id.present?
base.count
end
# ── Helpers de instância ─────────────────────────────────────
def numero_nf
reference_id
end
def local
contact_name.presence || address
end
def concluida?
status == 'completed'
end
def checkin_registrado?
checkin.present?
end
def elegivel_pagamento?
concluida? && checkin_registrado?
end
def atraso_minutos
return 0 unless delay.present?
delay.to_s.split(':').then { |h, m, _s| h.to_i * 60 + m.to_i }
end
end

View File

@@ -0,0 +1,32 @@
# app/models/historico_estimado.rb
class HistoricoEstimado < ApplicationRecord
validates :data_hora, presence: true
validates :valor_total_estimado, presence: true, numericality: { greater_than_or_equal_to: 0 }
validates :entregas_contadas, presence: true, numericality: { only_integer: true, greater_than_or_equal_to: 0 }
scope :recentes, -> { order(data_hora: :desc) }
scope :do_dia, ->(data = Date.current) { where(data_hora: data.all_day) }
# Chamado pelo job a cada 1 hora (Whenever gem — Fase 4)
def self.atualizar!
preco = Configuracao.preco_entrega
entregas = Entrega.pagas.where(planned_date: Date.current).count
total = entregas * preco
create!(
data_hora: Time.current,
valor_total_estimado: total,
entregas_contadas: entregas
)
end
# Cálculo ao vivo para o Dashboard (sem esperar o job)
def self.calcular_ao_vivo(inicio: Date.current.beginning_of_month, fim: Date.current)
preco = Configuracao.preco_entrega
entregas = Entrega.pagas.where(planned_date: inicio..fim).count
{
valor: entregas * preco,
entregas: entregas
}
end
end

69
app/models/user.rb Normal file
View File

@@ -0,0 +1,69 @@
# app/models/user.rb
class User < ApplicationRecord
devise :database_authenticatable,
:registerable,
:recoverable,
:rememberable,
:validatable
# ── Roles ──────────────────────────────────────────────────
enum role: { admin: 0, gerente: 1, operador: 2, motorista: 3 }
ROLES_LABEL = {
'admin' => 'Administrador',
'gerente' => 'Gerente',
'operador' => 'Operador',
'motorista' => 'Motorista'
}.freeze
# ── Validações ──────────────────────────────────────────────
validates :nome, presence: true, length: { minimum: 3 }
validates :role, presence: true
validates :pin_code,
length: { is: 4 },
numericality: { only_integer: true },
allow_nil: true,
if: :motorista?
validate :pin_unico_para_motoristas, if: :motorista?
# Motoristas podem não ter e-mail (login via PIN)
def email_required?
!motorista?
end
def password_required?
!motorista? ? super : false
end
# ── Escopos ─────────────────────────────────────────────────
scope :ativos, -> { where(ativo: true) }
scope :por_nome, -> { order(:nome) }
scope :nao_motoristas, -> { where.not(role: :motorista) }
# ── Helpers ─────────────────────────────────────────────────
def nome_display
nome.presence || email
end
def role_label
ROLES_LABEL[role] || role.humanize
end
def pode_ver_config?
admin? || gerente?
end
def pode_consolidar?
admin? || gerente? || operador?
end
private
def pin_unico_para_motoristas
return if pin_code.blank?
conflito = User.motorista.where(pin_code: pin_code).where.not(id: id)
errors.add(:pin_code, 'já está em uso por outro motorista') if conflito.exists?
end
end