diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..34cff05 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +[*] +indent_style = tab +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = false +insert_final_newline = true \ No newline at end of file diff --git a/RELATORIO_TESTES.md b/RELATORIO_TESTES.md deleted file mode 100644 index 3780090..0000000 --- a/RELATORIO_TESTES.md +++ /dev/null @@ -1,186 +0,0 @@ -# Relatório de Testes - Sistema de Central de Chamados - -**Data:** 16 de novembro de 2025 -**Testador:** Sistema Automatizado -**Página Testada:** `/ti/central-chamados` - -## Resumo Executivo - -Foram realizados testes completos na página de Central de Chamados do sistema SGSE. A maioria das funcionalidades está funcionando corretamente, mas foram identificados alguns problemas que precisam ser corrigidos. - -## Testes Realizados - -### ✅ Testes Bem-Sucedidos - -1. **Login no Sistema** - - Status: ✅ PASSOU - - Usuário logado: Deyvison (dfw@poli.br) - -2. **Visualização de SLAs Configurados** - - Status: ✅ PASSOU - - Tabela de SLAs exibe 7 SLAs ativos corretamente - - Resumo mostra: 4 Baixa, 2 Média, 1 Alta/Crítica - - Detalhes completos (tempos, prioridades) são exibidos corretamente - -3. **Cards de Prioridade** - - Status: ✅ PASSOU - - Cards mostram corretamente "Configurado" ou "Não configurado" - - Botão "Configurar" funciona corretamente - - Detalhes dos SLAs configurados são exibidos nos cards - -4. **Criação de SLA** - - Status: ✅ PASSOU - - SLA criado com sucesso para prioridade "Alta" - - Formulário preenche corretamente quando clica em "Configurar" - - Tabela atualiza automaticamente após criação - - Card de prioridade atualiza para "Configurado" - -5. **Edição de SLA** - - Status: ✅ PASSOU - - Botão "Editar" abre formulário com dados corretos - - Atualização funciona corretamente - -6. **Lista de Chamados** - - Status: ✅ PASSOU - - 4 chamados sendo exibidos corretamente - - Filtros funcionando (status, responsável, setor) - - Detalhes do chamado são exibidos ao selecionar - -7. **Atribuição de Responsável** - - Status: ✅ PASSOU - - Dropdown mostra 2 usuários TI: Deyvison e Suporte_TI - - Formulário está funcional - -8. **Prorrogação de Prazo** - - Status: ✅ PASSOU - - Dropdown de tickets carrega corretamente (4 tickets) - - Formulário permite selecionar tipo de prazo e horas - - Botão habilita quando todos os campos estão preenchidos - -### ⚠️ Problemas Identificados - -#### 1. Templates de Email - Listagem Após Criação - -- **Status:** ⚠️ PROBLEMA -- **Descrição:** Templates são criados com sucesso (mensagem "Templates padrão criados com sucesso" aparece), mas não são listados na interface após criação -- **Ação Realizada:** Botão "Criar templates padrão" foi clicado e retornou sucesso -- **Comportamento Esperado:** Templates deveriam aparecer em uma lista após criação -- **Comportamento Atual:** Seção continua mostrando "Nenhum template encontrado" -- **Severidade:** MÉDIA -- **Impacto:** Usuários não conseguem visualizar/editar templates de email após criação -- **Possível Causa:** Query de templates pode não estar sendo atualizada após criação, ou filtro pode estar excluindo templates de chamados - -#### 2. Warning no Console - Token de Autenticação - -- **Status:** ⚠️ AVISO (Não crítico) -- **Descrição:** `⚠️ [useConvexWithAuth] Token não disponível` aparece no console durante carregamento inicial -- **Severidade:** BAIXA -- **Impacto:** Não afeta funcionalidade (autenticação funciona corretamente após carregamento) -- **Observação:** Parece ser um problema de timing durante inicialização da página - -#### 3. Warning no Console - Formato de Query - -- **Status:** ⚠️ AVISO (Não crítico) -- **Descrição:** `🔍 [usuariosTI] Formato inesperado: object {data: undefined, isLoading: undefined, error: undefined, isStale: undefined}` aparece no console -- **Severidade:** BAIXA -- **Impacto:** Não afeta funcionalidade (usuários são carregados corretamente - 2 usuários TI encontrados) -- **Observação:** Indica possível inconsistência no formato de retorno da query durante carregamento inicial - -## Detalhes dos Testes - -### Teste de Criação de SLA - -- **Prioridade Testada:** Alta -- **Valores Inseridos:** - - Nome: "SLA - Alta - Teste" - - Tempo de Resposta: 2h - - Tempo de Conclusão: 8h - - Auto-encerramento: 24h - - Alerta: 2h antes -- **Resultado:** ✅ SLA criado e exibido na tabela e no card - -### Teste de Edição de SLA - -- **SLA Editado:** Prioridade Baixa -- **Alterações:** - - Nome: "SLA Baixa - Editado em Teste" - - Tempo de Resposta: 6h -- **Resultado:** ✅ Atualização bem-sucedida - -### Teste de Prorrogação - -- **Ticket Selecionado:** SGSE-202511-3750 -- **Prazo:** Conclusão -- **Horas Adicionais:** 24h -- **Motivo:** "Teste de prorrogação de prazo - necessário mais tempo para análise" -- **Resultado:** ✅ Formulário preenchido corretamente, botão habilitado - -## Lista de Erros Encontrados - -### Erros Críticos - -- **Nenhum erro crítico encontrado** - -### Erros de Funcionalidade - -1. **Templates de Email não aparecem após criação** - - Localização: Seção "Templates de Email - Chamados" - - Ação necessária: Verificar query de templates e atualização reativa após criação - -### Avisos (Warnings) - -1. **Token de autenticação não disponível durante carregamento inicial** - - Localização: Console do navegador - - Ação necessária: Melhorar timing de inicialização de autenticação - -2. **Formato inesperado de query durante carregamento** - - Localização: Console do navegador (usuariosTI) - - Ação necessária: Verificar formato de retorno de useQuery do convex-svelte - -## Recomendações - -### Prioridade ALTA - -1. **Corrigir listagem de templates de email após criação** - - Verificar se a query `templatesChamados` está sendo atualizada após criação - - Verificar se o filtro de templates está correto (deve incluir templates de chamados) - - Adicionar refresh automático após criação de templates - -### Prioridade MÉDIA - -2. **Investigar e corrigir warnings no console** - - Melhorar timing de autenticação para evitar warning inicial - - Padronizar formato de retorno de queries do convex-svelte - -### Prioridade BAIXA - -3. **Melhorar logs de debug** - - Reduzir verbosidade de logs informativos - - Manter apenas logs de erro e warnings importantes - -## Conclusão - -O sistema está **funcionalmente operacional**, com a maioria das funcionalidades testadas funcionando corretamente: - -✅ **Funcionalidades Testadas e Funcionando:** - -- Login e autenticação -- Visualização de SLAs (tabela e cards) -- Criação de SLAs -- Edição de SLAs -- Lista de chamados -- Atribuição de responsável -- Prorrogação de prazo (formulário funcional) -- Criação de templates (backend funciona, frontend não atualiza) - -⚠️ **Problemas Identificados:** - -- Templates não aparecem na lista após criação (problema de atualização reativa) -- Warnings no console (não afetam funcionalidade) - -**Status Geral:** ✅ **OPERACIONAL COM PEQUENOS AJUSTES NECESSÁRIOS** - -**Próximos Passos:** - -1. Corrigir atualização reativa de templates após criação -2. Investigar e resolver warnings do console (opcional, não crítico) diff --git a/apps/web/src/lib/components/Sidebar.svelte b/apps/web/src/lib/components/Sidebar.svelte index fe64cde..bc99b9f 100644 --- a/apps/web/src/lib/components/Sidebar.svelte +++ b/apps/web/src/lib/components/Sidebar.svelte @@ -9,7 +9,7 @@ import NotificationBell from '$lib/components/chat/NotificationBell.svelte'; import ChatWidget from '$lib/components/chat/ChatWidget.svelte'; import PresenceManager from '$lib/components/chat/PresenceManager.svelte'; - import { getAvatarUrl } from '$lib/utils/avatarGenerator'; + import { Menu, User, Home, UserPlus, XCircle, LogIn, Tag, Plus, Check } from 'lucide-svelte'; import { authClient } from '$lib/auth'; import { resolve } from '$app/paths'; @@ -33,8 +33,8 @@ return currentUser.data.avatar; } - // Fallback: gerar avatar baseado no nome - return getAvatarUrl(currentUser.data.nome); + // Fallback: retornar null para usar o ícone User do Lucide + return null; }); // Função para gerar classes do menu ativo @@ -328,8 +328,9 @@ >Contato - SuporteSuporte Dashboard - {#each setores as s} + {#each setores as s (s.link)} {@const isActive = currentPath.startsWith(s.link)}
  • -
    -
    +
    +
    -
    +
    - Logo SGSE + Logo SGSE

    Login

    @@ -441,7 +434,7 @@ {#if erroLogin}
    {erroLogin} @@ -453,16 +446,14 @@
    -
    +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + +
    +
    + + +
    + + + + + + + + + + + + + + + {#if isLoading} + + + + {:else if error} + + + + {:else if contratos.length === 0} + + + + {:else} + {#each contratos as contrato (contrato._id)} + + + + + + + + + + + {/each} + {/if} + +
    Nº ContratoObjetoContratadaVigênciaValorSituaçãoResponsávelAções
    + +
    + Erro ao carregar contratos: {error.message} +
    + Nenhum contrato encontrado. +
    +
    + {contrato.numeroContrato}/{contrato.anoContrato} + {#if isProximoVencimento(contrato.dataFimVigencia, contrato.diasAvisoVencimento)} +
    + +
    + {/if} +
    +
    + {contrato.objeto} + + {contrato.contratada?.razao_social || 'Empresa não encontrada'} + +
    + {formatarData(contrato.dataInicioVigencia)} até +
    + {formatarData(contrato.dataFimVigencia)} +
    +
    {formatarMoeda(contrato.valorTotal)} +
    + {contrato.situacao.replace('_', ' ').toUpperCase()} +
    +
    + {contrato.responsavel?.nome || '-'} + + + +
    +
    +
    diff --git a/apps/web/src/routes/(dashboard)/licitacoes/contratos/[id]/+page.svelte b/apps/web/src/routes/(dashboard)/licitacoes/contratos/[id]/+page.svelte new file mode 100644 index 0000000..918d402 --- /dev/null +++ b/apps/web/src/routes/(dashboard)/licitacoes/contratos/[id]/+page.svelte @@ -0,0 +1,394 @@ + + +
    +
    + +
    +

    Editar Contrato

    +

    Atualize os dados do contrato.

    +
    +
    + + {#if isLoading} +
    + +
    + {:else if error} +
    + Erro ao carregar contrato: {error.message} +
    + {:else if contrato} +
    + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + +
    + + +
    +
    + {/if} +
    diff --git a/apps/web/src/routes/(dashboard)/licitacoes/contratos/novo/+page.svelte b/apps/web/src/routes/(dashboard)/licitacoes/contratos/novo/+page.svelte new file mode 100644 index 0000000..b7dc59a --- /dev/null +++ b/apps/web/src/routes/(dashboard)/licitacoes/contratos/novo/+page.svelte @@ -0,0 +1,363 @@ + + +
    +
    + +
    +

    Novo Contrato

    +

    Preencha os dados do novo contrato.

    +
    +
    + +
    + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + +
    + + +
    +
    +
    diff --git a/apps/web/src/routes/(dashboard)/licitacoes/empresas/+page.svelte b/apps/web/src/routes/(dashboard)/licitacoes/empresas/+page.svelte new file mode 100644 index 0000000..24c8b47 --- /dev/null +++ b/apps/web/src/routes/(dashboard)/licitacoes/empresas/+page.svelte @@ -0,0 +1,931 @@ + + +
    + + +
    +
    +
    + +
    +
    +

    Empresas

    +

    + Cadastro, listagem e contatos de empresas fornecedoras. +

    +
    +
    + +
    + +
    +
    + {#if empresasQuery.isLoading} +
    + +
    + {:else if empresasQuery.error} +
    + Erro ao carregar empresas. +
    + {:else if empresasQuery.data && empresasQuery.data.length === 0} +
    +

    Nenhuma empresa cadastrada ainda.

    + +
    + {:else if empresasQuery.data} +
    + + + + + + + + + + + + {#each empresasQuery.data as empresa (empresa._id)} + + + + + + + + {/each} + +
    CNPJRazão social / Nome fantasiaTelefoneE-mailAções
    {empresa.cnpj} +
    + {empresa.razao_social} + {#if empresa.nome_fantasia} + {empresa.nome_fantasia} + {/if} +
    +
    + + {empresa.telefone} + + + {empresa.email} + +
    + + +
    +
    +
    + {/if} +
    +
    + + {#if modalAberto} + + {/if} + + {#if contatosModalAberto} + + {/if} +
    + diff --git a/apps/web/src/routes/(dashboard)/perfil/+page.svelte b/apps/web/src/routes/(dashboard)/perfil/+page.svelte index 1152e24..4227d09 100644 --- a/apps/web/src/routes/(dashboard)/perfil/+page.svelte +++ b/apps/web/src/routes/(dashboard)/perfil/+page.svelte @@ -7,7 +7,6 @@ import WizardSolicitacaoAusencia from '$lib/components/ausencias/WizardSolicitacaoAusencia.svelte'; import AprovarAusencias from '$lib/components/AprovarAusencias.svelte'; import CalendarioAusencias from '$lib/components/ausencias/CalendarioAusencias.svelte'; - import { generateAvatarGallery } from '$lib/utils/avatars'; import ProtectedRoute from '$lib/components/ProtectedRoute.svelte'; import type { Id } from '@sgse-app/backend/convex/_generated/dataModel'; import type { FunctionReturnType } from 'convex/server'; @@ -75,7 +74,6 @@ let uploadandoFoto = $state(false); let erroUpload = $state(''); let modoFoto = $state<'upload' | 'avatar'>('avatar'); - let avatarSelecionado = $state(''); let mostrarBotaoCamera = $state(false); // Estados para Minhas Férias @@ -100,8 +98,19 @@ let erroMensagemChamado = $state(null); let sucessoMensagemChamado = $state(null); - // Galeria de avatares (30 avatares profissionais 3D realistas) - const avatarGallery = generateAvatarGallery(30); + // Avatares padrão disponíveis + const defaultAvatars = [ + '/avatars/avatar-1.png', + '/avatars/avatar-2.png', + '/avatars/avatar-3.png', + '/avatars/avatar-4.png', + '/avatars/avatar-5.png', + '/avatars/avatar-6.png', + '/avatars/avatar-7.png', + '/avatars/avatar-8.png', + '/avatars/avatar-9.png', + '/avatars/avatar-10.png' + ]; // FuncionarioId disponível diretamente do usuário atual const funcionarioIdDisponivel = $derived(currentUser?.data?.funcionarioId ?? null); @@ -441,6 +450,30 @@ return; } + await processarUploadFoto(file); + } + + async function handleEscolherAvatarPadrao(avatarPath: string) { + try { + uploadandoFoto = true; + erroUpload = ''; + + // Buscar a imagem + const response = await fetch(avatarPath); + const blob = await response.blob(); + const file = new File([blob], avatarPath.split('/').pop() || 'avatar.png', { + type: 'image/png' + }); + + await processarUploadFoto(file); + } catch (e: unknown) { + const errorMessage = e instanceof Error ? e.message : String(e); + erroUpload = errorMessage || 'Erro ao processar avatar padrão'; + uploadandoFoto = false; + } + } + + async function processarUploadFoto(file: File) { uploadandoFoto = true; erroUpload = ''; @@ -463,16 +496,12 @@ // 4. Atualizar perfil com o novo storageId await client.mutation(api.usuarios.atualizarPerfil, { - fotoPerfil: storageId, - avatar: undefined // Remove avatar se colocar foto + fotoPerfil: storageId }); // 5. Aguardar um pouco para garantir que o backend processou await new Promise((resolve) => setTimeout(resolve, 300)); - // 8. Limpar o input para permitir novo upload - input.value = ''; - // 9. Fechar modal após sucesso mostrarModalFoto = false; @@ -496,45 +525,9 @@ uploadandoFoto = false; } - async function handleSelecionarAvatar(avatarUrl: string) { - uploadandoFoto = true; - erroUpload = ''; - - try { - // 2. Salvar avatar selecionado no backend - await client.mutation(api.usuarios.atualizarPerfil, { - avatar: avatarUrl, - fotoPerfil: undefined // Remove foto se colocar avatar - }); - - // 6. Fechar modal após sucesso - mostrarModalFoto = false; - - // Toast de sucesso - const toast = document.createElement('div'); - toast.className = 'toast toast-top toast-end'; - toast.innerHTML = ` -
    - - - - Avatar atualizado com sucesso! -
    - `; - document.body.appendChild(toast); - setTimeout(() => toast.remove(), 3000); - } catch (e: unknown) { - const errorMessage = e instanceof Error ? e.message : String(e); - erroUpload = errorMessage || 'Erro ao salvar avatar'; - } finally { - uploadandoFoto = false; - } - } - function abrirModalFoto() { erroUpload = ''; modoFoto = 'avatar'; - avatarSelecionado = ''; mostrarModalFoto = true; } @@ -571,22 +564,16 @@ onclick={abrirModalFoto} >
    {#if currentUser.data?.fotoPerfilUrl} Foto de perfil - {:else if currentUser.data?.avatar} - Avatar {:else} -
    - {currentUser.data?.nome.substring(0, 2).toUpperCase()} -
    + {/if}
    @@ -2338,18 +2325,16 @@
    {#if currentUser.data?.fotoPerfilUrl} - Foto atual - {:else if currentUser.data?.avatar} - Avatar atual + Foto atual {:else} -
    - {currentUser.data?.nome.substring(0, 2).toUpperCase()} -
    + {/if}
    @@ -2413,82 +2398,37 @@

    - Escolha um dos 30 avatares profissionais para seu + Escolha um dos avatares profissionais para seu perfil

    - {#each avatarGallery as avatar (avatar.id)} + {#each defaultAvatars as avatarPath, i (avatarPath)} {/each}
    - - - + - Dica: Clique uma vez para selecionar, clique duas vezes para aplicar - imediatamente! + Dica: Clique em um avatar para defini-lo como sua foto de perfil.
    - - {#if avatarSelecionado} -
    - -
    - {/if} {:else}
    diff --git a/apps/web/src/routes/(dashboard)/programas-esportivos/+page.svelte b/apps/web/src/routes/(dashboard)/programas-esportivos/+page.svelte index eed8a11..3da1d68 100644 --- a/apps/web/src/routes/(dashboard)/programas-esportivos/+page.svelte +++ b/apps/web/src/routes/(dashboard)/programas-esportivos/+page.svelte @@ -1,5 +1,5 @@ @@ -25,23 +25,37 @@
    -
    -
    -
    -
    - +
    + + - diff --git a/apps/web/src/routes/(dashboard)/ti/+page.svelte b/apps/web/src/routes/(dashboard)/ti/+page.svelte index 4a2935d..e0f92f2 100644 --- a/apps/web/src/routes/(dashboard)/ti/+page.svelte +++ b/apps/web/src/routes/(dashboard)/ti/+page.svelte @@ -11,7 +11,8 @@ | 'monitor' | 'document' | 'teams' - | 'userPlus'; + | 'userPlus' + | 'clock'; type PaletteKey = 'primary' | 'success' | 'secondary' | 'accent' | 'info' | 'error' | 'warning'; type TiRouteId = @@ -25,7 +26,9 @@ | '/(dashboard)/ti/solicitacoes-acesso' | '/(dashboard)/ti/times' | '/(dashboard)/ti/notificacoes' - | '/(dashboard)/ti/monitoramento'; + | '/(dashboard)/ti/monitoramento' + | '/(dashboard)/ti/configuracoes-ponto' + | '/(dashboard)/ti/configuracoes-relogio'; type FeatureCard = { title: string; @@ -192,6 +195,13 @@ strokeLinecap: 'round', strokeLinejoin: 'round' } + ], + clock: [ + { + d: 'M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z', + strokeLinecap: 'round', + strokeLinejoin: 'round' + } ] }; @@ -231,15 +241,6 @@ { label: 'Alertas', variant: 'outline' } ] }, - { - title: 'Suporte Técnico', - description: - 'Central de atendimento para resolução de problemas técnicos e dúvidas sobre o sistema.', - ctaLabel: 'Em breve', - palette: 'info', - icon: 'support', - disabled: true - }, { title: 'Gerenciar Permissões', description: @@ -298,15 +299,6 @@ palette: 'accent', icon: 'users' }, - { - title: 'Solicitações de Acesso', - description: - 'Gerencie e analise solicitações de acesso ao sistema. Aprove ou rejeite novas solicitações de forma eficiente.', - ctaLabel: 'Gerenciar Solicitações', - href: '/(dashboard)/ti/solicitacoes-acesso', - palette: 'warning', - icon: 'userPlus' - }, { title: 'Gestão de Times', description: diff --git a/apps/web/src/routes/(dashboard)/ti/notificacoes/+page.svelte b/apps/web/src/routes/(dashboard)/ti/notificacoes/+page.svelte index 89f0240..703ce53 100644 --- a/apps/web/src/routes/(dashboard)/ti/notificacoes/+page.svelte +++ b/apps/web/src/routes/(dashboard)/ti/notificacoes/+page.svelte @@ -1,12 +1,11 @@ - - -
    - - {#if mensagem} -
    - {#if mensagem.tipo === 'success'} - - - - {:else if mensagem.tipo === 'error'} - - - - {/if} - {mensagem.texto} -
    - {/if} - - -
    -
    -
    - - - -
    -
    -

    Solicitações de Acesso

    -

    - Gerencie e analise solicitações de acesso ao sistema -

    -
    -
    -
    - - - {#if stats} -
    - - - 0 - ? ((stats.pendentes / stats.total) * 100).toFixed(1) + '% do total' - : '0% do total'} - Icon={Clock} - color="warning" - /> - - 0 - ? ((stats.aprovadas / stats.total) * 100).toFixed(1) + '% do total' - : '0% do total'} - Icon={CheckCircle2} - color="success" - /> - - 0 - ? ((stats.rejeitadas / stats.total) * 100).toFixed(1) + '% do total' - : '0% do total'} - Icon={XCircle} - color="error" - /> -
    - {:else} -
    - -
    - {/if} - - -
    -
    - -
    - - - - -
    - - -
    - -
    - - - - -
    -
    -
    -
    - - - {#if carregando} -
    - -
    - {:else if solicitacoesFiltradas.length === 0} -
    -
    - - - -

    - Nenhuma solicitação encontrada -

    -

    - {#if busca.trim() || filtroStatus !== 'todos'} - Tente ajustar os filtros ou a busca. - {:else} - Ainda não há solicitações de acesso cadastradas. - {/if} -

    -
    -
    - {:else} -
    - {#each solicitacoesFiltradas as solicitacao} -
    -
    -
    -
    -
    -

    {solicitacao.nome}

    - - {getStatusTexto(solicitacao.status)} - -
    - -
    -
    - - - - Matrícula: - {solicitacao.matricula} -
    - -
    - - - - E-mail: - {solicitacao.email} -
    - -
    - - - - Telefone: - {solicitacao.telefone} -
    -
    - -
    - Solicitado em: - {formatarData(solicitacao.dataSolicitacao)} ({formatarDataRelativa( - solicitacao.dataSolicitacao - )}) - {#if solicitacao.dataResposta} - Processado em: - {formatarData(solicitacao.dataResposta)} - {/if} -
    -
    - -
    - - - {#if solicitacao.status === 'pendente'} - - - - {/if} -
    -
    -
    -
    - {/each} -
    - {/if} - - - {#if modalDetalhesAberto && solicitacaoSelecionada} - - - - - {/if} - - - {#if modalAprovarAberto && solicitacaoSelecionada} - - - - - {/if} - - - {#if modalRejeitarAberto && solicitacaoSelecionada} - - - - - {/if} -
    -
    diff --git a/apps/web/static/avatars/avatar-1.png b/apps/web/static/avatars/avatar-1.png new file mode 100644 index 0000000..ce3844a Binary files /dev/null and b/apps/web/static/avatars/avatar-1.png differ diff --git a/apps/web/static/avatars/avatar-10.png b/apps/web/static/avatars/avatar-10.png new file mode 100644 index 0000000..95c4ac2 Binary files /dev/null and b/apps/web/static/avatars/avatar-10.png differ diff --git a/apps/web/static/avatars/avatar-2.png b/apps/web/static/avatars/avatar-2.png new file mode 100644 index 0000000..6f62584 Binary files /dev/null and b/apps/web/static/avatars/avatar-2.png differ diff --git a/apps/web/static/avatars/avatar-3.png b/apps/web/static/avatars/avatar-3.png new file mode 100644 index 0000000..b4a3958 Binary files /dev/null and b/apps/web/static/avatars/avatar-3.png differ diff --git a/apps/web/static/avatars/avatar-4.png b/apps/web/static/avatars/avatar-4.png new file mode 100644 index 0000000..1bca315 Binary files /dev/null and b/apps/web/static/avatars/avatar-4.png differ diff --git a/apps/web/static/avatars/avatar-5.png b/apps/web/static/avatars/avatar-5.png new file mode 100644 index 0000000..da6419a Binary files /dev/null and b/apps/web/static/avatars/avatar-5.png differ diff --git a/apps/web/static/avatars/avatar-6.png b/apps/web/static/avatars/avatar-6.png new file mode 100644 index 0000000..0cbfd8c Binary files /dev/null and b/apps/web/static/avatars/avatar-6.png differ diff --git a/apps/web/static/avatars/avatar-7.png b/apps/web/static/avatars/avatar-7.png new file mode 100644 index 0000000..dd3f712 Binary files /dev/null and b/apps/web/static/avatars/avatar-7.png differ diff --git a/apps/web/static/avatars/avatar-8.png b/apps/web/static/avatars/avatar-8.png new file mode 100644 index 0000000..1abc883 Binary files /dev/null and b/apps/web/static/avatars/avatar-8.png differ diff --git a/apps/web/static/avatars/avatar-9.png b/apps/web/static/avatars/avatar-9.png new file mode 100644 index 0000000..bf769cc Binary files /dev/null and b/apps/web/static/avatars/avatar-9.png differ diff --git a/bun.lock b/bun.lock index ac40209..3d30081 100644 --- a/bun.lock +++ b/bun.lock @@ -1,5 +1,6 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "sgse-app", diff --git a/cibersecurity-after-restart.png b/cibersecurity-after-restart.png deleted file mode 100644 index 3a1c3e2..0000000 Binary files a/cibersecurity-after-restart.png and /dev/null differ diff --git a/cibersecurity-error-500.png b/cibersecurity-error-500.png deleted file mode 100644 index b691cbf..0000000 Binary files a/cibersecurity-error-500.png and /dev/null differ diff --git a/cibersecurity-final.png b/cibersecurity-final.png deleted file mode 100644 index eb4a062..0000000 Binary files a/cibersecurity-final.png and /dev/null differ diff --git a/cibersecurity-page-current.png b/cibersecurity-page-current.png deleted file mode 100644 index 3a1c3e2..0000000 Binary files a/cibersecurity-page-current.png and /dev/null differ diff --git a/cibersecurity-with-ratelimit.png b/cibersecurity-with-ratelimit.png deleted file mode 100644 index de3c72f..0000000 Binary files a/cibersecurity-with-ratelimit.png and /dev/null differ diff --git a/cibersecurity-working.png b/cibersecurity-working.png deleted file mode 100644 index e867d2b..0000000 Binary files a/cibersecurity-working.png and /dev/null differ diff --git a/erro-autenticacao.png b/erro-autenticacao.png deleted file mode 100644 index c79804c..0000000 Binary files a/erro-autenticacao.png and /dev/null differ diff --git a/packages/backend/convex/_generated/api.d.ts b/packages/backend/convex/_generated/api.d.ts index 10444d0..14944ae 100644 --- a/packages/backend/convex/_generated/api.d.ts +++ b/packages/backend/convex/_generated/api.d.ts @@ -23,12 +23,14 @@ import type * as chat from "../chat.js"; import type * as configuracaoEmail from "../configuracaoEmail.js"; import type * as configuracaoPonto from "../configuracaoPonto.js"; import type * as configuracaoRelogio from "../configuracaoRelogio.js"; +import type * as contratos from "../contratos.js"; import type * as crons from "../crons.js"; import type * as cursos from "../cursos.js"; import type * as dashboard from "../dashboard.js"; import type * as documentos from "../documentos.js"; import type * as email from "../email.js"; import type * as enderecosMarcacao from "../enderecosMarcacao.js"; +import type * as empresas from "../empresas.js"; import type * as ferias from "../ferias.js"; import type * as funcionarioEnderecos from "../funcionarioEnderecos.js"; import type * as funcionarios from "../funcionarios.js"; @@ -47,7 +49,6 @@ import type * as saldoFerias from "../saldoFerias.js"; import type * as security from "../security.js"; import type * as seed from "../seed.js"; import type * as simbolos from "../simbolos.js"; -import type * as solicitacoesAcesso from "../solicitacoesAcesso.js"; import type * as templatesMensagens from "../templatesMensagens.js"; import type * as times from "../times.js"; import type * as todos from "../todos.js"; @@ -77,12 +78,14 @@ declare const fullApi: ApiFromModules<{ configuracaoEmail: typeof configuracaoEmail; configuracaoPonto: typeof configuracaoPonto; configuracaoRelogio: typeof configuracaoRelogio; + contratos: typeof contratos; crons: typeof crons; cursos: typeof cursos; dashboard: typeof dashboard; documentos: typeof documentos; email: typeof email; enderecosMarcacao: typeof enderecosMarcacao; + empresas: typeof empresas; ferias: typeof ferias; funcionarioEnderecos: typeof funcionarioEnderecos; funcionarios: typeof funcionarios; @@ -101,7 +104,6 @@ declare const fullApi: ApiFromModules<{ security: typeof security; seed: typeof seed; simbolos: typeof simbolos; - solicitacoesAcesso: typeof solicitacoesAcesso; templatesMensagens: typeof templatesMensagens; times: typeof times; todos: typeof todos; diff --git a/packages/backend/convex/chat.ts b/packages/backend/convex/chat.ts index 7be20da..6354215 100644 --- a/packages/backend/convex/chat.ts +++ b/packages/backend/convex/chat.ts @@ -70,8 +70,7 @@ export const criarConversa = mutation({ args: { tipo: v.union(v.literal('individual'), v.literal('grupo'), v.literal('sala_reuniao')), participantes: v.array(v.id('usuarios')), - nome: v.optional(v.string()), - avatar: v.optional(v.string()) + nome: v.optional(v.string()) }, handler: async (ctx, args) => { const usuarioAtual = await getUsuarioAutenticado(ctx); @@ -103,7 +102,6 @@ export const criarConversa = mutation({ const dadosConversa: Omit, '_id' | '_creationTime'> = { tipo: args.tipo, nome: args.nome, - avatar: args.avatar, participantes: args.participantes, criadoPor: usuarioAtual._id, criadoEm: Date.now() @@ -152,8 +150,7 @@ export const criarConversa = mutation({ export const criarSalaReuniao = mutation({ args: { nome: v.string(), - participantes: v.array(v.id('usuarios')), - avatar: v.optional(v.string()) + participantes: v.array(v.id('usuarios')) }, handler: async (ctx, args) => { const usuarioAtual = await getUsuarioAutenticado(ctx); @@ -174,7 +171,6 @@ export const criarSalaReuniao = mutation({ const dadosConversa: Omit, '_id' | '_creationTime'> = { tipo: 'sala_reuniao' as const, nome: args.nome.trim(), - avatar: args.avatar, participantes: participantesUnicos, criadoPor: usuarioAtual._id, criadoEm: Date.now(), @@ -2010,7 +2006,6 @@ export const obterUsuariosOnline = query({ _id: u._id, nome: u.nome, email: u.email, - avatar: u.avatar, fotoPerfil: u.fotoPerfil, statusPresenca: u.statusPresenca, statusMensagem: u.statusMensagem, @@ -2055,7 +2050,6 @@ export const listarTodosUsuarios = query({ nome: u.nome, email: u.email, matricula, - avatar: u.avatar, fotoPerfil: u.fotoPerfil, fotoPerfilUrl, statusPresenca: u.statusPresenca, diff --git a/packages/backend/convex/contratos.ts b/packages/backend/convex/contratos.ts new file mode 100644 index 0000000..059101a --- /dev/null +++ b/packages/backend/convex/contratos.ts @@ -0,0 +1,200 @@ +import { mutation, query } from "./_generated/server"; +import { v } from "convex/values"; +import { situacaoContrato } from "./schema"; +import { getCurrentUserFunction } from "./auth"; +import { internal } from "./_generated/api"; + +export const listar = query({ + args: { + responsavelId: v.optional(v.id("funcionarios")), + dataInicio: v.optional(v.string()), + dataFim: v.optional(v.string()), + }, + handler: async (ctx, args) => { + await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, { + recurso: "contratos", + acao: "listar", + }); + + let q = ctx.db.query("contratos"); + + if (args.responsavelId) { + q = q.withIndex("by_responsavel", (q) => + q.eq("responsavelId", args.responsavelId!) + ) as typeof q; + } + + const contratos = await q.collect(); + + // Filtros em memória para datas (já que Convex não tem filtro de range nativo eficiente combinado com outros índices sem setup complexo) + // Se o volume for muito grande, ideal seria criar índices específicos ou usar search. + let resultado = contratos; + + if (args.dataInicio) { + resultado = resultado.filter( + (c) => c.dataInicioVigencia >= args.dataInicio! + ); + } + + if (args.dataFim) { + resultado = resultado.filter((c) => c.dataFimVigencia <= args.dataFim!); + } + + // Enriquecer com dados relacionados + const contratosEnriquecidos = await Promise.all( + resultado.map(async (c) => { + const contratada = await ctx.db.get(c.contratadaId); + const responsavel = await ctx.db.get(c.responsavelId); + return { + ...c, + contratada, + responsavel, + }; + }) + ); + + return contratosEnriquecidos; + }, +}); + +export const obter = query({ + args: { id: v.id("contratos") }, + handler: async (ctx, args) => { + await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, { + recurso: "contratos", + acao: "ver", + }); + const contrato = await ctx.db.get(args.id); + if (!contrato) return null; + + const contratada = await ctx.db.get(contrato.contratadaId); + const responsavel = await ctx.db.get(contrato.responsavelId); + + return { + ...contrato, + contratada, + responsavel, + }; + }, +}); + +export const criar = mutation({ + args: { + contratadaId: v.id("empresas"), + objeto: v.string(), + numeroNotaEmpenho: v.string(), + responsavelId: v.id("funcionarios"), + departamento: v.string(), + situacao: situacaoContrato, + numeroProcessoLicitatorio: v.string(), + modalidade: v.string(), + numeroContrato: v.string(), + anoContrato: v.number(), + dataInicioVigencia: v.string(), + dataFimVigencia: v.string(), + nomeFiscal: v.string(), + valorTotal: v.string(), + dataAditivoPrazo: v.optional(v.string()), + diasAvisoVencimento: v.number(), + }, + handler: async (ctx, args) => { + await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, { + recurso: "contratos", + acao: "criar", + }); + + const usuario = await getCurrentUserFunction(ctx); + if (!usuario) throw new Error("Não autenticado"); + + const id = await ctx.db.insert("contratos", { + ...args, + criadoPor: usuario._id, + criadoEm: Date.now(), + }); + + return id; + }, +}); + +export const editar = mutation({ + args: { + id: v.id("contratos"), + contratadaId: v.optional(v.id("empresas")), + objeto: v.optional(v.string()), + numeroNotaEmpenho: v.optional(v.string()), + responsavelId: v.optional(v.id("funcionarios")), + departamento: v.optional(v.string()), + situacao: v.optional(situacaoContrato), + numeroProcessoLicitatorio: v.optional(v.string()), + modalidade: v.optional(v.string()), + numeroContrato: v.optional(v.string()), + anoContrato: v.optional(v.number()), + dataInicioVigencia: v.optional(v.string()), + dataFimVigencia: v.optional(v.string()), + nomeFiscal: v.optional(v.string()), + valorTotal: v.optional(v.string()), + dataAditivoPrazo: v.optional(v.string()), + diasAvisoVencimento: v.optional(v.number()), + }, + handler: async (ctx, args) => { + await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, { + recurso: "contratos", + acao: "editar", + }); + + const { id, ...campos } = args; + + await ctx.db.patch(id, { + ...campos, + atualizadoEm: Date.now(), + }); + }, +}); + +export const excluir = mutation({ + args: { id: v.id("contratos") }, + handler: async (ctx, args) => { + await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, { + recurso: "contratos", + acao: "excluir", + }); + await ctx.db.delete(args.id); + }, +}); + +export const verificarVencimentos = query({ + args: {}, + handler: async (ctx) => { + // Esta query pode ser usada por um componente de notificação ou cron job + // Retorna contratos que estão próximos do vencimento baseados no diasAvisoVencimento + + const hoje = new Date(); + const hojeStr = hoje.toISOString().split("T")[0]; + + // Buscar contratos ativos (em execução ou aguardando assinatura) + const contratos = await ctx.db + .query("contratos") + .filter((q) => + q.or( + q.eq(q.field("situacao"), "em_execucao"), + q.eq(q.field("situacao"), "aguardando_assinatura") + ) + ) + .collect(); + + const proximosVencimento = contratos.filter((c) => { + if (!c.dataFimVigencia) return false; + + const dataFim = new Date(c.dataFimVigencia); + const dataAviso = new Date(dataFim); + dataAviso.setDate(dataAviso.getDate() - c.diasAvisoVencimento); + + const dataAvisoStr = dataAviso.toISOString().split("T")[0]; + + // Se hoje for maior ou igual a data de aviso e menor que a data fim + return hojeStr >= dataAvisoStr && hojeStr <= c.dataFimVigencia; + }); + + return proximosVencimento; + }, +}); diff --git a/packages/backend/convex/dashboard.ts b/packages/backend/convex/dashboard.ts index 7612ce9..1308d9d 100644 --- a/packages/backend/convex/dashboard.ts +++ b/packages/backend/convex/dashboard.ts @@ -7,8 +7,6 @@ export const getStats = query({ returns: v.object({ totalFuncionarios: v.number(), totalSimbolos: v.number(), - totalSolicitacoesAcesso: v.number(), - solicitacoesPendentes: v.number(), funcionariosAtivos: v.number(), funcionariosDesligados: v.number(), cargoComissionado: v.number(), @@ -42,19 +40,9 @@ export const getStats = query({ const simbolos = await ctx.db.query("simbolos").collect(); const totalSimbolos = simbolos.length; - // Contar solicitações de acesso - const solicitacoes = await ctx.db.query("solicitacoesAcesso").collect(); - const totalSolicitacoesAcesso = solicitacoes.length; - - const solicitacoesPendentes = solicitacoes.filter( - (s) => s.status === "pendente" - ).length; - return { totalFuncionarios, totalSimbolos, - totalSolicitacoesAcesso, - solicitacoesPendentes, funcionariosAtivos, funcionariosDesligados, cargoComissionado, @@ -68,7 +56,6 @@ export const getRecentActivity = query({ args: {}, returns: v.object({ funcionariosCadastrados24h: v.number(), - solicitacoesAcesso24h: v.number(), simbolosCadastrados24h: v.number(), }), handler: async (ctx) => { @@ -81,11 +68,6 @@ export const getRecentActivity = query({ (f) => f._creationTime >= last24h ).length; - // Solicitações de acesso nas últimas 24h - const solicitacoes = await ctx.db.query("solicitacoesAcesso").collect(); - const solicitacoesAcesso24h = solicitacoes.filter( - (s) => s.dataSolicitacao >= last24h - ).length; // Símbolos cadastrados nas últimas 24h const simbolos = await ctx.db.query("simbolos").collect(); @@ -95,7 +77,6 @@ export const getRecentActivity = query({ return { funcionariosCadastrados24h, - solicitacoesAcesso24h, simbolosCadastrados24h, }; }, @@ -137,15 +118,13 @@ export const getEvolucaoCadastros = query({ v.object({ mes: v.string(), funcionarios: v.number(), - solicitacoes: v.number(), }) ), handler: async (ctx) => { const funcionarios = await ctx.db.query("funcionarios").collect(); - const solicitacoes = await ctx.db.query("solicitacoesAcesso").collect(); const now = new Date(); - const meses: Array<{ mes: string; funcionarios: number; solicitacoes: number }> = []; + const meses: Array<{ mes: string; funcionarios: number }> = []; // Últimos 6 meses for (let i = 5; i >= 0; i--) { @@ -161,14 +140,9 @@ export const getEvolucaoCadastros = query({ (f) => f._creationTime >= date.getTime() && f._creationTime < nextDate.getTime() ).length; - const solCount = solicitacoes.filter( - (s) => s.dataSolicitacao >= date.getTime() && s.dataSolicitacao < nextDate.getTime() - ).length; - meses.push({ mes: mesNome, funcionarios: funcCount, - solicitacoes: solCount, }); } diff --git a/packages/backend/convex/email.ts b/packages/backend/convex/email.ts index 09befce..c136376 100644 --- a/packages/backend/convex/email.ts +++ b/packages/backend/convex/email.ts @@ -162,6 +162,29 @@ export const enfileirarEmail = mutation({ }, }); +/** + * Cancelar agendamento de email + */ +export const cancelarAgendamentoEmail = mutation({ + args: { + emailId: v.id("notificacoesEmail"), + }, + handler: async (ctx, args) => { + const email = await ctx.db.get(args.emailId); + if (!email) { + return { sucesso: false, erro: "Email não encontrado" }; + } + + if (email.status !== "pendente") { + return { sucesso: false, erro: "Apenas emails pendentes podem ser cancelados" }; + } + + // Remove o email da fila + await ctx.db.delete(args.emailId); + return { sucesso: true }; + }, +}); + /** * Enviar email usando template */ diff --git a/packages/backend/convex/empresas.ts b/packages/backend/convex/empresas.ts new file mode 100644 index 0000000..996699f --- /dev/null +++ b/packages/backend/convex/empresas.ts @@ -0,0 +1,292 @@ +import { v } from "convex/values"; +import { mutation, query } from "./_generated/server"; +import { internal } from "./_generated/api"; +import { getCurrentUserFunction } from "./auth"; +import type { Id } from "./_generated/dataModel"; + +export const list = query({ + args: {}, + handler: async (ctx) => { + await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, { + recurso: "empresas", + acao: "listar", + }); + + const empresas = await ctx.db.query("empresas").collect(); + return empresas; + }, +}); + +export const getById = query({ + args: { id: v.id("empresas") }, + handler: async (ctx, args) => { + await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, { + recurso: "empresas", + acao: "ver", + }); + + const empresa = await ctx.db.get(args.id); + if (!empresa) { + return null; + } + + const contatos = await ctx.db + .query("contatosEmpresa") + .withIndex("by_empresa", (q) => q.eq("empresaId", args.id)) + .collect(); + const endereco = empresa.enderecoId + ? await ctx.db.get(empresa.enderecoId as Id<"enderecos">) + : null; + + return { ...empresa, endereco, contatos }; + }, +}); + +const contatoInput = v.object({ + _id: v.optional(v.id("contatosEmpresa")), + empresaId: v.optional(v.id("empresas")), + nome: v.string(), + funcao: v.string(), + email: v.string(), + telefone: v.string(), + adicionadoPor: v.optional(v.id("usuarios")), + descricao: v.optional(v.string()), + _deleted: v.optional(v.boolean()), +}); + +const enderecoInput = v.object({ + cep: v.string(), + logradouro: v.string(), + numero: v.string(), + complemento: v.optional(v.string()), + bairro: v.string(), + cidade: v.string(), + uf: v.string(), +}); + +export const create = mutation({ + args: { + razao_social: v.string(), + nome_fantasia: v.optional(v.string()), + cnpj: v.string(), + telefone: v.string(), + email: v.string(), + descricao: v.optional(v.string()), + endereco: v.optional(enderecoInput), + contatos: v.optional(v.array(contatoInput)), + }, + returns: v.id("empresas"), + handler: async (ctx, args) => { + const usuarioAtual = await getCurrentUserFunction(ctx); + + if (!usuarioAtual) { + throw new Error("Usuário não autenticado."); + } + + await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, { + recurso: "empresas", + acao: "criar", + }); + + const cnpjExistente = await ctx.db + .query("empresas") + .withIndex("by_cnpj", (q) => q.eq("cnpj", args.cnpj)) + .unique(); + + if (cnpjExistente) { + throw new Error("Já existe uma empresa cadastrada com este CNPJ."); + } + let enderecoId: Id<"enderecos"> | undefined; + if (args.endereco) { + enderecoId = await ctx.db.insert("enderecos", { + cep: args.endereco.cep, + logradouro: args.endereco.logradouro, + numero: args.endereco.numero, + complemento: args.endereco.complemento, + bairro: args.endereco.bairro, + cidade: args.endereco.cidade, + uf: args.endereco.uf, + criadoPor: usuarioAtual._id, + atualizadoPor: usuarioAtual._id, + }); + } + + const empresaDoc: { + razao_social: string; + nome_fantasia?: string; + cnpj: string; + telefone: string; + email: string; + descricao?: string; + enderecoId?: Id<"enderecos">; + criadoPor: Id<"usuarios">; + } = { + razao_social: args.razao_social, + cnpj: args.cnpj, + telefone: args.telefone, + email: args.email, + criadoPor: usuarioAtual._id, + }; + + if (args.nome_fantasia !== undefined) { + empresaDoc.nome_fantasia = args.nome_fantasia; + } + if (args.descricao !== undefined) { + empresaDoc.descricao = args.descricao; + } + if (enderecoId) { + empresaDoc.enderecoId = enderecoId; + } + + const empresaId = await ctx.db.insert("empresas", empresaDoc); + + if (args.contatos && args.contatos.length > 0) { + for (const contato of args.contatos) { + await ctx.db.insert("contatosEmpresa", { + empresaId, + nome: contato.nome, + funcao: contato.funcao, + email: contato.email, + telefone: contato.telefone, + adicionadoPor: usuarioAtual._id, + descricao: contato.descricao, + }); + } + } + + return empresaId; + }, +}); + +export const update = mutation({ + args: { + id: v.id("empresas"), + razao_social: v.string(), + nome_fantasia: v.optional(v.string()), + cnpj: v.string(), + telefone: v.string(), + email: v.string(), + descricao: v.optional(v.string()), + endereco: v.optional(enderecoInput), + contatos: v.optional(v.array(contatoInput)), + }, + returns: v.null(), + handler: async (ctx, args) => { + await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, { + recurso: "empresas", + acao: "editar", + }); + + const cnpjExistente = await ctx.db + .query("empresas") + .withIndex("by_cnpj", (q) => q.eq("cnpj", args.cnpj)) + .unique(); + + if (cnpjExistente && cnpjExistente._id !== args.id) { + throw new Error("Já existe uma empresa cadastrada com este CNPJ."); + } + const empresa = await ctx.db.get(args.id); + if (!empresa) { + throw new Error("Empresa não encontrada."); + } + + if (args.endereco) { + if (empresa.enderecoId) { + const usuarioAtual = await getCurrentUserFunction(ctx); + await ctx.db.patch(empresa.enderecoId as Id<"enderecos">, { + cep: args.endereco.cep, + logradouro: args.endereco.logradouro, + numero: args.endereco.numero, + complemento: args.endereco.complemento, + bairro: args.endereco.bairro, + cidade: args.endereco.cidade, + uf: args.endereco.uf, + atualizadoPor: usuarioAtual?._id, + }); + } else { + const usuarioAtual = await getCurrentUserFunction(ctx); + + if (!usuarioAtual) { + throw new Error("Usuário não autenticado."); + } + + const novoEnderecoId: Id<"enderecos"> = await ctx.db.insert("enderecos", { + cep: args.endereco.cep, + logradouro: args.endereco.logradouro, + numero: args.endereco.numero, + complemento: args.endereco.complemento, + bairro: args.endereco.bairro, + cidade: args.endereco.cidade, + uf: args.endereco.uf, + criadoPor: usuarioAtual._id, + atualizadoPor: usuarioAtual._id, + }); + + await ctx.db.patch(args.id, { + enderecoId: novoEnderecoId, + }); + } + } + + const patchDoc: { + razao_social: string; + nome_fantasia?: string; + cnpj: string; + telefone: string; + email: string; + descricao?: string; + } = { + razao_social: args.razao_social, + cnpj: args.cnpj, + telefone: args.telefone, + email: args.email, + }; + + if (args.nome_fantasia !== undefined) { + patchDoc.nome_fantasia = args.nome_fantasia; + } + if (args.descricao !== undefined) { + patchDoc.descricao = args.descricao; + } + + await ctx.db.patch(args.id, patchDoc); + + if (!args.contatos) { + return null; + } + + for (const contato of args.contatos) { + if (contato._id && contato._deleted) { + await ctx.db.delete(contato._id); + } else if (contato._id) { + await ctx.db.patch(contato._id, { + nome: contato.nome, + funcao: contato.funcao, + email: contato.email, + telefone: contato.telefone, + descricao: contato.descricao, + }); + } else if (!contato._deleted) { + const usuarioAtual = await getCurrentUserFunction(ctx); + + if (!usuarioAtual) { + throw new Error("Usuário não autenticado."); + } + + await ctx.db.insert("contatosEmpresa", { + empresaId: args.id, + nome: contato.nome, + funcao: contato.funcao, + email: contato.email, + telefone: contato.telefone, + adicionadoPor: usuarioAtual._id, + descricao: contato.descricao, + }); + } + } + + return null; + }, +}); + + diff --git a/packages/backend/convex/monitoramento.ts b/packages/backend/convex/monitoramento.ts index 0cbd46f..a3bb14f 100644 --- a/packages/backend/convex/monitoramento.ts +++ b/packages/backend/convex/monitoramento.ts @@ -594,12 +594,11 @@ export const getStatusSistema = query({ } // Total de registros (estimativa baseada em tabelas principais) - const [usuarios, funcionarios, simbolos, solicitacoesAcesso, alertas, metricas] = + const [usuarios, funcionarios, simbolos, alertas, metricas] = await Promise.all([ ctx.db.query('usuarios').collect(), ctx.db.query('funcionarios').collect(), ctx.db.query('simbolos').collect(), - ctx.db.query('solicitacoesAcesso').collect(), ctx.db.query('alertConfigurations').collect(), ctx.db.query('systemMetrics').take(100) // não precisa contar tudo ]); @@ -607,7 +606,6 @@ export const getStatusSistema = query({ usuarios.length + funcionarios.length + simbolos.length + - solicitacoesAcesso.length + alertas.length + metricas.length; diff --git a/packages/backend/convex/permissoesAcoes.ts b/packages/backend/convex/permissoesAcoes.ts index ee982f4..d92e1d9 100644 --- a/packages/backend/convex/permissoesAcoes.ts +++ b/packages/backend/convex/permissoesAcoes.ts @@ -224,6 +224,36 @@ const PERMISSOES_BASE = { acao: 'ver', descricao: 'Acessar telas do módulo de licitações' }, + { + nome: 'contratos.listar', + recurso: 'contratos', + acao: 'listar', + descricao: 'Listar contratos' + }, + { + nome: 'contratos.criar', + recurso: 'contratos', + acao: 'criar', + descricao: 'Criar novos contratos' + }, + { + nome: 'contratos.editar', + recurso: 'contratos', + acao: 'editar', + descricao: 'Editar contratos' + }, + { + nome: 'contratos.excluir', + recurso: 'contratos', + acao: 'excluir', + descricao: 'Excluir contratos' + }, + { + nome: 'contratos.ver', + recurso: 'contratos', + acao: 'ver', + descricao: 'Visualizar detalhes de contratos' + }, // Compras { nome: 'compras.ver', diff --git a/packages/backend/convex/schema.ts b/packages/backend/convex/schema.ts index f891d83..dccc159 100644 --- a/packages/backend/convex/schema.ts +++ b/packages/backend/convex/schema.ts @@ -120,11 +120,78 @@ export const reportStatus = v.union( v.literal("falhou") ); +export const situacaoContrato = v.union( + v.literal("em_execucao"), + v.literal("rescendido"), + v.literal("aguardando_assinatura"), + v.literal("finalizado") +); + export default defineSchema({ + contratos: defineTable({ + contratadaId: v.id("empresas"), + objeto: v.string(), + numeroNotaEmpenho: v.string(), + responsavelId: v.id("funcionarios"), + departamento: v.string(), + situacao: situacaoContrato, + numeroProcessoLicitatorio: v.string(), + modalidade: v.string(), + numeroContrato: v.string(), + anoContrato: v.number(), + dataInicioVigencia: v.string(), + dataFimVigencia: v.string(), + nomeFiscal: v.string(), + valorTotal: v.string(), + dataAditivoPrazo: v.optional(v.string()), + diasAvisoVencimento: v.number(), + criadoPor: v.id("usuarios"), + criadoEm: v.number(), + atualizadoEm: v.optional(v.number()), + }) + .index("by_responsavel", ["responsavelId"]) + .index("by_situacao", ["situacao"]) + .index("by_vigencia_inicio", ["dataInicioVigencia"]) + .index("by_vigencia_fim", ["dataFimVigencia"]), + todos: defineTable({ text: v.string(), completed: v.boolean(), }), + enderecos: defineTable({ + cep: v.string(), + logradouro: v.string(), + numero: v.string(), + complemento: v.optional(v.string()), + bairro: v.string(), + cidade: v.string(), + uf: v.string(), + criadoPor: v.optional(v.id("usuarios")), + atualizadoPor: v.optional(v.id("usuarios")), + }).index("by_cep", ["cep"]), + empresas: defineTable({ + razao_social: v.string(), + nome_fantasia: v.optional(v.string()), + cnpj: v.string(), + telefone: v.string(), + email: v.string(), + descricao: v.optional(v.string()), + enderecoId: v.optional(v.id("enderecos")), + criadoPor: v.optional(v.id("usuarios")), + }) + .index("by_razao_social", ["razao_social"]) + .index("by_cnpj", ["cnpj"]), + contatosEmpresa: defineTable({ + empresaId: v.id("empresas"), + nome: v.string(), + funcao: v.string(), + email: v.string(), + telefone: v.string(), + adicionadoPor: v.optional(v.id("usuarios")), + descricao: v.optional(v.string()), + }) + .index("by_empresa", ["empresaId"]) + .index("by_email", ["email"]), funcionarios: defineTable({ // Campos obrigatórios existentes nome: v.string(), @@ -447,24 +514,6 @@ export default defineSchema({ valor: v.string(), }), - solicitacoesAcesso: defineTable({ - nome: v.string(), - matricula: v.string(), - email: v.string(), - telefone: v.string(), - status: v.union( - v.literal("pendente"), - v.literal("aprovado"), - v.literal("rejeitado") - ), - dataSolicitacao: v.number(), - dataResposta: v.optional(v.number()), - observacoes: v.optional(v.string()), - }) - .index("by_status", ["status"]) - .index("by_matricula", ["matricula"]) - .index("by_email", ["email"]), - // Sistema de Autenticação e Controle de Acesso usuarios: defineTable({ authId: v.string(), @@ -486,7 +535,7 @@ export default defineSchema({ ultimaTentativaLogin: v.optional(v.number()), // timestamp da última tentativa // Campos de Chat e Perfil - avatar: v.optional(v.string()), // "avatar-1" até "avatar-15" ou storageId + fotoPerfil: v.optional(v.id("_storage")), setor: v.optional(v.string()), statusMensagem: v.optional(v.string()), // max 100 chars @@ -712,7 +761,7 @@ export default defineSchema({ v.literal("sala_reuniao") ), nome: v.optional(v.string()), // nome do grupo/sala - avatar: v.optional(v.string()), // avatar do grupo/sala + participantes: v.array(v.id("usuarios")), // IDs dos participantes administradores: v.optional(v.array(v.id("usuarios"))), // IDs dos administradores (apenas para sala_reuniao) ultimaMensagem: v.optional(v.string()), diff --git a/packages/backend/convex/seed.ts b/packages/backend/convex/seed.ts index 2af404c..3fee8cb 100644 --- a/packages/backend/convex/seed.ts +++ b/packages/backend/convex/seed.ts @@ -164,27 +164,6 @@ const funcionariosData = [ } ]; -const solicitacoesAcessoData = [ - { - dataResposta: 1761445098933, - dataSolicitacao: 1761445038329, - email: 'severino@gmail.com', - matricula: '3231', - nome: 'Severino Gates', - observacoes: 'Aprovação realizada por Deyvison', - status: 'aprovado' as const, - telefone: '(81) 9942-3551' - }, - { - dataSolicitacao: 1761445187258, - email: 'michaeljackson@gmail.com', - matricula: '123321', - nome: 'Michael Jackson', - status: 'pendente' as const, - telefone: '(81) 99423-5551' - } -]; - /** * Seed inicial do banco de dados com os dados exportados do Convex Cloud */ @@ -338,8 +317,6 @@ export const seedCreateUsuariosParaFuncionarios = internalMutation({ }); delay += 50; } - // Agenda próxima etapa após as criações individuais - await ctx.scheduler.runAfter(delay + 300, internal.seed.seedInserirSolicitacoesAcesso, {}); return null; } }); @@ -402,55 +379,6 @@ export const seedCreateUsuarioParaFuncionario = internalMutation({ } }); -export const seedInserirSolicitacoesAcesso = internalMutation({ - args: {}, - returns: v.null(), - handler: async (ctx) => { - console.log('📋 Inserindo solicitações de acesso...'); - for (const solicitacao of solicitacoesAcessoData) { - // Evitar duplicidade por matrícula - const existente = await ctx.db - .query('solicitacoesAcesso') - .withIndex('by_matricula', (q) => q.eq('matricula', solicitacao.matricula)) - .first(); - if (existente) { - console.log(` ℹ️ Solicitação já existe p/ matrícula ${solicitacao.matricula}`); - continue; - } - const dadosSolicitacao: { - nome: string; - matricula: string; - email: string; - telefone: string; - status: 'pendente' | 'aprovado' | 'rejeitado'; - dataSolicitacao: number; - dataResposta?: number; - observacoes?: string; - } = { - nome: solicitacao.nome, - matricula: solicitacao.matricula, - email: solicitacao.email, - telefone: solicitacao.telefone, - status: solicitacao.status, - dataSolicitacao: solicitacao.dataSolicitacao - }; - - if (solicitacao.dataResposta) { - dadosSolicitacao.dataResposta = solicitacao.dataResposta; - } - - if (solicitacao.observacoes) { - dadosSolicitacao.observacoes = solicitacao.observacoes; - } - - await ctx.db.insert('solicitacoesAcesso', dadosSolicitacao); - console.log(` ✅ Solicitação criada: ${solicitacao.nome} (${solicitacao.status})`); - } - console.log('✨ Seed concluído!'); - return null; - } -}); - export const seedDatabase = internalAction({ args: {}, returns: v.null(), @@ -460,7 +388,6 @@ export const seedDatabase = internalAction({ await ctx.runMutation(internal.seed.seedCreateSimbolos, {}); await ctx.runMutation(internal.seed.seedCreateFuncionarios, {}); await ctx.runMutation(internal.seed.seedCreateUsuariosParaFuncionarios, {}); - await ctx.runMutation(internal.seed.seedInserirSolicitacoesAcesso, {}); console.log('✨ Seed do banco de dados concluído com sucesso pela action!'); return null; } @@ -677,13 +604,6 @@ export const clearDatabase = internalMutation({ } console.log(` ✅ ${funcionarios.length} funcionários removidos`); - // 20. Solicitações de acesso - const solicitacoesAcesso = await ctx.db.query('solicitacoesAcesso').collect(); - for (const solicitacao of solicitacoesAcesso) { - await ctx.db.delete(solicitacao._id); - } - console.log(` ✅ ${solicitacoesAcesso.length} solicitações de acesso removidas`); - // 21. Símbolos const simbolos = await ctx.db.query('simbolos').collect(); for (const simbolo of simbolos) { @@ -907,13 +827,6 @@ export const limparBanco = mutation({ } console.log(` ✅ ${funcionarios.length} funcionários removidos`); - // 20. Solicitações de acesso - const solicitacoesAcesso = await ctx.db.query('solicitacoesAcesso').collect(); - for (const solicitacao of solicitacoesAcesso) { - await ctx.db.delete(solicitacao._id); - } - console.log(` ✅ ${solicitacoesAcesso.length} solicitações de acesso removidas`); - // 21. Símbolos const simbolos = await ctx.db.query('simbolos').collect(); for (const simbolo of simbolos) { diff --git a/packages/backend/convex/solicitacoesAcesso.ts b/packages/backend/convex/solicitacoesAcesso.ts deleted file mode 100644 index b5ff1ef..0000000 --- a/packages/backend/convex/solicitacoesAcesso.ts +++ /dev/null @@ -1,234 +0,0 @@ -import { mutation, query } from "./_generated/server"; -import { v } from "convex/values"; - -// Criar uma nova solicitação de acesso -export const create = mutation({ - args: { - nome: v.string(), - matricula: v.string(), - email: v.string(), - telefone: v.string(), - }, - returns: v.object({ - solicitacaoId: v.id("solicitacoesAcesso"), - }), - handler: async (ctx, args) => { - // Verificar se já existe uma solicitação pendente com a mesma matrícula - const existingByMatricula = await ctx.db - .query("solicitacoesAcesso") - .withIndex("by_matricula", (q) => q.eq("matricula", args.matricula)) - .filter((q) => q.eq(q.field("status"), "pendente")) - .first(); - - if (existingByMatricula) { - throw new Error("Já existe uma solicitação pendente para esta matrícula."); - } - - // Verificar se já existe uma solicitação pendente com o mesmo email - const existingByEmail = await ctx.db - .query("solicitacoesAcesso") - .withIndex("by_email", (q) => q.eq("email", args.email)) - .filter((q) => q.eq(q.field("status"), "pendente")) - .first(); - - if (existingByEmail) { - throw new Error("Já existe uma solicitação pendente para este e-mail."); - } - - const solicitacaoId = await ctx.db.insert("solicitacoesAcesso", { - nome: args.nome, - matricula: args.matricula, - email: args.email, - telefone: args.telefone, - status: "pendente", - dataSolicitacao: Date.now(), - }); - - return { solicitacaoId }; - }, -}); - -// Listar todas as solicitações (para o painel administrativo) -export const getAll = query({ - args: {}, - returns: v.array( - v.object({ - _id: v.id("solicitacoesAcesso"), - _creationTime: v.number(), - nome: v.string(), - matricula: v.string(), - email: v.string(), - telefone: v.string(), - status: v.union( - v.literal("pendente"), - v.literal("aprovado"), - v.literal("rejeitado") - ), - dataSolicitacao: v.number(), - dataResposta: v.union(v.number(), v.null()), - observacoes: v.union(v.string(), v.null()), - }) - ), - handler: async (ctx) => { - const solicitacoes = await ctx.db - .query("solicitacoesAcesso") - .order("desc") - .collect(); - - return solicitacoes.map((s) => ({ - _id: s._id, - _creationTime: s._creationTime, - nome: s.nome, - matricula: s.matricula, - email: s.email, - telefone: s.telefone, - status: s.status, - dataSolicitacao: s.dataSolicitacao, - dataResposta: s.dataResposta ?? null, - observacoes: s.observacoes ?? null, - })); - }, -}); - -// Listar apenas solicitações pendentes -export const getPendentes = query({ - args: {}, - returns: v.array( - v.object({ - _id: v.id("solicitacoesAcesso"), - _creationTime: v.number(), - nome: v.string(), - matricula: v.string(), - email: v.string(), - telefone: v.string(), - status: v.union( - v.literal("pendente"), - v.literal("aprovado"), - v.literal("rejeitado") - ), - dataSolicitacao: v.number(), - dataResposta: v.union(v.number(), v.null()), - observacoes: v.union(v.string(), v.null()), - }) - ), - handler: async (ctx) => { - const solicitacoes = await ctx.db - .query("solicitacoesAcesso") - .withIndex("by_status", (q) => q.eq("status", "pendente")) - .order("desc") - .collect(); - - return solicitacoes.map((s) => ({ - _id: s._id, - _creationTime: s._creationTime, - nome: s.nome, - matricula: s.matricula, - email: s.email, - telefone: s.telefone, - status: s.status, - dataSolicitacao: s.dataSolicitacao, - dataResposta: s.dataResposta ?? null, - observacoes: s.observacoes ?? null, - })); - }, -}); - -// Aprovar uma solicitação -export const aprovar = mutation({ - args: { - solicitacaoId: v.id("solicitacoesAcesso"), - observacoes: v.optional(v.string()), - }, - returns: v.null(), - handler: async (ctx, args) => { - const solicitacao = await ctx.db.get(args.solicitacaoId); - if (!solicitacao) { - throw new Error("Solicitação não encontrada."); - } - - if (solicitacao.status !== "pendente") { - throw new Error("Esta solicitação já foi processada."); - } - - await ctx.db.patch(args.solicitacaoId, { - status: "aprovado", - dataResposta: Date.now(), - observacoes: args.observacoes, - }); - - return null; - }, -}); - -// Rejeitar uma solicitação -export const rejeitar = mutation({ - args: { - solicitacaoId: v.id("solicitacoesAcesso"), - observacoes: v.optional(v.string()), - }, - returns: v.null(), - handler: async (ctx, args) => { - const solicitacao = await ctx.db.get(args.solicitacaoId); - if (!solicitacao) { - throw new Error("Solicitação não encontrada."); - } - - if (solicitacao.status !== "pendente") { - throw new Error("Esta solicitação já foi processada."); - } - - await ctx.db.patch(args.solicitacaoId, { - status: "rejeitado", - dataResposta: Date.now(), - observacoes: args.observacoes, - }); - - return null; - }, -}); - -// Obter uma solicitação por ID -export const getById = query({ - args: { - solicitacaoId: v.id("solicitacoesAcesso"), - }, - returns: v.union( - v.object({ - _id: v.id("solicitacoesAcesso"), - _creationTime: v.number(), - nome: v.string(), - matricula: v.string(), - email: v.string(), - telefone: v.string(), - status: v.union( - v.literal("pendente"), - v.literal("aprovado"), - v.literal("rejeitado") - ), - dataSolicitacao: v.number(), - dataResposta: v.union(v.number(), v.null()), - observacoes: v.union(v.string(), v.null()), - }), - v.null() - ), - handler: async (ctx, args) => { - const solicitacao = await ctx.db.get(args.solicitacaoId); - if (!solicitacao) { - return null; - } - - return { - _id: solicitacao._id, - _creationTime: solicitacao._creationTime, - nome: solicitacao.nome, - matricula: solicitacao.matricula, - email: solicitacao.email, - telefone: solicitacao.telefone, - status: solicitacao.status, - dataSolicitacao: solicitacao.dataSolicitacao, - dataResposta: solicitacao.dataResposta ?? null, - observacoes: solicitacao.observacoes ?? null, - }; - }, -}); - diff --git a/packages/backend/convex/usuarios.ts b/packages/backend/convex/usuarios.ts index cd62bac..6816947 100644 --- a/packages/backend/convex/usuarios.ts +++ b/packages/backend/convex/usuarios.ts @@ -476,11 +476,10 @@ export const alterarRole = mutation({ }); /** - * Atualizar perfil do usuário (foto, avatar, setor, status, preferências) + * Atualizar perfil do usuário (foto, setor, status, preferências) */ export const atualizarPerfil = mutation({ args: { - avatar: v.optional(v.string()), fotoPerfil: v.optional(v.id('_storage')), setor: v.optional(v.string()), statusMensagem: v.optional(v.string()), @@ -511,7 +510,6 @@ export const atualizarPerfil = mutation({ atualizadoEm: Date.now() }; - if (args.avatar !== undefined) updates.avatar = args.avatar; if (args.fotoPerfil !== undefined) updates.fotoPerfil = args.fotoPerfil; if (args.setor !== undefined) updates.setor = args.setor; if (args.statusMensagem !== undefined) updates.statusMensagem = args.statusMensagem; @@ -541,7 +539,6 @@ export const obterPerfil = query({ email: v.string(), matricula: v.optional(v.string()), funcionarioId: v.optional(v.id('funcionarios')), - avatar: v.optional(v.string()), fotoPerfil: v.optional(v.id('_storage')), fotoPerfilUrl: v.union(v.string(), v.null()), setor: v.optional(v.string()), @@ -582,7 +579,6 @@ export const obterPerfil = query({ email: usuarioAtual.email, matricula: matricula || undefined, funcionarioId: usuarioAtual.funcionarioId, - avatar: usuarioAtual.avatar, fotoPerfil: usuarioAtual.fotoPerfil, fotoPerfilUrl, setor: usuarioAtual.setor, @@ -595,7 +591,7 @@ export const obterPerfil = query({ }); /** - * Listar todos usuários para o chat (com avatar, foto e status) + * Listar todos usuários para o chat (com foto e status) */ export const listarParaChat = query({ args: {}, @@ -605,7 +601,6 @@ export const listarParaChat = query({ nome: v.string(), email: v.string(), matricula: v.optional(v.string()), - avatar: v.optional(v.string()), fotoPerfil: v.optional(v.id('_storage')), fotoPerfilUrl: v.union(v.string(), v.null()), statusPresenca: v.optional( @@ -656,7 +651,6 @@ export const listarParaChat = query({ nome: usuario.nome, email: usuario.email, matricula: matricula || undefined, - avatar: usuario.avatar, fotoPerfil: usuario.fotoPerfil, fotoPerfilUrl, statusPresenca: usuario.statusPresenca || 'offline', diff --git a/pagina-cibersecurity-logado.png b/pagina-cibersecurity-logado.png deleted file mode 100644 index 6caefe7..0000000 Binary files a/pagina-cibersecurity-logado.png and /dev/null differ