Repository: toyiyo/todo
Pull request #121: Add project progress tracking and display functionality
Author: login: jdelgado2002
Created: 2025-03-20T02:54:59Z | Merged: 2025-03-22T23:02:11Z

Introduce functionality to track and display project progress, including calculations for task completion and status updates. Refactor existing code to improve progress calculations and enhance the visual representation of project statuses.

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Introduced project progress tracking with a new property to display detailed progress statistics.
  - Added a new Razor view for a progress bar component that visually represents task completion.
  - Enhanced the project listing view with a modern card layout, including interactive menus for project actions.
  - Implemented a health status system for projects, providing insights based on task completion and bug counts.
  
- **Style**
  - Updated visual styling for project cards, titles, and statistics to deliver a more polished and cohesive user experience.
  
- **Refactor**
  - Streamlined data retrieval and presentation processes to better integrate progress information in project views.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/aspnet-core/src/toyiyo.todo.Application/Projects/Dto/ProjectDto.cs b/aspnet-core/src/toyiyo.todo.Application/Projects/Dto/ProjectDto.cs
index c9e73e00..c4028e1f 100644
--- a/aspnet-core/src/toyiyo.todo.Application/Projects/Dto/ProjectDto.cs
+++ b/aspnet-core/src/toyiyo.todo.Application/Projects/Dto/ProjectDto.cs
@@ -22,5 +22,6 @@ public class ProjectDto : EntityDto<Guid>
         public string DeleterUserName { get; set; }
         public string DeleterUserId { get; set; }
         public DateTime? DeletionTime { get; set; }
+        public ProjectProgressDto Progress { get; set; }
     }
 }
diff --git a/aspnet-core/src/toyiyo.todo.Application/Projects/Dto/ProjectProgressDto.cs b/aspnet-core/src/toyiyo.todo.Application/Projects/Dto/ProjectProgressDto.cs
new file mode 100644
index 00000000..0a9a0d9b
--- /dev/null
+++ b/aspnet-core/src/toyiyo.todo.Application/Projects/Dto/ProjectProgressDto.cs
@@ -0,0 +1,48 @@
+using System;
+
+namespace toyiyo.todo.Projects.Dto
+{
+    /// <summary>
+    /// Data transfer object for project progress statistics
+    /// </summary>
+    public class ProjectProgressDto
+    {
+        // Base statistics mapped from domain
+        public int TotalJobCount { get; set; }
+        public int CompletedTasks { get; set; }
+        public int InProgressTasks { get; set; }
+        public int BacklogTasks { get; set; }
+        public int EpicCount { get; set; }
+        public int TaskCount { get; set; }
+        public int BugCount { get; set; }
+        public DateTime? DueDate { get; set; }
+        public int CompletedEpics { get; set; }
+        public decimal InProgressPercentage { get; set; }
+        public decimal TotalTasksPercentage { get; set; }
+
+        public string Status { get; set; }
+        public string StatusClass { get; set; }
+
+        public static ProjectProgressDto FromDomain(ProjectProgress progress)
+        {
+            if (progress == null) return new ProjectProgressDto();
+            
+            return new ProjectProgressDto
+            {
+                TotalJobCount = progress.TotalJobCount,
+                CompletedTasks = progress.CompletedTasks,
+                InProgressTasks = progress.InProgressTasks,
+                BacklogTasks = progress.BacklogTasks,
+                EpicCount = progress.EpicCount,
+                TaskCount = progress.TaskCount,
+                BugCount = progress.BugCount,
+                DueDate = progress.DueDate,
+                CompletedEpics = progress.CompletedEpics,
+                InProgressPercentage = progress.InProgressPercentage,
+                TotalTasksPercentage = progress.TotalTasksPercentage,
+                Status = progress.HealthStatus?.Status,
+                StatusClass = progress.HealthStatus?.CssClass
+            };
+        }
+    }
+}
diff --git a/aspnet-core/src/toyiyo.todo.Application/Projects/ProjectAppService.cs b/aspnet-core/src/toyiyo.todo.Application/Projects/ProjectAppService.cs
index 3a40ee21..0a870684 100644
--- a/aspnet-core/src/toyiyo.todo.Application/Projects/ProjectAppService.cs
+++ b/aspnet-core/src/toyiyo.todo.Application/Projects/ProjectAppService.cs
@@ -1,5 +1,6 @@
 using System;
 using System.Collections.Generic;
+using System.Linq;
 using System.Threading.Tasks;
 using Abp.Application.Services.Dto;
 using Abp.Authorization;
@@ -23,13 +24,34 @@ public async Task<ProjectDto> Create(CreateProjectInputDto input) {
         /// <summary> Gets a Project by Id. </summary>
         public async Task<ProjectDto> Get(Guid id) {
             var project = await _projectManager.Get(id);
-            return ObjectMapper.Map<ProjectDto>(project);
+            var progressMap = await _projectManager.GetProjectsProgress(
+                new Guid[] {project.Id}
+            );
+            var dto = ObjectMapper.Map<ProjectDto>(project);
+            dto.Progress = progressMap.TryGetValue(project.Id, out var progress)
+                ? ProjectProgressDto.FromDomain(progress)
+                : new ProjectProgressDto();
+            return dto;
         }
         /// <summary> Gets all Projects. Keyword filters by Title</summary>
         public async Task<PagedResultDto<ProjectDto>> GetAll(GetAllProjectsInput input) {
             var projects = await _projectManager.GetAll(input);
+            
+            // Get progress for all projects in one batch
+            var progressMap = await _projectManager.GetProjectsProgress(
+                projects.Select(p => p.Id)
+            );
+            
+            var dtos = projects.Select(p => {
+                var dto = ObjectMapper.Map<ProjectDto>(p);
+                dto.Progress = progressMap.TryGetValue(p.Id, out var progress)
+                    ? ProjectProgressDto.FromDomain(progress)
+                    : new ProjectProgressDto(); 
+                return dto;
+            }).ToList();
+            
             var projectsTotalCount = await _projectManager.GetAllCount(input);
-            return new PagedResultDto<ProjectDto>(projectsTotalCount, ObjectMapper.Map<List<ProjectDto>>(projects));
+            return new PagedResultDto<ProjectDto>(projectsTotalCount, dtos);
         }
 
         /// <summary> Sets the project's title </summary>
diff --git a/aspnet-core/src/toyiyo.todo.Core/Projects/IProjectManager.cs b/aspnet-core/src/toyiyo.todo.Core/Projects/IProjectManager.cs
index 8e61f95d..8095fd65 100644
--- a/aspnet-core/src/toyiyo.todo.Core/Projects/IProjectManager.cs
+++ b/aspnet-core/src/toyiyo.todo.Core/Projects/IProjectManager.cs
@@ -12,5 +12,6 @@ public interface IProjectManager
         Task<int> GetAllCount(GetAllProjectsInput input);
         Task<Project> Update(Project inputProject);
         Task Delete(Project inputProject);
+        Task<Dictionary<Guid, ProjectProgress>> GetProjectsProgress(IEnumerable<Guid> projectIds);
     }
 }
\ No newline at end of file
diff --git a/aspnet-core/src/toyiyo.todo.Core/Projects/Project.cs b/aspnet-core/src/toyiyo.todo.Core/Projects/Project.cs
index 7b26144b..959b3b2e 100644
--- a/aspnet-core/src/toyiyo.todo.Core/Projects/Project.cs
+++ b/aspnet-core/src/toyiyo.todo.Core/Projects/Project.cs
@@ -1,10 +1,12 @@
 using System;
+using System.Collections.Generic;
 using System.ComponentModel.DataAnnotations;
 using Abp.Domain.Entities;
 using Abp.Domain.Entities.Auditing;
 using Abp.Timing;
 using Microsoft.EntityFrameworkCore;
 using toyiyo.todo.Authorization.Users;
+using toyiyo.todo.Jobs;
 
 namespace toyiyo.todo.Projects
 {
@@ -19,6 +21,8 @@ public class Project : FullAuditedEntity<Guid>, IMustHaveTenant
         [Required]
         public virtual int TenantId { get; set; }
 
+        public virtual ICollection<Job> Jobs { get; set; }
+
         /// <summary>
         /// We don't make constructor public and forcing to create events using <see cref="Create"/> method.
         /// But constructor can not be private since it's used by EntityFramework.
diff --git a/aspnet-core/src/toyiyo.todo.Core/Projects/ProjectHealthStatus.cs b/aspnet-core/src/toyiyo.todo.Core/Projects/ProjectHealthStatus.cs
new file mode 100644
index 00000000..4b7c0edb
--- /dev/null
+++ b/aspnet-core/src/toyiyo.todo.Core/Projects/ProjectHealthStatus.cs
@@ -0,0 +1,48 @@
+using System;
+
+namespace toyiyo.todo.Projects
+{
+    public class ProjectHealthStatus
+    {
+        private const decimal RiskThreshold = 35m;
+        private const decimal OnTrackThreshold = 65m;
+        private const decimal BugRatioThreshold = 0.3m;
+
+        public string Status { get; }
+        public string CssClass { get; }
+
+        private ProjectHealthStatus(string status, string cssClass)
+        {
+            Status = status;
+            CssClass = cssClass;
+        }
+
+        public static ProjectHealthStatus Calculate(
+            int totalJobCount,
+            int completedTasks,
+            int bugCount,
+            DateTime? dueDate)
+        {
+            if (totalJobCount == 0)
+                return new ProjectHealthStatus("Not Started", "badge-secondary");
+
+            var completionRatio = (decimal)completedTasks / totalJobCount;
+            var bugRatio = totalJobCount > 0 ? (decimal)bugCount / totalJobCount : 0;
+            var isPastDue = dueDate.HasValue && DateTime.UtcNow > dueDate.Value;
+
+            if (completionRatio == 1m)
+                return new ProjectHealthStatus("Completed", "badge-success");
+
+            if (isPastDue && completionRatio < 1m)
+                return new ProjectHealthStatus("Overdue", "badge-danger");
+
+            if (completionRatio >= (OnTrackThreshold / 100) && bugRatio <= BugRatioThreshold)
+                return new ProjectHealthStatus("On Track", "badge-info");
+
+            if (completionRatio >= (RiskThreshold / 100))
+                return new ProjectHealthStatus("At Risk", "badge-warning");
+
+            return new ProjectHealthStatus("Behind", "badge-danger");
+        }
+    }
+}
diff --git a/aspnet-core/src/toyiyo.todo.Core/Projects/ProjectManager.cs b/aspnet-core/src/toyiyo.todo.Core/Projects/ProjectManager.cs
index 857226af..02c5433f 100644
--- a/aspnet-core/src/toyiyo.todo.Core/Projects/ProjectManager.cs
+++ b/aspnet-core/src/toyiyo.todo.Core/Projects/ProjectManager.cs
@@ -10,6 +10,7 @@
 using Abp.Extensions;
 using Abp.Linq.Extensions;
 using Microsoft.EntityFrameworkCore;
+using Abp.Domain.Entities;
 
 namespace toyiyo.todo.Projects
 {
@@ -49,7 +50,8 @@ private IQueryable<Project> GetAllProjectsQueryable(GetAllProjectsInput input)
         {
             //repository methods already filter by tenant, we can check other attributes by adding "or" "||" to the whereif clause
             return _projectRepository.GetAll()
-            .WhereIf(!input.keyword.IsNullOrWhiteSpace(), p => p.Title.ToUpper().Contains(input.keyword.ToUpper()));
+                .Include(p => p.Jobs)  // Include Jobs navigation property
+                .WhereIf(!input.keyword.IsNullOrWhiteSpace(), p => p.Title.ToUpper().Contains(input.keyword.ToUpper()));
         }
 
         public async Task<Project> Create(Project inputProject)
@@ -70,5 +72,30 @@ public async Task Delete(Project inputProject)
             //https://aspnetboilerplate.com/Pages/Documents/Entities?searchKey=soft%20delete
             await _projectRepository.DeleteAsync(inputProject.Id);
         }
+
+        public async Task<ProjectProgress> GetProjectProgress(Guid projectId)
+        {
+            var project = await _projectRepository.GetAll()
+                .Include(p => p.Jobs)  // Ensure Jobs are included
+                .FirstOrDefaultAsync(p => p.Id == projectId);
+
+            if (project == null)
+                throw new EntityNotFoundException(typeof(Project), projectId);
+
+            return ProjectProgress.Calculate(project);
+        }
+
+        public async Task<Dictionary<Guid, ProjectProgress>> GetProjectsProgress(IEnumerable<Guid> projectIds)
+        {
+            var projects = await _projectRepository.GetAll()
+                .Include(p => p.Jobs)
+                .Where(p => projectIds.Contains(p.Id))
+                .ToListAsync();
+
+            return projects.ToDictionary(
+                p => p.Id,
+                p => ProjectProgress.Calculate(p)
+            );
+        }
     }
 }
\ No newline at end of file
diff --git a/aspnet-core/src/toyiyo.todo.Core/Projects/ProjectProgress.cs b/aspnet-core/src/toyiyo.todo.Core/Projects/ProjectProgress.cs
new file mode 100644
index 00000000..eedef1ac
--- /dev/null
+++ b/aspnet-core/src/toyiyo.todo.Core/Projects/ProjectProgress.cs
@@ -0,0 +1,72 @@
+using System.Linq;
+using System.Collections.Generic;
+using toyiyo.todo.Jobs;
+using System;
+
+namespace toyiyo.todo.Projects
+{
+    public class ProjectProgress
+    {
+        public int TotalJobCount { get; private set; }
+        public int CompletedTasks { get; private set; }
+        public int InProgressTasks { get; private set; }
+        public int BacklogTasks { get; private set; }
+        public int EpicCount { get; private set; }
+        public int TaskCount { get; private set; }
+        public int BugCount { get; private set; }
+        public DateTime? DueDate { get; private set; }
+        public int CompletedEpics { get; private set; }
+        public decimal TotalTasksPercentage { get; private set; }
+        public decimal InProgressPercentage { get; private set; }
+        public ProjectHealthStatus HealthStatus { get; private set; }
+
+        private ProjectProgress() { }
+
+        public static ProjectProgress Calculate(Project project)
+        {
+            if (project?.Jobs == null || !project.Jobs.Any())
+                return new ProjectProgress();
+
+            var nonDeletedJobs = project.Jobs.Where(j => !j.IsDeleted);
+            var nonEpicJobs = nonDeletedJobs.Where(j => j.Level != Job.JobLevel.Epic);
+            var totalNonEpicJobs = nonEpicJobs.Count();
+
+            var progress = new ProjectProgress
+            {
+                // Only count non-epic jobs for task totals
+                TotalJobCount = totalNonEpicJobs,
+                CompletedTasks = nonEpicJobs.Count(j => j.JobStatus == Job.Status.Done),
+                InProgressTasks = nonEpicJobs.Count(j => j.JobStatus == Job.Status.InProgress),
+                BacklogTasks = nonEpicJobs.Count(j => j.JobStatus == Job.Status.Open),
+                
+                // Epic counts are separate
+                EpicCount = nonDeletedJobs.Count(j => j.Level == Job.JobLevel.Epic),
+                CompletedEpics = nonDeletedJobs.Count(j => j.Level == Job.JobLevel.Epic && j.JobStatus == Job.Status.Done),
+                
+                // Specific job type counts
+                TaskCount = nonDeletedJobs.Count(j => j.Level == Job.JobLevel.Task),
+                BugCount = nonDeletedJobs.Count(j => j.Level == Job.JobLevel.Bug),
+                
+                // Calculate percentages based on non-epic jobs only
+                TotalTasksPercentage = totalNonEpicJobs > 0
+                    ? Math.Round((decimal)nonEpicJobs.Count(j => j.JobStatus == Job.Status.Done) / totalNonEpicJobs * 100, 2) 
+                    : 0,
+                    
+                InProgressPercentage = totalNonEpicJobs > 0
+                    ? Math.Round((decimal)nonEpicJobs.Count(j => j.JobStatus == Job.Status.InProgress) / totalNonEpicJobs * 100, 2)
+                    : 0,
+
+                DueDate = nonDeletedJobs.Max(j => (DateTime?)j.DueDate) ?? null,
+
+                HealthStatus = ProjectHealthStatus.Calculate(
+                    totalJobCount: totalNonEpicJobs,
+                    completedTasks: nonEpicJobs.Count(j => j.JobStatus == Job.Status.Done),
+                    bugCount: nonDeletedJobs.Count(j => j.Level == Job.JobLevel.Bug),
+                    dueDate: nonDeletedJobs.Max(j => (DateTime?)j.DueDate)
+                )
+            };
+
+            return progress;
+        }
+    }
+}
diff --git a/aspnet-core/src/toyiyo.todo.Web.Mvc/Views/Projects/Index.cshtml b/aspnet-core/src/toyiyo.todo.Web.Mvc/Views/Projects/Index.cshtml
index bbb21a89..f2746182 100644
--- a/aspnet-core/src/toyiyo.todo.Web.Mvc/Views/Projects/Index.cshtml
+++ b/aspnet-core/src/toyiyo.todo.Web.Mvc/Views/Projects/Index.cshtml
@@ -49,8 +49,7 @@
                                 <caption>@L("ProjectList")</caption>
                                 <thead>
                                     <tr>
-                                        <th>@L("ProjectTitle")</th>
-                                        <th style="width: 150px">@L("Actions")</th>
+                                        <th>@L("Projects")</th>
                                     </tr>
                                 </thead>
                                 <tbody>
diff --git a/aspnet-core/src/toyiyo.todo.Web.Mvc/Views/Projects/_ProgressBar.cshtml b/aspnet-core/src/toyiyo.todo.Web.Mvc/Views/Projects/_ProgressBar.cshtml
new file mode 100644
index 00000000..883253c0
--- /dev/null
+++ b/aspnet-core/src/toyiyo.todo.Web.Mvc/Views/Projects/_ProgressBar.cshtml
@@ -0,0 +1,27 @@
+@model toyiyo.todo.Projects.Dto.ProjectProgressDto
+
+<div class="progress" style="height: 20px;">
+    <div class="progress-bar bg-success" 
+         role="progressbar" 
+         style="width: @Model.TotalTasksPercentage%"
+         aria-valuenow="@Model.TotalTasksPercentage" 
+         aria-valuemin="0" 
+         aria-valuemax="100"
+         data-toggle="tooltip" 
+         title="@Model.CompletedTasks completed">
+        @Model.CompletedTasks
+    </div>
+    <div class="progress-bar bg-info" 
+         role="progressbar" 
+         style="width: @Model.InProgressPercentage%"
+         aria-valuenow="@Model.InProgressPercentage" 
+         aria-valuemin="0" 
+         aria-valuemax="100"
+         data-toggle="tooltip" 
+         title="@Model.InProgressTasks in progress">
+        @Model.InProgressTasks
+    </div>
+</div>
+<small class="text-muted">
+    @Model.CompletedTasks of @Model.TotalJobCount tasks completed
+</small>
diff --git a/aspnet-core/src/toyiyo.todo.Web.Mvc/wwwroot/css/style.css b/aspnet-core/src/toyiyo.todo.Web.Mvc/wwwroot/css/style.css
index 7c5b5e9d..a36714cc 100644
--- a/aspnet-core/src/toyiyo.todo.Web.Mvc/wwwroot/css/style.css
+++ b/aspnet-core/src/toyiyo.todo.Web.Mvc/wwwroot/css/style.css
@@ -92,4 +92,58 @@ table.dataTable td.reorder {
 /* Sidebar User Panel */
 .user-panel .image .user-initials {
   margin-right: 10px; /* Adjust as needed */
+}
+
+/* Project card styles */
+.dataTables_wrapper .project-title {
+  font-size: 18px !important;
+  font-weight: 500 !important;
+  color: #2c3e50;
+  text-decoration: none;
+}
+
+.dataTables_wrapper .project-title:hover {
+  color: #3498db;
+  text-decoration: none;
+}
+
+.dataTables_wrapper .card-title {
+  font-size: 18px !important;
+}
+
+.project-title {
+    font-size: 1.4rem;
+    font-weight: 600;
+    color: #2c3e50;
+    text-decoration: none;
+}
+
+.project-title:hover {
+    color: #3498db;
+    text-decoration: none;
+}
+
+.project-stats {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 1.5rem;
+    margin-top: 0.5rem;
+}
+
+.stat-item {
+    display: inline-flex;
+    align-items: center;
+    color: #6c757d;
+    font-size: 0.95rem;
+}
+
+.stat-item i {
+    font-size: 1.1rem;
+    color: #495057;
+}
+
+/* Make sure badges look good with the new sizing */
+.badge.badge-pill {
+    font-size: 0.9rem;
+    padding: 0.4em 0.8em;
 }
\ No newline at end of file
diff --git a/aspnet-core/src/toyiyo.todo.Web.Mvc/wwwroot/view-resources/Views/Projects/Index.js b/aspnet-core/src/toyiyo.todo.Web.Mvc/wwwroot/view-resources/Views/Projects/Index.js
index 543570cc..79fb61a0 100644
--- a/aspnet-core/src/toyiyo.todo.Web.Mvc/wwwroot/view-resources/Views/Projects/Index.js
+++ b/aspnet-core/src/toyiyo.todo.Web.Mvc/wwwroot/view-resources/Views/Projects/Index.js
@@ -33,34 +33,103 @@
         columnDefs: [
             {
                 targets: 0,
-                data: 'title',
-                className: 'title',
-                defaultContent: '',
-                fnCreatedCell: function (nTd, sData, oData, iRow, iCol) {
-                    $(nTd).html('<a href="' + 'Projects/' + oData.id + '/jobs' + '">' + sData + '</a>');
-                }
-            },
-            {
-                targets: 1,
                 data: null,
-                sortable: false,
-                autoWidth: false,
-                width: '5em',
-                defaultContent: '',
-                render: (data, type, row, meta) => {
-                    return [
-                        `   <button type="button" class="btn btn-sm bg-secondary edit-project" data-project-id="${row.id}" data-toggle="modal" data-target="#ProjectEditModal">`,
-                        `       <i class="fas fa-pencil-alt" title=${l('Edit')}></i>`,
-                        '   </button>',
-                        `   <button type="button" class="btn btn-sm bg-danger delete-project" data-project-id="${row.id}" data-toggle="modal" data-target="#ProjectDeleteModal">`,
-                        `       <i class="fas fa-trash-alt" title=${l('Delete')}></i>`,
-                        '   </button>',
-                    ].join('');
+                className: 'project-card',
+                render: function(data, type, row) {
+                    const progress = row.progress;
+                    const total = progress.totalJobCount;
+                    const completed = progress.completedTasks;
+                    const actions = DOMPurify.sanitize(`
+                        <div class="dropdown">
+                            <button class="btn btn-sm btn-light dropdown-toggle ml-2" type="button" data-toggle="dropdown" aria-expanded="false">
+                                <i class="fas fa-ellipsis-v"></i>
+                            </button>
+                            <div class="dropdown-menu dropdown-menu-right">
+                                <button type="button" class="dropdown-item edit-project" data-project-id="${row.id}" data-toggle="modal" data-target="#ProjectEditModal">
+                                    <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-pencil mr-2" viewBox="0 0 16 16">
+                                        <path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/>
+                                    </svg>
+                                    ${l('Edit')}
+                                </button>
+                                <button type="button" class="dropdown-item delete-project" data-project-id="${row.id}" data-toggle="modal" data-target="#ProjectDeleteModal">
+                                    <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-trash mr-2" viewBox="0 0 16 16">
+                                        <path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/>
+                                        <path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"/>
+                                    </svg>
+                                    ${l('Delete')}
+                                </button>
+                            </div>
+                        </div>
+                    `);
+
+                    // Generate card content regardless of task count
+                    return `
+                        <div class="card">
+                            <div class="card-body">
+                                <div class="d-flex justify-content-between align-items-center">
+                                    <h5 class="card-title mb-0">
+                                        <a class="project-title" href="/Projects/${DOMPurify.sanitize(row.id)}/jobs">${DOMPurify.sanitize(row.title)}</a>
+                                    </h5>
+                                    <div>
+                                        <span class="badge badge-pill ${progress.statusClass}">${progress.status}</span>
+                                        <span class="ml-2">${actions}</span>
+                                    </div>
+                                </div>
+                                <div class="text-muted mt-3">
+                                    <div class="project-stats">
+                                        <span class="stat-item">
+                                            <i class="fas fa-layer-group mr-2"></i>
+                                            ${progress.epicCount} Epics (${progress.completedEpics} done)
+                                        </span>
+                                        <span class="stat-item">
+                                            <i class="fas fa-tasks mr-2"></i>
+                                            ${progress.taskCount} Tasks 
+                                        </span>
+                                        <span class="stat-item">
+                                            <i class="fas fa-bug mr-2"></i>
+                                            ${progress.bugCount} Bugs
+                                        </span>
+                                        ${progress.dueDate && !moment(progress.dueDate).isSame('0001-01-01T00:00:00Z') ? `
+                                            <span class="stat-item">
+                                                <i class="fas fa-calendar mr-2"></i>
+                                                Due: ${moment(progress.dueDate).format('M/D/YYYY')}
+                                            </span>
+                                        ` : `
+                                            <span class="stat-item text-muted">
+                                                <i class="fas fa-calendar mr-2"></i>
+                                                No due date set
+                                            </span>
+                                        `}
+                                    </div>
+                                </div>
+                            </div>
+                            <div class="card-footer p-2">
+                                <div class="progress position-relative" style="height: 20px;">
+                                    <div class="progress-bar bg-success" 
+                                         style="width: ${progress.totalTasksPercentage}%"
+                                         role="progressbar" 
+                                         aria-valuenow="${progress.totalTasksPercentage}" 
+                                         aria-valuemin="0" 
+                                         aria-valuemax="100"
+                                         data-toggle="tooltip"
+                                         title="${completed} completed (${progress.totalTasksPercentage}%)">
+                                    </div>
+                                    <div class="position-absolute w-100 text-center" style="line-height: 20px;">
+                                        ${total === 0 ? 'No tasks' : `${completed} of ${total} tasks completed (${progress.totalTasksPercentage}%)`}
+                                    </div>
+                                </div>
+                            </div>
+                        </div>
+                    `;
                 }
             }
         ]
     });
 
+    _$projectsTable.on('draw.dt', function() {
+        $('[data-toggle="tooltip"]').tooltip();
+    });
+
     _$form.find('.save-button').on('click', (e) => {
         e.preventDefault();
 
@@ -99,7 +168,7 @@
         })
     });
 
-    abp.event.on('project.edited', (data) => {
+    abp.event.on('project.edited', () => {
         _$projectsTable.ajax.reload();
     });
 
@@ -109,7 +178,7 @@
         _$form.clearForm();
     });
 
-    $('.btn-search').on('click', (e) => {
+    $('.btn-search').on('click', () => {
         _$projectsTable.ajax.reload();
     });
 
diff --git a/aspnet-core/test/toyiyo.todo.Tests/Projects/ProjectHealthStatusTests.cs b/aspnet-core/test/toyiyo.todo.Tests/Projects/ProjectHealthStatusTests.cs
new file mode 100644
index 00000000..23616880
--- /dev/null
+++ b/aspnet-core/test/toyiyo.todo.Tests/Projects/ProjectHealthStatusTests.cs
@@ -0,0 +1,170 @@
+using System;
+using Shouldly;
+using Xunit;
+using toyiyo.todo.Projects;
+
+
+namespace toyiyo.todo.Tests.Projects
+{
+    public class ProjectHealthStatusTests : todoTestBase
+    {
+        [Fact]
+        public void Calculate_WhenNoTasks_ShouldReturnNotStarted()
+        {
+            // Arrange
+            int TotalJobCount = 0;
+            int completedTasks = 0;
+            int bugCount = 0;
+            DateTime? dueDate = null;
+
+            // Act
+            var result = ProjectHealthStatus.Calculate(TotalJobCount, completedTasks, bugCount, dueDate);
+
+            // Assert
+            result.Status.ShouldBe("Not Started");
+            result.CssClass.ShouldBe("badge-secondary");
+        }
+
+        [Fact]
+        public void Calculate_WhenAllTasksComplete_ShouldReturnCompleted()
+        {
+            // Arrange
+            int TotalJobCount = 100;
+            int completedTasks = 100; // 100% completion ratio
+            int bugCount = 10;
+            DateTime? dueDate = DateTime.UtcNow.AddDays(10);
+
+            // Act
+            var result = ProjectHealthStatus.Calculate(TotalJobCount, completedTasks, bugCount, dueDate);
+
+            // Assert
+            result.Status.ShouldBe("Completed");
+            result.CssClass.ShouldBe("badge-success");
+        }
+
+        [Fact]
+        public void Calculate_WhenPastDueAndNotComplete_ShouldReturnOverdue()
+        {
+            // Arrange
+            int TotalJobCount = 100;
+            int completedTasks = 90; // 90% completion ratio
+            int bugCount = 5;
+            DateTime? dueDate = DateTime.UtcNow.AddDays(-1); // Past due
+
+            // Act
+            var result = ProjectHealthStatus.Calculate(TotalJobCount, completedTasks, bugCount, dueDate);
+
+            // Assert
+            result.Status.ShouldBe("Overdue");
+            result.CssClass.ShouldBe("badge-danger");
+        }
+
+        [Theory]
+        [InlineData(65, 10, 0.1)] // Just at threshold
+        [InlineData(80, 20, 0.25)] // Well above threshold
+        [InlineData(99, 30, 0.3)] // At bug ratio limit
+        public void Calculate_WhenHighCompletionAndLowBugRatio_ShouldReturnOnTrack(
+            int completionPercentage, int bugCount, decimal bugRatio)
+        {
+            // Arrange
+            int TotalJobCount = 100;
+            int completedTasks = completionPercentage; // completionRatio = completionPercentage/100
+            DateTime? dueDate = DateTime.UtcNow.AddDays(10);
+
+            // Act
+            var result = ProjectHealthStatus.Calculate(TotalJobCount, completedTasks, bugCount, dueDate);
+
+            // Assert
+            result.Status.ShouldBe("On Track");
+            result.CssClass.ShouldBe("badge-info");
+        }
+
+        [Theory]
+        [InlineData(35, 0)] // Just at risk threshold
+        [InlineData(50, 31)] // Above risk threshold but high bug count
+        public void Calculate_WhenMediumCompletionOrHighBugRatio_ShouldReturnAtRisk(
+            int completionPercentage, int bugCount)
+        {
+            // Arrange
+            int TotalJobCount = 100;
+            int completedTasks = completionPercentage; // completionRatio = completionPercentage/100
+            DateTime? dueDate = DateTime.UtcNow.AddDays(10);
+
+            // Act
+            var result = ProjectHealthStatus.Calculate(TotalJobCount, completedTasks, bugCount, dueDate);
+
+            // Assert
+            result.Status.ShouldBe("At Risk");
+            result.CssClass.ShouldBe("badge-warning");
+        }
+
+        [Theory]
+        [InlineData(0)]
+        [InlineData(10)]
+        [InlineData(34)] // Just below risk threshold
+        public void Calculate_WhenLowCompletion_ShouldReturnBehind(int completionPercentage)
+        {
+            // Arrange
+            int TotalJobCount = 100;
+            int completedTasks = completionPercentage; // completionRatio = completionPercentage/100
+            int bugCount = 10;
+            DateTime? dueDate = DateTime.UtcNow.AddDays(10);
+
+            // Act
+            var result = ProjectHealthStatus.Calculate(TotalJobCount, completedTasks, bugCount, dueDate);
+
+            // Assert
+            result.Status.ShouldBe("Behind");
+            result.CssClass.ShouldBe("badge-danger");
+        }
+
+        [Fact]
+        public void Calculate_WithEdgeCaseCompletionRatio_ShouldHandleCorrectly()
+        {
+            // Arrange - exactly at threshold boundaries
+            int TotalJobCount = 100;
+            int justBelowRiskThreshold = 34;
+            int exactlyAtRiskThreshold = 35;
+            int justBelowOnTrackThreshold = 64;
+            int exactlyAtOnTrackThreshold = 65;
+            int bugCount = 10; // Keep bug ratio below threshold
+            DateTime? dueDate = DateTime.UtcNow.AddDays(10);
+
+            // Act & Assert - Testing the threshold boundaries
+            ProjectHealthStatus.Calculate(TotalJobCount, justBelowRiskThreshold, bugCount, dueDate)
+                .Status.ShouldBe("Behind");
+
+            ProjectHealthStatus.Calculate(TotalJobCount, exactlyAtRiskThreshold, bugCount, dueDate)
+                .Status.ShouldBe("At Risk");
+
+            ProjectHealthStatus.Calculate(TotalJobCount, justBelowOnTrackThreshold, bugCount, dueDate)
+                .Status.ShouldBe("At Risk");
+
+            ProjectHealthStatus.Calculate(TotalJobCount, exactlyAtOnTrackThreshold, bugCount, dueDate)
+                .Status.ShouldBe("On Track");
+        }
+
+        [Fact]
+        public void Calculate_WithDifferentCompletionRatios_ShouldReflectCorrectProgress()
+        {
+            // Arrange
+            int TotalJobCount = 200;
+            int[] completedTasksArray = { 0, 70, 130, 200 };
+            int bugCount = 10;
+            DateTime? dueDate = DateTime.UtcNow.AddDays(10);
+
+            string[] expectedStatuses = { "Behind", "At Risk", "On Track", "Completed" };
+
+            // Act & Assert
+            for (int i = 0; i < completedTasksArray.Length; i++)
+            {
+                int completedTasks = completedTasksArray[i];
+                string expectedStatus = expectedStatuses[i];
+
+                var result = ProjectHealthStatus.Calculate(TotalJobCount, completedTasks, bugCount, dueDate);
+                result.Status.ShouldBe(expectedStatus,
+                    $"Failed with completionRatio = {(decimal)completedTasks / TotalJobCount:P0}");
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/aspnet-core/test/toyiyo.todo.Tests/Projects/ProjectProgressTests.cs b/aspnet-core/test/toyiyo.todo.Tests/Projects/ProjectProgressTests.cs
new file mode 100644
index 00000000..a6ca1e3a
--- /dev/null
+++ b/aspnet-core/test/toyiyo.todo.Tests/Projects/ProjectProgressTests.cs
@@ -0,0 +1,261 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Abp.Domain.Uow;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+using Shouldly;
+using toyiyo.todo.Authorization.Users;
+using toyiyo.todo.Jobs;
+using toyiyo.todo.Jobs.Dto;
+using toyiyo.todo.Projects;
+using toyiyo.todo.Projects.Dto;
+using toyiyo.todo.Sessions;
+using Xunit;
+using static toyiyo.todo.Jobs.Job;
+
+namespace toyiyo.todo.Tests.Projects
+{
+    public class ProjectProgressTests : todoTestBase
+    {
+        private readonly IProjectAppService _projectAppService;
+        private readonly IJobAppService _jobAppService;
+
+        public ProjectProgressTests()
+        {
+            _projectAppService = Resolve<IProjectAppService>();
+            _jobAppService = Resolve<IJobAppService>();
+            LoginAsDefaultTenantAdmin();
+        }
+
+        private async Task<Project> GetProjectEntityById(Guid id)
+        {
+            return await UsingDbContext(async context => 
+                await context.Projects
+                    .Include(p => p.Jobs)
+                    .FirstOrDefaultAsync(p => p.Id == id)
+            );
+        }
+
+        private async Task<Project> CreateProjectWithMixedJobs()
+        {
+            var project = await _projectAppService.Create(new CreateProjectInputDto { Title = "Test Project" });
+
+            // Create Epics
+            var epic1 = await _jobAppService.Create(new JobCreateInputDto 
+            { 
+                ProjectId = project.Id, 
+                Title = "Epic 1",
+                Level = JobLevel.Epic
+            });
+            await _jobAppService.SetJobStatus(new JobSetStatusInputDto { Id = epic1.Id, JobStatus = Status.Done });
+
+            var epic2 = await _jobAppService.Create(new JobCreateInputDto 
+            { 
+                ProjectId = project.Id, 
+                Title = "Epic 2",
+                Level = JobLevel.Epic
+            });
+            await _jobAppService.SetJobStatus(new JobSetStatusInputDto { Id = epic2.Id, JobStatus = Status.InProgress });
+
+            // Create Tasks with different statuses
+            for (int i = 0; i < 3; i++)
+            {
+                var task = await _jobAppService.Create(new JobCreateInputDto 
+                { 
+                    ProjectId = project.Id, 
+                    Title = $"Completed Task {i}",
+                    Level = JobLevel.Task
+                });
+                await _jobAppService.SetJobStatus(new JobSetStatusInputDto { Id = task.Id, JobStatus = Status.Done });
+            }
+
+            for (int i = 0; i < 2; i++)
+            {
+                var task = await _jobAppService.Create(new JobCreateInputDto 
+                { 
+                    ProjectId = project.Id, 
+                    Title = $"In Progress Task {i}",
+                    Level = JobLevel.Task
+                });
+                await _jobAppService.SetJobStatus(new JobSetStatusInputDto { Id = task.Id, JobStatus = Status.InProgress });
+            }
+
+            // Create Bugs
+            for (int i = 0; i < 3; i++)
+            {
+                await _jobAppService.Create(new JobCreateInputDto 
+                { 
+                    ProjectId = project.Id, 
+                    Title = $"Bug {i}",
+                    Level = JobLevel.Bug
+                });
+            }
+
+            return await GetProjectEntityById(project.Id);
+        }
+
+        private async Task<Project> CreateProjectWithDeletedJobs()
+        {
+            var project = await _projectAppService.Create(new CreateProjectInputDto { Title = "Test Project" });
+
+            // Create jobs with different statuses
+            var job1 = await _jobAppService.Create(new JobCreateInputDto 
+            { 
+                ProjectId = project.Id, 
+                Title = "Task 1"
+            });
+            await _jobAppService.SetJobStatus(new JobSetStatusInputDto { Id = job1.Id, JobStatus = Status.Done });
+
+            var job2 = await _jobAppService.Create(new JobCreateInputDto 
+            { 
+                ProjectId = project.Id, 
+                Title = "Task 2"
+            });
+            await _jobAppService.SetJobStatus(new JobSetStatusInputDto { Id = job2.Id, JobStatus = Status.InProgress });
+
+            var job3 = await _jobAppService.Create(new JobCreateInputDto 
+            { 
+                ProjectId = project.Id, 
+                Title = "Task 3"
+            });
+
+            var jobToDelete = await _jobAppService.Create(new JobCreateInputDto 
+            { 
+                ProjectId = project.Id, 
+                Title = "To Delete"
+            });
+
+            await _jobAppService.Delete(jobToDelete.Id);
+            return await GetProjectEntityById(project.Id);
+        }
+
+        private async Task<Project> CreateProjectWithDueDates()
+        {
+            var project = await _projectAppService.Create(new CreateProjectInputDto { Title = "Test Project" });
+
+            // Create jobs with different due dates
+            await _jobAppService.Create(new JobCreateInputDto 
+            { 
+                ProjectId = project.Id, 
+                Title = "Task 1",
+                DueDate = DateTime.UtcNow.AddDays(10)
+            });
+
+            await _jobAppService.Create(new JobCreateInputDto 
+            { 
+                ProjectId = project.Id, 
+                Title = "Task 2",
+                DueDate = DateTime.UtcNow.AddDays(20)
+            });
+
+            await _jobAppService.Create(new JobCreateInputDto 
+            { 
+                ProjectId = project.Id, 
+                Title = "Task 3",
+                DueDate = DateTime.UtcNow.AddDays(30)
+            });
+
+            return await GetProjectEntityById(project.Id);
+        }
+
+        private async Task<Project> CreateProjectWithAllTasksCompleted()
+        {
+            var project = await _projectAppService.Create(new CreateProjectInputDto { Title = "Test Project" });
+
+            // Create multiple tasks and mark them as completed
+            for (int i = 0; i < 5; i++)
+            {
+                var task = await _jobAppService.Create(new JobCreateInputDto 
+                { 
+                    ProjectId = project.Id, 
+                    Title = $"Completed Task {i}"
+                });
+                await _jobAppService.SetJobStatus(new JobSetStatusInputDto { Id = task.Id, JobStatus = Job.Status.Done });
+            }
+
+            return await GetProjectEntityById(project.Id);
+        }
+
+        [Fact]
+        public async Task Should_Calculate_Project_Progress_With_Mixed_Jobs()
+        {
+            var project = await CreateProjectWithMixedJobs();
+            var progress = ProjectProgress.Calculate(project);
+
+            progress.TotalJobCount.ShouldBe(8); // 5 tasks + 3 bugs
+            progress.CompletedTasks.ShouldBe(3);
+            progress.InProgressTasks.ShouldBe(2);
+            progress.BacklogTasks.ShouldBe(3);
+            progress.EpicCount.ShouldBe(2);
+            progress.CompletedEpics.ShouldBe(1);
+            progress.TaskCount.ShouldBe(5);
+            progress.BugCount.ShouldBe(3);
+            progress.TotalTasksPercentage.ShouldBe(37.50m); // 3 completed tasks out of 8 total jobs
+            progress.InProgressPercentage.ShouldBe(25.00m); // 2 in-progress tasks out of 8 total jobs
+            progress.DueDate.ShouldNotBeNull();
+            progress.HealthStatus.ShouldNotBeNull();
+        }
+
+        [Fact]
+        public async Task Should_Calculate_Project_Progress_With_Deleted_Jobs()
+        {
+            var project = await CreateProjectWithDeletedJobs();
+            var progress = ProjectProgress.Calculate(project);
+
+            progress.TotalJobCount.ShouldBe(3);
+            progress.CompletedTasks.ShouldBe(1);
+            progress.InProgressTasks.ShouldBe(1);
+            progress.BacklogTasks.ShouldBe(1);
+            progress.EpicCount.ShouldBe(0);
+            progress.CompletedEpics.ShouldBe(0);
+            progress.TaskCount.ShouldBe(3);
+            progress.BugCount.ShouldBe(0);
+            progress.TotalTasksPercentage.ShouldBe(33.33m);
+            progress.InProgressPercentage.ShouldBe(33.33m);
+            progress.DueDate.ShouldNotBeNull();
+            progress.HealthStatus.ShouldNotBeNull();
+        }
+
+        [Fact]
+        public async Task Should_Calculate_Project_Progress_With_Due_Dates()
+        {
+            var project = await CreateProjectWithDueDates();
+            var progress = ProjectProgress.Calculate(project);
+
+            progress.TotalJobCount.ShouldBe(3);
+            progress.CompletedTasks.ShouldBe(0);
+            progress.InProgressTasks.ShouldBe(0);
+            progress.BacklogTasks.ShouldBe(3);
+            progress.EpicCount.ShouldBe(0);
+            progress.CompletedEpics.ShouldBe(0);
+            progress.TaskCount.ShouldBe(3);
+            progress.BugCount.ShouldBe(0);
+            progress.TotalTasksPercentage.ShouldBe(0);
+            progress.InProgressPercentage.ShouldBe(0);
+            progress.DueDate.ShouldNotBeNull();
+            progress.HealthStatus.ShouldNotBeNull();
+        }
+
+        [Fact]
+        public async Task Should_Calculate_Project_Progress_With_All_Tasks_Completed()
+        {
+            var project = await CreateProjectWithAllTasksCompleted();
+            var progress = ProjectProgress.Calculate(project);
+
+            progress.TotalJobCount.ShouldBe(5);
+            progress.CompletedTasks.ShouldBe(5);
+            progress.InProgressTasks.ShouldBe(0);
+            progress.BacklogTasks.ShouldBe(0);
+            progress.EpicCount.ShouldBe(0);
+            progress.CompletedEpics.ShouldBe(0);
+            progress.TaskCount.ShouldBe(5);
+            progress.BugCount.ShouldBe(0);
+            progress.TotalTasksPercentage.ShouldBe(100);
+            progress.InProgressPercentage.ShouldBe(0);
+            progress.DueDate.ShouldNotBeNull();
+            progress.HealthStatus.ShouldNotBeNull();
+        }
+    }
+}


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>🧹 Nitpick comments (9)</summary><blockquote>

<details>
<summary>aspnet-core/src/toyiyo.todo.Core/Projects/Project.cs (1)</summary><blockquote>

`24-25`: **Consider initializing the Jobs collection in the constructor.**

While the virtual collection property is correctly defined for Entity Framework's lazy loading, it's not initialized in the constructor. To prevent potential null reference exceptions when adding jobs to a newly created project, you should initialize this collection.

```diff
protected Project()
{
+    Jobs = new List<Job>();
}
```

</blockquote></details>
<details>
<summary>aspnet-core/src/toyiyo.todo.Web.Mvc/Views/Projects/_ProgressBar.cshtml (1)</summary><blockquote>

`1-27`: **Accessibility and fallback improvements needed for the progress bar**

The progress bar implementation looks well-structured, using Bootstrap's stacked progress bars with appropriate ARIA attributes. However, consider these improvements:

1. Add fallback UI or conditional rendering for when there are no tasks (TotalTasks = 0)
2. Ensure sufficient color contrast for the numbers displayed directly on the progress bars
3. Consider adding screen reader text for better accessibility


```diff
@model toyiyo.todo.Projects.Dto.ProjectProgressDto

+@if (Model.TotalTasks == 0) {
+    <div class="alert alert-info small">No tasks created yet</div>
+} else {
<div class="progress" style="height: 20px;">
    <div class="progress-bar bg-success" 
         role="progressbar" 
         style="width: @Model.CompletionPercentage%"
         aria-valuenow="@Model.CompletionPercentage" 
         aria-valuemin="0" 
         aria-valuemax="100"
         data-toggle="tooltip" 
         title="@Model.CompletedTasks completed">
+        <span class="sr-only">@Model.CompletedTasks completed tasks (@Model.CompletionPercentage%)</span>
        @Model.CompletedTasks
    </div>
    <div class="progress-bar bg-info" 
         role="progressbar" 
         style="width: @Model.InProgressPercentage%"
         aria-valuenow="@Model.InProgressPercentage" 
         aria-valuemin="0" 
         aria-valuemax="100"
         data-toggle="tooltip" 
         title="@Model.InProgressTasks in progress">
+        <span class="sr-only">@Model.InProgressTasks tasks in progress (@Model.InProgressPercentage%)</span>
        @Model.InProgressTasks
    </div>
</div>
<small class="text-muted">
    @Model.CompletedTasks of @Model.TotalTasks tasks completed
</small>
+}
```

</blockquote></details>
<details>
<summary>aspnet-core/src/toyiyo.todo.Application/Projects/Dto/ProjectProgressDto.cs (2)</summary><blockquote>

`21-26`: **Clarify difference between percentage properties**

The class contains both `TotalTasksPercentage` and `CompletionPercentage` properties that appear to calculate the same thing - the percentage of completed tasks. This redundancy could lead to confusion for developers maintaining the code.


If these properties represent different metrics, add comments to clarify the distinction. Otherwise, consider removing one of them:

```diff
public decimal CompletedTasksPercentage { get; set; }
public decimal InProgressPercentage { get; set; } 
public decimal TotalTasksPercentage { get; set; }

- public decimal CompletionPercentage => TotalTasks > 0 ? 
-     (decimal)CompletedTasks / TotalTasks * 100 : 0;
+ // Calculate or use TotalTasksPercentage if they represent the same metric
+ public decimal CompletionPercentage => TotalTasksPercentage;
```

---

`28-33`: **Consider adding documentation for status thresholds**

The status determination logic uses magic values (65, 35) as thresholds without explaining their significance. These thresholds are critical business logic that should be well-documented.


```diff
+ /// <summary>
+ /// Determines the project status based on completion percentage:
+ /// - 100%: "Completed"
+ /// - >65%: "On Track"
+ /// - >35%: "At Risk"
+ /// - ≤35%: "Behind"
+ /// </summary>
public string Status => TotalTasksPercentage switch {
    100 => "Completed",
    > 65 => "On Track",
    > 35 => "At Risk",
    _ => "Behind"
};
```

</blockquote></details>
<details>
<summary>aspnet-core/src/toyiyo.todo.Web.Mvc/wwwroot/css/style.css (1)</summary><blockquote>

`97-127`: **Use CSS variables for consistent theming and maintenance**

The current implementation uses hardcoded color values. Converting these to CSS variables would improve maintainability and make theming (including dark mode support) easier to implement.


```diff
+ :root {
+     --project-title-color: #2c3e50;
+     --project-title-hover-color: #3498db;
+     --project-stats-text-color: #6c757d;
+     --project-stats-icon-color: #495057;
+ }

/* Project card styles */
.project-title {
    font-size: 1.4rem;
    font-weight: 600;
-    color: #2c3e50;
+    color: var(--project-title-color);
    text-decoration: none;
}

.project-title:hover {
-    color: #3498db;
+    color: var(--project-title-hover-color);
    text-decoration: none;
}

.project-stats {
    display: flex;
    flex-wrap: wrap;
    gap: 1.5rem;
    margin-top: 0.5rem;
}

.stat-item {
    display: inline-flex;
    align-items: center;
-    color: #6c757d;
+    color: var(--project-stats-text-color);
    font-size: 0.95rem;
}

.stat-item i {
    font-size: 1.1rem;
-    color: #495057;
+    color: var(--project-stats-icon-color);
}
```

</blockquote></details>
<details>
<summary>aspnet-core/src/toyiyo.todo.Core/Projects/ProjectManager.cs (1)</summary><blockquote>

`88-99`: **Efficiently implemented batch progress retrieval.**

This method efficiently retrieves multiple projects' progress in a single database query and converts the results to a dictionary for easy lookup. The use of `ToDictionary` provides a clean and performant solution.


However, consider adding error handling for cases where duplicate project IDs might be present in the input collection to prevent potential exceptions.

```diff
 public async Task<Dictionary<Guid, ProjectProgress>> GetProjectsProgress(IEnumerable<Guid> projectIds)
 {
+    // Ensure we don't have duplicate IDs to prevent exception in ToDictionary
+    var uniqueProjectIds = projectIds.Distinct();
     var projects = await _projectRepository.GetAll()
         .Include(p => p.Jobs)
-        .Where(p => projectIds.Contains(p.Id))
+        .Where(p => uniqueProjectIds.Contains(p.Id))
         .ToListAsync();

     return projects.ToDictionary(
         p => p.Id,
         p => ProjectProgress.Calculate(p)
     );
 }
```

</blockquote></details>
<details>
<summary>aspnet-core/src/toyiyo.todo.Core/Projects/ProjectProgress.cs (1)</summary><blockquote>

`31-54`: **Well-organized progress calculation with clear comments.**

The calculation logic is clearly organized with comments that explain each section's purpose. The code handles all the relevant metrics needed for tracking project progress.


Consider optimizing the repeated enumeration of collections for large projects.

```diff
 return new ProjectProgress
 {
+    // Pre-compute filtered collections to avoid repeated enumeration
+    var completedNonEpicJobs = nonEpicJobs.Count(j => j.JobStatus == Job.Status.Done);
+    var epicJobs = nonDeletedJobs.Where(j => j.Level == Job.JobLevel.Epic).ToList();
+    
     // Only count non-epic jobs for task totals
     TotalTasks = nonEpicJobs.Count(),
-    CompletedTasks = nonEpicJobs.Count(j => j.JobStatus == Job.Status.Done),
+    CompletedTasks = completedNonEpicJobs,
     InProgressTasks = nonEpicJobs.Count(j => j.JobStatus == Job.Status.InProgress),
     BacklogTasks = nonEpicJobs.Count(j => j.JobStatus == Job.Status.Open),
     
     // Epic counts are separate
-    EpicCount = nonDeletedJobs.Count(j => j.Level == Job.JobLevel.Epic),
-    CompletedEpics = nonDeletedJobs.Count(j => j.Level == Job.JobLevel.Epic && j.JobStatus == Job.Status.Done),
+    EpicCount = epicJobs.Count,
+    CompletedEpics = epicJobs.Count(j => j.JobStatus == Job.Status.Done),
     
     // Specific job type counts
     TaskCount = nonDeletedJobs.Count(j => j.Level == Job.JobLevel.Task),
     BugCount = nonDeletedJobs.Count(j => j.Level == Job.JobLevel.Bug),
     
     // Calculate percentages based on non-epic jobs only
     TotalTasksPercentage = nonEpicJobs.Any() 
-        ? (decimal)nonEpicJobs.Count(j => j.JobStatus == Job.Status.Done) / nonEpicJobs.Count() * 100 
+        ? (decimal)completedNonEpicJobs / nonEpicJobs.Count() * 100 
         : 0,
     
     DueDate = nonDeletedJobs.Max(j => (DateTime?)j.DueDate) ?? null
 };
```

</blockquote></details>
<details>
<summary>aspnet-core/src/toyiyo.todo.Web.Mvc/wwwroot/view-resources/Views/Projects/Index.js (2)</summary><blockquote>

`66-123`: **Comprehensive project card implementation with detailed statistics.**

The card layout provides a rich visual representation of project progress, including task counts, epic status, and due dates, significantly enhancing the user experience.


Consider extracting the inline styles to CSS classes for better maintainability.

```diff
 <div class="card-footer p-2">
-    <div class="progress position-relative" style="height: 20px;">
+    <div class="progress position-relative progress-tall">
         <div class="progress-bar bg-success" 
              style="width: ${progress.totalTasksPercentage}%"
              role="progressbar" 
              aria-valuenow="${progress.totalTasksPercentage}" 
              aria-valuemin="0" 
              aria-valuemax="100"
              data-toggle="tooltip"
              title="${completed} completed">
         </div>
-        <div class="position-absolute w-100 text-center" style="line-height: 20px;">
+        <div class="position-absolute w-100 text-center progress-text">
             ${total === 0 ? 'No tasks' : `${completed} of ${total} tasks completed`}
         </div>
     </div>
 </div>
```

Add this CSS to your stylesheet:
```css
.progress-tall {
    height: 20px;
}

.progress-text {
    line-height: 20px;
}
```

---

`92-102`: **Conditional rendering of due date with proper null checking.**

The implementation correctly handles different date scenarios, including undefined due dates and the special "0001-01-01" date that represents DateTime.MinValue in .NET.


Consider extracting date formatting logic to a utility function for better reusability across the application.

```diff
+ // Add at the top of the file
+ function formatDueDate(dateString) {
+     if (!dateString || moment(dateString).isSame('0001-01-01T00:00:00Z')) {
+         return null;
+     }
+     return moment(dateString).format('M/D/YYYY');
+ }

// Then update the implementation
- ${progress.dueDate && !moment(progress.dueDate).isSame('0001-01-01T00:00:00Z') ? `
+ ${formatDueDate(progress.dueDate) ? `
     <span class="stat-item">
         <i class="fas fa-calendar mr-2"></i>
-        Due: ${moment(progress.dueDate).format('M/D/YYYY')}
+        Due: ${formatDueDate(progress.dueDate)}
     </span>
` : `
     <span class="stat-item text-muted">
         <i class="fas fa-calendar mr-2"></i>
         No due date set
     </span>
`}
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 998fc4a8b8f7abc8b048a8b219075fdab4d22e73 and a12ebe305bbcb5091a37f91469f5e81f042acf48.

</details>

<details>
<summary>📒 Files selected for processing (11)</summary>

* `aspnet-core/src/toyiyo.todo.Application/Projects/Dto/ProjectDto.cs` (1 hunks)
* `aspnet-core/src/toyiyo.todo.Application/Projects/Dto/ProjectProgressDto.cs` (1 hunks)
* `aspnet-core/src/toyiyo.todo.Application/Projects/ProjectAppService.cs` (2 hunks)
* `aspnet-core/src/toyiyo.todo.Core/Projects/IProjectManager.cs` (1 hunks)
* `aspnet-core/src/toyiyo.todo.Core/Projects/Project.cs` (2 hunks)
* `aspnet-core/src/toyiyo.todo.Core/Projects/ProjectManager.cs` (3 hunks)
* `aspnet-core/src/toyiyo.todo.Core/Projects/ProjectProgress.cs` (1 hunks)
* `aspnet-core/src/toyiyo.todo.Web.Mvc/Views/Projects/Index.cshtml` (1 hunks)
* `aspnet-core/src/toyiyo.todo.Web.Mvc/Views/Projects/_ProgressBar.cshtml` (1 hunks)
* `aspnet-core/src/toyiyo.todo.Web.Mvc/wwwroot/css/style.css` (1 hunks)
* `aspnet-core/src/toyiyo.todo.Web.Mvc/wwwroot/view-resources/Views/Projects/Index.js` (3 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Definitions (3)</summary>

<details>
<summary>aspnet-core/src/toyiyo.todo.Core/Projects/Project.cs (1)</summary><blockquote>

<details>
<summary>aspnet-core/src/toyiyo.todo.Core/Jobs/Job.cs (15) (15)</summary>

* `Job` (56-59)
* `Job` (74-107)
* `Job` (109-129)
* `Job` (131-140)
* `Job` (142-158)
* `Job` (160-172)
* `Job` (180-189)
* `Job` (191-202)
* `Job` (204-216)
* `Job` (218-233)
* `Job` (235-248)
* `Job` (250-262)
* `Job` (264-291)
* `Job` (320-333)
* `Job` (335-345)

</details>

</blockquote></details>
<details>
<summary>aspnet-core/src/toyiyo.todo.Core/Projects/ProjectManager.cs (2)</summary><blockquote>

<details>
<summary>aspnet-core/src/toyiyo.todo.Core/Projects/IProjectManager.cs (7) (7)</summary>

* `Task` (9-9)
* `Task` (10-10)
* `Task` (11-11)
* `Task` (12-12)
* `Task` (13-13)
* `Task` (14-14)
* `Task` (15-15)

</details>
<details>
<summary>aspnet-core/src/toyiyo.todo.Core/Projects/ProjectProgress.cs (3) (3)</summary>

* `ProjectProgress` (8-55)
* `ProjectProgress` (21-21)
* `ProjectProgress` (23-54)

</details>

</blockquote></details>
<details>
<summary>aspnet-core/src/toyiyo.todo.Application/Projects/Dto/ProjectProgressDto.cs (2)</summary><blockquote>

<details>
<summary>aspnet-core/src/toyiyo.todo.Application/Projects/Dto/ProjectDto.cs (1) (1)</summary>

* `AutoMap` (7-26)

</details>
<details>
<summary>aspnet-core/src/toyiyo.todo.Core/Projects/ProjectProgress.cs (3) (3)</summary>

* `ProjectProgress` (8-55)
* `ProjectProgress` (21-21)
* `ProjectProgress` (23-54)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (2)</summary>

* GitHub Check: build
* GitHub Check: Analyze (csharp)

</details>

<details>
<summary>🔇 Additional comments (17)</summary><blockquote>

<details>
<summary>aspnet-core/src/toyiyo.todo.Core/Projects/Project.cs (2)</summary>

`2-3`: **Appropriate imports added for the new Jobs collection.**

The imports for `System.Collections.Generic` and `toyiyo.todo.Jobs` are correctly added to support the new Jobs collection property.



Also applies to: 9-9

---

`24-25`: <details>
<summary>❓ Verification inconclusive</summary>

**The property setter differs from the domain pattern used elsewhere.**

Unlike other properties in this class that use protected setters to enforce domain-driven design (as noted in the comment on line 17), the Jobs collection has a public setter. Consider whether this property should follow the same pattern or if there's a specific reason for the deviation.

---

🏁 Script executed:

```shell
#!/bin/bash
# Check if any other collection properties in the project use protected setters
rg -A 1 "virtual ICollection<.*> \{.*get; protected set;" --type csharp
```

Length of output: 73

---



</details>

**Ensure Consistency with the Domain Design Pattern**

The `Jobs` collection property currently exposes a public setter, which deviates from the pattern suggested by the comment on line 17. While it's expected that similar collection properties use a protected setter to enforce domain integrity, our initial search for other instances using `protected set;` did not return any matches. Please manually verify if:

- Other domain collection properties in the project follow the protected setter pattern.
- The public setter on `Jobs` is an intentional deviation or an oversight.

File to review:  
• `aspnet-core/src/toyiyo.todo.Core/Projects/Project.cs` (lines 24–25)

</details>
<details>
<summary>aspnet-core/src/toyiyo.todo.Application/Projects/Dto/ProjectDto.cs (1)</summary>

`25-25`: **Property added to support project progress tracking.**

The new Progress property correctly extends the ProjectDto to include project progress information, which aligns with the PR objectives of tracking and displaying project progress.


However, I should verify that the ProjectProgressDto class exists and contains the appropriate properties:

```shell
#!/bin/bash
# Check if ProjectProgressDto exists and its structure
rg -A 15 "class ProjectProgressDto" --type csharp
```

</details>
<details>
<summary>aspnet-core/src/toyiyo.todo.Core/Projects/IProjectManager.cs (1)</summary>

`15-15`: **Good batch retrieval method for project progress.**

Adding the `GetProjectsProgress` method is an efficient approach for retrieving progress data for multiple projects in a single operation, which is better than making separate calls for each project.


Let's verify the implementation to ensure it's properly optimized:

```shell
#!/bin/bash
# Look for the implementation of the GetProjectsProgress method
rg -A 20 "GetProjectsProgress\(IEnumerable<Guid> projectIds\)" --type csharp
```

</details>
<details>
<summary>aspnet-core/src/toyiyo.todo.Web.Mvc/Views/Projects/Index.cshtml (1)</summary>

`52-52`: **Table structure simplified for new project card layout.**

The table header has been simplified to a single column labeled "Projects", which aligns with the PR objectives of enhancing the visual representation of project statuses. This suggests a move from a traditional tabular layout to a more visual card-based layout.


Let's verify that the corresponding JavaScript has been updated to match this new structure:

```shell
#!/bin/bash
# Check the related JavaScript file for table rendering logic
rg -A 10 "ProjectsTable" ./aspnet-core/src/toyiyo.todo.Web.Mvc/wwwroot/view-resources/Views/Projects/Index.js
```

</details>
<details>
<summary>aspnet-core/src/toyiyo.todo.Core/Projects/ProjectManager.cs (3)</summary>

`13-13`: **Appropriate addition of the missing using directive.**

The added using directive for `Abp.Domain.Entities` is necessary for the `EntityNotFoundException` class used in the new `GetProjectProgress` method.

---

`53-54`: **Good include of the Jobs navigation property.**

Including the Jobs navigation property when retrieving projects ensures that all the necessary data is available for progress calculation, preventing potential N+1 query issues.

---

`76-86`: **Well-implemented progress retrieval method with proper null handling.**

The implementation correctly retrieves the project with its associated jobs and throws an appropriate `EntityNotFoundException` when the project doesn't exist. This follows good error handling practices.

</details>
<details>
<summary>aspnet-core/src/toyiyo.todo.Core/Projects/ProjectProgress.cs (4)</summary>

`8-20`: **Well-designed immutable progress class with appropriate properties.**

The `ProjectProgress` class encapsulates all relevant metrics for tracking project progress. The use of private setters ensures immutability after construction, which is a good practice for DTOs.

---

`21-21`: **Good use of private constructor to enforce factory method pattern.**

Using a private constructor in conjunction with the static `Calculate` method enforces a consistent way to create `ProjectProgress` instances, preventing invalid states.

---

`23-27`: **Proper null and empty collection handling.**

The method correctly handles cases where the project or its jobs collection might be null or empty, returning a default `ProjectProgress` instance instead of throwing exceptions.

---

`28-30`: **Efficient filtering of jobs for calculation.**

The code properly separates non-deleted jobs and non-epic jobs for accurate calculation of different metrics.

</details>
<details>
<summary>aspnet-core/src/toyiyo.todo.Web.Mvc/wwwroot/view-resources/Views/Projects/Index.js (5)</summary>

`37-38`: **Updated class name and render function to support the new card layout.**

The changes to the column definition properly prepare the DataTable for the enhanced card-based UI.

---

`39-63`: **Well-structured dropdown menu for project actions.**

The implementation provides a clean dropdown menu with edit and delete actions, improving the UI organization and user experience.

---

`129-131`: **Good initialization of tooltips after DataTable draw.**

The event handler ensures tooltips are initialized every time the DataTable redraws, maintaining the tooltip functionality across pagination and sorting operations.

---

`171-171`: **Simplified event handler for project edited event.**

Removing the unused parameter improves code readability.

---

`181-181`: **Simplified event handler for search button.**

Removing the unused parameter improves code readability.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T02:58:18Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 2**

<details>
<summary>🧹 Nitpick comments (3)</summary><blockquote>

<details>
<summary>aspnet-core/src/toyiyo.todo.Core/Projects/ProjectProgress.cs (3)</summary><blockquote>

`8-21`: **Well-structured class with appropriate properties**

The class design with private setters ensures proper encapsulation. All properties follow good naming conventions and represent the various aspects of project progress tracking.

Consider adding XML documentation for each property to improve API usability and make it clear what each metric represents, especially for properties like `TotalTasksPercentage` vs `CompletedTasksPercentage`.

---

`63-63`: **Simplify DueDate calculation**

The expression `nonDeletedJobs.Max(j => (DateTime?)j.DueDate) ?? null` is redundant since `?? null` doesn't affect the result when the left side is already null.


```diff
-DueDate = nonDeletedJobs.Max(j => (DateTime?)j.DueDate) ?? null
+DueDate = nonDeletedJobs.Max(j => (DateTime?)j.DueDate)
```

---

`34-48`: **Consider optimizing multiple LINQ queries**

The code makes multiple passes over the job collections. While this is clean and readable, it could be optimized for performance, especially for projects with many jobs.


Consider using a single pass approach to compute all counts:

```csharp
var stats = nonDeletedJobs.Aggregate(
    new { 
        TotalNonEpic = 0, 
        CompletedNonEpic = 0, 
        InProgressNonEpic = 0, 
        BacklogNonEpic = 0, 
        Epics = 0, 
        CompletedEpics = 0, 
        Tasks = 0, 
        Bugs = 0 
    },
    (acc, job) => {
        if (job.Level != Job.JobLevel.Epic) {
            acc.TotalNonEpic++;
            if (job.JobStatus == Job.Status.Done) acc.CompletedNonEpic++;
            else if (job.JobStatus == Job.Status.InProgress) acc.InProgressNonEpic++;
            else if (job.JobStatus == Job.Status.Open) acc.BacklogNonEpic++;
        } else {
            acc.Epics++;
            if (job.JobStatus == Job.Status.Done) acc.CompletedEpics++;
        }
        
        if (job.Level == Job.JobLevel.Task) acc.Tasks++;
        else if (job.Level == Job.JobLevel.Bug) acc.Bugs++;
        
        return acc;
    });

// Then use stats to populate the progress object
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between a12ebe305bbcb5091a37f91469f5e81f042acf48 and 18a077fcb10899539ede612dd291d065b1b20ae4.

</details>

<details>
<summary>📒 Files selected for processing (4)</summary>

* `aspnet-core/src/toyiyo.todo.Application/Projects/Dto/ProjectProgressDto.cs` (1 hunks)
* `aspnet-core/src/toyiyo.todo.Application/Projects/ProjectAppService.cs` (2 hunks)
* `aspnet-core/src/toyiyo.todo.Core/Projects/ProjectProgress.cs` (1 hunks)
* `aspnet-core/src/toyiyo.todo.Web.Mvc/Views/Projects/_ProgressBar.cshtml` (1 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (3)</summary>

* aspnet-core/src/toyiyo.todo.Application/Projects/Dto/ProjectProgressDto.cs
* aspnet-core/src/toyiyo.todo.Application/Projects/ProjectAppService.cs
* aspnet-core/src/toyiyo.todo.Web.Mvc/Views/Projects/_ProgressBar.cshtml

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Definitions (1)</summary>

<details>
<summary>aspnet-core/src/toyiyo.todo.Core/Projects/ProjectProgress.cs (2)</summary><blockquote>

<details>
<summary>aspnet-core/src/toyiyo.todo.Application/Projects/ProjectAppService.cs (5) (5)</summary>

* `Task` (18-23)
* `Task` (25-28)
* `Task` (30-48)
* `Task` (51-55)
* `Task` (58-61)

</details>
<details>
<summary>aspnet-core/src/toyiyo.todo.Core/Projects/IProjectManager.cs (7) (7)</summary>

* `Task` (9-9)
* `Task` (10-10)
* `Task` (11-11)
* `Task` (12-12)
* `Task` (13-13)
* `Task` (14-14)
* `Task` (15-15)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (2)</summary>

* GitHub Check: build
* GitHub Check: Analyze (csharp)

</details>

<details>
<summary>🔇 Additional comments (4)</summary><blockquote>

<details>
<summary>aspnet-core/src/toyiyo.todo.Core/Projects/ProjectProgress.cs (4)</summary>

`1-7`: **Imports and namespace structure look good**

The imports are appropriate for the functionality being implemented. The class is correctly placed in the `toyiyo.todo.Projects` namespace, which aligns with its purpose of tracking project progress.

---

`23-23`: **Good encapsulation with private constructor**

The private constructor ensures that the `ProjectProgress` instances can only be created through the `Calculate` static method, maintaining control over the object's initialization.

---

`25-29`: **Proper null handling**

The method correctly checks if the project is null or has no jobs before attempting calculations, returning a default `ProjectProgress` instance in such cases.

---

`30-33`: **Effective job filtering logic**

The code correctly filters out deleted jobs and separates epic from non-epic jobs, which is essential for accurate progress tracking.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T13:44:49Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (4)</summary><blockquote>

<details>
<summary>aspnet-core/src/toyiyo.todo.Application/Projects/Dto/ProjectProgressDto.cs (1)</summary><blockquote>

`23-24`: **Consider adding documentation for status properties.**

These string properties represent the project's health status and its associated CSS class. Adding XML documentation would clarify their purpose and expected values for consumers of this DTO.


```diff
+        /// <summary>
+        /// The current health status of the project (e.g., "On Track", "At Risk", "Behind")
+        /// </summary>
         public string Status { get; set; }
+        /// <summary>
+        /// CSS class associated with the status for UI display (e.g., "badge-info", "badge-warning")
+        /// </summary>
         public string StatusClass { get; set; }
```

</blockquote></details>
<details>
<summary>aspnet-core/src/toyiyo.todo.Core/Projects/ProjectProgress.cs (1)</summary><blockquote>

`59-59`: **Simplify DueDate calculation.**

The DueDate calculation can be simplified. The null coalescing operator (`?? null`) is unnecessary since the expression already returns `null` if no elements satisfy the condition.


```diff
-                DueDate = nonDeletedJobs.Max(j => (DateTime?)j.DueDate) ?? null,
+                DueDate = nonDeletedJobs.Max(j => (DateTime?)j.DueDate),
```

</blockquote></details>
<details>
<summary>aspnet-core/test/toyiyo.todo.Tests/Projects/ProjectHealthStatusTests.cs (1)</summary><blockquote>

`147-168`: **Consider adding more detailed assertions for CSS classes.**

While the test verifies status values across different completion ratios, it doesn't assert the CSS classes for each status. Adding these assertions would ensure complete verification of both status and visual representation.


```diff
             for (int i = 0; i < completedTasksArray.Length; i++)
             {
                 int completedTasks = completedTasksArray[i];
                 string expectedStatus = expectedStatuses[i];
+                string[] expectedCssClasses = { "badge-danger", "badge-warning", "badge-info", "badge-success" };
 
                 var result = ProjectHealthStatus.Calculate(totalTasks, completedTasks, bugCount, dueDate);
                 result.Status.ShouldBe(expectedStatus,
                     $"Failed with completionRatio = {(decimal)completedTasks / totalTasks:P0}");
+                result.CssClass.ShouldBe(expectedCssClasses[i],
+                    $"CSS class incorrect for {expectedStatus} status");
             }
```

</blockquote></details>
<details>
<summary>aspnet-core/src/toyiyo.todo.Core/Projects/ProjectHealthStatus.cs (1)</summary><blockquote>

`29-31`: **Consider adding validation for completedTasks.**

The method doesn't validate that `completedTasks` is not greater than `totalTasks`, which could lead to a completion ratio > 1 if incorrect data is provided. Adding this validation would improve robustness.


```diff
+            if (completedTasks > totalTasks)
+                throw new ArgumentException("Completed tasks cannot exceed total tasks", nameof(completedTasks));
+
             var completionRatio = (decimal)completedTasks / totalTasks;
-            var bugRatio = totalTasks > 0 ? (decimal)bugCount / totalTasks : 0;
+            var bugRatio = (decimal)bugCount / totalTasks;
             var isPastDue = dueDate.HasValue && DateTime.UtcNow > dueDate.Value;
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 18a077fcb10899539ede612dd291d065b1b20ae4 and 8844d70b4eeabe03690e561c18d676cfb1d56050.

</details>

<details>
<summary>📒 Files selected for processing (5)</summary>

* `aspnet-core/src/toyiyo.todo.Application/Projects/Dto/ProjectProgressDto.cs` (1 hunks)
* `aspnet-core/src/toyiyo.todo.Application/Projects/ProjectAppService.cs` (2 hunks)
* `aspnet-core/src/toyiyo.todo.Core/Projects/ProjectHealthStatus.cs` (1 hunks)
* `aspnet-core/src/toyiyo.todo.Core/Projects/ProjectProgress.cs` (1 hunks)
* `aspnet-core/test/toyiyo.todo.Tests/Projects/ProjectHealthStatusTests.cs` (1 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* aspnet-core/src/toyiyo.todo.Application/Projects/ProjectAppService.cs

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Definitions (2)</summary>

<details>
<summary>aspnet-core/src/toyiyo.todo.Core/Projects/ProjectProgress.cs (4)</summary><blockquote>

<details>
<summary>aspnet-core/src/toyiyo.todo.Core/Projects/ProjectHealthStatus.cs (3) (3)</summary>

* `ProjectHealthStatus` (5-47)
* `ProjectHealthStatus` (14-18)
* `ProjectHealthStatus` (20-46)

</details>
<details>
<summary>aspnet-core/src/toyiyo.todo.Core/Projects/Project.cs (4) (4)</summary>

* `Project` (31-34)
* `Project` (36-56)
* `Project` (58-70)
* `Project` (72-83)

</details>
<details>
<summary>aspnet-core/src/toyiyo.todo.Application/Projects/ProjectAppService.cs (5) (5)</summary>

* `Task` (18-23)
* `Task` (25-35)
* `Task` (37-55)
* `Task` (58-62)
* `Task` (65-68)

</details>
<details>
<summary>aspnet-core/src/toyiyo.todo.Core/Projects/IProjectManager.cs (7) (7)</summary>

* `Task` (9-9)
* `Task` (10-10)
* `Task` (11-11)
* `Task` (12-12)
* `Task` (13-13)
* `Task` (14-14)
* `Task` (15-15)

</details>

</blockquote></details>
<details>
<summary>aspnet-core/test/toyiyo.todo.Tests/Projects/ProjectHealthStatusTests.cs (1)</summary><blockquote>

<details>
<summary>aspnet-core/src/toyiyo.todo.Core/Projects/ProjectHealthStatus.cs (3) (3)</summary>

* `ProjectHealthStatus` (5-47)
* `ProjectHealthStatus` (14-18)
* `ProjectHealthStatus` (20-46)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (2)</summary>

* GitHub Check: build
* GitHub Check: Analyze (csharp)

</details>

<details>
<summary>🔇 Additional comments (10)</summary><blockquote>

<details>
<summary>aspnet-core/src/toyiyo.todo.Application/Projects/Dto/ProjectProgressDto.cs (1)</summary>

`26-46`: **Well-designed mapping from domain to DTO.**

The FromDomain method provides a clean, defensive implementation for mapping from the domain model to the DTO, including proper null handling on line 28 and null-safe property access with the null conditional operator on lines 43-44.

</details>
<details>
<summary>aspnet-core/src/toyiyo.todo.Core/Projects/ProjectProgress.cs (3)</summary>

`25-70`: **Well-structured progress calculation logic.**

The Calculate method effectively handles edge cases and provides comprehensive project metrics. The approach of using a static factory method with a private constructor enforces proper instantiation and encapsulation.

---

`51-53`: **Rename TotalTasksPercentage for clarity.**

The property name `TotalTasksPercentage` doesn't clearly convey that it's calculating the percentage of completed tasks. This matches the previous review comment about redundant percentage calculations.


```diff
-                TotalTasksPercentage = totalNonEpicJobs > 0
+                CompletedTasksPercentage = totalNonEpicJobs > 0
                     ? (decimal)nonEpicJobs.Count(j => j.JobStatus == Job.Status.Done) / totalNonEpicJobs * 100 
                     : 0,
```

---

`27-69`: **Unit Tests for ProjectProgress Calculation Logic Needed**

The `Calculate` method contains core business logic that currently lacks dedicated unit tests. It's crucial to add comprehensive tests to ensure the method behaves correctly across all scenarios, including:

- **Various Combinations**: Projects with a mix of job types (Task, Bug, Epic) and statuses (Done, InProgress, Open).
- **Edge Cases**: Handling empty projects or cases where all jobs share a single status.
- **Percentage Calculations**: Properly calculating percentages with safeguards against potential division by zero, particularly for non-epic jobs.

Please add these tests to maintain high reliability in project progress computations.


```shell
#!/bin/bash
# Check if there are tests covering ProjectProgress class
echo "Searching for tests covering ProjectProgress class..."
fd -e cs "ProjectProgress" --full-path "*/test/*"
```

</details>
<details>
<summary>aspnet-core/test/toyiyo.todo.Tests/Projects/ProjectHealthStatusTests.cs (4)</summary>

`11-60`: **Excellent basic test coverage for ProjectHealthStatus.**

The tests for key scenarios (`WhenNoTasks`, `WhenAllTasksComplete`, and `WhenPastDueAndNotComplete`) follow good testing practices with clear Arrange-Act-Assert structure and meaningful assertions.

---

`62-80`: **Well-designed parametrized tests for On Track status.**

Using the [Theory] attribute with [InlineData] is a great approach for testing multiple scenarios without code duplication. The test effectively covers threshold edge cases and various combinations of completion and bug ratios.

---

`82-99`: **Good test scenarios for At Risk status.**

The tests properly verify the "At Risk" status for threshold conditions and cases with high bug counts.

---

`121-145`: **Thorough boundary testing.**

This test effectively verifies that the status transitions occur exactly at the defined thresholds, which is critical for ensuring consistent behavior. Testing both sides of each threshold boundary is excellent practice.

</details>
<details>
<summary>aspnet-core/src/toyiyo.todo.Core/Projects/ProjectHealthStatus.cs (2)</summary>

`5-18`: **Well-designed immutable object pattern.**

The ProjectHealthStatus class uses read-only properties and a private constructor to ensure immutability. This is an excellent pattern for value objects that shouldn't change after creation.

---

`20-46`: **Well-structured status calculation with clear logic.**

The Calculate method effectively evaluates project health based on completion ratio, bug ratio, and due date. The priority order of conditions ensures consistent status determination.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T20:33:08Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>🧹 Nitpick comments (6)</summary><blockquote>

<details>
<summary>aspnet-core/src/toyiyo.todo.Core/Projects/ProjectProgress.cs (2)</summary><blockquote>

`30-49`: **Potential performance improvement by caching repeated enumerations.**  
Multiple calls to `Count(...)` on the same `IEnumerable` (e.g., `nonEpicJobs`) can be costly for large collections. Consider storing results in local variables to avoid re-enumerating:  

```diff
var nonDeletedJobs = project.Jobs.Where(j => !j.IsDeleted);
var nonEpicJobs = nonDeletedJobs.Where(j => j.Level != Job.JobLevel.Epic);
-var totalNonEpicJobs = nonEpicJobs.Count();
-var completedCount = nonEpicJobs.Count(j => j.JobStatus == Job.Status.Done);
-var inProgressCount = nonEpicJobs.Count(j => j.JobStatus == Job.Status.InProgress);
-var openCount = nonEpicJobs.Count(j => j.JobStatus == Job.Status.Open);

+var nonEpicJobsList = nonEpicJobs.ToList();
+var totalNonEpicJobs = nonEpicJobsList.Count;
+var completedCount = nonEpicJobsList.Count(j => j.JobStatus == Job.Status.Done);
+var inProgressCount = nonEpicJobsList.Count(j => j.JobStatus == Job.Status.InProgress);
+var openCount = nonEpicJobsList.Count(j => j.JobStatus == Job.Status.Open);
```

---

`50-53`: **Clarify naming for better readability.**  
`TotalTasksPercentage` represents “Completed Tasks Percentage.” A clearer name (e.g., `CompletedTasksPercentage`) would improve code readability and avoid confusion with the total count of tasks.

</blockquote></details>
<details>
<summary>aspnet-core/test/toyiyo.todo.Tests/Projects/ProjectProgressTests.cs (1)</summary><blockquote>

`134-161`: **Include a boundary test for due dates that match the current time.**  
Currently, tests focus on future due dates. Consider adding a scenario where the due date equals `DateTime.UtcNow` to verify correct “Overdue” or “On Track” classification.

</blockquote></details>
<details>
<summary>aspnet-core/src/toyiyo.todo.Core/Projects/ProjectHealthStatus.cs (1)</summary><blockquote>

`20-46`: **Logical flow covers a wide range of project states.**  
Your thresholds for “Completed,” “Overdue,” “On Track,” “At Risk,” and “Behind” are well structured, ensuring clear, maintainable conditions. Consider these minor refinements:  
1. If bug ratio is very high (> 0.3) while completion is high, you may want to degrade the status from “On Track.”  
2. Include a test case in your unit tests to confirm correct classification just below and just above threshold boundaries.

</blockquote></details>
<details>
<summary>aspnet-core/src/toyiyo.todo.Web.Mvc/wwwroot/view-resources/Views/Projects/Index.js (2)</summary><blockquote>

`92-102`: **Consider using a more robust date check.**

The current date checking relies on comparing with a specific date string format. A more robust approach would be to check if the date is valid using a dedicated method.


```diff
-                                        ${progress.dueDate && !moment(progress.dueDate).isSame('0001-01-01T00:00:00Z') ? `
+                                        ${progress.dueDate && moment(progress.dueDate).isValid() && !moment(progress.dueDate).isSame('0001-01-01T00:00:00Z') ? `
```

---

`107-120`: **Clear visual representation of project progress.**

The progress bar implementation provides an excellent visual cue for project completion status. The tooltip integration and text overlay enhance user understanding.

One potential accessibility improvement:


Consider adding more color contrast to the text overlay to ensure it's readable against both the progress bar and the background:

```diff
-                                    <div class="position-absolute w-100 text-center" style="line-height: 20px;">
+                                    <div class="position-absolute w-100 text-center" style="line-height: 20px; text-shadow: 0px 0px 2px rgba(255,255,255,0.7);">
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 8844d70b4eeabe03690e561c18d676cfb1d56050 and 71958afb3dfb078ca77f58ab08d60fde22737924.

</details>

<details>
<summary>📒 Files selected for processing (7)</summary>

* `aspnet-core/src/toyiyo.todo.Application/Projects/Dto/ProjectProgressDto.cs` (1 hunks)
* `aspnet-core/src/toyiyo.todo.Core/Projects/ProjectHealthStatus.cs` (1 hunks)
* `aspnet-core/src/toyiyo.todo.Core/Projects/ProjectProgress.cs` (1 hunks)
* `aspnet-core/src/toyiyo.todo.Web.Mvc/Views/Projects/_ProgressBar.cshtml` (1 hunks)
* `aspnet-core/src/toyiyo.todo.Web.Mvc/wwwroot/view-resources/Views/Projects/Index.js` (3 hunks)
* `aspnet-core/test/toyiyo.todo.Tests/Projects/ProjectHealthStatusTests.cs` (1 hunks)
* `aspnet-core/test/toyiyo.todo.Tests/Projects/ProjectProgressTests.cs` (1 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (3)</summary>

* aspnet-core/src/toyiyo.todo.Web.Mvc/Views/Projects/_ProgressBar.cshtml
* aspnet-core/src/toyiyo.todo.Application/Projects/Dto/ProjectProgressDto.cs
* aspnet-core/test/toyiyo.todo.Tests/Projects/ProjectHealthStatusTests.cs

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (2)</summary>

* GitHub Check: build
* GitHub Check: Analyze (csharp)

</details>

<details>
<summary>🔇 Additional comments (10)</summary><blockquote>

<details>
<summary>aspnet-core/src/toyiyo.todo.Core/Projects/ProjectProgress.cs (2)</summary>

`23-28`: **Good early return for empty or null projects.**  
The `Calculate` method correctly guards against missing or empty projects by returning a default `ProjectProgress` instance. This prevents unnecessary processing and potential null-reference errors.

---

`61-66`: **Excellent integration with project health calculation.**  
Invoking `ProjectHealthStatus.Calculate` here aligns well with the existing progress metrics. This keeps health logic encapsulated and easily updatable.

</details>
<details>
<summary>aspnet-core/test/toyiyo.todo.Tests/Projects/ProjectProgressTests.cs (4)</summary>

`41-95`: **Comprehensive test coverage for mixed job statuses.**  
Your `CreateProjectWithMixedJobs` method thoroughly covers multiple epic statuses, tasks, and bugs. This helps ensure stable behavior in complex scenarios.

---

`99-132`: **Good handling of deleted jobs scenario.**  
Verifying correctness with deleted jobs is crucial. These tests robustly confirm that deleted items do not affect progress metrics.

---

`163-179`: **All-tasks-completed scenario is well tested.**  
This ensures percentages, counts, and statuses align when all tasks are done. Great coverage of a common end-of-project state.

---

`181-259`: **Tests confirm intended percentage rounding.**  
Verifying explicit decimal values like `33.33m` ensures the rounding logic is correctly implemented. This improves confidence in real-world usage.

</details>
<details>
<summary>aspnet-core/src/toyiyo.todo.Web.Mvc/wwwroot/view-resources/Views/Projects/Index.js (4)</summary>

`37-123`: **Well-implemented card layout with comprehensive project statistics.**

The transformation from a basic table cell to a rich card layout significantly enhances the visual representation of project data. The implementation properly utilizes the progress object to display meaningful statistics and includes a visually appealing progress bar.

A few observations:
- Good use of conditional rendering for the due date display
- Clear organization of project statistics with appropriate icons
- Well-structured dropdown for actions

---

`129-131`: **Good addition of tooltip initialization.**

Adding the tooltip initialization on the DataTable draw event ensures tooltips work correctly after table updates.

---

`171-171`: **Clean up of unused parameter.**

Good cleanup by removing the unused data parameter from the event handler.

---

`181-181`: **Clean up of unused parameter.**

Consistent with other changes, this removes the unused parameter from the event handler.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-22T22:21:08Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>🧹 Nitpick comments (3)</summary><blockquote>

<details>
<summary>aspnet-core/test/toyiyo.todo.Tests/Projects/ProjectProgressTests.cs (3)</summary><blockquote>

`181-199`: **Consider adding specific assertions for DueDate and HealthStatus.**

While the test verifies that DueDate and HealthStatus are not null, it doesn't verify their specific values. Consider adding assertions to validate the expected values based on the project configuration.

```diff
 progress.DueDate.ShouldNotBeNull();
 progress.HealthStatus.ShouldNotBeNull();
+// Add specific assertions for expected values
+// For example:
+// progress.HealthStatus.ShouldBe(ProjectHealthStatus.OnTrack);
+// Verify the DueDate is calculated correctly based on job due dates
```

---

`215-216`: **Be cautious with decimal precision in percentage assertions.**

The assertions use specific decimal values (33.33m) which may be prone to floating-point precision issues. Consider using approximate comparison or rounding to avoid brittle tests.

```diff
-progress.TotalTasksPercentage.ShouldBe(33.33m);
-progress.InProgressPercentage.ShouldBe(33.33m);
+// Either use ShouldBeInRange
+progress.TotalTasksPercentage.ShouldBeInRange(33.3m, 33.4m);
+progress.InProgressPercentage.ShouldBeInRange(33.3m, 33.4m);
+// Or use Math.Round for more precise control
+Math.Round(progress.TotalTasksPercentage, 2).ShouldBe(33.33m);
+Math.Round(progress.InProgressPercentage, 2).ShouldBe(33.33m);
```

---

`41-179`: **Consider refactoring helper methods to reduce duplication.**

The helper methods have similar patterns for creating jobs. Consider extracting common functionality to reduce duplication and make the test setup more maintainable.


For example, you could create a helper method like:

```csharp
private async Task<Job> CreateJob(Guid projectId, string title, JobLevel level = JobLevel.Task, Status status = Status.Backlog, DateTime? dueDate = null)
{
    var job = await _jobAppService.Create(new JobCreateInputDto 
    { 
        ProjectId = projectId, 
        Title = title,
        Level = level,
        DueDate = dueDate
    });
    
    if (status != Status.Backlog)
    {
        await _jobAppService.SetJobStatus(new JobSetStatusInputDto { Id = job.Id, JobStatus = status });
    }
    
    return job;
}
```

Then use it in your setup methods to reduce code duplication.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 71958afb3dfb078ca77f58ab08d60fde22737924 and f9e219359ed4ad51bec5731e350663ddeb729054.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `aspnet-core/test/toyiyo.todo.Tests/Projects/ProjectProgressTests.cs` (1 hunks)

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (2)</summary>

* GitHub Check: build
* GitHub Check: Analyze (csharp)

</details>

<details>
<summary>🔇 Additional comments (4)</summary><blockquote>

<details>
<summary>aspnet-core/test/toyiyo.todo.Tests/Projects/ProjectProgressTests.cs (4)</summary>

`1-31`: **Good test class setup with proper dependency injection.**

The test class is well-structured with appropriate service resolution and authentication setup in the constructor. The imported namespaces cover all necessary dependencies for the tests.

---

`32-39`: **Efficient project entity retrieval with eager loading.**

The helper method correctly uses Entity Framework's Include method to eagerly load the Jobs collection, which is essential for the progress calculations being tested.

---

`41-97`: **Well-structured test data setup for mixed job scenarios.**

The method creates a comprehensive test scenario with a good mix of job types (Epics, Tasks, Bugs) and statuses (Done, InProgress, Backlog). This provides good coverage for testing the progress calculation logic.

---

`99-132`: **Test setup properly handles deleted jobs scenario.**

This helper method correctly creates and then deletes a job to test how the progress calculation handles deleted items.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-22T22:29:34Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>🧹 Nitpick comments (5)</summary><blockquote>

<details>
<summary>aspnet-core/src/toyiyo.todo.Web.Mvc/wwwroot/css/style.css (2)</summary><blockquote>

`97-108`: **The project title styling is defined twice with different selectors.**

There are two nearly identical style definitions for project titles - one with the `.dataTables_wrapper` prefix (lines 98-108) and another without (lines 114-124). This duplication makes maintenance more difficult and could lead to inconsistent styling if one is updated but not the other.


Consider consolidating these styles into a single class and using inheritance or composition where needed:

```diff
-.dataTables_wrapper .project-title {
-  font-size: 18px !important;
-  font-weight: 500 !important;
-  color: #2c3e50;
-  text-decoration: none;
-}
-
-.dataTables_wrapper .project-title:hover {
-  color: #3498db;
-  text-decoration: none;
-}
```

Then adjust line 71 in Index.js to use the non-prefixed class.

---

`110-112`: **Avoid using !important in CSS declarations.**

The use of `!important` overrides the natural cascading behavior of CSS and can lead to maintenance issues when styles need to be updated.


Remove the !important declaration and use more specific selectors if needed:

```diff
-.dataTables_wrapper .card-title {
-  font-size: 18px !important;
-}
+.dataTables_wrapper .card-title {
+  font-size: 18px;
+}
```

</blockquote></details>
<details>
<summary>aspnet-core/src/toyiyo.todo.Web.Mvc/wwwroot/view-resources/Views/Projects/Index.js (3)</summary><blockquote>

`66-123`: **Consider extracting the card template into a separate function.**

The HTML template is quite large and complex, making the render function difficult to read and maintain. This could lead to challenges when updating the UI in the future.


Extract the template generation into a separate function to improve code readability:

```diff
render: function(data, type, row) {
    const progress = row.progress;
    const total = progress.totalJobCount;
    const completed = progress.completedTasks;
    const actions = DOMPurify.sanitize(`...`);

-   // Generate card content regardless of task count
-   return `
-       <div class="card">
-           ...
-       </div>
-   `;
+   // Generate card content regardless of task count
+   return generateProjectCard(row, progress, completed, total, actions);
}

// Add this function outside the DataTable definition
function generateProjectCard(row, progress, completed, total, actions) {
    return `
        <div class="card">
            ...
        </div>
    `;
}
```

---

`107-120`: **Improve accessibility of the progress bar.**

While the progress bar includes aria attributes, it could benefit from additional context for screen readers.


Add an `aria-label` attribute to provide more context:

```diff
<div class="progress-bar bg-success" 
     style="width: ${progress.totalTasksPercentage}%"
     role="progressbar" 
     aria-valuenow="${progress.totalTasksPercentage}" 
     aria-valuemin="0" 
     aria-valuemax="100"
+    aria-label="Project completion: ${progress.totalTasksPercentage}% of tasks completed"
     data-toggle="tooltip"
     title="${completed} completed (${progress.totalTasksPercentage}%)">
</div>
```

---

`181-183`: **Consider adding a loading indicator during search operations.**

When users click the search button, there's no visual feedback that the search is in progress until the results appear.


Add a loading indicator when searching:

```diff
$('.btn-search').on('click', () => {
+    abp.ui.setBusy(_$table);
    _$projectsTable.ajax.reload();
+    // The busy state will be cleared automatically after the ajax call completes
});
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 5d99b74c25af009639574ca87b017550580c84cf and 7fed8ff4fb833d5dbf5edf4845cb31551d891f0a.

</details>

<details>
<summary>📒 Files selected for processing (2)</summary>

* `aspnet-core/src/toyiyo.todo.Web.Mvc/wwwroot/css/style.css` (1 hunks)
* `aspnet-core/src/toyiyo.todo.Web.Mvc/wwwroot/view-resources/Views/Projects/Index.js` (3 hunks)

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (2)</summary>

* GitHub Check: Analyze (csharp)
* GitHub Check: build

</details>

<details>
<summary>🔇 Additional comments (6)</summary><blockquote>

<details>
<summary>aspnet-core/src/toyiyo.todo.Web.Mvc/wwwroot/css/style.css (2)</summary>

`126-131`: **Good use of flexbox for responsive statistics layout.**

The flexible layout for project statistics will adapt well to different screen sizes.

---

`146-148`: **Improved badge styling enhances visual hierarchy.**

The updated badge styling with adjusted font size and padding will provide better readability and visual balance within the new card layout.

</details>
<details>
<summary>aspnet-core/src/toyiyo.todo.Web.Mvc/wwwroot/view-resources/Views/Projects/Index.js (4)</summary>

`39-63`: **Good use of DOMPurify to prevent XSS vulnerabilities.**

You've properly addressed the security concern raised in previous reviews by sanitizing dynamic content before inserting it into the HTML. This is a critical security practice.

---

`129-131`: **Good implementation of tooltips initialization.**

Adding tooltip initialization on the DataTable draw event ensures tooltips work correctly when the table is redrawn. This is a good practice for dynamic content.

---

`171-173`: **Simplified event listeners improve code maintainability.**

Removing the unused `data` parameter from event listeners makes the code cleaner and more maintainable.

---

`39-119`: <details>
<summary>❓ Verification inconclusive</summary>

**Verify handling of edge cases in progress calculation and display.**

The code assumes that progress data will always be available and formatted correctly. Edge cases like zero tasks or missing progress data might cause issues.


Check the server-side implementation to ensure proper handling of edge cases:

---

🏁 Script executed:

```shell
#!/bin/bash
# Look for the ProjectProgressDto implementation to verify edge case handling
echo "Searching for ProjectProgressDto implementation..."
find aspnet-core/src -type f -name "*.cs" -exec grep -l "ProjectProgressDto" {} \;

# Check how the progress percentage is calculated
echo "Checking progress calculation logic..."
find aspnet-core/src -type f -name "*.cs" -exec grep -A 10 "totalTasksPercentage" {} \;
```

Length of output: 586

---



</details>

**Verify progress edge case handling on both client and server sides**

The client-side code properly checks for zero tasks (displaying “No tasks” when `total === 0`), and the existence of a `ProjectProgressDto` (in both `ProjectDto.cs` and `ProjectProgressDto.cs`) confirms that progress data is being structured server-side. However, our search didn’t reveal explicit logic for computing `totalTasksPercentage`. Please verify that the server code (likely in `ProjectProgressDto.cs` or within the Project service logic in `ProjectAppService.cs`) correctly calculates and assigns a safe default to `totalTasksPercentage` in scenarios where progress data might be missing or when total tasks is zero. This will ensure robust handling of edge cases and prevent potential display issues.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-22T22:49:55Z

Review comments:
diff_hunk: @@ -28,8 +29,20 @@ public async Task<ProjectDto> Get(Guid id) {
         /// <summary> Gets all Projects. Keyword filters by Title</summary>
         public async Task<PagedResultDto<ProjectDto>> GetAll(GetAllProjectsInput input) {
             var projects = await _projectManager.GetAll(input);
+            
+            // Get progress for all projects in one batch
+            var progressMap = await _projectManager.GetProjectsProgress(
+                projects.Select(p => p.Id)
+            );
+            
+            var dtos = projects.Select(p => {
+                var dto = ObjectMapper.Map<ProjectDto>(p);
+                dto.Progress = ObjectMapper.Map<ProjectProgressDto>(progressMap[p.Id]);
+                return dto;
+            }).ToList();
+            
             var projectsTotalCount = await _projectManager.GetAllCount(input);
-            return new PagedResultDto<ProjectDto>(projectsTotalCount, ObjectMapper.Map<List<ProjectDto>>(projects));
+            return new PagedResultDto<ProjectDto>(projectsTotalCount, dtos);
path: aspnet-core/src/toyiyo.todo.Application/Projects/ProjectAppService.cs
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Handle potential key not found exception when mapping project progress**

The current implementation assumes that `progressMap` will always contain an entry for every project ID. If `_projectManager.GetProjectsProgress()` fails to return progress for any project, this could lead to a `KeyNotFoundException` when accessing `progressMap[p.Id]`.


Add a null check or use a safe dictionary access method:

```diff
var dtos = projects.Select(p => {
    var dto = ObjectMapper.Map<ProjectDto>(p);
-    dto.Progress = ObjectMapper.Map<ProjectProgressDto>(progressMap[p.Id]);
+    dto.Progress = progressMap.TryGetValue(p.Id, out var progress)
+        ? ObjectMapper.Map<ProjectProgressDto>(progress)
+        : new ProjectProgressDto();
    return dto;
}).ToList();
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
            // Get progress for all projects in one batch
            var progressMap = await _projectManager.GetProjectsProgress(
                projects.Select(p => p.Id)
            );
            
            var dtos = projects.Select(p => {
                var dto = ObjectMapper.Map<ProjectDto>(p);
                dto.Progress = progressMap.TryGetValue(p.Id, out var progress)
                    ? ObjectMapper.Map<ProjectProgressDto>(progress)
                    : new ProjectProgressDto();
                return dto;
            }).ToList();
            
            var projectsTotalCount = await _projectManager.GetAllCount(input);
            return new PagedResultDto<ProjectDto>(projectsTotalCount, dtos);
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commit 18a077f
created_at: 2025-03-20T02:58:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,69 @@
+using System.Linq;
+using System.Collections.Generic;
+using toyiyo.todo.Jobs;
+using System;
+
+namespace toyiyo.todo.Projects
+{
+    public class ProjectProgress
+    {
+        public int TotalTasks { get; private set; }
+        public int CompletedTasks { get; private set; }
+        public int InProgressTasks { get; private set; }
+        public int BacklogTasks { get; private set; }
+        public int EpicCount { get; private set; }
+        public int TaskCount { get; private set; }
+        public int BugCount { get; private set; }
+        public DateTime? DueDate { get; private set; }
+        public int CompletedEpics { get; private set; }
+        public decimal TotalTasksPercentage { get; private set; }
+        public decimal CompletedTasksPercentage { get; private set; }
+        public decimal InProgressPercentage { get; private set; }
+
+        private ProjectProgress() { }
+
+        public static ProjectProgress Calculate(Project project)
+        {
+            if (project?.Jobs == null || !project.Jobs.Any())
+                return new ProjectProgress();
+
+            var nonDeletedJobs = project.Jobs.Where(j => !j.IsDeleted);
+            var nonEpicJobs = nonDeletedJobs.Where(j => j.Level != Job.JobLevel.Epic);
+            var totalNonEpicJobs = nonEpicJobs.Count();
+
+            var progress = new ProjectProgress
+            {
+                // Only count non-epic jobs for task totals
+                TotalTasks = nonEpicJobs.Count(),
+                CompletedTasks = nonEpicJobs.Count(j => j.JobStatus == Job.Status.Done),
+                InProgressTasks = nonEpicJobs.Count(j => j.JobStatus == Job.Status.InProgress),
+                BacklogTasks = nonEpicJobs.Count(j => j.JobStatus == Job.Status.Open),
+                
+                // Epic counts are separate
+                EpicCount = nonDeletedJobs.Count(j => j.Level == Job.JobLevel.Epic),
+                CompletedEpics = nonDeletedJobs.Count(j => j.Level == Job.JobLevel.Epic && j.JobStatus == Job.Status.Done),
+                
+                // Specific job type counts
+                TaskCount = nonDeletedJobs.Count(j => j.Level == Job.JobLevel.Task),
+                BugCount = nonDeletedJobs.Count(j => j.Level == Job.JobLevel.Bug),
+                
+                // Calculate percentages based on non-epic jobs only
+                TotalTasksPercentage = totalNonEpicJobs > 0
+                    ? (decimal)nonEpicJobs.Count(j => j.JobStatus == Job.Status.Done) / totalNonEpicJobs * 100 
+                    : 0,
+                
+                CompletedTasksPercentage = totalNonEpicJobs > 0
+                    ? (decimal)nonEpicJobs.Count(j => j.JobStatus == Job.Status.Done) / totalNonEpicJobs * 100
+                    : 0,
+                    
+                InProgressPercentage = totalNonEpicJobs > 0
+                    ? (decimal)nonEpicJobs.Count(j => j.JobStatus == Job.Status.InProgress) / totalNonEpicJobs * 100
+                    : 0,
+
+                DueDate = nonDeletedJobs.Max(j => (DateTime?)j.DueDate) ?? null
+            };
+
+            return progress;
+        }
path: aspnet-core/src/toyiyo.todo.Core/Projects/ProjectProgress.cs
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**Add unit tests for this calculation logic**

The `Calculate` method contains important business logic for computing project progress. Ensure this is covered by comprehensive unit tests to verify correct behavior for:
- Projects with various combinations of job types and statuses
- Edge cases like empty projects or all jobs in a single status
- Percentage calculations, especially division by zero scenarios

---

🏁 Script executed:

```shell
#!/bin/bash
# Check if there are tests covering ProjectProgress class
echo "Searching for tests covering ProjectProgress class..."
fd -e cs "ProjectProgress" --full-path "*/test/*"
```

Length of output: 262

---



</details>

**Unit Tests for ProjectProgress Calculation Logic Needed**

The `Calculate` method in `aspnet-core/src/toyiyo.todo.Core/Projects/ProjectProgress.cs` contains core business logic that currently lacks dedicated unit tests. It’s crucial to add comprehensive tests to ensure the method behaves correctly across all scenarios, including:

- **Various Combinations**: Projects with a mix of job types (Task, Bug, Epic) and statuses (Done, InProgress, Open).
- **Edge Cases**: Handling empty projects or cases where all jobs share a single status.
- **Percentage Calculations**: Properly calculating percentages with safeguards against potential division by zero, particularly for non-epic jobs.

Please add these tests to maintain high reliability in project progress computations.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-20T13:44:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,69 @@
+using System.Linq;
+using System.Collections.Generic;
+using toyiyo.todo.Jobs;
+using System;
+
+namespace toyiyo.todo.Projects
+{
+    public class ProjectProgress
+    {
+        public int TotalTasks { get; private set; }
+        public int CompletedTasks { get; private set; }
+        public int InProgressTasks { get; private set; }
+        public int BacklogTasks { get; private set; }
+        public int EpicCount { get; private set; }
+        public int TaskCount { get; private set; }
+        public int BugCount { get; private set; }
+        public DateTime? DueDate { get; private set; }
+        public int CompletedEpics { get; private set; }
+        public decimal TotalTasksPercentage { get; private set; }
+        public decimal CompletedTasksPercentage { get; private set; }
+        public decimal InProgressPercentage { get; private set; }
+
+        private ProjectProgress() { }
+
+        public static ProjectProgress Calculate(Project project)
+        {
+            if (project?.Jobs == null || !project.Jobs.Any())
+                return new ProjectProgress();
+
+            var nonDeletedJobs = project.Jobs.Where(j => !j.IsDeleted);
+            var nonEpicJobs = nonDeletedJobs.Where(j => j.Level != Job.JobLevel.Epic);
+            var totalNonEpicJobs = nonEpicJobs.Count();
+
+            var progress = new ProjectProgress
+            {
+                // Only count non-epic jobs for task totals
+                TotalTasks = nonEpicJobs.Count(),
+                CompletedTasks = nonEpicJobs.Count(j => j.JobStatus == Job.Status.Done),
+                InProgressTasks = nonEpicJobs.Count(j => j.JobStatus == Job.Status.InProgress),
+                BacklogTasks = nonEpicJobs.Count(j => j.JobStatus == Job.Status.Open),
+                
+                // Epic counts are separate
+                EpicCount = nonDeletedJobs.Count(j => j.Level == Job.JobLevel.Epic),
+                CompletedEpics = nonDeletedJobs.Count(j => j.Level == Job.JobLevel.Epic && j.JobStatus == Job.Status.Done),
+                
+                // Specific job type counts
+                TaskCount = nonDeletedJobs.Count(j => j.Level == Job.JobLevel.Task),
+                BugCount = nonDeletedJobs.Count(j => j.Level == Job.JobLevel.Bug),
+                
+                // Calculate percentages based on non-epic jobs only
+                TotalTasksPercentage = totalNonEpicJobs > 0
+                    ? (decimal)nonEpicJobs.Count(j => j.JobStatus == Job.Status.Done) / totalNonEpicJobs * 100 
+                    : 0,
+                
+                CompletedTasksPercentage = totalNonEpicJobs > 0
+                    ? (decimal)nonEpicJobs.Count(j => j.JobStatus == Job.Status.Done) / totalNonEpicJobs * 100
+                    : 0,
+                    
+                InProgressPercentage = totalNonEpicJobs > 0
+                    ? (decimal)nonEpicJobs.Count(j => j.JobStatus == Job.Status.InProgress) / totalNonEpicJobs * 100
+                    : 0,
path: aspnet-core/src/toyiyo.todo.Core/Projects/ProjectProgress.cs
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Redundant percentage calculations**

`TotalTasksPercentage` and `CompletedTasksPercentage` appear to be calculating the same exact value. Both are computing the percentage of completed tasks relative to total non-epic jobs.


```diff
 // Calculate percentages based on non-epic jobs only
-TotalTasksPercentage = totalNonEpicJobs > 0
-    ? (decimal)nonEpicJobs.Count(j => j.JobStatus == Job.Status.Done) / totalNonEpicJobs * 100 
-    : 0,
+// Either rename TotalTasksPercentage to reflect what it actually measures or update its calculation
+TotalTasksPercentage = 100, // If this should be 100% always
```

Alternatively, if `TotalTasksPercentage` is intended to measure something different from `CompletedTasksPercentage`, please update its calculation to reflect its intended purpose.


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
                // Calculate percentages based on non-epic jobs only
                // Either rename TotalTasksPercentage to reflect what it actually measures or update its calculation
                TotalTasksPercentage = 100, // If this should be 100% always
                
                CompletedTasksPercentage = totalNonEpicJobs > 0
                    ? (decimal)nonEpicJobs.Count(j => j.JobStatus == Job.Status.Done) / totalNonEpicJobs * 100
                    : 0,
                    
                InProgressPercentage = totalNonEpicJobs > 0
                    ? (decimal)nonEpicJobs.Count(j => j.JobStatus == Job.Status.InProgress) / totalNonEpicJobs * 100
                    : 0,
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-20T13:44:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -33,34 +33,103 @@
         columnDefs: [
             {
                 targets: 0,
-                data: 'title',
-                className: 'title',
-                defaultContent: '',
-                fnCreatedCell: function (nTd, sData, oData, iRow, iCol) {
-                    $(nTd).html('<a href="' + 'Projects/' + oData.id + '/jobs' + '">' + sData + '</a>');
-                }
-            },
-            {
-                targets: 1,
                 data: null,
-                sortable: false,
-                autoWidth: false,
-                width: '5em',
-                defaultContent: '',
-                render: (data, type, row, meta) => {
-                    return [
-                        `   <button type="button" class="btn btn-sm bg-secondary edit-project" data-project-id="${row.id}" data-toggle="modal" data-target="#ProjectEditModal">`,
-                        `       <i class="fas fa-pencil-alt" title=${l('Edit')}></i>`,
-                        '   </button>',
-                        `   <button type="button" class="btn btn-sm bg-danger delete-project" data-project-id="${row.id}" data-toggle="modal" data-target="#ProjectDeleteModal">`,
-                        `       <i class="fas fa-trash-alt" title=${l('Delete')}></i>`,
-                        '   </button>',
-                    ].join('');
+                className: 'project-card',
+                render: function(data, type, row) {
+                    const progress = row.progress;
+                    const total = progress.totalJobCount;
+                    const completed = progress.completedTasks;
+                    const actions = `
+                        <div class="dropdown">
+                            <button class="btn btn-sm btn-light dropdown-toggle ml-2" type="button" data-toggle="dropdown" aria-expanded="false">
+                                <i class="fas fa-ellipsis-v"></i>
+                            </button>
+                            <div class="dropdown-menu dropdown-menu-right">
+                                <button type="button" class="dropdown-item edit-project" data-project-id="${row.id}" data-toggle="modal" data-target="#ProjectEditModal">
+                                    <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-pencil mr-2" viewBox="0 0 16 16">
+                                        <path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/>
+                                    </svg>
+                                    ${l('Edit')}
+                                </button>
+                                <button type="button" class="dropdown-item delete-project" data-project-id="${row.id}" data-toggle="modal" data-target="#ProjectDeleteModal">
+                                    <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-trash mr-2" viewBox="0 0 16 16">
+                                        <path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/>
+                                        <path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"/>
+                                    </svg>
+                                    ${l('Delete')}
+                                </button>
+                            </div>
+                        </div>
+                    `;
+
path: aspnet-core/src/toyiyo.todo.Web.Mvc/wwwroot/view-resources/Views/Projects/Index.js
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**Check for potential XSS vulnerabilities in the actions template.**

The project data is directly inserted into the HTML without sanitization, which could lead to XSS attacks if the data contains malicious content.


Consider using a library like DOMPurify to sanitize the data before inserting it into the HTML:

---

🏁 Script executed:

```shell
#!/bin/bash
# Check if the project uses any sanitization libraries
echo "Checking for sanitization libraries..."
grep -r "sanitize\|purify\|escape" --include="*.js" --include="*.cshtml" aspnet-core/ || echo "No sanitization libraries found"

# Check for other instances of direct HTML injection
echo "Checking for other instances of direct HTML injection..."
grep -r "\.html(" --include="*.js" aspnet-core/ || echo "No direct HTML injection found"
```

Length of output: 4775

---



</details>

**Security Concern: Sanitize Dynamic HTML in Actions Template**

The verification confirms that while sanitization utilities like DOMPurify and escape functions are available and used in other parts of the project (e.g., in Jobs views), the actions template in `aspnet-core/src/toyiyo.todo.Web.Mvc/wwwroot/view-resources/Views/Projects/Index.js` (lines 39–64) directly inserts dynamic data (such as `row.id`) without sanitization. This practice could lead to potential XSS vulnerabilities if malicious content were somehow injected.

**Recommendations:**
- **Sanitize Data:** Utilize a sanitization method (e.g., `DOMPurify.sanitize()`) for any dynamic HTML content before insertion.
- **Consistency:** Follow the same data-sanitizing approach observed in other modules like `aspnet-core/src/toyiyo.todo.Web.Mvc/wwwroot/view-resources/Views/Jobs/_EditModal.js`.
- **Review Translations:** Ensure that strings returned by the `l()` function are safe or appropriately escaped.

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commit 7fed8ff
created_at: 2025-03-22T22:21:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,261 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Abp.Domain.Uow;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+using Shouldly;
+using toyiyo.todo.Authorization.Users;
+using toyiyo.todo.Jobs;
+using toyiyo.todo.Jobs.Dto;
+using toyiyo.todo.Projects;
+using toyiyo.todo.Projects.Dto;
+using toyiyo.todo.Sessions;
+using Xunit;
+using static toyiyo.todo.Jobs.Job;
+
+namespace toyiyo.todo.Tests.Projects
+{
+    public class ProjectProgressTests : todoTestBase
+    {
+        private readonly IProjectAppService _projectAppService;
+        private readonly IJobAppService _jobAppService;
+
+        public ProjectProgressTests()
+        {
+            _projectAppService = Resolve<IProjectAppService>();
+            _jobAppService = Resolve<IJobAppService>();
+            LoginAsDefaultTenantAdmin();
+        }
+
+        private async Task<Project> GetProjectEntityById(Guid id)
+        {
+            return await UsingDbContext(async context => 
+                await context.Projects
+                    .Include(p => p.Jobs)
+                    .FirstOrDefaultAsync(p => p.Id == id)
+            );
+        }
+
+        private async Task<Project> CreateProjectWithMixedJobs()
+        {
+            var project = await _projectAppService.Create(new CreateProjectInputDto { Title = "Test Project" });
+
+            // Create Epics
+            var epic1 = await _jobAppService.Create(new JobCreateInputDto 
+            { 
+                ProjectId = project.Id, 
+                Title = "Epic 1",
+                Level = JobLevel.Epic
+            });
+            await _jobAppService.SetJobStatus(new JobSetStatusInputDto { Id = epic1.Id, JobStatus = Status.Done });
+
+            var epic2 = await _jobAppService.Create(new JobCreateInputDto 
+            { 
+                ProjectId = project.Id, 
+                Title = "Epic 2",
+                Level = JobLevel.Epic
+            });
+            await _jobAppService.SetJobStatus(new JobSetStatusInputDto { Id = epic2.Id, JobStatus = Status.InProgress });
+
+            // Create Tasks with different statuses
+            for (int i = 0; i < 3; i++)
+            {
+                var task = await _jobAppService.Create(new JobCreateInputDto 
+                { 
+                    ProjectId = project.Id, 
+                    Title = $"Completed Task {i}",
+                    Level = JobLevel.Task
+                });
+                await _jobAppService.SetJobStatus(new JobSetStatusInputDto { Id = task.Id, JobStatus = Status.Done });
+            }
+
+            for (int i = 0; i < 2; i++)
+            {
+                var task = await _jobAppService.Create(new JobCreateInputDto 
+                { 
+                    ProjectId = project.Id, 
+                    Title = $"In Progress Task {i}",
+                    Level = JobLevel.Task
+                });
+                await _jobAppService.SetJobStatus(new JobSetStatusInputDto { Id = task.Id, JobStatus = Status.InProgress });
+            }
+
+            // Create Bugs
+            for (int i = 0; i < 3; i++)
+            {
+                await _jobAppService.Create(new JobCreateInputDto 
+                { 
+                    ProjectId = project.Id, 
+                    Title = $"Bug {i}",
+                    Level = JobLevel.Bug
+                });
+            }
+
+            return await GetProjectEntityById(project.Id);
+        }
+
+        private async Task<Project> CreateProjectWithDeletedJobs()
+        {
+            var project = await _projectAppService.Create(new CreateProjectInputDto { Title = "Test Project" });
+
+            // Create jobs with different statuses
+            var job1 = await _jobAppService.Create(new JobCreateInputDto 
+            { 
+                ProjectId = project.Id, 
+                Title = "Task 1"
+            });
+            await _jobAppService.SetJobStatus(new JobSetStatusInputDto { Id = job1.Id, JobStatus = Status.Done });
+
+            var job2 = await _jobAppService.Create(new JobCreateInputDto 
+            { 
+                ProjectId = project.Id, 
+                Title = "Task 2"
+            });
+            await _jobAppService.SetJobStatus(new JobSetStatusInputDto { Id = job2.Id, JobStatus = Status.InProgress });
+
+            var job3 = await _jobAppService.Create(new JobCreateInputDto 
+            { 
+                ProjectId = project.Id, 
+                Title = "Task 3"
+            });
+
+            var jobToDelete = await _jobAppService.Create(new JobCreateInputDto 
+            { 
+                ProjectId = project.Id, 
+                Title = "To Delete"
+            });
+
+            await _jobAppService.Delete(jobToDelete.Id);
+            return await GetProjectEntityById(project.Id);
+        }
+
+        private async Task<Project> CreateProjectWithDueDates()
+        {
+            var project = await _projectAppService.Create(new CreateProjectInputDto { Title = "Test Project" });
+
+            // Create jobs with different due dates
+            await _jobAppService.Create(new JobCreateInputDto 
+            { 
+                ProjectId = project.Id, 
+                Title = "Task 1",
+                DueDate = DateTime.UtcNow.AddDays(10)
+            });
+
+            await _jobAppService.Create(new JobCreateInputDto 
+            { 
+                ProjectId = project.Id, 
+                Title = "Task 2",
+                DueDate = DateTime.UtcNow.AddDays(20)
+            });
+
+            await _jobAppService.Create(new JobCreateInputDto 
+            { 
+                ProjectId = project.Id, 
+                Title = "Task 3",
+                DueDate = DateTime.UtcNow.AddDays(30)
+            });
+
+            return await GetProjectEntityById(project.Id);
+        }
+
+        private async Task<Project> CreateProjectWithAllTasksCompleted()
+        {
+            var project = await _projectAppService.Create(new CreateProjectInputDto { Title = "Test Project" });
+
+            // Create multiple tasks and mark them as completed
+            for (int i = 0; i < 5; i++)
+            {
+                var task = await _jobAppService.Create(new JobCreateInputDto 
+                { 
+                    ProjectId = project.Id, 
+                    Title = $"Completed Task {i}"
+                });
+                await _jobAppService.SetJobStatus(new JobSetStatusInputDto { Id = task.Id, JobStatus = Job.Status.Done });
+            }
+
+            return await GetProjectEntityById(project.Id);
+        }
+
+        [Fact]
+        public async Task Should_Calculate_Project_Progress_With_Mixed_Jobs()
+        {
+            var project = await CreateProjectWithMixedJobs();
+            var progress = ProjectProgress.Calculate(project);
+
+            progress.TotalJobCount.ShouldBe(8); // 5 tasks + 3 bugs
+            progress.CompletedTasks.ShouldBe(3);
+            progress.InProgressTasks.ShouldBe(2);
+            progress.BacklogTasks.ShouldBe(3);
+            progress.EpicCount.ShouldBe(2);
+            progress.CompletedEpics.ShouldBe(1);
+            progress.TaskCount.ShouldBe(5);
+            progress.BugCount.ShouldBe(3);
+            progress.TotalTasksPercentage.ShouldBe(60);
+            progress.InProgressPercentage.ShouldBe(40);
+            progress.DueDate.ShouldNotBeNull();
+            progress.HealthStatus.ShouldNotBeNull();
+        }
+
+        [Fact]
+        public async Task Should_Calculate_Project_Progress_With_Deleted_Jobs()
+        {
+            var project = await CreateProjectWithDeletedJobs();
+            var progress = ProjectProgress.Calculate(project);
+
+            progress.TotalJobCount.ShouldBe(3);
+            progress.CompletedTasks.ShouldBe(1);
+            progress.InProgressTasks.ShouldBe(1);
+            progress.BacklogTasks.ShouldBe(1);
+            progress.EpicCount.ShouldBe(0);
+            progress.CompletedEpics.ShouldBe(0);
+            progress.TaskCount.ShouldBe(3);
+            progress.BugCount.ShouldBe(0);
+            progress.TotalTasksPercentage.ShouldBe(33.33m);
+            progress.InProgressPercentage.ShouldBe(33.33m);
+            progress.DueDate.ShouldNotBeNull();
+            progress.HealthStatus.ShouldNotBeNull();
+        }
+
+        [Fact]
+        public async Task Should_Calculate_Project_Progress_With_Due_Dates()
+        {
+            var project = await CreateProjectWithDueDates();
+            var progress = ProjectProgress.Calculate(project);
+
+            progress.TotalJobCount.ShouldBe(3);
+            progress.CompletedTasks.ShouldBe(0);
+            progress.InProgressTasks.ShouldBe(0);
+            progress.BacklogTasks.ShouldBe(3);
+            progress.EpicCount.ShouldBe(0);
+            progress.CompletedEpics.ShouldBe(0);
+            progress.TaskCount.ShouldBe(3);
+            progress.BugCount.ShouldBe(0);
+            progress.TotalTasksPercentage.ShouldBe(0);
+            progress.InProgressPercentage.ShouldBe(0);
+            progress.DueDate.ShouldNotBeNull();
+            progress.HealthStatus.ShouldNotBeNull();
+        }
+
+        [Fact]
+        public async Task Should_Calculate_Project_Progress_With_All_Tasks_Completed()
+        {
+            var project = await CreateProjectWithAllTasksCompleted();
+            var progress = ProjectProgress.Calculate(project);
+
+            progress.TotalJobCount.ShouldBe(5);
+            progress.CompletedTasks.ShouldBe(5);
+            progress.InProgressTasks.ShouldBe(0);
+            progress.BacklogTasks.ShouldBe(0);
+            progress.EpicCount.ShouldBe(0);
+            progress.CompletedEpics.ShouldBe(0);
+            progress.TaskCount.ShouldBe(5);
+            progress.BugCount.ShouldBe(0);
+            progress.TotalTasksPercentage.ShouldBe(100);
+            progress.InProgressPercentage.ShouldBe(0);
+            progress.DueDate.ShouldNotBeNull();
+            progress.HealthStatus.ShouldNotBeNull();
+        }
+    }
path: aspnet-core/test/toyiyo.todo.Tests/Projects/ProjectProgressTests.cs
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Add test for edge case with empty project.**

The tests cover various scenarios, but there's no test for a project with no jobs. This is an important edge case to verify that the progress calculation handles division by zero properly.


Consider adding a test like this:

```csharp
[Fact]
public async Task Should_Handle_Project_With_No_Jobs()
{
    var project = await _projectAppService.Create(new CreateProjectInputDto { Title = "Empty Project" });
    project = await GetProjectEntityById(project.Id);
    
    var progress = ProjectProgress.Calculate(project);
    
    progress.TotalJobCount.ShouldBe(0);
    progress.CompletedTasks.ShouldBe(0);
    progress.InProgressTasks.ShouldBe(0);
    progress.BacklogTasks.ShouldBe(0);
    progress.EpicCount.ShouldBe(0);
    progress.CompletedEpics.ShouldBe(0);
    progress.TaskCount.ShouldBe(0);
    progress.BugCount.ShouldBe(0);
    progress.TotalTasksPercentage.ShouldBe(0);
    progress.InProgressPercentage.ShouldBe(0);
    progress.DueDate.ShouldNotBeNull();
    progress.HealthStatus.ShouldNotBeNull();
}
```

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-22T22:29:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -33,34 +33,103 @@
         columnDefs: [
             {
                 targets: 0,
-                data: 'title',
-                className: 'title',
-                defaultContent: '',
-                fnCreatedCell: function (nTd, sData, oData, iRow, iCol) {
-                    $(nTd).html('<a href="' + 'Projects/' + oData.id + '/jobs' + '">' + sData + '</a>');
-                }
-            },
-            {
-                targets: 1,
                 data: null,
-                sortable: false,
-                autoWidth: false,
-                width: '5em',
-                defaultContent: '',
-                render: (data, type, row, meta) => {
-                    return [
-                        `   <button type="button" class="btn btn-sm bg-secondary edit-project" data-project-id="${row.id}" data-toggle="modal" data-target="#ProjectEditModal">`,
-                        `       <i class="fas fa-pencil-alt" title=${l('Edit')}></i>`,
-                        '   </button>',
-                        `   <button type="button" class="btn btn-sm bg-danger delete-project" data-project-id="${row.id}" data-toggle="modal" data-target="#ProjectDeleteModal">`,
-                        `       <i class="fas fa-trash-alt" title=${l('Delete')}></i>`,
-                        '   </button>',
-                    ].join('');
+                className: 'project-card',
+                render: function(data, type, row) {
+                    const progress = row.progress;
+                    const total = progress.totalJobCount;
+                    const completed = progress.completedTasks;
+                    const actions = DOMPurify.sanitize(`
+                        <div class="dropdown">
+                            <button class="btn btn-sm btn-light dropdown-toggle ml-2" type="button" data-toggle="dropdown" aria-expanded="false">
+                                <i class="fas fa-ellipsis-v"></i>
+                            </button>
+                            <div class="dropdown-menu dropdown-menu-right">
+                                <button type="button" class="dropdown-item edit-project" data-project-id="${row.id}" data-toggle="modal" data-target="#ProjectEditModal">
+                                    <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-pencil mr-2" viewBox="0 0 16 16">
+                                        <path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/>
+                                    </svg>
+                                    ${l('Edit')}
+                                </button>
+                                <button type="button" class="dropdown-item delete-project" data-project-id="${row.id}" data-toggle="modal" data-target="#ProjectDeleteModal">
+                                    <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-trash mr-2" viewBox="0 0 16 16">
+                                        <path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/>
+                                        <path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"/>
+                                    </svg>
+                                    ${l('Delete')}
+                                </button>
+                            </div>
+                        </div>
+                    `);
+
+                    // Generate card content regardless of task count
+                    return `
+                        <div class="card">
+                            <div class="card-body">
+                                <div class="d-flex justify-content-between align-items-center">
+                                    <h5 class="card-title mb-0">
+                                        <a class="project-title" href="/Projects/${DOMPurify.sanitize(row.id)}/jobs">${DOMPurify.sanitize(row.title)}</a>
+                                    </h5>
+                                    <div>
+                                        <span class="badge badge-pill ${progress.statusClass}">${progress.status}</span>
+                                        <span class="ml-2">${actions}</span>
+                                    </div>
+                                </div>
+                                <div class="text-muted mt-3">
+                                    <div class="project-stats">
+                                        <span class="stat-item">
+                                            <i class="fas fa-layer-group mr-2"></i>
+                                            ${progress.epicCount} Epics (${progress.completedEpics} done)
+                                        </span>
+                                        <span class="stat-item">
+                                            <i class="fas fa-tasks mr-2"></i>
+                                            ${progress.taskCount} Tasks 
+                                        </span>
+                                        <span class="stat-item">
+                                            <i class="fas fa-bug mr-2"></i>
+                                            ${progress.bugCount} Bugs
+                                        </span>
+                                        ${progress.dueDate && !moment(progress.dueDate).isSame('0001-01-01T00:00:00Z') ? `
+                                            <span class="stat-item">
+                                                <i class="fas fa-calendar mr-2"></i>
+                                                Due: ${moment(progress.dueDate).format('M/D/YYYY')}
+                                            </span>
+                                        ` : `
+                                            <span class="stat-item text-muted">
+                                                <i class="fas fa-calendar mr-2"></i>
+                                                No due date set
+                                            </span>
+                                        `}
path: aspnet-core/src/toyiyo.todo.Web.Mvc/wwwroot/view-resources/Views/Projects/Index.js
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Improve date validation logic.**

The current date validation checks for a specific date string (`0001-01-01T00:00:00Z`) which is implementation-specific and could break if the date format changes.


Use a more robust method to check for valid dates:

```diff
-${progress.dueDate && !moment(progress.dueDate).isSame('0001-01-01T00:00:00Z') ? `
+${progress.dueDate && moment(progress.dueDate).isValid() && !moment(progress.dueDate).year() === 1 ? `
```

Or better yet, consider handling this validation on the server side before sending the data to the client.


> Committable suggestion skipped: line range outside the PR's diff.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-22T22:49:54Z
author_association: CONTRIBUTOR

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-03-20T02:55:06Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough
The update integrates a comprehensive project progress feature into the application. It introduces new DTOs and classes to encapsulate progress information and extends existing classes with progress properties and job associations. The service layer is enhanced with methods to calculate and retrieve progress across multiple projects. Additionally, the UI has been updated to render project data with an improved card layout, including progress bars and detailed statistics, supported by new CSS styles and modified JavaScript logic.

## Changes

| Files | Change Summary |
| --- | --- |
| `aspnet-core/.../Projects/Dto/ProjectDto.cs`<br>`aspnet-core/.../Projects/Dto/ProjectProgressDto.cs` | Added a new `Progress` property to `ProjectDto` and introduced `ProjectProgressDto` for carrying detailed progress statistics. |
| `aspnet-core/.../Projects/IProjectManager.cs`<br>`aspnet-core/.../Projects/ProjectManager.cs`<br>`aspnet-core/.../Projects/Project.cs`<br>`aspnet-core/.../Projects/ProjectProgress.cs` | Introduced new methods (`GetProjectProgress`, `GetProjectsProgress`) in the project management layer, added a virtual `Jobs` property in `Project`, and created `ProjectProgress` with a calculation method for progress data. |
| `aspnet-core/.../Views/Projects/Index.cshtml`<br>`aspnet-core/.../Views/Projects/_ProgressBar.cshtml` | Modified the projects table header in `Index.cshtml` and added a new Razor view to render a progress bar component for displaying project progress. |
| `aspnet-core/.../wwwroot/css/style.css`<br>`aspnet-core/.../view-resources/Views/Projects/Index.js` | Updated JavaScript to render project entries in a card layout with dynamic progress bars and tooltips; added new CSS styles for project titles, statistics, and badges. |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant Client
    participant AppService as ProjectAppService
    participant Manager as ProjectManager
    participant Domain as Project Domain
    Client->>AppService: Request all projects
    AppService->>Manager: GetProjectsProgress(projectIDs)
    Manager->>Domain: Retrieve projects with Jobs
    Domain-->>Manager: Return project data
    Manager->>Domain: Calculate progress for each project
    Domain-->>Manager: Progress data computed
    Manager-->>AppService: Return mapping of project IDs to progress
    AppService->>Client: Deliver ProjectDto list with Progress
```

```mermaid
sequenceDiagram
    participant DT as DataTable Plugin
    participant JS as Index.js
    participant Razor as _ProgressBar.cshtml
    DT->>JS: Trigger rendering of project card
    JS->>Razor: Render progress bar component with DTO data
    Razor-->>JS: Return constructed progress bar HTML
    JS-->>DT: Display refined project card with statistics and progress visualization
```

## Possibly related PRs
- **toyiyo/todo#109**: The changes in the main PR, which involve adding a `Progress` property of type `ProjectProgressDto` to the `ProjectDto` class, are related to the retrieved PR as both involve the introduction of new data transfer objects (DTOs) that enhance project management capabilities, specifically through the `ProjectProgressDto` and its integration into the project structure.

## Poem
> I'm a rabbit with ears so keen,  
> Hopping through code in a field serene,  
> New progress bars bloom like carrots so bright,  
> With every project, my heart takes flight.  
> Bugs and tasks, now clear as day—  
> In this code garden, I joyfully play!  
> 🥕🐇 Happy coding on this sunny day!

<!-- walkthrough_end -->
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details>
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---



<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=toyiyo/todo&utm_content=121):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR
user: login: sonarqubecloud[bot]
created_at: 2025-03-22T23:00:09Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=toyiyo_todo&pullRequest=121) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [18 New issues](https://sonarcloud.io/project/issues?id=toyiyo_todo&pullRequest=121&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=toyiyo_todo&pullRequest=121&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=toyiyo_todo&pullRequest=121&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [84.7% Coverage on New Code](https://sonarcloud.io/component_measures?id=toyiyo_todo&pullRequest=121&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=toyiyo_todo&pullRequest=121&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=toyiyo_todo&pullRequest=121)


author_association: NONE