feat: implement vacation management system with request approval, notification handling, and employee training tracking; enhance UI components for improved user experience

This commit is contained in:
2025-10-29 22:05:29 -03:00
parent f219340cd8
commit 16bcd2ac25
21 changed files with 3910 additions and 617 deletions

View File

@@ -0,0 +1,378 @@
<script lang="ts">
import { useConvexClient } from "convex-svelte";
import { api } from "@sgse-app/backend/convex/_generated/api";
interface Periodo {
dataInicio: string;
dataFim: string;
diasCorridos: number;
}
interface Props {
solicitacao: any;
gestorId: string;
onSucesso?: () => void;
onCancelar?: () => void;
}
let { solicitacao, gestorId, onSucesso, onCancelar }: Props = $props();
const client = useConvexClient();
let modoAjuste = $state(false);
let periodos = $state<Periodo[]>([]);
let motivoReprovacao = $state("");
let processando = $state(false);
let erro = $state("");
$effect(() => {
if (modoAjuste && periodos.length === 0) {
periodos = solicitacao.periodos.map((p: any) => ({...p}));
}
});
function calcularDias(periodo: Periodo) {
if (!periodo.dataInicio || !periodo.dataFim) {
periodo.diasCorridos = 0;
return;
}
const inicio = new Date(periodo.dataInicio);
const fim = new Date(periodo.dataFim);
if (fim < inicio) {
erro = "Data final não pode ser anterior à data inicial";
periodo.diasCorridos = 0;
return;
}
const diff = fim.getTime() - inicio.getTime();
const dias = Math.ceil(diff / (1000 * 60 * 60 * 24)) + 1;
periodo.diasCorridos = dias;
erro = "";
}
async function aprovar() {
try {
processando = true;
erro = "";
await client.mutation(api.ferias.aprovar, {
solicitacaoId: solicitacao._id,
gestorId: gestorId as any,
});
if (onSucesso) onSucesso();
} catch (e: any) {
erro = e.message || "Erro ao aprovar solicitação";
} finally {
processando = false;
}
}
async function reprovar() {
if (!motivoReprovacao.trim()) {
erro = "Informe o motivo da reprovação";
return;
}
try {
processando = true;
erro = "";
await client.mutation(api.ferias.reprovar, {
solicitacaoId: solicitacao._id,
gestorId: gestorId as any,
motivoReprovacao,
});
if (onSucesso) onSucesso();
} catch (e: any) {
erro = e.message || "Erro ao reprovar solicitação";
} finally {
processando = false;
}
}
async function ajustarEAprovar() {
try {
processando = true;
erro = "";
await client.mutation(api.ferias.ajustarEAprovar, {
solicitacaoId: solicitacao._id,
gestorId: gestorId as any,
novosPeriodos: periodos,
});
if (onSucesso) onSucesso();
} catch (e: any) {
erro = e.message || "Erro ao ajustar e aprovar solicitação";
} finally {
processando = false;
}
}
function getStatusBadge(status: string) {
const badges: Record<string, string> = {
aguardando_aprovacao: "badge-warning",
aprovado: "badge-success",
reprovado: "badge-error",
data_ajustada_aprovada: "badge-info",
};
return badges[status] || "badge-neutral";
}
function getStatusTexto(status: string) {
const textos: Record<string, string> = {
aguardando_aprovacao: "Aguardando Aprovação",
aprovado: "Aprovado",
reprovado: "Reprovado",
data_ajustada_aprovada: "Data Ajustada e Aprovada",
};
return textos[status] || status;
}
function formatarData(data: number) {
return new Date(data).toLocaleString("pt-BR");
}
</script>
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<div class="flex items-start justify-between mb-4">
<div>
<h2 class="card-title text-2xl">
{solicitacao.funcionario?.nome || "Funcionário"}
</h2>
<p class="text-sm text-base-content/70 mt-1">
Ano de Referência: {solicitacao.anoReferencia}
</p>
</div>
<div class={`badge ${getStatusBadge(solicitacao.status)} badge-lg`}>
{getStatusTexto(solicitacao.status)}
</div>
</div>
<!-- Períodos Solicitados -->
<div class="mt-4">
<h3 class="font-semibold text-lg mb-3">Períodos Solicitados</h3>
<div class="space-y-2">
{#each solicitacao.periodos as periodo, index}
<div class="flex items-center gap-4 p-3 bg-base-200 rounded-lg">
<div class="badge badge-primary">{index + 1}</div>
<div class="flex-1 grid grid-cols-3 gap-2 text-sm">
<div>
<span class="text-base-content/70">Início:</span>
<span class="font-semibold ml-1">{new Date(periodo.dataInicio).toLocaleDateString("pt-BR")}</span>
</div>
<div>
<span class="text-base-content/70">Fim:</span>
<span class="font-semibold ml-1">{new Date(periodo.dataFim).toLocaleDateString("pt-BR")}</span>
</div>
<div>
<span class="text-base-content/70">Dias:</span>
<span class="font-bold ml-1 text-primary">{periodo.diasCorridos}</span>
</div>
</div>
</div>
{/each}
</div>
</div>
<!-- Observações -->
{#if solicitacao.observacao}
<div class="mt-4">
<h3 class="font-semibold mb-2">Observações</h3>
<div class="p-3 bg-base-200 rounded-lg text-sm">
{solicitacao.observacao}
</div>
</div>
{/if}
<!-- Histórico -->
{#if solicitacao.historicoAlteracoes && solicitacao.historicoAlteracoes.length > 0}
<div class="mt-4">
<h3 class="font-semibold mb-2">Histórico</h3>
<div class="space-y-1">
{#each solicitacao.historicoAlteracoes as hist}
<div class="text-xs text-base-content/70 flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span>{formatarData(hist.data)}</span>
<span>-</span>
<span>{hist.acao}</span>
</div>
{/each}
</div>
</div>
{/if}
<!-- Ações (apenas para status aguardando_aprovacao) -->
{#if solicitacao.status === "aguardando_aprovacao"}
<div class="divider mt-6"></div>
{#if !modoAjuste}
<!-- Modo Normal -->
<div class="space-y-4">
<div class="flex flex-wrap gap-2">
<button
type="button"
class="btn btn-success gap-2"
onclick={aprovar}
disabled={processando}
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
Aprovar
</button>
<button
type="button"
class="btn btn-info gap-2"
onclick={() => modoAjuste = true}
disabled={processando}
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
Ajustar Datas e Aprovar
</button>
</div>
<!-- Reprovar -->
<div class="card bg-base-200">
<div class="card-body p-4">
<h4 class="font-semibold text-sm mb-2">Reprovar Solicitação</h4>
<textarea
class="textarea textarea-bordered textarea-sm mb-2"
placeholder="Motivo da reprovação..."
bind:value={motivoReprovacao}
rows="2"
></textarea>
<button
type="button"
class="btn btn-error btn-sm gap-2"
onclick={reprovar}
disabled={processando || !motivoReprovacao.trim()}
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
Reprovar
</button>
</div>
</div>
</div>
{:else}
<!-- Modo Ajuste -->
<div class="space-y-4">
<h4 class="font-semibold">Ajustar Períodos</h4>
{#each periodos as periodo, index}
<div class="card bg-base-200">
<div class="card-body p-4">
<h5 class="font-medium mb-2">Período {index + 1}</h5>
<div class="grid grid-cols-3 gap-3">
<div class="form-control">
<label class="label" for={`ajuste-inicio-${index}`}>
<span class="label-text text-xs">Início</span>
</label>
<input
id={`ajuste-inicio-${index}`}
type="date"
class="input input-bordered input-sm"
bind:value={periodo.dataInicio}
onchange={() => calcularDias(periodo)}
/>
</div>
<div class="form-control">
<label class="label" for={`ajuste-fim-${index}`}>
<span class="label-text text-xs">Fim</span>
</label>
<input
id={`ajuste-fim-${index}`}
type="date"
class="input input-bordered input-sm"
bind:value={periodo.dataFim}
onchange={() => calcularDias(periodo)}
/>
</div>
<div class="form-control">
<label class="label" for={`ajuste-dias-${index}`}>
<span class="label-text text-xs">Dias</span>
</label>
<div id={`ajuste-dias-${index}`} class="flex items-center h-9 px-3 bg-base-300 rounded-lg" role="textbox" aria-readonly="true">
<span class="font-bold">{periodo.diasCorridos}</span>
</div>
</div>
</div>
</div>
</div>
{/each}
<div class="flex gap-2">
<button
type="button"
class="btn btn-ghost btn-sm"
onclick={() => modoAjuste = false}
disabled={processando}
>
Cancelar Ajuste
</button>
<button
type="button"
class="btn btn-primary btn-sm gap-2"
onclick={ajustarEAprovar}
disabled={processando}
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
Confirmar e Aprovar
</button>
</div>
</div>
{/if}
{/if}
<!-- Motivo Reprovação (se reprovado) -->
{#if solicitacao.status === "reprovado" && solicitacao.motivoReprovacao}
<div class="alert alert-error mt-4">
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div>
<div class="font-bold">Motivo da Reprovação:</div>
<div class="text-sm">{solicitacao.motivoReprovacao}</div>
</div>
</div>
{/if}
<!-- Erro -->
{#if erro}
<div class="alert alert-error mt-4">
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span>{erro}</span>
</div>
{/if}
<!-- Botão Fechar -->
{#if onCancelar}
<div class="card-actions justify-end mt-4">
<button
type="button"
class="btn btn-ghost"
onclick={onCancelar}
disabled={processando}
>
Fechar
</button>
</div>
{/if}
</div>
</div>

View File

@@ -0,0 +1,304 @@
<script lang="ts">
import { useConvexClient } from "convex-svelte";
import { api } from "@sgse-app/backend/convex/_generated/api";
interface Periodo {
id: string;
dataInicio: string;
dataFim: string;
diasCorridos: number;
}
interface Props {
funcionarioId: string;
onSucesso?: () => void;
onCancelar?: () => void;
}
let { funcionarioId, onSucesso, onCancelar }: Props = $props();
const client = useConvexClient();
let anoReferencia = $state(new Date().getFullYear());
let observacao = $state("");
let periodos = $state<Periodo[]>([]);
let processando = $state(false);
let erro = $state("");
// Adicionar primeiro período ao carregar
$effect(() => {
if (periodos.length === 0) {
adicionarPeriodo();
}
});
function adicionarPeriodo() {
if (periodos.length >= 3) {
erro = "Máximo de 3 períodos permitidos";
return;
}
periodos.push({
id: crypto.randomUUID(),
dataInicio: "",
dataFim: "",
diasCorridos: 0,
});
}
function removerPeriodo(id: string) {
periodos = periodos.filter(p => p.id !== id);
}
function calcularDias(periodo: Periodo) {
if (!periodo.dataInicio || !periodo.dataFim) {
periodo.diasCorridos = 0;
return;
}
const inicio = new Date(periodo.dataInicio);
const fim = new Date(periodo.dataFim);
if (fim < inicio) {
erro = "Data final não pode ser anterior à data inicial";
periodo.diasCorridos = 0;
return;
}
const diff = fim.getTime() - inicio.getTime();
const dias = Math.ceil(diff / (1000 * 60 * 60 * 24)) + 1;
periodo.diasCorridos = dias;
erro = "";
}
function validarPeriodos(): boolean {
if (periodos.length === 0) {
erro = "Adicione pelo menos 1 período";
return false;
}
for (const periodo of periodos) {
if (!periodo.dataInicio || !periodo.dataFim) {
erro = "Preencha as datas de todos os períodos";
return false;
}
if (periodo.diasCorridos <= 0) {
erro = "Todos os períodos devem ter pelo menos 1 dia";
return false;
}
}
// Verificar sobreposição de períodos
for (let i = 0; i < periodos.length; i++) {
for (let j = i + 1; j < periodos.length; j++) {
const p1Inicio = new Date(periodos[i].dataInicio);
const p1Fim = new Date(periodos[i].dataFim);
const p2Inicio = new Date(periodos[j].dataInicio);
const p2Fim = new Date(periodos[j].dataFim);
if (
(p2Inicio >= p1Inicio && p2Inicio <= p1Fim) ||
(p2Fim >= p1Inicio && p2Fim <= p1Fim) ||
(p1Inicio >= p2Inicio && p1Inicio <= p2Fim)
) {
erro = "Os períodos não podem se sobrepor";
return false;
}
}
}
return true;
}
async function enviarSolicitacao() {
if (!validarPeriodos()) return;
try {
processando = true;
erro = "";
await client.mutation(api.ferias.criarSolicitacao, {
funcionarioId: funcionarioId as any,
anoReferencia,
periodos: periodos.map(p => ({
dataInicio: p.dataInicio,
dataFim: p.dataFim,
diasCorridos: p.diasCorridos,
})),
observacao: observacao || undefined,
});
if (onSucesso) onSucesso();
} catch (e: any) {
erro = e.message || "Erro ao enviar solicitação";
} finally {
processando = false;
}
}
$effect(() => {
periodos.forEach(p => calcularDias(p));
});
</script>
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h2 class="card-title text-2xl mb-4">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
Solicitar Férias
</h2>
<!-- Ano de Referência -->
<div class="form-control">
<label class="label" for="ano-referencia">
<span class="label-text font-semibold">Ano de Referência</span>
</label>
<input
id="ano-referencia"
type="number"
class="input input-bordered w-full max-w-xs"
bind:value={anoReferencia}
min={new Date().getFullYear()}
max={new Date().getFullYear() + 2}
/>
</div>
<!-- Períodos -->
<div class="mt-6">
<div class="flex items-center justify-between mb-3">
<h3 class="font-semibold text-lg">Períodos ({periodos.length}/3)</h3>
{#if periodos.length < 3}
<button
type="button"
class="btn btn-sm btn-primary gap-2"
onclick={adicionarPeriodo}
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg>
Adicionar Período
</button>
{/if}
</div>
<div class="space-y-4">
{#each periodos as periodo, index}
<div class="card bg-base-200 border border-base-300">
<div class="card-body p-4">
<div class="flex items-center justify-between mb-3">
<h4 class="font-medium">Período {index + 1}</h4>
{#if periodos.length > 1}
<button
type="button"
class="btn btn-xs btn-error btn-square"
aria-label="Remover período"
onclick={() => removerPeriodo(periodo.id)}
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
{/if}
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="form-control">
<label class="label" for={`inicio-${periodo.id}`}>
<span class="label-text">Data Início</span>
</label>
<input
id={`inicio-${periodo.id}`}
type="date"
class="input input-bordered input-sm"
bind:value={periodo.dataInicio}
onchange={() => calcularDias(periodo)}
/>
</div>
<div class="form-control">
<label class="label" for={`fim-${periodo.id}`}>
<span class="label-text">Data Fim</span>
</label>
<input
id={`fim-${periodo.id}`}
type="date"
class="input input-bordered input-sm"
bind:value={periodo.dataFim}
onchange={() => calcularDias(periodo)}
/>
</div>
<div class="form-control">
<label class="label" for={`dias-${periodo.id}`}>
<span class="label-text">Dias Corridos</span>
</label>
<div id={`dias-${periodo.id}`} class="flex items-center h-9 px-3 bg-base-300 rounded-lg" role="textbox" aria-readonly="true">
<span class="font-bold text-lg">{periodo.diasCorridos}</span>
<span class="ml-1 text-sm">dias</span>
</div>
</div>
</div>
</div>
</div>
{/each}
</div>
</div>
<!-- Observações -->
<div class="form-control mt-6">
<label class="label" for="observacao">
<span class="label-text font-semibold">Observações (opcional)</span>
</label>
<textarea
id="observacao"
class="textarea textarea-bordered h-24"
placeholder="Adicione observações sobre sua solicitação..."
bind:value={observacao}
></textarea>
</div>
<!-- Erro -->
{#if erro}
<div class="alert alert-error mt-4">
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span>{erro}</span>
</div>
{/if}
<!-- Ações -->
<div class="card-actions justify-end mt-6">
{#if onCancelar}
<button
type="button"
class="btn btn-ghost"
onclick={onCancelar}
disabled={processando}
>
Cancelar
</button>
{/if}
<button
type="button"
class="btn btn-primary gap-2"
onclick={enviarSolicitacao}
disabled={processando}
>
{#if processando}
<span class="loading loading-spinner loading-sm"></span>
Enviando...
{:else}
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
Enviar Solicitação
{/if}
</button>
</div>
</div>
</div>

View File

@@ -8,16 +8,42 @@
// Queries e Client
const client = useConvexClient();
const notificacoes = useQuery(api.chat.obterNotificacoes, { apenasPendentes: true });
const count = useQuery(api.chat.contarNotificacoesNaoLidas, {});
const notificacoesQuery = useQuery(api.chat.obterNotificacoes, { apenasPendentes: true });
const countQuery = useQuery(api.chat.contarNotificacoesNaoLidas, {});
let dropdownOpen = $state(false);
let notificacoesFerias = $state<any[]>([]);
// 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) ?? []);
// Atualizar contador no store
$effect(() => {
if (count !== undefined) {
notificacoesCount.set(count);
const totalNotificacoes = count + (notificacoesFerias?.length || 0);
notificacoesCount.set(totalNotificacoes);
});
// Buscar notificações de férias
async function buscarNotificacoesFerias() {
try {
const usuarioStore = await import("$lib/stores/auth.svelte").then(m => m.authStore);
if (usuarioStore.usuario?._id) {
const notifsFerias = await client.query(api.ferias.obterNotificacoesNaoLidas, {
usuarioId: usuarioStore.usuario._id as any,
});
notificacoesFerias = notifsFerias || [];
}
} catch (e) {
console.error("Erro ao buscar notificações de férias:", e);
}
}
// Atualizar notificações de férias periodicamente
$effect(() => {
buscarNotificacoesFerias();
const interval = setInterval(buscarNotificacoesFerias, 30000); // A cada 30s
return () => clearInterval(interval);
});
function formatarTempo(timestamp: number): string {
@@ -33,7 +59,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 });
}
dropdownOpen = false;
await buscarNotificacoesFerias();
}
async function handleClickNotificacao(notificacaoId: string) {
@@ -41,6 +72,14 @@
dropdownOpen = false;
}
async function handleClickNotificacaoFerias(notificacaoId: string) {
await client.mutation(api.ferias.marcarComoLida, { notificacaoId: notificacaoId as any });
await buscarNotificacoesFerias();
dropdownOpen = false;
// Redirecionar para a página de férias
window.location.href = "/recursos-humanos/ferias";
}
function toggleDropdown() {
dropdownOpen = !dropdownOpen;
}
@@ -101,12 +140,13 @@
</svg>
<!-- Badge premium com gradiente -->
{#if count && count > 0}
{#if count + (notificacoesFerias?.length || 0) > 0}
{@const totalCount = count + (notificacoesFerias?.length || 0)}
<span
class="absolute -top-1 -right-1 flex h-6 w-6 items-center justify-center rounded-full bg-gradient-to-br from-red-500 via-error to-red-600 text-white text-[10px] font-black shadow-xl ring-2 ring-white z-20"
style="animation: badge-bounce 2s ease-in-out infinite;"
>
{count > 9 ? "9+" : count}
{totalCount > 9 ? "9+" : totalCount}
</span>
{/if}
</button>
@@ -119,7 +159,7 @@
<!-- Header -->
<div 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 && count > 0}
{#if count > 0}
<button
type="button"
class="btn btn-ghost btn-xs"
@@ -132,7 +172,7 @@
<!-- Lista de notificações -->
<div class="py-2">
{#if notificacoes && notificacoes.length > 0}
{#if notificacoes.length > 0}
{#each notificacoes.slice(0, 10) as notificacao (notificacao._id)}
<button
type="button"
@@ -212,7 +252,48 @@
</div>
</button>
{/each}
{:else}
{/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}
{#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">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-purple-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
</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-primary badge-xs"></div>
</div>
</div>
</button>
{/each}
{/if}
<!-- Sem notificações -->
{#if notificacoes.length === 0 && notificacoesFerias.length === 0}
<div class="px-4 py-8 text-center text-base-content/50">
<svg
xmlns="http://www.w3.org/2000/svg"