Implandação da customização das permissões dos usuarios e das notificaçoes

This commit is contained in:
2026-08-26 17:24:25 -03:00
parent 808c2c7545
commit 5bb5d74e22
71 changed files with 2487 additions and 269 deletions

View 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 %>

View 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>

View 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 %>

View 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>