diff --git a/apps/web/src/lib/components/chat/NotificationBell.svelte b/apps/web/src/lib/components/chat/NotificationBell.svelte index 6bf2489..76505b6 100644 --- a/apps/web/src/lib/components/chat/NotificationBell.svelte +++ b/apps/web/src/lib/components/chat/NotificationBell.svelte @@ -6,27 +6,38 @@ import { ptBR } from "date-fns/locale"; import { onMount } from "svelte"; import { authStore } from "$lib/stores/auth.svelte"; - import { Bell, Mail, AtSign, Users, Calendar, Clock, BellOff } from "lucide-svelte"; + import { Bell, Mail, AtSign, Users, Calendar, Clock, BellOff, Trash2, X } from "lucide-svelte"; // Queries e Client const client = useConvexClient(); - const notificacoesQuery = useQuery(api.chat.obterNotificacoes, { - apenasPendentes: true, - }); + // Query para contar apenas não lidas (para o badge) const countQuery = useQuery(api.chat.contarNotificacoesNaoLidas, {}); + // Query para obter TODAS as notificações (para o popup) + const todasNotificacoesQuery = useQuery(api.chat.obterNotificacoes, { + apenasPendentes: false, + }); - let dropdownOpen = $state(false); + let modalOpen = $state(false); let notificacoesFerias = $state>([]); let notificacoesAusencias = $state>([]); + let limpandoNotificacoes = $state(false); // Helpers para obter valores das queries const count = $derived( (typeof countQuery === "number" ? countQuery : countQuery?.data) ?? 0 ); - const notificacoes = $derived( - (Array.isArray(notificacoesQuery) - ? notificacoesQuery - : notificacoesQuery?.data) ?? [] + const todasNotificacoes = $derived( + (Array.isArray(todasNotificacoesQuery) + ? todasNotificacoesQuery + : todasNotificacoesQuery?.data) ?? [] + ); + + // Separar notificações lidas e não lidas + const notificacoesNaoLidas = $derived( + todasNotificacoes.filter((n) => !n.lida) + ); + const notificacoesLidas = $derived( + todasNotificacoes.filter((n) => n.lida) ); // Atualizar contador no store @@ -122,16 +133,40 @@ notificacaoId: notif._id, }); } - dropdownOpen = false; await buscarNotificacoesFerias(); await buscarNotificacoesAusencias(); } + async function handleLimparTodasNotificacoes() { + limpandoNotificacoes = true; + try { + await client.mutation(api.chat.limparTodasNotificacoes, {}); + await buscarNotificacoesFerias(); + await buscarNotificacoesAusencias(); + } catch (error) { + console.error("Erro ao limpar notificações:", error); + } finally { + limpandoNotificacoes = false; + } + } + + async function handleLimparNotificacoesNaoLidas() { + limpandoNotificacoes = true; + try { + await client.mutation(api.chat.limparNotificacoesNaoLidas, {}); + await buscarNotificacoesFerias(); + await buscarNotificacoesAusencias(); + } catch (error) { + console.error("Erro ao limpar notificações não lidas:", error); + } finally { + limpandoNotificacoes = false; + } + } + async function handleClickNotificacao(notificacaoId: string) { await client.mutation(api.chat.marcarNotificacaoLida, { notificacaoId: notificacaoId as any, }); - dropdownOpen = false; } async function handleClickNotificacaoFerias(notificacaoId: string) { @@ -139,7 +174,6 @@ notificacaoId: notificacaoId, }); await buscarNotificacoesFerias(); - dropdownOpen = false; // Redirecionar para a página de férias window.location.href = "/recursos-humanos/ferias"; } @@ -149,37 +183,52 @@ notificacaoId: notificacaoId, }); await buscarNotificacoesAusencias(); - dropdownOpen = false; // Redirecionar para a página de perfil na aba de ausências window.location.href = "/perfil?aba=minhas-ausencias"; } - function toggleDropdown() { - dropdownOpen = !dropdownOpen; + function openModal() { + modalOpen = true; } - // Fechar dropdown ao clicar fora - onMount(() => { + function closeModal() { + modalOpen = false; + } + + // Fechar popup ao clicar fora ou pressionar Escape + $effect(() => { + if (!modalOpen) return; + function handleClickOutside(event: MouseEvent) { const target = event.target as HTMLElement; - if (!target.closest(".notification-bell")) { - dropdownOpen = false; + if (!target.closest(".notification-popup") && !target.closest(".notification-bell")) { + modalOpen = false; + } + } + + function handleEscape(event: KeyboardEvent) { + if (event.key === "Escape") { + modalOpen = false; } } document.addEventListener("click", handleClickOutside); - return () => document.removeEventListener("click", handleClickOutside); + document.addEventListener("keydown", handleEscape); + return () => { + document.removeEventListener("click", handleClickOutside); + document.removeEventListener("keydown", handleEscape); + }; }); - - {/if} + + + {#if todasNotificacoes.length > 0 || notificacoesFerias.length > 0 || notificacoesAusencias.length > 0} +
+
+ + Total: {todasNotificacoes.length + notificacoesFerias.length + notificacoesAusencias.length} notificações + + {#if notificacoesNaoLidas.length > 0} + + {notificacoesNaoLidas.length} não lidas + + {/if} +
+
+ {/if} + +{/if} + diff --git a/apps/web/src/routes/(dashboard)/ti/notificacoes/+page.svelte b/apps/web/src/routes/(dashboard)/ti/notificacoes/+page.svelte index ff7e182..78a5641 100644 --- a/apps/web/src/routes/(dashboard)/ti/notificacoes/+page.svelte +++ b/apps/web/src/routes/(dashboard)/ti/notificacoes/+page.svelte @@ -208,6 +208,21 @@ templates.find((t) => t._id === templateId), ); + // Função para renderizar template com variáveis (similar à função do backend) + 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; + } + // Função para mostrar mensagens function mostrarMensagem(tipo: "success" | "error" | "info", texto: string) { mensagem = { tipo, texto }; @@ -733,8 +748,11 @@ ); if (conversaId) { - const mensagem = usarTemplate - ? templateSelecionado?.corpo || "" + const mensagem = usarTemplate && templateSelecionado + ? renderizarTemplate(templateSelecionado.corpo, { + nome: destinatario.nome, + matricula: destinatario.matricula || "", + }) : mensagemPersonalizada; if (agendadaPara) { @@ -988,10 +1006,12 @@ ); if (conversaId) { - // Para templates, usar corpo direto (o backend já faz substituição via email) - // Para mensagem personalizada, usar diretamente - const mensagem = usarTemplate - ? templateSelecionado?.corpo || "" + // Renderizar template com variáveis do destinatário + const mensagem = usarTemplate && templateSelecionado + ? renderizarTemplate(templateSelecionado.corpo, { + nome: destinatario.nome, + matricula: destinatario.matricula || "", + }) : mensagemPersonalizada; if (agendadaPara) { diff --git a/packages/backend/convex/chat.ts b/packages/backend/convex/chat.ts index 2ef4646..b2f024d 100644 --- a/packages/backend/convex/chat.ts +++ b/packages/backend/convex/chat.ts @@ -855,6 +855,54 @@ export const marcarTodasNotificacoesLidas = mutation({ }, }); +/** + * Deleta todas as notificações do usuário + * SEGURANÇA: Usuário só pode deletar suas próprias notificações + */ +export const limparTodasNotificacoes = mutation({ + args: {}, + handler: async (ctx) => { + const usuarioAtual = await getUsuarioAutenticado(ctx); + if (!usuarioAtual) throw new Error("Não autenticado"); + + const notificacoes = await ctx.db + .query("notificacoes") + .withIndex("by_usuario", (q) => q.eq("usuarioId", usuarioAtual._id)) + .collect(); + + for (const notificacao of notificacoes) { + await ctx.db.delete(notificacao._id); + } + + return { excluidas: notificacoes.length }; + }, +}); + +/** + * Deleta apenas as notificações não lidas do usuário + * SEGURANÇA: Usuário só pode deletar suas próprias notificações + */ +export const limparNotificacoesNaoLidas = mutation({ + args: {}, + handler: async (ctx) => { + const usuarioAtual = await getUsuarioAutenticado(ctx); + if (!usuarioAtual) throw new Error("Não autenticado"); + + const notificacoes = await ctx.db + .query("notificacoes") + .withIndex("by_usuario_lida", (q) => + q.eq("usuarioId", usuarioAtual._id).eq("lida", false) + ) + .collect(); + + for (const notificacao of notificacoes) { + await ctx.db.delete(notificacao._id); + } + + return { excluidas: notificacoes.length }; + }, +}); + /** * Deleta uma mensagem (soft delete) */