3

ASP.NET Core Maintenance Scheduling (Open-Source)

 10 months ago
source link: https://code.daypilot.org/92491/asp-net-core-maintenance-scheduling
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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.

  • When all checklist actions are completed, you can easily schedule the date of the next maintenance.

  • The fronted 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 a starting project:

License

Apache License 2.0

Scheduling maintenance tasks

asp.net core maintenance scheduling open source task type

This ASP.NET Core application lets you create and schedule regular maintenance tasks.

To create a new tasks, click a day where you want to schedule the task. 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:

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:

asp.net core maintenance scheduling open source machine

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 tasks and send the details to the /api/Tasks endpoint which creates a new DB record using Entity Framework.

Maintenance task types

asp.net core maintenance scheduling open source task types

Each task has a specified types which defines the checklist items.

Adding a checklist to the maintenance task helps with following the correct procedure during maintenance. It helps users keep track of what needs to be done and eliminates errors. Other users will be to 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 models 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 and icon for scheduling the next task after the specified interval.

Maintenance checklist

asp.net core maintenance scheduling open source checklist

The maintenance task consists of multiple checklist items. The checklist items are defined for the maintenance type associated with the task.

The status of each checklist item is stored using the MaintenanceTaskItem model class:

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 the item directly by clicking on the respective box without opening the full checking.

The checklist item text is displayed on hover.

asp.net core maintenance scheduling open source task

You can show the full checklist by clicking on the task box. This fires the onEventClick event handler that opens a modal dialog with task details.

onEventClick: async args => {
    const e = args.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

asp.net core maintenance scheduling open source next

As soon as the checklist is complete it is possible to schedule a next task. If you click the icon in the bottom-right corner of the task box the application will schedule the next maintenance task based on the predefined period.

After clicking the “next” icon the application will open a confirmation modal dialog with the calculated date of the next occurrence:

asp.net core maintenance scheduling open source next task

The scheduleNext() function checks if the next tasks 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   
    }
    
}

Task completion and next date

asp.net core maintenance scheduling open source complete task

The task is complete when all checklist items are checked off and when the next task is scheduled.

A complete task will be displayed in gray color.

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

You can download the full source code of this application using the link at the beginning of the tutorial.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK