DevExtreme v26.1 is now available.

Explore our newest features/capabilities and share your thoughts with us.

Your search did not match any results.

Vue Scheduler - Resolve Time Conflicts

This example addresses appointment time conflicts when using the DevExtreme Scheduler. Use the Allow Overlapping Appointments select-box to select a desired time conflict resolution mode.

Backend API
<template> <DxScheduler :data-source="data" :views="views" :current-view="'week'" :current-date="currentDate" :start-day-hour="9" :end-day-hour="19" :height="600" :show-all-day-panel="false" all-day-panel-mode="hidden" @appointment-adding="onAppointmentAdding" @appointment-updating="onAppointmentUpdating" > <DxResource field-expr="assigneeId" :data-source="assignees" value-expr="id" color-expr="color" icon="user" :allow-multiple="true" /> <DxEditing :popup="popupOptions"> <DxForm label-mode="hidden" :customize-item="customizeItem" :element-attr="formElementAttr" @initialized="onFormInitialized" > <DxItem name="conflictInformer" template="conflict-informer-template" /> <DxItem type="group" name="mainGroup" > <DxItem name="subjectGroup"/> <DxItem name="dateGroup"/> <DxItem name="repeatGroup"/> <DxItem name="assigneeIdGroup"> <DxItem name="assigneeIdIcon"/> <DxItem name="assigneeIdEditor" :is-required="true" :editor-options="assigneeIdEditorOptions" /> </DxItem> </DxItem> <DxItem type="group" name="recurrenceGroup" /> </DxForm> </DxEditing> <template #conflict-informer-template> <div class="conflict-informer">This time slot conflicts with another appointment.</div> </template> <template #tagTemplate="{ data: itemData }"> <div class="dx-tag-content" :style="{ backgroundColor: itemData.color, borderColor: itemData.color }" > {{ itemData.text }} <div class="dx-tag-remove-button"/> </div> </template> </DxScheduler> <div class="options"> <div class="option"> <span>Allow Overlapping Appointments</span> <DxSelectBox :items="overlappingRuleItems" value-expr="value" display-expr="text" value="sameResource" @value-changed="onOverlappingRuleChanged" /> </div> </div> </template> <script setup lang="ts"> import DxScheduler, { DxResource, DxEditing, DxForm, DxItem, type DxSchedulerTypes, } from 'devextreme-vue/scheduler'; import DxSelectBox, { type DxSelectBoxTypes } from 'devextreme-vue/select-box'; import { custom as customDialog } from 'devextreme/ui/dialog'; import type { DxFormTypes } from 'devextreme-vue/form'; import type { DxTagBoxTypes } from 'devextreme-vue/tag-box'; import type { DxPopupTypes } from 'devextreme-vue/popup'; import { data, assignees, type Appointment } from './data.ts'; type dxScheduler = NonNullable<DxSchedulerTypes.InitializedEvent['component']>; type dxForm = NonNullable<DxFormTypes.InitializedEvent['component']>; type dxPopup = NonNullable<DxPopupTypes.InitializedEvent['component']>; let form: dxForm | undefined; let popup: dxPopup | undefined; let showConflictError = false; let overlappingRule = 'sameResource'; const currentDate = new Date(2026, 1, 10); const views: DxSchedulerTypes.ViewType[] = ['day', 'week', 'workWeek', 'month']; const formElementAttr = { class: 'hide-informer', id: 'form' }; const overlappingRuleItems = [ { value: 'sameResource', text: 'Different Resources' }, { value: 'allResources', text: 'Never' }, ]; function getNextDay(date: Date): Date { const next = new Date(date); next.setDate(next.getDate() + 1); return next; } function getEndDate(occurrence: DxSchedulerTypes.Occurrence): Date { return (occurrence.appointmentData as Appointment).allDay ? getNextDay(occurrence.startDate) : occurrence.endDate; } function isOverlapping(a: DxSchedulerTypes.Occurrence, b: DxSchedulerTypes.Occurrence): boolean { const aEnd = getEndDate(a); const bEnd = getEndDate(b); if (a.startDate >= bEnd || b.startDate >= aEnd) return false; if (overlappingRule === 'sameResource') { return (a.appointmentData as Appointment).assigneeId[0] === (b.appointmentData as Appointment).assigneeId[0]; } return true; } function detectConflict(scheduler: dxScheduler, newAppointment: Appointment): boolean { const allAppointments = scheduler.getDataSource().items() as Appointment[]; const startDate = new Date(newAppointment.startDate); let endDate: Date; if (newAppointment.recurrenceRule) { endDate = scheduler.getEndViewDate(); } else if (newAppointment.allDay) { endDate = getNextDay(startDate); } else { endDate = new Date(newAppointment.endDate); } const existingOccurrences = scheduler .getOccurrences(startDate, endDate, allAppointments) .filter((occurrence) => (occurrence.appointmentData as Appointment).id !== newAppointment.id); const newOccurrences = scheduler.getOccurrences(startDate, endDate, [newAppointment]); return newOccurrences.some((newOccurrence) => existingOccurrences.some((existingOccurrence) => isOverlapping(newOccurrence, existingOccurrence), ), ); } const assigneeIdEditorOptions = { onValueChanged: (e: DxTagBoxTypes.ValueChangedEvent) => { if (e.value.length > 1) { e.component.option('value', [e.value[e.value.length - 1]]); } }, tagTemplate: 'tagTemplate', }; function setConflictError(show: boolean) { showConflictError = show; form?.option('elementAttr.class', show ? '' : 'hide-informer'); } const popupOptions = { onInitialized: (e: DxPopupTypes.InitializedEvent) => { popup = e.component as dxPopup; }, onHidden: () => { setConflictError(false); }, }; const handleConflict = ( e: DxSchedulerTypes.AppointmentAddingEvent | DxSchedulerTypes.AppointmentUpdatingEvent, appointmentData: Appointment, ) => { if (!detectConflict(e.component, appointmentData)) { setConflictError(false); return; } e.cancel = true; if (popup?.option('visible')) { setConflictError(true); form?.validate(); } else { const dialog = customDialog({ showTitle: false, messageHtml: '<p id="conflict-dialog">This time slot conflicts with another appointment.</p>', buttons: [{ type: 'default', text: 'Close', stylingMode: 'contained', onClick: () => { dialog.hide(); }, }], }); dialog.show(); } }; const onAppointmentAdding = (e: DxSchedulerTypes.AppointmentAddingEvent) => { handleConflict(e, e.appointmentData as Appointment); }; const onAppointmentUpdating = (e: DxSchedulerTypes.AppointmentUpdatingEvent) => { handleConflict(e, { ...e.oldData, ...e.newData } as Appointment); }; const onFormInitialized = (e: DxFormTypes.InitializedEvent) => { form = e.component; e.component!.on('fieldDataChanged', (fieldEvent: { dataField: string }) => { if ( showConflictError && ['startDate', 'endDate', 'assigneeId', 'recurrenceRule'].includes(fieldEvent.dataField) ) { setConflictError(false); form?.validate(); } }); }; const customizeItem = (item: DxFormTypes.SimpleItem) => { if (item.name === 'allDayEditor' || item.name === 'recurrenceEndEditor') { item.label!.visible = true; } else if (item.name === 'subjectEditor') { item.editorOptions = item.editorOptions || {}; item.editorOptions.placeholder = 'Add title'; } if (item.name === 'startTimeEditor' || item.name === 'endTimeEditor') { item.validationRules = [ { type: 'required' }, { type: 'custom', message: 'Time conflict', ignoreEmptyValue: true, reevaluate: true, validationCallback: () => !showConflictError, }, ]; } }; const onOverlappingRuleChanged = (e: DxSelectBoxTypes.ValueChangedEvent) => { overlappingRule = e.value; }; </script> <style> .dx-scheduler-appointment { color: #242424; } .options { padding: 20px; background-color: rgba(191, 191, 191, 0.15); margin-top: 20px; } .option { margin-top: 10px; display: flex; align-items: center; gap: 10px; } .hide-informer .dx-item:has(.conflict-informer) { display: none !important; } .conflict-informer { background-color: #FCEAE8; color: #C50F1F; font-size: 12px; padding: 0 12px; height: 36px; line-height: 36px; box-sizing: border-box; margin-bottom: 8px; } .dx-dialog:has(#conflict-dialog) .dx-overlay-content { width: 280px; } .dx-dialog:has(#conflict-dialog) .dx-dialog-content { padding-bottom: 16px; } .dx-dialog:has(#conflict-dialog) .dx-dialog-buttons { padding-top: 0; padding-bottom: 16px; } .dx-dialog:has(#conflict-dialog) .dx-toolbar-center, .dx-dialog:has(#conflict-dialog) .dx-button { width: 100%; } .dx-scheduler-form-main-group .dx-item:last-child, .dx-scheduler-form-main-group .dx-item:last-child .dx-field-item-content, .dx-scheduler-form-main-group .dx-item:last-child .dx-item:last-child, .dx-scheduler-form-main-group .dx-item:last-child .dx-item:last-child .dx-field-item-content { overflow: visible; } </style>
window.exports = window.exports || {}; window.config = { transpiler: 'plugin-babel', meta: { '*.vue': { loader: 'vue-loader', }, '*.ts': { loader: 'demo-ts-loader', }, '*.svg': { loader: 'svg-loader', }, 'devextreme/time_zone_utils.js': { 'esModule': true, }, 'devextreme/localization.js': { 'esModule': true, }, 'devextreme/viz/palette.js': { 'esModule': true, }, 'openai': { 'esModule': true, }, 'zod': { 'esModule': true, }, 'zod-to-json-schema': { 'esModule': true, }, }, paths: { 'project:': '../../../../', 'npm:': 'https://cdn.jsdelivr.net/npm/', 'bundles:': '../../../../bundles/', 'externals:': '../../../../bundles/externals/', 'anti-forgery:': '../../../../shared/anti-forgery/', }, map: { 'anti-forgery': 'anti-forgery:fetch-override.js', 'vue': 'npm:vue@3.5.32/dist/vue.esm-browser.js', '@vue/shared': 'npm:@vue/shared@3.4.27/dist/shared.cjs.prod.js', 'vue-loader': 'npm:dx-systemjs-vue-browser@1.1.2/index.js', 'demo-ts-loader': 'project:utils/demo-ts-loader.js', 'jszip': 'npm:jszip@3.10.1/dist/jszip.min.js', 'svg-loader': 'project:utils/svg-loader.js', 'mitt': 'npm:mitt/dist/mitt.umd.js', 'rrule': 'npm:rrule@2.6.4/dist/es5/rrule.js', 'luxon': 'npm:luxon@3.4.4/build/global/luxon.min.js', 'es6-object-assign': 'npm:es6-object-assign', 'devextreme': 'npm:devextreme@link:../../packages/devextreme/artifacts/npm/devextreme/cjs', 'devextreme-vue': 'npm:devextreme-vue@link:../../packages/devextreme-vue/npm/cjs', 'zod': 'externals:zod.bundle.js', 'zod-to-json-schema': 'externals:zod-to-json-schema.bundle.js', 'devextreme-quill': 'npm:devextreme-quill@1.7.9/dist/dx-quill.min.js', 'devexpress-diagram': 'npm:devexpress-diagram@2.2.29/dist/dx-diagram.js', 'devexpress-gantt': 'npm:devexpress-gantt@4.1.69/dist/dx-gantt.js', 'inferno': 'npm:inferno@8.2.3/dist/inferno.min.js', 'inferno-compat': 'npm:inferno-compat/dist/inferno-compat.min.js', 'inferno-create-element': 'npm:inferno-create-element@8.2.3/dist/inferno-create-element.min.js', 'inferno-dom': 'npm:inferno-dom/dist/inferno-dom.min.js', 'inferno-hydrate': 'npm:inferno-hydrate/dist/inferno-hydrate.min.js', 'inferno-clone-vnode': 'npm:inferno-clone-vnode/dist/inferno-clone-vnode.min.js', 'inferno-create-class': 'npm:inferno-create-class/dist/inferno-create-class.min.js', 'inferno-extras': 'npm:inferno-extras/dist/inferno-extras.min.js', '@preact/signals-core': 'npm:@preact/signals-core@1.14.1/dist/signals-core.min.js', 'plugin-babel': 'npm:systemjs-plugin-babel@0.0.25/plugin-babel.js', 'systemjs-babel-build': 'npm:systemjs-plugin-babel@0.0.25/systemjs-babel-browser.js', // Prettier 'prettier/standalone': 'npm:prettier@2.8.8/standalone.js', 'prettier/parser-html': 'npm:prettier@2.8.8/parser-html.js', }, packages: { 'devextreme-vue': { main: 'index.js', }, 'devextreme-vue/common': { main: 'index.js', }, 'devextreme': { defaultExtension: 'js', }, 'devextreme/events/utils': { main: 'index', }, 'devextreme/common/core/events/utils': { main: 'index', }, 'devextreme/events': { main: 'index', }, 'es6-object-assign': { main: './index.js', defaultExtension: 'js', }, }, packageConfigPaths: [ 'npm:@devextreme/*/package.json', ], babelOptions: { sourceMaps: false, stage0: true, }, }; window.process = { env: { NODE_ENV: 'production', }, }; System.config(window.config); // eslint-disable-next-line const useTgzInCSB = ['openai']; let packagesInfo = { "zod": { "version": "3.24.4" }, "zod-to-json-schema": { "version": "3.24.6" } };
export interface Appointment { id: number; text: string; startDate: Date; endDate: Date; assigneeId: number[]; recurrenceRule?: string; allDay?: boolean; } export interface Assignee { id: number; text: string; color: string; } export const assignees: Assignee[] = [ { id: 1, text: 'Samantha Bright', color: '#A7E3A5' }, { id: 2, text: 'John Heart', color: '#CFE4FA' }, { id: 3, text: 'Todd Hoffman', color: '#F9E2AE' }, { id: 4, text: 'Sandra Johnson', color: '#F1BBBC' }, ]; export const data: Appointment[] = [ { id: 1, text: 'Website Re-Design Plan', startDate: new Date(2026, 1, 9, 9, 30), endDate: new Date(2026, 1, 9, 11, 30), assigneeId: [2], }, { id: 2, text: 'Install New Router in Dev Room', startDate: new Date(2026, 1, 9, 14, 30), endDate: new Date(2026, 1, 9, 15, 30), assigneeId: [3], }, { id: 3, text: 'Approve Personal Computer Upgrade Plan', startDate: new Date(2026, 1, 10, 10, 0), endDate: new Date(2026, 1, 10, 11, 0), assigneeId: [1], }, { id: 4, text: 'Final Budget Review', startDate: new Date(2026, 1, 10, 12, 0), endDate: new Date(2026, 1, 10, 13, 35), assigneeId: [1], }, { id: 5, text: 'Install New Database', startDate: new Date(2026, 1, 11, 9, 45), endDate: new Date(2026, 1, 11, 11, 15), assigneeId: [4], }, { id: 6, text: 'Approve New Online Marketing Strategy', startDate: new Date(2026, 1, 11, 12, 0), endDate: new Date(2026, 1, 11, 14, 0), assigneeId: [2], }, { id: 7, text: 'Prepare 2021 Marketing Plan', startDate: new Date(2026, 1, 12, 11, 0), endDate: new Date(2026, 1, 12, 13, 30), assigneeId: [3], }, { id: 8, text: 'Brochure Design Review', startDate: new Date(2026, 1, 12, 14, 0), endDate: new Date(2026, 1, 12, 15, 30), assigneeId: [2], }, { id: 9, text: 'Create Icons for Website', startDate: new Date(2026, 1, 13, 10, 0), endDate: new Date(2026, 1, 13, 11, 30), assigneeId: [1], }, { id: 10, text: 'Launch New Website', startDate: new Date(2026, 1, 13, 12, 20), endDate: new Date(2026, 1, 13, 14, 0), assigneeId: [4], }, { id: 11, text: 'Upgrade Server Hardware', startDate: new Date(2026, 1, 13, 14, 30), endDate: new Date(2026, 1, 13, 16, 0), assigneeId: [2], }, ];
import { createApp } from 'vue'; import App from './App.vue'; createApp(App).mount('#app');
<!DOCTYPE html> <html lang="en"> <head> <title>DevExtreme Demo</title> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0" /> <link rel="stylesheet" type="text/css" href="https://cdn3.devexpress.com/jslib/26.1.3/css/dx.light.css" /> <script src="https://cdn.jsdelivr.net/npm/typescript@5.9.3/lib/typescript.js"></script> <script type="module"> import * as vueCompilerSFC from "https://cdn.jsdelivr.net/npm/@vue/compiler-sfc@3.4.27/dist/compiler-sfc.esm-browser.js"; window.vueCompilerSFC = vueCompilerSFC; </script> <script src="https://cdn.jsdelivr.net/npm/core-js@2.6.12/client/shim.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/systemjs@0.21.3/dist/system.js"></script> <script type="text/javascript" src="config.js"></script> <script type="text/javascript"> System.import("./index.ts"); </script> </head> <body class="dx-viewport"> <div class="demo-container"> <div id="app"> </div> </div> </body> </html>

Detect Conflicts

Handle the onAppointmentAdding and onAppointmentUpdating events to check if a new or updated appointment creates a time conflict. Set e.cancel = true to block the operation when necessary.

Call getOccurrences to expand recurring appointments into individual occurrences within the target range. Check for overlapping time range values.

Conflict Detection Modes

The demo implements the following detection modes:

  • Different Resources: appointments assigned to different resources (assignees) can overlap.
  • Never: overlapping appointments are not allowed, regardless of resource assignment.

To implement resource-aware checks, access appointments and compare their assigneeId field values.

Display Errors

When a conflict is detected, the demo displays the error as follows:

  • A message box.
  • An inline validation message (if an appointment edit form is active).

To display inline validation, configure a custom form item inside editing.form and use the customizeItem function to attach custom validationRules to time editors.