Features

  • Schedule days off for multiple employees.
  • See the annual leave data side by side.
  • Yearly totals for each of the employees.
  • Drag and drop date selection.
  • Programmatic modal dialog for entering the annual leave request details.
  • The Angular front end is created using Angular Scheduler component.
  • The back end is built using ASP.NET Core, Entity Framework and SQL Server (LocalDB).
  • Visual Studio 2019 project is included.
  • The database is created and initialized automatically on the first run.
  • The 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.

Annual Leave Planning using Angular Scheduler Component

In this tutorial, we will build an Angular application that allows planning annual leave days for multiple employees.

  • This application uses Angular Scheduler component from DayPilot Pro to build an annual leave schedule.
  • The Scheduler component visualizes the data and includes support for drag and drop creating, moving and resizing the days off records.
  • The employees are listed on the vertical axis (on the right side) and the timeline is displayed on the horizontal axis.
  • The initial Scheduler configuration was generated using Scheduler UI Builder. The UI Builder can help with the project setup - it generates all the required boilerplate. You can also use it to experiment with different configurations and preview the changes immediately.

The main logic is implemented in the SchedulerComponent class (ClientApp/src/app/scheduler/scheduler.component.ts). It wraps the DayPilot Scheduler component and defines the application-specific behavior.

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

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

  @ViewChild('scheduler', {static: false})
  scheduler: DayPilotSchedulerComponent;

  events: EventData[] = [];

  config: SchedulerConfig = {
    // ...
  };

  constructor(private ds: DataService) {
  }

  ngAfterViewInit(): void {
    // ...
  }

}

Scheduler: Configuring the Time Axis

angular-annual-leave-scheduling-application-asp.net-core-time-axis.png

The first thing to configure will be the time axis:

config: SchedulerConfig = {
  scale: "CellDuration",
  cellDuration: 720,
  timeHeaders: [{groupBy: "Month"}, {groupBy: "Day", format: "d"}],
  days: DayPilot.Date.today().daysInYear(),
  startDate: DayPilot.Date.today().firstDayOfYear(),
  cellWidth: 20,
  // ...
};

We want to schedule the annual leave days on a half-day level so we set the Scheduler cell duration to 720 minutes (scale: "CellDuration" and cellDuration: 720).

The time header will display two rows. The first row will display months (groupBy: "Month") and the second row will display days (groupBy: "Day").

In order to display the current year, we will set startDate and days properties.

Annual Leave Schedule: Displaying Employees

angular-annual-leave-scheduling-application-asp.net-core-employees.png

The Scheduler displays the resources on the vertical axis. In our case, we want to show one employee per row. This will allow to easily compare the annual leave days scheduled for each of them.

The resources can be defined using resources array of the config object. We will load the employee data from a server-side REST endpoint in ngAfterViewInit():

ngAfterViewInit(): void {

  this.ds.getResources().subscribe(result => this.config.resources = result);

  // ...
}

The getResources() method of DataService class loads the employees from "/api/resources" endpoint:

import { Injectable } from '@angular/core';
import { DayPilot } from 'daypilot-pro-angular';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import {map} from "rxjs/operators";
import EventData = DayPilot.EventData;

@Injectable()
export class DataService {

  constructor(private http: HttpClient) {
  }


  getResources(): Observable<any> {
    return this.http.get("/api/resources");
  }

  // ...

}

The REST API is implemented using ASP.NET Core Web API. The ResourcesController class that handles /api/resources is generated from the entity framework model class using Visual Studio:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using TutorialAnnualLeaveAngularAspnetCore.Models;

namespace TutorialAnnualLeaveAngularAspnetCore.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class ResourcesController : ControllerBase
    {
        private readonly AnnualLeaveContext _context;

        public ResourcesController(AnnualLeaveContext context)
        {
            _context = context;
        }

        // GET: api/Resources
        [HttpGet]
        public async Task<ActionResult<IEnumerable<Resource>>> GetResources()
        {
            return await _context.Resources.ToListAsync();
        }

        // ...
        
    }
}

The entity framework model classes are defined in Models/Data.cs file. The Resource class is very simple, it only defines Id and Name properties:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;

namespace TutorialAnnualLeaveAngularAspnetCore.Models
{
    public class AnnualLeaveContext : DbContext
    {
        public DbSet<Event> Events { get; set; }
        public DbSet<Resource> Resources { get; set; }

        public AnnualLeaveContext(DbContextOptions<AnnualLeaveContext> options) : base(options) { }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
          // ...
        }
    }

    public class Resource
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }
}

Loading Annual Leave Data

angular-annual-leave-scheduling-application-asp.net-core-data.png

The next step is to load the annual leave data. In order to load the annual leave data, we will add the following code to ngAfterViewInit():

ngAfterViewInit(): void {

  // ...

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

}

The DataService.getEvents() method loads the data from the server using HTTP get request. The range is limited to the time period currently visible in the Scheduler (visibleStart() and visibleEnd() methods).

import { Injectable } from '@angular/core';
import { DayPilot } from 'daypilot-pro-angular';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import {map} from "rxjs/operators";
import EventData = DayPilot.EventData;

@Injectable()
export class DataService {

  constructor(private http: HttpClient) {
  }
  
  // ...

  getEvents(from: DayPilot.Date, to: DayPilot.Date): Observable<EventData[]> {
    return this.http.get<any>("/api/events?start=" + from.toString() + "&end=" + to.toString()).pipe(
      map(array =>
        array.map(this.transformEventJsonToData)
      )
    );
  }

  transformEventJsonToData: ((json:EventJson) => EventData) = (json) => (
    {
      id: json.id,
      start: json.start,
      end: json.end,
      text: json.text,
      resource: json.resourceId
    }
  );

}

export class EventJson {
  id?: number;
  start?: string;
  end?: string;
  text?: string;
  resourceId?: number
}

In this case, the response data format doesn't match the Scheduler event data structure (see DayPilot.Event.data). This example shows how to transform the data structure using pipe() and map(). The transformEventJsonToData method translates the received JSON data (with resourceId property) to match the EventData interface (with resource property).

The /api/events endpoint is implemented using ASP.NET Core Web API just like in the previous example. The EventsController class can be found in Controllers/EventsController.cs file:

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using TutorialAnnualLeaveAngularAspnetCore.Models;

namespace TutorialAnnualLeaveAngularAspnetCore.Controllers
{
    [ApiController]
    [Route("api/[controller]")]
    public class EventsController : ControllerBase
    {


        private readonly AnnualLeaveContext _context;

        public EventsController(AnnualLeaveContext context)
        {
            _context = context;
        }


        // GET: api/Events
        [HttpGet]
        public IEnumerable GetEvents([FromQuery] DateTime start, [FromQuery] DateTime end)
        {
            return from e in _context.Events
                   where !((e.End <= start) || (e.Start >= end))
                   select new
                   {
                       e.Start,
                       e.End,
                       e.Id,
                       e.Text,
                       e.ResourceId
                   };
        }

        // ...

    }
}

And here is the Event model class that is persisted using Entity Framework (Model/Data.cs file):

public class Event
{
  public int Id { get; set; }
  public DateTime Start { get; set; }
  public DateTime End { get; set; }
  public int ResourceId { get; set; }
  public string Text { get; set; }
}

Employee Totals

angular-annual-leave-scheduling-application-asp.net-core-employee-totals.png

The Scheduler can display multiple columns in the row headers and we will use this feature to display the employee totals:

config: SchedulerConfig = {
  rowHeaderColumns: [
    {title: "Name", display: "name"},
    {title: "Total"}
  ],
  onBeforeRowHeaderRender: (args) => {
    let totalDuration = args.row.events.totalDuration();
    if (totalDuration.days() > 0) {
      args.row.columns[1].html = totalDuration.totalDays() + " days";
    }
  },
  
  // ...
  
};

The row header columns are defined using rowHeaderColumns property of the config object. It defines two columns ("Name" and "Total").

The content of the Name column is read from the name property of the resources[] array (the source property name is defined using display: "name").

The content of the Total column is calculated on the client side from the annual leave data (events) using onBeforeRowHeaderRender event handler.

Recording Days Off

angular-annual-leave-scheduling-application-asp.net-core-recording-days-off.png

The Scheduler allows adding new days of using drag and drop. Selecting a time range is enabled by default. All you need to do is to create a new onTimeRangeSelected event handler which will process the drag and drop selection.

The onTimeRangeSelected event handler has access to the selection properties (start and end dates, resource id). 

config: SchedulerConfig = {
  // ...
  onTimeRangeSelected: (args) => {
    const form = [
      {name: "Start", id: "start", dateFormat: "M/d/yyyy hh:mm tt"},
      {name: "End", id: "end", dateFormat: "M/d/yyyy hh:mm tt"},
      {name: "Employee", id: "resource", options: this.config.resources}
    ];
    const data: EventData = {
      id: 0,
      start: args.start,
      end: args.end,
      resource: args.resource,
      text: null
    };
    DayPilot.Modal.form(form, data).then(args => {
      this.scheduler.control.clearSelection();
      if (args.canceled) {
        return;
      }
      this.ds.createEvent(args.result).subscribe(result => {
        this.scheduler.control.events.add(result);
      });
    });
  },
  
};

Our annual leave application opens a modal dialog with the selection details. That allows users to review the details before recording the vacation request.

angular-annual-leave-scheduling-application-asp.net-core-vacation-request-dialog.png

The modal dialog is created using DayPilot.Modal.form() method which lets you define the content of the modal dialog programmatically (form). The data is specified using a plain object (data).

When the modal dialog is closed using OK button the updated data object is available as args.result.

We submit the result to the server using DataService.createEvent() method:

createEvent(e: EventData): Observable<EventData> {
  var transformed = this.transformEventDataToJson(e);
  return this.http.post<EventJson>("/api/events", transformed).pipe(
    map(this.transformEventJsonToData)
  );
}

Our server-side data model uses custom structure (see the previous chapter on annual leave data loading) and we are using transformEventDataToJson() and transformEventJsonToData() methods to convert the data between the different formats.

The server-side Web API endpoint is standard. Again, we rely on the Entity Framework to make the database changes:

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using TutorialAnnualLeaveAngularAspnetCore.Models;

namespace TutorialAnnualLeaveAngularAspnetCore.Controllers
{
    [ApiController]
    [Route("api/[controller]")]
    public class EventsController : ControllerBase
    {

        private readonly AnnualLeaveContext _context;

        public EventsController(AnnualLeaveContext context)
        {
            _context = context;
        }


        // ...

        // POST: api/Events
        [HttpPost]
        public async Task<IActionResult> PostEvent([FromBody] Event @event)
        {
            if (!ModelState.IsValid)
            {
                return BadRequest(ModelState);
            }

            _context.Events.Add(@event);
            await _context.SaveChangesAsync();

            return CreatedAtAction("GetEvent", new { id = @event.Id }, @event);
        }

        // ...

    }
}