Features

  • Schedule days off for multiple employees

  • See the annual leave data side by side

  • Display yearly totals for each of the employees

  • Drag and drop date selection

  • Programmatic modal dialog for entering the annual leave request details

  • The Angular 16 front end is created using Angular Scheduler component from DayPilot Pro for JavaScript

  • The back end is built using ASP.NET Core, Entity Framework and SQL Server (LocalDB)

  • Visual Studio 2022 project is included

  • The SQL Server database is created and initialized automatically on the first run

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 it includes support for creating, moving, and resizing the days-off records using drag and drop.

  • 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" #scheduler></daypilot-scheduler>`,
  styles: [``]
})
export class SchedulerComponent implements AfterViewInit {

  @ViewChild('scheduler')
  scheduler!: DayPilotSchedulerComponent;

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

  constructor(private ds: DataService) {
  }

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

}

Scheduler: Configuring the Time Axis

angular annual leave scheduling application asp.net core time axis

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 displays two rows. The first row displays months (groupBy: "Month") and the second row displays days (groupBy: "Day").

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

Annual Leave Schedule: Displaying Employees

angular annual leave scheduling application asp.net core employees

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

The resources can be defined using resources array of the config object. We 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; }
    }
}

The SQL Server database connection string is defined in appsettings.json. The database name is DayPilot.AnnualLeaveAngularAspnetCore and it uses a LocalDB instance.

{
    "ConnectionStrings": {
        "AnnualLeaveContext": "Server=(localdb)\\mssqllocaldb;Database=DayPilot.AnnualLeaveAngularAspnetCore;Trusted_Connection=True"
    },
    // ...
}

The following code in Program.cs ensures the database for the annual leave scheduling application's ASP.NET Core backend is created at startup.

var builder = WebApplication.CreateBuilder(args);

// ...

builder.Services.AddDbContext<AnnualLeaveContext>(options => options.UseSqlServer(builder.Configuration.GetConnectionString("AnnualLeaveContext")));


// ...


using (var serviceScope = app.Services.CreateScope())
{
    var services = serviceScope.ServiceProvider;
    try
    {
        var context = services.GetRequiredService<AnnualLeaveContext>();
        context.Database.EnsureCreated();
    }
    catch (Exception ex)
    {
        var logger = services.GetRequiredService<ILogger<Program>>();
        logger.LogError(ex, "An error occurred creating the DB.");
    }
}

Loading Annual Leave Data

angular annual leave scheduling application asp.net core data

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(). The update() call performs a partial update of the Angular Scheduler component - it renders the new event data set.

ngAfterViewInit(): void {

  // ...

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

}

The DataService.getEvents() method loads the data from the server using an 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 (which has a resourceId property) to match the EventData interface (which has a 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

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

The Scheduler allows adding days off using drag and dropSelecting 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: async (args) => {
    const form: ModalFormItem[] = [
      { name: "Note", id: "text" },
      { name: "Employee", id: "resource", options: this.config.resources as ModalFormOption[], disabled: true },
      { name: "Start", id: "startDisplay", disabled: true },
      { name: "End", id: "endDisplay", disabled: true },
      { name: "Total", id: "total", disabled: true}
    ];
    const data = {
      id: 0,
      start: args.start,
      end: args.end,
      startDisplay: this.formatDateTime(args.start),
      endDisplay: this.formatDateTime(args.end.addHours(-12)),
      resource: args.resource,
      text: "",
      total: new DayPilot.Duration(args.start, args.end).totalDays() + " days"
    };
    const options = {
      locale: "en-us"
    };
    const modal = await DayPilot.Modal.form(form, data, options);

    this.scheduler.control.clearSelection();
    if (modal.canceled) {
      return;
    }
    this.ds.createEvent(modal.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

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);
        }

        // ...

    }
}