---
title: "Vue Scheduler 24-Hour Rotating Crew Shift Rota Example | Mobiscroll"
component: "Scheduler"
framework: "Vue"
library_version: "6.1.5"
url: "https://demo.mobiscroll.com/vue/scheduler/24-hour-manufacturing-shift-rota-planning"
---

# 24-Hour Rotating Crew Shift Rota - Vue Scheduler demo

Build a 24-hour rotating crew rota with the Vue Scheduler — three crews, three shifts, and automatic conflict resolution when shifts move. For Vue and Nuxt.

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

## Description

The scheduler can be configured for industrial or service environments requiring 24/7 coverage, such as manufacturing lines or critical infrastructure maintenance. This setup uses three resources (Crew A, Crew B, Crew C) to demonstrate a cyclical, rotating roster, like the 3-Crew, 3-Shift Rota.

To visualize the continuous operation and manage shifts that span midnight, the scheduler view is configured with a custom, 24-hour cycle, starting at 06:00 and ending at 06:00 the following day. This is achieved using the custom day-offset feature added to the `startTime` (e.g., '20:00-1') and `endTime` (e.g., '08:00+1') properties under the [`view`](https://mobiscroll.com/docs/vue/eventcalendar/api#opt-view) option, which initiates the view relative to the previous or next calendar day. Furthermore, to ensure compliance with 8-hour shift lengths and prevent invalid scheduling, the drag-and-drop functionality is constrained by setting the [`dragTimeStep`](https://mobiscroll.com/docs/vue/eventcalendar/api#opt-dragTimeStep) to 8 hours, requiring all events to be moved only in 8-hour increments and anchoring the alignment with the [`dragTimeStepBase: 'viewStart'`](https://mobiscroll.com/docs/vue/eventcalendar/api#opt-dragTimeStepBase) option. The drag-and-drop logic is implemented for automatic conflict resolution: when an event is dragged to a different time or resource column within the same day, it automatically updates and shifts any conflicting events (those sharing the same time slot or resource column) to maintain scheduling integrity and prevent overlaps.

## What this demo shows

- A desktop weekly scheduler for 24-hour shift planning, with a fixed week strip, repeated resources for each day, and a vertically scrollable time grid running from 6 AM to 6 AM the following day.
- **Header navigation** The month and year label in the top left opens date navigation, while the blue previous and next arrows and the Today button on the right make it easy to move between weeks and jump back to the current day.
- **Week strip**: The fixed strip below the header displays the days in the selected week, with the current date highlighted by a blue circle.
- **Resources**: Each day shows the same three resources: Crew A, Crew B, and Crew C.
- **Resource columns**: Each crew has its own column within the day column, keeping resources grouped by date.
- **Time grid**: The scheduler uses a custom 24-hour range from 6 AM to 6 AM the next day, with midnight marked by a stronger horizontal line.
- **Scrolling behavior**: The time grid scrolls vertically through the hours of the selected week.
- **Timed events**: Shifts appear as colored event cards with a colored stripe, bold shift title, and start and end time. Morning Shift runs from 6 AM to 2 PM, Afternoon Shift from 2 PM to 10 PM, and Night Shift from 10 PM to 6 AM.
- **Event overlap**: Shift events cannot overlap.
- **Shift coverage**: By default, each resource has a scheduled shift. Users can delete, create, or move shifts.
- **Missing shifts**: When a shift is deleted, the affected resource column is highlighted with a red background to show that no shift is scheduled for that resource.
- **Available shift slots**: When a resource has no shift, hovering over an available 8-hour shift period highlights the slot in green.
- **Shift repositioning**: Moving a shift can automatically update another shift on the same day to preserve coverage.
- **Current time**: A blue current-time line appears across the time grid.
- **Hover behavior**: Hovering over the time grid shows a time indicator that follows the cursor in 8-hour increments.
- **Event interaction**: Hovering over or selecting an event highlights it.
- **Toast messages**: Clicking an occupied time slot or trying to create a shift when every slot is already assigned shows an "Already assigned" toast at the bottom center of the scheduler. Deleting a shift shows a deletion toast with the shift name.

## Best for

- **Manufacturing and production facilities**: Schedule morning, afternoon, and night shifts across production lines, departments, or work crews while maintaining continuous coverage.
- **Warehousing and logistics operations**: Coordinate warehouse teams, loading crews, dispatch staff, and distribution personnel across multiple shifts.
- **Construction and field service teams**: Manage rotating crews, subcontractors, inspectors, and site personnel working across different schedules and locations.
- **Security and monitoring services**: Plan guard rotations, patrol schedules, control room operators, and emergency response teams around the clock.
- **Healthcare and care facilities**: Organize nurse rotations, support staff schedules, and departmental coverage across day, evening, and overnight shifts.
- **Hospitality and tourism businesses**: Schedule hotel staff, front desk teams, housekeeping crews, event staff, and service personnel.
- **Retail operations**: Manage store associates, supervisors, cashiers, and department teams while balancing staffing levels during peak and off-peak hours.
- **Transportation and aviation services**: Coordinate drivers, dispatchers, pilots, cabin crews, ground staff, and maintenance teams.
- **Customer support and contact centers**: Plan agent coverage across time zones, shifts, and support channels to meet service level requirements.
- **Public services and emergency organizations**: Schedule firefighters, police officers, emergency medical personnel, and municipal service teams.
- **Media, broadcasting, and operations centers**: Coordinate production crews, technical operators, studio staff, and live-event teams that require continuous coverage.
- **IT and DevOps teams**: Manage on-call rotations, support shifts, operations coverage, and incident response schedules for 24/7 systems.

## 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
- [`cssClass`](https://mobiscroll.com/docs/vue/eventcalendar/api#opt-cssClass) - option
- [`data`](https://mobiscroll.com/docs/vue/eventcalendar/api#opt-data) - option
- [`colors`](https://mobiscroll.com/docs/vue/eventcalendar/api#opt-colors) - option
- [`dragToMove`](https://mobiscroll.com/docs/vue/eventcalendar/api#opt-dragToMove) - option
- [`dragToCreate`](https://mobiscroll.com/docs/vue/eventcalendar/api#opt-dragToCreate) - option
- [`clickToCreate`](https://mobiscroll.com/docs/vue/eventcalendar/api#opt-clickToCreate) - option
- [`dragToResize`](https://mobiscroll.com/docs/vue/eventcalendar/api#opt-dragToResize) - option
- [`dragTimeStep`](https://mobiscroll.com/docs/vue/eventcalendar/api#opt-dragTimeStep) - option
- [`eventDelete`](https://mobiscroll.com/docs/vue/eventcalendar/api#opt-eventDelete) - option
- [`eventOverlap`](https://mobiscroll.com/docs/vue/eventcalendar/api#opt-eventOverlap) - option
- [`extendDefaultEvent`](https://mobiscroll.com/docs/vue/eventcalendar/api#opt-extendDefaultEvent) - option
- [`cellHoverIn`](https://mobiscroll.com/docs/vue/eventcalendar/api#event-onCellHoverIn) - event
- [`cellHoverOut`](https://mobiscroll.com/docs/vue/eventcalendar/api#event-onCellHoverOut) - event
- [`eventCreate`](https://mobiscroll.com/docs/vue/eventcalendar/api#event-onEventCreate) - event
- [`eventClick`](https://mobiscroll.com/docs/vue/eventcalendar/api#event-onEventClick) - event
- [`eventDelete`](https://mobiscroll.com/docs/vue/eventcalendar/api#event-onEventDelete) - event
- [`eventDragStart`](https://mobiscroll.com/docs/vue/eventcalendar/api#event-onEventDragStart) - event
- [`eventUpdateFailed`](https://mobiscroll.com/docs/vue/eventcalendar/api#event-onEventUpdateFailed) - event
- [`eventUpdate`](https://mobiscroll.com/docs/vue/eventcalendar/api#event-onEventUpdate) - event
- [`groupBy`](https://mobiscroll.com/docs/vue/eventcalendar/api#opt-groupBy) - option
- [`resources`](https://mobiscroll.com/docs/vue/eventcalendar/api#opt-resources) - option
- [`view`](https://mobiscroll.com/docs/vue/eventcalendar/api#opt-view) - option
- [`close`](https://mobiscroll.com/docs/vue/notifications/toast#event-onClose) - event

## Code

### SFC JS

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

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

const morningColor = '#4a8c4d'
const afternoonColor = '#f87c6b'
const nightColor = '#8567AD'

const shifts = {
  morning: { startHour: 6, endHour: 14, title: 'Morning Shift', color: morningColor },
  afternoon: { startHour: 14, endHour: 22, title: 'Afternoon Shift', color: afternoonColor },
  night: { startHour: 22, endHour: 6, nextDay: true, title: 'Night Shift', color: nightColor }
}

const getShiftByHour = (hour) => {
  if (hour >= 6 && hour < 14) {
    return shifts.morning
  }
  if (hour >= 14 && hour < 22) {
    return shifts.afternoon
  }
  if (hour >= 22 || hour < 6) {
    return shifts.night
  }
  return shifts.afternoon
}

const calculateStart = (start) => {
  const d = new Date(start)
  const originalHour = d.getHours()
  const shift = getShiftByHour(originalHour)
  d.setHours(shift.startHour, 0, 0, 0)
  if (shift.startHour === 22 && originalHour < 6) {
    d.setDate(d.getDate() - 1)
  }
  return d
}

const calculateEnd = (start) => {
  const d = new Date(start)
  const startHour = d.getHours()
  const shift = getShiftByHour(d.getHours())
  d.setHours(shift.endHour, 0, 0, 0)
  if (shift.nextDay && startHour === 22) {
    d.setDate(d.getDate() + 1)
  }
  return d
}

const getTitle = (startHours) => getShiftByHour(startHours).title

const getColor = (startHours) => getShiftByHour(startHours).color

const initialEvents = [
  {
    id: 1,
    resource: 'A',
    title: 'Morning Shift',
    start: '2026-10-23T06:00',
    end: '2026-10-23T14:00',
    color: morningColor
  },
  {
    id: 2,
    resource: 'B',
    title: 'Afternoon Shift',
    start: '2026-10-23T14:00',
    end: '2026-10-23T22:00',
    color: afternoonColor
  },
  {
    id: 3,
    resource: 'C',
    title: 'Night Shift',
    start: '2026-10-23T22:00',
    end: '2026-10-24T06:00',
    color: nightColor
  },
  {
    id: 4,
    resource: 'A',
    title: 'Morning Shift',
    start: '2026-10-24T06:00',
    end: '2026-10-24T14:00',
    color: morningColor
  },
  {
    id: 5,
    resource: 'B',
    title: 'Afternoon Shift',
    start: '2026-10-24T14:00',
    end: '2026-10-24T22:00',
    color: afternoonColor
  },
  {
    id: 6,
    resource: 'C',
    title: 'Night Shift',
    start: '2026-10-24T22:00',
    end: '2026-10-25T06:00',
    color: nightColor
  },
  // Showing partial data. Download full source.
]

const resources = [
  { id: 'A', name: 'Crew A' },
  { id: 'B', name: 'Crew B' },
  { id: 'C', name: 'Crew C' }
]

const view = {
  scheduler: {
    type: 'week',
    allDay: false,
    startTime: '06:00',
    endTime: '06:00+1',
    timeCellStep: 120,
    timeLabelStep: 120
  }
}

const colors = ref([])
const redResources = ref({})
const draggedEventStart = ref(null)
const draggedEventEnd = ref(null)
const draggedEventResource = ref(null)
const availableSlotOnHover = ref(null)
const isToastOpen = ref(false)
const toastMessage = ref('')
const myEvents = ref(initialEvents)

const getAvailableSlots = (resourceId, dayStart) => {
  const dayEnd = new Date(dayStart)
  dayEnd.setDate(dayEnd.getDate() + 1)
  dayEnd.setHours(6, 0, 0, 0)
  const dayEvents = myEvents.value.filter((e) => {
    const eStart = new Date(e.start)
    return eStart >= dayStart && eStart < dayEnd
  })
  const slots = { morning: true, afternoon: true, night: true }
  dayEvents.forEach((e) => {
    if (e.resource === resourceId) {
      slots.morning = slots.afternoon = slots.night = false
    } else {
      const startHour = new Date(e.start).getHours()
      if (startHour === 6) {
        slots.morning = false
      } else if (startHour === 14) {
        slots.afternoon = false
      } else if (startHour === 22) {
        slots.night = false
      }
    }
  })
  return slots
}

const clearColorsForResource = (currentColors, resourceId, date) => {
  const base = new Date(date)
  base.setHours(0, 0, 0, 0)
  const baseTime = +base
  return currentColors.filter((c) => {
    if (c.resource === resourceId) {
      const cDate = new Date(c.start)
      cDate.setHours(0, 0, 0, 0)
      if (+cDate === baseTime) {
        const hour = new Date(c.start).getHours()
        if (hour === 6 || hour === 14 || hour === 22) {
          return false
        }
      }
    }
    return true
  })
}

const handleCellHoverIn = (args) => {
  const hoveredDate = new Date(args.date)
  const shift = getShiftByHour(hoveredDate.getHours())
  if (!shift) return

  const dayStart = new Date(args.date)
  dayStart.setHours(6, 0, 0, 0)
  const availableSlots = getAvailableSlots(args.resource.id, dayStart)
  const slotKey = Object.keys(availableSlots).find(
    (key) => availableSlots[key] && shifts[key].startHour === shift.startHour
  )
  if (slotKey) {
    const startTime = new Date(dayStart)
    startTime.setHours(shifts[slotKey].startHour, 0, 0, 0)
    const endTime = new Date(+startTime + 8 * 60 * 60 * 1000 - 1)
    const slot = {
      background: '#c1ffe180',
      cssClass: 'available-slot mbsc-font-icon mbsc-icon-plus',
      start: +startTime + 1,
      end: endTime,
      resource: args.resource.id
    }
    availableSlotOnHover.value = slot
    colors.value = [...colors.value, slot]
  }
}

const handleCellHoverOut = () => {
  if (availableSlotOnHover.value) {
    colors.value = colors.value.filter((c) => c !== availableSlotOnHover.value)
    availableSlotOnHover.value = null
  }
}

const handleEventCreate = (args) => {
  const event = { ...args.event, id: Date.now() }
  const dayStart = new Date(event.start)
  dayStart.setHours(6, 0, 0, 0)
  const dayEnd = new Date(dayStart)
  dayEnd.setDate(dayEnd.getDate() + 1)
  dayEnd.setHours(6, 0, 0, 0)
  const dayEvents = myEvents.value.filter((e) => {
    const eStart = new Date(e.start)
    return eStart >= dayStart && eStart < dayEnd
  })
  const conflict = dayEvents.find(
    (ev) => ev.resource === event.resource || +new Date(ev.start) === +new Date(event.start)
  )
  if (conflict) {
    toastMessage.value = 'Already assigned'
    isToastOpen.value = true
    return false
  }
  colors.value = clearColorsForResource(colors.value, event.resource, event.start)
  const day = new Date(event.start)
  day.setHours(0, 0, 0, 0)
  redResources.value = { ...redResources.value, [event.resource + day.toISOString()]: false }
  availableSlotOnHover.value = null
  myEvents.value = [...myEvents.value, event]
}

const handleEventClick = () => {
  toastMessage.value = 'Already assigned'
  isToastOpen.value = true
}

const handleEventDelete = (args) => {
  myEvents.value = myEvents.value.filter((e) => e.id !== args.event.id)
  const colorStart = new Date(args.event.start)
  colorStart.setHours(6, 0, 0, 0)
  const colorEnd = new Date(args.event.start)
  colorEnd.setDate(colorEnd.getDate() + 1)
  colorEnd.setHours(6, 0, 0, 0)
  colors.value = [
    ...colors.value,
    { start: colorStart, end: colorEnd, background: '#ffdbd280', resource: args.event.resource }
  ]
  const resource = args.event.resource
  const day = new Date(args.event.start)
  day.setHours(0, 0, 0, 0)
  redResources.value = { ...redResources.value, [resource + day.toISOString()]: true }
  toastMessage.value = args.event.title + ' deleted'
  isToastOpen.value = true
}

const handleEventDragStart = (args) => {
  draggedEventStart.value = args.event.start
  draggedEventEnd.value = args.event.end
  draggedEventResource.value = args.event.resource
}

const handleEventUpdateFailed = () => {
  toastMessage.value = 'Already assigned'
  isToastOpen.value = true
}

const handleEventUpdate = (args) => {
  const event = args.event
  const dayStart = new Date(event.start)
  dayStart.setHours(6, 0, 0, 0)
  const dragStartDay = new Date(draggedEventStart.value)
  dragStartDay.setHours(6, 0, 0, 0)
  if (+dayStart !== +dragStartDay) {
    return false
  }

  const dayEnd = new Date(dayStart)
  dayEnd.setDate(dayEnd.getDate() + 1)
  dayEnd.setHours(6, 0, 0, 0)
  const dayEvents = myEvents.value.filter((e) => {
    const eStart = new Date(e.start)
    return eStart >= dayStart && eStart < dayEnd
  })
  const conflicts = dayEvents.filter(
    (ev) =>
      ev.id !== event.id &&
      (ev.resource === event.resource || +new Date(ev.start) === +new Date(event.start))
  )
  const evStart = new Date(event.start)
  event.title = getTitle(evStart.getHours())
  event.color = getColor(evStart.getHours())

  if (conflicts.length) {
    let shouldReturn = false
    conflicts.forEach((sh, i) => {
      if (sh.resource === event.resource) {
        if (+event.start === +new Date(sh.start)) {
          shouldReturn = true
        }
        sh.resource = draggedEventResource.value
        conflicts[i] = sh
      } else {
        sh.start = draggedEventStart.value
        sh.end = draggedEventEnd.value
        const start = new Date(draggedEventStart.value)
        sh.title = getTitle(start.getHours())
        sh.color = getColor(start.getHours())
        conflicts[i] = sh
      }
    })
    if (shouldReturn) {
      return false
    }
    myEvents.value = myEvents.value.map((e) => {
      const updated = [...conflicts, event].find((u) => u.id === e.id)
      return updated || e
    })
  } else {
    if (dayEvents.some((ev) => ev.id === event.id)) {
      myEvents.value = myEvents.value.map((e) => (e.id === event.id ? event : e))
    }
  }

  colors.value = clearColorsForResource(colors.value, event.resource, event.start)
  const day = new Date(event.start)
  day.setHours(0, 0, 0, 0)
  redResources.value = { ...redResources.value, [event.resource + day.toISOString()]: false }
  if (!conflicts.length && event.resource !== draggedEventResource.value) {
    const colorEnd = new Date(dayStart)
    colorEnd.setDate(colorEnd.getDate() + 1)
    colorEnd.setHours(6, 0, 0, 0)
    colors.value = [
      ...colors.value,
      { start: dayStart, resource: args.oldEvent.resource, background: '#ffdbd280', end: colorEnd }
    ]
    redResources.value = {
      ...redResources.value,
      [args.oldEvent.resource + day.toISOString()]: true
    }
  }
  draggedEventStart.value = null
  draggedEventEnd.value = null
  draggedEventResource.value = null
}

const extendDefaultEvent = (args) => {
  const start = new Date(args.start)
  const newStart = calculateStart(start)
  const newEnd = calculateEnd(start)
  const title = getTitle(newStart.getHours())
  const color = getColor(newStart.getHours())
  return {
    title: title,
    start: newStart,
    end: newEnd,
    color: color
  }
}

const handleToastClose = () => {
  isToastOpen.value = false
}
</script>

<template>
  <div>
    <MbscEventcalendar
      cssClass="mds-24-hour-manufacturing-calendar"
      :data="myEvents"
      :colors="colors"
      :dragToMove="true"
      :dragToCreate="false"
      :clickToCreate="'single'"
      :dragToResize="false"
      :dragTimeStep="480"
      :dragTimeStepBase="'viewStart'"
      :eventDelete="true"
      :eventOverlap="false"
      :extendDefaultEvent="extendDefaultEvent"
      @cellHoverIn="handleCellHoverIn"
      @cellHoverOut="handleCellHoverOut"
      @eventCreate="handleEventCreate"
      @eventClick="handleEventClick"
      @eventDelete="handleEventDelete"
      @eventDragStart="handleEventDragStart"
      @eventUpdateFailed="handleEventUpdateFailed"
      @eventUpdate="handleEventUpdate"
      :groupBy="'date'"
      :resources="resources"
      :view="view"
    >
      <template #resource="resource, day">
        <div
          :style="
            redResources[resource.id + day.toISOString()]
              ? { color: '#a65037', background: '#ffdbd280', margin: '-0.5em', padding: '0.5em' }
              : {}
          "
        >
          {{ resource.name }}
        </div>
      </template>
    </MbscEventcalendar>
    <MbscToast :isOpen="isToastOpen" :message="toastMessage" @close="handleToastClose" />
  </div>
</template>

<style>
.mds-24-hour-manufacturing-calendar .mbsc-schedule-events {
  left: 0;
  right: 0;
}

.mds-24-hour-manufacturing-calendar .mbsc-schedule-time-wrapper {
  height: 2.5em;
}

.mds-24-hour-manufacturing-calendar .mbsc-schedule-item {
  height: 2.5em;
}

.mds-24-hour-manufacturing-calendar .mbsc-schedule-color.available-slot {
  z-index: auto;
}

.mds-24-hour-manufacturing-calendar .mbsc-schedule-color.available-slot .mbsc-schedule-color-text {
  position: absolute;
  z-index: 1;
  pointer-events: none;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 35px;
  height: 35px;
  border: none;
  border-radius: 50%;
  background: linear-gradient(135deg, #33333380, #00000080);
  box-sizing: border-box;
}

.mds-24-hour-manufacturing-calendar .mbsc-schedule-color.available-slot.mbsc-font-icon:before {
  position: absolute;
  z-index: 2;
  pointer-events: none;
  top: 50%;
  left: 50%;
  color: #e0fff0;
  font-size: 12px;
  opacity: 0.8;
  transform: translate(-50%, -50%);
}
</style>
```

### SFC TS

```vue
<script setup lang="ts">
import { MbscEventcalendar, MbscToast, setOptions } from '@mobiscroll/vue'
import type {
  MbscCalendarColor,
  MbscCalendarEvent,
  MbscCellHoverEvent,
  MbscEventcalendarView,
  MbscEventCreateEvent,
  MbscEventDeleteEvent,
  MbscEventDragEvent,
  MbscEventUpdateEvent,
  MbscNewEventData,
  MbscResource
} from '@mobiscroll/vue'
import { ref } from 'vue'

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

interface Shift {
  startHour: number
  endHour: number
  title: string
  color: string
  nextDay?: boolean
}

const morningColor = '#4a8c4d'
const afternoonColor = '#f87c6b'
const nightColor = '#8567AD'

const shifts = {
  morning: { startHour: 6, endHour: 14, title: 'Morning Shift', color: morningColor },
  afternoon: { startHour: 14, endHour: 22, title: 'Afternoon Shift', color: afternoonColor },
  night: { startHour: 22, endHour: 6, nextDay: true, title: 'Night Shift', color: nightColor }
}

const getShiftByHour = (hour: number): Shift => {
  if (hour >= 6 && hour < 14) {
    return shifts.morning
  }
  if (hour >= 14 && hour < 22) {
    return shifts.afternoon
  }
  if (hour >= 22 || hour < 6) {
    return shifts.night
  }
  return shifts.afternoon
}

const calculateStart = (start: Date | string) => {
  const d = new Date(start)
  const originalHour = d.getHours()
  const shift = getShiftByHour(originalHour)
  d.setHours(shift.startHour, 0, 0, 0)
  if (shift.startHour === 22 && originalHour < 6) {
    d.setDate(d.getDate() - 1)
  }
  return d
}

const calculateEnd = (start: Date | string) => {
  const d = new Date(start)
  const startHour = d.getHours()
  const shift = getShiftByHour(d.getHours())
  d.setHours(shift.endHour, 0, 0, 0)
  if (shift.nextDay && startHour === 22) {
    d.setDate(d.getDate() + 1)
  }
  return d
}

const getTitle = (startHours: number) => getShiftByHour(startHours).title

const getColor = (startHours: number) => getShiftByHour(startHours).color

const initialEvents: MbscCalendarEvent[] = [
  {
    id: 1,
    resource: 'A',
    title: 'Morning Shift',
    start: '2026-10-23T06:00',
    end: '2026-10-23T14:00',
    color: morningColor
  },
  {
    id: 2,
    resource: 'B',
    title: 'Afternoon Shift',
    start: '2026-10-23T14:00',
    end: '2026-10-23T22:00',
    color: afternoonColor
  },
  {
    id: 3,
    resource: 'C',
    title: 'Night Shift',
    start: '2026-10-23T22:00',
    end: '2026-10-24T06:00',
    color: nightColor
  },
  {
    id: 4,
    resource: 'A',
    title: 'Morning Shift',
    start: '2026-10-24T06:00',
    end: '2026-10-24T14:00',
    color: morningColor
  },
  {
    id: 5,
    resource: 'B',
    title: 'Afternoon Shift',
    start: '2026-10-24T14:00',
    end: '2026-10-24T22:00',
    color: afternoonColor
  },
  {
    id: 6,
    resource: 'C',
    title: 'Night Shift',
    start: '2026-10-24T22:00',
    end: '2026-10-25T06:00',
    color: nightColor
  },
  // Showing partial data. Download full source.
]

const resources: MbscResource[] = [
  { id: 'A', name: 'Crew' },
  { id: 'B', name: 'Crew B' },
  { id: 'C', name: 'Crew C' }
]

const view: MbscEventcalendarView = {
  scheduler: {
    type: 'week',
    allDay: false,
    startTime: '06:00',
    endTime: '06:00+1',
    timeCellStep: 120,
    timeLabelStep: 120
  }
}

const colors = ref<MbscCalendarColor[]>([])
const redResources = ref<Record<string, boolean>>({})
const draggedEventStart = ref<Date | null | undefined>(null)
const draggedEventEnd = ref<Date | null | undefined>(null)
const draggedEventResource = ref<string | number | (string | number)[] | null | undefined>(null)
const availableSlotOnHover = ref<MbscCalendarColor | null>(null)
const isToastOpen = ref<boolean>(false)
const toastMessage = ref<string>('')
const myEvents = ref<MbscCalendarEvent[]>(initialEvents)

const getAvailableSlots = (resourceId: string, dayStart: Date) => {
  const dayEnd = new Date(dayStart)
  dayEnd.setDate(dayEnd.getDate() + 1)
  dayEnd.setHours(6, 0, 0, 0)
  const dayEvents = myEvents.value.filter((e) => {
    const eStart = new Date(e.start! as string | number | Date)
    return eStart >= dayStart && eStart < dayEnd
  })
  const slots = { morning: true, afternoon: true, night: true }
  dayEvents.forEach((e) => {
    if (e.resource === resourceId) {
      slots.morning = slots.afternoon = slots.night = false
    } else {
      const startHour = new Date(e.start! as string | number | Date).getHours()
      if (startHour === 6) {
        slots.morning = false
      } else if (startHour === 14) {
        slots.afternoon = false
      } else if (startHour === 22) {
        slots.night = false
      }
    }
  })
  return slots
}

const clearColorsForResource = (
  currentColors: MbscCalendarColor[],
  resourceId: string,
  date: Date
) => {
  const base = new Date(date)
  base.setHours(0, 0, 0, 0)
  const baseTime = +base
  return currentColors.filter((c) => {
    if (c.resource === resourceId) {
      const cDate = new Date(c.start! as string | number | Date)
      cDate.setHours(0, 0, 0, 0)
      if (+cDate === baseTime) {
        const hour = new Date(c.start! as string | number | Date).getHours()
        if (hour === 6 || hour === 14 || hour === 22) {
          return false
        }
      }
    }
    return true
  })
}

const handleCellHoverIn = (args: MbscCellHoverEvent) => {
  const hoveredDate = new Date(args.date)
  const shift = getShiftByHour(hoveredDate.getHours())
  if (!shift) return

  const dayStart = new Date(args.date)
  dayStart.setHours(6, 0, 0, 0)
  const availableSlots = getAvailableSlots(String(args.resource.id), dayStart)
  const slotKey = Object.keys(availableSlots).find(
    (key) => (availableSlots as any)[key] && (shifts as any)[key].startHour === shift.startHour
  )
  if (slotKey) {
    const startTime = new Date(dayStart)
    startTime.setHours((shifts as any)[slotKey].startHour, 0, 0, 0)
    const endTime = new Date(+startTime + 8 * 60 * 60 * 1000 - 1)
    const slot = {
      background: '#c1ffe180',
      cssClass: 'available-slot mbsc-font-icon mbsc-icon-plus',
      start: +startTime + 1,
      end: endTime,
      resource: args.resource.id
    }
    availableSlotOnHover.value = slot as any
    colors.value = [...colors.value, slot as any]
  }
}

const handleCellHoverOut = () => {
  if (availableSlotOnHover.value) {
    colors.value = colors.value.filter((c) => c !== availableSlotOnHover.value)
    availableSlotOnHover.value = null
  }
}

const handleEventCreate = (args: MbscEventCreateEvent) => {
  const event = { ...args.event, id: Date.now() }
  const dayStart = new Date(event.start! as string | number | Date)
  dayStart.setHours(6, 0, 0, 0)
  const dayEnd = new Date(dayStart)
  dayEnd.setDate(dayEnd.getDate() + 1)
  dayEnd.setHours(6, 0, 0, 0)
  const dayEvents = myEvents.value.filter((e) => {
    const eStart = new Date(e.start! as string | number | Date)
    return eStart >= dayStart && eStart < dayEnd
  })
  const conflict = dayEvents.find(
    (ev) =>
      ev.resource === event.resource ||
      +new Date(ev.start! as string | number | Date) ===
        +new Date(event.start! as string | number | Date)
  )
  if (conflict) {
    toastMessage.value = 'Already assigned'
    isToastOpen.value = true
    return false
  }
  colors.value = clearColorsForResource(
    colors.value,
    String(event.resource),
    new Date(event.start! as string | number | Date)
  )
  const day = new Date(event.start! as string | number | Date)
  day.setHours(0, 0, 0, 0)
  redResources.value = { ...redResources.value, [event.resource + day.toISOString()]: false }
  availableSlotOnHover.value = null
  myEvents.value = [...myEvents.value, event]
}

const handleEventClick = () => {
  toastMessage.value = 'Already assigned'
  isToastOpen.value = true
}

const handleEventDelete = (args: MbscEventDeleteEvent) => {
  myEvents.value = myEvents.value.filter((e) => e.id !== args.event.id)
  const colorStart = new Date(args.event.start! as string | number | Date)
  colorStart.setHours(6, 0, 0, 0)
  const colorEnd = new Date(args.event.start! as string | number | Date)
  colorEnd.setDate(colorEnd.getDate() + 1)
  colorEnd.setHours(6, 0, 0, 0)
  colors.value = [
    ...colors.value,
    { start: colorStart, end: colorEnd, background: '#ffdbd280', resource: args.event.resource }
  ]
  const resource = args.event.resource
  const day = new Date(args.event.start! as string | number | Date)
  day.setHours(0, 0, 0, 0)
  redResources.value = { ...redResources.value, [resource + day.toISOString()]: true }
  toastMessage.value = args.event.title + ' deleted'
  isToastOpen.value = true
}

const handleEventDragStart = (args: MbscEventDragEvent) => {
  draggedEventStart.value = args.event.start
    ? new Date(args.event.start! as string | number | Date)
    : null
  draggedEventEnd.value = args.event.end
    ? new Date(args.event.end! as string | number | Date)
    : null
  draggedEventResource.value = args.event.resource
}

const handleEventUpdateFailed = () => {
  toastMessage.value = 'Already assigned'
  isToastOpen.value = true
}

const handleEventUpdate = (args: MbscEventUpdateEvent) => {
  const event = args.event
  const dayStart = new Date(event.start! as string | number | Date)
  dayStart.setHours(6, 0, 0, 0)
  const dragStartDay = new Date(draggedEventStart.value!)
  dragStartDay.setHours(6, 0, 0, 0)
  if (+dayStart !== +dragStartDay) {
    return false
  }

  const dayEnd = new Date(dayStart)
  dayEnd.setDate(dayEnd.getDate() + 1)
  dayEnd.setHours(6, 0, 0, 0)
  const dayEvents = myEvents.value.filter((e) => {
    const eStart = new Date(e.start! as string | number | Date)
    return eStart >= dayStart && eStart < dayEnd
  })
  const conflicts = dayEvents.filter(
    (ev) =>
      ev.id !== event.id &&
      (ev.resource === event.resource ||
        +new Date(ev.start! as string | number | Date) ===
          +new Date(event.start! as string | number | Date))
  )
  const evStart = new Date(event.start! as string | number | Date)
  event.title = getTitle(evStart.getHours())
  event.color = getColor(evStart.getHours())

  if (conflicts.length) {
    let shouldReturn = false
    conflicts.forEach((sh, i) => {
      if (sh.resource === event.resource) {
        if (+event.start! === +new Date(sh.start! as string | number | Date)) {
          shouldReturn = true
        }
        sh.resource = draggedEventResource.value as string
        conflicts[i] = sh
      } else {
        sh.start = draggedEventStart.value as Date
        sh.end = draggedEventEnd.value as Date
        const start = new Date(draggedEventStart.value!)
        sh.title = getTitle(start.getHours())
        sh.color = getColor(start.getHours())
        conflicts[i] = sh
      }
    })
    if (shouldReturn) {
      return false
    }
    myEvents.value = myEvents.value.map((e) => {
      const updated = [...conflicts, event].find((u) => u.id === e.id)
      return updated || e
    })
  } else {
    if (dayEvents.some((ev) => ev.id === event.id)) {
      myEvents.value = myEvents.value.map((e) => (e.id === event.id ? event : e))
    }
  }

  colors.value = clearColorsForResource(
    colors.value,
    String(event.resource),
    new Date(event.start! as string | number | Date)
  )
  const day = new Date(event.start! as string | number | Date)
  day.setHours(0, 0, 0, 0)
  redResources.value = { ...redResources.value, [event.resource + day.toISOString()]: false }
  if (!conflicts.length && event.resource !== draggedEventResource.value) {
    const colorEnd = new Date(dayStart)
    colorEnd.setDate(colorEnd.getDate() + 1)
    colorEnd.setHours(6, 0, 0, 0)
    colors.value = [
      ...colors.value,
      { start: dayStart, resource: args.oldEvent!.resource, background: '#ffdbd280', end: colorEnd }
    ]
    redResources.value = {
      ...redResources.value,
      [args.oldEvent!.resource + day.toISOString()]: true
    }
  }
  draggedEventStart.value = null
  draggedEventEnd.value = null
  draggedEventResource.value = null
}

const extendDefaultEvent = (args: MbscNewEventData) => {
  const start = new Date(args.start! as string | number | Date)
  const newStart = calculateStart(start)
  const newEnd = calculateEnd(start)
  const title = getTitle(newStart.getHours())
  const color = getColor(newStart.getHours())
  return {
    title: title,
    start: newStart,
    end: newEnd,
    color: color
  }
}

const handleToastClose = () => {
  isToastOpen.value = false
}

// Workaround for TypeScript error
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const day = new Date()
</script>

<template>
  <div>
    <MbscEventcalendar
      cssClass="mds-24-hour-manufacturing-calendar"
      :data="myEvents"
      :colors="colors"
      :dragToMove="true"
      :dragToCreate="false"
      :clickToCreate="'single'"
      :dragToResize="false"
      :dragTimeStep="480"
      :dragTimeStepBase="'viewStart'"
      :eventDelete="true"
      :eventOverlap="false"
      :extendDefaultEvent="extendDefaultEvent"
      @cellHoverIn="handleCellHoverIn"
      @cellHoverOut="handleCellHoverOut"
      @eventCreate="handleEventCreate"
      @eventClick="handleEventClick"
      @eventDelete="handleEventDelete"
      @eventDragStart="handleEventDragStart"
      @eventUpdateFailed="handleEventUpdateFailed"
      @eventUpdate="handleEventUpdate"
      :groupBy="'date'"
      :resources="resources"
      :view="view"
    >
      <template #resource="resource, day">
        <div
          :style="
            redResources[resource.id + day.toISOString()]
              ? { color: '#a65037', background: '#ffdbd280', margin: '-0.5em', padding: '0.5em' }
              : {}
          "
        >
          {{ resource.name }}
        </div>
      </template>
    </MbscEventcalendar>
    <MbscToast :isOpen="isToastOpen" :message="toastMessage" @close="handleToastClose" />
  </div>
</template>

<style>
.mds-24-hour-manufacturing-calendar .mbsc-schedule-events {
  left: 0;
  right: 0;
}

.mds-24-hour-manufacturing-calendar .mbsc-schedule-time-wrapper {
  height: 2.5em;
}

.mds-24-hour-manufacturing-calendar .mbsc-schedule-item {
  height: 2.5em;
}

.mds-24-hour-manufacturing-calendar .mbsc-schedule-color.available-slot {
  z-index: auto;
}

.mds-24-hour-manufacturing-calendar .mbsc-schedule-color.available-slot .mbsc-schedule-color-text {
  position: absolute;
  z-index: 1;
  pointer-events: none;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 35px;
  height: 35px;
  border: none;
  border-radius: 50%;
  background: linear-gradient(135deg, #33333380, #00000080);
  box-sizing: border-box;
}

.mds-24-hour-manufacturing-calendar .mbsc-schedule-color.available-slot.mbsc-font-icon:before {
  position: absolute;
  z-index: 2;
  pointer-events: none;
  top: 50%;
  left: 50%;
  color: #e0fff0;
  font-size: 12px;
  opacity: 0.8;
  transform: translate(-50%, -50%);
}
</style>
```

## Implementation notes

- Use `type: 'week'` with `allDay: false`, `startTime: '06:00'`, `endTime: '06:00+1'`, `timeCellStep: 120`, and `timeLabelStep: 120`. The `+1` suffix in `endTime` extends the view past midnight into the next calendar day, creating a continuous 24-hour column anchored at 06:00 per day.
- Define 3 resources (Crew A, B, C) and set `groupBy: 'date'` so all crews appear side by side under each day column.
- Define 3 shift objects each with `startHour`, `endHour`, `title`, and `color`: Morning (06-14, green), Afternoon (14-22, orange), Night (22-06 with `nextDay: true`, purple). Write a `getShiftByHour(hour)` helper that returns the matching shift for any given hour.
- Set `dragToMove: true`, `dragToCreate: false`, `dragToResize: false`, `clickToCreate: 'single'`, `eventDelete: true`, and `eventOverlap: false`. Set `dragTimeStep: 480` (8 hours in minutes) and `dragTimeStepBase: 'viewStart'` to snap all move operations to exact shift boundaries aligned to the 06:00 view start.
- Use `extendDefaultEvent` to snap click-created events to shift boundaries: call `getShiftByHour` on the clicked hour to determine the containing shift, then set `start`/`end` to that shift's boundaries (advancing the end date by one day for the Night shift), and derive `title` and `color` from the shift.
- In `onCellHoverIn`, determine the hovered shift via `getShiftByHour`, call `getAvailableSlots` to find which of the 3 shift slots are free for that resource and day, and if the hovered slot is free, push a color entry with a green background and a plus-icon `cssClass` to the `colors` array. Remove it in `onCellHoverOut`.
- In `onEventCreate`, define the day window as 06:00-06:00+1 and look for a conflict - another event on the same resource or at the same start hour. If found, show an "Already assigned" `Toast` and return `false`. On success, clear the hover-slot color for that resource/day and reset the red-resource flag.
- Store the original start, end, and resource in state via `onEventDragStart`. In `onEventUpdate`, first block cross-day moves (return `false` if the drop day's 06:00 boundary differs from the drag day's). Then find conflicts within the same day: if a conflicting event shares the same resource, reassign it to the dragged event's original resource; if it shares the same time slot, move it to the dragged event's original time and update its title and color. Return `false` if the exact drop position is already occupied. After resolving, update `colors` and the red-resource indicator for the source resource.
- In `onEventDelete`, remove the event from state, apply a red color overlay spanning 06:00-06:00+1 for that resource, set `redResources[resourceId + dayISO]` to `true`, and show a `Toast`.
- Use `renderResource` (Angular: `resourceTemplate`, Vue: `#resource` slot) with a `(resource, day)` signature: apply a red background style to the crew name div when `redResources[res.id + day.toISOString()]` is `true` for that crew and day.

## 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

- Previous: [Doctors Availability](https://demo.mobiscroll.com/vue/scheduler/doctors-availability-calendar-planner.md) - Display per-doctor availability in Vue Scheduler — color-coded tracks mark open slots per doctor, automatically respecting working hours.
- Next: [Drop Assignee on Event](https://demo.mobiscroll.com/vue/scheduler/drop-assignee-on-event-from-list.md) - Drag team members from an external list onto existing meetings in the Vue Scheduler — with visual drop feedback and conflict validation.
