Shifts and Tasks
Usage
theme: 'ios' You can turn auto theming on and let it decide based on the platform.
Usage
themeVariant: 'light' You can turn auto dark mode on based on system settings.
Usage
clickToCreate: true,
// click to create is on dragToCreate: true,
// drag to create is on dragToMove: true,
// reordering is on dragToResize: true,
// resizing is on eventDelete: true
// deleting is on Fine-tune drag & drop with these options, which are false by default.
Usage
locale: localeEn Selecting the locale loads the localized settings and the translated UI text.
Shifts and Tasks
Visualize shifts and tasks within them using the timeline view. With the help of the order property of the data object you can always guarantee that shifts are always displayed at the top (alternatively below tasks) and tasks within those shifts are rendered below.
The shift and task events are bind together with custom data properties. Every shift contains the task IDs in the tasks property, and every task has a shift property which indicates the parent shift. These properties are used later in the validation logic to handle the two distinct event type behaviours. The validation is implemented by dynamically setting invalid ranges through the onEventCreated, onEventUpdated and onEventDeleted lifecycle events.
Demo

Code
import { CommonModule } from '@angular/common';
import { Component, ViewChild } from '@angular/core';
import {
MbscCalendarEvent,
MbscEventcalendar,
MbscEventcalendarOptions,
MbscEventcalendarView,
MbscEventCreatedEvent,
MbscEventDeletedEvent,
MbscEventDragEvent,
MbscEventUpdateEvent,
MbscEventUpdateFailedEvent,
MbscModule,
MbscNewEventData,
MbscResource,
Notifications,
setOptions ,
} from '@mobiscroll/angular';
interface MyEvent extends MbscCalendarEvent {
shift?: number | string;
tasks?: (string | number)[];
}
interface MyBlockedTimes extends MbscCalendarEvent {
shift?: boolean;
task?: boolean;
workOff?: boolean;
}
setOptions({
locale: ,
theme: ' ',
themeVariant: ' '
});
const blockedOutTimes: MyBlockedTimes[] = [
{
id: 1,
start: '00:00',
end: '05:00',
recurring: {
repeat: 'daily',
},
workOff: true,
},
{
id: 2,
start: '19:00',
end: '00:00',
recurring: {
repeat: 'daily',
},
workOff: true,
},
];
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
providers: [Notifications],
standalone: true,
imports: [CommonModule, MbscModule],
})
export class AppComponent {
constructor(private notify: Notifications) {}
@ViewChild('timeline', { static: false })
timelineInst!: MbscEventcalendar;
myEvents: MyEvent[] = [
{
id: 1,
start: '2026-10-25T05:00',
end: '2026-10-25T12:00',
title: 'Daily Shift',
resource: 1,
tasks: ['es-1', 'es-2', 'es-3'],
order: 1,
color: '#513737',
cssClass: 'mds-task-shift',
},
{
id: 'es-1',
start: '2026-10-25T05:00',
end: '2026-10-25T08:30',
title: 'Server Maintenance',
resource: 1,
shift: 1,
order: 2,
cssClass: 'mds-task-subtask',
},
{
id: 'es-2',
start: '2026-10-25T09:00',
end: '2026-10-25T10:45',
title: 'Monitor System Performance',
resource: 1,
shift: 1,
order: 2,
cssClass: 'mds-task-subtask',
},
{
id: 'es-3',
start: '2026-10-25T11:00',
end: '2026-10-25T12:00',
title: 'Backup and Recovery',
resource: 1,
shift: 1,
order: 2,
cssClass: 'mds-task-subtask',
},
{
id: 2,
start: '2026-10-25T11:00',
end: '2026-10-25T18:00',
title: 'Flex Shift',
resource: 2,
tasks: ['jb-1', 'jb-2', 'jb-3', 'jb-4'],
order: 1,
color: '#513737',
cssClass: 'mds-task-shift',
},
{
id: 'jb-1',
start: '2026-10-25T11:00',
end: '2026-10-25T12:30',
title: 'Code Review',
resource: 2,
shift: 2,
order: 2,
cssClass: 'mds-task-subtask',
},
{
id: 'jb-2',
start: '2026-10-25T13:00',
end: '2026-10-25T14:45',
title: 'Develop New Features',
resource: 2,
shift: 2,
order: 2,
cssClass: 'mds-task-subtask',
},
{
id: 'jb-3',
start: '2026-10-25T15:00',
end: '2026-10-25T16:50',
title: 'Mentor Junior Developers',
resource: 2,
shift: 2,
order: 2,
cssClass: 'mds-task-subtask',
},
{
id: 'jb-4',
start: '2026-10-25T17:00',
end: '2026-10-25T18:00',
title: 'Attend Stand-Up Meeting',
resource: 2,
shift: 2,
order: 2,
cssClass: 'mds-task-subtask',
},
];
myView: MbscEventcalendarView = {
timeline: {
type: 'week',
startDay: 1,
startTime: '05:00',
endTime: '19:00',
endDay: 5,
eventHeight: 'variable',
},
};
myResources: MbscResource[] = [
{
id: 1,
color: '#ff0101',
name: 'Emma Smith',
eventDragBetweenResources: false,
},
{
id: 2,
color: '#239a21',
name: 'James Brown',
eventDragBetweenResources: false,
},
{
id: 3,
color: '#8f1ed6',
name: 'Olivia Miller',
eventDragBetweenResources: false,
},
{
id: 4,
color: '#01adff',
name: 'Robert Taylor',
eventDragBetweenResources: false,
},
{
id: 5,
color: '#F58585',
name: 'John Doe',
eventDragBetweenResources: false,
},
];
myInvalids = blockedOutTimes;
calendarOptions: MbscEventcalendarOptions = {
extendDefaultEvent: (args: MbscNewEventData) => {
if (this.timelineInst) {
const events = this.timelineInst.getEvents(args.start, new Date(+args.start + 3600000)).filter((e) => e.resource === args.resource);
const isShift = events.length === 0;
return {
order: isShift ? 1 : 2,
cssClass: isShift ? 'mds-task-shift' : 'mds-task-subtask',
color: isShift ? '#513737' : '',
title: isShift ? 'New Shift' : 'New Task',
tasks: isShift ? [] : undefined,
shift: isShift ? undefined : events[0].id,
} as any;
}
},
onEventCreated: (args: MbscEventCreatedEvent) => {
const event: MyEvent = args.event;
const inst = args.inst!;
const overlapEvents = inst.getEvents(event.start, event.end).filter((e) => e.resource === event.resource);
if (event.shift) {
// Tasks was created
const shift: MyEvent = { ...overlapEvents[0] };
const shiftIndex = this.myEvents.findIndex((x) => x.id === event.shift);
const newEventList = [...this.myEvents];
if (overlapEvents.length > 2) {
// Prevent overlap
this.myEvents = newEventList;
this.notify.toast({
message: 'No space for task',
});
} else {
// Update the shift
shift.tasks!.push(event.id as string);
newEventList.splice(shiftIndex, 1, shift);
// Update subtask
event.start = new Date(Math.max(+new Date(shift.start as Date), +event.start!));
event.end = new Date(Math.min(+new Date(shift.end as Date), +event.end!));
event.shift = shift.id;
newEventList.push(event);
this.myEvents = newEventList;
}
} else {
this.myEvents = [...this.myEvents, event];
}
},
onEventUpdated: (args: MbscEventUpdateEvent) => {
const inst = args.inst!;
const newEventList = [...this.myEvents];
const event: MyEvent = args.event!;
const evIndex = newEventList.findIndex((x) => x.id === event.id);
const oldEvent = args.oldEvent!;
if (event.tasks) {
// Shift was updated
let shiftStart = new Date(event.start as Date);
let shiftEnd = new Date(event.end as Date);
let subTasksDuration = 0;
const startDiff = +shiftStart - +new Date(oldEvent.start as Date);
const endDiff = +shiftEnd - +new Date(oldEvent.end as Date);
const startResize = startDiff > 0;
const endResize = endDiff < 0;
const isMove = startDiff === endDiff;
const isResize = !isMove && (startResize || endResize);
const tasks = event.tasks.map((el) => {
const t = newEventList.find((e) => e.id === el)!;
subTasksDuration += +new Date(t!.end as Date) - +new Date(t!.start as Date);
return t;
});
if (isResize && +shiftEnd - +shiftStart < subTasksDuration) {
// Limit the shift to don't be smaller than the containing subtasks
shiftStart = endResize ? shiftStart : new Date(+shiftEnd - subTasksDuration);
shiftEnd = startResize ? shiftEnd : new Date(+shiftStart + subTasksDuration);
event.start = shiftStart;
event.end = shiftEnd;
newEventList.splice(evIndex, 1, event);
this.myEvents = newEventList;
}
// Resize or move
if (isResize || startDiff === endDiff) {
// Update subtask
tasks.forEach((task, i) => {
const taskStart = new Date(task.start as Date);
const taskEnd = new Date(task.end as Date);
if (isResize) {
task.start = new Date(i === 0 ? +shiftStart : +new Date(tasks[i - 1].end as Date));
task.end = new Date(Math.min(+task.start + (+taskEnd - +taskStart), +new Date(event.end as Date)));
} else {
task.start = new Date(+taskStart + startDiff);
task.end = new Date(+taskEnd + startDiff);
}
const taskIndex = newEventList.findIndex((x) => x.id === task.id);
newEventList.splice(taskIndex, 1, task);
});
this.myEvents = newEventList;
}
} else {
// Subtask was updated
const eventOverlap = inst.getEvents(event.start, event.end).filter((e) => e.resource === event.resource);
if (eventOverlap.length > 2) {
// Don't let subtask to overlap
newEventList.splice(evIndex, 1, oldEvent);
this.myEvents = newEventList;
this.notify.toast({
message: 'No space for task',
});
}
}
},
onEventDeleted: (args: MbscEventDeletedEvent) => {
let newEventList = [...this.myEvents];
const event: MyEvent = args.event;
const evIndex = this.myEvents.findIndex((x) => x.id === event.id);
newEventList.splice(evIndex, 1);
if (event.tasks) {
// Update shift tasks
newEventList = newEventList.filter((ev) => !event.tasks!.includes(ev.id!));
}
if (event.shift) {
const shift = newEventList.find((ev) => ev.resource === event.resource && ev.id === event.shift)!;
// Remove the deleted task id from the shift data
shift.tasks = shift.tasks && shift.tasks.filter((t: number | string) => t !== event.id);
}
this.myEvents = newEventList;
},
onEventDragStart: (args: MbscEventDragEvent) => {
const inst = args.inst!;
const events = inst.getEvents();
const event: MyEvent = args.event;
const tempInvalid: MbscResource[] = [];
if (event.tasks) {
// Shift
const shiftsInResource = events.filter((e: MyEvent) => e.tasks !== undefined && e.resource === event.resource && e.id !== event.id);
shiftsInResource.forEach((e) => {
tempInvalid.push({
id: 'mds-s-' + e.id,
start: e.start,
end: e.end,
resource: e.resource,
cssClass: 'mds-task-blocked',
shift: true,
});
});
this.myInvalids = [...blockedOutTimes, ...tempInvalid];
} else {
// Subtask
const shift: MyEvent = events.find((ev) => ev.resource === event.resource && ev.id === event.shift)!;
tempInvalid.push(
{
id: 'mds-te-' + shift.id,
start: new Date(+new Date(shift.start as Date) - 7 * 86400000),
end: shift.start,
resource: shift.resource,
cssClass: 'mds-task-blocked',
task: true,
},
{
id: 'mds-tu-' + shift.id,
start: shift.end,
end: new Date(+new Date(shift.end as Date) + 7 * 86400000),
resource: shift.resource,
cssClass: 'mds-task-blocked',
task: true,
},
);
this.myInvalids = [...blockedOutTimes, ...tempInvalid];
}
},
onEventDragEnd: () => {
this.myInvalids = blockedOutTimes;
},
onEventUpdateFailed: (args: MbscEventUpdateFailedEvent) => {
const invalid: MyBlockedTimes = args.invalid;
let msg = '';
if (invalid.workOff) {
msg = 'Shift falls out of working hours';
} else if (invalid.task) {
msg = 'Task falls out of shift';
} else if (invalid.shift) {
msg = 'Shifts cannot overlap';
}
if (msg) {
this.notify.toast({
message: msg,
});
}
},
};
}
<mbsc-eventcalendar
#timeline
[view]="myView"
[resources]="myResources"
[dragToCreate]="true"
[dragToMove]="true"
[dragToResize]="true"
[data]="myEvents"
[invalid]="myInvalids"
[timelineEventContentTemplate]="scheduleEventContent"
[options]="calendarOptions"
>
<ng-template #scheduleEventContent let-data>
{{ data.title }}
<span className="mds-task-hours">
- {{ (+data.endDate - +data.startDate) / 3600000 + ((+data.endDate - +data.startDate) / 3600000 === 1 ? 'h' : 'hrs') }}</span
>
</ng-template>
</mbsc-eventcalendar>
.mds-task-shift {
height: 30px;
}
.mds-task-subtask {
height: 40px;
}
.mds-task-subtask .mbsc-schedule-event-range,
.mds-task-subtask .mbsc-schedule-event-title {
font-size: 10px;
line-height: 13px;
}
.mds-task-shift .mbsc-schedule-event-range,
.mds-task-shift .mbsc-schedule-event-title {
display: inline-block;
margin: 0 4px;
}
.mds-task-hours {
font-size: 10px;
font-weight: 400;
}
.mds-task-blocked.mbsc-schedule-invalid {
text-align: center;
align-items: center;
font-weight: bold;
background: repeating-linear-gradient(-45deg, #f3f3f3, #f3f3f3 11px, #e5e5e5 11px, #e5e5e5 22px);
}