import React, { createContext, useContext, useState, ReactNode, useEffect, useCallback, } from 'react'; import dayjs from 'dayjs'; import isoWeek from 'dayjs/plugin/isoWeek'; import { ECalendarMode, IEventsGroup, IOptions, IPartialRanges, ISlot, ISlotEventsToSpaces, ISlotsEvents, ISlotsEventsAndEventsWithSpaces, TOnChangeInCalender, } from '../types'; import { calculateMarginFromHours, calculateMarginFromMinutes, calculateMarginLeftFromHours, calculateMarginLeftFromMinutes, oneHourHeight, oneHourWidth, setNewHourHeight, setNewHourWidth, } from '../utils/timeUtils'; import { usePrevious } from './usePrevious'; dayjs.extend(isoWeek); export type DateRange = { [key: number]: { startDate: string; endDate: string; }; }; export type MiddlewareContent = { datesRange: DateRange; updateDatesRange: ( id: number, startDate: string, endDate: string, onChangeData?: any ) => void; hasCollision: (event: ISlot, list: ISlot[]) => boolean; day: number; incrementDay: () => void; updateEventsToSpaceByDatesRange: ( spaces: ISlotEventsToSpaces, onChange?: TOnChangeInCalender ) => { [x: number]: ISlot[]; }; draggableDatesRange: DateRange; setDraggableRange: (id: number, startDate: string, endDate: string) => void; options: IOptions; setOptions: (options: IOptions) => void; partialRanges: IPartialRanges; setPartialRanges: (range: IPartialRanges) => void; getPartialEvents: (events: ISlot[], from: number, to: number) => ISlot[]; getTopAndHeight: ( startDate: number, endDate: number, horizontal?: boolean ) => { top: number; height: number }; getEventsGroups: (events: ISlot[], horizontal?: boolean) => IEventsGroup[]; isResizing: boolean; setIsResizing: (isResizing: boolean) => void; initiateEventsToSpaces: (slots: ISlotsEventsAndEventsWithSpaces) => void; updateEventsData: () => void; handleDrop: ( eventId: number | string, parentId: number | string, spaceId: number | string, newDate: string, newEndDate?: string ) => void; eventsToSpaces: ISlotEventsToSpaces; events: ISlotsEvents; }; const MiddlewareContext = createContext({ datesRange: {}, updateDatesRange: ( id: number, startDate: string, endDate: string, onChangeData?: any ) => {}, hasCollision: (event: any, list: any[]) => false, day: 1, incrementDay: () => {}, updateEventsToSpaceByDatesRange: ( spaces: ISlotEventsToSpaces, onChange?: TOnChangeInCalender ) => ({}), draggableDatesRange: {}, setDraggableRange: (id: number, startDate: string, endDate: string) => {}, options: {}, setOptions: (options: IOptions) => ({}), partialRanges: { vertical: { from: 0, to: 0, }, horizontal: { from: 0, to: 0, }, }, setPartialRanges: (ranges: IPartialRanges) => ({}), getPartialEvents: (events: ISlot[], from: number, to: number) => [], getTopAndHeight: ( startDate: number, endDate: number, horizontal?: boolean ) => ({ top: 0, height: 0 }), getEventsGroups: (events: ISlot[], horizontal?: boolean) => [], isResizing: false, setIsResizing: (isResizing: boolean) => {}, initiateEventsToSpaces: (slots: ISlotsEventsAndEventsWithSpaces) => {}, updateEventsData: () => {}, handleDrop: ( eventId: number | string, parentId: number | string, spaceId: number | string, newDate: string, newEndDate?: string ) => {}, eventsToSpaces: {}, events: {}, }); export const MiddlewareContextProvider = ({ children, onChange, }: { children: ReactNode; onChange?: TOnChangeInCalender; }) => { const [eventsToSpaces, setEventsToSpaces] = useState({}); const [events, setEvents] = useState({}); const initiateEventsToSpaces = useCallback( (slots: ISlotsEventsAndEventsWithSpaces) => { setEventsToSpaces(slots.eventsToSpaces); setEvents(slots.events); }, [] ); const handleDrop = useCallback( ( eventId: number | string, parentId: number | string, spaceId: number | string, newDate: string, newEndDate?: string ) => { let newEvents = { ...eventsToSpaces }; let fullEvents = { ...events }; const dataEvent = newEvents[spaceId]?.find( (item: ISlot) => item.id === Number(eventId) ) || fullEvents[eventId]; let event = { ...dataEvent, spaceId }; if (newDate) { const date = dayjs.utc(newDate); let startDate = dayjs .utc(event.startDate) .set('date', date.get('date')) .set('month', date.get('month')) .set('year', date.get('year')) .toISOString(); let endDate = dayjs .utc(event.endDate) .set('date', date.get('date')) .set('month', date.get('month')) .set('year', date.get('year')) .toISOString(); // Drag'n'drop slot on daily view if (newEndDate) { startDate = newDate; endDate = newEndDate; } event = { ...event, startDate, endDate, }; } // remove event from the previous space if (newEvents[parentId]) { newEvents[parentId] = newEvents[parentId].filter( (item: ISlot) => Number(item.id) !== Number(eventId) ); } // add event to the new space if (newEvents[spaceId]) { newEvents[spaceId] = [...newEvents[spaceId], event]; } else { newEvents[spaceId] = [event]; } setEventsToSpaces(newEvents); // update the full events object const newEvent = { ...fullEvents[eventId], spaceId }; fullEvents[eventId] = newEvent; setEvents(fullEvents); }, [events, eventsToSpaces] ); const [options, setOptions] = useState({}); const previousOptions = usePrevious(options); useEffect(() => { setNewHourHeight(options?.hourSize?.vertical); setNewHourWidth(options?.hourSize?.horizontal); setDatesRange({}); }, [options?.hourSize?.vertical, options?.hourSize?.horizontal]); useEffect(() => { if ( options && previousOptions && Object.keys(previousOptions).length > 0 && (options.mode !== previousOptions.mode || options.date !== previousOptions.date) && onChange ) { const params = {} as { startDate?: string; endDate?: string; }; const dayShift = options.isSundayFirstDay ? 1 : 0; if (options.mode === ECalendarMode.DAILY) { params.startDate = dayjs.utc(options.date).format('DD/MM/YYYY'); params.endDate = dayjs.utc(options.date).format('DD/MM/YYYY'); } else if (options.mode === ECalendarMode.WEEKLY) { params.startDate = dayjs .utc(options.date) .isoWeekday(1 - dayShift) .format('DD/MM/YYYY'); params.endDate = dayjs .utc(options.date) .isoWeekday(7 - dayShift) .format('DD/MM/YYYY'); } else if (options.mode === ECalendarMode.MONTHLY) { const firstDay = dayjs.utc(options.date).date(1); let lastDay = dayjs.utc(options.date).date(31); while (lastDay.format('MM') !== firstDay.format('MM')) lastDay = lastDay.subtract(1, 'day'); if (options?.monthlySpecificMonth) { params.startDate = firstDay.format('DD/MM/YYYY'); params.endDate = lastDay.format('DD/MM/YYYY'); } else { params.startDate = dayjs .utc(firstDay) .isoWeek(firstDay.isoWeek()) .isoWeekday(1 - dayShift) .format('DD/MM/YYYY'); params.endDate = dayjs .utc(lastDay) .isoWeek(lastDay.isoWeek()) .isoWeekday(7 - dayShift) .format('DD/MM/YYYY'); } } onChange({ type: 'NEW_MODE', data: { mode: options.mode, ...params, }, }); } }, [options, previousOptions]); const [partialRanges, setPartialRanges] = useState({ vertical: { from: 0, to: 0, }, horizontal: { from: 0, to: 0, }, }); const [datesRange, setDatesRange] = useState({}); const [isResizing, setIsResizing] = useState(false); const updateDatesRange = useCallback( (id: number, startDate: string, endDate: string, onChangeData?: any) => { if (onChange) { onChange(onChangeData); } setDatesRange({ [id]: { startDate, endDate } }); }, [onChange] ); const [draggableDatesRange, setDraggableDatesRange] = useState({}); const setDraggableRange = useCallback( (id: number, startDate: string, endDate: string) => { setDraggableDatesRange({ [id]: { startDate, endDate } }); }, [] ); const hasCollision = useCallback((event: ISlot, list: ISlot[] = []) => { if (!event) return true; if (list.length === 0 || !Array.isArray(list)) return false; const eventStartDate = dayjs.utc(event.startDate).toDate().getTime(); const eventEndDate = dayjs.utc(event.endDate).toDate().getTime(); return Boolean( list.find((item) => { if (event.id === item.id) return false; const itemStartDate = dayjs.utc(item.startDate).toDate().getTime(); const itemEndDate = dayjs.utc(item.endDate).toDate().getTime(); if (eventEndDate <= itemStartDate || eventStartDate >= itemEndDate) return false; return true; }) ); }, []); /** Update events object after change datetime */ const updateEventsToSpaceByDatesRange = useCallback( (prevSpaces: ISlotEventsToSpaces) => { let newSpaces = { ...prevSpaces }; for (let eventId in datesRange) { for (let spaceId in newSpaces) { let events = newSpaces[spaceId]; let others = events.filter((item) => item.id !== Number(eventId)); let changeItem = events.find((item) => item.id === Number(eventId)); if (changeItem) { const updatedItem = { ...changeItem, startDate: datesRange[eventId].startDate, endDate: datesRange[eventId].endDate, }; others.push(updatedItem); } newSpaces[spaceId] = others; } } return newSpaces; }, [datesRange] ); const [day, setDay] = useState(1); const incrementDay = useCallback(() => { setDay((currentDay) => { onChange?.({ type: 'SCROLL_TO_NEXT_DAY', data: { date: dayjs .utc(options?.date) .add(currentDay, 'day') .format('DD/MM/YYYY'), }, }); return currentDay + 1; }); }, [onChange]); useEffect(() => { if (options.infiniteScrolling && options.mode === ECalendarMode.DAILY) { setDay(1); incrementDay(); } else setDay(1); }, [options.mode, options.view, options.date, options.infiniteScrolling]); const getPartialEvents = useCallback( (events: ISlot[], from: number, to: number) => events.filter((event) => { const date = dayjs.utc(options?.date); const difference = dayjs.utc(event.startDate).diff(date, 'hour'); return difference >= from && difference <= to; }), [options?.date] ); /** Calculating top/left position and height/width of events for daily mode */ const getTopAndHeight = useCallback( (startDate: number, endDate: number, horizontal?: boolean) => { const hour = Number(dayjs.utc(startDate).format('HH')); const minutes = Number(dayjs.utc(startDate).format('mm')); const diff = dayjs.utc(endDate).diff(dayjs.utc(startDate), 'minutes'); const h = horizontal ? calculateMarginLeftFromMinutes(diff) : calculateMarginFromMinutes(diff); const marginTop = horizontal ? calculateMarginLeftFromHours(hour) : calculateMarginFromHours(hour); const marginTopMin = horizontal ? calculateMarginLeftFromMinutes(minutes) : calculateMarginFromMinutes(minutes); const total = marginTop + marginTopMin; const today = dayjs.utc(options?.date); const date = dayjs.utc(startDate); const difference = date.diff(today, 'hour'); const differenceAtDays = Math.abs(Math.floor(difference / 24)); const size = horizontal ? oneHourWidth : oneHourHeight; let shift = size * 24 * differenceAtDays; if (difference < 0) shift *= -1; return { height: h || 0, top: total + shift || 0 + shift }; }, [options?.date] ); /** Making events groups with collision */ const getEventsGroups = useCallback( (events: ISlot[], horizontal?: boolean): IEventsGroup[] => { let groups = [] as ISlot[][]; for (let i = 0; i < events.length; i++) { let group = [ events[i], ...events.filter((event) => hasCollision(events[i], [event])), ]; groups.push(group); } for (let i = 0; i < groups.length; i++) { for (let j = i + 1; j < groups.length; j++) { if ( groups[i] && groups[j] && groups[i].find((item) => groups[j].find((event) => item.id === event.id) ) ) { groups[i] = Array.from(new Set([...groups[i], ...groups[j]])); delete groups[j]; } } } groups = groups.filter((group) => !!group); const result: IEventsGroup[] = []; groups.forEach((events) => { if (events.length > 1) { const startDates = events.map((event) => dayjs.utc(event.startDate).toDate().getTime() ); const endDates = events.map((event) => dayjs.utc(event.endDate).toDate().getTime() ); const minStart = Math.min(...startDates); const maxEnd = Math.max(...endDates); const { top, height } = getTopAndHeight(minStart, maxEnd, horizontal); const groups = events.length > 2 ? getSubGroups(events) : [[events[0]], [events[1]]].sort((a, b) => a[0].id - b[0].id); result.push({ items: events.sort((a, b) => a.id - b.id), groups, top, height, }); } else if (events.length === 1) result.push({ items: events, item: events[0] }); }); return result; }, [getTopAndHeight] ); /** Making groups inside group of events with collision */ const getSubGroups = useCallback((events: ISlot[]) => { const groups = [] as ISlot[][]; events.forEach((event) => { let added = false; for (let i = 0; i < groups.length; i++) { if (!hasCollision(event, groups[i])) { groups[i].push(event); added = true; break; } } if (!added) groups.push([event]); }); for (let i = 0; i < groups.length; i++) { groups[i] = groups[i].sort((a, b) => a.id - b.id); } return groups.sort((a, b) => a[0].id - b[0].id); }, []); const updateEventsData = useCallback(() => { setEvents((prevEvents) => { let newEvents = { ...prevEvents }; for (let eventId in datesRange) { newEvents[eventId] = { ...newEvents[eventId], startDate: datesRange[eventId].startDate, endDate: datesRange[eventId].endDate, }; } return newEvents; }); setEventsToSpaces((prevSpaces) => updateEventsToSpaceByDatesRange(prevSpaces) ); }, [updateEventsToSpaceByDatesRange]); useEffect(() => { updateEventsData(); }, [datesRange]); return ( {children} ); }; export const useMiddlewareContext = () => useContext(MiddlewareContext);