Overview

  • This React application lets you schedule shifts for 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)

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

See also an ASP.NET Core version of this 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.

React Work Schedule for Multiple Locations

react shift scheduling application php mysql by location 1

Each location view displays the shifts for the current work 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

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, { useState, useRef } from 'react';
import { DayPilot, DayPilotScheduler } from "daypilot-pro-react";
import { Locations } from "./Locations";
import axios from "axios";

const Scheduler = () => {

  const scheduler = useRef();
  const [config, setConfig] = useState({
    timeHeaders: [
      {groupBy: "Month"}, 
      {groupBy: "Day", format: "dddd M/d/yyyy"}, 
      {groupBy: "Cell"}
    ],
    startDate: DayPilot.Date.today().firstDayOfMonth(),
    days: DayPilot.Date.today().daysInMonth(),
    // ...
  });

  const scheduler = useRef(null);

  return (
    <div>
      <DayPilotScheduler
        {...config}
        ref={scheduler}
      />
    </div>
  );
}

export default Scheduler;

This basic configuration displays the current month, as defined using the startDate and days properties:

startDate: DayPilot.Date.today().firstDayOfMonth(),
days: DayPilot.Date.today().daysInMonth(),

In the next steps, we will extend this configuration with additional properties and event handlers which will let us customize the behavior.

Define Shifts

In this steps, we will define the shifts by overriding the default timeline generated by the Scheduler component.

In order to use a custom timeline, we switch the Scheduler to a manual timeline mode:

timeline: "Manual",

Now the Scheduler will use a timeline defined using individual cells in the timeline array:

timeline: createTimeline(),

Our createTimeline() function defines the shifts as timeline cells. Each shift has 8 hours and we define three shifts per day:

  • 12 AM - 8 AM

  • 8 AM - 4 PM

  • 4 PM - 12 AM

const createTimeline = () => {
  const days = DayPilot.Date.today().daysInMonth();
  const start = DayPilot.Date.today().firstDayOfMonth();

  const result = [];
  for (let i = 0; i < days; i++) {
    const day = start.addDays(i);
    result.push({
      start: day.addHours(0),
      end: day.addHours(8)
    });
    result.push({
      start: day.addHours(8),
      end: day.addHours(16)
    });
    result.push({
      start: day.addHours(16),
      end: day.addHours(24)
    });
  }
  return result;

}

Our updated Scheduler configuration now uses the generated timeline with custom shifts:

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

const Scheduler = () => {

  const scheduler = useRef();

  const createTimeline = () => {
    const days = DayPilot.Date.today().daysInMonth();
    const start = DayPilot.Date.today().firstDayOfMonth();

    const result = [];
    for (let i = 0; i < days; i++) {
      const day = start.addDays(i);
      result.push({
        start: day.addHours(0),
        end: day.addHours(8)
      });
      result.push({
        start: day.addHours(8),
        end: day.addHours(16)
      });
      result.push({
        start: day.addHours(16),
        end: day.addHours(24)
      });
    }
    return result;
  }

  const [config, setConfig] = useState({
    timeHeaders: [
      {groupBy: "Month"}, 
      {groupBy: "Day", format: "dddd M/d/yyyy"}, 
      {groupBy: "Cell"}
    ],
    businessBeginsHour: 0,
    businessEndsHour: 24,
    businessWeekends: true,
    scale: "Manual",
    timeline: createTimeline(),
    // ...
  });

  const scheduler = useRef(null);

  return (
    <div>
      <DayPilotScheduler
        {...config}
        ref={scheduler}
      />
    </div>
  );
}

export default Scheduler;

As this is a plan for 24/7 shift rotation schedule, we override the default business hours to include the entire 24-hour period.

Display Data for a Selected Work Location

react shift scheduling application php mysql select location

We are going to display a drop-down list with 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:

return (
  <div>
    <Locations onChange={setSelectedLocation} data={locations} selectedValue={selectedLocation} />
    <DayPilotScheduler
      {...config}
      ref={scheduler}
    />
  </div>
);

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

Location.js

import React, { useEffect, useState } from 'react';

const Locations = ({ onChange, data, selectedValue }) => {
  const [list, setList] = useState([]);

  const find = (id) => {
    return list.find(item => item.id === id);
  }

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

  const doOnChange = (location) => {
    if (onChange) {
      onChange(location);
    }
  }

  useEffect(() => {
    const listData = data || [];
    setList(listData);
    doOnChange(listData[0]);
  }, [data]);

  return (
    <div className="space">
      Location: &nbsp;
      <select id="locations" value={selectedValue?.id} onChange={change}>
        {list.map(item => <option key={item.id} value={item.id}>{item.name}</option>)}
      </select>
    </div>
  );
}

export default Locations;

How to Load Work Shift Data

The onChange event handler of the <Locations> component updates the selectedLocation state variable:

<Locations onChange={setSelectedLocation} data={locations} selectedValue={selectedLocation} />

Now we add a useEffect hook that triggers the loadData function whenever the selectedLocation state variable changes:

useEffect(() => {
  loadData(selectedLocation);
}, [selectedLocation]);

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 in the example: The update() call loads the new data for rows, events and links and refreshes the Scheduler as necessary.

const loadData = async (location) => {
  if (!location) {
    return;
  }
  const dp = scheduler.current.control;

  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({
      id: e.id,
      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

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 = scheduler.current.control;

  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 = selectedLocationRef.current.id;

  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({
    id:  "L" + id,
    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

Delete the Shift Assignment

react shift scheduling application php mysql delete assignment

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 = scheduler.current.control;

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

  if (isLocation) {

    // ...

    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);
        }
      }
    ];
  } else {

    // ...

  }

}

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

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

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
);