Overview

  • Schedule shifts at multiple locations

  • There are three shifts defined for each day, starting at 12 AM, 8 AM and 4 PM

  • The work schedule displays an overview for each of the locations, where you can see who is assigned to each shift

  • When adding new shift assignments, you can also see the existing assignments for each employee

  • The frontend is a React application built using the React Scheduler component from DayPilot Pro for JavaScript

  • The backend is a REST API application created using PHP (with MySQL database storage)

  • Includes a trial version of DayPilot Pro for JavaScript (see License below)

For an introduction to using the React Scheduler component please see the following tutorial:

License

Licensed for testing and evaluation purposes. Please see the license agreement included in the sample project. You can use the source code of the tutorial if you are a licensed user of DayPilot Pro for JavaScript. Buy a license.

React Work Schedule for Multiple Locations

react-shift-scheduling-application-php-mysql-by-location-1.png

Each location view displays the shifts for the current location in the first row. In the rows below, the Scheduler displays available employees.

  • Shifts planned for the selected location are displayed in blue and linked to the assigned person.

  • Each person row displays all shifts for that person, including assignments for other locations (they are displayed in gray).

  • This way you can see all shifts planned for the current location and also availability of all employees.

Location 2

You can see how the view changes after switching to a different location:

  • The first row displays all shifts for “Location 2”.

  • The shifts assigned to “Location 1” are displayed in gray color.

react-shift-scheduling-application-php-mysql-by-location-2.png

React Scheduler Configuration

This is the basic React Scheduler configuration that we will use to build our shift planning application. You can generate a blank React project with a pre-configured Scheduler component at https://builder.daypilot.org.

import React, {Component} from 'react';
import {DayPilot, DayPilotScheduler} from "daypilot-pro-react";
import {Locations} from "./Locations";
import axios from "axios";

class Scheduler extends Component {

  constructor(props) {
    super(props);

    this.state = {
      timeHeaders: [{groupBy: "Month"}, {groupBy: "Day", format: "dddd M/d/yyyy"}, {groupBy: "Cell"}],
      startDate: "2021-07-01",
      days: 31,
      businessBeginsHour: 8,
      businessEndsHour: 16,
      scale: "Manual",
      
      // ...
    };
  }

  render() {
    const {...config} = this.state;
    return (
      <div>
        <DayPilotScheduler
          {...config}
          ref={component => this.scheduler = component && component.control}
        />
      </div>
    );
  }
}

export default Scheduler;

We will extend this configuration with additional properties and event handlers which will let us customize the behavior.

Select a Work Location

react-shift-scheduling-application-php-mysql-select-location.png

We are going to display a drop-down list with the available locations above the main Scheduler. It will let us select the location to work with and display the shift overview.

The drop-down list is wrapped in a simple <Location> React component:

render() {
  const {...config} = this.state;
  return (
    <div>
      <Locations onChange={args => this.loadData(args.location)} ref={component => this.locations = component}></Locations>
      <DayPilotScheduler
        {...config}
        ref={component => this.scheduler = component && component.control}
      />
    </div>
  );
}

When the user selects a different location, the component fires the onChange event. We will handle this event an update the Scheduler to display the location shift data.

Location.js

import React, {Component} from 'react';

export class Locations extends Component {

  constructor(props) {
    super(props);
    this.state = {
      list: []
    };
  }

  find(id) {
    if (!this.state.list) {
      return null;
    }
    return this.state.list.find(item => item.id === id);
  }

  load(data) {
    const list = data || [];
    this.setState({ list });
    this.doOnChange(data[0]);
  }

  render() {
    return (
      <div className="space">
        Location: &nbsp;
        <select id="locations" onChange={ev => this.change(ev)} ref={component => this.select = component}>
          {this.state.list.map(item => <option key={item.id} value={item.id}>{item.name}</option>)}
        </select>
      </div>
    );
  }

  change(ev) {
    const value = ev.target.value;
    const item = this.find(value);
    this.doOnChange(item);
  }

  doOnChange(location) {
    const args = { location };

    if (this.props.onChange) {
      this.props.onChange(args);
    }

  }

  get selectedValue() {
    return this.select.value;
  }

}

Load Work Shift Data

The onChange event handler of the <Locations> component calls the loadData() method.

<Locations onChange={args => this.loadData(args.location)} ref={component => this.locations = component}></Locations>

The loadData() method loads rows and shift assignments using two parallel API calls (/api/shift_people.php and /api/shift_assignment.php endpoints). We will use axios to make the HTTP requests.

The two axios.get() calls are merged using Promise.all() - our application waits for both calls to complete and updates the React Scheduler component with the location data.

It is possible to update the Scheduler by storing the row and event data in the config (the Scheduler would detect a change and update itself automatically) but we will use the direct API which is more efficient. The update() call loads the new row, event and link data and refreshes the Scheduler as necessary.

async loadData(location) {
  const dp = this.scheduler;

  const start = dp.visibleStart();
  const end = dp.visibleEnd();
  const promisePeople = axios.get(`/api/shift_people.php`);
  const promiseAssignments = axios.get(`/api/shift_assignments.php?location=${location.id}&start=${start}&end=${end}`);
  const [{data: people}, {data: assignments}] = await Promise.all([promisePeople, promiseAssignments]);

  const rows = [
    {id: "L" + location.id, name: location.name, type: "location"},
    ...people
  ];

  const links = [];
  assignments.filter(e => e.type === "location").forEach(e => {
    links.push({
      from: e.id,
      to: e.join,
      type: "FinishToFinish",
      layer: "Above",
      color: "#e69138"
    });
  });

  dp.update({
    resources: rows,
    events: assignments,
    links
  });

}

Add a Shift Assignment

react-shift-scheduling-application-php-mysql-create-assignment.png

We will use the onTimeRangeSelected event to handle clicks in the Scheduler grid.

Our onTimeRangeSelected event handler checks the target row first - it’s not possible to create a new assignment in the first row which shows the location assignments. We will display a warning using DayPilot.Modal.alert() that it’s necessary to create the assignment by selecting an employee (one of the rows below the location row).

After asking for confirmation, the React Scheduler creates a new assignment in the database by calling the /api/shift_create.php endpoint using axios.

onTimeRangeSelected: async args => {
  const dp = this.scheduler;

  const row = dp.rows.find(args.resource);
  if (row.index === 0) {
    await DayPilot.Modal.alert("No assignment for this shift.<br><br>Click below to create a new assignment.");
    dp.clearSelection();
    return;
  }

  const modal = await DayPilot.Modal.confirm("Create a new assignment?");
  dp.clearSelection();
  if (modal.canceled) {
    return;
  }
  const locationId = this.locations.selectedValue;

  const {data} = await axios.post("/api/shift_create.php", {
    start: args.start,
    end: args.end,
    location: locationId,
    person: args.resource
  });

  const id = data.id;

  dp.events.add({
    start: args.start,
    end: args.end,
    id: id,
    resource: args.resource,
    location: locationId,
    person: args.resource,
    join: id
  });

  dp.events.add({
    start: args.start,
    end: args.end,
    id: "L" + id,
    resource: "L" + locationId,
    location: locationId,
    person: args.resource,
    type: "location",
    join: id
  });

  dp.links.add({
    from: "L" + id,
    to: id,
    type: "FinishToFinish",
    layer: "Above",
    color: "#e69138"
  });
}

Each assignment is represented by two Scheduler events (one in the location row and one in the person row) and a link between them which connects them visually:

react-shift-scheduling-application-php-mysql-new-assignment.png

Delete the Shift Assignment

react-shift-scheduling-application-php-mysql-delete-assignment.png

In this step, we will add a custom delete icon to the location assignment. That will let users delete the shift assignment and free the slot.

The delete icon can be added by creating an event active area in onBeforeEventRender event handler using args.data.areas property.

onBeforeEventRender: args => {
  const dp = this.scheduler;

  const isLocation = args.data.type === "location";

  if (isLocation) {
    const person = dp.rows.find(args.data.person);

    // ...

    args.data.areas = [
      {
        right: 5,
        top: 8,
        height: 22,
        width: 22,
        cssClass: "scheduler_default_event_delete",
        style: "background-color: #fff; border: 1px solid #ccc; box-sizing: border-box; border-radius: 10px; padding: 0px; border: 1px solid #999",
        visibility: "Visible",
        onClick: async args => {

          const modal = await DayPilot.Modal.confirm("Delete this assignment?");

          if (modal.canceled) {
            return;
          }

          const locationAssignment = args.source;
            
          const locationAssignmentId = locationAssignment.data.id;
          const personAssignmentId = locationAssignment.data.join;

          await axios.post("/api/shift_delete.php", {id: personAssignmentId});

          dp.events.remove(locationAssignmentId);
          dp.events.remove(personAssignmentId);
          dp.links.remove(locationAssignmentId);
        }
      }
    ];
  }


  // ...

}

The onClick handler of the active area calls the backend using /api/shift_delete.php and removes the corresponding events.

Change the Shift Assignment using Drag and Drop

It’s possible to change the assigned person by dragging the employee assignment vertically:

react-shift-scheduling-application-php-mysql-change-employee.png

It’s also possible to change the shift slot by dragging the assignment horizontally in the location row. The employee assignment remains the same in this case:

react-shift-scheduling-application-php-mysql-change-time-slot.png

These drag and drop modifications are implemented using onEventMove event handler.

When creating events representing the shift assignment, we have linked them together using join property of the event data object. Events with the same join value will be moved together.

dp.events.add({
  start: args.start,
  end: args.end,
  id: id,
  resource: args.resource,
  location: locationId,
  person: args.resource,
  join: id
});

dp.events.add({
  start: args.start,
  end: args.end,
  id: "L" + id,
  resource: "L" + locationId,
  location: locationId,
  person: args.resource,
  type: "location",
  join: id
});

Events in the location row will only move vertically (this will let us change the time slot):

onBeforeEventRender: args => {

  // ...

  args.data.moveVDisabled = true;

  // ...
}

And events in the employee row will only move horizontally (this will let us change the employee).

onBeforeEventRender: args => {

  // ...

  args.data.moveHDisabled = true;

  // ...
}

When the event is dropped at the target position, the Scheduler fires the onEventMove event handler. We will use it to update the database record.

onEventMove: async args => {
  const dp = this.scheduler;
  const e = args.e;
  if (e.data.type === "location") {
    const {data} = await axios.post("/api/shift_update_time.php", {
      id: e.data.join,
      start: args.newStart,
      end: args.newEnd
    });

    dp.message(data.message);
  } else {
    const {data} = await axios.post("/api/shift_update_person.php", {
      id: e.data.join,
      person: args.newResource
    });

    dp.message(data.message);

    const locationAssignment = dp.events.find("L" + e.data.join);
    locationAssignment.data.person = args.newResource;
    dp.events.update(locationAssignment);
  }
},

PHP Backend

The React application uses http-proxy-middleware to proxy the /api/* calls to the PHP backend application running at http://172.0.0.1:8090 (note that the proxy may not work properly if you start the PHP web server at localhost:8090 instead of 127.0.0.1:8090).

You can start the API backend (react-shift-scheduling-php project) using the PHP built-in web server.

Linux:

php -S 127.0.0.1:8090 -t /path/to/react-shift-scheduling-php

Windows:

C:\php\php.exe -S 127.0.0.1:8090 -t C:\path\to\react-shift-scheduling-php

MySQL Database Schema

By default, the PHP backend uses an SQLite database which will be created and initialized automatically in the local directory.

You can switch to MySQL by editing _db.php file. Make sure that you adjust the database connection parameters in _db_mysql.php (server name, username, password.). The database will be created and initialized automatically if it doesn’t exist.

This is the MySQL database schema that will be used for the new database:

CREATE TABLE person (
  id INTEGER PRIMARY KEY,
  name VARCHAR(200)
);

CREATE TABLE location (
  id INTEGER PRIMARY KEY,
  name VARCHAR(200)
);

CREATE TABLE assignment (
  id INTEGER PRIMARY KEY AUTO_INCREMENT,
  person_id INTEGER,
  location_id INTEGER,
  assignment_start DATETIME,
  assignment_end DATETIME
);