Overview

  • Resources organized by groups

  • Reorder and move groups/resources using drag and drop

  • Add events and milestones

  • Row context menu with resource operations (add, edit, delete)

  • Event context menu with event operations (edit, delete)

  • Move events and milestones using drag and drop

  • Edit events and milestone details using a modal dialog

  • Built using the React Scheduler component from DayPilot Pro for JavaScript scheduling library.

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.

Activity Planning System: Resources as Rows

react activity planning node express postgresql resources rows

The activity planning application lets you define your own resources (organized in groups) that will be displayed on the vertical axis.

The rows can display:

  • departments and people

  • locations and facilities

  • machines and devices

  • areas of responsibility and activity types

Activity Planning: Timeline

react activity planning node express postgresql timeline

The horizontal axis displays time. In our application, the timeline displays months and days but the scheduler component can be configured to display years, quarters, months, weeks, days, hours, minutes or custom units.

Activity Planning: Events and Milestones

react activity planning node express postgresql events and milestones

The activity planning application lets you display events and milestones for your resources.

You can create new events and milestones using drag and drop. You can also move or resize existing events and milestones using drag and drop.

Application Structure

The application consists of two projects:

  • the root project is a React frontend application

  • the server directory contains the backend application that provides access to the PostgreSQL database using an API

If you run npm start in the root project, the script will build the React application and run it using the server. The server defines the API endpoints and serves the React application.

Frontend: React Application

The boilerplate of the React Scheduler project was generated using DayPilot UI Builder. You can use this online application to create and download a new React project with a pre-configured React Scheduler component.

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

Backend: Node/Express Application

You can find the Node/Express application that defines the API in the server directory.

Before running the backend project, it is necessary to create and initialize the PostgreSQL database manually (see the DDL below).

You’ll also need to adjust the PostgreSQL database connection parameters defined at the top of the index.js file of the server project:

const pool = new pg.Pool({
  host: '127.0.0.1',
  database: 'daypilot',
  user: 'username',
  password: 'password',
  port: 5432,
});

Loading Activity Planning Data

react activity planning node express postgresql load data

The event and resource data are loaded using two API calls. We run these HTTP requests in parallel and wait for both of them to complete.

When we have the data, we update the React Scheduler using the direct API by calling the update() method. It is also possible to update the Scheduler by adding the resource and event data to the config but the direct update is more efficient.

componentDidMount() {
  this.loadData();
}

async loadData() {
  const start = this.scheduler.visibleStart();
  const end = this.scheduler.visibleEnd();
  const promiseResources = axios.get(`/api/resources`);
  const promiseEvents = axios.get(`/api/events?start=${start}&end=${end}`);
  const [{data: resources}, {data: events}] = await Promise.all([promiseResources, promiseEvents]);

  this.scheduler.update({
    resources,
    events
  });

}

The backend Node/Express project defines the /api/events and /api/resources API endpoints as follows:

The /api/events endpoint returns a JSON array with event and milestone data which are stored in the events PostgreSQL database table.

app.get("/api/events", async (req, res) => {
  const start = req.query.start;
  const end = req.query.end;

  const query = 'SELECT * FROM event WHERE NOT (("end" <= $1) OR (start >= $2))';
  const params = [start, end];

  try {
    const result = await pool.query(query, params);
    const data = result.rows.map(r => ({
      ...r,
      resource: r.resource_id,
      start: localDateTimeISOString(r.start),
      end:  localDateTimeISOString(r.end),
    }));
    res.status(200).json(data)
  }
  catch (e) {
    console.error(e);
    res.status(500).json(e);
  }

});

The /api/resources endpoint returns a JSON tree with a group/resource nodes.

app.get("/api/resources", async (req, res) => {

  try {
    const groups = await pool.query('SELECT * FROM resource_group ORDER BY ordinal ASC, ordinal_priority DESC');
    const resources = await pool.query("SELECT * FROM resource ORDER BY ordinal ASC, ordinal_priority DESC");

    const result = groups.rows.map(g => ({
      id: "G" + g.id,
      groupId: g.id,
      name: g.name
    }));
    result.forEach(g => {
      g.expanded = true;
      g.children = resources.rows.filter(r => r.group_id === g.groupId);
    });

    res.status(200).json(result);

  }
  catch (e) {
    console.error(e);
    res.status(500).json(e);
  }

});

Plan Events

react activity planning node express postgresql events

You can create new events using drag and drop, by selecting the desired date range in the target row. The onTimeRangeSelected event handler opens a modal dialog where you can enter the event details:

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

  const form = [
    {name: "Text", id: "text"}
  ];

  const data = {
    text: "Event 1",
    start: args.start,
    end: args.end,
    resource: args.resource,
    type: "Event"
  };

  const modal = await DayPilot.Modal.form(form, data);
  dp.clearSelection();
  if (modal.canceled) { return; }

  const {data: e} = await axios.post("/api/createEvent", modal.result);
  dp.events.add(e);
},

The modal dialog is created using DayPilot.Modal.form() from DayPilot Modal which lets you define the form fields programmatically. You can design your own modal dialog using Modal Dialog Builder app.

As soon as you confirm the event details, the event handler calls /api/createEvent endpoint to create the event in the PostgreSQL database:

app.post("/api/createEvent", async (req, res) => {

  const query = 'INSERT INTO event (text, start, "end", type, resource_id) VALUES ($1, $2, $3, $4, $5) RETURNING *';
  const params = [req.body.text, req.body.start, req.body.end, req.body.type, req.body.resource];

  try {
    const result = await pool.query(query, params);
    const data = result.rows.map(r => ({
      ...r,
      resource: r.resource_id,
      start: localDateTimeISOString(r.start),
      end:  localDateTimeISOString(r.end),
    }));
    res.status(200).json(data[0])
  }
  catch (e) {
    console.error(e);
    res.status(500).json(e);
  }

});

Plan Milestones

react activity planning node express postgresql milestones

The milestone is a special kind of event with an adjusted behavior. In order to display a milestone, the data record needs to specify type: "Milestone" in its properties.

The milestones don’t have a duration (the end date is ignored) and they are displayed as a diamond at the specified location. The milestone can display additional text on the left or on the right side.

You can move milestones using drag and drop.

Because of the limited space, the milestones don’t display the context menu icon on hover but the context menu is still available using right mouse click.

New milestones can be created by right-clicking the scheduler grid. The onTimeRangeRightClick event handler opens a modal dialog with milestone details and sends a request to the server.

onTimeRangeRightClick: async args => {
  var dp = this.scheduler;

  const form = [
    {name: "Text", id: "text"}
  ];

  const data = {
    text: "Event 1",
    start: args.start.addHours(12),
    end: args.start.addHours(12),
    resource: args.resource,
    type: "Milestone"
  };

  const modal = await DayPilot.Modal.form(form, data);
  dp.clearSelection();
  if (modal.canceled) { return; }

  const {data: e} = await axios.post("/api/createEvent", modal.result);
  dp.events.add(e);
},

The event handler uses the same endpoint (/api/createEvent) to store the new milestone in the database.

Managing Rows (People, Departments, Facilities, Areas)

react activity planning node express postgresql resources people departments facilities

The scheduler rows can display resources (such as people, departments, facilities or areas of responsibility) which are organized in groups.

You can use a context menu to add, edit and delete rows. The context menu is defined using contextMenuResource property of the React Scheduler component. The same context menu is used for groups and resources but resources don’t show the “Add resource…” item.

contextMenuResource: new DayPilot.Menu({
  onShow: args => {
    const row = args.source;
    const hasParent = !!row.parent();
    this.scheduler.contextMenuResource.items[0].hidden = hasParent;
  },
  items: [
    {
      text: "Add resource...",
      onClick: args => {
        this.clickAddResource(args.source);
      }
    },
    {
      text: "Edit...",
      onClick: async args => {
        const form = [
          { name: "Name", id: "name" }
        ];
        const isGroup = !!args.source.data.groupId;
        const id = isGroup ? args.source.data.groupId : args.source.data.id;
        const name = args.source.data.name;
        const data = {
          id,
          name
        };
        const modal = await DayPilot.Modal.form(form, data);
        if (modal.canceled) {
          return;
        }
        const url = isGroup ? "/api/updateGroup" : "/api/updateResource";
        const {data: updated} = await axios.post(url, {id: modal.result.id, name: modal.result.name});
        const row = this.scheduler.rows.find(args.source.data.id);
        row.data.name = updated.name;
        this.scheduler.update();
      }
    },
    {
      text: "Delete",
      onClick: async args => {
        const row = args.source;
        if (row.data.groupId) {
          await axios.post("/api/deleteGroup", {id: row.data.groupId});
        }
        else {
          await axios.post("/api/deleteResource", {id: row.data.id});
        }
        this.scheduler.rows.remove(row.data.id);
      }
    }
  ]
})

Move Resources using Drag and Drop

react activity planning node express postgresql row order

The row order can be modified using drag and drop. Just drag the resource to the target location (within the current groups or in a different group).

The Scheduler customizes the drag and drop behavior using onRowMoving event handler. Certain target positions need to be forbidden since they are not supported by the data model (e.g. dragging a group to a child position):

onRowMoving: args => {
  const sourceIsGroup = !!args.source.data.groupId;
  const targetIsGroup = !!args.target.data.groupId;
  if (sourceIsGroup) {
    switch (args.position) {
      case "before":
        if (!targetIsGroup) {
          args.position = "forbidden";
        }
        break;
      case "after":
        if (!targetIsGroup) {
          args.position = "forbidden";
        }
        break;
      case "child":
        args.position = "forbidden";
        break;
    }
  }
  else {
    switch (args.position) {
      case "before":
        if (targetIsGroup) {
          args.position = "child";
        }
        break;
      case "after":
        if (targetIsGroup) {
          args.position = "child";
        }
        break;
      case "child":
        if (!targetIsGroup) {
          args.position = "forbidden";
        }
        break;
    }
  }
}

PostgreSQL Database Schema (DDL)

Here is the schema of the PostgreSQL database that is used by the server project to store the activity planning data.

event table

create table event
(
    id          serial    not null
        constraint event_pk
            primary key,
    text        varchar,
    start       timestamp not null,
    "end"       timestamp not null,
    type        varchar   not null,
    resource_id integer
);

create index event_end_index
    on event ("end");

create index event_start_index
    on event (start);

create index event_resource_id_index
    on event (resource_id);

resource_group table

create table resource_group
(
    id               serial            not null
        constraint group_pk
            primary key,
    name             varchar,
    ordinal          integer default 0 not null,
    ordinal_priority timestamp
);

create index resource_group_ordinal_index
    on resource_group (ordinal);

create index resource_group_ordinal_priority_index
    on resource_group (ordinal_priority);

resource table

create table resource
(
    id               serial            not null
        constraint resource_pk
            primary key,
    name             varchar,
    group_id         integer           not null,
    ordinal          integer default 0 not null,
    ordinal_priority timestamp
);

create index resource_ordinal_index
    on resource (ordinal);

create index resource_ordinal_priority_index
    on resource (ordinal_priority);

create index resource_group_id_index
    on resource (group_id);