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 event-created, event-update and event-deleted lifecycle events.

Do you want to learn about the event ordering?  Learn more about it in the documentation →
Get started with this 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

<script setup>
import { MbscEventcalendar, MbscToast, setOptions  } from '@mobiscroll/vue'
import { ref } from 'vue'

setOptions({
  locale:  ,
  theme: ' ',
  themeVariant: ' '
})

const blockedOutTimes = [
  {
    start: '00:00',
    end: '05:00',
    recurring: {
      repeat: 'daily'
    },
    workOff: true
  },
  {
    start: '19:00',
    end: '00:00',
    recurring: {
      repeat: 'daily'
    },
    workOff: true
  }
]
const timelineInst = ref(null)
const myInvalids = ref(blockedOutTimes)
const toastMessage = ref('')
const isToastOpen = ref(false)
const myEvents = ref([
  {
    id: 1,
    start: '2026-10-25T05:00',
    end: '2026-10-25T12: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-25T05:00',
    end: '2026-10-25T08:30',
    title: 'Server Maintenance',
    resource: 1,
    shift: 1,
    order: 2,
    cssClass: 'mds-task-subtask'
  },
  {
    id: 'es-2',
    start: '2026-10-25T09:00',
    end: '2026-10-25T10:45',
    title: 'Monitor System Performance',
    resource: 1,
    shift: 1,
    order: 2,
    cssClass: 'mds-task-subtask'
  },
  {
    id: 'es-3',
    start: '2026-10-25T11:00',
    end: '2026-10-25T12:00',
    title: 'Backup and Recovery',
    resource: 1,
    shift: 1,
    order: 2,
    cssClass: 'mds-task-subtask'
  },
])
const myResources = [
  {
    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
  }
]

const myView = {
  timeline: {
    type: 'week',
    startDay: 1,
    startTime: '05:00',
    endTime: '19:00',
    endDay: 5,
    eventHeight: 'variable'
  }
}

const myDefaultEvent = (args) => {
  if (timelineInst.value.instance) {
    const events = timelineInst.value.instance
      .getEvents(args.start, new Date(+args.start + 3600000))
      .filter((e) => e.resource === args.resource)
    const 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
    }
  }
}

const handelEventCreate = (args, inst) => {
  const event = args.event
  const overlapEvents = inst
    .getEvents(event.start, event.end)
    .filter((e) => e.resource === event.resource)

  if (event.shift) {
    // Tasks was created
    const shift = { ...overlapEvents[0] }
    const shiftIndex = myEvents.value.findIndex((x) => x.id === event.shift)
    const newEventList = [...myEvents.value]

    if (overlapEvents.length > 2) {
      // Prevent overlap
      myEvents.value = newEventList
      toastMessage.value = 'No space for task'
      isToastOpen.value = true
    } else {
      // Update the shift
      shift.tasks.push(event.id)
      newEventList.splice(shiftIndex, 1, 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
      newEventList.push(event)
      myEvents.value = newEventList
    }
  } else {
    myEvents.value = [...myEvents.value, event]
  }
}

const handelEventDelete = (args) => {
  let newEventList = [...myEvents.value]
  const event = args.event
  const evIndex = myEvents.value.findIndex((x) => x.id === event.id)
  newEventList.splice(evIndex, 1)

  if (event.tasks) {
    // Update shift tasks
    newEventList = newEventList.filter((ev) => !event.tasks.includes(ev.id))
  }

  if (event.shift) {
    const shift = newEventList.find((ev) => ev.resource === event.resource && ev.id === event.shift)

    // Remove the deleted task id from the shift data
    shift.tasks = shift.tasks.filter((t) => t !== event.id)
  }
  myEvents.value = newEventList
}

const handelEventDragStart = (args, inst) => {
  const events = inst.getEvents()
  const event = args.event
  const tempInvalid = []

  if (event.tasks) {
    // Shift
    const shiftsInResource = events.filter(
      (e) => e.tasks !== undefined && e.resource === event.resource && e.id !== event.id
    )

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

    myInvalids.value = [...blockedOutTimes, ...tempInvalid]
  } else {
    // Subtask
    const shift = events.find((ev) => 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
      }
    )

    myInvalids.value = [...blockedOutTimes, ...tempInvalid]
  }
}

const handelEventDragEnd = () => {
  myInvalids.value = blockedOutTimes
}

const handelEventUpdate = (args, inst) => {
  const newEventList = [...myEvents.value]
  const event = args.event
  const evIndex = newEventList.findIndex((x) => x.id === event.id)
  const oldEvent = args.oldEvent

  if (event.tasks) {
    // Shift was updated
    let shiftStart = new Date(event.start)
    let shiftEnd = new Date(event.end)
    let subTasksDuration = 0
    const startDiff = +shiftStart - +new Date(oldEvent.start)
    const endDiff = +shiftEnd - +new Date(oldEvent.end)
    const startResize = startDiff > 0
    const endResize = endDiff < 0
    const isMove = startDiff === endDiff
    const isResize = !isMove && (startResize || endResize)

    const tasks = event.tasks.map((el) => {
      const t = newEventList.find((e) => 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
      newEventList.splice(evIndex, 1, event)
      myEvents.value = newEventList
    }

    // Resize or move
    if (isResize || startDiff === endDiff) {
      // Update subtask
      tasks.forEach((task, i) => {
        const taskStart = new Date(task.start)
        const 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)
        }

        const taskIndex = newEventList.findIndex((x) => x.id === task.id)
        newEventList.splice(taskIndex, 1, task)
      })

      myEvents.value = newEventList
    }
  } else {
    // Subtask was updated
    const eventOverlap = inst
      .getEvents(event.start, event.end)
      .filter((e) => e.resource === event.resource)
    if (eventOverlap.length > 2) {
      // Don't let subtask to overlap
      newEventList.splice(evIndex, 1, oldEvent)
      myEvents.value = newEventList
      toastMessage.value = 'No space for task'
      isToastOpen.value = true
    }
  }
}

function handleToastClose() {
  isToastOpen.value = false
}

const handelEventUpdateFailed = (args) => {
  const invalid = args.invalid
  let 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'
  }

  if (msg) {
    toastMessage.value = msg
    isToastOpen.value = true
  }
}
</script>

<template>
  <MbscEventcalendar
    ref="timelineInst"
    :view="myView"
    :resources="myResources"
    :dragToCreate="true"
    :dragToMove="true"
    :dragToResize="true"
    :data="myEvents"
    :invalid="myInvalids"
    :extendDefaultEvent="myDefaultEvent"
    @event-created="handelEventCreate"
    @event-deleted="handelEventDelete"
    @event-updated="handelEventUpdate"
    @event-drag-start="handelEventDragStart"
    @event-drag-end="handelEventDragEnd"
    @event-update-failed="handelEventUpdateFailed"
  >
    <template #timelineEventContent="args">
      {{ args.title }}
      <span className="mds-task-hours">
        -
        {{
          (+args.endDate - +args.startDate) / 3600000 +
          ((+args.endDate - +args.startDate) / 3600000 === 1 ? 'h' : 'hrs')
        }}</span
      >
    </template>
  </MbscEventcalendar>
  <MbscToast :message="toastMessage" :isOpen="isToastOpen" @close="handleToastClose" />
</template>

<style>
.mds-task-shift {
  height: 30px;
}

.mds-task-subtask {
  height: 40px;
}

.mds-task-subtask .mbsc-schedule-event-range,
.mds-task-subtask .mbsc-schedule-event-title {
  font-size: 10px;
  line-height: 13px;
}

.mds-task-shift .mbsc-schedule-event-range,
.mds-task-shift .mbsc-schedule-event-title {
  display: inline-block;
  margin: 0 4px;
}

.mds-task-hours {
  font-size: 10px;
  font-weight: 400;
}

.mds-task-blocked.mbsc-schedule-invalid {
  text-align: center;
  align-items: center;
  font-weight: bold;
  background: repeating-linear-gradient(-45deg, #f3f3f3, #f3f3f3 11px, #e5e5e5 11px, #e5e5e5 22px);
}
</style>

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.