feat: enhance chat components with improved accessibility features, including ARIA attributes for search and user status, and implement message length validation and file type checks in message input handling
This commit is contained in:
@@ -69,8 +69,9 @@ export async function verificarLicencaAtiva(
|
||||
dataAtual?: Date
|
||||
): Promise<boolean> {
|
||||
// Normalizar data atual para comparar apenas a parte da data (sem hora)
|
||||
// Usar timezone local para evitar problemas de conversão
|
||||
const hoje = dataAtual || new Date();
|
||||
const hojeStr = hoje.toISOString().split('T')[0]; // Formato: "YYYY-MM-DD"
|
||||
const hojeStr = `${hoje.getFullYear()}-${String(hoje.getMonth() + 1).padStart(2, '0')}-${String(hoje.getDate()).padStart(2, '0')}`; // Formato: "YYYY-MM-DD"
|
||||
|
||||
console.log(
|
||||
`[verificarLicencaAtiva] Verificando funcionário ${funcionarioId}, data atual: ${hojeStr}`
|
||||
@@ -966,9 +967,13 @@ export const criarDeclaracaoComparecimento = mutation({
|
||||
await recalcularBancoHorasPeriodo(ctx, args.funcionarioId, args.dataInicio, args.dataFim);
|
||||
|
||||
// Atualizar status do funcionário imediatamente
|
||||
console.log(
|
||||
`[criarDeclaracaoComparecimento] Atualizando status do funcionário ${args.funcionarioId} após criar declaração`
|
||||
);
|
||||
await ctx.runMutation(internal.ferias.atualizarStatusFuncionario, {
|
||||
funcionarioId: args.funcionarioId
|
||||
});
|
||||
console.log(`[criarDeclaracaoComparecimento] Status atualizado com sucesso`);
|
||||
|
||||
return atestadoId;
|
||||
}
|
||||
@@ -1027,9 +1032,13 @@ export const criarLicencaMaternidade = mutation({
|
||||
await recalcularBancoHorasPeriodo(ctx, args.funcionarioId, args.dataInicio, args.dataFim);
|
||||
|
||||
// Atualizar status do funcionário imediatamente
|
||||
console.log(
|
||||
`[criarLicencaMaternidade] Atualizando status do funcionário ${args.funcionarioId} após criar licença maternidade`
|
||||
);
|
||||
await ctx.runMutation(internal.ferias.atualizarStatusFuncionario, {
|
||||
funcionarioId: args.funcionarioId
|
||||
});
|
||||
console.log(`[criarLicencaMaternidade] Status atualizado com sucesso`);
|
||||
|
||||
return licencaId;
|
||||
}
|
||||
@@ -1081,9 +1090,13 @@ export const criarLicencaPaternidade = mutation({
|
||||
await recalcularBancoHorasPeriodo(ctx, args.funcionarioId, args.dataInicio, args.dataFim);
|
||||
|
||||
// Atualizar status do funcionário imediatamente
|
||||
console.log(
|
||||
`[criarLicencaPaternidade] Atualizando status do funcionário ${args.funcionarioId} após criar licença paternidade`
|
||||
);
|
||||
await ctx.runMutation(internal.ferias.atualizarStatusFuncionario, {
|
||||
funcionarioId: args.funcionarioId
|
||||
});
|
||||
console.log(`[criarLicencaPaternidade] Status atualizado com sucesso`);
|
||||
|
||||
return licencaId;
|
||||
}
|
||||
@@ -1165,6 +1178,13 @@ export const excluirAtestado = mutation({
|
||||
const atestado = await ctx.db.get(args.id);
|
||||
if (!atestado) throw new Error('Atestado não encontrado');
|
||||
|
||||
// IMPORTANTE: Salvar o período exato do atestado ANTES de excluir
|
||||
// para recalcular o banco de horas apenas para esse período específico
|
||||
const funcionarioId = atestado.funcionarioId;
|
||||
const dataInicio = atestado.dataInicio; // Data início do atestado
|
||||
const dataFim = atestado.dataFim; // Data fim do atestado
|
||||
|
||||
// Excluir o registro do banco de dados
|
||||
await ctx.db.delete(args.id);
|
||||
|
||||
await registrarAtividade(
|
||||
@@ -1176,9 +1196,13 @@ export const excluirAtestado = mutation({
|
||||
args.id
|
||||
);
|
||||
|
||||
// Recalcular banco de horas APENAS para o período específico do atestado excluído
|
||||
// Isso garante que os dias do atestado sejam removidos corretamente dos registros de ponto
|
||||
await recalcularBancoHorasPeriodo(ctx, funcionarioId, dataInicio, dataFim);
|
||||
|
||||
// Atualizar status do funcionário imediatamente
|
||||
await ctx.runMutation(internal.ferias.atualizarStatusFuncionario, {
|
||||
funcionarioId: atestado.funcionarioId
|
||||
funcionarioId
|
||||
});
|
||||
|
||||
return null;
|
||||
@@ -1200,6 +1224,13 @@ export const excluirLicenca = mutation({
|
||||
const licenca = await ctx.db.get(args.id);
|
||||
if (!licenca) throw new Error('Licença não encontrada');
|
||||
|
||||
// IMPORTANTE: Salvar o período exato da licença ANTES de excluir
|
||||
// para recalcular o banco de horas apenas para esse período específico
|
||||
const funcionarioId = licenca.funcionarioId;
|
||||
const dataInicio = licenca.dataInicio; // Data início da licença
|
||||
const dataFim = licenca.dataFim; // Data fim da licença
|
||||
|
||||
// Excluir o registro do banco de dados
|
||||
await ctx.db.delete(args.id);
|
||||
|
||||
await registrarAtividade(
|
||||
@@ -1211,9 +1242,13 @@ export const excluirLicenca = mutation({
|
||||
args.id
|
||||
);
|
||||
|
||||
// Recalcular banco de horas APENAS para o período específico da licença excluída
|
||||
// Isso garante que os dias da licença sejam removidos corretamente dos registros de ponto
|
||||
await recalcularBancoHorasPeriodo(ctx, funcionarioId, dataInicio, dataFim);
|
||||
|
||||
// Atualizar status do funcionário imediatamente
|
||||
await ctx.runMutation(internal.ferias.atualizarStatusFuncionario, {
|
||||
funcionarioId: licenca.funcionarioId
|
||||
funcionarioId
|
||||
});
|
||||
|
||||
return null;
|
||||
|
||||
@@ -281,6 +281,59 @@ export const enviarMensagem = mutation({
|
||||
});
|
||||
}
|
||||
|
||||
// Validar tamanho da mensagem
|
||||
const MAX_MENSAGEM_LENGTH = 5000;
|
||||
if (args.conteudo.length > MAX_MENSAGEM_LENGTH) {
|
||||
throw new Error(`Mensagem muito longa. O limite é de ${MAX_MENSAGEM_LENGTH} caracteres.`);
|
||||
}
|
||||
|
||||
// Validação de tipo de arquivo (se houver arquivo)
|
||||
if (args.arquivoTipo) {
|
||||
const TIPOS_PERMITIDOS = [
|
||||
// Imagens
|
||||
'image/jpeg',
|
||||
'image/png',
|
||||
'image/gif',
|
||||
'image/webp',
|
||||
'image/svg+xml',
|
||||
// Documentos
|
||||
'application/pdf',
|
||||
'application/msword',
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
'application/vnd.ms-excel',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
'application/vnd.ms-powerpoint',
|
||||
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
||||
'text/plain',
|
||||
'text/csv',
|
||||
// Arquivos
|
||||
'application/zip',
|
||||
'application/x-rar-compressed',
|
||||
'application/x-7z-compressed',
|
||||
'application/x-tar',
|
||||
'application/gzip'
|
||||
];
|
||||
|
||||
if (!TIPOS_PERMITIDOS.includes(args.arquivoTipo)) {
|
||||
throw new Error('Tipo de arquivo não permitido');
|
||||
}
|
||||
|
||||
// Validar tamanho de arquivo (10MB)
|
||||
const MAX_FILE_SIZE = 10 * 1024 * 1024;
|
||||
if (args.arquivoTamanho && args.arquivoTamanho > MAX_FILE_SIZE) {
|
||||
throw new Error(`Arquivo muito grande. O tamanho máximo é ${MAX_FILE_SIZE / 1024 / 1024}MB.`);
|
||||
}
|
||||
|
||||
// Validar nome do arquivo (sanitizar)
|
||||
if (args.arquivoNome) {
|
||||
const nomeSanitizado = args.arquivoNome.replace(/[^a-zA-Z0-9._-]/g, '_');
|
||||
if (nomeSanitizado !== args.arquivoNome) {
|
||||
// Se o nome foi alterado, usar o sanitizado
|
||||
args.arquivoNome = nomeSanitizado;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Verificar se usuário pertence à conversa
|
||||
const conversa = await ctx.db.get(args.conversaId);
|
||||
if (!conversa) throw new Error('Conversa não encontrada');
|
||||
@@ -1716,28 +1769,42 @@ export const listarConversas = query({
|
||||
export const obterMensagens = query({
|
||||
args: {
|
||||
conversaId: v.id('conversas'),
|
||||
limit: v.optional(v.number())
|
||||
limit: v.optional(v.number()),
|
||||
cursor: v.optional(v.id('mensagens')) // ID da última mensagem carregada (para paginação)
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const usuarioAtual = await getUsuarioAutenticado(ctx);
|
||||
if (!usuarioAtual) return [];
|
||||
if (!usuarioAtual) return { mensagens: [], hasMore: false };
|
||||
|
||||
// Verificar se usuário pertence à conversa (SEGURANÇA CRÍTICA)
|
||||
const conversa = await ctx.db.get(args.conversaId);
|
||||
if (!conversa || !conversa.participantes.includes(usuarioAtual._id)) {
|
||||
return [];
|
||||
return { mensagens: [], hasMore: false };
|
||||
}
|
||||
|
||||
// Buscar mensagens (excluir agendadas)
|
||||
const mensagens = await ctx.db
|
||||
const limit = args.limit || 50;
|
||||
let query = ctx.db
|
||||
.query('mensagens')
|
||||
.withIndex('by_conversa', (q) => q.eq('conversaId', args.conversaId))
|
||||
.order('desc')
|
||||
.take(args.limit || 50);
|
||||
.order('desc');
|
||||
|
||||
// Se há cursor, buscar mensagens anteriores a ele
|
||||
if (args.cursor) {
|
||||
const cursorMsg = await ctx.db.get(args.cursor);
|
||||
if (cursorMsg && cursorMsg.conversaId === args.conversaId) {
|
||||
// Buscar mensagens anteriores à mensagem do cursor
|
||||
query = query.filter((q) => q.lt(q.field('_creationTime'), cursorMsg._creationTime));
|
||||
}
|
||||
}
|
||||
|
||||
// Buscar uma mensagem a mais para verificar se há mais mensagens
|
||||
const mensagens = await query.take(limit + 1);
|
||||
const hasMore = mensagens.length > limit;
|
||||
const mensagensParaRetornar = hasMore ? mensagens.slice(0, limit) : mensagens;
|
||||
|
||||
// Filtrar mensagens agendadas e garantir que são da conversa correta
|
||||
// SEGURANÇA: Apenas mensagens de participantes da conversa são retornadas
|
||||
const mensagensFiltradas = mensagens.filter((m) => {
|
||||
const mensagensFiltradas = mensagensParaRetornar.filter((m) => {
|
||||
// Excluir agendadas
|
||||
if (m.agendadaPara) return false;
|
||||
|
||||
@@ -1751,7 +1818,7 @@ export const obterMensagens = query({
|
||||
|
||||
// Enriquecer com informações do remetente e mensagem respondida
|
||||
const mensagensEnriquecidas = await Promise.all(
|
||||
mensagensFiltradas.map(async (mensagem) => {
|
||||
mensagensParaRetornar.map(async (mensagem) => {
|
||||
const remetente = await ctx.db.get(mensagem.remetenteId);
|
||||
|
||||
// SEGURANÇA: Não retornar informações de remetente se não for participante
|
||||
@@ -1794,7 +1861,15 @@ export const obterMensagens = query({
|
||||
);
|
||||
|
||||
// Filtrar nulls (caso alguma mensagem tenha sido rejeitada por segurança)
|
||||
return mensagensEnriquecidas.filter((m) => m !== null).reverse();
|
||||
const mensagensFinais = mensagensEnriquecidas.filter((m) => m !== null).reverse();
|
||||
|
||||
return {
|
||||
mensagens: mensagensFinais,
|
||||
hasMore,
|
||||
nextCursor: hasMore && mensagensFinais.length > 0
|
||||
? mensagensFinais[mensagensFinais.length - 1]._id
|
||||
: null
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -941,18 +941,22 @@ export const atualizarStatusFuncionario = internalMutation({
|
||||
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
|
||||
console.log(
|
||||
`[atualizarStatusFuncionario] Verificando licença ativa para funcionário ${func._id}, data: ${hoje.toISOString()}`
|
||||
);
|
||||
const emLicenca = await verificarLicencaAtiva(ctx, func._id, hoje);
|
||||
novoStatus = emLicenca ? 'em_licenca' : 'ativo';
|
||||
console.log(
|
||||
`[atualizarStatusFuncionario] Funcionário ${func._id}: emLicenca=${emLicenca}, novoStatus=${novoStatus}`
|
||||
`[atualizarStatusFuncionario] Funcionário ${func._id}: emLicenca=${emLicenca}, statusAtual=${func.statusFerias}, novoStatus=${novoStatus}`
|
||||
);
|
||||
}
|
||||
|
||||
if (func.statusFerias !== novoStatus) {
|
||||
console.log(
|
||||
`[atualizarStatusFuncionario] Atualizando status de ${func.statusFerias} para ${novoStatus}`
|
||||
`[atualizarStatusFuncionario] ⚠️ ATUALIZANDO status de "${func.statusFerias}" para "${novoStatus}"`
|
||||
);
|
||||
await ctx.db.patch(func._id, { statusFerias: novoStatus });
|
||||
console.log(`[atualizarStatusFuncionario] ✅ Status atualizado com sucesso!`);
|
||||
} else {
|
||||
console.log(`[atualizarStatusFuncionario] Status já está correto: ${novoStatus}`);
|
||||
}
|
||||
|
||||
@@ -612,49 +612,62 @@ export const getStatusSistema = query({
|
||||
ultimaAtualizacao: v.number()
|
||||
}),
|
||||
handler: async (ctx) => {
|
||||
// Última métrica, se existir
|
||||
const ultimaMetrica = (await ctx.db.query('systemMetrics').order('desc').first()) ?? null;
|
||||
try {
|
||||
// Última métrica, se existir
|
||||
const ultimaMetrica = (await ctx.db.query('systemMetrics').order('desc').first()) ?? null;
|
||||
|
||||
// Usuários online: usar métrica se disponível, senão derivar de usuários
|
||||
let usuariosOnline = 0;
|
||||
if (ultimaMetrica?.usuariosOnline !== undefined) {
|
||||
usuariosOnline = ultimaMetrica.usuariosOnline;
|
||||
} else {
|
||||
const usuarios = await ctx.db.query('usuarios').collect();
|
||||
usuariosOnline = usuarios.filter((u) => u.statusPresenca === 'online').length;
|
||||
// Usuários online: usar métrica se disponível, senão derivar de usuários
|
||||
let usuariosOnline = 0;
|
||||
if (ultimaMetrica?.usuariosOnline !== undefined) {
|
||||
usuariosOnline = ultimaMetrica.usuariosOnline;
|
||||
} else {
|
||||
const usuarios = await ctx.db.query('usuarios').collect();
|
||||
usuariosOnline = usuarios.filter((u) => u.statusPresenca === 'online').length;
|
||||
}
|
||||
|
||||
// Total de registros (estimativa baseada em tabelas principais)
|
||||
const [usuarios, funcionarios, simbolos, alertas, metricas] = await Promise.all([
|
||||
ctx.db.query('usuarios').collect(),
|
||||
ctx.db.query('funcionarios').collect(),
|
||||
ctx.db.query('simbolos').collect(),
|
||||
ctx.db.query('alertConfigurations').collect(),
|
||||
ctx.db.query('systemMetrics').take(100) // não precisa contar tudo
|
||||
]);
|
||||
const totalRegistros =
|
||||
usuarios.length + funcionarios.length + simbolos.length + alertas.length + metricas.length;
|
||||
|
||||
// Métricas de performance com fallbacks seguros
|
||||
const tempoMedioResposta = ultimaMetrica?.tempoRespostaMedio ?? 0;
|
||||
const cpuUsada = Math.max(
|
||||
0,
|
||||
Math.min(100, Math.round((ultimaMetrica?.cpuUsage ?? 0) * 100) / 100)
|
||||
);
|
||||
const memoriaUsada = Math.max(
|
||||
0,
|
||||
Math.min(100, Math.round((ultimaMetrica?.memoryUsage ?? 0) * 100) / 100)
|
||||
);
|
||||
const ultimaAtualizacao = ultimaMetrica?.timestamp ?? Date.now();
|
||||
|
||||
return {
|
||||
usuariosOnline,
|
||||
totalRegistros,
|
||||
tempoMedioResposta,
|
||||
cpuUsada,
|
||||
memoriaUsada,
|
||||
ultimaAtualizacao
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Erro em getStatusSistema:', error);
|
||||
// Retornar valores padrão em caso de erro
|
||||
return {
|
||||
usuariosOnline: 0,
|
||||
totalRegistros: 0,
|
||||
tempoMedioResposta: 0,
|
||||
cpuUsada: 0,
|
||||
memoriaUsada: 0,
|
||||
ultimaAtualizacao: Date.now()
|
||||
};
|
||||
}
|
||||
|
||||
// Total de registros (estimativa baseada em tabelas principais)
|
||||
const [usuarios, funcionarios, simbolos, alertas, metricas] = await Promise.all([
|
||||
ctx.db.query('usuarios').collect(),
|
||||
ctx.db.query('funcionarios').collect(),
|
||||
ctx.db.query('simbolos').collect(),
|
||||
ctx.db.query('alertConfigurations').collect(),
|
||||
ctx.db.query('systemMetrics').take(100) // não precisa contar tudo
|
||||
]);
|
||||
const totalRegistros =
|
||||
usuarios.length + funcionarios.length + simbolos.length + alertas.length + metricas.length;
|
||||
|
||||
// Métricas de performance com fallbacks seguros
|
||||
const tempoMedioResposta = ultimaMetrica?.tempoRespostaMedio ?? 0;
|
||||
const cpuUsada = Math.max(
|
||||
0,
|
||||
Math.min(100, Math.round((ultimaMetrica?.cpuUsage ?? 0) * 100) / 100)
|
||||
);
|
||||
const memoriaUsada = Math.max(
|
||||
0,
|
||||
Math.min(100, Math.round((ultimaMetrica?.memoryUsage ?? 0) * 100) / 100)
|
||||
);
|
||||
const ultimaAtualizacao = ultimaMetrica?.timestamp ?? Date.now();
|
||||
|
||||
return {
|
||||
usuariosOnline,
|
||||
totalRegistros,
|
||||
tempoMedioResposta,
|
||||
cpuUsada,
|
||||
memoriaUsada,
|
||||
ultimaAtualizacao
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
@@ -673,22 +686,23 @@ export const getAtividadeBancoDados = query({
|
||||
)
|
||||
}),
|
||||
handler: async (ctx) => {
|
||||
const agora = Date.now();
|
||||
const haUmMinuto = agora - 60 * 1000;
|
||||
try {
|
||||
const agora = Date.now();
|
||||
const haUmMinuto = agora - 60 * 1000;
|
||||
|
||||
// Buscar atividades reais do sistema
|
||||
const atividadesRecentes = await ctx.db
|
||||
.query('logsAtividades')
|
||||
.withIndex('by_timestamp', (q) => q.gte('timestamp', haUmMinuto))
|
||||
.order('asc')
|
||||
.collect();
|
||||
// Buscar atividades reais do sistema
|
||||
const atividadesRecentes = await ctx.db
|
||||
.query('logsAtividades')
|
||||
.withIndex('by_timestamp', (q) => q.gte('timestamp', haUmMinuto))
|
||||
.order('asc')
|
||||
.collect();
|
||||
|
||||
// Buscar métricas também (para mensagens se houver)
|
||||
const metricasRecentes = await ctx.db
|
||||
.query('systemMetrics')
|
||||
.withIndex('by_timestamp', (q) => q.gte('timestamp', haUmMinuto))
|
||||
.order('asc')
|
||||
.collect();
|
||||
// Buscar métricas também (para mensagens se houver)
|
||||
const metricasRecentes = await ctx.db
|
||||
.query('systemMetrics')
|
||||
.withIndex('by_timestamp', (q) => q.gte('timestamp', haUmMinuto))
|
||||
.order('asc')
|
||||
.collect();
|
||||
|
||||
// Bucketizar em 30 pontos (~2s cada) para visualização
|
||||
const numBuckets = 30;
|
||||
@@ -727,6 +741,11 @@ export const getAtividadeBancoDados = query({
|
||||
}
|
||||
|
||||
return { historico };
|
||||
} catch (error) {
|
||||
console.error('Erro em getAtividadeBancoDados:', error);
|
||||
// Retornar histórico vazio em caso de erro
|
||||
return { historico: Array(30).fill({ entradas: 0, saidas: 0 }) };
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -742,20 +761,21 @@ export const getDistribuicaoRequisicoes = query({
|
||||
escritas: v.number()
|
||||
}),
|
||||
handler: async (ctx) => {
|
||||
const umaHoraAtras = Date.now() - 60 * 60 * 1000;
|
||||
try {
|
||||
const umaHoraAtras = Date.now() - 60 * 60 * 1000;
|
||||
|
||||
// Buscar atividades reais do sistema
|
||||
const atividades = await ctx.db
|
||||
.query('logsAtividades')
|
||||
.withIndex('by_timestamp', (q) => q.gte('timestamp', umaHoraAtras))
|
||||
.collect();
|
||||
// Buscar atividades reais do sistema
|
||||
const atividades = await ctx.db
|
||||
.query('logsAtividades')
|
||||
.withIndex('by_timestamp', (q) => q.gte('timestamp', umaHoraAtras))
|
||||
.collect();
|
||||
|
||||
// Buscar métricas também
|
||||
const metricas = await ctx.db
|
||||
.query('systemMetrics')
|
||||
.withIndex('by_timestamp', (q) => q.gte('timestamp', umaHoraAtras))
|
||||
.order('desc')
|
||||
.take(100);
|
||||
// Buscar métricas também
|
||||
const metricas = await ctx.db
|
||||
.query('systemMetrics')
|
||||
.withIndex('by_timestamp', (q) => q.gte('timestamp', umaHoraAtras))
|
||||
.order('desc')
|
||||
.take(100);
|
||||
|
||||
// Contar operações de leitura (consultas, visualizações)
|
||||
const leituras = atividades.filter(
|
||||
@@ -792,5 +812,10 @@ export const getDistribuicaoRequisicoes = query({
|
||||
const mutations = escritas + Math.round(totalMensagens * 0.3);
|
||||
|
||||
return { queries, mutations, leituras, escritas };
|
||||
} catch (error) {
|
||||
console.error('Erro em getDistribuicaoRequisicoes:', error);
|
||||
// Retornar valores padrão em caso de erro
|
||||
return { queries: 0, mutations: 0, leituras: 0, escritas: 0 };
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user