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:
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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)
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user