Emp perfis #25

Merged
killer-cf merged 4 commits from emp-perfis into master 2025-11-15 00:58:00 +00:00
10 changed files with 1261 additions and 1328 deletions
Showing only changes of commit 3c371bc35c - Show all commits

View File

@@ -543,7 +543,7 @@
</div> </div>
</div> </div>
</div> </div>
{:else if catalogoQuery.data} {:else if catalogoQuery.data && catalogoQuery.data.length > 0}
<div class="space-y-2"> <div class="space-y-2">
{#each catalogoQuery.data as item (item.recurso)} {#each catalogoQuery.data as item (item.recurso)}
{@const recursoExpandido = isRecursoExpandido(roleId, item.recurso)} {@const recursoExpandido = isRecursoExpandido(roleId, item.recurso)}
@@ -576,28 +576,40 @@
<!-- Lista de ações (visível quando expandido) --> <!-- Lista de ações (visível quando expandido) -->
{#if recursoExpandido} {#if recursoExpandido}
<div class="bg-base-100 border-base-300 border-t px-4 py-3"> <div class="bg-base-100 border-base-300 border-t px-4 py-3">
<div class="space-y-2"> {#if item.acoes.length === 0}
{#each ['ver', 'listar', 'criar', 'editar', 'excluir'] as acao (acao)} <p class="text-base-content/60 text-sm">
<label Nenhuma permissão cadastrada para este recurso.
class="hover:bg-base-200 flex cursor-pointer items-center gap-3 rounded p-2 transition-colors" </p>
> {:else}
<input <div class="space-y-2">
type="checkbox" {#each item.acoes as acao (acao)}
class="checkbox checkbox-primary" <label
checked={isConcedida(roleId, item.recurso, acao)} class="hover:bg-base-200 flex cursor-pointer items-center gap-3 rounded p-2 transition-colors"
disabled={salvando} >
onchange={(e) => <input
toggleAcao(roleId, item.recurso, acao, e.currentTarget.checked)} type="checkbox"
/> class="checkbox checkbox-primary"
<span class="flex-1 font-medium capitalize">{acao}</span> checked={isConcedida(roleId, item.recurso, acao)}
</label> disabled={salvando}
{/each} onchange={(e) =>
</div> toggleAcao(roleId, item.recurso, acao, e.currentTarget.checked)}
/>
<span class="flex-1 font-medium">{acao}</span>
</label>
{/each}
</div>
{/if}
</div> </div>
{/if} {/if}
</div> </div>
{/each} {/each}
</div> </div>
{:else}
<div class="alert alert-info mt-4">
<span class="font-semibold">
Nenhuma permissão cadastrada ainda. Use o botão Criar permissão para começar.
</span>
</div>
{/if} {/if}
</div> </div>
</div> </div>

View File

@@ -41,19 +41,15 @@
const stats = $derived.by(() => { const stats = $derived.by(() => {
if (carregando) return null; if (carregando) return null;
const porNivel = { const nivelMaximo = roles.filter((r) => r.nivel === 0).length;
0: roles.filter((r) => r.nivel === 0).length, const nivelAdministrativo = roles.filter((r) => r.nivel === 1).length;
1: roles.filter((r) => r.nivel === 1).length, const niveisLegado = roles.filter((r) => r.nivel > 1).length;
2: roles.filter((r) => r.nivel === 2).length,
3: roles.filter((r) => r.nivel >= 3).length
};
return { return {
total: roles.length, total: roles.length,
nivelMaximo: porNivel[0], nivelMaximo,
nivelAlto: porNivel[1], nivelAdministrativo,
nivelMedio: porNivel[2], niveisLegado,
nivelBaixo: porNivel[3],
comSetor: roles.filter((r) => r.setor).length comSetor: roles.filter((r) => r.setor).length
}; };
}); });
@@ -78,10 +74,11 @@
// Filtro por nível // Filtro por nível
if (filtroNivel !== '') { if (filtroNivel !== '') {
if (filtroNivel === 3) { if (filtroNivel === 0 || filtroNivel === 1) {
resultado = resultado.filter((r) => r.nivel >= 3);
} else {
resultado = resultado.filter((r) => r.nivel === filtroNivel); resultado = resultado.filter((r) => r.nivel === filtroNivel);
} else {
// Qualquer outro valor é considerado legado
resultado = resultado.filter((r) => r.nivel > 1);
} }
} }
@@ -96,22 +93,19 @@
function obterCorNivel(nivel: number): string { function obterCorNivel(nivel: number): string {
if (nivel === 0) return 'badge-error'; if (nivel === 0) return 'badge-error';
if (nivel === 1) return 'badge-warning'; if (nivel === 1) return 'badge-warning';
if (nivel === 2) return 'badge-info'; // Níveis > 1 são considerados legado
return 'badge-ghost'; return 'badge-ghost';
} }
function obterTextoNivel(nivel: number): string { function obterTextoNivel(nivel: number): string {
if (nivel === 0) return 'Máximo'; if (nivel === 0) return 'Máximo';
if (nivel === 1) return 'Alto'; if (nivel === 1) return 'Administrativo';
if (nivel === 2) return 'Médio'; return `Legado (${nivel})`;
if (nivel === 3) return 'Baixo';
return `Nível ${nivel}`;
} }
function obterCorCardNivel(nivel: number): string { function obterCorCardNivel(nivel: number): string {
if (nivel === 0) return 'border-l-4 border-error'; if (nivel === 0) return 'border-l-4 border-error';
if (nivel === 1) return 'border-l-4 border-warning'; if (nivel === 1) return 'border-l-4 border-warning';
if (nivel === 2) return 'border-l-4 border-info';
return 'border-l-4 border-base-300'; return 'border-l-4 border-base-300';
} }
@@ -176,31 +170,24 @@
<!-- Estatísticas --> <!-- Estatísticas -->
{#if stats} {#if stats}
<div class="mb-8 grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-5"> <div class="mb-8 grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-4">
<StatsCard title="Total de Perfis" value={stats.total} Icon={Users} color="primary" /> <StatsCard title="Total de Perfis" value={stats.total} Icon={Users} color="primary" />
<StatsCard <StatsCard
title="Nível Máximo" title="Nível Máximo (0)"
value={stats.nivelMaximo} value={stats.nivelMaximo}
description="Acesso total" description="Acesso total ao sistema"
Icon={Shield} Icon={Shield}
color="error" color="error"
/> />
<StatsCard <StatsCard
title="Nível Alto" title="Nível Administrativo (1)"
value={stats.nivelAlto} value={stats.nivelAdministrativo}
description="Acesso elevado" description="Perfis administrativos com acesso total"
Icon={AlertTriangle} Icon={AlertTriangle}
color="warning" color="warning"
/> />
<StatsCard <StatsCard
title="Nível Médio" title="Perfis com Setor"
value={stats.nivelMedio}
description="Acesso padrão"
Icon={Info}
color="info"
/>
<StatsCard
title="Com Setor"
value={stats.comSetor} value={stats.comSetor}
description={stats.total > 0 description={stats.total > 0
? ((stats.comSetor / stats.total) * 100).toFixed(0) + '% do total' ? ((stats.comSetor / stats.total) * 100).toFixed(0) + '% do total'
@@ -209,6 +196,18 @@
color="secondary" color="secondary"
/> />
</div> </div>
{#if stats.niveisLegado > 0}
<div class="alert alert-warning mb-6">
<Info class="h-5 w-5" />
<div>
<h3 class="font-bold">Perfis com níveis legados</h3>
<p class="text-sm">
Existem {stats.niveisLegado} perfis com nível acima de 1. Esses perfis continuarão
sendo tratados como nível 1 (administrativo) após a migração.
</p>
</div>
</div>
{/if}
{/if} {/if}
<!-- Filtros --> <!-- Filtros -->

View File

@@ -3,27 +3,271 @@ import { v } from 'convex/values';
import type { Doc } from './_generated/dataModel'; import type { Doc } from './_generated/dataModel';
import { getCurrentUserFunction } from './auth'; import { getCurrentUserFunction } from './auth';
// Catálogo base de recursos e ações // Catálogo de permissões base para seed controlado via mutation
// Ajuste/expanda conforme os módulos disponíveis no sistema const PERMISSOES_BASE = {
export const CATALOGO_RECURSOS = [ permissoes: [
{ // Funcionários
recurso: 'funcionarios', {
acoes: [ nome: 'funcionarios.dashboard',
'dashboard', recurso: 'funcionarios',
'ver', acao: 'dashboard',
'listar', descricao: 'Acessar o painel de funcionários'
'criar', },
'editar', {
'excluir', nome: 'funcionarios.ver',
'aprovar_ausencias', recurso: 'funcionarios',
'aprovar_ferias' acao: 'ver',
] descricao: 'Visualizar detalhes de funcionários'
}, },
{ {
recurso: 'simbolos', nome: 'funcionarios.listar',
acoes: ['dashboard', 'ver', 'listar', 'criar', 'editar', 'excluir'] recurso: 'funcionarios',
} acao: 'listar',
] as const; descricao: 'Listar funcionários'
},
{
nome: 'funcionarios.criar',
recurso: 'funcionarios',
acao: 'criar',
descricao: 'Criar novos funcionários'
},
{
nome: 'funcionarios.editar',
recurso: 'funcionarios',
acao: 'editar',
descricao: 'Editar dados de funcionários'
},
{
nome: 'funcionarios.excluir',
recurso: 'funcionarios',
acao: 'excluir',
descricao: 'Excluir funcionários'
},
{
nome: 'funcionarios.aprovar_ausencias',
recurso: 'funcionarios',
acao: 'aprovar_ausencias',
descricao: 'Aprovar ausências de funcionários'
},
{
nome: 'funcionarios.aprovar_ferias',
recurso: 'funcionarios',
acao: 'aprovar_ferias',
descricao: 'Aprovar férias de funcionários'
},
// Símbolos
{
nome: 'simbolos.dashboard',
recurso: 'simbolos',
acao: 'dashboard',
descricao: 'Acessar o painel de símbolos'
},
{
nome: 'simbolos.ver',
recurso: 'simbolos',
acao: 'ver',
descricao: 'Visualizar detalhes de símbolos'
},
{
nome: 'simbolos.listar',
recurso: 'simbolos',
acao: 'listar',
descricao: 'Listar símbolos'
},
{
nome: 'simbolos.criar',
recurso: 'simbolos',
acao: 'criar',
descricao: 'Criar novos símbolos'
},
{
nome: 'simbolos.editar',
recurso: 'simbolos',
acao: 'editar',
descricao: 'Editar símbolos'
},
{
nome: 'simbolos.excluir',
recurso: 'simbolos',
acao: 'excluir',
descricao: 'Excluir símbolos'
},
// TI - Usuários
{
nome: 'ti_usuarios.listar',
recurso: 'ti_usuarios',
acao: 'listar',
descricao: 'Listar usuários do sistema'
},
{
nome: 'ti_usuarios.criar',
recurso: 'ti_usuarios',
acao: 'criar',
descricao: 'Criar novos usuários de acesso'
},
{
nome: 'ti_usuarios.editar',
recurso: 'ti_usuarios',
acao: 'editar',
descricao: 'Editar usuários de acesso'
},
{
nome: 'ti_usuarios.bloquear',
recurso: 'ti_usuarios',
acao: 'bloquear',
descricao: 'Bloquear ou desbloquear usuários'
},
// TI - Perfis
{
nome: 'ti_perfis.listar',
recurso: 'ti_perfis',
acao: 'listar',
descricao: 'Listar perfis de acesso'
},
{
nome: 'ti_perfis.criar',
recurso: 'ti_perfis',
acao: 'criar',
descricao: 'Criar novos perfis de acesso'
},
{
nome: 'ti_perfis.editar',
recurso: 'ti_perfis',
acao: 'editar',
descricao: 'Editar perfis de acesso'
},
// TI - Painel de Permissões
{
nome: 'ti_painel_permissoes.gerenciar',
recurso: 'ti_painel_permissoes',
acao: 'gerenciar',
descricao: 'Gerenciar matriz de permissões por perfil'
},
// TI - Solicitações de Acesso
{
nome: 'ti_solicitacoes_acesso.ver',
recurso: 'ti_solicitacoes_acesso',
acao: 'ver',
descricao: 'Visualizar solicitações de acesso'
},
{
nome: 'ti_solicitacoes_acesso.aprovar',
recurso: 'ti_solicitacoes_acesso',
acao: 'aprovar',
descricao: 'Aprovar solicitações de acesso'
},
{
nome: 'ti_solicitacoes_acesso.reprovar',
recurso: 'ti_solicitacoes_acesso',
acao: 'reprovar',
descricao: 'Reprovar solicitações de acesso'
},
// TI - Configurações de E-mail
{
nome: 'ti_configuracoes_email.configurar',
recurso: 'ti_configuracoes_email',
acao: 'configurar',
descricao: 'Configurar parâmetros de envio de e-mail'
},
// TI - Monitoramento
{
nome: 'ti_monitoramento.ver',
recurso: 'ti_monitoramento',
acao: 'ver',
descricao: 'Acessar painel de monitoramento geral'
},
{
nome: 'ti_monitoramento_emails.ver',
recurso: 'ti_monitoramento_emails',
acao: 'ver',
descricao: 'Acessar monitoramento de envio de e-mails'
},
// TI - Notificações
{
nome: 'ti_notificacoes.configurar',
recurso: 'ti_notificacoes',
acao: 'configurar',
descricao: 'Configurar notificações do sistema'
},
// TI - Times
{
nome: 'ti_times.gerenciar',
recurso: 'ti_times',
acao: 'gerenciar',
descricao: 'Gerenciar times/equipes de TI'
},
// TI - Painel Administrativo
{
nome: 'ti_painel_administrativo.ver',
recurso: 'ti_painel_administrativo',
acao: 'ver',
descricao: 'Acessar painel administrativo de TI'
},
// Financeiro
{
nome: 'financeiro.ver',
recurso: 'financeiro',
acao: 'ver',
descricao: 'Acessar telas do módulo de financeiro'
},
// Controladoria
{
nome: 'controladoria.ver',
recurso: 'controladoria',
acao: 'ver',
descricao: 'Acessar telas do módulo de controladoria'
},
// Licitações
{
nome: 'licitacoes.ver',
recurso: 'licitacoes',
acao: 'ver',
descricao: 'Acessar telas do módulo de licitações'
},
// Compras
{
nome: 'compras.ver',
recurso: 'compras',
acao: 'ver',
descricao: 'Acessar telas do módulo de compras'
},
// Jurídico
{
nome: 'juridico.ver',
recurso: 'juridico',
acao: 'ver',
descricao: 'Acessar telas do módulo jurídico'
},
// Comunicação
{
nome: 'comunicacao.ver',
recurso: 'comunicacao',
acao: 'ver',
descricao: 'Acessar telas do módulo de comunicação'
},
// Programas Esportivos
{
nome: 'programas_esportivos.ver',
recurso: 'programas_esportivos',
acao: 'ver',
descricao: 'Acessar telas do módulo de programas esportivos'
},
// Secretaria Executiva
{
nome: 'secretaria_executiva.ver',
recurso: 'secretaria_executiva',
acao: 'ver',
descricao: 'Acessar telas do módulo de secretaria executiva'
},
// Gestão de Pessoas
{
nome: 'gestao_pessoas.ver',
recurso: 'gestao_pessoas',
acao: 'ver',
descricao: 'Acessar telas do módulo de gestão de pessoas'
}
]
} as const;
export const listarRecursosEAcoes = query({ export const listarRecursosEAcoes = query({
args: {}, args: {},
@@ -33,10 +277,18 @@ export const listarRecursosEAcoes = query({
acoes: v.array(v.string()) acoes: v.array(v.string())
}) })
), ),
handler: async () => { handler: async (ctx) => {
return CATALOGO_RECURSOS.map((r) => ({ const permissoes = await ctx.db.query('permissoes').collect();
recurso: r.recurso,
acoes: [...r.acoes] const recursos: Record<string, Set<string>> = {};
for (const perm of permissoes) {
const set = (recursos[perm.recurso] ||= new Set<string>());
set.add(perm.acao);
}
return Object.entries(recursos).map(([recurso, acoes]) => ({
recurso,
acoes: Array.from(acoes).sort()
})); }));
} }
}); });
@@ -56,7 +308,7 @@ export const listarPermissoesAcoesPorRole = query({
.withIndex('by_role', (q) => q.eq('roleId', args.roleId)) .withIndex('by_role', (q) => q.eq('roleId', args.roleId))
.collect(); .collect();
// Carregar documentos de permissões // Carregar documentos de permissões vinculadas a este role
const actionsByResource: Record<string, Set<string>> = {}; const actionsByResource: Record<string, Set<string>> = {};
for (const rp of rolePerms) { for (const rp of rolePerms) {
const perm = await ctx.db.get(rp.permissaoId); const perm = await ctx.db.get(rp.permissaoId);
@@ -65,13 +317,10 @@ export const listarPermissoesAcoesPorRole = query({
set.add(perm.acao); set.add(perm.acao);
} }
// Normalizar para todos os recursos do catálogo return Object.entries(actionsByResource).map(([recurso, acoes]) => ({
const result: Array<{ recurso: string; acoes: Array<string> }> = []; recurso,
for (const item of CATALOGO_RECURSOS) { acoes: Array.from(acoes).sort()
const granted = Array.from(actionsByResource[item.recurso] ?? new Set<string>()); }));
result.push({ recurso: item.recurso, acoes: granted });
}
return result;
} }
}); });
@@ -84,24 +333,12 @@ export const atualizarPermissaoAcao = mutation({
}, },
returns: v.null(), returns: v.null(),
handler: async (ctx, args) => { handler: async (ctx, args) => {
// Garantir documento de permissão (recurso+acao) // Buscar documento de permissão (recurso+acao)
let permissao = await ctx.db const permissao = await ctx.db
.query('permissoes') .query('permissoes')
.withIndex('by_recurso_e_acao', (q) => q.eq('recurso', args.recurso).eq('acao', args.acao)) .withIndex('by_recurso_e_acao', (q) => q.eq('recurso', args.recurso).eq('acao', args.acao))
.first(); .first();
if (!permissao) {
const nome = `${args.recurso}.${args.acao}`;
const descricao = `Permite ${args.acao} em ${args.recurso}`;
const id = await ctx.db.insert('permissoes', {
nome,
descricao,
recurso: args.recurso,
acao: args.acao
});
permissao = await ctx.db.get(id);
}
if (!permissao) return null; if (!permissao) return null;
// Verificar vínculo atual // Verificar vínculo atual
@@ -128,6 +365,36 @@ export const atualizarPermissaoAcao = mutation({
} }
}); });
export const seedPermissoesBase = mutation({
args: {},
returns: v.null(),
handler: async (ctx) => {
console.log('🔐 Seed de permissões base...');
for (const perm of PERMISSOES_BASE.permissoes) {
const existente = await ctx.db
.query('permissoes')
.withIndex('by_nome', (q) => q.eq('nome', perm.nome))
.first();
if (existente) {
console.log(` Permissão já existe: ${perm.nome}`);
continue;
}
await ctx.db.insert('permissoes', {
nome: perm.nome,
descricao: perm.descricao,
recurso: perm.recurso,
acao: perm.acao
});
console.log(` ✅ Permissão criada: ${perm.nome}`);
}
return null;
}
});
export const verificarAcao = query({ export const verificarAcao = query({
args: { args: {
usuarioId: v.id('usuarios'), usuarioId: v.id('usuarios'),

View File

@@ -1,5 +1,5 @@
import { v } from 'convex/values'; import { v } from 'convex/values';
import { query, mutation } from './_generated/server'; import { internalMutation, query, mutation } from './_generated/server';
import type { Id } from './_generated/dataModel'; import type { Id } from './_generated/dataModel';
import { getCurrentUserFunction } from './auth'; import { getCurrentUserFunction } from './auth';
@@ -98,13 +98,16 @@ export const criar = mutation({
permissoesParaCopiar = permissoesOrigem.map((item) => item.permissaoId); permissoesParaCopiar = permissoesOrigem.map((item) => item.permissaoId);
} }
const nivelAjustado = Math.min(Math.max(Math.round(args.nivel), 0), 10); // Agora só existem níveis 0 e 1.
// 0 = máximo (acesso total), 1 = administrativo (também com acesso total).
// Qualquer valor informado diferente de 0 é normalizado para 1.
const nivelNormalizado = Math.round(args.nivel) <= 0 ? 0 : 1;
const setor = args.setor?.trim(); const setor = args.setor?.trim();
const roleId = await ctx.db.insert('roles', { const roleId = await ctx.db.insert('roles', {
nome: nomeNormalizado, nome: nomeNormalizado,
descricao: args.descricao.trim() || args.nome.trim(), descricao: args.descricao.trim() || args.nome.trim(),
nivel: nivelAjustado, nivel: nivelNormalizado,
setor: setor && setor.length > 0 ? setor : undefined, setor: setor && setor.length > 0 ? setor : undefined,
customizado: true, customizado: true,
criadoPor: usuarioAtual._id, criadoPor: usuarioAtual._id,
@@ -123,3 +126,26 @@ export const criar = mutation({
return { sucesso: true as const, roleId }; return { sucesso: true as const, roleId };
} }
}); });
/**
* Migração de níveis de roles para o novo modelo (apenas 0 e 1).
* - Mantém níveis 0 e 1 como estão.
* - Converte qualquer nível > 1 para 1.
*/
export const migrarNiveisRoles = internalMutation({
args: {},
returns: v.null(),
handler: async (ctx) => {
const roles = await ctx.db.query('roles').collect();
for (const role of roles) {
if (role.nivel <= 1) continue;
await ctx.db.patch(role._id, {
nivel: 1
});
}
return null;
}
});

File diff suppressed because it is too large Load Diff