Features

  • Built using DayPilot Angular 6 Scheduler component
  • Fronted written using Angular 6 (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. Buy a license.

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

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

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

Linux

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

Initialize the Angular project (download dependencies to node_modules):

npm install

Run the project:

npm run start

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

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

For an introduction to Angular Scheduler component and a starting project with the required boilerplate code please see the following tutorial:

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

The HTML template of the scheduler component includes <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: any = {
    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.png

The time scale, timeline and time header rows (X axis) are configured using scale, timeHeaders, and cellDuration properties:

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

The visible range is defined using startDate and days properties:

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

angular-work-order-scheduling-time-header.png

We will also hide non-business hours using showNonBusiness property. This hidden parts of the timeline will be highlighted using a black vertical line:

angular-work-order-scheduling-non-business-hours.png

The resources are displayed on the Y axis. We have enabled a tree hierarchy using treeEnabled property.

angular-work-order-scheduling-resources.png

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 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"}
    ],"eventHeight":20},
  {"id":"group_2","name":"Team 2","expanded":true,"children":
    [
      {"id":"3","name":"Cindy"},
      {"id":"4","name":"Robert"}
    ],"eventHeight":20}
]

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":"2018-01-01T10:00:00","end":"2018-01-01T12:00:00","resource":"1","duration":120},
  {"id":"7","text":"Task 2","start":"2018-01-01T09:30:00","end":"2018-01-01T12:30:00","resource":"3","duration":180}
]

Adding a New Unscheduled Task

angular-work-order-scheduling-new-unscheduled.png

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

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.minutes(minutes);
    let result = duration.hours() + "h ";

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

    return result;
  }

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

}

Work Order Queue Context Menu

angular-work-order-queue-context-menu.png

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

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

Angular/TypeScript

export class SchedulerComponent implements AfterViewInit {

  // ...

  config: any = {

    // ...
  
    onBeforeEventRender: args => {
      args.data.backColor = args.data.color;
      args.data.html = args.data.text + "<br><span class='task-duration'>" + new DayPilot.Duration(args.data.start, args.data.end).totalHours() + " hours</span>";
      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);
          }
        },
      ],
    })
  };

}

Switching between SQLite and MySQL

By default, the PHP backend uses SQLite embedded database to make the installation easier:

_db.php

<?php

// use sqlite
require_once '_db_sqlite.php';

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

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"; 

The database (the default name is "workorder") will be created and initialized automatically if it doesn't exist. The DB user specified using $username variable must have the permissions to create a new database.

MySQL Database Schema

Table "groups":

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

Table "resources":

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

Table "events":

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

  • Jun 4, 2018: Updated to Angular 6, DayPilot Pro for JavaScript 2018.2.3297
  • Jan 11, 2018: Initial release