feat: integrate interactive absence calendar in atestados-licencas page
- Added the CalendarioAfastamentos component to display an interactive calendar for managing absences. - Removed the previous static calendar placeholder and replaced it with a dynamic query to fetch absence events. - Enhanced user experience by allowing users to visualize absence events directly in the calendar format.
This commit is contained in:
517
apps/web/src/lib/components/CalendarioAfastamentos.svelte
Normal file
517
apps/web/src/lib/components/CalendarioAfastamentos.svelte
Normal file
@@ -0,0 +1,517 @@
|
|||||||
|
<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-gradient-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>
|
||||||
@@ -1,377 +1,383 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { useQuery, useConvexClient } from "convex-svelte";
|
import { useQuery, useConvexClient } from "convex-svelte";
|
||||||
import { api } from "@sgse-app/backend/convex/_generated/api";
|
import { api } from "@sgse-app/backend/convex/_generated/api";
|
||||||
import type { Id } from "@sgse-app/backend/convex/_generated/dataModel";
|
import type { Id } from "@sgse-app/backend/convex/_generated/dataModel";
|
||||||
|
|
||||||
let { onClose }: { onClose: () => void } = $props();
|
let { onClose }: { onClose: () => void } = $props();
|
||||||
|
|
||||||
const client = useConvexClient();
|
const client = useConvexClient();
|
||||||
const alertas = useQuery(api.monitoramento.listarAlertas, {});
|
const alertasQuery = useQuery(api.monitoramento.listarAlertas, {});
|
||||||
|
const alertas = $derived.by(() => {
|
||||||
// Estado para novo alerta
|
if (!alertasQuery) return [];
|
||||||
let editingAlertId = $state<Id<"alertConfigurations"> | null>(null);
|
// O useQuery pode retornar o array diretamente ou em .data
|
||||||
let metricName = $state("cpuUsage");
|
if (Array.isArray(alertasQuery)) return alertasQuery;
|
||||||
let threshold = $state(80);
|
return alertasQuery.data ?? [];
|
||||||
let operator = $state<">" | "<" | ">=" | "<=" | "==">(">");
|
});
|
||||||
let enabled = $state(true);
|
|
||||||
let notifyByEmail = $state(false);
|
// Estado para novo alerta
|
||||||
let notifyByChat = $state(true);
|
let editingAlertId = $state<Id<"alertConfigurations"> | null>(null);
|
||||||
let saving = $state(false);
|
let metricName = $state("cpuUsage");
|
||||||
let showForm = $state(false);
|
let threshold = $state(80);
|
||||||
|
let operator = $state<">" | "<" | ">=" | "<=" | "==">(">");
|
||||||
const metricOptions = [
|
let enabled = $state(true);
|
||||||
{ value: "cpuUsage", label: "Uso de CPU (%)" },
|
let notifyByEmail = $state(false);
|
||||||
{ value: "memoryUsage", label: "Uso de Memória (%)" },
|
let notifyByChat = $state(true);
|
||||||
{ value: "networkLatency", label: "Latência de Rede (ms)" },
|
let saving = $state(false);
|
||||||
{ value: "storageUsed", label: "Armazenamento Usado (%)" },
|
let showForm = $state(false);
|
||||||
{ value: "usuariosOnline", label: "Usuários Online" },
|
|
||||||
{ value: "mensagensPorMinuto", label: "Mensagens por Minuto" },
|
const metricOptions = [
|
||||||
{ value: "tempoRespostaMedio", label: "Tempo de Resposta (ms)" },
|
{ value: "cpuUsage", label: "Uso de CPU (%)" },
|
||||||
{ value: "errosCount", label: "Contagem de Erros" },
|
{ value: "memoryUsage", label: "Uso de Memória (%)" },
|
||||||
];
|
{ value: "networkLatency", label: "Latência de Rede (ms)" },
|
||||||
|
{ value: "storageUsed", label: "Armazenamento Usado (%)" },
|
||||||
const operatorOptions = [
|
{ value: "usuariosOnline", label: "Usuários Online" },
|
||||||
{ value: ">", label: "Maior que (>)" },
|
{ value: "mensagensPorMinuto", label: "Mensagens por Minuto" },
|
||||||
{ value: ">=", label: "Maior ou igual (≥)" },
|
{ value: "tempoRespostaMedio", label: "Tempo de Resposta (ms)" },
|
||||||
{ value: "<", label: "Menor que (<)" },
|
{ value: "errosCount", label: "Contagem de Erros" },
|
||||||
{ value: "<=", label: "Menor ou igual (≤)" },
|
];
|
||||||
{ value: "==", label: "Igual a (=)" },
|
|
||||||
];
|
const operatorOptions = [
|
||||||
|
{ value: ">", label: "Maior que (>)" },
|
||||||
function resetForm() {
|
{ value: ">=", label: "Maior ou igual (≥)" },
|
||||||
editingAlertId = null;
|
{ value: "<", label: "Menor que (<)" },
|
||||||
metricName = "cpuUsage";
|
{ value: "<=", label: "Menor ou igual (≤)" },
|
||||||
threshold = 80;
|
{ value: "==", label: "Igual a (=)" },
|
||||||
operator = ">";
|
];
|
||||||
enabled = true;
|
|
||||||
notifyByEmail = false;
|
function resetForm() {
|
||||||
notifyByChat = true;
|
editingAlertId = null;
|
||||||
showForm = false;
|
metricName = "cpuUsage";
|
||||||
}
|
threshold = 80;
|
||||||
|
operator = ">";
|
||||||
function editAlert(alert: any) {
|
enabled = true;
|
||||||
editingAlertId = alert._id;
|
notifyByEmail = false;
|
||||||
metricName = alert.metricName;
|
notifyByChat = true;
|
||||||
threshold = alert.threshold;
|
showForm = false;
|
||||||
operator = alert.operator;
|
}
|
||||||
enabled = alert.enabled;
|
|
||||||
notifyByEmail = alert.notifyByEmail;
|
function editAlert(alert: any) {
|
||||||
notifyByChat = alert.notifyByChat;
|
editingAlertId = alert._id;
|
||||||
showForm = true;
|
metricName = alert.metricName;
|
||||||
}
|
threshold = alert.threshold;
|
||||||
|
operator = alert.operator;
|
||||||
async function saveAlert() {
|
enabled = alert.enabled;
|
||||||
saving = true;
|
notifyByEmail = alert.notifyByEmail;
|
||||||
try {
|
notifyByChat = alert.notifyByChat;
|
||||||
await client.mutation(api.monitoramento.configurarAlerta, {
|
showForm = true;
|
||||||
alertId: editingAlertId || undefined,
|
}
|
||||||
metricName,
|
|
||||||
threshold,
|
async function saveAlert() {
|
||||||
operator,
|
saving = true;
|
||||||
enabled,
|
try {
|
||||||
notifyByEmail,
|
await client.mutation(api.monitoramento.configurarAlerta, {
|
||||||
notifyByChat,
|
alertId: editingAlertId || undefined,
|
||||||
});
|
metricName,
|
||||||
|
threshold,
|
||||||
resetForm();
|
operator,
|
||||||
} catch (error) {
|
enabled,
|
||||||
console.error("Erro ao salvar alerta:", error);
|
notifyByEmail,
|
||||||
alert("Erro ao salvar alerta. Tente novamente.");
|
notifyByChat,
|
||||||
} finally {
|
});
|
||||||
saving = false;
|
|
||||||
}
|
resetForm();
|
||||||
}
|
} catch (error) {
|
||||||
|
console.error("Erro ao salvar alerta:", error);
|
||||||
async function deleteAlert(alertId: Id<"alertConfigurations">) {
|
alert("Erro ao salvar alerta. Tente novamente.");
|
||||||
if (!confirm("Tem certeza que deseja deletar este alerta?")) return;
|
} finally {
|
||||||
|
saving = false;
|
||||||
try {
|
}
|
||||||
await client.mutation(api.monitoramento.deletarAlerta, { alertId });
|
}
|
||||||
} catch (error) {
|
|
||||||
console.error("Erro ao deletar alerta:", error);
|
async function deleteAlert(alertId: Id<"alertConfigurations">) {
|
||||||
alert("Erro ao deletar alerta. Tente novamente.");
|
if (!confirm("Tem certeza que deseja deletar este alerta?")) return;
|
||||||
}
|
|
||||||
}
|
try {
|
||||||
|
await client.mutation(api.monitoramento.deletarAlerta, { alertId });
|
||||||
function getMetricLabel(metricName: string): string {
|
} catch (error) {
|
||||||
return metricOptions.find(m => m.value === metricName)?.label || metricName;
|
console.error("Erro ao deletar alerta:", error);
|
||||||
}
|
alert("Erro ao deletar alerta. Tente novamente.");
|
||||||
|
}
|
||||||
function getOperatorLabel(op: string): string {
|
}
|
||||||
return operatorOptions.find(o => o.value === op)?.label || op;
|
|
||||||
}
|
function getMetricLabel(metricName: string): string {
|
||||||
</script>
|
return metricOptions.find(m => m.value === metricName)?.label || metricName;
|
||||||
|
}
|
||||||
<dialog class="modal modal-open">
|
|
||||||
<div class="modal-box max-w-4xl bg-gradient-to-br from-base-100 to-base-200">
|
function getOperatorLabel(op: string): string {
|
||||||
<button
|
return operatorOptions.find(o => o.value === op)?.label || op;
|
||||||
type="button"
|
}
|
||||||
class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"
|
</script>
|
||||||
onclick={onClose}
|
|
||||||
>
|
<dialog class="modal modal-open">
|
||||||
✕
|
<div class="modal-box max-w-4xl bg-gradient-to-br from-base-100 to-base-200">
|
||||||
</button>
|
<button
|
||||||
|
type="button"
|
||||||
<h3 class="font-bold text-3xl text-primary mb-2">⚙️ Configuração de Alertas</h3>
|
class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"
|
||||||
<p class="text-base-content/60 mb-6">Configure alertas personalizados para monitoramento do sistema</p>
|
onclick={onClose}
|
||||||
|
>
|
||||||
<!-- Botão Novo Alerta -->
|
✕
|
||||||
{#if !showForm}
|
</button>
|
||||||
<button
|
|
||||||
type="button"
|
<h3 class="font-bold text-3xl text-primary mb-2">⚙️ Configuração de Alertas</h3>
|
||||||
class="btn btn-primary mb-6"
|
<p class="text-base-content/60 mb-6">Configure alertas personalizados para monitoramento do sistema</p>
|
||||||
onclick={() => showForm = true}
|
|
||||||
>
|
<!-- Botão Novo Alerta -->
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
{#if !showForm}
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
<button
|
||||||
</svg>
|
type="button"
|
||||||
Novo Alerta
|
class="btn btn-primary mb-6"
|
||||||
</button>
|
onclick={() => showForm = true}
|
||||||
{/if}
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<!-- Formulário de Alerta -->
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||||
{#if showForm}
|
</svg>
|
||||||
<div class="card bg-base-100 shadow-xl mb-6 border-2 border-primary/20">
|
Novo Alerta
|
||||||
<div class="card-body">
|
</button>
|
||||||
<h4 class="card-title text-xl">
|
{/if}
|
||||||
{editingAlertId ? "Editar Alerta" : "Novo Alerta"}
|
|
||||||
</h4>
|
<!-- Formulário de Alerta -->
|
||||||
|
{#if showForm}
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-4">
|
<div class="card bg-base-100 shadow-xl mb-6 border-2 border-primary/20">
|
||||||
<!-- Métrica -->
|
<div class="card-body">
|
||||||
<div class="form-control">
|
<h4 class="card-title text-xl">
|
||||||
<label class="label" for="metric">
|
{editingAlertId ? "Editar Alerta" : "Novo Alerta"}
|
||||||
<span class="label-text font-semibold">Métrica</span>
|
</h4>
|
||||||
</label>
|
|
||||||
<select
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-4">
|
||||||
id="metric"
|
<!-- Métrica -->
|
||||||
class="select select-bordered select-primary"
|
<div class="form-control">
|
||||||
bind:value={metricName}
|
<label class="label" for="metric">
|
||||||
>
|
<span class="label-text font-semibold">Métrica</span>
|
||||||
{#each metricOptions as option}
|
</label>
|
||||||
<option value={option.value}>{option.label}</option>
|
<select
|
||||||
{/each}
|
id="metric"
|
||||||
</select>
|
class="select select-bordered select-primary"
|
||||||
</div>
|
bind:value={metricName}
|
||||||
|
>
|
||||||
<!-- Operador -->
|
{#each metricOptions as option}
|
||||||
<div class="form-control">
|
<option value={option.value}>{option.label}</option>
|
||||||
<label class="label" for="operator">
|
{/each}
|
||||||
<span class="label-text font-semibold">Condição</span>
|
</select>
|
||||||
</label>
|
</div>
|
||||||
<select
|
|
||||||
id="operator"
|
<!-- Operador -->
|
||||||
class="select select-bordered select-primary"
|
<div class="form-control">
|
||||||
bind:value={operator}
|
<label class="label" for="operator">
|
||||||
>
|
<span class="label-text font-semibold">Condição</span>
|
||||||
{#each operatorOptions as option}
|
</label>
|
||||||
<option value={option.value}>{option.label}</option>
|
<select
|
||||||
{/each}
|
id="operator"
|
||||||
</select>
|
class="select select-bordered select-primary"
|
||||||
</div>
|
bind:value={operator}
|
||||||
|
>
|
||||||
<!-- Threshold -->
|
{#each operatorOptions as option}
|
||||||
<div class="form-control">
|
<option value={option.value}>{option.label}</option>
|
||||||
<label class="label" for="threshold">
|
{/each}
|
||||||
<span class="label-text font-semibold">Valor Limite</span>
|
</select>
|
||||||
</label>
|
</div>
|
||||||
<input
|
|
||||||
id="threshold"
|
<!-- Threshold -->
|
||||||
type="number"
|
<div class="form-control">
|
||||||
class="input input-bordered input-primary"
|
<label class="label" for="threshold">
|
||||||
bind:value={threshold}
|
<span class="label-text font-semibold">Valor Limite</span>
|
||||||
min="0"
|
</label>
|
||||||
step="1"
|
<input
|
||||||
/>
|
id="threshold"
|
||||||
</div>
|
type="number"
|
||||||
|
class="input input-bordered input-primary"
|
||||||
<!-- Ativo -->
|
bind:value={threshold}
|
||||||
<div class="form-control">
|
min="0"
|
||||||
<label class="label cursor-pointer justify-start gap-4">
|
step="1"
|
||||||
<span class="label-text font-semibold">Alerta Ativo</span>
|
/>
|
||||||
<input
|
</div>
|
||||||
type="checkbox"
|
|
||||||
class="toggle toggle-primary"
|
<!-- Ativo -->
|
||||||
bind:checked={enabled}
|
<div class="form-control">
|
||||||
/>
|
<label class="label cursor-pointer justify-start gap-4">
|
||||||
</label>
|
<span class="label-text font-semibold">Alerta Ativo</span>
|
||||||
</div>
|
<input
|
||||||
</div>
|
type="checkbox"
|
||||||
|
class="toggle toggle-primary"
|
||||||
<!-- Notificações -->
|
bind:checked={enabled}
|
||||||
<div class="divider">Método de Notificação</div>
|
/>
|
||||||
<div class="flex gap-6">
|
</label>
|
||||||
<label class="label cursor-pointer gap-3">
|
</div>
|
||||||
<input
|
</div>
|
||||||
type="checkbox"
|
|
||||||
class="checkbox checkbox-primary"
|
<!-- Notificações -->
|
||||||
bind:checked={notifyByChat}
|
<div class="divider">Método de Notificação</div>
|
||||||
/>
|
<div class="flex gap-6">
|
||||||
<span class="label-text">
|
<label class="label cursor-pointer gap-3">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 inline mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<input
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z" />
|
type="checkbox"
|
||||||
</svg>
|
class="checkbox checkbox-primary"
|
||||||
Notificar por Chat
|
bind:checked={notifyByChat}
|
||||||
</span>
|
/>
|
||||||
</label>
|
<span class="label-text">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 inline mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<label class="label cursor-pointer gap-3">
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z" />
|
||||||
<input
|
</svg>
|
||||||
type="checkbox"
|
Notificar por Chat
|
||||||
class="checkbox checkbox-secondary"
|
</span>
|
||||||
bind:checked={notifyByEmail}
|
</label>
|
||||||
/>
|
|
||||||
<span class="label-text">
|
<label class="label cursor-pointer gap-3">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 inline mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<input
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
type="checkbox"
|
||||||
</svg>
|
class="checkbox checkbox-secondary"
|
||||||
Notificar por E-mail
|
bind:checked={notifyByEmail}
|
||||||
</span>
|
/>
|
||||||
</label>
|
<span class="label-text">
|
||||||
</div>
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 inline mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||||
<!-- Preview -->
|
</svg>
|
||||||
<div class="alert alert-info mt-4">
|
Notificar por E-mail
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6">
|
</span>
|
||||||
<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>
|
</label>
|
||||||
</svg>
|
</div>
|
||||||
<div>
|
|
||||||
<h4 class="font-bold">Preview do Alerta:</h4>
|
<!-- Preview -->
|
||||||
<p class="text-sm">
|
<div class="alert alert-info mt-4">
|
||||||
Alertar quando <strong>{getMetricLabel(metricName)}</strong> for
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6">
|
||||||
<strong>{getOperatorLabel(operator)}</strong> a <strong>{threshold}</strong>
|
<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>
|
||||||
</p>
|
</svg>
|
||||||
</div>
|
<div>
|
||||||
</div>
|
<h4 class="font-bold">Preview do Alerta:</h4>
|
||||||
|
<p class="text-sm">
|
||||||
<!-- Botões -->
|
Alertar quando <strong>{getMetricLabel(metricName)}</strong> for
|
||||||
<div class="card-actions justify-end mt-4">
|
<strong>{getOperatorLabel(operator)}</strong> a <strong>{threshold}</strong>
|
||||||
<button
|
</p>
|
||||||
type="button"
|
</div>
|
||||||
class="btn btn-ghost"
|
</div>
|
||||||
onclick={resetForm}
|
|
||||||
disabled={saving}
|
<!-- Botões -->
|
||||||
>
|
<div class="card-actions justify-end mt-4">
|
||||||
Cancelar
|
<button
|
||||||
</button>
|
type="button"
|
||||||
<button
|
class="btn btn-ghost"
|
||||||
type="button"
|
onclick={resetForm}
|
||||||
class="btn btn-primary"
|
disabled={saving}
|
||||||
onclick={saveAlert}
|
>
|
||||||
disabled={saving || (!notifyByChat && !notifyByEmail)}
|
Cancelar
|
||||||
>
|
</button>
|
||||||
{#if saving}
|
<button
|
||||||
<span class="loading loading-spinner"></span>
|
type="button"
|
||||||
Salvando...
|
class="btn btn-primary"
|
||||||
{:else}
|
onclick={saveAlert}
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
disabled={saving || (!notifyByChat && !notifyByEmail)}
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
>
|
||||||
</svg>
|
{#if saving}
|
||||||
Salvar Alerta
|
<span class="loading loading-spinner"></span>
|
||||||
{/if}
|
Salvando...
|
||||||
</button>
|
{:else}
|
||||||
</div>
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
</div>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||||
</div>
|
</svg>
|
||||||
{/if}
|
Salvar Alerta
|
||||||
|
{/if}
|
||||||
<!-- Lista de Alertas -->
|
</button>
|
||||||
<div class="divider">Alertas Configurados</div>
|
</div>
|
||||||
|
</div>
|
||||||
{#if alertas && alertas.length > 0}
|
</div>
|
||||||
<div class="overflow-x-auto">
|
{/if}
|
||||||
<table class="table table-zebra">
|
|
||||||
<thead>
|
<!-- Lista de Alertas -->
|
||||||
<tr>
|
<div class="divider">Alertas Configurados</div>
|
||||||
<th>Métrica</th>
|
|
||||||
<th>Condição</th>
|
{#if alertas.length > 0}
|
||||||
<th>Status</th>
|
<div class="overflow-x-auto">
|
||||||
<th>Notificações</th>
|
<table class="table table-zebra">
|
||||||
<th>Ações</th>
|
<thead>
|
||||||
</tr>
|
<tr>
|
||||||
</thead>
|
<th>Métrica</th>
|
||||||
<tbody>
|
<th>Condição</th>
|
||||||
{#each alertas as alerta}
|
<th>Status</th>
|
||||||
<tr class={!alerta.enabled ? "opacity-50" : ""}>
|
<th>Notificações</th>
|
||||||
<td>
|
<th>Ações</th>
|
||||||
<div class="font-semibold">{getMetricLabel(alerta.metricName)}</div>
|
</tr>
|
||||||
</td>
|
</thead>
|
||||||
<td>
|
<tbody>
|
||||||
<div class="badge badge-outline">
|
{#each alertas as alerta}
|
||||||
{getOperatorLabel(alerta.operator)} {alerta.threshold}
|
<tr class={!alerta.enabled ? "opacity-50" : ""}>
|
||||||
</div>
|
<td>
|
||||||
</td>
|
<div class="font-semibold">{getMetricLabel(alerta.metricName)}</div>
|
||||||
<td>
|
</td>
|
||||||
{#if alerta.enabled}
|
<td>
|
||||||
<div class="badge badge-success gap-2">
|
<div class="badge badge-outline">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
{getOperatorLabel(alerta.operator)} {alerta.threshold}
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
</div>
|
||||||
</svg>
|
</td>
|
||||||
Ativo
|
<td>
|
||||||
</div>
|
{#if alerta.enabled}
|
||||||
{:else}
|
<div class="badge badge-success gap-2">
|
||||||
<div class="badge badge-ghost gap-2">
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<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="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
<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>
|
||||||
</svg>
|
Ativo
|
||||||
Inativo
|
</div>
|
||||||
</div>
|
{:else}
|
||||||
{/if}
|
<div class="badge badge-ghost gap-2">
|
||||||
</td>
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<td>
|
<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" />
|
||||||
<div class="flex gap-1">
|
</svg>
|
||||||
{#if alerta.notifyByChat}
|
Inativo
|
||||||
<div class="badge badge-primary badge-sm">Chat</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if alerta.notifyByEmail}
|
</td>
|
||||||
<div class="badge badge-secondary badge-sm">Email</div>
|
<td>
|
||||||
{/if}
|
<div class="flex gap-1">
|
||||||
</div>
|
{#if alerta.notifyByChat}
|
||||||
</td>
|
<div class="badge badge-primary badge-sm">Chat</div>
|
||||||
<td>
|
{/if}
|
||||||
<div class="flex gap-2">
|
{#if alerta.notifyByEmail}
|
||||||
<button
|
<div class="badge badge-secondary badge-sm">Email</div>
|
||||||
type="button"
|
{/if}
|
||||||
class="btn btn-ghost btn-xs"
|
</div>
|
||||||
onclick={() => editAlert(alerta)}
|
</td>
|
||||||
>
|
<td>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<div class="flex gap-2">
|
||||||
<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" />
|
<button
|
||||||
</svg>
|
type="button"
|
||||||
</button>
|
class="btn btn-ghost btn-xs"
|
||||||
<button
|
onclick={() => editAlert(alerta)}
|
||||||
type="button"
|
>
|
||||||
class="btn btn-ghost btn-xs text-error"
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
onclick={() => deleteAlert(alerta._id)}
|
<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>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
</button>
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
<button
|
||||||
</svg>
|
type="button"
|
||||||
</button>
|
class="btn btn-ghost btn-xs text-error"
|
||||||
</div>
|
onclick={() => deleteAlert(alerta._id)}
|
||||||
</td>
|
>
|
||||||
</tr>
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
{/each}
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||||
</tbody>
|
</svg>
|
||||||
</table>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
</td>
|
||||||
<div class="alert">
|
</tr>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-info shrink-0 w-6 h-6">
|
{/each}
|
||||||
<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>
|
</tbody>
|
||||||
</svg>
|
</table>
|
||||||
<span>Nenhum alerta configurado. Clique em "Novo Alerta" para criar um.</span>
|
</div>
|
||||||
</div>
|
{:else}
|
||||||
{/if}
|
<div class="alert">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-info shrink-0 w-6 h-6">
|
||||||
<div class="modal-action">
|
<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>
|
||||||
<button type="button" class="btn btn-lg" onclick={onClose}>Fechar</button>
|
</svg>
|
||||||
</div>
|
<span>Nenhum alerta configurado. Clique em "Novo Alerta" para criar um.</span>
|
||||||
</div>
|
</div>
|
||||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
{/if}
|
||||||
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
|
||||||
<form method="dialog" class="modal-backdrop" onclick={onClose}>
|
<div class="modal-action">
|
||||||
<button type="button">close</button>
|
<button type="button" class="btn btn-lg" onclick={onClose}>Fechar</button>
|
||||||
</form>
|
</div>
|
||||||
</dialog>
|
</div>
|
||||||
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||||
|
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
||||||
|
<form method="dialog" class="modal-backdrop" onclick={onClose}>
|
||||||
|
<button type="button">close</button>
|
||||||
|
</form>
|
||||||
|
</dialog>
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
import FuncionarioSelect from "$lib/components/FuncionarioSelect.svelte";
|
import FuncionarioSelect from "$lib/components/FuncionarioSelect.svelte";
|
||||||
import FileUpload from "$lib/components/FileUpload.svelte";
|
import FileUpload from "$lib/components/FileUpload.svelte";
|
||||||
import ErrorModal from "$lib/components/ErrorModal.svelte";
|
import ErrorModal from "$lib/components/ErrorModal.svelte";
|
||||||
|
import CalendarioAfastamentos from "$lib/components/CalendarioAfastamentos.svelte";
|
||||||
import type { Id } from "@sgse-app/backend/convex/_generated/dataModel";
|
import type { Id } from "@sgse-app/backend/convex/_generated/dataModel";
|
||||||
|
|
||||||
const client = useConvexClient();
|
const client = useConvexClient();
|
||||||
@@ -767,31 +768,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Calendário Simplificado -->
|
<!-- Calendário Interativo -->
|
||||||
<div class="card bg-base-100 shadow-xl mb-6">
|
{#if eventosQuery?.data}
|
||||||
<div class="card-body">
|
<CalendarioAfastamentos eventos={eventosQuery.data} tipoFiltro={filtroTipo} />
|
||||||
<h2 class="card-title mb-4">Calendário de Afastamentos</h2>
|
{/if}
|
||||||
<div class="alert alert-info">
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
class="stroke-current 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
|
|
||||||
>O calendário interativo completo será implementado em breve. Por
|
|
||||||
enquanto, visualize os eventos na tabela abaixo.</span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Gráficos -->
|
<!-- Gráficos -->
|
||||||
{#if graficosQuery?.data}
|
{#if graficosQuery?.data}
|
||||||
|
|||||||
Reference in New Issue
Block a user