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?

React Timesheet Component Setup

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?

React Timesheet Component - Load and Display 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?

React Timesheet Component - Hide Non-Business Hours

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?

React Timesheet Component - Show Daily Totals

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?

React Timesheet Component - New Task Modal Dialog

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:

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?

React Timesheet Component - Show Project Details

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 top

  • the task project (args.data.project) at the bottom

  • and 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.