---
title: "Vue Timeline Employee Shift Scheduling Example | Mobiscroll"
component: "Timeline"
framework: "Vue"
library_version: "6.1.5"
url: "https://demo.mobiscroll.com/vue/timeline/employee-shifts"
---

# Employee Shift Scheduling - Vue Timeline demo

Plan employee shifts on a Vue Timeline — show workers as resource rows with morning and afternoon slots, and manage shifts with a custom add/edit dialog. For Vue and Nuxt.

- [Live demo](https://demo.mobiscroll.com/vue/timeline/employee-shifts)
- [View SFC JS code on GitHub](https://github.com/acidb/mobiscroll-demos-vue/tree/main/src/demos/eventcalendar/timeline/employee-shifts)
- [View SFC TS code on GitHub](https://github.com/acidb/mobiscroll-demos-vue-ts/tree/main/src/demos/eventcalendar/timeline/employee-shifts)
- This demo in other frameworks: [JavaScript](https://demo.mobiscroll.com/javascript/timeline/employee-shifts.md), [jQuery](https://demo.mobiscroll.com/jquery/timeline/employee-shifts.md), [Angular](https://demo.mobiscroll.com/angular/timeline/employee-shifts.md), [React](https://demo.mobiscroll.com/react/timeline/employee-shifts.md)

## Description

Use the summary mode of the timeline view with `eventDisplay: 'fill'` that can be configured under the `timeline` settings of the [`view`](https://mobiscroll.com/docs/vue/eventcalendar/api#opt-view) option.

This will give you summarized daily events similar to the [event calendar labels](https://demo.mobiscroll.com/vue/eventcalendar/event-labels) where each event is printed under the appropriate day, time slot (shift) for the appropriate resource one after the other.

Build a custom add/edit dialog with the necessary fields. The event dialog can be fully custom as seen in this [CRUD example](https://demo.mobiscroll.com/vue/scheduler/create-read-update-delete-CRUD).

Customize the dialog header with a custom header template using the [`header`](https://mobiscroll.com/docs/vue/eventcalendar/api#slot-header) template slot.

## What this demo shows

- A weekly timeline-based shift planner that displays employees as resource rows and shows only work days.
- **Shift structure** Each day is divided into two named slots, Morning and Afternoon, instead of showing shifts by exact position on a time grid.
- **Resources** Each row represents an employee, and the events shown in that row are the scheduled shifts for that employee.
- **Visual distinction** Each employee has an assigned color, and their shifts use that color in the timeline.
- **Editing shifts** Clicking an existing shift opens a popup with an edit form where the shift start and end times can be changed and notes can be added.
- **Deleting shifts** The edit popup includes a delete action that removes the shift immediately, then shows a snackbar confirmation with an Undo action to restore it.
- **Creating shifts** Double-clicking an empty timeline slot opens a popup for adding a new shift.
- **Add form** The create popup lets users set the shift start and end times and add notes before saving.

## Best for

- **Employee shift planning** Weekly rota and shift scheduling interfaces where employees are shown as resources.
- **Work-week scheduling** Setups that focus on working days only rather than a full seven-day timeline.
- **Named-shift layouts** Scenarios where shifts are organized into fixed slots such as Morning and Afternoon instead of being positioned by exact time.
- **Color-coded teams** Calendars where resource-based color coding helps users quickly identify which shifts belong to which employee.
- **Direct calendar editing** Applications where users need to create, update, delete, and restore shifts directly from the calendar UI.

## API used in this demo

- [`theme`](https://mobiscroll.com/docs/vue/eventcalendar/api#opt-theme) - option
- [`themeVariant`](https://mobiscroll.com/docs/vue/eventcalendar/api#opt-themeVariant) - option
- [`display`](https://mobiscroll.com/docs/vue/popup/api#opt-display) - option
- [`fullScreen`](https://mobiscroll.com/docs/vue/popup/api#opt-fullScreen) - option
- [`touchUi`](https://mobiscroll.com/docs/vue/popup/api#opt-touchUi) - option
- [`width`](https://mobiscroll.com/docs/vue/popup/api#opt-width) - option
- [`cssClass`](https://mobiscroll.com/docs/vue/eventcalendar/api#opt-cssClass) - option
- [`clickToCreate`](https://mobiscroll.com/docs/vue/eventcalendar/api#opt-clickToCreate) - option
- [`dragToCreate`](https://mobiscroll.com/docs/vue/eventcalendar/api#opt-dragToCreate) - option
- [`dragToResize`](https://mobiscroll.com/docs/vue/eventcalendar/api#opt-dragToResize) - option
- [`dragToMove`](https://mobiscroll.com/docs/vue/eventcalendar/api#opt-dragToMove) - option
- [`data`](https://mobiscroll.com/docs/vue/eventcalendar/api#opt-data) - option
- [`eventOverlap`](https://mobiscroll.com/docs/vue/eventcalendar/api#opt-eventOverlap) - option
- [`extendDefaultEvent`](https://mobiscroll.com/docs/vue/eventcalendar/api#opt-extendDefaultEvent) - option
- [`invalid`](https://mobiscroll.com/docs/vue/eventcalendar/api#opt-invalid) - option
- [`resources`](https://mobiscroll.com/docs/vue/eventcalendar/api#opt-resources) - option
- [`slots`](https://mobiscroll.com/docs/vue/eventcalendar/api#opt-slots) - option
- [`view`](https://mobiscroll.com/docs/vue/eventcalendar/api#opt-view) - option
- [`event-click`](https://mobiscroll.com/docs/vue/eventcalendar/api#event-onEventClick) - event
- [`event-created`](https://mobiscroll.com/docs/vue/eventcalendar/api#event-onEventCreated) - event
- [`event-create-failed`](https://mobiscroll.com/docs/vue/eventcalendar/api#event-onEventCreateFailed) - event
- [`event-deleted`](https://mobiscroll.com/docs/vue/eventcalendar/api#event-onEventDeleted) - event
- [`event-updated`](https://mobiscroll.com/docs/vue/eventcalendar/api#event-onEventUpdated) - event
- [`event-update-failed`](https://mobiscroll.com/docs/vue/eventcalendar/api#event-onEventUpdateFailed) - event
- [`buttons`](https://mobiscroll.com/docs/vue/popup/api#opt-buttons) - option
- [`isOpen`](https://mobiscroll.com/docs/vue/popup/api#opt-isOpen) - option
- [`responsive`](https://mobiscroll.com/docs/vue/popup/api#opt-responsive) - option
- [`scrollLock`](https://mobiscroll.com/docs/vue/popup/api#opt-scrollLock) - option
- [`close`](https://mobiscroll.com/docs/vue/popup/api#event-onClose) - event
- [`display`](https://mobiscroll.com/docs/vue/datepicker/api#opt-display) - option
- [`select`](https://mobiscroll.com/docs/vue/datepicker/api#opt-select) - option
- [`timeWheels`](https://mobiscroll.com/docs/vue/datepicker/api#localization-timeWheels) - option
- [`controls`](https://mobiscroll.com/docs/vue/datepicker/api#opt-controls) - option
- [`minTime`](https://mobiscroll.com/docs/vue/datepicker/api#opt-minTime) - option
- [`maxTime`](https://mobiscroll.com/docs/vue/datepicker/api#opt-maxTime) - option
- [`showRangeLabels`](https://mobiscroll.com/docs/vue/datepicker/api#opt-showRangeLabels) - option
- [`stepMinute`](https://mobiscroll.com/docs/vue/datepicker/api#opt-stepMinute) - option
- [`startInput`](https://mobiscroll.com/docs/vue/datepicker/api#opt-startInput) - option
- [`endInput`](https://mobiscroll.com/docs/vue/datepicker/api#opt-endInput) - option
- [`touchUi`](https://mobiscroll.com/docs/vue/datepicker/api#opt-touchUi) - option
- [`close`](https://mobiscroll.com/docs/vue/notifications/snackbar#event-onClose) - event
- [`close`](https://mobiscroll.com/docs/vue/notifications/toast#event-onClose) - event

## Code

### SFC JS

```vue
<script setup>
import {
  formatDate,
  MbscButton,
  MbscDatepicker,
  MbscEventcalendar,
  MbscInput,
  MbscPopup,
  MbscSnackbar,
  MbscTextarea,
  MbscToast,
  setOptions
} from '@mobiscroll/vue'
import { ref } from 'vue'

setOptions({
  theme: 'ios',
  themeVariant: 'light'
})

const deletedShift = ref()
const isEdit = ref(false)
const isPopupOpen = ref(false)
const isSnackbarOpen = ref(false)
const isToastOpen = ref(false)
const minTime = ref('')
const maxTime = ref('')
const headerPrimary = ref('')
const headerDate = ref('')
const popupButtons = ref([])
const startInput = ref(null)
const endInput = ref(null)
const shift = ref()
const shiftDates = ref([])
const shiftNotes = ref('')
const toastMessage = ref('')

const popupResponsive = {
  medium: {
    display: 'center',
    fullScreen: false,
    touchUi: false,
    width: 400
  }
}

const snackbarButton = {
  action: () => {
    shifts.value = [...shifts.value, deletedShift.value]
  },
  text: 'Undo'
}

const staff = [
  {
    id: 1,
    name: 'Ryan',
    color: '#e20000',
    title: 'Cloud System Engineer',
    img: 'https://img.mobiscroll.com/demos/m1.png'
  },
  {
    id: 2,
    name: 'Kate',
    color: '#60e81a',
    title: 'Help Desk Specialist',
    img: 'https://img.mobiscroll.com/demos/f1.png'
  },
  {
    id: 3,
    name: 'John',
    color: '#3ba7ff',
    title: 'Application Developer',
    img: 'https://img.mobiscroll.com/demos/m2.png'
  },
  {
    id: 4,
    name: 'Mark',
    color: '#e25dd2',
    title: 'Network Administrator',
    img: 'https://img.mobiscroll.com/demos/m3.png'
  },
  {
    id: 5,
    name: 'Sharon',
    color: '#f1e920',
    title: 'Data Quality Manager',
    img: 'https://img.mobiscroll.com/demos/f2.png'
  },
  {
    id: 6,
    name: 'Emma',
    color: '#1ac38d',
    title: 'Product Tactics Agent',
    img: 'https://img.mobiscroll.com/demos/f3.png'
  },
  {
    id: 7,
    name: 'Michael',
    color: '#f18a1a',
    title: 'UX Designer',
    img: 'https://img.mobiscroll.com/demos/m4.png'
  },
  {
    id: 8,
    name: 'Laura',
    color: '#7b61ff',
    title: 'DevOps Engineer',
    img: 'https://img.mobiscroll.com/demos/f4.png'
  }
]

const shifts = ref([
  {
    start: '2026-10-21T07:00',
    end: '2026-10-21T13:00',
    title: '07:00 - 13:00',
    resource: 1,
    slot: 1
  },
  {
    start: '2026-10-21T07:00',
    end: '2026-10-21T13:00',
    title: '07:00 - 13:00',
    resource: 7,
    slot: 1
  },
  {
    start: '2026-10-21T12:00',
    end: '2026-10-21T18:00',
    title: '12:00 - 18:00',
    resource: 3,
    slot: 2
  },
  {
    start: '2026-10-21T12:00',
    end: '2026-10-21T18:00',
    title: '12:00 - 18:00',
    resource: 8,
    slot: 2
  },
  {
    start: '2026-10-22T07:00',
    end: '2026-10-22T13:00',
    title: '07:00 - 13:00',
    resource: 4,
    slot: 1
  },
  // Showing partial data. Download full source.
])

const mySlots = [
  { id: 1, name: 'Morning' },
  { id: 2, name: 'Afternoon' }
]

const myInvalids = [
  {
    start: '2026-10-26T00:00',
    end: '2026-10-26T23:59',
    resource: 4,
    slot: 1
  },
  {
    start: '2026-10-24T00:00',
    end: '2026-10-24T23:59',
    resource: 2,
    slot: 2
  }
]

const myView = {
  timeline: {
    type: 'week',
    eventDisplay: 'fill',
    startDay: 1,
    endDay: 5
  }
}

function getShiftTimes(event) {
  const d = event.start
  const start = new Date(d.getFullYear(), d.getMonth(), d.getDate(), event.slot === 1 ? 7 : 12)
  const end = new Date(d.getFullYear(), d.getMonth(), d.getDate(), event.slot === 1 ? 13 : 18)

  return {
    title: formatDate('HH:mm', start) + ' - ' + formatDate('HH:mm', end),
    start: start,
    end: end
  }
}

function fillPopup(event, edit) {
  isEdit.value = edit
  minTime.value = event.slot === 1 ? '07:00' : '12:00'
  maxTime.value = event.slot === 1 ? '13:00' : '18:00'
  shift.value = event
  shiftDates.value = [new Date(event.start), new Date(event.end)]
  shiftNotes.value = event.notes
}

function deleteEvent(event) {
  shifts.value = shifts.value.filter((item) => item.id !== event.id)
  deletedShift.value = event
  isSnackbarOpen.value = true
}

function updateEvent(event) {
  const index = shifts.value.findIndex((s) => s.id === event.id)
  const newShifts = [...shifts.value]

  newShifts.splice(index, 1, event)

  shifts.value = newShifts
}

function saveEvent(event) {
  const dates = shiftDates.value
  const start = dates[0]
  const end = dates[1] ? dates[1] : dates[0]

  const shiftStart = new Date(event.start)
  const shiftEnd = new Date(event.end)

  shiftStart.setHours(start.getHours(), start.getMinutes(), 0, 0)
  shiftEnd.setHours(end.getHours(), end.getMinutes(), 0, 0)

  event.start = shiftStart
  event.end = shiftEnd
  event.title = formatDate('HH:mm', start) + ' - ' + formatDate('HH:mm', end)
  event.notes = shiftNotes.value

  if (isEdit.value) {
    updateEvent(event)
  } else {
    // Add the new event to the list
    shifts.value = [...shifts.value, event]
  }
  isPopupOpen.value = false
}

function handleEventClick(args) {
  const event = args.event
  const resource = args.resourceObj
  const slot = args.slotObj

  fillPopup(event, true)
  popupButtons.value = [
    'cancel',
    {
      handler: () => {
        saveEvent(event)
      },
      keyCode: 'enter',
      text: 'Save',
      cssClass: 'mbsc-popup-button-primary'
    }
  ]
  headerPrimary.value = 'Edit ' + resource.name + "'s hours"
  headerDate.value =
    formatDate('DDDD', new Date(event.start)) +
    ' ' +
    slot.name +
    ', ' +
    formatDate('D MMMM YYYY', new Date(event.start))
  isPopupOpen.value = true
}

function handleEventCreated(args) {
  const event = args.event
  const slot = args.slotObj

  fillPopup(event, false)
  popupButtons.value = [
    'cancel',
    {
      handler: () => {
        saveEvent(event)
      },
      keyCode: 'enter',
      text: 'Add',
      cssClass: 'mbsc-popup-button-primary'
    }
  ]
  headerPrimary.value = 'New shift'
  headerDate.value =
    formatDate('DDDD', new Date(event.start)) +
    ' ' +
    slot.name +
    ', ' +
    formatDate('D MMMM YYYY', new Date(event.start))
  isPopupOpen.value = true
}

function handleEventCreateFailed() {
  toastMessage.value = "Can't create shift"
  isToastOpen.value = true
}

function handleEventDeleted(args) {
  deleteEvent(args.event)
}

function handleEventUpdated(args) {
  const shift = args.event
  if (shift.slot !== args.oldEvent.slot) {
    const data = getShiftTimes(shift)
    shift.start = data.start
    shift.end = data.end
    shift.title = data.title
    updateEvent(shift)
  }
}

function handleEventUpdateFailed() {
  toastMessage.value = "Can't move shift"
  isToastOpen.value = true
}

function handlePopupClose() {
  if (!isEdit.value) {
    // Remove event if popup is cancelled
    shifts.value = [...shifts.value]
  }
  isPopupOpen.value = false
}

function handleShiftDeleteClick() {
  deleteEvent(shift.value)
  isPopupOpen.value = false
}
</script>

<template>
  <MbscEventcalendar
    cssClass="mds-employee-shifts"
    :clickToCreate="true"
    :dragToCreate="false"
    :dragToResize="false"
    :dragToMove="true"
    :data="shifts"
    :eventOverlap="false"
    :extendDefaultEvent="getShiftTimes"
    :invalid="myInvalids"
    :resources="staff"
    :slots="mySlots"
    :view="myView"
    @event-click="handleEventClick"
    @event-created="handleEventCreated"
    @event-create-failed="handleEventCreateFailed"
    @event-deleted="handleEventDeleted"
    @event-updated="handleEventUpdated"
    @event-update-failed="handleEventUpdateFailed"
  >
    <template #resource="resource">
      <div class="mbsc-flex">
        <img class="mds-employee-shifts-avatar" :alt="resource.name" :src="resource.img" />
        <div class="mds-employee-shifts-cont">
          <div class="mds-employee-shifts-name">{{ resource.name }}</div>
          <div class="mds-employee-shifts-title">{{ resource.title }}</div>
        </div>
      </div>
    </template>
  </MbscEventcalendar>
  <MbscPopup
    display="bottom"
    :buttons="popupButtons"
    :contentPadding="false"
    :fullScreen="true"
    :isOpen="isPopupOpen"
    :responsive="popupResponsive"
    :scrollLock="false"
    @close="handlePopupClose"
  >
    <template #header>
      <div class="mds-employee-shifts-header-primary">{{ headerPrimary }}</div>
      <div class="mds-employee-shifts-header-date">{{ headerDate }}</div>
    </template>
    <MbscDatepicker
      v-model="shiftDates"
      display="anchored"
      select="range"
      timeWheels="|h:mm A|"
      :controls="['time']"
      :minTime="minTime"
      :maxTime="maxTime"
      :showRangeLabels="false"
      :stepMinute="30"
      :startInput="startInput"
      :endInput="endInput"
      :touchUi="false"
    />
    <div class="mbsc-form-group">
      <MbscInput ref="startInput" label="Shift start" :dropdown="true" />
      <MbscInput ref="endInput" label="Shift end" :dropdown="true" />
    </div>
    <div class="mbsc-form-group">
      <MbscTextarea v-model="shiftNotes" label="Notes" />
    </div>
    <div v-if="isEdit" class="mbsc-button-group">
      <MbscButton
        className="mbsc-button-block"
        color="danger"
        variant="outline"
        @click="handleShiftDeleteClick"
      >
        Delete shift
      </MbscButton>
    </div>
  </MbscPopup>
  <MbscSnackbar
    message="Event deleted"
    :button="snackbarButton"
    :isOpen="isSnackbarOpen"
    @close="isSnackbarOpen = false"
  />
  <MbscToast :message="toastMessage" :isOpen="isToastOpen" @close="isToastOpen = false" />
</template>

<style>
.mds-employee-shifts-header-primary {
  font-size: 16px;
  line-height: 20px;
}

.mds-employee-shifts-header-date {
  font-size: 14px;
  font-weight: 600;
  line-height: 20px;
  opacity: 0.6;
}

.mds-employee-shifts .mbsc-timeline-resource-col {
  width: 200px;
}

.mds-employee-shifts .mbsc-schedule-event-inner {
  text-align: center;
  height: 36px;
  line-height: 36px;
}

.mds-employee-shifts-cont {
  padding: 0 7px;
}

.mds-employee-shifts-name {
  font-size: 14px;
  line-height: 24px;
}

.mds-employee-shifts-title {
  font-size: 12px;
  font-weight: 400;
  line-height: 16px;
}

.mds-employee-shifts-avatar {
  width: 40px;
  height: 40px;
}
</style>
```

### SFC TS

```vue
<script setup lang="ts">
import {
  formatDate,
  MbscButton,
  MbscDatepicker,
  MbscEventcalendar,
  MbscInput,
  MbscPopup,
  MbscSnackbar,
  MbscTextarea,
  MbscToast,
  setOptions
} from '@mobiscroll/vue'
import type {
  MbscCalendarEvent,
  MbscDatepickerValue,
  MbscEventcalendarView,
  MbscEventClickEvent,
  MbscEventCreatedEvent,
  MbscEventDeletedEvent,
  MbscEventUpdatedEvent,
  MbscPopupButton,
  MbscPopupOptions,
  MbscResource
} from '@mobiscroll/vue'
import { ref } from 'vue'

setOptions({
  theme: 'ios',
  themeVariant: 'light'
})

const deletedShift = ref<MbscCalendarEvent>()
const isEdit = ref(false)
const isPopupOpen = ref(false)
const isSnackbarOpen = ref(false)
const isToastOpen = ref(false)
const minTime = ref('')
const maxTime = ref('')
const headerPrimary = ref('')
const headerDate = ref('')
const popupButtons = ref<Array<MbscPopupButton | 'cancel'>>([])
const startInput = ref<typeof MbscInput>()
const endInput = ref<typeof MbscInput>()
const shift = ref<MbscCalendarEvent>()
const shiftDates = ref<MbscDatepickerValue>([])
const shiftNotes = ref('')
const toastMessage = ref('')

const popupResponsive: { [key: string]: MbscPopupOptions } = {
  medium: {
    display: 'center',
    fullScreen: false,
    touchUi: false,
    width: 400
  }
}

const snackbarButton = {
  action: () => {
    shifts.value = [...shifts.value, deletedShift.value!]
  },
  text: 'Undo'
}

const staff: MbscResource[] = [
  {
    id: 1,
    name: 'Ryan',
    color: '#e20000',
    title: 'Cloud System Engineer',
    img: 'https://img.mobiscroll.com/demos/m1.png'
  },
  {
    id: 2,
    name: 'Kate',
    color: '#60e81a',
    title: 'Help Desk Specialist',
    img: 'https://img.mobiscroll.com/demos/f1.png'
  },
  {
    id: 3,
    name: 'John',
    color: '#3ba7ff',
    title: 'Application Developer',
    img: 'https://img.mobiscroll.com/demos/m2.png'
  },
  {
    id: 4,
    name: 'Mark',
    color: '#e25dd2',
    title: 'Network Administrator',
    img: 'https://img.mobiscroll.com/demos/m3.png'
  },
  {
    id: 5,
    name: 'Sharon',
    color: '#f1e920',
    title: 'Data Quality Manager',
    img: 'https://img.mobiscroll.com/demos/f2.png'
  },
  {
    id: 6,
    name: 'Emma',
    color: '#1ac38d',
    title: 'Product Tactics Agent',
    img: 'https://img.mobiscroll.com/demos/f3.png'
  },
  {
    id: 7,
    name: 'Michael',
    color: '#f18a1a',
    title: 'UX Designer',
    img: 'https://img.mobiscroll.com/demos/m4.png'
  },
  {
    id: 8,
    name: 'Laura',
    color: '#7b61ff',
    title: 'DevOps Engineer',
    img: 'https://img.mobiscroll.com/demos/f4.png'
  }
]

const shifts = ref<MbscCalendarEvent[]>([
  {
    start: '2026-10-21T07:00',
    end: '2026-10-21T13:00',
    title: '07:00 - 13:00',
    resource: 1,
    slot: 1
  },
  {
    start: '2026-10-21T07:00',
    end: '2026-10-21T13:00',
    title: '07:00 - 13:00',
    resource: 7,
    slot: 1
  },
  {
    start: '2026-10-21T12:00',
    end: '2026-10-21T18:00',
    title: '12:00 - 18:00',
    resource: 3,
    slot: 2
  },
  {
    start: '2026-10-21T12:00',
    end: '2026-10-21T18:00',
    title: '12:00 - 18:00',
    resource: 8,
    slot: 2
  },
  {
    start: '2026-10-22T07:00',
    end: '2026-10-22T13:00',
    title: '07:00 - 13:00',
    resource: 4,
    slot: 1
  },
  // Showing partial data. Download full source.
])

const mySlots = [
  { id: 1, name: 'Morning' },
  { id: 2, name: 'Afternoon' }
]

const myInvalids = [
  {
    start: '2026-10-26T00:00',
    end: '2026-10-26T23:59',
    resource: 4,
    slot: 1
  },
  {
    start: '2026-10-24T00:00',
    end: '2026-10-24T23:59',
    resource: 2,
    slot: 2
  }
]

const myView: MbscEventcalendarView = {
  timeline: {
    type: 'week',
    eventDisplay: 'fill',
    startDay: 1,
    endDay: 5
  }
}

function getShiftTimes(event: MbscCalendarEvent) {
  const d = event.start as Date
  const start = new Date(d.getFullYear(), d.getMonth(), d.getDate(), event.slot === 1 ? 7 : 12)
  const end = new Date(d.getFullYear(), d.getMonth(), d.getDate(), event.slot === 1 ? 13 : 18)

  return {
    title: formatDate('HH:mm', start) + ' - ' + formatDate('HH:mm', end),
    start: start,
    end: end
  }
}

function fillPopup(event: MbscCalendarEvent, edit: boolean) {
  isEdit.value = edit
  minTime.value = event.slot === 1 ? '07:00' : '12:00'
  maxTime.value = event.slot === 1 ? '13:00' : '18:00'
  shift.value = event
  shiftDates.value = [new Date(event.start as Date), new Date(event.end as Date)]
  shiftNotes.value = event.notes
}

function deleteEvent(event: MbscCalendarEvent) {
  shifts.value = shifts.value.filter((item) => item.id !== event.id)
  deletedShift.value = event
  isSnackbarOpen.value = true
}

function updateEvent(event: MbscCalendarEvent) {
  const index = shifts.value.findIndex((s) => s.id === event.id)
  const newShifts = [...shifts.value]

  newShifts.splice(index, 1, event)

  shifts.value = newShifts
}

function saveEvent(event: MbscCalendarEvent) {
  const dates = shiftDates.value as Date[]
  const start = dates[0]
  const end = dates[1] ? dates[1] : dates[0]

  const shiftStart = new Date(event.start as Date)
  const shiftEnd = new Date(event.end as Date)

  shiftStart.setHours(start.getHours(), start.getMinutes(), 0, 0)
  shiftEnd.setHours(end.getHours(), end.getMinutes(), 0, 0)

  event.start = shiftStart
  event.end = shiftEnd
  event.title = formatDate('HH:mm', start) + ' - ' + formatDate('HH:mm', end)
  event.notes = shiftNotes.value

  if (isEdit.value) {
    updateEvent(event)
  } else {
    // Add the new event to the list
    shifts.value = [...shifts.value, event]
  }
  isPopupOpen.value = false
}

function handleEventClick(args: MbscEventClickEvent) {
  const event = args.event
  const resource = args.resourceObj!
  const slot = args.slotObj!

  fillPopup(event, true)
  popupButtons.value = [
    'cancel',
    {
      handler: () => {
        saveEvent(event)
      },
      keyCode: 'enter',
      text: 'Save',
      cssClass: 'mbsc-popup-button-primary'
    }
  ]
  headerPrimary.value = 'Edit ' + resource.name + "'s hours"
  headerDate.value =
    formatDate('DDDD', new Date(event.start as Date)) +
    ' ' +
    slot.name +
    ', ' +
    formatDate('D MMMM YYYY', new Date(event.start as Date))
  isPopupOpen.value = true
}

function handleEventCreated(args: MbscEventCreatedEvent) {
  const event = args.event
  const slot = args.slotObj!

  fillPopup(event, false)
  popupButtons.value = [
    'cancel',
    {
      handler: () => {
        saveEvent(event)
      },
      keyCode: 'enter',
      text: 'Add',
      cssClass: 'mbsc-popup-button-primary'
    }
  ]
  headerPrimary.value = 'New shift'
  headerDate.value =
    formatDate('DDDD', new Date(event.start as Date)) +
    ' ' +
    slot.name +
    ', ' +
    formatDate('D MMMM YYYY', new Date(event.start as Date))
  isPopupOpen.value = true
}

function handleEventCreateFailed() {
  toastMessage.value = "Can't create shift"
  isToastOpen.value = true
}

function handleEventDeleted(args: MbscEventDeletedEvent) {
  deleteEvent(args.event)
}

function handleEventUpdated(args: MbscEventUpdatedEvent) {
  const shift = args.event
  if (shift.slot !== args.oldEvent!.slot) {
    const data = getShiftTimes(shift)
    shift.start = data.start
    shift.end = data.end
    shift.title = data.title
    updateEvent(shift)
  }
}

function handleEventUpdateFailed() {
  toastMessage.value = "Can't move shift"
  isToastOpen.value = true
}

function handlePopupClose() {
  if (!isEdit.value) {
    // Remove event if popup is cancelled
    shifts.value = [...shifts.value]
  }
  isPopupOpen.value = false
}

function handleShiftDeleteClick() {
  deleteEvent(shift.value!)
  isPopupOpen.value = false
}
</script>

<template>
  <MbscEventcalendar
    cssClass="mds-employee-shifts"
    :clickToCreate="true"
    :dragToCreate="false"
    :dragToResize="false"
    :dragToMove="true"
    :data="shifts"
    :eventOverlap="false"
    :extendDefaultEvent="getShiftTimes"
    :invalid="myInvalids"
    :resources="staff"
    :slots="mySlots"
    :view="myView"
    @event-click="handleEventClick"
    @event-created="handleEventCreated"
    @event-create-failed="handleEventCreateFailed"
    @event-deleted="handleEventDeleted"
    @event-updated="handleEventUpdated"
    @event-update-failed="handleEventUpdateFailed"
  >
    <template #resource="resource">
      <div class="mbsc-flex">
        <img class="mds-employee-shifts-avatar" :alt="resource.name" :src="resource.img" />
        <div class="mds-employee-shifts-cont">
          <div class="mds-employee-shifts-name">{{ resource.name }}</div>
          <div class="mds-employee-shifts-title">{{ resource.title }}</div>
        </div>
      </div>
    </template>
  </MbscEventcalendar>
  <MbscPopup
    display="bottom"
    :buttons="popupButtons"
    :contentPadding="false"
    :fullScreen="true"
    :isOpen="isPopupOpen"
    :responsive="popupResponsive"
    :scrollLock="false"
    @close="handlePopupClose"
  >
    <template #header>
      <div class="mds-employee-shifts-header-primary">{{ headerPrimary }}</div>
      <div class="mds-employee-shifts-header-date">{{ headerDate }}</div>
    </template>
    <MbscDatepicker
      v-model="shiftDates"
      display="anchored"
      select="range"
      timeWheels="|h:mm A|"
      :controls="['time']"
      :minTime="minTime"
      :maxTime="maxTime"
      :showRangeLabels="false"
      :stepMinute="30"
      :startInput="startInput"
      :endInput="endInput"
      :touchUi="false"
    />
    <div class="mbsc-form-group">
      <MbscInput ref="startInput" label="Shift start" :dropdown="true" />
      <MbscInput ref="endInput" label="Shift end" :dropdown="true" />
    </div>
    <div class="mbsc-form-group">
      <MbscTextarea v-model="shiftNotes" label="Notes" />
    </div>
    <div v-if="isEdit" class="mbsc-button-group">
      <MbscButton
        className="mbsc-button-block"
        color="danger"
        variant="outline"
        @click="handleShiftDeleteClick"
      >
        Delete shift
      </MbscButton>
    </div>
  </MbscPopup>
  <MbscSnackbar
    message="Event deleted"
    :button="snackbarButton"
    :isOpen="isSnackbarOpen"
    @close="isSnackbarOpen = false"
  />
  <MbscToast :message="toastMessage" :isOpen="isToastOpen" @close="isToastOpen = false" />
</template>

<style>
.mds-employee-shifts-header-primary {
  font-size: 16px;
  line-height: 20px;
}

.mds-employee-shifts-header-date {
  font-size: 14px;
  font-weight: 600;
  line-height: 20px;
  opacity: 0.6;
}

.mds-employee-shifts .mbsc-timeline-resource-col {
  width: 200px;
}

.mds-employee-shifts .mbsc-schedule-event-inner {
  text-align: center;
  height: 36px;
  line-height: 36px;
}

.mds-employee-shifts-cont {
  padding: 0 7px;
}

.mds-employee-shifts-name {
  font-size: 14px;
  line-height: 24px;
}

.mds-employee-shifts-title {
  font-size: 12px;
  font-weight: 400;
  line-height: 16px;
}

.mds-employee-shifts-avatar {
  width: 40px;
  height: 40px;
}
</style>
```

## Implementation notes

- Use the timeline view in summary mode with `eventDisplay: 'fill'` to render shift events within resource rows and named slot buckets. Set `startDay: 1` and `endDay: 5` to show only work days.
- Define the Morning and Afternoon shift buckets with the `slots` option. Each slot object needs an `id` and a `name`, and each event references its slot via the `slot` field.
- Give each employee a `color` property on the resource object. Mobiscroll automatically applies the resource color to its events, so no extra event-level styling is needed.
- Use `renderResource` (Angular: `resourceTemplate`, Vue: `#resource` slot) to display a custom resource row with the employee's avatar image, name, and job title.
- Enable `clickToCreate` so users can create a new shift by clicking an empty cell. Set `dragToMove: true` so existing shifts can be repositioned by dragging, and set `dragToCreate: false` and `dragToResize: false` to prevent accidental event creation by drag and to keep shift boundaries tied to the slot definition.
- Use `extendDefaultEvent` to set the correct start and end times on every newly created event based on which slot it lands in - for example, Morning maps to 07:00-13:00 and Afternoon to 12:00-18:00.
- Set `eventOverlap: false` to block placing a second shift for the same employee in the same slot on the same day.
- Use the `invalid` option to mark specific resource-slot-day combinations where shifts cannot be created or moved, such as an employee's day off or a slot they are not eligible for.
- Use `onEventCreated` to intercept the newly created event, populate the popup with the slot-derived time range, and open the add form. The `slotObj` in the event args provides the slot name for the popup header.
- Use `onEventClick` to open the edit form for an existing shift. Read `resourceObj` and `slotObj` from the event args to build a descriptive popup header showing the employee name, day, and slot.
- Use `onEventUpdated` to detect when a shift has been dragged from one slot to another. When the slot changes, recalculate and overwrite the event's start, end, and title to match the new slot's fixed time range.
- Use `onEventCreateFailed` and `onEventUpdateFailed` to show a toast message when a shift cannot be placed or moved, for example because the target cell is marked invalid or blocked by `eventOverlap`.
- Build the add/edit popup with the `Popup` component. Use the `responsive` option to switch from a full-screen bottom sheet on mobile to a centered fixed-width dialog on larger screens.
- Render a custom popup header with a primary line - such as "Edit Ryan's hours" or "New shift" - and a secondary line with the day name, slot name, and date.
- Inside the popup, use a `Datepicker` in range mode with `controls={['time']}`. Connect it to two `Input` fields via `startInput` and `endInput` refs so the start and end times appear as separate labelled inputs that open the shared range picker. Constrain the allowed range with `minTime` and `maxTime` derived from the current slot.
- For deletion, add a Delete button to the edit popup that removes the shift immediately and closes the popup, then shows a `Snackbar` with an Undo action that restores the deleted shift if the user reverses the action.

## Run it in your project

1. If not signed up yet, or don't own a license, you can [start a free trial](https://mobiscroll.com/starttrial).

2. Clone the repository:

   ```
   git clone https://github.com/acidb/mobiscroll-demos-vue.git
   ```

3. If not installed yet, install the Mobiscroll CLI from NPM:

   ```
   npm install -g @mobiscroll/cli
   ```

4. Config Mobiscroll - open a terminal window in the root directory and run:

   ```
   mobiscroll config vue
   ```

5. Install dependencies - open a terminal window in the root directory and run:

   ```
   npm install
   ```

6. Run the project:

   ```
   npm run dev
   ```

On a trial, add `--trial` to the config command: `mobiscroll config vue --trial`.

Running a demo locally requires a Mobiscroll trial or a license; setup is per framework at https://mobiscroll.com/docs/vue/getting-started/installation

## Related demos

- Next: [Work Order Scheduling](https://demo.mobiscroll.com/vue/timeline/work-order-scheduling.md) - Schedule work orders across grouped resources — drivers, trucks, and contractors — on a Vue timeline.
