---
title: "React Timeline Dynamic Resource Sort Example | Mobiscroll"
component: "Timeline"
framework: "React"
library_version: "6.1.5"
url: "https://demo.mobiscroll.com/react/timeline/dynamic-calculation-resource-sort"
---

# Dynamic Resource Sort - React Timeline demo

Sort React Timeline resources by metrics — header popup ranks trucks by standby time, payload efficiency, or deadhead, with instant reorder on event changes. For React and Next.JS.

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

## Description

For advanced resource sorting and ordering calculations, sorting just by resource properties might not be enough. In those situations you can provide a modal or popup that is invoked either from outside of the calendar or from within the calendar header itself and you can get as sophisticated as you have to.

In this example trucks are listed as resources along with the scheduled tours as events. The sort popup that can be invoked from the header [through templating](https://mobiscroll.com/docs/react/eventcalendar/api#renderer-renderHeader) allows you to perform calculations based on truck parameters and tour events. Once the calculation options are selected, the resulting metrics are displayed in the resources column, dynamically updating to reflect the results using the [renderResource](https://mobiscroll.com/docs/react/eventcalendar/api#renderer-renderResource) templating option.

Since the tours affect the resource order, on new event creation or reassignment a [snackbar](https://demo.mobiscroll.com/react/forms/notifications) is shown with a 3 second timer and a "Sort now" button for an instant order update.

## What this demo shows

- A desktop timeline that displays one week at a time, with days arranged horizontally and trucks listed as resources on the left.
- **Header navigation** The month and year label opens date navigation, while the blue previous and next arrows move between weeks.
- **Week view** The timeline shows the selected week from Sunday to Saturday, with the current date highlighted.
- **Resources** Trucks are displayed as separate resource rows, each with model, capacity, and the currently active calculated metric.
- **Sorting panel** In the header the Sort Trucks button opens a custom sort popup that lets users choose which metric controls the resource order.
- **Sort metrics** Resources can be sorted by Standby Time, Payload Efficiency, or Deadhead Time.
- **Sort direction** Users can choose ascending or descending order before applying the new sort.
- **Staged sorting** Sort options are staged until Apply is clicked, while Cancel leaves the current resource order unchanged.
- **Automatic reordering** Resources are reordered from calculated business metrics rather than a fixed resource order.
- **Metric display** The calculated metric is shown directly in each resource row, with a progress bar that reflects the active value.
- **Event cards** Tour events show the tour title and payload value, or an empty-run label when the payload is zero.
- **Date positioning** Events are positioned by their assigned truck and exact date range.
- **Event interaction** Events highlight on hover and show drag and resize handles for moving tours or changing their duration.
- **Event selection** Clicking an event selects and highlights it.
- **Event creation** New events can be created by double-clicking the timeline or by clicking and dragging across a time range.
- **Event changes** When a tour is created, deleted, updated, or reassigned, a snackbar appears with a 3-second timer and a "Sort now" action.
- **Delayed sorting** The snackbar lets users apply the new order immediately or wait for the timed update.
- **Scrolling** The timeline supports horizontal and vertical scrolling for navigating through days and truck rows.

## Best for

- **Fleet dispatching** Scheduling tours across trucks while keeping the resource order aligned with current operating metrics.
- **Logistics planning** Comparing vehicles by standby time, deadhead time, or payload efficiency in a weekly timeline.
- **Transportation operations** Identifying underused, overused, or inefficient trucks without manually scanning every assignment.
- **Asset management** Organizing resources automatically based on calculated performance data.
- **Operational dashboards** Combining scheduled work with resource-level metrics in the same timeline view.
- **Custom resource ordering** Building scheduler interfaces where resource order depends on event data, calculated values, or user-selected KPIs.

## 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
- [`getEvents`](https://mobiscroll.com/docs/react/eventcalendar/api#method-getEvents) - method
- [`cssClass`](https://mobiscroll.com/docs/react/popup/api#opt-cssClass) - option
- [`navigateToEvent`](https://mobiscroll.com/docs/react/eventcalendar/api#method-navigateToEvent) - method
- [`width`](https://mobiscroll.com/docs/react/eventcalendar/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
- [`data`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-data) - 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
- [`eventOverlap`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-eventOverlap) - option
- [`resources`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-resources) - option
- [`view`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-view) - option
- [`renderHeader`](https://mobiscroll.com/docs/react/eventcalendar/api#renderer-renderHeader) - renderer
- [`renderResourceHeader`](https://mobiscroll.com/docs/react/eventcalendar/api#renderer-renderResourceHeader) - renderer
- [`renderResource`](https://mobiscroll.com/docs/react/eventcalendar/api#renderer-renderResource) - renderer
- [`renderTimelineEventContent`](https://mobiscroll.com/docs/react/eventcalendar/api#renderer-renderTimelineEventContent) - renderer
- [`onPageLoading`](https://mobiscroll.com/docs/react/eventcalendar/api#event-onPageLoading) - event
- [`onEventCreated`](https://mobiscroll.com/docs/react/eventcalendar/api#event-onEventCreated) - event
- [`onEventDeleted`](https://mobiscroll.com/docs/react/eventcalendar/api#event-onEventDeleted) - event
- [`onEventUpdated`](https://mobiscroll.com/docs/react/eventcalendar/api#event-onEventUpdated) - event
- [`anchor`](https://mobiscroll.com/docs/react/popup/api#opt-anchor) - option
- [`buttons`](https://mobiscroll.com/docs/react/popup/api#opt-buttons) - option
- [`display`](https://mobiscroll.com/docs/react/popup/api#opt-display) - option
- [`focusOnClose`](https://mobiscroll.com/docs/react/popup/api#opt-focusOnClose) - option
- [`isOpen`](https://mobiscroll.com/docs/react/popup/api#opt-isOpen) - option
- [`showOverlay`](https://mobiscroll.com/docs/react/popup/api#opt-showOverlay) - option
- [`width`](https://mobiscroll.com/docs/react/popup/api#opt-width) - option
- [`onClose`](https://mobiscroll.com/docs/react/popup/api#event-onClose) - event
- [`cssClass`](https://mobiscroll.com/docs/react/notifications/snackbar#opt-cssClass) - option
- [`display`](https://mobiscroll.com/docs/react/notifications/snackbar#opt-display) - option
- [`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,
  CalendarNav,
  CalendarNext,
  CalendarPrev,
  Eventcalendar,
  Popup,
  Radio,
  RadioGroup,
  Segmented,
  SegmentedGroup,
  setOptions,
  Snackbar,
  Toast,
} from '@mobiscroll/react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';

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

function App() {
  const [myEvents, setMyEvents] = useState([
    {
      start: '2026-10-19T00:00',
      end: '2026-10-23T00:00',
      title: 'Tour #013 - Miami to Seattle',
      resource: 1,
      color: '#FF9933',
      payload: 25,
    },
    {
      start: '2026-10-21T00:00',
      end: '2026-10-23T00:00',
      title: 'Tour #014 - Denver to Boston',
      resource: 2,
      color: '#33FFA6',
      payload: 18,
    },
    {
      start: '2026-10-20T00:00',
      end: '2026-10-23T00:00',
      title: 'Tour #015 - Orlando to Austin',
      resource: 3,
      color: '#9933FF',
      payload: 20,
    },
    {
      start: '2026-10-21T00:00',
      end: '2026-10-24T00:00',
      title: 'Tour #016 - Detroit to Baltimore',
      resource: 4,
      color: '#33A6FF',
      payload: 0,
    },
    {
      start: '2026-10-22T00:00',
      end: '2026-10-25T00:00',
      title: 'Tour #017 - Las Vegas to Portland',
      resource: 5,
      color: '#FF5733',
      payload: 20,
    },
    {
      start: '2026-10-22T00:00',
      end: '2026-10-25T00:00',
      title: 'Tour #018 - Atlanta to Kansas City',
      resource: 6,
      color: '#33FF99',
      payload: 0,
    },
    {
      start: '2026-10-16T11:00',
      end: '2026-10-20T00:00',
      title: 'Tour #018 - Dallas to Atlanta',
      resource: 6,
      color: '#33FF99',
      payload: 14,
    },
    {
      start: '2026-10-20T00:00',
      end: '2026-10-24T00:00',
      title: 'Tour #019 - Charlotte to Indianapolis',
      resource: 7,
      color: '#FF5733',
      payload: 22,
    },
    {
      start: '2026-10-20T00:00',
      end: '2026-10-22T00:00',
      title: 'Tour #005 - Dallas to San Francisco',
      resource: 7,
      color: '#FF5733',
      payload: 18,
    },
    {
      start: '2026-10-24T00:00',
      end: '2026-10-26T00:00',
      title: 'Tour #001 - New York to Los Angeles',
      resource: 7,
      color: '#FF5733',
      payload: 20,
    },
    {
      start: '2026-10-20T00:00',
      end: '2026-10-22T00:00',
      title: 'Tour #009 - San Diego to Dallas',
      resource: 7,
      color: '#FF5733',
      payload: 16,
    },
    {
      start: '2026-10-24T00:00',
      end: '2026-10-26T00:00',
      title: 'Tour #006 - Los Angeles to Chicago',
      resource: 8,
      color: '#FF33A6',
      payload: 10,
    },
    {
      start: '2026-10-18T00:00',
      end: '2026-10-22T00:00',
      title: 'Tour #010 - San Francisco to Los Angeles',
      resource: 8,
      color: '#FF33A6',
      payload: 0,
    },
    {
      start: '2026-10-23T00:00',
      end: '2026-10-26T00:00',
      title: 'Tour #007 - Houston to New York',
      resource: 9,
      color: '#33FF57',
      payload: 0,
    },
    {
      start: '2026-10-20T00:00',
      end: '2026-10-22T00:00',
      title: 'Tour #003 - Philadelphia to Phoenix',
      resource: 9,
      color: '#33FF57',
      payload: 0,
    },
    {
      start: '2026-10-16T00:00',
      end: '2026-10-19T00:00',
      title: 'Tour #028 - Trenton to Philadelphia',
      resource: 9,
      color: '#33FF57',
      payload: 11,
    },
    {
      start: '2026-10-16T00:00',
      end: '2026-10-21T00:00',
      title: 'Tour #004 - San Antonio to San Diego',
      resource: 10,
      color: '#3357FF',
      payload: 15,
    },

    {
      start: '2026-10-23T00:00',
      end: '2026-10-26T00:00',
      title: 'Tour #022 - Cleveland to Cincinnati',
      resource: 10,
      color: '#3357FF',
      payload: 17,
    },
    {
      start: '2026-10-16T00:00',
      end: '2026-10-21T00:00',
      title: 'Tour #023 - Boston to Philadelphia',
      resource: 11,
      color: '#FF9933',
      payload: 19,
    },
    {
      start: '2026-10-20T00:00',
      end: '2026-10-22T00:00',
      title: 'Tour #024 - Las Vegas to San Diego',
      resource: 12,
      color: '#33FF57',
      payload: 28,
    },
    {
      start: '2026-10-17T00:00',
      end: '2026-10-20T00:00',
      title: 'Tour #025 - Miami to Charlotte',
      resource: 13,
      color: '#9933FF',
      payload: 26,
    },
    {
      start: '2026-10-22T00:00',
      end: '2026-10-25T00:00',
      title: 'Tour #026 - Seattle to Portland',
      resource: 14,
      color: '#33A6FF',
      payload: 12,
    },
    {
      start: '2026-10-19T00:00',
      end: '2026-10-22T00:00',
      title: 'Tour #027 - Atlanta to Orlando',
      resource: 15,
      color: '#FF5733',
      payload: 17,
    },
  ]);

  const myResources = useMemo(
    () => [
      { id: 1, name: 'NY-TRK-1200', capacity: 25, location: 'New York', model: 'Renault Magnum' },
      { id: 2, name: 'LA-TRK-0090', capacity: 18, location: 'Los Angeles', model: 'Mercedes Actros' },
      { id: 3, name: 'CH-TRK-0700', capacity: 22, location: 'Phoenix', model: 'Scania R500' },
      { id: 4, name: 'HO-TRK-0850', capacity: 28, location: 'Houston', model: 'Volvo FH16' },
      { id: 5, name: 'PH-TRK-0900', capacity: 24, location: 'Chicago', model: 'MAN TGX' },
      { id: 6, name: 'PA-TRK-0300', capacity: 15, location: 'Philadelphia', model: 'Renault T High' },
      { id: 8, name: 'SD-TRK-0250', capacity: 21, location: 'San Francisco', model: 'Mercedes Arocs' },
      { id: 9, name: 'DA-TRK-0400', capacity: 20, location: 'Dallas', model: 'DAF XF' },
      { id: 10, name: 'SF-TRK-0550', capacity: 17, location: 'San Diego', model: 'Iveco Stralis' },
      { id: 11, name: 'BO-TRK-1100', capacity: 23, location: 'Boston', model: 'Kenworth T680' },
      { id: 12, name: 'LV-TRK-2200', capacity: 30, location: 'Las Vegas', model: 'Volvo FH16' },
      { id: 13, name: 'MI-TRK-3300', capacity: 26, location: 'Miami', model: 'Peterbilt 579' },
      { id: 14, name: 'SE-TRK-4400', capacity: 16, location: 'Seattle', model: 'Mack Anthem' },
      { id: 15, name: 'AT-TRK-5500', capacity: 19, location: 'Atlanta', model: 'Renault Magnum' },
    ],
    [],
  );

  const [isPopupOpen, setPopupOpen] = useState(false);
  const [isSnackbarOpen, setSnackbarOpen] = useState(false);
  const [isToastOpen, setToastOpen] = useState(false);
  const [popupAnchor, setPopupAnchor] = useState();
  const [sortRequest, setSortRequest] = useState(0);
  const [snackbarKey, setSnackbarKey] = useState(0);
  const [sortedResources, setResources] = useState(myResources);
  const [tempSortColumn, setTempSortColumn] = useState();
  const [tempSortDirection, setTempSortDirection] = useState();

  const buttonRef = useRef();
  const calRef = useRef();
  const metricBarAnimation = useRef(true);
  const sortColumn = useRef('standby');
  const sortDirection = useRef('asc');
  const tempEvent = useRef();
  const weekStart = useRef();
  const weekEnd = useRef();

  const myView = useMemo(() => ({ timeline: { type: 'week', resolutionHorizontal: 'day' } }), []);

  const sortColumnLabels = useMemo(
    () => ({
      standby: 'Standby Time',
      payload: 'Payload Efficiency',
      deadhead: 'Deadhead Time',
    }),
    [],
  );

  const calcMetrics = useCallback(() => {
    const loadedEvents = calRef.current ? calRef.current.getEvents() : [];

    myResources.forEach((resource) => {
      let standby = 168;
      let deadhead = 0;
      let payload = 0;
      let numberOfTours = 0;

      const resourceEvents = loadedEvents.filter((event) => event.resource === resource.id);

      resourceEvents.forEach((event) => {
        const eventStart = new Date(event.start);
        const eventEnd = new Date(event.end);
        const effectiveStart = eventStart < weekStart.current ? weekStart.current : eventStart;
        const effectiveEnd = eventEnd > weekEnd.current ? weekEnd.current : eventEnd;

        if (effectiveStart < effectiveEnd) {
          standby -= (effectiveEnd - effectiveStart) / (1000 * 60 * 60);
        }

        if (effectiveStart < effectiveEnd && (!event.payload || event.payload <= 0)) {
          deadhead += (effectiveEnd - effectiveStart) / (1000 * 60 * 60);
        }

        if (eventEnd > weekStart.current && eventStart < weekEnd.current) {
          numberOfTours++;
          payload += event.payload || 0;
        }
      });

      resource.standby = standby;
      resource.deadhead = deadhead;
      resource.payload = numberOfTours && resource.capacity ? Math.round((payload / numberOfTours / resource.capacity) * 100) : 0;
    });
  }, [myResources]);

  const sortResources = useCallback(() => {
    metricBarAnimation.current = true;
    const updatedResources = [...myResources].sort((resource1, resource2) => {
      let col = sortColumn.current;
      if (resource1[col] === resource2[col]) {
        col = 'name';
      }
      return sortDirection.current === 'asc' ? (resource1[col] > resource2[col] ? 1 : -1) : resource1[col] < resource2[col] ? 1 : -1;
    });
    setResources(updatedResources);
    setTimeout(() => {
      metricBarAnimation.current = false;
    }, 100);
  }, [myResources, sortColumn, sortDirection]);

  const delayedSort = useCallback((event) => {
    tempEvent.current = event;
    setSnackbarOpen(false);
    setSortRequest((prevRequest) => prevRequest + 1);
  }, []);

  const popupButtons = useMemo(
    () => [
      'cancel',
      {
        text: 'Apply',
        keyCode: 'enter',
        cssClass: 'mbsc-popup-button-primary',
        handler: function () {
          setPopupOpen(false);
          setToastOpen(true);
          sortColumn.current = tempSortColumn;
          sortDirection.current = tempSortDirection;
          sortResources();
        },
      },
    ],
    [sortResources, tempSortColumn, tempSortDirection],
  );

  const snackbarButton = useMemo(() => ({ text: 'Sort now' }), []);

  const handleSnackbarClose = useCallback(() => {
    setSnackbarOpen(false);
    const event = tempEvent.current;
    const resource = myResources.find((resource) => resource.id === event.resource);
    const eventStart = new Date(event.start);
    const navStart = eventStart < weekStart.current ? weekStart.current : eventStart;
    sortResources();
    calRef.current.navigateToEvent({ start: navStart, resource: event.resource });
    resource.cssClass = 'mbsc-timeline-parent-hover';
    setTimeout(() => {
      resource.cssClass = '';
      setResources((prevResources) => prevResources.slice());
    }, 1000);
  }, [myResources, sortResources]);

  const handlePopupOpen = useCallback(() => {
    setTempSortColumn(sortColumn.current);
    setTempSortDirection(sortDirection.current);
    setPopupAnchor(buttonRef.current.nativeElement);
    setPopupOpen(true);
  }, []);

  const handlePopupClose = useCallback(() => {
    setPopupOpen(false);
  }, []);

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

  const handlePageLoading = useCallback(
    (args) => {
      weekStart.current = args.firstDay;
      weekEnd.current = args.lastDay;
      setTimeout(() => {
        calcMetrics();
        sortResources();
      });
    },
    [calcMetrics, sortResources],
  );

  const handleEventCreated = useCallback(
    (args) => {
      args.event.payload = Math.floor(Math.random() * (17 - 5 + 1)) + 5;
      setMyEvents((prevEvents) => [...prevEvents, args.event]);
      calcMetrics();
      delayedSort(args.event);
    },
    [calcMetrics, delayedSort],
  );

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

  const handleEventUpdated = useCallback(
    (args) => {
      const oldEventStart = new Date(args.oldEvent.start);
      const oldEventEnd = new Date(args.oldEvent.end);
      if (
        +oldEventStart !== +args.event.start &&
        +oldEventEnd !== +args.event.end &&
        oldEventStart >= weekStart.current &&
        oldEventEnd <= weekEnd.current &&
        args.event.start >= weekStart.current &&
        args.event.end <= weekEnd.current &&
        args.oldEvent.resource === args.event.resource
      ) {
        return;
      }
      calcMetrics();
      delayedSort(args.event);
    },
    [calcMetrics, delayedSort],
  );

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

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

  const myCustomHeader = useCallback(
    () => (
      <>
        <CalendarPrev />
        <CalendarNext />
        <CalendarNav />
        <div className="mbsc-flex mbsc-flex-1-1 mbsc-justify-content-end">
          <Button ref={buttonRef} startIcon="bars" variant="flat" onClick={handlePopupOpen}>
            Sort Trucks
          </Button>
        </div>
      </>
    ),
    [handlePopupOpen],
  );

  const myCustomResourceHeader = useCallback(() => <div className="mds-popup-sort-resource-header">Trucks</div>, []);

  const myCustomResource = useCallback(
    (resource) => {
      const metricValue = resource[sortColumn.current];
      const barValue = sortColumn.current === 'payload' ? metricValue : (metricValue / 168) * 100;
      const barColor = barValue <= 33 ? 'green' : barValue <= 66 ? 'yellow' : 'red';

      return (
        <div className="mds-popup-sort-resource-cell">
          <strong>{resource.name}</strong>
          <div className="mds-popup-sort-resource-attr">Model: {resource.model}</div>
          <div className="mds-popup-sort-resource-attr">Capacity: {resource.capacity}T</div>
          <div className="mds-popup-sort-resource-attr">
            {sortColumnLabels[sortColumn.current]}: {metricValue}
            {sortColumn.current === 'payload' ? '%' : 'h'}
          </div>
          <div className="mds-popup-sort-bar-track">
            <div
              className={`mds-popup-sort-bar mds-popup-sort-bar-${barColor}${
                metricBarAnimation.current ? ' mds-popup-sort-bar-animation' : ''
              }`}
              style={{ width: `${barValue}%` }}
            ></div>
          </div>
        </div>
      );
    },
    [metricBarAnimation, sortColumnLabels],
  );

  const myScheduleEvent = useCallback(
    (event) => (
      <>
        <div>{event.title}</div>
        <div className="mds-popup-sort-event-attr">Payload: {event.original.payload ? `${event.original.payload} T` : 'empty'}</div>
      </>
    ),
    [],
  );

  useEffect(() => {
    if (sortRequest > 0) {
      setSnackbarKey((prevKey) => prevKey + 1);
    }
  }, [sortRequest]);

  useEffect(() => {
    if (snackbarKey > 0) {
      setSnackbarOpen(true);
    }
  }, [snackbarKey]);

  return (
    <>
      <Eventcalendar
        ref={calRef}
        cssClass="mds-popup-sort"
        clickToCreate={true}
        data={myEvents}
        dragToCreate={true}
        dragToMove={true}
        dragToResize={true}
        eventOverlap={false}
        resources={sortedResources}
        view={myView}
        renderHeader={myCustomHeader}
        renderResourceHeader={myCustomResourceHeader}
        renderResource={myCustomResource}
        renderTimelineEventContent={myScheduleEvent}
        onPageLoading={handlePageLoading}
        onEventCreated={handleEventCreated}
        onEventDeleted={handleEventDeleted}
        onEventUpdated={handleEventUpdated}
      />
      <Popup
        anchor={popupAnchor}
        buttons={popupButtons}
        contentPadding={false}
        display="anchored"
        focusOnClose={false}
        focusOnOpen={false}
        isOpen={isPopupOpen}
        showOverlay={false}
        width={400}
        onClose={handlePopupClose}
      >
        <div className="mbsc-form-group">
          <div className="mbsc-form-group-title">Metric to calculate and sort by</div>
          <RadioGroup value={tempSortColumn} onChange={handleSortColumnChange}>
            <Radio value="standby" label="Standby Time" description="Time the truck is driven without cargo." />
            <Radio value="payload" label="Payload Efficiency" description="Truck capacity divided by the average cargo on tours." />
            <Radio value="deadhead" label="Deadhead Time" description="Time when the truck is not on a tour." />
          </RadioGroup>
        </div>
        <div className="mbsc-form-group">
          <div className="mbsc-form-group-title">Sort direction</div>
          <SegmentedGroup value={tempSortDirection} onChange={handleSortDirectionChange}>
            <Segmented value="asc">Ascending</Segmented>
            <Segmented value="desc">Descending</Segmented>
          </SegmentedGroup>
        </div>
      </Popup>
      <Snackbar
        key={snackbarKey}
        animation="pop"
        button={snackbarButton}
        cssClass="mds-popup-sort-snackbar"
        display="center"
        duration={3000}
        isOpen={isSnackbarOpen}
        onClose={handleSnackbarClose}
      />
      <Toast message="Resources sorted" isOpen={isToastOpen} onClose={handleToastClose} />
    </>
  );
}

export default App;
```

### TSX

```tsx
import {
  Button,
  CalendarNav,
  CalendarNext,
  CalendarPrev,
  Eventcalendar,
  MbscCalendarEvent,
  MbscCalendarEventData,
  MbscEventcalendarView,
  MbscEventCreatedEvent,
  MbscEventDeletedEvent,
  MbscEventUpdatedEvent,
  MbscPageLoadingEvent,
  MbscPopupButton,
  MbscResource,
  Popup,
  Radio,
  RadioGroup,
  Segmented,
  SegmentedGroup,
  setOptions,
  Snackbar,
  Toast,
} from '@mobiscroll/react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';

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

interface MyEvent extends MbscCalendarEvent {
  payload?: number;
}

interface MyResource extends MbscResource {
  standby?: number;
  deadhead?: number;
  payload?: number;
  model: string;
  location: string;
}

function App() {
  const [myEvents, setMyEvents] = useState<MyEvent[]>([
    {
      start: '2026-10-19T00:00',
      end: '2026-10-23T00:00',
      title: 'Tour #013 - Miami to Seattle',
      resource: 1,
      color: '#FF9933',
      payload: 25,
    },
    {
      start: '2026-10-21T00:00',
      end: '2026-10-23T00:00',
      title: 'Tour #014 - Denver to Boston',
      resource: 2,
      color: '#33FFA6',
      payload: 18,
    },
    {
      start: '2026-10-20T00:00',
      end: '2026-10-23T00:00',
      title: 'Tour #015 - Orlando to Austin',
      resource: 3,
      color: '#9933FF',
      payload: 20,
    },
    {
      start: '2026-10-21T00:00',
      end: '2026-10-24T00:00',
      title: 'Tour #016 - Detroit to Baltimore',
      resource: 4,
      color: '#33A6FF',
      payload: 0,
    },
    {
      start: '2026-10-22T00:00',
      end: '2026-10-25T00:00',
      title: 'Tour #017 - Las Vegas to Portland',
      resource: 5,
      color: '#FF5733',
      payload: 20,
    },
    {
      start: '2026-10-22T00:00',
      end: '2026-10-25T00:00',
      title: 'Tour #018 - Atlanta to Kansas City',
      resource: 6,
      color: '#33FF99',
      payload: 0,
    },
    {
      start: '2026-10-16T11:00',
      end: '2026-10-20T00:00',
      title: 'Tour #018 - Dallas to Atlanta',
      resource: 6,
      color: '#33FF99',
      payload: 14,
    },
    {
      start: '2026-10-20T00:00',
      end: '2026-10-24T00:00',
      title: 'Tour #019 - Charlotte to Indianapolis',
      resource: 7,
      color: '#FF5733',
      payload: 22,
    },
    {
      start: '2026-10-20T00:00',
      end: '2026-10-22T00:00',
      title: 'Tour #005 - Dallas to San Francisco',
      resource: 7,
      color: '#FF5733',
      payload: 18,
    },
    {
      start: '2026-10-24T00:00',
      end: '2026-10-26T00:00',
      title: 'Tour #001 - New York to Los Angeles',
      resource: 7,
      color: '#FF5733',
      payload: 20,
    },
    {
      start: '2026-10-20T00:00',
      end: '2026-10-22T00:00',
      title: 'Tour #009 - San Diego to Dallas',
      resource: 7,
      color: '#FF5733',
      payload: 16,
    },
    {
      start: '2026-10-24T00:00',
      end: '2026-10-26T00:00',
      title: 'Tour #006 - Los Angeles to Chicago',
      resource: 8,
      color: '#FF33A6',
      payload: 10,
    },
    {
      start: '2026-10-18T00:00',
      end: '2026-10-22T00:00',
      title: 'Tour #010 - San Francisco to Los Angeles',
      resource: 8,
      color: '#FF33A6',
      payload: 0,
    },
    {
      start: '2026-10-23T00:00',
      end: '2026-10-26T00:00',
      title: 'Tour #007 - Houston to New York',
      resource: 9,
      color: '#33FF57',
      payload: 0,
    },
    {
      start: '2026-10-20T00:00',
      end: '2026-10-22T00:00',
      title: 'Tour #003 - Philadelphia to Phoenix',
      resource: 9,
      color: '#33FF57',
      payload: 0,
    },
    {
      start: '2026-10-16T00:00',
      end: '2026-10-19T00:00',
      title: 'Tour #028 - Trenton to Philadelphia',
      resource: 9,
      color: '#33FF57',
      payload: 11,
    },
    {
      start: '2026-10-16T00:00',
      end: '2026-10-21T00:00',
      title: 'Tour #004 - San Antonio to San Diego',
      resource: 10,
      color: '#3357FF',
      payload: 15,
    },

    {
      start: '2026-10-23T00:00',
      end: '2026-10-26T00:00',
      title: 'Tour #022 - Cleveland to Cincinnati',
      resource: 10,
      color: '#3357FF',
      payload: 17,
    },
    {
      start: '2026-10-16T00:00',
      end: '2026-10-21T00:00',
      title: 'Tour #023 - Boston to Philadelphia',
      resource: 11,
      color: '#FF9933',
      payload: 19,
    },
    {
      start: '2026-10-20T00:00',
      end: '2026-10-22T00:00',
      title: 'Tour #024 - Las Vegas to San Diego',
      resource: 12,
      color: '#33FF57',
      payload: 28,
    },
    {
      start: '2026-10-17T00:00',
      end: '2026-10-20T00:00',
      title: 'Tour #025 - Miami to Charlotte',
      resource: 13,
      color: '#9933FF',
      payload: 26,
    },
    {
      start: '2026-10-22T00:00',
      end: '2026-10-25T00:00',
      title: 'Tour #026 - Seattle to Portland',
      resource: 14,
      color: '#33A6FF',
      payload: 12,
    },
    {
      start: '2026-10-19T00:00',
      end: '2026-10-22T00:00',
      title: 'Tour #027 - Atlanta to Orlando',
      resource: 15,
      color: '#FF5733',
      payload: 17,
    },
  ]);

  const myResources = useMemo<MyResource[]>(
    () => [
      { id: 1, name: 'NY-TRK-1200', capacity: 25, location: 'New York', model: 'Renault Magnum' },
      { id: 2, name: 'LA-TRK-0090', capacity: 18, location: 'Los Angeles', model: 'Mercedes Actros' },
      { id: 3, name: 'CH-TRK-0700', capacity: 22, location: 'Phoenix', model: 'Scania R500' },
      { id: 4, name: 'HO-TRK-0850', capacity: 28, location: 'Houston', model: 'Volvo FH16' },
      { id: 5, name: 'PH-TRK-0900', capacity: 24, location: 'Chicago', model: 'MAN TGX' },
      { id: 6, name: 'PA-TRK-0300', capacity: 15, location: 'Philadelphia', model: 'Renault T High' },
      { id: 8, name: 'SD-TRK-0250', capacity: 21, location: 'San Francisco', model: 'Mercedes Arocs' },
      { id: 9, name: 'DA-TRK-0400', capacity: 20, location: 'Dallas', model: 'DAF XF' },
      { id: 10, name: 'SF-TRK-0550', capacity: 17, location: 'San Diego', model: 'Iveco Stralis' },
      { id: 11, name: 'BO-TRK-1100', capacity: 23, location: 'Boston', model: 'Kenworth T680' },
      { id: 12, name: 'LV-TRK-2200', capacity: 30, location: 'Las Vegas', model: 'Volvo FH16' },
      { id: 13, name: 'MI-TRK-3300', capacity: 26, location: 'Miami', model: 'Peterbilt 579' },
      { id: 14, name: 'SE-TRK-4400', capacity: 16, location: 'Seattle', model: 'Mack Anthem' },
      { id: 15, name: 'AT-TRK-5500', capacity: 19, location: 'Atlanta', model: 'Renault Magnum' },
    ],
    [],
  );

  const [isPopupOpen, setPopupOpen] = useState<boolean>(false);
  const [isSnackbarOpen, setSnackbarOpen] = useState<boolean>(false);
  const [isToastOpen, setToastOpen] = useState<boolean>(false);
  const [popupAnchor, setPopupAnchor] = useState<HTMLButtonElement>();
  const [sortRequest, setSortRequest] = useState<number>(0);
  const [snackbarKey, setSnackbarKey] = useState<number>(0);
  const [sortedResources, setResources] = useState<MyResource[]>(myResources);
  const [tempSortColumn, setTempSortColumn] = useState<'standby' | 'payload' | 'deadhead' | 'name'>('standby');
  const [tempSortDirection, setTempSortDirection] = useState<'asc' | 'desc'>('asc');

  const buttonRef = useRef<Button>(null);
  const calRef = useRef<Eventcalendar | null>(null);
  const metricBarAnimation = useRef<boolean>(true);
  const sortColumn = useRef<'standby' | 'payload' | 'deadhead' | 'name'>('standby');
  const sortDirection = useRef<'asc' | 'desc'>('asc');
  const tempEvent = useRef<MyEvent>(undefined);
  const weekStart = useRef<Date>(undefined);
  const weekEnd = useRef<Date>(undefined);

  const myView = useMemo<MbscEventcalendarView>(() => ({ timeline: { type: 'week', resolutionHorizontal: 'day' } }), []);

  const sortColumnLabels = useMemo<{ [key: string]: string }>(
    () => ({
      standby: 'Standby Time',
      payload: 'Payload Efficiency',
      deadhead: 'Deadhead Time',
    }),
    [],
  );

  const calcMetrics = useCallback(() => {
    const loadedEvents: MyEvent[] = calRef.current ? calRef.current.getEvents() : [];

    myResources.forEach((resource) => {
      let standby = 168;
      let deadhead = 0;
      let payload = 0;
      let numberOfTours = 0;

      const resourceEvents = loadedEvents.filter((event) => event.resource === resource.id);

      resourceEvents.forEach((event) => {
        const eventStart = new Date(event.start as Date);
        const eventEnd = new Date(event.end as Date);
        const effectiveStart = eventStart < weekStart.current! ? weekStart.current : eventStart;
        const effectiveEnd = eventEnd > weekEnd.current! ? weekEnd.current : eventEnd;

        if (effectiveStart! < effectiveEnd!) {
          if (effectiveStart && effectiveEnd) {
            standby -= (effectiveEnd.getTime() - effectiveStart.getTime()) / (1000 * 60 * 60);
          }
        }

        if (effectiveStart! < effectiveEnd! && (!event.payload || event.payload <= 0)) {
          if (effectiveStart && effectiveEnd) {
            deadhead += (effectiveEnd.getTime() - effectiveStart.getTime()) / (1000 * 60 * 60);
          }
        }

        if (eventEnd > weekStart.current! && eventStart < weekEnd.current!) {
          numberOfTours++;
          payload += event.payload || 0;
        }
      });

      resource.standby = standby;
      resource.deadhead = deadhead;
      resource.payload = numberOfTours && resource.capacity ? Math.round((payload / numberOfTours / resource.capacity) * 100) : 0;
    });
  }, [myResources]);

  const sortResources = useCallback(() => {
    metricBarAnimation.current = true;
    const updatedResources = [...myResources].sort((resource1, resource2) => {
      let col = sortColumn.current;
      if (resource1[col] === resource2[col]) {
        col = 'name';
      }
      return sortDirection.current === 'asc' ? (resource1[col]! > resource2[col]! ? 1 : -1) : resource1[col]! < resource2[col]! ? 1 : -1;
    });
    setResources(updatedResources);
    setTimeout(() => {
      metricBarAnimation.current = false;
    }, 100);
  }, [myResources, sortColumn, sortDirection]);

  const delayedSort = useCallback((event: MbscCalendarEvent) => {
    tempEvent.current = event;
    setSnackbarOpen(false);
    setSortRequest((prevRequest) => prevRequest + 1);
  }, []);

  const popupButtons = useMemo<Array<MbscPopupButton | 'cancel'>>(
    () => [
      'cancel',
      {
        text: 'Apply',
        keyCode: 'enter',
        cssClass: 'mbsc-popup-button-primary',
        handler: function () {
          setPopupOpen(false);
          setToastOpen(true);
          sortColumn.current = tempSortColumn;
          sortDirection.current = tempSortDirection;
          sortResources();
        },
      },
    ],
    [sortResources, tempSortColumn, tempSortDirection],
  );

  const snackbarButton = useMemo(() => ({ text: 'Sort now' }), []);

  const handleSnackbarClose = useCallback(() => {
    setSnackbarOpen(false);
    const event = tempEvent.current;
    const resource = myResources.find((resource) => resource.id === event!.resource);
    const eventStart = new Date(event!.start as Date);
    const navStart = eventStart < weekStart.current! ? weekStart.current : eventStart;
    sortResources();
    if (calRef.current) {
      calRef.current.navigateToEvent({ start: navStart, resource: event!.resource });
    }
    if (resource) {
      resource.cssClass = 'mbsc-timeline-parent-hover';
      setTimeout(() => {
        resource.cssClass = '';
        setResources((prevResources) => prevResources.slice());
      }, 1000);
    }
  }, [myResources, sortResources]);

  const handlePopupOpen = useCallback(() => {
    setTempSortColumn(sortColumn.current);
    setTempSortDirection(sortDirection.current);
    if (buttonRef.current) {
      setPopupAnchor(buttonRef.current.nativeElement as HTMLButtonElement);
    }
    setPopupOpen(true);
  }, []);

  const handlePopupClose = useCallback(() => {
    setPopupOpen(false);
  }, []);

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

  const handlePageLoading = useCallback(
    (args: MbscPageLoadingEvent) => {
      weekStart.current = args.firstDay;
      weekEnd.current = args.lastDay;
      setTimeout(() => {
        calcMetrics();
        sortResources();
      });
    },
    [calcMetrics, sortResources],
  );

  const handleEventCreated = useCallback(
    (args: MbscEventCreatedEvent) => {
      args.event.payload = Math.floor(Math.random() * (17 - 5 + 1)) + 5;
      setMyEvents((prevEvents) => [...prevEvents, args.event]);
      calcMetrics();
      delayedSort(args.event);
    },
    [calcMetrics, delayedSort],
  );

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

  const handleEventUpdated = useCallback(
    (args: MbscEventUpdatedEvent) => {
      const oldEventStart = new Date(args.oldEvent!.start as Date);
      const oldEventEnd = new Date(args.oldEvent!.end as Date);
      if (
        +oldEventStart !== +args.event.start! &&
        +oldEventEnd !== +args.event.end! &&
        oldEventStart >= weekStart.current! &&
        oldEventEnd <= weekEnd.current! &&
        args.event.start! >= weekStart.current! &&
        args.event.end! <= weekEnd.current! &&
        args.oldEvent!.resource === args.event.resource
      ) {
        return;
      }
      calcMetrics();
      delayedSort(args.event);
    },
    [calcMetrics, delayedSort],
  );

  const handleSortColumnChange = useCallback((ev: React.ChangeEvent<HTMLInputElement>) => {
    setTempSortColumn(ev.target.value as 'standby' | 'payload' | 'deadhead');
  }, []);

  const handleSortDirectionChange = useCallback((ev: React.ChangeEvent<HTMLInputElement>) => {
    setTempSortDirection(ev.target.value as 'asc' | 'desc');
  }, []);

  const myCustomHeader = useCallback(
    () => (
      <>
        <CalendarPrev />
        <CalendarNext />
        <CalendarNav />
        <div className="mbsc-flex mbsc-flex-1-1 mbsc-justify-content-end">
          <Button ref={buttonRef} startIcon="bars" variant="flat" onClick={handlePopupOpen}>
            Sort Trucks
          </Button>
        </div>
      </>
    ),
    [handlePopupOpen],
  );

  const myCustomResourceHeader = useCallback(() => <div className="mds-popup-sort-resource-header">Trucks</div>, []);

  const myCustomResource = useCallback(
    (resource: MyResource) => {
      const metricValue = resource[sortColumn.current];
      const barValue = sortColumn.current === 'payload' ? metricValue : ((metricValue as number) / 168) * 100;
      const barColor = (barValue as number) <= 33 ? 'green' : (barValue as number) <= 66 ? 'yellow' : 'red';

      return (
        <div className="mds-popup-sort-resource-cell">
          <strong>{resource.name}</strong>
          <div className="mds-popup-sort-resource-attr">Model: {resource.model}</div>
          <div className="mds-popup-sort-resource-attr">Capacity: {resource.capacity}T</div>
          <div className="mds-popup-sort-resource-attr">
            {sortColumnLabels[sortColumn.current]}: {metricValue}
            {sortColumn.current === 'payload' ? '%' : 'h'}
          </div>
          <div className="mds-popup-sort-bar-track">
            <div
              className={`mds-popup-sort-bar mds-popup-sort-bar-${barColor}${
                metricBarAnimation.current ? ' mds-popup-sort-bar-animation' : ''
              }`}
              style={{ width: `${barValue}%` }}
            ></div>
          </div>
        </div>
      );
    },
    [metricBarAnimation, sortColumnLabels],
  );

  const myScheduleEvent = useCallback(
    (event: MbscCalendarEventData) => (
      <>
        <div>{event.title}</div>
        <div className="mds-popup-sort-event-attr">Payload: {event.original!.payload ? `${event.original!.payload} T` : 'empty'}</div>
      </>
    ),
    [],
  );

  useEffect(() => {
    if (sortRequest > 0) {
      setSnackbarKey((prevKey) => prevKey + 1);
    }
  }, [sortRequest]);

  useEffect(() => {
    if (snackbarKey > 0) {
      setSnackbarOpen(true);
    }
  }, [snackbarKey]);

  return (
    <>
      <Eventcalendar
        ref={calRef}
        cssClass="mds-popup-sort"
        clickToCreate={true}
        data={myEvents}
        dragToCreate={true}
        dragToMove={true}
        dragToResize={true}
        eventOverlap={false}
        resources={sortedResources}
        view={myView}
        renderHeader={myCustomHeader}
        renderResourceHeader={myCustomResourceHeader}
        renderResource={myCustomResource}
        renderTimelineEventContent={myScheduleEvent}
        onPageLoading={handlePageLoading}
        onEventCreated={handleEventCreated}
        onEventDeleted={handleEventDeleted}
        onEventUpdated={handleEventUpdated}
      />
      <Popup
        anchor={popupAnchor}
        buttons={popupButtons}
        contentPadding={false}
        display="anchored"
        focusOnClose={false}
        focusOnOpen={false}
        isOpen={isPopupOpen}
        showOverlay={false}
        width={400}
        onClose={handlePopupClose}
      >
        <div className="mbsc-form-group">
          <div className="mbsc-form-group-title">Metric to calculate and sort by</div>
          <RadioGroup value={tempSortColumn} onChange={handleSortColumnChange}>
            <Radio value="standby" label="Standby Time" description="Time the truck is driven without cargo." />
            <Radio value="payload" label="Payload Efficiency" description="Truck capacity divided by the average cargo on tours." />
            <Radio value="deadhead" label="Deadhead Time" description="Time when the truck is not on a tour." />
          </RadioGroup>
        </div>
        <div className="mbsc-form-group">
          <div className="mbsc-form-group-title">Sort direction</div>
          <SegmentedGroup value={tempSortDirection} onChange={handleSortDirectionChange}>
            <Segmented value="asc">Ascending</Segmented>
            <Segmented value="desc">Descending</Segmented>
          </SegmentedGroup>
        </div>
      </Popup>
      <Snackbar
        key={snackbarKey}
        animation="pop"
        button={snackbarButton}
        cssClass="mds-popup-sort-snackbar"
        display="center"
        duration={3000}
        isOpen={isSnackbarOpen}
        onClose={handleSnackbarClose}
      />
      <Toast message="Resources sorted" isOpen={isToastOpen} onClose={handleToastClose} />
    </>
  );
}

export default App;
```

### CSS

```css
/* Overrides */

.mds-popup-sort .mbsc-timeline-resource-header,
.mds-popup-sort .mbsc-timeline-resource-title {
  padding: 0;
}

.mds-popup-sort .mbsc-timeline-resource-col {
  width: 170px;
}

.mds-popup-sort .mbsc-timeline-row {
  height: 110px;
}

.mds-popup-sort .mbsc-timeline-events {
  top: 20px;
}

/* Resource grid */

.mds-popup-sort-resource-header {
  padding: 0 5px;
  line-height: 25px;
}

.mds-popup-sort-resource-cell {
  padding: 5px;
  line-height: 20px;
}

.mds-popup-sort-resource-attr {
  font-size: 12px;
  font-weight: 400;
}

/* Event template */

.mds-popup-sort-event-attr {
  font-size: 11px;
}

/* Metric bar */

.mds-popup-sort-bar-track {
  background-color: #f0f0f0;
  border-radius: 5px;
  height: 10px;
  width: 150px;
  margin-top: 5px;
  overflow: hidden;
}

.mds-popup-sort-bar {
  height: 100%;
}

.mds-popup-sort-bar-green {
  background-color: #4caf50;
}

.mds-popup-sort-bar-yellow {
  background-color: #ffeb3b;
}

.mds-popup-sort-bar-red {
  background-color: #f44336;
}

.mds-popup-sort-bar-animation {
  animation: mds-popup-sort-fill-bar 1s ease-in-out forwards;
}

/* Snackbar countdown */

.mds-popup-sort-snackbar .mbsc-snackbar-cont {
  border-radius: 4px;
  position: relative;
  overflow: hidden;
}

.mds-popup-sort-snackbar .mbsc-snackbar-cont::before {
  content: '';
  position: absolute;
  left: 0;
  height: 100%;
  width: 100%;
  background-color: rgba(0, 0, 0, 0.5);
  animation: mds-popup-sort-fill-bar 3s linear forwards;
}

.mds-popup-sort-snackbar .mbsc-snackbar-message::after {
  content: 'Sorting in 1 .';
  position: relative;
  z-index: 1;
  animation: mds-popup-sort-message 3s steps(3) forwards;
}

/* Animations */

@keyframes mds-popup-sort-fill-bar {
  from {
    transform: translateX(-100%);
  }
  to {
    transform: translateX(0);
  }
}

@keyframes mds-popup-sort-message {
  0% {
    content: 'Sorting in 3 ...';
  }
  43% {
    content: 'Sorting in 2 ..';
  }
  76% {
    content: 'Sorting in 1 .';
  }
}
```

## Implementation notes

- Use `timeline: { type: 'week', resolutionHorizontal: 'day' }` - a week view with a column per day.
- Define 14 truck resources, each with `id`, `name`, `capacity` (tons), `location`, and `model`. Resources also carry computed fields (`standby`, `deadhead`, `payload`) that are recalculated after every event change.
- Add 23 tour events with `dyndatetime` offsets around today. Each event carries a `payload` property (tons carried; `0` = empty/deadhead run) and a `color`. Enable all interactions: `clickToCreate: true`, `dragToCreate: true`, `dragToMove: true`, `dragToResize: true`, `eventOverlap: false`. When a new event is created, assign it a random payload (5-17 tons) in `onEventCreated`.
- **Three metrics**, all calculated per resource against the currently visible week (`weekStart` / `weekEnd` captured from `onPageLoading`'s `args.firstDay` / `args.lastDay`):
  - **Standby time** (hours): `168` minus the total hours the truck is on tour within the week. Clamp event start/end to week boundaries before computing overlap.
  - **Deadhead time** (hours): hours within the week where the truck is on a tour with `payload === 0` (empty runs).
  - **Payload efficiency** (%): `Math.round((sum of tour payloads / number of tours / truck capacity) * 100)`. Returns 0 if no tours.
- **Sorting**: Sort the resource array by the active `sortColumn` (`'standby'`, `'payload'`, or `'deadhead'`) in `sortDirection` (`'asc'` or `'desc'`). Ties are broken by `name`. Default: standby time ascending.
- **Sort popup** (`Popup`, display `'anchored'`, anchored to the "Sort Trucks" button): Contains a `RadioGroup` with three metric options and a `SegmentedGroup` for ascending/descending. Changes are staged in temporary state until "Apply" is clicked - Apply commits the selection, closes the popup, and shows a `Toast` "Resources sorted". Cancel discards changes.
- **Snackbar on event changes**: After any event created/deleted/updated that affects the metric, open a `Snackbar` (3-second auto-dismiss, "Sort now" button, `display: 'center'`). When the snackbar closes (either by timer or the button), sort the resources, navigate the calendar to the affected event using `calRef.current.navigateToEvent(...)`, and briefly apply `cssClass: 'mbsc-timeline-parent-hover'` to the resource row for a highlight animation (clear after 1 second). Increment a `snackbarKey` on each new event change to restart the snackbar timer from zero.
- **`onPageLoading`**: Capture `firstDay`/`lastDay`, then recalculate metrics and sort in a `setTimeout`.
- **Custom resource column header** (`renderResourceHeader` / `resourceHeaderTemplate` / `resourceHeader` slot): Static "Trucks" label.
- **Custom resource row** (`renderResource` / `resourceTemplate` / `resource` slot): Shows truck name (bold), model, capacity in tons, then the currently active metric label + value + unit (`h` for time-based metrics, `%` for payload). Below that, render a colored progress bar: green if ≤ 33%, yellow if ≤ 66%, red if > 66%. For payload efficiency the bar value equals the metric percentage directly; for standby/deadhead, compute `(metricValue / 168) * 100`. Apply a brief CSS animation class on the bar immediately after a sort.
- **Custom event content** (`renderTimelineEventContent`, Angular: `timelineEventContentTemplate`, Vue: `#timelineEventContent`): Shows the tour title and a payload line - `"X T"` if payload > 0, `"empty"` otherwise.
- **Custom header** (`renderHeader` / `headerTemplate` / `header` slot): `CalendarPrev`, `CalendarNext`, `CalendarNav`, then a "Sort Trucks" `Button` with a bars icon aligned to the right.

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

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: [Resource Drag-and-Drop Reorder](https://demo.mobiscroll.com/react/timeline/resource-drag-drop-reorder.md) - Reorder resources on a React Timeline by drag and drop — each row shows a drag handle, and an update callback runs on drop. For React and Next.JS.
- Next: [Resource Properties](https://demo.mobiscroll.com/react/timeline/resource-data-structure.md) - Define resource properties on the React Timeline — set up nested groups, fixed rows, and per-resource drag, resize, and overlap controls. For React and Next.JS.
