Features

  • Built using DayPilot Angular  Scheduler component

  • Fronted written using Angular (TypeScript)

  • REST/JSON backend written in PHP

  • Queue of unscheduled tasks

  • Scheduling a task using drag and drop (assign a person and time)

  • Context menu for additional work order actions (edit, delete, unschedule)

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

Scheduler UI Builder

This project was generated using Scheduler UI Builder. You can use this visual tool to configure the Scheduler appearance and properties and generate a downloadable project.

Running the Project

The project is split into two parts:

  • The frontend with the user interface is an Angular project written in TypeScript.

  • The backend that provides the REST API (to access the MySQL database) is written in PHP.

Since the two projects are separate you can replace the PHP backend with a custom backend running on a different platform if needed.

1. PHP backend (angular-work-order-php-backend directory)

In order to run the PHP backend that handles the database access simply start the built-in PHP web server. The web server is running on port 8090.

Linux

php -S 127.0.0.1:8090 -t /home/daypilot/tutorials/angular-work-order-php/angular-work-order-php-backend

Windows

C:\php\php.exe -S 127.0.0.1:8090 -t C:\Users\DayPilot\tutorials\angular-work-order-php\angular-work-order-php-backend

2. Angular frontend (angular-work-order-php-frontend directory)

The Angular project in the package doesn't include the dependencies (node_modules). It's necessary to download the dependencies first using npm install:

npm install

Then you can start the Angular frontend application:

npm run start

Note: The Angular project uses a reverse proxy to pass all local requests to /api/* URLs to the PHP backend running at http://localhost:8090. The "start" script from package.json is modified to include the proxy configuration:

{
  "scripts": {
    "start": "ng serve --proxy-config proxy.conf.json",
    // ...
  },
  / ...
}

proxy.conf.json

{
  "/api": {
    "target": "http://localhost:8090",
    "secure": false
  }
}

Angular Scheduler Configuration

angular work order scheduler

This Angular application uses DayPilot Angular Scheduler component to build the work order scheduling application UI.

The Angular project boilerplate was generated using DayPilot UI Builder. This is an online application that will let you configure the Scheduler component properties and immediately preview the changes.

The source code of the Scheduler configuration can be found in src/app/scheduler/scheduler.component.ts TypeScript file.

To add the Scheduler component to the view you need to add <daypilot-scheduler> tag:

<daypilot-scheduler [config]="config" [events]="events" #scheduler></daypilot-scheduler>

The scheduler tag uses [config] attribute to specify the object with configuration properties:

export class SchedulerComponent implements AfterViewInit {

  // ...

  config: DayPilot.SchedulerConfig = {
    eventHeight: 40,
    cellWidthSpec: "Fixed",
    cellWidth: 60,
    timeHeaders: [{"groupBy":"Day"},{"groupBy":"Hour"}],
    scale: "CellDuration",
    cellDuration: 30,
    showNonBusiness: false,
    treePreventParentUsage: true,
    days: DayPilot.Date.today().daysInMonth(),
    startDate: DayPilot.Date.today().firstDayOfMonth(),
    timeRangeSelectedHandling: "Enabled",
    treeEnabled: true,
    resources: [],
    
    // ...
  
  }
}

The Scheduler grid cell size (in pixels) is specified using eventHeight and cellWidth properties.

angular work order scheduling grid cell size

The time scale, timeline and time header rows (X axis) are configured using scale, timeHeaders, and cellDuration properties. We will display two time header rows (to display days and hours). 

The Scheduler grid cell size is set to 30 minutes. Note that the cell grid size doesn't need to match the time header groups - the time headers will be calculated automatically.

    timeHeaders: [{"groupBy":"Day"},{"groupBy":"Hour"}],
    scale: "CellDuration",
    cellDuration: 30,

The visible range is defined using startDate and days properties. We will display the current month.

    days: DayPilot.Date.today().daysInMonth(),
    startDate: DayPilot.Date.today().firstDayOfMonth(),

angular work order scheduling time header

We will also hide non-business hours using showNonBusiness property. This hidden parts of the timeline will be highlighted using a black vertical line. This provides a hint to the user that there is a break in the timeline.

angular work order scheduling non business hours

The resources are displayed on the Y axis. In order to display groups (Team 1, Team 2), we have enabled the tree hierarchy using treeEnabled property.

angular work order scheduling resources

The config sets the resources property to an empty array:

resources: [],

We need to load the rows from the server so we call the DataService.getResources() method during the component initialization:

  ngAfterViewInit(): void {

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

    // ...

  }

The DataService.getResource() method uses a GET request to load the data in JSON format from our PHP backend:

  getResources(): Observable<any[]> {
    return this.http.get<any[]>("/api/backend_resources.php");
  }

This is a sample JSON response:

[
  {"id":"group_1","name":"Team 1","expanded":true,"children":
    [
      {"id":"1","name":"John"},
      {"id":"2","name":"Mary"}
    ]
  },
  {"id":"group_2","name":"Team 2","expanded":true,"children":
    [
      {"id":"3","name":"Cindy"},
      {"id":"4","name":"Robert"}
    ]
  }
]

The object that holds work order data ("events") is specified using [events] attribute of the <daypilot-scheduler> tag:

<daypilot-scheduler [config]="config" [events]="events" #scheduler></daypilot-scheduler>

It's a property specified in the SchedulerComponent class:

export class SchedulerComponent implements AfterViewInit {

  events: any[] = [];
  
  // ...

}

Again, we load the data in during component initialization using DataService:

  ngAfterViewInit(): void {
    
    // ...

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

    // ...
  }

The getEvents() method of DataService class is very simple. It makes a GET request to load the work order data from the server in JSON format:

  getEvents(from: DayPilot.Date, to: DayPilot.Date): Observable<any[]> {
    return this.http.get<any[]>("/api/backend_events.php?from=" + from.toString() + "&to=" + to.toString());
  }

Sample JSON response:

[
  {"id":"6","text":"Task 1","start":"2021-01-01T10:00:00","end":"2021-01-01T12:00:00","resource":"1","duration":120},
  {"id":"7","text":"Task 2","start":"2021-01-01T09:30:00","end":"2021-01-01T12:30:00","resource":"3","duration":180}
]

Adding a New Unscheduled Task

angular work order scheduling new unscheduled

The “Add Task” button opens a modal dialog which lets users enter task details, such as text description and duration.

After confirmation, the new tasks will be added to the queue and displayed on the left side, next to the Scheduler.

HTML:

<button (click)="addToQueue()">Add Task</button>

...

<task-create-dialog #create></task-create-dialog>

Angular/TypeScript:

export class SchedulerComponent implements AfterViewInit {

  unscheduled: any[] = [];

  // ...

  addToQueue(): void {

    this.create.show({}).subscribe(result => {
      if (!result) {  // canceled
        return;
      }
      let params = {
        text: result.text,
        duration: result.duration
      };
      this.ds.createTaskInQueue(params).subscribe(result => {
        this.unscheduled.push(result);
      });
    });

    // ...

}

Work Order Queue

angular work order unscheduled queue

The queue is displayed as a simple list of <div> elements with formatted content. The tasks in the queue are marked with [draggableToScheduler] attribute. That activates the items and makes them draggable to the Scheduler.

The users are now able to schedule tasks by simply dragging them from the queue to the desired time slot.

HTML:

<div class="queue-list">
  <div 
  *ngFor="let item of unscheduled" 
  [draggableToScheduler]="{ text: item.text, externalHtml: item.text, duration: durationFromMinutes(item.duration), id: item.id }" 
  [menu]="{source: item, menu: queueMenu }" 
  (click)="queueEdit(item)"
  class="queue-item">
    {{item.text}}<br/>
    <span class="task-duration">{{formatDuration(item.duration)}}</span>
  </div>
  <div *ngIf="unscheduled.length === 0">No tasks in queue</div>
</div>

Angular/TypeScript:

export class SchedulerComponent implements AfterViewInit {

  @ViewChild("create") create: TaskCreateComponent;

  constructor(private ds: DataService) {
  }

  addToQueue(): void {

    this.create.show({}).subscribe(result => {
      if (!result) {  // canceled
        return;
      }
      let params = {
        text: result.text,
        duration: result.duration
      };
      this.ds.createTaskInQueue(params).subscribe(result => {
        this.unscheduled.push(result);
      });
    });
  }
  
  // ...

  formatDuration(minutes: number): string {
    let duration = DayPilot.Duration.ofMinutes(minutes);
    let result = duration.hours() + "h ";

    if (duration.minutes() > 0) {
      result += duration.minutes() + "m";
    }

    return result;
  }

  durationFromMinutes(minutes: number) : DayPilot.Duration {
    return DayPilot.Duration.ofMinutes(minutes);
  }

}

Work Order Queue Context Menu

angular work order queue context menu

In this step, we will add an active area to the elements in the work order queue. The active area will display a menu icon in the upper-left corner of the task. Upon click, the active area opens a context menu with “Edit…” and “Delete” actions.

HTML:

<div 
  *ngFor="let item of unscheduled" 
  [draggableToScheduler]="{ text: item.text, externalHtml: item.text, duration: durationFromMinutes(item.duration), id: item.id }" 
  [menu]="{source: item, menu: queueMenu }" 
  (click)="queueEdit(item)"
  class="queue-item">
    {{item.text}}<br/>
  <span class="task-duration">{{formatDuration(item.duration)}}</span>
</div>

Angular/TypeScript

export class SchedulerComponent implements AfterViewInit {

  // ...

  queueMenu: DayPilot.Menu = new DayPilot.Menu({
    items: [
      {
        text: "Edit...",
        onClick: args => {
          this.queueEdit(args.source);
        }
      },
      {
        text: "-",
      },
      {
        text: "Delete",
        onClick: args => {
          this.deleteTaskFromQueue(args.source);
        }
      },
    ]
  });

  // ...
  
}

menu.directive.ts

import { Directive, ElementRef, Input, AfterViewInit } from '@angular/core';
import { DayPilot } from 'daypilot-pro-angular';

@Directive({ selector: '[menu]' })
export class MenuDirective implements AfterViewInit {

  @Input('menu') options: any;

  constructor(private el: ElementRef) { }

  ngAfterViewInit(): void {
    let element = this.el.nativeElement;
    let areas = [
      { top: 5, right: 3, height: 12, icon: "icon-triangle-down", visibility: "Hover", action: "ContextMenu", menu: this.options.menu, style: "font-size: 12px; background-color: rgba(255, 255, 255, .5); border: 1px solid #aaa; padding: 3px; cursor:pointer;" }
    ];
    let source = this.options.source;

    let daypilot = DayPilot as any;
    daypilot.Areas.attach(element, source, { areas: areas});

  }
}

Scheduling a Work Order using Drag and Drop

angular work order scheduling drag drop

HTML

<div 
  *ngFor="let item of unscheduled" 
  [draggableToScheduler]="{ text: item.text, externalHtml: item.text, duration: durationFromMinutes(item.duration), id: item.id }" 
  [menu]="{source: item, menu: queueMenu }" 
  (click)="queueEdit(item)"
  class="queue-item">
    {{item.text}}<br/>
  <span class="task-duration">{{formatDuration(item.duration)}}</span>
</div>

draggable.directive.ts

import { Directive, ElementRef, Input, AfterViewInit } from '@angular/core';
import { DayPilot } from 'daypilot-pro-angular';

@Directive({ selector: '[draggableToScheduler]' })
export class DraggableDirective implements AfterViewInit {

  @Input('draggableToScheduler') options: any;

  constructor(private el: ElementRef) { }

  ngAfterViewInit(): void {
    this.options.element = this.el.nativeElement;
    DayPilot.Scheduler.makeDraggable(this.options);
  }
}

Unscheduling a Work Order

angular work order unschedule

It’s possible to unschedule a task using a context menu.

Angular/TypeScript

export class SchedulerComponent implements AfterViewInit {

  // ...

  config: any = {

    // ...
  
    onBeforeEventRender: args => {
      args.data.backColor = args.data.color;
      args.data.html = "<div>" + args.data.text + "<br><span class='task-duration'>" + new DayPilot.Duration(args.data.start, args.data.end).totalHours() + " hours</span></div>";
      args.data.areas = [
        { top: 5, right: 3, height: 12, icon: "icon-triangle-down", visibility: "Hover", action: "ContextMenu", style: "font-size: 12px; background-color: rgba(255, 255, 255, .5); border: 1px solid #aaa; padding: 3px; cursor:pointer;" }
      ];
    },
    
    // ...
    
    contextMenu: new DayPilot.Menu({
      items: [
        {
          text: "Edit...",
          onClick: args => {
            this.scheduler.control.onEventClick({e: args.source});
          }
        },
        {
          text: "-",
        },
        {
          text: "Unschedule",
          onClick: args => {
            var e = args.source;
            this.unscheduleTask(e);
          }
        },
        {
          text: "-",
        },
        {
          text: "Delete",
          onClick: args => {
            var e = args.source;
            this.deleteTask(e);
          }
        },
      ],
    })
  };

}

The “Unschedule” menu item calls unscheduleTask() method which moves the task back to the queue.

unscheduleTask(e: DayPilot.Event): void {
  let dp = this.scheduler.control;
  let params = {
    id: e.id(),
    start: null,
    end: null,
    resource: null
  };
  this.ds.moveToQueue(params).subscribe(result => {
    this.unscheduled.push(result);
    dp.events.remove(e);
  });
}

Switching from SQLite to MySQL

By default, the PHP backend uses SQLite embedded database to make the testing easier. It will automatically create a new disk-based database (you don't need to set up MySQL) - just make sure that the PHP server has write permissions.

_db.php

<?php

// use sqlite
require_once '_db_sqlite.php';

// use MySQL
//require_once '_db_mysql.php';

For a standard application, you'll want to switch to MySQL instead. The MySQL database ("workorder") will be created automatically as well (don't forget the permissions). In order to switch to MySQL, you need to update _db.php file:

<?php

// use sqlite
// require_once '_db_sqlite.php';

// use MySQL
require_once '_db_mysql.php';

Don't forget to update the MySQL server hostname, username and password in _db_mysql.php:

<?php
$host = "127.0.0.1";
$port = 3306;
$username = "username";
$password = "password";
$database = "workorder"; 

MySQL Database Schema

Here is the MySQL database schema used by the application.

This is the definition of the groups table.

CREATE TABLE `groups` (
	`id` INT(11) NOT NULL,
	`name` VARCHAR(200) NULL DEFAULT NULL,
	PRIMARY KEY (`id`)
);

This is the definition of the resources table. Resources are the people available for work order assignments.

CREATE TABLE `resources` (
	`id` INT(11) NOT NULL AUTO_INCREMENT,
	`name` VARCHAR(200) NULL DEFAULT NULL,
	`group_id` INT(11) NULL DEFAULT NULL,
	PRIMARY KEY (`id`)
);

This is the definition of the events table. This tables stores the work order items.

CREATE TABLE `events` (
	`id` INT(11) NOT NULL AUTO_INCREMENT,
	`name` TEXT NULL,
	`start` DATETIME NULL DEFAULT NULL,
	`end` DATETIME NULL DEFAULT NULL,
	`resource_id` VARCHAR(30) NULL DEFAULT NULL,
	`duration` INT(11) NULL DEFAULT NULL,
	`scheduled` TINYINT(1) NOT NULL DEFAULT '0',
	PRIMARY KEY (`id`)
);

History

  • December 16, 2020: Updated to Angular 11, DayPilot Pro for JavaScript 2020.4.4807; typed config

  • June 22, 2020: Updated to Angular 9, DayPilot Pro for JavaScript 2020.2.4514

  • Sep 3, 2019: Updated to Angular 8, DayPilot Pro for JavaScript 2019.2.3871

  • Jun 4, 2018: Updated to Angular 6, DayPilot Pro for JavaScript 2018.2.3297

  • Jan 11, 2018: Initial release