"use client"; import FullCalendar from "@fullcalendar/react"; import dayGridPlugin from "@fullcalendar/daygrid"; import timeGridPlugin from "@fullcalendar/timegrid"; import interactionPlugin from "@fullcalendar/interaction"; import rrulePlugin from "@fullcalendar/rrule"; // Import rrule plugin import { useList, useUpdate, useNavigation, CrudFilters, } from "@refinedev/core"; import { EventContentArg } from "@fullcalendar/core"; import Modal from "@app/components/modals/BaseModal"; import { useEffect, useState } from "react"; import { CreateEventForm } from "@app/components/CreateEventForm"; import { ScheduleEvent } from "@/types/schedules"; import toast from "react-hot-toast"; import { useBranch } from "@contexts/BranchContext"; import { GenericDropdown } from "@app/components/dropdown/DropDown"; import { classroomsDefaultFilter } from "@lib/filters"; const EventsPage: React.FC = () => { const { mutate: updateEvent } = useUpdate(); const { create } = useNavigation(); const { branchId } = useBranch(); const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); const [selectedEvent, setSelectedEvent] = useState( undefined ); const [selectedDate, setSelectedDate] = useState(null); const [selectedClassroomID, setSelectedClassroomID] = useState(null); const [hoveredEvent, setHoveredEvent] = useState(null); const [tooltipPos, setTooltipPos] = useState<{ x: number; y: number } | null>( null ); const [selectedEventsType, setSelectedEventsType] = useState("meal"); // Store the visible calendar date range in state. const [dateRange, setDateRange] = useState<{ start: string | null; end: string | null; }>({ start: new Date().toISOString().split("T")[0], end: new Date().toISOString().split("T")[0], }); function filterDateGreaterThanMinusOneDay(dateStr: string) { const date = new Date(dateStr); const prevDate = new Date(date.getTime() - 86400000); const prevDateStr = prevDate.toISOString().split("T")[0]; return prevDateStr; } function childrenFilter(classroom: number | null): CrudFilters { return classroom == null || classroom == -1 ? [] : [ { field: "classrooms_schedules.classroom_id", operator: "eq", value: classroom, }, ]; } // const { // data: combinedEvents2, // isLoading: ddd, // error, // } = useList({ // resource: "schedule_events", // filters: [ // { // field: "schedules.date", // operator: "gt", // value: dateRange.start // ? filterDateGreaterThanMinusOneDay(dateRange.start) // : dateRange.start, // }, // { field: "schedules.date", operator: "lte", value: dateRange.end }, // ...childrenFilter(selectedClassroomID), // ], // meta: { // select: ` // *, // classrooms_schedules!inner ( classroom_id ), // schedules!inner ( date ) // `, // }, // pagination: { current: 1, pageSize: 1000 }, // queryOptions: { // enabled: !!selectedClassroomID, // }, // }); // const { // data: combinedEvents2, // isLoading: ddd, // error, // } = useList({ // resource: "schedule_events", // filters: [ // { // field: "schedules.date", // operator: "gt", // value: dateRange.start // ? filterDateGreaterThanMinusOneDay(dateRange.start) // : dateRange.start, // }, // { field: "schedules.date", operator: "lte", value: dateRange.end }, // ...childrenFilter(selectedClassroomID), // ], // meta: { // select: ` // *, // classrooms_schedules!left( classroom_id ), // schedules!inner( date ), // meal:meals(*), // activity:activities(*) // `, // }, // pagination: { current: 1, pageSize: 1000 }, // queryOptions: { // enabled: !!selectedClassroomID, // }, // }); const baseFilters: CrudFilters = [ { field: "schedules.date", operator: "gt", value: filterDateGreaterThanMinusOneDay(dateRange.start!), }, { field: "schedules.date", operator: "lte", value: dateRange.end!, }, { field: "branch_id", operator: "eq", value: branchId }, { field: "event_type.category", operator: "eq", value: selectedEventsType, }, ]; // only tacked on when a classroom is actually selected const classroomFilter: CrudFilters = selectedClassroomID != null ? [ { operator: "or", value: [ // 1) all global events { field: "is_global", operator: "eq", value: true }, // 2) OR any event assigned to this classroom { field: "classrooms_schedules.classroom_id", operator: "eq", value: selectedClassroomID, }, ], }, ] : []; const filters: CrudFilters = [...baseFilters, ...classroomFilter]; const { data: combinedEvents, isLoading, error: eee, } = useList({ resource: "schedule_events", filters: filters as CrudFilters, meta: { select: ` start_time, end_time, is_global, branch_id, event_type!inner(*), classrooms_schedules!left( classroom_id ), schedules!inner( date ), meal:meals(*), activity:activities(*) `, }, pagination: { current: 1, pageSize: 1000 }, }); const events = combinedEvents?.data; console.log("events", events); const { data: classroomsData, isLoading: classroomLoading } = useList({ resource: "classrooms", filters: classroomsDefaultFilter(branchId), pagination: { current: 1, pageSize: 1000 }, }); const classrooms = classroomsData?.data || []; // Handle event click to open edit modal // const handleEventClick = (clickInfo: any) => { // const fcEvent = clickInfo.event; // // Build an event object in the shape your CreateEventPage expects. // // If you stored extra fields (like event_date, start_time, etc.) in extendedProps, use those. // const eventData: ScheduleEvent = { // id: fcEvent.id, // name: fcEvent.title, // description: fcEvent.extendedProps.meal_id, // meal_id: fcEvent.extendedProps.meal_id, // // For non-recurring events, derive date and time from the start string. // event_date: // fcEvent.extendedProps.event_date || fcEvent.startStr.split("T")[0], // start_time: // fcEvent.extendedProps.start_time || // (fcEvent.startStr.split("T")[1] // ? fcEvent.startStr.split("T")[1].substring(0, 8) // : ""), // end_time: // fcEvent.extendedProps.end_time || // (fcEvent.endStr ? fcEvent.endStr.split("T")[1].substring(0, 8) : ""), // // Pass along other fields such as type or category if needed. // meal_category: fcEvent.extendedProps.category, // type: fcEvent.extendedProps.type, // created_at: fcEvent.extendedProps.created_at || new Date().toISOString(), // updated_at: fcEvent.extendedProps.updated_at || new Date().toISOString(), // is_global: fcEvent.extendedProps.all_classes || true, // activity_category: fcEvent.extendedProps.activity_category || "", // }; // setSelectedEvent(eventData); // setIsCreateModalOpen(true); // }; // Update the date range when FullCalendar's view changes. const handleDatesSet = (arg: { startStr: string; endStr: string }) => { console.log("startStr", arg.startStr); console.log("endStr", arg.endStr); setDateRange({ start: arg.startStr, end: arg.endStr }); }; function renderEventContent(eventContent: EventContentArg) { // eventContent.timeText => e.g. "11:00 AM" // eventContent.event.title => e.g. "New Employee Welcome Lunch!" let eventColor = ""; switch (eventContent.event.extendedProps.category) { case "breakfast": eventColor = "text-green-800"; // Green for meetings. break; case "snack": eventColor = "text-red-800"; // Red for holidays. break; case "lunch": eventColor = "text-blue-800"; // Blue for birthdays. break; default: eventColor = "text-primary"; // Default color. } return (
{eventContent.timeText}
{eventContent.event.extendedProps.category}
{/* Insert your emoji or icon here */} {/* 🍔 {" "} */} {eventContent.event.title}
); } const handleEventDrop = (info: any) => { const event = info.event; const updatedValues = { event_date: event.start.toISOString().split("T")[0], start_time: event.start.toTimeString().substring(0, 8), end_time: event.end ? event.end.toTimeString().substring(0, 8) : undefined, }; updateEvent( { resource: "events", id: event.id, values: updatedValues, }, { onSuccess: () => { toast.success("Event updated successfully via drag-drop!"); }, onError: (error) => { toast.error("Failed to update event."); }, } ); }; // const formattedEvents = events?.flatMap((schedule: any) => { // const mealEvents = schedule.schedule_meals.map((mealEntry: any) => ({ // id: `meal-${schedule.id}-${mealEntry.meal.id}`, // title: mealEntry.meal.name, // start: `${schedule.date}T${mealEntry.start_time}`, // end: `${schedule.date}T${mealEntry.end_time}`, // allDay: false, // extendedProps: { // category: mealEntry.meal_type, // meal_id: mealEntry.meal.id, // description: mealEntry.meal.description, // image_url: mealEntry.meal.image_url, // type: "meal", // schedule_id: schedule.id, // }, // })); // const activityEvents = schedule.schedule_activities.map( // (activityEntry: any) => ({ // id: `activity-${schedule.id}-${activityEntry.activity.id}`, // title: activityEntry.activity.name, // start: `${schedule.date}T${activityEntry.start_time}`, // end: `${schedule.date}T${activityEntry.end_time}`, // allDay: false, // extendedProps: { // category: activityEntry.activity.category, // type: "activity", // schedule_id: schedule.id, // }, // }) // ); // return [...mealEvents, ...activityEvents]; // }); // const formattedEvents = events?.flatMap((schedule: any) => { // const mealEvents = schedule.schedule_events.map((mealEntry: any) => ({ // id: `meal-${schedule.id}-${mealEntry.meal.id}`, // title: mealEntry.meal.name, // start: `${schedule.date}T${mealEntry.start_time}`, // end: `${schedule.date}T${mealEntry.end_time}`, // allDay: false, // extendedProps: { // category: mealEntry.meal_type, // meal_id: mealEntry.meal.id, // description: mealEntry.meal.description, // image_url: mealEntry.meal.image_url, // type: "meal", // schedule_id: schedule.id, // }, // })); // return [...mealEvents]; // }); const filteredEvents = selectedClassroomID !== null ? combinedEvents?.data?.filter( (event) => event.classrooms_schedules != null && event.classrooms_schedules.length > 0 && event.is_global === false ) : combinedEvents?.data ?? []; // Client-side you can still do: const eventsForCalendar = selectedClassroomID !== null ? combinedEvents?.data?.filter( (evt) => evt.is_global || // non‐global events must have at least one classroom match evt.classrooms_schedules?.some( (cs) => cs.classroom_id === selectedClassroomID ) ) : combinedEvents?.data ?? []; console.log("filteredEvents", eventsForCalendar); const formattedEvents = eventsForCalendar?.map((event: ScheduleEvent) => { const base = { id: event.id?.toString?.() ?? "", start: event.schedules?.date && event.start_time ? `${event.schedules.date}T${event.start_time}` : "", end: event.schedules?.date && event.end_time ? `${event.schedules.date}T${event.end_time}` : "", allDay: false, }; if (event.meal) { return { ...base, title: event.meal.name, extendedProps: { category: event.event_type.name.toLowerCase() || "", // if you have meal_type here meal_id: event.meal.id, description: event.meal.description, image_url: event.meal.image_url, type: "meal", schedule_event_id: event.id, classrooms: event.classrooms_schedules, // optional }, }; } if (event.activity) { return { ...base, title: event.activity.name, extendedProps: { type: "activity", schedule_event_id: event.id, classrooms: event.classrooms_schedules, }, }; } // If neither meal nor activity, fallback title return { ...base, title: "Event", extendedProps: { type: "unknown", schedule_event_id: event.id, classrooms: event.classrooms_schedules, }, }; }); console.log("formattedEvents", formattedEvents); useEffect(() => { const loadParentRelationships = () => { console.log("dateRange", dateRange); }; loadParentRelationships(); }, [dateRange]); if (isLoading) return
Loading...
; if (eee) return
Error loading schedules
; return (

Schedule

setSelectedClassroomID(Number(id))} defaultLabel="All Classrooms" /> setSelectedEventsType(String(id))} />
{/* Add Event Button */}
{ const mealCat = arg.event.extendedProps.category; // Decide the base color (e.g., for breakfast -> green) // Then apply that color for both the left border and background. switch (mealCat) { case "breakfast": return [ "border-l-4", "border-l-green-500", // left border "border-green-500", // left border "bg-green-500/20", // slightly transparent background "rounded-sm", "text-green-500", "font-semibold", ]; case "snack": return [ "border-l-4", "border-l-red-500", // left border "border-red-500", // left border "bg-red-500/20", // slightly transparent background "rounded-sm", "font-semibold", ]; case "lunch": return [ "border-l-4", "border-l-blue-500", // left border "border-blue-500", // left border "bg-blue-500/10", // slightly transparent background "rounded-sm", "font-semibold", ]; default: return [ "border-l-4", "border-l-primary", // left border "border-primary", // left border "bg-primary/10", // slightly transparent background "rounded-sm", "text-white", "font-semibold", ]; } }} editable={true} droppable={true} selectable={true} selectMirror={true} eventDurationEditable={false} // Disables resizing of event duration slotMinTime="09:00:00" // 🔹 Earliest time to show (6 AM) slotMaxTime="18:00:00" // Extend visibility to midnight slotDuration="00:30:00" // 🔹 Time slot interval (30 min per row) allDaySlot={false} // 🔹 Remove "All Day" row height="auto" // 🔹 Adjust height automatically eventTimeFormat={{ hour: "numeric", minute: "2-digit", meridiem: false, }} dateClick={(arg) => { console.log("dateClick", arg); setSelectedDate(arg.dateStr); setIsCreateModalOpen(true); }} datesSet={handleDatesSet} //eventClick={handleEventClick} // <-- Handle click on an event eventDrop={handleEventDrop} // <-- Handle drag-drop eventMouseEnter={(mouseEnterInfo) => { const { event, jsEvent } = mouseEnterInfo; setHoveredEvent({ title: event.title, extendedProps: event.extendedProps, start: event.start, end: event.end, }); setTooltipPos({ x: jsEvent.pageX, y: jsEvent.pageY, }); }} eventMouseLeave={() => { setHoveredEvent(null); setTooltipPos(null); }} />
{ setIsCreateModalOpen(false); setSelectedEvent(undefined); // Clear the selected event }} title="Create Event" > setIsCreateModalOpen(false)} // Pass the callback initialEvent={selectedEvent} /> {hoveredEvent && tooltipPos && (

{hoveredEvent.title}

Start: {hoveredEvent.start?.toLocaleString()}

End: {hoveredEvent.end?.toLocaleString()}

{/* Show any extendedProps you want */} {Object.entries(hoveredEvent.extendedProps).map(([key, value]) => (

{key}: {String(value)}

))}
)}
); }; export default EventsPage;