Overview

  • The Angular 10 timesheet component displays tasks in a grid with one day per row

  • Tasks can be created and modified usign drag an drop

  • Each task displays a description and task duration.

  • Weekends and non-business hours are hidden.

  • The Angular 10 project with all required boilerplate was generated using DayPilot UI Builder.

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

Angular Timesheet Component

Our Angular timesheet component is created using Angular Scheduler component from DayPilot Pro for JavaScript. In order to activate the timesheet mode (with hours on the horizontal axis and days on the vertical axis) we need to set the viewType config property to 'Days'.

This is the minimum timesheet component configuration:

import {Component, ViewChild, AfterViewInit} from '@angular/core';
import {DayPilot, DayPilotSchedulerComponent} from 'daypilot-pro-angular';

@Component({
  selector: 'timesheet-component',
  template: `<daypilot-scheduler [config]="config" [events]="events" #timesheet></daypilot-scheduler>`,
  styles: [``]
})
export class TimesheetComponent {

  @ViewChild('timesheet')
  timesheet: DayPilotSchedulerComponent;

  events: DayPilot.EventData[] = [];

  config: DayPilot.SchedulerConfig = {
    viewType: 'Days',
    // ...
  };

}

Timesheet Time Header

angular10-timesheet-quick-start-project-time-header.png

In the next step, we will configure the time header.

  • The timesheet component will display 15-minute blocks in the grid (scale: 'CellDuration' and cellDuration: 15 config options).

  • The first row of the time header will show hours and the second row will show the starting minutes of each grid cell.

config: DayPilot.SchedulerConfig = {
  timeHeaders: [
    {groupBy: 'Hour'},
    {groupBy: 'Cell', format: 'mm'}
  ],
  scale: 'CellDuration',
  cellDuration: 15,  
  // ...
}

We set the business hours using businessBeginsHour and businessEndsHour properties (9 am to 5 pm):

config: DayPilot.SchedulerConfig = {
  // ...
  businessBeginsHour: 9,
  businessEndsHour: 17,    
  // ...
}

With this configuration, most of the hours of day (16 of 24) are now non-business hours. We can hide the non-business hours using showNonBusiness property. This will also hide weekend rows:

config: DayPilot.SchedulerConfig = {
  // ...
  showNonBusiness: false,
  // ...
}

In order to avoid the horizontal scrollbar, we activate the auto cell width mode. This will calculate the cell width automatically so that the full timesheet width is used:

config: DayPilot.SchedulerConfig = {
  // ...
  cellWidthSpec: "Auto",    
  // ...
}

Timesheet Task Duration

angular10-timesheet-quick-start-project-task-duration.png

By default, each task displays the description which is specified using the text property of the event data object (see DayPilot.Event.data).

We want to display the task duration on the right side. We will add it by adding a special active area (areas property of the event data object). The position of the active area is set using right, top, bottom and width properties. The content (duration string) is set using html property. We will also add custom CSS style that will center the text vertically (style property).

config: DayPilot.SchedulerConfig = {
  // ...
  onBeforeEventRender: args => {
    const duration = new DayPilot.Duration(args.data.start, args.data.end);
    args.data.areas = [
      { right: 2, top: 0, bottom: 0, width: 30, html: duration.toString('h:mm'), style: 'display: flex; align-items: center'}
    ];
  }
  // ...
};

Date Headers with Totals

angular10-timesheet-quick-start-project-date-header.png

The vertical axis shows the days and the date is displayed in the row header. We want to add one more column to the row header that will display the row total.

First, we need to define the columns using rowHeaderColumns property:

config: DayPilot.SchedulerConfig = {
  rowHeaderColumns: [
    {title: "Date"},
    {title: "Total"}
  ],
  // ...
}

The daily totals are calculated using onBeforeRowHeaderRender event handler:

config: DayPilot.SchedulerConfig = {
  onBeforeRowHeaderRender: args => {
    const duration = args.row.events.totalDuration();
    if (duration.totalSeconds() === 0) {
      return;
    }

    let str = duration.toString('H:mm');
    if (duration.totalDays() >= 1) {
      str = Math.floor(duration.totalHours()) + ':' + duration.toString('mm');
    }

    args.row.columns[1].html = str + ' hours';
  },
  // ...
};

Full Source Code

This is the full source code of our Angular 10 timesheet component. You can find it in the downloadable project in timesheet.component.ts file.

import {Component, ViewChild, AfterViewInit} from '@angular/core';
import {DayPilot, DayPilotSchedulerComponent} from 'daypilot-pro-angular';
import {DataService} from './data.service';

@Component({
  selector: 'timesheet-component',
  template: `<daypilot-scheduler [config]="config" [events]="events" #timesheet></daypilot-scheduler>`,
  styles: [``]
})
export class TimesheetComponent implements AfterViewInit {

  @ViewChild('timesheet')
  timesheet: DayPilotSchedulerComponent;

  events: DayPilot.EventData[] = [];

  config: DayPilot.SchedulerConfig = {
    viewType: 'Days',
    showNonBusiness: false,
    businessBeginsHour: 9,
    businessEndsHour: 17,
    rowHeaderColumns: [
      {title: 'Date'},
      {title: 'Total'}
    ],
    cellWidthSpec: 'Auto',
    timeHeaders: [
      {groupBy: 'Hour'},
      {groupBy: 'Cell', format: 'mm'}
    ],
    scale: 'CellDuration',
    cellDuration: 15,
    days: new DayPilot.Date('2020-08-01').daysInMonth(),
    startDate: '2020-08-01',
    heightSpec: 'Max',
    height: 300,
    onTimeRangeSelected : args => {
      DayPilot.Modal.prompt('Create a new task:', 'Task 1').then(modal => {
        const dp = this.timesheet.control;
        dp.clearSelection();
        if (modal.canceled) {
          return;
        }
        dp.events.add({
          start: args.start,
          end: args.end,
          id: DayPilot.guid(),
          resource: args.resource,
          text: modal.result
        });
      });
    },
    onBeforeRowHeaderRender: args => {
      const duration = args.row.events.totalDuration();
      if (duration.totalSeconds() === 0) {
        return;
      }

      let str = duration.toString('H:mm');
      if (duration.totalDays() >= 1) {
        str = Math.floor(duration.totalHours()) + ':' + duration.toString('mm');
      }

      args.row.columns[1].html = str + ' hours';
    },
    onBeforeEventRender: args => {
      const duration = new DayPilot.Duration(args.data.start, args.data.end);
      args.data.areas = [
        { right: 2, top: 0, bottom: 0, width: 30, html: duration.toString('h:mm'), style: 'display: flex; align-items: center'}
      ];
      args.data.backColor = '#d0e0e3';
      args.data.backColor = '#fce5cd';
      args.data.borderColor = 'darker';
      args.data.barColor = '#e69138';
    }
  };

  constructor(private ds: DataService) {
  }

  ngAfterViewInit(): void {
    const from = this.timesheet.control.visibleStart();
    const to = this.timesheet.control.visibleEnd();
    this.ds.getEvents(from, to).subscribe(result => {
      this.events = result;
    });
  }

}