---
title: "React Timeline Restaurant Shift Scheduling Example | Mobiscroll"
component: "Timeline"
framework: "React"
library_version: "6.2.0"
url: "https://demo.mobiscroll.com/react/timeline/restaurant-shift-management"
---

# Restaurant Shift Scheduling - React Timeline demo

Build a restaurant shift planning with React Timeline — organize staff by role, track occupancy per shift, and toggle shifts on or off. For React and Next.JS.

- [Live demo](https://demo.mobiscroll.com/react/timeline/restaurant-shift-management)
- [View React JS code on GitHub](https://github.com/acidb/mobiscroll-demos-react/tree/main/src/demos/eventcalendar/timeline/restaurant-shift-management)
- [View React TS code on GitHub](https://github.com/acidb/mobiscroll-demos-react-ts/tree/main/src/demos/eventcalendar/timeline/restaurant-shift-management)
- This demo in other frameworks: [JavaScript](https://demo.mobiscroll.com/javascript/timeline/restaurant-shift-management.md), [jQuery](https://demo.mobiscroll.com/jquery/timeline/restaurant-shift-management.md), [Angular](https://demo.mobiscroll.com/angular/timeline/restaurant-shift-management.md), [Vue](https://demo.mobiscroll.com/vue/timeline/restaurant-shift-management.md)

## Description

With the combination of the [timeline summary mode/event listing](https://demo.mobiscroll.com/react/timeline/event-listing), [time slots](https://mobiscroll.com/docs/react/eventcalendar/api#opt-slots) and the [time slot templating](https://demo.mobiscroll.com/react/timeline/shift-template) features along with [hierarchical resources](https://demo.mobiscroll.com/react/timeline/resource-grouping-hierarchy) you can create a restaurant shift planning UI.

The example features templating, dynamic occupancy count (how many employees are working on a specific shift), dynamically turning shifts on/off, switching between week and day view and much more. The [header features custom components](https://demo.mobiscroll.com/react/scheduler/customizing-header) for filtering the view.

Try signing somone up for work by double clicking on an empty shift position.

## What this demo shows

- A desktop weekly timeline for restaurant shift scheduling, with Monday to Friday working days arranged horizontally and a hierarchical resource tree on the left.
- **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.
- **Header filters:** Checkbox controls in the header let users show or hide individual shift types.
- **View switching:** A header select control lets users switch between Day and Week timeline views. Changing the view updates the timeline layout.
- **Week view:** The fixed date strip shows the selected work week from Monday to Friday, using the `DD DDD MMM YYYY` date format, with the current date highlighted.
- **Shift columns:** The timeline is divided into named work shifts instead of standard hourly intervals.
- **Shift structure:** Each day repeats the same shift columns: Night (2 AM - 10 AM), Breakfast (8 AM - 12 PM), Lunch (11 AM - 3 PM), Dinner (4 PM - 11 PM), and After hours (11 PM - 1 AM).
- **Shift headers:** Each shift header shows the shift name, time range, and occupancy indicators with icons and counts for how many people from each resource category are assigned to that shift.
- **Resources:** The left side shows expandable and collapsible resource groups for Barista, Bartenders, Chefs, Cleaners, Cooks, Hosts, Managers, Servers, and Sommeliers.
- **Resource styling:** Each parent resource group has its own color and representative icon.
- **Resource hierarchy:** Individual staff members are organized under their corresponding job categories.
- **Event cards:** Shift assignments appear as events in the matching shift column for each employee row.
- **Event colors:** Event colors visually distinguish assignments across resource groups.
- **Event creation:** Users can create new shift assignments by double-clicking an empty shift position on an individual employee row.
- **Event interaction:** Events are highlighted on hover.
- **Event selection:** Clicking an event selects and highlights it.
- **Vertical scrolling:** The resource list scrolls vertically so many employees can be shown in the same timeline.
- **Horizontal scrolling:** The timeline supports horizontal scrolling for navigating across multiple days and shift columns.

## Best for

- **Restaurant scheduling:** Planning staff coverage across predefined restaurant shifts.
- **Hospitality workforce planning:** Assigning employees to structured service periods such as breakfast, lunch, dinner, and after-hours work.
- **Retail staffing:** Managing teams where employees are scheduled into named shift blocks rather than arbitrary time slots.
- **Shift-based resource planning:** Showing role groups, individual employees, and occupancy counts in the same scheduling view.
- **Dense team schedules:** Working with many employees in a hierarchical timeline that supports both vertical and horizontal scrolling.

## 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
- [`theme`](https://mobiscroll.com/docs/react/forms/checkbox#opt-theme) - option
- [`data`](https://mobiscroll.com/docs/react/select/api#opt-data) - option
- [`onChange`](https://mobiscroll.com/docs/react/select/api#event-onChange) - event
- [`inputStyle`](https://mobiscroll.com/docs/react/select/api#opt-inputStyle) - option
- [`view`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-view) - option
- [`data`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-data) - option
- [`resources`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-resources) - option
- [`slots`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-slots) - option
- [`clickToCreate`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-clickToCreate) - option
- [`dragToMove`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-dragToMove) - option
- [`extendDefaultEvent`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-extendDefaultEvent) - option
- [`renderResource`](https://mobiscroll.com/docs/react/eventcalendar/api#renderer-renderResource) - renderer
- [`renderSlot`](https://mobiscroll.com/docs/react/eventcalendar/api#renderer-renderSlot) - renderer
- [`renderHeader`](https://mobiscroll.com/docs/react/eventcalendar/api#renderer-renderHeader) - renderer
- [`onEventUpdate`](https://mobiscroll.com/docs/react/eventcalendar/api#event-onEventUpdate) - event
- [`onEventCreate`](https://mobiscroll.com/docs/react/eventcalendar/api#event-onEventCreate) - event
- [`onEventDeleted`](https://mobiscroll.com/docs/react/eventcalendar/api#event-onEventDeleted) - event
- [`cssClass`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-cssClass) - option

## Code

### JSX

```jsx
import {
  CalendarNav,
  CalendarNext,
  CalendarPrev,
  CalendarToday,
  Checkbox,
  Eventcalendar,
  formatDate,
  Select,
  setOptions,
} from '@mobiscroll/react';
import { Fragment, useCallback, useMemo, useState } from 'react';

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

const myResources = [
  {
    id: 1,
    name: 'Barista',
    icon: '\u2615\uFE0F',
    eventCreation: false,
    children: [
      {
        id: 11,
        name: 'Jude Chester',
        color: '#1dab2f',
      },
      {
        id: 12,
        name: 'Willis Kane',
        color: '#1dab2f',
      },
      {
        id: 13,
        name: 'Ryan Melicent',
        color: '#1dab2f',
      },
    ],
  },
  {
    id: 2,
    name: 'Bartenders',
    icon: '\uD83C\uDF78',
    eventCreation: false,
    children: [
      {
        id: 21,
        name: 'Derek Austyn',
        color: '#4981d6',
      },
      {
        id: 22,
        name: 'Merv Kenny',
        color: '#4981d6',
      },
      {
        id: 23,
        name: 'Theo Calanthia',
        color: '#4981d6',
      },
    ],
  },
  {
    id: 3,
    name: 'Chefs',
    icon: '\uD83D\uDC69\u200D\uD83C\uDF73',
    eventCreation: false,
    children: [
      {
        id: 31,
        name: 'Ford Kaiden',
        color: '#d6d145',
      },
      {
        id: 32,
        name: 'Jewell Ryder',
        color: '#d6d145',
      },
      {
        id: 33,
        name: 'Dory Edie',
        color: '#d6d145',
      },
    ],
  },
  {
    id: 4,
    name: 'Cleaners',
    icon: '\uD83E\uDDF9',
    eventCreation: false,
    children: [
      {
        id: 41,
        name: 'Breanne Lorinda',
        color: '#ff1717',
      },
      {
        id: 42,
        name: 'Jenifer Kalyn',
        color: '#ff1717',
      },
      {
        id: 43,
        name: 'Natalie Racquel',
        color: '#ff1717',
      },
      {
        id: 44,
        name: 'Kaylin Toni',
        color: '#ff1717',
      },
      {
        id: 45,
        name: 'Gray Kestrel',
        color: '#ff1717',
      },
      {
        id: 46,
        name: 'Florence Amy',
        color: '#ff1717',
      },
    ],
  },
  {
    id: 5,
    name: 'Cooks',
    icon: '\uD83C\uDF72',
    eventCreation: false,
    children: [
      {
        id: 51,
        name: 'Meredith Chantelle',
        color: '#f7961e',
      },
      {
        id: 52,
        name: 'Jon Drake',
        color: '#f7961e',
      },
      {
        id: 53,
        name: 'Carlyn Dorothy',
        color: '#f7961e',
      },
      {
        id: 54,
        name: 'Isadora Lyric',
        color: '#f7961e',
      },
    ],
  },
  {
    id: 6,
    name: 'Hosts',
    icon: '\uD83D\uDECE',
    eventCreation: false,
    children: [
      {
        id: 61,
        name: 'Layton Candace',
        color: '#7056ff',
      },
      {
        id: 62,
        name: 'Sylvia Cale',
        color: '#7056ff',
      },
    ],
  },
  {
    id: 7,
    name: 'Managers',
    icon: '\uD83D\uDC69\u200D\uD83D\uDCBC',
    eventCreation: false,
    children: [
      {
        id: 71,
        name: 'Fred Valdez',
        color: '#3a8700',
      },
      {
        id: 72,
        name: 'Antonia Cindra',
        color: '#3a8700',
      },
      {
        id: 73,
        name: 'Gerry Irma',
        color: '#3a8700',
      },
    ],
  },
  {
    id: 8,
    name: 'Servers',
    icon: '\uD83E\uDD35',
    eventCreation: false,
    children: [
      {
        id: 81,
        name: 'Reg Izabelle',
        color: '#e25dd2',
      },
      {
        id: 82,
        name: 'Adrianna Sawyer',
        color: '#e25dd2',
      },
      {
        id: 83,
        name: 'Lou Andie',
        color: '#e25dd2',
      },
      {
        id: 84,
        name: 'Leon Porter',
        color: '#e25dd2',
      },
    ],
  },
  {
    id: 9,
    name: 'Sommeliers',
    icon: '\uD83C\uDF77',
    eventCreation: false,
    children: [
      {
        id: 91,
        name: 'Yancy Dustin',
        color: '#34c8e0',
      },
      {
        id: 92,
        name: 'Sierra Clark',
        color: '#34c8e0',
      },
    ],
  },
];

const allShifts = [
  // Showing partial data. Download full source.
  {
    start: '2026-10-31T02:00',
    end: '2026-10-31T10:00',
    title: 'Derek',
    resource: 21,
    slot: 1,
  },
  {
    start: '2026-10-31T02:00',
    end: '2026-10-31T10:00',
    title: 'Florence',
    resource: 46,
    slot: 1,
  },
  {
    start: '2026-10-31T08:00',
    end: '2026-10-31T12:00',
    title: 'Willis',
    resource: 12,
    slot: 2,
  },
  {
    start: '2026-10-31T08:00',
    end: '2026-10-31T12:00',
    title: 'Jewell',
    resource: 32,
    slot: 2,
  },
  {
    start: '2026-10-31T08:00',
    end: '2026-10-31T12:00',
    title: 'Jenifer',
    resource: 42,
    slot: 2,
  },
  {
    start: '2026-10-31T08:00',
    end: '2026-10-31T12:00',
    title: 'Jon',
    resource: 52,
    slot: 2,
  },
  {
    start: '2026-10-31T08:00',
    end: '2026-10-31T12:00',
    title: 'Fred',
    resource: 71,
    slot: 2,
  },
  {
    start: '2026-10-31T08:00',
    end: '2026-10-31T12:00',
    title: 'Adrianna',
    resource: 82,
    slot: 2,
  },
  {
    start: '2026-10-31T08:00',
    end: '2026-10-31T12:00',
    title: 'Jude',
    resource: 11,
    slot: 3,
  },
  {
    start: '2026-10-31T11:00',
    end: '2026-10-31T15:00',
    title: 'Jewell',
    resource: 32,
    slot: 3,
  },
  {
    start: '2026-10-31T11:00',
    end: '2026-10-31T15:00',
    title: 'Gray',
    resource: 45,
    slot: 3,
  },
  {
    start: '2026-10-31T11:00',
    end: '2026-10-31T15:00',
    title: 'Isadora',
    resource: 54,
    slot: 3,
  },
  {
    start: '2026-10-31T11:00',
    end: '2026-10-31T15:00',
    title: 'Antonia',
    resource: 72,
    slot: 3,
  },
  {
    start: '2026-10-31T11:00',
    end: '2026-10-31T15:00',
    title: 'Adrianna',
    resource: 82,
    slot: 3,
  },
  {
    start: '2026-10-31T11:00',
    end: '2026-10-31T15:00',
    title: 'Leon',
    resource: 84,
    slot: 3,
  },
  {
    start: '2026-10-31T11:00',
    end: '2026-10-31T15:00',
    title: 'Yancy',
    resource: 91,
    slot: 3,
  },
  {
    start: '2026-10-31T16:00',
    end: '2026-10-31T23:00',
    title: 'Ryan',
    resource: 13,
    slot: 4,
  },
  {
    start: '2026-10-31T16:00',
    end: '2026-10-31T23:00',
    title: 'Merv',
    resource: 22,
    slot: 4,
  },
  {
    start: '2026-10-31T16:00',
    end: '2026-10-31T23:00',
    title: 'Ford',
    resource: 31,
    slot: 4,
  },
  {
    start: '2026-10-31T16:00',
    end: '2026-10-31T23:00',
    title: 'Dory',
    resource: 33,
    slot: 4,
  },
  {
    start: '2026-10-31T16:00',
    end: '2026-10-31T23:00',
    title: 'Breanne',
    resource: 41,
    slot: 4,
  },
  {
    start: '2026-10-31T16:00',
    end: '2026-10-31T23:00',
    title: 'Meredith',
    resource: 51,
    slot: 4,
  },
  {
    start: '2026-10-31T16:00',
    end: '2026-10-31T23:00',
    title: 'Carlyn',
    resource: 53,
    slot: 4,
  },
  {
    start: '2026-10-31T16:00',
    end: '2026-10-31T23:00',
    title: 'Layton',
    resource: 61,
    slot: 4,
  },
  {
    start: '2026-10-31T16:00',
    end: '2026-10-31T23:00',
    title: 'Gerry',
    resource: 73,
    slot: 4,
  },
  {
    start: '2026-10-31T16:00',
    end: '2026-10-31T23:00',
    title: 'Reg',
    resource: 81,
    slot: 4,
  },
  {
    start: '2026-10-31T16:00',
    end: '2026-10-31T23:00',
    title: 'Lou',
    resource: 83,
    slot: 4,
  },
  {
    start: '2026-10-31T16:00',
    end: '2026-10-31T23:00',
    title: 'Sierra',
    resource: 92,
    slot: 4,
  },
  {
    start: '2026-10-31T11:00',
    end: '2026-10-31T11:59',
    title: 'Derek',
    resource: 21,
    slot: 5,
  },
  {
    start: '2026-10-31T11:00',
    end: '2026-10-31T11:59',
    title: 'Natalie',
    resource: 43,
    slot: 5,
  },
  {
    start: '2026-10-31T11:00',
    end: '2026-10-31T11:59',
    title: 'Kaylin',
    resource: 44,
    slot: 5,
  },
  {
    start: '2026-10-31T11:00',
    end: '2026-10-31T11:59',
    title: 'Fred',
    resource: 71,
    slot: 5,
  }, // Showing partial data. Download full source.
];

const allSlots = [
  {
    id: 1,
    name: 'Night',
    time: '2AM - 10AM',
  },
  {
    id: 2,
    name: 'Breakfast',
    time: '8AM - 12PM',
  },
  {
    id: 3,
    name: 'Lunch',
    time: '11AM - 3PM',
  },
  {
    id: 4,
    name: 'Dinner',
    time: '4PM - 11PM',
  },
  {
    id: 5,
    name: 'After hours',
    time: '11PM - 1AM',
  },
];

const views = [
  {
    text: 'Day',
    value: 'day',
  },
  {
    text: 'Week',
    value: 'week',
  },
];

function App() {
  const [shifts, setShifts] = useState(allShifts);
  const [mySlots, setMySlots] = useState(allSlots);
  const [shiftTimes, setShiftTimes] = useState([
    {
      id: 1,
      name: 'Night',
      checked: true,
      disabled: false,
    },
    {
      id: 2,
      name: 'Breakfast',
      checked: true,
      disabled: false,
    },
    {
      id: 3,
      name: 'Lunch',
      checked: true,
      disabled: false,
    },
    {
      id: 4,
      name: 'Dinner',
      checked: true,
      disabled: false,
    },
    {
      id: 5,
      name: 'After hours',
      checked: true,
      disabled: false,
    },
  ]);

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

  const getEmployeeName = useCallback((event) => {
    for (let i = 0; i < myResources.length; ++i) {
      for (let j = 0; j < myResources[i].children.length; ++j) {
        const employee = myResources[i].children[j];
        if (employee.id === event.resource) {
          return employee.name.substring(0, employee.name.indexOf(' '));
        }
      }
    }
  }, []);

  const extendMyDefaultEvent = useCallback(
    (event) => ({
      title: getEmployeeName(event),
    }),
    [getEmployeeName],
  );

  const formatMyDate = useCallback((date) => formatDate('YYYY-MM-DD', new Date(date)), []);

  const getShiftsNrs = useCallback(
    (date, slotId) => {
      const shiftList = [];

      for (let i = 0; i < shifts.length; ++i) {
        const shift = shifts[i];
        // Get slot id from resource id
        const resourceNr = +shift.resource.toString().charAt(0);
        if (shift.slot === slotId && date === formatMyDate(shift.start)) {
          shiftList[resourceNr - 1] = (shiftList[resourceNr - 1] || 0) + 1;
        }
      }
      return shiftList;
    },
    [shifts, formatMyDate],
  );

  const renderMyResource = useCallback((resource) => {
    const parent = resource.children;
    return (
      <div className={parent ? 'md-shift-resource' : ''} style={{ color: parent ? parent[0].color : '' }}>
        {parent && <span className="md-shift-resource-icon">{resource.icon}</span>}
        {resource.name}
      </div>
    );
  }, []);

  const renderMySlot = useCallback(
    (args) => {
      const slot = args.slot;
      const date = formatMyDate(args.date);
      const shiftList = getShiftsNrs(date, slot.id);
      let length = 0;

      return (
        <div className="md-shift-header">
          <div className="md-shift-name">
            {slot.name}
            <span className="md-shift-time">{slot.time}</span>
          </div>
          <div className={'md-shift-counts-' + date + '-' + slot.id}>
            {shiftList.length > 0 &&
              shiftList.map((shift, i) => {
                ++length;
                return (
                  <Fragment key={i}>
                    <div className="md-shift-count">
                      <span className="md-shift-icon">{myResources[i].icon}</span>
                      <span className={'md-shift-nr md-shift-nr-' + date + '-' + slot.id + '-' + (i + 1)}>{shift}</span>
                    </div>
                    {length === 4 && <br />}
                  </Fragment>
                );
              })}
            {shiftList.length === 0 && <div className="md-shift-count">{'\uD83D\uDE36'} empty...</div>}
          </div>
        </div>
      );
    },
    [formatMyDate, getShiftsNrs],
  );

  const checkboxChange = useCallback(
    (ev) => {
      const value = +ev.target.value;
      const checked = ev.target.checked;
      let filteredSlots = [];
      let updatedTimes = shiftTimes.map((cs) => (cs.id === value ? { ...cs, checked } : cs));

      for (const slot of allSlots) {
        if (updatedTimes.find((us) => us.id === slot.id && us.checked)) {
          filteredSlots.push(slot);
        }
      }

      if (filteredSlots.length === 1) {
        updatedTimes = updatedTimes.map((ut) => (ut.checked ? { ...ut, disabled: true } : ut));
      } else {
        updatedTimes = updatedTimes.map((ut) => ({ ...ut, disabled: false }));
      }

      setMySlots(filteredSlots);
      setShiftTimes(updatedTimes);
    },
    [shiftTimes],
  );

  const viewChange = useCallback((event) => {
    setView(event.value);
  }, []);

  const renderMyHeader = () => (
    <Fragment>
      <CalendarNav />
      <div className="mbsc-flex mbsc-flex-1-0 mbsc-justify-content-end">
        {shiftTimes.map((cs) => (
          <Checkbox key={cs.id} value={cs.id} checked={cs.checked} disabled={cs.disabled} onChange={checkboxChange} theme="material">
            {cs.name}
          </Checkbox>
        ))}
      </div>
      <Select data={views} value={selectedView} onChange={viewChange} inputStyle="box" />
      <CalendarPrev />
      <CalendarToday />
      <CalendarNext />
    </Fragment>
  );

  const isDouble = useCallback((event, inst) => {
    const date = event.start.setHours(0);
    const events = inst.getEvents(date);
    const ev = events.find((e) => new Date(e.start).setHours(0) === date && e.resource === event.resource && e.slot === event.slot);
    return ev;
  }, []);

  const handleCreateEvent = useCallback(
    (args, inst) => {
      if (isDouble(args.event, inst)) {
        return false;
      } else {
        setTimeout(() => {
          setShifts([...shifts, args.event]);
        });
      }
    },
    [shifts, isDouble],
  );

  const handleEventUpdate = useCallback(
    (args, inst) => {
      const event = args.event;

      if (isDouble(event, inst)) {
        return false;
      } else {
        const newName = getEmployeeName(event);
        if (newName) {
          const index = shifts.findIndex((x) => x.id === event.id);
          const newShifts = [...shifts];
          newShifts[index].title = newName;
          setShifts(newShifts);
        }
      }
    },
    [shifts, getEmployeeName, isDouble],
  );

  const handleDeleteEvent = useCallback(
    (args) => {
      setShifts(shifts.filter((item) => item.id !== args.event.id));
    },
    [shifts],
  );

  return (
    <Eventcalendar
      view={myView}
      data={shifts}
      resources={myResources}
      slots={mySlots}
      clickToCreate={true}
      dragToMove={true}
      extendDefaultEvent={extendMyDefaultEvent}
      renderResource={renderMyResource}
      renderSlot={renderMySlot}
      renderHeader={renderMyHeader}
      onEventUpdate={handleEventUpdate}
      onEventCreate={handleCreateEvent}
      onEventDeleted={handleDeleteEvent}
      cssClass="md-shift-management-calendar"
    />
  );
}

export default App;
```

### TSX

```tsx
import {
  CalendarNav,
  CalendarNext,
  CalendarPrev,
  CalendarToday,
  Checkbox,
  Eventcalendar,
  formatDate,
  MbscCalendarEvent,
  MbscEventcalendarView,
  MbscEventCreateEvent,
  MbscEventDeletedEvent,
  MbscEventUpdateEvent,
  MbscNewEventData,
  MbscResource,
  MbscSelectChangeEvent,
  MbscSlot,
  MbscSlotData,
  Select,
  setOptions,
} from '@mobiscroll/react';
import { ChangeEvent, Fragment, useCallback, useMemo, useState } from 'react';

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

const myResources: MbscResource[] = [
  {
    id: 1,
    name: 'Barista',
    icon: '\u2615\uFE0F',
    eventCreation: false,
    children: [
      {
        id: 11,
        name: 'Jude Chester',
        color: '#1dab2f',
      },
      {
        id: 12,
        name: 'Willis Kane',
        color: '#1dab2f',
      },
      {
        id: 13,
        name: 'Ryan Melicent',
        color: '#1dab2f',
      },
    ],
  },
  {
    id: 2,
    name: 'Bartenders',
    icon: '\uD83C\uDF78',
    eventCreation: false,
    children: [
      {
        id: 21,
        name: 'Derek Austyn',
        color: '#4981d6',
      },
      {
        id: 22,
        name: 'Merv Kenny',
        color: '#4981d6',
      },
      {
        id: 23,
        name: 'Theo Calanthia',
        color: '#4981d6',
      },
    ],
  },
  {
    id: 3,
    name: 'Chefs',
    icon: '\uD83D\uDC69\u200D\uD83C\uDF73',
    eventCreation: false,
    children: [
      {
        id: 31,
        name: 'Ford Kaiden',
        color: '#d6d145',
      },
      {
        id: 32,
        name: 'Jewell Ryder',
        color: '#d6d145',
      },
      {
        id: 33,
        name: 'Dory Edie',
        color: '#d6d145',
      },
    ],
  },
  {
    id: 4,
    name: 'Cleaners',
    icon: '\uD83E\uDDF9',
    eventCreation: false,
    children: [
      {
        id: 41,
        name: 'Breanne Lorinda',
        color: '#ff1717',
      },
      {
        id: 42,
        name: 'Jenifer Kalyn',
        color: '#ff1717',
      },
      {
        id: 43,
        name: 'Natalie Racquel',
        color: '#ff1717',
      },
      {
        id: 44,
        name: 'Kaylin Toni',
        color: '#ff1717',
      },
      {
        id: 45,
        name: 'Gray Kestrel',
        color: '#ff1717',
      },
      {
        id: 46,
        name: 'Florence Amy',
        color: '#ff1717',
      },
    ],
  },
  {
    id: 5,
    name: 'Cooks',
    icon: '\uD83C\uDF72',
    eventCreation: false,
    children: [
      {
        id: 51,
        name: 'Meredith Chantelle',
        color: '#f7961e',
      },
      {
        id: 52,
        name: 'Jon Drake',
        color: '#f7961e',
      },
      {
        id: 53,
        name: 'Carlyn Dorothy',
        color: '#f7961e',
      },
      {
        id: 54,
        name: 'Isadora Lyric',
        color: '#f7961e',
      },
    ],
  },
  {
    id: 6,
    name: 'Hosts',
    icon: '\uD83D\uDECE',
    eventCreation: false,
    children: [
      {
        id: 61,
        name: 'Layton Candace',
        color: '#7056ff',
      },
      {
        id: 62,
        name: 'Sylvia Cale',
        color: '#7056ff',
      },
    ],
  },
  {
    id: 7,
    name: 'Managers',
    icon: '\uD83D\uDC69\u200D\uD83D\uDCBC',
    eventCreation: false,
    children: [
      {
        id: 71,
        name: 'Fred Valdez',
        color: '#3a8700',
      },
      {
        id: 72,
        name: 'Antonia Cindra',
        color: '#3a8700',
      },
      {
        id: 73,
        name: 'Gerry Irma',
        color: '#3a8700',
      },
    ],
  },
  {
    id: 8,
    name: 'Servers',
    icon: '\uD83E\uDD35',
    eventCreation: false,
    children: [
      {
        id: 81,
        name: 'Reg Izabelle',
        color: '#e25dd2',
      },
      {
        id: 82,
        name: 'Adrianna Sawyer',
        color: '#e25dd2',
      },
      {
        id: 83,
        name: 'Lou Andie',
        color: '#e25dd2',
      },
      {
        id: 84,
        name: 'Leon Porter',
        color: '#e25dd2',
      },
    ],
  },
  {
    id: 9,
    name: 'Sommeliers',
    icon: '\uD83C\uDF77',
    eventCreation: false,
    children: [
      {
        id: 91,
        name: 'Yancy Dustin',
        color: '#34c8e0',
      },
      {
        id: 92,
        name: 'Sierra Clark',
        color: '#34c8e0',
      },
    ],
  },
];

const allShifts: MbscCalendarEvent[] = [
  // Showing partial data. Download full source.
  {
    start: '2026-10-31T02:00',
    end: '2026-10-31T10:00',
    title: 'Derek',
    resource: 21,
    slot: 1,
  },
  {
    start: '2026-10-31T02:00',
    end: '2026-10-31T10:00',
    title: 'Florence',
    resource: 46,
    slot: 1,
  },
  {
    start: '2026-10-31T08:00',
    end: '2026-10-31T12:00',
    title: 'Willis',
    resource: 12,
    slot: 2,
  },
  {
    start: '2026-10-31T08:00',
    end: '2026-10-31T12:00',
    title: 'Jewell',
    resource: 32,
    slot: 2,
  },
  {
    start: '2026-10-31T08:00',
    end: '2026-10-31T12:00',
    title: 'Jenifer',
    resource: 42,
    slot: 2,
  },
  {
    start: '2026-10-31T08:00',
    end: '2026-10-31T12:00',
    title: 'Jon',
    resource: 52,
    slot: 2,
  },
  {
    start: '2026-10-31T08:00',
    end: '2026-10-31T12:00',
    title: 'Fred',
    resource: 71,
    slot: 2,
  },
  {
    start: '2026-10-31T08:00',
    end: '2026-10-31T12:00',
    title: 'Adrianna',
    resource: 82,
    slot: 2,
  },
  {
    start: '2026-10-31T08:00',
    end: '2026-10-31T12:00',
    title: 'Jude',
    resource: 11,
    slot: 3,
  },
  {
    start: '2026-10-31T11:00',
    end: '2026-10-31T15:00',
    title: 'Jewell',
    resource: 32,
    slot: 3,
  },
  {
    start: '2026-10-31T11:00',
    end: '2026-10-31T15:00',
    title: 'Gray',
    resource: 45,
    slot: 3,
  },
  {
    start: '2026-10-31T11:00',
    end: '2026-10-31T15:00',
    title: 'Isadora',
    resource: 54,
    slot: 3,
  },
  {
    start: '2026-10-31T11:00',
    end: '2026-10-31T15:00',
    title: 'Antonia',
    resource: 72,
    slot: 3,
  },
  {
    start: '2026-10-31T11:00',
    end: '2026-10-31T15:00',
    title: 'Adrianna',
    resource: 82,
    slot: 3,
  },
  {
    start: '2026-10-31T11:00',
    end: '2026-10-31T15:00',
    title: 'Leon',
    resource: 84,
    slot: 3,
  },
  {
    start: '2026-10-31T11:00',
    end: '2026-10-31T15:00',
    title: 'Yancy',
    resource: 91,
    slot: 3,
  },
  {
    start: '2026-10-31T16:00',
    end: '2026-10-31T23:00',
    title: 'Ryan',
    resource: 13,
    slot: 4,
  },
  {
    start: '2026-10-31T16:00',
    end: '2026-10-31T23:00',
    title: 'Merv',
    resource: 22,
    slot: 4,
  },
  {
    start: '2026-10-31T16:00',
    end: '2026-10-31T23:00',
    title: 'Ford',
    resource: 31,
    slot: 4,
  },
  {
    start: '2026-10-31T16:00',
    end: '2026-10-31T23:00',
    title: 'Dory',
    resource: 33,
    slot: 4,
  },
  {
    start: '2026-10-31T16:00',
    end: '2026-10-31T23:00',
    title: 'Breanne',
    resource: 41,
    slot: 4,
  },
  {
    start: '2026-10-31T16:00',
    end: '2026-10-31T23:00',
    title: 'Meredith',
    resource: 51,
    slot: 4,
  },
  {
    start: '2026-10-31T16:00',
    end: '2026-10-31T23:00',
    title: 'Carlyn',
    resource: 53,
    slot: 4,
  },
  {
    start: '2026-10-31T16:00',
    end: '2026-10-31T23:00',
    title: 'Layton',
    resource: 61,
    slot: 4,
  },
  {
    start: '2026-10-31T16:00',
    end: '2026-10-31T23:00',
    title: 'Gerry',
    resource: 73,
    slot: 4,
  },
  {
    start: '2026-10-31T16:00',
    end: '2026-10-31T23:00',
    title: 'Reg',
    resource: 81,
    slot: 4,
  },
  {
    start: '2026-10-31T16:00',
    end: '2026-10-31T23:00',
    title: 'Lou',
    resource: 83,
    slot: 4,
  },
  {
    start: '2026-10-31T16:00',
    end: '2026-10-31T23:00',
    title: 'Sierra',
    resource: 92,
    slot: 4,
  },
  {
    start: '2026-10-31T11:00',
    end: '2026-10-31T11:59',
    title: 'Derek',
    resource: 21,
    slot: 5,
  },
  {
    start: '2026-10-31T11:00',
    end: '2026-10-31T11:59',
    title: 'Natalie',
    resource: 43,
    slot: 5,
  },
  {
    start: '2026-10-31T11:00',
    end: '2026-10-31T11:59',
    title: 'Kaylin',
    resource: 44,
    slot: 5,
  },
  {
    start: '2026-10-31T11:00',
    end: '2026-10-31T11:59',
    title: 'Fred',
    resource: 71,
    slot: 5,
  }, // Showing partial data. Download full source.
];

const allSlots: MbscSlot[] = [
  {
    id: 1,
    name: 'Night',
    time: '2AM - 10AM',
  },
  {
    id: 2,
    name: 'Breakfast',
    time: '8AM - 12PM',
  },
  {
    id: 3,
    name: 'Lunch',
    time: '11AM - 3PM',
  },
  {
    id: 4,
    name: 'Dinner',
    time: '4PM - 11PM',
  },
  {
    id: 5,
    name: 'After hours',
    time: '11PM - 1AM',
  },
];

const views = [
  {
    text: 'Day',
    value: 'day',
  },
  {
    text: 'Week',
    value: 'week',
  },
];

function App() {
  const [shifts, setShifts] = useState<MbscCalendarEvent[]>(allShifts);
  const [mySlots, setSlots] = useState<MbscSlot[]>(allSlots);
  const [shiftTimes, setShiftTimes] = useState([
    {
      id: 1,
      name: 'Night',
      checked: true,
      disabled: false,
    },
    {
      id: 2,
      name: 'Breakfast',
      checked: true,
      disabled: false,
    },
    {
      id: 3,
      name: 'Lunch',
      checked: true,
      disabled: false,
    },
    {
      id: 4,
      name: 'Dinner',
      checked: true,
      disabled: false,
    },
    {
      id: 5,
      name: 'After hours',
      checked: true,
      disabled: false,
    },
  ]);

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

  const getEmployeeName = useCallback((event: MbscCalendarEvent) => {
    for (let i = 0; i < myResources.length; ++i) {
      for (let j = 0; j < myResources[i].children!.length; ++j) {
        const employee = myResources[i].children![j];
        if (employee.id === event.resource) {
          return employee.name!.substring(0, employee.name!.indexOf(' '));
        }
      }
    }
  }, []);

  const extendDefaultEvent = useCallback(
    (event: MbscNewEventData) => ({
      title: getEmployeeName(event),
    }),
    [getEmployeeName],
  );

  const formatMyDate = useCallback((date: Date) => formatDate('YYYY-MM-DD', new Date(date)), []);

  const getShiftsNrs = useCallback(
    (date: string, slotId: number) => {
      const shiftList: Array<number> = [];

      for (let i = 0; i < shifts.length; ++i) {
        const shift = shifts[i];
        // Get slot id from resource id
        const resourceNr = +shift.resource!.toString().charAt(0);
        if (shift.slot === slotId && date === formatMyDate(new Date(shift.start as string))) {
          shiftList[resourceNr - 1] = (shiftList[resourceNr - 1] || 0) + 1;
        }
      }
      return shiftList;
    },
    [shifts, formatMyDate],
  );

  const renderMyResource = (resource: MbscResource) => {
    const parent = resource.children;
    return (
      <div className={parent ? 'md-shift-resource' : ''} style={{ color: parent ? parent[0].color : '' }}>
        {parent && <span className="md-shift-resource-icon">{resource.icon}</span>}
        {resource.name}
      </div>
    );
  };

  const renderMySlot = (args: MbscSlotData) => {
    const slot = args.slot;
    const date = formatMyDate(args.date);
    const shiftList = getShiftsNrs(date, slot.id as number);
    let length = 0;

    return (
      <div className="md-shift-header">
        <div className="md-shift-name">
          {slot.name}
          <span className="md-shift-time">{slot.time}</span>
        </div>
        <div className={'md-shift-counts-' + date + '-' + slot.id}>
          {shiftList.length > 0 &&
            shiftList.map((shift: number, i: number) => {
              ++length;
              return (
                <Fragment key={i}>
                  <div className="md-shift-count">
                    <span className="md-shift-icon">{myResources[i].icon}</span>
                    <span className={'md-shift-nr md-shift-nr-' + date + '-' + slot.id + '-' + (i + 1)}>{shift}</span>
                  </div>
                  {length === 4 && <br />}
                </Fragment>
              );
            })}
          {shiftList.length === 0 && <div className="md-shift-count">{'\uD83D\uDE36'} empty...</div>}
        </div>
      </div>
    );
  };

  const checkboxChange = useCallback(
    (ev: ChangeEvent<HTMLInputElement>) => {
      const value = +ev.target.value;
      const checked = ev.target.checked;
      const filteredSlots: Array<MbscSlot> = [];
      let updatedTimes = shiftTimes.map((cs) => (cs.id === value ? { ...cs, checked } : cs));

      for (const slot of allSlots) {
        if (updatedTimes.find((us) => us.id === slot.id && us.checked)) {
          filteredSlots.push(slot);
        }
      }

      if (filteredSlots.length === 1) {
        updatedTimes = updatedTimes.map((ut) => (ut.checked ? { ...ut, disabled: true } : ut));
      } else {
        updatedTimes = updatedTimes.map((ut) => ({ ...ut, disabled: false }));
      }

      setSlots(filteredSlots);
      setShiftTimes(updatedTimes);
    },
    [shiftTimes],
  );

  const viewChange = useCallback((event: MbscSelectChangeEvent) => {
    setView(event.value);
  }, []);

  const renderMyHeader = () => (
    <>
      <CalendarNav />
      <div className="mbsc-flex mbsc-flex-1-0 mbsc-justify-content-end">
        {shiftTimes.map((cs) => (
          <Checkbox key={cs.id} value={cs.id} checked={cs.checked} disabled={cs.disabled} onChange={checkboxChange} theme="material">
            {cs.name}
          </Checkbox>
        ))}
      </div>
      <Select data={views} value={selectedView} onChange={viewChange} inputStyle="box" />
      <CalendarPrev />
      <CalendarToday />
      <CalendarNext />
    </>
  );

  const isDouble = useCallback((event: MbscCalendarEvent, inst: Eventcalendar) => {
    const date = new Date(event.start as string).setHours(0);
    const events = inst.getEvents(new Date(date));
    const ev = events.find(
      (e) => new Date(e.start as string).setHours(0) === date && e.resource === event.resource && e.slot === event.slot,
    );
    return ev;
  }, []);

  const handleCreateEvent = useCallback(
    (args: MbscEventCreateEvent, inst: Eventcalendar) => {
      if (isDouble(args.event, inst)) {
        return false;
      } else {
        setTimeout(() => {
          setShifts([...shifts, args.event]);
        });
      }
    },
    [shifts, isDouble],
  );

  const handleEventUpdate = useCallback(
    (args: MbscEventUpdateEvent, inst: Eventcalendar) => {
      const event = args.event;

      if (isDouble(event, inst)) {
        return false;
      } else {
        const newName = getEmployeeName(event);
        if (newName) {
          const index = shifts.findIndex((x) => x.id === event.id);
          const newShifts = [...shifts];
          newShifts[index].title = newName;
          setShifts(newShifts);
        }
      }
    },
    [shifts, getEmployeeName, isDouble],
  );

  const handleDeleteEvent = useCallback(
    (args: MbscEventDeletedEvent) => {
      setShifts(shifts.filter((item) => item.id !== args.event.id));
    },
    [shifts],
  );

  return (
    <Eventcalendar
      view={myView}
      data={shifts}
      resources={myResources}
      slots={mySlots}
      clickToCreate={true}
      dragToMove={true}
      extendDefaultEvent={extendDefaultEvent}
      renderResource={renderMyResource}
      renderSlot={renderMySlot}
      renderHeader={renderMyHeader}
      onEventUpdate={handleEventUpdate}
      onEventCreate={handleCreateEvent}
      onEventDeleted={handleDeleteEvent}
      cssClass="md-shift-management-calendar"
    />
  );
}
export default App;
```

### CSS

```css
.md-shift-management-calendar .mbsc-schedule-event {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 36px;
}

.md-shift-management-calendar .mbsc-timeline-slots {
  height: 94px;
}

.md-shift-management-calendar .mbsc-timeline-header-placeholder {
  height: 120px;
}

.md-shift-management-calendar .mbsc-timeline-resource {
  display: flex;
  align-items: center;
}

.md-shift-management-calendar .mbsc-timeline-slot {
  width: 200px;
}

.md-shift-management-calendar .mbsc-timeline-parent {
  height: 40px;
}

.md-shift-management-calendar .mbsc-checkbox {
  padding: 18px 0 18px 45px;
}

.md-shift-management-calendar .mbsc-textfield-wrapper {
  margin: 12px 12px 12px 28px;
}

.md-shift-management-calendar .mbsc-select.mbsc-textfield {
  height: 32px;
  width: 120px;
}

.md-shift-management-calendar .mbsc-icon.mbsc-select-icon {
  width: 20px;
  height: 20px;
  top: 7px;
}

.md-shift-resource {
  font-size: 18px;
}

.md-shift-resource-icon {
  margin-right: 10px;
}

.md-shift-header {
  margin: 3px 6px;
  display: flex;
  flex-direction: column;
}

.md-shift-name {
  font-size: 15px;
}

.md-shift-time {
  opacity: 0.5;
  font-size: 12px;
  margin-left: 5px;
}

.md-shift-count {
  margin-top: 6px;
  margin-right: 12px;
  display: inline-block;
  width: 26px;
  height: 20px;
}

.md-shift-icon {
  padding-right: 3px;
}
```

## Implementation notes

- Use the timeline view in summary mode with `eventDisplay: 'fill'`. For the week view, set `startDay: 1` and `endDay: 5` to show only working days; support switching to `type: 'day'` via a `Select` control in the header.
- Define the named shift buckets with the `slots` option. Each slot object needs an `id`, a `name`, and a custom `time` string (e.g. `"8AM – 12PM"`) for display. Each event references its slot via the `slot` field.
- Structure resources as a two-level tree: role groups (Barista, Bartenders, Chefs, etc.) at the top level with individual employee leaf nodes beneath them. Set `eventCreation: false` on every group node so shifts are only created on employee rows. Give each employee a `color` property that Mobiscroll automatically applies to their events.
- Use `renderResource` (Angular: `resourceTemplate`, Vue: `#resource` slot) to differentiate group and leaf rows - show the role icon and a colored label for group nodes, and just the employee name for leaf nodes.
- Use `renderSlot` (Angular: `slotTemplate`, Vue: `#slot` slot) to render a custom slot column header showing the slot name and time range, followed by a live occupancy count per role category (icon + number of assigned employees) for that slot and date. Show an "empty..." fallback when no shifts are assigned.
- Calculate occupancy counts by iterating over the current events array and counting how many employees per role category have a shift matching the given slot ID and date string.
- Use `renderHeader` (Angular: `headerTemplate`, Vue: `#header` slot) to render a custom toolbar combining `CalendarNav`/`CalendarPrev`/`CalendarToday`/`CalendarNext` navigation controls with `Checkbox` components for toggling each slot's visibility and a `Select` for switching between day and week views.
- Toggle slot visibility by maintaining a filtered `slots` array in state: uncheck a checkbox to remove the slot, check it to add it back. When only one slot remains checked, disable its checkbox so at least one slot is always visible.
- Enable `clickToCreate` so users can sign an employee up for a shift by clicking an empty cell. Set `dragToMove: true` to allow repositioning shifts by dragging.
- Use `extendDefaultEvent` to pre-populate the newly created event's `title` with the employee's first name, looked up from the resource data using the event's `resource` ID.
- Use `onEventCreate` to validate new shifts before saving: call `inst.getEvents()` to check whether the same resource already has a shift in the same slot on the same day, and return `false` to cancel the creation if a duplicate is found.
- Use `onEventUpdate` to apply the same duplicate check after a drag; also update the event `title` when the resource changes so it reflects the new employee's name. Return `false` to revert if the target cell is already occupied.
- Use `onEventDeleted` to remove the deleted shift from the events array in state.

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

- Previous: [Timezone Meeting Planner](https://demo.mobiscroll.com/react/timeline/timezone-meeting-planner.md) - Plan team meetings across multiple time zones using the React Timeline — show availability, time-off blocks, with events in local time.
- Next: [Meal Planner](https://demo.mobiscroll.com/react/timeline/meal-planner.md) - Build a weekly meal planner with the React Timeline — meal types as color-coded resource rows, custom add/edit popover for creating or updating meals.
