Implandação da customização das permissões dos usuarios e das notificaçoes
This commit is contained in:
@@ -82,22 +82,54 @@
|
||||
</div>
|
||||
|
||||
<%# ── Variáveis ─────────────────────────────────────── %>
|
||||
<%# ── EMOJIS ─────────────────────────────────────────
|
||||
Digitar emoji no teclado do computador é o passo que trava o ADM;
|
||||
aqui ele clica. A lista é curta e curada de propósito — um seletor
|
||||
completo viraria uma biblioteca externa (e CSP) para inserir 3
|
||||
caracteres. %>
|
||||
<div class="bg-[#1a1a1a] rounded-2xl border border-white/5 p-4 mb-4">
|
||||
<p class="text-xs font-semibold text-gray-400 uppercase tracking-wider mb-3">Emojis</p>
|
||||
<div class="flex flex-wrap gap-1">
|
||||
<% %w[✅ ❌ ⚠️ 📌 📢 💰 💵 🧾 📅 🕒 🚚 📦 📍 👤 👍 🙏 ⭐ 🔴 🟢 🟡 ➡️ 📄 📷 🔔 ✍️ 😀 🎉].each do |emoji| %>
|
||||
<button type="button" class="chip-emoji w-9 h-9 rounded-lg bg-[#0a0a0a] border border-white/10
|
||||
hover:border-[#f97316] text-lg leading-none"
|
||||
data-emoji="<%= emoji %>" title="Inserir <%= emoji %>"><%= emoji %></button>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-[#1a1a1a] rounded-2xl border border-white/5 p-4">
|
||||
<p class="text-xs font-semibold text-gray-400 uppercase tracking-wider mb-3">Variáveis</p>
|
||||
<p class="text-xs text-gray-500 mb-3">
|
||||
Clique num campo da mensagem e depois numa variável para inseri-la ali.
|
||||
</p>
|
||||
<div class="flex flex-wrap gap-1.5">
|
||||
<% @variaveis.each do |nome, meta| %>
|
||||
<button type="button" title="<%= meta[:descricao] %> (ex.: <%= meta[:amostra] %>)"
|
||||
class="chip-variavel px-2.5 py-1 rounded-full bg-[#f97316]/10 border border-[#f97316]/30
|
||||
text-xs font-mono text-[#f97316] hover:bg-[#f97316]/20 hover:border-[#f97316]
|
||||
transition-colors cursor-pointer"
|
||||
data-variavel="<%= nome %>">
|
||||
<%= "{{#{nome}}}" %>
|
||||
</button>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<%# Agrupadas por ORIGEM e sem esconder o que não se aplica: a variável
|
||||
de outro gatilho aparece apagada, com o aviso de que sai em branco.
|
||||
Antes, a tela só listava as 6 do contexto — qualquer outro dado
|
||||
exigia deploy. %>
|
||||
<% @variaveis.each do |origem, itens| %>
|
||||
<% next if itens.empty? %>
|
||||
<p class="text-[11px] uppercase tracking-wider text-gray-500 mt-3 mb-1.5">
|
||||
<%= Notificacao::CatalogoVariaveis::ORIGENS[origem] %>
|
||||
</p>
|
||||
<div class="flex flex-wrap gap-1.5">
|
||||
<% itens.each do |v| %>
|
||||
<button type="button"
|
||||
title="<%= v[:descricao] %> (ex.: <%= v[:amostra] %>)<%= ' — não existe neste gatilho: sai em branco' unless v[:aplicavel] %>"
|
||||
class="chip-variavel px-2.5 py-1 rounded-full text-xs font-mono transition-colors cursor-pointer
|
||||
<%= v[:aplicavel] ? 'bg-[#f97316]/10 border border-[#f97316]/30 text-[#f97316] hover:bg-[#f97316]/20 hover:border-[#f97316]' : 'bg-white/5 border border-white/10 text-gray-500 hover:border-gray-500' %>"
|
||||
data-variavel="<%= v[:chave] %>">
|
||||
{{<%= v[:chave] %>}}<%= ' ·' unless v[:aplicavel] %>
|
||||
</button>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<p class="text-[11px] text-gray-500 mt-3 leading-relaxed">
|
||||
As apagadas (·) não existem neste gatilho e sairiam em branco.
|
||||
<%= link_to 'Criar variável própria', admin_variaveis_path, class: 'text-orange-500 hover:underline' %>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -312,20 +344,29 @@
|
||||
});
|
||||
}
|
||||
|
||||
// ── Variáveis: insere no cursor do último campo focado ─────
|
||||
// ── Inserção no cursor do último campo focado ──────────────
|
||||
// Serve para variável e para emoji: a mecânica é a mesma (recortar o valor
|
||||
// na posição do cursor e reemitir `input` para o JSON do editor atualizar).
|
||||
function inserirNoCampo(texto) {
|
||||
if (!ultimoCampo) { alert('Clique primeiro no campo onde o texto deve entrar.'); return; }
|
||||
var ini = ultimoCampo.selectionStart != null ? ultimoCampo.selectionStart : ultimoCampo.value.length;
|
||||
var fim = ultimoCampo.selectionEnd != null ? ultimoCampo.selectionEnd : ini;
|
||||
ultimoCampo.value = ultimoCampo.value.slice(0, ini) + texto + ultimoCampo.value.slice(fim);
|
||||
ultimoCampo.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
ultimoCampo.focus();
|
||||
ultimoCampo.selectionStart = ultimoCampo.selectionEnd = ini + texto.length;
|
||||
}
|
||||
|
||||
document.querySelectorAll('.chip-variavel').forEach(function (chip) {
|
||||
chip.addEventListener('click', function () {
|
||||
var texto = '{{' + chip.dataset.variavel + '}}';
|
||||
if (!ultimoCampo) { alert('Clique primeiro no campo onde a variável deve entrar.'); return; }
|
||||
var ini = ultimoCampo.selectionStart != null ? ultimoCampo.selectionStart : ultimoCampo.value.length;
|
||||
var fim = ultimoCampo.selectionEnd != null ? ultimoCampo.selectionEnd : ini;
|
||||
ultimoCampo.value = ultimoCampo.value.slice(0, ini) + texto + ultimoCampo.value.slice(fim);
|
||||
ultimoCampo.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
ultimoCampo.focus();
|
||||
ultimoCampo.selectionStart = ultimoCampo.selectionEnd = ini + texto.length;
|
||||
inserirNoCampo('{{' + chip.dataset.variavel + '}}');
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('.chip-emoji').forEach(function (chip) {
|
||||
chip.addEventListener('click', function () { inserirNoCampo(chip.dataset.emoji); });
|
||||
});
|
||||
|
||||
// ── Preview: renderizado no servidor, pelo mesmo código do envio ──
|
||||
var token = document.querySelector('meta[name="csrf-token"]');
|
||||
document.getElementById('btn-preview').addEventListener('click', function () {
|
||||
|
||||
83
app/views/admin/perfis_acesso/_form.html.erb
Normal file
83
app/views/admin/perfis_acesso/_form.html.erb
Normal file
@@ -0,0 +1,83 @@
|
||||
<%# app/views/admin/perfis_acesso/_form.html.erb
|
||||
O ADM marca o que o perfil enxerga. As caixas vêm agrupadas por área do
|
||||
catálogo (Permissao.por_grupo) e cada uma traz a descrição do EFEITO real —
|
||||
"Registrar pagamento" sem a linha de baixo não diz se inclui estornar. %>
|
||||
<%= form_with model: [:admin, perfil], url: (perfil.persisted? ? admin_perfil_path(perfil) : admin_perfis_path),
|
||||
data: { turbo: false }, class: 'space-y-6' do |f| %>
|
||||
|
||||
<% if perfil.errors.any? %>
|
||||
<div class="bg-red-500/10 border border-red-500/30 rounded-xl p-4">
|
||||
<p class="text-red-400 font-semibold mb-1"><%= icone :erro, cor: 'text-red-400' %> Não foi possível salvar</p>
|
||||
<ul class="text-red-300 text-sm list-disc list-inside space-y-0.5">
|
||||
<% perfil.errors.full_messages.each do |msg| %><li><%= msg %></li><% end %>
|
||||
</ul>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%# ── Identificação ──────────────────────────────────── %>
|
||||
<div class="bg-[#1a1a1a] border border-[#2a2a2a] rounded-2xl p-6 space-y-4">
|
||||
<div>
|
||||
<%= f.label :nome, 'Nome do perfil', class: 'block text-sm font-medium text-gray-300 mb-1.5' %>
|
||||
<%= f.text_field :nome, required: true, placeholder: 'Ex.: Gerente sem configurações',
|
||||
class: 'w-full px-4 py-3 bg-[#0a0a0a] border border-white/10 rounded-xl text-white
|
||||
focus:outline-none focus:border-[#f97316]' %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= f.label :descricao, 'Para que serve', class: 'block text-sm font-medium text-gray-300 mb-1.5' %>
|
||||
<%= f.text_field :descricao, placeholder: 'Quem usa este perfil e por quê',
|
||||
class: 'w-full px-4 py-3 bg-[#0a0a0a] border border-white/10 rounded-xl text-white
|
||||
focus:outline-none focus:border-[#f97316]' %>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<%= f.label :ativo, 'Perfil ativo', class: 'block text-sm font-medium text-gray-300' %>
|
||||
<p class="text-gray-500 text-xs mt-0.5">Desativado, quem estiver nele perde os acessos marcados aqui.</p>
|
||||
</div>
|
||||
<%= f.check_box :ativo, class: 'w-5 h-5 accent-orange-500' %>
|
||||
</div>
|
||||
|
||||
<% if perfil.sistema? %>
|
||||
<p class="text-amber-400 text-sm">
|
||||
<%= icone :alerta, cor: 'text-amber-400' %> Perfil que vem com o sistema: pode ser editado e desativado, mas não excluído.
|
||||
</p>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<%# ── Permissões ─────────────────────────────────────────
|
||||
O campo vazio antes das caixas é obrigatório: sem ele, desmarcar TUDO faria
|
||||
o parâmetro sumir do POST e o controller entenderia "não mexeu". %>
|
||||
<%= hidden_field_tag 'perfil_acesso[permissoes][]', '' %>
|
||||
|
||||
<% marcadas = perfil.permissoes_validas %>
|
||||
<% Permissao.por_grupo.each do |grupo, itens| %>
|
||||
<div class="bg-[#1a1a1a] border border-[#2a2a2a] rounded-2xl overflow-hidden">
|
||||
<div class="flex items-center justify-between gap-3 px-5 py-3 bg-[#0f0f0f] border-b border-[#2a2a2a]">
|
||||
<h2 class="text-white font-bold"><%= Permissao.grupo_label(grupo) %></h2>
|
||||
<span class="text-gray-500 text-xs"><%= itens.count { |chave, _| marcadas.include?(chave) } %> de <%= itens.size %></span>
|
||||
</div>
|
||||
|
||||
<div class="divide-y divide-[#2a2a2a]">
|
||||
<% itens.each do |chave, cfg| %>
|
||||
<label class="flex items-start gap-3 px-5 py-4 cursor-pointer hover:bg-[#222] min-h-[56px]">
|
||||
<%= check_box_tag 'perfil_acesso[permissoes][]', chave, marcadas.include?(chave),
|
||||
id: "perm-#{chave.tr('.', '-')}", class: 'mt-1 w-5 h-5 accent-orange-500 shrink-0' %>
|
||||
<span class="min-w-0">
|
||||
<span class="block text-white font-semibold"><%= cfg[:label] %></span>
|
||||
<span class="block text-gray-400 text-sm mt-0.5"><%= cfg[:descricao] %></span>
|
||||
</span>
|
||||
</label>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<%= f.submit perfil.persisted? ? 'Salvar perfil' : 'Criar perfil',
|
||||
class: 'bg-orange-500 hover:bg-orange-600 text-black font-bold px-6 py-3.5 rounded-xl
|
||||
min-h-[48px] cursor-pointer' %>
|
||||
<%= link_to 'Cancelar', admin_perfis_path,
|
||||
class: 'px-5 py-3.5 text-gray-300 hover:text-white border border-[#2a2a2a] rounded-xl min-h-[48px] flex items-center' %>
|
||||
</div>
|
||||
<% end %>
|
||||
10
app/views/admin/perfis_acesso/edit.html.erb
Normal file
10
app/views/admin/perfis_acesso/edit.html.erb
Normal file
@@ -0,0 +1,10 @@
|
||||
<% content_for :title, "Perfil #{@perfil.nome}" %>
|
||||
<div class="max-w-3xl">
|
||||
<h1 class="text-2xl font-bold text-white mb-1 flex items-center gap-2">
|
||||
<%= icone :usuarios, espaco: false %> <%= @perfil.nome %>
|
||||
</h1>
|
||||
<p class="text-gray-400 text-sm mb-6">
|
||||
<%= @perfil.users.count %> usuário(s) neste perfil — o que você mudar aqui vale para todos eles.
|
||||
</p>
|
||||
<%= render 'form', perfil: @perfil %>
|
||||
</div>
|
||||
70
app/views/admin/perfis_acesso/index.html.erb
Normal file
70
app/views/admin/perfis_acesso/index.html.erb
Normal file
@@ -0,0 +1,70 @@
|
||||
<%# app/views/admin/perfis_acesso/index.html.erb %>
|
||||
<% content_for :title, 'Perfis de acesso' %>
|
||||
|
||||
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-6">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-white flex items-center gap-2">
|
||||
<%= icone :usuarios, espaco: false %> Perfis de acesso
|
||||
</h1>
|
||||
<p class="text-gray-400 text-sm mt-0.5">
|
||||
O perfil define o que a pessoa enxerga e pode fazer. Mudou a regra, muda aqui — vale para todos do perfil.
|
||||
</p>
|
||||
</div>
|
||||
<%= link_to new_admin_perfil_path, data: { turbo: false },
|
||||
class: 'inline-flex items-center gap-2 px-5 py-3 bg-[#f97316] hover:bg-orange-500
|
||||
text-white font-semibold rounded-xl transition-colors min-h-[48px] whitespace-nowrap' do %>
|
||||
<%= icone :adicionar, cor: nil %> Novo perfil
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<% if @perfis.any? %>
|
||||
<div class="space-y-3">
|
||||
<% @perfis.each do |perfil| %>
|
||||
<div class="bg-[#1a1a1a] border border-[#2a2a2a] rounded-2xl p-5">
|
||||
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||
<div class="min-w-0">
|
||||
<div class="flex items-center gap-2 flex-wrap">
|
||||
<h2 class="text-white font-bold text-lg"><%= perfil.nome %></h2>
|
||||
<% if perfil.sistema? %>
|
||||
<span class="px-2.5 py-1 rounded-full text-xs font-bold bg-gray-700 text-gray-300">Do sistema</span>
|
||||
<% end %>
|
||||
<% unless perfil.ativo? %>
|
||||
<span class="px-2.5 py-1 rounded-full text-xs font-bold bg-red-600/20 text-red-400">Desativado</span>
|
||||
<% end %>
|
||||
<% if perfil.vazio? %>
|
||||
<span class="px-2.5 py-1 rounded-full text-xs font-bold bg-amber-500/20 text-amber-400">Sem nenhum acesso</span>
|
||||
<% end %>
|
||||
</div>
|
||||
<% if perfil.descricao.present? %>
|
||||
<p class="text-gray-400 text-sm mt-1"><%= perfil.descricao %></p>
|
||||
<% end %>
|
||||
<p class="text-gray-500 text-sm mt-1">
|
||||
<%= icone :pessoa %> <%= perfil.users.size %> usuário(s)
|
||||
· <%= icone :chave %> <%= perfil.total_permissoes %> de <%= Permissao.chaves.size %> permissões
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-2 shrink-0">
|
||||
<%= link_to rotulo(:editar, 'Editar', cor: nil), edit_admin_perfil_path(perfil), data: { turbo: false },
|
||||
class: 'px-4 py-3 text-gray-300 hover:text-white border border-[#2a2a2a] hover:border-orange-500 rounded-xl min-h-[48px] flex items-center' %>
|
||||
<%# button_to com BLOCO: passando o rótulo como primeiro argumento, o
|
||||
Rails escapa o HTML e o ícone sai como texto cru na tela. %>
|
||||
<% if policy(perfil).destroy? %>
|
||||
<%= button_to admin_perfil_path(perfil), method: :delete,
|
||||
class: 'px-4 py-3 text-red-400 hover:text-red-300 border border-[#2a2a2a] hover:border-red-500 rounded-xl min-h-[48px] flex items-center gap-2',
|
||||
form: { data: { turbo_confirm: "Excluir o perfil #{perfil.nome}?" } } do %>
|
||||
<%= icone :excluir, cor: nil, tamanho: 'w-4 h-4', espaco: false %> Excluir
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="bg-[#1a1a1a] border border-[#2a2a2a] rounded-2xl p-12 text-center">
|
||||
<p class="mb-4"><%= icone :vazio, tamanho: 'w-12 h-12', cor: 'text-gray-600', espaco: false %></p>
|
||||
<p class="text-gray-300">Nenhum perfil cadastrado ainda.</p>
|
||||
<p class="text-gray-500 text-sm mt-1">Enquanto não houver perfil, cada pessoa acessa conforme o tipo de conta dela.</p>
|
||||
</div>
|
||||
<% end %>
|
||||
8
app/views/admin/perfis_acesso/new.html.erb
Normal file
8
app/views/admin/perfis_acesso/new.html.erb
Normal file
@@ -0,0 +1,8 @@
|
||||
<% content_for :title, 'Novo perfil de acesso' %>
|
||||
<div class="max-w-3xl">
|
||||
<h1 class="text-2xl font-bold text-white mb-1 flex items-center gap-2">
|
||||
<%= icone :usuarios, espaco: false %> Novo perfil de acesso
|
||||
</h1>
|
||||
<p class="text-gray-400 text-sm mb-6">Marque o que este perfil enxerga. Você atribui o perfil a cada pessoa na tela de Usuários.</p>
|
||||
<%= render 'form', perfil: @perfil %>
|
||||
</div>
|
||||
@@ -40,16 +40,46 @@
|
||||
placeholder: 'João da Silva' %>
|
||||
</div>
|
||||
|
||||
<%# Perfil %>
|
||||
<div>
|
||||
<%= f.label :role, 'Perfil de acesso', class: 'block text-sm font-medium text-gray-300 mb-1.5' %>
|
||||
<%= f.select :role,
|
||||
options_for_select(role_options_for_select, usuario.role),
|
||||
{},
|
||||
class: 'w-full px-4 py-3 bg-[#0a0a0a] border border-white/10 rounded-xl text-white
|
||||
focus:outline-none focus:border-[#f97316] focus:ring-1 focus:ring-[#f97316]
|
||||
transition-colors cursor-pointer' %>
|
||||
</div>
|
||||
<%# TIPO DE CONTA x PERFIL DE ACESSO — são coisas diferentes:
|
||||
• tipo de conta define COMO a pessoa entra e onde ela vive (motorista
|
||||
entra por PIN e tem painel próprio);
|
||||
• perfil de acesso define O QUE ela enxerga (Permissao::TODAS).
|
||||
Antes este select se chamava "Perfil de acesso" e editava `role`.
|
||||
|
||||
Os dois só aparecem para quem pode alterar acesso — e nunca na própria
|
||||
ficha, senão qualquer usuário se promoveria editando o próprio cadastro
|
||||
(o controller também filtra os campos, não só a tela). %>
|
||||
<% if pode_alterar_acesso? %>
|
||||
<div>
|
||||
<%= f.label :role, 'Tipo de conta', class: 'block text-sm font-medium text-gray-300 mb-1.5' %>
|
||||
<%= f.select :role,
|
||||
options_for_select(role_options_for_select, usuario.role),
|
||||
{},
|
||||
class: 'w-full px-4 py-3 bg-[#0a0a0a] border border-white/10 rounded-xl text-white
|
||||
focus:outline-none focus:border-[#f97316] focus:ring-1 focus:ring-[#f97316]
|
||||
transition-colors cursor-pointer' %>
|
||||
</div>
|
||||
|
||||
<div id="campo-perfil-acesso">
|
||||
<%= f.label :perfil_acesso_id, 'Perfil de acesso', class: 'block text-sm font-medium text-gray-300 mb-1.5' %>
|
||||
<%= f.select :perfil_acesso_id,
|
||||
options_from_collection_for_select(PerfilAcesso.ativos.ordenados, :id, :nome, usuario.perfil_acesso_id),
|
||||
{ include_blank: 'Sem perfil (usa o padrão do tipo de conta)' },
|
||||
class: 'w-full px-4 py-3 bg-[#0a0a0a] border border-white/10 rounded-xl text-white
|
||||
focus:outline-none focus:border-[#f97316] focus:ring-1 focus:ring-[#f97316]
|
||||
transition-colors cursor-pointer' %>
|
||||
<p class="text-gray-500 text-xs mt-1.5">
|
||||
Define o que a pessoa enxerga.
|
||||
<%= link_to 'Gerenciar perfis', admin_perfis_path, class: 'text-orange-500 hover:underline' %>
|
||||
</p>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="bg-[#0a0a0a] border border-white/10 rounded-xl px-4 py-3">
|
||||
<p class="text-sm text-gray-400">Tipo de conta</p>
|
||||
<p class="text-white font-semibold"><%= usuario.role_label %></p>
|
||||
<p class="text-gray-500 text-xs mt-1">Só quem administra usuários altera tipo de conta e perfil de acesso.</p>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%# Ativo toggle %>
|
||||
<div class="flex items-center">
|
||||
@@ -142,10 +172,16 @@
|
||||
const fieldsPin = document.getElementById('fields-pin');
|
||||
if (!roleSelect || !fieldsEmail || !fieldsPin) return;
|
||||
|
||||
// Motorista não usa perfil de acesso: o que ele enxerga é o painel próprio,
|
||||
// governado pelo tipo de conta. Deixar o select à mostra sugeriria que dá
|
||||
// para liberar telas administrativas para ele.
|
||||
const campoPerfil = document.getElementById('campo-perfil-acesso');
|
||||
|
||||
function toggleFields() {
|
||||
const motorista = roleSelect.value === 'motorista';
|
||||
fieldsEmail.classList.toggle('hidden', motorista);
|
||||
fieldsPin.classList.toggle('hidden', !motorista);
|
||||
if (campoPerfil) campoPerfil.classList.toggle('hidden', motorista);
|
||||
}
|
||||
|
||||
roleSelect.addEventListener('change', toggleFields);
|
||||
|
||||
@@ -29,7 +29,8 @@
|
||||
<tr class="border-b border-white/10">
|
||||
<th class="px-6 py-4 text-left text-xs font-semibold text-gray-400 uppercase tracking-wider">Nome</th>
|
||||
<th class="px-6 py-4 text-left text-xs font-semibold text-gray-400 uppercase tracking-wider">E-mail / PIN</th>
|
||||
<th class="px-6 py-4 text-left text-xs font-semibold text-gray-400 uppercase tracking-wider">Perfil</th>
|
||||
<th class="px-6 py-4 text-left text-xs font-semibold text-gray-400 uppercase tracking-wider">Tipo de conta</th>
|
||||
<th class="px-6 py-4 text-left text-xs font-semibold text-gray-400 uppercase tracking-wider">Perfil de acesso</th>
|
||||
<th class="px-6 py-4 text-left text-xs font-semibold text-gray-400 uppercase tracking-wider">Status</th>
|
||||
<th class="px-6 py-4 text-right text-xs font-semibold text-gray-400 uppercase tracking-wider">Ações</th>
|
||||
</tr>
|
||||
@@ -56,6 +57,22 @@
|
||||
<td class="px-6 py-4">
|
||||
<%= badge_role(usuario.role) %>
|
||||
</td>
|
||||
<%# Tipo de conta x perfil são coisas diferentes: o primeiro diz como
|
||||
a pessoa entra, o segundo o que ela enxerga. Sem perfil, vale o
|
||||
padrão do tipo de conta — a tela precisa dizer isso, senão parece
|
||||
que a pessoa está sem acesso nenhum. %>
|
||||
<td class="px-6 py-4">
|
||||
<% if usuario.motorista? %>
|
||||
<span class="text-gray-500 text-sm">—</span>
|
||||
<% elsif usuario.perfil_acesso %>
|
||||
<span class="text-white text-sm font-semibold"><%= usuario.perfil_acesso.nome %></span>
|
||||
<% unless usuario.perfil_acesso.ativo? %>
|
||||
<span class="block text-red-400 text-xs">perfil desativado</span>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<span class="text-gray-400 text-sm">Padrão do tipo de conta</span>
|
||||
<% end %>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<%= badge_status_usuario(usuario.ativo?) %>
|
||||
</td>
|
||||
|
||||
55
app/views/admin/variaveis_personalizadas/_form.html.erb
Normal file
55
app/views/admin/variaveis_personalizadas/_form.html.erb
Normal file
@@ -0,0 +1,55 @@
|
||||
<%= form_with model: [:admin, variavel],
|
||||
url: (variavel.persisted? ? admin_variavel_path(variavel) : admin_variaveis_path),
|
||||
data: { turbo: false }, class: 'space-y-6' do |f| %>
|
||||
|
||||
<% if variavel.errors.any? %>
|
||||
<div class="bg-red-500/10 border border-red-500/30 rounded-xl p-4">
|
||||
<ul class="text-red-300 text-sm list-disc list-inside space-y-0.5">
|
||||
<% variavel.errors.full_messages.each do |msg| %><li><%= msg %></li><% end %>
|
||||
</ul>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="bg-[#1a1a1a] border border-[#2a2a2a] rounded-2xl p-6 space-y-4">
|
||||
<div>
|
||||
<%= f.label :chave, 'Nome da variável', class: 'block text-sm font-medium text-gray-300 mb-1.5' %>
|
||||
<%= f.text_field :chave, required: true, placeholder: 'telefone_suporte',
|
||||
class: 'w-full px-4 py-3 bg-[#0a0a0a] border border-white/10 rounded-xl text-white font-mono
|
||||
focus:outline-none focus:border-[#f97316]' %>
|
||||
<p class="text-gray-500 text-xs mt-1.5">
|
||||
Só letras minúsculas, números e <span class="font-mono">_</span>. Na mensagem você escreve
|
||||
<span class="font-mono text-orange-400">{{<%= variavel.chave.presence || 'telefone_suporte' %>}}</span>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= f.label :valor, 'Valor', class: 'block text-sm font-medium text-gray-300 mb-1.5' %>
|
||||
<%= f.text_field :valor, required: true, placeholder: '(11) 4002-8922',
|
||||
class: 'w-full px-4 py-3 bg-[#0a0a0a] border border-white/10 rounded-xl text-white
|
||||
focus:outline-none focus:border-[#f97316]' %>
|
||||
<p class="text-gray-500 text-xs mt-1.5">É este texto que aparece no lugar da variável, em toda mensagem que a usar.</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= f.label :descricao, 'Para que serve', class: 'block text-sm font-medium text-gray-300 mb-1.5' %>
|
||||
<%= f.text_field :descricao, placeholder: 'Telefone que atende o motorista',
|
||||
class: 'w-full px-4 py-3 bg-[#0a0a0a] border border-white/10 rounded-xl text-white
|
||||
focus:outline-none focus:border-[#f97316]' %>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<%= f.label :ativo, 'Variável ativa', class: 'block text-sm font-medium text-gray-300' %>
|
||||
<p class="text-gray-500 text-xs mt-0.5">Desativada, some do editor e sai vazia nas mensagens que já a usam.</p>
|
||||
</div>
|
||||
<%= f.check_box :ativo, class: 'w-5 h-5 accent-orange-500' %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<%= f.submit variavel.persisted? ? 'Salvar' : 'Criar variável',
|
||||
class: 'bg-orange-500 hover:bg-orange-600 text-black font-bold px-6 py-3.5 rounded-xl min-h-[48px] cursor-pointer' %>
|
||||
<%= link_to 'Cancelar', admin_variaveis_path,
|
||||
class: 'px-5 py-3.5 text-gray-300 hover:text-white border border-[#2a2a2a] rounded-xl min-h-[48px] flex items-center' %>
|
||||
</div>
|
||||
<% end %>
|
||||
8
app/views/admin/variaveis_personalizadas/edit.html.erb
Normal file
8
app/views/admin/variaveis_personalizadas/edit.html.erb
Normal file
@@ -0,0 +1,8 @@
|
||||
<% content_for :title, "Variável #{@variavel.chave}" %>
|
||||
<div class="max-w-2xl">
|
||||
<h1 class="text-2xl font-bold text-white mb-1 flex items-center gap-2">
|
||||
<%= icone :tag, espaco: false %> <span class="font-mono"><%= @variavel.marcador %></span>
|
||||
</h1>
|
||||
<p class="text-gray-400 text-sm mb-6">O novo valor passa a valer no próximo disparo de toda mensagem que usa esta variável.</p>
|
||||
<%= render 'form', variavel: @variavel %>
|
||||
</div>
|
||||
76
app/views/admin/variaveis_personalizadas/index.html.erb
Normal file
76
app/views/admin/variaveis_personalizadas/index.html.erb
Normal file
@@ -0,0 +1,76 @@
|
||||
<% content_for :title, 'Variáveis de mensagem' %>
|
||||
|
||||
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-6">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-white flex items-center gap-2"><%= icone :tag, espaco: false %> Variáveis de mensagem</h1>
|
||||
<p class="text-gray-400 text-sm mt-0.5">
|
||||
Escreva <span class="font-mono text-orange-400">{{assim}}</span> no texto e o sistema troca pelo valor na hora do envio.
|
||||
</p>
|
||||
</div>
|
||||
<%= link_to new_admin_variavel_path, data: { turbo: false },
|
||||
class: 'inline-flex items-center gap-2 px-5 py-3 bg-[#f97316] hover:bg-orange-500 text-white font-semibold rounded-xl min-h-[48px] whitespace-nowrap' do %>
|
||||
<%= icone :adicionar, cor: nil %> Nova variável
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<%# ── As suas ─────────────────────────────────────────── %>
|
||||
<% if @variaveis.any? %>
|
||||
<div class="space-y-3 mb-8">
|
||||
<% @variaveis.each do |v| %>
|
||||
<div class="bg-[#1a1a1a] border border-[#2a2a2a] rounded-2xl p-5 flex flex-col md:flex-row md:items-center md:justify-between gap-3">
|
||||
<div class="min-w-0">
|
||||
<div class="flex items-center gap-2 flex-wrap">
|
||||
<span class="font-mono text-orange-400 font-semibold"><%= v.marcador %></span>
|
||||
<% unless v.ativo? %>
|
||||
<span class="px-2.5 py-1 rounded-full text-xs font-bold bg-red-600/20 text-red-400">Desativada</span>
|
||||
<% end %>
|
||||
</div>
|
||||
<p class="text-white mt-1 break-words"><%= v.valor %></p>
|
||||
<% if v.descricao.present? %>
|
||||
<p class="text-gray-500 text-sm mt-0.5"><%= v.descricao %></p>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="flex gap-2 shrink-0">
|
||||
<%= link_to rotulo(:editar, 'Editar', cor: nil), edit_admin_variavel_path(v), data: { turbo: false },
|
||||
class: 'px-4 py-3 text-gray-300 hover:text-white border border-[#2a2a2a] hover:border-orange-500 rounded-xl min-h-[48px] flex items-center' %>
|
||||
<%= button_to admin_variavel_path(v), method: :delete,
|
||||
class: 'px-4 py-3 text-red-400 hover:text-red-300 border border-[#2a2a2a] hover:border-red-500 rounded-xl min-h-[48px] flex items-center gap-2',
|
||||
form: { data: { turbo_confirm: "Excluir #{v.marcador}? Mensagens que a usam passam a mostrar espaço em branco." } } do %>
|
||||
<%= icone :excluir, cor: nil, tamanho: 'w-4 h-4', espaco: false %> Excluir
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="bg-[#1a1a1a] border border-[#2a2a2a] rounded-2xl p-10 text-center mb-8">
|
||||
<p class="mb-3"><%= icone :vazio, tamanho: 'w-12 h-12', cor: 'text-gray-600', espaco: false %></p>
|
||||
<p class="text-gray-300">Você ainda não criou nenhuma variável.</p>
|
||||
<p class="text-gray-500 text-sm mt-1">Boas primeiras: telefone do suporte, horário de atendimento, endereço da base.</p>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%# ── As que já existem no sistema ─────────────────────
|
||||
Listadas aqui para o ADM não tentar cadastrar uma que já existe (o cadastro
|
||||
recusaria) e para descobrir o que dá para usar sem criar nada. %>
|
||||
<h2 class="text-white font-bold text-lg mb-1">Já disponíveis no sistema</h2>
|
||||
<p class="text-gray-400 text-sm mb-4">Use à vontade nas mensagens — não precisam ser cadastradas.</p>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-4">
|
||||
<% @do_sistema.each do |origem, itens| %>
|
||||
<% next if itens.empty? %>
|
||||
<div class="bg-[#1a1a1a] border border-[#2a2a2a] rounded-2xl overflow-hidden">
|
||||
<div class="px-5 py-3 bg-[#0f0f0f] border-b border-[#2a2a2a]">
|
||||
<h3 class="text-white font-semibold text-sm"><%= Notificacao::CatalogoVariaveis::ORIGENS[origem] %></h3>
|
||||
</div>
|
||||
<div class="divide-y divide-[#2a2a2a]">
|
||||
<% itens.each do |v| %>
|
||||
<div class="px-5 py-3">
|
||||
<span class="font-mono text-orange-400 text-sm">{{<%= v[:chave] %>}}</span>
|
||||
<p class="text-gray-400 text-xs mt-0.5"><%= v[:descricao] %></p>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
6
app/views/admin/variaveis_personalizadas/new.html.erb
Normal file
6
app/views/admin/variaveis_personalizadas/new.html.erb
Normal file
@@ -0,0 +1,6 @@
|
||||
<% content_for :title, 'Nova variável' %>
|
||||
<div class="max-w-2xl">
|
||||
<h1 class="text-2xl font-bold text-white mb-1 flex items-center gap-2"><%= icone :tag, espaco: false %> Nova variável</h1>
|
||||
<p class="text-gray-400 text-sm mb-6">Um texto fixo que você reaproveita em qualquer mensagem — mudou aqui, mudou em todas.</p>
|
||||
<%= render 'form', variavel: @variavel %>
|
||||
</div>
|
||||
Reference in New Issue
Block a user