Files
mwitnessing/components/calendar/avcalendar.tsx
2024-02-22 04:19:38 +02:00

479 lines
20 KiB
TypeScript

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';
// 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 [selectedEvent, setSelectedEvent] = useState(null);
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(() => {
setEvents(events);
// 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 }) => {
if (!start || !end) return;
if (start < new Date() || end < new Date() || start > end) return;
// Check if start and end are on the same day
if (start.toDateString() !== end.toDateString()) {
end = common.setTimeHHmm(start, "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.publisherId === publisherId && event.startTime === start.toDateString());
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;
// 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);
setSelectedEvent(eventForEditing);
setIsModalOpen(true);
};
const handleDialogClose = async (dialogEvent) => {
setIsModalOpen(false);
if (dialogEvent === null || dialogEvent === undefined) {
} else {
// if (dialogEvent.deleted) {
// // Remove the old event from the calendar
// setEvents(currentEvents => currentEvents.filter(e => e.id !== selectedEvent.id));
// }
// else {
// // Update the event data
// dialogEvent.start = dialogEvent.startTime;
// dialogEvent.end = dialogEvent.endTime;
// // 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 !== selectedEvent.id) || [];
// return [...filteredEvents, dialogEvent];
// });
// }
//refresh the events from the server
let events = await axiosInstance.get(`/api/?action=getCalendarEvents&publisherId=${publisherId}`);
var newEvents = events.data;
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";
if (event.type === "assignment") {
bgColor = 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',
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.isactive = 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];
});
};
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 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 (
<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>
);
};
return (
<> <div {...handlers} className="flex flex-col"
>
{/* достъпности на {publisherId} */}
<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={minTime} // Set minimum time
max={maxTime} // Set maximum time
messages={messages}
view={currentView}
views={['month', 'week', 'agenda']}
onView={view => setCurrentView(view)}
onRangeChange={onRangeChange}
components={{
event: EventWrapper,
toolbar: CustomToolbar,
// ... other custom components
}}
eventPropGetter={(eventStyleGetter)}
date={date}
onNavigate={setDate}
className="rounded-lg shadow-lg"
/>
{isModalOpen && (
<div className="fixed inset-0 flex items-center justify-center z-50">
<div className="modal-content">
<AvailabilityForm
publisherId={publisherId}
existingItem={selectedEvent}
onDone={handleDialogClose}
inline={true}
// Pass other props as needed
/>
</div>
<div className="fixed inset-0 bg-black opacity-50" onClick={handleCancel}></div>
</div>
)}
</>
);
};
export default AvCalendar;