Resource Group Aggregate Summaries
Usage
theme: 'ios' You can turn auto theming on and let it decide based on the platform.
Usage
themeVariant: 'light' You can turn auto dark mode on based on system settings.
Usage
clickToCreate: true,
// click to create is on dragToCreate: true,
// drag to create is on dragToMove: true,
// reordering is on dragToResize: true,
// resizing is on eventDelete: true
// deleting is on Fine-tune drag & drop with these options, which are false by default.
Usage
locale: localeEn Selecting the locale loads the localized settings and the translated UI text.
Resource Group Aggregate Summaries
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 event-created, event-update and event-deleted 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. This is necessary if the actual booked events have a different height than the summary events (for parent resources).
Demo

Code
View code on GitHub
<script setup>
import { formatDate, MbscEventcalendar, setOptions } from '@mobiscroll/vue'
import { ref } from 'vue'
setOptions({
locale: ,
theme: ' ',
themeVariant: ' '
})
let firstViewDay = null
let lastViewDay = null
const myEvents = ref([
{
id: 1,
allDay: true,
start: '2026-10-19T06:00',
end: '2026-10-19T19:00',
resource: 'orlando_1',
title: 'Sales meeting with JLL',
name: 'John C. Johnson',
distance: 120,
consumption: 35
},
{
id: 2,
allDay: true,
start: '2026-10-20T03:00',
end: '2026-10-20T20:00',
resource: 'orlando_1',
title: 'Client consultation',
name: 'Mary K. Adams',
distance: 95,
consumption: 30
},
{
id: 3,
allDay: true,
start: '2026-10-22T10:00',
end: '2026-10-22T17:00',
resource: 'orlando_1',
title: 'Project review at XYZ',
name: 'Robert B. Brown',
distance: 150,
consumption: 40
},
{
id: 4,
allDay: true,
start: '2026-10-24T09:00',
end: '2026-10-24T16:00',
resource: 'orlando_1',
title: 'Team building workshop',
name: 'Susan E. Smith',
distance: 60,
consumption: 20
},
// Showing partial data. Download full source.
])
const myView = {
timeline: {
type: 'week',
eventDisplay: 'fill',
eventHeight: 'variable'
}
}
const myResources = [
{
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 eventsWithSummaries = ref(myEvents.value)
function getAggregateEvents(dayEvents) {
const aggregateEvents = {}
myResources.forEach((resource) => {
for (let date = new Date(firstViewDay); date < lastViewDay; 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)
}
function updateCalendarEvents(inst) {
const dailyEvents = inst.getEvents()
const updatedSummaries = getAggregateEvents(dailyEvents)
eventsWithSummaries.value = [...myEvents.value, ...updatedSummaries]
}
function handleDefaultEvent(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
}
}
function handlePageLoading(args, inst) {
firstViewDay = new Date(args.firstDay)
lastViewDay = new Date(args.lastDay)
updateCalendarEvents(inst)
}
function handleEventUpdated(args, inst) {
updateCalendarEvents(inst)
}
function handleEventCreated(args, inst) {
const newEvent = args.event
myEvents.value.push(newEvent)
updateCalendarEvents(inst)
}
function handleEventDeleted(args, inst) {
const deletedEvent = args.event
myEvents.value = myEvents.value.filter((e) => deletedEvent.id !== e.id)
updateCalendarEvents(inst)
}
</script>
<template>
<MbscEventcalendar
cssClass="mds-aggregates"
:min="dyndatetime('y,m,d-7')"
:max="dyndatetime('y,m,d+6')"
:clickToCreate="true"
:data="eventsWithSummaries"
:dragToCreate="false"
:dragToMove="true"
:dragToResize="false"
:eventOverlap="false"
:extendDefaultEvent="handleDefaultEvent"
:resources="myResources"
:view="myView"
@event-created="handleEventCreated"
@event-deleted="handleEventDeleted"
@event-updated="handleEventUpdated"
@page-loading="handlePageLoading"
>
<template #resource="resource">
<template v-if="resource.isParent">
<div>{{ resource.name }}</div>
<div class="mds-aggregates-address">{{ resource.address }}</div>
</template>
<template v-else>
<div>{{ resource.car }}</div>
<div class="mds-aggregates-plate">Plate {{ resource.plate }}</div>
<div class="mds-aggregates-cartype">{{ resource.type }}</div>
</template>
</template>
<template #timelineEventContent="event">
<template v-if="event.currentResource.isParent">
<div class="mds-aggregates-summary-event mbsc-flex">
<div class="mds-aggregates-summary-details mbsc-flex mbsc-flex-1-1">
<div class="mds-aggregates-summary-label">
Total dist.
<div class="mds-aggregates-summary-value">{{ event.original.distance }} mi</div>
</div>
<div class="mds-aggregates-summary-label">
Total cons.
<div class="mds-aggregates-summary-value">{{ event.original.consumption }} gal</div>
</div>
<div class="mds-aggregates-summary-label">
Est. cost
<div class="mds-aggregates-summary-value">
$
{{ Math.round(event.original.consumption * 3.706 || 0) }}
</div>
</div>
</div>
</div>
</template>
<template v-else>
<div class="mds-aggregates-event">
<div class="mds-aggregates-title">
{{ event.title }}
</div>
<div class="mds-aggregates-name">
{{ event.original.name }}
</div>
<div class="mds-aggregates-details">
<div class="mds-aggregates-label">
Est. distance:
<span class="mds-aggregates-value"> {{ event.original.distance }} mi </span>
</div>
<div class="mds-aggregates-label">
Est. consumption:
<span class="mds-aggregates-value">{{ event.original.consumption }} gal </span>
</div>
</div>
<div class="mds-aggregates-label mbsc-flex">
<div class="mbsc-flex-1-1">
Pick up:
<span class="mds-aggregates-value">
{{ formatDate('hh:mm A', new Date(event.original.start)) }}
</span>
</div>
<div>
Drop off:
<span class="mds-aggregates-value">
{{ formatDate('hh:mm A', new Date(event.original.end)) }}
</span>
</div>
</div>
</div>
</template>
</template>
</MbscEventcalendar>
</template>
<style>
.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;
}
</style> <script setup lang="ts">
import { formatDate, MbscEventcalendar, setOptions } from '@mobiscroll/vue'
import type {
MbscCalendarEvent,
MbscEventcalendarView,
MbscEventCreatedEvent,
MbscEventDeletedEvent,
MbscEventUpdatedEvent,
MbscNewEventData,
MbscPageLoadingEvent,
MbscResource
} from '@mobiscroll/vue'
import { ref } from 'vue'
setOptions({
locale: ,
theme: ' ',
themeVariant: ' '
})
let firstViewDay: Date = new Date()
let lastViewDay: Date = new Date()
const myEvents = ref<MbscCalendarEvent[]>([
{
id: 1,
allDay: true,
start: '2026-10-19T06:00',
end: '2026-10-19T19:00',
resource: 'orlando_1',
title: 'Sales meeting with JLL',
name: 'John C. Johnson',
distance: 120,
consumption: 35
},
{
id: 2,
allDay: true,
start: '2026-10-20T03:00',
end: '2026-10-20T20:00',
resource: 'orlando_1',
title: 'Client consultation',
name: 'Mary K. Adams',
distance: 95,
consumption: 30
},
{
id: 3,
allDay: true,
start: '2026-10-22T10:00',
end: '2026-10-22T17:00',
resource: 'orlando_1',
title: 'Project review at XYZ',
name: 'Robert B. Brown',
distance: 150,
consumption: 40
},
{
id: 4,
allDay: true,
start: '2026-10-24T09:00',
end: '2026-10-24T16:00',
resource: 'orlando_1',
title: 'Team building workshop',
name: 'Susan E. Smith',
distance: 60,
consumption: 20
},
// Showing partial data. Download full source.
])
const myView: MbscEventcalendarView = {
timeline: {
type: 'week',
eventDisplay: 'fill',
eventHeight: 'variable'
}
}
const myResources: 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 eventsWithSummaries = ref<MbscCalendarEvent[]>(myEvents.value)
function getAggregateEvents(dayEvents: MbscCalendarEvent[]): MbscCalendarEvent[] {
const aggregateEvents: { [key: string]: MbscCalendarEvent } = {}
myResources.forEach((resource) => {
for (let date = new Date(firstViewDay); date < lastViewDay; 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)
}
function updateCalendarEvents(inst: any) {
const dailyEvents = inst.getEvents()
const updatedSummaries = getAggregateEvents(dailyEvents)
eventsWithSummaries.value = [...myEvents.value, ...updatedSummaries]
}
function handleDefaultEvent(args: MbscNewEventData) {
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
}
}
function handlePageLoading(args: MbscPageLoadingEvent, inst: any): void {
firstViewDay = new Date(args.firstDay)
lastViewDay = new Date(args.lastDay)
updateCalendarEvents(inst)
}
function handleEventUpdated(args: MbscEventUpdatedEvent, inst: any): void {
updateCalendarEvents(inst)
}
function handleEventCreated(args: MbscEventCreatedEvent, inst: any): void {
const newEvent = args.event
myEvents.value.push(newEvent)
updateCalendarEvents(inst)
}
function handleEventDeleted(args: MbscEventDeletedEvent, inst: any): void {
const deletedEvent = args.event
myEvents.value = myEvents.value.filter((e) => deletedEvent.id !== e.id)
updateCalendarEvents(inst)
}
</script>
<template>
<MbscEventcalendar
cssClass="mds-aggregates"
:min="dyndatetime('y,m,d-7')"
:max="dyndatetime('y,m,d+6')"
:clickToCreate="true"
:data="eventsWithSummaries"
:dragToCreate="false"
:dragToMove="true"
:dragToResize="false"
:eventOverlap="false"
:extendDefaultEvent="handleDefaultEvent"
:resources="myResources"
:view="myView"
@event-created="handleEventCreated"
@event-deleted="handleEventDeleted"
@event-updated="handleEventUpdated"
@page-loading="handlePageLoading"
>
<template #resource="resource">
<template v-if="resource.isParent">
<div>{{ resource.name }}</div>
<div class="mds-aggregates-address">{{ resource.address }}</div>
</template>
<template v-else>
<div>{{ resource.car }}</div>
<div class="mds-aggregates-plate">Plate {{ resource.plate }}</div>
<div class="mds-aggregates-cartype">{{ resource.type }}</div>
</template>
</template>
<template #timelineEventContent="event">
<template v-if="event.currentResource.isParent">
<div class="mds-aggregates-summary-event mbsc-flex">
<div class="mds-aggregates-summary-details mbsc-flex mbsc-flex-1-1">
<div class="mds-aggregates-summary-label">
Total dist.
<div class="mds-aggregates-summary-value">{{ event.original.distance }} mi</div>
</div>
<div class="mds-aggregates-summary-label">
Total cons.
<div class="mds-aggregates-summary-value">{{ event.original.consumption }} gal</div>
</div>
<div class="mds-aggregates-summary-label">
Est. cost
<div class="mds-aggregates-summary-value">
$
{{ Math.round(event.original.consumption * 3.706 || 0) }}
</div>
</div>
</div>
</div>
</template>
<template v-else>
<div class="mds-aggregates-event">
<div class="mds-aggregates-title">
{{ event.title }}
</div>
<div class="mds-aggregates-name">
{{ event.original.name }}
</div>
<div class="mds-aggregates-details">
<div class="mds-aggregates-label">
Est. distance:
<span class="mds-aggregates-value"> {{ event.original.distance }} mi </span>
</div>
<div class="mds-aggregates-label">
Est. consumption:
<span class="mds-aggregates-value">{{ event.original.consumption }} gal </span>
</div>
</div>
<div class="mds-aggregates-label mbsc-flex">
<div class="mbsc-flex-1-1">
Pick up:
<span class="mds-aggregates-value">
{{ formatDate('hh:mm A', new Date(event.original.start)) }}
</span>
</div>
<div>
Drop off:
<span class="mds-aggregates-value">
{{ formatDate('hh:mm A', new Date(event.original.end)) }}
</span>
</div>
</div>
</div>
</template>
</template>
</MbscEventcalendar>
</template>
<style>
.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;
}
</style>