Features

  • The Scheduler component displays a timeline for multiple machines, grouped by category (e.g., cutting, welding).

  • The timeline is limited to business hours (nights and weekends are hidden).

  • The application enables the scheduling of follow-up jobs for subsequent production steps across various machines.

  • The Spring Boot web application uses a JavaScript/HTML5 frontend and a REST HTTP backend API.

  • The app uses an in-memory H2 database to minimize dependencies.

  • The UI is built using JavaScript Scheduler component from DayPilot Pro for JavaScript.

This tutorial covers advanced scheduling features of DayPilot Pro. For a basic tutorial on using the Scheduler with Spring Boot please see Using JavaScript/HTML5 Scheduler in Spring Boot (Java).

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.

Scheduling Workflow Overview

Scheduling a New Job

machine production job scheduling spring java new task

Users of our production scheduling application can create a new task using drag and drop. It lets them choose the machine and start/end time using a visual UI.

The schedule displays available machines on the Y axis (grouped by type) and the working hours on the X axis. The time axis is limited to business hours (defined as 9AM to 5PM , Monday to Friday).

New Job Name

machine production job scheduling spring java new task name

A simple modal dialog allows users to enter the job name. You can replace it with a more advanced modal dialog with multiple custom fields.

Job Color

machine production job scheduling spring java task color

Users can assign a custom color to each job to specify priority, job type, or special requirements.

Scheduling Follow-Up Jobs

machine production job scheduling spring java follow up job

The application allows easy scheduling of a follow-up job by dragging the 'ยป' icon in the lower-right corner of the scheduled job to create a task for a subsequent step on another machine.

machine production job scheduling spring java follow up icon

When the user starts dragging the follow-up icon, the Scheduler displays a placeholder that outlines the new job position.

machine production job scheduling spring java follow up task drag

The follow-up job is automatically linked to the previous job. The new job has the default duration of 1 hour.

machine production job scheduling spring java follow up task created

Changing Job Duration

machine production job scheduling spring java change task duration

Users can change the job duration by dragging one of the edges.

Deleting a Scheduled Job

machine production job scheduling spring java delete task

It's possible to delete a job from the schedule using a context menu. This action also deletes all follow-up jobs.

Implementation

The application uses JavaScript Scheduler component from DayPilot Pro for JavaScript package. The Scheduler component includes built-in features that make the implementation easier, such as drag and drop support, customized rendering, hiding non-business hours and days, links that display dependencies between tasks, and so on.

Most of the functionality is implemented on the client side in a HTML5/JavaScript page. The backend is implemented in Spring Boot - it defines REST/JSON endpoints that let you load the data and save changes made by users.

This tutorial doesn't cover the Scheduler basics. For an introduction to using the Scheduler in a Spring Boot application please see the following tutorial:

Defining and Loading Machines

machine production job scheduling spring java resources

The resource tree (machines and their groups) is loaded from the server using an HTTP call:

dp.rows.load("/api/resources");

This URL (/api/resources) returns a JSON object with the tree:

[
  {"name":"Cutting","children":[
    {"name":"Cutting Machine 1","id":1},
    {"name":"Cutting Machine 2","id":2}
    ],"ordinal":1,"id":"G1","expanded":true},
  {"name":"Welding","children":[
    {"name":"Welding Cell 1","id":3},
    {"name":"Welding Cell 2","id":4},
    {"name":"Welding Cell 3","id":5}
    ],"ordinal":2,"id":"G2","expanded":true},
  {"name":"Sandblasting","children":[
    {"name":"Blast Booth 1","id":6}
    ],"ordinal":3,"id":"G3","expanded":true}
]

Disabling Parent Rows

machine production job scheduling spring java resources

We don't want the users to schedule jobs in the rows that represent groups. We will disable drag and drop for the parent rows:

{
  preventParentUsage: true,
  // ...
}

The grid cells that belong to rows with machine groups are automatically marked with scheduler_default_cellparent CSS class.

The following stylesheet overrides the default CSS theme and apply a light gray color to the parent cells:

.scheduler_default_main .scheduler_default_cell.scheduler_default_cellparent {
    background-color: #fcfcfc;
}

Business Hours

machine production job scheduling spring java work hours

We have configured the Scheduler to use a cell size of 1 hour:

const dp = new DayPilot.Scheduler("dp", {
  scale: "Hour",
  // ...
});
dp.init();

The time headers will display the hour name for each column. The topmost row groups the columns by day:

{ 
  timeHeaders: [
    {groupBy: "Day", format: "dddd M/d/yyyy"},
    {groupBy: "Hour"}
  ],
  // ...
}

Our machines are in operation only during working hours, defined as 9 a.m. - 6 p.m.:

{
  businessBeginsHour: 9,
  businessEndsHour: 18,
  // ...
}

Displaying the full timeline would result in big gaps (9 hours of operating hours vs 15 hours of non-operating hours). That's why will hide the time outside of the operating hours:

{
  showNonBusiness: false,
  // ...
}

We will also set the Scheduler to skip the hidden time during drag and drop operations (creating and moving jobs). This option will automatically adjust the start and end time as needed (it will be extended by the hidden time).

{
  eventMoveSkipNonBusiness: true,
  // ...
}

machine production job scheduling spring java skip non business

Context Menu

machine production job scheduling spring java context menu

In the next step, we will add a context menu that lets users delete scheduled jobs (it uses an HTTP call to a backend URL to save the changes to a database).

It also allows changing the job color (4 colors are predefined). The selected color will be applied to all linked jobs.

{
  contextMenu: new DayPilot.Menu({
    items: [
      {
        text: "Delete",
        onClick: async (args) => {
          const params = {
            id: args.source.id(),
          };
          const {data} = await DayPilot.Http.post("/api/events/delete", params);
          data.deleted.forEach((id) => {
            dp.events.removeById(id);
          });
          data.updated.forEach((data) => {
            const e = dp.events.find(data.id);
            if (e) {
              e.data.hasNext = data.hasNext;
              dp.events.update(e);
            }
          });
          dp.message("Job deleted");
        }
      },
      {
        text: "-"
      },
      {
        text: "Blue",
        icon: "icon icon-blue",
        color: "#1155cc",
        onClick: (args) => {
          app.updateColor(args.source, args.item.color);
        }
      },
      {
        text: "Green",
        icon: "icon icon-green",
        color: "#6aa84f",
        onClick: (args) => {
          app.updateColor(args.source, args.item.color);
        }
      },
      {
        text: "Yellow",
        icon: "icon icon-yellow",
        color: "#f1c232",
        onClick: (args) => {
          app.updateColor(args.source, args.item.color);
        }
      },
      {
        text: "Red",
        icon: "icon icon-red",
        color: "#cc0000",
        onClick: (args) => {
          app.updateColor(args.source, args.item.color);
        }
      },
    ]
  }),
  // ...
}

Creating a Follow-Up Job using Drag and Drop

machine production job scheduling spring java follow up

The follow-up jobs can be scheduled using the follow-up icon in the lower-right corner of existing jobs. Only one follow-up job is allowed (the icon is automatically hidden if the follow-up job has been already defined).

The follow-up icon is created using an active area:

{
  onBeforeEventRender: (args) => {
    args.data.barColor = args.data.color;

    const duration = new DayPilot.Duration(args.data.start, args.data.end);
    args.data.html = `<div><b>${args.data.text}</b><br>${duration.toString("h")} hours</div>`;

    if (args.data.hasNext) {
      return;
    }
    args.data.areas = [
      {
        right: 2,
        top: "calc(50% - 10px)",
        width: 20,
        height: 20,
        backColor: "#fff",
        fontColor: "#666",
        style: "box-sizing: border-box; border-radius: 10px; border: 1px solid #ccc;",
        symbol: "/icons/daypilot.svg#minichevron-right-2",
        toolTip: "Drag to schedule next step",
        action: "Move",
        data: "event-copy"
      }
    ];
  },
  // ...
}

We will provide real-time feedback to users during drag and drop using onEventMoving event handler. The event handler checks if the moving was initiated using the follow-up icon (args.areaData === "event-copy"). It displays a link to the original job (args.link):

{
  onEventMoving: (args) => {
    if (args.areaData && args.areaData === "event-copy") {
      args.link = {
        from: args.e,
        color: "#666"
      };
      args.start = args.end.addHours(-1);
      if (args.e.end() > args.start) {
        args.allowed = false;
        args.link.color = "red";
      }
    }
  },
  // ...
}

It also checks if the target position is valid (it must start after the original job has finished). If the target position doesn't meet the rules it will be marked as "not allowed" and it will be displayed in red. If the user releases the mouse button at this point no follow-up task will be created.

machine production job scheduling spring java invalid position

Job Editing

machine production job scheduling spring java edit job

When the users click the job box in the Scheduler the application opens a simple modal dialog that lets them change the job name.

The modal dialog can be replaced with a more complex form that would allow editing additional job properties. However, we will create a simplified dialog using the DayPilot.Modal.prompt() method which doesn't require creating a special page with the form.

{
  onEventClick: async (args) => {
    const modal = await DayPilot.Modal.prompt("Task name:", args.e.text());
    if (modal.canceled) {
      return;
    }
    const text = modal.result;
    const params = {
      join: args.e.data.join,
      text: text
    };
    const {data} = await DayPilot.Http.post("/api/events/setText", params);
    const list = data.events;
    list.forEach(data => {
      const e = dp.events.find(data.id);
      if (e) {
        e.data.text = text;
        dp.events.update(e);
      }
    });
    dp.message("Text updated");
  },
  // ...
}

Job Dependencies

machine production job scheduling spring java task dependencies

The dependencies are displayed using event links. When the scheduler is initialized it requests the links from the server side:

dp.links.load("/api/links");

The /api/links URL returns a JSON array with the events links:

[
  {"id":1,"to":2,"from":1}
]

Server-Side Controller (Java)

Here is the source code of the MainController class which implements the REST API behind this application.

It's a Spring MVC controller that specifies endpoints for loading machines (resources), jobs (events), and dependencies (links). It also saves changes done by the user through the Scheduler UI.

package org.daypilot.demo.machinescheduling.controller;

import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import org.daypilot.demo.machinescheduling.domain.Event;
import org.daypilot.demo.machinescheduling.domain.Link;
import org.daypilot.demo.machinescheduling.domain.Resource;
import org.daypilot.demo.machinescheduling.domain.ResourceGroup;
import org.daypilot.demo.machinescheduling.repository.EventRepository;
import org.daypilot.demo.machinescheduling.repository.LinkRepository;
import org.daypilot.demo.machinescheduling.repository.ResourceGroupRepository;
import org.daypilot.demo.machinescheduling.repository.ResourceRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.format.annotation.DateTimeFormat.ISO;
import org.springframework.web.bind.annotation.*;

import jakarta.transaction.Transactional;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

@RestController
public class MainController {

    @Autowired
    EventRepository er;

    @Autowired
    ResourceRepository rr;

    @Autowired
    ResourceGroupRepository gr;

    @Autowired
    LinkRepository lr;

    @RequestMapping("/api")
    @ResponseBody
    String home() {
        return "Welcome!";
    }

    @RequestMapping("/api/resources")
    List<ResourceGroup> resources() {
        List<ResourceGroup> groups = gr.findAllByOrderByOrdinal();

        groups.forEach(resourceGroup -> {
            List<Resource> children = rr.findByParent(resourceGroup);
            resourceGroup.setChildren(children);
        });

        return groups;
    }

    @GetMapping("/api/events")
    @JsonSerialize(using = LocalDateTimeSerializer.class)
    Iterable<Event> events(@RequestParam("start") @DateTimeFormat(iso = ISO.DATE_TIME) LocalDateTime start, @RequestParam("end") @DateTimeFormat(iso = ISO.DATE_TIME) LocalDateTime end) {
        return er.findBetween(start, end);
    }

    @GetMapping("/api/links")
    @JsonSerialize(using = LocalDateTimeSerializer.class)
    Iterable<Link> links(@RequestParam("start") @DateTimeFormat(iso = ISO.DATE_TIME) LocalDateTime start, @RequestParam("end") @DateTimeFormat(iso = ISO.DATE_TIME) LocalDateTime end) {
        return lr.findAll();
    }

    @PostMapping("/api/events/create")
    @JsonSerialize(using = LocalDateTimeSerializer.class)
    @Transactional
    UpdateResponse createEvent(@RequestBody EventCreateParams params) {

        List<Event> update = new ArrayList<>();

        Resource r = rr.findById(params.resource).orElse(null);

        Event e = new Event();
        e.setStart(params.start);
        e.setEnd(params.end);
        e.setText(params.text);
        e.setResource(r);
        er.save(e);

        e.setJoin(e.getId());
        er.save(e);

        update.add(e);

        if (params.link != null) {
            Event from = er.findById(params.link.from).orElse(null);
            from.setHasNext(true);
            er.save(from);

            update.add(from);

            Link link = new Link();
            link.setFrom(from);
            link.setTo(e);
            lr.save(link);

            e.setText(from.getText());
            e.setJoin(from.getJoin());
            e.setColor(from.getColor());
            er.save(e);
        }

        return new UpdateResponse(){{
            events = update;
        }};
    }

    @PostMapping("/api/events/move")
    @JsonSerialize(using = LocalDateTimeSerializer.class)
    @Transactional
    Event moveEvent(@RequestBody EventMoveParams params) {

        Event e = er.findById(params.id).orElse(null);
        Resource r = rr.findById(params.resource).orElse(null);

        e.setStart(params.start);
        e.setEnd(params.end);
        e.setResource(r);

        er.save(e);

        return e;
    }

    @PostMapping("/api/events/delete")
    @JsonSerialize(using = LocalDateTimeSerializer.class)
    @Transactional
    EventDeleteResponse deleteEvent(@RequestBody EventDeleteParams params) {

        List<Long> deletedIds = new ArrayList<>();
        List<Event> updatedEvents = new ArrayList<>();

        Event e = er.findById(params.id).orElse(null);

        List<Link> previous = lr.findByTo(e);
        previous.forEach(link -> {
            link.getFrom().setHasNext(false);
            er.save(link.getFrom());

            updatedEvents.add(link.getFrom());
        });

        deleteEventWithLinks(e, deletedIds);


        return new EventDeleteResponse() {{
            updated = updatedEvents;
            deleted = deletedIds;
        }};
    }

    private void deleteEventWithLinks(Event e, List<Long> deleted) {
        List<Link> toLinks = lr.findByTo(e);
        lr.deleteAll(toLinks);

        List<Link> fromLinks = lr.findByFrom(e);
        fromLinks.forEach(link -> {
            deleteEventWithLinks(link.getTo(), deleted);
        });

        er.delete(e);
        deleted.add(e.getId());

    }


    @PostMapping("/api/events/setColor")
    @JsonSerialize(using = LocalDateTimeSerializer.class)
    @Transactional
    UpdateResponse setColor(@RequestBody SetColorParams params) {

        List<Event> list = er.findByJoin(params.join);
        list.forEach(e -> {
            e.setColor(params.color);
            er.save(e);
        });

        return new UpdateResponse() {{
            events = list;
        }};
    }

    @PostMapping("/api/events/setText")
    @JsonSerialize(using = LocalDateTimeSerializer.class)
    @Transactional
    UpdateResponse setText(@RequestBody SetTextParams params) {

        List<Event> list = er.findByJoin(params.join);
        list.forEach(e -> {
            e.setText(params.text);
            er.save(e);
        });

        return new UpdateResponse() {{
            events = list;
        }};
    }

    public static class EventCreateParams {
        public LocalDateTime start;
        public LocalDateTime end;
        public String text;
        public Long resource;
        public LinkCreateParams link;
    }

    public static class LinkCreateParams {
        public Long from;
    }

    public static class EventMoveParams {
        public Long id;
        public LocalDateTime start;
        public LocalDateTime end;
        public Long resource;
    }

    public static class SetColorParams {
        public Long join;
        public String color;
    }

    public static class SetTextParams {
        public Long join;
        public String text;
    }

    public static class EventDeleteParams {
        public Long id;
    }

    public static class EventDeleteResponse {
        public List<Long> deleted;
        public List<Event> updated;
    }

    public static class UpdateResponse {
        public List<Event> events;
    }


}

JPA Entities

Here are the domain model classes that handle the persistence on the server side:

Event.java

The Event class represents the job task to be scheduled. It specifies the start and end date/time, assigned resource, the job sequence id ("join") and the next linked job.

package org.daypilot.demo.machinescheduling.domain;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;

import jakarta.persistence.*;
import java.time.LocalDateTime;

@Entity
@Table(indexes = { @Index(name = "index_join", columnList = "join_id") })
public class Event {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    Long id;

    String text;

    @Column(name="event_start")
    LocalDateTime start;

    @Column(name="event_end")
    LocalDateTime end;

    @ManyToOne
    @JsonIgnore
    Resource resource;

    String color;

    @Column(name="join_id")
    Long join;

    boolean hasNext = false;

    @JsonProperty("resource")
    public Long getResourceId() {
        return resource.getId();
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getText() {
        return text;
    }

    public void setText(String text) {
        this.text = text;
    }

    public LocalDateTime getStart() {
        return start;
    }

    public void setStart(LocalDateTime start) {
        this.start = start;
    }

    public LocalDateTime getEnd() {
        return end;
    }

    public void setEnd(LocalDateTime end) {
        this.end = end;
    }

    public Resource getResource() {
        return resource;
    }

    public void setResource(Resource resource) {
        this.resource = resource;
    }

    public String getColor() {
        return color;
    }

    public void setColor(String color) {
        this.color = color;
    }

    public Long getJoin() {
        return join;
    }

    public void setJoin(Long join) {
        this.join = join;
    }

    public boolean isHasNext() {
        return hasNext;
    }

    public void setHasNext(boolean hasNext) {
        this.hasNext = hasNext;
    }
}

Link.java

The Link class represents a link between individual events (jobs). It stores the source and target event IDs.

package org.daypilot.demo.machinescheduling.domain;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;

import jakarta.persistence.*;

@Entity
public class Link {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    Long id;

    @ManyToOne
    @JsonIgnore
    Event from;

    @ManyToOne
    @JsonIgnore
    Event to;

    @JsonProperty("from")
    public Long getFromId() {
        return from.getId();
    }

    @JsonProperty("to")
    public Long getToId() {
        return to.getId();
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public Event getFrom() {
        return from;
    }

    public void setFrom(Event from) {
        this.from = from;
    }

    public Event getTo() {
        return to;
    }

    public void setTo(Event to) {
        this.to = to;
    }
}

Resource.java

The Resource class represents machines that are used to perform the production jobs. It specifies a resource name and the group the resource belongs to.

package org.daypilot.demo.machinescheduling.domain;

import com.fasterxml.jackson.annotation.JsonIgnore;

import jakarta.persistence.*;

@Entity
public class Resource {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    Long Id;

    String name;

    @ManyToOne
    @JsonIgnore
    ResourceGroup parent;

    public Long getId() {
        return Id;
    }

    public void setId(Long id) {
        this.Id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public ResourceGroup getParent() {
        return parent;
    }

    public void setParent(ResourceGroup parent) {
        this.parent = parent;
    }
}

ResourceGroup.java

The ResourceGroup class represents the machine type. It's used to group the resources in the Scheduler tree.

package org.daypilot.demo.machinescheduling.domain;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;

import jakarta.persistence.*;
import java.util.List;

@Entity
public class ResourceGroup {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @JsonIgnore
    Long Id;

    String name;

    @Transient
    List<Resource> children;

    Long ordinal;

    @JsonProperty("id")
    public String getPrefixedId() {
        return "G" + Id;
    }

    public Long getId() {
        return Id;
    }

    public void setId(Long id) {
        this.Id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public List<Resource> getChildren() {
        return children;
    }

    public void setChildren(List<Resource> children) {
        this.children = children;
    }

    public Long getOrdinal() {
        return ordinal;
    }

    public void setOrdinal(Long ordinal) {
        this.ordinal = ordinal;
    }

    public boolean getExpanded() {
        return true;
    }
}

History

  • October 12, 2023: Spring Boot upgraded to 3.1.4 (Jakarta EE); DayPilot Pro for JavaScript upgraded to 2023.4.5749; ES2017+ syntax; SVG icons; curved links

  • December 20, 2020: Spring Boot upgraded to 2.4.1; DayPilot Pro for JavaScript upgraded to 2020.4.4807; jQuery dependency removed

  • September 17, 2019: Spring Boot upgraded to 2.1.8, DayPilot Pro for JavaScript upgraded to 2019.3.4023, JPA entities explained.

  • September 25, 2017: Initial release