Overview
The React Scheduler component can be set a timesheet mode, where days are displayed on the vertical axis and hours on the horizontal axis.
Learn how to hide non-business hours, add daily totals to the row headers and scroll to the specified time of day.
Tasks listed in the timesheet can be allocated to a particular project and differentiated using color-coding.
You can generate your own React project with a preconfigured timesheet using the UI Builder online application.
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.
How to Set Up the React Timesheet?
The React timesheet is implemented using the React Scheduler component from DayPilot Pro for JavaScript. In order to display the timesheet, you need to switch the Scheduler to the timesheet mode using the viewType
property. In the timesheet mode, the Scheduler displays days on the vertical axis and hours of day on the horizontal axis. Each day is displayed as one row.
The row headers show the date in the predefined format (datePattern
of the current locale). You can add additional information to the row. In this example, we add one more column which will show the day of week. In the next step, we explore how to add a column with a daily summary.
The timesheet displays 24 hours on the horizontal axis. Our component displays cells with a duration of 15 minutes. The time headers at the top of the timesheet show hours and minutes of each time column.
The cell width is set to be calculated automatically (cellWidthSpec: "Auto"
). The Scheduler will fill the available horizontal space with the grid and no horizontal scrollbar will be visible. However, to maintain readability, we also add the cellWidthMin
property and specify the minimum cell width. If the screen is not wide enough to display all cell in full width, the minimum cell width will be used and the Scheduler will display a horizontal scrollbar.
import React, {useState} from 'react';
import {DayPilot, DayPilotScheduler} from "daypilot-pro-react";
const Timesheet = () => {
const config = {
rowHeaderColumns: [
{title: "Date"},
{title: "Day", width: 40}
],
onBeforeRowHeaderRender: (args) => {
args.row.columns[0].horizontalAlignment = "center";
args.row.columns[1].text = args.row.start.toString("ddd");
},
cellWidthSpec: "Auto",
cellWidthMin: 25,
timeHeaders: [{groupBy: "Hour"}, {groupBy: "Cell", format: "mm"}],
scale: "CellDuration",
cellDuration: 15,
days: DayPilot.Date.today().daysInMonth(),
viewType: "Days",
startDate: DayPilot.Date.today().firstDayOfMonth(),
showNonBusiness: true,
// ...
};
return (
<div>
<DayPilotScheduler
{...config}
/>
</div>
);
}
export default Timesheet;
How to Load and Display Timesheet Records?
To load event data, create a new events
state variable that will store an array with timesheet event data:
const [events, setEvents] = useState([]);
Configure the Scheduler component to load the data from this state variable:
<DayPilotScheduler
{...config}
events={events}
/>
Now we can load the timesheet data using a special useEffect()
block:
useEffect(() => {
if (!timesheet) {
return;
}
const events = [
{
id: 1,
text: "Task 1",
start: "2025-05-02T10:00:00",
end: "2025-05-02T11:00:00",
project: 1,
},
{
id: 2,
text: "Task 2",
start: "2025-05-05T09:30:00",
end: "2025-05-05T11:30:00",
project: 2,
},
{
id: 3,
text: "Task 3",
start: "2025-05-07T10:30:00",
end: "2025-05-07T13:30:00",
project: 3,
}
];
setEvents(events);
}, [timesheet]);
How to Hide Non-Business Hours in the React Timesheet?
By default, the timesheet displays full 24 hours. The business hours defined as Monday to Friday, 9 am to 5 pm and the non-business hours use a different background color for the timesheet cells. If you don’t need to display the full time range in your React application, you can hide the non-business hours.
JSX of the checkbox that shows/hides the business hours:
<label><input type={"checkbox"} onChange={changeBusiness} checked={showBusinessOnly} /> Show only business hours</label>
The current value is stored is a showBusinessOnly
state variable:
const [showBusinessOnly, setShowBusinessOnly] = useState(false);
The Scheduler sets its showsNonBusiness
property accordingly:
<DayPilotScheduler
showNonBusiness={!showBusinessOnly}
{...config}
/>
The changeBusiness
event handler updates the showBusinessOnly when the checkbox status changes:
const changeBusiness = (e) => {
setShowBusinessOnly(e.target.checked);
};
By default, this mode hides weekends from the timesheet as well. If you want to display the weekends (Saturday, Sunday), you can use the businessWeekends property.
Here are the key parts of this logic integrated into our React timesheet component:
import React, {useState} from 'react';
import {DayPilot, DayPilotScheduler} from "daypilot-pro-react";
const Timesheet = () => {
const [showBusinessOnly, setShowBusinessOnly] = useState(false);
const config = {
// ...
};
const changeBusiness = (e) => {
setShowBusinessOnly(e.target.checked);
};
return (
<div>
<div className={"toolbar"}>
<label><input type={"checkbox"} onChange={changeBusiness} checked={showBusinessOnly} /> Show only business hours</label>
</div>
<DayPilotScheduler
showNonBusiness={!showBusinessOnly}
{...config}
/>
</div>
);
}
export default Timesheet;
How to Show Daily Totals in the React Timesheet?
You can show the daily totals by adding a custom column to the row header with a calculated value:
1. Add a new rowHeaderColumns
state variable that will store the current value of the Scheduler rowHeaderColumns property:
const [rowHeaderColumns, setRowHeaderColumns] = useState([
{title: "Date"},
{title: "Day", width: 40}
]);
2. Configure the Scheduler component to use the value of this state variable:
<DayPilotScheduler
{...config}
rowHeaderColumns={rowHeaderColumns}
/>
3. Extend the rowHeaderColumns
state variable array with a new column (“Total”).
if (showDailyTotals) {
setRowHeaderColumns([
{title: "Date"},
{title: "Day", width: 40},
{title: "Total", width: 60}
]);
}
4. Use the onBeforeRowHeaderRender event handler to define the column content. You can calculate the total hours using events.totalDuration() method of the DayPilot.Row object:
onBeforeRowHeaderRender: (args) => {
// ...
args.row.columns[2].text = args.row.events.totalDuration().toString("h:mm");
},
How to Scroll to the Specified Time of Day in the React Timesheet?
To set the initial scrollbar position (or scroll to a specified time of day anytime later), you can use the scrollTo() method of the React Scheduler component.
First, we need to get a reference to the DayPilot.Scheduler object so we can invoke its methods later:
const [timesheet, setTimesheet] = useState(null);
// ...
return (
<DayPilotScheduler
...
controlRef={setTimesheet}
/>
</div>
);
Now we can set the initial scrollbar position for our Timesheet component:
useEffect(() => {
if (!timesheet) {
return;
}
// ...
const firstDay = new DayPilot.Date("2025-05-01");
timesheet.scrollTo(firstDay.addHours(9));
}, [timesheet]);
The date part of the parameter will be used to scroll vertically to the given date. If you want to only set the horizontal position, use the date derived from the startDate
value (the first visible day).
How to Create a New Timesheet Record?
New timesheet records can be added using drag and drop. The timesheet component fires the onTimeRangeSelected
event handler when a user selects a time range:
1. It opens a modal dialog for entering the timesheet task details using DayPilot.Modal.form() method. This method lets you programmatically define a modal dialog with the specified fields. The modal form has the following fields:
Text description (text field)
Task start and end (date/time fields)
Project (searchable drop-down list)
2. The initial data
object defines the preset values (start, end, project id, and text).
3. The "en-us"
locale defined using the options
parameter defines how the date and time values will be formatted.
4. Note that the modal dialog implements a basic date range validation using onValidate
property of the end
date/time field. This makes sure that the provided end is not before the start.
When the user confirms the input data, a new record is added to the React timesheet using the events.add() method. The data provided by the user are available in the modal.result
property.
const config = {
// ...
onTimeRangeSelected: async (args) => {
const timesheet = args.control;
const form = [
{name: "Text", id: "text"},
{name: "Start", id: "start", type: "datetime"},
{name: "End", id: "end", type: "datetime", onValidate: (args) => {
if (args.values.end.getTime() < args.values.start.getTime()) {
args.valid = false;
args.message = "End must be after start";
}
}
},
{name: "Project", id: "project", options: projects}
];
const data = {
id: DayPilot.guid(),
start: args.start,
end: args.end,
project: projects[0].id,
text: "New task"
};
const options = {
locale: "en-us",
};
const modal = await DayPilot.Modal.form(form, data, options);
timesheet.clearSelection();
if (modal.canceled) { return; }
timesheet.events.add(modal.result);
},
// ...
};
How to Show Project Details in the Timesheet Tasks?
To display additional information in the task box, you can use the onBeforeEventRender
event handler. This handler lets you customize the event content (color, text, CSS class) and add active elements (icons, buttons, drag handles, etc.).
We will use the active areas feature to add multiple text fields at specific positions within the task box. Active areas are also supported during image and PDF export.
Note that we clear the default content (args.data.html = "";
) and show three active areas:
the task description (
args.data.text
) at the topthe task project (
args.data.project
) at the bottomand the task duration on the right
const config = {
// ...
onBeforeEventRender: (args) => {
const duration = new DayPilot.Duration(args.data.start, args.data.end);
const project = projects.find(p => p.id === args.data.project);
args.data.barColor = project.color;
args.data.html = "";
args.data.areas = [
{
top: 5,
left: 5,
text: args.data.text,
},
{
top: 20,
left: 5,
text: project.name,
fontColor: "#999999"
},
{
top: 13,
right: 5,
text: duration.toString("h:mm"),
fontColor: "#999999"
}
];
},
// ...
};
To provide an at-a-glance view of task distribution among projects, we will implement color-coding. Each task bar will be set to a project-specific color using args.data.backColor":
const project = projects.find(p => p.id === args.data.project);
args.data.barColor = project.color;
The timesheet projects are defined statically using a simple array. In a typical production React timesheet application, you would load them from a server-side API instead.
const projects = [
{id: 1, name: "Project A", color: "#38761d"},
{id: 2, name: "Project B", color: "#0d8ecf"},
{id: 3, name: "Project C", color: "#f1c232"},
];
Full Source Code
Here is the full source code of our React Timesheet component that displays a monthly timesheet, with days as rows:
import React, { useEffect, useState } from 'react';
import { DayPilot, DayPilotScheduler } from "daypilot-pro-react";
const Timesheet = () => {
const [timesheet, setTimesheet] = useState(null);
const [events, setEvents] = useState([]);
const [showBusinessOnly, setShowBusinessOnly] = useState(false);
const [showDailyTotals, setShowDailyTotals] = useState(false);
const [rowHeaderColumns, setRowHeaderColumns] = useState([
{title: "Date"},
{title: "Day", width: 40}
]);
const projects = [
{id: 1, name: "Project A", color: "#38761d"},
{id: 2, name: "Project B", color: "#0d8ecf"},
{id: 3, name: "Project C", color: "#f1c232"},
];
const config= {
locale: "en-us",
onBeforeRowHeaderRender: (args) => {
args.row.columns[0].horizontalAlignment = "center";
args.row.columns[1].text = args.row.start.toString("ddd");
if (args.row.columns[2]) {
args.row.columns[2].text = args.row.events.totalDuration().toString("h:mm");
}
},
onBeforeEventRender: (args) => {
const duration = new DayPilot.Duration(args.data.start, args.data.end);
const project = projects.find(p => p.id === args.data.project);
args.data.barColor = project.color;
args.data.areas = [
{
top: 13,
right: 5,
text: duration.toString("h:mm"),
fontColor: "#999999"
},
{
top: 5,
left: 5,
text: args.data.text,
},
{
top: 20,
left: 5,
text: project.name,
fontColor: "#999999"
}
];
args.data.html = "";
},
cellWidthSpec: "Auto",
cellWidthMin: 25,
timeHeaders: [{groupBy: "Hour"}, {groupBy: "Cell", format: "mm"}],
scale: "CellDuration",
cellDuration: 15,
eventHeight: 40,
heightSpec: "Max",
height: 450,
days: 31,
viewType: "Days",
startDate: "2025-05-01",
allowEventOverlap: false,
timeRangeSelectedHandling: "Enabled",
onTimeRangeSelected: async (args) => {
const timesheet = args.control;
const form = [
{name: "Text", id: "text"},
{name: "Start", id: "start", type: "datetime"},
{name: "End", id: "end", type: "datetime", onValidate: (args) => {
if (args.values.end.getTime() < args.values.start.getTime()) {
args.valid = false;
args.message = "End must be after start";
}
}
},
{name: "Project", id: "project", options: projects}
];
const data = {
id: DayPilot.guid(),
start: args.start,
end: args.end,
project: projects[0].id,
text: "New task"
};
const options = {
locale: "en-us",
};
const modal = await DayPilot.Modal.form(form, data, options);
timesheet.clearSelection();
if (modal.canceled) { return; }
timesheet.events.add(modal.result);
}
};
useEffect(() => {
if (!timesheet) {
return;
}
const events = [
{
id: 1,
text: "Task 1",
start: "2025-05-02T10:00:00",
end: "2025-05-02T11:00:00",
project: 1,
},
{
id: 2,
text: "Task 2",
start: "2025-05-05T09:30:00",
end: "2025-05-05T11:30:00",
project: 2,
},
{
id: 3,
text: "Task 3",
start: "2025-05-07T10:30:00",
end: "2025-05-07T13:30:00",
project: 3,
}
];
setEvents(events);
const firstDay = new DayPilot.Date("2025-05-01");
timesheet.scrollTo(firstDay.addHours(9));
}, [timesheet]);
const changeBusiness = (e) => {
setShowBusinessOnly(e.target.checked);
}
const changeSummary = (e) => {
setShowDailyTotals(e.target.checked);
};
useEffect(() => {
if (showDailyTotals) {
setRowHeaderColumns([
{title: "Date"},
{title: "Day", width: 40},
{title: "Total", width: 60}
]);
}
else {
setRowHeaderColumns([
{title: "Date"},
{title: "Day", width: 40}
]);
}
}, [showDailyTotals]);
return (
<div>
<div className={"toolbar"}>
<div className={"toolbar-item"}><label><input type={"checkbox"} onChange={changeBusiness}
checked={showBusinessOnly}/> Show only business hours</label>
</div>
<div className={"toolbar-item"}><label><input type={"checkbox"} onChange={changeSummary}
checked={showDailyTotals}/> Show daily totals</label></div>
</div>
<DayPilotScheduler
{...config}
showNonBusiness={!showBusinessOnly}
rowHeaderColumns={rowHeaderColumns}
events={events}
controlRef={setTimesheet}
/>
</div>
);
}
export default Timesheet;
History
July 28, 2024: Upgraded to React 18.2, DayPilot Pro for JavaScript 2024.3. Adding a new task fixed (project reference).
May 30, 2023: Upgraded to React 18, DayPilot Pro for JavaScript 2023.2. Timesheet converted to functional component with hooks API.
July 26, 2021: Initial release.