Overview
Dual Task Versions: Display both a planned and an actual version of each task on a single timeline.
Locked Planning View: Lock the planned tasks once finalized, preventing unintended changes and preserving a clear baseline.
Real-Time Comparison: Easily track deviations from the original plan by comparing task durations side by side.
Simple Mode Switching: Use a toggle switch to move between “planning” and “tracking” modes.
User-Friendly UI: Built with DayPilot React Scheduler component which lets you create an interactive timeline with drag-and-drop scheduling.
The Next.js project includes a trial version of DayPilot Pro for JavaScript (see License below).
License
Licensed for testing and evaluation purposes. Please see the license agreement included in the sample project. You can use the source code of the tutorial if you are a licensed user of DayPilot Pro for JavaScript. Buy a license.
Planning View: Next.js Scheduler with a Timeline for Multiple Resources
The planning view uses the React Scheduler UI component to define the task baseline (including the assigned resource, start, and duration).
For an introduction to using the React Scheduler in Next.js, please see Next.js Scheduler with DayPilot: A Step-by-Step Guide. It explains how to install and configure the React Scheduler component in a Next.js application. In this tutorial, we will focus on configuring the Scheduler to display different versions of tasks side by side.
In the planning view, we will display the planned tasks in an edit mode that allows modification using drag and drop.
The planned tasks are defined as a simple static array that is stored in the planned
state variable:
const [planned, setPlanned] = useState<DayPilot.EventData[]>([]);
const [events, setEvents] = useState<DayPilot.EventData[]>([]);
useEffect(() => {
// ...
setPlanned([
{
id: 1,
text: "Task 1",
start: "2025-05-02T00:00:00",
end: "2025-05-05T00:00:00",
resource: "A",
},
// ...
]);
}, []);
The <DayPilotScheduler>
tag defined in the component JSX loads the event data from the planned
state variable using the events
prop:
<DayPilotScheduler
events={planned}
...
/>
Tracking View: Use Event Versions to Display Planned Tasks
After switching to the tracking view which locks the plan, the Scheduler displays planned tasks as dedicated read-only bars with a lock icon. The actual task execution is displayed using the standard event boxes that allow drag-and-drop updates.
The tracking view can be activated by toggling the “Plan Locked” switch:
<ToggleSwitch label="Plan Locked" value={locked} onChange={setLocked} />
When the tracking view is active, we use an array with the actual task details as the data source. We will extend the data source with information about the planned tasks. Each planned task will be displayed as a read-only version above the main event.
1. Enable Event Versions
The event versions need to be enabled in the Scheduler component using the eventVersionsEnabled
property.
<DayPilotScheduler
eventVersionsEnabled={true}
...
/>
2. Adjust the Version Size and Position
Now we can tune the version appearance: We will display the planned version above the main task (eventVersionPosition={"Above”}
) and set the height to 20 pixels (eventVersionHeight={20}
):
<DayPilotScheduler
...
eventVersionsEnabled={true}
eventVersionHeight={20}
eventVersionPosition={"Above"}
...
/>
3. Define Event Versions
A version can be defined using the versions
property of an event data object. It’s an array that allows defining multiple versions for each event. Our app defines just a single version.
setEvents([
{
id: 1,
text: "Task 1",
start: "2025-05-02T00:00:00",
end: "2025-05-05T00:00:00",
resource: "A",
versions: [
{
start: "2025-05-02T00:00:00",
end: "2025-05-05T00:00:00",
}
]
},
// ...
]);
The only required properties of an event version are start
and end
. Versions are read-only and they are always displayed in the same row as the main event.
4. Load Event Versions
The versions can be sent from the server side as part of the event data. That would be the most common scenario.
In our example, we define two standalone data sets:
planned tasks (
planned
)actual tasks (
actual
)
When locking the plan using the switch, we use the actual
data set and add the corresponding planned task from the planned
data set as a version:
useEffect(() => {
// ...
setPlanned([
{
id: 1,
text: "Task 1",
start: "2025-05-02T00:00:00",
end: "2025-05-05T00:00:00",
resource: "A",
},
// ...
]);
setActual([
{
id: 1,
text: "Task 1",
start: "2025-05-02T00:00:00",
end: "2025-05-05T00:00:00",
resource: "A",
},
// ...
]);
}, [scheduler]);
useEffect(() => {
const events = locked ? actual : planned;
if (locked) {
events.forEach(actualEvent => {
const plannedEvent = planned.find(plannedEvent => plannedEvent.id === actualEvent.id);
if (plannedEvent) {
actualEvent.versions = [{
start: plannedEvent.start,
end: plannedEvent.end,
backColor: actualEvent.barColor || "#1066a8",
barHidden: true,
areas: [
{top: 3, left: 5, width: 14, height: 14, fontColor: "#ffffff", symbol: "icons/daypilot.svg#padlock"}
]
}];
actualEvent.moveVDisabled = true;
}
});
}
setEvents(events);
}, [locked, planned, actual]);
To adjust the appearance of the planned tasks, we have specified additional properties (backColor
, barHidden
). We also add a padlock icon using an active area.
In the tracking view, it is only possible to change the event start and duration, but not the assigned resource. That’s why we limit the drag and drop moving to the horizontal direction (along the timeline) using moveVDisabled
.
Full Source Code
Here is the full source code of our Next.js scheduling component that displays the task baseline in addition to the current state.
'use client';
import React, {useEffect, useState} from 'react';
import {DayPilot, DayPilotScheduler} from "daypilot-pro-react";
import ToggleSwitch from "./ToggleSwitch";
import "./Scheduler.css";
const Scheduler = () => {
const [locked, setLocked] = useState(false);
const [actual, setActual] = useState<DayPilot.EventData[]>([]);
const [planned, setPlanned] = useState<DayPilot.EventData[]>([]);
const [resources, setResources] = useState<DayPilot.ResourceData[]>([]);
const [events, setEvents] = useState<DayPilot.EventData[]>([]);
const [startDate, setStartDate] = useState("2025-01-01");
const [days, setDays] = useState(365);
const [scheduler, setScheduler] = useState<DayPilot.Scheduler>();
const onTimeRangeSelected = async (args: DayPilot.SchedulerTimeRangeSelectedArgs) => {
const dp = args.control;
const modal = await DayPilot.Modal.prompt("Create a new task:", "Task 1");
dp.clearSelection();
if (modal.canceled) { return; }
dp.events.add({
start: args.start,
end: args.end,
id: DayPilot.guid(),
resource: args.resource,
text: modal.result
});
};
useEffect(() => {
if (!scheduler) {
return;
}
setResources([
{name: "Resource A", id: "A"},
{name: "Resource B", id: "B"},
{name: "Resource C", id: "C"},
{name: "Resource D", id: "D"},
{name: "Resource E", id: "E"},
{name: "Resource F", id: "F"},
{name: "Resource G", id: "G"}
]);
setPlanned([
{
id: 1,
text: "Task 1",
start: "2025-05-02T00:00:00",
end: "2025-05-05T00:00:00",
resource: "A",
},
{
id: 2,
text: "Task 2",
start: "2025-05-03T00:00:00",
end: "2025-05-10T00:00:00",
resource: "C",
barColor: "#38761d",
barBackColor: "#93c47d"
},
{
id: 3,
text: "Task 3",
start: "2025-05-02T00:00:00",
end: "2025-05-08T00:00:00",
resource: "D",
barColor: "#f1c232",
barBackColor: "#f1c232"
},
{
id: 4,
text: "Task 3",
start: "2025-05-02T00:00:00",
end: "2025-05-08T00:00:00",
resource: "E",
barColor: "#cc0000",
barBackColor: "#ea9999"
}
]);
setActual([
{
id: 1,
text: "Task 1",
start: "2025-05-02T00:00:00",
end: "2025-05-05T00:00:00",
resource: "A",
},
{
id: 2,
text: "Task 2",
start: "2025-05-03T00:00:00",
end: "2025-05-10T00:00:00",
resource: "C",
barColor: "#38761d",
barBackColor: "#93c47d"
},
{
id: 3,
text: "Task 3",
start: "2025-05-02T00:00:00",
end: "2025-05-08T00:00:00",
resource: "D",
barColor: "#f1c232",
barBackColor: "#f1c232"
},
{
id: 4,
text: "Task 3",
start: "2025-05-02T00:00:00",
end: "2025-05-08T00:00:00",
resource: "E",
barColor: "#cc0000",
barBackColor: "#ea9999"
}
]);
scheduler.scrollTo("2025-05-01");
}, [scheduler]);
useEffect(() => {
const events = locked ? actual : planned;
if (locked) {
events.forEach(actualEvent => {
const plannedEvent = planned.find(plannedEvent => plannedEvent.id === actualEvent.id);
if (plannedEvent) {
actualEvent.versions = [{
start: plannedEvent.start,
end: plannedEvent.end,
backColor: actualEvent.barColor || "#1066a8",
barHidden: true,
areas: [
{top: 3, left: 5, width: 14, height: 14, fontColor: "#ffffff", symbol: "icons/daypilot.svg#padlock"}
]
}];
actualEvent.moveVDisabled = true;
}
});
}
setEvents(events);
}, [locked, planned, actual]);
return (
<div>
<div className="toolbar">
<ToggleSwitch label="Plan Locked" value={locked} onChange={setLocked} />
</div>
<DayPilotScheduler
startDate={startDate}
days={days}
scale={"Day"}
timeHeaders={[
{groupBy: "Month"},
{groupBy: "Day", format: "d"}
]}
onTimeRangeSelected={onTimeRangeSelected}
resources={resources}
events={events}
eventVersionsEnabled={true}
eventVersionHeight={20}
eventVersionPosition={"Above"}
eventMarginBottom={5}
rowMarginTop={5}
eventBorderRadius={6}
controlRef={setScheduler}
/>
</div>
);
}
export default Scheduler;