DevExtreme v26.1 is now available.

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

Your search did not match any results.

React 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
import React, { useCallback, useMemo, useRef, } from 'react'; import Scheduler, { Form, Editing, Resource, Item, } from 'devextreme-react/scheduler'; import type { SchedulerTypes } from 'devextreme-react/scheduler'; import SelectBox from 'devextreme-react/select-box'; import type { SelectBoxTypes } from 'devextreme-react/select-box'; import type { FormTypes } from 'devextreme-react/form'; import type { PopupTypes } from 'devextreme-react/popup'; import type { TagBoxTypes } from 'devextreme-react/tag-box'; import { custom as customDialog } from 'devextreme/ui/dialog'; import { Template } from 'devextreme-react/core/template'; import { data, assignees, type Appointment, type Assignee } from './data.ts'; type dxScheduler = NonNullable<SchedulerTypes.InitializedEvent['component']>; type dxForm = NonNullable<FormTypes.InitializedEvent['component']>; type dxPopup = NonNullable<PopupTypes.InitializedEvent['component']>; const currentDate = new Date(2026, 1, 10); const views: SchedulerTypes.ViewType[] = ['day', 'week', 'workWeek', 'month']; 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: SchedulerTypes.Occurrence): Date { return (occurrence.appointmentData as Appointment).allDay ? getNextDay(occurrence.startDate) : occurrence.endDate; } function isOverlapping( a: SchedulerTypes.Occurrence, b: SchedulerTypes.Occurrence, overlappingRule: string, ): 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, overlappingRule: string, ): 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, overlappingRule), ), ); } const assigneeIdEditorOptions = { onValueChanged: (e: TagBoxTypes.ValueChangedEvent) => { if (e.value?.length > 1) { e.component.option('value', [e.value[e.value.length - 1]]); } }, tagTemplate: 'tagTemplate', }; const tagTemplate = (itemData: Assignee) => ( <div className="dx-tag-content" style={{ backgroundColor: itemData.color, borderColor: itemData.color }}> {itemData.text} <div className="dx-tag-remove-button"></div> </div> ); const conflictInformerRender = () => ( <div className="conflict-informer">This time slot conflicts with another appointment.</div> ); const App = () => { const popupRef = useRef<dxPopup | null>(null); const formRef = useRef<dxForm | null>(null); const showConflictErrorRef = useRef(false); const overlappingRuleRef = useRef('sameResource'); const setConflictError = useCallback((show: boolean) => { showConflictErrorRef.current = show; formRef.current?.option('elementAttr.class', show ? '' : 'hide-informer'); }, []); const handleConflict = useCallback(( e: SchedulerTypes.AppointmentAddingEvent | SchedulerTypes.AppointmentUpdatingEvent, appointmentData: Appointment, ) => { if (!detectConflict(e.component, appointmentData, overlappingRuleRef.current)) { setConflictError(false); return; } e.cancel = true; if (popupRef.current?.option('visible')) { setConflictError(true); formRef.current?.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(); } }, [setConflictError]); const onAppointmentAdding = useCallback((e: SchedulerTypes.AppointmentAddingEvent) => { handleConflict(e, e.appointmentData as Appointment); }, [handleConflict]); const onAppointmentUpdating = useCallback((e: SchedulerTypes.AppointmentUpdatingEvent) => { handleConflict(e, { ...e.oldData, ...e.newData } as Appointment); }, [handleConflict]); const popupOptions = useMemo(() => ({ onInitialized: (e: PopupTypes.InitializedEvent) => { popupRef.current = e.component ?? null; }, onHidden: () => { setConflictError(false); }, }), [setConflictError]); const onFormInitialized = useCallback((e: FormTypes.InitializedEvent) => { if (!e.component) return; formRef.current = e.component; e.component.on('fieldDataChanged', (fieldEvent: FormTypes.FieldDataChangedEvent) => { if ( showConflictErrorRef.current && ['startDate', 'endDate', 'assigneeId', 'recurrenceRule'].includes(fieldEvent.dataField ?? '') ) { setConflictError(false); formRef.current?.validate(); } }); }, [setConflictError]); const customizeItem = useCallback((item: FormTypes.SimpleItem) => { if (item.name === 'allDayEditor' || item.name === 'recurrenceEndEditor') { item.label = { ...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: () => !showConflictErrorRef.current, }, ]; } }, []); return ( <> <Scheduler dataSource={data} views={views} defaultCurrentView="week" defaultCurrentDate={currentDate} startDayHour={9} endDayHour={19} height={600} showAllDayPanel={false} allDayPanelMode="hidden" onAppointmentAdding={onAppointmentAdding} onAppointmentUpdating={onAppointmentUpdating} > <Resource fieldExpr="assigneeId" dataSource={assignees} valueExpr="id" colorExpr="color" icon="user" allowMultiple={true} /> <Editing popup={popupOptions}> <Form labelMode="hidden" onInitialized={onFormInitialized} customizeItem={customizeItem} elementAttr={{ class: 'hide-informer', id: 'form' }} > <Item name="conflictInformer" render={conflictInformerRender} /> <Item type="group" name="mainGroup"> <Item name="subjectGroup" /> <Item name="dateGroup" /> <Item name="repeatGroup" /> <Item name="assigneeIdGroup"> <Item name="assigneeIdIcon" /> <Item name="assigneeIdEditor" isRequired={true} editorOptions={assigneeIdEditorOptions} /> </Item> </Item> <Item type="group" name="recurrenceGroup" /> </Form> </Editing> <Template name="tagTemplate" render={tagTemplate} /> </Scheduler> <div className="options"> <div className="option"> <span>Allow Overlapping Appointments</span> <SelectBox items={overlappingRuleItems} valueExpr="value" displayExpr="text" defaultValue="sameResource" onValueChanged={(e: SelectBoxTypes.ValueChangedEvent) => { overlappingRuleRef.current = e.value; }} /> </div> </div> </> ); }; export default App;
import React, { useCallback, useMemo, useRef } from 'react'; import Scheduler, { Form, Editing, Resource, Item, } from 'devextreme-react/scheduler'; import SelectBox from 'devextreme-react/select-box'; import { custom as customDialog } from 'devextreme/ui/dialog'; import { Template } from 'devextreme-react/core/template'; import { data, assignees } from './data.js'; const currentDate = new Date(2026, 1, 10); const views = ['day', 'week', 'workWeek', 'month']; const overlappingRuleItems = [ { value: 'sameResource', text: 'Different Resources' }, { value: 'allResources', text: 'Never' }, ]; function getNextDay(date) { const next = new Date(date); next.setDate(next.getDate() + 1); return next; } function getEndDate(occurrence) { return occurrence.appointmentData.allDay ? getNextDay(occurrence.startDate) : occurrence.endDate; } function isOverlapping(a, b, overlappingRule) { const aEnd = getEndDate(a); const bEnd = getEndDate(b); if (a.startDate >= bEnd || b.startDate >= aEnd) return false; if (overlappingRule === 'sameResource') { return a.appointmentData.assigneeId[0] === b.appointmentData.assigneeId[0]; } return true; } function detectConflict(scheduler, newAppointment, overlappingRule) { const allAppointments = scheduler.getDataSource().items(); const startDate = new Date(newAppointment.startDate); let endDate; 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.id !== newAppointment.id); const newOccurrences = scheduler.getOccurrences(startDate, endDate, [newAppointment]); return newOccurrences.some((newOccurrence) => existingOccurrences.some((existingOccurrence) => isOverlapping(newOccurrence, existingOccurrence, overlappingRule), ), ); } const assigneeIdEditorOptions = { onValueChanged: (e) => { if (e.value?.length > 1) { e.component.option('value', [e.value[e.value.length - 1]]); } }, tagTemplate: 'tagTemplate', }; const tagTemplate = (itemData) => ( <div className="dx-tag-content" style={{ backgroundColor: itemData.color, borderColor: itemData.color }} > {itemData.text} <div className="dx-tag-remove-button"></div> </div> ); const conflictInformerRender = () => ( <div className="conflict-informer">This time slot conflicts with another appointment.</div> ); const App = () => { const popupRef = useRef(null); const formRef = useRef(null); const showConflictErrorRef = useRef(false); const overlappingRuleRef = useRef('sameResource'); const setConflictError = useCallback((show) => { showConflictErrorRef.current = show; formRef.current?.option('elementAttr.class', show ? '' : 'hide-informer'); }, []); const handleConflict = useCallback( (e, appointmentData) => { if (!detectConflict(e.component, appointmentData, overlappingRuleRef.current)) { setConflictError(false); return; } e.cancel = true; if (popupRef.current?.option('visible')) { setConflictError(true); formRef.current?.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(); } }, [setConflictError], ); const onAppointmentAdding = useCallback( (e) => { handleConflict(e, e.appointmentData); }, [handleConflict], ); const onAppointmentUpdating = useCallback( (e) => { handleConflict(e, { ...e.oldData, ...e.newData }); }, [handleConflict], ); const popupOptions = useMemo( () => ({ onInitialized: (e) => { popupRef.current = e.component ?? null; }, onHidden: () => { setConflictError(false); }, }), [setConflictError], ); const onFormInitialized = useCallback( (e) => { if (!e.component) return; formRef.current = e.component; e.component.on('fieldDataChanged', (fieldEvent) => { if ( showConflictErrorRef.current && ['startDate', 'endDate', 'assigneeId', 'recurrenceRule'].includes( fieldEvent.dataField ?? '', ) ) { setConflictError(false); formRef.current?.validate(); } }); }, [setConflictError], ); const customizeItem = useCallback((item) => { if (item.name === 'allDayEditor' || item.name === 'recurrenceEndEditor') { item.label = { ...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: () => !showConflictErrorRef.current, }, ]; } }, []); return ( <> <Scheduler dataSource={data} views={views} defaultCurrentView="week" defaultCurrentDate={currentDate} startDayHour={9} endDayHour={19} height={600} showAllDayPanel={false} allDayPanelMode="hidden" onAppointmentAdding={onAppointmentAdding} onAppointmentUpdating={onAppointmentUpdating} > <Resource fieldExpr="assigneeId" dataSource={assignees} valueExpr="id" colorExpr="color" icon="user" allowMultiple={true} /> <Editing popup={popupOptions}> <Form labelMode="hidden" onInitialized={onFormInitialized} customizeItem={customizeItem} elementAttr={{ class: 'hide-informer', id: 'form' }} > <Item name="conflictInformer" render={conflictInformerRender} /> <Item type="group" name="mainGroup" > <Item name="subjectGroup" /> <Item name="dateGroup" /> <Item name="repeatGroup" /> <Item name="assigneeIdGroup"> <Item name="assigneeIdIcon" /> <Item name="assigneeIdEditor" isRequired={true} editorOptions={assigneeIdEditorOptions} /> </Item> </Item> <Item type="group" name="recurrenceGroup" /> </Form> </Editing> <Template name="tagTemplate" render={tagTemplate} /> </Scheduler> <div className="options"> <div className="option"> <span>Allow Overlapping Appointments</span> <SelectBox items={overlappingRuleItems} valueExpr="value" displayExpr="text" defaultValue="sameResource" onValueChanged={(e) => { overlappingRuleRef.current = e.value; }} /> </div> </div> </> ); }; export default App;
import React from 'react'; import ReactDOM from 'react-dom'; import App from './App.tsx'; ReactDOM.render( <App />, document.getElementById('app'), );
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], }, ];
window.exports = window.exports || {}; window.config = { transpiler: 'ts', typescriptOptions: { module: 'system', emitDecoratorMetadata: true, experimentalDecorators: true, jsx: 'react', }, meta: { 'react': { 'esModule': true, }, 'typescript': { 'exports': 'ts', }, '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: { 'npm:': 'https://cdn.jsdelivr.net/npm/', 'bundles:': '../../../../bundles/', 'externals:': '../../../../bundles/externals/', 'anti-forgery:': '../../../../shared/anti-forgery/', }, defaultExtension: 'js', map: { 'anti-forgery': 'anti-forgery:fetch-override.js', 'ts': 'npm:plugin-typescript@8.0.0/lib/plugin.js', 'typescript': 'npm:typescript@4.2.4/lib/typescript.js', 'jszip': 'npm:jszip@3.10.1/dist/jszip.min.js', 'react': 'npm:react@17.0.2/umd/react.development.js', 'react-dom': 'npm:react-dom@17.0.2/umd/react-dom.development.js', 'prop-types': 'npm:prop-types/prop-types.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-react': 'npm:devextreme-react@link:../../packages/devextreme-react/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', 'devextreme-cldr-data': 'npm:devextreme-cldr-data@1.0.3', // SystemJS plugins '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': { defaultExtension: 'js', }, 'devextreme-react': { main: 'index.js', }, 'devextreme-react/common': { main: 'index.js', }, 'devextreme/events/utils': { main: 'index', }, 'devextreme/common/core/events/utils': { main: 'index', }, 'devextreme/localization/messages': { format: 'json', defaultExtension: 'json', }, 'devextreme/events': { main: 'index', }, 'es6-object-assign': { main: './index.js', defaultExtension: 'js', }, }, packageConfigPaths: [ 'npm:@devextreme/*/package.json', ], babelOptions: { sourceMaps: false, stage0: true, react: 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" } };
import React from 'react'; import ReactDOM from 'react-dom'; import App from './App.js'; ReactDOM.render(<App />, document.getElementById('app'));
export const assignees = [ { 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 = [ { 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], }, ];
<!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" /> <link rel="stylesheet" type="text/css" href="styles.css" /> <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.tsx"); </script> </head> <body class="dx-viewport"> <div class="demo-container"> <div id="app"></div> </div> </body> </html>
.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%; }

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.