- Refactored multiple Svelte components to enhance code clarity and maintainability. - Standardized formatting and indentation across various files for consistency. - Improved error handling messages in the AprovarAusencias component for better user feedback. - Updated class names in the UI components to align with the new design system. - Removed unnecessary whitespace and comments to streamline the codebase.
541 lines
15 KiB
Svelte
541 lines
15 KiB
Svelte
<script lang="ts">
|
|
import { onMount } from "svelte";
|
|
import { Calendar } from "@fullcalendar/core";
|
|
import dayGridPlugin from "@fullcalendar/daygrid";
|
|
import interactionPlugin from "@fullcalendar/interaction";
|
|
import ptBrLocale from "@fullcalendar/core/locales/pt-br";
|
|
import type { EventInput } from "@fullcalendar/core/index.js";
|
|
|
|
interface Props {
|
|
eventos: Array<{
|
|
id: string;
|
|
title: string;
|
|
start: string;
|
|
end: string;
|
|
color: string;
|
|
tipo: string;
|
|
funcionarioNome: string;
|
|
funcionarioId: string;
|
|
}>;
|
|
tipoFiltro?: string;
|
|
}
|
|
|
|
let { eventos, tipoFiltro = "todos" }: Props = $props();
|
|
|
|
let calendarEl: HTMLDivElement;
|
|
let calendar: Calendar | null = null;
|
|
let filtroAtivo = $state<string>(tipoFiltro);
|
|
let showModal = $state(false);
|
|
let eventoSelecionado = $state<{
|
|
title: string;
|
|
start: string;
|
|
end: string;
|
|
tipo: string;
|
|
funcionarioNome: string;
|
|
} | null>(null);
|
|
|
|
// Eventos filtrados
|
|
const eventosFiltrados = $derived.by(() => {
|
|
if (filtroAtivo === "todos") return eventos;
|
|
return eventos.filter((e) => e.tipo === filtroAtivo);
|
|
});
|
|
|
|
// Converter eventos para formato FullCalendar
|
|
const eventosFullCalendar = $derived.by(() => {
|
|
return eventosFiltrados.map((evento) => ({
|
|
id: evento.id,
|
|
title: evento.title,
|
|
start: evento.start,
|
|
end: evento.end,
|
|
backgroundColor: evento.color,
|
|
borderColor: evento.color,
|
|
textColor: "#ffffff",
|
|
extendedProps: {
|
|
tipo: evento.tipo,
|
|
funcionarioNome: evento.funcionarioNome,
|
|
funcionarioId: evento.funcionarioId,
|
|
},
|
|
})) as EventInput[];
|
|
});
|
|
|
|
onMount(() => {
|
|
if (!calendarEl) return;
|
|
|
|
calendar = new Calendar(calendarEl, {
|
|
plugins: [dayGridPlugin, interactionPlugin],
|
|
initialView: "dayGridMonth",
|
|
locale: ptBrLocale,
|
|
firstDay: 0, // Domingo
|
|
headerToolbar: {
|
|
left: "prev,next today",
|
|
center: "title",
|
|
right: "dayGridMonth",
|
|
},
|
|
buttonText: {
|
|
today: "Hoje",
|
|
month: "Mês",
|
|
week: "Semana",
|
|
day: "Dia",
|
|
},
|
|
events: eventosFullCalendar,
|
|
eventClick: (info) => {
|
|
eventoSelecionado = {
|
|
title: info.event.title,
|
|
start: info.event.startStr || "",
|
|
end: info.event.endStr || "",
|
|
tipo: info.event.extendedProps.tipo as string,
|
|
funcionarioNome: info.event.extendedProps.funcionarioNome as string,
|
|
};
|
|
showModal = true;
|
|
},
|
|
eventDisplay: "block",
|
|
dayMaxEvents: 3,
|
|
moreLinkClick: "popover",
|
|
height: "auto",
|
|
contentHeight: "auto",
|
|
aspectRatio: 1.8,
|
|
eventMouseEnter: (info) => {
|
|
info.el.style.cursor = "pointer";
|
|
info.el.style.opacity = "0.9";
|
|
},
|
|
eventMouseLeave: (info) => {
|
|
info.el.style.opacity = "1";
|
|
},
|
|
});
|
|
|
|
calendar.render();
|
|
|
|
return () => {
|
|
if (calendar) {
|
|
calendar.destroy();
|
|
}
|
|
};
|
|
});
|
|
|
|
// Atualizar eventos quando mudarem
|
|
$effect(() => {
|
|
if (calendar) {
|
|
calendar.removeAllEvents();
|
|
calendar.addEventSource(eventosFullCalendar);
|
|
calendar.refetchEvents();
|
|
}
|
|
});
|
|
|
|
function formatarData(data: string): string {
|
|
return new Date(data).toLocaleDateString("pt-BR", {
|
|
day: "2-digit",
|
|
month: "long",
|
|
year: "numeric",
|
|
});
|
|
}
|
|
|
|
function getTipoNome(tipo: string): string {
|
|
const nomes: Record<string, string> = {
|
|
atestado_medico: "Atestado Médico",
|
|
declaracao_comparecimento: "Declaração de Comparecimento",
|
|
maternidade: "Licença Maternidade",
|
|
paternidade: "Licença Paternidade",
|
|
ferias: "Férias",
|
|
};
|
|
return nomes[tipo] || tipo;
|
|
}
|
|
|
|
function getTipoCor(tipo: string): string {
|
|
const cores: Record<string, string> = {
|
|
atestado_medico: "text-error",
|
|
declaracao_comparecimento: "text-warning",
|
|
maternidade: "text-secondary",
|
|
paternidade: "text-info",
|
|
ferias: "text-success",
|
|
};
|
|
return cores[tipo] || "text-base-content";
|
|
}
|
|
</script>
|
|
|
|
<div class="card bg-base-100 shadow-xl">
|
|
<div class="card-body">
|
|
<!-- Header com filtros -->
|
|
<div
|
|
class="flex flex-col md:flex-row items-start md:items-center justify-between gap-4 mb-6"
|
|
>
|
|
<h2 class="card-title text-2xl">Calendário de Afastamentos</h2>
|
|
|
|
<!-- Filtros -->
|
|
<div class="flex flex-wrap items-center gap-2">
|
|
<span class="text-sm font-medium text-base-content/70">Filtrar:</span>
|
|
<div class="join">
|
|
<button
|
|
class="join-item btn btn-sm {filtroAtivo === 'todos'
|
|
? 'btn-active btn-primary'
|
|
: 'btn-ghost'}"
|
|
onclick={() => (filtroAtivo = "todos")}
|
|
>
|
|
Todos
|
|
</button>
|
|
<button
|
|
class="join-item btn btn-sm {filtroAtivo === 'atestado_medico'
|
|
? 'btn-active btn-error'
|
|
: 'btn-ghost'}"
|
|
onclick={() => (filtroAtivo = "atestado_medico")}
|
|
>
|
|
Atestados
|
|
</button>
|
|
<button
|
|
class="join-item btn btn-sm {filtroAtivo ===
|
|
'declaracao_comparecimento'
|
|
? 'btn-active btn-warning'
|
|
: 'btn-ghost'}"
|
|
onclick={() => (filtroAtivo = "declaracao_comparecimento")}
|
|
>
|
|
Declarações
|
|
</button>
|
|
<button
|
|
class="join-item btn btn-sm {filtroAtivo === 'maternidade'
|
|
? 'btn-active btn-secondary'
|
|
: 'btn-ghost'}"
|
|
onclick={() => (filtroAtivo = "maternidade")}
|
|
>
|
|
Maternidade
|
|
</button>
|
|
<button
|
|
class="join-item btn btn-sm {filtroAtivo === 'paternidade'
|
|
? 'btn-active btn-info'
|
|
: 'btn-ghost'}"
|
|
onclick={() => (filtroAtivo = "paternidade")}
|
|
>
|
|
Paternidade
|
|
</button>
|
|
<button
|
|
class="join-item btn btn-sm {filtroAtivo === 'ferias'
|
|
? 'btn-active btn-success'
|
|
: 'btn-ghost'}"
|
|
onclick={() => (filtroAtivo = "ferias")}
|
|
>
|
|
Férias
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Legenda -->
|
|
<div class="flex flex-wrap gap-4 mb-4 p-4 bg-base-200/50 rounded-lg">
|
|
<div class="flex items-center gap-2">
|
|
<div class="w-4 h-4 rounded bg-error"></div>
|
|
<span class="text-sm">Atestado Médico</span>
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
<div class="w-4 h-4 rounded bg-warning"></div>
|
|
<span class="text-sm">Declaração</span>
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
<div class="w-4 h-4 rounded bg-secondary"></div>
|
|
<span class="text-sm">Licença Maternidade</span>
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
<div class="w-4 h-4 rounded bg-info"></div>
|
|
<span class="text-sm">Licença Paternidade</span>
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
<div class="w-4 h-4 rounded bg-success"></div>
|
|
<span class="text-sm">Férias</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Calendário -->
|
|
<div class="w-full overflow-x-auto">
|
|
<div bind:this={calendarEl} class="calendar-container"></div>
|
|
</div>
|
|
|
|
<!-- Modal de Detalhes -->
|
|
{#if showModal && eventoSelecionado}
|
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
<div
|
|
class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm"
|
|
onclick={() => (showModal = false)}
|
|
role="dialog"
|
|
aria-modal="true"
|
|
>
|
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
<div
|
|
class="bg-base-100 rounded-2xl shadow-2xl w-full max-w-md mx-4 transform transition-all"
|
|
onclick={(e) => e.stopPropagation()}
|
|
>
|
|
<!-- Header do Modal -->
|
|
<div
|
|
class="p-6 border-b border-base-300 bg-linear-to-r from-primary/10 to-secondary/10"
|
|
>
|
|
<div class="flex items-start justify-between">
|
|
<div class="flex-1">
|
|
<h3 class="text-xl font-bold text-base-content mb-2">
|
|
{eventoSelecionado.funcionarioNome}
|
|
</h3>
|
|
<p
|
|
class="text-sm {getTipoCor(
|
|
eventoSelecionado.tipo,
|
|
)} font-medium"
|
|
>
|
|
{getTipoNome(eventoSelecionado.tipo)}
|
|
</p>
|
|
</div>
|
|
<button
|
|
class="btn btn-sm btn-circle btn-ghost"
|
|
onclick={() => (showModal = false)}
|
|
aria-label="Fechar"
|
|
>
|
|
<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="M6 18L18 6M6 6l12 12"
|
|
/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Conteúdo do Modal -->
|
|
<div class="p-6 space-y-4">
|
|
<div class="flex items-center gap-3 p-4 bg-base-200/50 rounded-lg">
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
class="h-6 w-6 text-primary"
|
|
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>
|
|
<p class="text-sm text-base-content/60">Data Início</p>
|
|
<p class="font-semibold">
|
|
{formatarData(eventoSelecionado.start)}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex items-center gap-3 p-4 bg-base-200/50 rounded-lg">
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
class="h-6 w-6 text-secondary"
|
|
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>
|
|
<p class="text-sm text-base-content/60">Data Fim</p>
|
|
<p class="font-semibold">
|
|
{formatarData(eventoSelecionado.end)}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex items-center gap-3 p-4 bg-base-200/50 rounded-lg">
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
class="h-6 w-6 text-accent"
|
|
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>
|
|
<div>
|
|
<p class="text-sm text-base-content/60">Duração</p>
|
|
<p class="font-semibold">
|
|
{(() => {
|
|
const inicio = new Date(eventoSelecionado.start);
|
|
const fim = new Date(eventoSelecionado.end);
|
|
const diffTime = Math.abs(fim.getTime() - inicio.getTime());
|
|
const diffDays =
|
|
Math.ceil(diffTime / (1000 * 60 * 60 * 24)) + 1;
|
|
return `${diffDays} ${diffDays === 1 ? "dia" : "dias"}`;
|
|
})()}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Footer do Modal -->
|
|
<div class="p-6 border-t border-base-300 flex justify-end">
|
|
<button class="btn btn-primary" onclick={() => (showModal = false)}>
|
|
Fechar
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
:global(.calendar-container) {
|
|
font-family: inherit;
|
|
}
|
|
|
|
:global(.fc) {
|
|
font-family: inherit;
|
|
}
|
|
|
|
:global(.fc-header-toolbar) {
|
|
margin-bottom: 1.5rem;
|
|
flex-wrap: wrap;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
:global(.fc-button) {
|
|
background-color: hsl(var(--p));
|
|
border-color: hsl(var(--p));
|
|
color: hsl(var(--pc));
|
|
padding: 0.5rem 1rem;
|
|
border-radius: 0.5rem;
|
|
font-weight: 500;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
:global(.fc-button:hover) {
|
|
background-color: hsl(var(--pf));
|
|
border-color: hsl(var(--pf));
|
|
transform: translateY(-1px);
|
|
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
|
|
}
|
|
|
|
:global(.fc-button:active) {
|
|
transform: translateY(0);
|
|
}
|
|
|
|
:global(.fc-button-active) {
|
|
background-color: hsl(var(--a));
|
|
border-color: hsl(var(--a));
|
|
color: hsl(var(--ac));
|
|
}
|
|
|
|
:global(.fc-today-button) {
|
|
background-color: hsl(var(--s));
|
|
border-color: hsl(var(--s));
|
|
}
|
|
|
|
:global(.fc-daygrid-day-number) {
|
|
padding: 0.5rem;
|
|
font-weight: 500;
|
|
}
|
|
|
|
:global(.fc-day-today) {
|
|
background-color: hsl(var(--p) / 0.1) !important;
|
|
}
|
|
|
|
:global(.fc-day-today .fc-daygrid-day-number) {
|
|
background-color: hsl(var(--p));
|
|
color: hsl(var(--pc));
|
|
border-radius: 50%;
|
|
width: 2rem;
|
|
height: 2rem;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-weight: 700;
|
|
}
|
|
|
|
:global(.fc-event) {
|
|
border-radius: 0.375rem;
|
|
padding: 0.25rem 0.5rem;
|
|
font-size: 0.875rem;
|
|
font-weight: 500;
|
|
border: none;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1);
|
|
}
|
|
|
|
:global(.fc-event:hover) {
|
|
transform: translateY(-1px);
|
|
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
|
|
opacity: 0.9;
|
|
}
|
|
|
|
:global(.fc-event-title) {
|
|
font-weight: 600;
|
|
padding: 0;
|
|
}
|
|
|
|
:global(.fc-daygrid-event) {
|
|
margin: 0.125rem 0;
|
|
}
|
|
|
|
:global(.fc-daygrid-day-frame) {
|
|
min-height: 100px;
|
|
}
|
|
|
|
:global(.fc-col-header-cell) {
|
|
padding: 0.75rem 0;
|
|
background-color: hsl(var(--b2));
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
font-size: 0.875rem;
|
|
color: hsl(var(--bc));
|
|
}
|
|
|
|
:global(.fc-daygrid-day) {
|
|
border-color: hsl(var(--b3));
|
|
}
|
|
|
|
:global(.fc-scrollgrid) {
|
|
border-color: hsl(var(--b3));
|
|
}
|
|
|
|
:global(.fc-daygrid-day-frame) {
|
|
padding: 0.25rem;
|
|
}
|
|
|
|
:global(.fc-more-link) {
|
|
font-weight: 600;
|
|
color: hsl(var(--p));
|
|
background-color: hsl(var(--p) / 0.1);
|
|
padding: 0.25rem 0.5rem;
|
|
border-radius: 0.375rem;
|
|
margin-top: 0.25rem;
|
|
}
|
|
|
|
:global(.fc-popover) {
|
|
background-color: hsl(var(--b1));
|
|
border-color: hsl(var(--b3));
|
|
box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1);
|
|
border-radius: 0.5rem;
|
|
}
|
|
|
|
:global(.fc-popover-header) {
|
|
background-color: hsl(var(--b2));
|
|
border-color: hsl(var(--b3));
|
|
padding: 0.75rem;
|
|
font-weight: 600;
|
|
}
|
|
|
|
:global(.fc-popover-body) {
|
|
padding: 0.5rem;
|
|
}
|
|
</style>
|