feat: enhance notification bell component by refactoring notification fetching logic, improving type safety, and updating UI elements for better user experience

This commit is contained in:
2025-12-15 11:33:51 -03:00
parent c272ca05e8
commit f3288b9639
5 changed files with 589 additions and 575 deletions

View File

@@ -1,6 +1,8 @@
<script lang="ts">
import { onMount } from 'svelte';
import { useQuery, useConvexClient } from 'convex-svelte';
import { api } from '@sgse-app/backend/convex/_generated/api';
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
import { notificacoesCount } from '$lib/stores/chatStore';
import { formatDistanceToNow } from 'date-fns';
import { ptBR } from 'date-fns/locale';
@@ -18,9 +20,10 @@
const currentUser = useQuery(api.auth.getCurrentUser, {});
let modalOpen = $state(false);
let usuarioId = $derived((currentUser?.data?._id as Id<'usuarios'> | undefined) ?? null);
let notificacoesFerias = $state<
Array<{
_id: string;
_id: Id<'notificacoesFerias'>;
mensagem: string;
tipo: string;
_creationTime: number;
@@ -28,7 +31,7 @@
>([]);
let notificacoesAusencias = $state<
Array<{
_id: string;
_id: Id<'notificacoesAusencias'>;
mensagem: string;
tipo: string;
_creationTime: number;
@@ -47,51 +50,47 @@
// Separar notificações lidas e não lidas
let notificacoesNaoLidas = $derived(todasNotificacoes.filter((n) => !n.lida));
let notificacoesLidas = $derived(todasNotificacoes.filter((n) => n.lida));
let totalCount = $derived(count + (notificacoesFerias?.length || 0));
// Atualizar contador no store
$effect(() => {
const totalNotificacoes =
count + (notificacoesFerias?.length || 0) + (notificacoesAusencias?.length || 0);
notificacoesCount.set(totalNotificacoes);
$notificacoesCount = totalNotificacoes;
});
// Buscar notificações de férias
async function buscarNotificacoesFerias() {
async function buscarNotificacoesFerias(id: Id<'usuarios'> | null) {
try {
const usuarioId = currentUser?.data?._id;
if (usuarioId) {
const notifsFerias = await client.query(api.ferias.obterNotificacoesNaoLidas, {
usuarioId
});
notificacoesFerias = notifsFerias || [];
}
if (!id) return;
const notifsFerias = await client.query(api.ferias.obterNotificacoesNaoLidas, {
usuarioId: id
});
notificacoesFerias = notifsFerias || [];
} catch (e) {
console.error('Erro ao buscar notificações de férias:', e);
}
}
// Buscar notificações de ausências
async function buscarNotificacoesAusencias() {
async function buscarNotificacoesAusencias(id: Id<'usuarios'> | null) {
try {
const usuarioId = currentUser?.data?._id;
if (usuarioId) {
try {
const notifsAusencias = await client.query(api.ausencias.obterNotificacoesNaoLidas, {
usuarioId
});
notificacoesAusencias = notifsAusencias || [];
} catch (queryError: unknown) {
// Silenciar erros de timeout e função não encontrada
const errorMessage =
queryError instanceof Error ? queryError.message : String(queryError);
const isTimeout = errorMessage.includes('timed out') || errorMessage.includes('timeout');
const isFunctionNotFound = errorMessage.includes('Could not find public function');
if (!id) return;
try {
const notifsAusencias = await client.query(api.ausencias.obterNotificacoesNaoLidas, {
usuarioId: id
});
notificacoesAusencias = notifsAusencias || [];
} catch (queryError: unknown) {
// Silenciar erros de timeout e função não encontrada
const errorMessage = queryError instanceof Error ? queryError.message : String(queryError);
const isTimeout = errorMessage.includes('timed out') || errorMessage.includes('timeout');
const isFunctionNotFound = errorMessage.includes('Could not find public function');
if (!isTimeout && !isFunctionNotFound) {
console.error('Erro ao buscar notificações de ausências:', queryError);
}
notificacoesAusencias = [];
if (!isTimeout && !isFunctionNotFound) {
console.error('Erro ao buscar notificações de ausências:', queryError);
}
notificacoesAusencias = [];
}
} catch (e) {
// Erro geral - silenciar se for sobre função não encontrada ou timeout
@@ -106,13 +105,15 @@
}
// Atualizar notificações periodicamente
$effect(() => {
buscarNotificacoesFerias();
buscarNotificacoesAusencias();
onMount(() => {
void buscarNotificacoesFerias(usuarioId);
void buscarNotificacoesAusencias(usuarioId);
const interval = setInterval(() => {
buscarNotificacoesFerias();
buscarNotificacoesAusencias();
void buscarNotificacoesFerias(usuarioId);
void buscarNotificacoesAusencias(usuarioId);
}, 30000); // A cada 30s
return () => clearInterval(interval);
});
@@ -127,30 +128,12 @@
}
}
async function handleMarcarTodasLidas() {
await client.mutation(api.chat.marcarTodasNotificacoesLidas, {});
// Marcar todas as notificações de férias como lidas
for (const notif of notificacoesFerias) {
await client.mutation(api.ferias.marcarComoLida, {
notificacaoId: notif._id
});
}
// Marcar todas as notificações de ausências como lidas
for (const notif of notificacoesAusencias) {
await client.mutation(api.ausencias.marcarComoLida, {
notificacaoId: notif._id
});
}
await buscarNotificacoesFerias();
await buscarNotificacoesAusencias();
}
async function handleLimparTodasNotificacoes() {
limpandoNotificacoes = true;
try {
await client.mutation(api.chat.limparTodasNotificacoes, {});
await buscarNotificacoesFerias();
await buscarNotificacoesAusencias();
await buscarNotificacoesFerias(usuarioId);
await buscarNotificacoesAusencias(usuarioId);
} catch (error) {
console.error('Erro ao limpar notificações:', error);
} finally {
@@ -162,8 +145,8 @@
limpandoNotificacoes = true;
try {
await client.mutation(api.chat.limparNotificacoesNaoLidas, {});
await buscarNotificacoesFerias();
await buscarNotificacoesAusencias();
await buscarNotificacoesFerias(usuarioId);
await buscarNotificacoesAusencias(usuarioId);
} catch (error) {
console.error('Erro ao limpar notificações não lidas:', error);
} finally {
@@ -173,24 +156,24 @@
async function handleClickNotificacao(notificacaoId: string) {
await client.mutation(api.chat.marcarNotificacaoLida, {
notificacaoId: notificacaoId as any
notificacaoId: notificacaoId as Id<'notificacoes'>
});
}
async function handleClickNotificacaoFerias(notificacaoId: string) {
async function handleClickNotificacaoFerias(notificacaoId: Id<'notificacoesFerias'>) {
await client.mutation(api.ferias.marcarComoLida, {
notificacaoId: notificacaoId
});
await buscarNotificacoesFerias();
await buscarNotificacoesFerias(usuarioId);
// Redirecionar para a página de férias
window.location.href = '/recursos-humanos/ferias';
}
async function handleClickNotificacaoAusencias(notificacaoId: string) {
async function handleClickNotificacaoAusencias(notificacaoId: Id<'notificacoesAusencias'>) {
await client.mutation(api.ausencias.marcarComoLida, {
notificacaoId: notificacaoId
});
await buscarNotificacoesAusencias();
await buscarNotificacoesAusencias(usuarioId);
// Redirecionar para a página de perfil na aba de ausências
window.location.href = '/perfil?aba=minhas-ausencias';
}
@@ -204,19 +187,19 @@
}
// Fechar popup ao clicar fora ou pressionar Escape
$effect(() => {
if (!modalOpen) return;
onMount(() => {
function handleClickOutside(event: MouseEvent) {
if (!modalOpen) return;
const target = event.target as HTMLElement;
if (!target.closest('.notification-popup') && !target.closest('.notification-bell')) {
modalOpen = false;
closeModal();
}
}
function handleEscape(event: KeyboardEvent) {
if (!modalOpen) return;
if (event.key === 'Escape') {
modalOpen = false;
closeModal();
}
}
@@ -230,56 +213,32 @@
</script>
<div class="notification-bell relative">
<!-- Botão de Notificação ULTRA MODERNO (igual ao perfil) -->
<button
type="button"
tabindex="0"
class="group relative flex h-14 w-14 items-center justify-center overflow-hidden rounded-2xl transition-all duration-300 hover:scale-105"
style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); box-shadow: 0 8px 24px -4px rgba(102, 126, 234, 0.4);"
onclick={openModal}
aria-label="Notificações"
>
<!-- Efeito de brilho no hover -->
<div
class="absolute inset-0 bg-linear-to-br from-white/0 to-white/20 opacity-0 transition-opacity duration-300 group-hover:opacity-100"
></div>
<!-- Anel de pulso sutil -->
<div
class="absolute inset-0 rounded-2xl"
style="animation: pulse-ring-subtle 3s cubic-bezier(0.4, 0, 0.6, 1) infinite;"
></div>
<!-- Glow effect quando tem notificações -->
{#if count && count > 0}
<div class="bg-error/30 absolute inset-0 animate-pulse rounded-2xl blur-lg"></div>
{/if}
<!-- Ícone do sino PREENCHIDO moderno -->
<Bell
class="relative z-10 h-7 w-7 text-white transition-all duration-300 group-hover:scale-110"
style="filter: drop-shadow(0 2px 8px rgba(0,0,0,0.3)); animation: {count && count > 0
? 'bell-ring 2s ease-in-out infinite'
: 'none'};"
fill="currentColor"
/>
<!-- Badge premium MODERNO com gradiente -->
{#if count + (notificacoesFerias?.length || 0) > 0}
{@const totalCount = count + (notificacoesFerias?.length || 0)}
<span
class="absolute -top-1 -right-1 z-20 flex h-6 w-6 items-center justify-center rounded-full text-[10px] font-black text-white shadow-xl ring-2 ring-white"
style="background: linear-gradient(135deg, #ff416c, #ff4b2b); box-shadow: 0 8px 24px -4px rgba(255, 65, 108, 0.6), 0 4px 12px -2px rgba(255, 75, 43, 0.4); animation: badge-bounce 2s ease-in-out infinite;"
>
<!-- Botão de Notificação (padrão do tema) -->
<div class="indicator">
{#if totalCount > 0}
<span class="indicator-item badge badge-error badge-sm">
{totalCount > 9 ? '9+' : totalCount}
</span>
{/if}
</button>
<button
type="button"
tabindex="0"
class="btn ring-base-200 hover:ring-primary/50 size-10 p-0 ring-2 ring-offset-2 transition-all"
onclick={openModal}
aria-label="Notificações"
aria-expanded={modalOpen}
>
<Bell
class="size-6 transition-colors {totalCount > 0 ? 'text-primary' : 'text-base-content/70'}"
style="animation: {totalCount > 0 ? 'bell-ring 2s ease-in-out infinite' : 'none'};"
/>
</button>
</div>
<!-- Popup Flutuante de Notificações -->
{#if modalOpen}
<div
class="notification-popup bg-base-100 border-base-300 fixed top-24 right-4 z-[100] flex max-h-[calc(100vh-7rem)] w-[calc(100vw-2rem)] max-w-2xl flex-col overflow-hidden rounded-2xl border shadow-2xl backdrop-blur-sm"
class="notification-popup bg-base-100 border-base-300 fixed top-24 right-4 z-100 flex max-h-[calc(100vh-7rem)] w-[calc(100vw-2rem)] max-w-2xl flex-col overflow-hidden rounded-2xl border shadow-2xl backdrop-blur-sm"
style="animation: slideDown 0.2s ease-out;"
>
<!-- Header -->
@@ -310,7 +269,7 @@
Limpar todas
</button>
{/if}
<button type="button" class="btn btn-sm btn-circle btn-ghost" onclick={closeModal}>
<button type="button" class="btn btn-sm btn-circle" onclick={closeModal}>
<X class="h-5 w-5" />
</button>
</div>
@@ -439,17 +398,17 @@
<!-- Notificações de Férias -->
{#if notificacoesFerias.length > 0}
<div class="mb-4">
<h4 class="mb-2 px-2 text-sm font-semibold text-purple-600">Férias</h4>
<h4 class="text-secondary mb-2 px-2 text-sm font-semibold">Férias</h4>
{#each notificacoesFerias as notificacao (notificacao._id)}
<button
type="button"
class="hover:bg-base-200 mb-2 w-full rounded-lg border-l-4 border-purple-600 px-4 py-3 text-left transition-colors"
class="hover:bg-base-200 border-secondary mb-2 w-full rounded-lg border-l-4 px-4 py-3 text-left transition-colors"
onclick={() => handleClickNotificacaoFerias(notificacao._id)}
>
<div class="flex items-start gap-3">
<!-- Ícone -->
<div class="mt-1 shrink-0">
<Calendar class="h-5 w-5 text-purple-600" strokeWidth={2} />
<Calendar class="text-secondary h-5 w-5" strokeWidth={2} />
</div>
<!-- Conteúdo -->
@@ -464,7 +423,7 @@
<!-- Badge -->
<div class="shrink-0">
<div class="badge badge-primary badge-xs"></div>
<div class="badge badge-secondary badge-xs"></div>
</div>
</div>
</button>
@@ -475,17 +434,17 @@
<!-- Notificações de Ausências -->
{#if notificacoesAusencias.length > 0}
<div class="mb-4">
<h4 class="mb-2 px-2 text-sm font-semibold text-orange-600">Ausências</h4>
<h4 class="text-warning mb-2 px-2 text-sm font-semibold">Ausências</h4>
{#each notificacoesAusencias as notificacao (notificacao._id)}
<button
type="button"
class="hover:bg-base-200 mb-2 w-full rounded-lg border-l-4 border-orange-600 px-4 py-3 text-left transition-colors"
class="hover:bg-base-200 border-warning mb-2 w-full rounded-lg border-l-4 px-4 py-3 text-left transition-colors"
onclick={() => handleClickNotificacaoAusencias(notificacao._id)}
>
<div class="flex items-start gap-3">
<!-- Ícone -->
<div class="mt-1 shrink-0">
<Clock class="h-5 w-5 text-orange-600" strokeWidth={2} />
<Clock class="text-warning h-5 w-5" strokeWidth={2} />
</div>
<!-- Conteúdo -->
@@ -539,28 +498,6 @@
</div>
<style>
@keyframes badge-bounce {
0%,
100% {
transform: scale(1);
}
50% {
transform: scale(1.1);
}
}
@keyframes pulse-ring-subtle {
0%,
100% {
opacity: 0.1;
transform: scale(1);
}
50% {
opacity: 0.3;
transform: scale(1.05);
}
}
@keyframes bell-ring {
0%,
100% {