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

# Employee Shift Scheduling - Angular Timeline demo

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

- [Live demo](https://demo.mobiscroll.com/angular/timeline/employee-shifts)
- [View code on GitHub](https://github.com/acidb/mobiscroll-demos-angular/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), [React](https://demo.mobiscroll.com/react/timeline/employee-shifts.md), [Vue](https://demo.mobiscroll.com/vue/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/angular/eventcalendar/api#opt-view) option.

This will give you summarized daily events similar to the [event calendar labels](https://demo.mobiscroll.com/angular/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/angular/scheduler/create-read-update-delete-CRUD).

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

## 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/angular/eventcalendar/api#opt-theme) - option
- [`themeVariant`](https://mobiscroll.com/docs/angular/eventcalendar/api#opt-themeVariant) - option
- [`minTime`](https://mobiscroll.com/docs/angular/datepicker/api#opt-minTime) - option
- [`maxTime`](https://mobiscroll.com/docs/angular/datepicker/api#opt-maxTime) - option
- [`display`](https://mobiscroll.com/docs/angular/popup/api#opt-display) - option
- [`fullScreen`](https://mobiscroll.com/docs/angular/popup/api#opt-fullScreen) - option
- [`touchUi`](https://mobiscroll.com/docs/angular/popup/api#opt-touchUi) - option
- [`width`](https://mobiscroll.com/docs/angular/popup/api#opt-width) - option
- [`cssClass`](https://mobiscroll.com/docs/angular/popup/api#opt-cssClass) - option
- [`clickToCreate`](https://mobiscroll.com/docs/angular/eventcalendar/api#opt-clickToCreate) - option
- [`dragToCreate`](https://mobiscroll.com/docs/angular/eventcalendar/api#opt-dragToCreate) - option
- [`dragToResize`](https://mobiscroll.com/docs/angular/eventcalendar/api#opt-dragToResize) - option
- [`dragToMove`](https://mobiscroll.com/docs/angular/eventcalendar/api#opt-dragToMove) - option
- [`data`](https://mobiscroll.com/docs/angular/eventcalendar/api#opt-data) - option
- [`eventOverlap`](https://mobiscroll.com/docs/angular/eventcalendar/api#opt-eventOverlap) - option
- [`extendDefaultEvent`](https://mobiscroll.com/docs/angular/eventcalendar/api#opt-extendDefaultEvent) - option
- [`invalid`](https://mobiscroll.com/docs/angular/eventcalendar/api#opt-invalid) - option
- [`resources`](https://mobiscroll.com/docs/angular/eventcalendar/api#opt-resources) - option
- [`resourceTemplate`](https://mobiscroll.com/docs/angular/eventcalendar/api#template-resourceTemplate) - template
- [`slots`](https://mobiscroll.com/docs/angular/eventcalendar/api#opt-slots) - option
- [`view`](https://mobiscroll.com/docs/angular/eventcalendar/api#opt-view) - option
- [`onEventClick`](https://mobiscroll.com/docs/angular/eventcalendar/api#event-onEventClick) - event
- [`onEventCreated`](https://mobiscroll.com/docs/angular/eventcalendar/api#event-onEventCreated) - event
- [`onEventCreateFailed`](https://mobiscroll.com/docs/angular/eventcalendar/api#event-onEventCreateFailed) - event
- [`onEventDeleted`](https://mobiscroll.com/docs/angular/eventcalendar/api#event-onEventDeleted) - event
- [`onEventUpdated`](https://mobiscroll.com/docs/angular/eventcalendar/api#event-onEventUpdated) - event
- [`onEventUpdateFailed`](https://mobiscroll.com/docs/angular/eventcalendar/api#event-onEventUpdateFailed) - event
- [`buttons`](https://mobiscroll.com/docs/angular/popup/api#opt-buttons) - option
- [`headerTemplate`](https://mobiscroll.com/docs/angular/popup/api#template-headerTemplate) - template
- [`responsive`](https://mobiscroll.com/docs/angular/popup/api#opt-responsive) - option
- [`scrollLock`](https://mobiscroll.com/docs/angular/popup/api#opt-scrollLock) - option
- [`onClose`](https://mobiscroll.com/docs/angular/popup/api#event-onClose) - event
- [`display`](https://mobiscroll.com/docs/angular/datepicker/api#opt-display) - option
- [`select`](https://mobiscroll.com/docs/angular/datepicker/api#opt-select) - option
- [`timeWheels`](https://mobiscroll.com/docs/angular/datepicker/api#localization-timeWheels) - option
- [`controls`](https://mobiscroll.com/docs/angular/datepicker/api#opt-controls) - option
- [`showRangeLabels`](https://mobiscroll.com/docs/angular/datepicker/api#opt-showRangeLabels) - option
- [`stepMinute`](https://mobiscroll.com/docs/angular/datepicker/api#opt-stepMinute) - option
- [`startInput`](https://mobiscroll.com/docs/angular/datepicker/api#opt-startInput) - option
- [`endInput`](https://mobiscroll.com/docs/angular/datepicker/api#opt-endInput) - option
- [`touchUi`](https://mobiscroll.com/docs/angular/datepicker/api#opt-touchUi) - option

## Code

### Component

```ts
import { CommonModule } from '@angular/common';
import { Component, ViewChild } from '@angular/core';
import { FormsModule } from '@angular/forms';
import {
  formatDate,
  MbscCalendarEvent,
  MbscDatepickerValue,
  MbscEventcalendarView,
  MbscEventClickEvent,
  MbscEventCreatedEvent,
  MbscEventDeletedEvent,
  MbscEventUpdatedEvent,
  MbscModule,
  MbscPopup,
  MbscPopupButton,
  MbscPopupOptions,
  MbscResource,
  MbscSlot,
  Notifications,
  setOptions,
} from '@mobiscroll/angular';

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

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

  @ViewChild('popup', { static: false })
  popup!: MbscPopup;

  deletedShift!: MbscCalendarEvent;
  isEdit = false;
  minTime = '';
  maxTime = '';
  shift!: MbscCalendarEvent;
  shiftDates?: MbscDatepickerValue;
  shiftNotes?: string;
  headerPrimary = '';
  headerDate = '';

  popupButtons: (MbscPopupButton | 'cancel')[] = [];

  popupResponsive: Record<string, MbscPopupOptions> = {
    medium: {
      display: 'center',
      fullScreen: false,
      touchUi: false,
      width: 400,
    },
  };

  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',
    },
  ];

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

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

  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,
    };
  }

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

  deleteEvent(event: MbscCalendarEvent) {
    this.shifts = this.shifts.filter((s) => s.id !== event.id);
    this.notify.snackbar({
      button: {
        action: () => {
          this.shifts = [...this.shifts, event];
        },
        text: 'Undo',
      },
      message: 'Shift deleted',
    });
  }

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

    newShifts.splice(index, 1, event);

    this.shifts = newShifts;
  }

  saveEvent(event: MbscCalendarEvent) {
    const dates = this.shiftDates 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'] = this.shiftNotes;

    if (this.isEdit) {
      this.updateEvent(event);
    } else {
      // Add the new event to the list
      this.shifts = [...this.shifts, event];
    }
    this.popup.close();
  }

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

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

  handleEventCreated(args: MbscEventCreatedEvent) {
    setTimeout(() => {
      const event = args.event;
      const slot = args.slotObj!;

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

  handleEventCreateFailed() {
    this.notify.toast({ message: "Can't create shift" });
  }

  handleEventDeleted(args: MbscEventDeletedEvent) {
    setTimeout(() => {
      this.deleteEvent(args.event);
    });
  }

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

  handleEventUpdateFailed() {
    this.notify.toast({ message: "Can't move shift" });
  }

  handlePopupClose() {
    if (!this.isEdit) {
      // Remove event if popup is cancelled
      this.shifts = [...this.shifts];
    }
  }

  handleShiftDeleteClick() {
    this.deleteEvent(this.shift);
    this.popup.close();
  }
}
```

### Template

```html
<mbsc-eventcalendar
  class="mds-employee-shifts"
  [clickToCreate]="true"
  [dragToCreate]="false"
  [dragToResize]="false"
  [dragToMove]="true"
  [data]="shifts"
  [eventOverlap]="false"
  [extendDefaultEvent]="getShiftTimes"
  [invalid]="myInvalids"
  [resources]="staff"
  [resourceTemplate]="myResourceTemplate"
  [slots]="mySlots"
  [view]="myView"
  (onEventClick)="handleEventClick($event)"
  (onEventCreated)="handleEventCreated($event)"
  (onEventCreateFailed)="handleEventCreateFailed()"
  (onEventDeleted)="handleEventDeleted($event)"
  (onEventUpdated)="handleEventUpdated($event)"
  (onEventUpdateFailed)="handleEventUpdateFailed()"
>
  <ng-template #myResourceTemplate let-resource>
    <div class="mbsc-flex">
      <img class="mds-employee-shifts-avatar" [attr.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>
  </ng-template>
</mbsc-eventcalendar>

<mbsc-popup
  #popup
  display="bottom"
  [buttons]="popupButtons"
  [contentPadding]="false"
  [fullScreen]="true"
  [headerTemplate]="myHeaderTemplate"
  [responsive]="popupResponsive"
  [scrollLock]="false"
  (onClose)="handlePopupClose()"
>
  <mbsc-datepicker
    display="anchored"
    select="range"
    timeWheels="|h:mm A|"
    [controls]="['time']"
    [minTime]="minTime"
    [maxTime]="maxTime"
    [showRangeLabels]="false"
    [stepMinute]="30"
    [startInput]="start"
    [endInput]="end"
    [touchUi]="false"
    [(ngModel)]="shiftDates"
  />
  <div class="mbsc-form-group">
    <mbsc-input #start [dropdown]="true" label="Shift start"></mbsc-input>
    <mbsc-input #end [dropdown]="true" label="Shift end"></mbsc-input>
  </div>
  <div class="mbsc-form-group">
    <mbsc-textarea label="Notes" [(ngModel)]="shiftNotes"></mbsc-textarea>
  </div>
  <div *ngIf="isEdit" class="mbsc-button-group">
    <mbsc-button class="mbsc-button-block" color="danger" variant="outline" (click)="handleShiftDeleteClick()"> Delete shift </mbsc-button>
  </div>
</mbsc-popup>

<ng-template #myHeaderTemplate>
  <div class="mds-employee-shifts-header-primary">{{ headerPrimary }}</div>
  <div class="mds-employee-shifts-header-date">{{ headerDate }}</div>
</ng-template>
```

### CSS

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

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

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