import React, { useState, useEffect } 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 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'; 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'; // 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 }) => { const [date, setDate] = useState(new Date()); const [currentView, setCurrentView] = useState('month'); const [evts, setEvents] = useState(events); // Existing events const [displayedEvents, setDisplayedEvents] = useState(evts); // Events to display in the calendar const [isModalOpen, setIsModalOpen] = useState(false); const [selectedEvents, setSelectedEvents] = useState([]); 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 }; }); // Update internal state when `events` prop changes useEffect(() => { const 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]); 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); } }; // Define min and max times const minHour = 8; // 8:00 AM const maxHour = 20; // 8:00 PM const minTime = new Date(); minTime.setHours(minHour, 0, 0); const maxTime = new Date(); maxTime.setHours(maxHour, 0, 0); const totalHours = maxHour - minHour; const handleSelect = ({ start, end }) => { const startdate = typeof start === 'string' ? new Date(start) : start; const enddate = typeof end === 'string' ? new Date(end) : end; if (!start || !end) return; if (startdate < new Date() || end < new Date() || startdate > end) return; // Check if start and end are on the same day if (startdate.toDateString() !== enddate.toDateString()) { end = common.setTimeHHmm(startdate, "23:59"); } const startMinutes = common.getTimeInMinutes(start); const endMinutes = common.getTimeInMinutes(end); // Adjust start and end times to be within min and max hours if (startMinutes < common.getTimeInMinutes(common.setTimeHHmm(start, minHour))) { start = common.setTimeHHmm(start, minHour); } if (endMinutes > common.getTimeInMinutes(common.setTimeHHmm(end, maxHour))) { end = common.setTimeHHmm(end, maxHour); } setDate(start); // get exising events for the selected date const existingEvents = evts?.filter(event => (event.publisher?.id || event.publisherId) === publisherId && new Date(event.date).toDateString() === startdate.toDateString()); // const existingEvents = evts?.filter(event => { // return event.publisherId === publisherId && // new Date(event.startTime).getFullYear() === start.getFullYear() && // new Date(event.startTime).getMonth() === start.getMonth() && // new Date(event.startTime).getDate() === start.getDate(); // }); console.log("handleSelect: " + existingEvents); setSelectedEvents(existingEvents); // setSelectedEvent({ // date: start, // startTime: start, // endTime: end, // dayOfMonth: start.getDate(), // isActive: true, // publisherId: publisherId, // // Add any other initial values needed // //set dayOfMonth to null, so that we repeat the availability every week // dayOfMonth: null, // }); setIsModalOpen(true); }; const handleEventClick = (event) => { if (event.type === "assignment") return; handleSelect({ start: event.startTime, end: event.endTime }); // Handle event click // const eventForEditing = { // ...event, // startTime: new Date(event.startTime), // endTime: new Date(event.endTime), // publisherId: event.publisherId || event.publisher?.connect?.id, // repeatWeekly: event.repeatWeekly || false, // }; // //strip title, start, end and allDay properties // delete eventForEditing.title; // delete eventForEditing.start; // delete eventForEditing.end; // delete eventForEditing.type; // delete eventForEditing.publisher // console.log("handleEventClick: " + eventForEditing); // setSelectedEvents([eventForEditing]); // setIsModalOpen(true); }; 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); }; 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 //if event is not active - show in gray let bgColorClass = 'bg-gray-500'; // Default color for inactive events var bgColor = event.isActive ? "" : "bg-gray-500"; //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 ? "bg-green-500" : "bg-yellow-500"); //event.title = event.publisher.name; //ToDo: add other publishers names //event.title = common.getTimeFomatted(event.startTime) + " - " + common.getTimeFomatted(event.endTime); } else { if (event.start !== undefined && event.end !== undefined && event.startTime !== null && event.endTime !== null) { try { if (event.type === "recurring") { //bgColor = "bg-blue-300"; event.title = common.getTimeFomatted(event.startTime) + " - " + common.getTimeFomatted(event.endTime); } else { event.title = common.getTimeFomatted(event.startTime) + " - " + common.getTimeFomatted(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 }; } 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 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.isActive) { switch (event.type) { case 'assignment': 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: 0.8, color: 'white', border: '0px', display: 'block', } }; } // Custom Toolbar Component const CustomToolbar = ({ onNavigate, label, onView, view }) => { return (
{label} {/* Add more view buttons as needed */}
); }; return ( <>
{/* достъпности на {publisherId} */}
setCurrentView(view)} onRangeChange={onRangeChange} components={{ event: EventWrapper, toolbar: CustomToolbar, // ... other custom components }} eventPropGetter={(eventStyleGetter)} date={date} onNavigate={setDate} className="rounded-lg shadow-lg" /> {isModalOpen && (
)} ); }; export default AvCalendar;