feat: enhance notification management with new clearing functionalities

- Added functionality to clear all notifications and clear unread notifications for improved user control.
- Updated NotificationBell component to support modal display for notifications, enhancing user experience.
- Refactored notification handling to separate read and unread notifications, providing clearer organization.
- Introduced new UI elements for managing notifications, including buttons for clearing notifications directly from the modal.
- Improved backend mutations to handle notification deletion securely, ensuring users can only delete their own notifications.
This commit is contained in:
2025-11-05 15:06:41 -03:00
parent 6166043735
commit 1b02ea7c22
3 changed files with 394 additions and 157 deletions

View File

@@ -6,27 +6,38 @@
import { ptBR } from "date-fns/locale"; import { ptBR } from "date-fns/locale";
import { onMount } from "svelte"; import { onMount } from "svelte";
import { authStore } from "$lib/stores/auth.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 // Queries e Client
const client = useConvexClient(); const client = useConvexClient();
const notificacoesQuery = useQuery(api.chat.obterNotificacoes, { // Query para contar apenas não lidas (para o badge)
apenasPendentes: true,
});
const countQuery = useQuery(api.chat.contarNotificacoesNaoLidas, {}); 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<Array<{ _id: string; mensagem: string; tipo: string; _creationTime: number }>>([]); let notificacoesFerias = $state<Array<{ _id: string; mensagem: string; tipo: string; _creationTime: number }>>([]);
let notificacoesAusencias = $state<Array<{ _id: string; mensagem: string; tipo: string; _creationTime: number }>>([]); let notificacoesAusencias = $state<Array<{ _id: string; mensagem: string; tipo: string; _creationTime: number }>>([]);
let limpandoNotificacoes = $state(false);
// Helpers para obter valores das queries // Helpers para obter valores das queries
const count = $derived( const count = $derived(
(typeof countQuery === "number" ? countQuery : countQuery?.data) ?? 0 (typeof countQuery === "number" ? countQuery : countQuery?.data) ?? 0
); );
const notificacoes = $derived( const todasNotificacoes = $derived(
(Array.isArray(notificacoesQuery) (Array.isArray(todasNotificacoesQuery)
? notificacoesQuery ? todasNotificacoesQuery
: notificacoesQuery?.data) ?? [] : 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 // Atualizar contador no store
@@ -122,16 +133,40 @@
notificacaoId: notif._id, notificacaoId: notif._id,
}); });
} }
dropdownOpen = false;
await buscarNotificacoesFerias(); await buscarNotificacoesFerias();
await buscarNotificacoesAusencias(); 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) { async function handleClickNotificacao(notificacaoId: string) {
await client.mutation(api.chat.marcarNotificacaoLida, { await client.mutation(api.chat.marcarNotificacaoLida, {
notificacaoId: notificacaoId as any, notificacaoId: notificacaoId as any,
}); });
dropdownOpen = false;
} }
async function handleClickNotificacaoFerias(notificacaoId: string) { async function handleClickNotificacaoFerias(notificacaoId: string) {
@@ -139,7 +174,6 @@
notificacaoId: notificacaoId, notificacaoId: notificacaoId,
}); });
await buscarNotificacoesFerias(); await buscarNotificacoesFerias();
dropdownOpen = false;
// Redirecionar para a página de férias // Redirecionar para a página de férias
window.location.href = "/recursos-humanos/ferias"; window.location.href = "/recursos-humanos/ferias";
} }
@@ -149,37 +183,52 @@
notificacaoId: notificacaoId, notificacaoId: notificacaoId,
}); });
await buscarNotificacoesAusencias(); await buscarNotificacoesAusencias();
dropdownOpen = false;
// Redirecionar para a página de perfil na aba de ausências // Redirecionar para a página de perfil na aba de ausências
window.location.href = "/perfil?aba=minhas-ausencias"; window.location.href = "/perfil?aba=minhas-ausencias";
} }
function toggleDropdown() { function openModal() {
dropdownOpen = !dropdownOpen; modalOpen = true;
} }
// Fechar dropdown ao clicar fora function closeModal() {
onMount(() => { modalOpen = false;
}
// Fechar popup ao clicar fora ou pressionar Escape
$effect(() => {
if (!modalOpen) return;
function handleClickOutside(event: MouseEvent) { function handleClickOutside(event: MouseEvent) {
const target = event.target as HTMLElement; const target = event.target as HTMLElement;
if (!target.closest(".notification-bell")) { if (!target.closest(".notification-popup") && !target.closest(".notification-bell")) {
dropdownOpen = false; modalOpen = false;
}
}
function handleEscape(event: KeyboardEvent) {
if (event.key === "Escape") {
modalOpen = false;
} }
} }
document.addEventListener("click", handleClickOutside); document.addEventListener("click", handleClickOutside);
return () => document.removeEventListener("click", handleClickOutside); document.addEventListener("keydown", handleEscape);
return () => {
document.removeEventListener("click", handleClickOutside);
document.removeEventListener("keydown", handleEscape);
};
}); });
</script> </script>
<div class="dropdown dropdown-end notification-bell"> <div class="notification-bell relative">
<!-- Botão de Notificação ULTRA MODERNO (igual ao perfil) --> <!-- Botão de Notificação ULTRA MODERNO (igual ao perfil) -->
<button <button
type="button" type="button"
tabindex="0" tabindex="0"
class="relative flex items-center justify-center w-14 h-14 rounded-2xl overflow-hidden group transition-all duration-300 hover:scale-105" class="relative flex items-center justify-center w-14 h-14 rounded-2xl overflow-hidden group 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);" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); box-shadow: 0 8px 24px -4px rgba(102, 126, 234, 0.4);"
onclick={toggleDropdown} onclick={openModal}
aria-label="Notificações" aria-label="Notificações"
> >
<!-- Efeito de brilho no hover --> <!-- Efeito de brilho no hover -->
@@ -222,155 +271,263 @@
{/if} {/if}
</button> </button>
{#if dropdownOpen} <!-- Popup Flutuante de Notificações -->
<!-- svelte-ignore a11y_no_noninteractive_tabindex --> {#if modalOpen}
<div <div class="notification-popup fixed right-4 top-24 z-[100] w-[calc(100vw-2rem)] max-w-2xl max-h-[calc(100vh-7rem)] flex flex-col bg-base-100 rounded-2xl shadow-2xl border border-base-300 overflow-hidden backdrop-blur-sm" style="animation: slideDown 0.2s ease-out;">
tabindex="0" <!-- Header -->
class="dropdown-content z-50 mt-3 w-80 max-h-96 overflow-auto rounded-box bg-base-100 p-2 shadow-2xl border border-base-300" <div class="flex items-center justify-between px-6 py-4 border-b border-base-300 bg-gradient-to-r from-primary/5 to-primary/10">
> <h3 class="text-2xl font-bold text-primary">Notificações</h3>
<!-- Header --> <div class="flex items-center gap-2">
<div {#if notificacoesNaoLidas.length > 0}
class="flex items-center justify-between px-4 py-2 border-b border-base-300"
>
<h3 class="text-lg font-semibold">Notificações</h3>
{#if count > 0}
<button <button
type="button" type="button"
class="btn btn-ghost btn-xs" class="btn btn-sm btn-ghost"
onclick={handleMarcarTodasLidas} onclick={handleLimparNotificacoesNaoLidas}
disabled={limpandoNotificacoes}
> >
Marcar todas como lidas <Trash2 class="w-4 h-4" />
Limpar não lidas
</button> </button>
{/if} {/if}
{#if todasNotificacoes.length > 0}
<button
type="button"
class="btn btn-sm btn-error btn-outline"
onclick={handleLimparTodasNotificacoes}
disabled={limpandoNotificacoes}
>
<Trash2 class="w-4 h-4" />
Limpar todas
</button>
{/if}
<button
type="button"
class="btn btn-sm btn-circle btn-ghost"
onclick={closeModal}
>
<X class="w-5 h-5" />
</button>
</div> </div>
</div>
<!-- Lista de notificações --> <!-- Lista de notificações -->
<div class="py-2"> <div class="flex-1 overflow-y-auto px-2 py-4">
{#if notificacoes.length > 0} {#if todasNotificacoes.length > 0 || notificacoesFerias.length > 0 || notificacoesAusencias.length > 0}
{#each notificacoes.slice(0, 10) as notificacao (notificacao._id)} <!-- Notificações não lidas -->
<button {#if notificacoesNaoLidas.length > 0}
type="button" <div class="mb-4">
class="w-full text-left px-4 py-3 hover:bg-base-200 rounded-lg transition-colors" <h4 class="text-sm font-semibold text-primary mb-2 px-2">Não lidas</h4>
onclick={() => handleClickNotificacao(notificacao._id)} {#each notificacoesNaoLidas as notificacao (notificacao._id)}
> <button
<div class="flex items-start gap-3"> type="button"
<!-- Ícone --> class="w-full text-left px-4 py-3 hover:bg-base-200 rounded-lg transition-colors mb-2 border-l-4 border-primary"
<div class="flex-shrink-0 mt-1"> onclick={() => handleClickNotificacao(notificacao._id)}
{#if notificacao.tipo === "nova_mensagem"} >
<Mail class="w-5 h-5 text-primary" strokeWidth={1.5} /> <div class="flex items-start gap-3">
{:else if notificacao.tipo === "mencao"} <!-- Ícone -->
<AtSign class="w-5 h-5 text-warning" strokeWidth={1.5} /> <div class="flex-shrink-0 mt-1">
{:else} {#if notificacao.tipo === "nova_mensagem"}
<Users class="w-5 h-5 text-info" strokeWidth={1.5} /> <Mail class="w-5 h-5 text-primary" strokeWidth={1.5} />
{/if} {:else if notificacao.tipo === "mencao"}
</div> <AtSign class="w-5 h-5 text-warning" strokeWidth={1.5} />
{:else}
<Users class="w-5 h-5 text-info" strokeWidth={1.5} />
{/if}
</div>
<!-- Conteúdo --> <!-- Conteúdo -->
<div class="flex-1 min-w-0"> <div class="flex-1 min-w-0">
<p class="text-sm font-medium text-base-content"> {#if notificacao.tipo === "nova_mensagem" && notificacao.remetente}
{notificacao.titulo} <p class="text-sm font-semibold text-primary">
</p> {notificacao.remetente.nome}
<p class="text-xs text-base-content/70 truncate"> </p>
{notificacao.descricao} <p class="text-xs text-base-content/70 mt-1 line-clamp-2">
</p> {notificacao.descricao}
<p class="text-xs text-base-content/50 mt-1"> </p>
{formatarTempo(notificacao.criadaEm)} {:else if notificacao.tipo === "mencao" && notificacao.remetente}
</p> <p class="text-sm font-semibold text-warning">
</div> {notificacao.remetente.nome} mencionou você
</p>
<p class="text-xs text-base-content/70 mt-1 line-clamp-2">
{notificacao.descricao}
</p>
{:else}
<p class="text-sm font-semibold text-base-content">
{notificacao.titulo}
</p>
<p class="text-xs text-base-content/70 mt-1 line-clamp-2">
{notificacao.descricao}
</p>
{/if}
<p class="text-xs text-base-content/50 mt-1">
{formatarTempo(notificacao.criadaEm)}
</p>
</div>
<!-- Indicador de não lida --> <!-- Indicador de não lida -->
{#if !notificacao.lida} <div class="flex-shrink-0">
<div class="flex-shrink-0"> <div class="w-2 h-2 rounded-full bg-primary"></div>
<div class="w-2 h-2 rounded-full bg-primary"></div> </div>
</div> </div>
{/if} </button>
</div> {/each}
</button> </div>
{/each}
{/if}
<!-- Notificações de Férias -->
{#if notificacoesFerias.length > 0}
{#if notificacoes.length > 0}
<div class="divider my-2 text-xs">Férias</div>
{/if} {/if}
{#each notificacoesFerias.slice(0, 5) as notificacao (notificacao._id)}
<button
type="button"
class="w-full text-left px-4 py-3 hover:bg-base-200 rounded-lg transition-colors"
onclick={() => handleClickNotificacaoFerias(notificacao._id)}
>
<div class="flex items-start gap-3">
<!-- Ícone -->
<div class="flex-shrink-0 mt-1">
<Calendar class="w-5 h-5 text-purple-600" strokeWidth={2} />
</div>
<!-- Conteúdo --> <!-- Notificações lidas -->
<div class="flex-1 min-w-0"> {#if notificacoesLidas.length > 0}
<p class="text-sm font-medium text-base-content"> <div class="mb-4">
{notificacao.mensagem} <h4 class="text-sm font-semibold text-base-content/60 mb-2 px-2">Lidas</h4>
</p> {#each notificacoesLidas as notificacao (notificacao._id)}
<p class="text-xs text-base-content/50 mt-1"> <button
{formatarTempo(notificacao._creationTime)} type="button"
</p> class="w-full text-left px-4 py-3 hover:bg-base-200 rounded-lg transition-colors mb-2 opacity-75"
</div> onclick={() => handleClickNotificacao(notificacao._id)}
>
<div class="flex items-start gap-3">
<!-- Ícone -->
<div class="flex-shrink-0 mt-1">
{#if notificacao.tipo === "nova_mensagem"}
<Mail class="w-5 h-5 text-primary/60" strokeWidth={1.5} />
{:else if notificacao.tipo === "mencao"}
<AtSign class="w-5 h-5 text-warning/60" strokeWidth={1.5} />
{:else}
<Users class="w-5 h-5 text-info/60" strokeWidth={1.5} />
{/if}
</div>
<!-- Badge --> <!-- Conteúdo -->
<div class="flex-shrink-0"> <div class="flex-1 min-w-0">
<div class="badge badge-primary badge-xs"></div> {#if notificacao.tipo === "nova_mensagem" && notificacao.remetente}
</div> <p class="text-sm font-medium text-primary/70">
</div> {notificacao.remetente.nome}
</button> </p>
{/each} <p class="text-xs text-base-content/60 mt-1 line-clamp-2">
{/if} {notificacao.descricao}
</p>
<!-- Notificações de Ausências --> {:else if notificacao.tipo === "mencao" && notificacao.remetente}
{#if notificacoesAusencias.length > 0} <p class="text-sm font-medium text-warning/70">
{#if notificacoes.length > 0 || notificacoesFerias.length > 0} {notificacao.remetente.nome} mencionou você
<div class="divider my-2 text-xs">Ausências</div> </p>
<p class="text-xs text-base-content/60 mt-1 line-clamp-2">
{notificacao.descricao}
</p>
{:else}
<p class="text-sm font-medium text-base-content/70">
{notificacao.titulo}
</p>
<p class="text-xs text-base-content/60 mt-1 line-clamp-2">
{notificacao.descricao}
</p>
{/if}
<p class="text-xs text-base-content/50 mt-1">
{formatarTempo(notificacao.criadaEm)}
</p>
</div>
</div>
</button>
{/each}
</div>
{/if} {/if}
{#each notificacoesAusencias.slice(0, 5) as notificacao (notificacao._id)}
<button
type="button"
class="w-full text-left px-4 py-3 hover:bg-base-200 rounded-lg transition-colors"
onclick={() => handleClickNotificacaoAusencias(notificacao._id)}
>
<div class="flex items-start gap-3">
<!-- Ícone -->
<div class="flex-shrink-0 mt-1">
<Clock class="w-5 h-5 text-orange-600" strokeWidth={2} />
</div>
<!-- Conteúdo --> <!-- Notificações de Férias -->
<div class="flex-1 min-w-0"> {#if notificacoesFerias.length > 0}
<p class="text-sm font-medium text-base-content"> <div class="mb-4">
{notificacao.mensagem} <h4 class="text-sm font-semibold text-purple-600 mb-2 px-2">Férias</h4>
</p> {#each notificacoesFerias as notificacao (notificacao._id)}
<p class="text-xs text-base-content/50 mt-1"> <button
{formatarTempo(notificacao._creationTime)} type="button"
</p> class="w-full text-left px-4 py-3 hover:bg-base-200 rounded-lg transition-colors mb-2 border-l-4 border-purple-600"
</div> onclick={() => handleClickNotificacaoFerias(notificacao._id)}
>
<div class="flex items-start gap-3">
<!-- Ícone -->
<div class="flex-shrink-0 mt-1">
<Calendar class="w-5 h-5 text-purple-600" strokeWidth={2} />
</div>
<!-- Badge --> <!-- Conteúdo -->
<div class="flex-shrink-0"> <div class="flex-1 min-w-0">
<div class="badge badge-warning badge-xs"></div> <p class="text-sm font-medium text-base-content">
</div> {notificacao.mensagem}
</div> </p>
</button> <p class="text-xs text-base-content/50 mt-1">
{/each} {formatarTempo(notificacao._creationTime)}
{/if} </p>
</div>
<!-- Sem notificações --> <!-- Badge -->
{#if notificacoes.length === 0 && notificacoesFerias.length === 0 && notificacoesAusencias.length === 0} <div class="flex-shrink-0">
<div class="px-4 py-8 text-center text-base-content/50"> <div class="badge badge-primary badge-xs"></div>
<BellOff class="w-12 h-12 mx-auto mb-2 opacity-50" strokeWidth={1.5} /> </div>
<p class="text-sm">Nenhuma notificação</p> </div>
</button>
{/each}
</div>
{/if}
<!-- Notificações de Ausências -->
{#if notificacoesAusencias.length > 0}
<div class="mb-4">
<h4 class="text-sm font-semibold text-orange-600 mb-2 px-2">Ausências</h4>
{#each notificacoesAusencias as notificacao (notificacao._id)}
<button
type="button"
class="w-full text-left px-4 py-3 hover:bg-base-200 rounded-lg transition-colors mb-2 border-l-4 border-orange-600"
onclick={() => handleClickNotificacaoAusencias(notificacao._id)}
>
<div class="flex items-start gap-3">
<!-- Ícone -->
<div class="flex-shrink-0 mt-1">
<Clock class="w-5 h-5 text-orange-600" strokeWidth={2} />
</div>
<!-- Conteúdo -->
<div class="flex-1 min-w-0">
<p class="text-sm font-medium text-base-content">
{notificacao.mensagem}
</p>
<p class="text-xs text-base-content/50 mt-1">
{formatarTempo(notificacao._creationTime)}
</p>
</div>
<!-- Badge -->
<div class="flex-shrink-0">
<div class="badge badge-warning badge-xs"></div>
</div>
</div>
</button>
{/each}
</div>
{/if}
{:else}
<!-- Sem notificações -->
<div class="px-4 py-12 text-center text-base-content/50">
<BellOff class="w-16 h-16 mx-auto mb-4 opacity-50" strokeWidth={1.5} />
<p class="text-base font-medium">Nenhuma notificação</p>
<p class="text-sm mt-1">Você está em dia!</p>
</div> </div>
{/if} {/if}
</div> </div>
</div>
{/if} <!-- Footer com estatísticas -->
{#if todasNotificacoes.length > 0 || notificacoesFerias.length > 0 || notificacoesAusencias.length > 0}
<div class="px-6 py-4 border-t border-base-300 bg-base-200/50">
<div class="flex items-center justify-between text-xs text-base-content/60">
<span>
Total: {todasNotificacoes.length + notificacoesFerias.length + notificacoesAusencias.length} notificações
</span>
{#if notificacoesNaoLidas.length > 0}
<span class="text-primary font-semibold">
{notificacoesNaoLidas.length} não lidas
</span>
{/if}
</div>
</div>
{/if}
</div>
{/if}
</div> </div>
<style> <style>
@@ -413,4 +570,16 @@
transform: rotate(0deg); transform: rotate(0deg);
} }
} }
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style> </style>

View File

@@ -208,6 +208,21 @@
templates.find((t) => t._id === templateId), 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, string>,
): 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 // Função para mostrar mensagens
function mostrarMensagem(tipo: "success" | "error" | "info", texto: string) { function mostrarMensagem(tipo: "success" | "error" | "info", texto: string) {
mensagem = { tipo, texto }; mensagem = { tipo, texto };
@@ -733,8 +748,11 @@
); );
if (conversaId) { if (conversaId) {
const mensagem = usarTemplate const mensagem = usarTemplate && templateSelecionado
? templateSelecionado?.corpo || "" ? renderizarTemplate(templateSelecionado.corpo, {
nome: destinatario.nome,
matricula: destinatario.matricula || "",
})
: mensagemPersonalizada; : mensagemPersonalizada;
if (agendadaPara) { if (agendadaPara) {
@@ -988,10 +1006,12 @@
); );
if (conversaId) { if (conversaId) {
// Para templates, usar corpo direto (o backend já faz substituição via email) // Renderizar template com variáveis do destinatário
// Para mensagem personalizada, usar diretamente const mensagem = usarTemplate && templateSelecionado
const mensagem = usarTemplate ? renderizarTemplate(templateSelecionado.corpo, {
? templateSelecionado?.corpo || "" nome: destinatario.nome,
matricula: destinatario.matricula || "",
})
: mensagemPersonalizada; : mensagemPersonalizada;
if (agendadaPara) { if (agendadaPara) {

View File

@@ -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) * Deleta uma mensagem (soft delete)
*/ */