Features

  • A queue of unscheduled orders

  • Each order can include a checklist of tasks

  • Drag orders onto a resource’s calendar to schedule them

  • Resources displayed as columns, each split into three sub-columns for three days

  • Unschedule an order by dragging it back to the queue

  • Change the current day using a date picker or previous/next buttons

  • Assign a custom color to each order

  • ASP.NET Core 9, Entity Framework, and SQL Server power the backend

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

Queue of Unscheduled Orders

Queue of Unscheduled Orders - Workforce Scheduling in ASP.NET Core

On the left side of the main scheduling view, our app displays a queue of unscheduled orders. The queue helps with managing incoming orders:

  • Holds Unscheduled Orders
    The queue is a list of all orders that haven’t yet been assigned to a specific time or resource in the calendar.

  • Central Place for Organization
    Because the queue is always visible (on the left side of the UI), it acts like a “staging area.” Planners can easily see which items are waiting to be scheduled, reorder them based on priority, and quickly schedule them when the time is right.

The queue supports drag and drop:

  • The list is sorted, and you can change the order by dragging items.

  • You can drag orders to the calendar grid to schedule them.

  • You can also drag them back to the queue to unschedule them.

Loading and Displaying Unscheduled Orders

The queue is implemented using the DayPilot Queue component (DayPilot.Queue class).

The queue placeholder is defined in the page HTML. It is located in a column on the left side of the main page.

<div id="queue" class="queue"></div>

The Queue UI is initialized using JavaScript:

const queue = new DayPilot.Queue("queue", {
  // ... config
});
queue.init();

On startup, the application calls loadQueue(), which fetches the list of orders that are currently in the “unscheduled” state from the backend (GET /api/orders/queued).

const app = {
    async loadQueue() {
        const {data: events} = await DayPilot.Http.get("/api/orders/queued");
        queue.update({events});
    },
    // ...
};

The ASP.NET Core backend defines a GetOrdersQueued() method in the OrdersController. This method loads orders without an assigned resource and returns them in JSON format.

using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Project.Models;

namespace Project.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class OrdersController : ControllerBase
    {
        private readonly WorkforceDbContext _context;

        public OrdersController(WorkforceDbContext context)
        {
            _context = context;
        }

        // GET: api/OrdersQueued
        [HttpGet("queued")]
        public async Task<ActionResult<IEnumerable<Order>>> GetOrdersQueued()
        {
            return await _context.Orders.Where(e => e.ResourceId == null).Include(o => o.Tasks).OrderBy(e => e.Ordinal).ThenByDescending(e => e.OrdinalPriority).ToListAsync();
        }

    }
    
}

Reordering the Queue using Drag and Drop

Reordering the Queue using Drag and Drop - Workforce Scheduling in ASP.NET Core

Orders in the queue component are sorted by default.

In the database, the sort order is specified using the Ordinal field. The /api/orders/queued endpoint returns the orders already sorted. The Queue UI component respects the original order of items.

Users can drag items up or down within the queue to change their order. When an item is moved, the onEventMove event fires. It sends a PUT request to the server (/api/orders/${args.e.data.id}) to update the item’s position (i.e., the new place in the queue).

const queue = new DayPilot.Queue("queue", {
    // ...
    onEventMove: async args => {
        args.async = true;
        const {data: event} = await DayPilot.Http.put(`/api/orders/${args.e.data.id}`, {
            id: args.e.data.id,
            start: args.e.data.start,
            end: args.e.data.end,
            resource: null,
            position: args.position
        });

        if (args.external) {
            calendar.events.remove(args.e.data.id);
        }
        args.loaded();
    },
    // ...
});
queue.init();

The PutOrder() method of the OrderController in the ASP.NET Core backend updates the item position in the queue:

[HttpPut("{id}")]
public async Task<IActionResult> PutOrder(int id, PutOrderParams p)
{
    
    Order? order = await _context.Orders.FindAsync(id);

    // ...
    
    if (p.Resource == null)
    {
        order.ResourceId = null;

        // ...
        
        if (p.Position != null)
        {
            order.Ordinal = (int)p.Position;
            order.OrdinalPriority = DateTime.Now;
        }
        await _context.SaveChangesAsync();
        await CompactOrdinals();
    }


    return Ok(order);
}

Resource Calendar with Staff as Columns for Workforce Scheduling

Resource Calendar for Workforce Scheduling in ASP.NET Core

To display the main schedule, we use the JavaScript Calendar component from the DayPilot package, switched to a resource-scheduling mode, which displays staff as columns.

First, we include the calendar placeholder in the HTML:

<div id="calendar"></div>

Now we can initialize the resource-scheduling calendar:

const calendar = new DayPilot.Calendar("calendar", {
  viewType: "Resources",
  // ...
});
calendar.init();

The columns (representing staff) and the events (representing orders) are loaded from the server side.

The Calendar component supports a hierarchy of columns, allowing us to add three subcolumns for each staff member—one for the previous day, one for the current day, and one for the next day.

const app = {
    // ...
    async loadDataForDate(date) {
        const days = [-1, 0, 1];
        date = date || calendar.startDate;

        const {data: resources} = await DayPilot.Http.get("/api/resources");
        
        const columns = resources.map(r => {
            return {
                id: r.id,
                name: r.name,
                children: days.map(d => {
                    return {
                        id: r.id,
                        name: date.addDays(d).toString("ddd d MMM"),
                        start: date.addDays(d),
                        color: "#a3e484",
                    };
                })
            };
        });

        const start = date.addDays(days[0]);
        const end = date.addDays(days[days.length - 1]).addDays(1);
        
        const {data: events} = await DayPilot.Http.get(`/api/orders?start=${start}&end=${end}`);

        calendar.update({
            columns,
            events
        });
    },
    // ...
};

This is the GetResource() method in the ASP.NET Core backend, which returns the staff members:

using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Project.Models;

namespace Project.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class ResourcesController : ControllerBase
    {
        private readonly WorkforceDbContext _context;

        public ResourcesController(WorkforceDbContext context)
        {
            _context = context;
        }

        // GET: api/Resources
        [HttpGet]
        public async Task<ActionResult<IEnumerable<Resource>>> GetResource()
        {
            return await _context.Resources.ToListAsync();
        }

    }
}

And here is the GetOrders() method that returns orders for the specified date range (start and end query string parameters):

using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Project.Models;

namespace Project.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class OrdersController : ControllerBase
    {
        private readonly WorkforceDbContext _context;

        public OrdersController(WorkforceDbContext context)
        {
            _context = context;
        }

        // GET: api/Orders
        [HttpGet]
        public async Task<ActionResult<IEnumerable<Order>>> GetOrders(DateTime start, DateTime end)
        {
            return await _context.Orders.Where(e => e.ResourceId != null && e.End > start && e.Start < end).Include(o => o.Tasks).ToListAsync();
        }        
    }        
}

Scheduling an Order (Dragging to the Calendar)

Scheduling an Order (Dragging to the Calendar) - Workforce Scheduling in ASP.NET Core

You can grab an order from the queue and drag it directly onto the calendar.

  • The onEventMove event detects that the source was the queue and destination is the calendar. If args.external is true, it means the item came from a different component (the queue).

  • The backend request updates the order to have a valid resource (the staff column) and a start/end time.

  • The order is removed from the queue.

Dragging items from the queue to the calendar is enabled by default. You need to add an onEventMove handler to the calendar to update the database on drop:

const calendar = new DayPilot.Calendar("calendar", {

    // ...
    
    onEventMove: async args => {
        args.async = true;
        const {data: event} = await DayPilot.Http.put(`/api/orders/${args.e.data.id}`, {
            id: args.e.data.id,
            text: args.e.data.text,
            start: args.newStart,
            end: args.newEnd,
            resource: args.newResource
        });
        if (args.external) {
            queue.events.remove(args.e.data.id);
        }
        args.loaded();
    },

    // ...
    
});

Orders dragged from the queue are marked as external (args.external property).

Unscheduling an Order (Dragging Back to the Queue)

Unscheduling an Order (Dragging Back to the Queue) - Workforce Scheduling in ASP.NET Core

If an order needs to be removed from the calendar, you an drag it out of the calendar and onto the queue area.

  • This triggers the onEventMove event of the Queue object.

  • The backend updates the order by setting its resource property to null, indicating it’s no longer scheduled.

  • The order is removed from the calendar.

The onEventMove event handler of the Queue component manages all drop events, including those originating from an external source (in this case, the Calendar component). When an order is moved from the Calendar, it is marked as external using args.external.

const queue = new DayPilot.Queue("queue", {
    onEventMove: async args => {
        args.async = true;
        const {data: event} = await DayPilot.Http.put(`/api/orders/${args.e.data.id}`, {
            id: args.e.data.id,
            start: args.e.data.start,
            end: args.e.data.end,
            resource: null,
            position: args.position
        });

        if (args.external) {
            calendar.events.remove(args.e.data.id);
        }
        args.loaded();
    },
});

Task Checklist for Orders

Task Checklist for Orders - Workforce Scheduling in ASP.NET Core

Each order can define a set of tasks that need to be performed.

  • The tasks appear as checkboxes on each order.

  • You can use the “Tasks” > “Edit…” context menu item to open a dialog for adding or removing tasks.

  • All tasks are also listed in the “Tasks” submenu as individual items, where you can mark them as completed.

To show task indicators on each order, we use the onBeforeEventRender event handler.

  • Each task is displayed as a square box at the bottom of the order (an active area).

  • The boxes display the task number.

  • Completed tasks are marked with a check mark.

The following onBeforeEventRender handler is used to customize the order appearance in both components (Calendar and Queue):

onBeforeEventRender(args) {
    const items = args.data.tasks || [];

    const color = args.data.color || "#aaaaaa";
    args.data.backColor = color + "cc";
    args.data.borderColor = color + "44";
    args.data.barColor = DayPilot.ColorUtil.darker(args.data.backColor, 2);
    args.data.fontColor = "#ffffff";
    args.data.html = "";
    
    const barColor = DayPilot.ColorUtil.darker(args.data.backColor, 2);

    const barWidth = 15;
    const barHeight = 15;
    const barSpace = 2;

    args.data.areas = [
        {
            top: 4,
            left: 10,
            text: args.data.text,
            fontColor: "#ffffff",
        },
        {
            top: 4,
            right: 4,
            height: 22,
            width: 22,
            padding: 2,
            fontColor: "#ffffff",
            backColor: barColor,
            borderRadius: 20,
            cssClass: "area-action",
            symbol: "icons/daypilot.svg#threedots-v",
            style: "cursor: pointer",
            action: "ContextMenu",
            toolTip: "Menu"
        },
        {
            bottom: 4,
            height: barHeight + 6,
            left: 8,
            right: 4,
            backColor: "#ffffff44",
            fontColor: "#333333",
            padding: 0,
            action: "None",
            borderRadius: 3,
            style: "cursor: pointer",
            onClick: async args => {
                app.editOrderTasks(args.source);
            }
        }
    ];

    items.forEach((it, x) => {
        const isChecked = it.done;

        const area = {
            bottom: barSpace + 4,
            height: barHeight,
            left: x * (barWidth + barSpace) + 10,
            width: barWidth,
            backColor: barColor,
            fontColor: "#f0f0f0",
            padding: 0,
            toolTip: it.text,
            action: "None",
            cssClass: "area-action",
            borderRadius: 3,
            text: x + 1,
            style: "font-size: 10px",
            verticalAlignment: "center",
            horizontalAlignment: "center",
        };
        if (it.done) {
            area.text = null;
            area.symbol = "icons/daypilot.svg#checkmark-4";
        }
        args.data.areas.push(area);
    });
},

The items of the “Tasks” submenu are generated dynamically for each event, using the onShow event handler of the Menu.

const contextMenu = new DayPilot.Menu({
    onShow: args => {
        const e = args.source;
        const menu = args.menu;
        const items = menu.items;
        const tasksMenuItem = items.find(i => i.text === "Tasks");
        
        if (!tasksMenuItem) {
            return;
        }

        const tasks = e.data.tasks?.map(task => {
            return {
                text: task.text,
                symbol: task.done ? "icons/daypilot.svg#checkmark-4" : null,
                onClick: async args => {
                    const e = args.source;
                    const control = args.source.calendar;
                    task.done = !task.done;

                    await DayPilot.Http.put(`/api/orders/${e.data.id}/tasks`, e.data.tasks);

                    control.events.update(args.source);
                }
            };
        }) || [];
        
        const submenu = [
            {
                text: "Edit...",
                onClick: args => {
                    app.editOrderTasks(args.source);
                },
            },
        ];
        if (tasks.length > 0) {
            submenu.push({text: "-"});
        }
        submenu.push(...tasks);
        tasksMenuItem.items = submenu;
    },
    items: [
        {
            text: "Edit...",
            onClick: async args => {
                const e = args.source;
                const modal = await DayPilot.Modal.form(orderForm, e.data);
                if (modal.canceled) {
                    return;
                }
                const {data: event} = await DayPilot.Http.put(`/api/orders/${e.data.id}`, {
                    id: e.data.id,
                    text: modal.result.text,
                    start: e.data.start,
                    end: e.data.end,
                    resource: e.data.resource,
                    color: modal.result.color
                });
                e.data.text = modal.result.text;
                e.data.color = modal.result.color;
                args.source.calendar.events.update(e);
            }
        },
        {
            text: "-"
        },
        {
            text: "Tasks",
            items: []
        },
        {
            text: "-"
        },
        {
            text: "Delete",
            onClick: async args => {
                const e = args.source;
                const control = args.source.calendar;
                await DayPilot.Http.delete(`/api/orders/${e.data.id}`);
                control.events.remove(e.data.id);
            }
        },
    ]
});

Entity Framework Data Classes

Our ASP.NET Core class uses Entity Framework to handle the database access. We define 3 model classes that hold the data:

  • Order

  • OrderTask

  • Resource

The Order class holds the order data. It stores both scheduled and unscheduled items.

public class Order
{
    public int Id { get; set; }
    public string Text { get; set; }
    public string? Description { get; set; }
    public DateTime? Start { get; set; }
    public DateTime? End { get; set; }
    
    [JsonPropertyName("resource")]
    public int? ResourceId { get; set; }
    
    public int Ordinal { get; set; }

    public DateTime OrdinalPriority { get; set; }
    
    public string? Color { get; set; }
    
    public List<OrderTask> Tasks { get; set; }
}

The OrderTask class stores tasks related to orders:

public class OrderTask
{
    public int Id { get; set; }
    public string Text { get; set; }
    public int OrderId { get; set; }
    public bool Done { get; set; }
}

The Resource class stores the staff members:

public class Resource
{
    public int Id { get; set; }
    public string? Name { get; set; }
}

The DB context class (WorkforceDbContext) defines the entities and initializes the database with sample resources:

public class WorkforceDbContext : DbContext
{
    
    public DbSet<Resource> Resources { get; set; }
    public DbSet<Order> Orders { get; set; }
    public DbSet<OrderTask> OrderTasks { get; set; }

    public WorkforceDbContext(DbContextOptions<WorkforceDbContext> options) : base(options) { }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Resource>().HasData(new Resource { Id = 1, Name = "Staff 1" });
        modelBuilder.Entity<Resource>().HasData(new Resource { Id = 2, Name = "Staff 2" });
        modelBuilder.Entity<Resource>().HasData(new Resource { Id = 3, Name = "Staff 3" });
        modelBuilder.Entity<Resource>().HasData(new Resource { Id = 4, Name = "Staff 4" });
        modelBuilder.Entity<Resource>().HasData(new Resource { Id = 5, Name = "Staff 5" });
        modelBuilder.Entity<Resource>().HasData(new Resource { Id = 6, Name = "Staff 6" });
    }
}

Full Source Code

Here is the full source code of the UI implementation of our workforce scheduling app.

The HTML page (Index.cshtml) is quite simple - it defines the layout (two columns) and the Queue and Calendar placeholders:

@page
@model IndexModel
@{
    ViewData["Title"] = "Workforce Scheduling in ASP.NET Core: Queue, Drag-and-Drop Resource Allocation, and Task Checklists";
}

<div class="columns-parent">
    <div class="column-left">
        <button id="button-new" class="button-new">New Order...</button>
        <div id="queue" class="queue"></div>
    </div>
    <div class="column-right">
        <div class="toolbar">
            <button id="button-previous">Previous</button>
            <span id="span-date" class="selected-date"></span>
            <button id="button-next">Next</button>
            &nbsp;
            <button id="button-today">Today</button>
        </div>
        <div id="calendar"></div>        
    </div>
</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>
}

The index.js file configures the UI components (Queue, Calendar, date picker and previous/next buttons) and defines the app logic:

const contextMenu = new DayPilot.Menu({
    onShow: args => {
        const e = args.source;
        const menu = args.menu;
        const items = menu.items;
        const tasksItem = items.find(i => i.text === "Tasks");
        
        if (!tasksItem) {
            return;
        }

        const tasks = e.data.tasks?.map(task => {
            return {
                text: task.text,
                symbol: task.done ? "icons/daypilot.svg#checkmark-4" : null,
                onClick: async args => {
                    const e = args.source;
                    const control = args.source.calendar;
                    //const task = e.data.tasks.find(t => t.id === task.id);
                    task.done = !task.done;

                    // Send the updated checklist to the server
                    await DayPilot.Http.put(`/api/orders/${e.data.id}/tasks`, e.data.tasks);

                    control.events.update(args.source);
                }
            };
        }) || [];
        
        const submenu = [
            {
                text: "Edit...",
                onClick: args => {
                    app.editOrderTasks(args.source);
                },
            },
        ];
        if (tasks.length > 0) {
            submenu.push({text: "-"});
        }
        submenu.push(...tasks);
        tasksItem.items = submenu;
    },
    items: [
        {
            text: "Edit...",
            onClick: async args => {
                const e = args.source;
                const modal = await DayPilot.Modal.form(orderForm, e.data);
                if (modal.canceled) {
                    return;
                }
                const {data: event} = await DayPilot.Http.put(`/api/orders/${e.data.id}`, {
                    id: e.data.id,
                    text: modal.result.text,
                    start: e.data.start,
                    end: e.data.end,
                    resource: e.data.resource,
                    color: modal.result.color
                });
                e.data.text = modal.result.text;
                e.data.color = modal.result.color;
                args.source.calendar.events.update(e);
            }
        },
        {
            text: "-"
        },
        {
            text: "Tasks",
            items: []
        },
        {
            text: "-"
        },
        {
            text: "Delete",
            onClick: async args => {
                const e = args.source;
                const control = args.source.calendar;
                await DayPilot.Http.delete(`/api/orders/${e.data.id}`);
                control.events.remove(e.data.id);
            }
        },
    ]
});

const orderTasksForm = [
    {name: "", id: "tasks", type: "table", columns: [
            {
                name: 'Task Name',
                id: 'text',
            },
        ],}
];

const colors = [
    {name: "Green", id: "#6aa84f"},
    {name: "Blue", id: "#3d85c6"},
    {name: "Turquoise", id: "#00aba9"},
    {name: "Light Blue", id: "#56c5ff"},
    {name: "Yellow", id: "#f1c232"},
    {name: "Orange", id: "#e69138"},
    {name: "Red", id: "#cc4125"},
    {name: "Light Red", id: "#ff0000"},
    {name: "Purple", id: "#af8ee5"},
];

const orderForm = [
    {name: "Order Name", id: "text", type: "text"},
    {name: "Order Color", id: "color", type: "select", options: colors }
];

const calendar = new DayPilot.Calendar("calendar", {
    viewType: "Resources",
    dragOutAllowed: true,
    eventBorderRadius: "5px",
    columnWidthMin: 100,
    contextMenu: contextMenu,
    headerHeight: 40,
    onEventMove: async args => {
        args.async = true;
        const {data: event} = await DayPilot.Http.put(`/api/orders/${args.e.data.id}`, {
            id: args.e.data.id,
            text: args.e.data.text,
            start: args.newStart,
            end: args.newEnd,
            resource: args.newResource
        });
        if (args.external) {
            queue.events.remove(args.e.data.id);
        }
        args.loaded();
    },
    onEventResize: async args => {
        args.async = true;
        const {data: event} = await DayPilot.Http.put(`/api/orders/${args.e.data.id}`, {
            id: args.e.data.id,
            text: args.e.data.text,
            start: args.newStart,
            end: args.newEnd,
            resource: args.e.data.resource
        });
        args.loaded();
    },
    onTimeRangeSelect: async args => {
        const modal = await DayPilot.Modal.prompt("New order name:", "Order");
        calendar.clearSelection();
        if (modal.canceled) {
            return;
        }
        const e = {
            text: modal.result,
            start: args.start,
            end: args.end,
            resource: args.resource
        };
        const {data} = await DayPilot.Http.post("/api/orders", e);
        e.id = data.id;
        calendar.events.add(e);
    },
    onBeforeEventRender: args => {
        app.onBeforeEventRender(args);
    },
    onBeforeCellRender: args => {
        const column = args.cell.column;
        if (column.data.color) {
            // args.cell.backColor = column.data.color + "44";
        }
    }
    
});
calendar.init();

const queue = new DayPilot.Queue("queue", {
    eventHeight: 60,
    eventBorderRadius: "5px",
    contextMenu: contextMenu,
    onEventMove: async args => {
        args.async = true;
        const {data: event} = await DayPilot.Http.put(`/api/orders/${args.e.data.id}`, {
            id: args.e.data.id,
            start: args.e.data.start,
            end: args.e.data.end,
            resource: null,
            position: args.position
        });

        if (args.external) {
            calendar.events.remove(args.e.data.id);
        }
        args.loaded();
    },
    onBeforeEventRender: args => {
        app.onBeforeEventRender(args);
    }
});
queue.init();

const datePicker = new DayPilot.DatePicker({
    target: "span-date",
    pattern: "MMMM d, yyyy",
    onTimeRangeSelected: async args => {
        app.changeDate(args.date);
    }
});
datePicker.init();

const app = {
    elements: {
        buttonNew: document.getElementById("button-new"),
        buttonPrevious: document.getElementById("button-previous"),
        buttonNext: document.getElementById("button-next"),
        buttonToday: document.getElementById("button-today"),
        spanDate: document.getElementById("span-date")
    },
    async loadQueue() {
        const {data: events} = await DayPilot.Http.get("/api/orders/queued");
        queue.update({events});
    },
    async changeDate(date) {
        const days = [-1, 0, 1];
        date = date || calendar.startDate;

        const {data: resources} = await DayPilot.Http.get("/api/resources");
        
        const columns = resources.map(r => {
            return {
                id: r.id,
                name: r.name,
                children: days.map(d => {
                    return {
                        id: r.id,
                        name: date.addDays(d).toString("ddd d MMM"),
                        start: date.addDays(d),
                        color: "#a3e484",
                    };
                })
            };
        });

        const start = date.addDays(days[0]);
        const end = date.addDays(days[days.length - 1]).addDays(1);
        
        const {data: events} = await DayPilot.Http.get(`/api/orders?start=${start}&end=${end}`);

        calendar.update({
            columns,
            events
        });
    },
    async editOrderTasks(e) {
        const control = e.calendar;
        const modal = await DayPilot.Modal.form(orderTasksForm, e.data);
        if (modal.canceled) {
            return;
        }

        const response = await DayPilot.Http.put(`/api/orders/${e.data.id}/tasks`, modal.result.tasks);
        const updatedOrder = response.data;

        e.data.tasks = updatedOrder.tasks;
        control.events.update(e);
    },
    onBeforeEventRender(args) {
        const items = args.data.tasks || [];

        const color = args.data.color || "#aaaaaa";
        args.data.backColor = color + "cc";
        args.data.borderColor = color + "44";
        args.data.barColor = DayPilot.ColorUtil.darker(args.data.backColor, 2);
        args.data.fontColor = "#ffffff";
        args.data.html = "";
        
        const barColor = DayPilot.ColorUtil.darker(args.data.backColor, 2);

        const barWidth = 15;
        const barHeight = 15;
        const barSpace = 2;

        args.data.areas = [
            {
                top: 4,
                left: 10,
                text: args.data.text,
                fontColor: "#ffffff",
            },
            {
                top: 4,
                right: 4,
                height: 22,
                width: 22,
                padding: 2,
                fontColor: "#ffffff",
                backColor: barColor,
                borderRadius: 20,
                cssClass: "area-action",
                symbol: "icons/daypilot.svg#threedots-v",
                style: "cursor: pointer",
                action: "ContextMenu",
                toolTip: "Menu"
            },
            {
                bottom: 4,
                height: barHeight + 6,
                left: 8,
                right: 4,
                backColor: "#ffffff44",
                fontColor: "#333333",
                padding: 0,
                action: "None",
                borderRadius: 3,
                style: "cursor: pointer",
                onClick: async args => {
                    app.editOrderTasks(args.source);
                }
            }
        ];

        items.forEach((it, x) => {
            const isChecked = it.done;

            const area = {
                bottom: barSpace + 4,
                height: barHeight,
                left: x * (barWidth + barSpace) + 10,
                width: barWidth,
                backColor: barColor,
                fontColor: "#f0f0f0",
                padding: 0,
                toolTip: it.text,
                action: "None",
                cssClass: "area-action",
                borderRadius: 3,
                text: x + 1,
                // symbol: isChecked ? "icons/daypilot.svg#checkmark-4" : null,
                // symbol: "icons/daypilot.svg#checkmark-4",
                style: "font-size: 10px",
                verticalAlignment: "center",
                horizontalAlignment: "center",
            };
            if (it.done) {
                area.text = null;
                area.symbol = "icons/daypilot.svg#checkmark-4";
            }
            args.data.areas.push(area);
        });
    },
    addEventHandlers() {
        this.elements.buttonNew.addEventListener("click", async () => {
            
            const modal = await DayPilot.Modal.prompt("New order name:", "Order");
            if (modal.canceled) {
                return;
            }
            
            const e = {
                text: modal.result,
                start: DayPilot.Date.today(),
                end: DayPilot.Date.today().addHours(1),
            };
            
            const {data} = await DayPilot.Http.post("/api/orders", e);
            
            e.id = data.id;
            queue.events.add(e);
        });
        this.elements.buttonPrevious.addEventListener("click", () => {
            datePicker.select(datePicker.date.addDays(-1));
        });
        this.elements.buttonNext.addEventListener("click", () => {
            datePicker.select(datePicker.date.addDays(1));
        });
        this.elements.buttonToday.addEventListener("click", () => {
            datePicker.select(DayPilot.Date.today());
        });
        this.elements.spanDate.addEventListener("click", () => {
            datePicker.show();
        });
    },
    init() {
        this.addEventHandlers();
        datePicker.select("2025-09-01");
        this.loadQueue();
    }
};

app.init();