Overview
-
Resources organized by groups
-
Reorder and move groups/resources using drag and drop
-
Add events and milestones
-
Row context menu with resource operations (add, edit, delete)
-
Event context menu with event operations (edit, delete)
-
Move events and milestones using drag and drop
-
Edit events and milestone details using a modal dialog
-
Built using the React Scheduler component from DayPilot Pro for JavaScript scheduling library.
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.
Activity Planning System: Resources as Rows
The activity planning application lets you define your own resources (organized in groups) that will be displayed on the vertical axis.
The rows can display:
-
departments and people
-
locations and facilities
-
machines and devices
-
areas of responsibility and activity types
Activity Planning: Timeline
The horizontal axis displays time. In our application, the timeline displays months and days, but the Scheduler component can be configured to display years, quarters, months, weeks, days, hours, minutes, or custom units.
Activity Planning: Events and Milestones
The activity planning application lets you display events and milestones for your resources.
You can create new events and milestones using drag and drop. You can also move or resize existing events and milestones using drag and drop.
Application Structure
The application consists of two projects:
-
the root project is a React frontend application (built with Vite)
-
the
serverdirectory contains the backend application that provides access to the PostgreSQL database using an API
If you run npm start in the root project, the script will build the React application and run it using the server. The server defines the API endpoints and serves the React application. For development, you can run npm run dev to start the Vite dev server with API proxying to the backend.
Frontend: React Application
The boilerplate of the React Scheduler project was generated using DayPilot UI Builder. You can use this online application to create and download a new React project with a pre-configured React Scheduler component.
For an introduction to using the React Scheduler component, please see the basic tutorial:
Backend: Node/Express Application
You can find the Node/Express application that defines the API in the server directory.
Before running the backend project, it is necessary to create and initialize the PostgreSQL database manually (see the DDL below).
You'll also need to adjust the PostgreSQL database connection parameters defined at the top of the index.js file of the server project:
const pool = new pg.Pool({
host: '127.0.0.1',
database: 'daypilot',
user: 'username',
password: 'password',
port: 5432,
});
Loading Activity Planning Data
The event and resource data are loaded using two API calls. We run these HTTP requests in parallel and wait for both of them to complete.
When we have the data, we update the React Scheduler using the direct API by calling the update() method.
useEffect(() => {
if (!scheduler) {
return;
}
loadData();
}, [scheduler]);
const loadData = async () => {
const start = scheduler.visibleStart();
const end = scheduler.visibleEnd();
const [{data: resources}, {data: events}] = await Promise.all([
DayPilot.Http.get(`/api/resources`),
DayPilot.Http.get(`/api/events?start=${start}&end=${end}`)
]);
scheduler.update({
resources,
events
});
};
The backend Node/Express project defines the /api/events and /api/resources API endpoints as follows:
The /api/events endpoint returns a JSON array with event and milestone data which are stored in the events PostgreSQL database table.
app.get("/api/events", async (req, res) => {
const start = req.query.start;
const end = req.query.end;
const query = 'SELECT * FROM event WHERE NOT (("end" <= $1) OR (start >= $2))';
const params = [start, end];
try {
const result = await pool.query(query, params);
const data = result.rows.map(r => ({
...r,
resource: r.resource_id,
start: localDateTimeISOString(r.start),
end: localDateTimeISOString(r.end),
}));
res.status(200).json(data)
}
catch (e) {
console.error(e);
res.status(500).json(e);
}
});
The /api/resources endpoint returns a JSON tree with group/resource nodes.
app.get("/api/resources", async (req, res) => {
try {
const groups = await pool.query('SELECT * FROM resource_group ORDER BY ordinal ASC, ordinal_priority DESC');
const resources = await pool.query("SELECT * FROM resource ORDER BY ordinal ASC, ordinal_priority DESC");
const result = groups.rows.map(g => ({
id: "G" + g.id,
groupId: g.id,
name: g.name
}));
result.forEach(g => {
g.expanded = true;
g.children = resources.rows.filter(r => r.group_id === g.groupId);
});
res.status(200).json(result);
}
catch (e) {
console.error(e);
res.status(500).json(e);
}
});
Plan Events
You can create new events using drag and drop, by selecting the desired date range in the target row. The onTimeRangeSelected event handler opens a modal dialog where you can enter the event details:
onTimeRangeSelected: async args => {
const dp = args.control;
const form = [
{name: "Text", id: "text"}
];
const data = {
text: "Event 1",
start: args.start,
end: args.end,
resource: args.resource,
type: "Event"
};
const modal = await DayPilot.Modal.form(form, data);
dp.clearSelection();
if (modal.canceled) { return; }
const response = await DayPilot.Http.post("/api/createEvent", modal.result);
dp.events.add(response.data);
},
The modal dialog is created using DayPilot.Modal.form() from DayPilot Modal which lets you define the form fields programmatically. You can design your own modal dialog using the Modal Dialog Builder app.
As soon as you confirm the event details, the event handler calls the /api/createEvent endpoint to create the event in the PostgreSQL database:
app.post("/api/createEvent", async (req, res) => {
const query = 'INSERT INTO event (text, start, "end", type, resource_id) VALUES ($1, $2, $3, $4, $5) RETURNING *';
const params = [req.body.text, req.body.start, req.body.end, req.body.type, req.body.resource];
try {
const result = await pool.query(query, params);
const data = result.rows.map(r => ({
...r,
resource: r.resource_id,
start: localDateTimeISOString(r.start),
end: localDateTimeISOString(r.end),
}));
res.status(200).json(data[0])
}
catch (e) {
console.error(e);
res.status(500).json(e);
}
});
Plan Milestones
The milestone is a special kind of event with adjusted behavior. In order to display a milestone, the data record needs to specify type: "Milestone" in its properties.
Milestones don't have a duration (the end date is ignored) and they are displayed as a diamond at the specified location. The milestone can display additional text on the left or on the right side.
You can move milestones using drag and drop.
Because of the limited space, the milestones don't display the context menu icon on hover, but the context menu is still available using right mouse click.
New milestones can be created by right-clicking the scheduler grid. The onTimeRangeRightClick event handler opens a modal dialog with milestone details and sends a request to the server.
onTimeRangeRightClick: async args => {
const dp = args.control;
const form = [
{name: "Text", id: "text"}
];
const data = {
text: "Milestone 1",
start: args.start.addHours(12),
end: args.start.addHours(12),
resource: args.resource,
type: "Milestone"
};
const modal = await DayPilot.Modal.form(form, data);
dp.clearSelection();
if (modal.canceled) { return; }
const response = await DayPilot.Http.post("/api/createEvent", modal.result);
dp.events.add(response.data);
},
The event handler uses the same endpoint (/api/createEvent) to store the new milestone in the database.
Managing Rows (People, Departments, Facilities, Areas)
The Scheduler rows can display resources (such as people, departments, facilities, or areas of responsibility) which are organized in groups.
You can use a context menu to add, edit, and delete rows. The context menu is defined using the contextMenuResource property of the React Scheduler component. The same context menu is used for groups and resources, but resources don't show the "Add resource…" item.
contextMenuResource: new DayPilot.Menu({
onShow: args => {
const row = args.source;
const hasParent = !!row.parent();
args.menu.items[0].hidden = hasParent;
},
items: [
{
text: "Add resource...",
onClick: async args => {
const dp = args.source.calendar;
const group = args.source;
const form = [
{name: "Name", id: "name"}
];
const modal = await DayPilot.Modal.form(form);
if (modal.canceled) {
return;
}
const name = modal.result.name;
const parent = group.data.groupId;
const response = await DayPilot.Http.post("/api/createResource", {name, parent});
if (!group.data.children) {
group.data.children = [];
}
group.data.children.push(response.data);
dp.update();
}
},
{
text: "Edit...",
onClick: async args => {
const dp = args.source.calendar;
const form = [
{ name: "Name", id: "name" }
];
const isGroup = !!args.source.data.groupId;
const id = isGroup ? args.source.data.groupId : args.source.data.id;
const name = args.source.data.name;
const data = {
id,
name
};
const modal = await DayPilot.Modal.form(form, data);
if (modal.canceled) {
return;
}
const url = isGroup ? "/api/updateGroup" : "/api/updateResource";
const response = await DayPilot.Http.post(url, {id: modal.result.id, name: modal.result.name});
const row = dp.rows.find(args.source.data.id);
row.data.name = response.data.name;
dp.update();
}
},
{
text: "Delete",
onClick: async args => {
const dp = args.source.calendar;
const row = args.source;
if (row.data.groupId) {
await DayPilot.Http.post("/api/deleteGroup", {id: row.data.groupId});
}
else {
await DayPilot.Http.post("/api/deleteResource", {id: row.data.id});
}
dp.rows.remove(row.data.id);
}
}
]
})
Move Resources using Drag and Drop
The row order can be modified using drag and drop. Just drag the resource to the target location (within the current group or in a different group).
The Scheduler customizes the drag and drop behavior using the onRowMoving event handler. Certain target positions need to be forbidden since they are not supported by the data model (e.g., dragging a group to a child position):
onRowMoving: args => {
const sourceIsGroup = !!args.source.data.groupId;
const targetIsGroup = !!args.target.data.groupId;
if (sourceIsGroup) {
switch (args.position) {
case "before":
if (!targetIsGroup) {
args.position = "forbidden";
}
break;
case "after":
if (!targetIsGroup) {
args.position = "forbidden";
}
break;
case "child":
args.position = "forbidden";
break;
}
}
else {
switch (args.position) {
case "before":
if (targetIsGroup) {
args.position = "child";
}
break;
case "after":
if (targetIsGroup) {
args.position = "child";
}
break;
case "child":
if (!targetIsGroup) {
args.position = "forbidden";
}
break;
}
}
}
PostgreSQL Database Schema (DDL)
Here is the schema of the PostgreSQL database that is used by the server project to store the activity planning data.
event table
create table event
(
id serial not null
constraint event_pk
primary key,
text varchar,
start timestamp not null,
"end" timestamp not null,
type varchar not null,
resource_id integer
);
create index event_end_index
on event ("end");
create index event_start_index
on event (start);
create index event_resource_id_index
on event (resource_id);
resource_group table
create table resource_group
(
id serial not null
constraint group_pk
primary key,
name varchar,
ordinal integer default 0 not null,
ordinal_priority timestamp
);
create index resource_group_ordinal_index
on resource_group (ordinal);
create index resource_group_ordinal_priority_index
on resource_group (ordinal_priority);
resource table
create table resource
(
id serial not null
constraint resource_pk
primary key,
name varchar,
group_id integer not null,
ordinal integer default 0 not null,
ordinal_priority timestamp
);
create index resource_ordinal_index
on resource (ordinal);
create index resource_ordinal_priority_index
on resource (ordinal_priority);
create index resource_group_id_index
on resource (group_id);
History
-
April 8, 2026: Upgraded to React 19, Vite, DayPilot Pro 2026.2.6899; converted class component to functional component with hooks; replaced axios with DayPilot.Http; Express 5; updated SVG icon paths
-
April 8, 2021: Initial release
DayPilot




