---
title: "Vue Timeline Resource Search and Filtering Example | Mobiscroll"
component: "Timeline"
framework: "Vue"
library_version: "6.1.5"
url: "https://demo.mobiscroll.com/vue/timeline/resource-filtering-search"
---

# Resource Search and Filtering - Vue Timeline demo

Add live search and filter to a Vue Timeline resource header — multi-select by status or group, with a custom empty state and reset. For Vue and Nuxt.

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

## Description

When dealing with numerous resources, filtering and search are essential tools. Using the [`resourceHeader`](https://mobiscroll.com/docs/vue/eventcalendar/timeline#slot-resourceHeader) slot you can customize the resource header and fit a search input and filter button. Having full control over what you render in the resource header, you can implement live search and filering with a flyout, allowing users to quickly locate specific resources based on selected filters.

To handle cases where no search results are found, use the `resourceEmpty` slot to craft a custom placeholder with specific call-to-action buttons eg. clearing all search terms and resetting filters. The placeholder appears when an empty resource array is passed to the calendar.

## What this demo shows

- A desktop resource timeline for scheduling construction equipment, with Monday to Friday working days arranged horizontally and a hierarchical resource tree arranged vertically on the left.
- **Header navigation** The month and year label in the top left opens date navigation, while the blue previous and next arrows and the Today button on the right make it easy to move between weeks and jump back to the current day.
- **Week numbers** A fixed strip below the header shows the week number for the visible work week.
- **Week view** The timeline displays the selected work week from Monday to Friday using the `DD DDD MMM YYYY` date format, with the current date highlighted.
- **Time grid** Each day shows working hours from 5 AM to 9 PM with hourly columns.
- **Resources** The left resource column shows expandable and collapsible job site groups, with equipment resources nested under each site.
- **Resource rendering** Custom resource content displays the equipment name, identifier, operational status, and a color-coded status indicator.
- **Resource search** A search field in the resource header lets users locate resources by name or keyword.
- **Filter panel** A filter popup provides selectable categories for operational status and job sites.
- **Multi-select filtering** Users can enable multiple statuses and job sites at the same time.
- **Filter actions** Apply updates the visible resources, while Cancel closes the popup without applying the temporary filter selections.
- **Filtered results** The timeline updates to show only resources that match the active search query and selected filters.
- **Empty state** When no resources match, the resource area shows an illustration, a guidance message, and a Reset Filters action.
- **Current time** A vertical blue line with a time label marks the current time.
- **Hover behavior** Hovering over the time grid shows a time indicator that follows the cursor in 15-minute increments.
- **Event cards** Events appear as colored cards with a left color stripe, job name, and exact start and end time.
- **Event positioning** Events are placed by assigned resource, date, start time, and end time.
- **Event interaction** Events highlight on hover and expose drag and resize handles for moving or changing duration.
- **Event selection** Clicking an event selects and highlights it.
- **Vertical scrolling** The resource area scrolls vertically so many resources can be shown in the same timeline.
- **Horizontal scrolling** The timeline supports horizontal scrolling for navigating the visible date and time range.

## Best for

- **Construction equipment scheduling** Plan equipment assignments across job sites in a work-week timeline.
- **Fleet management** Search and filter vehicle or equipment resources before assigning scheduled work.
- **Asset planning** Keep many grouped resources visible while scheduling jobs by date, time, and duration.
- **Maintenance coordination** Separate resources by operational status, such as on-site equipment and equipment in maintenance.
- **Field operations** Manage schedules where dispatchers need resource search, job site filtering, and a dense desktop timeline.
- **Large resource lists** Support planning workflows where users need search, multi-select filters, vertical scrolling, and an empty state for no matching resources.

## 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
- [`cssClass`](https://mobiscroll.com/docs/vue/eventcalendar/api#opt-cssClass) - option
- [`view`](https://mobiscroll.com/docs/vue/eventcalendar/api#opt-view) - option
- [`data`](https://mobiscroll.com/docs/vue/eventcalendar/api#opt-data) - option
- [`resources`](https://mobiscroll.com/docs/vue/eventcalendar/api#opt-resources) - option
- [`clickToCreate`](https://mobiscroll.com/docs/vue/eventcalendar/api#opt-clickToCreate) - 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
- [`display`](https://mobiscroll.com/docs/vue/popup/api#opt-display) - option
- [`anchor`](https://mobiscroll.com/docs/vue/popup/api#opt-anchor) - option
- [`width`](https://mobiscroll.com/docs/vue/popup/api#opt-width) - option
- [`buttons`](https://mobiscroll.com/docs/vue/popup/api#opt-buttons) - option
- [`close`](https://mobiscroll.com/docs/vue/popup/api#event-onClose) - event
- [`isOpen`](https://mobiscroll.com/docs/vue/popup/api#opt-isOpen) - option
- [`close`](https://mobiscroll.com/docs/vue/notifications/toast#event-onClose) - event

## Code

### SFC JS

```vue
<script setup>
import {
  MbscButton,
  MbscCheckbox,
  MbscEventcalendar,
  MbscInput,
  MbscPopup,
  MbscToast,
  setOptions
} from '@mobiscroll/vue'
import { ref } from 'vue'

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

const myEvents = [
  {
    start: '2026-10-29T05:00',
    end: '2026-10-29T19:00',
    title: 'Excavate Foundation',
    resource: 'bulldozer - TX1234'
  },
  {
    start: '2026-10-29T08:00',
    end: '2026-10-29T18:00',
    title: 'Install Framing',
    resource: 'crane - NY9101'
  },
  {
    start: '2026-10-29T07:00',
    end: '2026-10-29T20:00',
    title: 'Electrical Wiring',
    resource: 'excavator - FL1213'
  },
  {
    start: '2026-10-29T05:00',
    end: '2026-10-29T22:00',
    title: 'Roofing',
    resource: 'crane - IL1617'
  },
  {
    start: '2026-10-29T07:00',
    end: '2026-10-29T19:00',
    title: 'Site Cleanup',
    resource: 'bulldozer - PA1819'
  },
  {
    start: '2026-10-29T05:00',
    end: '2026-10-29T18:00',
    title: 'Foundation Work',
    resource: 'concrete mixer - CA2223'
  },
  {
    start: '2026-10-29T05:00',
    end: '2026-10-29T22:00',
    title: 'Steel Framing',
    resource: 'crane - NY2425'
  },
  {
    start: '2026-10-29T07:00',
    end: '2026-10-29T16:00',
    title: 'Interior Plumbing',
    resource: 'excavator - FL2627'
  },
  {
    start: '2026-10-29T07:00',
    end: '2026-10-29T16:00',
    title: 'Masonry Work',
    resource: 'crane - IL3031'
  },
  {
    start: '2026-10-29T06:00',
    end: '2026-10-29T15:00',
    title: 'Exterior Work',
    resource: 'bulldozer - PA3233'
  },
  {
    start: '2026-10-29T06:00',
    end: '2026-10-29T15:00',
    title: 'Ground Breaking',
    resource: 'bulldozer - TX3435'
  },
  {
    start: '2026-10-29T09:00',
    end: '2026-10-29T21:00',
    title: 'Wall Construction',
    resource: 'crane - NY3839'
  },
  {
    start: '2026-10-29T07:00',
    end: '2026-10-29T16:00',
    title: 'Roof Installation',
    resource: 'excavator - FL4041'
  },
  {
    start: '2026-10-29T06:00',
    end: '2026-10-29T19:00',
    title: 'Painting',
    resource: 'crane - IL4445'
  },
  {
    start: '2026-10-29T05:00',
    end: '2026-10-29T20:00',
    title: 'Driveway Paving',
    resource: 'bulldozer - PA4647'
  },
  {
    start: '2026-10-29T05:00',
    end: '2026-10-29T18:00',
    title: 'Foundation',
    resource: 'concrete mixer - CA5051'
  },
  {
    start: '2026-10-29T05:00',
    end: '2026-10-29T19:00',
    title: 'Demolition',
    resource: 'crane - NY5253'
  },
  {
    start: '2026-10-29T06:00',
    end: '2026-10-29T16:00',
    title: 'Electrical Wiring',
    resource: 'excavator - FL5455'
  },
  {
    start: '2026-10-29T05:00',
    end: '2026-10-29T17:00',
    title: 'Roofing',
    resource: 'crane - IL5859'
  },
  {
    start: '2026-10-29T08:00',
    end: '2026-10-29T18:00',
    title: 'Finishing Touches',
    resource: 'bulldozer - PA6061'
  },
  {
    start: '2026-10-29T12:00',
    end: '2026-10-29T22:00',
    title: 'Site Preparation',
    resource: 'bulldozer - TX6263'
  },
  {
    start: '2026-10-29T08:00',
    end: '2026-10-29T19:00',
    title: 'Piling Work',
    resource: 'crane - NY6667'
  },
  {
    start: '2026-10-29T07:00',
    end: '2026-10-29T18:00',
    title: 'Concrete Pouring',
    resource: 'excavator - FL6869'
  },
  {
    start: '2026-10-29T05:00',
    end: '2026-10-29T21:00',
    title: 'Deck Construction',
    resource: 'drill - OH7071'
  },
  {
    start: '2026-10-29T05:00',
    end: '2026-10-29T20:00',
    title: 'Final Touches',
    resource: 'bulldozer - PA7475'
  }, // Showing partial data. Download full source.
]

const myResources = [
  {
    id: 'site1',
    name: '123 Main St, Downtown City',
    eventCreation: false,
    children: [
      {
        id: 'bulldozer - TX1234',
        name: 'Bulldozer - TX1234',
        color: '#1dab2f',
        status: 'on site'
      },
      {
        id: 'concrete mixer - CA5678',
        name: 'Concrete Mixer - CA5678',
        color: '#1dab2f',
        status: 'in maintenance'
      },
      {
        id: 'crane - NY9101',
        name: 'Crane - NY9101',
        color: '#1dab2f',
        status: 'on site'
      },
      {
        id: 'excavator - FL1213',
        name: 'Excavator - FL1213',
        color: '#1dab2f',
        status: 'on site'
      },
      {
        id: 'drill - OH1415',
        name: 'Drill - OH1415',
        color: '#1dab2f',
        status: 'in maintenance'
      },
      {
        id: 'crane - IL1617',
        name: 'Crane - IL1617',
        color: '#1dab2f',
        status: 'on site'
      },
      {
        id: 'bulldozer - PA1819',
        name: 'Bulldozer - PA1819',
        color: '#1dab2f',
        status: 'on site'
      }
    ]
  },
  {
    id: 'site2',
    name: '456 Elm St, Uptown City',
    eventCreation: false,
    children: [
      {
        id: 'bulldozer - TX2021',
        name: 'Bulldozer - TX2021',
        color: '#4981d6',
        status: 'in maintenance'
      },
      {
        id: 'concrete mixer - CA2223',
        name: 'Concrete Mixer - CA2223',
        color: '#4981d6',
        status: 'on site'
      },
      {
        id: 'crane - NY2425',
        name: 'Crane - NY2425',
        color: '#4981d6',
        status: 'on site'
      },
      {
        id: 'excavator - FL2627',
        name: 'Excavator - FL2627',
        color: '#4981d6',
        status: 'on site'
      },
      {
        id: 'drill - OH2829',
        name: 'Drill - OH2829',
        color: '#4981d6',
        status: 'in maintenance'
      },
      {
        id: 'crane - IL3031',
        name: 'Crane - IL3031',
        color: '#4981d6',
        status: 'on site'
      },
      {
        id: 'bulldozer - PA3233',
        name: 'Bulldozer - PA3233',
        color: '#4981d6',
        status: 'on site'
      }
    ]
  },
  {
    id: 'site3',
    name: '789 Maple Ave, Suburban Area',
    eventCreation: false,
    children: [
      {
        id: 'bulldozer - TX3435',
        name: 'Bulldozer - TX3435',
        color: '#f7961e',
        status: 'on site'
      },
      {
        id: 'concrete mixer - CA3637',
        name: 'Concrete Mixer - CA3637',
        color: '#f7961e',
        status: 'in maintenance'
      },
      {
        id: 'crane - NY3839',
        name: 'Crane - NY3839',
        color: '#f7961e',
        status: 'on site'
      },
      {
        id: 'excavator - FL4041',
        name: 'Excavator - FL4041',
        color: '#f7961e',
        status: 'on site'
      },
      {
        id: 'crane - IL4445',
        name: 'Crane - IL4445',
        color: '#f7961e',
        status: 'on site'
      },
      {
        id: 'bulldozer - PA4647',
        name: 'Bulldozer - PA4647',
        color: '#f7961e',
        status: 'on site'
      }
    ]
  },
  {
    id: 'site4',
    name: '101 Industrial Blvd, Industrial City',
    eventCreation: false,
    children: [
      {
        id: 'bulldozer - TX4849',
        name: 'Bulldozer - TX4849',
        color: '#e25dd2',
        status: 'in maintenance'
      },
      {
        id: 'concrete mixer - CA5051',
        name: 'Concrete Mixer - CA5051',
        color: '#e25dd2',
        status: 'on site'
      },
      {
        id: 'crane - NY5253',
        name: 'Crane - NY5253',
        color: '#e25dd2',
        status: 'on site'
      },
      {
        id: 'excavator - FL5455',
        name: 'Excavator - FL5455',
        color: '#e25dd2',
        status: 'on site'
      },
      {
        id: 'drill - OH5657',
        name: 'Drill - OH5657',
        color: '#e25dd2',
        status: 'in maintenance'
      },
      {
        id: 'crane - IL5859',
        name: 'Crane - IL5859',
        color: '#e25dd2',
        status: 'on site'
      },
      {
        id: 'bulldozer - PA6061',
        name: 'Bulldozer - PA6061',
        color: '#e25dd2',
        status: 'on site'
      }
    ]
  },
  {
    id: 'site5',
    name: '202 River Rd, Riverside City',
    eventCreation: false,
    children: [
      {
        id: 'bulldozer - TX6263',
        name: 'Bulldozer - TX6263',
        color: '#34c8e0',
        status: 'on site'
      },
      {
        id: 'concrete mixer - CA6465',
        name: 'Concrete Mixer - CA6465',
        color: '#34c8e0',
        status: 'in maintenance'
      },
      {
        id: 'crane - NY6667',
        name: 'Crane - NY6667',
        color: '#34c8e0',
        status: 'on site'
      },
      {
        id: 'excavator - FL6869',
        name: 'Excavator - FL6869',
        color: '#34c8e0',
        status: 'on site'
      },
      {
        id: 'drill - OH7071',
        name: 'Drill - OH7071',
        color: '#34c8e0',
        status: 'on site'
      },
      {
        id: 'crane - IL7273',
        name: 'Crane - IL7273',
        color: '#34c8e0',
        status: 'in maintenance'
      },
      {
        id: 'bulldozer - PA7475',
        name: 'Bulldozer - PA7475',
        color: '#34c8e0',
        status: 'on site'
      }
    ]
  }
]

const myFilters = [
  { id: 'on site', name: 'On site', value: true },
  { id: 'in maintenance', name: 'In maintenance', value: true }
]

myResources.forEach((site) => {
  myFilters.push({ id: site.id, name: site.name, value: true })
})

const filteredResources = ref(myResources)
const isPopupOpen = ref(false)
const searchQuery = ref('')
const myAnchor = ref(null)
const filters = ref(
  myFilters.reduce((map, f) => {
    map[f.id] = true
    return map
  }, {})
)
const tempFilters = ref({})
const isToastOpen = ref(false)
const toastMessage = ref('')

const buttonRef = ref(null)
const searchTimeout = ref(null)

const myView = {
  timeline: {
    type: 'week',
    startTime: '05:00',
    endTime: '22:00',
    startDay: 1,
    endDay: 5,
    timeCellStep: 60,
    timeLabelStep: 60,
    weekNumbers: true
  }
}

const filterResources = (currentFilters, currentQuery) => {
  filteredResources.value = myResources
    .map((site) => ({
      id: site.id,
      name: site.name,
      color: site.color,
      eventCreation: site.eventCreation,
      children: site.children.filter(
        (resource) =>
          currentFilters[resource.status] &&
          (!currentQuery.value ||
            resource.name.toLowerCase().includes(currentQuery.value.toLowerCase()))
      )
    }))
    .filter((site) => site.children.length > 0 && currentFilters[site.id])
}

const openToast = (message) => {
  toastMessage.value = message
  isToastOpen.value = true
}

const openFilters = () => {
  tempFilters.value = { ...filters.value }
  myAnchor.value = buttonRef.value?.instance.nativeElement
  isPopupOpen.value = true
}

const applyFilters = () => {
  filters.value = { ...tempFilters.value }
  isPopupOpen.value = false
  filterResources(tempFilters.value, searchQuery)
  openToast('Filters applied')
}

const resetFilters = () => {
  searchQuery.value = ''
  Object.keys(filters).forEach((key) => {
    filters[key].value = true
  })
  filterResources()
  openToast('Filters cleared')
}

const handleSearch = (ev) => {
  const query = ev.target.value
  searchQuery.value = query
  clearTimeout(searchTimeout.value)
  searchTimeout.value = setTimeout(() => filterResources(filters, query), 300)
}

const handleCheckboxChange = (ev) => {
  const key = ev.target.value
  tempFilters.value[key] = !tempFilters.value[key]
}
</script>

<template>
  <MbscEventcalendar
    cssClass="mds-resource-filtering-calendar"
    :view="myView"
    :data="myEvents"
    :resources="filteredResources"
    :clickToCreate="true"
    :dragToCreate="true"
    :dragToMove="true"
    :dragToResize="true"
  >
    <template #resourceHeader>
      <div class="mbsc-flex mbsc-align-items-center mbsc-font mds-resource-filtering-search">
        <label class="mbsc-flex-1-1">
          <MbscInput
            type="text"
            autocomplete="off"
            input-style="outline"
            start-icon="material-search"
            placeholder="Search..."
            v-model="searchQuery"
            @input="handleSearch"
          />
        </label>
        <MbscButton
          ref="buttonRef"
          start-icon="material-filter-list"
          variant="outline"
          @click="openFilters"
        >
          Filter
        </MbscButton>
      </div>
    </template>

    <template #resource="resource">
      <div>
        <div class="mds-resource-filtering-name">{{ resource.name }}</div>
        <div v-if="resource.status" class="mds-resource-filtering-status">
          <span
            class="mds-resource-filtering-status-dot"
            :style="{ backgroundColor: resource.status === 'on site' ? 'green' : 'orange' }"
          ></span>
          {{ resource.status }}
        </div>
      </div>
    </template>

    <template #resourceEmpty>
      <div class="mds-resource-filtering-empty mbsc-flex mbsc-align-items-center">
        <div class="mbsc-flex-1-1">
          <img
            src="https://img.mobiscroll.com/demos/filter-no-result.png"
            alt="Empty list"
            style="width: 100px"
          />
          <p class="mbsc-font mbsc-margin mbsc-medium mbsc-italic mbsc-txt-muted">
            No resources match your search.
          </p>
          <p class="mbsc-margin mbsc-medium mbsc-italic mbsc-txt-muted">
            Adjust your filters or try a different keyword.
          </p>
          <MbscButton @click="resetFilters" variant="outline">Reset Filters</MbscButton>
        </div>
      </div>
    </template>
  </MbscEventcalendar>

  <MbscPopup
    :contentPadding="false"
    display="anchored"
    :anchor="myAnchor"
    width="400"
    :buttons="[
      'cancel',
      {
        text: 'Apply',
        keyCode: 'enter',
        handler: applyFilters,
        cssClass: 'mbsc-popup-button-primary'
      }
    ]"
    @close="isPopupOpen = false"
    :isOpen="isPopupOpen"
  >
    <div class="mbsc-form-group">
      <div class="mbsc-form-group-title">Operational Status</div>
      <div v-for="filter in myFilters.slice(0, 2)" :key="filter">
        <MbscCheckbox
          :key="filter.id"
          :label="filter.name"
          :value="filter.value"
          v-model="tempFilters[filter.id]"
          @change="handleCheckboxChange"
        />
      </div>
    </div>

    <div class="mbsc-form-group">
      <div class="mbsc-form-group-title">Job sites</div>
      <div v-for="filter in myFilters.slice(2)" :key="filter">
        <MbscCheckbox
          :key="filter.id"
          :label="filter.name"
          :value="filter.id"
          v-model="tempFilters[filter.id]"
          @change="handleCheckboxChange"
        />
      </div>
    </div>
  </MbscPopup>
  <MbscToast :message="toastMessage" :isOpen="isToastOpen" @close="isToastOpen = false" />
</template>

<style>
.mds-resource-filtering-calendar .mbsc-timeline-resource-header {
  height: 100%;
  padding: 8px;
  box-sizing: border-box;
}

.mds-resource-filtering-calendar .mbsc-timeline-resource-col {
  width: 350px;
}

.mds-resource-filtering-calendar .mbsc-timeline-resource-title {
  height: 100%;
  box-sizing: border-box;
}

.mds-resource-filtering-calendar .mbsc-timeline-parent {
  height: 34px;
}

.mds-resource-filtering-calendar .mbsc-timeline-row-gutter {
  height: 6px;
}

.mds-resource-filtering-search {
  height: 100%;
}

.mds-resource-filtering-search .mbsc-textfield-wrapper {
  margin: 0;
}

.mds-resource-filtering-search .mbsc-textfield-wrapper.mbsc-ltr {
  margin-right: 8px;
}

.mds-resource-filtering-search .mbsc-textfield-wrapper.mbsc-rtl {
  margin-left: 8px;
}

.mds-resource-filtering-search .mbsc-textfield-wrapper.mbsc-material {
  margin-top: 2px;
}

.mds-resource-filtering-search .mbsc-textfield {
  height: 36px;
  padding: 0 8px 0 38px;
}

.mds-resource-filtering-search .mbsc-textfield-icon {
  top: 50%;
  left: 8px;
  font-size: 20px;
  height: 24px;
  line-height: 24px;
  margin-top: -12px;
}

.mds-resource-filtering-search .mbsc-button {
  margin: 0;
  height: 36px;
  font-size: 14px;
}

.mds-resource-filtering-name {
  font-size: 14px;
  font-weight: bold;
  margin-bottom: 7px;
}

.mds-resource-filtering-status-dot {
  display: inline-block;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  margin-right: 5px;
}

.mds-resource-filtering-status {
  font-size: 13px;
  font-weight: normal;
}

.mds-resource-filtering-empty {
  height: 100%;
  text-align: center;
}
</style>
```

### SFC TS

```vue
<script setup lang="ts">
import {
  MbscButton,
  MbscCheckbox,
  MbscEventcalendar,
  MbscInput,
  MbscPopup,
  MbscToast,
  setOptions
} from '@mobiscroll/vue'
import type { MbscCalendarEvent, MbscEventcalendarView, MbscResource } from '@mobiscroll/vue'
import { ref } from 'vue'

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

const myEvents: MbscCalendarEvent[] = [
  {
    start: '2026-10-29T05:00',
    end: '2026-10-29T19:00',
    title: 'Excavate Foundation',
    resource: 'bulldozer - TX1234'
  },
  {
    start: '2026-10-29T08:00',
    end: '2026-10-29T18:00',
    title: 'Install Framing',
    resource: 'crane - NY9101'
  },
  {
    start: '2026-10-29T07:00',
    end: '2026-10-29T20:00',
    title: 'Electrical Wiring',
    resource: 'excavator - FL1213'
  },
  {
    start: '2026-10-29T05:00',
    end: '2026-10-29T22:00',
    title: 'Roofing',
    resource: 'crane - IL1617'
  },
  {
    start: '2026-10-29T07:00',
    end: '2026-10-29T19:00',
    title: 'Site Cleanup',
    resource: 'bulldozer - PA1819'
  },
  {
    start: '2026-10-29T05:00',
    end: '2026-10-29T18:00',
    title: 'Foundation Work',
    resource: 'concrete mixer - CA2223'
  },
  {
    start: '2026-10-29T05:00',
    end: '2026-10-29T22:00',
    title: 'Steel Framing',
    resource: 'crane - NY2425'
  },
  {
    start: '2026-10-29T07:00',
    end: '2026-10-29T16:00',
    title: 'Interior Plumbing',
    resource: 'excavator - FL2627'
  },
  {
    start: '2026-10-29T07:00',
    end: '2026-10-29T16:00',
    title: 'Masonry Work',
    resource: 'crane - IL3031'
  },
  {
    start: '2026-10-29T06:00',
    end: '2026-10-29T15:00',
    title: 'Exterior Work',
    resource: 'bulldozer - PA3233'
  },
  {
    start: '2026-10-29T06:00',
    end: '2026-10-29T15:00',
    title: 'Ground Breaking',
    resource: 'bulldozer - TX3435'
  },
  {
    start: '2026-10-29T09:00',
    end: '2026-10-29T21:00',
    title: 'Wall Construction',
    resource: 'crane - NY3839'
  },
  {
    start: '2026-10-29T07:00',
    end: '2026-10-29T16:00',
    title: 'Roof Installation',
    resource: 'excavator - FL4041'
  },
  {
    start: '2026-10-29T06:00',
    end: '2026-10-29T19:00',
    title: 'Painting',
    resource: 'crane - IL4445'
  },
  {
    start: '2026-10-29T05:00',
    end: '2026-10-29T20:00',
    title: 'Driveway Paving',
    resource: 'bulldozer - PA4647'
  },
  {
    start: '2026-10-29T05:00',
    end: '2026-10-29T18:00',
    title: 'Foundation',
    resource: 'concrete mixer - CA5051'
  },
  {
    start: '2026-10-29T05:00',
    end: '2026-10-29T19:00',
    title: 'Demolition',
    resource: 'crane - NY5253'
  },
  {
    start: '2026-10-29T06:00',
    end: '2026-10-29T16:00',
    title: 'Electrical Wiring',
    resource: 'excavator - FL5455'
  },
  {
    start: '2026-10-29T05:00',
    end: '2026-10-29T17:00',
    title: 'Roofing',
    resource: 'crane - IL5859'
  },
  {
    start: '2026-10-29T08:00',
    end: '2026-10-29T18:00',
    title: 'Finishing Touches',
    resource: 'bulldozer - PA6061'
  },
  {
    start: '2026-10-29T12:00',
    end: '2026-10-29T22:00',
    title: 'Site Preparation',
    resource: 'bulldozer - TX6263'
  },
  {
    start: '2026-10-29T08:00',
    end: '2026-10-29T19:00',
    title: 'Piling Work',
    resource: 'crane - NY6667'
  },
  {
    start: '2026-10-29T07:00',
    end: '2026-10-29T18:00',
    title: 'Concrete Pouring',
    resource: 'excavator - FL6869'
  },
  {
    start: '2026-10-29T05:00',
    end: '2026-10-29T21:00',
    title: 'Deck Construction',
    resource: 'drill - OH7071'
  },
  {
    start: '2026-10-29T05:00',
    end: '2026-10-29T20:00',
    title: 'Final Touches',
    resource: 'bulldozer - PA7475'
  }, // Showing partial data. Download full source.
]

const myResources: MbscResource[] = [
  {
    id: 'site1',
    name: '123 Main St, Downtown City',
    eventCreation: false,
    children: [
      {
        id: 'bulldozer - TX1234',
        name: 'Bulldozer - TX1234',
        color: '#1dab2f',
        status: 'on site'
      },
      {
        id: 'concrete mixer - CA5678',
        name: 'Concrete Mixer - CA5678',
        color: '#1dab2f',
        status: 'in maintenance'
      },
      {
        id: 'crane - NY9101',
        name: 'Crane - NY9101',
        color: '#1dab2f',
        status: 'on site'
      },
      {
        id: 'excavator - FL1213',
        name: 'Excavator - FL1213',
        color: '#1dab2f',
        status: 'on site'
      },
      {
        id: 'drill - OH1415',
        name: 'Drill - OH1415',
        color: '#1dab2f',
        status: 'in maintenance'
      },
      {
        id: 'crane - IL1617',
        name: 'Crane - IL1617',
        color: '#1dab2f',
        status: 'on site'
      },
      {
        id: 'bulldozer - PA1819',
        name: 'Bulldozer - PA1819',
        color: '#1dab2f',
        status: 'on site'
      }
    ]
  },
  {
    id: 'site2',
    name: '456 Elm St, Uptown City',
    eventCreation: false,
    children: [
      {
        id: 'bulldozer - TX2021',
        name: 'Bulldozer - TX2021',
        color: '#4981d6',
        status: 'in maintenance'
      },
      {
        id: 'concrete mixer - CA2223',
        name: 'Concrete Mixer - CA2223',
        color: '#4981d6',
        status: 'on site'
      },
      {
        id: 'crane - NY2425',
        name: 'Crane - NY2425',
        color: '#4981d6',
        status: 'on site'
      },
      {
        id: 'excavator - FL2627',
        name: 'Excavator - FL2627',
        color: '#4981d6',
        status: 'on site'
      },
      {
        id: 'drill - OH2829',
        name: 'Drill - OH2829',
        color: '#4981d6',
        status: 'in maintenance'
      },
      {
        id: 'crane - IL3031',
        name: 'Crane - IL3031',
        color: '#4981d6',
        status: 'on site'
      },
      {
        id: 'bulldozer - PA3233',
        name: 'Bulldozer - PA3233',
        color: '#4981d6',
        status: 'on site'
      }
    ]
  },
  {
    id: 'site3',
    name: '789 Maple Ave, Suburban Area',
    eventCreation: false,
    children: [
      {
        id: 'bulldozer - TX3435',
        name: 'Bulldozer - TX3435',
        color: '#f7961e',
        status: 'on site'
      },
      {
        id: 'concrete mixer - CA3637',
        name: 'Concrete Mixer - CA3637',
        color: '#f7961e',
        status: 'in maintenance'
      },
      {
        id: 'crane - NY3839',
        name: 'Crane - NY3839',
        color: '#f7961e',
        status: 'on site'
      },
      {
        id: 'excavator - FL4041',
        name: 'Excavator - FL4041',
        color: '#f7961e',
        status: 'on site'
      },
      {
        id: 'crane - IL4445',
        name: 'Crane - IL4445',
        color: '#f7961e',
        status: 'on site'
      },
      {
        id: 'bulldozer - PA4647',
        name: 'Bulldozer - PA4647',
        color: '#f7961e',
        status: 'on site'
      }
    ]
  },
  {
    id: 'site4',
    name: '101 Industrial Blvd, Industrial City',
    eventCreation: false,
    children: [
      {
        id: 'bulldozer - TX4849',
        name: 'Bulldozer - TX4849',
        color: '#e25dd2',
        status: 'in maintenance'
      },
      {
        id: 'concrete mixer - CA5051',
        name: 'Concrete Mixer - CA5051',
        color: '#e25dd2',
        status: 'on site'
      },
      {
        id: 'crane - NY5253',
        name: 'Crane - NY5253',
        color: '#e25dd2',
        status: 'on site'
      },
      {
        id: 'excavator - FL5455',
        name: 'Excavator - FL5455',
        color: '#e25dd2',
        status: 'on site'
      },
      {
        id: 'drill - OH5657',
        name: 'Drill - OH5657',
        color: '#e25dd2',
        status: 'in maintenance'
      },
      {
        id: 'crane - IL5859',
        name: 'Crane - IL5859',
        color: '#e25dd2',
        status: 'on site'
      },
      {
        id: 'bulldozer - PA6061',
        name: 'Bulldozer - PA6061',
        color: '#e25dd2',
        status: 'on site'
      }
    ]
  },
  {
    id: 'site5',
    name: '202 River Rd, Riverside City',
    eventCreation: false,
    children: [
      {
        id: 'bulldozer - TX6263',
        name: 'Bulldozer - TX6263',
        color: '#34c8e0',
        status: 'on site'
      },
      {
        id: 'concrete mixer - CA6465',
        name: 'Concrete Mixer - CA6465',
        color: '#34c8e0',
        status: 'in maintenance'
      },
      {
        id: 'crane - NY6667',
        name: 'Crane - NY6667',
        color: '#34c8e0',
        status: 'on site'
      },
      {
        id: 'excavator - FL6869',
        name: 'Excavator - FL6869',
        color: '#34c8e0',
        status: 'on site'
      },
      {
        id: 'drill - OH7071',
        name: 'Drill - OH7071',
        color: '#34c8e0',
        status: 'on site'
      },
      {
        id: 'crane - IL7273',
        name: 'Crane - IL7273',
        color: '#34c8e0',
        status: 'in maintenance'
      },
      {
        id: 'bulldozer - PA7475',
        name: 'Bulldozer - PA7475',
        color: '#34c8e0',
        status: 'on site'
      }
    ]
  }
]

const myFilters = [
  { id: 'on site', name: 'On site', value: true },
  { id: 'in maintenance', name: 'In maintenance', value: true }
]

myResources.forEach((site) => {
  myFilters.push({ id: site.id as string, name: site.name as string, value: true })
})

const filteredResources = ref<MbscResource[]>(myResources)
const isPopupOpen = ref<boolean>(false)
const searchQuery = ref<string>('')
const myAnchor = ref<HTMLElement>()
const filters = ref<{ [key: string]: boolean }>(
  myFilters.reduce(
    (map, f) => {
      map[f.id] = true
      return map
    },
    {} as { [key: string]: boolean }
  )
)
const tempFilters = ref<{ [key: string]: boolean }>({})
const isToastOpen = ref<boolean>(false)
const toastMessage = ref<string>('')

const buttonRef = ref<typeof MbscButton>()
const searchTimeout = ref<ReturnType<typeof setTimeout>>()

const myView: MbscEventcalendarView = {
  timeline: {
    type: 'week',
    startTime: '05:00',
    endTime: '22:00',
    startDay: 1,
    endDay: 5,
    timeCellStep: 60,
    timeLabelStep: 60,
    weekNumbers: true
  }
}

const filterResources = (currentFilters: { [key: string]: boolean }, currentQuery: string) => {
  filteredResources.value = myResources
    .map((site) => ({
      id: site.id,
      name: site.name,
      eventCreation: site.eventCreation,
      children: (site.children ?? []).filter(
        (resource) =>
          currentFilters[resource.status] &&
          (!currentQuery ||
            (resource.name && resource.name.toLowerCase().includes(currentQuery.toLowerCase())))
      )
    }))
    .filter((site) => site.children.length > 0 && currentFilters[site.id])
}

const openToast = (message: string) => {
  toastMessage.value = message
  isToastOpen.value = true
}

const openFilters = () => {
  tempFilters.value = { ...filters.value }
  myAnchor.value = buttonRef.value?.instance.nativeElement
  isPopupOpen.value = true
}

const applyFilters = () => {
  filters.value = { ...tempFilters.value }
  isPopupOpen.value = false
  filterResources(tempFilters.value, searchQuery.value)
  openToast('Filters applied')
}

const resetFilters = () => {
  searchQuery.value = ''
  const updatedFilters = myFilters.reduce(
    (map, f) => {
      map[f.id] = true
      return map
    },
    {} as { [key: string]: boolean }
  )
  filterResources(updatedFilters, '')
  openToast('Filters cleared')
}

const handleSearch = (ev: Event) => {
  const query = (ev.target as HTMLInputElement).value
  searchQuery.value = query
  clearTimeout(searchTimeout.value)
  searchTimeout.value = setTimeout(() => filterResources(filters.value, query), 300)
}

const handleCheckboxChange = (ev: Event) => {
  const key = (ev.target as HTMLInputElement).value
  tempFilters.value[key] = !tempFilters.value[key]
}
</script>

<template>
  <MbscEventcalendar
    cssClass="mds-resource-filtering-calendar"
    :view="myView"
    :data="myEvents"
    :resources="filteredResources"
    :clickToCreate="true"
    :dragToCreate="true"
    :dragToMove="true"
    :dragToResize="true"
  >
    <template #resourceHeader>
      <div class="mbsc-flex mbsc-align-items-center mbsc-font mds-resource-filtering-search">
        <label class="mbsc-flex-1-1">
          <MbscInput
            type="text"
            autocomplete="off"
            input-style="outline"
            start-icon="material-search"
            placeholder="Search..."
            v-model="searchQuery"
            @input="handleSearch"
          />
        </label>
        <MbscButton
          ref="buttonRef"
          start-icon="material-filter-list"
          variant="outline"
          @click="openFilters"
        >
          Filter
        </MbscButton>
      </div>
    </template>

    <template #resource="resource">
      <div>
        <div class="mds-resource-filtering-name">{{ resource.name }}</div>
        <div v-if="resource.status" class="mds-resource-filtering-status">
          <span
            class="mds-resource-filtering-status-dot"
            :style="{ backgroundColor: resource.status === 'on site' ? 'green' : 'orange' }"
          ></span>
          {{ resource.status }}
        </div>
      </div>
    </template>

    <template #resourceEmpty>
      <div class="mds-resource-filtering-empty mbsc-flex mbsc-align-items-center">
        <div class="mbsc-flex-1-1">
          <img
            src="https://img.mobiscroll.com/demos/filter-no-result.png"
            alt="Empty list"
            style="width: 100px"
          />
          <p class="mbsc-font mbsc-margin mbsc-medium mbsc-italic mbsc-txt-muted">
            No resources match your search.
          </p>
          <p class="mbsc-margin mbsc-medium mbsc-italic mbsc-txt-muted">
            Adjust your filters or try a different keyword.
          </p>
          <MbscButton @click="resetFilters" variant="outline">Reset Filters</MbscButton>
        </div>
      </div>
    </template>
  </MbscEventcalendar>

  <MbscPopup
    :contentPadding="false"
    display="anchored"
    :anchor="myAnchor"
    width="400"
    :buttons="[
      'cancel',
      {
        text: 'Apply',
        keyCode: 'enter',
        handler: applyFilters,
        cssClass: 'mbsc-popup-button-primary'
      }
    ]"
    @close="isPopupOpen = false"
    :isOpen="isPopupOpen"
  >
    <div class="mbsc-form-group">
      <div class="mbsc-form-group-title">Operational Status</div>
      <div v-for="filter in myFilters.slice(0, 2)" :key="filter">
        <MbscCheckbox
          :key="filter.id"
          :label="filter.name"
          :value="filter.value"
          v-model="tempFilters[filter.id]"
          @change="handleCheckboxChange"
        />
      </div>
    </div>

    <div class="mbsc-form-group">
      <div class="mbsc-form-group-title">Job sites</div>
      <div v-for="filter in myFilters.slice(2)" :key="filter">
        <MbscCheckbox
          :key="filter.id"
          :label="filter.name"
          :value="filter.id"
          v-model="tempFilters[filter.id]"
          @change="handleCheckboxChange"
        />
      </div>
    </div>
  </MbscPopup>
  <MbscToast :message="toastMessage" :isOpen="isToastOpen" @close="isToastOpen = false" />
</template>

<style>
.mds-resource-filtering-calendar .mbsc-timeline-resource-header {
  height: 100%;
  padding: 8px;
  box-sizing: border-box;
}

.mds-resource-filtering-calendar .mbsc-timeline-resource-col {
  width: 350px;
}

.mds-resource-filtering-calendar .mbsc-timeline-resource-title {
  height: 100%;
  box-sizing: border-box;
}

.mds-resource-filtering-calendar .mbsc-timeline-parent {
  height: 34px;
}

.mds-resource-filtering-calendar .mbsc-timeline-row-gutter {
  height: 6px;
}

.mds-resource-filtering-search {
  height: 100%;
}

.mds-resource-filtering-search .mbsc-textfield-wrapper {
  margin: 0;
}

.mds-resource-filtering-search .mbsc-textfield-wrapper.mbsc-ltr {
  margin-right: 8px;
}

.mds-resource-filtering-search .mbsc-textfield-wrapper.mbsc-rtl {
  margin-left: 8px;
}

.mds-resource-filtering-search .mbsc-textfield-wrapper.mbsc-material {
  margin-top: 2px;
}

.mds-resource-filtering-search .mbsc-textfield {
  height: 36px;
  padding: 0 8px 0 38px;
}

.mds-resource-filtering-search .mbsc-textfield-icon {
  top: 50%;
  left: 8px;
  font-size: 20px;
  height: 24px;
  line-height: 24px;
  margin-top: -12px;
}

.mds-resource-filtering-search .mbsc-button {
  margin: 0;
  height: 36px;
  font-size: 14px;
}

.mds-resource-filtering-name {
  font-size: 14px;
  font-weight: bold;
  margin-bottom: 7px;
}

.mds-resource-filtering-status-dot {
  display: inline-block;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  margin-right: 5px;
}

.mds-resource-filtering-status {
  font-size: 13px;
  font-weight: normal;
}

.mds-resource-filtering-empty {
  height: 100%;
  text-align: center;
}
</style>
```

## Implementation notes

- Use `type: 'week'` with `startDay: 1`/`endDay: 5`, `timeCellStep: 60`, `timeLabelStep: 60`, and `weekNumbers: true` for a Mon-Fri hourly view with week numbers in the header.
- Define resources as a two-level tree of job site groups (`eventCreation: false`) and equipment leaf nodes, each with a custom `status` property (`'on site'` or `'in maintenance'`) and a `color`.
- Keep a `filteredResources` state and pass it to the `resources` prop. Write a `filterResources(currentFilters, currentQuery)` function that maps each site, filters its children by status and name match, and removes sites whose children are all filtered out or whose site checkbox is unchecked.
- Use `renderResourceHeader` (Angular: `resourceHeaderTemplate`, Vue: `#resourceHeader` slot) to render a search `Input` (with a search start icon) and a Filter `Button` side by side in the resource column header.
- Debounce the search input by 300ms using a `searchTimeout` ref: on each keystroke, clear the previous timeout and schedule a new call to `filterResources` with the current filters and query.
- Use `renderResource` (Angular: `resourceTemplate`, Vue: `#resource` slot) to show the equipment name and, for leaf nodes, a status line with a small colored dot (green for `'on site'`, orange for `'in maintenance'`).
- Use `renderResourceEmpty` (Angular: `resourceEmptyTemplate`, Vue: `#resourceEmpty` slot) to render a no-results placeholder with an image, a descriptive message, and a "Reset Filters" button that calls the reset handler. This appears automatically when the `resources` prop receives an empty array.
- Open the filter `Popup` anchored to the Filter button's DOM element (obtained via a `ref` on the `Button`). On open, copy the current filters into a `tempFilters` state so changes can be discarded on cancel.
- Inside the popup, render two `Checkbox` groups: one for operational status (on site / in maintenance) and one with a checkbox per job site. Wire each checkbox to toggle its key in `tempFilters`.
- On Apply, commit `tempFilters` to the active `filters` state, call `filterResources` with the new values, and show a `Toast`. On reset, restore all filter keys to `true`, clear the search query, re-run `filterResources`, and show a `Toast`.
- Enable `clickToCreate`, `dragToCreate`, `dragToMove`, and `dragToResize` for full event management on the filtered timeline.

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

- Previous: [Shifts and Tasks](https://demo.mobiscroll.com/vue/timeline/tasks-subtasks-under-shifts.md) - Display shifts and linked tasks on a Vue Timeline — group tasks under parent shifts and restrict movement to each shift's time range.
- Next: [External Resource Drag & Drop](https://demo.mobiscroll.com/vue/timeline/resource-management-with-dynamic-team-creation.md) - Drag external resources onto a Vue Timeline to add team members, or drag them off to remove them — build crew scheduling and team management.
