191 lines
7.3 KiB
Ruby
191 lines
7.3 KiB
Ruby
# app/models/user.rb
|
|
class User < ApplicationRecord
|
|
devise :database_authenticatable,
|
|
|
|
:recoverable,
|
|
:rememberable,
|
|
:validatable
|
|
|
|
# Perfil de acesso: nulo é válido (motorista não usa; e quem ainda não foi
|
|
# migrado cai no padrão do papel — ver #permissoes_efetivas).
|
|
belongs_to :perfil_acesso, optional: true, inverse_of: :users
|
|
|
|
# ── Roles ──────────────────────────────────────────────────
|
|
# externo: acesso somente-leitura ao dashboard (ex.: gerências de outras áreas
|
|
# que só precisam visualizar os indicadores). Login normal por e-mail/senha.
|
|
enum role: { admin: 0, gerente: 1, operador: 2, motorista: 3, externo: 4 }
|
|
|
|
ROLES_LABEL = {
|
|
'admin' => 'Administrador',
|
|
'gerente' => 'Gerente',
|
|
'operador' => 'Operador',
|
|
'motorista' => 'Motorista',
|
|
'externo' => 'Externo (só dashboard)'
|
|
}.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?
|
|
|
|
# E-mail em branco vira NULL: evita colisão no índice único quando vários
|
|
# motoristas são criados sem e-mail (no Postgres NULLs são distintos entre si).
|
|
before_validation :normalizar_email_em_branco
|
|
|
|
# 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
|
|
|
|
# Conta desativada não entra — nem por e-mail/senha.
|
|
#
|
|
# Só o login por PIN checava `ativo?` (Users::SessionsController), então
|
|
# desativar um gerente não impedia nada, apesar do formulário prometer
|
|
# "usuários inativos não conseguem fazer login". Devise chama este método em
|
|
# todo login e em toda request de sessão existente, então desativar alguém já
|
|
# derruba a sessão aberta dele.
|
|
def active_for_authentication?
|
|
super && ativo?
|
|
end
|
|
|
|
def inactive_message
|
|
ativo? ? super : :conta_desativada
|
|
end
|
|
|
|
# ── Permissões ──────────────────────────────────────────────
|
|
# O que o usuário pode fazer sai do PERFIL DE ACESSO (PerfilAcesso), que o ADM
|
|
# monta na tela. Enquanto ele não tem perfil, vale o padrão do papel legado
|
|
# (Permissao::PADRAO_POR_ROLE) — é o que mantém o sistema funcionando durante a
|
|
# migração e o que faz um usuário recém-criado não ficar sem nada.
|
|
def pode?(chave)
|
|
permissoes_efetivas.include?(chave.to_s)
|
|
end
|
|
|
|
# Set (não Array): `pode?` é chamado dezenas de vezes por render de tela — uma
|
|
# vez por item de menu e por botão.
|
|
#
|
|
# ⚠️ A primeira linha existe para NÃO tocar o banco quando não há perfil: os
|
|
# specs de policy usam `build(:admin)` (objeto sem id), e `perfil_acesso`
|
|
# dispararia consulta com id nulo.
|
|
def permissoes_efetivas
|
|
@permissoes_efetivas ||=
|
|
if perfil_acesso_id.nil?
|
|
Set.new(Permissao.do_role(role))
|
|
else
|
|
Set.new(perfil_acesso&.permissoes_validas || Permissao.do_role(role))
|
|
end
|
|
end
|
|
|
|
# A coluna nasce numa migration própria. Se o código subir antes de `db:migrate`
|
|
# rodar, `perfil_acesso_id` não existe e TODA tela quebraria por NoMethodError —
|
|
# aqui o sistema simplesmente continua no padrão do papel até a coluna chegar.
|
|
# Pode sair depois que a migration estiver aplicada em produção.
|
|
def perfil_acesso_id
|
|
return nil unless self.class.column_names.include?('perfil_acesso_id')
|
|
|
|
super
|
|
end
|
|
|
|
# Trocar o perfil no meio da request (tela de usuários) tem que refletir.
|
|
def recarregar_permissoes!
|
|
@permissoes_efetivas = nil
|
|
self
|
|
end
|
|
|
|
# Primeira tela que este usuário pode abrir, na ordem em que o menu as mostra.
|
|
#
|
|
# Existe para não haver LOOP: a raiz do app é o dashboard e o tratamento de
|
|
# "acesso negado" redireciona para a raiz — um perfil sem `dashboard.ver`
|
|
# entraria em nega → raiz → nega para sempre. `nil` significa "não pode abrir
|
|
# nada", e quem chama mostra a tela de sem-acesso.
|
|
HOME_POR_PERMISSAO = [
|
|
['dashboard.ver', :dashboard_path],
|
|
['dashboard.operacoes', :operacoes_dashboard_path],
|
|
['consolidacao.ver', :consolidacoes_path],
|
|
['admin.usuarios', :admin_usuarios_path],
|
|
['admin.perfis', :admin_perfis_path],
|
|
['admin.configuracoes', :admin_configuracoes_path],
|
|
['admin.romaneio', :admin_romaneios_path],
|
|
['notificacao.contatos', :admin_contatos_path],
|
|
['notificacao.eventos', :admin_eventos_path],
|
|
['notificacao.envios', :admin_envios_path]
|
|
].freeze
|
|
|
|
def home_rota
|
|
return :motorista_dashboard_path if motorista?
|
|
|
|
HOME_POR_PERMISSAO.find { |chave, _| pode?(chave) }&.last
|
|
end
|
|
|
|
# Mantidos porque a navbar e vários controllers já os chamam — agora são só
|
|
# apelidos das chaves correspondentes.
|
|
def pode_ver_config?
|
|
pode?('admin.configuracoes')
|
|
end
|
|
|
|
def pode_consolidar?
|
|
pode?('consolidacao.ver')
|
|
end
|
|
|
|
# ── Trava do último administrador ───────────────────────────
|
|
# Quem, ATIVO, ainda consegue reabrir a porta do sistema (mexer em perfis e em
|
|
# usuários). Sem isto, o ADM consegue se trancar para fora: a tela que controla
|
|
# acesso é justamente a que ele perderia, e a volta só existiria por console.
|
|
#
|
|
# `ignorando` simula "e se esta pessoa for desativada, excluída ou trocada de
|
|
# perfil?" — passe o usuário que está sendo alterado.
|
|
def self.administradores_de_acesso(ignorando: nil)
|
|
ativos.includes(:perfil_acesso)
|
|
.reject { |u| ignorando && u.id == ignorando.id }
|
|
.select { |u| u.pode?('admin.perfis') && u.pode?('admin.usuarios_gerenciar') }
|
|
end
|
|
|
|
def self.ultimo_administrador?(usuario)
|
|
return false unless usuario.pode?('admin.perfis') && usuario.pode?('admin.usuarios_gerenciar')
|
|
|
|
administradores_de_acesso(ignorando: usuario).empty?
|
|
end
|
|
|
|
# Token de login rápido via QR Code (extrato) — Fase 7/8
|
|
def regenerar_login_token!
|
|
update!(login_token: SecureRandom.urlsafe_base64(24))
|
|
end
|
|
|
|
private
|
|
|
|
def normalizar_email_em_branco
|
|
self.email = nil if email.blank?
|
|
end
|
|
|
|
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
|