import React, { useState, useEffect, use } from 'react'; import { Calendar, momentLocalizer, dateFnsLocalizer } from 'react-big-calendar'; import 'react-big-calendar/lib/css/react-big-calendar.css'; import AvailabilityForm from '../availability/AvailabilityForm'; import ProtectedRoute from '../protectedRoute'; import { UserRole } from "@prisma/client"; import common from '../../src/helpers/common'; import { toast } from 'react-toastify'; import { ToastContainer } from 'react-toastify'; import 'react-toastify/dist/ReactToastify.css'; import moment from 'moment'; // ToDo: obsolete, remove it import 'moment/locale/bg'; // Import Bulgarian locale import { ArrowLeftCircleIcon } from '@heroicons/react/24/outline'; import { FaArrowLeft, FaArrowRight, FaRegCalendarAlt, FaRegListAlt, FaRegCalendarCheck } from 'react-icons/fa'; import { MdToday } from 'react-icons/md'; import { useSwipeable } from 'react-swipeable'; import axiosInstance from '../../src/axiosSecure'; import { set } from 'date-fns'; import { get } from 'http'; // import { set, format, addDays } from 'date-fns'; // import { isEqual, isSameDay, getHours, getMinutes } from 'date-fns'; // import { filter } from 'jszip'; // import e from 'express'; // Set moment to use the Bulgarian locale moment.locale('bg'); const localizer = momentLocalizer(moment); // Bulgarian translations for Calendar labels const messages = { allDay: 'Цял ден', previous: 'Предишен', next: 'Следващ', today: 'Днес', month: 'Месец', week: 'Седмица', day: 'Ден', agenda: 'Дневен ред', date: 'Дата', time: 'Час', event: 'Събитие', // or 'Събитие' depending on context // Any other labels you want to translate... }; const AvCalendar = ({ publisherId, events, selectedDate, cartEvents, lastPublishedDate }) => { const [editLockedBefore, setEditLockedBefore] = useState(new Date(lastPublishedDate)); const [isAdmin, setIsAdmin] = useState(false); // const [isPowerUser, setIsPowerUser] = useState(false); // const [currentUserId, setCurrentUserId] = useState(null); useEffect(() => { (async () => { try { setIsAdmin(await ProtectedRoute.IsInRole(UserRole.ADMIN)); // setIsPowerUser(await ProtectedRoute.IsInRole(UserRole.POWERUSER)); // // Assuming you have a way to get the current user's ID // setCurrentUserId(await ProtectedRoute.GetCurrentUserId()); } catch (error) { console.error("Failed to check admin role:", error); } })(); }, []); //const isAdmin = ProtectedRoute.IsInRole(UserRole.ADMIN); //block dates between 1 and 18 august 2024 const blockedDates = [ new Date(2024, 7, 1), new Date(2024, 7, 2), new Date(2024, 7, 3), new Date(2024, 7, 4), new Date(2024, 7, 5), new Date(2024, 7, 6), new Date(2024, 7, 7), new Date(2024, 7, 8), new Date(2024, 7, 9), new Date(2024, 7, 10), new Date(2024, 7, 11), new Date(2024, 7, 12), new Date(2024, 7, 13), new Date(2024, 7, 14), new Date(2024, 7, 15), new Date(2024, 7, 16), new Date(2024, 7, 17), new Date(2024, 7, 18), ]; const [date, setDate] = useState(new Date()); //ToDo: see if we can optimize this const [evts, setEvents] = useState(events); // Existing events const [selectedEvents, setSelectedEvents] = useState([]); const [displayedEvents, setDisplayedEvents] = useState(evts); // Events to display in the calendar const [currentView, setCurrentView] = useState('month'); const [isModalOpen, setIsModalOpen] = useState(false); const [visibleRange, setVisibleRange] = useState(() => { const start = new Date(); start.setDate(1); // Set to the first day of the current month const end = new Date(start.getFullYear(), start.getMonth() + 1, 0); // Last day of the current month return { start, end }; }); const [cartEvent, setCartEvent] = useState(null); function getCartEvent(date) { const dayOfWeek = common.getDayOfWeekNameEnEnumForDate(date); const ce = cartEvents?.find(e => e.dayofweek === dayOfWeek); return ce; } useEffect(() => { //console.log("useEffect: ", date, selectedEvents, cartEvents); setCartEvent(getCartEvent(date)); }, [date, selectedEvents]); // Update internal state when `events` prop changes useEffect(() => { //if we have isBySystem - set type to assignment let updatedEvents = events?.map(event => { if (event.isBySystem) { event.type = "assignment"; } return event; }); updatedEvents = events?.map(event => ({ ...event, date: new Date(event.startTime).setHours(0, 0, 0, 0), startTime: new Date(event.startTime), endTime: event.endTime ? new Date(event.endTime) : undefined })); setEvents(updatedEvents); // Call any function here to process and set displayedEvents // based on the new events, if necessary }, [events]); const onRangeChange = (range) => { if (Array.isArray(range)) { // For week and day views, range is an array of dates setVisibleRange({ start: range[0], end: range[range.length - 1] }); } else { // For month view, range is an object with start and end setVisibleRange(range); } }; useEffect(() => { if (currentView === 'agenda') { const filtered = evts?.filter(event => event.type === "assignment"); setDisplayedEvents(filtered); } else { // Function to generate weekly occurrences of an event const recurringEvents = evts?.filter(event => event.type !== "assignment" && (event.dayOfMonth === null || event.dayOfMonth === undefined)) || []; const occurrences = recurringEvents?.flatMap(event => generateOccurrences(event, visibleRange.start, visibleRange.end)) || []; const nonRecurringEvents = evts?.filter(event => event.dayOfMonth !== null) || []; setDisplayedEvents([...nonRecurringEvents, ...recurringEvents, ...occurrences]); } //setDisplayedEvents(evts); }, [visibleRange, evts, currentView]); // todo: review that const handlers = useSwipeable({ onSwipedLeft: () => navigate('NEXT'), onSwipedRight: () => navigate('PREV'), preventDefaultTouchmoveEvent: true, trackMouse: true, }); const navigate = (action) => { console.log('navigate', action); setDate((currentDate) => { const newDate = new Date(currentDate); if (action === 'NEXT') { newDate.setMonth(newDate.getMonth() + 1); } else if (action === 'PREV') { newDate.setMonth(newDate.getMonth() - 1); } return newDate; }); }; const generateOccurrences = (event, start, end) => { const occurrences = []; const eventStart = new Date(event.startTime); let current = new Date(event.startTime); // Assuming startTime has the start date // Determine the end date for the event series const seriesEndDate = event.endDate ? new Date(event.endDate) : end; seriesEndDate.setHours(23, 59, 59); // Set to the end of the day while (current <= seriesEndDate && current <= end) { // Check if the event should be repeated weekly or on a specific day of the month if (event.repeatWeekly && current.getDay() === eventStart.getDay()) { // For weekly recurring events addOccurrence(event, current, occurrences); } else if (event.dayOfMonth && current.getDate() === event.dayOfMonth) { // For specific day of month events addOccurrence(event, current, occurrences); } // Move to the next day current = new Date(current.setDate(current.getDate() + 1)); } return occurrences; }; // Helper function to add an occurrence const addOccurrence = (event, current, occurrences) => { // Skip the original event date const eventStart = new Date(event.startTime); const eventEnd = new Date(event.endTime); if (current.toDateString() !== eventStart.toDateString()) { const occurrence = { ...event, startTime: new Date(current.setHours(eventStart.getHours(), eventStart.getMinutes())), endTime: new Date(current.setHours(eventEnd.getHours(), eventEnd.getMinutes())), date: current, type: 'recurring' }; occurrences.push(occurrence); } }; const filterEvents = (evts, publisherId, startdate) => { setDate(startdate); // Assuming setDate is a function that sets some state or context const filterDayOfWeek = startdate.getDay(); // Sunday - 0, Monday - 1, ..., Saturday - 6 // Filter events based on the publisher ID and the start date/time const existingEvents = evts?.filter(event => { // Ensure the event belongs to the specified publisher const isPublisherMatch = (event.publisher?.id || event.publisherId) === publisherId; const eventDayOfWeek = event.startTime.getDay(); let isDateMatch; const eventDate = new Date(event.startTime); if (event.repeatWeekly && filterDayOfWeek === eventDayOfWeek) { isDateMatch = true; } else if (event.date) { // Compare the full date. issameday is not working. do it manually isDateMatch = eventDate.setHours(0, 0, 0, 0) === new Date(startdate).setHours(0, 0, 0, 0); } return isPublisherMatch && isDateMatch; }); return existingEvents; }; // const totalHours = maxHour - minHour; const handleSelect = ({ mode, start, end }) => { //we set the time to proper timezone const startdate = common.setTimezone(start); const enddate = common.setTimezone(end); if (!start || !end) return; //readonly for past dates (ToDo: if not admin or current user) //const isCurrentUser = selectedEvents.some(event => event.userId === currentUserId); if (!isAdmin) { if (startdate < new Date() || end < new Date() || startdate > end) return; //or if schedule is published (lastPublishedDate) if (editLockedBefore && startdate < editLockedBefore) { toast.error(`Не можете да променяте предпочитанията си за дати преди ${common.getDateFormattedShort(editLockedBefore)}.`, { autoClose: 5000 }); return; } if (blockedDates[0] <= startdate && startdate <= blockedDates[blockedDates.length - 1]) { toast.error(`Не можете да въвеждате предпочитания за ${common.getDateFormattedShort(startdate)}`, { autoClose: 5000 }); return; } } // Check if start and end are on the same day if (startdate.toDateString() !== enddate.toDateString()) { end = common.setTimeHHmm(startdate, "23:59"); } // Update date state and calculate events based on the new startdate setDate(startdate); const existingEvents = filterEvents(evts, publisherId, startdate); console.log("handleSelect: ", existingEvents); // Use the updated startdate for getCartEvent and ensure it reflects in the state properly const cartEvent = getCartEvent(startdate); setCartEvent(cartEvent); console.log("cartEvent: ", cartEvent); setSelectedEvents(existingEvents); setIsModalOpen(true); }; const handleEventClick = (event) => { if (event.type === "assignment") return; //select the whole day let start = new Date(event.startTime); start.setHours(0, 0, 0, 0); let end = new Date(event.startTime); end.setHours(23, 59, 59, 999); handleSelect({ mode: 'select', start: start, end: end }); }; const handleDialogClose = async (dialogEvent) => { setIsModalOpen(false); if (dialogEvent === null || dialogEvent === undefined) { } else { let e = await axiosInstance.get(`/api/?action=getCalendarEvents&publisherId=${publisherId}`); var newEvents = e.data; // set start and end to Date objects for all events. Fix for the calendar component newEvents.forEach(event => { event.startTime = new Date(event.startTime); event.endTime = new Date(event.endTime); }); setEvents(newEvents); } console.log("handleSave: ", dialogEvent); }; const handleCancel = () => { setIsModalOpen(false); }; // REDRAW (PAGING) STYLES FOR THE EVENT DEFINED HERE const eventStyleGetter = (event, start, end, isSelected) => { //console.log("eventStyleGetter: " + event); let backgroundColor = '#3174ad'; // default color for calendar events - #3174ad if (currentView === 'agenda') { return { style: {} } } if (event.type === "assignment") { //event.title = event.publisher.name; //ToDo: add other publishers names } if (event.type === "availability") { } if (event.isFromPreviousAssignment) { //ToDo: does it work? // orange-500 from Tailwind CSS backgroundColor = '#f56565'; } if (event.isFromPreviousMonth) { //gray backgroundColor = '#a0aec0'; } // if (event.isActive) { switch (event.type) { case 'assignment': backgroundColor = '#48bb78' // always green-500 as we don't pass isConfirmed correctly //backgroundColor = event.isConfirmed ? '#48bb78' : '#f6e05e'; // green-500 and yellow-300 from Tailwind CSS break; case 'recurring': backgroundColor = '#63b3ed'; // blue-300 from Tailwind CSS break; default: // availability //backgroundColor = '#a0aec0'; // gray-400 from Tailwind CSS break; } // } else { // backgroundColor = '#a0aec0'; // Default color for inactive events // } return { style: { backgroundColor, opacity: event.startTime < new Date() ? 0.5 : 1, color: 'white', border: '0px', display: 'block', } }; } // INITIAL STYLE FOR THE EVENT DEFINED HERE const EventWrapper = ({ event, style }) => { const [isHovered, setIsHovered] = useState(false); let eventStyle = { ...style }; const handleMouseEnter = () => setIsHovered(true); const handleMouseLeave = () => setIsHovered(false); if (currentView !== 'agenda') { //if event.type is availability show in blue. if it is schedule - green if confirmed, yellow if not confirmed var bgColor = ""; //ToDo: fix this. maybe we're missing some properties // if (event.isFromPreviousMonth) { // // set opacity to 0.5 // bgColor = "bg-orange-500"; // } if (event.type === "assignment") { bgColor = event.isBySystem ? "bg-red-500" : (event.isConfirmed || true ? "bg-green-500" : "bg-yellow-500"); //event.title = event.publisher.name; //ToDo: add other publishers names //event.title = common.getTimeFormatted(event.startTime) + " - " + common.getTimeFormatted(event.endTime); } else { if (event.start !== undefined && event.end !== undefined && event.startTime !== null && event.endTime !== null) { try { if (event.type === "recurring") { event.title = common.getTimeFormatted(event.startTime) + " - " + common.getTimeFormatted(event.endTime); } else { event.title = common.getTimeFormatted(event.startTime) + " - " + common.getTimeFormatted(event.endTime); } } catch (err) { event.title = event.startTime + " - " + event.endTime; console.log("Error in EventWrapper: " + err); } } } eventStyle = { ...style, // backgroundColor: bgColorClass, //height: "50px", //color: 'white', //if (event.isFromPreviousAssignment) { set opacity to 0.5 } // opacity: event.isFromPreviousMonth ? 0.5 : 1, whiteSpace: 'normal', // Allow the text to wrap to the next line overflow: 'hidden', // Hide overflowed content textOverflow: 'ellipsis' // Add ellipsis to text that's too long to fit }; if (event.date < new Date()) { eventStyle.opacity = 0.5; } } const onDelete = (event) => { // Remove the event from the calendar setEvents(currentEvents => currentEvents.filter(e => e.id !== event.id)); }; const onConfirm = (event) => { console.log("onConfirm: " + event.id); toast.info("Потвърдено!", { autoClose: 2000 }); // Update the event data event.isConfirmed = true; event.isBySystem = false; // Update the events array by first removing the old event and then adding the updated one setEvents(currentEvents => { const filteredEvents = currentEvents.filter(e => e.id !== event.id); return [...filteredEvents, event]; }); //store the updated event in the database var assignment = { isConfirmed: true, isBySystem: false }; axiosInstance.put('/api/data/assignments/' + event.id, assignment) .then((response) => { console.log(response); }) .catch((error) => { console.log(error); }); }; return (
{event.title} {isHovered && event.type == "assignment" && (event.status == "pending" || event.status == undefined) && (
{/* Delete Icon */} {/* onDelete(event)} > ✕ */} {/* Confirm Icon */} {/* {!event.isConfirmed && ( onConfirm(event)} > ✓ )} */}
)}
); }; const CustomToolbar = ({ onNavigate, label, onView, view }) => { return (
{label} {/* Add more view buttons as needed */}
); }; const CustomEventAgenda = ({ event }) => ( {event.title}

{event.desc}

); return ( <>
{/* достъпности на {publisherId} */} {/* having multiple ToastContainers causes double rendering of toasts and all kind of problems */} {/* */}
setCurrentView(view)} onRangeChange={onRangeChange} components={{ event: EventWrapper, toolbar: CustomToolbar, view: CustomEventAgenda, agenda: { event: CustomEventAgenda }, // ... other custom components }} dayPropGetter={(date) => { // Highlight the current day // if (date.toDateString() === new Date().toDateString()) { // return { // style: { // // white-500 from Tailwind CSS // backgroundColor: '#f9fafb', // color: 'white' // } // }; // } if (blockedDates[0] <= date && date <= blockedDates[blockedDates.length - 1]) { return { style: { // red-100 from Tailwind CSS backgroundColor: '#fee2e2', color: 'white' } }; } }} eventPropGetter={(eventStyleGetter)} date={date} showAllEvents={true} onNavigate={setDate} className="rounded-lg shadow-lg" longPressThreshold={150} // default value 250 /> {isModalOpen && (
)} ); }; export default AvCalendar;