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

# Employee Shift Scheduling - React Timeline demo

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

- [Live demo](https://demo.mobiscroll.com/react/timeline/employee-shifts)
- [View React JS code on GitHub](https://github.com/acidb/mobiscroll-demos-react/tree/main/src/demos/eventcalendar/timeline/employee-shifts)
- [View React TS code on GitHub](https://github.com/acidb/mobiscroll-demos-react-ts/tree/main/src/demos/eventcalendar/timeline/employee-shifts)
- This demo in other frameworks: [JavaScript](https://demo.mobiscroll.com/javascript/timeline/employee-shifts.md), [jQuery](https://demo.mobiscroll.com/jquery/timeline/employee-shifts.md), [Angular](https://demo.mobiscroll.com/angular/timeline/employee-shifts.md), [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/react/eventcalendar/api#opt-view) option.

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

Customize the dialog header with a custom header renderer function using the [`renderHeader`](https://mobiscroll.com/docs/react/eventcalendar/api#renderer-renderHeader) 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/react/eventcalendar/api#opt-theme) - option
- [`themeVariant`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-themeVariant) - option
- [`cssClass`](https://mobiscroll.com/docs/react/popup/api#opt-cssClass) - option
- [`display`](https://mobiscroll.com/docs/react/popup/api#opt-display) - option
- [`fullScreen`](https://mobiscroll.com/docs/react/popup/api#opt-fullScreen) - option
- [`touchUi`](https://mobiscroll.com/docs/react/popup/api#opt-touchUi) - option
- [`width`](https://mobiscroll.com/docs/react/popup/api#opt-width) - option
- [`cssClass`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-cssClass) - option
- [`clickToCreate`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-clickToCreate) - option
- [`dragToCreate`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-dragToCreate) - option
- [`dragToResize`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-dragToResize) - option
- [`dragToMove`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-dragToMove) - option
- [`data`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-data) - option
- [`eventOverlap`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-eventOverlap) - option
- [`invalid`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-invalid) - option
- [`resources`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-resources) - option
- [`slots`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-slots) - option
- [`view`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-view) - option
- [`extendDefaultEvent`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-extendDefaultEvent) - option
- [`renderResource`](https://mobiscroll.com/docs/react/eventcalendar/api#renderer-renderResource) - renderer
- [`onEventClick`](https://mobiscroll.com/docs/react/eventcalendar/api#event-onEventClick) - event
- [`onEventCreated`](https://mobiscroll.com/docs/react/eventcalendar/api#event-onEventCreated) - event
- [`onEventCreateFailed`](https://mobiscroll.com/docs/react/eventcalendar/api#event-onEventCreateFailed) - event
- [`onEventDeleted`](https://mobiscroll.com/docs/react/eventcalendar/api#event-onEventDeleted) - event
- [`onEventUpdated`](https://mobiscroll.com/docs/react/eventcalendar/api#event-onEventUpdated) - event
- [`onEventUpdateFailed`](https://mobiscroll.com/docs/react/eventcalendar/api#event-onEventUpdateFailed) - event
- [`buttons`](https://mobiscroll.com/docs/react/popup/api#opt-buttons) - option
- [`renderHeader`](https://mobiscroll.com/docs/react/popup/api#renderer-renderHeader) - renderer
- [`isOpen`](https://mobiscroll.com/docs/react/popup/api#opt-isOpen) - option
- [`responsive`](https://mobiscroll.com/docs/react/popup/api#opt-responsive) - option
- [`scrollLock`](https://mobiscroll.com/docs/react/popup/api#opt-scrollLock) - option
- [`onClose`](https://mobiscroll.com/docs/react/popup/api#event-onClose) - event
- [`controls`](https://mobiscroll.com/docs/react/datepicker/api#opt-controls) - option
- [`display`](https://mobiscroll.com/docs/react/datepicker/api#opt-display) - option
- [`minTime`](https://mobiscroll.com/docs/react/datepicker/api#opt-minTime) - option
- [`maxTime`](https://mobiscroll.com/docs/react/datepicker/api#opt-maxTime) - option
- [`select`](https://mobiscroll.com/docs/react/datepicker/api#opt-select) - option
- [`showRangeLabels`](https://mobiscroll.com/docs/react/datepicker/api#opt-showRangeLabels) - option
- [`stepMinute`](https://mobiscroll.com/docs/react/datepicker/api#opt-stepMinute) - option
- [`startInput`](https://mobiscroll.com/docs/react/datepicker/api#opt-startInput) - option
- [`endInput`](https://mobiscroll.com/docs/react/datepicker/api#opt-endInput) - option
- [`timeWheels`](https://mobiscroll.com/docs/react/datepicker/api#localization-timeWheels) - option
- [`touchUi`](https://mobiscroll.com/docs/react/datepicker/api#opt-touchUi) - option
- [`onChange`](https://mobiscroll.com/docs/react/datepicker/api#event-onChange) - event
- [`onClose`](https://mobiscroll.com/docs/react/notifications/snackbar#event-onClose) - event
- [`onClose`](https://mobiscroll.com/docs/react/notifications/toast#event-onClose) - event

## Code

### JSX

```jsx
import {
  Button,
  Datepicker,
  Eventcalendar,
  formatDate,
  Input,
  Popup,
  setOptions,
  Snackbar,
  Textarea,
  Toast,
} from '@mobiscroll/react';
import { useCallback, useMemo, useState } from 'react';

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

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

function App() {
  const [shifts, setShifts] = useState(defaultShifts);
  const [shift, setShift] = useState();
  const [shiftDates, setShiftDates] = useState([]);
  const [shiftNotes, setShiftNotes] = useState();
  const [deletedShift, setDeletedShift] = useState();
  const [startInput, setStartInput] = useState();
  const [endInput, setEndInput] = useState();
  const [minTime, setMinTime] = useState();
  const [maxTime, setMaxTime] = useState();
  const [headerPrimary, setHeaderPrimary] = useState('');
  const [headerDate, setHeaderDate] = useState('');
  const [toastMessage, setToastMessage] = useState();
  const [isEdit, setEdit] = useState(false);
  const [isPopupOpen, setPopupOpen] = useState(false);
  const [isSnackbarOpen, setSnackbarOpen] = useState(false);
  const [isToastOpen, setToastOpen] = useState(false);

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

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

  const fillPopup = useCallback((event, isEdit) => {
    setEdit(isEdit);
    setMinTime(event.slot === 1 ? '07:00' : '12:00');
    setMaxTime(event.slot === 1 ? '13:00' : '18:00');
    setShift(event);
    setShiftDates([new Date(event.start), new Date(event.end)]);
    setShiftNotes(event.notes);
  }, []);

  const deleteEvent = useCallback(
    (event) => {
      setShifts(shifts.filter((s) => s.id !== event.id));
      setDeletedShift(event);
      setSnackbarOpen(true);
    },
    [shifts],
  );

  const updateEvent = useCallback(
    (event) => {
      // Update the event in the list
      const index = shifts.findIndex((s) => s.id === event.id);
      const newShifts = [...shifts];

      newShifts.splice(index, 1, event);
      setShifts(newShifts);
    },
    [shifts],
  );

  const saveEvent = useCallback(
    (event) => {
      const start = shiftDates[0];
      const end = shiftDates[1] ? shiftDates[1] : shiftDates[0];

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

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

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

      if (isEdit) {
        updateEvent(event);
      } else {
        // Add the new event to the list
        setShifts([...shifts, event]);
      }
      setPopupOpen(false);
    },
    [isEdit, shifts, shiftDates, shiftNotes, updateEvent],
  );

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

  const mySlots = useMemo(
    () => [
      { id: 1, name: 'Morning' },
      { id: 2, name: 'Afternoon' },
    ],
    [],
  );

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

  const myView = useMemo(
    () => ({
      timeline: {
        type: 'week',
        eventDisplay: 'fill',
        startDay: 1,
        endDay: 5,
      },
    }),
    [],
  );

  const popupButtons = useMemo(
    () =>
      isEdit
        ? [
            'cancel',
            {
              handler: () => {
                saveEvent(shift);
              },
              keyCode: 'enter',
              text: 'Save',
              cssClass: 'mbsc-popup-button-primary',
            },
          ]
        : [
            'cancel',
            {
              handler: () => {
                saveEvent(shift);
              },
              keyCode: 'enter',
              text: 'Add',
              cssClass: 'mbsc-popup-button-primary',
            },
          ],
    [isEdit, saveEvent, shift],
  );

  const popupResponsive = useMemo(
    () => ({
      medium: {
        display: 'center',
        fullScreen: false,
        touchUi: false,
        width: 400,
      },
    }),
    [],
  );

  const snackbarButton = useMemo(
    () => ({
      action: () => {
        setShifts((prevShifts) => [...prevShifts, deletedShift]);
      },
      text: 'Undo',
    }),
    [deletedShift],
  );

  const renderPopupHeader = useCallback(
    () => (
      <>
        <div className="mds-employee-shifts-header-primary">{headerPrimary}</div>
        <div className="mds-employee-shifts-header-date">{headerDate}</div>
      </>
    ),
    [headerPrimary, headerDate],
  );

  const handleEventClick = useCallback(
    (args) => {
      const event = args.event;
      const resource = args.resourceObj;
      const slot = args.slotObj;

      fillPopup(event, true);
      setHeaderPrimary('Edit ' + resource.name + "'s hours");
      setHeaderDate(formatDate('DDDD', new Date(event.start)) + ' ' + slot.name + ', ' + formatDate('D MMMM YYYY', new Date(event.start)));
      setPopupOpen(true);
    },
    [fillPopup],
  );

  const handleEventCreated = useCallback(
    (args) => {
      const event = args.event;
      const slot = args.slotObj;

      fillPopup(event, false);
      setHeaderPrimary('New shift');
      setHeaderDate(formatDate('DDDD', new Date(event.start)) + ' ' + slot.name + ', ' + formatDate('D MMMM YYYY', new Date(event.start)));
      setPopupOpen(true);
    },
    [fillPopup],
  );

  const handleEventCreateFailed = useCallback(() => {
    setToastMessage("Can't create shift");
    setToastOpen(true);
  }, []);

  const handleEventDeleted = useCallback(
    (args) => {
      deleteEvent(args.event);
    },
    [deleteEvent],
  );

  const handleEventUpdated = useCallback(
    (args) => {
      const shift = args.event;
      if (shift.slot !== args.oldEvent.slot) {
        const data = getShiftTimes(shift);
        shift.start = data.start;
        shift.end = data.end;
        shift.title = data.title;
        updateEvent(shift);
      }
    },
    [getShiftTimes, updateEvent],
  );

  const handleEventUpdateFailed = useCallback(() => {
    setToastMessage("Can't move shift");
    setToastOpen(true);
  }, []);

  const handleShiftNotesChange = useCallback((ev) => {
    setShiftNotes(ev.target.value);
  }, []);

  const handleShiftDatesChange = useCallback((args) => {
    setShiftDates(args.value);
  }, []);

  const handleShiftDeleteClick = useCallback(() => {
    deleteEvent(shift);
    setPopupOpen(false);
  }, [deleteEvent, shift]);

  const handlePopupClose = useCallback(() => {
    if (!isEdit) {
      // Remove event if popup is cancelled
      setShifts([...shifts]);
    }
    setPopupOpen(false);
  }, [isEdit, shifts]);

  const handleSnackbarClose = useCallback(() => {
    setSnackbarOpen(false);
  }, []);

  const handleToastClose = useCallback(() => {
    setToastOpen(false);
  }, []);

  const renderMyResource = useCallback(
    (resource) => (
      <div className="mbsc-flex">
        <img className="mds-employee-shifts-avatar" alt={resource.name} src={resource.img} />
        <div className="mds-employee-shifts-cont">
          <div className="mds-employee-shifts-name">{resource.name}</div>
          <div className="mds-employee-shifts-title">{resource.title}</div>
        </div>
      </div>
    ),
    [],
  );

  return (
    <>
      <Eventcalendar
        cssClass="mds-employee-shifts"
        clickToCreate={true}
        dragToCreate={false}
        dragToResize={false}
        dragToMove={true}
        data={shifts}
        eventOverlap={false}
        invalid={myInvalids}
        resources={staff}
        slots={mySlots}
        view={myView}
        extendDefaultEvent={getShiftTimes}
        renderResource={renderMyResource}
        onEventClick={handleEventClick}
        onEventCreated={handleEventCreated}
        onEventCreateFailed={handleEventCreateFailed}
        onEventDeleted={handleEventDeleted}
        onEventUpdated={handleEventUpdated}
        onEventUpdateFailed={handleEventUpdateFailed}
      />
      <Popup
        buttons={popupButtons}
        display="bottom"
        contentPadding={false}
        fullScreen={true}
        renderHeader={renderPopupHeader}
        isOpen={isPopupOpen}
        responsive={popupResponsive}
        scrollLock={false}
        onClose={handlePopupClose}
      >
        <Datepicker
          controls={['time']}
          display="anchored"
          minTime={minTime}
          maxTime={maxTime}
          select="range"
          showRangeLabels={false}
          stepMinute={30}
          startInput={startInput}
          endInput={endInput}
          timeWheels="|h:mm A|"
          touchUi={false}
          value={shiftDates}
          onChange={handleShiftDatesChange}
        />
        <div className="mbsc-form-group">
          <Input ref={setStartInput} dropdown={true} label="Shift start" />
          <Input ref={setEndInput} dropdown={true} label="Shift end" />
        </div>
        <div className="mbsc-form-group">
          <Textarea label="Notes" value={shiftNotes} onChange={handleShiftNotesChange} />
        </div>
        {isEdit && (
          <div className="mbsc-button-group">
            <Button className="mbsc-button-block" color="danger" variant="outline" onClick={handleShiftDeleteClick}>
              Delete shift
            </Button>
          </div>
        )}
      </Popup>
      <Snackbar message="Shift deleted" isOpen={isSnackbarOpen} onClose={handleSnackbarClose} button={snackbarButton} />
      <Toast message={toastMessage} isOpen={isToastOpen} onClose={handleToastClose} />
    </>
  );
}

export default App;
```

### TSX

```tsx
import {
  Button,
  Datepicker,
  Eventcalendar,
  formatDate,
  Input,
  MbscCalendarEvent,
  MbscDatepickerChangeEvent,
  MbscDatepickerValue,
  MbscEventcalendarView,
  MbscEventClickEvent,
  MbscEventCreatedEvent,
  MbscEventDeletedEvent,
  MbscEventUpdatedEvent,
  MbscPopupButton,
  MbscPopupOptions,
  MbscResource,
  MbscResponsiveOptions,
  MbscSlot,
  Popup,
  setOptions,
  Snackbar,
  Textarea,
  Toast,
} from '@mobiscroll/react';
import { ChangeEvent, useCallback, useMemo, useState } from 'react';

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

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

function App() {
  const [shifts, setShifts] = useState<MbscCalendarEvent[]>(defaultShifts);
  const [shift, setShift] = useState<MbscCalendarEvent>();
  const [shiftDates, setShiftDates] = useState<MbscDatepickerValue>([]);
  const [shiftNotes, setShiftNotes] = useState<string>();
  const [deletedShift, setDeletedShift] = useState<MbscCalendarEvent>();
  const [startInput, setStartInput] = useState<Input | null>(null);
  const [endInput, setEndInput] = useState<Input | null>(null);
  const [minTime, setMinTime] = useState<string>();
  const [maxTime, setMaxTime] = useState<string>();
  const [headerPrimary, setHeaderPrimary] = useState<string>('');
  const [headerDate, setHeaderDate] = useState<string>('');
  const [toastMessage, setToastMessage] = useState<string>();
  const [isEdit, setEdit] = useState(false);
  const [isPopupOpen, setPopupOpen] = useState(false);
  const [isSnackbarOpen, setSnackbarOpen] = useState(false);
  const [isToastOpen, setToastOpen] = useState(false);

  const getShiftTimes = useCallback((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,
    };
  }, []);

  const fillPopup = useCallback((event: MbscCalendarEvent, isEdit: boolean) => {
    setEdit(isEdit);
    setMinTime(event.slot === 1 ? '07:00' : '12:00');
    setMaxTime(event.slot === 1 ? '13:00' : '18:00');
    setShift(event);
    setShiftDates([new Date(event.start as Date), new Date(event.end as Date)]);
    setShiftNotes(event.notes);
  }, []);

  const deleteEvent = useCallback(
    (event: MbscCalendarEvent) => {
      setShifts(shifts.filter((s) => s.id !== event.id));
      setDeletedShift(event);
      setSnackbarOpen(true);
    },
    [shifts],
  );

  const updateEvent = useCallback(
    (event: MbscCalendarEvent) => {
      // Update the event in the list
      const index = shifts.findIndex((s) => s.id === event.id);
      const newShifts = [...shifts];

      newShifts.splice(index, 1, event);
      setShifts(newShifts);
    },
    [shifts],
  );

  const saveEvent = useCallback(
    (event: MbscCalendarEvent) => {
      const dates = 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 = shiftNotes;

      if (isEdit) {
        updateEvent(event);
      } else {
        // Add the new event to the list
        setShifts([...shifts, event]);
      }
      setPopupOpen(false);
    },
    [isEdit, shifts, shiftDates, shiftNotes, updateEvent],
  );

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

  const mySlots = useMemo<MbscSlot[]>(
    () => [
      { id: 1, name: 'Morning' },
      { id: 2, name: 'Afternoon' },
    ],
    [],
  );

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

  const myView = useMemo<MbscEventcalendarView>(
    () => ({
      timeline: {
        type: 'week',
        eventDisplay: 'fill',
        startDay: 1,
        endDay: 5,
      },
    }),
    [],
  );

  const popupButtons = useMemo<Array<MbscPopupButton | string>>(
    () =>
      isEdit
        ? [
            'cancel',
            {
              handler: () => {
                saveEvent(shift!);
              },
              keyCode: 'enter',
              text: 'Save',
              cssClass: 'mbsc-popup-button-primary',
            },
          ]
        : [
            'cancel',
            {
              handler: () => {
                saveEvent(shift!);
              },
              keyCode: 'enter',
              text: 'Add',
              cssClass: 'mbsc-popup-button-primary',
            },
          ],
    [isEdit, saveEvent, shift],
  );

  const popupResponsive: MbscResponsiveOptions<MbscPopupOptions> = useMemo(
    () => ({
      medium: {
        display: 'center',
        fullScreen: false,
        touchUi: false,
        width: 400,
      },
    }),
    [],
  );

  const snackbarButton = useMemo(
    () => ({
      action: () => {
        setShifts((prevShifts) => [...prevShifts, deletedShift!]);
      },
      text: 'Undo',
    }),
    [deletedShift],
  );

  const renderPopupHeader = useCallback(
    () => (
      <>
        <div className="mds-employee-shifts-header-primary">{headerPrimary}</div>
        <div className="mds-employee-shifts-header-date">{headerDate}</div>
      </>
    ),
    [headerPrimary, headerDate],
  );

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

      fillPopup(event, true);
      setHeaderPrimary('Edit ' + resource.name + "'s hours");
      setHeaderDate(
        formatDate('DDDD', new Date(event.start as Date)) +
          ' ' +
          slot.name +
          ', ' +
          formatDate('D MMMM YYYY', new Date(event.start as Date)),
      );
      setPopupOpen(true);
    },
    [fillPopup],
  );

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

      fillPopup(event, false);
      setHeaderPrimary('New shift');
      setHeaderDate(
        formatDate('DDDD', new Date(event.start as Date)) +
          ' ' +
          slot.name +
          ', ' +
          formatDate('D MMMM YYYY', new Date(event.start as Date)),
      );
      setPopupOpen(true);
    },
    [fillPopup],
  );

  const handleEventCreateFailed = useCallback(() => {
    setToastMessage("Can't create shift");
    setToastOpen(true);
  }, []);

  const handleEventDeleted = useCallback(
    (args: MbscEventDeletedEvent) => {
      deleteEvent(args.event);
    },
    [deleteEvent],
  );

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

  const handleEventUpdateFailed = useCallback(() => {
    setToastMessage("Can't move shift");
    setToastOpen(true);
  }, []);

  const handleShiftNotesChange = useCallback((ev: ChangeEvent<HTMLTextAreaElement>) => {
    setShiftNotes(ev.target.value);
  }, []);

  const handleShiftDatesChange = useCallback((args: MbscDatepickerChangeEvent) => {
    setShiftDates(args.value);
  }, []);

  const handleShiftDeleteClick = useCallback(() => {
    deleteEvent(shift!);
    setPopupOpen(false);
  }, [deleteEvent, shift]);

  const handlePopupClose = useCallback(() => {
    if (!isEdit) {
      // Remove event if popup is cancelled
      setShifts([...shifts]);
    }
    setPopupOpen(false);
  }, [isEdit, shifts]);

  const handleSnackbarClose = useCallback(() => {
    setSnackbarOpen(false);
  }, []);

  const handleToastClose = useCallback(() => {
    setToastOpen(false);
  }, []);

  const renderMyResource = useCallback(
    (resource: MbscResource) => (
      <div className="mbsc-flex">
        <img className="mds-employee-shifts-avatar" alt={resource.name} src={resource.img} />
        <div className="mds-employee-shifts-cont">
          <div className="mds-employee-shifts-name">{resource.name}</div>
          <div className="mds-employee-shifts-title">{resource.title}</div>
        </div>
      </div>
    ),
    [],
  );

  return (
    <>
      <Eventcalendar
        cssClass="mds-employee-shifts"
        clickToCreate={true}
        dragToCreate={false}
        dragToResize={false}
        dragToMove={true}
        data={shifts}
        eventOverlap={false}
        invalid={myInvalids}
        resources={staff}
        slots={mySlots}
        view={myView}
        extendDefaultEvent={getShiftTimes}
        renderResource={renderMyResource}
        onEventClick={handleEventClick}
        onEventCreated={handleEventCreated}
        onEventCreateFailed={handleEventCreateFailed}
        onEventDeleted={handleEventDeleted}
        onEventUpdated={handleEventUpdated}
        onEventUpdateFailed={handleEventUpdateFailed}
      />
      <Popup
        buttons={popupButtons}
        display="bottom"
        contentPadding={false}
        fullScreen={true}
        renderHeader={renderPopupHeader}
        isOpen={isPopupOpen}
        responsive={popupResponsive}
        scrollLock={false}
        onClose={handlePopupClose}
      >
        <Datepicker
          controls={['time']}
          display="anchored"
          minTime={minTime}
          maxTime={maxTime}
          select="range"
          showRangeLabels={false}
          stepMinute={30}
          startInput={startInput}
          endInput={endInput}
          timeWheels="|h:mm A|"
          touchUi={false}
          value={shiftDates}
          onChange={handleShiftDatesChange}
        />
        <div className="mbsc-form-group">
          <Input ref={setStartInput} dropdown={true} label="Shift start" />
          <Input ref={setEndInput} dropdown={true} label="Shift end" />
        </div>
        <div className="mbsc-form-group">
          <Textarea label="Notes" value={shiftNotes} onChange={handleShiftNotesChange} />
        </div>
        {isEdit && (
          <div className="mbsc-button-group">
            <Button className="mbsc-button-block" color="danger" variant="outline" onClick={handleShiftDeleteClick}>
              Delete shift
            </Button>
          </div>
        )}
      </Popup>
      <Snackbar message="Shift deleted" isOpen={isSnackbarOpen} onClose={handleSnackbarClose} button={snackbarButton} />
      <Toast message={toastMessage} isOpen={isToastOpen} onClose={handleToastClose} />
    </>
  );
}

export default App;
```

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

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 react --trial`.

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

## Related demos

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