From 2c3d231d20ae81a88a9aa33472d8face15a8d6a4 Mon Sep 17 00:00:00 2001 From: deyvisonwanderley Date: Mon, 17 Nov 2025 16:27:15 -0300 Subject: [PATCH 01/10] refactor: enhance authentication and access control in ProtectedRoute component - Updated the ProtectedRoute component to improve access control logic, including a timeout mechanism for handling authentication checks. - Refactored the checkAccess function to streamline user access verification based on roles and authentication status. - Added comments for clarity on the authentication flow and the use of the convexClient plugin in the auth.ts file. - Improved the overall structure and readability of the code in auth.ts and ProtectedRoute.svelte. --- apps/web/src/lib/auth.ts | 5 + .../src/lib/components/ProtectedRoute.svelte | 61 +- apps/web/src/routes/(dashboard)/+page.svelte | 1698 +++++++++-------- .../routes/(dashboard)/perfil/+page.svelte | 542 +++++- packages/backend/convex/_generated/api.d.ts | 34 +- .../backend/convex/_generated/server.d.ts | 16 +- packages/backend/convex/_generated/server.js | 13 +- 7 files changed, 1399 insertions(+), 970 deletions(-) diff --git a/apps/web/src/lib/auth.ts b/apps/web/src/lib/auth.ts index 7881eb4..42ba6eb 100644 --- a/apps/web/src/lib/auth.ts +++ b/apps/web/src/lib/auth.ts @@ -8,6 +8,11 @@ import { createAuthClient } from "better-auth/svelte"; import { convexClient } from "@convex-dev/better-auth/client/plugins"; +// O baseURL deve apontar para o frontend (SvelteKit), não para o Convex diretamente +// O Better Auth usa as rotas HTTP do Convex que são acessadas via proxy do SvelteKit +// ou diretamente se configurado. Com o plugin convexClient, o token é gerenciado automaticamente. export const authClient = createAuthClient({ + // baseURL padrão é window.location.origin, que é o correto para SvelteKit + // O Better Auth será acessado via rotas HTTP do Convex registradas em http.ts plugins: [convexClient()], }); diff --git a/apps/web/src/lib/components/ProtectedRoute.svelte b/apps/web/src/lib/components/ProtectedRoute.svelte index 6bcf57a..3e41367 100644 --- a/apps/web/src/lib/components/ProtectedRoute.svelte +++ b/apps/web/src/lib/components/ProtectedRoute.svelte @@ -20,26 +20,32 @@ let isChecking = $state(true); let hasAccess = $state(false); + let timeoutId: ReturnType | null = null; const currentUser = useQuery(api.auth.getCurrentUser, {}); - onMount(() => { + // Usar $effect para reagir às mudanças na query + $effect(() => { checkAccess(); }); function checkAccess() { - isChecking = true; + // Limpar timeout anterior se existir + if (timeoutId) { + clearTimeout(timeoutId); + timeoutId = null; + } - // Aguardar um pouco para o authStore carregar do localStorage - setTimeout(() => { - // Verificar autenticação - if (requireAuth && !currentUser?.data) { - const currentPath = window.location.pathname; - window.location.href = `${redirectTo}?error=auth_required&redirect=${encodeURIComponent(currentPath)}`; - return; - } + // Se a query ainda está carregando (undefined), aguardar + if (currentUser === undefined) { + isChecking = true; + hasAccess = false; + return; + } + // Se a query retornou dados, verificar autenticação + if (currentUser?.data) { // Verificar roles - if (allowedRoles.length > 0 && currentUser?.data) { + if (allowedRoles.length > 0) { const hasRole = allowedRoles.includes(currentUser.data.role?.nome ?? ''); if (!hasRole) { const currentPath = window.location.pathname; @@ -49,19 +55,40 @@ } // Verificar nível - if ( - currentUser?.data && - currentUser.data.role?.nivel && - currentUser.data.role.nivel > maxLevel - ) { + if (currentUser.data.role?.nivel && currentUser.data.role.nivel > maxLevel) { const currentPath = window.location.pathname; window.location.href = `${redirectTo}?error=access_denied&route=${encodeURIComponent(currentPath)}`; return; } + // Se chegou aqui, permitir acesso hasAccess = true; isChecking = false; - }, 100); + return; + } + + // Se não tem dados e requer autenticação, aguardar um pouco antes de redirecionar + // (pode estar carregando ainda) + if (requireAuth && !currentUser?.data) { + isChecking = true; + hasAccess = false; + + // Aguardar 3 segundos antes de redirecionar (dar tempo para a query carregar) + timeoutId = setTimeout(() => { + // Verificar novamente antes de redirecionar + if (!currentUser?.data) { + const currentPath = window.location.pathname; + window.location.href = `${redirectTo}?error=auth_required&redirect=${encodeURIComponent(currentPath)}`; + } + }, 3000); + return; + } + + // Se não requer autenticação, permitir acesso + if (!requireAuth) { + hasAccess = true; + isChecking = false; + } } diff --git a/apps/web/src/routes/(dashboard)/+page.svelte b/apps/web/src/routes/(dashboard)/+page.svelte index 005b954..8330127 100644 --- a/apps/web/src/routes/(dashboard)/+page.svelte +++ b/apps/web/src/routes/(dashboard)/+page.svelte @@ -1,845 +1,853 @@ - - - -
- - {#if showAlert} - {@const alertData = getAlertMessage()} -
-
- {alertData.icon} -
-

{alertData.title}

-

{alertData.message}

- {#if alertType === "access_denied"} - - {/if} -
- -
-
- {/if} - - -
-
-
-

- {getSaudacao()}! 👋 -

-

- Bem-vindo ao Sistema de Gerenciamento da Secretaria de Esportes -

-

- {currentTime.toLocaleDateString("pt-BR", { - weekday: "long", - year: "numeric", - month: "long", - day: "numeric", - })} - {" - "} - {currentTime.toLocaleTimeString("pt-BR")} -

-
-
-
Sistema Online
-
Atualizado
-
-
-
- - - {#if statsQuery.isLoading} -
- -
- {:else if statsQuery.data} -
- -
-
-
-
-

- Total de Funcionários -

-

- {formatNumber(statsQuery.data.totalFuncionarios)} -

-

- {statsQuery.data.funcionariosAtivos} ativos -

-
-
- {calcPercentage( - statsQuery.data.funcionariosAtivos, - statsQuery.data.totalFuncionarios, - )}% -
-
-
-
- - -
-
-
-
-

- Solicitações Pendentes -

-

- {formatNumber(statsQuery.data.solicitacoesPendentes)} -

-

- de {statsQuery.data.totalSolicitacoesAcesso} total -

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

- Símbolos Cadastrados -

-

- {formatNumber(statsQuery.data.totalSimbolos)} -

-

- {statsQuery.data.cargoComissionado} CC / {statsQuery.data - .funcaoGratificada} FG -

-
-
- - - -
-
-
-
- - - {#if activityQuery.data} -
-
-
-
-

- Atividade (24h) -

-

- {formatNumber( - activityQuery.data.funcionariosCadastrados24h + - activityQuery.data.solicitacoesAcesso24h, - )} -

-

- {activityQuery.data.funcionariosCadastrados24h} cadastros -

-
-
- - - -
-
-
-
- {/if} -
- - - {#if statusSistemaQuery.data && atividadeBDQuery.data && distribuicaoQuery.data} - {@const status = statusSistemaQuery.data} - {@const atividade = atividadeBDQuery.data} - {@const distribuicao = distribuicaoQuery.data} - -
-
-
- - - -
-
-

- Monitoramento em Tempo Real -

-

- Atualizado a cada segundo • {new Date( - status.ultimaAtualizacao, - ).toLocaleTimeString("pt-BR")} -

-
-
- - - LIVE -
-
- - -
- -
-
-
-
-

- Usuários Online -

-

- {status.usuariosOnline} -

-

- sessões ativas -

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

- Total Registros -

-

- {status.totalRegistros.toLocaleString("pt-BR")} -

-

- no banco de dados -

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

- Tempo Resposta -

-

- {status.tempoMedioResposta}ms -

-

média atual

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

- Uso do Sistema -

-
-
-
- CPU - {status.cpuUsada}% -
- -
-
-
- Memória - {status.memoriaUsada}% -
- -
-
-
-
-
-
- - -
-
-
-
-

- Atividade do Banco de Dados -

-

- Entradas e saídas em tempo real (último minuto) -

-
-
- - Atualizando -
-
- -
- -
- {#each [10, 8, 6, 4, 2, 0] as val} - {val} - {/each} -
- - -
- - {#each Array.from({ length: 6 }) as _, i} -
- {/each} - - -
- {#each atividade.historico as ponto, idx} - {@const maxAtividade = Math.max( - ...atividade.historico.map((p) => - Math.max(p.entradas, p.saidas), - ), - )} -
- -
- -
- - -
-
↑ {ponto.entradas} entradas
-
↓ {ponto.saidas} saídas
-
-
- {/each} -
-
- - -
- - -
- -60s - -30s - agora -
-
- - -
-
-
- Entradas no BD -
-
-
- Saídas do BD -
-
-
-
- - -
-
-
-

- Tipos de Operações -

-
-
-
- Queries (Leituras) - {distribuicao.queries} -
- -
-
-
- Mutations (Escritas) - {distribuicao.mutations} -
- -
-
-
-
- -
-
-

- Operações no Banco -

-
-
-
- Leituras - {distribuicao.leituras} -
- -
-
-
- Escritas - {distribuicao.escritas} -
- -
-
-
-
-
-
- {/if} - - -
-
-
-

Status do Sistema

-
-
- Banco de Dados - Online -
-
- API - Operacional -
-
- Backup - Atualizado -
-
-
-
- - - -
-
-

Informações

-
-

- Versão: 1.0.0 -

-

- Última Atualização: - {new Date().toLocaleDateString("pt-BR")} -

-

- Suporte: TI SGSE -

-
-
-
-
- {/if} -
-
- - + + + +
+ + {#if showAlert} + {@const alertData = getAlertMessage()} +
+
+ {alertData.icon} +
+

{alertData.title}

+

{alertData.message}

+ {#if alertType === "access_denied"} + + {/if} +
+ +
+
+ {/if} + + +
+
+
+

+ {getSaudacao()}! 👋 +

+

+ Bem-vindo ao Sistema de Gerenciamento da Secretaria de Esportes +

+

+ {currentTime.toLocaleDateString("pt-BR", { + weekday: "long", + year: "numeric", + month: "long", + day: "numeric", + })} + {" - "} + {currentTime.toLocaleTimeString("pt-BR")} +

+
+
+
Sistema Online
+
Atualizado
+
+
+
+ + + {#if statsQuery.isLoading} +
+ +
+ {:else if statsQuery.data} +
+ +
+
+
+
+

+ Total de Funcionários +

+

+ {formatNumber(statsQuery.data.totalFuncionarios)} +

+

+ {statsQuery.data.funcionariosAtivos} ativos +

+
+
+ {calcPercentage( + statsQuery.data.funcionariosAtivos, + statsQuery.data.totalFuncionarios, + )}% +
+
+
+
+ + +
+
+
+
+

+ Solicitações Pendentes +

+

+ {formatNumber(statsQuery.data.solicitacoesPendentes)} +

+

+ de {statsQuery.data.totalSolicitacoesAcesso} total +

+
+
+ + + +
+
+
+
+ + +
+
+
+
+

+ Símbolos Cadastrados +

+

+ {formatNumber(statsQuery.data.totalSimbolos)} +

+

+ {statsQuery.data.cargoComissionado} CC / {statsQuery.data + .funcaoGratificada} FG +

+
+
+ + + +
+
+
+
+ + + {#if activityQuery.data} +
+
+
+
+

+ Atividade (24h) +

+

+ {formatNumber( + activityQuery.data.funcionariosCadastrados24h + + activityQuery.data.solicitacoesAcesso24h, + )} +

+

+ {activityQuery.data.funcionariosCadastrados24h} cadastros +

+
+
+ + + +
+
+
+
+ {/if} +
+ + + {#if statusSistemaQuery.data && atividadeBDQuery.data && distribuicaoQuery.data} + {@const status = statusSistemaQuery.data} + {@const atividade = atividadeBDQuery.data} + {@const distribuicao = distribuicaoQuery.data} + +
+
+
+ + + +
+
+

+ Monitoramento em Tempo Real +

+

+ Atualizado a cada segundo • {new Date( + status.ultimaAtualizacao, + ).toLocaleTimeString("pt-BR")} +

+
+
+ + + LIVE +
+
+ + +
+ +
+
+
+
+

+ Usuários Online +

+

+ {status.usuariosOnline} +

+

+ sessões ativas +

+
+
+ + + +
+
+
+
+ + +
+
+
+
+

+ Total Registros +

+

+ {status.totalRegistros.toLocaleString("pt-BR")} +

+

+ no banco de dados +

+
+
+ + + +
+
+
+
+ + +
+
+
+
+

+ Tempo Resposta +

+

+ {status.tempoMedioResposta}ms +

+

média atual

+
+
+ + + +
+
+
+
+ + +
+
+
+

+ Uso do Sistema +

+
+
+
+ CPU + {status.cpuUsada}% +
+ +
+
+
+ Memória + {status.memoriaUsada}% +
+ +
+
+
+
+
+
+ + +
+
+
+
+

+ Atividade do Banco de Dados +

+

+ Entradas e saídas em tempo real (último minuto) +

+
+
+ + Atualizando +
+
+ +
+ +
+ {#each [10, 8, 6, 4, 2, 0] as val} + {val} + {/each} +
+ + +
+ + {#each Array.from({ length: 6 }) as _, i} +
+ {/each} + + +
+ {#each atividade.historico as ponto, idx} + {@const maxAtividade = Math.max( + ...atividade.historico.map((p) => + Math.max(p.entradas, p.saidas), + ), + )} +
+ +
+ +
+ + +
+
↑ {ponto.entradas} entradas
+
↓ {ponto.saidas} saídas
+
+
+ {/each} +
+
+ + +
+ + +
+ -60s + -30s + agora +
+
+ + +
+
+
+ Entradas no BD +
+
+
+ Saídas do BD +
+
+
+
+ + +
+
+
+

+ Tipos de Operações +

+
+
+
+ Queries (Leituras) + {distribuicao.queries} +
+ +
+
+
+ Mutations (Escritas) + {distribuicao.mutations} +
+ +
+
+
+
+ +
+
+

+ Operações no Banco +

+
+
+
+ Leituras + {distribuicao.leituras} +
+ +
+
+
+ Escritas + {distribuicao.escritas} +
+ +
+
+
+
+
+
+ {/if} + + +
+
+
+

Status do Sistema

+
+
+ Banco de Dados + Online +
+
+ API + Operacional +
+
+ Backup + Atualizado +
+
+
+
+ + + +
+
+

Informações

+
+

+ Versão: 1.0.0 +

+

+ Última Atualização: + {new Date().toLocaleDateString("pt-BR")} +

+

+ Suporte: TI SGSE +

+
+
+
+
+ {/if} +
+
+ + diff --git a/apps/web/src/routes/(dashboard)/perfil/+page.svelte b/apps/web/src/routes/(dashboard)/perfil/+page.svelte index 2e0f1a7..0424489 100644 --- a/apps/web/src/routes/(dashboard)/perfil/+page.svelte +++ b/apps/web/src/routes/(dashboard)/perfil/+page.svelte @@ -1,7 +1,7 @@ - - - - - - + + + + + + + diff --git a/apps/web/src/lib/components/Sidebar.svelte b/apps/web/src/lib/components/Sidebar.svelte index fbc1386..05cc6b7 100644 --- a/apps/web/src/lib/components/Sidebar.svelte +++ b/apps/web/src/lib/components/Sidebar.svelte @@ -203,7 +203,7 @@ @@ -521,7 +521,7 @@

SGSE

- Sistema de Gerenciamento da
Secretaria de Esportes + Sistema de Gerenciamento de Secretaria

diff --git a/apps/web/src/lib/components/chamados/TicketForm.svelte b/apps/web/src/lib/components/chamados/TicketForm.svelte index 132bcab..a7acf17 100644 --- a/apps/web/src/lib/components/chamados/TicketForm.svelte +++ b/apps/web/src/lib/components/chamados/TicketForm.svelte @@ -76,38 +76,52 @@ const loading = $derived(props.loading ?? false);
-
-
- - - {#if errors.titulo} - {errors.titulo} - {/if} -
+ +
+ + + {#if errors.titulo} + {errors.titulo} + {/if} +
+ +
-
- {#each ["chamado", "reclamacao", "elogio", "sugestao"] as opcao} -
+
+ + {#if errors.categoria} + {errors.categoria} + {/if} +
+ + +
+ {#if errors.descricao} @@ -155,7 +197,8 @@ const loading = $derived(props.loading ?? false); {/if}
-
+ +

Anexos (opcional)

@@ -164,6 +207,20 @@ const loading = $derived(props.loading ?? false);

@@ -196,16 +253,31 @@ const loading = $derived(props.loading ?? false); {/if}
-
+ +
@@ -215,6 +287,20 @@ const loading = $derived(props.loading ?? false); onclick={resetForm} disabled={loading} > + + + Limpar
diff --git a/apps/web/src/lib/components/ferias/WizardSolicitacaoFerias.svelte b/apps/web/src/lib/components/ferias/WizardSolicitacaoFerias.svelte index ea98b30..c748d87 100644 --- a/apps/web/src/lib/components/ferias/WizardSolicitacaoFerias.svelte +++ b/apps/web/src/lib/components/ferias/WizardSolicitacaoFerias.svelte @@ -227,12 +227,13 @@
-
+
{#each Array(totalPassos) as _, i (i)} -
+ {@const labels = ['Ano & Saldo', 'Períodos', 'Confirmação']} +
i + 1} class:text-white={passoAtual > i + 1} class:border-4={passoAtual === i + 1} @@ -261,10 +262,16 @@ {/if}
+ +

+ {labels[i]} +

+ {#if i < totalPassos - 1}
i + 1} class:bg-base-300={passoAtual <= i + 1} >
@@ -272,19 +279,6 @@
{/each}
- - -
-
-

Ano & Saldo

-
-
-

Períodos

-
-
-

Confirmação

-
-
diff --git a/apps/web/src/lib/components/ti/CybersecurityWizcard.svelte b/apps/web/src/lib/components/ti/CybersecurityWizcard.svelte index a60e04a..97f0d08 100644 --- a/apps/web/src/lib/components/ti/CybersecurityWizcard.svelte +++ b/apps/web/src/lib/components/ti/CybersecurityWizcard.svelte @@ -896,7 +896,7 @@ doc.setFontSize(8); doc.setFont('helvetica', 'normal'); doc.setTextColor(128, 128, 128); - doc.text('SGSE - Sistema de Gerenciamento da Secretaria de Esportes', 105, 285, { + doc.text('SGSE - Sistema de Gerenciamento de Secretaria', 105, 285, { align: 'center' }); doc.text(`Gerado em: ${agoraStr}`, 105, 290, { align: 'center' }); diff --git a/apps/web/src/lib/components/ti/ReportGeneratorModal.svelte b/apps/web/src/lib/components/ti/ReportGeneratorModal.svelte index bf566b2..edd26a3 100644 --- a/apps/web/src/lib/components/ti/ReportGeneratorModal.svelte +++ b/apps/web/src/lib/components/ti/ReportGeneratorModal.svelte @@ -197,7 +197,7 @@ doc.setFontSize(8); doc.setTextColor(128, 128, 128); doc.text( - `SGSE - Sistema de Gestão da Secretaria de Esportes | Página ${i} de ${pageCount}`, + `SGSE - Sistema de Gerenciamento de Secretaria | Página ${i} de ${pageCount}`, doc.internal.pageSize.getWidth() / 2, doc.internal.pageSize.getHeight() - 10, { align: 'center' } diff --git a/apps/web/src/lib/utils/declaracoesGenerator.ts b/apps/web/src/lib/utils/declaracoesGenerator.ts index d4fd33a..530df2f 100644 --- a/apps/web/src/lib/utils/declaracoesGenerator.ts +++ b/apps/web/src/lib/utils/declaracoesGenerator.ts @@ -172,7 +172,7 @@ export async function gerarDeclaracaoAcumulacaoCargo(funcionario: Funcionario): // Rodapé doc.setFontSize(8); doc.setTextColor(100); - doc.text('SGSE - Sistema de Gerenciamento da Secretaria de Esportes', 105, 285, { align: 'center' }); + doc.text('SGSE - Sistema de Gerenciamento de Secretaria', 105, 285, { align: 'center' }); return doc.output('blob'); } @@ -260,7 +260,7 @@ export async function gerarDeclaracaoDependentesIR(funcionario: Funcionario): Pr // Rodapé doc.setFontSize(8); doc.setTextColor(100); - doc.text('SGSE - Sistema de Gerenciamento da Secretaria de Esportes', 105, 285, { align: 'center' }); + doc.text('SGSE - Sistema de Gerenciamento de Secretaria', 105, 285, { align: 'center' }); return doc.output('blob'); } @@ -341,7 +341,7 @@ export async function gerarDeclaracaoIdoneidade(funcionario: Funcionario): Promi // Rodapé doc.setFontSize(8); doc.setTextColor(100); - doc.text('SGSE - Sistema de Gerenciamento da Secretaria de Esportes', 105, 285, { align: 'center' }); + doc.text('SGSE - Sistema de Gerenciamento de Secretaria', 105, 285, { align: 'center' }); return doc.output('blob'); } @@ -440,7 +440,7 @@ export async function gerarTermoNepotismo(funcionario: Funcionario): Promise

- Bem-vindo ao Sistema de Gerenciamento da Secretaria de Esportes + Bem-vindo ao SGSE - Sistema de Gerenciamento de Secretaria

{currentTime.toLocaleDateString("pt-BR", { diff --git a/apps/web/src/routes/(dashboard)/abrir-chamado/+page.svelte b/apps/web/src/routes/(dashboard)/abrir-chamado/+page.svelte index 83af3b3..7df3090 100644 --- a/apps/web/src/routes/(dashboard)/abrir-chamado/+page.svelte +++ b/apps/web/src/routes/(dashboard)/abrir-chamado/+page.svelte @@ -136,7 +136,7 @@

Registre reclamações, sugestões, elogios ou chamados técnicos. Toda interação gera - notificações automáticas via e-mail e chat com a assinatura do SGSE. + notificações automáticas via e-mail e chat com a assinatura do SGSE - Sistema de Gerenciamento de Secretaria.

Resposta ágil @@ -164,11 +164,31 @@
-
-

Formulário

-

- Informe os detalhes para que nossa equipe possa priorizar o atendimento. -

+
+
+
+ + + +
+
+

Formulário

+

+ Informe os detalhes para que nossa equipe possa priorizar o atendimento. +

+
+
{#if resetSignal % 2 === 0} @@ -180,13 +200,37 @@
diff --git a/apps/web/src/routes/(dashboard)/perfil/+page.svelte b/apps/web/src/routes/(dashboard)/perfil/+page.svelte index 0424489..95ea561 100644 --- a/apps/web/src/routes/(dashboard)/perfil/+page.svelte +++ b/apps/web/src/routes/(dashboard)/perfil/+page.svelte @@ -1233,11 +1233,11 @@ {#if ehGestor}

- SGSE - Sistema de Gerenciamento da Secretaria de Esportes. + SGSE - Sistema de Gerenciamento de Secretaria.

-
-
-

Detalhes do chamado

- {#if !detalheSelecionado} -

Selecione um chamado na tabela.

- {:else} -
-
-
-

Solicitante

-

{detalheSelecionado.solicitanteNome}

+ + {#if !detalheSelecionado} +
+
+ + + +

Nenhum chamado selecionado

+

+ Selecione um chamado na tabela acima para visualizar detalhes e realizar ações. +

+
+
+ {:else} + +
+ +
+
+
+
+
+ {detalheSelecionado.numero} +
+ + {getStatusLabel(detalheSelecionado.status)} + +
+

{detalheSelecionado.titulo}

+
+
+ + + + Solicitante: + {detalheSelecionado.solicitanteNome} +
+ {#if detalheSelecionado.prazoConclusao} +
+ + + + Prazo: + {prazoRestante(detalheSelecionado.prazoConclusao) ?? "--"} +
+ {/if}
- - {getStatusLabel(detalheSelecionado.status)} -
-

{detalheSelecionado.descricao}

-
-
-

Prazo resposta

-

{prazoRestante(detalheSelecionado.prazoResposta) ?? "--"}

+
+
+ + +
+ +
+
+ + + +

Detalhes do chamado

+
+
+
+

Descrição

+

+ {detalheSelecionado.descricao} +

+
+
+
+

Prazo de resposta

+

+ {prazoRestante(detalheSelecionado.prazoResposta) ?? "--"} +

-
-

Prazo conclusão

-

{prazoRestante(detalheSelecionado.prazoConclusao) ?? "--"}

+
+

Prazo de conclusão

+

+ {prazoRestante(detalheSelecionado.prazoConclusao) ?? "--"} +

+

Histórico e Timeline

- {/if} -
+
- -
-

Responder chamado

- {#if !detalheSelecionado} -

Selecione um chamado na tabela para responder.

- {:else} -
+ +
+
+ + + +

Responder chamado

+
+
- {/if} -
- -
-

Atribuir responsável

-
- - - {#if assignFeedback} -
- {assignFeedback} -
- {/if} -
- -
-

Prorrogar prazo

-

Recurso exclusivo para a equipe de TI

+ +
+
+ + + +

Atribuir responsável

+
-
- - + + + {#if assignFeedback} +
+ {assignFeedback} +
+ {/if} + +
+
+ + +
+
+ + + +
+

Prorrogar prazo

+

Recurso exclusivo para a equipe de TI

+
+
-
+
+ {/if}
@@ -1057,7 +1194,7 @@ {:else} -
+
{slaConfigsAtivos.length}
Total de SLAs
@@ -1071,8 +1208,12 @@
Prioridade Média
-
{slaConfigsPorPrioridadeCount.alta + slaConfigsPorPrioridadeCount.critica}
-
Prioridade Alta/Crítica
+
{slaConfigsPorPrioridadeCount.alta}
+
Prioridade Alta
+
+
+
{slaConfigsPorPrioridadeCount.critica}
+
Prioridade Crítica
diff --git a/apps/web/src/routes/(dashboard)/ti/cibersecurity/+page.svelte b/apps/web/src/routes/(dashboard)/ti/cibersecurity/+page.svelte index b31d71b..6541008 100644 --- a/apps/web/src/routes/(dashboard)/ti/cibersecurity/+page.svelte +++ b/apps/web/src/routes/(dashboard)/ti/cibersecurity/+page.svelte @@ -11,14 +11,14 @@ - Cibersecurity SGSE • Wizcard TI + Cibersecurity SGSE - Sistema de Gerenciamento de Secretaria • Wizcard TI

- Cibersecurity • SGSE + Cibersecurity • SGSE - Sistema de Gerenciamento de Secretaria

Segurança Avançada

diff --git a/apps/web/src/routes/(dashboard)/ti/configuracoes-email/+page.svelte b/apps/web/src/routes/(dashboard)/ti/configuracoes-email/+page.svelte index 6e05735..15a7f1a 100644 --- a/apps/web/src/routes/(dashboard)/ti/configuracoes-email/+page.svelte +++ b/apps/web/src/routes/(dashboard)/ti/configuracoes-email/+page.svelte @@ -404,7 +404,7 @@ id="smtp-nome-remetente" type="text" bind:value={nomeRemetente} - placeholder="SGSE - Sistema de Gestão" + placeholder="SGSE - Sistema de Gerenciamento de Secretaria" class="input input-bordered" />

diff --git a/apps/web/src/routes/(dashboard)/ti/monitoramento/+page.svelte b/apps/web/src/routes/(dashboard)/ti/monitoramento/+page.svelte index 32b2e99..8eb9a14 100644 --- a/apps/web/src/routes/(dashboard)/ti/monitoramento/+page.svelte +++ b/apps/web/src/routes/(dashboard)/ti/monitoramento/+page.svelte @@ -24,7 +24,7 @@
-

Monitoramento SGSE

+

Monitoramento SGSE - Sistema de Gerenciamento de Secretaria

Sistema de monitoramento técnico em tempo real

diff --git a/apps/web/src/routes/(dashboard)/ti/painel-administrativo/+page.svelte b/apps/web/src/routes/(dashboard)/ti/painel-administrativo/+page.svelte index c8a798b..54e5976 100644 --- a/apps/web/src/routes/(dashboard)/ti/painel-administrativo/+page.svelte +++ b/apps/web/src/routes/(dashboard)/ti/painel-administrativo/+page.svelte @@ -122,6 +122,6 @@
- Sistema de Gestão da Secretaria de Esportes - Versão 2.0 com controle avançado de acesso + SGSE - Sistema de Gerenciamento de Secretaria - Versão 2.0 com controle avançado de acesso
diff --git a/packages/backend/convex/actions/email.ts b/packages/backend/convex/actions/email.ts index adc368d..c22ea26 100644 --- a/packages/backend/convex/actions/email.ts +++ b/packages/backend/convex/actions/email.ts @@ -1,233 +1,233 @@ -"use node"; - -import { action } from "../_generated/server"; -import { v } from "convex/values"; -import { internal } from "../_generated/api"; -import { decryptSMTPPasswordNode } from "./utils/nodeCrypto"; -import nodemailer from "nodemailer"; - -export const enviar = action({ - args: { - emailId: v.id("notificacoesEmail"), - }, - returns: v.object({ sucesso: v.boolean(), erro: v.optional(v.string()) }), - handler: async (ctx, args) => { - "use node"; - - let email; - try { - // Buscar email da fila - email = await ctx.runQuery(internal.email.getEmailById, { - emailId: args.emailId, - }); - - if (!email) { - return { sucesso: false, erro: "Email não encontrado" }; - } - - // Buscar configuração SMTP ativa - const configRaw = await ctx.runQuery( - internal.email.getActiveEmailConfig, - {} - ); - - if (!configRaw) { - console.error( - "❌ Configuração SMTP não encontrada ou inativa para email:", - email.destinatario - ); - return { - sucesso: false, - erro: "Configuração de email não encontrada ou inativa. Verifique as configurações SMTP no painel de TI.", - }; - } - - console.log("📧 Tentando enviar email:", { - para: email.destinatario, - assunto: email.assunto, - servidor: configRaw.servidor, - porta: configRaw.porta, - }); - - // Descriptografar senha usando função compatível com Node.js - let senhaDescriptografada: string; - try { - senhaDescriptografada = await decryptSMTPPasswordNode( - configRaw.senhaHash - ); - } catch (decryptError) { - const decryptErrorMessage = - decryptError instanceof Error - ? decryptError.message - : String(decryptError); - console.error( - "Erro ao descriptografar senha SMTP:", - decryptErrorMessage - ); - return { - sucesso: false, - erro: `Erro ao descriptografar senha SMTP: ${decryptErrorMessage}`, - }; - } - - const config = { - ...configRaw, - senha: senhaDescriptografada, - }; - - // Config já foi validado acima - - // Avisar mas não bloquear se não foi testado - if (!config.testadoEm) { - console.warn( - "⚠️ Configuração SMTP não foi testada. Tentando enviar mesmo assim..." - ); - } - - // Marcar como enviando - await ctx.runMutation(internal.email.markEmailEnviando, { - emailId: args.emailId, - }); - - // Criar transporter do nodemailer com configuração melhorada - const transporterOptions: { - host: string; - port: number; - secure: boolean; - requireTLS?: boolean; - auth: { - user: string; - pass: string; - }; - tls?: { - rejectUnauthorized: boolean; - ciphers?: string; - }; - connectionTimeout: number; - greetingTimeout: number; - socketTimeout: number; - pool?: boolean; - maxConnections?: number; - maxMessages?: number; - } = { - host: config.servidor, - port: config.porta, - secure: config.usarSSL, - auth: { - user: config.usuario, - pass: config.senha, // Senha já descriptografada - }, - connectionTimeout: 15000, // 15 segundos - greetingTimeout: 15000, - socketTimeout: 15000, - pool: true, // Usar pool de conexões - maxConnections: 5, - maxMessages: 100, - }; - - // Adicionar TLS apenas se necessário - if (config.usarTLS) { - transporterOptions.requireTLS = true; - transporterOptions.tls = { - rejectUnauthorized: false, // Permitir certificados autoassinados - }; - } else if (config.usarSSL) { - transporterOptions.tls = { - rejectUnauthorized: false, - }; - } - - const transporter = nodemailer.createTransport(transporterOptions); - - // Verificar conexão antes de enviar - try { - await transporter.verify(); - console.log("✅ Conexão SMTP verificada com sucesso"); - } catch (verifyError) { - const verifyErrorMessage = - verifyError instanceof Error - ? verifyError.message - : String(verifyError); - console.warn( - "⚠️ Falha na verificação SMTP, mas tentando enviar mesmo assim:", - verifyErrorMessage - ); - // Não bloquear envio por falha na verificação, apenas avisar - } - - // Validar email destinatário antes de enviar - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - if (!emailRegex.test(email.destinatario)) { - throw new Error(`Email destinatário inválido: ${email.destinatario}`); - } - - // Criar versão texto do HTML (remover tags e decodificar entidades básicas) - const textoPlano = email.corpo - .replace(/<[^>]*>/g, "") // Remover tags HTML - .replace(/ /g, " ") - .replace(/&/g, "&") - .replace(/</g, "<") - .replace(/>/g, ">") - .replace(/"/g, '"') - .replace(/'/g, "'") - .trim(); - - // Enviar email - const info = await transporter.sendMail({ - from: `"${config.nomeRemetente}" <${config.emailRemetente}>`, - to: email.destinatario, - subject: email.assunto, - html: email.corpo, - text: textoPlano || email.assunto, // Versão texto para clientes que não suportam HTML - headers: { - "X-Mailer": "SGSE-Sistema", - "X-Priority": "3", - }, - }); - - interface MessageInfo { - messageId?: string; - response?: string; - } - - const messageInfo = info as MessageInfo; - - console.log("✅ Email enviado com sucesso!", { - para: email.destinatario, - assunto: email.assunto, - messageId: messageInfo.messageId, - response: messageInfo.response, - }); - - // Marcar como enviado - await ctx.runMutation(internal.email.markEmailEnviado, { - emailId: args.emailId, - }); - - return { sucesso: true }; - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : String(error); - const errorStack = error instanceof Error ? error.stack : undefined; - - console.error("❌ Erro ao enviar email:", { - emailId: args.emailId, - destinatario: email?.destinatario, - erro: errorMessage, - stack: errorStack, - }); - - // Marcar como falha com detalhes completos - const erroCompleto = errorStack - ? `${errorMessage}\n\nStack: ${errorStack}` - : errorMessage; - - await ctx.runMutation(internal.email.markEmailFalha, { - emailId: args.emailId, - erro: erroCompleto.substring(0, 2000), // Limitar tamanho do erro - }); - - return { sucesso: false, erro: errorMessage }; - } - }, -}); +"use node"; + +import { action } from "../_generated/server"; +import { v } from "convex/values"; +import { internal } from "../_generated/api"; +import { decryptSMTPPasswordNode } from "./utils/nodeCrypto"; +import nodemailer from "nodemailer"; + +export const enviar = action({ + args: { + emailId: v.id("notificacoesEmail"), + }, + returns: v.object({ sucesso: v.boolean(), erro: v.optional(v.string()) }), + handler: async (ctx, args) => { + "use node"; + + let email; + try { + // Buscar email da fila + email = await ctx.runQuery(internal.email.getEmailById, { + emailId: args.emailId, + }); + + if (!email) { + return { sucesso: false, erro: "Email não encontrado" }; + } + + // Buscar configuração SMTP ativa + const configRaw = await ctx.runQuery( + internal.email.getActiveEmailConfig, + {} + ); + + if (!configRaw) { + console.error( + "❌ Configuração SMTP não encontrada ou inativa para email:", + email.destinatario + ); + return { + sucesso: false, + erro: "Configuração de email não encontrada ou inativa. Verifique as configurações SMTP no painel de TI.", + }; + } + + console.log("📧 Tentando enviar email:", { + para: email.destinatario, + assunto: email.assunto, + servidor: configRaw.servidor, + porta: configRaw.porta, + }); + + // Descriptografar senha usando função compatível com Node.js + let senhaDescriptografada: string; + try { + senhaDescriptografada = await decryptSMTPPasswordNode( + configRaw.senhaHash + ); + } catch (decryptError) { + const decryptErrorMessage = + decryptError instanceof Error + ? decryptError.message + : String(decryptError); + console.error( + "Erro ao descriptografar senha SMTP:", + decryptErrorMessage + ); + return { + sucesso: false, + erro: `Erro ao descriptografar senha SMTP: ${decryptErrorMessage}`, + }; + } + + const config = { + ...configRaw, + senha: senhaDescriptografada, + }; + + // Config já foi validado acima + + // Avisar mas não bloquear se não foi testado + if (!config.testadoEm) { + console.warn( + "⚠️ Configuração SMTP não foi testada. Tentando enviar mesmo assim..." + ); + } + + // Marcar como enviando + await ctx.runMutation(internal.email.markEmailEnviando, { + emailId: args.emailId, + }); + + // Criar transporter do nodemailer com configuração melhorada + const transporterOptions: { + host: string; + port: number; + secure: boolean; + requireTLS?: boolean; + auth: { + user: string; + pass: string; + }; + tls?: { + rejectUnauthorized: boolean; + ciphers?: string; + }; + connectionTimeout: number; + greetingTimeout: number; + socketTimeout: number; + pool?: boolean; + maxConnections?: number; + maxMessages?: number; + } = { + host: config.servidor, + port: config.porta, + secure: config.usarSSL, + auth: { + user: config.usuario, + pass: config.senha, // Senha já descriptografada + }, + connectionTimeout: 15000, // 15 segundos + greetingTimeout: 15000, + socketTimeout: 15000, + pool: true, // Usar pool de conexões + maxConnections: 5, + maxMessages: 100, + }; + + // Adicionar TLS apenas se necessário + if (config.usarTLS) { + transporterOptions.requireTLS = true; + transporterOptions.tls = { + rejectUnauthorized: false, // Permitir certificados autoassinados + }; + } else if (config.usarSSL) { + transporterOptions.tls = { + rejectUnauthorized: false, + }; + } + + const transporter = nodemailer.createTransport(transporterOptions); + + // Verificar conexão antes de enviar + try { + await transporter.verify(); + console.log("✅ Conexão SMTP verificada com sucesso"); + } catch (verifyError) { + const verifyErrorMessage = + verifyError instanceof Error + ? verifyError.message + : String(verifyError); + console.warn( + "⚠️ Falha na verificação SMTP, mas tentando enviar mesmo assim:", + verifyErrorMessage + ); + // Não bloquear envio por falha na verificação, apenas avisar + } + + // Validar email destinatário antes de enviar + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(email.destinatario)) { + throw new Error(`Email destinatário inválido: ${email.destinatario}`); + } + + // Criar versão texto do HTML (remover tags e decodificar entidades básicas) + const textoPlano = email.corpo + .replace(/<[^>]*>/g, "") // Remover tags HTML + .replace(/ /g, " ") + .replace(/&/g, "&") + .replace(/</g, "<") + .replace(/>/g, ">") + .replace(/"/g, '"') + .replace(/'/g, "'") + .trim(); + + // Enviar email + const info = await transporter.sendMail({ + from: `"${config.nomeRemetente}" <${config.emailRemetente}>`, + to: email.destinatario, + subject: email.assunto, + html: email.corpo, + text: textoPlano || email.assunto, // Versão texto para clientes que não suportam HTML + headers: { + "X-Mailer": "SGSE-Sistema-de-Gerenciamento-de-Secretaria", + "X-Priority": "3", + }, + }); + + interface MessageInfo { + messageId?: string; + response?: string; + } + + const messageInfo = info as MessageInfo; + + console.log("✅ Email enviado com sucesso!", { + para: email.destinatario, + assunto: email.assunto, + messageId: messageInfo.messageId, + response: messageInfo.response, + }); + + // Marcar como enviado + await ctx.runMutation(internal.email.markEmailEnviado, { + emailId: args.emailId, + }); + + return { sucesso: true }; + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : String(error); + const errorStack = error instanceof Error ? error.stack : undefined; + + console.error("❌ Erro ao enviar email:", { + emailId: args.emailId, + destinatario: email?.destinatario, + erro: errorMessage, + stack: errorStack, + }); + + // Marcar como falha com detalhes completos + const erroCompleto = errorStack + ? `${errorMessage}\n\nStack: ${errorStack}` + : errorMessage; + + await ctx.runMutation(internal.email.markEmailFalha, { + emailId: args.emailId, + erro: erroCompleto.substring(0, 2000), // Limitar tamanho do erro + }); + + return { sucesso: false, erro: errorMessage }; + } + }, +}); diff --git a/packages/backend/convex/chamados.ts b/packages/backend/convex/chamados.ts index 7bfe140..f25df21 100644 --- a/packages/backend/convex/chamados.ts +++ b/packages/backend/convex/chamados.ts @@ -127,7 +127,7 @@ async function registrarNotificacoes( destinatario: ticket.solicitanteEmail, destinatarioId: ticket.solicitanteId, assunto: `${titulo} - Chamado ${ticket.numero}`, - corpo: `${mensagem}\n\n---\nCentral de Chamados SGSE`, + corpo: `${mensagem}\n\n---\nCentral de Chamados SGSE - Sistema de Gerenciamento de Secretaria`, enviadoPor: usuarioEvento, }); } @@ -151,7 +151,7 @@ async function registrarNotificacoes( destinatario: responsavel.email, destinatarioId: ticket.responsavelId, assunto: `${titulo} - Chamado ${ticket.numero}`, - corpo: `${mensagem}\n\n---\nCentral de Chamados SGSE`, + corpo: `${mensagem}\n\n---\nCentral de Chamados SGSE - Sistema de Gerenciamento de Secretaria`, enviadoPor: usuarioEvento, }); } diff --git a/packages/backend/convex/templatesMensagens.ts b/packages/backend/convex/templatesMensagens.ts index c7b027a..1b92ecf 100644 --- a/packages/backend/convex/templatesMensagens.ts +++ b/packages/backend/convex/templatesMensagens.ts @@ -1,421 +1,421 @@ -import { v } from "convex/values"; -import { mutation, query } from "./_generated/server"; -import { registrarAtividade } from "./logsAtividades"; -import { Doc } from "./_generated/dataModel"; - -/** - * Listar todos os templates - */ -export const listarTemplates = query({ - args: {}, - handler: async (ctx) => { - const templates = await ctx.db.query("templatesMensagens").collect(); - return templates; - }, -}); - -/** - * Obter template por código - */ -export const obterTemplatePorCodigo = query({ - args: { - codigo: v.string(), - }, - handler: async (ctx, args) => { - const template = await ctx.db - .query("templatesMensagens") - .withIndex("by_codigo", (q) => q.eq("codigo", args.codigo)) - .first(); - - return template; - }, -}); - -/** - * Criar template customizado (apenas TI_MASTER) - */ -export const criarTemplate = mutation({ - args: { - codigo: v.string(), - nome: v.string(), - titulo: v.string(), - corpo: v.string(), - variaveis: v.optional(v.array(v.string())), - criadoPorId: v.id("usuarios"), - }, - returns: v.union( - v.object({ sucesso: v.literal(true), templateId: v.id("templatesMensagens") }), - v.object({ sucesso: v.literal(false), erro: v.string() }) - ), - handler: async (ctx, args) => { - // Verificar se código já existe - const existente = await ctx.db - .query("templatesMensagens") - .withIndex("by_codigo", (q) => q.eq("codigo", args.codigo)) - .first(); - - if (existente) { - return { sucesso: false as const, erro: "Código de template já existe" }; - } - - // Criar template - const templateId = await ctx.db.insert("templatesMensagens", { - codigo: args.codigo, - nome: args.nome, - tipo: "customizado", - titulo: args.titulo, - corpo: args.corpo, - variaveis: args.variaveis, - criadoPor: args.criadoPorId, - criadoEm: Date.now(), - }); - - // Log de atividade - await registrarAtividade( - ctx, - args.criadoPorId, - "criar", - "templates", - JSON.stringify({ templateId, codigo: args.codigo, nome: args.nome }), - templateId - ); - - return { sucesso: true as const, templateId }; - }, -}); - -/** - * Editar template customizado (apenas TI_MASTER, não edita templates do sistema) - */ -export const editarTemplate = mutation({ - args: { - templateId: v.id("templatesMensagens"), - nome: v.optional(v.string()), - titulo: v.optional(v.string()), - corpo: v.optional(v.string()), - variaveis: v.optional(v.array(v.string())), - editadoPorId: v.id("usuarios"), - }, - returns: v.union( - v.object({ sucesso: v.literal(true) }), - v.object({ sucesso: v.literal(false), erro: v.string() }) - ), - handler: async (ctx, args) => { - const template = await ctx.db.get(args.templateId); - if (!template) { - return { sucesso: false as const, erro: "Template não encontrado" }; - } - - // Não permite editar templates do sistema - if (template.tipo === "sistema") { - return { sucesso: false as const, erro: "Templates do sistema não podem ser editados" }; - } - - // Atualizar template - const updates: Partial> = {}; - if (args.nome !== undefined) updates.nome = args.nome; - if (args.titulo !== undefined) updates.titulo = args.titulo; - if (args.corpo !== undefined) updates.corpo = args.corpo; - if (args.variaveis !== undefined) updates.variaveis = args.variaveis; - - await ctx.db.patch(args.templateId, updates); - - // Log de atividade - await registrarAtividade( - ctx, - args.editadoPorId, - "editar", - "templates", - JSON.stringify(updates), - args.templateId - ); - - return { sucesso: true as const }; - }, -}); - -/** - * Excluir template customizado (apenas TI_MASTER, não exclui templates do sistema) - */ -export const excluirTemplate = mutation({ - args: { - templateId: v.id("templatesMensagens"), - excluidoPorId: v.id("usuarios"), - }, - returns: v.union( - v.object({ sucesso: v.literal(true) }), - v.object({ sucesso: v.literal(false), erro: v.string() }) - ), - handler: async (ctx, args) => { - const template = await ctx.db.get(args.templateId); - if (!template) { - return { sucesso: false as const, erro: "Template não encontrado" }; - } - - // Não permite excluir templates do sistema - if (template.tipo === "sistema") { - return { sucesso: false as const, erro: "Templates do sistema não podem ser excluídos" }; - } - - // Excluir template - await ctx.db.delete(args.templateId); - - // Log de atividade - await registrarAtividade( - ctx, - args.excluidoPorId, - "excluir", - "templates", - JSON.stringify({ templateId: args.templateId, codigo: template.codigo }), - args.templateId - ); - - return { sucesso: true as const }; - }, -}); - -/** - * Renderizar template com variáveis - */ -export function renderizarTemplate(template: string, variaveis: Record): string { - let resultado = template; - - for (const [chave, valor] of Object.entries(variaveis)) { - const placeholder = `{{${chave}}}`; - resultado = resultado.replace(new RegExp(placeholder, "g"), valor); - } - - return resultado; -} - -/** - * Criar templates padrão do sistema (chamado no seed) - */ -export const criarTemplatesPadrao = mutation({ - args: {}, - handler: async (ctx) => { - const templatesPadrao = [ - { - codigo: "USUARIO_BLOQUEADO", - nome: "Usuário Bloqueado", - titulo: "Sua conta foi bloqueada", - corpo: "Sua conta no SGSE foi bloqueada.\n\nMotivo: {{motivo}}\n\nPara mais informações, entre em contato com a TI.", - variaveis: ["motivo"], - }, - { - codigo: "USUARIO_DESBLOQUEADO", - nome: "Usuário Desbloqueado", - titulo: "Sua conta foi desbloqueada", - corpo: "Sua conta no SGSE foi desbloqueada e você já pode acessar o sistema normalmente.", - variaveis: [], - }, - { - codigo: "SENHA_RESETADA", - nome: "Senha Resetada", - titulo: "Sua senha foi resetada", - corpo: "Sua senha foi resetada pela equipe de TI.\n\nNova senha temporária: {{senha}}\n\nPor favor, altere sua senha no próximo login.", - variaveis: ["senha"], - }, - { - codigo: "PERMISSAO_ALTERADA", - nome: "Permissão Alterada", - titulo: "Suas permissões foram atualizadas", - corpo: "Suas permissões de acesso ao sistema foram atualizadas.\n\nPara verificar suas novas permissões, acesse o menu de perfil.", - variaveis: [], - }, - { - codigo: "AVISO_GERAL", - nome: "Aviso Geral", - titulo: "{{titulo}}", - corpo: "{{mensagem}}", - variaveis: ["titulo", "mensagem"], - }, - { - codigo: "BEM_VINDO", - nome: "Boas-vindas", - titulo: "Bem-vindo ao SGSE", - corpo: "Olá {{nome}},\n\nSeja bem-vindo ao Sistema de Gestão da Secretaria de Esportes!\n\nSuas credenciais de acesso:\nMatrícula: {{matricula}}\nSenha temporária: {{senha}}\n\nPor favor, altere sua senha no primeiro acesso.\n\nEquipe de TI", - variaveis: ["nome", "matricula", "senha"], - }, - { - codigo: "chat_mensagem", - nome: "Nova Mensagem no Chat", - titulo: "Nova mensagem de {{remetente}}", - corpo: "" - + "
" - + "

Nova mensagem no chat

" - + "

{{remetente}} enviou uma nova mensagem:

" - + "
" - + "

{{mensagem}}

" - + "
" - + "

" - + "" - + "Ver conversa" - + "" - + "

" - + "

" - + "Você está recebendo este email porque não estava online quando a mensagem foi enviada. " - + "Você pode desativar essas notificações nas configurações da conversa." - + "

" - + "
", - variaveis: ["remetente", "mensagem", "conversaId", "urlSistema"], - }, - { - codigo: "chat_mencao", - nome: "Menção no Chat", - titulo: "{{remetente}} mencionou você", - corpo: "" - + "
" - + "

Você foi mencionado!

" - + "

{{remetente}} mencionou você em uma mensagem:

" - + "
" - + "

{{mensagem}}

" - + "
" - + "

" - + "" - + "Ver mensagem" - + "" - + "

" - + "

" - + "Você está recebendo este email porque foi mencionado em uma conversa. " - + "Você pode desativar essas notificações nas configurações da conversa." - + "

" - + "
", - variaveis: ["remetente", "mensagem", "conversaId", "urlSistema"], - }, - { - codigo: "chamado_registrado", - nome: "Chamado Registrado", - titulo: "Chamado {{numeroTicket}} registrado", - corpo: "" - + "
" - + "

Chamado registrado com sucesso!

" - + "

Olá {{solicitante}},

" - + "

Recebemos sua solicitação e iniciaremos o atendimento em breve.

" - + "
" - + "

Ticket: {{numeroTicket}}

" - + "

Prioridade: {{prioridade}}

" - + "

Categoria: {{categoria}}

" - + "
" - + "

" - + "" - + "Acompanhar chamado" - + "" - + "

" - + "

" - + "Central de Chamados SGSE" - + "

" - + "
", - variaveis: ["solicitante", "numeroTicket", "prioridade", "categoria", "urlSistema"], - }, - { - codigo: "chamado_atualizado", - nome: "Atualização no Chamado", - titulo: "Atualização no chamado {{numeroTicket}}", - corpo: "" - + "
" - + "

Nova atualização no seu chamado

" - + "

Olá {{solicitante}},

" - + "

Há uma nova atualização no seu chamado:

" - + "
" - + "

Ticket: {{numeroTicket}}

" - + "

Mensagem:

" - + "

{{mensagem}}

" - + "
" - + "

" - + "" - + "Ver detalhes" - + "" - + "

" - + "

" - + "Central de Chamados SGSE" - + "

" - + "
", - variaveis: ["solicitante", "numeroTicket", "mensagem", "urlSistema"], - }, - { - codigo: "chamado_atribuido", - nome: "Chamado Atribuído", - titulo: "Chamado {{numeroTicket}} atribuído", - corpo: "" - + "
" - + "

Chamado atribuído

" - + "

Olá {{responsavel}},

" - + "

Um novo chamado foi atribuído para você:

" - + "
" - + "

Ticket: {{numeroTicket}}

" - + "

Solicitante: {{solicitante}}

" - + "

Prioridade: {{prioridade}}

" - + "

Descrição: {{descricao}}

" - + "
" - + "

" - + "" - + "Acessar chamado" - + "" - + "

" - + "

" - + "Central de Chamados SGSE" - + "

" - + "
", - variaveis: ["responsavel", "numeroTicket", "solicitante", "prioridade", "descricao", "urlSistema"], - }, - { - codigo: "chamado_alerta_prazo", - nome: "Alerta de Prazo do Chamado", - titulo: "⚠️ Alerta de prazo - Chamado {{numeroTicket}}", - corpo: "" - + "
" - + "

⚠️ Alerta de prazo

" - + "

Olá {{destinatario}},

" - + "

O chamado abaixo está próximo do prazo de {{tipoPrazo}}:

" - + "
" - + "

Ticket: {{numeroTicket}}

" - + "

Prazo de {{tipoPrazo}}: {{prazo}}

" - + "

Status: {{status}}

" - + "
" - + "

" - + "" - + "Ver chamado" - + "" - + "

" - + "

" - + "Central de Chamados SGSE" - + "

" - + "
", - variaveis: ["destinatario", "numeroTicket", "tipoPrazo", "prazo", "status", "urlSistema", "rotaAcesso"], - }, - ]; - - for (const template of templatesPadrao) { - // Verificar se já existe - const existente = await ctx.db - .query("templatesMensagens") - .withIndex("by_codigo", (q) => q.eq("codigo", template.codigo)) - .first(); - - if (!existente) { - await ctx.db.insert("templatesMensagens", { - ...template, - tipo: "sistema", - criadoEm: Date.now(), - }); - } - } - - return { sucesso: true }; - }, -}); - - +import { v } from "convex/values"; +import { mutation, query } from "./_generated/server"; +import { registrarAtividade } from "./logsAtividades"; +import { Doc } from "./_generated/dataModel"; + +/** + * Listar todos os templates + */ +export const listarTemplates = query({ + args: {}, + handler: async (ctx) => { + const templates = await ctx.db.query("templatesMensagens").collect(); + return templates; + }, +}); + +/** + * Obter template por código + */ +export const obterTemplatePorCodigo = query({ + args: { + codigo: v.string(), + }, + handler: async (ctx, args) => { + const template = await ctx.db + .query("templatesMensagens") + .withIndex("by_codigo", (q) => q.eq("codigo", args.codigo)) + .first(); + + return template; + }, +}); + +/** + * Criar template customizado (apenas TI_MASTER) + */ +export const criarTemplate = mutation({ + args: { + codigo: v.string(), + nome: v.string(), + titulo: v.string(), + corpo: v.string(), + variaveis: v.optional(v.array(v.string())), + criadoPorId: v.id("usuarios"), + }, + returns: v.union( + v.object({ sucesso: v.literal(true), templateId: v.id("templatesMensagens") }), + v.object({ sucesso: v.literal(false), erro: v.string() }) + ), + handler: async (ctx, args) => { + // Verificar se código já existe + const existente = await ctx.db + .query("templatesMensagens") + .withIndex("by_codigo", (q) => q.eq("codigo", args.codigo)) + .first(); + + if (existente) { + return { sucesso: false as const, erro: "Código de template já existe" }; + } + + // Criar template + const templateId = await ctx.db.insert("templatesMensagens", { + codigo: args.codigo, + nome: args.nome, + tipo: "customizado", + titulo: args.titulo, + corpo: args.corpo, + variaveis: args.variaveis, + criadoPor: args.criadoPorId, + criadoEm: Date.now(), + }); + + // Log de atividade + await registrarAtividade( + ctx, + args.criadoPorId, + "criar", + "templates", + JSON.stringify({ templateId, codigo: args.codigo, nome: args.nome }), + templateId + ); + + return { sucesso: true as const, templateId }; + }, +}); + +/** + * Editar template customizado (apenas TI_MASTER, não edita templates do sistema) + */ +export const editarTemplate = mutation({ + args: { + templateId: v.id("templatesMensagens"), + nome: v.optional(v.string()), + titulo: v.optional(v.string()), + corpo: v.optional(v.string()), + variaveis: v.optional(v.array(v.string())), + editadoPorId: v.id("usuarios"), + }, + returns: v.union( + v.object({ sucesso: v.literal(true) }), + v.object({ sucesso: v.literal(false), erro: v.string() }) + ), + handler: async (ctx, args) => { + const template = await ctx.db.get(args.templateId); + if (!template) { + return { sucesso: false as const, erro: "Template não encontrado" }; + } + + // Não permite editar templates do sistema + if (template.tipo === "sistema") { + return { sucesso: false as const, erro: "Templates do sistema não podem ser editados" }; + } + + // Atualizar template + const updates: Partial> = {}; + if (args.nome !== undefined) updates.nome = args.nome; + if (args.titulo !== undefined) updates.titulo = args.titulo; + if (args.corpo !== undefined) updates.corpo = args.corpo; + if (args.variaveis !== undefined) updates.variaveis = args.variaveis; + + await ctx.db.patch(args.templateId, updates); + + // Log de atividade + await registrarAtividade( + ctx, + args.editadoPorId, + "editar", + "templates", + JSON.stringify(updates), + args.templateId + ); + + return { sucesso: true as const }; + }, +}); + +/** + * Excluir template customizado (apenas TI_MASTER, não exclui templates do sistema) + */ +export const excluirTemplate = mutation({ + args: { + templateId: v.id("templatesMensagens"), + excluidoPorId: v.id("usuarios"), + }, + returns: v.union( + v.object({ sucesso: v.literal(true) }), + v.object({ sucesso: v.literal(false), erro: v.string() }) + ), + handler: async (ctx, args) => { + const template = await ctx.db.get(args.templateId); + if (!template) { + return { sucesso: false as const, erro: "Template não encontrado" }; + } + + // Não permite excluir templates do sistema + if (template.tipo === "sistema") { + return { sucesso: false as const, erro: "Templates do sistema não podem ser excluídos" }; + } + + // Excluir template + await ctx.db.delete(args.templateId); + + // Log de atividade + await registrarAtividade( + ctx, + args.excluidoPorId, + "excluir", + "templates", + JSON.stringify({ templateId: args.templateId, codigo: template.codigo }), + args.templateId + ); + + return { sucesso: true as const }; + }, +}); + +/** + * Renderizar template com variáveis + */ +export function renderizarTemplate(template: string, variaveis: Record): string { + let resultado = template; + + for (const [chave, valor] of Object.entries(variaveis)) { + const placeholder = `{{${chave}}}`; + resultado = resultado.replace(new RegExp(placeholder, "g"), valor); + } + + return resultado; +} + +/** + * Criar templates padrão do sistema (chamado no seed) + */ +export const criarTemplatesPadrao = mutation({ + args: {}, + handler: async (ctx) => { + const templatesPadrao = [ + { + codigo: "USUARIO_BLOQUEADO", + nome: "Usuário Bloqueado", + titulo: "Sua conta foi bloqueada", + corpo: "Sua conta no SGSE foi bloqueada.\n\nMotivo: {{motivo}}\n\nPara mais informações, entre em contato com a TI.", + variaveis: ["motivo"], + }, + { + codigo: "USUARIO_DESBLOQUEADO", + nome: "Usuário Desbloqueado", + titulo: "Sua conta foi desbloqueada", + corpo: "Sua conta no SGSE foi desbloqueada e você já pode acessar o sistema normalmente.", + variaveis: [], + }, + { + codigo: "SENHA_RESETADA", + nome: "Senha Resetada", + titulo: "Sua senha foi resetada", + corpo: "Sua senha foi resetada pela equipe de TI.\n\nNova senha temporária: {{senha}}\n\nPor favor, altere sua senha no próximo login.", + variaveis: ["senha"], + }, + { + codigo: "PERMISSAO_ALTERADA", + nome: "Permissão Alterada", + titulo: "Suas permissões foram atualizadas", + corpo: "Suas permissões de acesso ao sistema foram atualizadas.\n\nPara verificar suas novas permissões, acesse o menu de perfil.", + variaveis: [], + }, + { + codigo: "AVISO_GERAL", + nome: "Aviso Geral", + titulo: "{{titulo}}", + corpo: "{{mensagem}}", + variaveis: ["titulo", "mensagem"], + }, + { + codigo: "BEM_VINDO", + nome: "Boas-vindas", + titulo: "Bem-vindo ao SGSE", + corpo: "Olá {{nome}},\n\nSeja bem-vindo ao SGSE - Sistema de Gerenciamento de Secretaria!\n\nSuas credenciais de acesso:\nMatrícula: {{matricula}}\nSenha temporária: {{senha}}\n\nPor favor, altere sua senha no primeiro acesso.\n\nEquipe de TI", + variaveis: ["nome", "matricula", "senha"], + }, + { + codigo: "chat_mensagem", + nome: "Nova Mensagem no Chat", + titulo: "Nova mensagem de {{remetente}}", + corpo: "" + + "
" + + "

Nova mensagem no chat

" + + "

{{remetente}} enviou uma nova mensagem:

" + + "
" + + "

{{mensagem}}

" + + "
" + + "

" + + "" + + "Ver conversa" + + "" + + "

" + + "

" + + "Você está recebendo este email porque não estava online quando a mensagem foi enviada. " + + "Você pode desativar essas notificações nas configurações da conversa." + + "

" + + "
", + variaveis: ["remetente", "mensagem", "conversaId", "urlSistema"], + }, + { + codigo: "chat_mencao", + nome: "Menção no Chat", + titulo: "{{remetente}} mencionou você", + corpo: "" + + "
" + + "

Você foi mencionado!

" + + "

{{remetente}} mencionou você em uma mensagem:

" + + "
" + + "

{{mensagem}}

" + + "
" + + "

" + + "" + + "Ver mensagem" + + "" + + "

" + + "

" + + "Você está recebendo este email porque foi mencionado em uma conversa. " + + "Você pode desativar essas notificações nas configurações da conversa." + + "

" + + "
", + variaveis: ["remetente", "mensagem", "conversaId", "urlSistema"], + }, + { + codigo: "chamado_registrado", + nome: "Chamado Registrado", + titulo: "Chamado {{numeroTicket}} registrado", + corpo: "" + + "
" + + "

Chamado registrado com sucesso!

" + + "

Olá {{solicitante}},

" + + "

Recebemos sua solicitação e iniciaremos o atendimento em breve.

" + + "
" + + "

Ticket: {{numeroTicket}}

" + + "

Prioridade: {{prioridade}}

" + + "

Categoria: {{categoria}}

" + + "
" + + "

" + + "" + + "Acompanhar chamado" + + "" + + "

" + + "

" + + "Central de Chamados SGSE - Sistema de Gerenciamento de Secretaria" + + "

" + + "
", + variaveis: ["solicitante", "numeroTicket", "prioridade", "categoria", "urlSistema"], + }, + { + codigo: "chamado_atualizado", + nome: "Atualização no Chamado", + titulo: "Atualização no chamado {{numeroTicket}}", + corpo: "" + + "
" + + "

Nova atualização no seu chamado

" + + "

Olá {{solicitante}},

" + + "

Há uma nova atualização no seu chamado:

" + + "
" + + "

Ticket: {{numeroTicket}}

" + + "

Mensagem:

" + + "

{{mensagem}}

" + + "
" + + "

" + + "" + + "Ver detalhes" + + "" + + "

" + + "

" + + "Central de Chamados SGSE - Sistema de Gerenciamento de Secretaria" + + "

" + + "
", + variaveis: ["solicitante", "numeroTicket", "mensagem", "urlSistema"], + }, + { + codigo: "chamado_atribuido", + nome: "Chamado Atribuído", + titulo: "Chamado {{numeroTicket}} atribuído", + corpo: "" + + "
" + + "

Chamado atribuído

" + + "

Olá {{responsavel}},

" + + "

Um novo chamado foi atribuído para você:

" + + "
" + + "

Ticket: {{numeroTicket}}

" + + "

Solicitante: {{solicitante}}

" + + "

Prioridade: {{prioridade}}

" + + "

Descrição: {{descricao}}

" + + "
" + + "

" + + "" + + "Acessar chamado" + + "" + + "

" + + "

" + + "Central de Chamados SGSE - Sistema de Gerenciamento de Secretaria" + + "

" + + "
", + variaveis: ["responsavel", "numeroTicket", "solicitante", "prioridade", "descricao", "urlSistema"], + }, + { + codigo: "chamado_alerta_prazo", + nome: "Alerta de Prazo do Chamado", + titulo: "⚠️ Alerta de prazo - Chamado {{numeroTicket}}", + corpo: "" + + "
" + + "

⚠️ Alerta de prazo

" + + "

Olá {{destinatario}},

" + + "

O chamado abaixo está próximo do prazo de {{tipoPrazo}}:

" + + "
" + + "

Ticket: {{numeroTicket}}

" + + "

Prazo de {{tipoPrazo}}: {{prazo}}

" + + "

Status: {{status}}

" + + "
" + + "

" + + "" + + "Ver chamado" + + "" + + "

" + + "

" + + "Central de Chamados SGSE - Sistema de Gerenciamento de Secretaria" + + "

" + + "
", + variaveis: ["destinatario", "numeroTicket", "tipoPrazo", "prazo", "status", "urlSistema", "rotaAcesso"], + }, + ]; + + for (const template of templatesPadrao) { + // Verificar se já existe + const existente = await ctx.db + .query("templatesMensagens") + .withIndex("by_codigo", (q) => q.eq("codigo", template.codigo)) + .first(); + + if (!existente) { + await ctx.db.insert("templatesMensagens", { + ...template, + tipo: "sistema", + criadoEm: Date.now(), + }); + } + } + + return { sucesso: true }; + }, +}); + + From 3420872a3755dfde99898f3389c66bb132ba3c63 Mon Sep 17 00:00:00 2001 From: deyvisonwanderley Date: Tue, 18 Nov 2025 03:31:54 -0300 Subject: [PATCH 05/10] refactor: enhance Sidebar and dashboard components for improved UI and functionality - Updated the Sidebar component to change the support link and improve modal styling for better user experience. - Refined the dashboard page by optimizing data handling for real-time monitoring, ensuring fallback values for activity and distribution data. - Improved progress bar calculations to prevent division by zero errors, enhancing stability and user feedback. - Adjusted layout and styling for consistency and better visual appeal across components. --- apps/web/src/lib/components/Sidebar.svelte | 60 ++++++++++---------- apps/web/src/routes/(dashboard)/+page.svelte | 35 ++++++------ 2 files changed, 46 insertions(+), 49 deletions(-) diff --git a/apps/web/src/lib/components/Sidebar.svelte b/apps/web/src/lib/components/Sidebar.svelte index 05cc6b7..b9cc92b 100644 --- a/apps/web/src/lib/components/Sidebar.svelte +++ b/apps/web/src/lib/components/Sidebar.svelte @@ -325,7 +325,7 @@ >Contato - Suporte @@ -500,77 +500,77 @@ {#if showAboutModal}