diff --git a/apps/web/src/routes/(dashboard)/ti/painel-permissoes/+page.svelte b/apps/web/src/routes/(dashboard)/ti/painel-permissoes/+page.svelte index 8424660..1929feb 100644 --- a/apps/web/src/routes/(dashboard)/ti/painel-permissoes/+page.svelte +++ b/apps/web/src/routes/(dashboard)/ti/painel-permissoes/+page.svelte @@ -1,848 +1,836 @@ - - - + + + - -
-
-
- - - -
-
-

- Gerenciar Perfis & Permissões de Acesso -

-

- Configure as permissões de acesso aos menus do sistema por função -

-
- - -
-
+ +
+
+
+ + + +
+
+

+ Gerenciar Perfis & Permissões de Acesso +

+

+ Configure as permissões de acesso aos menus do sistema por função +

+
+ + +
+
- - {#if mensagem} -
- {#if mensagem.tipo === "success"} - - - - {:else} - - - - {/if} - {mensagem.texto} -
- {/if} + + {#if mensagem} +
+ {#if mensagem.tipo === 'success'} + + + + {:else} + + + + {/if} + {mensagem.texto} +
+ {/if} - -
-
-
- -
- -
- - - - -
-
+ +
+
+
+ +
+ +
+ + + + +
+
- -
- - -
-
+ +
+ + +
+
- {#if busca || filtroRole} -
- Filtros ativos: - {#if busca} -
- Busca: {busca} - -
- {/if} - {#if filtroRole} -
- Perfil filtrado - -
- {/if} -
- {/if} -
-
+ {#if busca || filtroRole} +
+ Filtros ativos: + {#if busca} +
+ Busca: {busca} + +
+ {/if} + {#if filtroRole} +
+ Perfil filtrado + +
+ {/if} +
+ {/if} +
+
- -
- - - -
-

Como funciona o sistema de permissões:

-
-
-

Tipos de Permissão:

-
    -
  • - • Acessar: Visualizar menu e acessar página -
  • -
  • Consultar: Ver dados (requer "Acessar")
  • -
  • - • Gravar: Criar/editar/excluir (requer "Consultar") -
  • -
-
-
-

Perfis Especiais:

-
    -
  • Admin e TI: Acesso total automático
  • -
  • Dashboard: Público para todos
  • -
  • - • Perfil Customizado: Permissões personalizadas -
  • -
-
-
-
-
+ +
+ + + +
+

Como funciona o sistema de permissões:

+
+
+

Tipos de Permissão:

+
    +
  • + • Acessar: Visualizar menu e acessar página +
  • +
  • Consultar: Ver dados (requer "Acessar")
  • +
  • + • Gravar: Criar/editar/excluir (requer "Consultar") +
  • +
+
+
+

Perfis Especiais:

+
    +
  • Admin: Acesso total automático
  • +
  • TI Master: Controle administrativo completo
  • +
  • Dashboard: Público para todos
  • +
+
+
+
+
- - {#if rolesQuery.isLoading || catalogoQuery.isLoading} -
- -
- {:else if rolesQuery.error} -
- - - - Erro ao carregar perfis: {rolesQuery.error.message} -
- {:else if rolesQuery.data && catalogoQuery.data} - {#if rolesFiltradas.length === 0} -
-
- - - -

Nenhum resultado encontrado

-

- {busca - ? `Não foram encontrados perfis com "${busca}"` - : "Nenhum perfil corresponde aos filtros aplicados"} -

- -
-
- {/if} + + {#if rolesQuery.isLoading || catalogoQuery.isLoading} +
+ +
+ {:else if rolesQuery.error} +
+ + + + Erro ao carregar perfis: {rolesQuery.error.message} +
+ {:else if rolesQuery.data && catalogoQuery.data} + {#if rolesFiltradas.length === 0} +
+
+ + + +

Nenhum resultado encontrado

+

+ {busca + ? `Não foram encontrados perfis com "${busca}"` + : 'Nenhum perfil corresponde aos filtros aplicados'} +

+ +
+
+ {/if} - {#each rolesFiltradas as roleRow} - {@const roleId = roleRow._id} -
-
-
-
-
-

{roleRow.descricao}

-
- Nível {roleRow.nivel} -
- {#if roleRow.nivel <= 1} -
- - - - Acesso Total -
- {/if} -
-

- {roleRow.nome} -

-
-
- -
-
+ {#each rolesFiltradas as roleRow (roleRow._id)} + {@const roleId = roleRow._id} +
+
+
+
+
+

{roleRow.descricao}

+
+ Nível {roleRow.nivel} +
+ {#if roleRow.nivel <= 1} +
+ + + + Acesso Total +
+ {/if} +
+

+ {roleRow.nome} +

+
+
- {#if roleRow.nivel <= 1} -
- - - -
-

Perfil Administrativo

-
- Este perfil possui acesso total ao sistema automaticamente, - sem necessidade de configuração manual. -
-
-
- {:else if catalogoQuery.data} -
- {#each catalogoQuery.data as item} - {@const recursoExpandido = isRecursoExpandido( - roleId, - item.recurso, - )} -
- - + {#if roleRow.nivel <= 1} +
+ + + +
+

Perfil Administrativo

+
+ Este perfil possui acesso total ao sistema automaticamente, sem necessidade de + configuração manual. +
+
+
+ {:else if catalogoQuery.data} +
+ {#each catalogoQuery.data as item (item.recurso)} + {@const recursoExpandido = isRecursoExpandido(roleId, item.recurso)} +
+ + - - {#if recursoExpandido} -
-
- {#each ["ver", "listar", "criar", "editar", "excluir"] as acao} - - {/each} -
-
- {/if} -
- {/each} -
- {/if} -
-
- {/each} - {/if} + + {#if recursoExpandido} +
+
+ {#each ['ver', 'listar', 'criar', 'editar', 'excluir'] as acao (acao)} + + {/each} +
+
+ {/if} +
+ {/each} +
+ {/if} +
+
+ {/each} + {/if} - - {#if modalGerenciarPerfisAberto} - - + + + {/if}
diff --git a/apps/web/src/routes/(dashboard)/ti/times/+page.svelte b/apps/web/src/routes/(dashboard)/ti/times/+page.svelte index f342979..38f7b96 100644 --- a/apps/web/src/routes/(dashboard)/ti/times/+page.svelte +++ b/apps/web/src/routes/(dashboard)/ti/times/+page.svelte @@ -1,873 +1,818 @@ - -
- - + +
+ + - -
-
-
-
- - - -
-
-

- Gestão de Times -

-

- Organize funcionários em equipes e defina gestores -

-
-
-
- - -
-
-
+ +
+
+
+
+ + + +
+
+

Gestão de Times

+

+ Organize funcionários em equipes e defina gestores +

+
+
+
+ + +
+
+
- - {#if modoEdicao} -
-
-

- {timeEmEdicao ? "Editar Time" : "Novo Time"} -

+ + {#if modoEdicao} +
+
+

+ {timeEmEdicao ? 'Editar Time' : 'Novo Time'} +

-
-
- - -
+
+
+ + +
-
- - -
+
+ + +
-
- - -
+
+ + +
-
- -
- {#each coresDisponiveis as cor} - - {/each} -
-
-
+
+ +
+ {#each coresDisponiveis as cor (cor)} + + {/each} +
+
+
-
- - -
-
-
- {/if} +
+ + +
+
+
+ {/if} - - {#if carregando} -
- -
- {:else} -
- {#each times.filter((t: TimeComDetalhes) => t.ativo) as time} -
-
-
-
-
-

{time.nome}

-
- -
+ + {#if carregando} +
+ +
+ {:else} +
+ {#each times.filter((t: TimeComDetalhes) => t.ativo) as time (time._id)} +
+
+
+
+
+

{time.nome}

+
+ +
-

- {time.descricao || "Sem descrição"} -

+

+ {time.descricao || 'Sem descrição'} +

-
+
-
-
- - - - Gestor: - {time.gestor?.nome || "Não definido"} -
-
- - - - Membros: - {time.totalMembros || 0} -
-
+
+
+ + + + Gestor: + {time.gestor?.nome || 'Não definido'} +
+
+ + + + Membros: + {time.totalMembros || 0} +
+
-
- -
-
-
- {/each} +
+ +
+
+
+ {/each} - {#if times.filter((t: TimeComDetalhes) => t.ativo).length === 0} -
-
- - - -

Nenhum time cadastrado

-

- Clique em "Novo Time" para criar seu primeiro time -

-
-
- {/if} -
- {/if} + {#if times.filter((t: TimeComDetalhes) => t.ativo).length === 0} +
+
+ + + +

Nenhum time cadastrado

+

+ Clique em "Novo Time" para criar seu primeiro time +

+
+
+ {/if} +
+ {/if} - - {#if mostrarModalMembros && timeParaMembros} - - + + + {/if} - - {#if mostrarConfirmacaoExclusao && timeParaExcluir} - - - - - {/if} -
+ + {#if mostrarConfirmacaoExclusao && timeParaExcluir} + + + + + {/if} +
diff --git a/packages/backend/convex/roles.ts b/packages/backend/convex/roles.ts index 6f9d25f..f21547e 100644 --- a/packages/backend/convex/roles.ts +++ b/packages/backend/convex/roles.ts @@ -1,35 +1,125 @@ -import { v } from "convex/values"; -import { query } from "./_generated/server"; +import { v } from 'convex/values'; +import { query, mutation } from './_generated/server'; +import type { Id } from './_generated/dataModel'; +import { getCurrentUserFunction } from './auth'; /** * Listar todas as roles */ export const listar = query({ - args: {}, - handler: async (ctx) => { - return await ctx.db.query("roles").collect(); - }, + args: {}, + handler: async (ctx) => { + return await ctx.db.query('roles').collect(); + } }); /** * Buscar role por ID */ export const buscarPorId = query({ - args: { - roleId: v.id("roles"), - }, - returns: v.union( - v.object({ - _id: v.id("roles"), - nome: v.string(), - descricao: v.string(), - nivel: v.number(), - setor: v.optional(v.string()), - }), - v.null() - ), - handler: async (ctx, args) => { - return await ctx.db.get(args.roleId); - }, + args: { + roleId: v.id('roles') + }, + returns: v.union( + v.object({ + _id: v.id('roles'), + nome: v.string(), + descricao: v.string(), + nivel: v.number(), + setor: v.optional(v.string()) + }), + v.null() + ), + handler: async (ctx, args) => { + return await ctx.db.get(args.roleId); + } }); +const slugify = (value: string) => + value + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, '') + .trim() + .toLowerCase() + .replace(/[^a-z0-9]+/g, '_') + .replace(/^_+|_+$/g, '') + .replace(/_{2,}/g, '_'); + +export const criar = mutation({ + args: { + nome: v.string(), + descricao: v.string(), + nivel: v.number(), + setor: v.optional(v.string()), + copiarDeRoleId: v.optional(v.id('roles')) + }, + returns: v.union( + v.object({ sucesso: v.literal(true), roleId: v.id('roles') }), + v.object({ sucesso: v.literal(false), erro: v.string() }) + ), + handler: async (ctx, args) => { + const usuarioAtual = await getCurrentUserFunction(ctx); + if (!usuarioAtual) { + return { sucesso: false as const, erro: 'nao_autenticado' }; + } + + const roleAtual = await ctx.db.get(usuarioAtual.roleId); + if (!roleAtual || roleAtual.nivel > 1) { + return { sucesso: false as const, erro: 'sem_permissao' }; + } + + const nomeNormalizado = slugify(args.nome); + if (!nomeNormalizado) { + return { sucesso: false as const, erro: 'nome_invalido' }; + } + + const existente = await ctx.db + .query('roles') + .withIndex('by_nome', (q) => q.eq('nome', nomeNormalizado)) + .unique(); + + if (existente) { + return { sucesso: false as const, erro: 'nome_ja_utilizado' }; + } + + let permissoesParaCopiar: Array> = []; + + if (args.copiarDeRoleId) { + const roleOrigem = await ctx.db.get(args.copiarDeRoleId); + if (!roleOrigem) { + return { sucesso: false as const, erro: 'role_origem_nao_encontrada' }; + } + + const permissoesOrigem = await ctx.db + .query('rolePermissoes') + .withIndex('by_role', (q) => q.eq('roleId', args.copiarDeRoleId!)) + .collect(); + + permissoesParaCopiar = permissoesOrigem.map((item) => item.permissaoId); + } + + const nivelAjustado = Math.min(Math.max(Math.round(args.nivel), 0), 10); + const setor = args.setor?.trim(); + + const roleId = await ctx.db.insert('roles', { + nome: nomeNormalizado, + descricao: args.descricao.trim() || args.nome.trim(), + nivel: nivelAjustado, + setor: setor && setor.length > 0 ? setor : undefined, + customizado: true, + criadoPor: usuarioAtual._id, + editavel: true + }); + + if (permissoesParaCopiar.length > 0) { + for (const permissaoId of permissoesParaCopiar) { + await ctx.db.insert('rolePermissoes', { + roleId, + permissaoId + }); + } + } + + return { sucesso: true as const, roleId }; + } +});