react-scheduler/react-big-schedule

View on GitHub
src/components/SchedulerData.js

Summary

Maintainability
F
2 wks
Test Coverage
import dayjs from 'dayjs';
import quarterOfYear from 'dayjs/plugin/quarterOfYear';
import utc from 'dayjs/plugin/utc';
import weekday from 'dayjs/plugin/weekday';
import { RRuleSet, rrulestr } from 'rrule';
import { CellUnit, DATE_FORMAT, DATETIME_FORMAT, ViewType } from '../config/default';
import config from '../config/scheduler';
import behaviors from '../helper/behaviors';

export default class SchedulerData {
  constructor(date = dayjs(), viewType = ViewType.Week, showAgenda = false, isEventPerspective = false, newConfig = undefined, newBehaviors = undefined) {
    this.resources = [];
    this.events = [];
    this.eventGroups = [];
    this.eventGroupsAutoGenerated = true;
    this.viewType = viewType;
    this.cellUnit = viewType === ViewType.Day ? CellUnit.Hour : CellUnit.Day;
    this.showAgenda = showAgenda;
    this.isEventPerspective = isEventPerspective;
    this.resizing = false;
    this.scrollToSpecialDayjs = false;
    this.documentWidth = 0;
    this._shouldReloadViewType = false;

    this.calendarPopoverLocale = undefined;
    dayjs.extend(quarterOfYear);
    dayjs.extend(weekday);
    dayjs.extend(utc);
    this.localeDayjs = dayjs;
    this.config = newConfig === undefined ? config : { ...config, ...newConfig };
    this._validateMinuteStep(this.config.minuteStep);
    this.behaviors = newBehaviors === undefined ? behaviors : { ...behaviors, ...newBehaviors };
    this._resolveDate(0, date);
    this._createHeaders();
    this._createRenderData();
  }

  setSchedulerLocale(preset) {
    if (!preset) return;

    this.localeDayjs.locale(preset);
    this._shouldReloadViewType = true;
    this.setViewType(this.viewType, this.showAgenda, this.isEventPerspective);
  }

  setCalendarPopoverLocale(lang) {
    if (lang) {
      this.calendarPopoverLocale = lang;
    }
  }

  setResources(resources) {
    this._validateResource(resources);
    this.resources = Array.from(new Set(resources));
    this._createRenderData();
    this.setScrollToSpecialDayjs(true);
  }

  setEventGroupsAutoGenerated(autoGenerated) {
    this.eventGroupsAutoGenerated = autoGenerated;
  }

  // optional
  setEventGroups(eventGroups) {
    this._validateEventGroups(eventGroups);
    this.eventGroups = Array.from(new Set(eventGroups));
    this.eventGroupsAutoGenerated = false;
    this._createRenderData();
    this.setScrollToSpecialDayjs(true);
  }

  setMinuteStep(minuteStep) {
    if (this.config.minuteStep !== minuteStep) {
      this._validateMinuteStep(minuteStep);
      this.config.minuteStep = minuteStep;
      this._createHeaders();
      this._createRenderData();
    }
  }

  setBesidesWidth(besidesWidth) {
    if (besidesWidth >= 0) {
      this.config.besidesWidth = besidesWidth;
    }
  }

  getMinuteStepsInHour() {
    return 60 / this.config.minuteStep;
  }

  addResource(resource) {
    const existedResources = this.resources.filter(x => x.id === resource.id);
    if (existedResources.length === 0) {
      this.resources.push(resource);
      this._createRenderData();
    }
  }

  addEventGroup(eventGroup) {
    const existedEventGroups = this.eventGroups.filter(x => x.id === eventGroup.id);
    if (existedEventGroups.length === 0) {
      this.eventGroups.push(eventGroup);
      this._createRenderData();
    }
  }

  removeEventGroupById(eventGroupId) {
    let index = -1;
    this.eventGroups.forEach((item, idx) => {
      if (item.id === eventGroupId) index = idx;
    });
    if (index !== -1) this.eventGroups.splice(index, 1);
  }

  containsEventGroupId(eventGroupId) {
    let index = -1;
    this.eventGroups.forEach((item, idx) => {
      if (item.id === eventGroupId) index = idx;
    });
    return index !== -1;
  }

  setEvents(events) {
    this._validateEvents(events);
    this.events = Array.from(events);
    if (this.eventGroupsAutoGenerated) this._generateEventGroups();
    if (this.config.recurringEventsEnabled) this._handleRecurringEvents();

    this._createRenderData();
  }

  setScrollToSpecialDayjs(scrollToSpecialDayjs) {
    if (this.config.scrollToSpecialDayjsEnabled) this.scrollToSpecialDayjs = scrollToSpecialDayjs;
  }

  prev() {
    this._resolveDate(-1);
    this.events = [];
    this._createHeaders();
    this._createRenderData();
  }

  next() {
    this._resolveDate(1);
    this.events = [];
    this._createHeaders();
    this._createRenderData();
  }

  setDate(date = dayjs(new Date())) {
    this._resolveDate(0, date);
    this.events = [];
    this._createHeaders();
    this._createRenderData();
  }

  setViewType(viewType = ViewType.Week, showAgenda = false, isEventPerspective = false) {
    this.showAgenda = showAgenda;
    this.isEventPerspective = isEventPerspective;
    this.cellUnit = CellUnit.Day;

    if (this.viewType !== viewType || this._shouldReloadViewType) {
      let date = this.startDate;

      if (viewType === ViewType.Custom || viewType === ViewType.Custom1 || viewType === ViewType.Custom2) {
        this.viewType = viewType;
        this._resolveDate(0, date);
      } else {
        if (this.viewType < viewType) {
          if (viewType === ViewType.Week) {
            this.startDate = this.localeDayjs(new Date(date)).startOf('week');
            this.endDate = this.localeDayjs(new Date(this.startDate)).endOf('week');
          } else if (viewType === ViewType.Month) {
            this.startDate = this.localeDayjs(new Date(date)).startOf('month');
            this.endDate = this.localeDayjs(new Date(this.startDate)).endOf('month');
          } else if (viewType === ViewType.Quarter) {
            this.startDate = this.localeDayjs(new Date(date)).startOf('quarter');
            this.endDate = this.localeDayjs(new Date(this.startDate)).endOf('quarter');
          } else if (viewType === ViewType.Year) {
            this.startDate = this.localeDayjs(new Date(date)).startOf('year');
            this.endDate = this.localeDayjs(new Date(this.startDate)).endOf('year');
          }
        } else {
          const start = this.localeDayjs(new Date(this.startDate));
          const end = this.localeDayjs(new Date(this.endDate)).add(1, 'days');

          if (this.selectDate !== undefined) {
            const selectDate = this.localeDayjs(new Date(this.selectDate));
            if (selectDate >= start && selectDate < end) {
              date = this.selectDate;
            }
          }

          const now = this.localeDayjs();
          if (now >= start && now < end) {
            date = now.startOf('day');
          }

          if (viewType === ViewType.Day) {
            this.startDate = date;
            this.endDate = this.startDate;
            this.cellUnit = CellUnit.Hour;
          } else if (viewType === ViewType.Week) {
            this.startDate = this.localeDayjs(new Date(date)).startOf('week');
            this.endDate = this.localeDayjs(new Date(this.startDate)).endOf('week');
          } else if (viewType === ViewType.Month) {
            this.startDate = this.localeDayjs(new Date(date)).startOf('month');
            this.endDate = this.localeDayjs(new Date(this.startDate)).endOf('month');
          } else if (viewType === ViewType.Quarter) {
            this.startDate = this.localeDayjs(new Date(date)).startOf('quarter');
            this.endDate = this.localeDayjs(new Date(this.startDate)).endOf('quarter');
          }
        }

        this.viewType = viewType;
      }

      this._shouldReloadViewType = false;

      this.events = [];
      this._createHeaders();
      this._createRenderData();
      this.setScrollToSpecialDayjs(true);
    }
  }

  setSchedulerMaxHeight(newSchedulerMaxHeight) {
    this.config.schedulerMaxHeight = newSchedulerMaxHeight;
  }

  isSchedulerResponsive() {
    return !!this.config.schedulerWidth.endsWith && this.config.schedulerWidth.endsWith('%');
  }

  toggleExpandStatus(slotId) {
    let slotEntered = false;
    let slotIndent = -1;
    let isExpanded = false;
    const expandedMap = new Map();
    this.renderData.forEach(item => {
      if (slotEntered === false) {
        if (item.slotId === slotId && item.hasChildren) {
          slotEntered = true;

          isExpanded = !item.expanded;
          item.expanded = isExpanded;
          slotIndent = item.indent;
          expandedMap.set(item.indent, {
            expanded: item.expanded,
            render: item.render,
          });
        }
      } else if (item.indent > slotIndent) {
        const expandStatus = expandedMap.get(item.indent - 1);
        item.render = expandStatus.expanded && expandStatus.render;

        if (item.hasChildren) {
          expandedMap.set(item.indent, {
            expanded: item.expanded,
            render: item.render,
          });
        }
      } else {
        slotEntered = false;
      }
    });
  }

  isResourceViewResponsive() {
    const resourceTableWidth = this.getResourceTableConfigWidth();
    return !!resourceTableWidth.endsWith && resourceTableWidth.endsWith('%');
  }

  isContentViewResponsive() {
    const contentCellWidth = this.getContentCellConfigWidth();
    return !!contentCellWidth.endsWith && contentCellWidth.endsWith('%');
  }

  getSchedulerWidth() {
    const baseWidth = this.documentWidth - this.config.besidesWidth > 0 ? this.documentWidth - this.config.besidesWidth : 0;
    return this.isSchedulerResponsive() ? parseInt((baseWidth * Number(this.config.schedulerWidth.slice(0, -1))) / 100, 10) : this.config.schedulerWidth;
  }

  getResourceTableWidth() {
    const resourceTableConfigWidth = this.getResourceTableConfigWidth();
    const schedulerWidth = this.getSchedulerWidth();
    let resourceTableWidth = this.isResourceViewResponsive() ? parseInt((schedulerWidth * Number(resourceTableConfigWidth.slice(0, -1))) / 100, 10) : resourceTableConfigWidth;
    if (this.isSchedulerResponsive() && this.getContentTableWidth() + resourceTableWidth < schedulerWidth) resourceTableWidth = schedulerWidth - this.getContentTableWidth();
    return resourceTableWidth;
  }

  getContentCellWidth() {
    const contentCellConfigWidth = this.getContentCellConfigWidth();
    const schedulerWidth = this.getSchedulerWidth();
    return this.isContentViewResponsive() ? parseInt((schedulerWidth * Number(contentCellConfigWidth.slice(0, -1))) / 100, 10) : contentCellConfigWidth;
  }

  getContentTableWidth() {
    return this.headers.length * this.getContentCellWidth();
  }

  getScrollToSpecialDayjs() {
    if (this.config.scrollToSpecialDayjsEnabled) return this.scrollToSpecialDayjs;
    return false;
  }

  getSlots() {
    return this.isEventPerspective ? this.eventGroups : this.resources;
  }

  getSlotById(slotId) {
    const slots = this.getSlots();
    let slot;
    slots.forEach(item => {
      if (item.id === slotId) slot = item;
    });
    return slot;
  }

  getResourceById(resourceId) {
    let resource;
    this.resources.forEach(item => {
      if (item.id === resourceId) resource = item;
    });
    return resource;
  }

  getTableHeaderHeight() {
    return this.config.tableHeaderHeight;
  }

  getSchedulerContentDesiredHeight() {
    let height = 0;
    this.renderData.forEach(item => {
      if (item.render) height += item.rowHeight;
    });
    return height;
  }

  getCellMaxEvents() {
    const viewConfigMap = {
      [ViewType.Week]: 'weekMaxEvents',
      [ViewType.Day]: 'dayMaxEvents',
      [ViewType.Month]: 'monthMaxEvents',
      [ViewType.Year]: 'yearMaxEvents',
      [ViewType.Quarter]: 'quarterMaxEvents',
    };

    const configProperty = viewConfigMap[this.viewType] || 'customMaxEvents';

    return this.config[configProperty];
  }

  getCalendarPopoverLocale() {
    return this.calendarPopoverLocale;
  }

  getSelectedDate() {
    return this.selectDate.format(DATE_FORMAT);
  }

  getViewStartDate() {
    return this.startDate;
  }

  getViewEndDate() {
    return this.endDate;
  }

  getViewDates() {
    return {
      startDate: this.startDate,
      endDate: this.endDate,
    };
  }

  getDateLabel() {
    const start = this.localeDayjs(new Date(this.startDate));
    const end = this.localeDayjs(new Date(this.endDate));
    let dateLabel = start.format('LL');

    if (start !== end) dateLabel = `${start.format('LL')}-${end.format('LL')}`;

    if (this.behaviors.getDateLabelFunc) dateLabel = this.behaviors.getDateLabelFunc(this, this.viewType, this.startDate, this.endDate);

    return dateLabel;
  }

  addEvent(newEvent) {
    this._attachEvent(newEvent);
    if (this.eventGroupsAutoGenerated) this._generateEventGroups();
    this._createRenderData();
  }

  updateEventStart(event, newStart) {
    this._detachEvent(event);
    event.start = newStart;
    this._attachEvent(event);
    this._createRenderData();
  }

  updateEventEnd(event, newEnd) {
    event.end = newEnd;
    this._createRenderData();
  }

  swapEvent(eventSource, eventDest) {
    // Swap group or resource IDs
    if (this.isEventPerspective) {
      [eventSource.groupId, eventDest.groupId] = [eventDest.groupId, eventSource.groupId];
      [eventSource.groupName, eventDest.groupName] = [eventDest.groupName, eventSource.groupName];
    } else {
      [eventSource.resourceId, eventDest.resourceId] = [eventDest.resourceId, eventSource.resourceId];
    }

    // Swap start and end times
    [eventSource.start, eventDest.start] = [eventDest.start, eventSource.start];
    [eventSource.end, eventDest.end] = [eventDest.end, eventSource.end];

    // Update the events
    this._detachEvent(eventSource);
    this._detachEvent(eventDest);
    this._attachEvent(eventSource);
    this._attachEvent(eventDest);
    this._createRenderData();
  }

  swapEvent2(eventSource, eventDest) {
    const tempEventSource = { ...eventSource };
    const tempEventDest = { ...eventDest };
    this._detachEvent(eventSource);
    this._detachEvent(eventDest);
    if (this.isEventPerspective) {
      tempEventSource.groupId = eventDest.groupId;
      tempEventSource.groupName = eventDest.groupName;
      tempEventDest.groupId = eventSource.groupId;
      tempEventDest.groupName = eventSource.groupName;
    } else {
      tempEventSource.resourceId = eventDest.resourceId;
      tempEventDest.resourceId = eventSource.resourceId;
    }
    tempEventSource.end = eventDest.end;
    tempEventSource.start = eventDest.start;
    tempEventDest.end = eventSource.end;
    tempEventDest.start = eventSource.start;
    this._attachEvent(tempEventSource);
    this._attachEvent(tempEventDest);
    this._createRenderData();
  }

  moveEvent(event, newSlotId, newSlotName, newStart, newEnd) {
    this._detachEvent(event);
    if (this.isEventPerspective) {
      event.groupId = newSlotId;
      event.groupName = newSlotName;
    } else event.resourceId = newSlotId;
    event.end = newEnd;
    event.start = newStart;
    this._attachEvent(event);
    this._createRenderData();
  }

  isEventInTimeWindow(eventStart, eventEnd, windowStart, windowEnd) {
    return eventStart < windowEnd && eventEnd > windowStart;
  }

  removeEvent(event) {
    const index = this.events.indexOf(event);
    if (index !== -1) {
      this.events.splice(index, 1);
      this._createRenderData();
    }
  }

  removeEventById(eventId) {
    let index = -1;
    this.events.forEach((item, idx) => {
      if (item.id === eventId) index = idx;
    });
    if (index !== -1) {
      this.events.splice(index, 1);
      this._createRenderData();
    }
  }

  getResourceTableConfigWidth() {
    if (this.showAgenda) {
      return this.config.agendaResourceTableWidth;
    }

    const viewConfigMap = {
      [ViewType.Week]: 'weekResourceTableWidth',
      [ViewType.Day]: 'dayResourceTableWidth',
      [ViewType.Month]: 'monthResourceTableWidth',
      [ViewType.Year]: 'yearResourceTableWidth',
      [ViewType.Quarter]: 'quarterResourceTableWidth',
    };

    const configProperty = viewConfigMap[this.viewType] || 'customResourceTableWidth';

    return this.config[configProperty];
  }

  getContentCellConfigWidth() {
    const viewConfigMap = {
      [ViewType.Week]: 'weekCellWidth',
      [ViewType.Day]: 'dayCellWidth',
      [ViewType.Month]: 'monthCellWidth',
      [ViewType.Year]: 'yearCellWidth',
      [ViewType.Quarter]: 'quarterCellWidth',
    };

    const configProperty = viewConfigMap[this.viewType] || 'customCellWidth';

    return this.config[configProperty];
  }

  _setDocumentWidth(documentWidth) {
    if (documentWidth >= 0) {
      this.documentWidth = documentWidth;
    }
  }

  _detachEvent(event) {
    const index = this.events.indexOf(event);
    if (index !== -1) this.events.splice(index, 1);
  }

  _attachEvent(event) {
    let pos = 0;
    const eventStart = this.localeDayjs(new Date(event.start));
    this.events.forEach((item, index) => {
      const start = this.localeDayjs(new Date(item.start));
      if (eventStart >= start) pos = index + 1;
    });
    this.events.splice(pos, 0, event);
  }

  _handleRecurringEvents() {
    const recurringEvents = this.events.filter(x => !!x.rrule);
    recurringEvents.forEach(item => {
      this._detachEvent(item);
    });

    recurringEvents.forEach(item => {
      const windowStart = this.startDate;
      const windowEnd = this.endDate.add(1, 'days');
      const oldStart = this.localeDayjs(new Date(item.start));
      const oldEnd = this.localeDayjs(new Date(item.end));
      let rule = rrulestr(item.rrule);
      let oldDtstart;
      const oldUntil = rule.origOptions.until || windowEnd.toDate();
      if (rule.origOptions.dtstart) {
        oldDtstart = this.localeDayjs(new Date(rule.origOptions.dtstart));
      }
      // rule.origOptions.dtstart = oldStart.toDate();
      if (windowEnd < oldUntil) {
        rule.origOptions.until = windowEnd.toDate();
      }

      // reload
      rule = rrulestr(rule.toString());
      if (item.exdates || item.exrule) {
        const rruleSet = new RRuleSet();
        rruleSet.rrule(rule);
        if (item.exrule) {
          rruleSet.exrule(rrulestr(item.exrule));
        }
        if (item.exdates) {
          item.exdates.forEach(exdate => {
            rruleSet.exdate(this.localeDayjs(exdate).toDate());
          });
        }
        rule = rruleSet;
      }

      const all = rule.between(new Date(windowStart), new Date(windowEnd));
      all.forEach((time, index) => {
        const newEvent = {
          ...item,
          recurringEventId: item.id,
          recurringEventStart: item.start,
          recurringEventEnd: item.end,
          id: `${item.id}-${index}`,
          start: rule.origOptions.tzid
            ? this.localeDayjs.utc(time).utcOffset(this.localeDayjs(new Date()).utcOffset(), true).format(DATETIME_FORMAT)
            : this.localeDayjs(new Date(time)).format(DATETIME_FORMAT),
          end: rule.origOptions.tzid
            ? this.localeDayjs
              .utc(time)
              .utcOffset(this.localeDayjs(new Date()).utcOffset(), true)
              .add(oldEnd.diff(oldStart), 'ms')
              .add(this.localeDayjs(new Date(oldUntil)).utcOffset() - this.localeDayjs(new Date(item.start)).utcOffset(), 'm')
              .format(DATETIME_FORMAT)
            : this.localeDayjs(new Date(time)).add(oldEnd.diff(oldStart), 'ms').format(DATETIME_FORMAT),
        };

        const eventStart = this.localeDayjs(newEvent.start);
        const eventEnd = this.localeDayjs(newEvent.end);
        if (this.isEventInTimeWindow(eventStart, eventEnd, windowStart, windowEnd) && (!oldDtstart || eventStart >= oldDtstart)) {
          this._attachEvent(newEvent);
        }
      });
    });
  }

  _resolveDate(num, date = undefined) {
    if (date !== undefined) {
      this.selectDate = this.localeDayjs(date);
    }

    const setStartAndEndDates = unit => {
      this.startDate = date !== undefined ? this.selectDate.startOf(unit) : this.startDate.add(num, `${unit}s`);
      this.endDate = this.startDate.endOf(unit);
    };

    switch (this.viewType) {
      case ViewType.Week:
        setStartAndEndDates('week');
        break;

      case ViewType.Day:
        this.startDate = date !== undefined ? this.selectDate : this.startDate.add(num, 'days');
        this.endDate = this.startDate;
        break;

      case ViewType.Month:
        setStartAndEndDates('month');
        break;

      case ViewType.Quarter:
        setStartAndEndDates('quarter');
        break;

      case ViewType.Year:
        setStartAndEndDates('year');
        break;

      case ViewType.Custom:
      case ViewType.Custom1:
      case ViewType.Custom2:
        if (this.behaviors.getCustomDateFunc !== undefined) {
          const customDate = this.behaviors.getCustomDateFunc(this, num, date);
          this.startDate = this.localeDayjs(customDate.startDate);
          this.endDate = this.localeDayjs(customDate.endDate);
          if (customDate.cellUnit) {
            this.cellUnit = customDate.cellUnit;
          }
        } else {
          throw new Error('This is a custom view type, set behaviors.getCustomDateFunc func to resolve the time window (startDate and endDate) yourself');
        }
        break;

      default:
        break;
    }
  }

  // Previous Code
  _createHeaders() {
    const headers = [];
    let start = this.localeDayjs(new Date(this.startDate));
    let end = this.localeDayjs(new Date(this.endDate));
    let header = start;

    if (this.showAgenda) {
      headers.push({ time: header.format(DATETIME_FORMAT), nonWorkingTime: false });
    } else if (this.cellUnit === CellUnit.Hour) {
      if (start.hour() === 0) {
        start = start.add(this.config.dayStartFrom, 'hours');
      }
      if (end.hour() === 0) {
        end = end.add(this.config.dayStopTo, 'hours');
      }
      header = start;

      let prevHour = -1;
      while (header >= start && header <= end) {
        // prevent doubled hours on time change
        if (header.hour() === prevHour) {
          header = header.add(1, 'hours');
          // eslint-disable-next-line no-continue
          continue;
        }
        prevHour = header.hour();
        const minuteSteps = this.getMinuteStepsInHour();
        for (let i = 0; i < minuteSteps; i += 1) {
          const hour = header.hour();
          if (hour >= this.config.dayStartFrom && hour <= this.config.dayStopTo) {
            const time = header.format(DATETIME_FORMAT);
            const nonWorkingTime = this.behaviors.isNonWorkingTimeFunc(this, time);
            headers.push({ time, nonWorkingTime });
          }

          header = header.add(this.config.minuteStep, 'minutes');
        }
      }
    } else if (this.cellUnit === CellUnit.Day) {
      while (header >= start && header <= end) {
        const time = header.format(DATETIME_FORMAT);
        const dayOfWeek = header.day();
        if (this.config.displayWeekend || (dayOfWeek !== 0 && dayOfWeek !== 6)) {
          const nonWorkingTime = this.behaviors.isNonWorkingTimeFunc(this, time);
          headers.push({ time, nonWorkingTime });
        }

        header = header.add(1, 'days');
      }
    } else if (this.cellUnit === CellUnit.Week) {
      while (header >= start && header <= end) {
        const time = header.format(DATE_FORMAT);
        headers.push({ time });
        header = header.add(1, 'weeks').startOf('week');
      }
    } else if (this.cellUnit === CellUnit.Month) {
      while (header >= start && header <= end) {
        const time = header.format(DATE_FORMAT);
        headers.push({ time });
        header = header.add(1, 'months').startOf('month');
      }
    } else if (this.cellUnit === CellUnit.Year) {
      while (header >= start && header <= end) {
        const time = header.format(DATE_FORMAT);
        headers.push({ time });
        header = header.add(1, 'years').startOf('year');
      }
    }

    this.headers = headers;
  }

  // Fix Optimited code
  // _createHeaders() {
  //   const headers = [];
  //   const start = this.localeDayjs(new Date(this.startDate));
  //   const end = this.localeDayjs(new Date(this.endDate));

  //   const processHeader = (header, format, unit, incrementFn) => {
  //     let head = header;
  //     while (head >= start && head <= end) {
  //       const time = head.format(format);
  //       if (unit === CellUnit.Day) {
  //         const dayOfWeek = head.weekday();
  //         if (this.config.displayWeekend || (dayOfWeek !== 0 && dayOfWeek !== 6)) {
  //           const nonWorkingTime = this.behaviors.isNonWorkingTimeFunc(this, time);
  //           headers.push({ time, nonWorkingTime });
  //         }
  //       } else {
  //         headers.push({ time });
  //       }
  //       head = head.add(1, incrementFn);
  //     }
  //   };

  //   if (this.showAgenda) {
  //     headers.push({ time: start.format(DATETIME_FORMAT), nonWorkingTime: false });
  //   } else if (this.cellUnit === CellUnit.Hour) {
  //     const hourIncrement = this.config.minuteStep < 60 ? 'minutes' : 'hours';
  //     const minuteSteps = this.getMinuteStepsInHour();
  //     let header = start.hour() === 0 ? start.add(this.config.dayStartFrom, 'hours') : start;
  //     while (header <= end) {
  //       const hour = header.hour();
  //       if (hour >= this.config.dayStartFrom && hour <= this.config.dayStopTo) {
  //         const time = header.format(DATETIME_FORMAT);
  //         const nonWorkingTime = this.behaviors.isNonWorkingTimeFunc(this, time);
  //         headers.push({ time, nonWorkingTime });
  //       }
  //       header = header.add(minuteSteps, hourIncrement);
  //     }
  //   } else {
  //     const header = start;
  //     const format = this.cellUnit === CellUnit.Day ? DATETIME_FORMAT : DATE_FORMAT;
  //     const incrementFn = this.cellUnit === CellUnit.Day ? 'days' : `${this.cellUnit}s`;
  //     processHeader(header, format, this.cellUnit, incrementFn);
  //   }

  //   this.headers = headers;
  // }

  _createInitHeaderEvents(header) {
    const start = this.localeDayjs(new Date(header.time));
    const startValue = start.format(DATETIME_FORMAT);

    let endValue;
    if (this.showAgenda) {
      const incrementUnit = {
        [ViewType.Day]: 'days',
        [ViewType.Week]: 'weeks',
        [ViewType.Month]: 'months',
        [ViewType.Year]: 'years',
        [ViewType.Quarter]: 'quarters',
      }[this.viewType] || 'days';

      if (incrementUnit === 'days') {
        endValue = this.localeDayjs(new Date(this.endDate)).add(1, 'days').format(DATETIME_FORMAT);
      } else {
        endValue = start.add(1, incrementUnit).format(DATETIME_FORMAT);
      }
    } else {
      const incrementUnit = {
        [CellUnit.Hour]: 'minutes',
        [CellUnit.Week]: 'weeks',
        [CellUnit.Month]: 'months',
        [CellUnit.Year]: 'years',
      }[this.cellUnit] || 'days';

      endValue = start
        .add(incrementUnit === 'minutes' ? this.config.minuteStep : 1, incrementUnit)
        .format(this.cellUnit === CellUnit.Year || this.cellUnit === CellUnit.Month || this.cellUnit === CellUnit.Week ? DATE_FORMAT : DATETIME_FORMAT);
    }

    return {
      time: header.time,
      nonWorkingTime: header.nonWorkingTime,
      start: startValue,
      end: endValue,
      count: 0,
      addMore: 0,
      addMoreIndex: 0,
      events: Array(3),
    };
  }

  _createHeaderEvent(render, span, eventItem) {
    return { render, span, eventItem };
  }

  _getEventSlotId(event) {
    return this.isEventPerspective ? this._getEventGroupId(event) : event.resourceId;
  }

  _getEventGroupId(event) {
    return event.groupId ? event.groupId.toString() : event.id.toString();
  }

  _getEventGroupName(event) {
    return event.groupName ? event.groupName : event.title;
  }

  _generateEventGroups() {
    const eventGroups = [];
    const set = new Set();
    this.events.forEach(item => {
      const groupId = this._getEventGroupId(item);
      const groupName = this._getEventGroupName(item);

      if (!set.has(groupId)) {
        eventGroups.push({
          id: groupId,
          name: groupName,
          state: item,
        });
        set.add(groupId);
      }
    });
    this.eventGroups = eventGroups;
  }

  _createInitRenderData(isEventPerspective, eventGroups, resources, headers) {
    const slots = isEventPerspective ? eventGroups : resources;
    const slotTree = [];
    const slotMap = new Map();
    slots.forEach(slot => {
      const headerEvents = headers.map(header => this._createInitHeaderEvents(header));

      const slotRenderData = {
        slotId: slot.id,
        slotName: slot.name,
        slotTitle: slot.title,
        parentId: slot.parentId,
        groupOnly: slot.groupOnly,
        hasSummary: false,
        rowMaxCount: 0,
        rowHeight: this.config.nonAgendaSlotMinHeight !== 0 ? this.config.nonAgendaSlotMinHeight : this.config.eventItemLineHeight + 2,
        headerItems: headerEvents,
        indent: 0,
        hasChildren: false,
        expanded: true,
        render: true,
      };
      const { id } = slot;
      let value;
      if (slotMap.has(id)) {
        value = slotMap.get(id);
        value.data = slotRenderData;
      } else {
        value = {
          data: slotRenderData,
          children: [],
        };
        slotMap.set(id, value);
      }

      const { parentId } = slot;
      if (!parentId || parentId === id) {
        slotTree.push(value);
      } else {
        let parentValue;
        if (slotMap.has(parentId)) {
          parentValue = slotMap.get(parentId);
        } else {
          parentValue = {
            data: undefined,
            children: [],
          };
          slotMap.set(parentId, parentValue);
        }

        parentValue.children.push(value);
      }
    });

    const slotStack = [];
    let i;
    for (i = slotTree.length - 1; i >= 0; i -= 1) {
      slotStack.push(slotTree[i]);
    }
    const initRenderData = [];
    let currentNode;
    while (slotStack.length > 0) {
      currentNode = slotStack.pop();
      if (currentNode.data.indent > 0) {
        currentNode.data.render = this.config.defaultExpanded;
      }
      if (currentNode.children.length > 0) {
        currentNode.data.hasChildren = true;
        currentNode.data.expanded = this.config.defaultExpanded;
      }
      initRenderData.push(currentNode.data);

      for (i = currentNode.children.length - 1; i >= 0; i -= 1) {
        currentNode.children[i].data.indent = currentNode.data.indent + 1;
        slotStack.push(currentNode.children[i]);
      }
    }

    return initRenderData;
  }

  _getSpan(startTime, endTime, headers) {
    if (this.showAgenda) return 1;

    // function startOfWeek(date) {
    //   const day = date.getDay();
    //   const diff = date.getDate() - day;
    //   return new Date(date.getFullYear(), date.getMonth(), diff);
    // }

    const timeBetween = (date1, date2, timeIn) => {
      if (timeIn === 'days' || timeIn === 'day') {
        if (date1.getDate() === date2.getDate() && date1.getMonth() === date2.getMonth()) {
          return 1;
        }
      }

      let one;
      switch (timeIn) {
        case 'days':
        case 'day':
          one = 1000 * 60 * 60 * 24;
          break;
        case 'minutes':
        case 'minute':
          one = 1000 * 60;
          break;
        default:
          return 0;
      }

      const date1Ms = date1.getTime();
      const date2Ms = date2.getTime();

      const diff = (date2Ms - date1Ms) / one;
      return diff < 0 ? 0 : diff;
    };

    const eventStart = new Date(startTime);
    const eventEnd = new Date(endTime);
    let span = 0;
    const windowStart = new Date(this.startDate);
    const windowEnd = new Date(this.endDate);

    windowStart.setHours(0, 0, 0, 0);
    windowEnd.setHours(23, 59, 59);

    if (this.viewType === ViewType.Day) {
      if (headers.length > 0) {
        const day = new Date(headers[0].time);
        if (day.getDate() > eventStart.getDate() && day.getDate() < eventEnd.getDate()) {
          span = 1440 / this.config.minuteStep;
        } else if (day.getDate() > eventStart.getDate() && day.getDate() === eventEnd.getDate()) {
          span = Math.ceil(timeBetween(day, eventEnd, 'minutes') / this.config.minuteStep);
        } else if (day.getDate() === eventStart.getDate() && day.getDate() < eventEnd.getDate()) {
          day.setHours(23, 59, 59);
          span = Math.ceil(timeBetween(eventStart, day, 'minutes') / this.config.minuteStep);
        } else if ((day.getDate() === eventStart.getDate() && day.getDate() === eventEnd.getDate()) || eventEnd.getDate() === eventStart.getDate()) {
          span = Math.ceil(timeBetween(eventStart, eventEnd, 'minutes') / this.config.minuteStep);
        }
      }
    } else if (this.viewType === ViewType.Week || this.viewType === ViewType.Month || this.viewType === ViewType.Quarter || this.viewType === ViewType.Year) {
      const startDate = windowStart < eventStart ? eventStart : windowStart;
      const endDate = windowEnd > eventEnd ? eventEnd : windowEnd;
      span = Math.ceil(timeBetween(startDate, endDate, 'days'));
    } else {
      if (this.cellUnit === CellUnit.Day) {
        eventEnd.setHours(23, 59, 59);
        eventStart.setHours(0, 0, 0, 0);
      }

      const timeIn = this.cellUnit === CellUnit.Day ? 'days' : 'minutes';
      const dividedBy = this.cellUnit === CellUnit.Day ? 1 : this.config.minuteStep;

      if (windowStart >= eventStart && eventEnd <= windowEnd) {
        span = Math.ceil(timeBetween(windowStart, eventEnd, timeIn) / dividedBy);
      } else if (windowStart > eventStart && eventEnd > windowEnd) {
        span = Math.ceil(timeBetween(windowStart, windowEnd, timeIn) / dividedBy);
      } else if (windowStart <= eventStart && eventEnd >= windowEnd) {
        span = Math.ceil(timeBetween(eventStart, windowEnd, timeIn) / dividedBy);
      } else {
        span = Math.ceil(timeBetween(eventStart, eventEnd, timeIn) / dividedBy);
      }
    }

    return span;
  }

  _validateResource(resources) {
    if (Object.prototype.toString.call(resources) !== '[object Array]') {
      throw new Error('Resources should be Array object');
    }

    resources.forEach((item, index) => {
      if (item === undefined) {
        console.error(`Resource undefined: ${index}`);
        throw new Error(`Resource undefined: ${index}`);
      }
      if (item.id === undefined || item.name === undefined) {
        console.error('Resource property missed', index, item);
        throw new Error(`Resource property undefined: ${index}`);
      }
    });
  }

  _validateEventGroups(eventGroups) {
    if (Object.prototype.toString.call(eventGroups) !== '[object Array]') {
      throw new Error('Event groups should be Array object');
    }

    eventGroups.forEach((item, index) => {
      if (item === undefined) {
        console.error(`Event group undefined: ${index}`);
        throw new Error(`Event group undefined: ${index}`);
      }
      if (item.id === undefined || item.name === undefined) {
        console.error('Event group property missed', index, item);
        throw new Error(`Event group property undefined: ${index}`);
      }
    });
  }

  _validateEvents(events) {
    if (Object.prototype.toString.call(events) !== '[object Array]') {
      throw new Error('Events should be Array object');
    }

    events.forEach((e, index) => {
      if (e === undefined) {
        console.error(`Event undefined: ${index}`);
        throw new Error(`Event undefined: ${index}`);
      }
      if (e.id === undefined || e.resourceId === undefined || e.title === undefined || e.start === undefined || e.end === undefined) {
        console.error('Event property missed', index, e);
        throw new Error(`Event property undefined: ${index}`);
      }
    });
  }

  _validateMinuteStep(minuteStep) {
    if (60 % minuteStep !== 0) {
      console.error('Minute step is not set properly - 60 minutes must be divisible without remainder by this number');
      throw new Error('Minute step is not set properly - 60 minutes must be divisible without remainder by this number');
    }
  }

  _compare(event1, event2) {
    const start1 = this.localeDayjs(event1.start);
    const start2 = this.localeDayjs(event2.start);
    if (start1 !== start2) return start1 < start2 ? -1 : 1;

    const end1 = this.localeDayjs(event1.end);
    const end2 = this.localeDayjs(event2.end);
    if (end1 !== end2) return end1 < end2 ? -1 : 1;

    return event1.id < event2.id ? -1 : 1;
  }

  _createRenderData() {
    const initRenderData = this._createInitRenderData(this.isEventPerspective, this.eventGroups, this.resources, this.headers);
    // this.events.sort(this._compare);
    const cellMaxEventsCount = this.getCellMaxEvents();
    const cellMaxEventsCountValue = 30;

    this.events.forEach(item => {
      const resourceEventsList = initRenderData.filter(x => x.slotId === this._getEventSlotId(item));
      if (resourceEventsList.length > 0) {
        const resourceEvents = resourceEventsList[0];
        const span = this._getSpan(item.start, item.end, this.headers);
        const eventStart = new Date(item.start);
        const eventEnd = new Date(item.end);
        let pos = -1;

        resourceEvents.headerItems.forEach((header, index) => {
          const headerStart = new Date(header.start);
          const headerEnd = new Date(header.end);
          if (headerEnd > eventStart && headerStart < eventEnd) {
            header.count += 1;
            if (header.count > resourceEvents.rowMaxCount) {
              resourceEvents.rowMaxCount = header.count;
              const rowsCount = cellMaxEventsCount <= cellMaxEventsCountValue && resourceEvents.rowMaxCount > cellMaxEventsCount ? cellMaxEventsCount : resourceEvents.rowMaxCount;
              const newRowHeight = rowsCount * this.config.eventItemLineHeight + (this.config.creatable && this.config.checkConflict === false ? 20 : 2);
              if (newRowHeight > resourceEvents.rowHeight) resourceEvents.rowHeight = newRowHeight;
            }

            if (pos === -1) {
              let tmp = 0;
              while (header.events[tmp] !== undefined) tmp += 1;

              pos = tmp;
            }
            let render = headerStart <= eventStart || index === 0;
            if (render === false) {
              const previousHeader = resourceEvents.headerItems[index - 1];
              const previousHeaderStart = new Date(previousHeader.start);
              const previousHeaderEnd = new Date(previousHeader.end);
              if (previousHeaderEnd <= eventStart || previousHeaderStart >= eventEnd) render = true;
            }
            // console.log(`span: ${span}`)
            header.events[pos] = this._createHeaderEvent(render, span, item);
          }
        });
      }
    });

    if (cellMaxEventsCount <= cellMaxEventsCountValue || this.behaviors.getSummaryFunc !== undefined) {
      initRenderData.forEach(resourceEvents => {
        let hasSummary = false;

        resourceEvents.headerItems.forEach(headerItem => {
          if (cellMaxEventsCount <= cellMaxEventsCountValue) {
            let renderItemsCount = 0;
            let addMoreIndex = 0;
            let index = 0;
            while (index < cellMaxEventsCount - 1) {
              if (headerItem.events[index] !== undefined) {
                renderItemsCount += 1;
                addMoreIndex = index + 1;
              }

              index += 1;
            }

            if (headerItem.events[index] !== undefined) {
              if (renderItemsCount + 1 < headerItem.count) {
                headerItem.addMore = headerItem.count - renderItemsCount;
                headerItem.addMoreIndex = addMoreIndex;
              }
            } else if (renderItemsCount < headerItem.count) {
              headerItem.addMore = headerItem.count - renderItemsCount;
              headerItem.addMoreIndex = addMoreIndex;
            }
          }

          if (this.behaviors.getSummaryFunc !== undefined) {
            const events = [];
            headerItem.events.forEach(e => {
              if (!!e && !!e.eventItem) events.push(e.eventItem);
            });

            headerItem.summary = this.behaviors.getSummaryFunc(this, events, resourceEvents.slotId, resourceEvents.slotName, headerItem.start, headerItem.end);
            if (!!headerItem.summary && headerItem.summary.text !== undefined) hasSummary = true;
          }
        });

        resourceEvents.hasSummary = hasSummary;
        if (hasSummary) {
          const rowsCount = cellMaxEventsCount <= cellMaxEventsCountValue && resourceEvents.rowMaxCount > cellMaxEventsCount ? cellMaxEventsCount : resourceEvents.rowMaxCount;
          const newRowHeight = (rowsCount + 1) * this.config.eventItemLineHeight + (this.config.creatable && this.config.checkConflict === false ? 20 : 2);
          if (newRowHeight > resourceEvents.rowHeight) resourceEvents.rowHeight = newRowHeight;
        }
      });
    }

    this.renderData = initRenderData;
  }

  _startResizing() {
    this.resizing = true;
  }

  _stopResizing() {
    this.resizing = false;
  }

  _isResizing() {
    return this.resizing;
  }
}