Shifts and Tasks

Visualize shifts and tasks within them using the timeline view. With the help of the order property of the data object you can always guarantee that shifts are always displayed at the top (alternatively below tasks) and tasks within those shifts are rendered below.

The shift and task events are bind together with custom data properties. Every shift contains the task IDs in the tasks property, and every task has a shift property which indicates the parent shift. These properties are used later in the validation logic to handle the two distinct event type behaviours. The validation is implemented by dynamically setting invalid ranges through the onEventCreated, onEventUpdated and onEventDeleted lifecycle events.

Do you want to learn about the event ordering?  Learn more about it in the documentation →
Download and try example

Demo

Loading the live demo

Preview in full screen

Display shifts and linked tasks on a Timeline — group tasks under parent shifts and restrict movement to each shift's time range.

Code

mobiscroll.setOptions({
  locale:  ,
  theme: ' ',
  themeVariant: ' '
});

$(function () {
  var myInvalids = [
    {
      start: '00:00',
      end: '05:00',
      recurring: {
        repeat: 'daily',
      },
      workOff: true,
    },
    {
      start: '19:00',
      end: '00:00',
      recurring: {
        repeat: 'daily',
      },
      workOff: true,
    },
  ];

  var timelineInst = $('#demo-tasks-subtasks-under-shifts')
    .mobiscroll()
    .eventcalendar({
      dragToCreate: true,
      dragToMove: true,
      dragToResize: true,
      view: {
        timeline: {
          type: 'week',
          startDay: 1,
          startTime: '05:00',
          endTime: '19:00',
          endDay: 5,
          eventHeight: 'variable',
        },
      },
      extendDefaultEvent: function (args) {
        var events = timelineInst.getEvents(args.start, new Date(+args.start + 3600000)).filter(function (e) {
          return e.resource === args.resource;
        });
        var isShift = events.length === 0;
        return {
          order: isShift ? 1 : 2,
          cssClass: isShift ? 'mds-task-shift' : 'mds-task-subtask',
          color: isShift ? '#513737' : '',
          title: isShift ? 'New Shift' : 'New Task',
          tasks: isShift ? [] : undefined,
          shift: isShift ? undefined : events[0].id,
        };
      },
      onEventCreated: function (args, inst) {
        var event = args.event;
        var overlapEvents = inst.getEvents(event.start, event.end).filter(function (e) {
          return e.resource === event.resource;
        });

        if (event.shift) {
          // Tasks was created
          var shift = overlapEvents[0];
          if (overlapEvents.length > 2) {
            // Prevent task overlap
            inst.removeEvent(event);
            mobiscroll.toast({
              message: 'No space for task',
            });
          } else {
            // Update the shift
            shift.tasks.push(event.id);
            inst.updateEvent(shift);
            // Update subtask
            event.start = new Date(Math.max(+new Date(shift.start), +event.start));
            event.end = new Date(Math.min(+new Date(shift.end), +event.end));
            event.shift = shift.id;
            inst.updateEvent(event);
          }
        }
      },
      onEventDeleted: function (args, inst) {
        var event = args.event;
        if (event.tasks) {
          inst.removeEvent(event.tasks);
        }

        if (event.shift) {
          var shift = inst.getEvents().find(function (ev) {
            return ev.resource === event.resource && ev.id === event.shift;
          });
          // Remove the deleted task id from the shift data
          shift.tasks = shift.tasks.filter(function (t) {
            return t !== event.id;
          });

          inst.updateEvent(shift);
        }
      },
      onEventDragStart: function (args, inst) {
        var events = inst.getEvents();
        var event = args.event;
        var tempInvalid = [];

        if (event.tasks) {
          // Shift
          var shiftsInResource = events.filter(function (e) {
            return e.tasks !== undefined && e.resource === event.resource && e.id !== event.id;
          });

          shiftsInResource.forEach(function (e) {
            tempInvalid.push({
              start: e.start,
              end: e.end,
              resource: e.resource,
              cssClass: 'mds-task-blocked',
              shift: true,
            });
          });

          inst.setOptions({
            invalid: tempInvalid.concat(myInvalids),
          });
        } else {
          // Subtask
          var shift = events.find(function (ev) {
            return ev.resource === event.resource && ev.id === event.shift;
          });
          tempInvalid.push(
            {
              start: new Date(+new Date(shift.start) - 7 * 86400000),
              end: shift.start,
              resource: shift.resource,
              cssClass: 'mds-task-blocked',
              task: true,
            },
            {
              start: shift.end,
              end: new Date(+new Date(shift.end) + 7 * 86400000),
              resource: shift.resource,
              cssClass: 'mds-task-blocked',
              task: true,
            },
          );
          inst.setOptions({
            invalid: tempInvalid.concat(myInvalids),
          });
        }
      },
      onEventDragEnd: function (args, inst) {
        inst.setOptions({
          invalid: myInvalids,
        });
      },
      onEventUpdated: function (args, inst) {
        var events = inst.getEvents();
        var event = args.event;
        var oldEvent = args.oldEvent;

        if (event.tasks) {
          // Shift was updated
          var shiftStart = new Date(event.start);
          var shiftEnd = new Date(event.end);
          var startDiff = +shiftStart - +new Date(oldEvent.start);
          var endDiff = +shiftEnd - +new Date(oldEvent.end);
          var startResize = startDiff > 0;
          var endResize = endDiff < 0;
          var isMove = startDiff === endDiff;
          var isResize = !isMove && (startResize || endResize);
          var subTasksDuration = 0;
          var tasks = event.tasks.map(function (el) {
            var t = events.find(function (e) {
              return e.id === el;
            });
            subTasksDuration += +new Date(t.end) - +new Date(t.start);
            return t;
          });

          if (isResize && +shiftEnd - +shiftStart < subTasksDuration) {
            // Limit the shift to don't be smaller than the containing subtasks
            shiftStart = endResize ? shiftStart : new Date(+shiftEnd - subTasksDuration);
            shiftEnd = startResize ? shiftEnd : new Date(+shiftStart + subTasksDuration);
            event.start = shiftStart;
            event.end = shiftEnd;
            inst.updateEvent(event);
          }

          // Resize or move
          if (isResize || startDiff === endDiff) {
            var updatedTasks = [];
            // Update subtask
            tasks.forEach(function (task, i) {
              var taskStart = new Date(task.start);
              var taskEnd = new Date(task.end);
              if (isResize) {
                task.start = new Date(i === 0 ? +shiftStart : +new Date(tasks[i - 1].end));
                task.end = new Date(Math.min(+task.start + (+taskEnd - +taskStart), +new Date(event.end)));
              } else {
                task.start = new Date(+taskStart + startDiff);
                task.end = new Date(+taskEnd + startDiff);
              }
              updatedTasks.push(task);
            });

            inst.updateEvent(updatedTasks);
          }
        } else {
          // Subtask was updated
          var eventOverlap = inst.getEvents(event.start, event.end).filter(function (e) {
            return e.resource === event.resource;
          });
          if (eventOverlap.length > 2) {
            // Don't let subtask to overlap
            inst.updateEvent(oldEvent);

            mobiscroll.toast({
              message: 'No space for task',
            });
          }
        }
      },
      onEventUpdateFailed: function (args) {
        var invalid = args.invalid;
        var msg = '';

        if (invalid.workOff) {
          msg = 'Shift falls out of working hours';
        } else if (invalid.task) {
          msg = 'Task falls out of shift';
        } else if (invalid.shift) {
          msg = 'Shifts cannot overlap';
        }

        msg &&
          mobiscroll.toast({
            message: msg,
          });
      },
      renderTimelineEventContent: function (args) {
        var duration = (+args.endDate - +args.startDate) / 3600000;
        return (
          args.title +
          (args.original.shift ? ' <span class="mds-task-hours"> - ' + (duration + (duration === 1 ? 'h' : 'hrs')) + '</span>' : '')
        );
      },
      data: [
        {
          id: 1,
          start: '2026-10-30T05:00',
          end: '2026-10-30T12:00',
          title: 'Daily Shift',
          resource: 1,
          tasks: ['es-1', 'es-2', 'es-3'],
          order: 1,
          color: '#513737',
          cssClass: 'mds-task-shift',
        },
        {
          id: 'es-1',
          start: '2026-10-30T05:00',
          end: '2026-10-30T08:30',
          title: 'Server Maintenance',
          resource: 1,
          shift: 1,
          order: 2,
          cssClass: 'mds-task-subtask',
        },
        {
          id: 'es-2',
          start: '2026-10-30T09:00',
          end: '2026-10-30T10:45',
          title: 'Monitor System Performance',
          resource: 1,
          shift: 1,
          order: 2,
          cssClass: 'mds-task-subtask',
        },
        {
          id: 'es-3',
          start: '2026-10-30T11:00',
          end: '2026-10-30T12:00',
          title: 'Backup and Recovery',
          resource: 1,
          shift: 1,
          order: 2,
          cssClass: 'mds-task-subtask',
        },
        {
          id: 2,
          start: '2026-10-30T11:00',
          end: '2026-10-30T18:00',
          title: 'Flex Shift',
          resource: 2,
          tasks: ['jb-1', 'jb-2', 'jb-3', 'jb-4'],
          order: 1,
          color: '#513737',
          cssClass: 'mds-task-shift',
        },
        {
          id: 'jb-1',
          start: '2026-10-30T11:00',
          end: '2026-10-30T12:30',
          title: 'Code Review',
          resource: 2,
          shift: 2,
          order: 2,
          cssClass: 'mds-task-subtask',
        },
        {
          id: 'jb-2',
          start: '2026-10-30T13:00',
          end: '2026-10-30T14:45',
          title: 'Develop New Features',
          resource: 2,
          shift: 2,
          order: 2,
          cssClass: 'mds-task-subtask',
        },
        {
          id: 'jb-3',
          start: '2026-10-30T15:00',
          end: '2026-10-30T16:50',
          title: 'Mentor Junior Developers',
          resource: 2,
          shift: 2,
          order: 2,
          cssClass: 'mds-task-subtask',
        },
        {
          id: 'jb-4',
          start: '2026-10-30T17:00',
          end: '2026-10-30T18:00',
          title: 'Attend Stand-Up Meeting',
          resource: 2,
          shift: 2,
          order: 2,
          cssClass: 'mds-task-subtask',
        },
      ],
      resources: [
        {
          id: 1,
          color: '#ff0101',
          name: 'Emma Smith',
          eventDragBetweenResources: false,
        },
        {
          id: 2,
          color: '#239a21',
          name: 'James Brown',
          eventDragBetweenResources: false,
        },
        {
          id: 3,
          color: '#8f1ed6',
          name: 'Olivia Miller',
          eventDragBetweenResources: false,
        },
        {
          id: 4,
          color: '#01adff',
          name: 'Robert Taylor',
          eventDragBetweenResources: false,
        },

        {
          id: 5,
          color: '#F58585',
          name: 'John Doe',
          eventDragBetweenResources: false,
        },
      ],
      invalid: myInvalids,
    })
    .mobiscroll('getInst');
});
  

Duplicate Session

You have signed in on a different machine. To keep everything in sync, you have been signed out of this session.

If you need to invite team members, manage seats and accounts you can do it from the licenses page.

Duplicate Session

We noticed you are logged in on a different machine and have closed your old session.

If you need to invite team members, manage seats and accounts you can do it from the licenses page.