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 { createDispatchEvent, getDisplayDate, getEvents, getTeams } from './data';
//
const avatarUrls = {
'Maya Chen': 'resources/avatars/maya-chen.png',
'Noah Reed': 'resources/avatars/noah-reed.png',
'Priya Shah': 'resources/avatars/priya-shah.png',
'Liam Brooks': 'resources/avatars/liam-brooks.png',
'Sofia Martinez': 'resources/avatars/sofia-martinez.png',
'Ethan Cole': 'resources/avatars/ethan-cole.png',
};
const statusLabels = {
scheduled: 'Scheduled',
enRoute: 'En route',
onSite: 'On site',
complete: 'Complete',
};
const statusProgress = {
scheduled: 10,
enRoute: 35,
onSite: 70,
complete: 100,
};
document.readyState === 'loading' ? document.addEventListener('DOMContentLoaded', init) : init();
function init() {
const eventView = new CollectionView(getEvents());
const toggleButton = document.querySelector('#toggleLiveButton');
const nextButton = document.querySelector('#nextUpdateButton');
const indicator = document.querySelector('#liveIndicator');
const status = document.querySelector('#liveStatus');
const history = document.querySelector('#updateHistory');
const historyCount = document.querySelector('#historyCount');
let updateIndex = 0;
let appliedUpdates = 0;
let isLive = true;
const scheduler = new wjcScheduler.FlexScheduler('#theScheduler', {
itemsSource: eventView,
resources: [
{
fieldExpr: 'teamId',
label: 'Team',
dataSource: getTeams(),
valueExpr: 'id',
displayExpr: 'name',
backgroundColorExpr: 'color',
foreColorExpr: 'textColor',
},
],
definition: getDayDefinition(renderEvent),
displayDate: getDisplayDate(),
workHours: [{ start: '08:00', end: '18:00' }],
toolbarViewItems: [{ name: 'Day', key: wjcScheduler.SchedulerBuiltInViewItem.Day }],
});
scheduler.itemChanging.addHandler((_sender, args) => initializeEventPriority(args));
const applyNextUpdate = () => {
const items = eventView.sourceCollection;
const item = items[updateIndex % items.length];
Object.assign(item, createDispatchEvent(item));
const statuses = ['scheduled', 'enRoute', 'onSite', 'complete'];
const nextStatus = statuses[(statuses.indexOf(item.status) + 1) % statuses.length];
const previousStatus = item.status;
eventView.editItem(item);
item.status = nextStatus;
item.progress = statusProgress[nextStatus];
eventView.commitEdit();
updateIndex++;
appliedUpdates++;
status.value = `${appliedUpdates} applied | ${item.subject}: ${statusLabels[nextStatus]}`;
prependHistory(history, historyCount, item, previousStatus, nextStatus, appliedUpdates);
};
const timer = window.setInterval(() => {
if (isLive) {
applyNextUpdate();
}
}, 4500);
toggleButton.addEventListener('click', () => {
isLive = !isLive;
indicator.classList.toggle('is-live', isLive);
updateToggleButton(toggleButton, isLive);
status.value = `${appliedUpdates} applied | Feed ${isLive ? 'running' : 'paused'}`;
});
nextButton.addEventListener('click', applyNextUpdate);
window.addEventListener('unload', () => window.clearInterval(timer), { once: true });
}
function updateToggleButton(button, isLive) {
const icon = document.createElement('i');
const label = document.createElement('span');
icon.className = `bi ${isLive ? 'bi-pause-fill' : 'bi-play-fill'}`;
icon.setAttribute('aria-hidden', 'true');
label.textContent = isLive ? 'Pause' : 'Resume';
button.replaceChildren(icon, label);
}
function getDayDefinition(itemTemplate) {
const definition = wjcScheduler.createSchedulerDefinitionPreset(wjcScheduler.SchedulerBuiltInViewItem.Day);
return Object.assign(Object.assign({}, definition), { itemTemplate, rows: {
bindings: [
{
type: wjcScheduler.SchedulerAxisBindingType.Time,
unit: wjcScheduler.SchedulerTimeUnit.Hour,
visible: (args) => args.hour >= 8 && args.hour < 18,
},
],
} });
}
function renderEvent(ctx, element) {
const item = ctx.item;
Object.assign(item, createDispatchEvent(item));
const header = document.createElement('span');
const title = document.createElement('strong');
const ticket = document.createElement('span');
const state = document.createElement('span');
const details = document.createElement('span');
const technician = document.createElement('span');
const technicianImage = document.createElement('img');
const progress = document.createElement('span');
const progressValue = document.createElement('span');
header.className = 'dispatch-header';
title.textContent = item.subject;
ticket.className = 'dispatch-ticket';
ticket.textContent = item.ticket;
header.append(title, ticket);
state.className = `dispatch-state state-${item.status}`;
state.textContent = statusLabels[item.status];
details.className = 'dispatch-details';
technician.className = 'dispatch-technician';
technicianImage.src = avatarUrls[item.technician] || '';
technicianImage.alt = item.technician;
technicianImage.addEventListener('error', () => technicianImage.remove(), { once: true });
technician.append(technicianImage, document.createTextNode(item.technician));
details.append(technician, document.createTextNode(` | ${item.location}`));
progress.className = 'dispatch-progress';
progressValue.style.width = `${item.progress}%`;
progress.appendChild(progressValue);
element.textContent = '';
element.dataset.priority = item.priority.toLocaleLowerCase();
element.append(header, state, details, progress);
return null;
}
function initializeEventPriority(args) {
if (args.request.action !== wjcScheduler.SchedulerItemChangeAction.Create || !args.request.values)
return;
Object.assign(args.request.values, createDispatchEvent(args.request.values));
}
function prependHistory(history, count, item, previousStatus, nextStatus, updateNumber) {
var _a, _b;
const entry = document.createElement('li');
const marker = document.createElement('span');
const content = document.createElement('div');
const title = document.createElement('strong');
const transition = document.createElement('span');
const meta = document.createElement('small');
entry.className = `history-entry history-${nextStatus}`;
marker.className = 'history-marker';
title.textContent = item.subject;
transition.textContent = `${statusLabels[previousStatus]} -> ${statusLabels[nextStatus]}`;
meta.textContent = `Update ${updateNumber} | ${item.ticket} | ${item.technician}`;
content.append(title, transition, meta);
entry.append(marker, content);
(_a = history.querySelector('.empty-history')) === null || _a === void 0 ? void 0 : _a.remove();
history.prepend(entry);
while (history.children.length > 10)
(_b = history.lastElementChild) === null || _b === void 0 ? void 0 : _b.remove();
count.textContent = `${updateNumber} ${updateNumber === 1 ? 'change' : 'changes'}`;
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>MESCIUS Wijmo Scheduler Live Updates</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">
<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="live-toolbar">
<div class="live-heading">
<span id="liveIndicator" class="live-indicator is-live" aria-hidden="true"></span>
<div>
<strong>Operations feed</strong>
<span>Simulated external changes are committed through CollectionView</span>
</div>
</div>
<div class="live-actions" role="group" aria-label="Live update controls">
<button id="toggleLiveButton" class="btn btn-sm btn-outline-primary" type="button">
<i class="bi bi-pause-fill" aria-hidden="true"></i><span>Pause</span>
</button>
<button id="nextUpdateButton" class="btn btn-sm btn-primary" type="button">
<i class="bi bi-arrow-repeat" aria-hidden="true"></i><span>Apply next update</span>
</button>
</div>
<output id="liveStatus" aria-live="polite">0 updates applied</output>
</div>
<div class="live-workspace">
<div id="theScheduler"></div>
<aside class="update-panel" aria-label="Update history">
<div class="update-panel-header">
<div><i class="bi bi-activity" aria-hidden="true"></i><strong>Update history</strong></div>
<span id="historyCount">0 changes</span>
</div>
<ol id="updateHistory" class="update-history" aria-live="polite">
<li class="empty-history">Updates will appear here as the external feed changes events.</li>
</ol>
</aside>
</div>
</main>
</body>
</html>
export const DefaultDispatchEvent = {
status: 'scheduled',
ticket: 'WO-1851',
technician: 'Priya Shah',
location: 'Harbor Office',
progress: 0,
priority: 'Standard',
};
export function createDispatchEvent(values) {
return Object.assign(Object.assign({}, values), { status: values.status || DefaultDispatchEvent.status, ticket: values.ticket || DefaultDispatchEvent.ticket, technician: values.technician || DefaultDispatchEvent.technician, location: values.location || DefaultDispatchEvent.location, progress: typeof values.progress === 'number' ? values.progress : DefaultDispatchEvent.progress, priority: values.priority || DefaultDispatchEvent.priority });
}
function date(day, hour, minute = 0) {
return new Date(2026, 4, day, hour, minute);
}
export function getDisplayDate() {
return date(13, 9);
}
export function getTeams() {
return [
{ id: 'north', name: 'North team', color: '#2563eb', textColor: '#ffffff' },
{ id: 'central', name: 'Central team', color: '#0f766e', textColor: '#ffffff' },
{ id: 'south', name: 'South team', color: '#a3460a', textColor: '#ffffff' },
];
}
export function getEvents() {
const events = [
{
id: 1,
subject: 'Router Installation',
start: date(13, 8, 30),
end: date(13, 10),
teamId: 'north',
status: 'enRoute',
ticket: 'WO-1842',
technician: 'Maya Chen',
location: 'Riverside Branch',
progress: 30,
priority: 'Priority',
},
{
id: 2,
subject: 'Fiber Inspection',
start: date(13, 10),
end: date(13, 11, 30),
teamId: 'central',
status: 'onSite',
ticket: 'WO-1847',
technician: 'Noah Reed',
location: 'Market Street Hub',
progress: 65,
priority: 'Standard',
},
{
id: 3,
subject: 'Branch Site Survey',
start: date(13, 12),
end: date(13, 13, 30),
teamId: 'south',
status: 'scheduled',
ticket: 'WO-1851',
technician: 'Priya Shah',
location: 'Harbor Office',
progress: 10,
priority: 'Standard',
},
{
id: 4,
subject: 'Access Point Upgrade',
start: date(13, 14),
end: date(13, 15, 30),
teamId: 'north',
status: 'scheduled',
ticket: 'WO-1854',
technician: 'Liam Brooks',
location: 'North Campus',
progress: 10,
priority: 'Priority',
},
{
id: 5,
subject: 'Customer Handoff',
start: date(13, 15),
end: date(13, 16, 30),
teamId: 'central',
status: 'scheduled',
ticket: 'WO-1858',
technician: 'Sofia Martinez',
location: 'Central Plaza',
progress: 10,
priority: 'Standard',
},
{
id: 6,
subject: 'Emergency Repair',
start: date(13, 16),
end: date(13, 18),
teamId: 'south',
status: 'scheduled',
ticket: 'INC-932',
technician: 'Ethan Cole',
location: 'Airport Terminal',
progress: 10,
priority: 'Critical',
},
];
return events.map((event) => createDispatchEvent(event));
}
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;
}
.live-toolbar {
display: flex;
align-items: center;
gap: 10px 18px;
min-height: 56px;
margin-bottom: 6px;
padding: 6px 10px;
border: 1px solid #dbe2ea;
background: #f8fafc;
}
.live-heading {
display: flex;
align-items: center;
gap: 9px;
}
.live-heading > div {
display: flex;
flex-direction: column;
}
.live-heading span:not(.live-indicator) {
color: #64748b;
font-size: 12px;
}
.live-indicator {
width: 9px;
height: 9px;
border: 2px solid #94a3b8;
border-radius: 50%;
background: #ffffff;
}
.live-indicator.is-live {
border-color: #16825d;
background: #24a778;
box-shadow: 0 0 0 3px rgb(36 167 120 / 14%);
}
.live-actions {
display: flex;
gap: 6px;
margin-left: auto;
}
.live-actions .btn {
display: inline-flex;
align-items: center;
gap: 6px;
}
#liveStatus {
min-width: 230px;
color: #475569;
font-size: 13px;
}
.btn:focus-visible {
border-color: #0f6cbd;
box-shadow: 0 0 0 2px rgb(15 108 189 / 22%);
}
.dispatch-header,
.dispatch-details {
display: flex;
overflow: hidden;
align-items: center;
gap: 5px;
text-overflow: ellipsis;
white-space: nowrap;
}
.dispatch-header strong {
overflow: hidden;
text-overflow: ellipsis;
}
.dispatch-ticket {
margin-left: auto;
font-size: 10px;
opacity: .82;
}
.dispatch-state {
display: inline-block;
margin-top: 2px;
padding: 1px 5px;
border: 1px solid rgb(255 255 255 / 42%);
border-radius: 8px;
font-size: 11px;
font-weight: 600;
}
.dispatch-details {
margin-top: 2px;
font-size: 10px;
opacity: .9;
}
.dispatch-technician {
display: inline-flex;
align-items: center;
gap: 4px;
}
.dispatch-technician img {
width: 18px;
height: 18px;
border: 1px solid rgb(255 255 255 / 75%);
border-radius: 50%;
object-fit: cover;
flex: 0 0 auto;
}
.dispatch-progress {
display: block;
overflow: hidden;
height: 4px;
margin-top: 4px;
border-radius: 2px;
background: rgb(255 255 255 / 28%);
}
.dispatch-progress > span {
display: block;
height: 100%;
background: #ffffff;
transition: width .25s ease;
}
.live-workspace {
display: grid;
grid-template-columns: minmax(0, 1fr) 300px;
border: 1px solid #cbd5e1;
}
.update-panel {
min-width: 0;
max-height: calc(100vh - 64px);
overflow: auto;
border-left: 1px solid #cbd5e1;
background: #f8fafc;
}
.update-panel-header {
position: sticky;
top: 0;
z-index: 1;
display: flex;
min-height: 48px;
align-items: center;
justify-content: space-between;
padding: 0 12px;
border-bottom: 1px solid #dbe2ea;
background: #eef3f8;
}
.update-panel-header > div {
display: flex;
align-items: center;
gap: 7px;
}
.update-panel-header span {
color: #64748b;
font-size: 12px;
}
.update-history {
margin: 0;
padding: 0;
list-style: none;
}
.empty-history {
padding: 18px 12px;
color: #64748b;
font-size: 13px;
line-height: 1.45;
}
.history-entry {
display: grid;
grid-template-columns: 10px minmax(0, 1fr);
gap: 8px;
padding: 11px 12px;
border-bottom: 1px solid #e2e8f0;
background: #ffffff;
}
.history-marker {
width: 8px;
height: 8px;
margin-top: 5px;
border-radius: 50%;
background: #64748b;
}
.history-entry > div {
display: flex;
min-width: 0;
flex-direction: column;
gap: 2px;
}
.history-entry strong {
overflow: hidden;
color: #0f172a;
font-size: 13px;
text-overflow: ellipsis;
white-space: nowrap;
}
.history-entry span:not(.history-marker) {
color: #334155;
font-size: 12px;
font-weight: 600;
}
.history-entry small {
color: #64748b;
font-size: 11px;
}
.history-enRoute .history-marker {
background: #2563eb;
}
.history-onSite .history-marker {
background: #b45309;
}
.history-complete .history-marker {
background: #16825d;
}
.history-scheduled .history-marker {
background: #64748b;
}
#theScheduler {
min-width: 0;
height: calc(100vh - 66px);
min-height: 600px;
}
@media (max-width: 760px) {
.live-toolbar {
align-items: flex-start;
flex-wrap: wrap;
}
.live-heading {
flex: 1 1 100%;
}
.live-actions {
margin-left: 0;
}
#liveStatus {
flex: 1 1 100%;
min-width: 0;
}
#theScheduler {
height: 680px;
min-height: 680px;
}
.live-workspace {
grid-template-columns: 1fr;
}
.update-panel {
max-height: 340px;
border-top: 1px solid #cbd5e1;
border-left: 0;
}
}
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);