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:
2025-12-08 23:16:05 -03:00
parent e46738c5bf
commit 1810cbabe2
22 changed files with 1364 additions and 249 deletions

View File

@@ -0,0 +1,142 @@
/**
* Cache de avatares para reduzir requisições repetidas
* Usa Cache API do navegador e cache em memória
*/
interface AvatarCacheEntry {
url: string;
timestamp: number;
}
// Cache em memória (útil durante a sessão)
const memoryCache = new Map<string, AvatarCacheEntry>();
// Nome do cache no Cache API
const CACHE_NAME = 'sgse-avatars-v1';
const CACHE_DURATION = 7 * 24 * 60 * 60 * 1000; // 7 dias
/**
* Obtém avatar do cache ou faz requisição
*/
export async function getCachedAvatar(
avatarUrl: string | null | undefined,
userId?: string
): Promise<string | null> {
if (!avatarUrl) return null;
// Usar userId como chave se disponível, senão usar a URL
const cacheKey = userId || avatarUrl;
// Verificar cache em memória primeiro
const memoryEntry = memoryCache.get(cacheKey);
if (memoryEntry && Date.now() - memoryEntry.timestamp < CACHE_DURATION) {
return memoryEntry.url;
}
// Verificar Cache API do navegador
if (typeof window !== 'undefined' && 'caches' in window) {
try {
const cache = await caches.open(CACHE_NAME);
const cachedResponse = await cache.match(avatarUrl);
if (cachedResponse) {
const blob = await cachedResponse.blob();
const url = URL.createObjectURL(blob);
// Atualizar cache em memória
memoryCache.set(cacheKey, {
url,
timestamp: Date.now()
});
return url;
}
} catch (error) {
console.warn('Erro ao acessar cache de avatares:', error);
}
}
// Se não está no cache, fazer requisição e armazenar
try {
const response = await fetch(avatarUrl);
if (!response.ok) return null;
const blob = await response.blob();
const url = URL.createObjectURL(blob);
// Armazenar no cache em memória
memoryCache.set(cacheKey, {
url,
timestamp: Date.now()
});
// Armazenar no Cache API
if (typeof window !== 'undefined' && 'caches' in window) {
try {
const cache = await caches.open(CACHE_NAME);
await cache.put(avatarUrl, new Response(blob));
} catch (error) {
console.warn('Erro ao armazenar avatar no cache:', error);
}
}
return url;
} catch (error) {
console.warn('Erro ao carregar avatar:', error);
return null;
}
}
/**
* Limpa o cache de avatares
*/
export async function clearAvatarCache(): Promise<void> {
memoryCache.clear();
if (typeof window !== 'undefined' && 'caches' in window) {
try {
await caches.delete(CACHE_NAME);
} catch (error) {
console.warn('Erro ao limpar cache de avatares:', error);
}
}
}
/**
* Limpa avatares antigos do cache (mais de 7 dias)
*/
export async function cleanOldAvatars(): Promise<void> {
const now = Date.now();
// Limpar cache em memória
for (const [key, entry] of memoryCache.entries()) {
if (now - entry.timestamp > CACHE_DURATION) {
URL.revokeObjectURL(entry.url);
memoryCache.delete(key);
}
}
// Limpar Cache API (manter apenas os últimos 100)
if (typeof window !== 'undefined' && 'caches' in window) {
try {
const cache = await caches.open(CACHE_NAME);
const keys = await cache.keys();
// Se há mais de 100 avatares, remover os mais antigos
if (keys.length > 100) {
const toDelete = keys.slice(0, keys.length - 100);
await Promise.all(toDelete.map((key) => cache.delete(key)));
}
} catch (error) {
console.warn('Erro ao limpar cache antigo:', error);
}
}
}
// Limpar cache antigo periodicamente (a cada hora)
if (typeof window !== 'undefined') {
setInterval(() => {
cleanOldAvatars();
}, 60 * 60 * 1000);
}

View File

@@ -434,3 +434,5 @@ export function adicionarRodape(doc: jsPDF): void {
}

View File

@@ -77,7 +77,7 @@ async function measureNetworkLatency(): Promise<number> {
const start = performance.now();
// Fazer uma requisição pequena para medir latência
await fetch(window.location.origin + '/favicon.ico', {
await fetch(window.location.origin + '/favicon.png', {
method: 'HEAD',
cache: 'no-cache'
});

View File

@@ -97,6 +97,11 @@ export async function gerarPDFComSelecao(
yPosition = adicionarDadosFuncionario(doc, yPosition, funcionario, dataInicio, dataFim);
}
// SEÇÃO: TABELA PRINCIPAL DE REGISTROS (PRIMEIRO)
if (sections.registrosPonto) {
yPosition = gerarTabelaRegistrosPDF(doc, yPosition, dias, configPonto, sections);
}
// Resumo do Período
yPosition = adicionarResumoPeriodo(doc, yPosition, resumo, formatarHoras, formatarMinutos);
@@ -106,11 +111,6 @@ export async function gerarPDFComSelecao(
// Legenda
yPosition = adicionarLegenda(doc, yPosition);
// SEÇÃO: TABELA PRINCIPAL DE REGISTROS
if (sections.registrosPonto) {
yPosition = gerarTabelaRegistrosPDF(doc, yPosition, dias, configPonto, sections);
}
// SEÇÃO: BANCO DE HORAS
if (sections.bancoHoras) {
yPosition = await gerarSecaoBancoHorasPDF(