Overview
This tutorial demonstrates how to build an ASP.NET Core application that uses the Scheduler UI component for managing restaurant table reservations. Key features:
Filtering Tables: Filter tables based on the currently selected reservation’s time range and seat requirements.
Filter Indicators: Display the filter criteria (time, number of seats) in a dedicated section above the scheduler. Rows that don’t match the criteria are hidden, and non-applicable time slots are highlighted or disabled.
Unassigned Reservations: Use a special row for new reservations that aren’t assigned to a specific table yet. This row is frozen at the top so it’s always visible.
Context Menu: Quickly assign an unassigned reservation to the first available table. You can also edit, delete, or filter tables for any existing reservation.
Drag and Drop: Move reservations to a different table by dragging them. By default, the same start/end times are preserved; holding Shift while dragging allows time changes (within enforced business hours and seat limits).
ASP.NET Core and DayPilot Pro: The project includes a trial version of DayPilot Pro for JavaScript (see License information in the repository).
For an introduction to using DayPilot JavaScript Scheduler component in an ASP.NET Core application (loading data, configuring the Scheduler, handling user events), see the ASP.NET Core Scheduler tutorial.
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.
Displaying Restaurant Tables as Rows in the Scheduler
To load the rows into the JavaScript Scheduler component, call the server-side endpoint that returns the table data (organized by location) from /api/Tables
. The following code retrieves the table list and updates the Scheduler resources:
const app = {
async init() {
const {data: tables} = await DayPilot.Http.get("/api/Tables");
tables.forEach(table => {
table.id = "R" + table.id;
});
const unassigned = { name: "Unassigned", id: -1, frozen: "top", marginBottom: 10, columns: [] };
tables.splice(0, 0, unassigned);
scheduler.update({
resources: tables
});
},
};
Before passing the table data to the Scheduler, we need to make two adjustments:
The top level of the resource tree stores locations. In order to make the IDs unique, we prefix the location IDs with
"R"
.We insert a special frozen row at the top to hold any unassigned reservations.
Frozen rows are a powerful feature that make it easier to work with a Scheduler grid containing many rows. In addition to storing unscheduled reservations, you can use them to display summaries and availability charts.
The structure of the row data needs to follow the format described in the documentation for the resources property of the Scheduler component.
The ASP.NET Core backend defines the JSON endpoint that returns table data in the TablesController
class:
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 Project.Models;
namespace Project.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class TablesController : ControllerBase
{
private readonly RestaurantDbContext _context;
public TablesController(RestaurantDbContext context)
{
_context = context;
}
// GET: api/Tables
[HttpGet]
public async Task<ActionResult<IEnumerable<Location>>> GetTables()
{
// return locations with tables
return await _context.Locations.Include(t => t.Tables).ToListAsync();
}
// ...
}
}
Here is a sample response in JSON format:
[
{
"id": 1,
"name": "Location 1",
"children": [
{
"id": 1,
"name": "Table 1",
"seats": 3,
"locationId": 1
},
{
"id": 2,
"name": "Table 2",
"seats": 2,
"locationId": 1
},
{
"id": 3,
"name": "Table 3",
"seats": 2,
"locationId": 1
},
{
"id": 4,
"name": "Table 4",
"seats": 4,
"locationId": 1
}
],
"expanded": true
},
{
"id": 2,
"name": "Location 2",
"children": [
{
"id": 5,
"name": "Table 5",
"seats": 4,
"locationId": 2
},
// ...
],
"expanded": true
},
// ...
]
Creating a New Unassigned Reservation
When you create a new reservation, you have two options:
Assign it directly to the selected table, or
Place it in the Unassigned row to store it without a designated table.
This Unassigned row is displayed at the top of the Scheduler in a frozen section that always remains visible, making it easy to drag the reservation to any table, even if it’s at the very bottom of the list.
Each reservation specifies the basic requirements:
Start and end time
Number of seats
These parameters allow our app to determine whether a table is available and can accommodate the requested number of seats.
The reservation creation is handled by the onTimeRangeSelected event:
onTimeRangeSelected: async args => {
const initData = {
start: args.start,
end: args.end,
resource: args.resource,
text: "Person 1",
seats: 4
};
const modal = await DayPilot.Modal.form(app.reservationForm, initData);
scheduler.clearSelection();
if (modal.canceled) {
return;
}
const resourceId = modal.result.resource;
if (resourceId !== -1) {
const row = scheduler.rows.find(resourceId);
if (row.data.seats < modal.result.seats) {
await DayPilot.Modal.alert("Not enough seats at this table. The reservation will be created without a table.");
modal.result.resource = -1;
}
}
const {data} = await DayPilot.Http.post("/api/Reservations", modal.result);
scheduler.events.add(data);
},
The POST HTTP request creates the new reservation in the database:
[Route("api/[controller]")]
[ApiController]
public class ReservationsController : ControllerBase
{
// ...
// POST: api/Reservations
[HttpPost]
public async Task<ActionResult<Reservation>> PostReservation(Reservation reservation)
{
_context.Reservations.Add(reservation);
await _context.SaveChangesAsync();
return CreatedAtAction("GetReservation", new { id = reservation.Id }, reservation);
}
// ...
}
Finding a Free Table for an Unassigned Reservation
The reservation’s context menu includes a “Find free tables” option, which helps locate a free table for the selected reservation:
const scheduler = new DayPilot.Scheduler("scheduler", {
// ...
contextMenu: new DayPilot.Menu({
onShow: args => {
const menu = args.menu;
menu.items[1].disabled = args.source.data.resource !== -1;
},
items: [
{
text: "Find free tables",
onClick: args => {
app.useFilter(args.source);
}
},
{
text: "Assign to the first available table",
onClick: args => {
app.assignReservation(args.source);
}
},
{
text: "-"
},
{
text: "Edit...",
onClick: args => {
app.editReservation(args.source);
}
},
{
text: "Delete",
onClick: args => {
app.deleteReservation(args.source);
}
},
]
}),
// ...
});
When “Find free tables” is selected, the onClick
handler calls the app.useFilter()
function, which applies a filter to the Scheduler rows. This filter hides tables that are unsuitable for the reservation’s requirements (for instance, if they’re already occupied or have insufficient seating).
In the next section, we’ll explore how the row filter logic works.
Filtering Matching Tables
The useFilter()
function applies a row filter based on the reservation’s requirements:
It stores the selected reservation in the
app.filter
variable.It displays the reservation’s start/end time and required seats in a dedicated section above the Scheduler.
It calls rows.filter() to hide rows that don’t meet the criteria.
useFilter(event) {
app.filter = event;
const start = event.start();
const end = event.end();
const timeFilter = app.elements.timeFilter;
timeFilter.innerText = `${start.toString("h:mm tt")} - ${end.toString("h:mm tt")} (${start.toString("MMMM d, yyyy")})`;
const seatFilter = app.elements.seatFilter;
seatFilter.innerText = event.data.seats + " seat" + (event.data.seats > 1 ? "s" : "");
app.elements.filter.style.visibility = "visible";
scheduler.rows.filter(event);
},
The Scheduler uses the onRowFilter event handler to determine whether each row remains visible:
onRowFilter: args => {
const event = args.filterParam;
const {resource, start, end, seats} = event.data;
const existingReservations = args.row.events.forRange(start, end);
const availableSeats = args.row.data.seats;
const isThisRow = args.row.id === resource;
const enoughSeats = seats <= availableSeats;
const free = existingReservations.length === 0;
args.visible = isThisRow || (enoughSeats && free);
},
Here, we check for existing reservations in the selected time range (existingReservations
) and whether the table has enough seats (availableSeats
).
When the filter is active, the onBeforeCellRender event updates the appearance of other time slots to clearly indicate which ones are outside the specified range:
onBeforeCellRender: args => {
// ...
if (app.filter) {
const start = app.filter.start();
const end = app.filter.end();
if (args.cell.start < start || args.cell.end > end) {
args.cell.backColor = "#b6d7a8";
args.cell.disabled = true;
}
}
},
By disabling these slots, you ensure the user doesn’t accidentally move the reservation to a time that doesn’t match the filter criteria.
Clearing the Row Filter
To clear the filter, users can click the Clear button located in the filter section above the Scheduler:
const app = {
elements: {
filter: document.querySelector("#filter"),
timeFilter: document.querySelector("#time-filter"),
seatFilter: document.querySelector("#seat-filter"),
clearFilter: document.querySelector("#clear-filter")
},
filter: null,
// ...
addEventListeners: function() {
this.elements.clearFilter.addEventListener("click", () => {
app.clearFilter();
});
},
clearFilter() {
app.filter = null;
app.elements.filter.style.visibility = "hidden";
scheduler.rows.filter(null);
}
};
Calling scheduler.rows.filter(null)
resets the filter and displays all rows again. The filter is also cleared automatically in the onEventMove event when a reservation is moved to a new position (i.e., assigned to a table):
onEventMove: async args => {
const data = {
...args.e.data,
start: args.newStart,
end: args.newEnd,
resource: args.newResource,
};
await DayPilot.Http.put(`/api/Reservations/${args.e.id()}`, data);
if (app.filter && args.e.data.id === app.filter.id()) {
app.clearFilter();
}
},
Changing the Table Assignment
To prevent accidental changes to a reservation’s time when moving it to another table, we handle the onEventMoving event. This event handler lets us customize the drag and drop moving in real time - if the user is not holding the Shift key, the start and end times are reset to their original values, effectively locking the reservation’s time.
Changing the time is allowed only if the user holds Shift while dragging. In that case, the reservation can be moved freely, but the code still checks the business hour boundaries to ensure the reservation doesn’t overlap into unavailable times:
onEventMoving: args => {
// only allow changing time if the shift key is pressed
const shift = args.shift;
if (!shift) {
args.start = args.e.start();
args.end = args.e.end();
}
else {
const duration = args.end.getTime() - args.start.getTime();
const dayStart = args.start.getDatePart().addHours(scheduler.businessBeginsHour);
if (args.start < dayStart) {
args.start = dayStart;
args.end = dayStart.addTime(duration);
}
const dayEnd = args.start.getDatePart().addHours(scheduler.businessEndsHour);
if (args.end > dayEnd) {
args.end = dayEnd;
args.start = dayEnd.addTime(-duration);
}
}
},
Checking Number of Seats during Drag and Drop
In addition to preventing accidental time changes, we also need to validate whether the target table has enough seats and ensure there are no conflicting reservations in the same time slot.
onEventMoving: args => {
const requiredSeats = args.e.data.seats;
const targetRow = args.row;
const availableSeats = targetRow.data.seats;
// ...
const existingReservations = targetRow.events.forRange(args.start, args.end).filter(e => e.id() !== args.e.id());
if (requiredSeats > availableSeats) {
args.allowed = false;
args.right.enabled = true;
args.right.html = "Not enough seats";
}
else if (existingReservations.length > 0 && targetRow.id !== -1) {
args.allowed = false;
args.right.enabled = true;
args.right.html = "Conflict with an existing reservation";
}
},
In this step, we extend the onEventMoving
handler implementation with the following rules:
Seats check: If the required seats exceed
availableSeats
, the move is forbidden.Conflict check: If another reservation exists in the same timeframe (
existingReservations.length > 0
), the move is also forbidden.
Enforcing a Minimum Reservation Duration
We will use the onTimeRangeSelecting and onEventResizing event handlers to apply some real-time rules:
For rows other than the frozen “Unassigned” row, we check for any existing reservations to prevent double booking.
We enforce a minimum reservations duration of 1 hour.
We adjust the reservation start and end to prevent booking reservations that span multiple days.
The onTimeRangeSelecting
event handler offers two ways to handle disallowed scenarios:
Modify the selection (
args.start
/args.end
) to correct the issue (e.g., shorten a reservation that would extend past midnight).Mark the selection as forbidden (
args.allowed = false
) if it cannot be fixed automatically.
onTimeRangeSelecting: args => {
const movingEnd = args.anchor === args.start;
const dayStart = args.anchor.getDatePart().addHours(scheduler.businessBeginsHour);
const dayEnd = args.anchor.getDatePart().addHours(scheduler.businessEndsHour);
if (args.start < dayStart) {
args.start = dayStart;
}
if (args.end > dayEnd) {
args.end = dayEnd;
}
const duration = new DayPilot.Duration(args.start, args.end);
const minDuration = DayPilot.Duration.ofHours(1);
if (duration < minDuration) {
if (movingEnd) {
args.end = args.start.addTime(minDuration.ticks);
} else {
args.start = args.end.addTime(-minDuration.ticks);
}
}
const finalDuration = new DayPilot.Duration(args.start, args.end);
if (args.start < dayStart) {
args.start = dayStart;
args.end = dayStart.addTime(finalDuration.ticks);
}
if (args.end > dayEnd) {
args.end = dayEnd;
args.start = dayEnd.addTime(-finalDuration.ticks);
}
const totalHours = finalDuration.totalHours();
const endingS = totalHours > 1 ? "s" : "";
args.right.enabled = true;
args.right.html = `${totalHours} hour${endingS}`;
if (args.row.id !== -1) {
const existingReservations = args.row.events.forRange(args.start, args.end);
if (existingReservations.length > 0) {
args.allowed = false;
args.right.enabled = true;
args.right.html = "Conflict with an existing reservation";
}
}
}
A similar approach is used in onEventResizing
. In addition to enforcing business rules during resizing, it dynamically updates a label (next to the shadow of the event being resized) to show the new duration in real time:
onEventResizing: args => {
const targetRow = scheduler.rows.find(args.e.resource());
const movingEnd = args.anchor === args.start;
const dayStart = args.anchor.getDatePart().addHours(scheduler.businessBeginsHour);
if (args.start < dayStart) {
args.start = dayStart;
}
const dayEnd = args.anchor.getDatePart().addHours(scheduler.businessEndsHour);
if (args.end > dayEnd) {
args.end = dayEnd;
}
const duration = new DayPilot.Duration(args.start, args.end);
const minDuration = DayPilot.Duration.ofHours(1);
if (duration < minDuration) {
if (movingEnd) {
args.end = args.start.addTime(minDuration.ticks);
} else {
args.start = args.end.addTime(-minDuration.ticks);
}
}
const messageTarget = movingEnd ? args.right : args.left;
const totalHours = new DayPilot.Duration(args.start, args.end).totalHours();
const endingS = totalHours > 1 ? "s" : "";
messageTarget.enabled = true;
messageTarget.html = `${totalHours} hour${endingS}`;
const existingReservations = targetRow.events.forRange(args.start, args.end).filter(e => e.id() !== args.e.id());
if (existingReservations.length > 0 && targetRow.id !== -1) {
args.allowed = false;
messageTarget.html = "Conflict with an existing reservation";
}
},
Entity Framework Model Classes
The database structure consists of three Entity Framework model classes: Table, Location, and Reservation.
public class Reservation
{
public int Id { get; set; }
public string Text { get; set; }
public DateTime? Start { get; set; }
public DateTime? End { get; set; }
public int Seats { get; set; }
[JsonPropertyName("resource")]
public int? TableId { get; set; }
}
public class Table
{
public int Id { get; set; }
public string? Name { get; set; }
public int Seats { get; set; }
public int LocationId { get; set; }
}
public class Location
{
public int Id { get; set; }
public string? Name { get; set; }
[JsonPropertyName("children")]
public List<Table> Tables { get; set; }
[IgnoreDataMember]
public bool Expanded => true;
}
The connection string is defined in the appsettings.json
file:
{
...
"ConnectionStrings": {
"RestaurantDbContext": "Server=(localdb)\\mssqllocaldb;Database=DayPilot.TutorialAspNetCoreRestaurant;Trusted_Connection=True"
}
}
When you run the application for the first time, the database is created and initialized automatically. To use this setup, ensure that Microsoft SQL Server (LocalDB) is installed on your development machine.
Full Source Code
Here is the full source code of the frontend of our restaurant table reservation application:
Index.cshtml
@page
@model IndexModel
@{
ViewData["Title"] = "Home page";
}
<div class="filter toolbar" id="filter">
Filter: <span id="time-filter" class="filter-item"></span> <span id="seat-filter" class="filter-item"></span> <button id="clear-filter">Clear</button>
</div>
<div id="scheduler"></div>
@section Scripts {
<script src="lib/daypilot/daypilot-all.min.js" asp-append-version="true"></script>
<script src="js/index.js" asp-append-version="true"></script>
}
index.js
const scheduler = new DayPilot.Scheduler("scheduler", {
treeEnabled: true,
treePreventParentUsage: true,
cellWidth: 50,
scale: "CellDuration",
cellDuration: 15,
timeHeaders: [
{ groupBy: "Day" },
{ groupBy: "Hour" },
{ groupBy: "Cell", format: "mm" }
],
businessBeginsHour: 11,
businessEndsHour: 24,
businessWeekends: true,
showNonBusiness: false,
rowMarginBottom: 2,
rowMarginTop: 2,
eventBorderRadius: 6,
durationBarVisible: false,
startDate: DayPilot.Date.today().firstDayOfMonth(),
days: DayPilot.Date.today().daysInMonth(),
rowHeaderColumns: [
{ name: "Name", display: "name" },
{ name: "Seats", display: "seats" }
],
contextMenu: new DayPilot.Menu({
onShow: args => {
const menu = args.menu;
menu.items[1].disabled = args.source.data.resource !== -1;
},
items: [
{
text: "Find free tables",
onClick: args => {
app.useFilter(args.source);
}
},
{
text: "Assign to the first available table",
onClick: args => {
app.assignReservation(args.source);
}
},
{
text: "-"
},
{
text: "Edit...",
onClick: args => {
app.editReservation(args.source);
}
},
{
text: "Delete",
onClick: args => {
app.deleteReservation(args.source);
}
},
]
}),
onTimeRangeSelected: async args => {
const initData = {
start: args.start,
end: args.end,
resource: args.resource,
text: "Person 1",
seats: 4
};
const modal = await DayPilot.Modal.form(app.reservationForm, initData);
scheduler.clearSelection();
if (modal.canceled) {
return;
}
const resourceId = modal.result.resource;
if (resourceId !== -1) {
const row = scheduler.rows.find(resourceId);
if (row.data.seats < modal.result.seats) {
await DayPilot.Modal.alert("Not enough seats at this table. The reservation will be created without a table.");
modal.result.resource = -1;
}
}
const {data} = await DayPilot.Http.post("/api/Reservations", modal.result);
scheduler.events.add(data);
},
onRowFilter: args => {
const event = args.filterParam;
const {resource, start, end, seats} = event.data;
const existingReservations = args.row.events.forRange(start, end);
const availableSeats = args.row.data.seats;
const isThisRow = args.row.id === resource;
const enoughSeats = seats <= availableSeats;
const free = existingReservations.length === 0;
args.visible = isThisRow || (enoughSeats && free);
},
onEventClick: args => {
app.editReservation(args.e);
},
onEventMoving: args => {
const requiredSeats = args.e.data.seats;
const targetRow = args.row;
const availableSeats = targetRow.data.seats;
// only allow changing time if the shift key is pressed
const shift = args.shift;
if (!shift) {
args.start = args.e.start();
args.end = args.e.end();
}
else {
const duration = args.end.getTime() - args.start.getTime();
const dayStart = args.start.getDatePart().addHours(scheduler.businessBeginsHour);
if (args.start < dayStart) {
args.start = dayStart;
args.end = dayStart.addTime(duration);
}
const dayEnd = args.start.getDatePart().addHours(scheduler.businessEndsHour);
if (args.end > dayEnd) {
args.end = dayEnd;
args.start = dayEnd.addTime(-duration);
}
}
const existingReservations = targetRow.events.forRange(args.start, args.end).filter(e => e.id() !== args.e.id());
if (requiredSeats > availableSeats) {
args.allowed = false;
args.right.enabled = true;
args.right.html = "Not enough seats";
}
else if (existingReservations.length > 0 && targetRow.id !== -1) {
args.allowed = false;
args.right.enabled = true;
args.right.html = "Conflict with an existing reservation";
}
},
onEventResizing: args => {
const targetRow = scheduler.rows.find(args.e.resource());
const movingEnd = args.anchor === args.start;
const dayStart = args.anchor.getDatePart().addHours(scheduler.businessBeginsHour);
if (args.start < dayStart) {
args.start = dayStart;
}
const dayEnd = args.anchor.getDatePart().addHours(scheduler.businessEndsHour);
if (args.end > dayEnd) {
args.end = dayEnd;
}
const duration = new DayPilot.Duration(args.start, args.end);
const minDuration = DayPilot.Duration.ofHours(1);
if (duration < minDuration) {
if (movingEnd) {
args.end = args.start.addTime(minDuration.ticks);
} else {
args.start = args.end.addTime(-minDuration.ticks);
}
}
const messageTarget = movingEnd ? args.right : args.left;
const totalHours = new DayPilot.Duration(args.start, args.end).totalHours();
const endingS = totalHours > 1 ? "s" : "";
messageTarget.enabled = true;
messageTarget.html = `${totalHours} hour${endingS}`;
const existingReservations = targetRow.events.forRange(args.start, args.end).filter(e => e.id() !== args.e.id());
if (existingReservations.length > 0 && targetRow.id !== -1) {
args.allowed = false;
messageTarget.html = "Conflict with an existing reservation";
}
},
onEventMove: async args => {
const data = {
...args.e.data,
start: args.newStart,
end: args.newEnd,
resource: args.newResource,
};
await DayPilot.Http.put(`/api/Reservations/${args.e.id()}`, data);
if (app.filter && args.e.data.id === app.filter.id()) {
app.clearFilter();
}
},
onTimeRangeSelecting: args => {
const movingEnd = args.anchor === args.start;
const dayStart = args.anchor.getDatePart().addHours(scheduler.businessBeginsHour);
const dayEnd = args.anchor.getDatePart().addHours(scheduler.businessEndsHour);
if (args.start < dayStart) {
args.start = dayStart;
}
if (args.end > dayEnd) {
args.end = dayEnd;
}
const duration = new DayPilot.Duration(args.start, args.end);
const minDuration = DayPilot.Duration.ofHours(1);
if (duration < minDuration) {
if (movingEnd) {
args.end = args.start.addTime(minDuration.ticks);
} else {
args.start = args.end.addTime(-minDuration.ticks);
}
}
const finalDuration = new DayPilot.Duration(args.start, args.end);
if (args.start < dayStart) {
args.start = dayStart;
args.end = dayStart.addTime(finalDuration.ticks);
}
if (args.end > dayEnd) {
args.end = dayEnd;
args.start = dayEnd.addTime(-finalDuration.ticks);
}
const totalHours = finalDuration.totalHours();
const endingS = totalHours > 1 ? "s" : "";
args.right.enabled = true;
args.right.html = `${totalHours} hour${endingS}`;
if (args.row.id !== -1) {
const existingReservations = args.row.events.forRange(args.start, args.end);
if (existingReservations.length > 0) {
args.allowed = false;
args.right.enabled = true;
args.right.html = "Conflict with an existing reservation";
}
}
},
onBeforeCellRender: args => {
if (args.cell.isParent) {
args.cell.backColor = "#f8f8f8";
}
if (app.filter) {
const start = app.filter.start();
const end = app.filter.end();
if (args.cell.start < start || args.cell.end > end) {
args.cell.backColor = "#b6d7a8";
args.cell.disabled = true;
}
}
},
onBeforeEventRender: args => {
args.data.backColor = "#F4CE5B";
args.data.borderColor = "darker";
args.data.areas = [
{
right: 30,
top: 10,
text: `${args.data.seats} seat${args.data.seats > 1 ? "s" : ""}`,
style: "font-weight: bold",
backColor: "#F4CE5B"
},
{
top: 8,
right: 5,
width: 20,
height: 20,
symbol: "icons/daypilot.svg#threedots-v",
action: "ContextMenu",
borderRadius: "50%",
backColor: "#ffffffcc",
fontColor: "#666666",
}
];
}
});
scheduler.init();
const app = {
elements: {
filter: document.querySelector("#filter"),
timeFilter: document.querySelector("#time-filter"),
seatFilter: document.querySelector("#seat-filter"),
clearFilter: document.querySelector("#clear-filter")
},
filter: null,
reservationForm: [
{name: "Name", id: "text"},
{name: "Seats", id: "seats", type: "select", options: [
{ name: "1", id: 1 },
{ name: "2", id: 2 },
{ name: "3", id: 3 },
{ name: "4", id: 4 },
{ name: "5", id: 5 },
{ name: "6", id: 6 },
{ name: "7", id: 7 },
{ name: "8", id: 8 },
{ name: "9", id: 9 },
{ name: "10", id: 10 }
]
}
],
async editReservation(event) {
const modal = await DayPilot.Modal.form(app.reservationForm, event.data);
if (modal.canceled) {
return;
}
await DayPilot.Http.put(`/api/Reservations/${event.id()}`, modal.result);
scheduler.events.update(modal.result);
},
async assignReservation(event) {
const table = scheduler.rows.find(row => row.data.id !== -1 && row.data.seats >= event.data.seats && row.events.forRange(event.start(), event.end()).length === 0);
if (!table) {
await DayPilot.Modal.alert("No tables available.");
return;
}
const reservation = {...event.data, resource: table.id};
await DayPilot.Http.put(`/api/Reservations/${event.id()}`, reservation);
scheduler.events.update(reservation);
},
async deleteReservation(event) {
await DayPilot.Http.delete(`/api/Reservations/${event.id()}`);
scheduler.events.remove(event);
},
async init() {
app.addEventListeners();
const from = scheduler.visibleStart();
const to = scheduler.visibleEnd();
const promiseTables = DayPilot.Http.get("/api/Tables");
const promiseReservations = DayPilot.Http.get(`/api/Reservations?start=${from}&end=${to}`);
const [{data:tables}, {data:reservations}] = await Promise.all([promiseTables, promiseReservations]);
tables.forEach(table => {
table.id = "R" + table.id;
});
const unassigned = { name: "Unassigned", id: -1, frozen: "top", marginBottom: 10, columns: [] };
tables.splice(0, 0, unassigned);
scheduler.update({
resources: tables,
events: reservations,
scrollTo: DayPilot.Date.now().addHours(-1)
});
},
addEventListeners: function() {
this.elements.clearFilter.addEventListener("click", () => {
app.clearFilter();
});
},
useFilter(event) {
app.filter = event;
const start = event.start();
const end = event.end();
const timeFilter = app.elements.timeFilter;
timeFilter.innerText = `${start.toString("h:mm tt")} - ${end.toString("h:mm tt")} (${start.toString("MMMM d, yyyy")})`;
const seatFilter = app.elements.seatFilter;
seatFilter.innerText = event.data.seats + " seat" + (event.data.seats > 1 ? "s" : "");
app.elements.filter.style.visibility = "visible";
scheduler.rows.filter(event);
},
clearFilter() {
app.filter = null;
app.elements.filter.style.visibility = "hidden";
scheduler.rows.filter(null);
}
};
app.init();
You can download the full ASP.NET Core project using the link at the top of the tutorial.