Feat ausencia #7

Merged
deyvisonwanderley merged 8 commits from feat-ausencia into master 2025-11-05 13:49:12 +00:00
25 changed files with 4972 additions and 1758 deletions
Showing only changes of commit c1e0998a5f - Show all commits

View File

@@ -39,7 +39,7 @@
<p class="py-4 text-base-content">{message}</p> <p class="py-4 text-base-content">{message}</p>
{#if details} {#if details}
<div class="bg-base-200 rounded-lg p-3 mb-4"> <div class="bg-base-200 rounded-lg p-3 mb-4">
<p class="text-sm text-base-content/70 font-mono">{details}</p> <p class="text-sm text-base-content/70 whitespace-pre-line">{details}</p>
</div> </div>
{/if} {/if}
<div class="modal-action"> <div class="modal-action">

View File

@@ -124,10 +124,223 @@
return diffDays; return diffDays;
} }
// Helper: Verificar se há sobreposição de datas
function verificarSobreposicao(
inicio1: Date,
fim1: Date,
inicio2: string,
fim2: string
): boolean {
const d2Inicio = new Date(inicio2);
const d2Fim = new Date(fim2);
// Verificar sobreposição: início1 <= fim2 && início2 <= fim1
return inicio1 <= d2Fim && d2Inicio <= fim1;
}
// Helper: Verificar se período selecionado sobrepõe com ausências existentes
function verificarSobreposicaoComAusencias(inicio: Date, fim: Date): boolean {
if (!ausenciasExistentes || ausenciasExistentes.length === 0) return false;
// Verificar apenas ausências aprovadas ou aguardando aprovação
const ausenciasBloqueantes = ausenciasExistentes.filter(
(a) => a.status === "aprovado" || a.status === "aguardando_aprovacao"
);
return ausenciasBloqueantes.some((ausencia) =>
verificarSobreposicao(inicio, fim, ausencia.dataInicio, ausencia.dataFim)
);
}
// Helper: Atualizar classe de seleção em uma célula
function atualizarClasseSelecionado(info: any) {
if (dataInicio && dataFim && !readonly) {
const cellDate = new Date(info.date);
const inicio = new Date(dataInicio);
const fim = new Date(dataFim);
cellDate.setHours(0, 0, 0, 0);
inicio.setHours(0, 0, 0, 0);
fim.setHours(0, 0, 0, 0);
if (cellDate >= inicio && cellDate <= fim) {
info.el.classList.add("fc-day-selected");
} else {
info.el.classList.remove("fc-day-selected");
}
} else {
info.el.classList.remove("fc-day-selected");
}
}
// Helper: Atualizar classe de bloqueio para dias com ausências existentes
function atualizarClasseBloqueado(info: any) {
if (readonly || !ausenciasExistentes || ausenciasExistentes.length === 0) {
info.el.classList.remove("fc-day-blocked");
return;
}
const cellDate = new Date(info.date);
cellDate.setHours(0, 0, 0, 0);
// Verificar se a data está dentro de alguma ausência aprovada ou aguardando aprovação
const estaBloqueado = ausenciasExistentes
.filter((a) => a.status === "aprovado" || a.status === "aguardando_aprovacao")
.some((ausencia) => {
const inicio = new Date(ausencia.dataInicio);
const fim = new Date(ausencia.dataFim);
inicio.setHours(0, 0, 0, 0);
fim.setHours(0, 0, 0, 0);
return cellDate >= inicio && cellDate <= fim;
});
if (estaBloqueado) {
info.el.classList.add("fc-day-blocked");
} else {
info.el.classList.remove("fc-day-blocked");
}
}
// Helper: Atualizar todos os dias selecionados no calendário
function atualizarDiasSelecionados() {
if (!calendar || !calendarEl || !dataInicio || !dataFim || readonly) return;
// Usar a API do FullCalendar para iterar sobre todas as células visíveis
const view = calendar.view;
if (!view) return;
const inicio = new Date(dataInicio);
const fim = new Date(dataFim);
inicio.setHours(0, 0, 0, 0);
fim.setHours(0, 0, 0, 0);
// O FullCalendar renderiza as células, então podemos usar dayCellDidMount
// Mas também precisamos atualizar células existentes
const cells = calendarEl.querySelectorAll(".fc-daygrid-day");
cells.forEach((cell) => {
// Remover classe primeiro
cell.classList.remove("fc-day-selected");
// Tentar obter a data do aria-label ou do elemento
const ariaLabel = cell.getAttribute("aria-label");
if (ariaLabel) {
// Formato: "dia mês ano" ou similar
try {
const cellDate = new Date(ariaLabel);
if (!isNaN(cellDate.getTime())) {
cellDate.setHours(0, 0, 0, 0);
if (cellDate >= inicio && cellDate <= fim) {
cell.classList.add("fc-day-selected");
}
}
} catch (e) {
// Ignorar erros de parsing
}
}
});
}
// Helper: Atualizar todos os dias bloqueados no calendário
function atualizarDiasBloqueados() {
if (!calendar || !calendarEl || readonly || !ausenciasExistentes || ausenciasExistentes.length === 0) {
// Remover classes de bloqueio se não houver ausências
if (calendarEl) {
const cells = calendarEl.querySelectorAll(".fc-daygrid-day");
cells.forEach((cell) => cell.classList.remove("fc-day-blocked"));
}
return;
}
const cells = calendarEl.querySelectorAll(".fc-daygrid-day");
const ausenciasBloqueantes = ausenciasExistentes.filter(
(a) => a.status === "aprovado" || a.status === "aguardando_aprovacao"
);
if (ausenciasBloqueantes.length === 0) {
cells.forEach((cell) => cell.classList.remove("fc-day-blocked"));
return;
}
cells.forEach((cell) => {
cell.classList.remove("fc-day-blocked");
// Tentar obter a data de diferentes formas
let cellDate: Date | null = null;
// Método 1: aria-label
const ariaLabel = cell.getAttribute("aria-label");
if (ariaLabel) {
try {
const parsed = new Date(ariaLabel);
if (!isNaN(parsed.getTime())) {
cellDate = parsed;
}
} catch (e) {
// Ignorar
}
}
// Método 2: data-date attribute
if (!cellDate) {
const dataDate = cell.getAttribute("data-date");
if (dataDate) {
try {
const parsed = new Date(dataDate);
if (!isNaN(parsed.getTime())) {
cellDate = parsed;
}
} catch (e) {
// Ignorar
}
}
}
// Método 3: Tentar obter do número do dia e contexto do calendário
if (!cellDate && calendar.view) {
const dayNumberEl = cell.querySelector(".fc-daygrid-day-number");
if (dayNumberEl) {
const dayNumber = parseInt(dayNumberEl.textContent || "0");
if (dayNumber > 0 && dayNumber <= 31) {
// Usar a data da view atual e o número do dia
const viewStart = new Date(calendar.view.activeStart);
const cellIndex = Array.from(cells).indexOf(cell);
if (cellIndex >= 0) {
const possibleDate = new Date(viewStart);
possibleDate.setDate(viewStart.getDate() + cellIndex);
// Verificar se o número do dia corresponde
if (possibleDate.getDate() === dayNumber) {
cellDate = possibleDate;
}
}
}
}
}
if (cellDate) {
cellDate.setHours(0, 0, 0, 0);
const estaBloqueado = ausenciasBloqueantes.some((ausencia) => {
const inicio = new Date(ausencia.dataInicio);
const fim = new Date(ausencia.dataFim);
inicio.setHours(0, 0, 0, 0);
fim.setHours(0, 0, 0, 0);
return cellDate >= inicio && cellDate <= fim;
});
if (estaBloqueado) {
cell.classList.add("fc-day-blocked");
}
}
});
}
// Atualizar eventos quando mudanças ocorrem (evitar loop infinito) // Atualizar eventos quando mudanças ocorrem (evitar loop infinito)
$effect(() => { $effect(() => {
if (!calendar || selecionando) return; // Não atualizar durante seleção if (!calendar || selecionando) return; // Não atualizar durante seleção
// Garantir que temos as ausências antes de atualizar
const ausencias = ausenciasExistentes;
atualizarEventos(); atualizarEventos();
// Usar requestAnimationFrame para evitar múltiplas atualizações durante seleção // Usar requestAnimationFrame para evitar múltiplas atualizações durante seleção
@@ -135,10 +348,37 @@
if (calendar && !selecionando) { if (calendar && !selecionando) {
calendar.removeAllEvents(); calendar.removeAllEvents();
calendar.addEventSource(eventos); calendar.addEventSource(eventos);
// Atualizar classes de seleção e bloqueio quando as datas mudarem
setTimeout(() => {
atualizarDiasSelecionados();
atualizarDiasBloqueados();
}, 150);
} }
}); });
}); });
// Efeito separado para atualizar quando ausências mudarem
$effect(() => {
if (!calendar || readonly) return;
const ausencias = ausenciasExistentes;
const ausenciasBloqueantes = ausencias?.filter(
(a) => a.status === "aprovado" || a.status === "aguardando_aprovacao"
) || [];
// Se houver ausências bloqueantes, forçar atualização
if (ausenciasBloqueantes.length > 0) {
setTimeout(() => {
if (calendar && calendarEl) {
atualizarDiasBloqueados();
// Forçar re-render para aplicar classes via dayCellClassNames
calendar.render();
}
}, 200);
}
});
onMount(() => { onMount(() => {
if (!calendarEl) return; if (!calendarEl) return;
@@ -157,6 +397,11 @@
selectable: !readonly, selectable: !readonly,
selectMirror: true, selectMirror: true,
unselectAuto: false, unselectAuto: false,
selectOverlap: false,
selectConstraint: null, // Permite seleção entre meses diferentes
validRange: {
start: new Date().toISOString().split("T")[0], // Não permite selecionar datas passadas
},
events: eventos, events: eventos,
// Estilo customizado // Estilo customizado
@@ -196,6 +441,14 @@
return; return;
} }
// Validar sobreposição com ausências existentes
if (verificarSobreposicaoComAusencias(inicio, fim)) {
alert("Este período sobrepõe com uma ausência já aprovada ou aguardando aprovação. Por favor, escolha outro período.");
calendar?.unselect();
selecionando = false;
return;
}
// Chamar callback de forma assíncrona para evitar loop // Chamar callback de forma assíncrona para evitar loop
if (onPeriodoSelecionado) { if (onPeriodoSelecionado) {
onPeriodoSelecionado({ onPeriodoSelecionado({
@@ -204,7 +457,8 @@
}); });
} }
calendar?.unselect(); // Não remover seleção imediatamente para manter visualização
// calendar?.unselect();
// Liberar flag após um pequeno delay para garantir que o estado foi atualizado // Liberar flag após um pequeno delay para garantir que o estado foi atualizado
setTimeout(() => { setTimeout(() => {
@@ -233,19 +487,88 @@
info.el.style.cursor = readonly ? "default" : "pointer"; info.el.style.cursor = readonly ? "default" : "pointer";
}, },
// Desabilitar datas passadas // Desabilitar datas passadas e períodos que sobrepõem com ausências existentes
selectAllow: (selectInfo) => { selectAllow: (selectInfo) => {
const hoje = new Date(); const hoje = new Date();
hoje.setHours(0, 0, 0, 0); hoje.setHours(0, 0, 0, 0);
return new Date(selectInfo.start) >= hoje;
// Bloquear datas passadas
if (new Date(selectInfo.start) < hoje) {
return false;
}
// Verificar sobreposição com ausências existentes
if (!readonly && ausenciasExistentes && ausenciasExistentes.length > 0) {
const inicioSelecao = new Date(selectInfo.start);
const fimSelecao = new Date(selectInfo.end);
fimSelecao.setDate(fimSelecao.getDate() - 1); // FullCalendar usa exclusive end
inicioSelecao.setHours(0, 0, 0, 0);
fimSelecao.setHours(0, 0, 0, 0);
if (verificarSobreposicaoComAusencias(inicioSelecao, fimSelecao)) {
return false;
}
}
return true;
}, },
// Highlight de fim de semana // Adicionar classe CSS aos dias selecionados e bloqueados
dayCellDidMount: (info) => {
atualizarClasseSelecionado(info);
atualizarClasseBloqueado(info);
},
// Atualizar quando as datas mudarem (navegação do calendário)
datesSet: () => {
setTimeout(() => {
atualizarDiasSelecionados();
atualizarDiasBloqueados();
}, 100);
},
// Garantir que as classes sejam aplicadas após renderização inicial
viewDidMount: () => {
setTimeout(() => {
if (calendar && calendarEl) {
atualizarDiasSelecionados();
atualizarDiasBloqueados();
}
}, 100);
},
// Highlight de fim de semana e aplicar classe de bloqueio
dayCellClassNames: (arg) => { dayCellClassNames: (arg) => {
const classes: string[] = [];
if (arg.date.getDay() === 0 || arg.date.getDay() === 6) { if (arg.date.getDay() === 0 || arg.date.getDay() === 6) {
return ["fc-day-weekend-custom"]; classes.push("fc-day-weekend-custom");
} }
return [];
// Verificar se o dia está bloqueado
if (!readonly && ausenciasExistentes && ausenciasExistentes.length > 0) {
const cellDate = new Date(arg.date);
cellDate.setHours(0, 0, 0, 0);
const ausenciasBloqueantes = ausenciasExistentes.filter(
(a) => a.status === "aprovado" || a.status === "aguardando_aprovacao"
);
const estaBloqueado = ausenciasBloqueantes.some((ausencia) => {
const inicio = new Date(ausencia.dataInicio);
const fim = new Date(ausencia.dataFim);
inicio.setHours(0, 0, 0, 0);
fim.setHours(0, 0, 0, 0);
return cellDate >= inicio && cellDate <= fim;
});
if (estaBloqueado) {
classes.push("fc-day-blocked");
}
}
return classes;
}, },
}); });
@@ -260,7 +583,8 @@
<div class="calendario-ausencias-wrapper"> <div class="calendario-ausencias-wrapper">
<!-- Header com instruções --> <!-- Header com instruções -->
{#if !readonly} {#if !readonly}
<div class="alert alert-info mb-4 shadow-lg"> <div class="space-y-4 mb-4">
<div class="alert alert-info shadow-lg">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
fill="none" fill="none"
@@ -282,6 +606,34 @@
<li>A data de início não pode ser no passado</li> <li>A data de início não pode ser no passado</li>
</ul> </ul>
</div> </div>
</div>
<!-- Alerta sobre dias bloqueados -->
{#if ausenciasExistentes && ausenciasExistentes.filter(a => a.status === "aprovado" || a.status === "aguardando_aprovacao").length > 0}
{@const ausenciasBloqueantes = ausenciasExistentes.filter(a => a.status === "aprovado" || a.status === "aguardando_aprovacao")}
<div class="alert alert-warning shadow-lg border-2 border-warning/50">
<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="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
/>
</svg>
<div class="flex-1">
<h3 class="font-bold">Atenção: Períodos Indisponíveis</h3>
<div class="text-sm mt-1">
<p>Os dias marcados em <span class="font-bold text-error">vermelho</span> estão bloqueados porque você já possui solicitações <strong>aprovadas</strong> ou <strong>aguardando aprovação</strong> para esses períodos.</p>
<p class="mt-2">Você não pode criar novas solicitações que sobreponham esses períodos. Escolha um período diferente.</p>
</div>
</div>
</div>
{/if}
</div> </div>
{/if} {/if}
@@ -293,7 +645,8 @@
<!-- Legenda de status --> <!-- Legenda de status -->
{#if ausenciasExistentes.length > 0 || readonly} {#if ausenciasExistentes.length > 0 || readonly}
<div class="mt-6 flex flex-wrap gap-4 justify-center"> <div class="mt-6 space-y-4">
<div class="flex flex-wrap gap-4 justify-center">
<div class="badge badge-lg gap-2" style="background-color: #f59e0b; border-color: #d97706; color: white;"> <div class="badge badge-lg gap-2" style="background-color: #f59e0b; border-color: #d97706; color: white;">
<div class="w-3 h-3 rounded-full bg-white"></div> <div class="w-3 h-3 rounded-full bg-white"></div>
Aguardando Aprovação Aguardando Aprovação
@@ -306,6 +659,21 @@
<div class="w-3 h-3 rounded-full bg-white"></div> <div class="w-3 h-3 rounded-full bg-white"></div>
Reprovado Reprovado
</div> </div>
{#if !readonly && ausenciasExistentes && ausenciasExistentes.filter(a => a.status === "aprovado" || a.status === "aguardando_aprovacao").length > 0}
<div class="badge badge-lg gap-2" style="background-color: rgba(239, 68, 68, 0.2); border-color: #ef4444; color: #dc2626;">
<div class="w-3 h-3 rounded-full" style="background-color: #ef4444;"></div>
Dias Bloqueados (Indisponíveis)
</div>
{/if}
</div>
{#if !readonly && ausenciasExistentes && ausenciasExistentes.filter(a => a.status === "aprovado" || a.status === "aguardando_aprovacao").length > 0}
<div class="text-center">
<p class="text-sm text-base-content/70">
<span class="font-semibold text-error">Dias bloqueados</span> não podem ser selecionados para novas solicitações
</p>
</div>
{/if}
</div> </div>
{/if} {/if}
@@ -447,6 +815,71 @@
border: 2px dashed #f59e0b; border: 2px dashed #f59e0b;
} }
/* Dias selecionados (período confirmado) */
:global(.calendario-ausencias .fc .fc-day-selected) {
background: rgba(102, 126, 234, 0.2) !important;
border: 2px solid #667eea !important;
position: relative;
}
:global(.calendario-ausencias .fc .fc-day-selected .fc-daygrid-day-number) {
color: #667eea !important;
font-weight: 700 !important;
background: rgba(102, 126, 234, 0.1);
border-radius: 50%;
width: 2rem;
height: 2rem;
display: flex;
align-items: center;
justify-content: center;
}
/* Primeiro e último dia do período selecionado */
:global(.calendario-ausencias .fc .fc-day-selected:first-child),
:global(.calendario-ausencias .fc .fc-day-selected:last-child) {
background: rgba(102, 126, 234, 0.3) !important;
border-color: #667eea !important;
}
/* Dias bloqueados (com ausências aprovadas ou aguardando aprovação) */
:global(.calendario-ausencias .fc-daygrid-day.fc-day-blocked) {
background-color: rgba(239, 68, 68, 0.2) !important;
position: relative !important;
}
:global(.calendario-ausencias .fc-daygrid-day.fc-day-blocked .fc-daygrid-day-frame) {
background-color: rgba(239, 68, 68, 0.2) !important;
border-color: rgba(239, 68, 68, 0.4) !important;
}
:global(.calendario-ausencias .fc-daygrid-day.fc-day-blocked .fc-daygrid-day-number) {
color: #dc2626 !important;
font-weight: 700 !important;
text-decoration: line-through !important;
background-color: rgba(239, 68, 68, 0.1) !important;
border-radius: 50% !important;
padding: 0.25rem !important;
}
:global(.calendario-ausencias .fc-daygrid-day.fc-day-blocked::before) {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: repeating-linear-gradient(
45deg,
transparent,
transparent 6px,
rgba(239, 68, 68, 0.15) 6px,
rgba(239, 68, 68, 0.15) 12px
);
pointer-events: none;
z-index: 1;
border-radius: inherit;
}
/* Datas desabilitadas (passado) */ /* Datas desabilitadas (passado) */
:global(.calendario-ausencias .fc .fc-day-past .fc-daygrid-day-number) { :global(.calendario-ausencias .fc .fc-day-past .fc-daygrid-day-number) {
opacity: 0.4; opacity: 0.4;

View File

@@ -2,6 +2,7 @@
import { useConvexClient, useQuery } from "convex-svelte"; import { useConvexClient, useQuery } from "convex-svelte";
import { api } from "@sgse-app/backend/convex/_generated/api"; import { api } from "@sgse-app/backend/convex/_generated/api";
import CalendarioAusencias from "./CalendarioAusencias.svelte"; import CalendarioAusencias from "./CalendarioAusencias.svelte";
import ErrorModal from "../ErrorModal.svelte";
import { toast } from "svelte-sonner"; import { toast } from "svelte-sonner";
import type { Id } from "@sgse-app/backend/convex/_generated/dataModel"; import type { Id } from "@sgse-app/backend/convex/_generated/dataModel";
@@ -26,16 +27,24 @@
let motivo = $state(""); let motivo = $state("");
let processando = $state(false); let processando = $state(false);
// Estados para modal de erro
let mostrarModalErro = $state(false);
let mensagemErroModal = $state("");
let detalhesErroModal = $state("");
// Buscar ausências existentes para exibir no calendário // Buscar ausências existentes para exibir no calendário
const ausenciasExistentesQuery = useQuery(api.ausencias.listarMinhasSolicitacoes, { const ausenciasExistentesQuery = useQuery(api.ausencias.listarMinhasSolicitacoes, {
funcionarioId, funcionarioId,
}); });
// Filtrar apenas ausências aprovadas ou aguardando aprovação (que bloqueiam novas solicitações)
const ausenciasExistentes = $derived( const ausenciasExistentes = $derived(
(ausenciasExistentesQuery?.data || []).map((a) => ({ (ausenciasExistentesQuery?.data || [])
.filter((a) => a.status === "aprovado" || a.status === "aguardando_aprovacao")
.map((a) => ({
dataInicio: a.dataInicio, dataInicio: a.dataInicio,
dataFim: a.dataFim, dataFim: a.dataFim,
status: a.status as "aguardando_aprovacao" | "aprovado" | "reprovado", status: a.status as "aguardando_aprovacao" | "aprovado",
})) }))
); );
@@ -97,6 +106,8 @@
try { try {
processando = true; processando = true;
mostrarModalErro = false;
mensagemErroModal = "";
await client.mutation(api.ausencias.criarSolicitacao, { await client.mutation(api.ausencias.criarSolicitacao, {
funcionarioId, funcionarioId,
@@ -112,14 +123,32 @@
} }
} catch (error) { } catch (error) {
console.error("Erro ao criar solicitação:", error); console.error("Erro ao criar solicitação:", error);
toast.error( const mensagemErro = error instanceof Error ? error.message : String(error);
error instanceof Error ? error.message : "Erro ao criar solicitação de ausência"
); // Verificar se é erro de sobreposição de período
if (
mensagemErro.includes("Já existe uma solicitação") ||
mensagemErro.includes("já existe") ||
mensagemErro.includes("solicitação aprovada ou pendente")
) {
mensagemErroModal = "Não é possível criar esta solicitação.";
detalhesErroModal = `Já existe uma solicitação aprovada ou pendente para o período selecionado:\n\nPeríodo selecionado: ${new Date(dataInicio).toLocaleDateString("pt-BR")} até ${new Date(dataFim).toLocaleDateString("pt-BR")}\n\nPor favor, escolha um período diferente ou aguarde a resposta da solicitação existente.`;
mostrarModalErro = true;
} else {
// Outros erros continuam usando toast
toast.error(mensagemErro);
}
} finally { } finally {
processando = false; processando = false;
} }
} }
function fecharModalErro() {
mostrarModalErro = false;
mensagemErroModal = "";
detalhesErroModal = "";
}
function handlePeriodoSelecionado(periodo: { dataInicio: string; dataFim: string }) { function handlePeriodoSelecionado(periodo: { dataInicio: string; dataFim: string }) {
dataInicio = periodo.dataInicio; dataInicio = periodo.dataInicio;
dataFim = periodo.dataFim; dataFim = periodo.dataFim;
@@ -206,12 +235,19 @@
</p> </p>
</div> </div>
{#if ausenciasExistentesQuery === undefined}
<div class="flex items-center justify-center py-12">
<span class="loading loading-spinner loading-lg"></span>
<span class="ml-4 text-base-content/70">Carregando ausências existentes...</span>
</div>
{:else}
<CalendarioAusencias <CalendarioAusencias
dataInicio={dataInicio} dataInicio={dataInicio}
dataFim={dataFim} dataFim={dataFim}
ausenciasExistentes={ausenciasExistentes} ausenciasExistentes={ausenciasExistentes}
onPeriodoSelecionado={handlePeriodoSelecionado} onPeriodoSelecionado={handlePeriodoSelecionado}
/> />
{/if}
{#if dataInicio && dataFim} {#if dataInicio && dataFim}
<div class="alert alert-success shadow-lg"> <div class="alert alert-success shadow-lg">
@@ -428,6 +464,15 @@
</div> </div>
</div> </div>
<!-- Modal de Erro -->
<ErrorModal
open={mostrarModalErro}
title="Período Indisponível"
message={mensagemErroModal || "Já existe uma solicitação para este período."}
details={detalhesErroModal}
onClose={fecharModalErro}
/>
<style> <style>
.wizard-ausencia { .wizard-ausencia {
max-width: 1000px; max-width: 1000px;

View File

@@ -6,6 +6,7 @@
import WizardSolicitacaoFerias from "$lib/components/ferias/WizardSolicitacaoFerias.svelte"; import WizardSolicitacaoFerias from "$lib/components/ferias/WizardSolicitacaoFerias.svelte";
import WizardSolicitacaoAusencia from "$lib/components/ausencias/WizardSolicitacaoAusencia.svelte"; import WizardSolicitacaoAusencia from "$lib/components/ausencias/WizardSolicitacaoAusencia.svelte";
import AprovarAusencias from "$lib/components/AprovarAusencias.svelte"; import AprovarAusencias from "$lib/components/AprovarAusencias.svelte";
import CalendarioAusencias from "$lib/components/ausencias/CalendarioAusencias.svelte";
import { generateAvatarGallery, type Avatar } from "$lib/utils/avatars"; import { generateAvatarGallery, type Avatar } from "$lib/utils/avatars";
import type { Id } from "@sgse-app/backend/convex/_generated/dataModel"; import type { Id } from "@sgse-app/backend/convex/_generated/dataModel";
import { page } from "$app/stores"; import { page } from "$app/stores";
@@ -151,6 +152,15 @@
}) })
); );
// Formatar ausências para o calendário
const ausenciasParaCalendario = $derived(
minhasAusencias.map((a) => ({
dataInicio: a.dataInicio,
dataFim: a.dataFim,
status: a.status as "aguardando_aprovacao" | "aprovado" | "reprovado",
}))
);
// Estatísticas das minhas férias // Estatísticas das minhas férias
const statsMinhasFerias = $derived({ const statsMinhasFerias = $derived({
total: minhasSolicitacoes.length, total: minhasSolicitacoes.length,
@@ -1375,6 +1385,46 @@
</div> </div>
</div> </div>
<!-- Calendário de Ausências -->
<div class="card bg-base-100 shadow-lg">
<div class="card-body">
<h2 class="card-title text-lg mb-4">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6 text-orange-500"
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>
Calendário de Ausências
</h2>
<p class="text-sm text-base-content/70 mb-4">
Visualize todas as suas solicitações de ausência no calendário
</p>
{#if ausenciasParaCalendario.length > 0}
<CalendarioAusencias
ausenciasExistentes={ausenciasParaCalendario}
readonly={true}
modoVisualizacao="month"
/>
{:else}
<div class="alert alert-info">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-info shrink-0 w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<span>Você ainda não possui solicitações de ausência. Clique em "Solicitar Ausência" para criar uma nova.</span>
</div>
{/if}
</div>
</div>
<!-- Filtros e Botão Nova Solicitação --> <!-- Filtros e Botão Nova Solicitação -->
<div class="card bg-base-100 shadow-lg"> <div class="card bg-base-100 shadow-lg">
<div class="card-body"> <div class="card-body">
@@ -2128,8 +2178,17 @@
<div class="max-h-[80vh] overflow-y-auto"> <div class="max-h-[80vh] overflow-y-auto">
<WizardSolicitacaoAusencia <WizardSolicitacaoAusencia
funcionarioId={funcionarioIdDisponivel} funcionarioId={funcionarioIdDisponivel}
onSucesso={() => { onSucesso={async () => {
mostrarWizardAusencia = false; mostrarWizardAusencia = false;
// As queries do Convex são reativas e devem atualizar automaticamente
// Mas garantimos que o componente será re-renderizado
// Forçar recarregamento das queries mudando temporariamente o filtro
const filtroAnterior = filtroStatusAusencias;
if (filtroAnterior !== "todos") {
filtroStatusAusencias = "todos";
await new Promise(resolve => setTimeout(resolve, 200));
}
filtroStatusAusencias = filtroAnterior;
}} }}
onCancelar={() => (mostrarWizardAusencia = false)} onCancelar={() => (mostrarWizardAusencia = false)}
/> />