Features
Angular 18 frontend
Built using Angular Scheduler UI component from DayPilot Pro for JavaScipt
Restaurant tables displayed in a tree hierarchy (by location)
New reservations can be created using drag and drop
Moving existing reservations using drag and drop (allows changing time and/or table)
Table filtering by number of table seats
Table filtering by availability at the specified time
Uses a REST API backend, implemented using PHP/MySQL
This tutorial is also available for vanilla JavaScript and PHP:
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.
Angular Frontend Project
The Angular frontend project ("angular-restaurant-php-frontend") is created using Angular CLI and uses Angular 18.
For a quick Scheduler project setup see Angular Scheduler Quick Start Project. This downloadable project includes the boilerplate code required to get started with the Scheduler quickly.
All the scheduling logic is encapsulated in a special module (SchedulerModule
). The module can be found in src/app/scheduler
directory of the project and includes the following files:
scheduler.module.ts (the wrapping module)
scheduler.component.ts (UI, main logic)
data.service.ts (a service that communicates with the PHP backend using REST API)
The Angular frontend project doesn't include NPM dependencies (node_modules
directory). You can download and install the dependencies using npm:
npm install
You can run the Angular project using npm:
npm run start
PHP Backend Project
The PHP backend is a standalone project (angular-restaurant-php-backend
).
We will use the embedded PHP web server in PHP to run the backend on port 8090:
Linux:
php -S 127.0.0.1:8090 -t /home/daypilot/tutorials/angular-restaurant-php-backend
Windows:
php.exe -S 127.0.0.1:8090 -t C:\Users\daypilot\tutorials\angular-restaurant-php-backend
During development, the Angular project uses a proxy to make the backend available as /api
on the same server:
proxy.conf.json:
{
"/api": {
"target": "http://localhost:8090",
"secure": false
}
}
The start
script in package.json applies the proxy configuration. All requests to /api/*
will be proxied to the PHP web server running on port 8090.
{
"name": "angular-restaurant-php-frontend",
// ...
"scripts": {
// ...
"start": "ng serve --proxy-config proxy.conf.json",
// ...
},
}
Scheduler Configuration
The Scheduler installation (installing DayPilot Pro NPM package, using <daypilot-scheduler> tag to display the Scheduler) and basic configuration is explained in the Angular Scheduler Tutorial.
See also the documentation on using the Angular Scheduler.
Scheduler Scale and Time Header Units
First, we will set the scale (cell duration) of the Scheduler to 15 minutes and set the time header rows to display days/hours/minutes. This will let the users schedule the restaurant table reservations in 15-minute blocks (snap-to-grid is enabled by default).
scheduler.component.ts
import {Component, ViewChild, AfterViewInit} from "@angular/core";
import {DayPilot, DayPilotSchedulerComponent} from "daypilot-pro-angular";
import {DataService, CreateEventParams, MoveEventParams, UpdateEventParams} from "./data.service";{}
@Component({
selector: 'scheduler-component',
template: `<daypilot-scheduler [config]="config" #scheduler></daypilot-scheduler>`,
styles: [``]
})
export class SchedulerComponent implements AfterViewInit {
@ViewChild("scheduler", {static: false})
scheduler!: DayPilotSchedulerComponent;
config: DayPilot.SchedulerConfig = {
// ...
eventHeight: 40,
cellWidth: 50,
timeHeaders: [
{groupBy: "Day", format: "dddd, d MMMM yyyy"},
{groupBy: "Hour"},
{groupBy: "Cell", format: "mm"}
],
scale: "CellDuration",
cellDuration: 15,
// ...
};
// ...
}
Loading Restaurant Table Data
In order to display the restaurant tables on the vertical axis we need to fill the resources
array of the config
object. This array specifies the hierarchy of resources that will be displayed as rows.
By default, the Scheduler displays a flat list. We want to display the tables in a tree hierarchy so we need to enable it using treeEnabled property.
scheduler.component.ts
import {Component, ViewChild, AfterViewInit} from "@angular/core";
import {DayPilot, DayPilotSchedulerComponent} from "daypilot-pro-angular";
import {DataService} from "./data.service";
@Component({
selector: 'scheduler-component',
template: `<daypilot-scheduler [config]="config" #scheduler></daypilot-scheduler>`,
styles: [``]
})
export class SchedulerComponent implements AfterViewInit {
@ViewChild("scheduler", {static: false})
scheduler!: DayPilotSchedulerComponent;
config: DayPilot.SchedulerConfig = {
treeEnabled: true,
resources: []
// ...
};
constructor(private ds: DataService) {
}
ngAfterViewInit(): void {
this.ds.getResources().subscribe(result => this.config.resources = result);
}
}
The change of the config object is detected by Angular and the Scheduler is updated automatically. This is convenient but the cost of the automatic change detection increases with the number of resources. In order to improve the Angular Scheduler performance, we switch to the direct API for resource (table) loading:
import {Component, ViewChild, AfterViewInit} from "@angular/core";
import {DayPilot, DayPilotSchedulerComponent} from "daypilot-pro-angular";
import {DataService} from "./data.service";
@Component({
selector: 'scheduler-component',
template: `<daypilot-scheduler [config]="config" #scheduler></daypilot-scheduler>`,
styles: [``]
})
export class SchedulerComponent implements AfterViewInit {
@ViewChild("scheduler", {static: false})
scheduler!: DayPilotSchedulerComponent;
config: DayPilot.SchedulerConfig = {
treeEnabled: true,
// ...
};
constructor(private ds: DataService) {
}
ngAfterViewInit(): void {
this.ds.getResources().subscribe(resources => this.scheduler.control.update({resources}));
}
}
We load the resources from the server-side PHP backend using DataService class. The getResources()
method makes an HTTP request to backend_resources.php
.
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Rx';
import 'rxjs';
import {DayPilot} from 'daypilot-pro-angular';
import {HttpClient} from "@angular/common/http";
@Injectable()
export class DataService {
constructor(private http : HttpClient){
}
getResources(): Observable<any[]> {
return this.http.get("/api/backend_resources.php") as Observable<any>;
}
// ...
}
The PHP script (backend_resources.php) loads the tables from a MySQL database and returns a JSON array:
<?php
require_once '_db.php';
$scheduler_groups = $db->query('SELECT * FROM groups ORDER BY name');
class Group {
public $id;
public $name;
public $expanded;
public $children;
}
class Resource {
public $id;
public $name;
public $seats;
}
$groups = array();
foreach($scheduler_groups as $group) {
$g = new Group();
$g->id = "group_".$group['id'];
$g->name = $group['name'];
$g->expanded = true;
$g->children = array();
$groups[] = $g;
$stmt = $db->prepare('SELECT * FROM resources WHERE group_id = :group ORDER BY name');
$stmt->bindParam(':group', $group['id']);
$stmt->execute();
$scheduler_resources = $stmt->fetchAll();
foreach($scheduler_resources as $resource) {
$r = new Resource();
$r->id = $resource['id'];
$r->name = $resource['name'];
$r->seats = $resource['seats'];
$g->children[] = $r;
}
}
header('Content-Type: application/json');
echo json_encode($groups);
Sample JSON response:
[
{"id":"group_1","name":"Indoors","expanded":true,"children":[
{"id":"1","name":"Table 1","seats":"2"},
{"id":"2","name":"Table 2","seats":"3"},
{"id":"3","name":"Table 3","seats":"4"},
{"id":"4","name":"Table 4","seats":"4"},
{"id":"5","name":"Table 5","seats":"6"},
{"id":"6","name":"Table 6","seats":"6"}
]},
{"id":"group_2","name":"Terrace","expanded":true,"children":[
{"id":"7","name":"Table 21","seats":"2"},
{"id":"8","name":"Table 22","seats":"2"},
{"id":"9","name":"Table 23","seats":"5"},
{"id":"10","name":"Table 24","seats":"5"},
{"id":"11","name":"Table 25","seats":"6"},
{"id":"12","name":"Table 26","seats":"6"}
]}
]
Displaying Table Seats
In the next step, we will add a column to the row header that will display a number of seats for every table.
The row header columns can be specified using rowHeaderColumns property. In the default configuration, the Scheduler automatically adjusts the column width to match the content (row header width auto-fit is enabled).
The actual number of seats for every table is available in the resources
array (see backend_resources.php script) as seats
property of each child item. We will use onBeforeRowHeaderRender event handler to display and format the column HTML.
scheduler.component.ts
import {Component, ViewChild, AfterViewInit} from "@angular/core";
import {DayPilot, DayPilotSchedulerComponent} from "daypilot-pro-angular";
import {DataService, CreateEventParams, MoveEventParams, UpdateEventParams} from "./data.service";
@Component({
selector: 'scheduler-component',
template: `<daypilot-scheduler [config]="config" #scheduler></daypilot-scheduler>`,
styles: [``]
})
export class SchedulerComponent implements AfterViewInit {
@ViewChild("scheduler")
scheduler!: DayPilotSchedulerComponent;
config: DayPilot.SchedulerConfig = {
// ...
rowHeaderColumns: [
{title: "Table", display: "name"},
{title: "Seats"}
],
onBeforeRowHeaderRender: args => {
if (args.row.data.seats && args.row.columns[0]) {
args.row.columns[1].html = args.row.data.seats + " seats";
}
},
};
constructor(private ds: DataService) {
}
ngAfterViewInit(): void {
this.ds.getResources().subscribe(resources => this.scheduler.control.update({ resources }));
// ...
}
// ...
}
Business Hours
We will limit the displayed timeline to business hours (from 11 AM to 12 AM). The restaurant business hours can be defined using businessBeginsHour and businessEndsHour properties. By default, the non-business hours are visible and have a different cell background color. The Angular Scheduler component can hide the non-business hours - instead of the the non-business hours, the Scheduler will display a vertical line that indicates that the timeline is interrupted. See also Hiding Non-Business Hours.
import {Component, ViewChild, AfterViewInit} from "@angular/core";
import {DayPilot, DayPilotSchedulerComponent} from "daypilot-pro-angular";
import {DataService, CreateEventParams, MoveEventParams, UpdateEventParams} from "./data.service";
@Component({
selector: 'scheduler-component',
template: `<daypilot-scheduler [config]="config" #scheduler></daypilot-scheduler>`,
styles: [``]
})
export class SchedulerComponent implements AfterViewInit {
@ViewChild("scheduler")
scheduler!: DayPilotSchedulerComponent;
config: DayPilot.SchedulerConfig = {
// ...
businessBeginsHour: 11,
businessEndsHour: 24,
showNonBusiness: false,
// ...
};
// ...
}
Loading Table Reservations
Now it's time to load the table reservation data. We subscribe to getEvents() method of the DataService class which loads the data from the server side. We limit the event date by the Scheduler grid start (visibleStart() method) and end (visibleEnd() method).
While it is possible to use the Angular change detection and load the data using the [events]
attribute of the <daypilot-scheduler>
tag, we will use the direct API to load the reservation data to improve performance.
scheduler.component.ts
import {Component, ViewChild, AfterViewInit} from "@angular/core";
import {DayPilot, DayPilotSchedulerComponent} from "daypilot-pro-angular";
import {DataService} from "./data.service";
@Component({
selector: 'scheduler-component',
template: `<daypilot-scheduler [config]="config" #scheduler></daypilot-scheduler>`,
styles: [``]
})
export class SchedulerComponent implements AfterViewInit {
@ViewChild("scheduler")
scheduler!: DayPilotSchedulerComponent;
// ...
constructor(private ds: DataService) {
}
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});
});
}
}
data.service.ts
import {Injectable} from '@angular/core';
import {Observable} from 'rxjs';
import {DayPilot} from 'daypilot-pro-angular';
import {HttpClient} from "@angular/common/http";
@Injectable()
export class DataService {
constructor(private http : HttpClient){
}
getEvents(from: DayPilot.Date, to: DayPilot.Date): Observable<any[]> {
return this.http.get(`/api/backend_events.php?from=${from}&to=${to}`) as Observable<any>;
}
// ...
}
The backend_events.php script loads the table reservations from a database and returns a JSON array. Only the reservations for the selected time range (from
and to
query string parameters) are loaded.
<?php
require_once '_db.php';
$stmt = $db->prepare('SELECT * FROM events WHERE NOT ((end <= :start) OR (start >= :end))');
$stmt->bindParam(':start', $_GET["from"]);
$stmt->bindParam(':end', $_GET["to"]);
$stmt->execute();
$result = $stmt->fetchAll();
class Event {
public $id;
public $text;
public $start;
public $end;
public $resource;
}
$events = array();
foreach($result as $row) {
$e = new Event();
$e->id = $row['id'];
$e->text = $row['name'];
$e->start = $row['start'];
$e->end = $row['end'];
$e->resource = (int) $row['resource_id'];
$events[] = $e;
}
header('Content-Type: application/json');
echo json_encode($events);
Sample JSON response:
[
{"id":8,"text":"Ms. Williams","start":"2025-06-06T18:00:00","end":"2025-06-06T19:30:00","resource":1},
{"id":9,"text":"Mr. Lee","start":"2025-06-06T18:00:00","end":"2025-06-06T20:00:00","resource":3},
{"id":10,"text":"Mr. Brown","start":"2025-06-06T17:00:00","end":"2025-06-06T19:00:00","resource":5}
]
We need to load both the table and reservation data in ngAfterViewInit()
. We have used two separate HTTP calls (/api/backend_events.php
for reservations and /api/backend_resources.php
for tables).
So far, the HTTP calls were executed independently and the Scheduler was updated after each HTTP call was finished. This causes two updates of the Angular Scheduler component, which is redundant.
In the next step, we will execute the HTTP calls in parallel and wait for both of them to complete before updating the Scheduler (just once):
async ngAfterViewInit(): Promise<void> {
const promiseResources = lastValueFrom(this.ds.getResources());
const from = this.scheduler.control.visibleStart();
const to = this.scheduler.control.visibleEnd();
const promiseEvents = lastValueFrom(this.ds.getEvents(from, to));
const [resources, events] = await Promise.all([promiseResources, promiseEvents]);
this.scheduler.control.update({
resources,
events
});
}
Filtering Tables by Number of Seats
Now we are going to add a filter that helps users find a free table with a specified number of seats.
The filter is implemented as a simple drop-down list:
<div class="filter">
Filter:
<select (change)="seatFilterChange($event);" [(ngModel)]="seatFilter">
<option *ngFor="let it of seats" [ngValue]="it.value">{{it.name}}</option>
</select>
</div>
We will watch the <select> element for changes (see seatFilter() method) and use row filtering feature of the Scheduler to hide rows that don't meet the selected criteria.
The rows.filter() method requires the filter parameter as an argument. However, we replace it with an empty object ({}) and read the filter parameters directly from this.seatFilter. This will make it easier to add a filter with multiple conditions in the next step (see Filtering Tables by Time Slot below). The empty object is necessary because calling rows.filter() without an argument clears the filter.
The Scheduler uses onRowFilter event handler to determine whether a row should be visible or not. In this event handler, we use the row metadata (number of seats available as args.row.data.seats) to check the condition.
See also the row filtering tutorial:
scheduler.component.ts
import {Component, ViewChild, AfterViewInit} from "@angular/core";
import {DayPilot, DayPilotSchedulerComponent} from "daypilot-pro-angular";
import {DataService, CreateEventParams, MoveEventParams, UpdateEventParams} from "./data.service";
@Component({
selector: 'scheduler-component',
template: `
<div class="filter">
Filter:
<select (change)="seatFilterChange($event);" [(ngModel)]="seatFilter">
<option *ngFor="let it of seats" [ngValue]="it.value">{{it.name}}</option>
</select>
</div>
<daypilot-scheduler [config]="config" #scheduler></daypilot-scheduler>`,
styles: [`
...
`]
})
export class SchedulerComponent implements AfterViewInit {
@ViewChild("scheduler")
scheduler!: DayPilotSchedulerComponent;
seats: any[] = [
{ name: "All", value: 0},
{ name: "2+ seats", value: 2},
{ name: "3+ seats", value: 3},
{ name: "4+ seats", value: 4},
{ name: "5+ seats", value: 5},
{ name: "6 seats", value: 6},
];
seatFilter: number = 0;
config: DayPilot.SchedulerConfig = {
// ...
onRowFilter: args => {
let seatsMatching = args.row.data.seats >= this.seatFilter;
args.visible = seatsMatching;
},
};
constructor(private ds: DataService) {
}
seatFilterChange(ev): void {
this.scheduler.control.rows.filter({});
}
// ...
}
Filtering Tables by Time Slot
We will add one more way to filter the tables: by a specified time slot. After selecting a time slot the Scheduler will hide tables (rows) that are not available at that time. The selected time slot will be highlighted (green background):
First, we add an event handler for onTimeHeaderClick event. This will activate the filter for the given time range.
We also expand the onRowFilter event handler to use both the number of seats and the time range during filtering.
// ...
export class SchedulerComponent implements AfterViewInit {
// ...
timeFilter: {start: DayPilot.Date, end: DayPilot.Date } | null = null;
config: DayPilot.SchedulerConfig = {
// ...
onTimeHeaderClick: args => {
this.timeFilter = {
start: args.header.start,
end: args.header.end
};
this.scheduler.control.rows.filter({});
},
onRowFilter: args => {
const seatsMatching = args.row.data.seats >= this.seatFilter;
const timeMatching = !this.timeFilter || !args.row.events.all().some(e => this.overlaps(e.start(), e.end(), this.timeFilter.start, this.timeFilter.end));
args.visible = seatsMatching && timeMatching;
},
// ...
};
// ...
overlaps(start1: DayPilot.Date, end1: DayPilot.Date, start2: DayPilot.Date, end2: DayPilot.Date): boolean {
return !(end1 <= start2 || start1 >= end2);
}
}
The selected time range will be highlighted using onBeforeTimeHeaderRender(time headers) and onBeforeCellRender (grid cells) event handlers. It applies a custom CSS class to headers/cells that overlap with the selection.
// ...
export class SchedulerComponent implements AfterViewInit {
// ...
timeFilter: {start: DayPilot.Date, end: DayPilot.Date } | null = null;
config: DayPilot.SchedulerConfig = {
// ...
onBeforeCellRender: args => {
if (!this.timeFilter) {
return;
}
if (this.overlaps(args.cell.start, args.cell.end, this.timeFilter.start, this.timeFilter.end)) {
args.cell.cssClass = "cell_selected";
}
},
onBeforeTimeHeaderRender: args => {
if (this.timeFilter) {
if (args.header.start >= this.timeFilter.start && args.header.end <= this.timeFilter.end) {
args.header.cssClass = "timeheader_selected";
}
}
},
// ...
};
// ...
}
Users can clear the time filter by clicking the "X" link in the filter label. This fires clearTimeFilter() method:
import {Component, ViewChild, AfterViewInit} from "@angular/core";
import {DayPilot, DayPilotSchedulerComponent} from "daypilot-pro-angular";
import {DataService, CreateEventParams, MoveEventParams, UpdateEventParams} from "./data.service";
@Component({
selector: 'scheduler-component',
template: `
<div class="filter">
Filter:
<select (change)="seatFilterChange($event);" [(ngModel)]="seatFilter">
<option *ngFor="let it of seats" [ngValue]="it.value">{{it.name}}</option>
</select>
<span *ngIf="timeFilter" class="timefilter">
{{timeFilter.start.toString("d/M/yyyy")}}
{{timeFilter.start.toString("h:mm tt")}} - {{timeFilter.end.toString("h:mm tt")}}
<a href="#" (click)="clearTimeFilter()" class="remove">×</a>
</span>
</div>
<daypilot-scheduler [config]="config" #scheduler></daypilot-scheduler>`,
styles: [`...`]
})
export class SchedulerComponent implements AfterViewInit {
// ...
clearTimeFilter(): boolean {
this.timeFilter = null;
this.scheduler.control.update();
return false;
}
}
MySQL Support
By default, the PHP backend project uses a local SQLite database. This is helpful for quick testing but the capabilities of SQLite database are limited.
You can switch to a MySQL database by editing the _db.php
file:
<?php
// use sqlite
// require_once '_db_sqlite.php';
// use MySQL
require_once '_db_mysql.php';
Don't forget to edit the file header and adjust the MySQL connection parameters as needed:
_db_mysql.php
<?php
$host = "127.0.0.1";
$port = 3306;
$username = "username";
$password = "password";
$database = "restaurant"; // the database will be created if it doesn't exist
// ...
The MySQL database specified using the $database
variable ("restaurant"
by default) will be created automatically. However, make sure that the specified user has permissions to create/access the database.
MySQL Database Schema
The project uses a simple database schema with three tables:
groups
table defines table groupsresources
table defines tablesevents
table stores reservation data
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,
PRIMARY KEY (`id`)
);
CREATE TABLE `groups` (
`id` INT(11) NOT NULL,
`name` VARCHAR(200) NULL DEFAULT NULL,
PRIMARY KEY (`id`)
);
CREATE TABLE `resources` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`name` VARCHAR(200) NULL DEFAULT NULL,
`seats` INT(11) NULL DEFAULT NULL,
`group_id` INT(11) NULL DEFAULT NULL,
PRIMARY KEY (`id`)
);
History
July 7, 2024: Upgraded to Angular 18, DayPilot Pro for JavaScript 2024.3.5969. PHP 8+ compatibility. Styling updated.
June 15, 2021: Upgraded to Angular 12, DayPilot Pro for JavaScript 2021.2.5009; using system fonts; using direct API for table and reservation loading.
November 26, 2020: Upgraded to Angular 11, DayPilot Pro for JavaScript 2020.4.4766.
August 3, 2020: Upgraded to Angular 10, DayPilot Pro for JavaScript 2020.3.
May 22, 2020: Upgraded to Angular 9, DayPilot Pro for JavaScript 2020.2.4470, tabular mode for row headers
June 8, 2019: Upgraded to Angular 8, DayPilot Pro for JavaScript 2019.2.3871
June 3, 2018: Upgraded to Angular 6, DayPilot Pro for JavaScript 2018.2.3297
February 17, 2018: Upgraded to Angular 5, DayPilot Pro for JavaScript 2018.1.3169
June 6, 2017: Initial release (Angular 4)