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

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

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

- [Live demo](https://demo.mobiscroll.com/angular/scheduler/24-hour-manufacturing-shift-rota-planning)
- [View code on GitHub](https://github.com/acidb/mobiscroll-demos-angular/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), [React](https://demo.mobiscroll.com/react/scheduler/24-hour-manufacturing-shift-rota-planning.md), [Vue](https://demo.mobiscroll.com/vue/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/angular/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/angular/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/angular/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/angular/eventcalendar/api#opt-theme) - option
- [`themeVariant`](https://mobiscroll.com/docs/angular/eventcalendar/api#opt-themeVariant) - option
- [`colors`](https://mobiscroll.com/docs/angular/eventcalendar/api#opt-colors) - option
- [`cssClass`](https://mobiscroll.com/docs/angular/eventcalendar/api#opt-cssClass) - option
- [`dragToMove`](https://mobiscroll.com/docs/angular/eventcalendar/api#opt-dragToMove) - option
- [`dragToCreate`](https://mobiscroll.com/docs/angular/eventcalendar/api#opt-dragToCreate) - option
- [`clickToCreate`](https://mobiscroll.com/docs/angular/eventcalendar/api#opt-clickToCreate) - option
- [`dragToResize`](https://mobiscroll.com/docs/angular/eventcalendar/api#opt-dragToResize) - option
- [`dragTimeStep`](https://mobiscroll.com/docs/angular/eventcalendar/api#opt-dragTimeStep) - option
- [`eventDelete`](https://mobiscroll.com/docs/angular/eventcalendar/api#opt-eventDelete) - option
- [`eventOverlap`](https://mobiscroll.com/docs/angular/eventcalendar/api#opt-eventOverlap) - option
- [`extendDefaultEvent`](https://mobiscroll.com/docs/angular/eventcalendar/api#opt-extendDefaultEvent) - option
- [`onCellHoverIn`](https://mobiscroll.com/docs/angular/eventcalendar/api#event-onCellHoverIn) - event
- [`onCellHoverOut`](https://mobiscroll.com/docs/angular/eventcalendar/api#event-onCellHoverOut) - event
- [`onEventCreate`](https://mobiscroll.com/docs/angular/eventcalendar/api#event-onEventCreate) - event
- [`onEventClick`](https://mobiscroll.com/docs/angular/eventcalendar/api#event-onEventClick) - event
- [`onEventDelete`](https://mobiscroll.com/docs/angular/eventcalendar/api#event-onEventDelete) - event
- [`onEventDragStart`](https://mobiscroll.com/docs/angular/eventcalendar/api#event-onEventDragStart) - event
- [`onEventUpdateFailed`](https://mobiscroll.com/docs/angular/eventcalendar/api#event-onEventUpdateFailed) - event
- [`onEventUpdate`](https://mobiscroll.com/docs/angular/eventcalendar/api#event-onEventUpdate) - event
- [`groupBy`](https://mobiscroll.com/docs/angular/eventcalendar/api#opt-groupBy) - option
- [`resources`](https://mobiscroll.com/docs/angular/eventcalendar/api#opt-resources) - option
- [`view`](https://mobiscroll.com/docs/angular/eventcalendar/api#opt-view) - option
- [`data`](https://mobiscroll.com/docs/angular/eventcalendar/api#opt-data) - option
- [`resourceTemplate`](https://mobiscroll.com/docs/angular/eventcalendar/api#template-resourceTemplate) - template

## Code

### Component

```ts
import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
import {
  MbscCalendarColor,
  MbscCalendarEvent,
  MbscCellHoverEvent,
  MbscEventcalendarOptions,
  MbscEventCreateEvent,
  MbscEventDeleteEvent,
  MbscEventDragEvent,
  MbscEventUpdateEvent,
  MbscModule,
  MbscNewEventData,
  MbscResource,
  Notifications,
  setOptions,
} from '@mobiscroll/angular';

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

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

@Component({
  selector: 'app-root', 
  templateUrl: './app.component.html',
  providers: [Notifications],
})
export class AppComponent {
  constructor(private notify: Notifications) {}

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

  shifts: Record<string, Shift> = {
    morning: { startHour: 6, endHour: 14, title: 'Morning Shift', color: '#4a8c4d' },
    afternoon: { startHour: 14, endHour: 22, title: 'Afternoon Shift', color: '#f87c6b' },
    night: { startHour: 22, endHour: 6, nextDay: true, title: 'Night Shift', color: '#8567AD' },
  };

  draggedEventStart: Date | null = null;
  draggedEventEnd: Date | null = null;
  draggedEventResource: string | null = null;
  availableSlotOnHover: MbscCalendarColor | null = null;
  redResources: Record<string, boolean> = {};
  colors: MbscCalendarColor[] = [];

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

  myResources: MbscResource[] = [
    { id: 'A', name: 'Crew A' },
    { id: 'B', name: 'Crew B' },
    { id: 'C', name: 'Crew C' },
  ];

  getShiftByHour(hour: number) {
    if (hour >= 6 && hour < 14) {
      return this.shifts['morning'];
    }
    if (hour >= 14 && hour < 22) {
      return this.shifts['afternoon'];
    }
    if (hour >= 22 || hour < 6) {
      return this.shifts['night'];
    }
    return this.shifts['afternoon'];
  }

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

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

  getTitle(startHours: number): string {
    return this.getShiftByHour(startHours).title;
  }

  getColor(startHours: number): string {
    return this.getShiftByHour(startHours).color;
  }

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

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

  myOptions: MbscEventcalendarOptions = {
    cssClass: 'mds-24-hour-manufacturing-calendar',
    dragToMove: true,
    dragToCreate: false,
    clickToCreate: 'single',
    dragToResize: false,
    dragTimeStep: 480,
    dragTimeStepBase: 'viewStart',
    eventDelete: true,
    eventOverlap: false,
    extendDefaultEvent: (args: MbscNewEventData): MbscCalendarEvent => {
      const start = new Date(args.start);
      const newStart = this.calculateStart(start);
      const newEnd = this.calculateEnd(start);
      const title = this.getTitle(newStart.getHours());
      const color = this.getColor(newStart.getHours());
      return {
        title: title,
        start: newStart,
        end: newEnd,
        color: color,
      };
    },
    onCellHoverIn: (args: MbscCellHoverEvent) => {
      const hoveredDate = new Date(args.date);
      const shift = this.getShiftByHour(hoveredDate.getHours());
      if (!shift) {
        return;
      }

      const dayStart = new Date(args.date);
      dayStart.setHours(6, 0, 0, 0);
      const availableSlots = this.getAvailableSlots(String(args.resource.id), dayStart);
      const slotKey = Object.keys(availableSlots).find((key) => availableSlots[key] && this.shifts[key].startHour === shift.startHour);
      if (slotKey) {
        const startTime = new Date(dayStart);
        startTime.setHours(this.shifts[slotKey].startHour, 0, 0, 0);
        const endTime = new Date(+startTime + 8 * 60 * 60 * 1000 - 1);
        this.availableSlotOnHover = {
          background: '#c1ffe180',
          cssClass: 'available-slot mbsc-font-icon mbsc-icon-plus',
          start: (+startTime + 1) as any,
          end: endTime,
          resource: String(args.resource.id),
        };
        this.colors = [...this.colors, this.availableSlotOnHover!];
      }
    },
    onCellHoverOut: () => {
      if (this.availableSlotOnHover) {
        this.colors = this.colors.filter((c) => c !== this.availableSlotOnHover);
        this.availableSlotOnHover = null;
      }
    },
    onEventCreate: (args: MbscEventCreateEvent) => {
      const event = args.event as MbscCalendarEvent;
      const dayStart = new Date(event.start as 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 = this.myEvents.filter((e: MbscCalendarEvent) => {
        const eStart = new Date(e.start as Date);
        return eStart >= dayStart && eStart < dayEnd;
      });
      const conflict = dayEvents.find(
        (ev: MbscCalendarEvent) => ev.resource === event.resource || +new Date(ev.start as Date) === +new Date(event.start as Date),
      );
      if (conflict) {
        this.notify.toast({ message: 'Already assigned' });
        return false;
      }
      this.colors = this.clearColorsForResource(this.colors, event.resource as string, event.start as Date);
      const day = new Date(event.start as Date);
      day.setHours(0, 0, 0, 0);
      this.redResources = { ...this.redResources, [String(event.resource) + day.toISOString()]: false };
      this.availableSlotOnHover = null;
      this.myEvents = [...this.myEvents, { ...event, id: Date.now() }];
      return;
    },
    onEventClick: () => {
      this.notify.toast({
        message: 'Already assigned',
      });
    },
    onEventDelete: (args: MbscEventDeleteEvent) => {
      this.myEvents = this.myEvents.filter((e) => e.id !== args.event.id);
      const colorStart = new Date(args.event.start as Date);
      colorStart.setHours(6, 0, 0, 0);
      const colorEnd = new Date(args.event.start as Date);
      colorEnd.setDate(colorEnd.getDate() + 1);
      colorEnd.setHours(6, 0, 0, 0);

      this.colors = [
        ...this.colors,
        { start: colorStart, end: colorEnd, background: '#ffdbd280', resource: args.event.resource as string },
      ];
      const resource = args.event.resource as string;
      const day = new Date(args.event.start as Date);
      day.setHours(0, 0, 0, 0);
      this.redResources = { ...this.redResources, [resource + day.toISOString()]: true };
      this.notify.toast({
        message: (args.event.title as string) + ' deleted',
      });
    },
    onEventDragStart: (args: MbscEventDragEvent) => {
      this.draggedEventStart = args.event.start as Date | null;
      this.draggedEventEnd = args.event.end as Date | null;
      this.draggedEventResource = args.event.resource as string | null;
    },
    onEventUpdateFailed: () => {
      this.notify.toast({
        message: 'Already assigned',
      });
    },
    onEventUpdate: (args: MbscEventUpdateEvent) => {
      const event = args.event;
      const dayStart = new Date(event.start as Date);
      dayStart.setHours(6, 0, 0, 0);
      const dragStartDay = new Date(this.draggedEventStart!);
      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 = this.myEvents.filter((e: MbscCalendarEvent) => {
        const eStart = new Date(e.start as Date);
        return eStart >= dayStart && eStart < dayEnd;
      });

      const conflicts = dayEvents.filter(
        (ev: MbscCalendarEvent) =>
          ev.id !== event.id && (ev.resource === event.resource || +new Date(ev.start as Date) === +new Date(event.start as Date)),
      );
      const evStart = new Date(event.start as Date);
      event.title = this.getTitle(evStart.getHours());
      event.color = this.getColor(evStart.getHours());

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

      this.colors = this.clearColorsForResource(this.colors, event.resource as string, event.start as Date);
      const day = new Date(event.start as Date);
      day.setHours(0, 0, 0, 0);
      this.redResources = { ...this.redResources, [String(event.resource) + day.toISOString()]: false };
      if (!conflicts.length && event.resource !== this.draggedEventResource) {
        const colorEnd = new Date(dayStart);
        colorEnd.setDate(colorEnd.getDate() + 1);
        colorEnd.setHours(6, 0, 0, 0);
        this.colors = [
          ...this.colors,
          { start: dayStart, end: colorEnd, background: '#ffdbd280', resource: args.oldEvent!.resource as string },
        ];
        this.redResources = { ...this.redResources, [(args.oldEvent!.resource as string) + day.toISOString()]: true };
      }
      this.draggedEventStart = null;
      this.draggedEventEnd = null;
      this.draggedEventResource = null;
      return;
    },
    groupBy: 'date',
    resources: this.myResources,
    view: {
      scheduler: {
        type: 'week',
        allDay: false,
        startTime: '06:00',
        endTime: '06:00+1',
        timeCellStep: 120,
        timeLabelStep: 120,
      },
    },
  };

  getResourceStyle(resource: MbscResource, day: Date): Record<string, string> {
    return day && this.redResources[resource.id + day.toISOString()]
      ? { color: '#53000080', background: '#ffdbd280', margin: '-0.5em', padding: '0.5em' }
      : {};
  }
}
```

### Template

```html
<mbsc-eventcalendar [options]="myOptions" [data]="myEvents" [colors]="colors" [resourceTemplate]="myResourceTemplate">
  <ng-template #myResourceTemplate let-resource let-day="day">
    <div [ngStyle]="getResourceStyle(resource, day)">{{ resource.name }}</div>
  </ng-template>
</mbsc-eventcalendar>
```

### CSS

```css
.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%);
}
```

## 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-angular.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 angular
   ```

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

   ```
   npm install
   ```

6. Run the project:

   ```
   ng serve
   ```

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

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

## Related demos

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