Implandação da customização das permissões dos usuarios e das notificaçoes
This commit is contained in:
@@ -78,7 +78,13 @@ class EventoNotificacao < ApplicationRecord
|
||||
t&.utilizavel? ? t : nil
|
||||
end
|
||||
|
||||
def variaveis = Notificacao::Variaveis.para(gatilho)
|
||||
# Todas as variáveis do sistema, agrupadas por origem e já marcadas com
|
||||
# `aplicavel:` para este gatilho — a lista curta de antes obrigava deploy para
|
||||
# o ADM escrever qualquer número no texto.
|
||||
def variaveis = Notificacao::CatalogoVariaveis.por_origem(gatilho)
|
||||
|
||||
# Só os nomes que ESTE gatilho preenche (usado pela invariante dos specs).
|
||||
def variaveis_do_contexto = Notificacao::Variaveis.nomes(gatilho)
|
||||
|
||||
# Contatos que devem receber este evento, sem repetição — a mesma pessoa pode
|
||||
# estar em dois grupos assinantes.
|
||||
|
||||
@@ -39,7 +39,7 @@ class MensagemTemplate < ApplicationRecord
|
||||
end
|
||||
|
||||
def preview(dados = nil)
|
||||
dados ||= Notificacao::Variaveis.amostra(evento_notificacao&.gatilho)
|
||||
dados ||= Notificacao::CatalogoVariaveis.amostra(evento_notificacao&.gatilho)
|
||||
r = renderizador(dados)
|
||||
email? ? r.html : r.texto
|
||||
end
|
||||
|
||||
62
app/models/perfil_acesso.rb
Normal file
62
app/models/perfil_acesso.rb
Normal file
@@ -0,0 +1,62 @@
|
||||
# app/models/perfil_acesso.rb
|
||||
#
|
||||
# O conjunto de permissões que o ADM monta na tela e atribui a um usuário.
|
||||
# O catálogo do que PODE ser marcado é código (Permissao::TODAS); aqui fica só a
|
||||
# escolha do ADM.
|
||||
class PerfilAcesso < ApplicationRecord
|
||||
# Nome da tabela fixado: o Inflector pt-BR resolveria `perfil_acesso` como
|
||||
# "perfil_acessos" (mesma pegadinha de ConsolidacaoMotorista e das tabelas de
|
||||
# notificação). A tabela real é `perfis_acesso`.
|
||||
self.table_name = 'perfis_acesso'
|
||||
|
||||
has_many :users, foreign_key: :perfil_acesso_id, dependent: :nullify, inverse_of: :perfil_acesso
|
||||
|
||||
validates :nome, presence: true
|
||||
validates :nome, uniqueness: { case_sensitive: false }
|
||||
|
||||
before_validation :normalizar_permissoes
|
||||
|
||||
scope :ativos, -> { where(ativo: true) }
|
||||
scope :ordenados, -> { order(Arel.sql('LOWER(nome)')) }
|
||||
scope :de_sistema, -> { where(sistema: true) }
|
||||
|
||||
# Chaves que ainda existem no catálogo. Uma permissão removida do código não
|
||||
# pode travar um perfil salvo meses atrás — ela simplesmente deixa de valer.
|
||||
def permissoes_validas
|
||||
Permissao.filtrar(permissoes)
|
||||
end
|
||||
|
||||
def pode?(chave)
|
||||
permissoes_validas.include?(chave.to_s)
|
||||
end
|
||||
|
||||
# Perfil de sistema é editável e desativável, mas não apagável (apagar deixaria
|
||||
# usuários órfãos e a tela sem ponto de partida). Perfil em uso também não sai:
|
||||
# o usuário cairia no fallback do papel sem ninguém perceber.
|
||||
def apagavel?
|
||||
!sistema? && users.none?
|
||||
end
|
||||
|
||||
# Este perfil ainda consegue reabrir a porta do sistema? É o que a trava do
|
||||
# último administrador pergunta antes de deixar salvar.
|
||||
def administra_acesso?
|
||||
ativo? && pode?('admin.perfis') && pode?('admin.usuarios_gerenciar')
|
||||
end
|
||||
|
||||
def total_permissoes
|
||||
permissoes_validas.size
|
||||
end
|
||||
|
||||
# Um perfil que não enxerga nada existe (é uma forma de suspender acesso sem
|
||||
# desativar a conta), mas a tela precisa avisar.
|
||||
def vazio?
|
||||
permissoes_validas.empty?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def normalizar_permissoes
|
||||
self.nome = nome.to_s.strip
|
||||
self.permissoes = Permissao.filtrar(permissoes)
|
||||
end
|
||||
end
|
||||
245
app/models/permissao.rb
Normal file
245
app/models/permissao.rb
Normal file
@@ -0,0 +1,245 @@
|
||||
# app/models/permissao.rb
|
||||
#
|
||||
# Catálogo das permissões do sistema — fonte ÚNICA de "o que dá para liberar".
|
||||
# Não é tabela: permissão é código (cada chave corresponde a um `authorize` real),
|
||||
# então versioná-la no banco só criaria duas verdades. O que o ADM cadastra é o
|
||||
# PERFIL (PerfilAcesso), que guarda uma lista destas chaves.
|
||||
#
|
||||
# REGRA DE GRANULARIDADE: uma chave por TELA (`ver`) e uma chave a mais só quando
|
||||
# a ação tem régua diferente do "ver" — foi assim que o sistema já se comportava
|
||||
# (ex.: ver consolidação x registrar pagamento). Quebrar mais fino do que isso
|
||||
# encheria a tela de perfil de caixinhas que ninguém sabe responder.
|
||||
#
|
||||
# ⚠️ Toda chave nova precisa (1) entrar aqui, (2) ser usada por alguma policy ou
|
||||
# view e (3) entrar em PADRAO_POR_ROLE se algum papel legado já tinha aquele
|
||||
# acesso — senão o fallback silenciosamente tira acesso de quem tinha.
|
||||
module Permissao
|
||||
# Ordem em que os grupos aparecem na tela de perfil.
|
||||
GRUPOS = {
|
||||
'dashboard' => 'Dashboard e operações',
|
||||
'consolidacao' => 'Consolidações e pagamento',
|
||||
'notificacao' => 'Notificações',
|
||||
'admin' => 'Administração'
|
||||
}.freeze
|
||||
|
||||
# chave => { grupo, label (o que o ADM lê), descricao (o efeito real) }
|
||||
TODAS = {
|
||||
# ── Dashboard ────────────────────────────────────────────
|
||||
'dashboard.ver' => {
|
||||
grupo: 'dashboard', label: 'Ver o dashboard',
|
||||
descricao: 'Tela inicial com os números do período.'
|
||||
},
|
||||
'dashboard.financeiro' => {
|
||||
grupo: 'dashboard', label: 'Ver custos e pagamentos',
|
||||
descricao: 'Bloco financeiro, relatório em PDF e a lista de entregas em aberto.'
|
||||
},
|
||||
'dashboard.operacoes' => {
|
||||
grupo: 'dashboard', label: 'Ver o dashboard de operações',
|
||||
descricao: 'Entregas por operação, insucessos, mapa e planilha espelho.'
|
||||
},
|
||||
'dashboard.exportar' => {
|
||||
grupo: 'dashboard', label: 'Baixar a planilha da operação',
|
||||
descricao: 'Download do .xlsx que é entregue ao cliente — é dado saindo do sistema.'
|
||||
},
|
||||
'dashboard.metricas' => {
|
||||
grupo: 'dashboard', label: 'Consultar as métricas pela API',
|
||||
descricao: 'Endpoint interno /api/v1/dashboard/metricas.'
|
||||
},
|
||||
|
||||
# ── Consolidações ────────────────────────────────────────
|
||||
'consolidacao.ver' => {
|
||||
grupo: 'consolidacao', label: 'Ver consolidações',
|
||||
descricao: 'Lista, arquivadas, detalhe e prévia do extrato.'
|
||||
},
|
||||
'consolidacao.criar' => {
|
||||
grupo: 'consolidacao', label: 'Criar consolidação',
|
||||
descricao: 'Nova consolidação e nota avulsa.'
|
||||
},
|
||||
'consolidacao.editar' => {
|
||||
grupo: 'consolidacao', label: 'Classificar e lançar',
|
||||
descricao: 'Wizard, classificação das entregas, apontamentos e veículos.'
|
||||
},
|
||||
'consolidacao.editar_finalizada' => {
|
||||
grupo: 'consolidacao', label: 'Mexer depois de finalizada',
|
||||
descricao: 'Alterar uma consolidação já fechada — muda valor que o motorista vai receber.'
|
||||
},
|
||||
'consolidacao.finalizar' => {
|
||||
grupo: 'consolidacao', label: 'Finalizar consolidação',
|
||||
descricao: 'Fechar o período (inclusive o fechamento parcial).'
|
||||
},
|
||||
'consolidacao.arquivar' => {
|
||||
grupo: 'consolidacao', label: 'Arquivar e reativar',
|
||||
descricao: 'Tirar da lista sem apagar, e restaurar.'
|
||||
},
|
||||
'consolidacao.excluir' => {
|
||||
grupo: 'consolidacao', label: 'Excluir de vez',
|
||||
descricao: 'Apagar consolidação ou remover um motorista definitivamente.'
|
||||
},
|
||||
'consolidacao.registrar_pagamento' => {
|
||||
grupo: 'consolidacao', label: 'Registrar pagamento',
|
||||
descricao: 'Marcar motorista como pago (forma de pagamento e nota fiscal).'
|
||||
},
|
||||
'consolidacao.cancelar_pagamento' => {
|
||||
grupo: 'consolidacao', label: 'Estornar pagamento',
|
||||
descricao: 'Desfazer um pagamento já registrado.'
|
||||
},
|
||||
'consolidacao.gerir_motoristas' => {
|
||||
grupo: 'consolidacao', label: 'Adicionar e arquivar motorista',
|
||||
descricao: 'Quem entra e quem sai do fechamento (sem excluir).'
|
||||
},
|
||||
'consolidacao.exportar_pdf' => {
|
||||
grupo: 'consolidacao', label: 'Baixar relatórios em PDF',
|
||||
descricao: 'Extrato do motorista, relatório individual, financeiro e totais.'
|
||||
},
|
||||
|
||||
# ── Notificações ─────────────────────────────────────────
|
||||
'notificacao.contatos' => {
|
||||
grupo: 'notificacao', label: 'Ver contatos e grupos',
|
||||
descricao: 'Quem recebe as mensagens.'
|
||||
},
|
||||
'notificacao.contatos_gerenciar' => {
|
||||
grupo: 'notificacao', label: 'Criar e editar contatos/grupos',
|
||||
descricao: 'Cadastro de destinatários e de grupos de WhatsApp.'
|
||||
},
|
||||
'notificacao.contatos_excluir' => {
|
||||
grupo: 'notificacao', label: 'Excluir contatos/grupos',
|
||||
descricao: 'Remoção definitiva de destinatário.'
|
||||
},
|
||||
'notificacao.eventos' => {
|
||||
grupo: 'notificacao', label: 'Ver eventos',
|
||||
descricao: 'Lista dos avisos configurados e seus gatilhos.'
|
||||
},
|
||||
'notificacao.eventos_gerenciar' => {
|
||||
grupo: 'notificacao', label: 'Criar e editar eventos',
|
||||
descricao: 'Gatilho, grupos assinantes e o editor de mensagem.'
|
||||
},
|
||||
'notificacao.disparar_manual' => {
|
||||
grupo: 'notificacao', label: 'Disparar mensagem agora',
|
||||
descricao: 'Botão que envia de verdade, em nome da empresa.'
|
||||
},
|
||||
'notificacao.variaveis' => {
|
||||
grupo: 'notificacao', label: 'Criar variáveis de mensagem',
|
||||
descricao: 'Cadastra {{variaveis}} próprias usadas nos textos de aviso.'
|
||||
},
|
||||
'notificacao.envios' => {
|
||||
grupo: 'notificacao', label: 'Ver o histórico de envios',
|
||||
descricao: 'O que saiu, para quem, e o que falhou.'
|
||||
},
|
||||
'notificacao.credenciais' => {
|
||||
grupo: 'notificacao', label: 'Configurar e-mail e WhatsApp',
|
||||
descricao: 'Credenciais de SMTP/Twilio e limites de envio.'
|
||||
},
|
||||
'notificacao.whatsapp_sessao' => {
|
||||
grupo: 'notificacao', label: 'Parear o WhatsApp',
|
||||
descricao: 'Ler o QR Code, testar e desconectar a sessão.'
|
||||
},
|
||||
|
||||
# ── Administração ────────────────────────────────────────
|
||||
'admin.usuarios' => {
|
||||
grupo: 'admin', label: 'Ver usuários',
|
||||
descricao: 'Lista de quem tem acesso ao sistema.'
|
||||
},
|
||||
'admin.usuarios_gerenciar' => {
|
||||
grupo: 'admin', label: 'Criar, editar e desativar usuários',
|
||||
descricao: 'Inclui definir o tipo de conta e o perfil de acesso de cada um.'
|
||||
},
|
||||
'admin.perfis' => {
|
||||
grupo: 'admin', label: 'Gerenciar perfis de acesso',
|
||||
descricao: 'Criar perfis e marcar o que cada um enxerga — esta tela.'
|
||||
},
|
||||
'admin.configuracoes' => {
|
||||
grupo: 'admin', label: 'Ver configurações',
|
||||
descricao: 'Tabela de preços e parâmetros do sistema.'
|
||||
},
|
||||
'admin.configuracoes_editar' => {
|
||||
grupo: 'admin', label: 'Alterar preços',
|
||||
descricao: 'Muda o valor de todo fechamento feito daqui para a frente.'
|
||||
},
|
||||
'admin.auditoria' => {
|
||||
grupo: 'admin', label: 'Ver a auditoria',
|
||||
descricao: 'Registro de quem fez o quê no sistema.'
|
||||
},
|
||||
'admin.edicao_lancamento' => {
|
||||
grupo: 'admin', label: 'Corrigir lançamento no SimpliRoute',
|
||||
descricao: 'Escreve na API do rastreio — altera o dado de origem.'
|
||||
},
|
||||
'admin.planilha_simpli_route' => {
|
||||
grupo: 'admin', label: 'Gerar planilha de carga',
|
||||
descricao: 'Download do .xlsx de importação do SimpliRoute.'
|
||||
}
|
||||
}.freeze
|
||||
|
||||
# O que cada papel legado podia fazer ANTES dos perfis existirem. É o fallback
|
||||
# de quem ainda não tem perfil (usuário recém-criado, objeto não persistido nos
|
||||
# specs) e a receita dos perfis pré-prontos criados na migration de dados.
|
||||
#
|
||||
# ⚠️ Reproduz o comportamento atual de propósito — com duas exceções
|
||||
# deliberadas, que são justamente o que o ADM pediu para consertar:
|
||||
# • `externo` perde `dashboard.financeiro` (hoje enxerga custo e pagamento
|
||||
# porque o DashboardController não passa pelo Pundit);
|
||||
# • `operador` não recebe `admin.auditoria` (hoje a URL responde para ele por
|
||||
# causa do `policy_class: ApplicationPolicy`, mas o menu nunca ofereceu).
|
||||
PADRAO_POR_ROLE = {
|
||||
'admin' => :todas,
|
||||
'gerente' => %w[
|
||||
dashboard.ver dashboard.financeiro dashboard.operacoes dashboard.exportar dashboard.metricas
|
||||
consolidacao.ver consolidacao.criar consolidacao.editar consolidacao.editar_finalizada
|
||||
consolidacao.finalizar consolidacao.arquivar consolidacao.excluir
|
||||
consolidacao.registrar_pagamento consolidacao.cancelar_pagamento
|
||||
consolidacao.gerir_motoristas consolidacao.exportar_pdf
|
||||
notificacao.contatos notificacao.contatos_gerenciar notificacao.envios
|
||||
admin.usuarios admin.configuracoes admin.auditoria admin.planilha_simpli_route
|
||||
].freeze,
|
||||
'operador' => %w[
|
||||
dashboard.ver dashboard.financeiro dashboard.operacoes dashboard.exportar dashboard.metricas
|
||||
consolidacao.ver consolidacao.criar consolidacao.editar consolidacao.finalizar
|
||||
consolidacao.gerir_motoristas consolidacao.exportar_pdf
|
||||
].freeze,
|
||||
'externo' => %w[dashboard.ver dashboard.operacoes].freeze,
|
||||
# Motorista não usa permissão: o que ele acessa é a área própria dele, que
|
||||
# continua governada pelo tipo de conta (Motorista::DashboardController).
|
||||
'motorista' => [].freeze
|
||||
}.freeze
|
||||
|
||||
module_function
|
||||
|
||||
def chaves
|
||||
@chaves ||= TODAS.keys.freeze
|
||||
end
|
||||
|
||||
def existe?(chave)
|
||||
TODAS.key?(chave.to_s)
|
||||
end
|
||||
|
||||
def label(chave)
|
||||
TODAS.dig(chave.to_s, :label) || chave.to_s
|
||||
end
|
||||
|
||||
def descricao(chave)
|
||||
TODAS.dig(chave.to_s, :descricao)
|
||||
end
|
||||
|
||||
def grupo_label(grupo)
|
||||
GRUPOS[grupo.to_s] || grupo.to_s
|
||||
end
|
||||
|
||||
# { 'dashboard' => [['dashboard.ver', {...}], ...], ... } na ordem de GRUPOS —
|
||||
# é como a tela de perfil desenha as seções.
|
||||
def por_grupo
|
||||
@por_grupo ||= GRUPOS.keys.index_with { |g| TODAS.select { |_, cfg| cfg[:grupo] == g }.to_a }.freeze
|
||||
end
|
||||
|
||||
# Chaves do papel legado. `:todas` (admin) vira a lista inteira.
|
||||
def do_role(role)
|
||||
padrao = PADRAO_POR_ROLE[role.to_s]
|
||||
return chaves if padrao == :todas
|
||||
|
||||
padrao || []
|
||||
end
|
||||
|
||||
# Descarta o que não existe no catálogo. Uma chave removida do código não pode
|
||||
# travar o cadastro de um perfil salvo meses atrás — ela simplesmente some.
|
||||
def filtrar(lista)
|
||||
Array(lista).map(&:to_s).uniq.select { |chave| existe?(chave) }
|
||||
end
|
||||
end
|
||||
@@ -6,6 +6,10 @@ class User < ApplicationRecord
|
||||
: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.
|
||||
@@ -57,12 +61,112 @@ class User < ApplicationRecord
|
||||
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],
|
||||
['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?
|
||||
admin? || gerente?
|
||||
pode?('admin.configuracoes')
|
||||
end
|
||||
|
||||
def pode_consolidar?
|
||||
admin? || gerente? || operador?
|
||||
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
|
||||
|
||||
46
app/models/variavel_personalizada.rb
Normal file
46
app/models/variavel_personalizada.rb
Normal file
@@ -0,0 +1,46 @@
|
||||
# app/models/variavel_personalizada.rb
|
||||
#
|
||||
# Variável criada pelo ADM, com valor fixo, usável em qualquer mensagem.
|
||||
class VariavelPersonalizada < ApplicationRecord
|
||||
# Nome da tabela fixado: o Inflector pt-BR não resolve variavel → variaveis.
|
||||
self.table_name = 'variaveis_personalizadas'
|
||||
|
||||
# Mesmo formato que o renderizador aceita em {{...}}: letras minúsculas,
|
||||
# números e _ . Sem isso, o ADM cadastraria "Telefone Suporte" e a variável
|
||||
# nunca seria substituída — falha muda dentro da mensagem enviada.
|
||||
FORMATO_CHAVE = /\A[a-z][a-z0-9_]*\z/
|
||||
|
||||
before_validation :normalizar
|
||||
|
||||
validates :chave, presence: true, uniqueness: { case_sensitive: false },
|
||||
format: { with: FORMATO_CHAVE,
|
||||
message: 'use apenas letras minúsculas, números e _ (ex.: telefone_suporte)' }
|
||||
validates :valor, presence: true
|
||||
validate :nao_colidir_com_variavel_do_sistema
|
||||
|
||||
scope :ativas, -> { where(ativo: true) }
|
||||
scope :ordenadas, -> { order(:chave) }
|
||||
|
||||
def marcador = "{{#{chave}}}"
|
||||
|
||||
private
|
||||
|
||||
def normalizar
|
||||
self.chave = chave.to_s.strip.downcase.tr(' ', '_')
|
||||
self.valor = valor.to_s.strip
|
||||
end
|
||||
|
||||
# Deixar o ADM criar {{valor}} ou {{motorista}} faria a variável dele
|
||||
# sobrescrever (ou ser sobrescrita por) a do sistema, dependendo da ordem —
|
||||
# exatamente o tipo de bug que não aparece no preview e aparece no envio.
|
||||
def nao_colidir_com_variavel_do_sistema
|
||||
return if chave.blank?
|
||||
|
||||
reservadas = Notificacao::CatalogoVariaveis::SISTEMA.keys +
|
||||
Notificacao::CatalogoVariaveis::CONTEXTO.keys +
|
||||
Notificacao::CatalogoVariaveis::DADOS.keys
|
||||
return unless reservadas.include?(chave)
|
||||
|
||||
errors.add(:chave, "já existe no sistema — escolha outro nome (ex.: #{chave}_empresa)")
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user