import React, {useState, useEffect, useRef, useCallback} from "react"; import { useNavigate } from "react-router-dom"; import { AuthService, EventsService, CustomerService, ResourceService, VehicleService, EmployeeService } from "../../services"; import moment from 'moment'; import { Breadcrumb, Tabs, Tab, Button, Modal, Dropdown, Spinner } from "react-bootstrap"; import { useCalendarApp, ScheduleXCalendar } from '@schedule-x/react'; import { viewMonthGrid, createViewDay, createViewWeek, createViewMonthGrid } from '@schedule-x/calendar'; import { createEventsServicePlugin } from '@schedule-x/events-service'; import { createEventModalPlugin} from '@schedule-x/event-modal'; import { createEventRecurrencePlugin } from "@schedule-x/event-recurrence"; import '@schedule-x/theme-default/dist/calendar.css'; import { Archive, PencilSquare, Filter, Plus, X } from "react-bootstrap-icons"; import DatePicker from "react-datepicker"; import { vehicleSlice } from "../../store"; import Select from 'react-select'; // import { Scheduler } from "@aldabil/react-scheduler"; const EventsCalendar = () => { const navigate = useNavigate(); const calendarTabOrder = ['medicalCalendar', 'activitiesCalendar', 'incidentsCalendar', 'mealPlanCalendar', 'reminderDatesCalendar']; const getFirstVisibleCalendarTab = () => { return calendarTabOrder.find((tabKey) => AuthService.canViewCalendarTab(tabKey) || AuthService.canEditCalendarTab(tabKey)) || 'medicalCalendar'; }; const [events, setEvents] = useState([]); const calendarColumnRef = useRef(null); const [listHeight, setListHeight] = useState(null); const [allEvents, setAllEvents] = useState([]); const [targetedEventType, setTargetedEventType] = useState('medical'); const [currentTab, setCurrentTab] = useState(getFirstVisibleCalendarTab()); const currentTabRef = useRef(getFirstVisibleCalendarTab()); const [customers, setCustomers] = useState([]); const [resources, setResources] = useState([]); const [fromDate, setFromDate] = useState(new Date(new Date().getFullYear(), new Date().getMonth(), 1)); const [toDate, setToDate] = useState(new Date(new Date().getFullYear(), new Date().getMonth() + 1, 0)); const [currentTotalTranslate1, setCurrentTotalTranslate1] = useState(0); const [currentTotalTranslate2, setCurrentTotalTranslate2] = useState(0); const [currentTotalResource, setCurrentTotalResource] = useState(0); const [showDeletedItems, setShowDeletedItems] = useState(false); const [selectedColorFilters, setSelectedColorFilters] = useState([]); const [timeData, setTimeData] = useState([]); const [showFilterDropdown, setShowFilterDropdown] = useState(false); const eventsServicePlugin = createEventsServicePlugin(); const eventModalService = createEventModalPlugin(); const eventRecurrence = createEventRecurrencePlugin(); const [groupedEvents, setGroupedEvents] = useState(new Map()); const [currentRangeStart, setCurrentRangeStart] = useState(null); const [currentRangeEnd, setCurrentRangeEnd] = useState(null); const [showCreationModal, setShowCreationModal] = useState(false); const [newEventStartDateTime, setNewEventStartDateTime] = useState(new Date()); const [newEventEndDateTime, setNewEventEndDateTime] = useState(new Date()) const [newEventType, setNewEventType] = useState(''); const [newEventTitle, setNewEventTitle] = useState(''); const [newEventDescription, setNewEventDescription] = useState(''); const [newEventLocation, setNewEventLocation] = useState(''); const [newEventTarget, setNewEventTarget] = useState(undefined); const [newEventSource, setNewEventSource] = useState(undefined); const [newEventDepartment, setNewEventDepartment] = useState(''); const [newEventColor, setNewEventColor] = useState(''); const [newEventSourceType, setNewEventSourceType] = useState(''); const [newEventTargetType, setNewEventTargetType] = useState(''); const [newEventFutureDate, setNewEventFutureDate] = useState(undefined); const [newEventReminderType, setNewEventReminderType] = useState(''); const [newEventRecurring, setNewEventRecurring] = useState(undefined); const [vehicles, setVehicles] = useState([]); const [employees, setEmployees] = useState([]); // Medical appointment specific fields const [newEventCustomer, setNewEventCustomer] = useState(''); const [newEventResource, setNewEventResource] = useState(''); const [newEventInterpreter, setNewEventInterpreter] = useState(''); const [newEventFasting, setNewEventFasting] = useState(''); const [newEventNeedId, setNewEventNeedId] = useState(''); const [newEventNewPatient, setNewEventNewPatient] = useState(''); const [newEventDisability, setNewEventDisability] = useState(''); const [newEventTransMethod, setNewEventTransMethod] = useState(''); // Activity specific fields const [newActivityCategory, setNewActivityCategory] = useState(''); // Attendance Note specific fields const [newAttendanceCustomer, setNewAttendanceCustomer] = useState(null); const [newAttendanceReason, setNewAttendanceReason] = useState(''); // Meal Plan specific fields const [newMealType, setNewMealType] = useState(''); const [newMealIngredients, setNewMealIngredients] = useState(''); // Important Dates specific fields const [newReminderTitleCategory, setNewReminderTitleCategory] = useState(''); const [newReminderAssociatedEntity, setNewReminderAssociatedEntity] = useState(null); // Repeat date fields (new modal) const [newRepeatStartDate, setNewRepeatStartDate] = useState(null); const [newRepeatEndDate, setNewRepeatEndDate] = useState(null); const [newIndefiniteRepeat, setNewIndefiniteRepeat] = useState(false); // Event recurrences data const [allEventRecurrences, setAllEventRecurrences] = useState([]); // Edit modal state (for non-medical tabs) const [showEditModal, setShowEditModal] = useState(false); const [editingEventId, setEditingEventId] = useState(null); const [editEventTitle, setEditEventTitle] = useState(''); const [editEventStartDateTime, setEditEventStartDateTime] = useState(null); const [editEventLocation, setEditEventLocation] = useState(''); const [editEventRecurring, setEditEventRecurring] = useState(''); // Activity edit fields const [editActivityCategory, setEditActivityCategory] = useState(''); // Attendance edit fields const [editAttendanceCustomer, setEditAttendanceCustomer] = useState(null); const [editAttendanceReason, setEditAttendanceReason] = useState(''); // Meal Plan edit fields const [editMealType, setEditMealType] = useState(''); const [editMealIngredients, setEditMealIngredients] = useState(''); // Important Dates edit fields const [editReminderTitleCategory, setEditReminderTitleCategory] = useState(''); const [editReminderAssociatedEntity, setEditReminderAssociatedEntity] = useState(null); // Repeat date fields (edit modal) const [editRepeatStartDate, setEditRepeatStartDate] = useState(null); const [editRepeatEndDate, setEditRepeatEndDate] = useState(null); const [editIndefiniteRepeat, setEditIndefiniteRepeat] = useState(false); const [editingRecurId, setEditingRecurId] = useState(null); // tracks if editing a recurrence rule // Delete confirmation modal const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const [deleteTargetId, setDeleteTargetId] = useState(null); const [showSpinner, setShowSpinner] = useState(false); const visibleCalendarTabs = calendarTabOrder.filter((tabKey) => AuthService.canViewCalendarTab(tabKey) || AuthService.canEditCalendarTab(tabKey)); const canEditCurrentTab = () => AuthService.canEditCalendarTab(currentTab); // Helper function to format name from "lastname, firstname" to "firstname lastname" const formatFullName = (name) => { if (!name) return ''; if (name.includes(',')) { const parts = name.split(',').map(part => part.trim()); return `${parts[1]} ${parts[0]}`; // firstname lastname } return name; }; // Helper function to get shortened name const getShortenedName = (name) => { if (!name) return ''; const fullName = formatFullName(name); const parts = fullName.split(' '); if (parts.length >= 2) { return `${parts[0]} ${parts[parts.length - 1].charAt(0)}`; // FirstName L } return fullName; }; // Helper function to format event title - now uses full name, CSS handles overflow const formatEventTitle = (customerName, startTime) => { const fullName = formatFullName(customerName); // Return full name - CSS will handle truncation with text-overflow: ellipsis return fullName; }; // Get full name for description/tooltip const getEventDescription = (customerName, doctorName) => { const fullName = formatFullName(customerName); return doctorName ? `${fullName} - ${doctorName}` : fullName; }; // Helper: expand a recurring rule into individual event instances for a date range const expandRecurrence = (rule, rangeFrom, rangeTo) => { const instances = []; const startDate = new Date(rule.start_repeat_date); const endDate = new Date(rule.end_repeat_date); const from = new Date(rangeFrom); const to = new Date(rangeTo); // Determine the effective range (overlap of rule range and visible range) const effectiveStart = from > startDate ? from : startDate; const effectiveEnd = to < endDate ? to : endDate; if (effectiveStart > effectiveEnd) return instances; const freq = rule.rrule; // e.g. 'FREQ=DAILY', 'FREQ=WEEKLY', 'FREQ=MONTHLY', 'FREQ=YEARLY' let current = new Date(startDate); // Limit to 1000 instances max to avoid infinite loops let count = 0; while (current <= effectiveEnd && count < 1000) { if (current >= effectiveStart) { const dateStr = moment(current).format('YYYY-MM-DD'); instances.push({ ...rule, id: `recur-${rule.id}-${dateStr}`, _recur_id: rule.id, start_time: new Date(current), stop_time: new Date(current), }); } // Advance to next occurrence if (freq === 'FREQ=DAILY') { current = new Date(current.getTime() + 24 * 60 * 60 * 1000); } else if (freq === 'FREQ=WEEKLY') { current = new Date(current.getTime() + 7 * 24 * 60 * 60 * 1000); } else if (freq === 'FREQ=MONTHLY') { const next = new Date(current); next.setMonth(next.getMonth() + 1); current = next; } else if (freq === 'FREQ=YEARLY') { const next = new Date(current); next.setFullYear(next.getFullYear() + 1); current = next; } else { break; } count++; } return instances; }; const buildDynamicBirthdayEvents = (customerList = [], rangeFrom, rangeTo) => { if (!rangeFrom || !rangeTo) return []; const from = new Date(rangeFrom); const to = new Date(rangeTo); const startYear = from.getFullYear(); const endYear = to.getFullYear(); const dynamicEvents = []; customerList.forEach((customer) => { const birthDate = `${customer?.birth_date || ''}`; const [month, day] = birthDate.split('/').map(Number); if (!month || !day) return; for (let year = startYear; year <= endYear; year++) { const birthday = new Date(year, month - 1, day); if (Number.isNaN(birthday.getTime())) continue; if (birthday < from || birthday > to) continue; dynamicEvents.push({ id: `dynamic-birthday-${customer.id}-${year}`, type: 'reminder', status: 'active', title: `${customer?.name}'s Birthday`, description: `Happy Birthday to ${customer?.name}`, start_time: birthday, stop_time: new Date(birthday.getTime() + 10 * 60 * 1000), color: 'member_related', target_type: 'customer', target_uuid: customer?.id, target_name: customer?.name, event_reminder_type: 'birthday', rrule: 'FREQ=YEARLY', create_by: 'system-dynamic', edit_by: 'system-dynamic' }); } }); return dynamicEvents; }; const eventTypeMap = { medicalCalendar: 'medical', activitiesCalendar: 'activity', incidentsCalendar: 'incident', mealPlanCalendar: 'meal_plan', reminderDatesCalendar: 'reminder' } const calendar = useCalendarApp({ views: [createViewMonthGrid(), createViewDay(), createViewWeek()], monthGridOptions: { /** * Number of events to display in a day cell before the "+ N events" button is shown * */ nEventsPerDay: 50, }, defaultView: viewMonthGrid.name, skipValidation: true, selectedDate: moment(new Date()).format('YYYY-MM-DD HH:mm'), events: events, plugins: [eventModalService, eventsServicePlugin, eventRecurrence], callbacks: { onRangeUpdate(range) { console.log('new calendar range start date', range.start); console.log('new calendar range end date', range.end); setCurrentRangeStart(range.start); setCurrentRangeEnd(range.end); // Update fromDate/toDate for API fetching based on the range const startDate = new Date(range.start); const endDate = new Date(range.end); setFromDate(new Date(startDate.getFullYear(), startDate.getMonth(), 1)); setToDate(new Date(endDate.getFullYear(), endDate.getMonth() + 1, 0)); }, onClickDate(date) { if (currentTabRef.current === 'medicalCalendar' || !AuthService.canEditCalendarTab(currentTabRef.current)) return; // Parse as local date to avoid UTC timezone offset shifting the day const [y, m, d] = date.split('-').map(Number); const localDate = new Date(y, m - 1, d); // Default to 10:00 AM for Activities tab if (currentTabRef.current === 'activitiesCalendar') { localDate.setHours(10, 0, 0, 0); } setNewEventStartDateTime(localDate); setNewEventEndDateTime(localDate); setShowCreationModal(true); }, onClickDateTime(dateTime) { if (currentTabRef.current === 'medicalCalendar' || !AuthService.canEditCalendarTab(currentTabRef.current)) return; setNewEventStartDateTime(new Date(dateTime.replace(' ', 'T'))); setNewEventEndDateTime(new Date(dateTime.replace(' ', 'T'))); setShowCreationModal(true); } } }); // Filter events based on current calendar range const getFilteredEvents = () => { console.log("CenterCalendar - range:", currentRangeStart, "to", currentRangeEnd, "events:", events.length); if (!currentRangeStart || !currentRangeEnd) { // If no range set yet, show all events for current month const now = moment(); return events.filter(event => { const eventDate = moment(event.start_time); return eventDate.isSame(now, 'month'); }); } // Filter events within the calendar's visible range const rangeStart = moment(currentRangeStart); const rangeEnd = moment(currentRangeEnd); return events.filter(event => { const eventDate = moment(event.start_time); return eventDate.isBetween(rangeStart, rangeEnd, 'day', '[]'); }); }; const getGroupedEvents = () => { const eventsDateMap = new Map(); const filteredEvents = getFilteredEvents(); const sortedFilteredEvents = [...filteredEvents].sort((a, b) => { const aTime = moment(a?.start_time).valueOf(); const bTime = moment(b?.start_time).valueOf(); return aTime - bTime; }); for (const eventItem of sortedFilteredEvents) { const dateString = moment(eventItem.start_time).format('MMM Do, YYYY'); if (eventsDateMap.has(dateString)) { eventsDateMap.set(dateString, [...eventsDateMap.get(dateString), eventItem]); } else { const value = []; value.push(eventItem); eventsDateMap.set(dateString, value); } } // Sort each day by earliest start time first eventsDateMap.forEach((items, key) => { const sortedItems = [...items].sort((a, b) => { const aTime = moment(a?.start_time).valueOf(); const bTime = moment(b?.start_time).valueOf(); return aTime - bTime; }); eventsDateMap.set(key, sortedItems); }); return new Map( [...eventsDateMap.entries()].sort((a, b) => { const aDate = moment(a[1]?.[0]?.start_time).valueOf(); const bDate = moment(b[1]?.[0]?.start_time).valueOf(); return aDate - bDate; }) ); }; useEffect(() => { if (!AuthService.canViewCalendar()) { window.alert('You haven\'t login yet OR this user does not have access to this page. Please change an admin account to login.') AuthService.logout(); navigate(`/login`); } setShowSpinner(true); Promise.all([ VehicleService.getAllActiveVehicles().then((data) => { setVehicles(data.data) }), EmployeeService.getAllEmployees().then((data) => { setEmployees(data.data); }), CustomerService.getAllActiveCustomers().then((data) => { setCustomers(data.data); }), ResourceService.getAll().then((data) => { setResources(data.data); }), EventsService.getTimeData().then(data => { setTimeData(data.data); }) ]).finally(() => { setShowSpinner(false); }); }, []); useEffect(() => { if (!visibleCalendarTabs.includes(currentTab)) { const nextTab = getFirstVisibleCalendarTab(); setCurrentTab(nextTab); currentTabRef.current = nextTab; } }, [currentTab]); useEffect(() => { EventsService.getAllEvents({ from: EventsService.formatDate(fromDate), to: EventsService.formatDate(toDate) }).then(data => setAllEvents(data?.data)); EventsService.getAllEventRecurrences().then(data => setAllEventRecurrences(data?.data || [])); }, [fromDate, toDate]); // Auto-fill repeat start date from event date when repeat option is selected (new modal) useEffect(() => { if (newEventRecurring && newEventStartDateTime) { setNewRepeatStartDate(new Date(newEventStartDateTime)); } if (!newEventRecurring) { setNewRepeatStartDate(null); setNewRepeatEndDate(null); setNewIndefiniteRepeat(false); } }, [newEventRecurring, newEventStartDateTime]); useEffect(() => { setNewEventType(eventTypeMap[currentTab]); if (currentTab === 'medicalCalendar') { if (customers?.length > 0 && resources.length > 0) { const orignialEvents = [...allEvents]; setEvents(orignialEvents?.filter(item => item.type === 'medical')?.map((item) => { const customerField = item?.data?.customer ? (customers?.find(c => c.id === item?.data?.customer)?.name || item?.data?.client_name || '') : (item?.data?.client_name || ''); const doctorField = item?.data?.resource ? ((resources?.find(r => r.id === item?.data?.resource))?.name || item?.data?.resource_name || '') : (item?.data?.resource_name || ''); item.event_id = item.id; item.customer = customerField; item.doctor = doctorField; item.phone = item?.data?.resource ? ((resources?.find(r => r.id === item?.data?.resource))?.phone || item?.data?.resource_phone || '') : (item?.data?.resource_phone || ''); item.contact = item?.data?.resource? ((resources?.find(r => r.id === item?.data?.resource))?.contact || item?.data?.resource_contact || '') : (item?.data?.resource_contact || '') item.address = item?.data?.resource ? ((resources?.find(r => r.id === item?.data?.resource))?.address || item?.data?.resource_address || '') : (item?.data?.resource_address || ''); item.translation = item?.data?.interpreter || ''; item.newPatient = item?.data?.new_patient || ''; item.needId = item?.data?.need_id || ''; item.disability = item?.data?.disability || ''; item.startTime = item?.start_time? `${moment(new Date(item?.start_time)).format('YYYY-MM-DD HH:mm')}` : '' ; item.endTime = item?.start_time? `${moment(new Date(item?.end_time)).format('YYYY-MM-DD HH:mm')}` : '' ; item.fasting = item?.data?.fasting || ''; item.transportation = item?.link_event_name || ''; item.title = currentTab==='medicalCalendar' ? formatEventTitle(customerField, item?.start_time) : item.title; item.description = currentTab==='medicalCalendar' ? getEventDescription(customerField, doctorField) : item.description; // Full info for tooltip item.start = item?.start_time? `${moment(new Date(item?.start_time)).format('YYYY-MM-DD HH:mm')}` : `${moment().format('YYYY-MM-DD HH:mm')}`; item.end = item?.stop_time? `${moment(new Date(item?.stop_time)).format('YYYY-MM-DD HH:mm')}` : (item?.start_time? `${moment(item?.start_time).format('YYYY-MM-DD HH:mm')}` : `${moment().format('YYYY-MM-DD HH:mm')}`); const transportationInfo = EventsService.getTransportationInfo(allEvents, item, timeData); const { isFutureEvent, maxTranslate1, maxTranslate2, maxResource, totalTranslate1, totalTranslate2, totalResource} = transportationInfo; item.color = item?.color; item._options = { additionalClasses: [`event-${item?.color || 'primary'}`]}; item.showWarnings = isFutureEvent; item.maxTranslate1 = maxTranslate1; item.maxTranslate2 = maxTranslate2; item.maxResource = maxResource; item.totalTranslate1 = totalTranslate1; setCurrentTotalTranslate1(item.totalTranslate1); item.totalTranslate2 = totalTranslate2; setCurrentTotalTranslate2(item.totalTranslate2); item.totalResource = totalResource; setCurrentTotalResource(item.totalResource); return item; })?.filter(item => (!showDeletedItems && item.status === 'active') || showDeletedItems) ?.filter(item => { if (selectedColorFilters.length === 0) return true; if (selectedColorFilters.includes(item.color)) return true; // When "Drop-Off Only" (purple) is selected, also show events with no label if (selectedColorFilters.includes('purple') && !item.color) return true; return false; })); } } else { // Expand recurring event rules into individual instances and merge with regular events let recurInstances = allEventRecurrences .filter(rule => rule.type === eventTypeMap[currentTab] && rule.status === 'active') .flatMap(rule => expandRecurrence(rule, fromDate, toDate)); let baseEvents = [...allEvents]; if (currentTab === 'reminderDatesCalendar') { // Birthday reminders are rendered dynamically from customer data. baseEvents = baseEvents.filter((item) => item?.event_reminder_type !== 'birthday'); recurInstances = recurInstances.filter((item) => item?.event_reminder_type !== 'birthday'); } const originalEvents = [...baseEvents, ...recurInstances]; const dynamicBirthdayEvents = currentTab === 'reminderDatesCalendar' ? buildDynamicBirthdayEvents(customers, fromDate, toDate) : []; const mergedEvents = [...originalEvents, ...dynamicBirthdayEvents]; let filteredEvents = mergedEvents?.filter(item => item.type === eventTypeMap[currentTab])?.map(item => { // For Important Dates, remap old blue/orange colors to new member_related/vehicle_maintenance let eventColor = item?.color; if (currentTab === 'reminderDatesCalendar') { const isMember = ['birthday', 'adcaps_completion', 'center_qualification_expiration'].includes(item?.event_reminder_type) || item?.target_type === 'customer'; eventColor = isMember ? 'member_related' : 'vehicle_maintenance'; } const additionalClasses = [`event-${eventColor || 'primary'}`]; // Disable clicking for birthday events on Important Dates tab if (currentTab === 'reminderDatesCalendar' && item?.event_reminder_type === 'birthday') { additionalClasses.push('event-no-click'); } // For Important Dates, use date-only format so tiles don't show hour:minute const dateFormat = (currentTab === 'reminderDatesCalendar' || currentTab === 'incidentsCalendar') ? 'YYYY-MM-DD' : 'YYYY-MM-DD HH:mm'; return { ...item, color: eventColor, title: item?.title, start: item?.start_time? `${moment(new Date(item?.start_time)).format(dateFormat)}` : `${moment().format(dateFormat)}`, end: item?.stop_time? `${moment(new Date(item?.stop_time)).format(dateFormat)}` : (item?.start_time? `${moment(item?.start_time).format(dateFormat)}` : `${moment().format(dateFormat)}`), _options: { additionalClasses } }; })?.filter(item => (!showDeletedItems && item.status === 'active') || showDeletedItems) ?.filter(item => { if (selectedColorFilters.length === 0) return true; // For Important Dates, filter by event_reminder_type if (currentTab === 'reminderDatesCalendar') { return selectedColorFilters.includes(item.event_reminder_type); } if (selectedColorFilters.includes(item.color)) return true; // When "Drop-Off Only" (purple) is selected, also show events with no label if (selectedColorFilters.includes('purple') && !item.color) return true; return false; }); // For Important Dates tab, filter to only show events for active customers if (currentTab === 'reminderDatesCalendar' && customers?.length > 0) { filteredEvents = filteredEvents?.filter(item => { // If it's a customer-related event, check if the customer is active if (item.target_type === 'customer' && item.target_uuid) { const customer = customers.find(c => c.id === item.target_uuid); // Only show if customer exists and is active (not discharged/transferred/deceased) return customer && customer.status === 'active' && customer.type !== 'discharged' && customer.type !== 'transferred' && customer.type !== 'deceased'; } // Vehicle-related events are always shown return true; }); } setEvents(filteredEvents); } }, [customers, resources, timeData, currentTab, allEvents, allEventRecurrences, showDeletedItems, selectedColorFilters]) useEffect(() => { if (events && calendar) { console.log("CenterCalendar useEffect - events:", events.length, "range:", currentRangeStart, "to", currentRangeEnd); calendar?.eventsService?.set(events); setGroupedEvents(getGroupedEvents()); } }, [events, currentRangeStart, currentRangeEnd]); // Fallback close handler: ensure ScheduleX modal can always be dismissed via X button. useEffect(() => { const onDocumentClickCapture = (event) => { const target = event.target; if (!(target instanceof HTMLElement)) return; if (!target.closest('.sx__event-modal')) return; const closeControl = target.closest('button,[role="button"]'); if (!closeControl) return; const closeAriaLabel = `${closeControl.getAttribute('aria-label') || ''}`.toLowerCase(); const closeText = `${closeControl.textContent || ''}`.trim().toLowerCase(); const isCloseControl = closeAriaLabel.includes('close') || closeText === '\u00d7' || closeText === 'x' || closeText === '\u2715'; if (!isCloseControl) return; event.preventDefault(); event.stopPropagation(); eventModalService?.close?.(); try { calendar?.config?.plugins?.eventModal?.close(); } catch (_) { // no-op fallback } setTimeout(() => { document.querySelectorAll('.sx__event-modal').forEach(el => el.remove()); }, 0); }; document.addEventListener('click', onDocumentClickCapture, true); return () => { document.removeEventListener('click', onDocumentClickCapture, true); }; }, [calendar, eventModalService]); // Sync list column height with calendar column height useEffect(() => { const updateListHeight = () => { if (calendarColumnRef.current) { const calendarHeight = calendarColumnRef.current.offsetHeight; setListHeight(calendarHeight); } }; // Initial measurement after render const timer = setTimeout(updateListHeight, 100); // Update on window resize window.addEventListener('resize', updateListHeight); return () => { clearTimeout(timer); window.removeEventListener('resize', updateListHeight); }; }, [events, currentTab]); const redirectToAdmin = () => { navigate(`/medical`) } const goToEdit = (id) => { if (!AuthService.canEditMedicalEvents()) return; navigate(`/medical/events/edit/${id}?from=calendar`) } const goToCreateNew = () => { if (!AuthService.canEditMedicalEvents()) return; navigate(`/medical/events`) } const goToList = () => { navigate(`/medical/events/list`) } const goToMultipleList = () => { navigate(`/medical/events/multiple-list`) } const goToView = (id) => { navigate(`/medical/events/${id}`) } const disableEvent = (id) => { if (currentTab === 'medicalCalendar') { if (!AuthService.canEditMedicalEvents()) return; } else { if (!AuthService.canEditCalendarTab(currentTab)) return; } // Handle recurring event instances const isRecurInstance = typeof id === 'string' && id.startsWith('recur-'); if (isRecurInstance) { const currentEvent = events.find(item => item.id === id); const recurId = currentEvent?._recur_id || id.split('-')[1]; EventsService.disableEventRecurrence(recurId, { status: 'inactive' }).then(() => { Promise.all([ EventsService.getAllEvents({ from: EventsService.formatDate(fromDate), to: EventsService.formatDate(toDate) }), EventsService.getAllEventRecurrences() ]).then(([eventsRes, recurRes]) => { setAllEvents(eventsRes.data); setAllEventRecurrences(recurRes.data || []); }); }); return; } const currentEvent = events.find(item => item.id === id); EventsService.disableEvent(id, { status: 'inactive', edit_by: localStorage.getItem('user') && JSON.parse(localStorage.getItem('user'))?.name, edit_date: new Date(), edit_history: currentEvent?.edit_history? [...currentEvent.edit_history, { employee: localStorage.getItem('user') && JSON.parse(localStorage.getItem('user'))?.name, date: new Date() }] : [{ employee: localStorage.getItem('user') && JSON.parse(localStorage.getItem('user'))?.name, date: new Date() }]}).then(() => { EventsService.getAllEvents({ from: EventsService.formatDate(fromDate), to: EventsService.formatDate(toDate) }).then((data) => { setAllEvents(data?.data); // if (currentTab === 'medicalCalendar') { // setEvents(data.data.filter((item) => { // const customerField = item?.data?.customer ? (customers?.find(c => c.id === item?.data?.customer)?.name || item?.data?.client_name || '') : (item?.data?.client_name || ''); // const doctorField = item?.data?.resource ? ((resources?.find(r => r.id === item?.data?.resource))?.name || item?.data?.resource_name || '') : (item?.data?.resource_name || ''); // item.event_id = item.id; // item.customer = customerField; // item.doctor = doctorField; // item.phone = item?.data?.resource ? ((resources?.find(r => r.id === item?.data?.resource))?.phone || item?.data?.resource_phone || '') : (item?.data?.resource_phone || ''); // item.contact = item?.data?.resource? ((resources?.find(r => r.id === item?.data?.resource))?.contact || item?.data?.resource_contact || '') : (item?.data?.resource_contact || '') // item.address = item?.data?.resource ? ((resources?.find(r => r.id === item?.data?.resource))?.address || item?.data?.resource_address || '') : (item?.data?.resource_address || ''); // item.translation = item?.data?.interpreter || ''; // item.newPatient = item?.data?.new_patient || ''; // item.needId = item?.data?.need_id || ''; // item.disability = item?.data?.disability || ''; // item.startTime = item?.start_time? `${moment(new Date(item?.start_time)).format('YYYY-MM-DD HH:mm')}` : `${moment().format('YYYY-MM-DD HH:mm')}` ; // item.endTime = item.stop_time? `${moment(new Date(item?.stop_time)).format('YYYY-MM-DD HH:mm')}` : (item?.start_time? `${moment(item?.start_time).format('YYYY-MM-DD HH:mm')}` : `${moment().format('YYYY-MM-DD HH:mm')}`); // item.fasting = item?.data?.fasting || ''; // item.transportation = item?.link_event_name || ''; // item.title = `${customerField}, provider: ${doctorField}`; // item.start = item?.start_time? `${moment(new Date(item?.start_time)).format('YYYY-MM-DD HH:mm')}` : `${moment().format('YYYY-MM-DD HH:mm')}`; // item.end = item.stop_time? `${moment(new Date(item?.stop_time)).format('YYYY-MM-DD HH:mm')}` : (item?.start_time? `${moment(item?.start_time).format('YYYY-MM-DD HH:mm')}` : `${moment().format('YYYY-MM-DD HH:mm')}`); // item._options = { additionalClasses: [`event-${item?.color || 'primary'}`]}; // const transportationInfo = EventsService.getTransportationInfo(data.data, item, timeData); // const { isFutureEvent, maxTranslate1, maxTranslate2, maxResource, totalTranslate1, totalTranslate2, totalResource} = transportationInfo; // item.color = item?.color; // item.showWarnings = isFutureEvent; // item.maxTranslate1 = maxTranslate1; // item.maxTranslate2 = maxTranslate2; // item.maxResource = maxResource; // item.totalTranslate1 = totalTranslate1; // setCurrentTotalTranslate1(item.totalTranslate1); // item.totalTranslate2 = totalTranslate2; // setCurrentTotalTranslate2(item.totalTranslate2); // item.totalResource = totalResource; // setCurrentTotalResource(item.totalResource); // return item; // }).filter(item => item.type === 'medical')); // } }) }); } // Edit modal functions (for non-medical tabs) const openEditModal = (calendarEvent) => { if (!AuthService.canEditCalendarTab(currentTab)) return; // Check if this is a recurring event instance (ID starts with 'recur-') const isRecurInstance = typeof calendarEvent.id === 'string' && calendarEvent.id.startsWith('recur-'); let eventData; if (isRecurInstance) { // Find the recurrence rule using the _recur_id stored on expanded instances const recurId = calendarEvent._recur_id || calendarEvent.id.split('-')[1]; const recurRule = allEventRecurrences.find(r => r.id === recurId); if (recurRule) { eventData = { ...recurRule }; setEditingRecurId(recurId); setEditingEventId(null); setEditRepeatStartDate(recurRule.start_repeat_date ? new Date(recurRule.start_repeat_date) : null); setEditRepeatEndDate(recurRule.end_repeat_date ? new Date(recurRule.end_repeat_date) : null); setEditIndefiniteRepeat(recurRule.indefinite_repeat || false); } else { return; // Can't find rule, bail } } else { eventData = allEvents.find(e => e.id === calendarEvent.id) || calendarEvent; setEditingEventId(eventData.id); setEditingRecurId(null); setEditRepeatStartDate(null); setEditRepeatEndDate(null); setEditIndefiniteRepeat(false); } setEditEventStartDateTime(eventData.start_time ? new Date(eventData.start_time) : (eventData.start_repeat_date ? new Date(eventData.start_repeat_date) : new Date())); if (currentTab === 'activitiesCalendar') { setEditEventTitle(eventData.title || ''); setEditActivityCategory(eventData.activity_category || eventData.color || ''); setEditEventLocation(eventData.event_location || ''); setEditEventRecurring(eventData.rrule || ''); } else if (currentTab === 'incidentsCalendar') { setEditAttendanceCustomer(eventData.target_uuid ? { value: eventData.target_uuid, label: eventData.target_name } : null); setEditAttendanceReason(eventData.description || ''); setEditEventRecurring(eventData.rrule || ''); } else if (currentTab === 'mealPlanCalendar') { setEditEventTitle(eventData.title || ''); setEditMealType(eventData.meal_type || ''); setEditMealIngredients(eventData.ingredients || ''); setEditEventRecurring(eventData.rrule || ''); } else if (currentTab === 'reminderDatesCalendar') { setEditReminderTitleCategory(eventData.event_reminder_type || ''); setEditReminderAssociatedEntity(eventData.target_uuid ? { value: eventData.target_uuid, label: eventData.target_name } : null); setEditEventRecurring(eventData.rrule || ''); } // If editing a regular event that has an rrule, populate repeat date fields if (!isRecurInstance && eventData.rrule) { setEditRepeatStartDate(eventData.start_time ? new Date(eventData.start_time) : null); setEditRepeatEndDate(null); setEditIndefiniteRepeat(false); } setShowEditModal(true); }; const closeEditModal = () => { setShowEditModal(false); setEditingEventId(null); setEditingRecurId(null); setEditEventTitle(''); setEditEventStartDateTime(null); setEditEventLocation(''); setEditEventRecurring(''); setEditActivityCategory(''); setEditAttendanceCustomer(null); setEditAttendanceReason(''); setEditMealType(''); setEditMealIngredients(''); setEditReminderTitleCategory(''); setEditReminderAssociatedEntity(null); setEditRepeatStartDate(null); setEditRepeatEndDate(null); setEditIndefiniteRepeat(false); }; const handleEditSave = () => { if (!AuthService.canEditCalendarTab(currentTab)) return; const userName = localStorage.getItem('user') && JSON.parse(localStorage.getItem('user'))?.name; let data = { edit_by: userName, edit_date: new Date(), }; if (currentTab === 'activitiesCalendar') { data = { ...data, title: editEventTitle, start_time: editEventStartDateTime, stop_time: editEventStartDateTime, activity_category: editActivityCategory, color: editActivityCategory, event_location: editEventLocation, }; } else if (currentTab === 'incidentsCalendar') { data = { ...data, title: editAttendanceCustomer?.label ? `Attendance Note - ${editAttendanceCustomer.label}` : 'Attendance Note', start_time: editEventStartDateTime, stop_time: editEventStartDateTime, target_type: 'customer', target_uuid: editAttendanceCustomer?.value, target_name: editAttendanceCustomer?.label, description: editAttendanceReason, }; } else if (currentTab === 'mealPlanCalendar') { data = { ...data, title: editEventTitle, start_time: editEventStartDateTime, stop_time: editEventStartDateTime, meal_type: editMealType, color: editMealType === 'breakfast' ? 'brown' : (editMealType === 'lunch' ? 'green' : 'red'), ingredients: editMealIngredients, }; } else if (currentTab === 'reminderDatesCalendar') { const isMemberCategory = ['birthday', 'adcaps_completion', 'center_qualification_expiration'].includes(editReminderTitleCategory); data = { ...data, title: getReminderTitleLabel(editReminderTitleCategory), start_time: editEventStartDateTime, stop_time: editEventStartDateTime, event_reminder_type: editReminderTitleCategory, target_type: isMemberCategory ? 'customer' : 'vehicle', target_uuid: editReminderAssociatedEntity?.value, target_name: editReminderAssociatedEntity?.label, color: isMemberCategory ? 'member_related' : 'vehicle_maintenance', }; } const refreshAll = () => { Promise.all([ EventsService.getAllEvents({ from: EventsService.formatDate(fromDate), to: EventsService.formatDate(toDate) }), EventsService.getAllEventRecurrences() ]).then(([eventsRes, recurRes]) => { setAllEvents(eventsRes.data); setAllEventRecurrences(recurRes.data || []); closeEditModal(); }); }; // If editing a recurrence rule if (editingRecurId) { const recurData = { ...data, rrule: editEventRecurring, start_repeat_date: editRepeatStartDate, end_repeat_date: editIndefiniteRepeat ? new Date('2099-12-31') : editRepeatEndDate, indefinite_repeat: editIndefiniteRepeat, }; delete recurData.start_time; delete recurData.stop_time; if (!editEventRecurring) { // Changed from recurring to one-time: delete recurrence rule, create regular event EventsService.deleteEventRecurrence(editingRecurId).then(() => { const eventData = { ...data }; delete eventData.rrule; eventData.start_time = editEventStartDateTime; eventData.stop_time = editEventStartDateTime; eventData.type = eventTypeMap[currentTab]; eventData.status = 'active'; EventsService.createNewEvent(eventData).then(() => refreshAll()); }); } else { EventsService.updateEventRecurrence(editingRecurId, recurData).then(() => refreshAll()); } } else if (editingEventId) { // Editing a regular event if (editEventRecurring) { // Changed from one-time to recurring: create recurrence rule, delete regular event const recurData = { ...data, type: eventTypeMap[currentTab], status: 'active', rrule: editEventRecurring, start_repeat_date: editRepeatStartDate || editEventStartDateTime, end_repeat_date: editIndefiniteRepeat ? new Date('2099-12-31') : editRepeatEndDate, indefinite_repeat: editIndefiniteRepeat, create_by: data.edit_by, create_date: new Date(), }; delete recurData.start_time; delete recurData.stop_time; EventsService.createEventRecurrence(recurData).then(() => { EventsService.disableEvent(editingEventId, {}).then(() => refreshAll()); }); } else { // Still one-time, just update normally delete data.rrule; EventsService.updateEvent(editingEventId, data).then(() => refreshAll()); } } }; const toggleColorFilter = (colorValue) => { setSelectedColorFilters(prev => prev.includes(colorValue) ? prev.filter(c => c !== colorValue) : [...prev, colorValue] ); }; const FilterAndClose = () => { setShowFilterDropdown(false); } const cleanFilterAndClose = () => { setShowFilterDropdown(false); setShowDeletedItems(false); setSelectedColorFilters([]); } const goToTab = (value) => { if (!value || !visibleCalendarTabs.includes(value)) return; setTargetedEventType(eventTypeMap[value]); setCurrentTab(value); currentTabRef.current = value; // Always hide deleted events by default when switching tabs. setShowDeletedItems(false); setSelectedColorFilters([]); // Dismiss any open calendar event tile popup via plugin API try { calendar?.config?.plugins?.eventModal?.close(); } catch (e) { // Fallback: remove the modal element from DOM document.querySelectorAll('.sx__event-modal').forEach(el => el.remove()); } } const dismissEventModal = (e) => { e?.preventDefault?.(); e?.stopPropagation?.(); try { eventModalService?.close?.(); } catch (_e) {} try { calendar?.config?.plugins?.eventModal?.close(); } catch (e) { document.querySelectorAll('.sx__event-modal').forEach(el => el.remove()); } }; const customComponents = { eventModal: ({calendarEvent}) => { return <>