<script>
import { onMount } from "svelte";
import { Calendar, Editor, Willow, ContextMenu, getMenuOptions } from "@svar-ui/svelte-calendar";
import { Layout } from "@svar-ui/svelte-layout";
import { Segmented, Locale } from "@svar-ui/svelte-core";
import {
en as enCore,
es as esCore
} from "@svar-ui/core-locales";
import {
en,
es
} from "@svar-ui/calendar-locales";
import EventCard from "./EventCard.svelte";
import Swal from "sweetalert2";
import apiRest from "../lib/api.js";
import EventDrawer from "./EventDrawer.svelte";
const dictionaries = {
en: { calendar: en, core: enCore },
es: { calendar: es, core: esCore },
};
let events = $state([]);
// let calendars = $state([]);
let eventsCalendar = $state([]);
let calendars = $state([]);
let calendarsCSS = $state([]);
let loading = $state(true);
let errorMessage = $state(null);
let filter = $state("");
let filteredData = $state([]);
let selectedCalendars = $state([]); // array de idsvar_calendar Seleccionados para mostrar en el calendario
let locale = $state("es"); // Cambiar a "en" para inglés
let api = $state(); // Variable para almacenar la instancia de la API de Calendar
let fechaBase = new Date();
let vistaCalendar = "month";
let { start, end } = getRangeFromView(fechaBase, vistaCalendar);
let words = $derived({
...dictionaries[locale].calendar,
...dictionaries[locale].core,
});
let drawerOpen = $state(false);
let drawerMode = $state("view");
let selectedEvent = $state({});
// Config options calendar Views
const views = [
{
id: "month",
sections: {
month: {
yScale: {
visible: true,
format: "weekNumberFormat"
}
}
}
},
{
id: "week",
sections: {
timeGrid: {
yScale: { startHour: 9, endHour: 17 }
}
}
},
{
id: "day",
sections: {
timeGrid: {
yScale: { startHour: 9, endHour: 17 }
}
}
}
];
// Configuración de celdas para resaltar fines de semana y días festivos
function cellCss(ctx) {
const { date } = ctx;
if (!date) return "";
const day = date.getDay();
if (day === 0 || day === 6) return "weekend";
if (day === 5) return "holiday";
return "";
}
// Filtrado de datos según el valor del input y selección de calendarios
function onFilterInput(event) {
filter = event.target.value;
filteredData = filterData();
}
function clearFilter() {
filter = "";
filteredData = filterData();
}
function toggleCalendar(id) {
if (selectedCalendars.includes(id)) {
selectedCalendars = selectedCalendars.filter(x => x !== id);
} else {
selectedCalendars = [...selectedCalendars, id];
}
// cada cambio de selección recalcula el filtro
filteredData = filterData();
}
function normalize(str) {
return String(str)
.trim()
.normalize("NFD")
.replace(/[\u0300-\u036f]/g, "")
.toLowerCase();
}
function formatDateEs(dateStr) {
const [fecha, hora] = dateStr.split(" ");
const [y, m, d] = fecha.split("-");
const [hh, mm] = hora.split(":");
return `${d}/${m}/${y} ${hh}:${mm}`;
}
function filterData() {
let base = events;
// 1. FILTRO POR CALENDARIO
if (selectedCalendars.length > 0) {
base = base.filter(e => selectedCalendars.includes(e.id_calendar));
}
// si no hay seleccionados, base = events (todos)
// 2. FILTRO POR TEXTO
let filtered;
if (!filter) {
filtered = base;
} else {
const f = normalize(filter);
filtered = base.filter((row) => {
return (
normalize(row.title).includes(f) ||
normalize(row.calendar_name).includes(f) ||
normalize(row.priority_label).includes(f) ||
normalize(row.names_people).includes(f) ||
normalize(row.names_resource).includes(f) ||
normalize(formatDateEs(row.start)).includes(f) ||
normalize(formatDateEs(row.end)).includes(f)
);
});
}
// console.log("Datos filtrados: ", filtered);
// 3. RECONSTRUIR eventsCalendar desde los Eventos filtrados para que el calendario los muestre correctamente
eventsCalendar = filtered.map(e => {
const isAllDay = e.all_day === "1";
// Convertimos fechas
const start = new Date(e.start);
const end = new Date(e.end);
if (isAllDay) {
// Ajuste para evitar que SVAR Calendar reparta el día en 24 horas
start.setHours(1, 0, 0, 0); // 01:00:00
end.setHours(23, 0, 0, 0); // 23:00:00
}
return {
...e,
id: e.id,
text: e.title,
start,
end,
allDay: isAllDay,
calendarId: e.id_calendar, // clave para el color
};
// console.log("Evento para Calendar: ", eventsCalendar);
});
return filtered;
}
// Función para obtener los calendarios y eventos desde el backend
async function fetchCalendars() {
try {
const res2 = await apiRest.get("/calendar/list");
calendars = res2.data;
console.log("Calendars: ", calendars);
// reconstruye CSS de Calendarios
calendarsCSS = calendars.map(c => ({
id: c.idsvar_calendar,
label: c.name,
css: `cal-${c.idsvar_calendar}`, // clase base para SVAR
color: c.color, // por si lo necesitas en otros sitios
}));
console.log("Calendars CSS: ", calendarsCSS);
} catch (err) {
errorMessage = "No se pudieron cargar los eventos.";
} finally {
// loading = false;
}
}
//
async function fetchEvents() {
console.log("Fetching Calendars...");
try {
console.log("Rango de fechas para fetchEvents: ", start, end);
const resEvents = await apiRest.post("/event/list", {
start: start,
end: end
});
console.log("datos de Eventos: ",resEvents);
events = resEvents.data; // Carga nueva cartga de eventos
console.log("Datos de eventos cargados: ", events);
filteredData = filterData();
console.log("Datos filtrados: ", filteredData);
} catch (err) {
errorMessage = "No se pudieron cargar los eventos.";
} finally {
loading = false;
}
}
// Llamamos a fetchEvents y fetchCalendars al montar el componente
onMount(() => {
fetchCalendars(); // Llamada inicial para cargar calendarios
fetchEvents(); // Llamada inicial para cargar eventos
});
function handleAdd() {
drawerMode = "add";
const now = new Date();
const yyyy = now.getFullYear();
const mm = String(now.getMonth() + 1).padStart(2, "0");
const dd = String(now.getDate()).padStart(2, "0");
const hh = "00";
const min = "00";
selectedEvent = {
id: null,
title: "",
details: "",
start: `${yyyy}-${mm}-${dd}T${hh}:${min}`,
end: `${yyyy}-${mm}-${dd}T${hh}:${min}`,
all_day: "0",
id_calendar: "",
id_priority: "",
id_people: "",
id_resource: ""
};
drawerOpen = true;
}
function handleView(row) {
drawerMode = "view";
selectedEvent = {
...row,
start: formatDateEs(row.start),
end: formatDateEs(row.end)
};
drawerOpen = true;
}
function handleEdit(row) {
drawerMode = "edit";
selectedEvent = { ...row };
drawerOpen = true;
}
async function handleDelete(row) {
const id = row.id;
const name = row.title;
const result = await Swal.fire({
title: "¿Eliminar evento?",
text: `Se eliminará "${name}".`,
icon: "warning",
showCancelButton: true,
confirmButtonText: "Eliminar",
cancelButtonText: "Cancelar"
});
if (!result.isConfirmed) return;
try {
const res = await apiRest.delete(`/event/${id}`);
Swal.fire("Eliminado", res.data.message, "success");
fetchEvents();
} catch (err) {
const msg = err.response?.data?.message ?? "Error desconocido";
Swal.fire("Error", msg, "error");
}
}
async function saveEvent(values) {
try {
let res;
if (drawerMode === "add") {
res = await apiRest.post("/event", values);
} else {
res = await apiRest.put(`/event/${selectedEvent.id}`, values);
}
Swal.fire("Correcto", res.data.message, "success");
closeDrawer();
fetchEvents();
} catch (err) {
const msg = err.response?.data?.message ?? "Error desconocido";
Swal.fire("Error", msg, "error");
}
}
function closeDrawer() {
drawerOpen = false;
selectedEvent = {};
}
// Menú conextual del Calendario
const contextOptions = [
// { id: "switch", text: " Cambia 🧺 ", icon: "wxi-arrows-h" },
{ id: "view", text: "Ver", icon: "wxi-eye" },
{ id: "edit", text: "Editar", icon: "wxi-edit" },
// { id: "add", text: "Agregar", icon: "wxi-plus" },
{ id: "delete", text: "Eliminar", icon: "wxi-delete-outline" }
];
function contextHandler( ev) {
console.log("Contexto del menú: ", ev, ev.action, ev.context);
console.log("Contexto del menú, Id: ", ev.action.id);
// handleView(ev.context );
const id = ev.context.id;
const row = id ? filteredData.find(r => r.id === id) : null;
/*
if (!Array.isArray(editorItems)) {
console.warn("editorItems no es un array:", editorItems);
}
*/
switch (ev.action?.id) {
case "add":
handleAdd();
break;
case "edit":
if (row) {
handleEdit(row);
}
break;
case "view":
if (row) {
handleView(row);
};
break;
case "delete":
if (row) {
handleDelete(row);
}
break;
}
}
// Menú de Calendario
const toolbar = {
items: [
{ id: "nav", comp: "dateNav" },
{ id: "today", comp: "todayButton" },
{ comp: "spacer" },
{ id: "title", comp: "dateLabel" },
{ comp: "spacer" },
{ id: "modes", comp: "richselect" },
// { id: "add-event", comp: "addEventButton" },
],
};
// Función para manejar el clic en un evento del calendario
function getRangeFromView(baseDate, view) {
const start = new Date(baseDate);
const end = new Date(baseDate);
if (view === "day") {
// Día completo
start.setHours(0, 0, 0, 0);
end.setHours(23, 59, 59, 999);
}
else if (view === "week") {
// Semana completa (lunes a domingo)
const day = start.getDay(); // 0=domingo, 1=lunes...
const diff = (day === 0 ? -6 : 1 - day); // mover al lunes
start.setDate(start.getDate() + diff);
start.setHours(0, 0, 0, 0);
end.setTime(start.getTime());
end.setDate(start.getDate() + 6);
end.setHours(23, 59, 59, 999);
}
else if (view === "month") {
// Primer día del mes
start.setDate(1);
start.setHours(0, 0, 0, 0);
// Último día del mes
end.setMonth(start.getMonth() + 1);
end.setDate(0); // día 0 = último del mes anterior
end.setHours(23, 59, 59, 999);
}
return {
start: toMySQL(start),
end: toMySQL(end)
};
}
function toMySQL(date) {
const pad = n => String(n).padStart(2, "0");
return (
date.getFullYear() + "-" +
pad(date.getMonth() + 1) + "-" +
pad(date.getDate()) + " " +
pad(date.getHours()) + ":" +
pad(date.getMinutes()) + ":" +
pad(date.getSeconds())
);
}
// Marcar estilos para la visualización de los eventos en el calendario
function cssByCalendar(ctx) {
return `cal-${ctx.event.calendarId}`;
}
// Aplicar los estilos de los calendarios al inicializar el calendario
function handleInit(api) {
console.log("Calendar inicializado, generando CSS dinámico…");
// Después de tener `calendars` con idsvar_calendar y color
const style = document.createElement("style");
style.innerHTML = calendars.map(c => `
.cal-${c.idsvar_calendar}.wx-box-event,
.cal-${c.idsvar_calendar}.wx-bar-event,
.cal-${c.idsvar_calendar}.wx-month-event,
.cal-${c.idsvar_calendar}.wx-month-box-event {
background-color: ${c.color} !important;
color: white !important;
}
`).join("\n");
document.head.appendChild(style);
}
</script>
<div class="drawer drawer-end">
<input id="event-drawer" type="checkbox" class="drawer-toggle" bind:checked={drawerOpen} />
<div class="drawer-content">
<div class="max-w-5xl mt-6 px-4">
<h2 class="text-2xl font-bold mb-4">Gestión de Eventos</h2>
<div class="flex items-center justify-between gap-4 mb-4">
<!-- 1️⃣ CALENDARIOS -->
<div class="flex items-center gap-2 flex-wrap">
<span class="font-semibold">Calendarios:</span>
{#each calendars as cal}
<label
class="flex items-center gap-2 px-3 py-1 rounded-full text-sm cursor-pointer border"
style={`background-color: ${cal.color}; color: white;`}
>
<input
type="checkbox"
class="checkbox checkbox-xs"
checked={selectedCalendars.includes(cal.idsvar_calendar)}
onchange={() => toggleCalendar(cal.idsvar_calendar)}
/>
{cal.name}
</label>
{/each}
</div>
<!-- 2️⃣ FILTRO (más pequeño) -->
<div class="relative w-60"> <!-- antes w-1/2 -->
<input
type="text"
placeholder="🔎 Filtrar..."
class="input input-bordered w-full pr-10 input-sm"
value={filter}
oninput={onFilterInput}
/>
{#if filter}
<button
type="button"
class="absolute right-2 top-1/2 -translate-y-1/2 text-gray-500 hover:text-gray-700"
onclick={clearFilter}
>
✖
</button>
{/if}
</div>
<!-- 3️⃣ BOTÓN "+" con tooltip -->
<div class="tooltip tooltip-left" data-tip="Añadir nuevo evento">
<button
class="btn btn-circle btn-success w-12 h-12 text-2xl"
onclick={handleAdd}
>
+
</button>
</div>
</div>
{#if loading}
<p class="text-center py-6">Cargando eventos...</p>
{:else if errorMessage}
<div class="alert alert-error">{errorMessage}</div>
{:else}
<Willow>
<div class="h-150">
{#key locale}
<ContextMenu api={api} options={contextOptions} onclick={contextHandler} >
<Locale {words} >
<Calendar
// init={(v) => api = v}
bind:this={api}
init={handleInit}
events={eventsCalendar}
eventCss={cssByCalendar}
view="month"
// views={["day", "week", "month"]}
views={views}
date={new Date()}
toolbar={toolbar}
readonly={true}
eventPopup={EventCard}
onnavigateto={p => {
console.log("Navegando a: ", p);
fechaBase = p.date ?? fechaBase;
vistaCalendar = p.view ?? vistaCalendar;
console.log("Fecha base: ", fechaBase);
console.log("Vista calendar: ", vistaCalendar);
const range = getRangeFromView(fechaBase, vistaCalendar);
start = range.start;
end = range.end;
console.log("Rango de fechas: ", start, end);
fetchEvents(); // llamada a tu backend para recargar eventos según el nuevo rango
}}
{cellCss}
/>
<!-- {#if api}<Editor {api} />{/if} -->
</Locale>
</ContextMenu>
{/key}
</div>
</Willow>
{/if}
</div>
</div>
<div class="drawer-side">
<label for="event-drawer" class="drawer-overlay"></label>
{#if drawerOpen && selectedEvent}
<EventDrawer
mode={drawerMode}
event={selectedEvent}
onSave={saveEvent}
onClose={closeDrawer}
/>
{/if}
</div>
</div>
<style>
:global(.weekend) {
background-color: #f5f0ff !important;
}
:global(.holiday) {
background-color: #fde8e8 !important;
}
:global(.wx-x-headers-row) {
z-index: 5 !important;
}
/*
:global {
.cal-1.wx-box-event,
.cal-1.wx-bar-event {
background-color: #4081EB !important;
color: white !important;
}
.cal-2.wx-box-event,
.cal-2.wx-bar-event {
background-color: #35E638 !important;
color: #444 !important;
}
.cal-3.wx-box-event,
.cal-3.wx-bar-event {
background-color: #EDA92B !important;
color: #444 !important;
}
}
*/
</style>