rubycentral/cfp-app

View on GitHub
app/javascript/components/Schedule.js

Summary

Maintainability
C
1 day
Test Coverage
import React, { Component } from 'react';
import PropTypes from 'prop-types';

import { Nav } from './Schedule/Nav';
import { Ruler } from './Schedule/Ruler';
import { DayView } from './Schedule/DayView';
import { UnscheduledArea } from './Schedule/UnscheduledArea';
import { GenerateGridButton } from './Schedule/GenerateGridButton';
import { BulkCreateModal } from './Schedule/BulkCreateModal';
import { BulkGenerateConfirm } from './Schedule/BulkGenerateConfirm';
import { Alert } from './Schedule/Alert';

import { postBulkTimeSlots } from '../apiCalls';

class Schedule extends Component {
  constructor(props) {
    super(props);
    this.state = {
      dayViewing: 1,
      startTime: 10,
      endTime: 17,
      counts: {},
      draggedSession: null,
      slots: [],
      rooms: [],
      sessionFormats: [],
      bulkTimeSlotModalOpen: false,
      previewSlots: [],
      bulkTimeSlotModalEditState: null,
      errorMessages: [],
      bulkErrorMessages: [],
    };
  }

  changeDayView = (day) => {
    this.setState({ dayViewing: day });
  };

  ripTime = (time) => {
    const hours = parseInt(time.split('T')[1].split(':')[0]);
    const minutes = parseInt(time.split(':')[1]) / 60;
    return hours + minutes;
  };

  determineHours = (slots) => {
    let hours = {
      startTime: 12,
      endTime: 12,
    };

    slots.forEach((slot) => {
      if (this.ripTime(slot.start_time) < hours.startTime) {
        hours.startTime = this.ripTime(slot.start_time);
      }
      if (this.ripTime(slot.end_time) > hours.endTime) {
        hours.endTime = this.ripTime(slot.end_time);
      }
    });
    return hours;
  };

  changeDragged = (programSession) => {
    this.setState({ draggedSession: programSession });
  };

  handleMoveSessionResponse = (sessions, unscheduledSessions, slots, session) => {
    if (session) {
      slots.forEach((slot) => {
        if (slot.id === session.slot.id) {
          slot.program_session_id = null;
        }
      });
    }

    this.setState({ sessions, unscheduledSessions, slots });
  };

  openBulkTimeSlotModal = () => {
    this.setState({
      bulkTimeSlotModalOpen: true,
      previewSlots: [],
    });
  };

  closeBulkTimeSlotModal = (e) => {
    e.preventDefault();
    this.setState({
      previewSlots: [],
      bulkTimeSlotModalEditState: null,
      bulkTimeSlotModalOpen: false,
    });
  };

  cancelBulkPreview = () => {
    let hours = this.determineHours(this.state.slots);
    this.setState({
      previewSlots: [],
      bulkTimeSlotModalEditState: null,
      ...hours,
    });
  };

  findTimeSlotConflicts = (previewSlots) => {
    const { slots } = this.state;
    let conflicts = [];

    previewSlots.forEach((ps) => {
      slots.forEach((s) => {
        let sameDaySameRoom = s.room_id === parseInt(ps.room) && s.conference_day === ps.day;

        if (sameDaySameRoom) {
          let timeConflict = this.determineTimeConflict(ps, this.ripTime(s.start_time), this.ripTime(s.end_time));
          if (timeConflict) {
            conflicts.push(s);
          }
        }
      });

      previewSlots.forEach((preview) => {
        if (Object.is(ps, preview)) {
          return;
        }
        let sameDaySameRoom = parseInt(preview.room) === parseInt(ps.room) && preview.day === ps.day;

        if (sameDaySameRoom) {
          let timeConflict = this.determineTimeConflict(ps, preview.startTime, preview.endTime);

          if (timeConflict) {
            conflicts.push(Object.assign(preview, { previewConflict: true }));
          }
        }
      });
    });

    return conflicts;
  };

  determineTimeConflict = (previewSlot, compareStartTime, compareEndTime) => {
    return (
      (previewSlot.startTime > compareStartTime && previewSlot.startTime < compareEndTime) ||
      (previewSlot.endTime > compareStartTime && previewSlot.startTime < compareEndTime)
    );
  };

  handleConflicts = (conflicts, bulkTimeSlotModalEditState) => {
    const { rooms } = this.state;

    let errorMessages = [];

    conflicts.forEach((c) => {
      let message;

      if (c.previewConflict) {
        message = `You attempted to make two new time slots that overlap. The overlap occurs on Day ${c.day} at the ${
          rooms.find((r) => r.id == parseInt(c.room)).name
        } location.`;
      } else {
        message = `You attempted to preview a slot which overlaps an existing slot. The overlap involves a previously existing slot on Day ${
          c.conference_day
        } at the ${rooms.find((r) => r.id == c.room_id).name} location, between  ${
          c.start_time.split('T')[1].split('.')[0]
        } and ${c.end_time.split('T')[1].split('.')[0]}`;
      }

      errorMessages.push(message);
    });

    errorMessages = [...new Set(errorMessages)];
    this.setState({
      errorMessages,
      bulkTimeSlotModalEditState,
      bulkTimeSlotModalOpen: false,
    });
  };

  createTimeSlotPreviews = (previewSlots, bulkTimeSlotModalEditState) => {
    let { startTime, endTime } = this.state;

    previewSlots.forEach((preview) => {
      if (preview.startTime < startTime) {
        startTime = preview.startTime;
      }
      if (preview.endTime > endTime) {
        endTime = preview.endTime;
      }
    });

    let conflicts = this.findTimeSlotConflicts(previewSlots);

    if (conflicts.length > 0) {
      this.handleConflicts(conflicts, bulkTimeSlotModalEditState);
    } else {
      this.setState({
        previewSlots,
        bulkTimeSlotModalEditState,
        bulkTimeSlotModalOpen: false,
        dayViewing: parseInt(bulkTimeSlotModalEditState.day),
        startTime,
        endTime,
      });
    }
  };

  requestBulkTimeSlotCreate = () => {
    const { bulkTimeSlotModalEditState, bulkPath } = this.state;
    const { day, duration, rooms, startTimes } = bulkTimeSlotModalEditState;

    // the API expects time strings to have a minutes declaration, this following code adds a minute decaration to each time in a string, if needed.
    const formattedTimes = startTimes
      .replace(/\s/g, '')
      .split(',')
      .map((time) => {
        if (time.split(':').length > 1) {
          return time;
        } else {
          return time + ':00';
        }
      })
      .join(', ');

    postBulkTimeSlots(bulkPath, day, rooms, duration, formattedTimes)
      .then((response) => response.json())
      .then((data) => {
        const { errors } = data;

        if (errors) {
          this.setState({ bulkErrorMessages: errors });
          return;
        }

        this.setState(
          {
            slots: data.slots,
            previewSlots: [],
            bulkTimeSlotModalEditState: null,
          },
          () => {
            let hours = this.determineHours(this.state.slots);
            this.setState({ ...hours });
          }
        );
      })
      .catch((err) => console.log('Error: ', err));
  };

  componentDidMount() {
    let hours = this.determineHours(this.props.slots);
    const trackColors = palette('tol-rainbow', this.props.tracks.length);
    this.props.tracks.forEach((track, i) => {
      track.color = '#' + trackColors[i];
    });

    this.setState(Object.assign(this.state, this.props, hours));
  }

  showErrors = (messages) => {
    this.setState({ errorMessages: messages });
  };

  removeErrors = () => {
    this.setState({ errorMessages: [], bulkErrorMessages: [] });
  };

  render() {
    const {
      counts,
      dayViewing,
      startTime,
      endTime,
      sessions,
      unscheduledSessions,
      draggedSession,
      tracks,
      bulkTimeSlotModalOpen,
      bulkTimeSlotModalEditState,
      previewSlots,
      slots,
      rooms,
      sessionFormats,
      errorMessages,
      bulkErrorMessages,
    } = this.state;

    const headers = rooms.map((room) => (
      <div className="schedule_column_head" key={'column_head_' + room.name}>
        {room.name}
      </div>
    ));

    const headersMinWidth = 180 * rooms.length + 'px';

    const bulkTimeSlotModal = bulkTimeSlotModalOpen && (
      <BulkCreateModal
        closeBulkTimeSlotModal={this.closeBulkTimeSlotModal}
        dayViewing={dayViewing}
        counts={counts}
        rooms={rooms}
        createTimeSlotPreviews={this.createTimeSlotPreviews}
        editState={bulkTimeSlotModalEditState}
        sessionFormats={sessionFormats}
      />
    );

    return (
      <div className="schedule_grid">
        {errorMessages.length > 0 && <Alert messages={errorMessages} onClose={this.removeErrors} />}
        {bulkErrorMessages.length > 0 && <Alert messages={bulkErrorMessages} onClose={this.removeErrors} />}
        {bulkTimeSlotModal}
        <div className="nav_wrapper">
          <Nav counts={counts} changeDayView={this.changeDayView} dayViewing={dayViewing} slots={slots} />
          <GenerateGridButton
            dayViewing={dayViewing}
            generateGridPath={this.props.bulk_path}
            openBulkTimeSlotModal={this.openBulkTimeSlotModal}
          />
        </div>
        <div className="grid_headers_wrapper" style={{ minWidth: headersMinWidth }}>
          {headers}
        </div>
        <div className="grid_container">
          {previewSlots.length > 0 && (
            <BulkGenerateConfirm
              cancelBulkPreview={this.cancelBulkPreview}
              openBulkTimeSlotModal={this.openBulkTimeSlotModal}
              requestBulkTimeSlotCreate={this.requestBulkTimeSlotCreate}
            />
          )}
          <Ruler startTime={startTime} endTime={endTime} />
          <DayView
            dayViewing={dayViewing}
            startTime={startTime}
            endTime={endTime}
            ripTime={this.ripTime}
            changeDragged={this.changeDragged}
            draggedSession={draggedSession}
            sessions={sessions}
            tracks={tracks}
            previewSlots={previewSlots}
            rooms={rooms}
            slots={slots}
            handleMoveSessionResponse={this.handleMoveSessionResponse}
            unscheduledSessions={unscheduledSessions}
            sessionFormats={sessionFormats}
            showErrors={this.showErrors}
          />
          <UnscheduledArea
            unscheduledSessions={unscheduledSessions}
            sessions={sessions}
            changeDragged={this.changeDragged}
            draggedSession={draggedSession}
            tracks={tracks}
            handleMoveSessionResponse={this.handleMoveSessionResponse}
          />
        </div>
      </div>
    );
  }
}

Schedule.propTypes = {
  schedule: PropTypes.object,
  sessions: PropTypes.array,
  counts: PropTypes.object,
  unscheduledSessions: PropTypes.array,
  tracks: PropTypes.array,
};

export default Schedule;