feat: implement error handling and logging in server hooks to capture and notify on 404 and 500 errors, enhancing server reliability and monitoring
This commit is contained in:
2
packages/backend/convex/_generated/api.d.ts
vendored
2
packages/backend/convex/_generated/api.d.ts
vendored
@@ -35,6 +35,7 @@ import type * as documentos from "../documentos.js";
|
||||
import type * as email from "../email.js";
|
||||
import type * as empresas from "../empresas.js";
|
||||
import type * as enderecosMarcacao from "../enderecosMarcacao.js";
|
||||
import type * as errosServidor from "../errosServidor.js";
|
||||
import type * as ferias from "../ferias.js";
|
||||
import type * as flows from "../flows.js";
|
||||
import type * as funcionarioEnderecos from "../funcionarioEnderecos.js";
|
||||
@@ -123,6 +124,7 @@ declare const fullApi: ApiFromModules<{
|
||||
email: typeof email;
|
||||
empresas: typeof empresas;
|
||||
enderecosMarcacao: typeof enderecosMarcacao;
|
||||
errosServidor: typeof errosServidor;
|
||||
ferias: typeof ferias;
|
||||
flows: typeof flows;
|
||||
funcionarioEnderecos: typeof funcionarioEnderecos;
|
||||
|
||||
@@ -59,6 +59,85 @@ async function recalcularBancoHorasPeriodo(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper para verificar se um funcionário tem licença ou atestado ativo
|
||||
* Retorna true se há algum registro ativo (data atual entre dataInicio e dataFim)
|
||||
*/
|
||||
export async function verificarLicencaAtiva(
|
||||
ctx: QueryCtx | MutationCtx,
|
||||
funcionarioId: Id<'funcionarios'>,
|
||||
dataAtual?: Date
|
||||
): Promise<boolean> {
|
||||
// Normalizar data atual para comparar apenas a parte da data (sem hora)
|
||||
const hoje = dataAtual || new Date();
|
||||
const hojeStr = hoje.toISOString().split('T')[0]; // Formato: "YYYY-MM-DD"
|
||||
|
||||
console.log(
|
||||
`[verificarLicencaAtiva] Verificando funcionário ${funcionarioId}, data atual: ${hojeStr}`
|
||||
);
|
||||
|
||||
// Buscar atestados e licenças do funcionário
|
||||
const [atestados, licencas] = await Promise.all([
|
||||
ctx.db
|
||||
.query('atestados')
|
||||
.withIndex('by_funcionario', (q) => q.eq('funcionarioId', funcionarioId))
|
||||
.collect(),
|
||||
ctx.db
|
||||
.query('licencas')
|
||||
.withIndex('by_funcionario', (q) => q.eq('funcionarioId', funcionarioId))
|
||||
.collect()
|
||||
]);
|
||||
|
||||
console.log(
|
||||
`[verificarLicencaAtiva] Encontrados ${atestados.length} atestados e ${licencas.length} licenças`
|
||||
);
|
||||
|
||||
// Verificar se há algum atestado ativo
|
||||
for (const atestado of atestados) {
|
||||
// Normalizar datas para formato "YYYY-MM-DD" (pode vir como "YYYY-MM-DD" ou "YYYY-MM-DDTHH:mm:ss")
|
||||
const inicioStr = atestado.dataInicio.includes('T')
|
||||
? atestado.dataInicio.split('T')[0]
|
||||
: atestado.dataInicio.substring(0, 10);
|
||||
const fimStr = atestado.dataFim.includes('T')
|
||||
? atestado.dataFim.split('T')[0]
|
||||
: atestado.dataFim.substring(0, 10);
|
||||
|
||||
console.log(
|
||||
`[verificarLicencaAtiva] Atestado: ${inicioStr} a ${fimStr}, hoje: ${hojeStr}, ativo: ${hojeStr >= inicioStr && hojeStr <= fimStr}`
|
||||
);
|
||||
|
||||
// Comparar strings de data diretamente (formato ISO permite comparação lexicográfica)
|
||||
if (hojeStr >= inicioStr && hojeStr <= fimStr) {
|
||||
console.log(`[verificarLicencaAtiva] ✅ Atestado ativo encontrado!`);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Verificar se há alguma licença ativa
|
||||
for (const licenca of licencas) {
|
||||
// Normalizar datas para formato "YYYY-MM-DD" (pode vir como "YYYY-MM-DD" ou "YYYY-MM-DDTHH:mm:ss")
|
||||
const inicioStr = licenca.dataInicio.includes('T')
|
||||
? licenca.dataInicio.split('T')[0]
|
||||
: licenca.dataInicio.substring(0, 10);
|
||||
const fimStr = licenca.dataFim.includes('T')
|
||||
? licenca.dataFim.split('T')[0]
|
||||
: licenca.dataFim.substring(0, 10);
|
||||
|
||||
console.log(
|
||||
`[verificarLicencaAtiva] Licença: ${inicioStr} a ${fimStr}, hoje: ${hojeStr}, ativa: ${hojeStr >= inicioStr && hojeStr <= fimStr}`
|
||||
);
|
||||
|
||||
// Comparar strings de data diretamente (formato ISO permite comparação lexicográfica)
|
||||
if (hojeStr >= inicioStr && hojeStr <= fimStr) {
|
||||
console.log(`[verificarLicencaAtiva] ✅ Licença ativa encontrada!`);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[verificarLicencaAtiva] ❌ Nenhuma licença/atestado ativo encontrado`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// ========== QUERIES ==========
|
||||
|
||||
/**
|
||||
@@ -238,6 +317,19 @@ export const listarPorPeriodo = query({
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Verificar se o funcionário atual tem licença/atestado ativo
|
||||
*/
|
||||
export const verificarStatusLicenca = query({
|
||||
args: {
|
||||
funcionarioId: v.id('funcionarios')
|
||||
},
|
||||
returns: v.boolean(),
|
||||
handler: async (ctx, args) => {
|
||||
return await verificarLicencaAtiva(ctx, args.funcionarioId);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Obter dados para gráficos
|
||||
*/
|
||||
@@ -816,6 +908,15 @@ export const criarAtestadoMedico = mutation({
|
||||
// Recalcular banco de horas para todas as datas do período do atestado
|
||||
await recalcularBancoHorasPeriodo(ctx, args.funcionarioId, args.dataInicio, args.dataFim);
|
||||
|
||||
// Atualizar status do funcionário imediatamente
|
||||
console.log(
|
||||
`[criarAtestadoMedico] Atualizando status do funcionário ${args.funcionarioId} após criar atestado`
|
||||
);
|
||||
await ctx.runMutation(internal.ferias.atualizarStatusFuncionario, {
|
||||
funcionarioId: args.funcionarioId
|
||||
});
|
||||
console.log(`[criarAtestadoMedico] Status atualizado com sucesso`);
|
||||
|
||||
return atestadoId;
|
||||
}
|
||||
});
|
||||
@@ -864,6 +965,11 @@ export const criarDeclaracaoComparecimento = mutation({
|
||||
// Recalcular banco de horas para todas as datas do período da declaração
|
||||
await recalcularBancoHorasPeriodo(ctx, args.funcionarioId, args.dataInicio, args.dataFim);
|
||||
|
||||
// Atualizar status do funcionário imediatamente
|
||||
await ctx.runMutation(internal.ferias.atualizarStatusFuncionario, {
|
||||
funcionarioId: args.funcionarioId
|
||||
});
|
||||
|
||||
return atestadoId;
|
||||
}
|
||||
});
|
||||
@@ -920,6 +1026,11 @@ export const criarLicencaMaternidade = mutation({
|
||||
// Recalcular banco de horas para todas as datas do período da licença
|
||||
await recalcularBancoHorasPeriodo(ctx, args.funcionarioId, args.dataInicio, args.dataFim);
|
||||
|
||||
// Atualizar status do funcionário imediatamente
|
||||
await ctx.runMutation(internal.ferias.atualizarStatusFuncionario, {
|
||||
funcionarioId: args.funcionarioId
|
||||
});
|
||||
|
||||
return licencaId;
|
||||
}
|
||||
});
|
||||
@@ -969,6 +1080,11 @@ export const criarLicencaPaternidade = mutation({
|
||||
// Recalcular banco de horas para todas as datas do período da licença
|
||||
await recalcularBancoHorasPeriodo(ctx, args.funcionarioId, args.dataInicio, args.dataFim);
|
||||
|
||||
// Atualizar status do funcionário imediatamente
|
||||
await ctx.runMutation(internal.ferias.atualizarStatusFuncionario, {
|
||||
funcionarioId: args.funcionarioId
|
||||
});
|
||||
|
||||
return licencaId;
|
||||
}
|
||||
});
|
||||
@@ -1025,6 +1141,11 @@ export const prorrogarLicencaMaternidade = mutation({
|
||||
prorrogacaoId
|
||||
);
|
||||
|
||||
// Atualizar status do funcionário imediatamente
|
||||
await ctx.runMutation(internal.ferias.atualizarStatusFuncionario, {
|
||||
funcionarioId: licencaOriginal.funcionarioId
|
||||
});
|
||||
|
||||
return prorrogacaoId;
|
||||
}
|
||||
});
|
||||
@@ -1055,6 +1176,11 @@ export const excluirAtestado = mutation({
|
||||
args.id
|
||||
);
|
||||
|
||||
// Atualizar status do funcionário imediatamente
|
||||
await ctx.runMutation(internal.ferias.atualizarStatusFuncionario, {
|
||||
funcionarioId: atestado.funcionarioId
|
||||
});
|
||||
|
||||
return null;
|
||||
}
|
||||
});
|
||||
@@ -1085,6 +1211,11 @@ export const excluirLicenca = mutation({
|
||||
args.id
|
||||
);
|
||||
|
||||
// Atualizar status do funcionário imediatamente
|
||||
await ctx.runMutation(internal.ferias.atualizarStatusFuncionario, {
|
||||
funcionarioId: licenca.funcionarioId
|
||||
});
|
||||
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
424
packages/backend/convex/errosServidor.ts
Normal file
424
packages/backend/convex/errosServidor.ts
Normal file
@@ -0,0 +1,424 @@
|
||||
import { action, internalMutation, internalAction, internalQuery, query } from './_generated/server';
|
||||
import { v } from 'convex/values';
|
||||
import { api, internal } from './_generated/api';
|
||||
import type { Id } from './_generated/dataModel';
|
||||
import { getCurrentUserFunction } from './auth';
|
||||
|
||||
/**
|
||||
* Action pública para registrar erro do servidor e notificar equipe técnica
|
||||
* Esta função será chamada pelo handleError do SvelteKit
|
||||
*/
|
||||
export const registrarErroServidor = action({
|
||||
args: {
|
||||
statusCode: v.number(),
|
||||
mensagem: v.string(),
|
||||
stack: v.optional(v.string()),
|
||||
url: v.optional(v.string()),
|
||||
method: v.optional(v.string()),
|
||||
ipAddress: v.optional(v.string()),
|
||||
userAgent: v.optional(v.string()),
|
||||
usuarioId: v.optional(v.id('usuarios'))
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
// Registrar erro no banco
|
||||
const erroId = await ctx.runMutation(internal.errosServidor.inserirErro, {
|
||||
statusCode: args.statusCode,
|
||||
mensagem: args.mensagem,
|
||||
stack: args.stack,
|
||||
url: args.url,
|
||||
method: args.method,
|
||||
ipAddress: args.ipAddress,
|
||||
userAgent: args.userAgent,
|
||||
usuarioId: args.usuarioId
|
||||
});
|
||||
|
||||
// Notificar equipe técnica (assíncrono)
|
||||
ctx.scheduler
|
||||
.runAfter(0, internal.errosServidor.notificarEquipeTecnica, {
|
||||
erroId
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Erro ao agendar notificação de erro:', error);
|
||||
});
|
||||
|
||||
return { sucesso: true, erroId };
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Mutation interna para inserir erro no banco
|
||||
*/
|
||||
export const inserirErro = internalMutation({
|
||||
args: {
|
||||
statusCode: v.number(),
|
||||
mensagem: v.string(),
|
||||
stack: v.optional(v.string()),
|
||||
url: v.optional(v.string()),
|
||||
method: v.optional(v.string()),
|
||||
ipAddress: v.optional(v.string()),
|
||||
userAgent: v.optional(v.string()),
|
||||
usuarioId: v.optional(v.id('usuarios'))
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const erroId = await ctx.db.insert('errosServidor', {
|
||||
statusCode: args.statusCode,
|
||||
mensagem: args.mensagem,
|
||||
stack: args.stack,
|
||||
url: args.url,
|
||||
method: args.method,
|
||||
ipAddress: args.ipAddress,
|
||||
userAgent: args.userAgent,
|
||||
usuarioId: args.usuarioId,
|
||||
notificado: false,
|
||||
criadoEm: Date.now()
|
||||
});
|
||||
|
||||
return erroId;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Action interna para notificar equipe técnica sobre erro do servidor
|
||||
*/
|
||||
export const notificarEquipeTecnica = internalAction({
|
||||
args: {
|
||||
erroId: v.id('errosServidor')
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
// Buscar detalhes do erro
|
||||
const erro = await ctx.runQuery(internal.errosServidor.obterErroPorId, {
|
||||
erroId: args.erroId
|
||||
});
|
||||
|
||||
if (!erro) {
|
||||
console.error('Erro não encontrado:', args.erroId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Buscar usuários da equipe técnica (roles com nível <= 1)
|
||||
const rolesAdminOuTi = await ctx.runQuery(internal.errosServidor.obterRolesTI, {});
|
||||
|
||||
if (rolesAdminOuTi.length === 0) {
|
||||
console.warn('Nenhuma role de TI encontrada para notificação de erro');
|
||||
return;
|
||||
}
|
||||
|
||||
const rolesPermitidas = new Set(rolesAdminOuTi.map((r) => r._id));
|
||||
const usuarios = await ctx.runQuery(internal.errosServidor.obterUsuariosTI, {
|
||||
rolesPermitidas: Array.from(rolesPermitidas)
|
||||
});
|
||||
|
||||
if (usuarios.length === 0) {
|
||||
console.warn('Nenhum usuário de TI encontrado para notificação de erro');
|
||||
return;
|
||||
}
|
||||
|
||||
// Preparar informações do erro para notificação
|
||||
const urlFormatada = erro.url || 'N/A';
|
||||
const metodoFormatado = erro.method || 'N/A';
|
||||
const stackFormatado = erro.stack
|
||||
? erro.stack.substring(0, 500) + (erro.stack.length > 500 ? '...' : '')
|
||||
: 'N/A';
|
||||
|
||||
// Notificar via chat (notificações internas)
|
||||
for (const usuario of usuarios) {
|
||||
await ctx.runMutation(internal.errosServidor.criarNotificacaoChat, {
|
||||
usuarioId: usuario._id,
|
||||
statusCode: erro.statusCode,
|
||||
mensagem: erro.mensagem,
|
||||
url: urlFormatada,
|
||||
method: metodoFormatado
|
||||
});
|
||||
}
|
||||
|
||||
// Notificar via email (apenas para usuários com email)
|
||||
const usuariosComEmail = usuarios.filter((u) => u.email);
|
||||
|
||||
for (const usuario of usuariosComEmail) {
|
||||
try {
|
||||
// Determinar código do template baseado no status code
|
||||
const templateCodigo =
|
||||
erro.statusCode === 404 ? 'ERRO_SERVIDOR_404' : 'ERRO_SERVIDOR_500';
|
||||
|
||||
// Verificar se existe template de erro do servidor
|
||||
const templateExiste = await ctx.runQuery(
|
||||
api.templatesMensagens.obterTemplatePorCodigo,
|
||||
{
|
||||
codigo: templateCodigo
|
||||
}
|
||||
);
|
||||
|
||||
if (templateExiste) {
|
||||
// Usar template personalizado
|
||||
await ctx.scheduler.runAfter(0, api.email.enviarEmailComTemplate, {
|
||||
destinatario: usuario.email!,
|
||||
destinatarioId: usuario._id,
|
||||
templateCodigo,
|
||||
variaveis: {
|
||||
destinatarioNome: usuario.nome,
|
||||
statusCode: erro.statusCode.toString(),
|
||||
mensagem: erro.mensagem,
|
||||
url: urlFormatada,
|
||||
method: metodoFormatado,
|
||||
stack: stackFormatado,
|
||||
timestamp: new Date(erro.criadoEm).toLocaleString('pt-BR')
|
||||
},
|
||||
enviadoPor: usuario._id // Usar o próprio usuário como remetente
|
||||
});
|
||||
} else {
|
||||
// Template não existe, criar email simples com HTML básico
|
||||
const assunto =
|
||||
erro.statusCode === 404
|
||||
? `⚠️ Erro 404 - Página não encontrada: ${urlFormatada.substring(0, 50)}`
|
||||
: `🚨 Erro ${erro.statusCode} no Servidor - ${urlFormatada.substring(0, 50)}`;
|
||||
const corpo = `<html><body>
|
||||
<h2>${erro.statusCode === 404 ? 'Página Não Encontrada (404)' : 'Erro do Servidor Detectado'}</h2>
|
||||
<p><strong>Código:</strong> ${erro.statusCode}</p>
|
||||
<p><strong>Mensagem:</strong> ${erro.mensagem}</p>
|
||||
<p><strong>URL:</strong> ${urlFormatada}</p>
|
||||
<p><strong>Método:</strong> ${metodoFormatado}</p>
|
||||
<p><strong>Data/Hora:</strong> ${new Date(erro.criadoEm).toLocaleString('pt-BR')}</p>
|
||||
${erro.stack && erro.statusCode !== 404 ? `<p><strong>Stack Trace:</strong><br><pre style="white-space: pre-wrap; word-wrap: break-word;">${stackFormatado}</pre></p>` : ''}
|
||||
</body></html>`;
|
||||
|
||||
await ctx.runMutation(api.email.enfileirarEmail, {
|
||||
destinatario: usuario.email!,
|
||||
destinatarioId: usuario._id,
|
||||
assunto,
|
||||
corpo,
|
||||
enviadoPor: usuario._id
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Erro ao enviar email de notificação para ${usuario.email}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// Marcar erro como notificado
|
||||
await ctx.runMutation(internal.errosServidor.marcarComoNotificado, {
|
||||
erroId: args.erroId
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Query interna para obter erro por ID
|
||||
*/
|
||||
export const obterErroPorId = internalQuery({
|
||||
args: {
|
||||
erroId: v.id('errosServidor')
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
return await ctx.db.get(args.erroId);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Query interna para obter roles de TI
|
||||
*/
|
||||
export const obterRolesTI = internalQuery({
|
||||
args: {},
|
||||
handler: async (ctx) => {
|
||||
return await ctx.db
|
||||
.query('roles')
|
||||
.filter((q) => q.lte(q.field('nivel'), 1))
|
||||
.collect();
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Query interna para obter usuários de TI
|
||||
*/
|
||||
export const obterUsuariosTI = internalQuery({
|
||||
args: {
|
||||
rolesPermitidas: v.array(v.id('roles'))
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const usuarios = await ctx.db.query('usuarios').collect();
|
||||
return usuarios.filter((u) => args.rolesPermitidas.includes(u.roleId));
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Mutation interna para criar notificação no chat
|
||||
*/
|
||||
export const criarNotificacaoChat = internalMutation({
|
||||
args: {
|
||||
usuarioId: v.id('usuarios'),
|
||||
statusCode: v.number(),
|
||||
mensagem: v.string(),
|
||||
url: v.string(),
|
||||
method: v.string()
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const tituloNotificacao =
|
||||
args.statusCode === 404
|
||||
? `⚠️ Erro 404 - Página não encontrada`
|
||||
: `🚨 Erro ${args.statusCode} no Servidor`;
|
||||
const descricaoNotificacao =
|
||||
args.statusCode === 404
|
||||
? `Página não encontrada: ${args.url} (${args.method})`
|
||||
: `Erro detectado em ${args.url} (${args.method}): ${args.mensagem.substring(0, 100)}`;
|
||||
|
||||
await ctx.db.insert('notificacoes', {
|
||||
usuarioId: args.usuarioId,
|
||||
tipo: 'nova_mensagem',
|
||||
titulo: tituloNotificacao,
|
||||
descricao: descricaoNotificacao,
|
||||
lida: false,
|
||||
criadaEm: Date.now()
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Mutation interna para marcar erro como notificado
|
||||
*/
|
||||
export const marcarComoNotificado = internalMutation({
|
||||
args: {
|
||||
erroId: v.id('errosServidor')
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
await ctx.db.patch(args.erroId, {
|
||||
notificado: true,
|
||||
notificadoEm: Date.now()
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Query pública para listar erros do servidor (apenas para TI)
|
||||
*/
|
||||
export const listarErros = query({
|
||||
args: {
|
||||
limite: v.optional(v.number()),
|
||||
statusCode: v.optional(v.number()),
|
||||
notificado: v.optional(v.boolean()),
|
||||
dataInicio: v.optional(v.number()),
|
||||
dataFim: v.optional(v.number())
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
// Verificar se usuário tem permissão (nível <= 1)
|
||||
const usuario = await getCurrentUserFunction(ctx);
|
||||
if (!usuario) {
|
||||
throw new Error('Não autenticado');
|
||||
}
|
||||
|
||||
const role = await ctx.db.get(usuario.roleId);
|
||||
if (!role || role.nivel > 1) {
|
||||
throw new Error('Acesso negado. Apenas usuários de TI podem visualizar erros do servidor.');
|
||||
}
|
||||
|
||||
// Construir query com filtros
|
||||
let erros;
|
||||
if (args.statusCode !== undefined) {
|
||||
erros = await ctx.db
|
||||
.query('errosServidor')
|
||||
.withIndex('by_status_code', (q) => q.eq('statusCode', args.statusCode!))
|
||||
.collect();
|
||||
} else {
|
||||
erros = await ctx.db
|
||||
.query('errosServidor')
|
||||
.withIndex('by_criado_em')
|
||||
.collect();
|
||||
}
|
||||
|
||||
// Aplicar filtros adicionais que não são índices
|
||||
if (args.notificado !== undefined) {
|
||||
erros = erros.filter((e) => e.notificado === args.notificado);
|
||||
}
|
||||
|
||||
if (args.dataInicio !== undefined) {
|
||||
erros = erros.filter((e) => e.criadoEm >= args.dataInicio!);
|
||||
}
|
||||
|
||||
if (args.dataFim !== undefined) {
|
||||
erros = erros.filter((e) => e.criadoEm <= args.dataFim!);
|
||||
}
|
||||
|
||||
// Ordenar por data (mais recentes primeiro)
|
||||
erros.sort((a, b) => b.criadoEm - a.criadoEm);
|
||||
|
||||
// Aplicar limite
|
||||
const limite = args.limite || 100;
|
||||
erros = erros.slice(0, limite);
|
||||
|
||||
// Buscar informações do usuário se houver usuarioId
|
||||
const errosComUsuario = await Promise.all(
|
||||
erros.map(async (erro) => {
|
||||
let usuarioNome = null;
|
||||
if (erro.usuarioId) {
|
||||
const usuarioErro = await ctx.db.get(erro.usuarioId);
|
||||
usuarioNome = usuarioErro?.nome || null;
|
||||
}
|
||||
return {
|
||||
...erro,
|
||||
usuarioNome
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return errosComUsuario;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Query pública para obter estatísticas de erros
|
||||
*/
|
||||
export const obterEstatisticasErros = query({
|
||||
args: {
|
||||
dataInicio: v.optional(v.number()),
|
||||
dataFim: v.optional(v.number())
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
// Verificar se usuário tem permissão (nível <= 1)
|
||||
const usuario = await getCurrentUserFunction(ctx);
|
||||
if (!usuario) {
|
||||
throw new Error('Não autenticado');
|
||||
}
|
||||
|
||||
const role = await ctx.db.get(usuario.roleId);
|
||||
if (!role || role.nivel > 1) {
|
||||
throw new Error('Acesso negado. Apenas usuários de TI podem visualizar estatísticas de erros.');
|
||||
}
|
||||
|
||||
// Buscar todos os erros no período
|
||||
let erros = await ctx.db.query('errosServidor').withIndex('by_criado_em').collect();
|
||||
|
||||
// Aplicar filtros de data
|
||||
if (args.dataInicio !== undefined) {
|
||||
erros = erros.filter((e) => e.criadoEm >= args.dataInicio!);
|
||||
}
|
||||
|
||||
if (args.dataFim !== undefined) {
|
||||
erros = erros.filter((e) => e.criadoEm <= args.dataFim!);
|
||||
}
|
||||
|
||||
// Calcular estatísticas
|
||||
const total = erros.length;
|
||||
const porStatus = erros.reduce(
|
||||
(acc, erro) => {
|
||||
const status = erro.statusCode.toString();
|
||||
acc[status] = (acc[status] || 0) + 1;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, number>
|
||||
);
|
||||
|
||||
const notificados = erros.filter((e) => e.notificado).length;
|
||||
const naoNotificados = total - notificados;
|
||||
|
||||
// Erros mais recentes (últimas 24 horas)
|
||||
const agora = Date.now();
|
||||
const ultimas24h = erros.filter((e) => agora - e.criadoEm <= 24 * 60 * 60 * 1000).length;
|
||||
|
||||
return {
|
||||
total,
|
||||
porStatus,
|
||||
notificados,
|
||||
naoNotificados,
|
||||
ultimas24h
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2,6 +2,8 @@ import { v } from 'convex/values';
|
||||
import { mutation, query, internalMutation } from './_generated/server';
|
||||
import { internal } from './_generated/api';
|
||||
import { Id, Doc } from './_generated/dataModel';
|
||||
import { verificarLicencaAtiva } from './atestadosLicencas';
|
||||
import { getCurrentUserFunction } from './auth';
|
||||
|
||||
// Validador para períodos
|
||||
const periodoValidator = v.object({
|
||||
@@ -46,7 +48,7 @@ function agruparPorSolicitacao(registros: Array<Doc<'ferias'>>): Array<{
|
||||
grupos.get(chave)!.push(registro);
|
||||
}
|
||||
|
||||
return Array.from(grupos.entries()).map(([_, periodos]) => {
|
||||
return Array.from(grupos.entries()).map(([, periodos]) => {
|
||||
// Ordenar por data de criação para manter ordem
|
||||
periodos.sort((a, b) => a._creationTime - b._creationTime);
|
||||
|
||||
@@ -819,7 +821,15 @@ export const atualizarStatusTodosFuncionarios = internalMutation({
|
||||
}
|
||||
}
|
||||
|
||||
const novoStatus = emFerias ? 'em_ferias' : 'ativo';
|
||||
// Determinar o status: férias tem prioridade sobre licença
|
||||
let novoStatus: 'ativo' | 'em_ferias' | 'em_licenca';
|
||||
if (emFerias) {
|
||||
novoStatus = 'em_ferias';
|
||||
} else {
|
||||
// Se não está em férias, verificar se está em licença
|
||||
const emLicenca = await verificarLicencaAtiva(ctx, func._id, hoje);
|
||||
novoStatus = emLicenca ? 'em_licenca' : 'ativo';
|
||||
}
|
||||
|
||||
if (func.statusFerias !== novoStatus) {
|
||||
await ctx.db.patch(func._id, { statusFerias: novoStatus });
|
||||
@@ -829,3 +839,142 @@ export const atualizarStatusTodosFuncionarios = internalMutation({
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
// Internal Mutation: Atualizar status de um funcionário específico
|
||||
export const atualizarStatusFuncionario = internalMutation({
|
||||
args: {
|
||||
funcionarioId: v.id('funcionarios')
|
||||
},
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
const func = await ctx.db.get(args.funcionarioId);
|
||||
if (!func) return null;
|
||||
|
||||
const hoje = new Date();
|
||||
hoje.setHours(0, 0, 0, 0);
|
||||
|
||||
// Buscar todos os registros de férias que podem estar em férias
|
||||
const feriasAprovadas = await ctx.db
|
||||
.query('ferias')
|
||||
.withIndex('by_funcionario_and_status', (q) =>
|
||||
q.eq('funcionarioId', func._id).eq('status', 'aprovado')
|
||||
)
|
||||
.collect();
|
||||
|
||||
const feriasAjustadas = await ctx.db
|
||||
.query('ferias')
|
||||
.withIndex('by_funcionario_and_status', (q) =>
|
||||
q.eq('funcionarioId', func._id).eq('status', 'data_ajustada_aprovada')
|
||||
)
|
||||
.collect();
|
||||
|
||||
const feriasEmFerias = await ctx.db
|
||||
.query('ferias')
|
||||
.withIndex('by_funcionario_and_status', (q) =>
|
||||
q.eq('funcionarioId', func._id).eq('status', 'EmFérias')
|
||||
)
|
||||
.collect();
|
||||
|
||||
const idsAprovados = new Set(feriasAprovadas.map((f) => f._id));
|
||||
const idsAjustados = new Set(feriasAjustadas.map((f) => f._id));
|
||||
const statusAnteriorPorId = new Map<Id<'ferias'>, 'aprovado' | 'data_ajustada_aprovada'>();
|
||||
|
||||
for (const ferias of feriasEmFerias) {
|
||||
if (ferias.historicoAlteracoes && ferias.historicoAlteracoes.length > 0) {
|
||||
const historico = ferias.historicoAlteracoes;
|
||||
for (let i = historico.length - 1; i >= 0; i--) {
|
||||
const entrada = historico[i];
|
||||
if (entrada.acao.includes('Aprovado') || entrada.acao.includes('aprovado')) {
|
||||
statusAnteriorPorId.set(ferias._id, 'aprovado');
|
||||
break;
|
||||
} else if (entrada.acao.includes('Data ajustada') || entrada.acao.includes('ajustada')) {
|
||||
statusAnteriorPorId.set(ferias._id, 'data_ajustada_aprovada');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!statusAnteriorPorId.has(ferias._id)) {
|
||||
statusAnteriorPorId.set(ferias._id, 'aprovado');
|
||||
}
|
||||
}
|
||||
|
||||
const todasFerias = [...feriasAprovadas, ...feriasAjustadas, ...feriasEmFerias];
|
||||
|
||||
let emFerias = false;
|
||||
for (const ferias of todasFerias) {
|
||||
const inicio = new Date(ferias.dataInicio);
|
||||
const fim = new Date(ferias.dataFim);
|
||||
inicio.setHours(0, 0, 0, 0);
|
||||
fim.setHours(23, 59, 59, 999);
|
||||
|
||||
if (hoje >= inicio && hoje <= fim) {
|
||||
emFerias = true;
|
||||
|
||||
if (ferias.status !== 'EmFérias') {
|
||||
await ctx.db.patch(ferias._id, {
|
||||
status: 'EmFérias'
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (ferias.status === 'EmFérias') {
|
||||
let statusAnterior: 'aprovado' | 'data_ajustada_aprovada';
|
||||
|
||||
if (idsAprovados.has(ferias._id)) {
|
||||
statusAnterior = 'aprovado';
|
||||
} else if (idsAjustados.has(ferias._id)) {
|
||||
statusAnterior = 'data_ajustada_aprovada';
|
||||
} else {
|
||||
statusAnterior = statusAnteriorPorId.get(ferias._id) || 'aprovado';
|
||||
}
|
||||
|
||||
await ctx.db.patch(ferias._id, {
|
||||
status: statusAnterior
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Determinar o status: férias tem prioridade sobre licença
|
||||
let novoStatus: 'ativo' | 'em_ferias' | 'em_licenca';
|
||||
if (emFerias) {
|
||||
novoStatus = 'em_ferias';
|
||||
console.log(`[atualizarStatusFuncionario] Funcionário ${func._id} está em férias`);
|
||||
} else {
|
||||
// Se não está em férias, verificar se está em licença
|
||||
const emLicenca = await verificarLicencaAtiva(ctx, func._id, hoje);
|
||||
novoStatus = emLicenca ? 'em_licenca' : 'ativo';
|
||||
console.log(
|
||||
`[atualizarStatusFuncionario] Funcionário ${func._id}: emLicenca=${emLicenca}, novoStatus=${novoStatus}`
|
||||
);
|
||||
}
|
||||
|
||||
if (func.statusFerias !== novoStatus) {
|
||||
console.log(
|
||||
`[atualizarStatusFuncionario] Atualizando status de ${func.statusFerias} para ${novoStatus}`
|
||||
);
|
||||
await ctx.db.patch(func._id, { statusFerias: novoStatus });
|
||||
} else {
|
||||
console.log(`[atualizarStatusFuncionario] Status já está correto: ${novoStatus}`);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
// Mutation pública para atualizar status do funcionário atual (útil para debug/teste)
|
||||
export const atualizarMeuStatus = mutation({
|
||||
args: {},
|
||||
returns: v.null(),
|
||||
handler: async (ctx) => {
|
||||
const usuario = await getCurrentUserFunction(ctx);
|
||||
if (!usuario || !usuario.funcionarioId) {
|
||||
throw new Error('Usuário não encontrado ou não possui funcionário associado');
|
||||
}
|
||||
|
||||
await ctx.runMutation(internal.ferias.atualizarStatusFuncionario, {
|
||||
funcionarioId: usuario.funcionarioId
|
||||
});
|
||||
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2919,14 +2919,30 @@ export const obterEstatisticasBancoHorasGerencial = query({
|
||||
const funcionariosComDetalhes = await Promise.all(
|
||||
bancosMensais.map(async (banco) => {
|
||||
const funcionario = await ctx.db.get(banco.funcionarioId);
|
||||
if (!funcionario) {
|
||||
return {
|
||||
...banco,
|
||||
funcionario: null
|
||||
};
|
||||
}
|
||||
|
||||
// Buscar foto do perfil do funcionário através do usuário associado
|
||||
let fotoPerfilUrl: string | null = null;
|
||||
const usuario = await ctx.db
|
||||
.query('usuarios')
|
||||
.withIndex('by_funcionarioId', (q) => q.eq('funcionarioId', funcionario._id))
|
||||
.first();
|
||||
if (usuario?.fotoPerfil) {
|
||||
fotoPerfilUrl = await ctx.storage.getUrl(usuario.fotoPerfil);
|
||||
}
|
||||
|
||||
return {
|
||||
...banco,
|
||||
funcionario: funcionario
|
||||
? {
|
||||
nome: funcionario.nome,
|
||||
matricula: funcionario.matricula
|
||||
}
|
||||
: null
|
||||
funcionario: {
|
||||
nome: funcionario.nome,
|
||||
matricula: funcionario.matricula,
|
||||
fotoPerfilUrl
|
||||
}
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
@@ -35,7 +35,9 @@ export const funcionariosTables = {
|
||||
simboloId: v.id('simbolos'),
|
||||
simboloTipo: simboloTipo,
|
||||
gestorId: v.optional(v.id('usuarios')),
|
||||
statusFerias: v.optional(v.union(v.literal('ativo'), v.literal('em_ferias'))),
|
||||
statusFerias: v.optional(
|
||||
v.union(v.literal('ativo'), v.literal('em_ferias'), v.literal('em_licenca'))
|
||||
),
|
||||
|
||||
// Regime de trabalho (para cálculo correto de férias)
|
||||
regimeTrabalho: v.optional(
|
||||
|
||||
@@ -217,5 +217,24 @@ export const systemTables = {
|
||||
sshUsername: v.optional(v.string()), // Usuário SSH para acesso ao servidor
|
||||
sshPasswordHash: v.optional(v.string()), // Hash da senha SSH (criptografada)
|
||||
sshPort: v.optional(v.number()) // Porta SSH (padrão: 22)
|
||||
}).index('by_ativo', ['ativo'])
|
||||
}).index('by_ativo', ['ativo']),
|
||||
|
||||
// Logs de Erros do Servidor (500, etc)
|
||||
errosServidor: defineTable({
|
||||
statusCode: v.number(), // Código HTTP do erro (500, 502, etc)
|
||||
mensagem: v.string(), // Mensagem do erro
|
||||
stack: v.optional(v.string()), // Stack trace do erro
|
||||
url: v.optional(v.string()), // URL onde ocorreu o erro
|
||||
method: v.optional(v.string()), // Método HTTP (GET, POST, etc)
|
||||
ipAddress: v.optional(v.string()), // IP do cliente
|
||||
userAgent: v.optional(v.string()), // User agent do navegador
|
||||
usuarioId: v.optional(v.id('usuarios')), // Usuário autenticado (se houver)
|
||||
notificado: v.boolean(), // Se a equipe técnica já foi notificada
|
||||
notificadoEm: v.optional(v.number()), // Timestamp da notificação
|
||||
criadoEm: v.number() // Timestamp do erro
|
||||
})
|
||||
.index('by_status_code', ['statusCode'])
|
||||
.index('by_notificado', ['notificado'])
|
||||
.index('by_criado_em', ['criadoEm'])
|
||||
.index('by_usuario', ['usuarioId'])
|
||||
};
|
||||
|
||||
@@ -859,6 +859,85 @@ export const criarTemplatesPadrao = mutation({
|
||||
],
|
||||
categoria: 'email' as const,
|
||||
tags: ['seguranca', 'anomalia', 'alerta', 'cybersecurity']
|
||||
},
|
||||
// ===================== NOTIFICAÇÕES DE ERROS DO SERVIDOR =====================
|
||||
{
|
||||
codigo: 'ERRO_SERVIDOR_404',
|
||||
nome: 'Erro 404 - Página Não Encontrada',
|
||||
titulo: '⚠️ Erro 404 - Página não encontrada',
|
||||
corpo:
|
||||
"<html><body style='font-family: Arial, sans-serif; line-height: 1.6; color: #333;'>" +
|
||||
"<div style='max-width: 600px; margin: 0 auto; padding: 20px;'>" +
|
||||
"<h2 style='color: #F59E0B;'>⚠️ Erro 404 - Página Não Encontrada</h2>" +
|
||||
'<p>Olá <strong>{{destinatarioNome}}</strong>,</p>' +
|
||||
'<p>O sistema detectou uma tentativa de acesso a uma página que não existe:</p>' +
|
||||
"<div style='background-color: #FFFBEB; border-left: 4px solid #F59E0B; padding: 15px; border-radius: 8px; margin: 20px 0;'>" +
|
||||
"<p style='margin: 0;'><strong>URL:</strong> {{url}}</p>" +
|
||||
"<p style='margin: 5px 0 0 0;'><strong>Método HTTP:</strong> {{method}}</p>" +
|
||||
"<p style='margin: 5px 0 0 0;'><strong>Mensagem:</strong> {{mensagem}}</p>" +
|
||||
"<p style='margin: 5px 0 0 0;'><strong>Data/Hora:</strong> {{timestamp}}</p>" +
|
||||
'</div>' +
|
||||
"<p style='color: #6B7280; font-size: 14px; margin-top: 20px;'>" +
|
||||
'<strong>Possíveis causas:</strong><br>' +
|
||||
'• Link quebrado ou desatualizado<br>' +
|
||||
'• URL digitada incorretamente<br>' +
|
||||
'• Página movida ou removida<br>' +
|
||||
'• Tentativa de acesso a recurso inexistente' +
|
||||
'</p>' +
|
||||
"<p style='color: #6B7280; font-size: 12px; margin-top: 30px;'>" +
|
||||
'SGSE - Sistema de Gerenciamento de Secretaria - Equipe de TI' +
|
||||
'</p>' +
|
||||
'</div></body></html>',
|
||||
variaveis: [
|
||||
'destinatarioNome',
|
||||
'url',
|
||||
'method',
|
||||
'mensagem',
|
||||
'timestamp'
|
||||
],
|
||||
categoria: 'email' as const,
|
||||
tags: ['erro', '404', 'servidor', 'notificacao', 'ti']
|
||||
},
|
||||
{
|
||||
codigo: 'ERRO_SERVIDOR_500',
|
||||
nome: 'Erro 500 - Erro Interno do Servidor',
|
||||
titulo: '🚨 Erro 500 - Erro Interno do Servidor',
|
||||
corpo:
|
||||
"<html><body style='font-family: Arial, sans-serif; line-height: 1.6; color: #333;'>" +
|
||||
"<div style='max-width: 600px; margin: 0 auto; padding: 20px;'>" +
|
||||
"<h2 style='color: #DC2626;'>🚨 Erro 500 - Erro Interno do Servidor</h2>" +
|
||||
'<p>Olá <strong>{{destinatarioNome}}</strong>,</p>' +
|
||||
'<p>O sistema detectou um <strong>erro interno do servidor</strong> que requer atenção imediata:</p>' +
|
||||
"<div style='background-color: #FEF2F2; border-left: 4px solid #DC2626; padding: 15px; border-radius: 8px; margin: 20px 0;'>" +
|
||||
"<p style='margin: 0;'><strong>Código HTTP:</strong> {{statusCode}}</p>" +
|
||||
"<p style='margin: 5px 0 0 0;'><strong>URL:</strong> {{url}}</p>" +
|
||||
"<p style='margin: 5px 0 0 0;'><strong>Método HTTP:</strong> {{method}}</p>" +
|
||||
"<p style='margin: 5px 0 0 0;'><strong>Mensagem:</strong> {{mensagem}}</p>" +
|
||||
"<p style='margin: 5px 0 0 0;'><strong>Data/Hora:</strong> {{timestamp}}</p>" +
|
||||
'</div>' +
|
||||
"<div style='background-color: #F9FAFB; border: 1px solid #E5E7EB; padding: 15px; border-radius: 8px; margin: 20px 0;'>" +
|
||||
"<p style='margin: 0; font-size: 12px; color: #6B7280;'><strong>Stack Trace:</strong></p>" +
|
||||
"<pre style='margin: 10px 0 0 0; padding: 10px; background-color: #FFFFFF; border: 1px solid #E5E7EB; border-radius: 4px; font-size: 11px; white-space: pre-wrap; word-wrap: break-word; overflow-x: auto;'>{{stack}}</pre>" +
|
||||
'</div>' +
|
||||
"<p style='color: #DC2626; font-weight: bold; margin-top: 20px;'>" +
|
||||
'⚠️ AÇÃO IMEDIATA NECESSÁRIA' +
|
||||
'</p>' +
|
||||
"<p style='color: #6B7280; font-size: 12px; margin-top: 30px;'>" +
|
||||
'SGSE - Sistema de Gerenciamento de Secretaria - Equipe de TI<br>' +
|
||||
'Este é um alerta automático do sistema de monitoramento de erros.' +
|
||||
'</p>' +
|
||||
'</div></body></html>',
|
||||
variaveis: [
|
||||
'destinatarioNome',
|
||||
'statusCode',
|
||||
'url',
|
||||
'method',
|
||||
'mensagem',
|
||||
'stack',
|
||||
'timestamp'
|
||||
],
|
||||
categoria: 'email' as const,
|
||||
tags: ['erro', '500', 'servidor', 'critico', 'notificacao', 'ti']
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user