diff --git a/apps/web/src/lib/components/documentacao/DocumentacaoCard.svelte b/apps/web/src/lib/components/documentacao/DocumentacaoCard.svelte deleted file mode 100644 index e6bb380..0000000 --- a/apps/web/src/lib/components/documentacao/DocumentacaoCard.svelte +++ /dev/null @@ -1,167 +0,0 @@ - - -
{ - // Se não houver seleção habilitada, redireciona para a página do documento - if (!onToggleSelecao) { - window.location.href = resolve(`/ti/documentacao/${documento._id}`); - } - // Se houver seleção, não faz nada no clique do card (apenas o checkbox seleciona) - }} -> - - {#if onToggleSelecao} - - {/if} - - -
-
- -
-
-

- {documento.titulo} -

-
- - {tipoLabels[documento.tipo] || documento.tipo} - - {#if documento.geradoAutomaticamente} - Auto - {/if} -
-
-
- - - {#if documento.conteudo} -

- {documento.conteudo.substring(0, 150)}... -

- {/if} - - - {#if documento.tags && documento.tags.length > 0} -
- - {#each documento.tags.slice(0, 3) as tag} - {tag} - {/each} - {#if documento.tags.length > 3} - +{documento.tags.length - 3} - {/if} -
- {/if} - - -
-
- - - {documento.atualizadoEm && !isNaN(new Date(documento.atualizadoEm).getTime()) - ? format(new Date(documento.atualizadoEm), "dd/MM/yyyy 'às' HH:mm", { locale: ptBR }) - : 'Data não disponível'} - -
-
- {#if documento.visualizacoes !== undefined} - {documento.visualizacoes} visualizações - {/if} - {#if onVisualizar} - - {/if} -
-
-
- diff --git a/apps/web/src/lib/components/documentacao/DocumentacaoModal.svelte b/apps/web/src/lib/components/documentacao/DocumentacaoModal.svelte deleted file mode 100644 index 10713fa..0000000 --- a/apps/web/src/lib/components/documentacao/DocumentacaoModal.svelte +++ /dev/null @@ -1,322 +0,0 @@ - - -{#if documentoId} - {#if documentoQuery === undefined} - - {:else if documentoQuery} - {@const doc = documentoQuery} - {#if doc} - - {/if} - {:else} - - {/if} -{/if} - - - diff --git a/apps/web/src/lib/components/documentacao/DocumentacaoSearch.svelte b/apps/web/src/lib/components/documentacao/DocumentacaoSearch.svelte deleted file mode 100644 index 8dbc4c9..0000000 --- a/apps/web/src/lib/components/documentacao/DocumentacaoSearch.svelte +++ /dev/null @@ -1,31 +0,0 @@ - - -
-
- - - {#if busca} - - {/if} -
-
- diff --git a/apps/web/src/lib/components/documentacao/DocumentacaoSidebar.svelte b/apps/web/src/lib/components/documentacao/DocumentacaoSidebar.svelte deleted file mode 100644 index a0611ab..0000000 --- a/apps/web/src/lib/components/documentacao/DocumentacaoSidebar.svelte +++ /dev/null @@ -1,133 +0,0 @@ - - -
- -
-

- - Categorias -

-
- {#each categorias as categoria} - - {/each} - {#if categorias.length === 0} -

Nenhuma categoria disponível

- {/if} -
-
- - -
-

- - Tipos -

-
- {#each tipos as tipo} - - {/each} -
-
- - -
-

- - Tags -

-
- {#each tags.slice(0, 20) as tag} - - {/each} - {#if tags.length === 0} -

Nenhuma tag disponível

- {/if} -
-
-
- diff --git a/apps/web/src/lib/components/documentacao/PdfGenerator.svelte b/apps/web/src/lib/components/documentacao/PdfGenerator.svelte deleted file mode 100644 index 847e14f..0000000 --- a/apps/web/src/lib/components/documentacao/PdfGenerator.svelte +++ /dev/null @@ -1,306 +0,0 @@ - - - - diff --git a/apps/web/src/routes/(dashboard)/ti/+page.svelte b/apps/web/src/routes/(dashboard)/ti/+page.svelte index 97cf9e2..15a0281 100644 --- a/apps/web/src/routes/(dashboard)/ti/+page.svelte +++ b/apps/web/src/routes/(dashboard)/ti/+page.svelte @@ -427,16 +427,6 @@ href: '/(dashboard)/ti/configuracoes', palette: 'secondary', icon: 'control' - }, - { - title: 'Documentação', - description: - 'Manuais, guias e documentação técnica do sistema para usuários e administradores.', - ctaLabel: 'Acessar Biblioteca', - href: '/(dashboard)/ti/documentacao', - palette: 'primary', - icon: 'document', - disabled: false } ]; diff --git a/apps/web/src/routes/(dashboard)/ti/documentacao/+page.svelte b/apps/web/src/routes/(dashboard)/ti/documentacao/+page.svelte deleted file mode 100644 index d52750c..0000000 --- a/apps/web/src/routes/(dashboard)/ti/documentacao/+page.svelte +++ /dev/null @@ -1,232 +0,0 @@ - - - -
- -
-
-
-
-
- - Biblioteca de Documentação - -

- Documentação Técnica do SGSE -

-

- Biblioteca completa com todas as funcionalidades, recursos, manuais técnicos e - explicações detalhadas dos algoritmos do sistema. -

-
-
- - - Configuração - - {#if documentosSelecionados.length > 0} - - {/if} -
-
-
- - -
-
- - -
- - {#if busca || categoriaSelecionada || tipoSelecionado || tagsSelecionadas.length > 0} - - {/if} -
-
- - - {#if mostrarFiltros} -
- -
- {/if} -
- - -
-
-

- Documentos - {#if documentosQuery} - - ({totalDocumentos}) - - {/if} -

-
- - {#if documentosQuery === undefined} -
- -
- {:else if documentos.length === 0} -
- -

Nenhum documento encontrado

-

- {#if busca || categoriaSelecionada || tipoSelecionado || tagsSelecionadas.length > 0} - Tente ajustar os filtros de busca. - {:else} - Ainda não há documentos cadastrados. Execute uma varredura para gerar documentação - automaticamente. - {/if} -

-
- {:else} -
- {#each documentos as documento (documento._id)} - toggleSelecaoDocumento(documento._id)} - onVisualizar={() => abrirModal(documento._id)} - /> - {/each} -
- {/if} -
-
- - - {#if mostrarPdfGenerator} - { - mostrarPdfGenerator = false; - documentosSelecionados = []; - }} - /> - {/if} - - - -
diff --git a/apps/web/src/routes/(dashboard)/ti/documentacao/[id]/+page.svelte b/apps/web/src/routes/(dashboard)/ti/documentacao/[id]/+page.svelte deleted file mode 100644 index 90894ba..0000000 --- a/apps/web/src/routes/(dashboard)/ti/documentacao/[id]/+page.svelte +++ /dev/null @@ -1,531 +0,0 @@ - - - -
- -
- - - Voltar - - -
- - {#if documentoQuery === undefined} -
- -
- {:else if !documentoQuery} -
-

Documento não encontrado

-

O documento solicitado não existe ou foi removido.

-
- {:else} - -
- - {#if indice().length > 0} - - {/if} - - -
- -
-
-
-
- - {documentoQuery.tipo} -
-

{documentoQuery.titulo}

-
-
- - - Atualizado em {documentoQuery.atualizadoEm && !isNaN(new Date(documentoQuery.atualizadoEm).getTime()) - ? format(new Date(documentoQuery.atualizadoEm), "dd/MM/yyyy 'às' HH:mm", { - locale: ptBR - }) - : 'Data não disponível'} - -
- {#if documentoQuery.criadoPorUsuario} -
- - Por {documentoQuery.criadoPorUsuario.nome} -
- {/if} -
- - Versão {documentoQuery.versao} -
-
-
-
-
- - -
- - {#if documentoQuery.tags && documentoQuery.tags.length > 0} -
- - {#each documentoQuery.tags as tag} - {tag} - {/each} -
- {/if} - - -
- {@html conteudoHtml()} -
- - - {#if documentoQuery.arquivoOrigem} -
-

- - Informações do Arquivo -

-
-
- Arquivo Origem: - {documentoQuery.arquivoOrigem} -
- {#if documentoQuery.funcaoOrigem} -
- Função Origem: - {documentoQuery.funcaoOrigem} -
- {/if} -
- Hash: - {documentoQuery.hash} -
-
-
- {/if} - - - {#if documentoQuery.metadados} -
-

Detalhes Técnicos

-
- {#if documentoQuery.metadados.parametros && documentoQuery.metadados.parametros.length > 0} -
-

Parâmetros de Entrada:

-
-
    - {#each documentoQuery.metadados.parametros as param} -
  • - - {param} -
  • - {/each} -
-
-
- {/if} - {#if documentoQuery.metadados.retorno} -
-

Valor de Retorno:

-
-

{documentoQuery.metadados.retorno}

-
-
- {/if} - {#if documentoQuery.metadados.dependencias && documentoQuery.metadados.dependencias.length > 0} -
-

Dependências:

-
-
    - {#each documentoQuery.metadados.dependencias as dep} -
  • - - {dep} -
  • - {/each} -
-
-
- {/if} -
-
- {/if} -
-
-
- {/if} -
-
- - - diff --git a/apps/web/src/routes/(dashboard)/ti/documentacao/configuracao/+page.svelte b/apps/web/src/routes/(dashboard)/ti/documentacao/configuracao/+page.svelte deleted file mode 100644 index 5b3b800..0000000 --- a/apps/web/src/routes/(dashboard)/ti/documentacao/configuracao/+page.svelte +++ /dev/null @@ -1,521 +0,0 @@ - - - -
- - - -
-
-
-
-

Configuração de Varredura

-

- Configure o agendamento automático de varredura de documentação -

-
-
- - {#if configQuery === undefined} -
- -
- {:else} - -
-

- - Agendamento -

- -
- - {#if config._id} -
-

Configuração Atual

-
-
- Status: - - {config.ativo ? 'Ativa' : 'Inativa'} - -
- {#if config.ultimaExecucao && !isNaN(new Date(config.ultimaExecucao).getTime())} -
- Última execução: - - {format(new Date(config.ultimaExecucao), "dd/MM/yyyy 'às' HH:mm", { - locale: ptBR - })} - -
- {/if} - {#if config.proximaExecucao && !isNaN(new Date(config.proximaExecucao).getTime())} -
- Próxima execução: - - {format(new Date(config.proximaExecucao), "dd/MM/yyyy 'às' HH:mm", { - locale: ptBR - })} - -
- {/if} - {#if config.atualizadoEm && !isNaN(new Date(config.atualizadoEm).getTime())} -
- Atualizado em: - - {format(new Date(config.atualizadoEm), "dd/MM/yyyy 'às' HH:mm", { - locale: ptBR - })} - -
- {/if} -
-
- {/if} - - -
- -
- - -
- -
- {#each diasSemana as dia} - - {/each} -
-
- - -
- - -
- - -
- - -
-
-
- - -
-

- - Histórico de Varreduras -

- - {#if historicoQuery === undefined} -
- -
- {:else if historicoVarreduras.length === 0} -
-

Nenhuma varredura executada ainda

-
- {:else} -
- {#each historicoVarreduras as varredura} -
-
-
- - {varredura.tipo === 'automatica' ? 'Automática' : 'Manual'} - - - {statusLabels[varredura.status] || varredura.status} - - - {varredura.iniciadoEm && !isNaN(new Date(varredura.iniciadoEm).getTime()) - ? format(new Date(varredura.iniciadoEm), "dd/MM/yyyy 'às' HH:mm", { - locale: ptBR - }) - : 'Data não disponível'} - -
-
- {varredura.duracaoMs - ? `${(varredura.duracaoMs / 1000).toFixed(1)}s` - : '-'} -
-
- -
-
- Arquivos analisados: - {varredura.arquivosAnalisados} -
-
- Documentos novos: - {varredura.documentosNovos} -
-
- Documentos atualizados: - {varredura.documentosAtualizados} -
-
- Executado por: - - {varredura.executadoPorUsuario?.nome || 'N/A'} - -
-
- - {#if varredura.totalArquivosModificados !== undefined && varredura.totalArquivosModificados > 0} -
-
- - Arquivos modificados: {varredura.totalArquivosModificados} - -
-
- {#each varredura.arquivosModificados || [] as arquivo} -
-
- - {arquivo.arquivo} - - v{arquivo.versao} -
- {#if arquivo.funcoes.length > 0} -
- Funções: {arquivo.funcoes.join(', ')} -
- {/if} -
- {/each} -
-
- {/if} -
- {/each} -
- {/if} -
- {/if} -
-
- - -{#if showSuccessModal} - -{/if} - - -{#if showErrorModal} - -{/if} - diff --git a/packages/backend/convex/_generated/api.d.ts b/packages/backend/convex/_generated/api.d.ts index f0e786d..deaea6d 100644 --- a/packages/backend/convex/_generated/api.d.ts +++ b/packages/backend/convex/_generated/api.d.ts @@ -9,7 +9,6 @@ */ import type * as acoes from "../acoes.js"; -import type * as actions_documentacaoVarredura from "../actions/documentacaoVarredura.js"; import type * as actions_email from "../actions/email.js"; import type * as actions_linkPreview from "../actions/linkPreview.js"; import type * as actions_pushNotifications from "../actions/pushNotifications.js"; @@ -32,8 +31,6 @@ 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 documentacao from "../documentacao.js"; -import type * as documentacaoVarredura from "../documentacaoVarredura.js"; import type * as documentos from "../documentos.js"; import type * as email from "../email.js"; import type * as empresas from "../empresas.js"; @@ -67,7 +64,6 @@ import type * as tables_auth from "../tables/auth.js"; import type * as tables_chat from "../tables/chat.js"; import type * as tables_contratos from "../tables/contratos.js"; import type * as tables_cursos from "../tables/cursos.js"; -import type * as tables_documentacao from "../tables/documentacao.js"; import type * as tables_empresas from "../tables/empresas.js"; import type * as tables_enderecos from "../tables/enderecos.js"; import type * as tables_ferias from "../tables/ferias.js"; @@ -101,7 +97,6 @@ import type { declare const fullApi: ApiFromModules<{ acoes: typeof acoes; - "actions/documentacaoVarredura": typeof actions_documentacaoVarredura; "actions/email": typeof actions_email; "actions/linkPreview": typeof actions_linkPreview; "actions/pushNotifications": typeof actions_pushNotifications; @@ -124,8 +119,6 @@ declare const fullApi: ApiFromModules<{ crons: typeof crons; cursos: typeof cursos; dashboard: typeof dashboard; - documentacao: typeof documentacao; - documentacaoVarredura: typeof documentacaoVarredura; documentos: typeof documentos; email: typeof email; empresas: typeof empresas; @@ -159,7 +152,6 @@ declare const fullApi: ApiFromModules<{ "tables/chat": typeof tables_chat; "tables/contratos": typeof tables_contratos; "tables/cursos": typeof tables_cursos; - "tables/documentacao": typeof tables_documentacao; "tables/empresas": typeof tables_empresas; "tables/enderecos": typeof tables_enderecos; "tables/ferias": typeof tables_ferias; diff --git a/packages/backend/convex/actions/documentacaoVarredura.ts b/packages/backend/convex/actions/documentacaoVarredura.ts deleted file mode 100644 index adab71a..0000000 --- a/packages/backend/convex/actions/documentacaoVarredura.ts +++ /dev/null @@ -1,137 +0,0 @@ -'use node'; - -import { action } from '../_generated/server'; -import { v } from 'convex/values'; -import { internal } from '../_generated/api'; -import * as fs from 'fs'; -import * as path from 'path'; - -/** - * Action para ler arquivos do sistema de arquivos e analisar funções - */ -export const analisarArquivos = action({ - args: { - varreduraId: v.id('documentacaoVarredura'), - arquivos: v.array(v.string()), - executadoPor: v.id('usuarios') - }, - returns: v.object({ - arquivosAnalisados: v.number(), - documentosNovos: v.number(), - documentosAtualizados: v.number(), - erros: v.array(v.string()) - }), - handler: async (ctx, args) => { - 'use node'; - - const erros: string[] = []; - let arquivosAnalisados = 0; - let documentosNovos = 0; - let documentosAtualizados = 0; - - console.log('🔍 [analisarArquivos] Iniciando análise de arquivos...'); - console.log(`📁 Total de arquivos: ${args.arquivos.length}`); - console.log(`📋 Arquivos: ${args.arquivos.join(', ')}`); - - // Tentar diferentes caminhos possíveis - const caminhosPossiveis = [ - path.join(process.cwd(), 'packages/backend/convex'), - path.join(process.cwd(), 'convex'), - path.resolve(__dirname, '../../convex'), - path.resolve(__dirname, '../../../convex') - ]; - - let basePath: string | null = null; - for (const caminho of caminhosPossiveis) { - if (fs.existsSync(caminho)) { - basePath = caminho; - console.log(`✅ Caminho encontrado: ${basePath}`); - break; - } - } - - if (!basePath) { - const erro = `Nenhum caminho válido encontrado. Tentados: ${caminhosPossiveis.join(', ')}`; - console.error(`❌ ${erro}`); - erros.push(erro); - await ctx.runMutation(internal.documentacaoVarredura.atualizarVarreduraComResultados, { - varreduraId: args.varreduraId, - documentosNovos: 0, - documentosAtualizados: 0, - arquivosAnalisados: 0, - erros: [erro] - }); - return { - arquivosAnalisados: 0, - documentosNovos: 0, - documentosAtualizados: 0, - erros: [erro] - }; - } - - for (const nomeArquivo of args.arquivos) { - try { - const arquivoPath = path.join(basePath, nomeArquivo); - console.log(`📄 Processando: ${arquivoPath}`); - - // Verificar se o arquivo existe - if (!fs.existsSync(arquivoPath)) { - const erro = `Arquivo não encontrado: ${nomeArquivo} (caminho: ${arquivoPath})`; - console.error(`❌ ${erro}`); - erros.push(erro); - continue; - } - - // Ler conteúdo do arquivo - const conteudo = fs.readFileSync(arquivoPath, 'utf-8'); - console.log(`✅ Arquivo lido: ${nomeArquivo} (${conteudo.length} caracteres)`); - - // Analisar funções no arquivo usando a mutation interna - const resultado = await ctx.runMutation( - internal.documentacaoVarredura.processarArquivo, - { - varreduraId: args.varreduraId, - nomeArquivo, - conteudo, - executadoPor: args.executadoPor - } - ); - - console.log( - `✅ Processado ${nomeArquivo}: ${resultado.documentosNovos} novos, ${resultado.documentosAtualizados} atualizados` - ); - - arquivosAnalisados++; - documentosNovos += resultado.documentosNovos; - documentosAtualizados += resultado.documentosAtualizados; - } catch (error) { - const erroMsg = error instanceof Error ? error.message : 'Erro desconhecido'; - const erroStack = error instanceof Error ? error.stack : undefined; - const erroCompleto = `Erro ao processar ${nomeArquivo}: ${erroMsg}${erroStack ? `\n${erroStack}` : ''}`; - console.error(`❌ ${erroCompleto}`); - erros.push(erroCompleto); - } - } - - console.log( - `📊 Resumo: ${arquivosAnalisados} arquivos analisados, ${documentosNovos} novos, ${documentosAtualizados} atualizados` - ); - - // Atualizar varredura com os resultados - await ctx.runMutation(internal.documentacaoVarredura.atualizarVarreduraComResultados, { - varreduraId: args.varreduraId, - documentosNovos, - documentosAtualizados, - arquivosAnalisados, - erros - }); - - return { - arquivosAnalisados, - documentosNovos, - documentosAtualizados, - erros - }; - } -}); - diff --git a/packages/backend/convex/crons.ts b/packages/backend/convex/crons.ts index 2588dee..3ef6845 100644 --- a/packages/backend/convex/crons.ts +++ b/packages/backend/convex/crons.ts @@ -58,12 +58,4 @@ crons.interval( {} ); -// Verificar e executar varredura de documentação a cada hora (verifica configuração internamente) -crons.interval( - 'verificar-varredura-documentacao', - { hours: 1 }, - internal.documentacaoVarredura.verificarEExecutarVarreduraAgendada, - {} -); - export default crons; diff --git a/packages/backend/convex/documentacao.ts b/packages/backend/convex/documentacao.ts deleted file mode 100644 index 9fac055..0000000 --- a/packages/backend/convex/documentacao.ts +++ /dev/null @@ -1,842 +0,0 @@ -import { v } from 'convex/values'; -import { mutation, query } from './_generated/server'; -import { Doc, Id } from './_generated/dataModel'; -import type { QueryCtx, MutationCtx } from './_generated/server'; -import { getCurrentUserFunction } from './auth'; -import { api } from './_generated/api'; - -// ========== HELPERS ========== - -/** - * Normaliza texto para busca (remove acentos, converte para lowercase) - */ -function normalizarTextoParaBusca(texto: string): string { - return texto - .toLowerCase() - .normalize('NFD') - .replace(/[\u0300-\u036f]/g, '') // Remove diacríticos - .trim(); -} - -/** - * Verifica se o usuário tem permissão de TI (nível 0 ou 1) - */ -async function verificarPermissaoTI(ctx: QueryCtx | MutationCtx): Promise> { - const usuarioAtual = await getCurrentUserFunction(ctx); - if (!usuarioAtual) { - throw new Error('Não autenticado'); - } - - // Verificar se é TI (nível 0 ou 1) - const role = await ctx.db.get(usuarioAtual.roleId); - if (!role || (role.nivel > 1 && role.nome !== 'ti_master' && role.nome !== 'ti_usuario')) { - throw new Error('Acesso negado. Apenas usuários TI podem acessar a documentação.'); - } - - return usuarioAtual; -} - -// ========== QUERIES ========== - -/** - * Listar todos os documentos (com filtros opcionais) - */ -export const listarDocumentos = query({ - args: { - categoriaId: v.optional(v.id('documentacaoCategorias')), - tipo: v.optional( - v.union( - v.literal('query'), - v.literal('mutation'), - v.literal('action'), - v.literal('component'), - v.literal('route'), - v.literal('modulo'), - v.literal('manual'), - v.literal('outro') - ) - ), - tags: v.optional(v.array(v.string())), - ativo: v.optional(v.boolean()), - busca: v.optional(v.string()), - limite: v.optional(v.number()), - offset: v.optional(v.number()) - }, - handler: async (ctx, args) => { - await verificarPermissaoTI(ctx); - - let documentos: Doc<'documentacao'>[] = []; - - // Aplicar filtros usando índices quando possível - if (args.categoriaId) { - documentos = await ctx.db - .query('documentacao') - .withIndex('by_categoria', (q) => q.eq('categoriaId', args.categoriaId)) - .collect(); - } else if (args.tipo) { - documentos = await ctx.db - .query('documentacao') - .withIndex('by_tipo', (q) => q.eq('tipo', args.tipo!)) - .collect(); - } else if (args.ativo !== undefined) { - documentos = await ctx.db - .query('documentacao') - .withIndex('by_ativo', (q) => q.eq('ativo', args.ativo!)) - .collect(); - } else { - documentos = await ctx.db.query('documentacao').collect(); - } - - // Filtrar por tags (se fornecido) - if (args.tags && args.tags.length > 0) { - documentos = documentos.filter((doc) => - args.tags!.some((tag) => doc.tags.includes(tag)) - ); - } - - // Filtrar por busca (full-text) - if (args.busca && args.busca.trim().length > 0) { - const buscaNormalizada = normalizarTextoParaBusca(args.busca); - documentos = documentos.filter((doc) => { - const tituloBusca = normalizarTextoParaBusca(doc.titulo); - return ( - doc.conteudoBusca.includes(buscaNormalizada) || - tituloBusca.includes(buscaNormalizada) - ); - }); - } - - // Ordenar por data de atualização (mais recentes primeiro) - documentos.sort((a, b) => b.atualizadoEm - a.atualizadoEm); - - // Aplicar paginação - const offset = args.offset || 0; - const limite = args.limite || 50; - const documentosPaginados = documentos.slice(offset, offset + limite); - - // Enriquecer com informações de categoria - const documentosEnriquecidos = await Promise.all( - documentosPaginados.map(async (doc) => { - let categoria = null; - if (doc.categoriaId) { - categoria = await ctx.db.get(doc.categoriaId); - } - - return { - ...doc, - categoria - }; - }) - ); - - return { - documentos: documentosEnriquecidos, - total: documentos.length, - offset, - limite - }; - } -}); - -/** - * Obter um documento por ID - */ -export const obterDocumento = query({ - args: { - documentoId: v.id('documentacao') - }, - handler: async (ctx, args) => { - await verificarPermissaoTI(ctx); - - const documento = await ctx.db.get(args.documentoId); - if (!documento) { - throw new Error('Documento não encontrado'); - } - - // Nota: Não podemos fazer patch em uma query, então o contador de visualizações - // será atualizado apenas quando o documento for atualizado via mutation - - // Enriquecer com informações - let categoria = null; - if (documento.categoriaId) { - categoria = await ctx.db.get(documento.categoriaId); - } - - const criadoPor = await ctx.db.get(documento.criadoPor); - const atualizadoPor = documento.atualizadoPor - ? await ctx.db.get(documento.atualizadoPor) - : null; - - return { - ...documento, - categoria, - criadoPorUsuario: criadoPor - ? { - _id: criadoPor._id, - nome: criadoPor.nome, - email: criadoPor.email - } - : null, - atualizadoPorUsuario: atualizadoPor - ? { - _id: atualizadoPor._id, - nome: atualizadoPor.nome, - email: atualizadoPor.email - } - : null - }; - } -}); - -/** - * Buscar documentos (full-text search) - */ -export const buscarDocumentos = query({ - args: { - query: v.string(), - categoriaId: v.optional(v.id('documentacaoCategorias')), - tipo: v.optional( - v.union( - v.literal('query'), - v.literal('mutation'), - v.literal('action'), - v.literal('component'), - v.literal('route'), - v.literal('modulo'), - v.literal('manual'), - v.literal('outro') - ) - ), - tags: v.optional(v.array(v.string())), - limite: v.optional(v.number()) - }, - handler: async (ctx, args) => { - await verificarPermissaoTI(ctx); - - const queryNormalizada = normalizarTextoParaBusca(args.query); - const limite = args.limite || 50; - - // Buscar todos os documentos ativos - let documentos = await ctx.db - .query('documentacao') - .withIndex('by_ativo', (q) => q.eq('ativo', true)) - .collect(); - - // Filtrar por busca - documentos = documentos.filter((doc) => { - const tituloBusca = normalizarTextoParaBusca(doc.titulo); - return ( - doc.conteudoBusca.includes(queryNormalizada) || - tituloBusca.includes(queryNormalizada) || - doc.tags.some((tag) => normalizarTextoParaBusca(tag).includes(queryNormalizada)) - ); - }); - - // Aplicar filtros adicionais - if (args.categoriaId) { - documentos = documentos.filter((doc) => doc.categoriaId === args.categoriaId); - } - - if (args.tipo) { - documentos = documentos.filter((doc) => doc.tipo === args.tipo); - } - - if (args.tags && args.tags.length > 0) { - documentos = documentos.filter((doc) => - args.tags!.some((tag) => doc.tags.includes(tag)) - ); - } - - // Ordenar por relevância (simples: documentos com mais matches primeiro) - documentos.sort((a, b) => { - const aMatches = - (normalizarTextoParaBusca(a.titulo).includes(queryNormalizada) ? 2 : 0) + - (a.conteudoBusca.includes(queryNormalizada) ? 1 : 0); - const bMatches = - (normalizarTextoParaBusca(b.titulo).includes(queryNormalizada) ? 2 : 0) + - (b.conteudoBusca.includes(queryNormalizada) ? 1 : 0); - return bMatches - aMatches; - }); - - // Limitar resultados - documentos = documentos.slice(0, limite); - - // Enriquecer com categoria - const documentosEnriquecidos = await Promise.all( - documentos.map(async (doc) => { - let categoria = null; - if (doc.categoriaId) { - categoria = await ctx.db.get(doc.categoriaId); - } - return { - ...doc, - categoria - }; - }) - ); - - return documentosEnriquecidos; - } -}); - -/** - * Listar categorias - */ -export const listarCategorias = query({ - args: { - parentId: v.optional(v.id('documentacaoCategorias')), - ativo: v.optional(v.boolean()) - }, - handler: async (ctx, args) => { - await verificarPermissaoTI(ctx); - - let categorias: Doc<'documentacaoCategorias'>[] = []; - - if (args.parentId !== undefined) { - categorias = await ctx.db - .query('documentacaoCategorias') - .withIndex('by_parent', (q) => q.eq('parentId', args.parentId)) - .collect(); - } else if (args.ativo !== undefined) { - categorias = await ctx.db - .query('documentacaoCategorias') - .withIndex('by_ativo', (q) => q.eq('ativo', args.ativo!)) - .collect(); - } else { - categorias = await ctx.db.query('documentacaoCategorias').collect(); - } - - // Ordenar por ordem - categorias.sort((a, b) => a.ordem - b.ordem); - - return categorias; - } -}); - -/** - * Obter múltiplos documentos por IDs - */ -export const obterDocumentosPorIds = query({ - args: { - documentosIds: v.array(v.id('documentacao')) - }, - handler: async (ctx, args) => { - await verificarPermissaoTI(ctx); - - const documentos = await Promise.all( - args.documentosIds.map(async (id) => { - const doc = await ctx.db.get(id); - if (!doc) return null; - - let categoria = null; - if (doc.categoriaId) { - categoria = await ctx.db.get(doc.categoriaId); - } - - return { - ...doc, - categoria - }; - }) - ); - - return documentos.filter((doc): doc is NonNullable => doc !== null); - } -}); - -/** - * Listar tags - */ -export const listarTags = query({ - args: { - ativo: v.optional(v.boolean()), - limite: v.optional(v.number()) - }, - handler: async (ctx, args) => { - await verificarPermissaoTI(ctx); - - let tags: Doc<'documentacaoTags'>[] = []; - - if (args.ativo !== undefined) { - tags = await ctx.db - .query('documentacaoTags') - .withIndex('by_ativo', (q) => q.eq('ativo', args.ativo!)) - .collect(); - } else { - tags = await ctx.db.query('documentacaoTags').collect(); - } - - // Ordenar por uso (mais usadas primeiro) - tags.sort((a, b) => b.usadoEm - a.usadoEm); - - // Limitar se necessário - if (args.limite) { - tags = tags.slice(0, args.limite); - } - - return tags; - } -}); - -// ========== MUTATIONS ========== - -/** - * Criar novo documento - */ -export const criarDocumento = mutation({ - args: { - titulo: v.string(), - conteudo: v.string(), - conteudoHtml: v.optional(v.string()), - categoriaId: v.optional(v.id('documentacaoCategorias')), - tags: v.array(v.string()), - tipo: v.union( - v.literal('query'), - v.literal('mutation'), - v.literal('action'), - v.literal('component'), - v.literal('route'), - v.literal('modulo'), - v.literal('manual'), - v.literal('outro') - ), - versao: v.string(), - arquivoOrigem: v.optional(v.string()), - funcaoOrigem: v.optional(v.string()), - hashOrigem: v.optional(v.string()), - metadados: v.optional( - v.object({ - parametros: v.optional(v.array(v.string())), - retorno: v.optional(v.string()), - dependencias: v.optional(v.array(v.string())), - exemplos: v.optional(v.array(v.string())), - algoritmo: v.optional(v.string()) - }) - ), - geradoAutomaticamente: v.optional(v.boolean()) - }, - handler: async (ctx, args) => { - const usuarioAtual = await verificarPermissaoTI(ctx); - - const agora = Date.now(); - const conteudoBusca = normalizarTextoParaBusca(args.titulo + ' ' + args.conteudo); - - const documentoId = await ctx.db.insert('documentacao', { - titulo: args.titulo, - conteudo: args.conteudo, - conteudoHtml: args.conteudoHtml, - conteudoBusca, - categoriaId: args.categoriaId, - tags: args.tags, - tipo: args.tipo, - versao: args.versao, - arquivoOrigem: args.arquivoOrigem, - funcaoOrigem: args.funcaoOrigem, - hashOrigem: args.hashOrigem, - metadados: args.metadados, - ativo: true, - criadoPor: usuarioAtual._id, - criadoEm: agora, - atualizadoEm: agora, - visualizacoes: 0, - geradoAutomaticamente: args.geradoAutomaticamente ?? false - }); - - // Atualizar contadores de uso das tags - for (const tagNome of args.tags) { - const tag = await ctx.db - .query('documentacaoTags') - .withIndex('by_nome', (q) => q.eq('nome', tagNome)) - .first(); - - if (tag) { - await ctx.db.patch(tag._id, { - usadoEm: tag.usadoEm + 1 - }); - } - } - - return documentoId; - } -}); - -/** - * Atualizar documento - */ -export const atualizarDocumento = mutation({ - args: { - documentoId: v.id('documentacao'), - titulo: v.optional(v.string()), - conteudo: v.optional(v.string()), - conteudoHtml: v.optional(v.string()), - categoriaId: v.optional(v.id('documentacaoCategorias')), - tags: v.optional(v.array(v.string())), - tipo: v.optional( - v.union( - v.literal('query'), - v.literal('mutation'), - v.literal('action'), - v.literal('component'), - v.literal('route'), - v.literal('modulo'), - v.literal('manual'), - v.literal('outro') - ) - ), - versao: v.optional(v.string()), - metadados: v.optional( - v.object({ - parametros: v.optional(v.array(v.string())), - retorno: v.optional(v.string()), - dependencias: v.optional(v.array(v.string())), - exemplos: v.optional(v.array(v.string())), - algoritmo: v.optional(v.string()) - }) - ), - ativo: v.optional(v.boolean()) - }, - handler: async (ctx, args) => { - const usuarioAtual = await verificarPermissaoTI(ctx); - - const documento = await ctx.db.get(args.documentoId); - if (!documento) { - throw new Error('Documento não encontrado'); - } - - const atualizacoes: Partial> = { - atualizadoPor: usuarioAtual._id, - atualizadoEm: Date.now() - }; - - if (args.titulo !== undefined) atualizacoes.titulo = args.titulo; - if (args.conteudo !== undefined) atualizacoes.conteudo = args.conteudo; - if (args.conteudoHtml !== undefined) atualizacoes.conteudoHtml = args.conteudoHtml; - if (args.categoriaId !== undefined) atualizacoes.categoriaId = args.categoriaId; - if (args.tags !== undefined) atualizacoes.tags = args.tags; - if (args.tipo !== undefined) atualizacoes.tipo = args.tipo; - if (args.versao !== undefined) atualizacoes.versao = args.versao; - if (args.metadados !== undefined) atualizacoes.metadados = args.metadados; - if (args.ativo !== undefined) atualizacoes.ativo = args.ativo; - - // Atualizar conteúdo de busca se título ou conteúdo mudaram - if (args.titulo !== undefined || args.conteudo !== undefined) { - const novoTitulo = args.titulo ?? documento.titulo; - const novoConteudo = args.conteudo ?? documento.conteudo; - atualizacoes.conteudoBusca = normalizarTextoParaBusca(novoTitulo + ' ' + novoConteudo); - } - - // Atualizar contadores de tags se tags mudaram - if (args.tags !== undefined) { - // Decrementar tags antigas - for (const tagNome of documento.tags) { - if (!args.tags.includes(tagNome)) { - const tag = await ctx.db - .query('documentacaoTags') - .withIndex('by_nome', (q) => q.eq('nome', tagNome)) - .first(); - - if (tag && tag.usadoEm > 0) { - await ctx.db.patch(tag._id, { - usadoEm: tag.usadoEm - 1 - }); - } - } - } - - // Incrementar tags novas - for (const tagNome of args.tags) { - if (!documento.tags.includes(tagNome)) { - const tag = await ctx.db - .query('documentacaoTags') - .withIndex('by_nome', (q) => q.eq('nome', tagNome)) - .first(); - - if (tag) { - await ctx.db.patch(tag._id, { - usadoEm: tag.usadoEm + 1 - }); - } - } - } - } - - await ctx.db.patch(args.documentoId, atualizacoes); - - return { sucesso: true }; - } -}); - -/** - * Deletar documento (soft delete) - */ -export const deletarDocumento = mutation({ - args: { - documentoId: v.id('documentacao') - }, - handler: async (ctx, args) => { - const usuarioAtual = await verificarPermissaoTI(ctx); - - const documento = await ctx.db.get(args.documentoId); - if (!documento) { - throw new Error('Documento não encontrado'); - } - - // Soft delete: apenas marcar como inativo - await ctx.db.patch(args.documentoId, { - ativo: false, - atualizadoPor: usuarioAtual._id, - atualizadoEm: Date.now() - }); - - // Decrementar contadores de tags - for (const tagNome of documento.tags) { - const tag = await ctx.db - .query('documentacaoTags') - .withIndex('by_nome', (q) => q.eq('nome', tagNome)) - .first(); - - if (tag && tag.usadoEm > 0) { - await ctx.db.patch(tag._id, { - usadoEm: tag.usadoEm - 1 - }); - } - } - - return { sucesso: true }; - } -}); - -/** - * Criar categoria - */ -export const criarCategoria = mutation({ - args: { - nome: v.string(), - descricao: v.optional(v.string()), - icone: v.optional(v.string()), - cor: v.optional( - v.union( - v.literal('primary'), - v.literal('secondary'), - v.literal('accent'), - v.literal('success'), - v.literal('warning'), - v.literal('error'), - v.literal('info') - ) - ), - parentId: v.optional(v.id('documentacaoCategorias')), - ordem: v.number() - }, - handler: async (ctx, args) => { - await verificarPermissaoTI(ctx); - - const usuarioAtual = await getCurrentUserFunction(ctx); - if (!usuarioAtual) { - throw new Error('Não autenticado'); - } - - const agora = Date.now(); - const categoriaId = await ctx.db.insert('documentacaoCategorias', { - nome: args.nome, - descricao: args.descricao, - icone: args.icone, - cor: args.cor, - parentId: args.parentId, - ordem: args.ordem, - ativo: true, - criadoPor: usuarioAtual._id, - criadoEm: agora, - atualizadoEm: agora - }); - - return categoriaId; - } -}); - -/** - * Criar tag - */ -export const criarTag = mutation({ - args: { - nome: v.string(), - descricao: v.optional(v.string()), - cor: v.optional( - v.union( - v.literal('primary'), - v.literal('secondary'), - v.literal('accent'), - v.literal('success'), - v.literal('warning'), - v.literal('error'), - v.literal('info') - ) - ) - }, - handler: async (ctx, args) => { - await verificarPermissaoTI(ctx); - - // Verificar se tag já existe - const tagExistente = await ctx.db - .query('documentacaoTags') - .withIndex('by_nome', (q) => q.eq('nome', args.nome)) - .first(); - - if (tagExistente) { - throw new Error('Tag já existe'); - } - - const usuarioAtual = await getCurrentUserFunction(ctx); - if (!usuarioAtual) { - throw new Error('Não autenticado'); - } - - const agora = Date.now(); - const tagId = await ctx.db.insert('documentacaoTags', { - nome: args.nome, - descricao: args.descricao, - cor: args.cor, - usadoEm: 0, - ativo: true, - criadoPor: usuarioAtual._id, - criadoEm: agora - }); - - return tagId; - } -}); - -/** - * Obter configuração de agendamento de varredura - */ -export const obterConfigVarredura = query({ - args: {}, - handler: async (ctx) => { - await verificarPermissaoTI(ctx); - - const config = await ctx.db - .query('documentacaoConfig') - .withIndex('by_ativo', (q) => q.eq('ativo', true)) - .first(); - - return config; - } -}); - -/** - * Salvar configuração de agendamento de varredura - */ -export const salvarConfigVarredura = mutation({ - args: { - ativo: v.boolean(), - diasSemana: v.array( - v.union( - v.literal('domingo'), - v.literal('segunda'), - v.literal('terca'), - v.literal('quarta'), - v.literal('quinta'), - v.literal('sexta'), - v.literal('sabado') - ) - ), - horario: v.string(), // Formato "HH:MM" - fusoHorario: v.optional(v.string()) - }, - handler: async (ctx, args) => { - const usuarioAtual = await verificarPermissaoTI(ctx); - - // Buscar configuração existente - const configExistente = await ctx.db - .query('documentacaoConfig') - .withIndex('by_ativo', (q) => q.eq('ativo', true)) - .first(); - - const agora = Date.now(); - - if (configExistente) { - // Atualizar configuração existente - await ctx.db.patch(configExistente._id, { - ativo: args.ativo, - diasSemana: args.diasSemana, - horario: args.horario, - fusoHorario: args.fusoHorario, - atualizadoPor: usuarioAtual._id, - atualizadoEm: agora - }); - - return configExistente._id; - } else { - // Criar nova configuração - const configId = await ctx.db.insert('documentacaoConfig', { - ativo: args.ativo, - diasSemana: args.diasSemana, - horario: args.horario, - fusoHorario: args.fusoHorario || 'America/Recife', - configuradoPor: usuarioAtual._id, - configuradoEm: agora, - atualizadoEm: agora - }); - - return configId; - } - } -}); - -/** - * Notificar TI_master sobre novos documentos criados - */ -export const notificarNovosDocumentos = mutation({ - args: { - documentosIds: v.array(v.id('documentacao')), - quantidade: v.number() - }, - handler: async (ctx, args) => { - // Buscar usuários TI_master (nível 0) - const roleTIMaster = await ctx.db - .query('roles') - .filter((q) => q.eq(q.field('nivel'), 0)) - .first(); - - if (!roleTIMaster) { - return; // Não há TI_master configurado - } - - const usuariosTIMaster = await ctx.db - .query('usuarios') - .filter((q) => q.eq(q.field('roleId'), roleTIMaster._id)) - .collect(); - - // Criar notificações no chat - for (const usuario of usuariosTIMaster) { - await ctx.db.insert('notificacoes', { - usuarioId: usuario._id, - tipo: 'nova_mensagem', - titulo: '📚 Novos Documentos Criados', - descricao: `${args.quantidade} novo(s) documento(s) de documentação foram criados automaticamente pela varredura do sistema.`, - lida: false, - criadaEm: Date.now() - }); - } - - // Enviar emails para TI_master (usando scheduler para não bloquear) - for (const usuario of usuariosTIMaster) { - if (usuario.email) { - await ctx.scheduler.runAfter(0, api.email.enviarEmailComTemplate, { - destinatario: usuario.email, - destinatarioId: usuario._id, - templateCodigo: 'documentacao_novos_documentos', - variaveis: { - destinatarioNome: usuario.nome, - quantidade: args.quantidade.toString(), - urlSistema: process.env.FRONTEND_URL || 'http://localhost:5173' - }, - enviadoPor: usuario._id - }); - } - } - - return { sucesso: true }; - } -}); - diff --git a/packages/backend/convex/documentacaoVarredura.ts b/packages/backend/convex/documentacaoVarredura.ts deleted file mode 100644 index 599ac5b..0000000 --- a/packages/backend/convex/documentacaoVarredura.ts +++ /dev/null @@ -1,1152 +0,0 @@ -import { v } from 'convex/values'; -import { internalMutation, mutation, query } from './_generated/server'; -import { Doc, Id } from './_generated/dataModel'; -import { internal, api } from './_generated/api'; -import { getCurrentUserFunction } from './auth'; - -// ========== HELPERS ========== - -/** - * Normaliza texto para busca (remove acentos, converte para lowercase) - */ -function normalizarTextoParaBusca(texto: string): string { - return texto - .toLowerCase() - .normalize('NFD') - .replace(/[\u0300-\u036f]/g, '') // Remove diacríticos - .trim(); -} - -/** - * Gera hash simples de uma string (usando soma de caracteres como identificador único) - * Nota: Para produção, considere usar uma Action com Node.js para hash MD5/SHA256 real - */ -function gerarHash(texto: string): string { - // Hash simples baseado em soma de códigos de caracteres - let hash = 0; - for (let i = 0; i < texto.length; i++) { - const char = texto.charCodeAt(i); - hash = (hash << 5) - hash + char; - hash = hash & hash; // Convert to 32bit integer - } - return Math.abs(hash).toString(16); -} - -/** - * Extrai informações de uma função Convex (query, mutation, action) - */ -function analisarFuncaoConvex( - conteudo: string, - nomeArquivo: string -): Array<{ - nome: string; - tipo: 'query' | 'mutation' | 'action'; - descricao: string; - parametros: string[]; - retorno: string; - hash: string; - corpoFuncao: string; -}> { - const funcoes: Array<{ - nome: string; - tipo: 'query' | 'mutation' | 'action'; - descricao: string; - parametros: string[]; - retorno: string; - hash: string; - corpoFuncao: string; - }> = []; - - // Padrões para detectar funções - const padraoQuery = /export\s+const\s+(\w+)\s*=\s*query\s*\(/g; - const padraoMutation = /export\s+const\s+(\w+)\s*=\s*mutation\s*\(/g; - const padraoAction = /export\s+const\s+(\w+)\s*=\s*action\s*\(/g; - - // Extrair JSDoc comments com melhor parsing - const padraoJSDoc = /\/\*\*([\s\S]*?)\*\//g; - const jsdocs: Map; retorno?: string }> = new Map(); - - let match; - while ((match = padraoJSDoc.exec(conteudo)) !== null) { - const jsdocRaw = match[1].trim(); - // Tentar encontrar a função seguinte - const proximaFuncao = conteudo.indexOf('export', match.index + match[0].length); - if (proximaFuncao !== -1) { - const nomeFuncao = conteudo - .substring(proximaFuncao, proximaFuncao + 200) - .match(/export\s+const\s+(\w+)/); - if (nomeFuncao) { - // Parse JSDoc para extrair descrição, @param, @returns - const linhas = jsdocRaw.split('\n').map((l) => l.trim().replace(/^\*\s*/, '')); - let descricao = ''; - const parametros = new Map(); - let retorno: string | undefined; - - for (let i = 0; i < linhas.length; i++) { - const linha = linhas[i]; - if (linha.startsWith('@param')) { - const paramMatch = linha.match(/@param\s+(\w+)\s+(.+)/); - if (paramMatch) { - parametros.set(paramMatch[1], paramMatch[2]); - } - } else if (linha.startsWith('@returns') || linha.startsWith('@return')) { - retorno = linha.replace(/@returns?\s+/, ''); - } else if (!linha.startsWith('@') && linha.length > 0) { - if (descricao) descricao += ' '; - descricao += linha; - } - } - - jsdocs.set(nomeFuncao[1], { - descricao: descricao || jsdocRaw, - parametros, - retorno - }); - } - } - } - - // Buscar queries - while ((match = padraoQuery.exec(conteudo)) !== null) { - const nome = match[1]; - const inicio = match.index; - const fim = conteudo.indexOf('});', inicio) + 3; - const corpoFuncao = conteudo.substring(inicio, fim); - const hash = gerarHash(corpoFuncao); - - // Extrair args com mais detalhes - const argsMatch = corpoFuncao.match(/args:\s*\{([\s\S]*?)\}/); - const parametros: string[] = []; - const jsdocInfo = jsdocs.get(nome); - if (argsMatch) { - const argsContent = argsMatch[1]; - const paramMatches = argsContent.matchAll(/(\w+):\s*v\.(\w+)/g); - for (const paramMatch of paramMatches) { - const paramNome = paramMatch[1]; - const paramTipo = paramMatch[2]; - const paramDesc = jsdocInfo?.parametros.get(paramNome); - if (paramDesc) { - parametros.push(`${paramNome}: ${paramTipo} - ${paramDesc}`); - } else { - parametros.push(`${paramNome}: ${paramTipo}`); - } - } - } - - // Extrair returns - const returnsMatch = corpoFuncao.match(/returns:\s*v\.(\w+)/); - let retorno = returnsMatch ? returnsMatch[1] : 'void'; - if (jsdocInfo?.retorno) { - retorno = `${retorno} - ${jsdocInfo.retorno}`; - } - - let descricao = jsdocInfo?.descricao || `Query ${nome} do arquivo ${nomeArquivo}. Esta função realiza uma consulta no banco de dados.`; - - // Adicionar análise da lógica se não houver JSDoc detalhado - if (!jsdocInfo?.descricao || jsdocInfo.descricao.length < 50) { - const logica = analisarLogicaFuncao(corpoFuncao, 'query'); - if (logica) { - descricao += ' ' + logica; - } - } - - funcoes.push({ - nome, - tipo: 'query', - descricao, - parametros, - retorno, - hash, - corpoFuncao - }); - } - - // Buscar mutations - padraoMutation.lastIndex = 0; - while ((match = padraoMutation.exec(conteudo)) !== null) { - const nome = match[1]; - const inicio = match.index; - const fim = conteudo.indexOf('});', inicio) + 3; - const corpoFuncao = conteudo.substring(inicio, fim); - const hash = gerarHash(corpoFuncao); - - const argsMatch = corpoFuncao.match(/args:\s*\{([\s\S]*?)\}/); - const parametros: string[] = []; - const jsdocInfo = jsdocs.get(nome); - if (argsMatch) { - const argsContent = argsMatch[1]; - const paramMatches = argsContent.matchAll(/(\w+):\s*v\.(\w+)/g); - for (const paramMatch of paramMatches) { - const paramNome = paramMatch[1]; - const paramTipo = paramMatch[2]; - const paramDesc = jsdocInfo?.parametros.get(paramNome); - if (paramDesc) { - parametros.push(`${paramNome}: ${paramTipo} - ${paramDesc}`); - } else { - parametros.push(`${paramNome}: ${paramTipo}`); - } - } - } - - const returnsMatch = corpoFuncao.match(/returns:\s*v\.(\w+)/); - let retorno = returnsMatch ? returnsMatch[1] : 'void'; - if (jsdocInfo?.retorno) { - retorno = `${retorno} - ${jsdocInfo.retorno}`; - } - - let descricao = jsdocInfo?.descricao || `Mutation ${nome} do arquivo ${nomeArquivo}. Esta função modifica dados no banco de dados.`; - - // Adicionar análise da lógica se não houver JSDoc detalhado - if (!jsdocInfo?.descricao || jsdocInfo.descricao.length < 50) { - const logica = analisarLogicaFuncao(corpoFuncao, 'mutation'); - if (logica) { - descricao += ' ' + logica; - } - } - - funcoes.push({ - nome, - tipo: 'mutation', - descricao, - parametros, - retorno, - hash, - corpoFuncao - }); - } - - // Buscar actions - padraoAction.lastIndex = 0; - while ((match = padraoAction.exec(conteudo)) !== null) { - const nome = match[1]; - const inicio = match.index; - const fim = conteudo.indexOf('});', inicio) + 3; - const corpoFuncao = conteudo.substring(inicio, fim); - const hash = gerarHash(corpoFuncao); - - const argsMatch = corpoFuncao.match(/args:\s*\{([\s\S]*?)\}/); - const parametros: string[] = []; - const jsdocInfo = jsdocs.get(nome); - if (argsMatch) { - const argsContent = argsMatch[1]; - const paramMatches = argsContent.matchAll(/(\w+):\s*v\.(\w+)/g); - for (const paramMatch of paramMatches) { - const paramNome = paramMatch[1]; - const paramTipo = paramMatch[2]; - const paramDesc = jsdocInfo?.parametros.get(paramNome); - if (paramDesc) { - parametros.push(`${paramNome}: ${paramTipo} - ${paramDesc}`); - } else { - parametros.push(`${paramNome}: ${paramTipo}`); - } - } - } - - const returnsMatch = corpoFuncao.match(/returns:\s*v\.(\w+)/); - let retorno = returnsMatch ? returnsMatch[1] : 'void'; - if (jsdocInfo?.retorno) { - retorno = `${retorno} - ${jsdocInfo.retorno}`; - } - - let descricao = jsdocInfo?.descricao || `Action ${nome} do arquivo ${nomeArquivo}. Esta função executa ações no servidor, podendo acessar APIs externas e realizar operações assíncronas.`; - - // Adicionar análise da lógica se não houver JSDoc detalhado - if (!jsdocInfo?.descricao || jsdocInfo.descricao.length < 50) { - const logica = analisarLogicaFuncao(corpoFuncao, 'action'); - if (logica) { - descricao += ' ' + logica; - } - } - - funcoes.push({ - nome, - tipo: 'action', - descricao, - parametros, - retorno, - hash, - corpoFuncao - }); - } - - return funcoes; -} - -/** - * Analisa a lógica de uma função e gera uma explicação detalhada - */ -function analisarLogicaFuncao(corpoFuncao: string, tipo: 'query' | 'mutation' | 'action'): string { - const explicacoes: string[] = []; - - // Detectar operações de banco de dados - const operacoesDB = { - query: corpoFuncao.match(/ctx\.db\.query\(['"]([^'"]+)['"]\)/g) || [], - insert: corpoFuncao.match(/ctx\.db\.insert\(['"]([^'"]+)['"]/g) || [], - update: corpoFuncao.match(/ctx\.db\.patch\(/g) || [], - delete: corpoFuncao.match(/ctx\.db\.delete\(/g) || [], - replace: corpoFuncao.match(/ctx\.db\.replace\(/g) || [] - }; - - if (operacoesDB.query.length > 0) { - const tabelas = operacoesDB.query.map((q) => { - const match = q.match(/['"]([^'"]+)['"]/); - return match ? match[1] : ''; - }); - explicacoes.push( - `Esta função realiza consultas na(s) tabela(s): **${tabelas.filter((t) => t).join(', ')}**.` - ); - } - - if (operacoesDB.insert.length > 0) { - const tabelas = operacoesDB.insert.map((i) => { - const match = i.match(/['"]([^'"]+)['"]/); - return match ? match[1] : ''; - }); - explicacoes.push( - `A função **insere novos registros** na(s) tabela(s): **${tabelas.filter((t) => t).join(', ')}**.` - ); - } - - if (operacoesDB.update.length > 0) { - explicacoes.push(`A função **atualiza registros existentes** no banco de dados.`); - } - - if (operacoesDB.delete.length > 0) { - explicacoes.push(`A função **remove registros** do banco de dados.`); - } - - if (operacoesDB.replace.length > 0) { - explicacoes.push(`A função **substitui registros completos** no banco de dados.`); - } - - // Detectar uso de índices - if (corpoFuncao.includes('.withIndex(')) { - explicacoes.push(`A função utiliza **índices do banco de dados** para otimizar as consultas.`); - } - - // Detectar filtros - if (corpoFuncao.includes('.filter(')) { - explicacoes.push(`A função aplica **filtros** para restringir os resultados da consulta.`); - } - - // Detectar ordenação - if (corpoFuncao.includes('.order(')) { - explicacoes.push(`A função **ordena os resultados** de acordo com critérios específicos.`); - } - - // Detectar paginação - if (corpoFuncao.includes('.take(') || corpoFuncao.includes('.limit')) { - explicacoes.push(`A função implementa **paginação** para limitar a quantidade de resultados retornados.`); - } - - // Detectar validações - if (corpoFuncao.includes('if (') || corpoFuncao.includes('if(')) { - const condicoes = (corpoFuncao.match(/if\s*\([^)]+\)/g) || []).length; - if (condicoes > 0) { - explicacoes.push( - `A função contém **${condicoes} condição(ões) de validação** para garantir a integridade dos dados.` - ); - } - } - - // Detectar loops - if (corpoFuncao.includes('for (') || corpoFuncao.includes('forEach(') || corpoFuncao.includes('.map(')) { - explicacoes.push(`A função **processa múltiplos itens** iterando sobre coleções de dados.`); - } - - // Detectar chamadas a outras funções - const chamadasInternas = corpoFuncao.match(/ctx\.run(Query|Mutation|Action)\(/g) || []; - if (chamadasInternas.length > 0) { - explicacoes.push( - `A função **chama outras funções** do sistema (${chamadasInternas.length} chamada(s)) para realizar operações auxiliares.` - ); - } - - // Detectar tratamento de erros - if (corpoFuncao.includes('try {') || corpoFuncao.includes('catch')) { - explicacoes.push(`A função implementa **tratamento de erros** para garantir robustez.`); - } - - // Detectar operações assíncronas (para actions) - if (tipo === 'action') { - if (corpoFuncao.includes('fetch(') || corpoFuncao.includes('await fetch')) { - explicacoes.push(`A função realiza **chamadas HTTP** para APIs externas.`); - } - if (corpoFuncao.includes('fs.') || corpoFuncao.includes('readFile') || corpoFuncao.includes('writeFile')) { - explicacoes.push(`A função realiza **operações de arquivo** no sistema.`); - } - } - - // Detectar agregações - if (corpoFuncao.includes('.collect()')) { - explicacoes.push(`A função **coleta todos os resultados** da consulta em memória.`); - } - - // Detectar contagem - if (corpoFuncao.includes('.count()') || corpoFuncao.includes('length')) { - explicacoes.push(`A função **conta elementos** ou calcula quantidades.`); - } - - // Detectar transformações de dados - if (corpoFuncao.includes('.map(') || corpoFuncao.includes('.reduce(') || corpoFuncao.includes('.filter(')) { - explicacoes.push(`A função **transforma os dados** antes de retorná-los.`); - } - - // Detectar autenticação/autorização - if (corpoFuncao.includes('getCurrentUser') || corpoFuncao.includes('auth') || corpoFuncao.includes('usuario')) { - explicacoes.push(`A função verifica **autenticação e autorização** do usuário.`); - } - - if (explicacoes.length === 0) { - return 'A função executa operações básicas conforme sua implementação.'; - } - - return explicacoes.join(' '); -} - -/** - * Gera conteúdo Markdown para uma função - */ -function gerarMarkdownFuncao( - funcao: { - nome: string; - tipo: 'query' | 'mutation' | 'action'; - descricao: string; - parametros: string[]; - retorno: string; - hash: string; - corpoFuncao: string; - }, - arquivoOrigem: string -): string { - const tipoCapitalizado = funcao.tipo.charAt(0).toUpperCase() + funcao.tipo.slice(1); - - // Descrição do tipo - let tipoDescricao = ''; - switch (funcao.tipo) { - case 'query': - tipoDescricao = 'Uma **Query** é uma função de leitura que busca dados do banco de dados. Queries são otimizadas para leitura e podem ser executadas múltiplas vezes sem efeitos colaterais.'; - break; - case 'mutation': - tipoDescricao = 'Uma **Mutation** é uma função de escrita que modifica dados no banco de dados. Mutations garantem consistência e podem ser executadas de forma transacional.'; - break; - case 'action': - tipoDescricao = 'Uma **Action** é uma função que pode executar operações no servidor, incluindo chamadas a APIs externas, operações de arquivo e outras tarefas assíncronas.'; - break; - } - - let markdown = `# ${funcao.nome}\n\n`; - markdown += `## Visão Geral\n\n`; - markdown += `${funcao.descricao}\n\n`; - - markdown += `## Tipo de Função\n\n`; - markdown += `**${tipoCapitalizado}**\n\n`; - markdown += `${tipoDescricao}\n\n`; - - // Adicionar seção de lógica implementada - const logicaDetalhada = analisarLogicaFuncao(funcao.corpoFuncao, funcao.tipo); - if (logicaDetalhada && logicaDetalhada !== 'A função executa operações básicas conforme sua implementação.') { - markdown += `## Lógica Implementada\n\n`; - markdown += `${logicaDetalhada}\n\n`; - } - - if (funcao.parametros.length > 0) { - markdown += `## Parâmetros de Entrada\n\n`; - markdown += `Esta função aceita os seguintes parâmetros:\n\n`; - for (const param of funcao.parametros) { - // Separar nome, tipo e descrição se houver - const paramMatch = param.match(/^(.+?):\s*(.+?)(?:\s*-\s*(.+))?$/); - if (paramMatch) { - const [, nome, tipo, desc] = paramMatch; - markdown += `### \`${nome}\`\n\n`; - markdown += `- **Tipo:** \`${tipo}\`\n`; - if (desc) { - markdown += `- **Descrição:** ${desc}\n`; - } - markdown += `\n`; - } else { - markdown += `- \`${param}\`\n`; - } - } - markdown += `\n`; - } else { - markdown += `## Parâmetros de Entrada\n\n`; - markdown += `Esta função não requer parâmetros.\n\n`; - } - - markdown += `## Valor de Retorno\n\n`; - const retornoMatch = funcao.retorno.match(/^(.+?)(?:\s*-\s*(.+))?$/); - if (retornoMatch) { - const [, tipo, desc] = retornoMatch; - markdown += `- **Tipo:** \`${tipo}\`\n`; - if (desc) { - markdown += `- **Descrição:** ${desc}\n`; - } - } else { - markdown += `\`${funcao.retorno}\`\n`; - } - markdown += `\n`; - - markdown += `## Arquivo Origem\n\n`; - markdown += `\`${arquivoOrigem}\`\n\n`; - - markdown += `## Hash\n\n`; - markdown += `\`${funcao.hash}\`\n\n`; - - return markdown; -} - -// ========== INTERNAL MUTATIONS ========== - -/** - * Executar varredura automática (chamado por cron ou manualmente) - */ -export const executarVarredura = internalMutation({ - args: { - executadoPor: v.id('usuarios'), - tipo: v.union(v.literal('automatica'), v.literal('manual')) - }, - handler: async (ctx, args) => { - const inicio = Date.now(); - const varreduraId = await ctx.db.insert('documentacaoVarredura', { - tipo: args.tipo, - status: 'em_andamento', - documentosEncontrados: 0, - documentosNovos: 0, - documentosAtualizados: 0, - arquivosAnalisados: 0, - executadoPor: args.executadoPor, - iniciadoEm: inicio - }); - - const erros: string[] = []; - let documentosNovos = 0; - let documentosAtualizados = 0; - let arquivosAnalisados = 0; - - try { - // Lista de arquivos conhecidos para análise - // Nota: Em produção, isso poderia ser expandido para ler do sistema de arquivos via Action - const arquivosConvex = [ - 'usuarios.ts', - 'funcionarios.ts', - 'chat.ts', - 'email.ts', - 'pontos.ts', - 'ferias.ts', - 'ausencias.ts', - 'chamados.ts', - 'pedidos.ts', - 'produtos.ts', - 'flows.ts', - 'contratos.ts', - 'empresas.ts', - 'setores.ts', - 'times.ts', - 'cursos.ts', - 'lgpd.ts', - 'security.ts', - 'monitoramento.ts', - 'config.ts', - 'configuracaoEmail.ts', - 'configuracaoPonto.ts', - 'configuracaoRelogio.ts', - 'configuracaoJitsi.ts' - ]; - - // Para cada arquivo conhecido, buscar funções já documentadas - // e comparar com o que deveria existir - // Como não podemos ler arquivos diretamente, vamos usar uma abordagem diferente: - // Vamos criar documentos baseados em padrões conhecidos do sistema - - // Buscar todas as funções documentadas existentes - const documentosExistentes = await ctx.db - .query('documentacao') - .filter((q) => q.eq(q.field('geradoAutomaticamente'), true)) - .collect(); - - const hashDocumentosExistentes = new Map>(); - for (const doc of documentosExistentes) { - if (doc.hashOrigem) { - hashDocumentosExistentes.set(doc.hashOrigem, doc._id); - } - } - - // Chamar Action para ler e analisar arquivos do sistema de arquivos - // Usar scheduler para não bloquear, mas a Action atualizará a varredura quando terminar - console.log(`🚀 [executarVarredura] Iniciando análise de ${arquivosConvex.length} arquivos`); - - ctx.scheduler.runAfter( - 0, - api.actions.documentacaoVarredura.analisarArquivos, - { - varreduraId, - arquivos: arquivosConvex, - executadoPor: args.executadoPor - } - ).catch((error) => { - console.error('❌ [executarVarredura] Erro ao agendar Action:', error); - erros.push(`Erro ao agendar análise: ${error instanceof Error ? error.message : 'Erro desconhecido'}`); - }); - - // Atualizar status inicial (a Action atualizará quando terminar) - arquivosAnalisados = arquivosConvex.length; - const duracao = Date.now() - inicio; - await ctx.db.patch(varreduraId, { - status: 'em_andamento', // Manter como em_andamento até a Action terminar - documentosEncontrados: documentosExistentes.length, - documentosNovos: 0, - documentosAtualizados: 0, - arquivosAnalisados, - erros: erros.length > 0 ? erros : undefined, - duracaoMs: duracao - }); - - console.log(`✅ [executarVarredura] Varredura agendada, ID: ${varreduraId}`); - - // A notificação será feita pela Action quando terminar - - return varreduraId; - } catch (error) { - const duracao = Date.now() - inicio; - erros.push(error instanceof Error ? error.message : 'Erro desconhecido'); - - await ctx.db.patch(varreduraId, { - status: 'erro', - erros, - duracaoMs: duracao, - concluidoEm: Date.now() - }); - - throw error; - } - } -}); - -/** - * Atualizar varredura com resultados da Action - */ -export const atualizarVarreduraComResultados = internalMutation({ - args: { - varreduraId: v.id('documentacaoVarredura'), - documentosNovos: v.number(), - documentosAtualizados: v.number(), - arquivosAnalisados: v.number(), - erros: v.array(v.string()) - }, - handler: async (ctx, args) => { - const varredura = await ctx.db.get(args.varreduraId); - if (!varredura) return; - - const duracao = Date.now() - varredura.iniciadoEm; - - // Buscar documentos encontrados - const documentosEncontrados = await ctx.db - .query('documentacao') - .filter((q) => q.eq(q.field('geradoAutomaticamente'), true)) - .collect(); - - await ctx.db.patch(args.varreduraId, { - status: 'concluida', - documentosEncontrados: documentosEncontrados.length, - documentosNovos: args.documentosNovos, - documentosAtualizados: args.documentosAtualizados, - arquivosAnalisados: args.arquivosAnalisados, - erros: args.erros.length > 0 ? args.erros : undefined, - duracaoMs: duracao, - concluidoEm: Date.now() - }); - - // Notificar TI_master se houver novos documentos - if (args.documentosNovos > 0) { - // Buscar IDs dos novos documentos criados durante esta varredura - const novosDocumentos = await ctx.db - .query('documentacao') - .filter((q) => q.eq(q.field('geradoAutomaticamente'), true)) - .filter((q) => q.gte(q.field('criadoEm'), varredura.iniciadoEm)) - .collect(); - - const novosDocumentosIds = novosDocumentos.map((doc) => doc._id); - - if (novosDocumentosIds.length > 0) { - // Notificar via scheduler para não bloquear - await ctx.scheduler.runAfter(0, api.documentacao.notificarNovosDocumentos, { - documentosIds: novosDocumentosIds, - quantidade: args.documentosNovos - }); - } - } - } -}); - -/** - * Processar um arquivo e criar/atualizar documentos (chamado pela Action) - */ -export const processarArquivo = internalMutation({ - args: { - varreduraId: v.id('documentacaoVarredura'), - nomeArquivo: v.string(), - conteudo: v.string(), - executadoPor: v.id('usuarios') - }, - returns: v.object({ - documentosNovos: v.number(), - documentosAtualizados: v.number() - }), - handler: async (ctx, args) => { - let documentosNovos = 0; - let documentosAtualizados = 0; - - console.log(`📝 [processarArquivo] Processando arquivo: ${args.nomeArquivo}`); - - try { - // Analisar funções no arquivo - const funcoes = analisarFuncaoConvex(args.conteudo, args.nomeArquivo); - console.log(`🔍 [processarArquivo] Encontradas ${funcoes.length} funções em ${args.nomeArquivo}`); - - if (funcoes.length === 0) { - console.log(`⚠️ [processarArquivo] Nenhuma função encontrada em ${args.nomeArquivo}`); - } - - // Processar cada função encontrada - for (const funcao of funcoes) { - console.log(` 📌 Processando função: ${funcao.nome} (${funcao.tipo})`); - const markdown = gerarMarkdownFuncao(funcao, args.nomeArquivo); - const hashOrigem = `${args.nomeArquivo}:${funcao.hash}`; - - // Verificar se já existe documento com este hash - const documentoExistente = await ctx.db - .query('documentacao') - .withIndex('by_hash_origem', (q) => q.eq('hashOrigem', hashOrigem)) - .first(); - - const conteudoBusca = normalizarTextoParaBusca( - `${funcao.nome} ${funcao.descricao} ${markdown}` - ); - - if (documentoExistente) { - // Verificar se houve mudança no conteúdo (comparar hash) - const hashAtual = funcao.hash; - const hashAnterior = documentoExistente.hashOrigem?.split(':')[1] || ''; - - // Calcular nova versão se houver mudança - let novaVersao = documentoExistente.versao; - if (hashAtual !== hashAnterior) { - // Incrementar versão (ex: 1.0.0 -> 1.0.1, 1.0.1 -> 1.0.2, etc) - const partes = documentoExistente.versao.split('.'); - const patch = parseInt(partes[2] || '0', 10) + 1; - novaVersao = `${partes[0]}.${partes[1]}.${patch}`; - console.log( - ` 🔄 Função ${funcao.nome} alterada: versão ${documentoExistente.versao} -> ${novaVersao}` - ); - } - - // Atualizar documento existente - await ctx.db.patch(documentoExistente._id, { - titulo: `${funcao.nome} (${funcao.tipo})`, - conteudo: markdown, - conteudoBusca, - versao: novaVersao, - metadados: { - parametros: funcao.parametros, - retorno: funcao.retorno, - algoritmo: funcao.descricao - }, - atualizadoEm: Date.now() - }); - console.log(` ✅ Documento atualizado: ${funcao.nome} (versão ${novaVersao})`); - documentosAtualizados++; - } else { - // Criar novo documento - const agora = Date.now(); - - // Determinar categoria baseada no tipo - let categoriaId: Id<'documentacaoCategorias'> | undefined; - const categoriaBackend = await ctx.db - .query('documentacaoCategorias') - .filter((q) => q.eq(q.field('nome'), 'Backend')) - .first(); - - if (categoriaBackend) { - categoriaId = categoriaBackend._id; - } - - const documentoId = await ctx.db.insert('documentacao', { - titulo: `${funcao.nome} (${funcao.tipo})`, - conteudo: markdown, - conteudoBusca, - categoriaId, - tags: [funcao.tipo, 'automatico'], - tipo: funcao.tipo, - versao: '1.0.0', - arquivoOrigem: args.nomeArquivo, - funcaoOrigem: funcao.nome, - hashOrigem: hashOrigem, - metadados: { - parametros: funcao.parametros, - retorno: funcao.retorno, - algoritmo: funcao.descricao - }, - ativo: true, - criadoPor: args.executadoPor, - criadoEm: agora, - atualizadoEm: agora, - visualizacoes: 0, - geradoAutomaticamente: true - }); - console.log(` ✨ Novo documento criado: ${funcao.nome} (ID: ${documentoId})`); - documentosNovos++; - } - } - } catch (error) { - const erroMsg = error instanceof Error ? error.message : 'Erro desconhecido'; - console.error(`❌ [processarArquivo] Erro ao processar arquivo ${args.nomeArquivo}:`, erroMsg); - throw error; - } - - console.log( - `✅ [processarArquivo] Arquivo ${args.nomeArquivo} processado: ${documentosNovos} novos, ${documentosAtualizados} atualizados` - ); - - return { documentosNovos, documentosAtualizados }; - } -}); - -/** - * Criar documento a partir de função detectada - */ -export const criarDocumentoFuncao = internalMutation({ - args: { - titulo: v.string(), - conteudo: v.string(), - tipo: v.union( - v.literal('query'), - v.literal('mutation'), - v.literal('action'), - v.literal('component'), - v.literal('route'), - v.literal('modulo'), - v.literal('manual'), - v.literal('outro') - ), - arquivoOrigem: v.string(), - funcaoOrigem: v.string(), - hashOrigem: v.string(), - metadados: v.optional( - v.object({ - parametros: v.optional(v.array(v.string())), - retorno: v.optional(v.string()), - dependencias: v.optional(v.array(v.string())), - exemplos: v.optional(v.array(v.string())), - algoritmo: v.optional(v.string()) - }) - ), - criadoPor: v.id('usuarios') - }, - handler: async (ctx, args) => { - // Verificar se já existe documento com este hash - const documentoExistente = await ctx.db - .query('documentacao') - .withIndex('by_hash_origem', (q) => q.eq('hashOrigem', args.hashOrigem)) - .first(); - - if (documentoExistente) { - // Atualizar documento existente - const conteudoBusca = args.titulo.toLowerCase() + ' ' + args.conteudo.toLowerCase(); - await ctx.db.patch(documentoExistente._id, { - titulo: args.titulo, - conteudo: args.conteudo, - conteudoBusca, - metadados: args.metadados, - atualizadoEm: Date.now() - }); - - return { documentoId: documentoExistente._id, novo: false }; - } - - // Criar novo documento - const agora = Date.now(); - const conteudoBusca = args.titulo.toLowerCase() + ' ' + args.conteudo.toLowerCase(); - - // Determinar categoria baseada no tipo - let categoriaId: Id<'documentacaoCategorias'> | undefined; - const categoriaBackend = await ctx.db - .query('documentacaoCategorias') - .filter((q) => q.eq(q.field('nome'), 'Backend')) - .first(); - - if (categoriaBackend) { - categoriaId = categoriaBackend._id; - } - - const documentoId = await ctx.db.insert('documentacao', { - titulo: args.titulo, - conteudo: args.conteudo, - conteudoBusca, - categoriaId, - tags: [args.tipo, 'automatico'], - tipo: args.tipo, - versao: '1.0.0', - arquivoOrigem: args.arquivoOrigem, - funcaoOrigem: args.funcaoOrigem, - hashOrigem: args.hashOrigem, - metadados: args.metadados, - ativo: true, - criadoPor: args.criadoPor, - criadoEm: agora, - atualizadoEm: agora, - visualizacoes: 0, - geradoAutomaticamente: true - }); - - return { documentoId, novo: true }; - } -}); - -// ========== PUBLIC MUTATIONS ========== - -/** - * Executar varredura manualmente (chamado pelo frontend) - */ -export const executarVarreduraManual = mutation({ - args: {}, - handler: async (ctx) => { - const usuarioAtual = await getCurrentUserFunction(ctx); - if (!usuarioAtual) { - throw new Error('Não autenticado'); - } - - // Verificar se é TI - const role = await ctx.db.get(usuarioAtual.roleId); - if (!role || (role.nivel > 1 && role.nome !== 'ti_master' && role.nome !== 'ti_usuario')) { - throw new Error('Acesso negado. Apenas usuários TI podem executar varredura.'); - } - - // Executar varredura - const varreduraId = await ctx.scheduler.runAfter( - 0, - (internal as any).documentacaoVarredura.executarVarredura, - { - executadoPor: usuarioAtual._id, - tipo: 'manual' - } - ); - - return varreduraId; - } -}); - -/** - * Obter histórico de varreduras - */ -export const obterHistoricoVarreduras = query({ - args: { - limite: v.optional(v.number()) - }, - handler: async (ctx, args) => { - const usuarioAtual = await getCurrentUserFunction(ctx); - if (!usuarioAtual) { - throw new Error('Não autenticado'); - } - - // Verificar se é TI - const role = await ctx.db.get(usuarioAtual.roleId); - if (!role || (role.nivel > 1 && role.nome !== 'ti_master' && role.nome !== 'ti_usuario')) { - throw new Error('Acesso negado.'); - } - - const limite = args.limite || 20; - const varreduras = await ctx.db - .query('documentacaoVarredura') - .withIndex('by_iniciado_em', (q) => q.gte('iniciadoEm', 0)) - .order('desc') - .take(limite); - - // Enriquecer com informações do usuário e arquivos modificados - const varredurasEnriquecidas = await Promise.all( - varreduras.map(async (varredura) => { - const usuario = await ctx.db.get(varredura.executadoPor); - - // Buscar documentos atualizados durante esta varredura - const documentosAtualizados = await ctx.db - .query('documentacao') - .filter((q) => q.eq(q.field('geradoAutomaticamente'), true)) - .filter((q) => q.gte(q.field('atualizadoEm'), varredura.iniciadoEm)) - .filter((q) => q.lte(q.field('atualizadoEm'), varredura.concluidoEm || Date.now())) - .collect(); - - // Agrupar por arquivo e obter versões - const arquivosModificados = new Map(); - - for (const doc of documentosAtualizados) { - if (doc.arquivoOrigem) { - const arquivo = doc.arquivoOrigem; - if (!arquivosModificados.has(arquivo)) { - arquivosModificados.set(arquivo, { versao: doc.versao, funcoes: [] }); - } - const info = arquivosModificados.get(arquivo)!; - if (doc.funcaoOrigem) { - info.funcoes.push(doc.funcaoOrigem); - } - // Manter a versão mais recente (comparar numericamente) - const partesAtual = doc.versao.split('.').map(Number); - const partesInfo = info.versao.split('.').map(Number); - let versaoMaisNova = info.versao; - for (let i = 0; i < Math.max(partesAtual.length, partesInfo.length); i++) { - const atual = partesAtual[i] || 0; - const infoParte = partesInfo[i] || 0; - if (atual > infoParte) { - versaoMaisNova = doc.versao; - break; - } else if (atual < infoParte) { - break; - } - } - info.versao = versaoMaisNova; - } - } - - const arquivosModificadosArray = Array.from(arquivosModificados.entries()).map(([arquivo, info]) => ({ - arquivo, - versao: info.versao, - funcoes: info.funcoes - })); - - return { - ...varredura, - executadoPorUsuario: usuario - ? { - _id: usuario._id, - nome: usuario.nome, - email: usuario.email - } - : null, - arquivosModificados: arquivosModificadosArray, - totalArquivosModificados: arquivosModificados.size - }; - }) - ); - - return varredurasEnriquecidas; - } -}); - -/** - * Verificar e executar varredura agendada (chamado pelo cron) - */ -export const verificarEExecutarVarreduraAgendada = internalMutation({ - args: {}, - handler: async (ctx) => { - // Buscar configuração ativa - const config = await ctx.db - .query('documentacaoConfig') - .withIndex('by_ativo', (q) => q.eq('ativo', true)) - .first(); - - if (!config) { - return; // Nenhuma configuração ativa - } - - const agora = new Date(); - const diaSemanaAtual = agora.getDay(); // 0 = domingo, 1 = segunda, etc. - const diasSemanaMap: Record< - 'domingo' | 'segunda' | 'terca' | 'quarta' | 'quinta' | 'sexta' | 'sabado', - number - > = { - domingo: 0, - segunda: 1, - terca: 2, - quarta: 3, - quinta: 4, - sexta: 5, - sabado: 6 - }; - - // Verificar se hoje é um dos dias configurados - const diaAtualNome = Object.keys(diasSemanaMap).find( - (key) => diasSemanaMap[key as keyof typeof diasSemanaMap] === diaSemanaAtual - ) as 'domingo' | 'segunda' | 'terca' | 'quarta' | 'quinta' | 'sexta' | 'sabado' | undefined; - - if (!diaAtualNome || !config.diasSemana.includes(diaAtualNome)) { - return; // Hoje não é um dia configurado - } - - // Verificar se é o horário configurado (com tolerância de 5 minutos) - const [horaConfig, minutoConfig] = config.horario.split(':').map(Number); - const horaAtual = agora.getHours(); - const minutoAtual = agora.getMinutes(); - - const diferencaMinutos = horaAtual * 60 + minutoAtual - (horaConfig * 60 + minutoConfig); - - if (Math.abs(diferencaMinutos) > 5) { - return; // Não é o horário configurado - } - - // Verificar se já foi executado hoje neste horário - const hojeInicio = new Date(agora); - hojeInicio.setHours(0, 0, 0, 0); - const hojeFim = new Date(agora); - hojeFim.setHours(23, 59, 59, 999); - - const varredurasHoje = await ctx.db - .query('documentacaoVarredura') - .filter((q) => - q.and( - q.gte(q.field('iniciadoEm'), hojeInicio.getTime()), - q.lte(q.field('iniciadoEm'), hojeFim.getTime()), - q.eq(q.field('tipo'), 'automatica') - ) - ) - .collect(); - - // Se já foi executado hoje neste horário, não executar novamente - if (varredurasHoje.length > 0) { - const ultimaVarredura = varredurasHoje.sort((a, b) => b.iniciadoEm - a.iniciadoEm)[0]; - const ultimaVarreduraData = new Date(ultimaVarredura.iniciadoEm); - const ultimaVarreduraHora = ultimaVarreduraData.getHours(); - const ultimaVarreduraMinuto = ultimaVarreduraData.getMinutes(); - - if ( - ultimaVarreduraHora === horaConfig && - Math.abs(ultimaVarreduraMinuto - minutoConfig) <= 5 - ) { - return; // Já foi executado hoje neste horário - } - } - - // Buscar um usuário TI_master para executar a varredura - const roleTIMaster = await ctx.db - .query('roles') - .filter((q) => q.eq(q.field('nivel'), 0)) - .first(); - - if (!roleTIMaster) { - return; // Não há TI_master configurado - } - - const usuarioTIMaster = await ctx.db - .query('usuarios') - .filter((q) => q.eq(q.field('roleId'), roleTIMaster._id)) - .first(); - - if (!usuarioTIMaster) { - return; // Não há usuário TI_master - } - - // Executar varredura - await ctx.scheduler.runAfter( - 0, - (internal as any).documentacaoVarredura.executarVarredura, - { - executadoPor: usuarioTIMaster._id, - tipo: 'automatica' - } - ); - - // Atualizar próxima execução na configuração - await ctx.db.patch(config._id, { - ultimaExecucao: Date.now() - }); - } -}); - diff --git a/packages/backend/convex/schema.ts b/packages/backend/convex/schema.ts index 310c524..7ef1415 100644 --- a/packages/backend/convex/schema.ts +++ b/packages/backend/convex/schema.ts @@ -20,7 +20,6 @@ import { pontoTables } from './tables/ponto'; import { pedidosTables } from './tables/pedidos'; import { produtosTables } from './tables/produtos'; import { lgpdTables } from './tables/lgpdTables'; -import { documentacaoTables } from './tables/documentacao'; export default defineSchema({ ...setoresTables, @@ -43,6 +42,5 @@ export default defineSchema({ ...pontoTables, ...pedidosTables, ...produtosTables, - ...lgpdTables, - ...documentacaoTables + ...lgpdTables }); diff --git a/packages/backend/convex/tables/documentacao.ts b/packages/backend/convex/tables/documentacao.ts deleted file mode 100644 index 1ded1fd..0000000 --- a/packages/backend/convex/tables/documentacao.ts +++ /dev/null @@ -1,152 +0,0 @@ -import { defineTable } from 'convex/server'; -import { v } from 'convex/values'; - -export const documentacaoTables = { - // Documentos principais - documentacao: defineTable({ - titulo: v.string(), - conteudo: v.string(), // Conteúdo em Markdown - conteudoHtml: v.optional(v.string()), // Conteúdo renderizado em HTML - conteudoBusca: v.string(), // Versão normalizada para busca (lowercase, sem acentos) - categoriaId: v.optional(v.id('documentacaoCategorias')), - tags: v.array(v.string()), - tipo: v.union( - v.literal('query'), - v.literal('mutation'), - v.literal('action'), - v.literal('component'), - v.literal('route'), - v.literal('modulo'), - v.literal('manual'), - v.literal('outro') - ), - versao: v.string(), // Versão do documento (ex: "1.0.0") - arquivoOrigem: v.optional(v.string()), // Caminho do arquivo que gerou este documento - funcaoOrigem: v.optional(v.string()), // Nome da função/componente que gerou este documento - hashOrigem: v.optional(v.string()), // Hash do arquivo/função para detectar mudanças - metadados: v.optional( - v.object({ - parametros: v.optional(v.array(v.string())), - retorno: v.optional(v.string()), - dependencias: v.optional(v.array(v.string())), - exemplos: v.optional(v.array(v.string())), - algoritmo: v.optional(v.string()) - }) - ), - ativo: v.boolean(), - criadoPor: v.id('usuarios'), - criadoEm: v.number(), - atualizadoPor: v.optional(v.id('usuarios')), - atualizadoEm: v.number(), - visualizacoes: v.number(), // Contador de visualizações - geradoAutomaticamente: v.boolean() // Se foi gerado pela varredura automática - }) - .index('by_categoria', ['categoriaId']) - .index('by_tipo', ['tipo']) - .index('by_busca', ['conteudoBusca']) - .index('by_ativo', ['ativo']) - .index('by_criado_em', ['criadoEm']) - .index('by_atualizado_em', ['atualizadoEm']) - .index('by_arquivo_origem', ['arquivoOrigem']) - .index('by_hash_origem', ['hashOrigem']), - - // Categorias hierárquicas (Módulos, Seções, Funções) - documentacaoCategorias: defineTable({ - nome: v.string(), - descricao: v.optional(v.string()), - icone: v.optional(v.string()), // Nome do ícone (lucide-svelte) - cor: v.optional( - v.union( - v.literal('primary'), - v.literal('secondary'), - v.literal('accent'), - v.literal('success'), - v.literal('warning'), - v.literal('error'), - v.literal('info') - ) - ), - parentId: v.optional(v.id('documentacaoCategorias')), // Para hierarquia - ordem: v.number(), // Ordem de exibição - ativo: v.boolean(), - criadoPor: v.id('usuarios'), - criadoEm: v.number(), - atualizadoEm: v.number() - }) - .index('by_parent', ['parentId']) - .index('by_ordem', ['ordem']) - .index('by_ativo', ['ativo']), - - // Tags para busca e organização - documentacaoTags: defineTable({ - nome: v.string(), - descricao: v.optional(v.string()), - cor: v.optional( - v.union( - v.literal('primary'), - v.literal('secondary'), - v.literal('accent'), - v.literal('success'), - v.literal('warning'), - v.literal('error'), - v.literal('info') - ) - ), - usadoEm: v.number(), // Contador de quantos documentos usam esta tag - ativo: v.boolean(), - criadoPor: v.id('usuarios'), - criadoEm: v.number() - }) - .index('by_nome', ['nome']) - .index('by_ativo', ['ativo']), - - // Histórico de varreduras realizadas - documentacaoVarredura: defineTable({ - tipo: v.union(v.literal('automatica'), v.literal('manual')), - status: v.union( - v.literal('em_andamento'), - v.literal('concluida'), - v.literal('erro'), - v.literal('cancelada') - ), - documentosEncontrados: v.number(), // Quantidade de documentos encontrados - documentosNovos: v.number(), // Quantidade de novos documentos criados - documentosAtualizados: v.number(), // Quantidade de documentos atualizados - arquivosAnalisados: v.number(), // Quantidade de arquivos analisados - erros: v.optional(v.array(v.string())), // Lista de erros encontrados - duracaoMs: v.optional(v.number()), // Duração em milissegundos - executadoPor: v.id('usuarios'), - iniciadoEm: v.number(), - concluidoEm: v.optional(v.number()) - }) - .index('by_status', ['status']) - .index('by_tipo', ['tipo']) - .index('by_executado_por', ['executadoPor']) - .index('by_iniciado_em', ['iniciadoEm']), - - // Configurações de agendamento de varredura - documentacaoConfig: defineTable({ - ativo: v.boolean(), - diasSemana: v.array( - v.union( - v.literal('domingo'), - v.literal('segunda'), - v.literal('terca'), - v.literal('quarta'), - v.literal('quinta'), - v.literal('sexta'), - v.literal('sabado') - ) - ), // Dias da semana para executar varredura - horario: v.string(), // Horário no formato "HH:MM" (ex: "08:00") - fusoHorario: v.optional(v.string()), // Fuso horário (padrão: "America/Recife") - ultimaExecucao: v.optional(v.number()), // Timestamp da última execução - proximaExecucao: v.optional(v.number()), // Timestamp da próxima execução agendada - configuradoPor: v.id('usuarios'), - configuradoEm: v.number(), - atualizadoPor: v.optional(v.id('usuarios')), - atualizadoEm: v.number() - }) - .index('by_ativo', ['ativo']) -}; -