Files
mwitnessing/components/calendar/avcalendar.tsx
Dobromir Popov 68915628a9 fix toast problem
2024-06-17 23:26:55 +03:00

570 lines
23 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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);
useEffect(() => {
(async () => {
try {
setIsAdmin(await ProtectedRoute.IsInRole(UserRole.ADMIN));
} catch (error) {
console.error("Failed to check admin role:", error);
}
})();
}, []);
//const isAdmin = ProtectedRoute.IsInRole(UserRole.ADMIN);
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)
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;
}
}
// 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 (
<div style={eventStyle} className={bgColor + " relative"}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave} >
{event.title}
{isHovered && event.type == "assignment" && (event.status == "pending" || event.status == undefined)
&& (
<div className="absolute top-1 left-0 right-0 flex justify-between px-1">
{/* Delete Icon */}
{/* <span
className="disabled cursor-pointer rounded-full bg-red-500 text-white flex items-center justify-center"
style={{ width: '24px', height: '24px' }} // Adjust the size as needed
onClick={() => onDelete(event)}
>
</span> */}
{/* Confirm Icon */}
{/* {!event.isConfirmed && (
<span
className=" cursor-pointer rounded-full bg-green-500 text-white flex items-center justify-center"
style={{ width: '24px', height: '24px' }} // Adjust the size as needed
onClick={() => onConfirm(event)}
>
</span>
)} */}
</div>
)}
</div>
);
};
const CustomToolbar = ({ onNavigate, label, onView, view }) => {
return (
<div className="rbc-toolbar">
<span className="rbc-btn-group">
<button type="button" onClick={() => onNavigate('PREV')}>
<FaArrowLeft className="icon-large" />
</button>
<button type="button" onClick={() => onNavigate('TODAY')}>
<MdToday className="icon-large" />
</button>
<button type="button" onClick={() => onNavigate('NEXT')}>
<FaArrowRight className="icon-large" />
</button>
</span>
<span className="rbc-toolbar-label">{label}</span>
<span className="rbc-btn-group">
<button type="button" onClick={() => onView('month')} className={view === 'month' ? 'rbc-active' : ''}>
<FaRegCalendarAlt className="icon-large" />
</button>
<button type="button" onClick={() => onView('week')} className={view === 'week' ? 'rbc-active' : ''}>
<FaRegListAlt className="icon-large" />
</button>
<button type="button" onClick={() => onView('agenda')} className={view === 'agenda' ? 'rbc-active' : ''}>
<FaRegCalendarCheck className="icon-large" />
</button>
{/* Add more view buttons as needed */}
</span>
</div>
);
};
const CustomEventAgenda = ({
event
}) => (
<span>
<em style={{ color: 'black' }}>{event.title}</em>
<p>{event.desc}</p>
</span>
);
return (
<> <div {...handlers} className="flex flex-col"
>
{/* достъпности на {publisherId} */}
{/* having multiple ToastContainers causes double rendering of toasts and all kind of problems */}
{/* <ToastContainer position="top-center" style={{ zIndex: 9999 }} /> */}
</div>
<Calendar
localizer={localizer}
events={displayedEvents}
startAccessor="startTime"
endAccessor="endTime"
selectable={true}
onSelectSlot={handleSelect}
onSelectEvent={handleEventClick}
style={{ height: '100%', width: '100%' }}
min={cartEvent?.startTime} // Set minimum time
max={cartEvent?.endTime} // Set maximum time
messages={messages}
view={currentView}
views={['month', 'week', 'agenda']}
onView={view => setCurrentView(view)}
onRangeChange={onRangeChange}
components={{
event: EventWrapper,
toolbar: CustomToolbar,
view: CustomEventAgenda,
agenda: {
event: CustomEventAgenda
},
// ... other custom components
}}
eventPropGetter={(eventStyleGetter)}
date={date}
showAllEvents={true}
onNavigate={setDate}
className="rounded-lg shadow-lg"
longPressThreshold={150} // default value 250
/>
{isModalOpen && (
<div className="fixed inset-0 flex items-center justify-center z-50">
<div className="modal-content">
<AvailabilityForm
publisherId={publisherId}
existingItems={selectedEvents}
date={date}
onDone={handleDialogClose}
inline={true}
cartEvent={cartEvent}
// Pass other props as needed
/>
</div>
<div className="fixed inset-0 bg-black opacity-50" onClick={handleCancel}></div>
</div>
)}
</>
);
};
export default AvCalendar;