Conflict Detection

This sample handles the FlexScheduler conflictChecked event and commits each candidate so the Scheduler can present current conflicts. After the collection refreshes, conflicting events receive the SchedulerClsNames.itemConflict class automatically.

FlexScheduler API Reference | SchedulerDefinition API Reference

import 'bootstrap.css'; import '@mescius/wijmo.styles/wijmo.css'; import './styles.css'; // import { CollectionView } from '@mescius/wijmo'; import * as wjcScheduler from '@mescius/wijmo.scheduler'; import { getDisplayDate, getEvents, getOtherRoomCandidate, getRooms, getSharedRoomCandidate, getUpdateValues, } from './data'; // document.readyState === 'loading' ? document.addEventListener('DOMContentLoaded', init) : init(); function init() { const eventView = new CollectionView(getEvents()); const candidateSelect = document.querySelector('#candidateSelect'); const tryButton = document.querySelector('#tryButton'); const status = document.querySelector('#conflictStatus'); let pendingAction = ''; let pendingConflictNames = []; const scheduler = new wjcScheduler.FlexScheduler('#theScheduler', { itemsSource: eventView, eventFields: { subject: 'subject', start: 'start', end: 'end', }, resources: [ { fieldExpr: 'roomId', label: 'Room', dataSource: getRooms(), valueExpr: 'id', displayExpr: 'name', backgroundColorExpr: 'color', foreColorExpr: 'textColor', }, ], definition: getWorkWeekDefinition(), displayDate: getDisplayDate(), workHours: [{ start: '08:00', end: '17:00' }], toolbarViewItems: [{ name: 'Work Week', key: wjcScheduler.SchedulerBuiltInViewItem.WorkWeek }], }); scheduler.conflictChecked.addHandler((_sender, args) => { const conflicts = args.result.conflicts; if (conflicts.length) { pendingConflictNames = Array.from(new Set(conflicts.flatMap((conflict) => conflict.type === wjcScheduler.SchedulerConflictType.TimeOverlap ? conflict.conflictingItems.map((item) => item.subject) : [conflict.item.subject]))); setStatus(`Detected before commit | Conflicts with: ${pendingConflictNames.join(', ')}`, 'conflict'); } }); scheduler.itemChanged.addHandler(() => { const message = pendingConflictNames.length ? `Committed ${pendingAction} | Conflicts highlighted: ${pendingConflictNames.join(', ')}` : `Completed ${pendingAction} | No conflict | ${getEventCount()} events`; setStatus(message, pendingConflictNames.length ? 'conflict' : 'completed'); pendingConflictNames = []; updateCandidateAvailability(); }); candidateSelect.addEventListener('change', updateCandidateAvailability); tryButton.addEventListener('click', () => { pendingConflictNames = []; switch (candidateSelect.value) { case 'other': pendingAction = 'add Support Handoff in Cedar Room'; scheduler.create(getOtherRoomCandidate()); break; case 'shared': pendingAction = 'add Roadmap Review in Atlas Room'; scheduler.create(getSharedRoomCandidate()); break; case 'update': { const item = getSourceItems().find((event) => event.id === 2); if (item) { pendingAction = 'update Client Prep in Atlas Room'; scheduler.update(item, getUpdateValues()); } break; } } }); function getSourceItems() { return eventView.sourceCollection; } function getEventCount() { return getSourceItems().length; } function updateCandidateAvailability() { const candidateId = candidateSelect.value === 'other' ? 101 : 102; const wasAdded = candidateSelect.value !== 'update' && getSourceItems().some((item) => item.id === candidateId); tryButton.disabled = wasAdded; tryButton.textContent = wasAdded ? 'Candidate committed' : 'Commit candidate'; } function setStatus(message, state) { const icon = document.createElement('i'); const text = document.createElement('span'); icon.className = `bi ${state === 'conflict' ? 'bi-exclamation-octagon-fill' : state === 'completed' ? 'bi-check-circle-fill' : 'bi-shield-check'}`; icon.setAttribute('aria-hidden', 'true'); text.textContent = message; status.replaceChildren(icon, text); status.className = `conflict-status is-${state}`; } } function getWorkWeekDefinition() { const definition = wjcScheduler.createSchedulerDefinitionPreset(wjcScheduler.SchedulerBuiltInViewItem.WorkWeek); return Object.assign(Object.assign({}, definition), { rows: { bindings: [ { type: wjcScheduler.SchedulerAxisBindingType.Time, unit: wjcScheduler.SchedulerTimeUnit.Hour, visible: (args) => args.hour >= 8 && args.hour < 17, }, ], } }); }
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title>MESCIUS Wijmo Scheduler Conflict Detection</title> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <link href="https://cdn.jsdelivr.net/npm/bootstrap-icons/font/bootstrap-icons.css" rel="stylesheet"> <!-- SystemJS --> <script src="https://cdn.jsdelivr.net/npm/systemjs@0.21.5/dist/system.src.js" integrity="sha512-skZbMyvYdNoZfLmiGn5ii6KmklM82rYX2uWctBhzaXPxJgiv4XBwJnFGr5k8s+6tE1pcR1nuTKghozJHyzMcoA==" crossorigin="anonymous"></script> <script src="systemjs.config.js"></script> <script> System.import('./src/app'); </script> </head> <body> <main class="container-fluid"> <div class="conflict-toolbar" role="group" aria-label="Conflict check controls"> <div class="conflict-controls"> <div class="field-group candidate-field"> <label for="candidateSelect">Candidate</label> <select id="candidateSelect" class="form-select form-select-sm"> <option value="other">Different room at same time (no conflict)</option> <option value="shared">Same room at same time (conflict)</option> <option value="update">Move existing event into conflict</option> </select> </div> <button id="tryButton" class="btn btn-primary btn-sm" type="button"><i class="bi bi-shield-check" aria-hidden="true"></i> Commit candidate</button> </div> <div class="conflict-feedback"> <span>Conflicts are detected before commit, then marked with the Scheduler conflict class</span> <p id="conflictStatus" class="conflict-status is-ready" role="status" aria-live="assertive"><i class="bi bi-shield-check" aria-hidden="true"></i><span>Ready | 6 events</span></p> </div> </div> <div id="theScheduler"></div> </main> </body> </html>
function createDate(day, hour, minute = 0) { return new Date(2026, 4, day, hour, minute); } export function getDisplayDate() { return createDate(13, 9); } export function getRooms() { return [ { id: 'atlas', name: 'Atlas Room', color: '#1d4ed8', textColor: '#ffffff' }, { id: 'cedar', name: 'Cedar Room', color: '#087f5b', textColor: '#ffffff' }, { id: 'studio', name: 'Design Studio', color: '#7e22ce', textColor: '#ffffff' }, { id: 'summit', name: 'Summit Room', color: '#a1420b', textColor: '#ffffff' }, ]; } export function getEvents() { return [ { id: 1, subject: 'Team Planning', start: createDate(12, 10), end: createDate(12, 11), roomId: 'atlas' }, { id: 2, subject: 'Client Prep', start: createDate(12, 13), end: createDate(12, 14), roomId: 'atlas' }, { id: 3, subject: 'Design Review', start: createDate(11, 14), end: createDate(11, 15), roomId: 'studio' }, { id: 4, subject: 'Customer Workshop', start: createDate(13, 9), end: createDate(13, 10, 30), roomId: 'summit', }, { id: 5, subject: 'Release Checkpoint', start: createDate(14, 11), end: createDate(14, 12), roomId: 'cedar' }, { id: 6, subject: 'Weekly Wrap-up', start: createDate(15, 15), end: createDate(15, 16), roomId: 'studio' }, ]; } export function getOtherRoomCandidate() { return { id: 101, subject: 'Support Handoff', start: createDate(12, 10, 30), end: createDate(12, 11, 30), roomId: 'cedar', }; } export function getSharedRoomCandidate() { return { id: 102, subject: 'Roadmap Review', start: createDate(12, 10, 30), end: createDate(12, 11, 30), roomId: 'atlas', }; } export function getUpdateValues() { return { subject: 'Client Prep - moved', start: createDate(12, 10, 15), end: createDate(12, 10, 45), }; }
body { color: #1f2937; font-size: 15px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; user-select: none; margin: 0; } .container-fluid { box-sizing: border-box; margin: 0; padding: 5px; } .conflict-toolbar { display: flex; flex-direction: column; gap: 5px; margin-bottom: 6px; padding: 6px 9px; border: 1px solid #dbe2ea; background: #f8fafc; } .conflict-controls, .conflict-feedback { display: flex; align-items: center; gap: 8px 12px; width: 100%; } .conflict-feedback { color: #526170; font-size: 13px; } .field-group { display: flex; align-items: center; gap: 7px; min-width: 0; } .field-group label { margin: 0; font-weight: 600; white-space: nowrap; } #modeSelect { width: 118px; } #candidateSelect { width: 286px; } .conflict-status { display: flex; min-height: 34px; align-items: center; gap: 8px; min-width: 0; margin: 0 0 1px auto; padding: 6px 10px; border: 1px solid transparent; border-radius: 4px; color: #334155; font-size: 13px; font-weight: 600; line-height: 1.35; } .conflict-status .bi { flex: 0 0 auto; font-size: 16px; } .conflict-status.is-completed { border-color: #86d7b0; background: #edf9f3; color: #146c43; } .conflict-status.is-conflict { border-color: #f0a4a8; background: #fff1f2; color: #a1101d; box-shadow: 0 0 0 3px rgb(180 35 24 / 8%); } .conflict-status.is-ready { border-color: #cbd5e1; background: #ffffff; color: #475569; } .form-select:focus, .btn:focus-visible { border-color: #0f6cbd; box-shadow: 0 0 0 2px rgb(15 108 189 / 22%); } #theScheduler { height: calc(100vh - 82px); min-height: 600px; } @media (max-width: 760px) { .conflict-controls { align-items: stretch; flex-direction: column; } .field-group { flex: 0 0 auto; width: 100%; } .field-group select, #modeSelect, #candidateSelect { width: 100%; } .field-group label { width: 70px; flex: 0 0 70px; } #tryButton { align-self: flex-start; } .conflict-status { flex: 0 0 auto; margin-left: 0; overflow-wrap: anywhere; } .conflict-feedback { align-items: flex-start; flex-direction: column; } #theScheduler { height: 680px; min-height: 680px; } } .wj-scheduler-item-conflict { outline: 3px solid #dc2626; outline-offset: -2px; box-shadow: 0 0 0 3px rgb(220 38 38 / 20%); animation: conflict-pulse 1.2s ease-in-out 2; } @keyframes conflict-pulse { 50% { filter: brightness(1.22) saturate(1.35); } } input, textarea, [contenteditable="true"] { user-select: text; }
(function (global) { System.config({ transpiler: 'plugin-babel', babelOptions: { es2015: true, }, meta: { '*.css': { loader: 'css' }, }, paths: { // paths serve as alias 'npm:': 'node_modules/', 'cdn:': 'https://cdn.mescius.io/demoapps/packages/wijmojs/5.20262.60-rc/', 'pcdn:': 'https://cdn.jsdelivr.net/npm/', }, // map tells the System loader where to look for things map: { '@mescius/wijmo': 'cdn:@mescius/wijmo/index.js', '@mescius/wijmo.input': 'cdn:@mescius/wijmo.input/index.js', '@mescius/wijmo.scheduler': 'cdn:@mescius/wijmo.scheduler/index.js', '@mescius/wijmo.styles': 'cdn:@mescius/wijmo.styles', '@mescius/wijmo.cultures': 'cdn:@mescius/wijmo.cultures', jszip: 'pcdn:jszip@3.8.0/dist/jszip.js', 'bootstrap.css': 'pcdn:bootstrap@5.3.8/dist/css/bootstrap.min.css', css: 'pcdn:systemjs-plugin-css@0.1.37/css.js', 'plugin-babel': 'pcdn:systemjs-plugin-babel@0.0.25/plugin-babel.js', 'systemjs-babel-build': 'pcdn:systemjs-plugin-babel@0.0.25/systemjs-babel-browser.js', }, packages: { src: { defaultExtension: 'js', }, node_modules: { defaultExtension: 'js', }, }, }); })(this);