Overview
This tutorial uses the monthly calendar component from the open-source DayPilot Lite for JavaScript package to implement a maintenance schedule application.
This maintenance scheduling application has the following features:
-
The application works with maintenance types that define a blueprint for each maintenance task.
-
Each maintenance type has a specified color, and defines a checklist of actions that need to be performed during maintenance.
-
The scheduled tasks are displayed in a monthly scheduling calendar view.
-
You can adjust the plan using drag and drop and move the scheduled tasks as needed.
-
You can schedule the date of the next maintenance directly from the task box.
-
The frontend is implemented in HTML5/JavaScript.
-
The backend is implemented using ASP.NET Core, Entity Framework and SQL Server.
This tutorial doesn’t cover the calendar basics. For an introduction to using the monthly calendar component in ASP.NET Core, please see the following tutorial:
You can also use the visual UI Builder app to configure the monthly calendar component and generate the client-side starting point for the page:
Scheduling maintenance tasks
This ASP.NET Core application lets you create and schedule regular maintenance tasks.
To create a new task, click a day where you want to schedule it. The application opens a modal dialog where you can enter the task details:
-
Resource name (machine, room, etc.)
-
Due date of the maintenance task (this is set to the day clicked)
-
Maintenance type.
The maintenance task details are stored in a MaintenanceTask model class with the following structure. The MaintenanceTask class stores information about a maintenance task scheduled for a specific date. Each task has a linked type (MaintenanceType) and stores checked checklist items in TaskItems.
public class MaintenanceTask
{
public int Id { get; set; }
public DateTime DueDate { get; set; }
public string Text { get; set; }
[JsonPropertyName("type")]
public int MaintenanceTypeId { get; set; }
public MaintenanceType MaintenanceType { get; set; }
public List<MaintenanceTaskItem> TaskItems { get; set; }
public MaintenanceTask? Next { get; set; }
public MaintenanceTask()
{
TaskItems = new List<MaintenanceTaskItem>();
}
}
Choosing the right maintenance type is important - it defines the actions that need to be performed during task completion.
As soon as you confirm the details, the application will add the task to the scheduling calendar:
The creation of a new task is handled using the onTimeRangeSelected event of the calendar component:
onTimeRangeSelected: async args => {
const form = [
{name: "Resource", id: "text"},
{name: "Date", id: "date", type: "date", disabled: true},
{name: "Type", id: "type", type: "select", options: app.data.types}
];
const data = {
start: args.start,
end: args.end,
type: app.data.types[0].id,
text: "",
};
const modal = await DayPilot.Modal.form(form, data);
if (modal.canceled) {
return;
}
const result = modal.result;
const {data:task} = await DayPilot.Http.post("/api/Tasks", result);
dp.events.add(task);
}
If the user enters the task details and confirms the submission, the app creates a new task and sends the details to the /api/Tasks endpoint, which stores a new database record using Entity Framework.
Maintenance task types
Each task has a specified type that defines the checklist items.
Adding a checklist to the maintenance task helps users follow the correct procedure during maintenance. It keeps track of what needs to be done, reduces errors, and lets other users see the current task status.
In the sample database, there are a couple of sample maintenance types defined:
-
Basic Cleanup (once a week)
-
Safety Inspection (once a month)
-
Machine Calibration (once every 3 months)
-
Preventive Maintenance (once every 6 months)
Each maintenance type defines the following task properties:
-
checklist items
-
color that will be used in the calendar
-
interval between repeated tasks
This is the structure of the MaintenanceType model class:
public class MaintenanceType
{
public int Id { get; set; }
public string Name { get; set; }
public string Period { get; set; }
public string? Color { get; set; }
public ICollection<MaintenanceTypeItem> TypeItems { get; set; }
}
The period is encoded as a simple string with the following structure "{number}{unit}", e.g. "1w" for 1 week.
The event content is customized using the onBeforeEventRender event handler:
onBeforeEventRender: args => {
const type = app.findType(args.data.type);
const items = type.checklist || [];
const checked = args.data.checklist || {};
const completed = items.filter(i => checked[i.id]);
args.data.html = "";
const total = items.length;
const done = completed.length === total && args.data.next;
args.data.backColor = type.color;
if (done) {
args.data.backColor = "#aaaaaa";
}
args.data.borderColor = "darker";
args.data.barColor = DayPilot.ColorUtil.darker(args.data.backColor, 2);
args.data.fontColor = "#ffffff";
const barColor = DayPilot.ColorUtil.darker(args.data.backColor, 3);
const barColorChecked = DayPilot.ColorUtil.darker(args.data.backColor, 3);
const barWidth = 15;
const barHeight = 15;
const barSpace = 2;
args.data.areas = [
{
top: 4,
left: 10,
text: args.data.text,
fontColor: "#ffffff",
style: "font-weight: bold"
},
{
top: 24,
left: 10,
text: `${type.name} (${completed.length}/${total})`,
fontColor: "#ffffff",
},
{
top: 4,
right: 4,
height: 22,
width: 22,
padding: 2,
fontColor: "#ffffff",
backColor: barColor,
cssClass: "area-action",
symbol: "icons/daypilot.svg#threedots-v",
style: "border-radius: 20px",
action: "ContextMenu",
toolTip: "Menu"
}
];
const nextIcon = {
bottom: 5,
right: 5,
height: 20,
width: 20,
padding: 2,
backColor: barColor,
fontColor: "#ffffff",
cssClass: "area-action",
toolTip: "Next",
action: "None",
onClick: async args => {
app.scheduleNext(args.source);
}
};
args.data.areas.push(nextIcon);
if (args.data.next) {
args.data.moveDisabled = true;
nextIcon.symbol = "#next";
}
items.forEach((it, x) => {
const isChecked = checked[it.id];
const area = {
bottom: barSpace + 4,
height: barHeight,
left: x * (barWidth + barSpace) + 10,
width: barWidth,
backColor: isChecked ? barColorChecked : barColor,
fontColor: "#ffffff",
padding: 0,
toolTip: it.name,
action: "None",
cssClass: "area-action",
onClick: async args => {
const e = args.source;
const itemId = it.id;
if (!e.data.checklist) {
e.data.checklist = {};
}
e.data.checklist[itemId] = !isChecked;
// Send the updated checklist to the server
await DayPilot.Http.post(`/api/tasks/${e.data.id}/update-checklist`, e.data.checklist);
dp.events.update(args.source);
}
};
if (isChecked) {
area.symbol = "icons/daypilot.svg#checkmark-4";
}
args.data.areas.push(area);
});
}
This event handler is a bit more complex than usual. Instead of displaying the default text, it uses active areas to create rich content and interactive elements:
-
It sets the background color depending on the maintenance task type
-
It displays the resource name and maintenance type at the specified positions.
-
It generates checkboxes for the maintenance checklist items.
-
It creates an icon for scheduling the next task after the specified interval.
Maintenance checklist
The checklist items are defined by the maintenance type associated with the task.
The task itself stores only the checked items using the MaintenanceTaskItem model class. When a user checks or unchecks an item, the application updates these records through the /api/Tasks/{id}/update-checklist endpoint.
public class MaintenanceTaskItem
{
public int Id { get; set; }
public int MaintenanceTypeItemId { get; set; }
public MaintenanceTypeItem MaintenanceTypeItem { get; set; }
public bool Checked { get; set; }
}
The number of checklist items and their status are displayed directly in the task box in the scheduling calendar.
-
You can display the full checklist by clicking on the maintenance task box in the calendar.
-
You can also check an item directly by clicking the respective box without opening the full checklist.
The checklist item text is displayed on hover.
You can show the full checklist by clicking on the task box. The onEventClick event handler delegates the work to app.showChecklist(), which opens a modal dialog with the checklist and saves the updated state.
onEventClick: async args => {
const e = args.e;
app.showChecklist(e);
},
...
async showChecklist(e) {
const type = app.findType(e.data.type);
const nextDate = e.data.nextDate ? new DayPilot.Date(e.data.nextDate).toString("dddd M/d/yyyy") : "N/A";
const form = [
{name: "Checklist"},
...type.checklist.map(i => ({...i, id: i.id.toString(), type: "checkbox" }) ),
{text: `Next: ${nextDate}`},
];
const data = e.data.checklist;
const modal = await DayPilot.Modal.form(form, data);
if (modal.canceled) {
return;
}
e.data.checklist = modal.result;
await DayPilot.Http.post(`/api/tasks/${e.data.id}/update-checklist`, e.data.checklist);
dp.events.update(e);
},
Scheduling a follow-up maintenance task
You can schedule a follow-up task using the icon in the bottom-right corner of the task box. The application calculates the next maintenance date using the period defined by the maintenance type.
After clicking the “next” icon the application opens a confirmation modal dialog with the calculated date of the next occurrence:
The scheduleNext() function checks if the next task has already been scheduled. If not, it opens a modal dialog and asks for a confirmation.
async scheduleNext(e) {
const type = app.findType(e.data.type);
const nextDate = app.nextDate(e.start(), type);
if (e.data.next) {
DayPilot.Modal.alert("Already scheduled.");
return;
}
const modal = await DayPilot.Modal.confirm(`Schedule next task for: ${nextDate.toString("M/d/yyyy")}?`);
if (modal.canceled) {
return;
}
const next = {
start: nextDate,
end: nextDate,
type: e.data.type,
text: e.data.text,
};
const {data:task} = await DayPilot.Http.post(`/api/Tasks/${e.data.id}/schedule-next`, next);
dp.events.add(task);
e.data.next = task.id;
e.data.nextDate = nextDate;
dp.events.update(e);
},
The nextDate() function calculates the date of the next task:
nextDate(date, type) {
const spec = type.period;
const {number, unit} = app.parsePeriod(spec);
if (unit.days) {
return date.addDays(number);
}
if (unit.weeks) {
return date.addDays(number*7);
}
if (unit.months) {
return date.addMonths(number);
}
if (unit.years) {
return date.addYears(number);
}
},
First, it parses the maintenance period associated with the task type using the parsePeriod() function:
parsePeriod(spec) {
const number = parseInt(spec);
const unit = {
days: spec.includes("d"),
weeks: spec.includes("w"),
months: spec.includes("m"),
years: spec.includes("y"),
get value() {
if (this.days) {
return "days";
}
if (this.weeks) {
return "weeks";
}
if (this.months) {
return "months";
}
if (this.years) {
return "years";
}
}
};
return {
number,
unit
}
}
Then it calculates the next date. The scheduleNext() function creates the follow-up task in the database using an HTTP call to the /api/Tasks/{id}/schedule-next endpoint, adds the new task to the schedule using the events.add() method and updates the current task.
Task completion and next date
The task is complete when all checklist items are checked off and when the next task is scheduled.
Completed tasks are displayed in gray.
ASP.NET Core API Controller: Tasks
This is the C# source code of the Tasks controller that defines the /api/Tasks endpoints.
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Project.Models;
namespace Project.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class Tasks : ControllerBase
{
private readonly MaintenanceDbContext _context;
public Tasks(MaintenanceDbContext context)
{
_context = context;
}
[HttpGet]
public async Task<ActionResult<IEnumerable<object>>> GetTasks([FromQuery] DateTime? start, [FromQuery] DateTime? end)
{
if (_context.Tasks == null)
{
return NotFound();
}
IQueryable<MaintenanceTask> query = _context.Tasks
.Include(t => t.MaintenanceType)
.Include(t => t.TaskItems)
.Include(t => t.Next);
if (start.HasValue)
{
query = query.Where(t => t.DueDate >= start.Value);
}
if (end.HasValue)
{
query = query.Where(t => t.DueDate <= end.Value);
}
var tasks = await query.ToListAsync();
// prevent the whole chain from loading in TaskTransformer.TransformTask
foreach(var task in tasks)
{
if(task.Next != null) {
task.Next = new MaintenanceTask
{
Id = task.Next.Id,
DueDate = task.Next.DueDate,
};
}
else {
task.Next = null;
}
}
var result = tasks.Select(TaskTransformer.TransformTask).ToList();
return result;
}
// GET: api/Tasks/5
[HttpGet("{id}")]
public async Task<ActionResult<MaintenanceTask>> GetMaintenanceTask(int id)
{
if (_context.Tasks == null)
{
return NotFound();
}
var maintenanceTask = await _context.Tasks.FindAsync(id);
if (maintenanceTask == null)
{
return NotFound();
}
return maintenanceTask;
}
// PUT: api/Tasks/5
// To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754
[HttpPut("{id}")]
public async Task<IActionResult> PutMaintenanceTask(int id, MaintenanceTask maintenanceTask)
{
if (id != maintenanceTask.Id)
{
return BadRequest();
}
_context.Entry(maintenanceTask).State = EntityState.Modified;
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!MaintenanceTaskExists(id))
{
return NotFound();
}
else
{
throw;
}
}
return NoContent();
}
// POST: api/Tasks
[HttpPost]
public async Task<ActionResult<MaintenanceTask>> PostMaintenanceTask(MaintenanceTaskDto dto)
{
var maintenanceType = await _context.Types.FindAsync(dto.Type);
if (maintenanceType == null)
{
return NotFound();
}
var task = new MaintenanceTask
{
DueDate = dto.Start,
Text = dto.Text,
MaintenanceType = maintenanceType, // Link to MaintenanceType
};
_context.Tasks.Add(task);
await _context.SaveChangesAsync();
// Re-fetch the task, including related entities
var newTask = await _context.Tasks
.Include(t => t.MaintenanceType)
.Include(t => t.TaskItems)
.FirstOrDefaultAsync(t => t.Id == task.Id);
return CreatedAtAction("GetMaintenanceTask", new { id = newTask.Id }, TaskTransformer.TransformTask(newTask));
}
// DELETE: api/Tasks/5
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteMaintenanceTask(int id)
{
var maintenanceTask = await _context.Tasks
.Include(t => t.Next)
.Include(t => t.TaskItems) // Include TaskItems
.FirstOrDefaultAsync(t => t.Id == id);
if (maintenanceTask == null)
{
return NotFound();
}
// Don't allow deletion if the next task was scheduled
if (maintenanceTask.Next != null)
{
return BadRequest("Can't delete a task that has a next task scheduled.");
}
// If this task is the "next" task of another task, clear that link
var previousTask = await _context.Tasks.FirstOrDefaultAsync(t => t.Next == maintenanceTask);
if (previousTask != null)
{
previousTask.Next = null;
_context.Entry(previousTask).State = EntityState.Modified;
}
// Remove the TaskItems
_context.TaskItems.RemoveRange(maintenanceTask.TaskItems);
_context.Tasks.Remove(maintenanceTask);
await _context.SaveChangesAsync();
return NoContent();
}
[HttpPost("{id}/update-checklist")]
public async Task<IActionResult> UpdateChecklist(int id, Dictionary<int, bool> checklist)
{
var task = await _context.Tasks
.Include(t => t.TaskItems)
.FirstOrDefaultAsync(t => t.Id == id);
if (task == null)
{
return NotFound();
}
// Check each item in the received checklist
foreach (var item in checklist)
{
var taskItem = task.TaskItems.FirstOrDefault(ti => ti.MaintenanceTypeItemId == item.Key);
if (item.Value) // If item is checked
{
if (taskItem == null) // If item doesn't exist, create it
{
taskItem = new MaintenanceTaskItem
{
Checked = true,
MaintenanceTypeItemId = item.Key
};
task.TaskItems.Add(taskItem);
}
else // If item exists, check it
{
taskItem.Checked = true;
}
}
else if (taskItem != null) // If item is unchecked and it exists, delete it
{
_context.TaskItems.Remove(taskItem);
}
}
await _context.SaveChangesAsync();
return NoContent();
}
[HttpPost("{id}/schedule-next")]
public async Task<ActionResult<MaintenanceTask>> ScheduleNextTask(int id, MaintenanceTaskDto dto)
{
var originalTask = await _context.Tasks.Include(t => t.Next).FirstOrDefaultAsync(t => t.Id == id);
if (originalTask == null)
{
return NotFound();
}
if (originalTask.Next != null)
{
return BadRequest("A next task is already scheduled for this task.");
}
var maintenanceType = await _context.Types.FindAsync(dto.Type);
if (maintenanceType == null)
{
return NotFound();
}
var nextTask = new MaintenanceTask
{
DueDate = dto.Start,
Text = dto.Text,
MaintenanceType = maintenanceType, // Link to MaintenanceType
};
_context.Tasks.Add(nextTask);
await _context.SaveChangesAsync();
// Linking the next task to the original one
originalTask.Next = nextTask;
await _context.SaveChangesAsync();
return CreatedAtAction("GetMaintenanceTask", new { id = nextTask.Id }, TaskTransformer.TransformTask(nextTask));
}
// POST: api/Tasks/{id}/due-date
[HttpPost("{id}/due-date")]
public async Task<IActionResult> UpdateTaskDueDate(int id, DueDateUpdateDto dto)
{
var task = await _context.Tasks.FindAsync(id);
if (task == null)
{
return NotFound();
}
task.DueDate = dto.Date;
_context.Entry(task).State = EntityState.Modified;
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!MaintenanceTaskExists(id))
{
return NotFound();
}
else
{
throw;
}
}
return NoContent();
}
private bool MaintenanceTaskExists(int id)
{
return (_context.Tasks?.Any(e => e.Id == id)).GetValueOrDefault();
}
}
public class DueDateUpdateDto
{
public DateTime Date { get; set; }
}
public class MaintenanceTaskDto
{
public DateTime Start { get; set; }
public DateTime End { get; set; }
public string Text { get; set; }
public int Type { get; set; }
public int Resource { get; set; } // Assuming this is an integer ID
}
public static class TaskTransformer {
public static object TransformTask(MaintenanceTask task)
{
var result = new
{
id = task.Id,
start = task.DueDate.ToString("yyyy-MM-dd"),
end = task.DueDate.ToString("yyyy-MM-dd"),
text = task.Text,
type = task.MaintenanceType.Id,
checklist = task.TaskItems.ToDictionary(ti => ti.MaintenanceTypeItemId.ToString(), ti => ti.Checked),
next = task.Next?.Id,
nextDate = task.Next?.DueDate,
};
return result;
}
}
}
Entity Framework Model Classes
Our ASP.NET Core application uses Entity Framework to handle database access. This lets us define the database structure using a model-first approach based on .NET classes.
The project defines four model classes:
-
MaintenanceTask Class: Represents a maintenance task with properties like ID, due date, description text, and a reference to its type (
MaintenanceTypeIdandMaintenanceType). It includes a list of checkedMaintenanceTaskItemobjects and an optionalNexttask, forming a linked-list style reference. The constructor initializes an empty list ofMaintenanceTaskItem. -
MaintenanceType Class: Represents different types of maintenance activities. Each type has an ID, a name, a periodicity (
Period), and an optional color. It also includes a collection ofMaintenanceTypeItemobjects that define specific items/tasks associated with that type. -
MaintenanceTypeItem Class: Defines individual items or tasks within a maintenance type. Each item has an ID, a name, and a reference to its parent maintenance type (
MaintenanceTypeIdandMaintenanceType). -
MaintenanceTaskItem Class: Represents a checked checklist item within a
MaintenanceTask. It includes an ID, a reference to the correspondingMaintenanceTypeItem, and a boolean that stores the checked state.
These classes are defined in the Models/Data.cs file, along with the MaintenanceDbContext class. The MaintenanceDbContext class provides the data access interface using DbSet properties.
In the OnModelCreating method, it seeds the database with initial data for MaintenanceType and MaintenanceTypeItem, providing pre-defined maintenance types and their associated checklists.
public class MaintenanceDbContext : DbContext
{
public DbSet<MaintenanceTask> Tasks { get; set; }
public DbSet<MaintenanceTaskItem> TaskItems { get; set; }
public DbSet<MaintenanceType> Types { get; set; }
public DbSet<MaintenanceTypeItem> TypeItems { get; set; }
public MaintenanceDbContext(DbContextOptions<MaintenanceDbContext> options) : base(options)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<MaintenanceType>().HasData(new MaintenanceType
{ Id = 1, Name = "Basic Cleanup", Period = "1w", Color = "#6aa84f"});
modelBuilder.Entity<MaintenanceType>().HasData(new MaintenanceType
{ Id = 2, Name = "Safety Inspection", Period = "1m", Color = "#f1c232"});
modelBuilder.Entity<MaintenanceType>().HasData(new MaintenanceType
{ Id = 3, Name = "Machine Calibration", Period = "3m", Color = "#4a86e8" });
modelBuilder.Entity<MaintenanceType>().HasData(new MaintenanceType
{ Id = 4, Name = "Preventive Maintenance", Period = "6m", Color = "#e06666" });
modelBuilder.Entity<MaintenanceTypeItem>().HasData(new MaintenanceTypeItem
{ Id = 1, Name = "Clean and sanitize work surfaces", MaintenanceTypeId = 1 });
modelBuilder.Entity<MaintenanceTypeItem>().HasData(new MaintenanceTypeItem
{ Id = 2, Name = "Remove waste material", MaintenanceTypeId = 1 });
modelBuilder.Entity<MaintenanceTypeItem>().HasData(new MaintenanceTypeItem
{ Id = 3, Name = "Inspect safety equipment", MaintenanceTypeId = 2 });
modelBuilder.Entity<MaintenanceTypeItem>().HasData(new MaintenanceTypeItem
{ Id = 4, Name = "Check emergency exits", MaintenanceTypeId = 2 });
modelBuilder.Entity<MaintenanceTypeItem>().HasData(new MaintenanceTypeItem
{ Id = 5, Name = "Test fire alarms", MaintenanceTypeId = 2 });
modelBuilder.Entity<MaintenanceTypeItem>().HasData(new MaintenanceTypeItem
{ Id = 6, Name = "Inspect first aid kits", MaintenanceTypeId = 2 });
modelBuilder.Entity<MaintenanceTypeItem>().HasData(new MaintenanceTypeItem
{ Id = 7, Name = "Calibrate machine sensors", MaintenanceTypeId = 3 });
modelBuilder.Entity<MaintenanceTypeItem>().HasData(new MaintenanceTypeItem
{ Id = 8, Name = "Check machine alignment", MaintenanceTypeId = 3 });
modelBuilder.Entity<MaintenanceTypeItem>().HasData(new MaintenanceTypeItem
{ Id = 9, Name = "Check for any abnormal sounds", MaintenanceTypeId = 3 });
modelBuilder.Entity<MaintenanceTypeItem>().HasData(new MaintenanceTypeItem
{ Id = 10, Name = "Inspect for wear and tear", MaintenanceTypeId = 4 });
modelBuilder.Entity<MaintenanceTypeItem>().HasData(new MaintenanceTypeItem
{ Id = 11, Name = "Replace worn parts", MaintenanceTypeId = 4 });
modelBuilder.Entity<MaintenanceTypeItem>().HasData(new MaintenanceTypeItem
{ Id = 12, Name = "Check for leaks", MaintenanceTypeId = 4 });
modelBuilder.Entity<MaintenanceTypeItem>().HasData(new MaintenanceTypeItem
{ Id = 13, Name = "Lubricate moving parts", MaintenanceTypeId = 4 });
}
}
SQL Server Database
The database connection string is defined in the appsettings.json file:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"ConnectionStrings": {
"MaintenanceContext": "Server=(localdb)\\mssqllocaldb;Database=DayPilot.TutorialAspNetCoreMaintenance;Trusted_Connection=True"
},
"AllowedHosts": "*"
}
The sample uses SQL Server with the LocalDB interface by default. On the first run, Program.cs calls context.Database.EnsureCreated();, which creates the database and seed data automatically.
To run the project, execute dotnet restore and dotnet run --project Project/Project.csproj in the solution directory. The frontend assets are bundled in the repository, so there is no separate npm install step. LocalDB is a Windows feature; on other platforms, point ConnectionStrings:MaintenanceContext to a reachable SQL Server instance.
Download
You can download the full Visual Studio solution and source code of this .NET 10 application using the link at the beginning of the tutorial.
History
-
April 19, 2026: Upgraded to .NET 10, migrated the solution to
.slnx, refreshed the screenshots, and updated to DayPilot Lite for JavaScript 2026.2.817 -
September 9, 2025: Upgraded to .NET 9, DayPilot Lite for JavaScript 2025.3.703
-
December 1, 2023: Upgraded to .NET 8, DayPilot Lite for JavaScript 2023.4.504
-
June 26, 2023: Initial release, .NET 7
DayPilot




