---
title: "React Timeline Resource Group Aggregate Summaries Example | Mobiscroll"
component: "Timeline"
framework: "React"
library_version: "6.1.5"
url: "https://demo.mobiscroll.com/react/timeline/show-summaries-aggregates-for-resource-groups"
---

# Resource Group Aggregate Summaries - React Timeline demo

Show aggregate totals in parent resource rows on a React Timeline — summary values recalculate as bookings are added, changed, or removed. For React and Next.JS.

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

## Description

When scheduling across multiple groups of resources it sometimes is helpful to provide summaries and aggregate calculations on a group level. These dynamically calculated values can be presented as custom templated events in resource parent rows. Whenever new bookings are made, updated or deleted, use the [`onEventCreated`](https://mobiscroll.com/docs/react/eventcalendar/api#event-onEventCreated), [`onEventUpdated`](https://mobiscroll.com/docs/react/eventcalendar/api#event-onEventUpdated) and [`onEventDeleted`](https://mobiscroll.com/docs/react/eventcalendar/api#event-onEventDeleted) lifecycle events to update the aggregates for their parent resource. As we are working with variable event heights, make sure to set `eventHeight: 'variable'` which can be configured under the [`view`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-view). This is necessary if the actual booked events have a different height than the summary events (for parent resources).

**Variable event height is an experimental feature.** [Learn more about it in the documentation →](https://mobiscroll.com/docs/react/eventcalendar/timeline#variable-event-height)

## What this demo shows

- A desktop timeline that combines a weekly date range with hierarchical resources and group-level summary events.
- **Header navigation** The month and year label opens date navigation, the previous and next arrows move between weeks, and the Today button returns to the current date.
- **Week view** The date strip shows the selected week from Sunday to Saturday, with the current date highlighted.
- **Resource hierarchy** The resource column shows expandable parent groups with nested vehicle resources grouped by fleet or location.
- **Resource details** Child resource rows display vehicle metadata such as model, license plate, and vehicle type.
- **Group summaries** Parent resource rows show daily aggregate metrics for all scheduled trips in that group.
- **Summary metrics** Each group summary displays total estimated distance, total fuel consumption, and estimated operating cost.
- **Event cards** Trip events show booking details such as destination or appointment, assigned driver, estimated distance, estimated fuel consumption, and pick-up and drop-off times.
- **Dynamic aggregates** Summary values are recalculated from the scheduled trips in each resource group when events are created, updated, or deleted.
- **Date positioning** Events are positioned by their assigned resource and exact date range.
- **Event interaction** Events highlight on hover.
- **Event selection** Clicking an event selects and highlights it.
- **Event creation** New events can be created with double click on timeline cells.
- **Scrolling** The timeline supports horizontal and vertical scrolling for navigating through time and resources.

## Best for

- **Fleet management** Scheduling vehicles while tracking daily distance, fuel consumption, and estimated operating cost by fleet or location.
- **Transportation planning** Comparing trip assignments across vehicles and resource groups in a timeline layout.
- **Vehicle reservations** Managing bookings for individual vehicles while keeping parent rows available for group-level utilization summaries.
- **Logistics operations** Reviewing scheduled trips alongside aggregate travel metrics for each resource group.
- **Field service scheduling** Assigning vehicles or crews to appointments where travel estimates and operating costs need to stay visible with the schedule.

## API used in this demo

- [`theme`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-theme) - option
- [`themeVariant`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-themeVariant) - option
- [`cssClass`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-cssClass) - option
- [`clickToCreate`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-clickToCreate) - option
- [`dragToCreate`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-dragToCreate) - option
- [`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
- [`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
- [`min`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-min) - option
- [`max`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-max) - option
- [`extendDefaultEvent`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-extendDefaultEvent) - option
- [`onPageLoading`](https://mobiscroll.com/docs/react/eventcalendar/api#event-onPageLoading) - event
- [`onEventUpdated`](https://mobiscroll.com/docs/react/eventcalendar/api#event-onEventUpdated) - event
- [`onEventCreated`](https://mobiscroll.com/docs/react/eventcalendar/api#event-onEventCreated) - event
- [`onEventDeleted`](https://mobiscroll.com/docs/react/eventcalendar/api#event-onEventDeleted) - event
- [`renderResource`](https://mobiscroll.com/docs/react/eventcalendar/api#renderer-renderResource) - renderer
- [`renderTimelineEventContent`](https://mobiscroll.com/docs/react/eventcalendar/api#renderer-renderTimelineEventContent) - renderer

## Code

### JSX

```jsx
import { Eventcalendar, formatDate, setOptions } from '@mobiscroll/react';
import { useCallback, useMemo, useRef, useState } from 'react';

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

function App() {
  const [myEvents, setEvents] = useState([
    {
      id: 1,
      allDay: true,
      start: '2026-10-21T06:00',
      end: '2026-10-21T19:00',
      resource: 'orlando_1',
      title: 'Sales meeting with JLL',
      name: 'John C. Johnson',
      distance: 120,
      consumption: 35,
    },
    {
      id: 2,
      allDay: true,
      start: '2026-10-22T03:00',
      end: '2026-10-22T20:00',
      resource: 'orlando_1',
      title: 'Client consultation',
      name: 'Mary K. Adams',
      distance: 95,
      consumption: 30,
    },
    {
      id: 3,
      allDay: true,
      start: '2026-10-24T10:00',
      end: '2026-10-24T17:00',
      resource: 'orlando_1',
      title: 'Project review at XYZ',
      name: 'Robert B. Brown',
      distance: 150,
      consumption: 40,
    },
    {
      id: 4,
      allDay: true,
      start: '2026-10-26T09:00',
      end: '2026-10-26T16:00',
      resource: 'orlando_1',
      title: 'Team building workshop',
      name: 'Susan E. Smith',
      distance: 60,
      consumption: 20,
    },
    // Showing partial data. Download full source.
  ]);

  const [eventsWithSummaries, setEventsWithSummaries] = useState(myEvents);

  const firstViewDay = useRef(null);
  const lastViewDay = useRef(null);

  const myView = useMemo(
    () => ({
      timeline: {
        type: 'week',
        eventDisplay: 'fill',
        eventHeight: 'variable',
      },
    }),
    [],
  );

  const myResources = useMemo(
    () => [
      {
        id: 'orlando',
        name: 'CNL Tower II',
        address: '420 S Orange Ave, Orlando, FL 32801',
        eventCreation: false,
        eventDragBetweenResources: false,
        eventDragInTime: false,
        eventResize: false,
        color: 'transparent',
        children: [
          {
            id: 'orlando_1',
            car: 'Lincoln Continental',
            plate: '51-5375',
            type: 'Sedan',
          },
          {
            id: 'orlando_2',
            car: 'Porsche Panamera',
            plate: '72-2590',
            type: 'Sports sedan',
          },
          {
            id: 'orlando_3',
            car: 'Range Rover Velar',
            plate: '41-7701',
            type: 'Compact SUV',
          },
        ],
      },
      // Showing partial data. Download full source.
    ],
    [],
  );

  const handleDefaultEvent = useCallback((args) => {
    const d = args.start;
    const start = new Date(d.getFullYear(), d.getMonth(), d.getDate(), 9);
    const end = new Date(start.getFullYear(), start.getMonth(), start.getDate(), 18);
    return {
      allDay: true,
      start: start,
      end: end,
      title: 'New event',
      name: 'John Doe',
      distance: 42,
      consumption: 6,
    };
  }, []);

  const getAggregateEvents = useCallback(
    (dayEvents) => {
      const aggregateEvents = {};

      myResources.forEach((resource) => {
        for (let date = new Date(firstViewDay.current); date < lastViewDay.current; date.setDate(date.getDate() + 1)) {
          aggregateEvents[resource.id + +date] = {
            id: resource.id + +date,
            isSummary: true,
            allDay: true,
            start: new Date(date),
            end: new Date(date),
            consumption: 0,
            distance: 0,
            resource: resource.id,
          };
        }
      });
      dayEvents.forEach((event) => {
        if (!event.isSummary) {
          const parentResource = event.resource.split('_')[0];
          const dayStart = new Date(event.start).setHours(0, 0, 0, 0);
          if (aggregateEvents[parentResource + dayStart]) {
            aggregateEvents[parentResource + dayStart].consumption += event.consumption;
            aggregateEvents[parentResource + dayStart].distance += event.distance;
          }
        }
      });

      return Object.values(aggregateEvents);
    },
    [firstViewDay, lastViewDay, myResources],
  );

  const updateCalendarEvents = useCallback(
    (inst) => {
      const dailyEvents = inst.getEvents();
      const updatedSummaries = getAggregateEvents(dailyEvents);
      setEventsWithSummaries([...myEvents, ...updatedSummaries]);
    },
    [getAggregateEvents, myEvents],
  );

  const handlePageLoading = useCallback(
    (args, inst) => {
      firstViewDay.current = new Date(args.firstDay);
      lastViewDay.current = new Date(args.lastDay);
      setTimeout(() => updateCalendarEvents(inst));
    },
    [updateCalendarEvents],
  );

  const handleEventUpdated = useCallback(
    (args, inst) => {
      const updatedEvent = args.event;
      const index = myEvents.indexOf(updatedEvent);
      myEvents.splice(index, 1, updatedEvent);
      setTimeout(() => updateCalendarEvents(inst));
    },
    [myEvents, updateCalendarEvents],
  );

  const handleEventCreated = useCallback(
    (args, inst) => {
      setEvents((prevEvents) => [...prevEvents, args.event]);
      setTimeout(() => updateCalendarEvents(inst));
    },
    [updateCalendarEvents],
  );

  const handleEventDeleted = useCallback(
    (args, inst) => {
      setEvents((prevEvents) => prevEvents.filter((item) => item.id !== args.event.id));
      setTimeout(() => updateCalendarEvents(inst));
    },
    [updateCalendarEvents],
  );

  const customResource = useCallback((res) => {
    if (res.isParent) {
      return (
        <>
          <div>{res.name}</div>
          <div className="mds-aggregates-address">{res.address}</div>
        </>
      );
    }
    return (
      <>
        <div>{res.car}</div>
        <div className="mds-aggregates-plate">Plate {res.plate}</div>
        <div className="mds-aggregates-cartype">{res.type}</div>
      </>
    );
  }, []);

  const customScheduleEventContent = useCallback((event) => {
    if (event.currentResource.isParent) {
      return (
        <>
          <div className="mds-aggregates-summary-event mbsc-flex">
            <div className="mds-aggregates-summary-details mbsc-flex mbsc-flex-1-1">
              <div className="mds-aggregates-summary-label">
                Total dist.
                <div className="mds-aggregates-summary-value">{event.original.distance} mi</div>
              </div>
              <div className="mds-aggregates-summary-label">
                Total cons.
                <div className="mds-aggregates-summary-value">{event.original.consumption} gal</div>
              </div>
              <div className="mds-aggregates-summary-label">
                Est. cost
                <div className="mds-aggregates-summary-value">$ {Math.round(event.original.consumption * 3.706 || 0)}</div>
              </div>
            </div>
          </div>
        </>
      );
    }
    return (
      <>
        <div className="mds-aggregates-event">
          <div className="mds-aggregates-title">{event.title}</div>
          <div className="mds-aggregates-name">{event.original.name}</div>
          <div className="mds-aggregates-details">
            <div className="mds-aggregates-label">
              Est. distance:
              <span className="mds-aggregates-value">{event.original.distance} mi</span>
            </div>
            <div className="mds-aggregates-label">
              Est. consumption:
              <span className="mds-aggregates-value">{event.original.consumption} gal</span>
            </div>
          </div>
          <div className="mds-aggregates-label mbsc-flex">
            <div className="mbsc-flex-1-1">
              Pick up: <span className="mds-aggregates-value">{formatDate('hh:mm A', new Date(event.original.start))}</span>
            </div>
            <div>
              Drop off: <span className="mds-aggregates-value">{formatDate('hh:mm A', new Date(event.original.end))}</span>
            </div>
          </div>
        </div>
      </>
    );
  }, []);

  return (
    <Eventcalendar
      cssClass="mds-aggregates"
      clickToCreate={true}
      dragToCreate={false}
      dragToMove={true}
      dragToResize={false}
      eventOverlap={false}
      view={myView}
      data={eventsWithSummaries}
      resources={myResources}
      min={'2026-10-21T00:00'}
      max={'2026-11-03T00:00'}
      extendDefaultEvent={handleDefaultEvent}
      onPageLoading={handlePageLoading}
      onEventUpdated={handleEventUpdated}
      onEventCreated={handleEventCreated}
      onEventDeleted={handleEventDeleted}
      renderResource={customResource}
      renderTimelineEventContent={customScheduleEventContent}
    />
  );
}

export default App;
```

### TSX

```tsx
import {
  Eventcalendar,
  formatDate,
  MbscCalendarEvent,
  MbscCalendarEventData,
  MbscEventcalendarView,
  MbscEventCreatedEvent,
  MbscEventDeletedEvent,
  MbscEventUpdatedEvent,
  MbscNewEventData,
  MbscPageLoadingEvent,
  MbscResource,
  setOptions,
} from '@mobiscroll/react';
import { useCallback, useMemo, useRef, useState } from 'react';

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

function App() {
  const [myEvents, setEvents] = useState<MbscCalendarEvent[]>([
    {
      id: 1,
      allDay: true,
      start: '2026-10-21T06:00',
      end: '2026-10-21T19:00',
      resource: 'orlando_1',
      title: 'Sales meeting with JLL',
      name: 'John C. Johnson',
      distance: 120,
      consumption: 35,
    },
    {
      id: 2,
      allDay: true,
      start: '2026-10-22T03:00',
      end: '2026-10-22T20:00',
      resource: 'orlando_1',
      title: 'Client consultation',
      name: 'Mary K. Adams',
      distance: 95,
      consumption: 30,
    },
    {
      id: 3,
      allDay: true,
      start: '2026-10-24T10:00',
      end: '2026-10-24T17:00',
      resource: 'orlando_1',
      title: 'Project review at XYZ',
      name: 'Robert B. Brown',
      distance: 150,
      consumption: 40,
    },
    {
      id: 4,
      allDay: true,
      start: '2026-10-26T09:00',
      end: '2026-10-26T16:00',
      resource: 'orlando_1',
      title: 'Team building workshop',
      name: 'Susan E. Smith',
      distance: 60,
      consumption: 20,
    },
    // Showing partial data. Download full source.
  ]);

  const [eventsWithSummaries, setEventsWithSummaries] = useState<MbscCalendarEvent[]>(myEvents);

  const firstViewDay = useRef<Date>(null);
  const lastViewDay = useRef<Date>(null);

  const myView = useMemo<MbscEventcalendarView>(
    () => ({
      timeline: {
        type: 'week',
        eventDisplay: 'fill',
        eventHeight: 'variable',
      },
    }),
    [],
  );

  const myResources = useMemo<MbscResource[]>(
    () => [
      {
        id: 'orlando',
        name: 'CNL Tower II',
        address: '420 S Orange Ave, Orlando, FL 32801',
        eventCreation: false,
        eventDragBetweenResources: false,
        eventDragInTime: false,
        eventResize: false,
        color: 'transparent',
        children: [
          {
            id: 'orlando_1',
            car: 'Lincoln Continental',
            plate: '51-5375',
            type: 'Sedan',
          },
          {
            id: 'orlando_2',
            car: 'Porsche Panamera',
            plate: '72-2590',
            type: 'Sports sedan',
          },
          {
            id: 'orlando_3',
            car: 'Range Rover Velar',
            plate: '41-7701',
            type: 'Compact SUV',
          },
        ],
      },
      // Showing partial data. Download full source.
    ],
    [],
  );

  const handleDefaultEvent = useCallback((args: MbscNewEventData): MbscCalendarEvent => {
    const d = args.start;
    const start = new Date(d.getFullYear(), d.getMonth(), d.getDate(), 9);
    const end = new Date(start.getFullYear(), start.getMonth(), start.getDate(), 18);
    return {
      allDay: true,
      start: start,
      end: end,
      title: 'New event',
      name: 'John Doe',
      distance: 42,
      consumption: 6,
    };
  }, []);

  const getAggregateEvents = useCallback(
    (dayEvents: MbscCalendarEvent[]): MbscCalendarEvent[] => {
      const aggregateEvents: { [key: string]: MbscCalendarEvent } = {};

      myResources.forEach((resource) => {
        for (let date = new Date(firstViewDay.current as Date); date < (lastViewDay.current as Date); date.setDate(date.getDate() + 1)) {
          aggregateEvents[(resource.id as string) + +date] = {
            id: (resource.id as string) + +date,
            isSummary: true,
            allDay: true,
            start: new Date(date),
            end: new Date(date),
            consumption: 0,
            distance: 0,
            resource: resource.id,
          };
        }
      });
      dayEvents.forEach((event) => {
        if (!event.isSummary) {
          const parentResource = (event.resource as string).split('_')[0];
          const dayStart = new Date(event.start as Date).setHours(0, 0, 0, 0);
          if (aggregateEvents[parentResource + dayStart]) {
            aggregateEvents[parentResource + dayStart].consumption += event.consumption;
            aggregateEvents[parentResource + dayStart].distance += event.distance;
          }
        }
      });

      return Object.values(aggregateEvents);
    },
    [firstViewDay, lastViewDay, myResources],
  );

  const updateCalendarEvents = useCallback(
    (inst: Eventcalendar) => {
      const dailyEvents = inst.getEvents();
      const updatedSummaries = getAggregateEvents(dailyEvents);
      setEventsWithSummaries([...myEvents, ...updatedSummaries]);
    },
    [getAggregateEvents, myEvents],
  );

  const handlePageLoading = useCallback(
    (args: MbscPageLoadingEvent, inst: Eventcalendar) => {
      firstViewDay.current = new Date(args.firstDay);
      lastViewDay.current = new Date(args.lastDay);
      setTimeout(() => updateCalendarEvents(inst));
    },
    [updateCalendarEvents],
  );

  const handleEventUpdated = useCallback(
    (args: MbscEventUpdatedEvent, inst: Eventcalendar) => {
      const updatedEvent = args.event;
      const index = myEvents.indexOf(updatedEvent);
      myEvents.splice(index, 1, updatedEvent);
      setTimeout(() => updateCalendarEvents(inst));
    },
    [myEvents, updateCalendarEvents],
  );

  const handleEventCreated = useCallback(
    (args: MbscEventCreatedEvent, inst: Eventcalendar) => {
      setEvents((prevEvents) => [...prevEvents, args.event]);
      setTimeout(() => updateCalendarEvents(inst));
    },
    [updateCalendarEvents],
  );

  const handleEventDeleted = useCallback(
    (args: MbscEventDeletedEvent, inst: Eventcalendar) => {
      setEvents((prevEvents) => prevEvents.filter((item) => item.id !== args.event.id));
      setTimeout(() => updateCalendarEvents(inst));
    },
    [updateCalendarEvents],
  );

  const customResource = useCallback((res: MbscResource) => {
    if (res.isParent) {
      return (
        <>
          <div>{res.name}</div>
          <div className="mds-aggregates-address">{res.address}</div>
        </>
      );
    }
    return (
      <>
        <div>{res.car}</div>
        <div className="mds-aggregates-plate">Plate {res.plate}</div>
        <div className="mds-aggregates-cartype">{res.type}</div>
      </>
    );
  }, []);

  const customScheduleEventContent = useCallback((event: MbscCalendarEventData) => {
    if (event.currentResource!.isParent) {
      return (
        <>
          <div className="mds-aggregates-summary-event mbsc-flex">
            <div className="mds-aggregates-summary-details mbsc-flex mbsc-flex-1-1">
              <div className="mds-aggregates-summary-label">
                Total dist.
                <div className="mds-aggregates-summary-value">{event.original!.distance} mi</div>
              </div>
              <div className="mds-aggregates-summary-label">
                Total cons.
                <div className="mds-aggregates-summary-value">{event.original!.consumption} gal</div>
              </div>
              <div className="mds-aggregates-summary-label">
                Est. cost
                <div className="mds-aggregates-summary-value">$ {Math.round(event.original!.consumption * 3.706 || 0)}</div>
              </div>
            </div>
          </div>
        </>
      );
    }
    return (
      <>
        <div className="mds-aggregates-event">
          <div className="mds-aggregates-title">{event.title}</div>
          <div className="mds-aggregates-name">{event.original!.name}</div>
          <div className="mds-aggregates-details">
            <div className="mds-aggregates-label">
              Est. distance:
              <span className="mds-aggregates-value">{event.original!.distance} mi</span>
            </div>
            <div className="mds-aggregates-label">
              Est. consumption:
              <span className="mds-aggregates-value">{event.original!.consumption} gal</span>
            </div>
          </div>
          <div className="mds-aggregates-label mbsc-flex">
            <div className="mbsc-flex-1-1">
              Pick up: <span className="mds-aggregates-value">{formatDate('hh:mm A', new Date(event.original!.start as Date))}</span>
            </div>
            <div>
              Drop off: <span className="mds-aggregates-value">{formatDate('hh:mm A', new Date(event.original!.end as Date))}</span>
            </div>
          </div>
        </div>
      </>
    );
  }, []);

  return (
    <Eventcalendar
      cssClass="mds-aggregates"
      clickToCreate={true}
      dragToCreate={false}
      dragToMove={true}
      dragToResize={false}
      eventOverlap={false}
      view={myView}
      data={eventsWithSummaries}
      resources={myResources}
      min={'2026-10-21T00:00'}
      max={'2026-11-03T00:00'}
      extendDefaultEvent={handleDefaultEvent}
      onPageLoading={handlePageLoading}
      onEventUpdated={handleEventUpdated}
      onEventCreated={handleEventCreated}
      onEventDeleted={handleEventDeleted}
      renderResource={customResource}
      renderTimelineEventContent={customScheduleEventContent}
    />
  );
}

export default App;
```

### CSS

```css
.mds-aggregates .mbsc-timeline-column,
.mds-aggregates .mbsc-timeline-header-column {
  width: 14.5em;
}

.mds-aggregates .mbsc-timeline-row-gutter {
  height: 6px;
}

.mds-aggregates .mbsc-timeline-resource-title {
  font-size: 14px;
  font-weight: 600;
}

.mds-aggregates .mbsc-ios .mbsc-schedule-event:after {
  display: none;
}

.mds-aggregates .mbsc-schedule-event-inner {
  height: auto;
  width: 100%;
}

.mbsc-windows-dark .mds-aggregates-summary-event,
.mbsc-ios-dark .mds-aggregates-summary-event,
.mbsc-material-dark .mds-aggregates-summary-event {
  color: #fff;
}

.mds-aggregates-address {
  padding: 8px 0;
  font-size: 11px;
  line-height: 16px;
  font-weight: 600;
}

.mds-aggregates-plate {
  padding: 8px 0;
  font-weight: 600;
}

.mds-aggregates-cartype {
  font-size: 11px;
  font-weight: 600;
}

.mds-aggregates-summary-event {
  color: #000;
}

.mds-aggregates-summary-details {
  white-space: normal;
  justify-content: space-between;
  padding: 8px;
}

.mds-aggregates-summary-label {
  font-size: 11px;
  font-weight: 400;
}

.mds-aggregates-summary-value {
  font-weight: 600;
  font-size: 15px;
  line-height: 24px;
}

.mds-aggregates-event {
  padding: 4px;
}

.mds-aggregates-title {
  white-space: normal;
  line-height: 20px;
  font-size: 14px;
  font-weight: 600;
}

.mds-aggregates-name {
  padding: 4px 0;
  font-size: 12px;
  line-height: 16px;
  font-weight: 600;
}

.mds-aggregates-value {
  font-weight: 600;
}

.mds-aggregates-label {
  font-size: 11px;
  font-weight: 400;
}

.mds-aggregates-details {
  white-space: normal;
  justify-content: space-between;
  padding: 8px 0;
}
```

## Implementation notes

- Use `timeline: { type: 'week', eventDisplay: 'fill', eventHeight: 'variable' }`. `eventHeight: 'variable'` is required because summary events on parent rows and regular events on child rows have different heights.
- Restrict the calendar to a 2-week window centered on today: `min: dyndatetime('y,m,d-7')`, `max: dyndatetime('y,m,d+6')`.
- Define 3 parent resource groups (by city), each with 3 child vehicle resources. Use city-prefixed IDs to establish parent-child relationships (e.g. `orlando`, `orlando_1`, `orlando_2`, `orlando_3`). Parent resources carry `name`, `address`, `color: 'transparent'`, and `eventCreation: false`, `eventDragBetweenResources: false`, `eventDragInTime: false`, `eventResize: false` - they display summary data only and cannot be booked directly. Child resources carry `car`, `plate`, and `type` as custom properties.
- Add ~66 regular events (all `allDay: true`), each assigned to a child resource. Every event carries two custom numeric properties: `distance` (miles) and `consumption` (gallons). Use `dyndatetime` offsets relative to today for dates.
- Enable `clickToCreate: true`, `dragToMove: true`. Set `dragToCreate: false`, `dragToResize: false`, `eventOverlap: false`. Provide `extendDefaultEvent` to supply defaults for newly created events (`title: 'New event'`, `name: 'John Doe'`, `distance: 42`, `consumption: 6`, `allDay: true`).
- **Summary (aggregate) events** - generated programmatically, not from user input:
  - For each visible day and each parent resource, create one summary event object: `{ id: parentId + dayTimestamp, isSummary: true, allDay: true, start: day, end: day, resource: parentId, distance: 0, consumption: 0 }`.
  - Iterate over all non-summary events; for each, derive the parent resource ID by splitting the child resource ID on `_` (e.g. `'orlando_1'.split('_')[0]` → `'orlando'`). Add the event's `distance` and `consumption` to the matching (parent, day) summary object.
  - Estimated cost is computed in the render layer: `Math.round(consumption * 3.706)`.
  - Merge the original events and the generated summaries into a single array and pass it to the calendar's `data` prop.
- **Refresh cycle**: Store original events in a mutable ref (not state). On `onPageLoading`, capture `args.firstDay` / `args.lastDay`, then call the aggregate recalculation in a `setTimeout`. On `onEventCreated`, `onEventUpdated`, and `onEventDeleted`, update the events ref and call the same recalculation.
- **Custom resource column** (`renderResource` / `resourceTemplate` / `resource` slot): check `res.isParent` - if true, render `name` + `address`; otherwise render `car`, licence plate, and vehicle type.
- **Custom event content** (`renderTimelineEventContent` / `timelineEventContentTemplate` / `timeline-event-content` slot): check `event.currentResource.isParent` - if true (summary row), render "Total dist.", "Total cons.", "Est. cost" aggregates; otherwise render the booking title, driver name, estimated distance/consumption, and formatted pick-up / drop-off times using `formatDate('hh:mm A', ...)`.

## 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: [Resource Grouping & Hierarchy](https://demo.mobiscroll.com/react/timeline/resource-grouping-hierarchy.md) - Build a React resource timeline with collapsible nested groups, pinned resources, and per-group event creation controls.
- Next: [Resource Drag & Drop Reorder](https://demo.mobiscroll.com/react/timeline/resource-drag-drop-reorder.md) - Reorder resources on the React Timeline by drag and drop — each row shows a drag handle, and an update callback runs on drop.
