From c1d9958c9f3a434c0048d4315df6b9ba96b08b65 Mon Sep 17 00:00:00 2001 From: deyvisonwanderley Date: Mon, 3 Nov 2025 15:12:10 -0300 Subject: [PATCH] refactor: update user role management and enhance UI components - Updated the user role management logic to improve type safety and error handling, including better handling of role permissions and user associations. - Refactored the UI components for user management, enhancing the layout and styling for better user experience. - Removed outdated code related to menu permissions and streamlined the database schema for roles and profiles. - Improved the overall structure and readability of the codebase, ensuring consistency across components. --- .editorconfig | 12 + README.md | 4 +- apps/web/src/app.css | 55 ++ apps/web/src/lib/auth.ts | 7 + .../routes/(dashboard)/perfil/+page.svelte | 269 +++++-- .../recursos-humanos/ferias/+page.svelte | 67 +- .../(dashboard)/solicitar-acesso/+page.svelte | 135 +++- .../src/routes/(dashboard)/ti/+page.svelte | 661 ++++++++++-------- .../ti/painel-administrativo/+page.svelte | 31 +- bun.lock | 2 +- 10 files changed, 749 insertions(+), 494 deletions(-) create mode 100644 .editorconfig create mode 100644 apps/web/src/lib/auth.ts diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..c1322dc --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +[*] +indent_style = space +indent_size = 4 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = false +insert_final_newline = false \ No newline at end of file diff --git a/README.md b/README.md index 9e865c6..553ae3d 100644 --- a/README.md +++ b/README.md @@ -126,8 +126,8 @@ npx convex dev ### **Banco vazio:** ```powershell cd packages\backend -npx convex run seed:clearDatabase -npx convex run seed:seedDatabase +npx convex run seed:limparBanco +npx convex run seed:popularBanco ``` **Mais soluções:** Veja `TESTAR_SISTEMA_COMPLETO.md` seção "Problemas Comuns" diff --git a/apps/web/src/app.css b/apps/web/src/app.css index 5e25b7b..7d3fb0f 100644 --- a/apps/web/src/app.css +++ b/apps/web/src/app.css @@ -17,4 +17,59 @@ .btn-error { @apply font-medium flex items-center justify-center gap-2 text-center px-4 py-2 rounded-xl border border-error bg-base-100 hover:bg-error/60 active:bg-error text-error hover:text-white active:text-white transition-colors; +} + +:where(.card, .card-hover) { + position: relative; + overflow: hidden; + transform: translateY(0); + transition: transform 220ms ease, box-shadow 220ms ease; +} + +:where(.card, .card-hover)::before { + content: ""; + position: absolute; + inset: -2px; + border-radius: 1.15rem; + box-shadow: + 0 0 0 1px rgba(15, 23, 42, 0.04), + 0 14px 32px -22px rgba(15, 23, 42, 0.45), + 0 6px 18px -16px rgba(102, 126, 234, 0.35); + opacity: 0.55; + transition: opacity 220ms ease, transform 220ms ease; + pointer-events: none; + z-index: 0; +} + +:where(.card, .card-hover)::after { + content: ""; + position: absolute; + inset: 0; + border-radius: 1rem; + background: linear-gradient(135deg, rgba(102, 126, 234, 0.12), rgba(118, 75, 162, 0.12)); + opacity: 0; + transform: scale(0.96); + transition: opacity 220ms ease, transform 220ms ease; + pointer-events: none; + z-index: 1; +} + +:where(.card, .card-hover):hover { + transform: translateY(-6px); + box-shadow: 0 20px 45px -20px rgba(15, 23, 42, 0.35); +} + +:where(.card, .card-hover):hover::before { + opacity: 0.9; + transform: scale(1); +} + +:where(.card, .card-hover):hover::after { + opacity: 1; + transform: scale(1); +} + +:where(.card, .card-hover) > * { + position: relative; + z-index: 2; } \ No newline at end of file diff --git a/apps/web/src/lib/auth.ts b/apps/web/src/lib/auth.ts new file mode 100644 index 0000000..6de9cf0 --- /dev/null +++ b/apps/web/src/lib/auth.ts @@ -0,0 +1,7 @@ +import { createAuthClient } from "better-auth/client"; +import { convexClient } from "@convex-dev/better-auth/client/plugins"; + +export const authClient = createAuthClient({ + baseURL: "http://localhost:5173", + plugins: [convexClient()], +}); diff --git a/apps/web/src/routes/(dashboard)/perfil/+page.svelte b/apps/web/src/routes/(dashboard)/perfil/+page.svelte index 2154ea7..d9d5d09 100644 --- a/apps/web/src/routes/(dashboard)/perfil/+page.svelte +++ b/apps/web/src/routes/(dashboard)/perfil/+page.svelte @@ -2,18 +2,17 @@ import { useConvexClient, useQuery } from "convex-svelte"; import { api } from "@sgse-app/backend/convex/_generated/api"; import { authStore } from "$lib/stores/auth.svelte"; - import WizardSolicitacaoFerias from "$lib/components/ferias/WizardSolicitacaoFerias.svelte"; - import DashboardFerias from "$lib/components/ferias/DashboardFerias.svelte"; import AprovarFerias from "$lib/components/AprovarFerias.svelte"; + import WizardSolicitacaoFerias from "$lib/components/ferias/WizardSolicitacaoFerias.svelte"; import { generateAvatarGallery, type Avatar } from "$lib/utils/avatars"; import type { Id } from "@sgse-app/backend/convex/_generated/dataModel"; + import { page } from "$app/stores"; const client = useConvexClient(); let abaAtiva = $state<"meu-perfil" | "minhas-ferias" | "aprovar-ferias">( "meu-perfil" ); - let mostrarFormSolicitar = $state(false); let solicitacaoSelecionada = $state(null); let mostrarModalFoto = $state(false); let uploadandoFoto = $state(false); @@ -26,6 +25,10 @@ let fotoPerfilLocal = $state(null); let avatarLocal = $state(null); + // Estados para Minhas Férias + let mostrarWizard = $state(false); + let filtroStatusFerias = $state("todos"); + // Galeria de avatares (30 avatares profissionais 3D realistas) const avatarGallery = generateAvatarGallery(30); @@ -54,14 +57,6 @@ : { data: null } ); - const minhasSolicitacoesQuery = $derived( - funcionarioQuery.data - ? useQuery(api.ferias.listarMinhasSolicitacoes, { - funcionarioId: funcionarioQuery.data._id, - }) - : { data: [] } - ); - const solicitacoesSubordinadosQuery = $derived( authStore.usuario?._id ? useQuery(api.ferias.listarSolicitacoesSubordinados, { @@ -70,6 +65,14 @@ : { data: [] } ); + const minhasSolicitacoesQuery = $derived( + funcionarioQuery.data + ? useQuery(api.ferias.listarMinhasSolicitacoes, { + funcionarioId: funcionarioQuery.data._id, + }) + : { data: [] } + ); + const meuTimeQuery = $derived( funcionarioQuery.data ? useQuery(api.times.obterTimeFuncionario, { @@ -87,18 +90,34 @@ ); const funcionario = $derived(funcionarioQuery.data); - const minhasSolicitacoes = $derived(minhasSolicitacoesQuery?.data || []); const solicitacoesSubordinados = $derived( solicitacoesSubordinadosQuery?.data || [] ); + const minhasSolicitacoes = $derived(minhasSolicitacoesQuery?.data || []); const meuTime = $derived(meuTimeQuery?.data); const meusTimesGestor = $derived(meusTimesGestorQuery?.data || []); // Verificar se é gestor const ehGestor = $derived((meusTimesGestor || []).length > 0); + // Filtrar minhas solicitações + const solicitacoesFiltradas = $derived( + minhasSolicitacoes.filter((s) => { + if (filtroStatusFerias !== "todos" && s.status !== filtroStatusFerias) return false; + return true; + }) + ); + + // Estatísticas das minhas férias + const statsMinhasFerias = $derived({ + total: minhasSolicitacoes.length, + aguardando: minhasSolicitacoes.filter((s) => s.status === "aguardando_aprovacao").length, + aprovadas: minhasSolicitacoes.filter((s) => s.status === "aprovado" || s.status === "data_ajustada_aprovada").length, + reprovadas: minhasSolicitacoes.filter((s) => s.status === "reprovado").length, + emFerias: funcionario?.statusFerias === "em_ferias" ? 1 : 0, + }); + async function recarregar() { - mostrarFormSolicitar = false; solicitacaoSelecionada = null; } @@ -1060,72 +1079,147 @@ {/if} {:else if abaAtiva === "minhas-ferias"} - -
- {#if !mostrarFormSolicitar} - - {#if funcionario} - - - -
- -
- {:else} -
- - + +
+ +
+
+
+ + -
-

Perfil de funcionário não encontrado

-
- Seu usuário ainda não está associado a um cadastro de - funcionário. Entre em contato com o RH. -
+
+
Total
+
{statsMinhasFerias.total}
+
Solicitações
+
+ +
+
+ + + +
+
Aguardando
+
{statsMinhasFerias.aguardando}
+
Pendentes
+
+ +
+
+ + + +
+
Aprovadas
+
{statsMinhasFerias.aprovadas}
+
Deferidas
+
+ +
+
+ + + +
+
Reprovadas
+
{statsMinhasFerias.reprovadas}
+
Indeferidas
+
+ +
+
+ + + +
+
Em Férias
+
{statsMinhasFerias.emFerias}
+
Agora
+
+
+ + +
+
+
+

Filtros

+ {#if funcionario} + + {/if} +
+
+
+ +
- {/if} - {:else} - - {#if funcionario} - (mostrarFormSolicitar = false)} - /> - {/if} - {/if} +
+
+ + +
+
+

+ Minhas Solicitações ({solicitacoesFiltradas.length}) +

+ + {#if solicitacoesFiltradas.length === 0} +
+ + + + Nenhuma solicitação encontrada com os filtros aplicados. +
+ {:else} +
+ + + + + + + + + + + + {#each solicitacoesFiltradas as solicitacao} + + + + + + + + {/each} + +
AnoPeríodosTotal DiasStatusSolicitado em
{solicitacao.anoReferencia}{solicitacao.periodos.length} período(s){solicitacao.periodos.reduce((acc: number, p: any) => acc + p.diasCorridos, 0)} dias +
+ {getStatusTexto(solicitacao.status)} +
+
{new Date(solicitacao._creationTime).toLocaleDateString("pt-BR")}
+
+ {/if} +
+
{:else if abaAtiva === "aprovar-ferias"} @@ -1599,3 +1693,26 @@ animation: float 6s ease-in-out infinite; } + + +{#if mostrarWizard && funcionario} + + + + + + +{/if} diff --git a/apps/web/src/routes/(dashboard)/recursos-humanos/ferias/+page.svelte b/apps/web/src/routes/(dashboard)/recursos-humanos/ferias/+page.svelte index 8b95711..73a9dd1 100644 --- a/apps/web/src/routes/(dashboard)/recursos-humanos/ferias/+page.svelte +++ b/apps/web/src/routes/(dashboard)/recursos-humanos/ferias/+page.svelte @@ -3,49 +3,30 @@ import { useQuery } from "convex-svelte"; import { api } from "@sgse-app/backend/convex/_generated/api"; - // Buscar todas as solicitações (RH vê tudo) + // Buscar TODAS as solicitações de férias (Dashboard RH) const todasSolicitacoesQuery = useQuery(api.ferias.listarTodas, {}); - const todosFuncionariosQuery = useQuery(api.funcionarios.getAll, {}); let filtroStatus = $state("todos"); - let filtroTime = $state("todos"); - let filtroBusca = $state(""); const solicitacoes = $derived(todasSolicitacoesQuery?.data || []); - const funcionarios = $derived(todosFuncionariosQuery?.data || []); // Filtrar solicitações const solicitacoesFiltradas = $derived( solicitacoes.filter((s: any) => { // Filtro de status if (filtroStatus !== "todos" && s.status !== filtroStatus) return false; - - // Filtro de time - if (filtroTime !== "todos" && s.time?._id !== filtroTime) return false; - - // Filtro de busca - if (filtroBusca && !s.funcionario?.nome.toLowerCase().includes(filtroBusca.toLowerCase())) { - return false; - } - return true; }) ); - // Estatísticas + // Estatísticas gerais const stats = $derived({ total: solicitacoes.length, aguardando: solicitacoes.filter((s: any) => s.status === "aguardando_aprovacao").length, aprovadas: solicitacoes.filter((s: any) => s.status === "aprovado" || s.status === "data_ajustada_aprovada").length, reprovadas: solicitacoes.filter((s: any) => s.status === "reprovado").length, - emFerias: funcionarios.filter((f: any) => f.statusFerias === "em_ferias").length, }); - // Times únicos para filtro - const timesDisponiveis = $derived( - Array.from(new Set(solicitacoes.map((s: any) => s.time).filter(Boolean))) - ); - function getStatusBadge(status: string) { const badges: Record = { aguardando_aprovacao: "badge-warning", @@ -104,7 +85,7 @@
-
+
@@ -148,38 +129,13 @@
{stats.reprovadas}
Indeferidas
- -
-
- - - -
-
Em Férias
-
{stats.emFerias}
-
Agora
-

Filtros

-
- -
- - -
- +
- - -
- - -
diff --git a/apps/web/src/routes/(dashboard)/solicitar-acesso/+page.svelte b/apps/web/src/routes/(dashboard)/solicitar-acesso/+page.svelte index 4801dd4..3cdd9c3 100644 --- a/apps/web/src/routes/(dashboard)/solicitar-acesso/+page.svelte +++ b/apps/web/src/routes/(dashboard)/solicitar-acesso/+page.svelte @@ -72,17 +72,28 @@ } -
-
-

Solicitar Acesso ao SGSE

-

- Preencha o formulário abaixo para solicitar acesso ao Sistema de Gerenciamento da Secretaria de Esportes. - Sua solicitação será analisada pela equipe de Tecnologia da Informação. -

-
+
+ +
+
+
+
+ + Acesso ao Sistema + +

+ Solicitar Acesso ao SGSE +

+

+ Preencha o formulário abaixo para solicitar acesso ao Sistema de Gerenciamento da Secretaria de Esportes. + Sua solicitação será analisada pela equipe de Tecnologia da Informação. +

+
+
+ {#if notice} -
+
{/if} - {notice.message} + {notice.message}
{/if} -
-
+ +
+
+
{ e.preventDefault(); @@ -118,25 +131,28 @@ form.handleSubmit(); }} > -
+
{#snippet children(field)}
field.handleChange(e.currentTarget.value)} /> {#if field.state.meta.errors.length > 0} - {field.state.meta.errors[0]} + {/if}
{/snippet} @@ -147,19 +163,22 @@ {#snippet children(field)}
field.handleChange(e.currentTarget.value)} /> {#if field.state.meta.errors.length > 0} - {field.state.meta.errors[0]} + {/if}
{/snippet} @@ -170,19 +189,22 @@ {#snippet children(field)}
field.handleChange(e.currentTarget.value)} /> {#if field.state.meta.errors.length > 0} - {field.state.meta.errors[0]} + {/if}
{/snippet} @@ -193,13 +215,14 @@ {#snippet children(field)}
{ @@ -210,26 +233,36 @@ maxlength="15" /> {#if field.state.meta.errors.length > 0} - {field.state.meta.errors[0]} + {/if}
{/snippet}
-
- -
-
+
-
+ +
-

Informações Importantes

-
-
    -
  • Todos os campos marcados com * são obrigatórios
  • -
  • Sua solicitação será analisada pela equipe de TI em até 48 horas úteis
  • -
  • Você receberá um e-mail com o resultado da análise
  • -
  • Em caso de dúvidas, entre em contato com o suporte técnico
  • -
+

Informações Importantes

+
+
+ + Todos os campos marcados com * são obrigatórios +
+
+ + Sua solicitação será analisada pela equipe de TI em até 48 horas úteis +
+
+ + Você receberá um e-mail com o resultado da análise +
+
+ + Em caso de dúvidas, entre em contato com o suporte técnico +
+ diff --git a/apps/web/src/routes/(dashboard)/ti/+page.svelte b/apps/web/src/routes/(dashboard)/ti/+page.svelte index ade7b7f..085e8c1 100644 --- a/apps/web/src/routes/(dashboard)/ti/+page.svelte +++ b/apps/web/src/routes/(dashboard)/ti/+page.svelte @@ -1,337 +1,384 @@ -
-

Tecnologia da Informação

- -
- -
-
-
-
- - - -
-

Painel Administrativo

-
-

- Acesso restrito para gerenciamento de solicitações de acesso ao sistema e outras configurações administrativas. +

+
+
+
+
+
+ + Tecnologia da Informação + +

+ Sistemas de Informação +

+

+ Acesso restrito para gerenciamento de solicitações de acesso ao sistema, configuração de permissões e monitoramento técnico das operações do SGSE.

- +
+
+

Status

+

Operacional

+
+
+

Última atualização

+

Agora mesmo

+
+
+
+ Monitoramento em tempo real. + SGSE
+
- -
-
-
-
+
+ {#each featureCards as card (card.title)} +
+
+
+
+ {#each iconPaths[card.icon] as path (path.d)} + d={path.d} + stroke-linecap={path.strokeLinecap ?? "round"} + stroke-linejoin={path.strokeLinejoin ?? "round"} + stroke-width={path.strokeWidth ?? 2} + /> + {/each}
-

Suporte Técnico

-
-

- Central de atendimento para resolução de problemas técnicos e dúvidas sobre o sistema. -

-
-
+ + {#if card.highlightBadges} +
+ {#each card.highlightBadges as badge (badge.label)} + {#if badge.variant === "solid"} + {badge.label} + {:else} + + {badge.label} + + {/if} + {/each} +
+ {/if} + +
+ {#if card.href && !card.disabled} + + {card.ctaLabel} + + {:else} + + {/if}
-
-
+ + {/each} + - -
-
-
-
- - - -
-

Gerenciar Permissões

-
-

- Configure as permissões de acesso aos menus do sistema por função. Controle quem pode acessar, consultar e gravar dados. -

- -
-
- - -
-
-
-
- - - -
-

Configuração de Email

-
-

- Configure o servidor SMTP para envio automático de notificações e emails do sistema. -

- -
-
- - -
-
-
-
- - - -
-

Gerenciar Usuários

-
-

- Criar, editar, bloquear e gerenciar usuários do sistema. Controle total sobre contas de acesso. -

- -
-
- - -
-
-
-
- - - -
-

Gerenciar Perfis

-
-

- Crie e gerencie perfis de acesso personalizados com permissões específicas para grupos de usuários. -

- -
-
- - -
-
-
-
- - - -
-

Notificações e Mensagens

-
-

- Envie notificações para usuários do sistema via chat ou email. Configure templates de mensagens reutilizáveis. -

- -
-
- - -
-
-
-
- - - -
-

Monitorar SGSE

-
-

- Monitore em tempo real as métricas técnicas do sistema: CPU, memória, rede, usuários online e muito mais. Configure alertas personalizados. -

-
-
Tempo Real
-
Alertas
-
Relatórios
-
- -
-
- - -
-
-
-
- - - -
-

Documentação

-
-

- Manuais, guias e documentação técnica do sistema para usuários e administradores. -

-
- -
-
-
-
- -
- +
+
+
+
+ + /> +
-

Área Restrita

-
- Esta é uma área de acesso restrito. Apenas usuários autorizados pela equipe de TI podem acessar o Painel Administrativo. +

Área Restrita

+

+ Esta área é exclusiva da equipe de Tecnologia da Informação. Garanta que apenas usuários autorizados acessem o Painel Administrativo e mantenha suas credenciais em segurança. +

-
+
diff --git a/apps/web/src/routes/(dashboard)/ti/painel-administrativo/+page.svelte b/apps/web/src/routes/(dashboard)/ti/painel-administrativo/+page.svelte index 10630be..0349c24 100644 --- a/apps/web/src/routes/(dashboard)/ti/painel-administrativo/+page.svelte +++ b/apps/web/src/routes/(dashboard)/ti/painel-administrativo/+page.svelte @@ -4,18 +4,35 @@ import StatsCard from "$lib/components/ti/StatsCard.svelte"; const client = useConvexClient(); - const usuarios = useQuery(api.usuarios.listar, {}); + const usuariosQuery = useQuery(api.usuarios.listar, {}); + + // Verificar se está carregando + const carregando = $derived(usuariosQuery === undefined); + + // Extrair dados dos usuários + const usuarios = $derived(usuariosQuery?.data ?? []); // Estatísticas derivadas const stats = $derived.by(() => { - if (!usuarios?.data || !Array.isArray(usuarios.data)) return null; + // Se ainda está carregando, retorna null para mostrar loading + if (carregando) return null; - const ativos = usuarios.data.filter(u => u.ativo && !u.bloqueado).length; - const bloqueados = usuarios.data.filter(u => u.bloqueado).length; - const inativos = usuarios.data.filter(u => !u.ativo).length; + // Se não há usuários, retorna stats zeradas (mas não null para não mostrar loading) + if (!Array.isArray(usuarios) || usuarios.length === 0) { + return { + total: 0, + ativos: 0, + bloqueados: 0, + inativos: 0 + }; + } + + const ativos = usuarios.filter(u => u.ativo && !u.bloqueado).length; + const bloqueados = usuarios.filter(u => u.bloqueado === true).length; + const inativos = usuarios.filter(u => !u.ativo).length; return { - total: usuarios.data.length, + total: usuarios.length, ativos, bloqueados, inativos @@ -52,7 +69,7 @@ diff --git a/bun.lock b/bun.lock index 21e4aab..16a9a59 100644 --- a/bun.lock +++ b/bun.lock @@ -61,7 +61,7 @@ "version": "1.0.0", "dependencies": { "@dicebear/avataaars": "^9.2.4", - "convex": "catalog:", + "convex": "^1.17.4", "nodemailer": "^7.0.10", }, "devDependencies": {