---
title: "React Timeline Work Order Scheduling Example | Mobiscroll"
component: "Timeline"
framework: "React"
library_version: "6.1.5"
url: "https://demo.mobiscroll.com/react/timeline/work-order-scheduling"
---

# Work Order Scheduling - React Timeline demo

Schedule work orders across grouped resources — drivers, trucks, and contractors — on a React timeline. For React and Next.JS.

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

## Description

When scheduling work orders and synchronizing them across various assets, the timeline can be of enormous value. Resources of the same type can be grouped and arranged in multi-depth hierarchies. The list of various assets, employees, contractors can be added and organized under it.

For add/edit a custom form with the necessary fields (including resources) can be built. Resources linked to a work order can be of various categories, eg. drivers, trucks, contractors and will show up in their respective rows.

You can use the [date header template](https://demo.mobiscroll.com/react/timeline/hour-day-week-month-quarter-year-header-footer-template) and add a simple [event template](https://demo.mobiscroll.com/react/timeline/timeline-custom-event-rendering) to show the $ value and print the total daily revenue below every day in the header.

## What this demo shows

- A desktop weekly timeline for work order scheduling, with Monday to Friday working days arranged horizontally and a hierarchical resource tree arranged vertically 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.
- **Week view** The fixed strip below the header shows the selected work week from Monday to Friday, using the `DD DDD MMM YYYY` date format.
- **Daily totals** A summary row below the date labels displays aggregated daily revenue in dollars.
- **Time grid** The timeline shows working hours from 5 AM to 9 PM with hourly columns under each day.
- **Resources** The left side shows expandable and collapsible resource groups, including parent groups such as Contractors, Employees, and Equipment.
- **Resource hierarchy** Nested resources are grouped by department or category, such as Cement masons and Drivers, before listing individual team members.
- **Current time** Demonstrates a current time indicator, shown as a vertical blue line with the current time label.
- **Hover behavior** Hovering over the time grid shows a time indicator that follows the cursor in 15-minute increments.
- **Event cards** Events are rendered as colored cards with a colored stripe on the left, the exact start and end time below the title, the project name in bold, and the project value shown as a badge next to the title.
- **Event positioning** Events are positioned by assigned resource, date, start time, and end time.
- **Multi-resource assignments** The same project can be assigned to multiple resources at the same time and appears in each assigned resource row.
- **Resource utilization** Different projects can be assigned to different employees during the same day, making resource usage visible across the timeline.
- **Event creation** New events can be created by double-clicking an individual resource row in the timeline or by clicking and dragging across a time range.
- **New event dialog** Creating an event opens a dialog with fields for Title, Location, Bill to customer ($), and Notes.
- **Resource assignment** The event dialog includes start and end time selection and a checklist for choosing the resources where the event should appear.
- **Time selection** The start and end inputs open a time picker with range selection.
- **Event editing** Clicking or tapping an existing event opens the same dialog in edit mode with the current values prefilled.
- **Editable properties** In edit mode, event details can be changed before saving.
- **Save and close behavior** Clicking outside the dialog or pressing Cancel closes the editor without applying changes, while Save updates or save the event.
- **Delete flow** The edit dialog includes a Delete work order action that removes the event from the timeline.
- **Undo feedback** After deletion, a centered bottom toast appears with an Undo action so the deleted event can be restored.
- **Event interaction** Events are highlighted on hover and show drag and resize handles for repositioning or changing duration.
- **Event selection** Clicking an event selects and highlights it.
- **Vertical scrolling** The resource area scrolls vertically so many resources can be displayed in one timeline.
- **Horizontal scrolling** The timeline supports horizontal scrolling for navigating a larger date range.

## Best for

- **Work order scheduling** Planning jobs across employees, contractors, equipment, and other assets in a single timeline.
- **Construction and field service planning** Assigning crews, vehicles, tools, and subcontractors to project work by day and hour.
- **Multi-resource dispatching** Scheduling the same job across several resource types when one work order requires multiple people or assets.
- **Revenue-aware scheduling** Showing daily revenue totals and per-project values directly in the timeline while planning work.
- **Hierarchical resource management** Organizing large resource lists by group, department, role, or asset type without losing the time-based scheduling context.
- **Interactive schedule editing** Supporting create, edit, delete, drag, resize, and undo flows for teams that need to adjust work orders 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
- [`display`](https://mobiscroll.com/docs/react/popup/api#opt-display) - option
- [`width`](https://mobiscroll.com/docs/react/popup/api#opt-width) - option
- [`fullScreen`](https://mobiscroll.com/docs/react/popup/api#opt-fullScreen) - option
- [`touchUi`](https://mobiscroll.com/docs/react/popup/api#opt-touchUi) - option
- [`navigateToEvent`](https://mobiscroll.com/docs/react/eventcalendar/api#method-navigateToEvent) - method
- [`cssClass`](https://mobiscroll.com/docs/react/popup/api#opt-cssClass) - 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
- [`clickToCreate`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-clickToCreate) - option
- [`dragToCreate`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-dragToCreate) - option
- [`dragToMove`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-dragToMove) - option
- [`dragToResize`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-dragToResize) - option
- [`dragTimeStep`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-dragTimeStep) - option
- [`onEventClick`](https://mobiscroll.com/docs/react/eventcalendar/api#event-onEventClick) - event
- [`onEventCreated`](https://mobiscroll.com/docs/react/eventcalendar/api#event-onEventCreated) - event
- [`onEventDeleted`](https://mobiscroll.com/docs/react/eventcalendar/api#event-onEventDeleted) - event
- [`extendDefaultEvent`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-extendDefaultEvent) - option
- [`renderTimelineDay`](https://mobiscroll.com/docs/react/eventcalendar/api#renderer-renderTimelineDay) - renderer
- [`renderTimelineEventContent`](https://mobiscroll.com/docs/react/eventcalendar/api#renderer-renderTimelineEventContent) - renderer
- [`headerText`](https://mobiscroll.com/docs/react/popup/api#opt-headerText) - option
- [`anchor`](https://mobiscroll.com/docs/react/popup/api#opt-anchor) - option
- [`buttons`](https://mobiscroll.com/docs/react/popup/api#opt-buttons) - option
- [`isOpen`](https://mobiscroll.com/docs/react/popup/api#opt-isOpen) - option
- [`onClose`](https://mobiscroll.com/docs/react/popup/api#event-onClose) - event
- [`responsive`](https://mobiscroll.com/docs/react/popup/api#opt-responsive) - option
- [`select`](https://mobiscroll.com/docs/react/datepicker/api#opt-select) - option
- [`controls`](https://mobiscroll.com/docs/react/datepicker/api#opt-controls) - option
- [`touchUi`](https://mobiscroll.com/docs/react/datepicker/api#opt-touchUi) - option
- [`startInput`](https://mobiscroll.com/docs/react/datepicker/api#opt-startInput) - option
- [`endInput`](https://mobiscroll.com/docs/react/datepicker/api#opt-endInput) - option
- [`showRangeLabels`](https://mobiscroll.com/docs/react/datepicker/api#opt-showRangeLabels) - option
- [`onChange`](https://mobiscroll.com/docs/react/datepicker/api#event-onChange) - event
- [`theme`](https://mobiscroll.com/docs/react/forms/checkbox#opt-theme) - option
- [`onClose`](https://mobiscroll.com/docs/react/notifications/snackbar#event-onClose) - event

## Code

### JSX

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

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

const defaultEvents = [
  {
    start: '2026-10-22T06:00',
    end: '2026-10-22T14:00',
    title: 'Farmhouse TPH',
    location: '3339 Spruce Drive',
    resource: ['d2', 'cm2', 'd4', 'cp1', 'cm2', 'ce2', 'b1'],
    color: '#12ca6c',
    cost: 48000,
  },
  {
    start: '2026-10-23T08:00',
    end: '2026-10-23T18:00',
    title: 'Block of flats KXT',
    location: '4698 Mercer Street',
    resource: ['d1', 'cm1', 'd3', 'cp1', 'cm3', 'ce2', 'b2'],
    color: '#c170c3',
    cost: 36000,
  },
  {
    start: '2026-10-24T12:00',
    end: '2026-10-24T20:00',
    title: 'Apartment house UGL',
    location: '3647 Tavern Place',
    resource: ['d3', 'cm2', 'd4', 'cp2', 'cm3', 'ce1', 'b2'],
    color: '#03c9d2',
    cost: 50000,
  },
  {
    start: '2026-10-25T11:00',
    end: '2026-10-25T19:00',
    title: 'Detached house WKB',
    location: '956 Dovetail Estates',
    resource: ['d1', 'cm3', 'd4', 'cp3', 'cm4', 'c2', 'b1', 'ce2'],
    color: '#ff1515',
    cost: 55000,
  },
  {
    start: '2026-10-26T10:00',
    end: '2026-10-26T18:00',
    title: 'Apartment house XAZ',
    location: '4919 Jett Lane, Inglewood',
    resource: ['d1', 'cm4', 'd4', 'cp1', 'cm2', 'c2', 'b2'],
    color: '#12ca6c',
    cost: 62000,
  },
  {
    start: '2026-10-26T08:00',
    end: '2026-10-26T16:00',
    title: 'Block of flats DRG',
    location: '486 Sycamore Fork Road',
    resource: ['d2', 'cm1', 'd3', 'cp2', 'ce2', 'c1', 'b1'],
    color: '#efd414',
    cost: 39000,
  },
  {
    start: '2026-10-27T09:00',
    end: '2026-10-27T17:00',
    title: 'Farmhouse YQK',
    location: '1563 Retreat Avenue',
    resource: ['d2', 'cm4', 'd4', 'cm2', 'cp1', 'c2', 'b2'],
    color: '#cf49d8',
    cost: 45000,
  },
  {
    start: '2026-10-28T07:00',
    end: '2026-10-28T15:00',
    title: 'Apartment house SWP',
    location: '628 Daylene Drive',
    resource: ['d2', 'cm3', 'd3', 'cm1', 'cp2', 'c1', 'b1'],
    color: '#c170c3',
    cost: 53000,
  },
  {
    start: '2026-10-29T10:00',
    end: '2026-10-29T18:00',
    title: 'Detached house OZL',
    location: '1830 Rinehart Road',
    resource: ['d3', 'cm2', 'd4', 'cp2', 'cm3', 'ce1', 'b2'],
    color: '#ff1515',
    cost: 47000,
  },
  {
    start: '2026-10-30T11:00',
    end: '2026-10-30T19:00',
    title: 'Farmhouse PSZ',
    location: '2410 Union Street',
    resource: ['d1', 'cm3', 'd4', 'cp3', 'cm4', 'c2', 'b1', 'ce2'],
    color: '#ff1515',
    cost: 64000,
  },
];
const myResources = [
  {
    id: 'contractors',
    name: 'Contractors',
    collapsed: true,
    eventCreation: false,
    children: [
      {
        id: 'builders',
        name: 'Builders',
        eventCreation: false,
        children: [
          {
            id: 'b1',
            name: 'Jude Chester',
          },
          {
            id: 'b2',
            name: 'Willis Kane',
          },
        ],
      },
      {
        id: 'carpenters',
        name: 'Carpenters',
        eventCreation: false,
        children: [
          {
            id: 'c1',
            name: 'Derek Austyn',
          },
          {
            id: 'c2',
            name: 'Merv Kenny',
          },
        ],
      },
    ],
  },
  {
    id: 'employees',
    name: 'Employees',
    eventCreation: false,
    children: [
      {
        id: 'cement_masons',
        name: 'Cement masons',
        eventCreation: false,
        children: [
          {
            id: 'ce1',
            name: 'Ford Kaiden',
          },
          {
            id: 'ce2',
            name: 'Jewell Ryder',
          },
        ],
      },
      {
        id: 'divers',
        name: 'Drivers',
        eventCreation: false,
        children: [
          {
            id: 'd1',
            name: 'Fred Valdez',
          },
          {
            id: 'd2',
            name: 'Jon Drake',
          },
          {
            id: 'd3',
            name: 'Lou Andie',
          },
          {
            id: 'd4',
            name: 'Leon Porter',
          },
        ],
      },
    ],
  },
  {
    id: 'equipment',
    name: 'Equipment',
    collapsed: true,
    eventCreation: false,
    children: [
      {
        id: 'concrete_mixers',
        name: 'Concrete mixers',
        eventCreation: false,
        children: [
          {
            id: 'cm1',
            name: 'AL 45 RFT',
          },
          {
            id: 'cm2',
            name: 'KQ 62 PVZ',
          },
          {
            id: 'cm3',
            name: 'RG 91 ZAL',
          },
          {
            id: 'cm4',
            name: 'XF 83 GFM',
          },
        ],
      },
      {
        id: 'concrete_pumps',
        name: 'Concrete pumps',
        eventCreation: false,
        children: [
          {
            id: 'cp1',
            name: 'GF 61 BVM',
          },
          {
            id: 'cp2',
            name: 'YC 55 ECT',
          },
        ],
      },
    ],
  },
];

const viewSettings = {
  timeline: {
    type: 'week',
    startDay: 1,
    endDay: 5,
  },
};

const responsivePopup = {
  medium: {
    display: 'anchored',
    width: 520,
    fullScreen: false,
    touchUi: false,
  },
};

function App() {
  const [myEvents, setMyEvents] = useState(defaultEvents);
  const [tempEvent, setTempEvent] = useState(null);
  const [isPopupOpen, setPopupOpen] = useState(false);
  const [isEdit, setEdit] = useState(false);
  const [anchor, setAnchor] = useState(null);
  const [start, startRef] = useState(null);
  const [end, endRef] = useState(null);
  const [popupEventTitle, setTitle] = useState('');
  const [popupEventLocation, setLocation] = useState('');
  const [popupEventBill, setBill] = useState(0);
  const [popupEventNotes, setNotes] = useState('');
  const [popupEventDate, setDate] = useState([]);
  const [checkedResources, setCheckedResources] = useState([]);
  const [isSnackbarOpen, setSnackbarOpen] = useState(false);

  const calInst = useRef();

  const snackbarButton = useMemo(
    () => ({
      action: () => {
        setMyEvents([...myEvents, tempEvent]);
      },
      text: 'Undo',
    }),
    [myEvents, tempEvent],
  );

  const checkboxChange = useCallback(
    (ev) => {
      const value = ev.target.value;

      if (ev.target.checked) {
        setCheckedResources((checkedResources) => [...checkedResources, value]);
      } else {
        setCheckedResources(checkedResources.filter((r) => r !== value));
      }
    },
    [checkedResources],
  );

  const saveEvent = useCallback(() => {
    const newEvent = {
      id: tempEvent.id,
      title: popupEventTitle,
      location: popupEventLocation,
      cost: popupEventBill,
      notes: popupEventNotes,
      start: popupEventDate[0],
      end: popupEventDate[1],
      color: tempEvent.color,
      resource: checkedResources,
    };
    if (isEdit) {
      // Update the event in the list
      const index = myEvents.findIndex((x) => x.id === tempEvent.id);
      const newEventList = [...myEvents];

      newEventList.splice(index, 1, newEvent);
      setMyEvents(newEventList);
      // Here you can update the event in your storage as well
      // ...
    } else {
      // Add the new event to the list
      setMyEvents([...myEvents, newEvent]);
      // Here you can add the event to your storage as well
      // ...
    }
    calInst.current.navigateToEvent(newEvent);
    // Close the popup
    setPopupOpen(false);
  }, [isEdit, myEvents, popupEventDate, popupEventNotes, popupEventTitle, popupEventLocation, popupEventBill, tempEvent, checkedResources]);

  const deleteEvent = useCallback(
    (event) => {
      setMyEvents(myEvents.filter((item) => item.id !== event.id));
      setTempEvent(event);
      setSnackbarOpen(true);
    },
    [myEvents],
  );

  const loadPopupForm = useCallback((event) => {
    setTitle(event.title);
    setLocation(event.location);
    setBill(event.cost);
    setNotes(event.notes);
    setDate([event.start, event.end]);
    setCheckedResources(event.resource);
  }, []);

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

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

  const billChange = useCallback((ev) => {
    setBill(+ev.target.value || 0);
  }, []);

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

  const dateChange = useCallback((args) => {
    setDate(args.value);
  }, []);

  const handleDeleteClick = useCallback(() => {
    deleteEvent(tempEvent);
    setPopupOpen(false);
  }, [deleteEvent, tempEvent]);

  const handleEventClick = useCallback(
    (args) => {
      setEdit(true);
      setTempEvent({ ...args.event });
      // Fill popup form with event data
      loadPopupForm(args.event);
      setAnchor(args.domEvent.target);
      setPopupOpen(true);
    },
    [loadPopupForm],
  );

  const handleEventCreated = useCallback(
    (args) => {
      setEdit(false);
      setTempEvent(args.event);
      // Fill popup form with event data
      loadPopupForm(args.event);
      setAnchor(args.target);
      // Open the popup
      setPopupOpen(true);
    },
    [loadPopupForm],
  );

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

  const headerText = useMemo(() => (isEdit ? 'Edit work order' : 'New work order'), [isEdit]);

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

  const handlePopupClose = useCallback(() => {
    if (!isEdit) {
      // Refresh the list, if add popup was canceled, to remove the temporary event
      setMyEvents([...myEvents]);
    }
    setPopupOpen(false);
  }, [isEdit, myEvents]);

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

  const extendMyDefaultEvent = useCallback(
    () => ({
      title: 'Work order',
      location: '',
      cost: 0,
    }),
    [],
  );

  const getCostString = useCallback((cost) => cost.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ','), []);

  const renderCustomDay = useCallback(
    (args) => {
      const events = args.events;
      let costs = 0;

      if (events) {
        for (const event of events) {
          costs += event.cost;
        }
      }

      return (
        <div>
          <div className="md-work-order-date">{formatDate('DD DDD MMM YYYY', args.date)}</div>
          <div className="md-work-order-date-title">{'Total revenue: $' + getCostString(costs)}</div>
        </div>
      );
    },
    [getCostString],
  );

  const myScheduleEvent = useCallback(
    (event) => (
      <div>
        {event.title}
        <span className="md-work-order-price-tag">${getCostString(event.original.cost)}</span>
      </div>
    ),
    [getCostString],
  );

  return (
    <div>
      <Eventcalendar
        className="md-work-order-scheduling"
        view={viewSettings}
        data={myEvents}
        ref={calInst}
        resources={myResources}
        clickToCreate="double"
        dragToCreate={true}
        dragToMove={true}
        dragToResize={true}
        dragTimeStep={30}
        onEventClick={handleEventClick}
        onEventCreated={handleEventCreated}
        onEventDeleted={handleEventDeleted}
        extendDefaultEvent={extendMyDefaultEvent}
        renderTimelineDay={renderCustomDay}
        renderTimelineEventContent={myScheduleEvent}
      />
      <Popup
        display="bottom"
        fullScreen={true}
        contentPadding={false}
        headerText={headerText}
        anchor={anchor}
        buttons={popupButtons}
        isOpen={isPopupOpen}
        onClose={handlePopupClose}
        responsive={responsivePopup}
      >
        <div className="mbsc-form-group">
          <Input label="Title" value={popupEventTitle} onChange={titleChange} />
          <Input label="Location" value={popupEventLocation} onChange={locationChange} />
          <Input label="Bill to customer ($)" value={popupEventBill} onChange={billChange} />
          <Textarea label="Notes" value={popupEventNotes} onChange={notesChange} />
        </div>
        <div className="mbsc-form-group">
          <Input ref={startRef} label="Starts" />
          <Input ref={endRef} label="Ends" />
          <Datepicker
            select="range"
            controls={['time']}
            touchUi={true}
            startInput={start}
            endInput={end}
            showRangeLabels={false}
            onChange={dateChange}
            value={popupEventDate}
          />
        </div>
        <div className="mbsc-form-group">
          <div className="mbsc-grid mbsc-no-padding">
            <div className="mbsc-row">
              {myResources.map((resources) =>
                resources.children.map((res) => (
                  <div className="mbsc-col-sm-4" key={res.id}>
                    <>
                      <div className="mbsc-form-group-title">{res.name}</div>
                      {res.children.map((r) => (
                        <Checkbox
                          key={r.id}
                          value={r.id}
                          checked={checkedResources.indexOf(r.id) > -1}
                          onChange={checkboxChange}
                          theme="material"
                          className="md-work-order-checkbox-label"
                        >
                          {r.name}
                        </Checkbox>
                      ))}
                    </>
                  </div>
                )),
              )}
            </div>
          </div>
        </div>
        <div className="mbsc-form-group">
          {isEdit && (
            <div className="mbsc-button-group">
              <Button className="mbsc-button-block" color="danger" variant="outline" onClick={handleDeleteClick}>
                Delete event
              </Button>
            </div>
          )}
        </div>
      </Popup>
      <Snackbar message="Event deleted" isOpen={isSnackbarOpen} onClose={handleSnackbarClose} button={snackbarButton} />
    </div>
  );
}

export default App;
```

### TSX

```tsx
import {
  Button,
  Checkbox,
  Datepicker,
  Eventcalendar,
  formatDate,
  Input,
  MbscCalendarDayData,
  MbscCalendarEvent,
  MbscCalendarEventData,
  MbscDatepickerChangeEvent,
  MbscDateType,
  MbscEventcalendarView,
  MbscEventClickEvent,
  MbscEventCreatedEvent,
  MbscEventDeletedEvent,
  MbscPopupButton,
  MbscPopupOptions,
  MbscResource,
  MbscResponsiveOptions,
  Popup,
  setOptions,
  Snackbar,
  Textarea,
} from '@mobiscroll/react';
import { ChangeEvent, FC, useCallback, useMemo, useRef, useState } from 'react';

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

const defaultEvents: MbscCalendarEvent[] = [
  {
    start: '2026-10-22T06:00',
    end: '2026-10-22T14:00',
    title: 'Farmhouse TPH',
    location: '3339 Spruce Drive',
    resource: ['d2', 'cm2', 'd4', 'cp1', 'cm2', 'ce2', 'b1'],
    color: '#12ca6c',
    cost: 48000,
  },
  {
    start: '2026-10-23T08:00',
    end: '2026-10-23T18:00',
    title: 'Block of flats KXT',
    location: '4698 Mercer Street',
    resource: ['d1', 'cm1', 'd3', 'cp1', 'cm3', 'ce2', 'b2'],
    color: '#c170c3',
    cost: 36000,
  },
  {
    start: '2026-10-24T12:00',
    end: '2026-10-24T20:00',
    title: 'Apartment house UGL',
    location: '3647 Tavern Place',
    resource: ['d3', 'cm2', 'd4', 'cp2', 'cm3', 'ce1', 'b2'],
    color: '#03c9d2',
    cost: 50000,
  },
  {
    start: '2026-10-25T11:00',
    end: '2026-10-25T19:00',
    title: 'Detached house WKB',
    location: '956 Dovetail Estates',
    resource: ['d1', 'cm3', 'd4', 'cp3', 'cm4', 'c2', 'b1', 'ce2'],
    color: '#ff1515',
    cost: 55000,
  },
  {
    start: '2026-10-26T10:00',
    end: '2026-10-26T18:00',
    title: 'Apartment house XAZ',
    location: '4919 Jett Lane, Inglewood',
    resource: ['d1', 'cm4', 'd4', 'cp1', 'cm2', 'c2', 'b2'],
    color: '#12ca6c',
    cost: 62000,
  },
  {
    start: '2026-10-26T08:00',
    end: '2026-10-26T16:00',
    title: 'Block of flats DRG',
    location: '486 Sycamore Fork Road',
    resource: ['d2', 'cm1', 'd3', 'cp2', 'ce2', 'c1', 'b1'],
    color: '#efd414',
    cost: 39000,
  },
  {
    start: '2026-10-27T09:00',
    end: '2026-10-27T17:00',
    title: 'Farmhouse YQK',
    location: '1563 Retreat Avenue',
    resource: ['d2', 'cm4', 'd4', 'cm2', 'cp1', 'c2', 'b2'],
    color: '#cf49d8',
    cost: 45000,
  },
  {
    start: '2026-10-28T07:00',
    end: '2026-10-28T15:00',
    title: 'Apartment house SWP',
    location: '628 Daylene Drive',
    resource: ['d2', 'cm3', 'd3', 'cm1', 'cp2', 'c1', 'b1'],
    color: '#c170c3',
    cost: 53000,
  },
  {
    start: '2026-10-29T10:00',
    end: '2026-10-29T18:00',
    title: 'Detached house OZL',
    location: '1830 Rinehart Road',
    resource: ['d3', 'cm2', 'd4', 'cp2', 'cm3', 'ce1', 'b2'],
    color: '#ff1515',
    cost: 47000,
  },
  {
    start: '2026-10-30T11:00',
    end: '2026-10-30T19:00',
    title: 'Farmhouse PSZ',
    location: '2410 Union Street',
    resource: ['d1', 'cm3', 'd4', 'cp3', 'cm4', 'c2', 'b1', 'ce2'],
    color: '#ff1515',
    cost: 64000,
  },
];

const myResources: MbscResource[] = [
  {
    id: 'contractors',
    name: 'Contractors',
    collapsed: true,
    eventCreation: false,
    children: [
      {
        id: 'builders',
        name: 'Builders',
        eventCreation: false,
        children: [
          {
            id: 'b1',
            name: 'Jude Chester',
          },
          {
            id: 'b2',
            name: 'Willis Kane',
          },
        ],
      },
      {
        id: 'carpenters',
        name: 'Carpenters',
        eventCreation: false,
        children: [
          {
            id: 'c1',
            name: 'Derek Austyn',
          },
          {
            id: 'c2',
            name: 'Merv Kenny',
          },
        ],
      },
    ],
  },
  {
    id: 'employees',
    name: 'Employees',
    eventCreation: false,
    children: [
      {
        id: 'cement_masons',
        name: 'Cement masons',
        eventCreation: false,
        children: [
          {
            id: 'ce1',
            name: 'Ford Kaiden',
          },
          {
            id: 'ce2',
            name: 'Jewell Ryder',
          },
        ],
      },
      {
        id: 'divers',
        name: 'Drivers',
        eventCreation: false,
        children: [
          {
            id: 'd1',
            name: 'Fred Valdez',
          },
          {
            id: 'd2',
            name: 'Jon Drake',
          },
          {
            id: 'd3',
            name: 'Lou Andie',
          },
          {
            id: 'd4',
            name: 'Leon Porter',
          },
        ],
      },
    ],
  },
  {
    id: 'equipment',
    name: 'Equipment',
    collapsed: true,
    eventCreation: false,
    children: [
      {
        id: 'concrete_mixers',
        name: 'Concrete mixers',
        eventCreation: false,
        children: [
          {
            id: 'cm1',
            name: 'AL 45 RFT',
          },
          {
            id: 'cm2',
            name: 'KQ 62 PVZ',
          },
          {
            id: 'cm3',
            name: 'RG 91 ZAL',
          },
          {
            id: 'cm4',
            name: 'XF 83 GFM',
          },
        ],
      },
      {
        id: 'concrete_pumps',
        name: 'Concrete pumps',
        eventCreation: false,
        children: [
          {
            id: 'cp1',
            name: 'GF 61 BVM',
          },
          {
            id: 'cp2',
            name: 'YC 55 ECT',
          },
        ],
      },
    ],
  },
];

const viewSettings: MbscEventcalendarView = {
  timeline: {
    type: 'week',
    startDay: 1,
    endDay: 5,
  },
};

const responsivePopup: MbscResponsiveOptions<MbscPopupOptions> = {
  medium: {
    display: 'anchored',
    width: 520,
    fullScreen: false,
    touchUi: false,
  },
};

const App: FC = () => {
  const [myEvents, setMyEvents] = useState<MbscCalendarEvent[]>(defaultEvents);
  const [tempEvent, setTempEvent] = useState<MbscCalendarEvent>();
  const [isPopupOpen, setPopupOpen] = useState<boolean>(false);
  const [isEdit, setEdit] = useState<boolean>(false);
  const [anchor, setAnchor] = useState<HTMLElement>();
  const [start, startRef] = useState<Input | null>(null);
  const [end, endRef] = useState<Input | null>(null);
  const [popupEventTitle, setTitle] = useState<string | undefined>('');
  const [popupEventLocation, setLocation] = useState<string | undefined>('');
  const [popupEventBill, setBill] = useState<number>(0);
  const [popupEventNotes, setNotes] = useState<string>('');
  const [popupEventDate, setDate] = useState<MbscDateType[]>([]);
  const [checkedResources, setCheckedResources] = useState<string[]>([]);
  const [isSnackbarOpen, setSnackbarOpen] = useState<boolean>(false);

  const calInst = useRef<Eventcalendar | null>(null);

  const snackbarButton = useMemo(
    () => ({
      action: () => {
        setMyEvents([...myEvents, tempEvent!]);
      },
      text: 'Undo',
    }),
    [myEvents, tempEvent],
  );

  const checkboxChange = useCallback(
    (ev: ChangeEvent<HTMLInputElement>) => {
      const value = ev.target.value;

      if (ev.target.checked) {
        setCheckedResources((checkedResources) => [...checkedResources, value]);
      } else {
        setCheckedResources(checkedResources.filter((r) => r !== value));
      }
    },
    [checkedResources],
  );

  const saveEvent = useCallback(() => {
    const newEvent = {
      id: tempEvent!.id,
      title: popupEventTitle,
      location: popupEventLocation,
      cost: popupEventBill,
      notes: popupEventNotes,
      start: popupEventDate[0],
      end: popupEventDate[1],
      color: tempEvent!.color,
      resource: checkedResources,
    };
    if (isEdit) {
      // Update the event in the list
      const index = myEvents.findIndex((x) => x.id === tempEvent!.id);
      const newEventList = [...myEvents];

      newEventList.splice(index, 1, newEvent);
      setMyEvents(newEventList);
      // Here you can update the event in your storage as well
      // ...
    } else {
      // Add the new event to the list
      setMyEvents([...myEvents, newEvent]);
      // Here you can add the event to your storage as well
      // ...
    }
    calInst.current?.navigateToEvent(newEvent);
    // Close the popup
    setPopupOpen(false);
  }, [isEdit, myEvents, popupEventDate, popupEventNotes, popupEventTitle, popupEventLocation, popupEventBill, tempEvent, checkedResources]);

  const deleteEvent = useCallback(
    (event: MbscCalendarEvent) => {
      setMyEvents(myEvents.filter((item) => item.id !== event.id));
      setTempEvent(event);
      setSnackbarOpen(true);
    },
    [myEvents],
  );

  const loadPopupForm = useCallback((event: MbscCalendarEvent) => {
    setTitle(event.title);
    setLocation(event.location);
    setBill(event.cost);
    setNotes(event.notes);
    setDate([event.start!, event.end!]);
    setCheckedResources([event.resource as string]);
  }, []);

  // Handle popup form changes

  const titleChange = useCallback((ev: ChangeEvent<HTMLInputElement>) => {
    setTitle(ev.target.value);
  }, []);

  const locationChange = useCallback((ev: ChangeEvent<HTMLInputElement>) => {
    setLocation(ev.target.value);
  }, []);

  const billChange = useCallback((ev: ChangeEvent<HTMLInputElement>) => {
    setBill(+ev.target.value || 0);
  }, []);

  const notesChange = useCallback((ev: ChangeEvent<HTMLInputElement>) => {
    setNotes(ev.target.value);
  }, []);

  const dateChange = useCallback((args: MbscDatepickerChangeEvent) => {
    setDate(args.value as MbscDateType[]);
  }, []);

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

  const handleEventClick = useCallback(
    (args: MbscEventClickEvent) => {
      setEdit(true);
      setTempEvent({ ...args.event });
      // Fill popup form with event data
      loadPopupForm(args.event);
      setAnchor(args.domEvent.target);
      setPopupOpen(true);
    },
    [loadPopupForm],
  );

  const handleEventCreated = useCallback(
    (args: MbscEventCreatedEvent) => {
      setEdit(false);
      setTempEvent(args.event);
      // Fill popup form with event data
      loadPopupForm(args.event);
      setAnchor(args.target);
      // Open the popup
      setPopupOpen(true);
    },
    [loadPopupForm],
  );

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

  const headerText = useMemo(() => (isEdit ? 'Edit work order' : 'New work order'), [isEdit]);

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

  const handlePopupClose = useCallback(() => {
    if (!isEdit) {
      // Refresh the list, if add popup was canceled, to remove the temporary event
      setMyEvents([...myEvents]);
    }
    setPopupOpen(false);
  }, [isEdit, myEvents]);

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

  const extendDefaultEvent = useCallback(
    () => ({
      title: 'Work order',
      location: '',
      cost: 0,
    }),
    [],
  );

  const getCostString = (cost: number) => cost.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ',');

  const renderCustomDay = (args: MbscCalendarDayData) => {
    const events = args.events;
    let costs = 0;

    if (events) {
      for (const event of events) {
        costs += event.cost;
      }
    }

    return (
      <div>
        <div className="md-work-order-date">{formatDate('DD DDD MMM YYYY', args.date)}</div>
        <div className="md-work-order-date-title">{'Total revenue: $' + getCostString(costs)}</div>
      </div>
    );
  };

  const myScheduleEvent = useCallback(
    (event: MbscCalendarEventData) => (
      <div>
        {event.title}
        <span className="md-work-order-price-tag">${getCostString(event.original!.cost)}</span>
      </div>
    ),
    [],
  );

  return (
    <div>
      <Eventcalendar
        className="md-work-order-scheduling"
        view={viewSettings}
        data={myEvents}
        ref={calInst}
        resources={myResources}
        clickToCreate="double"
        dragToCreate={true}
        dragToMove={true}
        dragToResize={true}
        dragTimeStep={30}
        onEventClick={handleEventClick}
        onEventCreated={handleEventCreated}
        onEventDeleted={handleEventDeleted}
        extendDefaultEvent={extendDefaultEvent}
        renderTimelineDay={renderCustomDay}
        renderTimelineEventContent={myScheduleEvent}
      />
      <Popup
        display="bottom"
        fullScreen={true}
        contentPadding={false}
        headerText={headerText}
        anchor={anchor}
        buttons={popupButtons}
        isOpen={isPopupOpen}
        onClose={handlePopupClose}
        responsive={responsivePopup}
      >
        <div className="mbsc-form-group">
          <Input label="Title" value={popupEventTitle} onChange={titleChange} />
          <Input label="Location" value={popupEventLocation} onChange={locationChange} />
          <Input label="Bill to customer ($)" value={popupEventBill.toString()} onChange={billChange} />
          <Textarea label="Notes" value={popupEventNotes} onChange={notesChange} />
        </div>
        <div className="mbsc-form-group">
          <Input ref={startRef} label="Starts" />
          <Input ref={endRef} label="Ends" />
          <Datepicker
            select="range"
            controls={['time']}
            touchUi={true}
            startInput={start}
            endInput={end}
            showRangeLabels={false}
            onChange={dateChange}
            value={popupEventDate}
          />
        </div>
        <div className="mbsc-form-group">
          <div className="mbsc-grid mbsc-no-padding">
            <div className="mbsc-row">
              {myResources.map(
                (resources: MbscResource) =>
                  resources.children &&
                  resources.children.map((res: MbscResource) => (
                    <div className="mbsc-col-sm-4" key={res.id}>
                      <>
                        <div className="mbsc-form-group-title">{res.name}</div>
                        {res.children &&
                          res.children.map((r: MbscResource) => (
                            <Checkbox
                              key={r.id}
                              value={r.id}
                              checked={checkedResources.indexOf(r.id as string) > -1}
                              onChange={checkboxChange}
                              theme="material"
                              className="md-work-order-checkbox-label"
                            >
                              {r.name}
                            </Checkbox>
                          ))}
                      </>
                    </div>
                  )),
              )}
            </div>
          </div>
        </div>
        <div className="mbsc-form-group">
          {isEdit && (
            <div className="mbsc-button-group">
              <Button className="mbsc-button-block" color="danger" variant="outline" onClick={handleDeleteClick}>
                Delete work order
              </Button>
            </div>
          )}
        </div>
      </Popup>
      <Snackbar message="Event deleted" isOpen={isSnackbarOpen} onClose={handleSnackbarClose} button={snackbarButton} />
    </div>
  );
};
export default App;
```

### CSS

```css
.md-work-order-checkbox-label.mbsc-checkbox {
  padding-top: 5px;
  padding-bottom: 5px;
}

.md-work-order-date {
  font-size: 14px;
  border-bottom: 1px solid #ccc;
  padding: 5px 10px;
}

.md-work-order-date-title {
  font-size: 13px;
  padding: 5px 10px;
  line-height: 18px;
  opacity: 0.6;
}

.md-work-order-price-tag {
  display: inline-block;
  font-size: 11px;
  line-height: 16px;
  vertical-align: middle;
  border: 1px solid currentColor;
  border-radius: 5px;
  margin: 0 10px;
  padding: 0px 5px;
  opacity: 0.6;
}

.md-work-order-scheduling .mbsc-timeline-parent {
  height: 32px;
}
```

## Implementation notes

- Use the timeline view with `type: 'week'` and `startDay: 1` / `endDay: 5` to show a Mon-Fri planning week.
- Define a three-level resource tree: top-level groups → subcategory groups → individual leaf resources. Set `eventCreation: false` on all group and subgroup nodes so events are only associated with individual resources, not with the category rows themselves.
- Set `collapsed: true` on top-level groups that should start folded, such as Contractors and Equipment.
- Assign a work order to multiple resources by setting its `resource` property to an array of leaf resource IDs. The event will appear in every row whose ID is included in the array.
- Enable `clickToCreate="double"` for double-click creation and `dragToCreate={true}` for drag-based creation. Set `dragToMove: true` and `dragToResize: true` to allow repositioning and resizing. Use `dragTimeStep: 30` to snap all drag operations to 30-minute increments.
- Use `extendDefaultEvent` to pre-populate sensible defaults (title, location, cost) on every newly created work order event.
- Use `renderTimelineEventContent` (Angular: `timelineEventContentTemplate`, Vue: `#timelineEventContent` slot) to display a custom event body that shows the work order title alongside a cost badge.
- Use `renderTimelineDay` (Angular: `timelineDayTemplate`, Vue: `#timelineDay` slot) to render a custom column header for each day. Calculate the total daily revenue by summing the `cost` field from the `events` array provided in the render args, and display it below the date label.
- Use `onEventCreated` to intercept newly created events and open the add popup. Use `onEventClick` to open the edit popup for existing events. Pass `args.target` from `onEventCreated` as the popup `anchor` so the anchored popover positions itself relative to the new event.
- Build the popup with the `Popup` component. Use the `responsive` option to show a full-screen bottom sheet on mobile and a fixed-width anchored popover on larger screens.
- Include a resource assignment section in the popup using checkboxes for each leaf resource, grouped visually by subcategory. Initialize the checked state from the event's `resource` array and write it back on save so the work order is reassigned across the correct rows.
- After saving, call `navigateToEvent` on the calendar instance ref to scroll the timeline to the updated or newly created work order.
- For deletion, remove the event immediately from the data array and show a `Snackbar` with an Undo action that restores it 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

- Previous: [Employee Shift Scheduling](https://demo.mobiscroll.com/react/timeline/employee-shifts.md) - 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.
- Next: [Timezone Meeting Planner](https://demo.mobiscroll.com/react/timeline/timezone-meeting-planner.md) - Plan team meetings across time zones with the React Timeline — show per-resource availability in working, flex, and time-off blocks, with local-time events.
