refactor: enhance authentication and access control in ProtectedRoute component

- Updated the ProtectedRoute component to improve access control logic, including a timeout mechanism for handling authentication checks.
- Refactored the checkAccess function to streamline user access verification based on roles and authentication status.
- Added comments for clarity on the authentication flow and the use of the convexClient plugin in the auth.ts file.
- Improved the overall structure and readability of the code in auth.ts and ProtectedRoute.svelte.
This commit is contained in:
2025-11-17 16:27:15 -03:00
parent d4e70b5e52
commit 2c3d231d20
7 changed files with 1399 additions and 970 deletions

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import { useConvexClient, useQuery } from 'convex-svelte';
import { api } from '@sgse-app/backend/convex/_generated/api';
import { resolve } from '$app/paths';
import { useConvexClient, useQuery } from 'convex-svelte';
import { api } from '@sgse-app/backend/convex/_generated/api';
import { resolve } from '$app/paths';
import AprovarFerias from '$lib/components/AprovarFerias.svelte';
import WizardSolicitacaoFerias from '$lib/components/ferias/WizardSolicitacaoFerias.svelte';
import WizardSolicitacaoAusencia from '$lib/components/ausencias/WizardSolicitacaoAusencia.svelte';
@@ -12,8 +12,21 @@ import { resolve } from '$app/paths';
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
import type { FunctionReturnType } from 'convex/server';
import { X, Calendar } from 'lucide-svelte';
import TicketCard from '$lib/components/chamados/TicketCard.svelte';
import TicketTimeline from '$lib/components/chamados/TicketTimeline.svelte';
import { chamadosStore } from '$lib/stores/chamados';
import {
formatarData,
getStatusBadge as getStatusBadgeChamado,
getStatusDescription,
getStatusLabel,
prazoRestante
} from '$lib/utils/chamados';
import { useConvexWithAuth } from '$lib/hooks/useConvexWithAuth';
import type { Doc } from '@sgse-app/backend/convex/_generated/dataModel';
const client = useConvexClient();
// @ts-expect-error - Convex types issue with getCurrentUser
const currentUser = useQuery(api.auth.getCurrentUser, {});
type FuncionarioAtual = FunctionReturnType<typeof api.funcionarios.getCurrent>;
@@ -26,7 +39,12 @@ import { resolve } from '$app/paths';
let minhasAusenciasEstaveis = $state<MinhasAusencias>([]);
let abaAtiva = $state<
'meu-perfil' | 'minhas-ferias' | 'minhas-ausencias' | 'aprovar-ferias' | 'aprovar-ausencias'
| 'meu-perfil'
| 'meus-chamados'
| 'minhas-ferias'
| 'minhas-ausencias'
| 'aprovar-ferias'
| 'aprovar-ausencias'
>('meu-perfil');
let periodoSelecionado = $state<Id<'ferias'> | null>(null);
@@ -49,6 +67,17 @@ import { resolve } from '$app/paths';
// Estados para Aprovar Ausências (Gestores)
let solicitacaoAusenciaAprovar = $state<Id<'solicitacoesAusencias'> | null>(null);
// Estados para Meus Chamados
type Ticket = Doc<'tickets'>;
const detalhesStore = chamadosStore.detalhes;
let carregandoDetalheChamado = $state(false);
let filtroStatusChamados = $state<'todos' | Ticket['status']>('todos');
let filtroTipoChamados = $state<'todos' | Ticket['tipo']>('todos');
let selectedTicketId = $state<Id<'tickets'> | null>(null);
let mensagemChamado = $state('');
let erroMensagemChamado = $state<string | null>(null);
let sucessoMensagemChamado = $state<string | null>(null);
// Galeria de avatares (30 avatares profissionais 3D realistas)
const avatarGallery = generateAvatarGallery(30);
@@ -151,6 +180,123 @@ import { resolve } from '$app/paths';
const ehGestor = $derived((timesSubordinados || []).length > 0 || rolePermiteAprovacao);
// Query reativa para Meus Chamados (igual às outras abas)
const chamadosQuery = $derived(
currentUser?.data?._id ? useQuery(api.chamados.listarChamadosUsuario, {}) : null
);
// Estados estáveis para Meus Chamados (igual às outras abas)
let chamadosEstaveis = $state<Array<Doc<'tickets'>>>([]);
// Sincronizar query com estado estável (igual às outras abas)
$effect(() => {
if (chamadosQuery && Array.isArray(chamadosQuery.data)) {
chamadosEstaveis = chamadosQuery.data;
// Sincronizar com a store para compatibilidade
chamadosStore.setTickets(chamadosQuery.data);
// Selecionar primeiro chamado automaticamente se não houver seleção
if (!selectedTicketId && chamadosQuery.data.length > 0) {
selectedTicketId = chamadosQuery.data[0]._id;
}
} else if (!currentUser?.data?._id) {
chamadosEstaveis = [];
chamadosStore.setTickets([]);
}
});
// Deriveds para Meus Chamados
const listaChamados = $derived(chamadosEstaveis);
const carregandoListaChamados = $derived(
chamadosQuery === undefined || chamadosQuery === null || chamadosQuery.data === undefined
);
const detalheAtualChamado = $derived(
selectedTicketId ? ($detalhesStore[selectedTicketId] ?? null) : null
);
const ticketsFiltrados = $derived(
listaChamados.filter((ticket) => {
if (filtroStatusChamados !== 'todos' && ticket.status !== filtroStatusChamados) return false;
if (filtroTipoChamados !== 'todos' && ticket.tipo !== filtroTipoChamados) return false;
return true;
})
);
// Funções para Meus Chamados
async function recarregarChamados() {
// A query é reativa e atualiza automaticamente
// Esta função pode ser usada para forçar atualização se necessário
if (chamadosQuery?.data) {
chamadosEstaveis = chamadosQuery.data;
chamadosStore.setTickets(chamadosQuery.data);
}
}
async function selecionarChamado(ticketId: Id<'tickets'>) {
selectedTicketId = ticketId;
if (!$detalhesStore[ticketId]) {
try {
carregandoDetalheChamado = true;
const detalhe = await client.query(api.chamados.obterChamado, { ticketId });
if (detalhe) {
chamadosStore.setDetalhe(ticketId, detalhe);
}
} catch (error) {
console.error('Erro ao carregar detalhe:', error);
} finally {
carregandoDetalheChamado = false;
}
}
}
async function enviarMensagemChamado() {
if (!selectedTicketId || !mensagemChamado.trim()) {
erroMensagemChamado = 'Informe uma mensagem para atualizar o chamado.';
return;
}
try {
erroMensagemChamado = null;
sucessoMensagemChamado = null;
await client.mutation(api.chamados.registrarAtualizacao, {
ticketId: selectedTicketId,
conteudo: mensagemChamado.trim(),
visibilidade: 'publico'
});
mensagemChamado = '';
sucessoMensagemChamado = 'Atualização registrada com sucesso.';
await selecionarChamado(selectedTicketId);
// A query é reativa e atualiza automaticamente, mas podemos forçar atualização
await recarregarChamados();
} catch (error) {
const mensagemErro =
error instanceof Error ? error.message : 'Erro ao enviar atualização. Tente novamente.';
erroMensagemChamado = mensagemErro;
}
}
function statusAlertasChamado(ticket: Ticket) {
const alertas: Array<{ label: string; tipo: 'success' | 'warning' | 'error' }> = [];
if (ticket.prazoResposta) {
const diff = ticket.prazoResposta - Date.now();
if (diff < 0) alertas.push({ label: 'Prazo de resposta vencido', tipo: 'error' });
else if (diff <= 4 * 60 * 60 * 1000)
alertas.push({ label: 'Resposta vence em breve', tipo: 'warning' });
}
if (ticket.prazoConclusao) {
const diff = ticket.prazoConclusao - Date.now();
if (diff < 0) alertas.push({ label: 'Prazo de conclusão vencido', tipo: 'error' });
else if (diff <= 24 * 60 * 60 * 1000)
alertas.push({ label: 'Conclusão vence em breve', tipo: 'warning' });
}
return alertas;
}
// Garantir autenticação quando a aba for ativada
$effect(() => {
if (abaAtiva === 'meus-chamados') {
useConvexWithAuth();
}
});
// Filtrar minhas solicitações
const solicitacoesFiltradas = $derived(
minhasSolicitacoes.filter((s) => {
@@ -378,7 +524,6 @@ import { resolve } from '$app/paths';
onmouseenter={() => (mostrarBotaoCamera = true)}
onmouseleave={() => (mostrarBotaoCamera = false)}
>
s
<button
type="button"
class="avatar cursor-pointer border-0 bg-transparent p-0"
@@ -560,35 +705,12 @@ import { resolve } from '$app/paths';
Meu Perfil
</button>
<a
role="tab"
href={resolve('/perfil/chamados')}
class="tab tab-lg font-semibold transition-all duration-300 hover:bg-base-100"
aria-label="Meus Chamados"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="mr-2 h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M3 7h18M3 12h12M3 17h18"
/>
</svg>
Meus Chamados
</a>
{#if ehGestor}
<button
type="button"
role="tab"
class={`tab tab-lg font-semibold transition-all duration-300 ${abaAtiva === 'minhas-ferias' ? 'tab-active scale-105 bg-linear-to-r from-purple-600 to-blue-600 text-white shadow-lg' : 'hover:bg-base-100'}`}
onclick={() => (abaAtiva = 'minhas-ferias')}
class={`tab tab-lg font-semibold transition-all duration-300 ${abaAtiva === 'meus-chamados' ? 'tab-active scale-105 bg-linear-to-r from-purple-600 to-blue-600 text-white shadow-lg' : 'hover:bg-base-100'}`}
onclick={() => (abaAtiva = 'meus-chamados')}
aria-label="Meus Chamados"
>
<svg
xmlns="http://www.w3.org/2000/svg"
@@ -598,44 +720,17 @@ import { resolve } from '$app/paths';
stroke="currentColor"
stroke-width="2"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
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"
/>
<path stroke-linecap="round" stroke-linejoin="round" d="M3 7h18M3 12h12M3 17h18" />
</svg>
Minhas Férias
</button>
<button
type="button"
role="tab"
class={`tab tab-lg font-semibold transition-all duration-300 ${abaAtiva === 'minhas-ausencias' ? 'tab-active scale-105 bg-linear-to-r from-orange-600 to-amber-600 text-white shadow-lg' : 'hover:bg-base-100'}`}
onclick={() => (abaAtiva = 'minhas-ausencias')}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="mr-2 h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
Minhas Ausências
Meus Chamados
</button>
{#if ehGestor}
<button
type="button"
role="tab"
class={`tab tab-lg font-semibold transition-all duration-300 ${abaAtiva === 'aprovar-ferias' ? 'tab-active scale-105 bg-linear-to-r from-purple-600 to-blue-600 text-white shadow-lg' : 'hover:bg-base-100'}`}
onclick={() => (abaAtiva = 'aprovar-ferias')}
class={`tab tab-lg font-semibold transition-all duration-300 ${abaAtiva === 'minhas-ferias' ? 'tab-active scale-105 bg-linear-to-r from-purple-600 to-blue-600 text-white shadow-lg' : 'hover:bg-base-100'}`}
onclick={() => (abaAtiva = 'minhas-ferias')}
>
<svg
xmlns="http://www.w3.org/2000/svg"
@@ -648,23 +743,17 @@ import { resolve } from '$app/paths';
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
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>
Aprovar Férias
{#if (solicitacoesSubordinados || []).filter((s) => s.status === 'aguardando_aprovacao').length > 0}
<span class="badge badge-error badge-sm ml-2 animate-pulse">
{(solicitacoesSubordinados || []).filter((s) => s.status === 'aguardando_aprovacao')
.length}
</span>
{/if}
Minhas Férias
</button>
<button
type="button"
role="tab"
class={`tab tab-lg font-semibold transition-all duration-300 ${abaAtiva === 'aprovar-ausencias' ? 'tab-active scale-105 bg-linear-to-r from-orange-600 to-amber-600 text-white shadow-lg' : 'hover:bg-base-100'}`}
onclick={() => (abaAtiva = 'aprovar-ausencias')}
class={`tab tab-lg font-semibold transition-all duration-300 ${abaAtiva === 'minhas-ausencias' ? 'tab-active scale-105 bg-linear-to-r from-orange-600 to-amber-600 text-white shadow-lg' : 'hover:bg-base-100'}`}
onclick={() => (abaAtiva = 'minhas-ausencias')}
>
<svg
xmlns="http://www.w3.org/2000/svg"
@@ -680,14 +769,69 @@ import { resolve } from '$app/paths';
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
Aprovar Ausências
{#if (ausenciasSubordinados || []).filter((a) => a.status === 'aguardando_aprovacao').length > 0}
<span class="badge badge-error badge-sm ml-2 animate-pulse">
{(ausenciasSubordinados || []).filter((a) => a.status === 'aguardando_aprovacao')
.length}
</span>
{/if}
Minhas Ausências
</button>
{#if ehGestor}
<button
type="button"
role="tab"
class={`tab tab-lg font-semibold transition-all duration-300 ${abaAtiva === 'aprovar-ferias' ? 'tab-active scale-105 bg-linear-to-r from-purple-600 to-blue-600 text-white shadow-lg' : 'hover:bg-base-100'}`}
onclick={() => (abaAtiva = 'aprovar-ferias')}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="mr-2 h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
Aprovar Férias
{#if (solicitacoesSubordinados || []).filter((s) => s.status === 'aguardando_aprovacao').length > 0}
<span class="badge badge-error badge-sm ml-2 animate-pulse">
{(solicitacoesSubordinados || []).filter(
(s) => s.status === 'aguardando_aprovacao'
).length}
</span>
{/if}
</button>
<button
type="button"
role="tab"
class={`tab tab-lg font-semibold transition-all duration-300 ${abaAtiva === 'aprovar-ausencias' ? 'tab-active scale-105 bg-linear-to-r from-orange-600 to-amber-600 text-white shadow-lg' : 'hover:bg-base-100'}`}
onclick={() => (abaAtiva = 'aprovar-ausencias')}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="mr-2 h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
Aprovar Ausências
{#if (ausenciasSubordinados || []).filter((a) => a.status === 'aguardando_aprovacao').length > 0}
<span class="badge badge-error badge-sm ml-2 animate-pulse">
{(ausenciasSubordinados || []).filter((a) => a.status === 'aguardando_aprovacao')
.length}
</span>
{/if}
</button>
{/if}
{/if}
</div>
@@ -1188,6 +1332,239 @@ import { resolve } from '$app/paths';
</div>
{/if}
</div>
{:else if abaAtiva === 'meus-chamados'}
<!-- Meus Chamados -->
<div class="space-y-6">
<section class="border-base-200 bg-base-100/90 rounded-3xl border p-8 shadow-xl">
<div class="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
<div>
<p class="text-primary text-xs tracking-[0.25em] uppercase">Meu Perfil</p>
<h1 class="text-base-content text-3xl font-black">Meus Chamados</h1>
<p class="text-base-content/70 mt-2 text-sm">
Acompanhe o status, interaja com a equipe de TI e visualize a timeline de SLA em
tempo real.
</p>
</div>
<div class="flex flex-wrap gap-3">
<a href={resolve('/abrir-chamado')} class="btn btn-primary">Abrir novo chamado</a>
<button class="btn btn-ghost" type="button" onclick={recarregarChamados}>
Atualizar
</button>
</div>
</div>
</section>
<div class="grid gap-6 lg:grid-cols-[340px,1fr]">
<aside class="border-base-200 bg-base-100/80 rounded-3xl border p-4 shadow-lg">
<div class="flex items-center justify-between">
<h2 class="text-base-content text-lg font-semibold">Meus tickets</h2>
{#if carregandoListaChamados}
<span class="loading loading-spinner loading-xs"></span>
{/if}
</div>
<div class="mt-4 space-y-3">
<select
class="select select-sm select-bordered w-full"
bind:value={filtroStatusChamados}
>
<option value="todos">Todos os status</option>
<option value="aberto">Aberto</option>
<option value="em_andamento">Em andamento</option>
<option value="aguardando_usuario">Aguardando usuário</option>
<option value="resolvido">Resolvido</option>
<option value="encerrado">Encerrado</option>
<option value="cancelado">Cancelado</option>
</select>
<select
class="select select-sm select-bordered w-full"
bind:value={filtroTipoChamados}
>
<option value="todos">Todos os tipos</option>
<option value="chamado">Chamados técnicos</option>
<option value="reclamacao">Reclamações</option>
<option value="elogio">Elogios</option>
<option value="sugestao">Sugestões</option>
</select>
</div>
<div
class="mt-6 space-y-3 overflow-y-auto pr-1"
style="max-height: calc(100vh - 260px);"
>
{#if ticketsFiltrados.length === 0}
<div class="alert alert-info">
<span>Nenhum chamado encontrado.</span>
</div>
{:else}
{#each ticketsFiltrados as ticket (ticket._id)}
<TicketCard
{ticket}
selected={ticket._id === selectedTicketId}
on:select={({ detail }) => selecionarChamado(detail.ticketId)}
/>
{/each}
{/if}
</div>
</aside>
<section class="border-base-200 bg-base-100/90 rounded-3xl border p-6 shadow-xl">
{#if !selectedTicketId || !detalheAtualChamado}
<div class="text-base-content/60 flex min-h-[400px] items-center justify-center">
{#if carregandoDetalheChamado}
<span class="loading loading-spinner loading-lg"></span>
{:else}
<p>Selecione um chamado para visualizar os detalhes.</p>
{/if}
</div>
{:else}
<div class="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
<div>
<p class="text-base-content/60 text-xs uppercase">
Ticket {detalheAtualChamado.ticket.numero}
</p>
<h2 class="text-base-content text-2xl font-bold">
{detalheAtualChamado.ticket.titulo}
</h2>
<p class="text-base-content/70 mt-1 text-sm">
{detalheAtualChamado.ticket.descricao}
</p>
</div>
<span class={getStatusBadgeChamado(detalheAtualChamado.ticket.status)}>
{getStatusLabel(detalheAtualChamado.ticket.status)}
</span>
</div>
<div class="text-base-content/70 mt-4 flex flex-wrap gap-3 text-sm">
<span class="badge badge-outline badge-sm">
Tipo: {detalheAtualChamado.ticket.tipo.charAt(0).toUpperCase() +
detalheAtualChamado.ticket.tipo.slice(1)}
</span>
<span class="badge badge-outline badge-sm">
Prioridade: {detalheAtualChamado.ticket.prioridade}
</span>
<span class="badge badge-outline badge-sm">
Última interação: {formatarData(detalheAtualChamado.ticket.ultimaInteracaoEm)}
</span>
</div>
{#if statusAlertasChamado(detalheAtualChamado.ticket).length > 0}
<div class="mt-4 space-y-2">
{#each statusAlertasChamado(detalheAtualChamado.ticket) as alerta (alerta.label)}
<div
class={`alert ${
alerta.tipo === 'error'
? 'alert-error'
: alerta.tipo === 'warning'
? 'alert-warning'
: 'alert-success'
}`}
>
<span>{alerta.label}</span>
</div>
{/each}
</div>
{/if}
<div class="mt-6 grid gap-6 lg:grid-cols-2">
<div class="border-base-200 bg-base-100/80 rounded-2xl border p-4">
<h3 class="text-base-content font-semibold">Timeline e SLA</h3>
<p class="text-base-content/60 text-xs">
Etapas monitoradas com indicadores de prazo.
</p>
<div class="mt-4">
<TicketTimeline timeline={detalheAtualChamado.ticket.timeline ?? []} />
</div>
</div>
<div class="border-base-200 bg-base-100/80 rounded-2xl border p-4">
<h3 class="text-base-content font-semibold">Responsabilidade</h3>
<p class="text-base-content/60 text-sm">
{detalheAtualChamado.ticket.responsavelId
? `Responsável: ${detalheAtualChamado.ticket.setorResponsavel ?? 'Equipe TI'}`
: 'Aguardando atribuição'}
</p>
<div class="text-base-content/70 mt-4 space-y-2 text-sm">
<p>
Prazo resposta: {prazoRestante(detalheAtualChamado.ticket.prazoResposta) ??
'--'}
</p>
<p>
Prazo conclusão: {prazoRestante(
detalheAtualChamado.ticket.prazoConclusao
) ?? '--'}
</p>
<p>
Prazo encerramento: {prazoRestante(
detalheAtualChamado.ticket.prazoEncerramento
) ?? '--'}
</p>
</div>
<p class="text-base-content/50 mt-2 text-xs">
{getStatusDescription(detalheAtualChamado.ticket.status)}
</p>
</div>
</div>
<div class="mt-8 grid gap-6 lg:grid-cols-2">
<div class="border-base-200 bg-base-100/70 rounded-2xl border p-4">
<h3 class="text-base-content font-semibold">Interações</h3>
<div class="mt-4 max-h-[360px] space-y-3 overflow-y-auto pr-2">
{#if detalheAtualChamado.interactions.length === 0}
<p class="text-base-content/60 text-sm">
Nenhuma interação registrada ainda.
</p>
{:else}
{#each detalheAtualChamado.interactions as interacao (interacao._id)}
<div class="border-base-200 bg-base-100/90 rounded-2xl border p-3">
<div
class="text-base-content/60 flex items-center justify-between text-xs"
>
<span
>{interacao.origem === 'usuario' ? 'Você' : interacao.origem}</span
>
<span>{formatarData(interacao.criadoEm)}</span>
</div>
<p class="text-base-content mt-2 text-sm whitespace-pre-wrap">
{interacao.conteudo}
</p>
{#if interacao.statusNovo && interacao.statusNovo !== interacao.statusAnterior}
<span class="badge badge-xs badge-outline mt-2">
Status: {getStatusLabel(interacao.statusNovo)}
</span>
{/if}
</div>
{/each}
{/if}
</div>
</div>
<div class="border-base-200 bg-base-100/70 rounded-2xl border p-4">
<h3 class="text-base-content font-semibold">Enviar atualização</h3>
<textarea
class="textarea textarea-bordered mt-3 min-h-[140px] w-full"
placeholder="Compartilhe informações adicionais, aprovações ou anexos enviados por outros canais."
bind:value={mensagemChamado}
></textarea>
{#if erroMensagemChamado}
<p class="text-error mt-2 text-sm">{erroMensagemChamado}</p>
{/if}
{#if sucessoMensagemChamado}
<p class="text-success mt-2 text-sm">{sucessoMensagemChamado}</p>
{/if}
<button
type="button"
class="btn btn-primary mt-3 w-full"
onclick={enviarMensagemChamado}
>
Enviar
</button>
</div>
</div>
{/if}
</section>
</div>
</div>
{:else if abaAtiva === 'minhas-ferias'}
<!-- Minhas Férias -->
<div class="space-y-6">
@@ -2332,7 +2709,14 @@ import { resolve } from '$app/paths';
>
<div
class="modal-box flex max-h-[90vh] max-w-4xl flex-col p-0"
role="presentation"
tabindex="-1"
onclick={(e) => e.stopPropagation()}
onkeydown={(e) => {
if (e.key === 'Escape') {
mostrarWizardAusencia = false;
}
}}
>
<!-- Header -->
<div class="border-base-300 flex items-center justify-between border-b px-6 py-4">