---
title: "Vue Timeline Resource Footers & Revenue Sidebar Example | Mobiscroll"
component: "Timeline"
framework: "Vue"
library_version: "6.1.5"
url: "https://demo.mobiscroll.com/vue/timeline/timeline-resource-details-side-panel-footer"
---

# Resource Footers & Revenue Sidebar - Vue Timeline demo

Add custom resource column footers, a revenue sidebar, and per-day occupancy to a Vue resource timeline — all column headers are sortable. For Vue and Nuxt.

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

## Description

The resources can be rendered vertically either as plain text or based on a custom grid template. This example utilizes the properties of the resource objects, which are conference rooms with capacities and pricing. Along with the resource template defined using [`resource`](https://mobiscroll.com/docs/vue/eventcalendar/api#slot-resource) slot , an additional sidebar is rendered on the opposite side of the row using [`sidebar` template](https://mobiscroll.com/docs/vue/eventcalendar/api#slot-sidebar) , displaying the calculated revenue.

Both the resource and sidebar columns have customizable headers and footers, which are set using the [`resourceHeader`,](https://mobiscroll.com/docs/vue/eventcalendar/api#slot-resourceHeader)[`resourceFooter`,](https://mobiscroll.com/docs/vue/eventcalendar/api#slot-resourceFooter)[`sidebarHeader`,](https://mobiscroll.com/docs/vue/eventcalendar/api#slot-sidebarHeader)[`sidebarFooter`](https://mobiscroll.com/docs/vue/eventcalendar/api#slot-sidebarFooter) templates.

The occupancy percentages that can be seen for each day are set through the [`dayFooter`](https://mobiscroll.com/docs/vue/eventcalendar/api#slot-dayFooter) template in a similar way to the [day header templates](https://demo.mobiscroll.com/vue/scheduler/custom-resource-header-template).

The headers are equipped with sorting functionality, allowing data to be sorted on click. Event listeners are attached to the resource, day, and sidebar headers, making them clickable. Each header has a custom icon indicating the current sorting state (ascending, descending, or unordered). Clicking a header cycles through these sorting states, with the corresponding icon reflecting the current state. The [initial order](https://mobiscroll.com/docs/vue/eventcalendar/timeline#resource-order) in which the resources appear follows the order of the array passed to the component.

The resources column has a default width that may not be sufficient to fit its content. This can be adjusted by overriding it with a CSS rule:

```css
.md-resource-details .mbsc-timeline-resource-col {
            width: 280px;
            }
```

## What this demo shows

- A desktop month timeline where days are arranged horizontally and conference room resources are listed as rows.
- **Header navigation** The month and year label opens date navigation, while the previous and next arrows move between months and the Today button returns to the current date.
- **Month view** The day strip below the header shows the selected month, with the current date highlighted.
- **Resources** Each room is shown as a separate timeline row with custom resource details for room name, capacity, and price per day.
- **Sidebar revenue** A right-side sidebar column displays the calculated revenue for each resource over the visible month.
- **Resource summary** The resource footer shows an occupancy label, and the sidebar footer shows total revenue across all resources.
- **Day footer** Each day displays an occupancy percentage based on how many resources have bookings on that date.
- **Sorting** Resource, day, and sidebar headers are clickable and cycle through ascending, descending, and default sorting states.
- **Sorting indicators** Header icons show the current sorting state for each sortable column or day.
- **Event cards** Events are shown as colored cards with a left color stripe, a bold title, and the start and end time of the booking.
- **Date positioning** Events are positioned by assigned resource and exact date range.
- **Event interaction** Events highlight on hover and show drag and resize handles for moving bookings 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 date range.
- **Live summaries** Revenue, occupancy, and sorting data are recalculated when events are added, updated, deleted, or when the visible month changes.
- **Scrolling** The timeline supports horizontal and vertical scrolling for navigating through dates and resources.

## Best for

- **Venue booking** Showing room availability, capacity, pricing, and revenue in the same timeline view.
- **Hospitality timeline** Managing conference rooms, meeting spaces, or rentable areas with occupancy and revenue summaries.
- **Facility management** Comparing resource usage across a month while keeping operational details visible beside the timeline.
- **Coworking spaces** Tracking bookings for rooms or shared spaces with capacity, price, and utilization data.
- **Equipment rental** Scheduling rentable resources where availability, daily pricing, and revenue need to be reviewed together.
- **Operational reporting** Combining booking data with business metrics such as occupancy and revenue without leaving the timeline.

## API used in this demo

- [`theme`](https://mobiscroll.com/docs/vue/eventcalendar/api#opt-theme) - option
- [`themeVariant`](https://mobiscroll.com/docs/vue/eventcalendar/api#opt-themeVariant) - option
- [`getEvents`](https://mobiscroll.com/docs/vue/eventcalendar/api#method-getEvents) - method
- [`clickToCreate`](https://mobiscroll.com/docs/vue/eventcalendar/api#opt-clickToCreate) - option
- [`data`](https://mobiscroll.com/docs/vue/eventcalendar/api#opt-data) - option
- [`dragToCreate`](https://mobiscroll.com/docs/vue/eventcalendar/api#opt-dragToCreate) - option
- [`dragToMove`](https://mobiscroll.com/docs/vue/eventcalendar/api#opt-dragToMove) - option
- [`dragToResize`](https://mobiscroll.com/docs/vue/eventcalendar/api#opt-dragToResize) - option
- [`resources`](https://mobiscroll.com/docs/vue/eventcalendar/api#opt-resources) - option
- [`view`](https://mobiscroll.com/docs/vue/eventcalendar/api#opt-view) - option
- [`onPageLoading`](https://mobiscroll.com/docs/vue/eventcalendar/api#event-onPageLoading) - event
- [`onEventCreated`](https://mobiscroll.com/docs/vue/eventcalendar/api#event-onEventCreated) - event
- [`onEventDeleted`](https://mobiscroll.com/docs/vue/eventcalendar/api#event-onEventDeleted) - event
- [`onEventUpdated`](https://mobiscroll.com/docs/vue/eventcalendar/api#event-onEventUpdated) - event

## Code

### SFC JS

```vue
<script setup>
import {
  formatDate,
  getJson,
  MbscEventcalendar,
  setOptions
} from '@mobiscroll/vue'
import { onMounted, ref } from 'vue'

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

const calRef = ref(null)
const myEvents = ref()
const loadedEvents = ref([])
const sortColumn = ref('')
const sortDirection = ref('')
const sortDay = ref()
const totalRevenue = ref(0)

const myResources = ref([
  { id: 1, name: 'Horizon', seats: 1200, color: '#4a4a4a', price: 1000, revenue: 0 },
  { id: 2, name: 'Apex Hall', seats: 90, color: '#fdf500', price: 600, revenue: 0 },
  { id: 3, name: 'Jade Room', seats: 700, color: '#00aaff', price: 900, revenue: 0 },
  { id: 4, name: 'Dome Arena', seats: 850, color: '#239a21', price: 750, revenue: 0 },
  { id: 5, name: 'Forum Plaza', seats: 900, color: '#8f1ed6', price: 700, revenue: 0 },
  { id: 6, name: 'Gallery', seats: 300, color: '#0077b6', price: 650, revenue: 0 },
  { id: 7, name: 'Icon Hall', seats: 450, color: '#e63946', price: 850, revenue: 0 },
  { id: 8, name: 'Broadway', seats: 250, color: '#ff0101', price: 800, revenue: 0 },
  { id: 9, name: 'Central Hub', seats: 400, color: '#01adff', price: 1100, revenue: 0 },
  { id: 10, name: 'Empire Hall', seats: 550, color: '#ff4600', price: 950, revenue: 0 }
])

const myView = {
  timeline: {
    type: 'month'
  }
}

function getUTCDateOnly(d) {
  return Date.UTC(d.getFullYear(), d.getMonth(), d.getDate())
}

function getDayDiff(d1, d2) {
  return Math.round((getUTCDateOnly(d2) - getUTCDateOnly(d1)) / (60000 * 60 * 24)) + 1
}

function getRevenue(resource) {
  let days = 0
  for (const event of loadedEvents.value) {
    if (event.resource === resource.id) {
      days += getDayDiff(new Date(event.start), new Date(event.end))
    }
  }
  return days * resource.price
}

function getOccuppancy(events) {
  let occuppancy = 0
  if (events) {
    const resourceIds = []
    let nr = 0
    for (const event of events) {
      if (resourceIds.indexOf(event.resource) < 0) {
        nr++
        resourceIds.push(event.resource)
      }
    }
    occuppancy = (nr * 100) / myResources.value.length
  }
  return occuppancy.toFixed(0)
}

function getSortArrow(column, day = undefined) {
  if (sortColumn.value === column && day === sortDay.value) {
    return sortDirection.value === 'asc' ? 'asc' : sortDirection.value === 'desc' ? 'desc' : 'def'
  }
  return 'def'
}

function getBusyHours(resource, timestamp) {
  const startOfDay = new Date(timestamp)
  const endOfDay = new Date(
    startOfDay.getFullYear(),
    startOfDay.getMonth(),
    startOfDay.getDate() + 1
  )
  return loadedEvents.value.reduce((totalHours, event) => {
    if (event.resource === resource.id) {
      const eventStart = Math.max(+startOfDay, +new Date(event.start))
      const eventEnd = Math.min(+endOfDay, +new Date(event.end))
      return totalHours + (eventStart < eventEnd ? (eventEnd - eventStart) / (60 * 60 * 1000) : 0)
    }
    return totalHours
  }, 0)
}

function sortResources(column, day) {
  if (column) {
    if (sortColumn.value === column && day === sortDay.value) {
      sortDirection.value =
        sortDirection.value === 'asc' ? 'desc' : sortDirection.value === 'desc' ? 'def' : 'asc'
    } else {
      sortColumn.value = column
      sortDirection.value = 'asc'
    }
    sortDay.value = day
  }

  if (sortDay.value) {
    // Precalculate busy hours for the clicked day
    myResources.value.forEach((resource) => {
      resource.busyHours = getBusyHours(resource, sortDay.value)
    })
  }

  myResources.value = [
    ...myResources.value.sort((a, b) => {
      const col = sortColumn.value
      if (sortDirection.value === 'asc') {
        return a[col] > b[col] ? 1 : -1
      }
      if (sortDirection.value === 'desc') {
        return a[col] < b[col] ? 1 : -1
      }
      return +a.id - +b.id
    })
  ]
}

function refreshData() {
  setTimeout(() => {
    loadedEvents.value = calRef.value.instance.getEvents()

    myResources.value.forEach((resource) => {
      resource.revenue = getRevenue(resource)
    })

    totalRevenue.value = myResources.value.reduce((total, resource) => total + (resource.revenue || 0), 0)

    sortResources()
  })
}

onMounted(() => {
  getJson(
    'https://trial.mobiscroll.com/multiday-events/',
    (events) => {
      myEvents.value = events
      refreshData()
    },
    'jsonp'
  )
})
</script>

<template>
  <MbscEventcalendar
    className="mds-resource-details"
    ref="calRef"
    :clickToCreate="true"
    :data="myEvents"
    :dragToCreate="true"
    :dragToMove="true"
    :dragToResize="true"
    :resources="myResources"
    :view="myView"
    :onPageLoading="refreshData"
    :onEventCreated="refreshData"
    :onEventDeleted="refreshData"
    :onEventUpdated="refreshData"
  >
    <template #resourceHeader>
      <div
        :class="
          'mds-resource-sort-header mds-resource-cell mds-resource-cell-name mds-resource-sort-' +
          getSortArrow('name')
        "
        @click="sortResources('name')"
      >
        Room
      </div>
      <div
        :class="
          'mds-resource-sort-header mds-resource-cell mds-resource-cell-seats mds-resource-sort-' +
          getSortArrow('seats')
        "
        @click="sortResources('seats')"
      >
        Capacity
      </div>
      <div
        :class="
          'mds-resource-sort-header mds-resource-cell mds-resource-cell-price mds-resource-sort-' +
          getSortArrow('price')
        "
        @click="sortResources('price')"
      >
        Price/day
      </div>
    </template>

    <template #resource="resource">
      <div class="mds-resource-cell mds-resource-cell-name">{{ resource.name }}</div>
      <div class="mds-resource-cell mds-resource-cell-seats">
        {{ resource.seats + ' seats' }}
      </div>
      <div class="mds-resource-cell mds-resource-cell-price">{{ '$' + resource.price }}</div>
    </template>

    <template #sidebar="resource">
      <div class="mds-sidebar-cell">${{ resource.revenue }}</div>
    </template>

    <template #resourceFooter>
      <div class="mds-resource-details-footer mds-resource-details-occuppancy">Occuppancy</div>
    </template>

    <template #sidebarHeader>
      <div
        :class="'mds-resource-sort-header mds-resource-sort-' + getSortArrow('revenue')"
        @click="sortResources('revenue')"
      >
        Revenue
      </div>
    </template>

    <template #timelineDay="data">
      <div
        :class="
          'mds-resource-sort-header mds-resource-sort-' +
          getSortArrow('busyHours', data.date.getTime())
        "
        @click="sortResources('busyHours', data.date.getTime())"
      >
        <span>{{ formatDate('D DDD', data.date) }}</span>
      </div>
    </template>

    <template #dayFooter="data">
      <div class="mds-resource-details-footer mds-resource-details-footer-day">
        {{ getOccuppancy(data.events) }}%
      </div>
    </template>

    <template #sidebarFooter>
      <div class="mds-resource-details-footer mds-resource-details-total">${{ totalRevenue }}</div>
    </template>
  </MbscEventcalendar>
</template>

<style>
/* Overrides */

.mds-resource-details .mbsc-timeline-resource-header,
.mds-resource-details .mbsc-timeline-resource-title,
.mds-resource-details .mbsc-timeline-resource-footer,
.mds-resource-details .mbsc-timeline-sidebar-header,
.mds-resource-details .mbsc-timeline-sidebar-resource-title,
.mds-resource-details .mbsc-timeline-sidebar-footer {
  padding: 0;
}

.mds-resource-details .mbsc-timeline-resource-col {
  width: 335px;
}

.mds-resource-details .mbsc-timeline-sidebar-col {
  width: 98px;
}

.mds-resource-details .mbsc-timeline-day {
  width: 144px;
}

.mds-resource-details .mbsc-timeline-resource-title {
  height: 100%;
}

/* Resource grid */

.mds-resource-cell {
  display: inline-block;
  height: 100%;
  padding: 0 5px;
  box-sizing: border-box;
  vertical-align: top;
  line-height: 50px;
}

.mds-resource-cell-name {
  width: 120px;
}

.mds-resource-cell-seats,
.mds-resource-cell-price {
  width: 107px;
}

.mds-resource-cell-seats {
  border-left: 1px solid #ccc;
  border-right: 1px solid #ccc;
}

.mds-resource-details.mbsc-ios-dark .mds-resource-cell-seats,
.mds-resource-details.mbsc-material-dark .mds-resource-cell-seats,
.mds-resource-details.mbsc-windows-dark .mds-resource-cell-seats {
  border-left: 1px solid #333;
  border-right: 1px solid #333;
}

.mds-sidebar-cell {
  padding: 0 5px;
  line-height: 50px;
  text-align: center;
}

/* Sort arrows */

.mds-resource-sort-header {
  cursor: pointer;
  position: relative;
  line-height: 25px;
  padding: 0 5px;
  font-size: 14px;
}

.mds-resource-sort-header::after {
  position: absolute;
  opacity: 0.5;
  right: 8px;
}

.mds-resource-sort-header:hover::after {
  opacity: 1;
}

.mds-resource-sort-asc::after {
  content: '↑';
}

.mds-resource-sort-desc::after {
  content: '↓';
}

.mds-resource-sort-def::after {
  content: '‹›';
  right: 5px;
  top: 12px;
  transform: translateY(-50%) rotate(90deg);
}

/* Footer */

.mds-resource-details-footer {
  line-height: 50px;
  background: rgba(150, 150, 150, 0.1);
}

.mds-resource-details-footer-day {
  font-size: 15px;
  font-weight: 600;
  text-align: center;
  padding: 0 5px;
}

.mds-resource-details-occuppancy {
  font-size: 15px;
  text-align: end;
  text-align: right;
  padding: 0 15px;
}

.mds-resource-details-total {
  font-size: 18px;
  padding: 0 5px;
  color: #8c0000;
  text-align: center;
}
</style>
```

### SFC TS

```vue
<script setup lang="ts">
import {
  formatDate,
  getJson,
  MbscEventcalendar,
  setOptions
} from '@mobiscroll/vue'
import type { MbscCalendarEvent, MbscEventcalendarView, MbscResource } from '@mobiscroll/vue'
import { onMounted, ref } from 'vue'

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

interface MyResource extends MbscResource {
  busyHours?: number
  price: number
  revenue?: number
  seats: number
}

const calRef = ref<typeof MbscEventcalendar>()
const myEvents = ref<MbscCalendarEvent[]>()
const loadedEvents = ref<MbscCalendarEvent[]>([])
const sortColumn = ref<string>('')
const sortDirection = ref<string>('')
const sortDay = ref<number>()
const totalRevenue = ref<number>(0)

const myResources = ref<MyResource[]>([
  { id: 1, name: 'Horizon', seats: 1200, color: '#4a4a4a', price: 1000, revenue: 0 },
  { id: 2, name: 'Apex Hall', seats: 90, color: '#fdf500', price: 600, revenue: 0 },
  { id: 3, name: 'Jade Room', seats: 700, color: '#00aaff', price: 900, revenue: 0 },
  { id: 4, name: 'Dome Arena', seats: 850, color: '#239a21', price: 750, revenue: 0 },
  { id: 5, name: 'Forum Plaza', seats: 900, color: '#8f1ed6', price: 700, revenue: 0 },
  { id: 6, name: 'Gallery', seats: 300, color: '#0077b6', price: 650, revenue: 0 },
  { id: 7, name: 'Icon Hall', seats: 450, color: '#e63946', price: 850, revenue: 0 },
  { id: 8, name: 'Broadway', seats: 250, color: '#ff0101', price: 800, revenue: 0 },
  { id: 9, name: 'Central Hub', seats: 400, color: '#01adff', price: 1100, revenue: 0 },
  { id: 10, name: 'Empire Hall', seats: 550, color: '#ff4600', price: 950, revenue: 0 }
])

const myView: MbscEventcalendarView = {
  timeline: {
    type: 'month'
  }
}

function getUTCDateOnly(d: Date) {
  return Date.UTC(d.getFullYear(), d.getMonth(), d.getDate())
}

function getDayDiff(d1: Date, d2: Date) {
  return Math.round((getUTCDateOnly(d2) - getUTCDateOnly(d1)) / (60000 * 60 * 24)) + 1
}

function getRevenue(resource: MyResource) {
  let days = 0
  for (const event of loadedEvents.value) {
    if (event.resource === resource.id) {
      days += getDayDiff(new Date(event.start as Date), new Date(event.end as Date))
    }
  }
  return days * resource.price
}

function getOccuppancy(events: MbscCalendarEvent[]) {
  let occuppancy = 0
  if (events) {
    const resourceIds: string[] = []
    let nr = 0
    for (const event of events) {
      if (resourceIds.indexOf(event.resource as string) < 0) {
        nr++
        resourceIds.push(event.resource as string)
      }
    }
    occuppancy = (nr * 100) / myResources.value.length
  }
  return occuppancy.toFixed(0)
}

function getSortArrow(column: string, day?: undefined) {
  if (sortColumn.value === column && day === sortDay.value) {
    return sortDirection.value === 'asc' ? 'asc' : sortDirection.value === 'desc' ? 'desc' : 'def'
  }
  return 'def'
}

function getBusyHours(resource: MyResource, timestamp: number) {
  const startOfDay = new Date(timestamp)
  const endOfDay = new Date(
    startOfDay.getFullYear(),
    startOfDay.getMonth(),
    startOfDay.getDate() + 1
  )
  return loadedEvents.value.reduce((totalHours, event) => {
    if (event.resource === resource.id) {
      const eventStart = Math.max(+startOfDay, +new Date(event.start as Date))
      const eventEnd = Math.min(+endOfDay, +new Date(event.end as Date))
      return totalHours + (eventStart < eventEnd ? (eventEnd - eventStart) / (60 * 60 * 1000) : 0)
    }
    return totalHours
  }, 0)
}

function sortResources(column?: string, day?: number) {
  if (column) {
    if (sortColumn.value === column && day === sortDay.value) {
      sortDirection.value =
        sortDirection.value === 'asc' ? 'desc' : sortDirection.value === 'desc' ? 'def' : 'asc'
    } else {
      sortColumn.value = column
      sortDirection.value = 'asc'
    }
    sortDay.value = day
  }

  if (sortDay.value) {
    // Precalculate busy hours for the clicked day
    myResources.value.forEach((resource) => {
      resource.busyHours = getBusyHours(resource, sortDay.value!)
    })
  }

  myResources.value = [
    ...myResources.value.sort((a, b) => {
      const col = sortColumn.value as keyof typeof a
      if (sortDirection.value === 'asc') {
        return a[col] > b[col] ? 1 : -1
      }
      if (sortDirection.value === 'desc') {
        return a[col] < b[col] ? 1 : -1
      }
      return +a.id - +b.id
    })
  ]
}

function refreshData() {
  setTimeout(() => {
    loadedEvents.value = calRef.value!.instance.getEvents()

    myResources.value.forEach((resource) => {
      resource.revenue = getRevenue(resource)
    })

    totalRevenue.value = myResources.value.reduce(
      (total, resource) => total + (resource.revenue || 0),
      0
    )

    sortResources()
  })
}

onMounted(() => {
  getJson(
    'https://trial.mobiscroll.com/multiday-events/',
    (events: MbscCalendarEvent[]) => {
      myEvents.value = events
      refreshData()
    },
    'jsonp'
  )
})
</script>

<template>
  <MbscEventcalendar
    className="mds-resource-details"
    ref="calRef"
    :clickToCreate="true"
    :data="myEvents"
    :dragToCreate="true"
    :dragToMove="true"
    :dragToResize="true"
    :resources="myResources"
    :view="myView"
    :onPageLoading="refreshData"
    :onEventCreated="refreshData"
    :onEventDeleted="refreshData"
    :onEventUpdated="refreshData"
  >
    <template #resourceHeader>
      <div
        :class="
          'mds-resource-sort-header mds-resource-cell mds-resource-cell-name mds-resource-sort-' +
          getSortArrow('name')
        "
        @click="sortResources('name')"
      >
        Room
      </div>
      <div
        :class="
          'mds-resource-sort-header mds-resource-cell mds-resource-cell-seats mds-resource-sort-' +
          getSortArrow('seats')
        "
        @click="sortResources('seats')"
      >
        Capacity
      </div>
      <div
        :class="
          'mds-resource-sort-header mds-resource-cell mds-resource-cell-price mds-resource-sort-' +
          getSortArrow('price')
        "
        @click="sortResources('price')"
      >
        Price/day
      </div>
    </template>

    <template #resource="resource">
      <div class="mds-resource-cell mds-resource-cell-name">{{ resource.name }}</div>
      <div class="mds-resource-cell mds-resource-cell-seats">
        {{ resource.seats + ' seats' }}
      </div>
      <div class="mds-resource-cell mds-resource-cell-price">{{ '$' + resource.price }}</div>
    </template>

    <template #sidebar="resource">
      <div class="mds-sidebar-cell">${{ resource.revenue }}</div>
    </template>

    <template #resourceFooter>
      <div class="mds-resource-details-footer mds-resource-details-occuppancy">Occuppancy</div>
    </template>

    <template #sidebarHeader>
      <div
        :class="'mds-resource-sort-header mds-resource-sort-' + getSortArrow('revenue')"
        @click="sortResources('revenue')"
      >
        Revenue
      </div>
    </template>

    <template #timelineDay="data">
      <div
        :class="
          'mds-resource-sort-header mds-resource-sort-' +
          getSortArrow('busyHours', data.date.getTime())
        "
        @click="sortResources('busyHours', data.date.getTime())"
      >
        <span>{{ formatDate('D DDD', data.date) }}</span>
      </div>
    </template>

    <template #dayFooter="data">
      <div class="mds-resource-details-footer mds-resource-details-footer-day">
        {{ getOccuppancy(data.events) }}%
      </div>
    </template>

    <template #sidebarFooter>
      <div class="mds-resource-details-footer mds-resource-details-total">${{ totalRevenue }}</div>
    </template>
  </MbscEventcalendar>
</template>

<style>
/* Overrides */

.mds-resource-details .mbsc-timeline-resource-header,
.mds-resource-details .mbsc-timeline-resource-title,
.mds-resource-details .mbsc-timeline-resource-footer,
.mds-resource-details .mbsc-timeline-sidebar-header,
.mds-resource-details .mbsc-timeline-sidebar-resource-title,
.mds-resource-details .mbsc-timeline-sidebar-footer {
  padding: 0;
}

.mds-resource-details .mbsc-timeline-resource-col {
  width: 335px;
}

.mds-resource-details .mbsc-timeline-sidebar-col {
  width: 98px;
}

.mds-resource-details .mbsc-timeline-day {
  width: 144px;
}

.mds-resource-details .mbsc-timeline-resource-title {
  height: 100%;
}

/* Resource grid */

.mds-resource-cell {
  display: inline-block;
  height: 100%;
  padding: 0 5px;
  box-sizing: border-box;
  vertical-align: top;
  line-height: 50px;
}

.mds-resource-cell-name {
  width: 120px;
}

.mds-resource-cell-seats,
.mds-resource-cell-price {
  width: 107px;
}

.mds-resource-cell-seats {
  border-left: 1px solid #ccc;
  border-right: 1px solid #ccc;
}

.mds-resource-details.mbsc-ios-dark .mds-resource-cell-seats,
.mds-resource-details.mbsc-material-dark .mds-resource-cell-seats,
.mds-resource-details.mbsc-windows-dark .mds-resource-cell-seats {
  border-left: 1px solid #333;
  border-right: 1px solid #333;
}

.mds-sidebar-cell {
  padding: 0 5px;
  line-height: 50px;
  text-align: center;
}

/* Sort arrows */

.mds-resource-sort-header {
  cursor: pointer;
  position: relative;
  line-height: 25px;
  padding: 0 5px;
  font-size: 14px;
}

.mds-resource-sort-header::after {
  position: absolute;
  opacity: 0.5;
  right: 8px;
}

.mds-resource-sort-header:hover::after {
  opacity: 1;
}

.mds-resource-sort-asc::after {
  content: '↑';
}

.mds-resource-sort-desc::after {
  content: '↓';
}

.mds-resource-sort-def::after {
  content: '‹›';
  right: 5px;
  top: 12px;
  transform: translateY(-50%) rotate(90deg);
}

/* Footer */

.mds-resource-details-footer {
  line-height: 50px;
  background: rgba(150, 150, 150, 0.1);
}

.mds-resource-details-footer-day {
  font-size: 15px;
  font-weight: 600;
  text-align: center;
  padding: 0 5px;
}

.mds-resource-details-occuppancy {
  font-size: 15px;
  text-align: end;
  text-align: right;
  padding: 0 15px;
}

.mds-resource-details-total {
  font-size: 18px;
  padding: 0 5px;
  color: #8c0000;
  text-align: center;
}
</style>
```

## Implementation notes

- Use `timeline: { type: 'month' }` - a full-month view where each row is a conference room resource.
- Define 10 resources, each with `id`, `name`, `seats`, `color`, and `price` (per day). Resources also carry computed `revenue` and `busyHours` fields, updated at runtime.
- Load events from a remote JSON endpoint (`getJson`) - multiday booking events. On load, call a `refreshData()` function that reads all current calendar events, computes per-resource revenue, and updates the total.
- Enable all interactions: `clickToCreate: true`, `dragToCreate: true`, `dragToMove: true`, `dragToResize: true`.
- **Resource column - 3 sub-columns:** Each row shows name, seat count, and price. Implement this with `renderResource` / `resourceTemplate` / `resource` slot. Each row renders three `<div>` cells: name, `{seats} seats`, and `${price}`.
- **Resource column header:** Render three sortable header cells - "Room", "Capacity", "Price/day" - using `renderResourceHeader` / `resourceHeaderTemplate` / `resourceHeader` slot. Each cell is clickable; clicking cycles the sort state for that column (ascending → descending → default). Apply a CSS class (`mds-resource-sort-asc`, `mds-resource-sort-desc`, `mds-resource-sort-def`) to show the current sort arrow.
- **Resource column footer:** Render a static label "Occuppancy" using `renderResourceFooter` / `resourceFooterTemplate` / `resourceFooter` slot.
- **Sidebar column (right side):** Shows calculated revenue per resource using `renderSidebar` / `sidebarTemplate` / `sidebar` slot. Revenue = sum of `days × price` across all events booked on that resource in the visible month, where days = date difference between event start and end.
- **Sidebar header:** Render a sortable "Revenue" header using `renderSidebarHeader` / `sidebarHeaderTemplate` / `sidebarHeader` slot. Clicking it sorts resources by revenue with the same asc/desc/def cycle.
- **Sidebar footer:** Show the total revenue across all resources using `renderSidebarFooter` / `sidebarFooterTemplate` / `sidebarFooter` slot. Recompute on every event change.
- **Day column header:** Render each day's header as a clickable sort control using `renderTimelineDay` (Angular: `timelineDayTemplate`, Vue: `#timelineDay`). Clicking a day header sorts resources by `busyHours` on that day - total booked hours per resource for that specific date. Apply the same sort arrow CSS class as resource headers.
- **Day footer:** Show occupancy percentage per day using `renderDayFooter` / `dayFooterTemplate` / `dayFooter` slot. Occupancy = count of distinct resources that have at least one event on that day, divided by total resource count, expressed as a percentage.
- **Sorting logic:** Maintain `sortColumn`, `sortDirection` (asc/desc/def), and `sortDay` (the clicked day's timestamp, or null for resource-column sorts). Clicking the same column again cycles direction; clicking a different column resets to ascending. When `sortDirection` is `def`, restore original resource order (by `id`). Before sorting by `busyHours`, pre-compute `resource.busyHours` for the clicked day from the loaded events.
- **Data refresh:** Call `refreshData()` in `onPageLoading` (fires on initial load and month navigation) and after `onEventCreated`, `onEventDeleted`, and `onEventUpdated`. After refresh, re-apply the current sort so the order stays consistent.
- **Resource column width:** The demo requires a wider resource column to accommodate the three sub-cells. Override Mobiscroll's default width by targeting the internal `.mbsc-timeline-resource-col` class with a `min-width` or `width` rule scoped to the calendar's CSS class.

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

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 vue --trial`.

Running a demo locally requires a Mobiscroll trial or a license; setup is per framework at https://mobiscroll.com/docs/vue/getting-started/installation

## Related demos

- Next: [Resource Grouping & Hierarchy](https://demo.mobiscroll.com/vue/timeline/resource-grouping-hierarchy.md) - Build a Vue resource timeline with collapsible nested groups, pinned resources, and per-group event creation controls.
