All checks were successful
Build And Deploy Main / build-and-deploy (push) Successful in 37s
833 lines
31 KiB
JavaScript
833 lines
31 KiB
JavaScript
import React, {useState, useEffect, useMemo, useRef} from "react";
|
|
import { useNavigate } from "react-router-dom";
|
|
import { AuthService, EventsService, CustomerService, ResourceService } from "../../services";
|
|
import moment from 'moment';
|
|
import { Breadcrumb, Tabs, Tab, Button, Modal, Dropdown, ProgressBar } 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 '@schedule-x/theme-default/dist/calendar.css';
|
|
import { Archive, Filter, PencilSquare, X } from "react-bootstrap-icons";
|
|
import DatePicker from "react-datepicker";
|
|
|
|
|
|
const EventsCalendar = () => {
|
|
const navigate = useNavigate();
|
|
const [events, setEvents] = useState([]);
|
|
const calendarColumnRef = useRef(null);
|
|
const [listHeight, setListHeight] = useState(null);
|
|
const [allEvents, setAllEvents] = useState([]);
|
|
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 [selectedColorFilters, setSelectedColorFilters] = useState([]);
|
|
const [timeData, setTimeData] = useState([]);
|
|
const [showFilterDropdown, setShowFilterDropdown] = useState(false);
|
|
const eventsServicePluginRef = useRef(null);
|
|
if (!eventsServicePluginRef.current) {
|
|
eventsServicePluginRef.current = createEventsServicePlugin();
|
|
}
|
|
const eventsServicePlugin = eventsServicePluginRef.current;
|
|
const eventModalServiceRef = useRef(null);
|
|
if (!eventModalServiceRef.current) {
|
|
eventModalServiceRef.current = createEventModalPlugin();
|
|
}
|
|
const eventModalService = eventModalServiceRef.current;
|
|
const [groupedEvents, setGroupedEvents] = useState(new Map());
|
|
const [currentRangeStart, setCurrentRangeStart] = useState(null);
|
|
const [currentRangeEnd, setCurrentRangeEnd] = useState(null);
|
|
const [showCreationModal, setShowCreationModal] = useState(false);
|
|
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
|
const [deleteTargetId, setDeleteTargetId] = useState(null);
|
|
const [showLoadingModal, setShowLoadingModal] = useState(true);
|
|
const [loadingProgress, setLoadingProgress] = useState(12);
|
|
const [initialLookupsLoaded, setInitialLookupsLoaded] = useState(false);
|
|
const [initialEventsLoaded, setInitialEventsLoaded] = useState(false);
|
|
const hasMarkedInitialEventsLoadedRef = useRef(false);
|
|
const eventsFetchRequestIdRef = useRef(0);
|
|
const [newEventStartDateTime, setNewEventStartDateTime] = useState(new Date());
|
|
const [newEventEndDateTime, setNewEventEndDateTime] = useState(new Date());
|
|
const [newEventColor, setNewEventColor] = 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('');
|
|
|
|
// 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 format event title - now uses full name, CSS handles overflow
|
|
const formatEventTitle = (customerName, startTime) => {
|
|
const fullName = formatFullName(customerName);
|
|
return fullName;
|
|
};
|
|
|
|
// Get full name for description/tooltip
|
|
const getEventDescription = (customerName, doctorName) => {
|
|
const fullName = formatFullName(customerName);
|
|
return doctorName ? `${fullName} - ${doctorName}` : fullName;
|
|
};
|
|
|
|
const customerById = useMemo(() => {
|
|
const map = new Map();
|
|
(customers || []).forEach((customer) => {
|
|
if (customer?.id) map.set(customer.id, customer);
|
|
});
|
|
return map;
|
|
}, [customers]);
|
|
|
|
const resourceById = useMemo(() => {
|
|
const map = new Map();
|
|
(resources || []).forEach((resource) => {
|
|
if (resource?.id) map.set(resource.id, resource);
|
|
});
|
|
return map;
|
|
}, [resources]);
|
|
|
|
const calendar = useCalendarApp({
|
|
views: [createViewMonthGrid(), createViewDay(), createViewWeek()],
|
|
monthGridOptions: {
|
|
nEventsPerDay: 50,
|
|
},
|
|
defaultView: viewMonthGrid.name,
|
|
skipValidation: true,
|
|
selectedDate: moment(new Date()).format('YYYY-MM-DD HH:mm'),
|
|
events: events,
|
|
plugins: [eventModalService, eventsServicePlugin],
|
|
callbacks: {
|
|
onRangeUpdate(range) {
|
|
setCurrentRangeStart(range.start);
|
|
setCurrentRangeEnd(range.end);
|
|
const startDate = new Date(range.start);
|
|
const endDate = new Date(range.end);
|
|
const nextFromDate = new Date(startDate.getFullYear(), startDate.getMonth(), 1);
|
|
const nextToDate = new Date(endDate.getFullYear(), endDate.getMonth() + 1, 0);
|
|
setFromDate((prev) => (prev?.getTime?.() === nextFromDate.getTime() ? prev : nextFromDate));
|
|
setToDate((prev) => (prev?.getTime?.() === nextToDate.getTime() ? prev : nextToDate));
|
|
},
|
|
onClickDate(date) {
|
|
// Disabled: prevent creating new event by tapping calendar date
|
|
return;
|
|
},
|
|
onClickDateTime(dateTime) {
|
|
// Disabled: prevent creating new event by tapping calendar time slot
|
|
return;
|
|
}
|
|
}
|
|
});
|
|
|
|
// Filter events based on current calendar range
|
|
const getFilteredEvents = () => {
|
|
if (!currentRangeStart || !currentRangeEnd) {
|
|
const now = moment();
|
|
return events.filter(event => {
|
|
const eventDate = moment(event.start_time);
|
|
return eventDate.isSame(now, 'month');
|
|
});
|
|
}
|
|
|
|
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();
|
|
for (const eventItem of filteredEvents) {
|
|
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 eventsDateMap;
|
|
};
|
|
|
|
|
|
useEffect(() => {
|
|
if (!AuthService.canViewMedicalEvents()) {
|
|
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`);
|
|
}
|
|
setShowLoadingModal(true);
|
|
setLoadingProgress(12);
|
|
setInitialLookupsLoaded(false);
|
|
setInitialEventsLoaded(false);
|
|
Promise.all([
|
|
CustomerService.getAllActiveCustomers(),
|
|
ResourceService.getAll(),
|
|
EventsService.getTimeData()
|
|
]).then(([customersData, resourcesData, timeDataResult]) => {
|
|
setCustomers(customersData?.data || []);
|
|
setResources(resourcesData?.data || []);
|
|
setTimeData(timeDataResult?.data || []);
|
|
}).finally(() => {
|
|
setInitialLookupsLoaded(true);
|
|
});
|
|
// Hide deleted appointments by default on page load.
|
|
setSelectedColorFilters([]);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
const requestId = ++eventsFetchRequestIdRef.current;
|
|
const timer = setTimeout(() => {
|
|
EventsService.getAllEvents({ from: EventsService.formatDate(fromDate), to: EventsService.formatDate(toDate) }).then(data => {
|
|
if (requestId !== eventsFetchRequestIdRef.current) return;
|
|
setAllEvents(data?.data || []);
|
|
if (!hasMarkedInitialEventsLoadedRef.current) {
|
|
hasMarkedInitialEventsLoadedRef.current = true;
|
|
setInitialEventsLoaded(true);
|
|
}
|
|
}).catch(() => {
|
|
if (requestId !== eventsFetchRequestIdRef.current) return;
|
|
setAllEvents([]);
|
|
if (!hasMarkedInitialEventsLoadedRef.current) {
|
|
hasMarkedInitialEventsLoadedRef.current = true;
|
|
setInitialEventsLoaded(true);
|
|
}
|
|
});
|
|
}, 180);
|
|
return () => clearTimeout(timer);
|
|
}, [fromDate, toDate]);
|
|
|
|
useEffect(() => {
|
|
if (!showLoadingModal) return;
|
|
const timer = setInterval(() => {
|
|
setLoadingProgress((prev) => {
|
|
if (prev >= 90) return prev;
|
|
return prev + Math.max(1, Math.ceil((90 - prev) / 8));
|
|
});
|
|
}, 220);
|
|
return () => clearInterval(timer);
|
|
}, [showLoadingModal]);
|
|
|
|
useEffect(() => {
|
|
if (!initialLookupsLoaded || !initialEventsLoaded) return;
|
|
setLoadingProgress(100);
|
|
const closeTimer = setTimeout(() => {
|
|
setShowLoadingModal(false);
|
|
}, 250);
|
|
return () => clearTimeout(closeTimer);
|
|
}, [initialLookupsLoaded, initialEventsLoaded]);
|
|
|
|
useEffect(() => {
|
|
if (customers?.length > 0 && resources.length > 0) {
|
|
const orignialEvents = [...allEvents];
|
|
setEvents(orignialEvents?.filter(item => item.type === 'medical')?.map((item) => {
|
|
const customer = item?.data?.customer ? customerById.get(item?.data?.customer) : null;
|
|
const resource = item?.data?.resource ? resourceById.get(item?.data?.resource) : null;
|
|
const customerField = customer?.name || item?.data?.client_name || '';
|
|
const doctorField = resource?.name || item?.data?.resource_name || '';
|
|
item.event_id = item.id;
|
|
item.customer = customerField;
|
|
item.doctor = doctorField;
|
|
item.phone = resource?.phone || item?.data?.resource_phone || '';
|
|
item.contact = resource?.contact || item?.data?.resource_contact || '';
|
|
item.address = 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 = formatEventTitle(customerField, item?.start_time);
|
|
item.description = getEventDescription(customerField, 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')}`);
|
|
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;
|
|
item.totalTranslate2 = totalTranslate2;
|
|
item.totalResource = totalResource;
|
|
return item;
|
|
})?.filter(item => {
|
|
const includeDeleted = selectedColorFilters.includes('gray');
|
|
const isDeletedAppt = item?.status !== 'active' || item?.color === 'gray';
|
|
if (!isDeletedAppt) return true;
|
|
return includeDeleted;
|
|
})
|
|
?.filter(item => {
|
|
if (selectedColorFilters.length === 0) return true;
|
|
if (item.status !== 'active') {
|
|
return selectedColorFilters.includes('gray');
|
|
}
|
|
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;
|
|
}));
|
|
}
|
|
}, [customers, resources, timeData, allEvents, selectedColorFilters, customerById, resourceById])
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
if (events && calendar) {
|
|
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);
|
|
}
|
|
};
|
|
|
|
const timer = setTimeout(updateListHeight, 100);
|
|
window.addEventListener('resize', updateListHeight);
|
|
|
|
return () => {
|
|
clearTimeout(timer);
|
|
window.removeEventListener('resize', updateListHeight);
|
|
};
|
|
}, [events]);
|
|
|
|
|
|
const redirectToAdmin = () => {
|
|
navigate(`/medical`)
|
|
}
|
|
|
|
const goToCreateNew = () => {
|
|
navigate(`/medical/events`)
|
|
}
|
|
|
|
const goToList = () => {
|
|
navigate(`/medical/events/list`)
|
|
}
|
|
|
|
const goToMultipleList = () => {
|
|
navigate(`/medical/events/multiple-list`)
|
|
}
|
|
|
|
const goToView = (id) => {
|
|
navigate(`/medical/events/${id}`)
|
|
}
|
|
|
|
const goToEdit = (id) => {
|
|
if (!AuthService.canEditMedicalEvents()) return;
|
|
navigate(`/medical/events/edit/${id}?from=calendar`)
|
|
}
|
|
|
|
const disableEvent = (id) => {
|
|
if (!AuthService.canEditMedicalEvents()) 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);
|
|
})
|
|
});
|
|
}
|
|
|
|
const toggleColorFilter = (colorValue) => {
|
|
setSelectedColorFilters(prev =>
|
|
prev.includes(colorValue)
|
|
? prev.filter(c => c !== colorValue)
|
|
: [...prev, colorValue]
|
|
);
|
|
};
|
|
|
|
const FilterAndClose = () => {
|
|
setShowFilterDropdown(false);
|
|
}
|
|
|
|
const cleanFilterAndClose = () => {
|
|
setShowFilterDropdown(false);
|
|
setSelectedColorFilters([]);
|
|
}
|
|
|
|
const dismissEventModal = (e) => {
|
|
e?.preventDefault?.();
|
|
e?.stopPropagation?.();
|
|
e?.nativeEvent?.stopImmediatePropagation?.();
|
|
try {
|
|
eventModalService?.close?.();
|
|
} catch (_e) {}
|
|
try {
|
|
calendar?.config?.plugins?.eventModal?.close();
|
|
} catch (_e) {}
|
|
setTimeout(() => {
|
|
document.querySelectorAll('.sx__event-modal').forEach((el) => {
|
|
const dialogContainer = el.closest('[role="dialog"]');
|
|
if (dialogContainer) {
|
|
dialogContainer.remove();
|
|
} else {
|
|
el.remove();
|
|
}
|
|
});
|
|
}, 0);
|
|
};
|
|
|
|
const customComponents = {
|
|
eventModal: ({calendarEvent}) => {
|
|
return <>
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: '8px' }}>
|
|
<div className="sx__event-modal__title">{calendarEvent?.customer}</div>
|
|
<button
|
|
type="button"
|
|
onClick={dismissEventModal}
|
|
aria-label="Close"
|
|
title="Close"
|
|
data-custom-close="true"
|
|
style={{
|
|
border: 'none',
|
|
background: 'transparent',
|
|
padding: '8px',
|
|
marginTop: '4px',
|
|
marginRight: '4px',
|
|
cursor: 'pointer',
|
|
lineHeight: 1,
|
|
color: '#666',
|
|
minWidth: '36px',
|
|
minHeight: '36px',
|
|
display: 'inline-flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center'
|
|
}}
|
|
>
|
|
<X size={24} />
|
|
</button>
|
|
</div>
|
|
{calendarEvent?.doctor && <div className="sx__event-modal__time">{`${calendarEvent?.doctor}`}</div>}
|
|
<div className="sx__event-modal__time">{`${calendarEvent?.start}`}</div>
|
|
{AuthService.canEditMedicalEvents() && <div className="sx__event-modal__time" style={{ display: 'flex', gap: '12px', marginTop: '8px' }}>
|
|
<PencilSquare
|
|
size={16}
|
|
onClick={() => goToEdit(calendarEvent?.id)}
|
|
style={{ cursor: 'pointer' }}
|
|
title="Edit"
|
|
/>
|
|
<Archive
|
|
size={16}
|
|
onClick={() => { setDeleteTargetId(calendarEvent?.id); setShowDeleteConfirm(true); }}
|
|
style={{ cursor: 'pointer' }}
|
|
title="Delete"
|
|
/>
|
|
</div>}
|
|
</>
|
|
}
|
|
};
|
|
|
|
const customMenu = React.forwardRef(
|
|
({ children, style, className, 'aria-labelledby': labeledBy }, ref) => {
|
|
return (
|
|
<div
|
|
ref={ref}
|
|
style={style}
|
|
className={className}
|
|
aria-labelledby={labeledBy}
|
|
>
|
|
<h6>Filter By</h6>
|
|
<hr style={{ margin: '8px 0' }} />
|
|
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
|
<div className="field-label" style={{ marginBottom: '8px' }}>Filter by Type</div>
|
|
{EventsService.labelOptions.map((item) => (
|
|
<div key={item.value} className="d-flex align-items-center mb-1" style={{ cursor: 'pointer' }} onClick={() => toggleColorFilter(item.value)}>
|
|
<input
|
|
type="checkbox"
|
|
checked={selectedColorFilters.includes(item.value)}
|
|
onChange={() => toggleColorFilter(item.value)}
|
|
style={{ marginRight: '8px', marginLeft: '0' }}
|
|
onClick={(e) => e.stopPropagation()}
|
|
/>
|
|
<span
|
|
className={`event-${item.value}`}
|
|
style={{
|
|
width: '14px',
|
|
height: '14px',
|
|
borderRadius: '3px',
|
|
display: 'inline-block',
|
|
marginRight: '6px',
|
|
flexShrink: 0
|
|
}}
|
|
></span>
|
|
<span style={{ fontSize: '12px' }}>{item.label}</span>
|
|
</div>
|
|
))}
|
|
{selectedColorFilters.length > 0 && (
|
|
<div style={{ marginTop: '4px' }}>
|
|
<span
|
|
style={{ fontSize: '11px', color: '#0066B1', cursor: 'pointer', textDecoration: 'underline' }}
|
|
onClick={() => setSelectedColorFilters([])}
|
|
>
|
|
Clear all
|
|
</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div className="list row" style={{ marginTop: '8px' }}>
|
|
<div className="col-md-12">
|
|
<button className="btn btn-default btn-sm float-right" onClick={() => cleanFilterAndClose()}> Cancel </button>
|
|
<button className="btn btn-primary btn-sm float-right" onClick={() => FilterAndClose()}> Filter </button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
},
|
|
);
|
|
|
|
const calendarView = <div className="multi-columns-container" style={{ display: 'flex', alignItems: 'flex-start', width: '100%' }}>
|
|
<div className="column-container" ref={calendarColumnRef} style={{ minWidth: '1000px', flexShrink: 0, display: 'flex', flexDirection: 'column' }}>
|
|
{calendar && <ScheduleXCalendar customComponents={customComponents} calendarApp={calendar} />}
|
|
{/* Legend */}
|
|
<div className="calendar-legend mt-3">
|
|
<h6 className="text-muted mb-2" style={{ fontSize: '12px' }}>Legend:</h6>
|
|
<div className="d-flex flex-wrap gap-3">
|
|
{EventsService.labelOptions?.map((item) => (
|
|
<div key={item.value} className="d-flex align-items-center">
|
|
<span
|
|
className={`event-${item.value}`}
|
|
style={{
|
|
width: '16px',
|
|
height: '16px',
|
|
borderRadius: '4px',
|
|
display: 'inline-block',
|
|
marginRight: '6px'
|
|
}}
|
|
></span>
|
|
<span style={{ fontSize: '12px' }}>{item.label}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="column-container calendar-list-column" style={{ display: 'flex', flexDirection: 'column', flex: 1, minWidth: 0 }}>
|
|
<div className="column-card" style={{ height: listHeight ? `${listHeight}px` : 'auto', overflowY: 'auto', overflowX: 'hidden', padding: '16px', display: 'flex', flexDirection: 'column', width: '100%', boxSizing: 'border-box' }}>
|
|
<h6 className="text-primary mb-3">List</h6>
|
|
<div style={{ flex: 1, overflowY: 'auto', width: '100%' }}>
|
|
{(!groupedEvents || groupedEvents.size === 0) && (
|
|
<div style={{ padding: '24px', textAlign: 'center', color: '#999' }}>
|
|
No events for this period
|
|
</div>
|
|
)}
|
|
{
|
|
Array.from(groupedEvents?.keys())?.map((key) => {
|
|
return <div key={key}>
|
|
<h6 className="text-primary me-2">{key}</h6>
|
|
{
|
|
groupedEvents.get(key).map(eventItem => <div
|
|
key={eventItem.id}
|
|
className={`event-${eventItem.color || 'primary'} mb-3 event-list-item-container`}
|
|
onClick={() => goToView(eventItem.id)}
|
|
style={{ cursor: 'pointer', padding: '8px 12px', borderRadius: '4px' }}
|
|
>
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
<span className="sx__month-agenda-event__title">{formatFullName(eventItem.customer)}</span>
|
|
<span style={{ fontSize: '12px' }}>{moment(eventItem?.start_time).format('HH:mm')} - {moment(eventItem?.stop_time || eventItem?.start_time).format('HH:mm')}</span>
|
|
</div>
|
|
<div className="sx__event-modal__time" style={{ fontSize: '12px', marginTop: '4px' }}>Provider: {eventItem?.doctor || '-'}</div>
|
|
{AuthService.canEditMedicalEvents() && <div style={{ display: 'flex', gap: '10px', marginTop: '6px' }} onClick={(e) => e.stopPropagation()}>
|
|
<PencilSquare
|
|
size={14}
|
|
onClick={() => goToEdit(eventItem?.id)}
|
|
style={{ cursor: 'pointer' }}
|
|
title="Edit"
|
|
/>
|
|
<Archive
|
|
size={14}
|
|
onClick={() => { setDeleteTargetId(eventItem?.id); setShowDeleteConfirm(true); }}
|
|
style={{ cursor: 'pointer' }}
|
|
title="Delete"
|
|
/>
|
|
</div>}
|
|
</div>)
|
|
}
|
|
</div>
|
|
})
|
|
}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
const handleClose = () => {
|
|
setShowCreationModal(false);
|
|
setNewEventStartDateTime(undefined);
|
|
setNewEventEndDateTime(undefined);
|
|
setNewEventColor('');
|
|
setNewEventCustomer('');
|
|
setNewEventResource('');
|
|
setNewEventInterpreter('');
|
|
setNewEventFasting('');
|
|
setNewEventNeedId('');
|
|
setNewEventNewPatient('');
|
|
setNewEventDisability('');
|
|
setNewEventTransMethod('');
|
|
}
|
|
|
|
const handleSave = () => {
|
|
if (!AuthService.canEditMedicalEvents()) return;
|
|
const userName = localStorage.getItem('user') && JSON.parse(localStorage.getItem('user'))?.name;
|
|
const selectedCustomer = customers.find(c => c.id === newEventCustomer);
|
|
const selectedResource = resources.find(r => r.id === newEventResource);
|
|
|
|
const data = {
|
|
type: 'medical',
|
|
status: 'active',
|
|
create_by: userName,
|
|
edit_by: userName,
|
|
edit_date: new Date(),
|
|
create_date: new Date(),
|
|
edit_history: [{ employee: userName, date: new Date() }],
|
|
title: selectedCustomer ? `${selectedCustomer.name} - Medical Appointment` : 'Medical Appointment',
|
|
start_time: newEventStartDateTime,
|
|
stop_time: newEventStartDateTime,
|
|
color: newEventColor,
|
|
confirmed: true,
|
|
data: {
|
|
customer: newEventCustomer,
|
|
client_name: selectedCustomer?.name || '',
|
|
resource: newEventResource,
|
|
resource_name: selectedResource?.name || '',
|
|
resource_phone: selectedResource?.phone || '',
|
|
resource_address: selectedResource?.address || '',
|
|
interpreter: newEventInterpreter,
|
|
fasting: newEventFasting,
|
|
need_id: newEventNeedId,
|
|
new_patient: newEventNewPatient,
|
|
disability: newEventDisability,
|
|
trans_method: newEventTransMethod,
|
|
}
|
|
};
|
|
|
|
EventsService.createNewEvent(data).then(() => {
|
|
EventsService.getAllEvents({ from: EventsService.formatDate(fromDate), to: EventsService.formatDate(toDate) }).then((data) => {
|
|
setAllEvents(data.data);
|
|
handleClose();
|
|
})
|
|
});
|
|
}
|
|
|
|
|
|
return (
|
|
<>
|
|
<Modal show={showLoadingModal} centered backdrop="static" keyboard={false}>
|
|
<Modal.Header>
|
|
<Modal.Title>Loading Medical Event Calendar</Modal.Title>
|
|
</Modal.Header>
|
|
<Modal.Body>
|
|
<div style={{ marginBottom: '8px', fontSize: '13px', color: '#666' }}>
|
|
Fetching and rendering calendar data...
|
|
</div>
|
|
<ProgressBar now={loadingProgress} animated label={`${Math.round(loadingProgress)}%`} />
|
|
</Modal.Body>
|
|
</Modal>
|
|
<div className="list row mb-4">
|
|
<Breadcrumb>
|
|
<Breadcrumb.Item href="/medical/index">Medical</Breadcrumb.Item>
|
|
<Breadcrumb.Item active>
|
|
Medical Event Calendar
|
|
</Breadcrumb.Item>
|
|
</Breadcrumb>
|
|
<div className="col-md-12 text-primary">
|
|
<h4>
|
|
Medical Event Calendar
|
|
</h4>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="app-main-content-list-container" style={{"min-width": "1500px"}}>
|
|
<div className="app-main-content-list-func-container">
|
|
<Tabs defaultActiveKey="medicalCalendar" id="events-calendar-tab">
|
|
<Tab eventKey="medicalCalendar" title="Medical Appointments">
|
|
|
|
</Tab>
|
|
</Tabs>
|
|
{ calendarView }
|
|
<div className="list-func-panel">
|
|
<Dropdown
|
|
key={'event-calendar-filter'}
|
|
id="event-calendar-filter"
|
|
show={showFilterDropdown}
|
|
onToggle={() => setShowFilterDropdown(!showFilterDropdown)}
|
|
autoClose={false}
|
|
>
|
|
<Dropdown.Toggle variant="primary">
|
|
<Filter size={16} className="me-2"></Filter>Filter
|
|
</Dropdown.Toggle>
|
|
<Dropdown.Menu as={customMenu}/>
|
|
</Dropdown>
|
|
</div>
|
|
<Modal show={showCreationModal} onHide={handleClose} size="sm" centered dialogClassName="calendar-event-modal">
|
|
<Modal.Header closeButton>
|
|
<Modal.Title>New Medical Appointment</Modal.Title>
|
|
</Modal.Header>
|
|
<Modal.Body>
|
|
<div className="mb-3">
|
|
<div className="field-label">Customer<span className="required">*</span></div>
|
|
<select className="form-control" value={newEventCustomer} onChange={(e) => setNewEventCustomer(e.target.value)}>
|
|
<option value="">Select Customer</option>
|
|
{customers.filter(item => item?.status === 'active' && item?.type !== 'discharged').map((item) => <option key={item.id} value={item.id}>{item.name}</option>)}
|
|
</select>
|
|
</div>
|
|
<div className="mb-3">
|
|
<div className="field-label">Provider<span className="required">*</span></div>
|
|
<select className="form-control" value={newEventResource} onChange={(e) => setNewEventResource(e.target.value)}>
|
|
<option value="">Select Provider</option>
|
|
{resources.filter(item => item?.status === 'active').map((item) => <option key={item.id} value={item.id}>{item.name}</option>)}
|
|
</select>
|
|
</div>
|
|
<div className="mb-3">
|
|
<div className="field-label">Appointment Time<span className="required">*</span></div>
|
|
<DatePicker
|
|
className="form-control"
|
|
selected={newEventStartDateTime}
|
|
onChange={setNewEventStartDateTime}
|
|
showTimeSelect
|
|
timeFormat="HH:mm"
|
|
timeIntervals={15}
|
|
dateFormat="MM/dd/yyyy, HH:mm"
|
|
/>
|
|
</div>
|
|
<div className="mb-3">
|
|
<div className="field-label">Language Support</div>
|
|
<select className="form-control" value={newEventInterpreter} onChange={(e) => setNewEventInterpreter(e.target.value)}>
|
|
<option value="">Select</option>
|
|
<option value="Checkin">Checkin</option>
|
|
<option value="Center">Center</option>
|
|
<option value="Family">Family</option>
|
|
<option value="Office in person">Office in person</option>
|
|
<option value="Office by phone">Office by phone</option>
|
|
<option value="Nurse">Nurse</option>
|
|
</select>
|
|
</div>
|
|
<div className="mb-3">
|
|
<div className="field-label">Transportation Support</div>
|
|
<select className="form-control" value={newEventTransMethod} onChange={(e) => setNewEventTransMethod(e.target.value)}>
|
|
<option value="">Select</option>
|
|
<option value="ByOwn">Self-Transport</option>
|
|
<option value="By Center Transportation">By Center Transportation</option>
|
|
<option value="DropOff Only">Drop-Off Only</option>
|
|
<option value="Pickup Only">Pick-Up Only</option>
|
|
<option value="Client Does Not need to Go">Medication Pickup Only</option>
|
|
</select>
|
|
</div>
|
|
<div className="mb-3">
|
|
<div className="field-label">Label</div>
|
|
<select className="form-control" value={newEventColor} onChange={(e) => setNewEventColor(e.target.value)}>
|
|
<option value="">Select</option>
|
|
{EventsService.labelOptions?.map((item) => <option key={item.value} value={item.value}>{item.label}</option>)}
|
|
</select>
|
|
</div>
|
|
</Modal.Body>
|
|
<Modal.Footer>
|
|
<Button variant="secondary" size="sm" onClick={handleClose}>
|
|
Cancel
|
|
</Button>
|
|
<Button variant="primary" size="sm" onClick={handleSave} disabled={
|
|
!AuthService.canEditMedicalEvents() || !newEventCustomer || !newEventResource || !newEventStartDateTime
|
|
}>
|
|
Save
|
|
</Button>
|
|
</Modal.Footer>
|
|
</Modal>
|
|
<Modal show={showDeleteConfirm} onHide={() => { setShowDeleteConfirm(false); setDeleteTargetId(null); }} size="sm" centered>
|
|
<Modal.Header closeButton>
|
|
<Modal.Title>Confirm Delete</Modal.Title>
|
|
</Modal.Header>
|
|
<Modal.Body>
|
|
Are you sure you want to delete this event?
|
|
</Modal.Body>
|
|
<Modal.Footer>
|
|
<Button variant="secondary" size="sm" onClick={() => { setShowDeleteConfirm(false); setDeleteTargetId(null); }}>
|
|
Cancel
|
|
</Button>
|
|
<Button variant="danger" size="sm" onClick={() => { disableEvent(deleteTargetId); setShowDeleteConfirm(false); setDeleteTargetId(null); }}>
|
|
Delete
|
|
</Button>
|
|
</Modal.Footer>
|
|
</Modal>
|
|
</div>
|
|
</div>
|
|
</>
|
|
)
|
|
};
|
|
|
|
export default EventsCalendar;
|