User commited on
Commit
4671199
·
1 Parent(s): 92025cf

fix: reports

Browse files
Files changed (23) hide show
  1. .nuget-scratch/NuGet.Config +7 -0
  2. TaskTrackingSystem.Database/AppDbContextModels/AppDbContext.cs +4 -0
  3. TaskTrackingSystem.Database/AppDbContextModels/Issue.cs +8 -0
  4. TaskTrackingSystem.Shared/Models/Issue/CreateIssueDto.cs +11 -0
  5. TaskTrackingSystem.Shared/Models/Issue/IssueDto.cs +4 -0
  6. TaskTrackingSystem.Shared/Models/Issue/UpdateIssueDto.cs +11 -0
  7. TaskTrackingSystem.Shared/Models/Report/EmployeeProductivityReportDto.cs +8 -0
  8. TaskTrackingSystem.Shared/Models/Report/IssueReportSummaryDto.cs +13 -0
  9. TaskTrackingSystem.Shared/Models/Report/OverdueCriticalTaskDto.cs +5 -0
  10. TaskTrackingSystem.Shared/Models/Report/ProjectProgressReportDto.cs +7 -0
  11. TaskTrackingSystem.Shared/Models/Report/TaskReportDto.cs +5 -0
  12. TaskTrackingSystem.WebApi/Features/Issue/IssueService.cs +12 -0
  13. TaskTrackingSystem.WebApi/Features/Report/ReportController.cs +1 -1
  14. TaskTrackingSystem.WebApi/Features/Report/ReportService.cs +187 -11
  15. TaskTrackingSystem.WebApi/Program.cs +83 -0
  16. TaskTrackingSystem.WebApp/Components/Pages/Features/Issues/AddIssue.razor +42 -0
  17. TaskTrackingSystem.WebApp/Components/Pages/Features/Issues/Issues.razor +79 -11
  18. TaskTrackingSystem.WebApp/Components/Pages/Features/Reports/EmployeeReport.razor +50 -47
  19. TaskTrackingSystem.WebApp/Components/Pages/Features/Reports/IssueReport.razor +325 -0
  20. TaskTrackingSystem.WebApp/Components/Pages/Features/Reports/OverdueTasksReport.razor +18 -23
  21. TaskTrackingSystem.WebApp/Components/Pages/Features/Reports/ProjectProgressReport.razor +32 -26
  22. TaskTrackingSystem.WebApp/Components/Pages/Features/Reports/TaskReport.razor +115 -5
  23. TaskTrackingSystem.WebApp/Components/Pages/Features/Reports/TimeTrackingReport.razor +62 -15
.nuget-scratch/NuGet.Config ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <configuration>
3
+ <packageSources>
4
+ <clear />
5
+ <add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
6
+ </packageSources>
7
+ </configuration>
TaskTrackingSystem.Database/AppDbContextModels/AppDbContext.cs CHANGED
@@ -178,7 +178,11 @@ public partial class AppDbContext : DbContext
178
  .HasDefaultValueSql("(getdate())")
179
  .HasColumnType("datetime");
180
  entity.Property(e => e.Description).HasMaxLength(1000);
 
 
181
  entity.Property(e => e.DueDate).HasColumnType("datetime");
 
 
182
  entity.Property(e => e.IsDeleted).HasDefaultValue(false);
183
  entity.Property(e => e.PriorityId).HasDefaultValue(TaskPriority.Medium);
184
  entity.Property(e => e.StartDate).HasColumnType("datetime");
 
178
  .HasDefaultValueSql("(getdate())")
179
  .HasColumnType("datetime");
180
  entity.Property(e => e.Description).HasMaxLength(1000);
181
+ entity.Property(e => e.DelayReason).HasMaxLength(300);
182
+ entity.Property(e => e.BlockedBy).HasMaxLength(200);
183
  entity.Property(e => e.DueDate).HasColumnType("datetime");
184
+ entity.Property(e => e.EscalationLevel).HasDefaultValue(0);
185
+ entity.Property(e => e.IsBlocked).HasDefaultValue(false);
186
  entity.Property(e => e.IsDeleted).HasDefaultValue(false);
187
  entity.Property(e => e.PriorityId).HasDefaultValue(TaskPriority.Medium);
188
  entity.Property(e => e.StartDate).HasColumnType("datetime");
TaskTrackingSystem.Database/AppDbContextModels/Issue.cs CHANGED
@@ -20,6 +20,14 @@ public partial class Issue
20
 
21
  public decimal? ActualHours { get; set; }
22
 
 
 
 
 
 
 
 
 
23
  public DateTime StartDate { get; set; }
24
 
25
  public DateTime DueDate { get; set; }
 
20
 
21
  public decimal? ActualHours { get; set; }
22
 
23
+ public string? DelayReason { get; set; }
24
+
25
+ public bool IsBlocked { get; set; }
26
+
27
+ public string? BlockedBy { get; set; }
28
+
29
+ public int EscalationLevel { get; set; }
30
+
31
  public DateTime StartDate { get; set; }
32
 
33
  public DateTime DueDate { get; set; }
TaskTrackingSystem.Shared/Models/Issue/CreateIssueDto.cs CHANGED
@@ -27,6 +27,17 @@ namespace TaskTrackingSystem.Shared.Models.Issue
27
  [Range(typeof(decimal), "0", "100000")]
28
  public decimal? ActualHours { get; set; }
29
 
 
 
 
 
 
 
 
 
 
 
 
30
  [Required]
31
  public DateTime StartDate { get; set; }
32
 
 
27
  [Range(typeof(decimal), "0", "100000")]
28
  public decimal? ActualHours { get; set; }
29
 
30
+ [MaxLength(300)]
31
+ public string? DelayReason { get; set; }
32
+
33
+ public bool IsBlocked { get; set; }
34
+
35
+ [MaxLength(200)]
36
+ public string? BlockedBy { get; set; }
37
+
38
+ [Range(0, 3)]
39
+ public int EscalationLevel { get; set; }
40
+
41
  [Required]
42
  public DateTime StartDate { get; set; }
43
 
TaskTrackingSystem.Shared/Models/Issue/IssueDto.cs CHANGED
@@ -16,6 +16,10 @@ namespace TaskTrackingSystem.Shared.Models.Issue
16
  public string? AssignedToName { get; set; }
17
  public decimal? EstimatedHours { get; set; }
18
  public decimal? ActualHours { get; set; }
 
 
 
 
19
  public DateTime StartDate { get; set; }
20
  public DateTime DueDate { get; set; }
21
  public AppTaskStatus StatusId { get; set; }
 
16
  public string? AssignedToName { get; set; }
17
  public decimal? EstimatedHours { get; set; }
18
  public decimal? ActualHours { get; set; }
19
+ public string? DelayReason { get; set; }
20
+ public bool IsBlocked { get; set; }
21
+ public string? BlockedBy { get; set; }
22
+ public int EscalationLevel { get; set; }
23
  public DateTime StartDate { get; set; }
24
  public DateTime DueDate { get; set; }
25
  public AppTaskStatus StatusId { get; set; }
TaskTrackingSystem.Shared/Models/Issue/UpdateIssueDto.cs CHANGED
@@ -23,6 +23,17 @@ namespace TaskTrackingSystem.Shared.Models.Issue
23
  [Range(typeof(decimal), "0", "100000")]
24
  public decimal? ActualHours { get; set; }
25
 
 
 
 
 
 
 
 
 
 
 
 
26
  [Required]
27
  public DateTime StartDate { get; set; }
28
 
 
23
  [Range(typeof(decimal), "0", "100000")]
24
  public decimal? ActualHours { get; set; }
25
 
26
+ [MaxLength(300)]
27
+ public string? DelayReason { get; set; }
28
+
29
+ public bool IsBlocked { get; set; }
30
+
31
+ [MaxLength(200)]
32
+ public string? BlockedBy { get; set; }
33
+
34
+ [Range(0, 3)]
35
+ public int EscalationLevel { get; set; }
36
+
37
  [Required]
38
  public DateTime StartDate { get; set; }
39
 
TaskTrackingSystem.Shared/Models/Report/EmployeeProductivityReportDto.cs CHANGED
@@ -7,6 +7,14 @@ public class EmployeeProductivityReportDto
7
  public string Username { get; set; } = string.Empty;
8
  public int AssignedCount { get; set; }
9
  public int CompletedCount { get; set; }
 
 
 
 
 
 
 
 
10
  public double Efficiency { get; set; }
11
  public double OnTimeDeliveryRate { get; set; }
12
  }
 
7
  public string Username { get; set; } = string.Empty;
8
  public int AssignedCount { get; set; }
9
  public int CompletedCount { get; set; }
10
+ public int AssignedIssues { get; set; }
11
+ public int OpenIssues { get; set; }
12
+ public int OverdueIssues { get; set; }
13
+ public decimal ActualHours { get; set; }
14
+ public string WorkloadLevel { get; set; } = "Balanced";
15
+ public double AverageCompletionTimeDays { get; set; }
16
+ public string Trend { get; set; } = "Stable";
17
+ public double CompletionRate { get; set; }
18
  public double Efficiency { get; set; }
19
  public double OnTimeDeliveryRate { get; set; }
20
  }
TaskTrackingSystem.Shared/Models/Report/IssueReportSummaryDto.cs ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ namespace TaskTrackingSystem.Shared.Models.Report;
2
+
3
+ public class IssueReportSummaryDto
4
+ {
5
+ public int TotalIssues { get; set; }
6
+ public int OpenIssues { get; set; }
7
+ public int OverdueIssues { get; set; }
8
+ public int BlockedIssues { get; set; }
9
+ public decimal EstimatedHours { get; set; }
10
+ public decimal ActualHours { get; set; }
11
+ public decimal VarianceHours { get; set; }
12
+ public double UtilizationPercent { get; set; }
13
+ }
TaskTrackingSystem.Shared/Models/Report/OverdueCriticalTaskDto.cs CHANGED
@@ -15,5 +15,10 @@ namespace TaskTrackingSystem.Shared.Models.Report
15
  public DateTime DueDate { get; set; }
16
  public int DaysOverdue { get; set; }
17
  public DateTime CreatedAt { get; set; }
 
 
 
 
 
18
  }
19
  }
 
15
  public DateTime DueDate { get; set; }
16
  public int DaysOverdue { get; set; }
17
  public DateTime CreatedAt { get; set; }
18
+ public int OverdueIssues { get; set; }
19
+ public string DelayReason { get; set; } = string.Empty;
20
+ public string BlockedBy { get; set; } = string.Empty;
21
+ public int EscalationLevel { get; set; }
22
+ public string RecordType { get; set; } = "Task";
23
  }
24
  }
TaskTrackingSystem.Shared/Models/Report/ProjectProgressReportDto.cs CHANGED
@@ -8,7 +8,14 @@ public class ProjectProgressReportDto
8
  public DateTime EndDate { get; set; }
9
  public int TotalTasks { get; set; }
10
  public int CompletedTasks { get; set; }
 
 
 
11
  public double Progress { get; set; }
12
  public bool IsAhead { get; set; }
13
  public bool IsAtRisk { get; set; }
 
 
 
 
14
  }
 
8
  public DateTime EndDate { get; set; }
9
  public int TotalTasks { get; set; }
10
  public int CompletedTasks { get; set; }
11
+ public int OpenIssues { get; set; }
12
+ public int OverdueIssues { get; set; }
13
+ public int BlockedIssues { get; set; }
14
  public double Progress { get; set; }
15
  public bool IsAhead { get; set; }
16
  public bool IsAtRisk { get; set; }
17
+ public string ProjectHealth { get; set; } = "Healthy";
18
+ public DateTime LastActivity { get; set; }
19
+ public DateTime? NextMilestone { get; set; }
20
+ public decimal ActualHours { get; set; }
21
  }
TaskTrackingSystem.Shared/Models/Report/TaskReportDto.cs CHANGED
@@ -20,5 +20,10 @@ namespace TaskTrackingSystem.Shared.Models.Report
20
  public string? AssignedByUser { get; set; }
21
  public DateTime DueDate { get; set; }
22
  public DateTime CreatedAt { get; set; }
 
 
 
 
 
23
  }
24
  }
 
20
  public string? AssignedByUser { get; set; }
21
  public DateTime DueDate { get; set; }
22
  public DateTime CreatedAt { get; set; }
23
+ public DateTime LastActivity { get; set; }
24
+ public int OpenIssues { get; set; }
25
+ public int OverdueIssues { get; set; }
26
+ public string IssueSummary { get; set; } = string.Empty;
27
+ public string TaskHealth { get; set; } = "Healthy";
28
  }
29
  }
TaskTrackingSystem.WebApi/Features/Issue/IssueService.cs CHANGED
@@ -157,6 +157,10 @@ namespace TaskTrackingSystem.WebApi.Features.Issue
157
  AssignedTo = dto.AssignedTo,
158
  EstimatedHours = dto.EstimatedHours,
159
  ActualHours = dto.ActualHours,
 
 
 
 
160
  StartDate = dto.StartDate,
161
  DueDate = dto.DueDate,
162
  StatusId = dto.StatusId == 0 ? AppTaskStatus.Todo : dto.StatusId,
@@ -206,6 +210,10 @@ namespace TaskTrackingSystem.WebApi.Features.Issue
206
  issue.AssignedTo = dto.AssignedTo;
207
  issue.EstimatedHours = dto.EstimatedHours;
208
  issue.ActualHours = dto.ActualHours;
 
 
 
 
209
  issue.StartDate = dto.StartDate;
210
  issue.DueDate = dto.DueDate;
211
  issue.StatusId = dto.StatusId;
@@ -307,6 +315,10 @@ namespace TaskTrackingSystem.WebApi.Features.Issue
307
  : null,
308
  EstimatedHours = i.EstimatedHours,
309
  ActualHours = i.ActualHours,
 
 
 
 
310
  StartDate = i.StartDate,
311
  DueDate = i.DueDate,
312
  StatusId = i.StatusId,
 
157
  AssignedTo = dto.AssignedTo,
158
  EstimatedHours = dto.EstimatedHours,
159
  ActualHours = dto.ActualHours,
160
+ DelayReason = dto.DelayReason,
161
+ IsBlocked = dto.IsBlocked,
162
+ BlockedBy = dto.BlockedBy,
163
+ EscalationLevel = dto.EscalationLevel,
164
  StartDate = dto.StartDate,
165
  DueDate = dto.DueDate,
166
  StatusId = dto.StatusId == 0 ? AppTaskStatus.Todo : dto.StatusId,
 
210
  issue.AssignedTo = dto.AssignedTo;
211
  issue.EstimatedHours = dto.EstimatedHours;
212
  issue.ActualHours = dto.ActualHours;
213
+ issue.DelayReason = dto.DelayReason;
214
+ issue.IsBlocked = dto.IsBlocked;
215
+ issue.BlockedBy = dto.BlockedBy;
216
+ issue.EscalationLevel = dto.EscalationLevel;
217
  issue.StartDate = dto.StartDate;
218
  issue.DueDate = dto.DueDate;
219
  issue.StatusId = dto.StatusId;
 
315
  : null,
316
  EstimatedHours = i.EstimatedHours,
317
  ActualHours = i.ActualHours,
318
+ DelayReason = i.DelayReason,
319
+ IsBlocked = i.IsBlocked,
320
+ BlockedBy = i.BlockedBy,
321
+ EscalationLevel = i.EscalationLevel,
322
  StartDate = i.StartDate,
323
  DueDate = i.DueDate,
324
  StatusId = i.StatusId,
TaskTrackingSystem.WebApi/Features/Report/ReportController.cs CHANGED
@@ -29,7 +29,7 @@ namespace TaskTrackingSystem.WebApi.Features.Report
29
  [ProducesResponseType(typeof(List<IssueDto>), StatusCodes.Status200OK)]
30
  public async Task<ActionResult<Result<List<IssueDto>>>> GetIssuesReport()
31
  {
32
- var result = await _reportService.GetIssuesReportAsync();
33
  return StatusCode(result.StatusCode, result);
34
  }
35
 
 
29
  [ProducesResponseType(typeof(List<IssueDto>), StatusCodes.Status200OK)]
30
  public async Task<ActionResult<Result<List<IssueDto>>>> GetIssuesReport()
31
  {
32
+ var result = await _reportService.GetIssuesReportAsync(User.GetRoleId(), User.GetUserId());
33
  return StatusCode(result.StatusCode, result);
34
  }
35
 
TaskTrackingSystem.WebApi/Features/Report/ReportService.cs CHANGED
@@ -25,14 +25,16 @@ namespace TaskTrackingSystem.WebApi.Features.Report
25
  _db = db;
26
  }
27
 
28
- public async Task<Result<List<IssueDto>>> GetIssuesReportAsync()
29
  {
30
- var list = await _db.Issues
31
- .Where(i => i.IsDeleted != true && i.Task.IsDeleted != true && i.Task.Project.IsDeleted != true)
 
32
  .Include(i => i.Task)
33
  .ThenInclude(t => t.Project)
34
  .Include(i => i.AssignedToNavigation)
35
- .OrderBy(i => i.Title)
 
36
  .Select(i => new IssueDto
37
  {
38
  Id = i.Id,
@@ -48,6 +50,10 @@ namespace TaskTrackingSystem.WebApi.Features.Report
48
  : null,
49
  EstimatedHours = i.EstimatedHours,
50
  ActualHours = i.ActualHours,
 
 
 
 
51
  StartDate = i.StartDate,
52
  DueDate = i.DueDate,
53
  StatusId = i.StatusId,
@@ -75,6 +81,29 @@ namespace TaskTrackingSystem.WebApi.Features.Report
75
  return _db.Users.Where(u => !u.IsDeleted);
76
  }
77
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
  private static PagedResult<TDestination> MapPagedResult<TSource, TDestination>(
79
  PagedResult<TSource> source,
80
  Func<TSource, TDestination> map)
@@ -187,10 +216,96 @@ namespace TaskTrackingSystem.WebApi.Features.Report
187
  AssignedToUser = t.AssignedToNavigation != null ? $"{t.AssignedToNavigation.FirstName} {t.AssignedToNavigation.LastName}" : null,
188
  AssignedByUser = t.AssignedByNavigation != null ? $"{t.AssignedByNavigation.FirstName} {t.AssignedByNavigation.LastName}" : null,
189
  DueDate = t.DueDate,
190
- CreatedAt = t.CreatedAt ?? DateTime.UtcNow
 
191
  };
192
  }
193
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
194
  // ——— Legacy endpoints (kept for backward compatibility) ———
195
 
196
  public async Task<Result<IEnumerable<TaskReportDto>>> GetTasksReportAsync(
@@ -205,6 +320,7 @@ namespace TaskTrackingSystem.WebApi.Features.Report
205
  .ThenByDescending(t => t.CreatedAt ?? DateTime.UtcNow)
206
  .ToListAsync();
207
  var list = tasks.Select(MapTaskReport).ToList();
 
208
  return Result<IEnumerable<TaskReportDto>>.Success(list);
209
  }
210
 
@@ -228,7 +344,9 @@ namespace TaskTrackingSystem.WebApi.Features.Report
228
  .ThenByDescending(t => t.CreatedAt ?? DateTime.UtcNow)
229
  .ToPagedResultAsync(page, pageSize);
230
 
231
- return MapPagedResult(paged, MapTaskReport);
 
 
232
  }
233
 
234
  public async Task<Result<IEnumerable<UserProductivityDto>>> GetUserProductivityReportAsync(long roleId, long currentUserId)
@@ -517,6 +635,7 @@ namespace TaskTrackingSystem.WebApi.Features.Report
517
  DaysOverdue = t.DueDate < now ? (int)(now - t.DueDate).TotalDays : 0,
518
  CreatedAt = t.CreatedAt ?? DateTime.UtcNow
519
  }).ToList();
 
520
 
521
  return Result<IEnumerable<OverdueCriticalTaskDto>>.Success(list);
522
  }
@@ -598,6 +717,7 @@ namespace TaskTrackingSystem.WebApi.Features.Report
598
  DaysOverdue = t.DueDate < now ? (int)(now - t.DueDate).TotalDays : 0,
599
  CreatedAt = t.CreatedAt ?? DateTime.UtcNow
600
  });
 
601
 
602
  return mapped;
603
  }
@@ -659,12 +779,21 @@ namespace TaskTrackingSystem.WebApi.Features.Report
659
  }
660
 
661
  var tasks = await BuildAccessibleTaskQuery(isAdmin, isManager, currentUserId).ToListAsync();
 
 
 
662
 
663
  if (startDate.HasValue)
 
664
  tasks = tasks.Where(t => t.CreatedAt >= startDate.Value.Date).ToList();
 
 
665
 
666
  if (endDate.HasValue)
 
667
  tasks = tasks.Where(t => t.CreatedAt < endDate.Value.Date.AddDays(1)).ToList();
 
 
668
 
669
  if (!string.IsNullOrWhiteSpace(status))
670
  {
@@ -672,11 +801,15 @@ namespace TaskTrackingSystem.WebApi.Features.Report
672
  if (sl == "overdue")
673
  {
674
  tasks = tasks.Where(t => t.StatusId != AppTaskStatus.Done && t.DueDate < DateTime.Today).ToList();
 
675
  }
676
  else if (sl != "0")
677
  {
678
  if (Enum.TryParse<AppTaskStatus>(status, true, out var parsedStatus))
 
679
  tasks = tasks.Where(t => t.StatusId == parsedStatus).ToList();
 
 
680
  }
681
  }
682
 
@@ -684,14 +817,25 @@ namespace TaskTrackingSystem.WebApi.Features.Report
684
  foreach (var user in users)
685
  {
686
  var userTasks = tasks.Where(t => t.AssignedTo == user.Id).ToList();
 
687
  int total = userTasks.Count;
688
  var completedTasks = userTasks.Where(t => t.StatusId == AppTaskStatus.Done).ToList();
689
  int done = completedTasks.Count;
 
690
 
691
  int onTimeCount = completedTasks.Count(t =>
692
  (t.UpdatedAt ?? t.CreatedAt) <= t.DueDate);
693
 
694
- double onTimeDeliveryRate = done > 0 ? Math.Round(((double)onTimeCount / done) * 100, 2) : 0;
 
 
 
 
 
 
 
 
 
695
 
696
  list.Add(new EmployeeProductivityReportDto
697
  {
@@ -700,7 +844,15 @@ namespace TaskTrackingSystem.WebApi.Features.Report
700
  FullName = $"{user.FirstName} {user.LastName}".Trim(),
701
  AssignedCount = total,
702
  CompletedCount = done,
703
- Efficiency = total > 0 ? Math.Round(((double)done / total) * 100, 2) : 0,
 
 
 
 
 
 
 
 
704
  OnTimeDeliveryRate = onTimeDeliveryRate
705
  });
706
  }
@@ -711,7 +863,7 @@ namespace TaskTrackingSystem.WebApi.Features.Report
711
  list = list.Where(u => u.FullName.ToLower().Contains(s) || u.Username.ToLower().Contains(s)).ToList();
712
  }
713
 
714
- return Result<IEnumerable<EmployeeProductivityReportDto>>.Success(list.OrderByDescending(u => u.Efficiency));
715
  }
716
 
717
  public async Task<PagedResult<EmployeeProductivityReportDto>> GetPagedEmployeeProductivityReportAsync(
@@ -732,7 +884,7 @@ namespace TaskTrackingSystem.WebApi.Features.Report
732
  return new PagedResult<EmployeeProductivityReportDto>();
733
  }
734
 
735
- var ordered = full.Value.OrderByDescending(u => u.Efficiency).ToList();
736
  var normalizedPageSize = PaginationExtensions.NormalizePageSize(pageSize);
737
  var totalCount = ordered.Count;
738
  var totalPages = totalCount == 0 ? 0 : (int)Math.Ceiling(totalCount / (double)normalizedPageSize);
@@ -781,6 +933,9 @@ namespace TaskTrackingSystem.WebApi.Features.Report
781
  }
782
 
783
  var tasks = await BuildAccessibleTaskQuery(isAdmin, isManager, currentUserId).ToListAsync();
 
 
 
784
  var today = DateTime.Today;
785
 
786
  if (startDate.HasValue)
@@ -793,16 +948,19 @@ namespace TaskTrackingSystem.WebApi.Features.Report
793
  foreach (var project in projects)
794
  {
795
  var projectTasks = tasks.Where(t => t.ProjectId == project.Id).ToList();
 
796
 
797
  if (!string.IsNullOrWhiteSpace(status))
798
  {
799
  if (status.Equals("overdue", StringComparison.OrdinalIgnoreCase))
800
  {
801
  projectTasks = projectTasks.Where(t => t.DueDate.Date < today && t.StatusId != AppTaskStatus.Done).ToList();
 
802
  }
803
  else if (status != "0" && Enum.TryParse<AppTaskStatus>(status, true, out var parsedStatus))
804
  {
805
  projectTasks = projectTasks.Where(t => t.StatusId == parsedStatus).ToList();
 
806
  }
807
  }
808
 
@@ -821,8 +979,19 @@ namespace TaskTrackingSystem.WebApi.Features.Report
821
  }
822
 
823
  bool isAhead = progress > elapsedPct && progress > 0 && progress < 100;
 
 
 
824
  bool isAtRisk = (projectTasks.Any(t => t.StatusId != AppTaskStatus.Done && t.DueDate.Date < today)) ||
 
 
825
  (project.EndDate.Date < today && progress < 100);
 
 
 
 
 
 
826
 
827
  list.Add(new ProjectProgressReportDto
828
  {
@@ -832,9 +1001,16 @@ namespace TaskTrackingSystem.WebApi.Features.Report
832
  EndDate = project.EndDate,
833
  TotalTasks = totalTasks,
834
  CompletedTasks = completedTasks,
 
 
 
835
  Progress = progress,
836
  IsAhead = isAhead,
837
- IsAtRisk = isAtRisk
 
 
 
 
838
  });
839
  }
840
 
 
25
  _db = db;
26
  }
27
 
28
+ public async Task<Result<List<IssueDto>>> GetIssuesReportAsync(long roleId, long currentUserId)
29
  {
30
+ var isAdmin = await DataScopeAuthorization.IsAdminScopeAsync(_db, roleId);
31
+ var isManager = await DataScopeAuthorization.IsManagerScopeAsync(_db, roleId);
32
+ var list = await BuildAccessibleIssueQuery(isAdmin, isManager, currentUserId)
33
  .Include(i => i.Task)
34
  .ThenInclude(t => t.Project)
35
  .Include(i => i.AssignedToNavigation)
36
+ .OrderBy(i => i.DueDate)
37
+ .ThenBy(i => i.Title)
38
  .Select(i => new IssueDto
39
  {
40
  Id = i.Id,
 
50
  : null,
51
  EstimatedHours = i.EstimatedHours,
52
  ActualHours = i.ActualHours,
53
+ DelayReason = i.DelayReason,
54
+ IsBlocked = i.IsBlocked,
55
+ BlockedBy = i.BlockedBy,
56
+ EscalationLevel = i.EscalationLevel,
57
  StartDate = i.StartDate,
58
  DueDate = i.DueDate,
59
  StatusId = i.StatusId,
 
81
  return _db.Users.Where(u => !u.IsDeleted);
82
  }
83
 
84
+ private IQueryable<TaskTrackingSystem.Database.AppDbContextModels.Issue> BuildAccessibleIssueQuery(bool isAdmin, bool isManager, long currentUserId)
85
+ {
86
+ var query = _db.Issues
87
+ .Where(i => i.IsDeleted != true && i.Task.IsDeleted != true && i.Task.Project.IsDeleted != true);
88
+
89
+ if (isAdmin)
90
+ {
91
+ return query;
92
+ }
93
+
94
+ if (isManager)
95
+ {
96
+ return query.Where(i =>
97
+ i.AssignedTo == currentUserId ||
98
+ i.CreatedBy == currentUserId ||
99
+ i.Task.Project.ProjectMembers.Any(pm => pm.UserId == currentUserId));
100
+ }
101
+
102
+ return query.Where(i =>
103
+ i.AssignedTo == currentUserId ||
104
+ i.CreatedBy == currentUserId);
105
+ }
106
+
107
  private static PagedResult<TDestination> MapPagedResult<TSource, TDestination>(
108
  PagedResult<TSource> source,
109
  Func<TSource, TDestination> map)
 
216
  AssignedToUser = t.AssignedToNavigation != null ? $"{t.AssignedToNavigation.FirstName} {t.AssignedToNavigation.LastName}" : null,
217
  AssignedByUser = t.AssignedByNavigation != null ? $"{t.AssignedByNavigation.FirstName} {t.AssignedByNavigation.LastName}" : null,
218
  DueDate = t.DueDate,
219
+ CreatedAt = t.CreatedAt ?? DateTime.UtcNow,
220
+ LastActivity = t.UpdatedAt ?? t.CreatedAt ?? DateTime.UtcNow
221
  };
222
  }
223
 
224
+ private async System.Threading.Tasks.Task EnrichTaskReportsAsync(List<TaskReportDto> reports)
225
+ {
226
+ if (reports.Count == 0)
227
+ {
228
+ return;
229
+ }
230
+
231
+ var today = DateTime.Today;
232
+ var taskIds = reports.Select(r => r.TaskId).ToList();
233
+ var issues = await _db.Issues
234
+ .Where(i => i.IsDeleted != true && taskIds.Contains(i.TaskId))
235
+ .Select(i => new
236
+ {
237
+ i.TaskId,
238
+ i.Title,
239
+ i.StatusId,
240
+ i.PriorityId,
241
+ i.DueDate,
242
+ i.UpdatedAt,
243
+ i.CreatedAt,
244
+ i.IsBlocked
245
+ })
246
+ .ToListAsync();
247
+
248
+ foreach (var report in reports)
249
+ {
250
+ var taskIssues = issues.Where(i => i.TaskId == report.TaskId).ToList();
251
+ report.OpenIssues = taskIssues.Count(i => i.StatusId != AppTaskStatus.Done);
252
+ report.OverdueIssues = taskIssues.Count(i => i.StatusId != AppTaskStatus.Done && i.DueDate.Date < today);
253
+ report.IssueSummary = taskIssues.Count == 0
254
+ ? "No issues"
255
+ : $"{report.OpenIssues} open, {report.OverdueIssues} overdue, {taskIssues.Count(i => i.IsBlocked)} blocked";
256
+ report.LastActivity = taskIssues
257
+ .Select(i => i.UpdatedAt ?? i.CreatedAt ?? report.LastActivity)
258
+ .DefaultIfEmpty(report.LastActivity)
259
+ .Max();
260
+
261
+ report.TaskHealth = report.StatusId == AppTaskStatus.Done
262
+ ? "Healthy"
263
+ : report.OverdueIssues > 0 || report.DueDate.Date < today || taskIssues.Any(i => i.IsBlocked)
264
+ ? "Critical"
265
+ : report.PriorityId == TaskPriority.High || report.DueDate.Date <= today.AddDays(3)
266
+ ? "At Risk"
267
+ : "Healthy";
268
+ }
269
+ }
270
+
271
+ private async System.Threading.Tasks.Task EnrichOverdueTaskReportsAsync(List<OverdueCriticalTaskDto> reports)
272
+ {
273
+ if (reports.Count == 0)
274
+ {
275
+ return;
276
+ }
277
+
278
+ var today = DateTime.Today;
279
+ var taskIds = reports.Select(r => r.TaskId).ToList();
280
+ var issues = await _db.Issues
281
+ .Where(i => i.IsDeleted != true && taskIds.Contains(i.TaskId))
282
+ .Select(i => new
283
+ {
284
+ i.TaskId,
285
+ i.StatusId,
286
+ i.DueDate,
287
+ i.DelayReason,
288
+ i.IsBlocked,
289
+ i.BlockedBy,
290
+ i.EscalationLevel
291
+ })
292
+ .ToListAsync();
293
+
294
+ foreach (var report in reports)
295
+ {
296
+ var taskIssues = issues.Where(i => i.TaskId == report.TaskId).ToList();
297
+ report.OverdueIssues = taskIssues.Count(i => i.StatusId != AppTaskStatus.Done && i.DueDate.Date < today);
298
+ var riskIssue = taskIssues
299
+ .OrderByDescending(i => i.IsBlocked)
300
+ .ThenByDescending(i => i.EscalationLevel)
301
+ .ThenBy(i => i.DueDate)
302
+ .FirstOrDefault();
303
+ report.DelayReason = riskIssue?.DelayReason ?? (report.DaysOverdue > 0 ? "Past due date" : "High priority due soon");
304
+ report.BlockedBy = riskIssue?.BlockedBy ?? string.Empty;
305
+ report.EscalationLevel = riskIssue?.EscalationLevel ?? 0;
306
+ }
307
+ }
308
+
309
  // ——— Legacy endpoints (kept for backward compatibility) ———
310
 
311
  public async Task<Result<IEnumerable<TaskReportDto>>> GetTasksReportAsync(
 
320
  .ThenByDescending(t => t.CreatedAt ?? DateTime.UtcNow)
321
  .ToListAsync();
322
  var list = tasks.Select(MapTaskReport).ToList();
323
+ await EnrichTaskReportsAsync(list);
324
  return Result<IEnumerable<TaskReportDto>>.Success(list);
325
  }
326
 
 
344
  .ThenByDescending(t => t.CreatedAt ?? DateTime.UtcNow)
345
  .ToPagedResultAsync(page, pageSize);
346
 
347
+ var mapped = MapPagedResult(paged, MapTaskReport);
348
+ await EnrichTaskReportsAsync(mapped.Items);
349
+ return mapped;
350
  }
351
 
352
  public async Task<Result<IEnumerable<UserProductivityDto>>> GetUserProductivityReportAsync(long roleId, long currentUserId)
 
635
  DaysOverdue = t.DueDate < now ? (int)(now - t.DueDate).TotalDays : 0,
636
  CreatedAt = t.CreatedAt ?? DateTime.UtcNow
637
  }).ToList();
638
+ await EnrichOverdueTaskReportsAsync(list);
639
 
640
  return Result<IEnumerable<OverdueCriticalTaskDto>>.Success(list);
641
  }
 
717
  DaysOverdue = t.DueDate < now ? (int)(now - t.DueDate).TotalDays : 0,
718
  CreatedAt = t.CreatedAt ?? DateTime.UtcNow
719
  });
720
+ await EnrichOverdueTaskReportsAsync(mapped.Items);
721
 
722
  return mapped;
723
  }
 
779
  }
780
 
781
  var tasks = await BuildAccessibleTaskQuery(isAdmin, isManager, currentUserId).ToListAsync();
782
+ var issues = await BuildAccessibleIssueQuery(isAdmin, isManager, currentUserId)
783
+ .Include(i => i.Task)
784
+ .ToListAsync();
785
 
786
  if (startDate.HasValue)
787
+ {
788
  tasks = tasks.Where(t => t.CreatedAt >= startDate.Value.Date).ToList();
789
+ issues = issues.Where(i => (i.CreatedAt ?? DateTime.UtcNow) >= startDate.Value.Date).ToList();
790
+ }
791
 
792
  if (endDate.HasValue)
793
+ {
794
  tasks = tasks.Where(t => t.CreatedAt < endDate.Value.Date.AddDays(1)).ToList();
795
+ issues = issues.Where(i => (i.CreatedAt ?? DateTime.UtcNow) < endDate.Value.Date.AddDays(1)).ToList();
796
+ }
797
 
798
  if (!string.IsNullOrWhiteSpace(status))
799
  {
 
801
  if (sl == "overdue")
802
  {
803
  tasks = tasks.Where(t => t.StatusId != AppTaskStatus.Done && t.DueDate < DateTime.Today).ToList();
804
+ issues = issues.Where(i => i.StatusId != AppTaskStatus.Done && i.DueDate.Date < DateTime.Today).ToList();
805
  }
806
  else if (sl != "0")
807
  {
808
  if (Enum.TryParse<AppTaskStatus>(status, true, out var parsedStatus))
809
+ {
810
  tasks = tasks.Where(t => t.StatusId == parsedStatus).ToList();
811
+ issues = issues.Where(i => i.StatusId == parsedStatus).ToList();
812
+ }
813
  }
814
  }
815
 
 
817
  foreach (var user in users)
818
  {
819
  var userTasks = tasks.Where(t => t.AssignedTo == user.Id).ToList();
820
+ var userIssues = issues.Where(i => i.AssignedTo == user.Id).ToList();
821
  int total = userTasks.Count;
822
  var completedTasks = userTasks.Where(t => t.StatusId == AppTaskStatus.Done).ToList();
823
  int done = completedTasks.Count;
824
+ var completedIssues = userIssues.Where(i => i.StatusId == AppTaskStatus.Done).ToList();
825
 
826
  int onTimeCount = completedTasks.Count(t =>
827
  (t.UpdatedAt ?? t.CreatedAt) <= t.DueDate);
828
 
829
+ var onTimeIssueCount = completedIssues.Count(i => (i.UpdatedAt ?? i.CreatedAt ?? DateTime.UtcNow) <= i.DueDate);
830
+ double onTimeDeliveryRate = completedIssues.Count > 0
831
+ ? Math.Round(((double)onTimeIssueCount / completedIssues.Count) * 100, 2)
832
+ : done > 0 ? Math.Round(((double)onTimeCount / done) * 100, 2) : 0;
833
+ var openIssues = userIssues.Count(i => i.StatusId != AppTaskStatus.Done);
834
+ var overdueIssues = userIssues.Count(i => i.StatusId != AppTaskStatus.Done && i.DueDate.Date < DateTime.Today);
835
+ var completionRate = userIssues.Count > 0 ? Math.Round(((double)completedIssues.Count / userIssues.Count) * 100, 2) : 0;
836
+ var avgCompletionDays = completedIssues.Count > 0
837
+ ? Math.Round(completedIssues.Average(i => Math.Max(0, ((i.UpdatedAt ?? i.CreatedAt ?? DateTime.UtcNow).Date - i.StartDate.Date).TotalDays)), 1)
838
+ : 0;
839
 
840
  list.Add(new EmployeeProductivityReportDto
841
  {
 
844
  FullName = $"{user.FirstName} {user.LastName}".Trim(),
845
  AssignedCount = total,
846
  CompletedCount = done,
847
+ AssignedIssues = userIssues.Count,
848
+ OpenIssues = openIssues,
849
+ OverdueIssues = overdueIssues,
850
+ ActualHours = userIssues.Sum(i => i.ActualHours ?? 0),
851
+ WorkloadLevel = openIssues >= 10 || overdueIssues >= 3 ? "High" : openIssues <= 2 ? "Light" : "Balanced",
852
+ AverageCompletionTimeDays = avgCompletionDays,
853
+ Trend = overdueIssues > 0 ? "Declining" : completionRate >= 80 ? "Improving" : "Stable",
854
+ CompletionRate = completionRate,
855
+ Efficiency = completionRate,
856
  OnTimeDeliveryRate = onTimeDeliveryRate
857
  });
858
  }
 
863
  list = list.Where(u => u.FullName.ToLower().Contains(s) || u.Username.ToLower().Contains(s)).ToList();
864
  }
865
 
866
+ return Result<IEnumerable<EmployeeProductivityReportDto>>.Success(list.OrderByDescending(u => u.OpenIssues).ThenByDescending(u => u.ActualHours));
867
  }
868
 
869
  public async Task<PagedResult<EmployeeProductivityReportDto>> GetPagedEmployeeProductivityReportAsync(
 
884
  return new PagedResult<EmployeeProductivityReportDto>();
885
  }
886
 
887
+ var ordered = full.Value.OrderByDescending(u => u.OpenIssues).ThenByDescending(u => u.ActualHours).ToList();
888
  var normalizedPageSize = PaginationExtensions.NormalizePageSize(pageSize);
889
  var totalCount = ordered.Count;
890
  var totalPages = totalCount == 0 ? 0 : (int)Math.Ceiling(totalCount / (double)normalizedPageSize);
 
933
  }
934
 
935
  var tasks = await BuildAccessibleTaskQuery(isAdmin, isManager, currentUserId).ToListAsync();
936
+ var issues = await BuildAccessibleIssueQuery(isAdmin, isManager, currentUserId)
937
+ .Include(i => i.Task)
938
+ .ToListAsync();
939
  var today = DateTime.Today;
940
 
941
  if (startDate.HasValue)
 
948
  foreach (var project in projects)
949
  {
950
  var projectTasks = tasks.Where(t => t.ProjectId == project.Id).ToList();
951
+ var projectIssues = issues.Where(i => i.Task.ProjectId == project.Id).ToList();
952
 
953
  if (!string.IsNullOrWhiteSpace(status))
954
  {
955
  if (status.Equals("overdue", StringComparison.OrdinalIgnoreCase))
956
  {
957
  projectTasks = projectTasks.Where(t => t.DueDate.Date < today && t.StatusId != AppTaskStatus.Done).ToList();
958
+ projectIssues = projectIssues.Where(i => i.DueDate.Date < today && i.StatusId != AppTaskStatus.Done).ToList();
959
  }
960
  else if (status != "0" && Enum.TryParse<AppTaskStatus>(status, true, out var parsedStatus))
961
  {
962
  projectTasks = projectTasks.Where(t => t.StatusId == parsedStatus).ToList();
963
+ projectIssues = projectIssues.Where(i => i.StatusId == parsedStatus).ToList();
964
  }
965
  }
966
 
 
979
  }
980
 
981
  bool isAhead = progress > elapsedPct && progress > 0 && progress < 100;
982
+ var openIssues = projectIssues.Count(i => i.StatusId != AppTaskStatus.Done);
983
+ var overdueIssues = projectIssues.Count(i => i.StatusId != AppTaskStatus.Done && i.DueDate.Date < today);
984
+ var blockedIssues = projectIssues.Count(i => i.IsBlocked);
985
  bool isAtRisk = (projectTasks.Any(t => t.StatusId != AppTaskStatus.Done && t.DueDate.Date < today)) ||
986
+ overdueIssues > 0 ||
987
+ blockedIssues > 0 ||
988
  (project.EndDate.Date < today && progress < 100);
989
+ var health = isAtRisk && (overdueIssues > 0 || blockedIssues > 0) ? "Critical" : isAtRisk ? "At Risk" : "Healthy";
990
+ var lastActivity = projectIssues
991
+ .Select(i => i.UpdatedAt ?? i.CreatedAt ?? project.CreatedAt ?? DateTime.UtcNow)
992
+ .Concat(projectTasks.Select(t => t.UpdatedAt ?? t.CreatedAt ?? DateTime.UtcNow))
993
+ .DefaultIfEmpty(project.CreatedAt ?? DateTime.UtcNow)
994
+ .Max();
995
 
996
  list.Add(new ProjectProgressReportDto
997
  {
 
1001
  EndDate = project.EndDate,
1002
  TotalTasks = totalTasks,
1003
  CompletedTasks = completedTasks,
1004
+ OpenIssues = openIssues,
1005
+ OverdueIssues = overdueIssues,
1006
+ BlockedIssues = blockedIssues,
1007
  Progress = progress,
1008
  IsAhead = isAhead,
1009
+ IsAtRisk = isAtRisk,
1010
+ ProjectHealth = health,
1011
+ LastActivity = lastActivity,
1012
+ NextMilestone = project.EndDate.Date >= today ? project.EndDate : null,
1013
+ ActualHours = projectIssues.Sum(i => i.ActualHours ?? 0)
1014
  });
1015
  }
1016
 
TaskTrackingSystem.WebApi/Program.cs CHANGED
@@ -134,6 +134,8 @@ static async System.Threading.Tasks.Task EnsureSeedDataAsync(WebApplication app)
134
  using var scope = app.Services.CreateScope();
135
  var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
136
 
 
 
137
  var dashboardMenu = await db.Menus.FirstOrDefaultAsync(m => m.MenuCode == "DASHBOARD");
138
  if (dashboardMenu == null)
139
  {
@@ -316,6 +318,8 @@ static async System.Threading.Tasks.Task EnsureSeedDataAsync(WebApplication app)
316
 
317
  await db.SaveChangesAsync();
318
 
 
 
319
  // Soft-delete existing tasks that belong to deleted projects
320
  var orphanedTasks = await db.Tasks
321
  .Where(t => t.IsDeleted != true && t.Project.IsDeleted == true)
@@ -332,3 +336,82 @@ static async System.Threading.Tasks.Task EnsureSeedDataAsync(WebApplication app)
332
 
333
  await db.SaveChangesAsync();
334
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
134
  using var scope = app.Services.CreateScope();
135
  var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
136
 
137
+ await EnsureReportUpgradeSchemaAsync(db);
138
+
139
  var dashboardMenu = await db.Menus.FirstOrDefaultAsync(m => m.MenuCode == "DASHBOARD");
140
  if (dashboardMenu == null)
141
  {
 
318
 
319
  await db.SaveChangesAsync();
320
 
321
+ await EnsureIssueReportMenuAsync(db);
322
+
323
  // Soft-delete existing tasks that belong to deleted projects
324
  var orphanedTasks = await db.Tasks
325
  .Where(t => t.IsDeleted != true && t.Project.IsDeleted == true)
 
336
 
337
  await db.SaveChangesAsync();
338
  }
339
+
340
+ static async System.Threading.Tasks.Task EnsureReportUpgradeSchemaAsync(AppDbContext db)
341
+ {
342
+ await db.Database.ExecuteSqlRawAsync(@"
343
+ IF COL_LENGTH('dbo.Issues', 'DelayReason') IS NULL
344
+ ALTER TABLE [dbo].[Issues] ADD [DelayReason] NVARCHAR(300) NULL;
345
+
346
+ IF COL_LENGTH('dbo.Issues', 'IsBlocked') IS NULL
347
+ ALTER TABLE [dbo].[Issues] ADD [IsBlocked] BIT NOT NULL CONSTRAINT [DF_Issues_IsBlocked] DEFAULT ((0));
348
+
349
+ IF COL_LENGTH('dbo.Issues', 'BlockedBy') IS NULL
350
+ ALTER TABLE [dbo].[Issues] ADD [BlockedBy] NVARCHAR(200) NULL;
351
+
352
+ IF COL_LENGTH('dbo.Issues', 'EscalationLevel') IS NULL
353
+ ALTER TABLE [dbo].[Issues] ADD [EscalationLevel] INT NOT NULL CONSTRAINT [DF_Issues_EscalationLevel] DEFAULT ((0));
354
+ ");
355
+ }
356
+
357
+ static async System.Threading.Tasks.Task EnsureIssueReportMenuAsync(AppDbContext db)
358
+ {
359
+ var reportsMenu = await db.Menus.FirstOrDefaultAsync(m => m.MenuCode == "REPORTS" && !m.IsDeleted);
360
+ var issueReportMenu = await db.Menus.FirstOrDefaultAsync(m => m.MenuCode == "REPORTS_ISSUES");
361
+
362
+ if (issueReportMenu == null)
363
+ {
364
+ issueReportMenu = new Menu
365
+ {
366
+ MenuCode = "REPORTS_ISSUES",
367
+ ParentMenuId = reportsMenu?.MenuId,
368
+ MenuName = "Issue Report",
369
+ MenuUrl = "/reports/issues",
370
+ Icon = "file-pen-line",
371
+ Visible = true,
372
+ OrderNo = 15,
373
+ IsDeleted = false,
374
+ CreatedAt = DateTime.UtcNow
375
+ };
376
+ db.Menus.Add(issueReportMenu);
377
+ await db.SaveChangesAsync();
378
+ }
379
+ else
380
+ {
381
+ issueReportMenu.ParentMenuId = reportsMenu?.MenuId;
382
+ issueReportMenu.MenuName = "Issue Report";
383
+ issueReportMenu.MenuUrl = "/reports/issues";
384
+ issueReportMenu.Icon = "file-pen-line";
385
+ issueReportMenu.Visible = true;
386
+ issueReportMenu.OrderNo = 15;
387
+ issueReportMenu.IsDeleted = false;
388
+ issueReportMenu.UpdatedAt = DateTime.UtcNow;
389
+ await db.SaveChangesAsync();
390
+ }
391
+
392
+ var reportRoles = await db.Roles
393
+ .Where(role => !role.IsDeleted && (role.Name == "Admin" || role.Name == "Manager" || role.Name == "Employee"))
394
+ .ToListAsync();
395
+
396
+ foreach (var role in reportRoles)
397
+ {
398
+ var roleMenu = await db.RoleMenus.FirstOrDefaultAsync(rm => rm.RoleId == role.Id && rm.MenuId == issueReportMenu.MenuId);
399
+ if (roleMenu == null)
400
+ {
401
+ db.RoleMenus.Add(new RoleMenu
402
+ {
403
+ RoleId = role.Id,
404
+ MenuId = issueReportMenu.MenuId,
405
+ IsDeleted = false,
406
+ CreatedAt = DateTime.UtcNow
407
+ });
408
+ }
409
+ else
410
+ {
411
+ roleMenu.IsDeleted = false;
412
+ roleMenu.UpdatedAt = DateTime.UtcNow;
413
+ }
414
+ }
415
+
416
+ await db.SaveChangesAsync();
417
+ }
TaskTrackingSystem.WebApp/Components/Pages/Features/Issues/AddIssue.razor CHANGED
@@ -111,6 +111,36 @@
111
  </select>
112
  </div>
113
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
  <div>
115
  <label class="block text-sm font-medium text-slate-700 mb-1.5">Project</label>
116
  <input value="@SelectedProjectLabel" readonly
@@ -251,6 +281,10 @@
251
  private DateTime formDueDate = DateTime.Today;
252
  private AppTaskStatus formStatusId = AppTaskStatus.Todo;
253
  private TaskPriority formPriorityId = TaskPriority.Medium;
 
 
 
 
254
  private string assigneeSearchInput = "";
255
  private bool isAssigneeDropdownOpen;
256
  private bool isLoadingProjectMembers;
@@ -471,6 +505,10 @@
471
  AssignedTo = formAssignedTo,
472
  EstimatedHours = formEstimatedHours,
473
  ActualHours = formActualHours,
 
 
 
 
474
  StartDate = formStartDate,
475
  DueDate = formDueDate,
476
  StatusId = formStatusId,
@@ -512,6 +550,10 @@
512
  formDueDate = DateTime.Today;
513
  formStatusId = AppTaskStatus.Todo;
514
  formPriorityId = TaskPriority.Medium;
 
 
 
 
515
  taskErrorMessage = "";
516
  titleErrorMessage = "";
517
  isTaskError = false;
 
111
  </select>
112
  </div>
113
 
114
+ <div>
115
+ <label class="block text-sm font-medium text-slate-700 mb-1.5">Blocked Status</label>
116
+ <label class="flex h-10 items-center gap-2 rounded-lg border border-slate-200 px-3 text-sm text-slate-700">
117
+ <input type="checkbox" @bind="formIsBlocked" class="h-4 w-4 rounded border-slate-300 text-violet-600 focus:ring-violet-500" />
118
+ <span>Blocked</span>
119
+ </label>
120
+ </div>
121
+
122
+ <div>
123
+ <label class="block text-sm font-medium text-slate-700 mb-1.5">Escalation Level</label>
124
+ <select @bind="formEscalationLevel" class="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all">
125
+ <option value="0">None</option>
126
+ <option value="1">Team Lead</option>
127
+ <option value="2">PM</option>
128
+ <option value="3">Manager</option>
129
+ </select>
130
+ </div>
131
+
132
+ <div>
133
+ <label class="block text-sm font-medium text-slate-700 mb-1.5">Blocked By</label>
134
+ <input @bind="formBlockedBy" type="text" placeholder="Dependency, person, or team"
135
+ class="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all" />
136
+ </div>
137
+
138
+ <div>
139
+ <label class="block text-sm font-medium text-slate-700 mb-1.5">Delay Reason</label>
140
+ <input @bind="formDelayReason" type="text" placeholder="Why is this issue delayed?"
141
+ class="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all" />
142
+ </div>
143
+
144
  <div>
145
  <label class="block text-sm font-medium text-slate-700 mb-1.5">Project</label>
146
  <input value="@SelectedProjectLabel" readonly
 
281
  private DateTime formDueDate = DateTime.Today;
282
  private AppTaskStatus formStatusId = AppTaskStatus.Todo;
283
  private TaskPriority formPriorityId = TaskPriority.Medium;
284
+ private string? formDelayReason;
285
+ private bool formIsBlocked;
286
+ private string? formBlockedBy;
287
+ private int formEscalationLevel;
288
  private string assigneeSearchInput = "";
289
  private bool isAssigneeDropdownOpen;
290
  private bool isLoadingProjectMembers;
 
505
  AssignedTo = formAssignedTo,
506
  EstimatedHours = formEstimatedHours,
507
  ActualHours = formActualHours,
508
+ DelayReason = formDelayReason,
509
+ IsBlocked = formIsBlocked,
510
+ BlockedBy = formBlockedBy,
511
+ EscalationLevel = formEscalationLevel,
512
  StartDate = formStartDate,
513
  DueDate = formDueDate,
514
  StatusId = formStatusId,
 
550
  formDueDate = DateTime.Today;
551
  formStatusId = AppTaskStatus.Todo;
552
  formPriorityId = TaskPriority.Medium;
553
+ formDelayReason = null;
554
+ formIsBlocked = false;
555
+ formBlockedBy = null;
556
+ formEscalationLevel = 0;
557
  taskErrorMessage = "";
558
  titleErrorMessage = "";
559
  isTaskError = false;
TaskTrackingSystem.WebApp/Components/Pages/Features/Issues/Issues.razor CHANGED
@@ -108,8 +108,9 @@
108
  <col class="w-[11%]" />
109
  <col class="w-[11%]" />
110
  <col class="w-[12%]" />
111
- <col class="w-[16%]" />
112
- <col class="w-[14%]" />
 
113
  <col class="w-[10%]" />
114
  </colgroup>
115
  <thead>
@@ -121,6 +122,7 @@
121
  <th class="px-5 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Priority</th>
122
  <th class="px-5 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Dates</th>
123
  <th class="px-5 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Assignee</th>
 
124
  <th class="px-5 py-3 text-center text-xs font-semibold text-slate-500 uppercase tracking-wider">Actions</th>
125
  </tr>
126
  </thead>
@@ -160,6 +162,20 @@
160
  <td class="px-5 py-4 text-sm text-slate-600 break-words">
161
  @(!string.IsNullOrWhiteSpace(issue.AssignedToName) ? issue.AssignedToName : "Unassigned")
162
  </td>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
163
  <td class="px-5 py-4 text-center">
164
  <div class="flex items-center justify-center gap-1">
165
  @if (canEditIssue)
@@ -181,7 +197,7 @@
181
  }
182
  else
183
  {
184
- <EmptyState ColSpan="8" Icon="file-pen-line" Title="No issues yet" Subtitle="Create your first issue to start tracking daily work." />
185
  }
186
  </tbody>
187
  </table>
@@ -199,9 +215,9 @@
199
 
200
  @if (showModal)
201
  {
202
- <div class="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm">
203
- <div class="bg-white rounded-2xl shadow-lg w-full max-w-2xl mx-4 overflow-hidden">
204
- <div class="flex items-center justify-between px-6 py-4 border-b border-slate-100">
205
  <div>
206
  <h3 class="text-lg font-semibold text-slate-900">Edit Issue</h3>
207
  <p class="text-xs text-slate-500 mt-0.5">Issue #@editingId</p>
@@ -211,18 +227,50 @@
211
  </button>
212
  </div>
213
 
214
- <div class="p-6 space-y-4">
215
  <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
216
- <div class="rounded-xl border border-slate-200 bg-slate-50 p-4">
217
  <p class="text-xs font-semibold uppercase tracking-wider text-slate-400">Task</p>
218
  <p class="mt-1 text-sm font-semibold text-slate-900">@SelectedTaskLabel</p>
219
  </div>
220
- <div class="rounded-xl border border-slate-200 bg-slate-50 p-4">
221
  <p class="text-xs font-semibold uppercase tracking-wider text-slate-400">Project</p>
222
  <p class="mt-1 text-sm font-semibold text-slate-900">@SelectedProjectLabel</p>
223
  </div>
224
  </div>
225
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
226
  <div>
227
  <label class="block text-sm font-medium text-slate-700 mb-1.5">Title <span class="text-red-500">*</span></label>
228
  <input @bind="formTitle" type="text" placeholder="Issue title"
@@ -231,7 +279,7 @@
231
 
232
  <div>
233
  <label class="block text-sm font-medium text-slate-700 mb-1.5">Description</label>
234
- <textarea @bind="formDescription" rows="4" placeholder="Describe the work..."
235
  class="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all"></textarea>
236
  </div>
237
 
@@ -293,7 +341,7 @@
293
  }
294
  </div>
295
 
296
- <div class="flex items-center justify-end gap-3 px-6 py-4 border-t border-slate-100 bg-slate-50/50">
297
  <button @onclick="CloseModal" class="rounded-lg px-4 py-2 text-sm font-medium text-slate-600 hover:bg-slate-100 transition-colors">Cancel</button>
298
  <button @onclick="RequestSaveConfirmation" disabled="@isSaving" class="rounded-lg bg-violet-600 px-4 py-2 text-sm font-medium text-white hover:bg-violet-700 disabled:opacity-50 transition-colors">
299
  @(isSaving ? "Saving..." : "Update")
@@ -369,6 +417,10 @@
369
  private DateTime formDueDate = DateTime.Today;
370
  private AppTaskStatus formStatusId = AppTaskStatus.Todo;
371
  private TaskPriority formPriorityId = TaskPriority.Medium;
 
 
 
 
372
 
373
  private string SelectedTaskLabel => tasks.FirstOrDefault(t => t.Id == formTaskId)?.Title ?? $"Task #{formTaskId}";
374
  private string SelectedProjectLabel => tasks.FirstOrDefault(t => t.Id == formTaskId) is { } task
@@ -539,6 +591,10 @@
539
  formDueDate = issue.DueDate;
540
  formStatusId = issue.StatusId;
541
  formPriorityId = issue.PriorityId;
 
 
 
 
542
  errorMessage = null;
543
  showModal = true;
544
 
@@ -612,6 +668,10 @@
612
  Description = formDescription,
613
  AssignedTo = formAssignedTo,
614
  ActualHours = formActualHours,
 
 
 
 
615
  StartDate = formStartDate,
616
  DueDate = formDueDate,
617
  StatusId = formStatusId,
@@ -735,4 +795,12 @@
735
  TaskPriority.High => "High",
736
  _ => "Unknown"
737
  };
 
 
 
 
 
 
 
 
738
  }
 
108
  <col class="w-[11%]" />
109
  <col class="w-[11%]" />
110
  <col class="w-[12%]" />
111
+ <col class="w-[12%]" />
112
+ <col class="w-[12%]" />
113
+ <col class="w-[8%]" />
114
  <col class="w-[10%]" />
115
  </colgroup>
116
  <thead>
 
122
  <th class="px-5 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Priority</th>
123
  <th class="px-5 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Dates</th>
124
  <th class="px-5 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Assignee</th>
125
+ <th class="px-5 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Risk</th>
126
  <th class="px-5 py-3 text-center text-xs font-semibold text-slate-500 uppercase tracking-wider">Actions</th>
127
  </tr>
128
  </thead>
 
162
  <td class="px-5 py-4 text-sm text-slate-600 break-words">
163
  @(!string.IsNullOrWhiteSpace(issue.AssignedToName) ? issue.AssignedToName : "Unassigned")
164
  </td>
165
+ <td class="px-5 py-4">
166
+ @if (issue.IsBlocked)
167
+ {
168
+ <span class="inline-flex items-center rounded-full bg-rose-50 px-2 py-0.5 text-xs font-semibold text-rose-700">Blocked</span>
169
+ }
170
+ else if (issue.EscalationLevel > 0)
171
+ {
172
+ <span class="inline-flex items-center rounded-full bg-amber-50 px-2 py-0.5 text-xs font-semibold text-amber-700">@GetEscalationLabel(issue.EscalationLevel)</span>
173
+ }
174
+ else
175
+ {
176
+ <span class="text-xs text-slate-400">Clear</span>
177
+ }
178
+ </td>
179
  <td class="px-5 py-4 text-center">
180
  <div class="flex items-center justify-center gap-1">
181
  @if (canEditIssue)
 
197
  }
198
  else
199
  {
200
+ <EmptyState ColSpan="9" Icon="file-pen-line" Title="No issues yet" Subtitle="Create your first issue to start tracking daily work." />
201
  }
202
  </tbody>
203
  </table>
 
215
 
216
  @if (showModal)
217
  {
218
+ <div class="fixed inset-0 z-50 flex items-start justify-center overflow-y-auto bg-black/40 p-4 backdrop-blur-sm sm:p-6">
219
+ <div class="flex max-h-[calc(100vh-2rem)] w-full max-w-4xl flex-col overflow-hidden rounded-2xl bg-white shadow-xl sm:max-h-[calc(100vh-3rem)]">
220
+ <div class="shrink-0 flex items-center justify-between px-6 py-4 border-b border-slate-100">
221
  <div>
222
  <h3 class="text-lg font-semibold text-slate-900">Edit Issue</h3>
223
  <p class="text-xs text-slate-500 mt-0.5">Issue #@editingId</p>
 
227
  </button>
228
  </div>
229
 
230
+ <div class="min-h-0 flex-1 overflow-y-auto p-6 space-y-4">
231
  <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
232
+ <div class="rounded-xl border border-slate-200 bg-slate-50 p-3">
233
  <p class="text-xs font-semibold uppercase tracking-wider text-slate-400">Task</p>
234
  <p class="mt-1 text-sm font-semibold text-slate-900">@SelectedTaskLabel</p>
235
  </div>
236
+ <div class="rounded-xl border border-slate-200 bg-slate-50 p-3">
237
  <p class="text-xs font-semibold uppercase tracking-wider text-slate-400">Project</p>
238
  <p class="mt-1 text-sm font-semibold text-slate-900">@SelectedProjectLabel</p>
239
  </div>
240
  </div>
241
 
242
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
243
+ <div>
244
+ <label class="block text-sm font-medium text-slate-700 mb-1.5">Blocked Status</label>
245
+ <label class="flex h-10 items-center gap-2 rounded-lg border border-slate-200 px-3 text-sm text-slate-700">
246
+ <input type="checkbox" @bind="formIsBlocked" class="h-4 w-4 rounded border-slate-300 text-violet-600 focus:ring-violet-500" />
247
+ <span>Blocked</span>
248
+ </label>
249
+ </div>
250
+ <div>
251
+ <label class="block text-sm font-medium text-slate-700 mb-1.5">Escalation Level</label>
252
+ <select @bind="formEscalationLevel" class="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all">
253
+ <option value="0">None</option>
254
+ <option value="1">Team Lead</option>
255
+ <option value="2">PM</option>
256
+ <option value="3">Manager</option>
257
+ </select>
258
+ </div>
259
+ </div>
260
+
261
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
262
+ <div>
263
+ <label class="block text-sm font-medium text-slate-700 mb-1.5">Blocked By</label>
264
+ <input @bind="formBlockedBy" type="text" placeholder="Dependency, person, or team"
265
+ class="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all" />
266
+ </div>
267
+ <div>
268
+ <label class="block text-sm font-medium text-slate-700 mb-1.5">Delay Reason</label>
269
+ <input @bind="formDelayReason" type="text" placeholder="Why is this issue delayed?"
270
+ class="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all" />
271
+ </div>
272
+ </div>
273
+
274
  <div>
275
  <label class="block text-sm font-medium text-slate-700 mb-1.5">Title <span class="text-red-500">*</span></label>
276
  <input @bind="formTitle" type="text" placeholder="Issue title"
 
279
 
280
  <div>
281
  <label class="block text-sm font-medium text-slate-700 mb-1.5">Description</label>
282
+ <textarea @bind="formDescription" rows="3" placeholder="Describe the work..."
283
  class="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all"></textarea>
284
  </div>
285
 
 
341
  }
342
  </div>
343
 
344
+ <div class="shrink-0 flex items-center justify-end gap-3 border-t border-slate-100 bg-white px-6 py-4 shadow-[0_-12px_24px_rgba(15,23,42,0.06)]">
345
  <button @onclick="CloseModal" class="rounded-lg px-4 py-2 text-sm font-medium text-slate-600 hover:bg-slate-100 transition-colors">Cancel</button>
346
  <button @onclick="RequestSaveConfirmation" disabled="@isSaving" class="rounded-lg bg-violet-600 px-4 py-2 text-sm font-medium text-white hover:bg-violet-700 disabled:opacity-50 transition-colors">
347
  @(isSaving ? "Saving..." : "Update")
 
417
  private DateTime formDueDate = DateTime.Today;
418
  private AppTaskStatus formStatusId = AppTaskStatus.Todo;
419
  private TaskPriority formPriorityId = TaskPriority.Medium;
420
+ private string? formDelayReason;
421
+ private bool formIsBlocked;
422
+ private string? formBlockedBy;
423
+ private int formEscalationLevel;
424
 
425
  private string SelectedTaskLabel => tasks.FirstOrDefault(t => t.Id == formTaskId)?.Title ?? $"Task #{formTaskId}";
426
  private string SelectedProjectLabel => tasks.FirstOrDefault(t => t.Id == formTaskId) is { } task
 
591
  formDueDate = issue.DueDate;
592
  formStatusId = issue.StatusId;
593
  formPriorityId = issue.PriorityId;
594
+ formDelayReason = issue.DelayReason;
595
+ formIsBlocked = issue.IsBlocked;
596
+ formBlockedBy = issue.BlockedBy;
597
+ formEscalationLevel = issue.EscalationLevel;
598
  errorMessage = null;
599
  showModal = true;
600
 
 
668
  Description = formDescription,
669
  AssignedTo = formAssignedTo,
670
  ActualHours = formActualHours,
671
+ DelayReason = formDelayReason,
672
+ IsBlocked = formIsBlocked,
673
+ BlockedBy = formBlockedBy,
674
+ EscalationLevel = formEscalationLevel,
675
  StartDate = formStartDate,
676
  DueDate = formDueDate,
677
  StatusId = formStatusId,
 
795
  TaskPriority.High => "High",
796
  _ => "Unknown"
797
  };
798
+
799
+ private static string GetEscalationLabel(int level) => level switch
800
+ {
801
+ 1 => "Team Lead",
802
+ 2 => "PM",
803
+ 3 => "Manager",
804
+ _ => "None"
805
+ };
806
  }
TaskTrackingSystem.WebApp/Components/Pages/Features/Reports/EmployeeReport.razor CHANGED
@@ -10,14 +10,14 @@
10
  @inject ApiClientService ApiClient
11
  @inject IJSRuntime JS
12
 
13
- <PageTitle>Employee Productivity Report</PageTitle>
14
 
15
  <div class="space-y-6">
16
  <!-- Page Header -->
17
  <div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
18
  <div>
19
- <h2 class="text-3xl font-bold tracking-tight text-slate-900">Employee Productivity</h2>
20
- <p class="text-slate-500 mt-1">Monitor task completion and performance metric charts.</p>
21
  </div>
22
  <div class="flex items-center space-x-3">
23
  <div class="inline-flex rounded-lg border border-slate-200 bg-white p-0.5 shadow-sm">
@@ -113,7 +113,7 @@
113
  {
114
  <!-- Chart View: SVG Horizontal Bar Chart showing Efficiency -->
115
  <div class="rounded-xl border border-slate-200 bg-white p-6 shadow-sm">
116
- <h3 class="font-semibold text-slate-900 mb-4 font-sans text-md">Employee Productivity Chart</h3>
117
  @if (!allEmployees.Any())
118
  {
119
  <p class="text-slate-400 text-center py-8">No data available.</p>
@@ -126,10 +126,10 @@
126
  <div>
127
  <div class="flex justify-between text-xs font-semibold text-slate-700 mb-1.5">
128
  <span>@emp.FullName (@emp.Username)</span>
129
- <span>@emp.CompletedCount / @emp.AssignedCount tasks (@emp.Efficiency.ToString("F0")% efficiency)</span>
130
  </div>
131
  <div class="h-4 rounded-full bg-slate-100 overflow-hidden flex">
132
- <div class="h-full bg-emerald-500 rounded-full transition-all" style="width: @(Math.Min(emp.Efficiency, 100))%"></div>
133
  </div>
134
  </div>
135
  }
@@ -146,10 +146,11 @@
146
  <tr class="border-b border-slate-100 bg-slate-50/60">
147
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">No.</th>
148
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Employee</th>
149
- <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Total Assigned</th>
150
- <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Completed Tasks</th>
151
- <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Efficiency</th>
152
- <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">On-Time Delivery</th>
 
153
  </tr>
154
  </thead>
155
  <tbody class="divide-y divide-slate-100">
@@ -171,34 +172,18 @@
171
  </div>
172
  </div>
173
  </td>
174
- <td class="px-6 py-4 text-sm font-medium text-slate-900">@emp.AssignedCount</td>
175
- <td class="px-6 py-4 text-sm font-medium text-emerald-600">@emp.CompletedCount</td>
176
- <td class="px-6 py-4">
177
- <div class="flex items-center space-x-3">
178
- <div class="flex-1 h-2.5 rounded-full bg-slate-100 overflow-hidden max-w-[120px]">
179
- <div class="h-full rounded-full bg-emerald-500 transition-all" style="width: @(Math.Min(emp.Efficiency, 100))%"></div>
180
- </div>
181
- <span class="text-sm font-semibold text-slate-700">@emp.Efficiency.ToString("F0")%</span>
182
- </div>
183
- </td>
184
- <td class="px-6 py-4">
185
- <div class="flex items-center space-x-3">
186
- <div class="flex-1 h-2.5 rounded-full bg-slate-100 overflow-hidden max-w-[120px]">
187
- <div class="h-full rounded-full @(emp.OnTimeDeliveryRate >= 80 ? "bg-emerald-500" : emp.OnTimeDeliveryRate >= 50 ? "bg-amber-400" : "bg-rose-500") transition-all"
188
- style="width: @(Math.Min(emp.OnTimeDeliveryRate, 100))%"></div>
189
- </div>
190
- <span class="text-sm font-semibold @(emp.OnTimeDeliveryRate >= 80 ? "text-emerald-600" : emp.OnTimeDeliveryRate >= 50 ? "text-amber-500" : "text-rose-600")">
191
- @emp.OnTimeDeliveryRate.ToString("F0")%
192
- </span>
193
- </div>
194
- </td>
195
  </tr>
196
  }
197
  }
198
  else
199
  {
200
  <tr>
201
- <td colspan="6" class="px-6 py-12 text-center">
202
  <p class="text-sm text-slate-400">No employee data found matching criteria.</p>
203
  </td>
204
  </tr>
@@ -365,16 +350,18 @@
365
  {
366
  emp.FullName,
367
  emp.Username,
368
- emp.AssignedCount,
369
- emp.CompletedCount,
370
- $"{emp.Efficiency:F0}%",
371
- $"{emp.OnTimeDeliveryRate:F0}%"
 
 
372
  });
373
 
374
  var workbookBytes = SpreadsheetReportBuilder.BuildTableWorkbook(
375
- "Employee Productivity",
376
- "Employee Productivity Report",
377
- new[] { "Employee", "Username", "Total Assigned", "Completed", "Efficiency", "On-Time Delivery" },
378
  rows,
379
  new[]
380
  {
@@ -386,7 +373,7 @@
386
 
387
  await JS.InvokeVoidAsync(
388
  "downloadFileFromBase64",
389
- $"EmployeeProductivityReport_{DateTime.Today:yyyyMMdd}.xlsx",
390
  "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
391
  Convert.ToBase64String(workbookBytes));
392
  }
@@ -405,22 +392,38 @@
405
  {
406
  emp.FullName,
407
  emp.Username,
408
- emp.AssignedCount.ToString(),
409
- emp.CompletedCount.ToString(),
410
- $"{emp.Efficiency:F0}%",
411
- $"{emp.OnTimeDeliveryRate:F0}%"
 
 
412
  });
413
 
414
  var pdfBytes = SimplePdfReportBuilder.BuildTableReport(
415
- "Employee Productivity Report",
416
  summaryLines,
417
- new[] { "Employee", "Username", "Total Assigned", "Completed", "Efficiency", "On-Time Delivery" },
418
  rows);
419
 
420
  await JS.InvokeVoidAsync(
421
  "downloadFileFromBase64",
422
- $"EmployeeProductivityReport_{DateTime.Today:yyyyMMdd}.pdf",
423
  "application/pdf",
424
  Convert.ToBase64String(pdfBytes));
425
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
426
  }
 
10
  @inject ApiClientService ApiClient
11
  @inject IJSRuntime JS
12
 
13
+ <PageTitle>Employee Workload &amp; Performance</PageTitle>
14
 
15
  <div class="space-y-6">
16
  <!-- Page Header -->
17
  <div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
18
  <div>
19
+ <h2 class="text-3xl font-bold tracking-tight text-slate-900">Employee Workload &amp; Performance</h2>
20
+ <p class="text-slate-500 mt-1">Monitor issue workload, execution health, and actual effort.</p>
21
  </div>
22
  <div class="flex items-center space-x-3">
23
  <div class="inline-flex rounded-lg border border-slate-200 bg-white p-0.5 shadow-sm">
 
113
  {
114
  <!-- Chart View: SVG Horizontal Bar Chart showing Efficiency -->
115
  <div class="rounded-xl border border-slate-200 bg-white p-6 shadow-sm">
116
+ <h3 class="font-semibold text-slate-900 mb-4 font-sans text-md">Workload by Employee</h3>
117
  @if (!allEmployees.Any())
118
  {
119
  <p class="text-slate-400 text-center py-8">No data available.</p>
 
126
  <div>
127
  <div class="flex justify-between text-xs font-semibold text-slate-700 mb-1.5">
128
  <span>@emp.FullName (@emp.Username)</span>
129
+ <span>@emp.OpenIssues open, @emp.OverdueIssues overdue, @emp.ActualHours.ToString("N1") hrs</span>
130
  </div>
131
  <div class="h-4 rounded-full bg-slate-100 overflow-hidden flex">
132
+ <div class="h-full bg-violet-600 rounded-full transition-all" style="width: @(Math.Min(emp.OpenIssues * 10, 100))%"></div>
133
  </div>
134
  </div>
135
  }
 
146
  <tr class="border-b border-slate-100 bg-slate-50/60">
147
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">No.</th>
148
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Employee</th>
149
+ <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Assigned Issues</th>
150
+ <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Open / Overdue</th>
151
+ <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Actual Hours</th>
152
+ <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Workload</th>
153
+ <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Trend</th>
154
  </tr>
155
  </thead>
156
  <tbody class="divide-y divide-slate-100">
 
172
  </div>
173
  </div>
174
  </td>
175
+ <td class="px-6 py-4 text-sm font-medium text-slate-900">@emp.AssignedIssues</td>
176
+ <td class="px-6 py-4 text-sm font-medium"><span class="text-blue-600">@emp.OpenIssues</span> / <span class="text-rose-600">@emp.OverdueIssues</span></td>
177
+ <td class="px-6 py-4 text-sm font-semibold text-slate-800">@emp.ActualHours.ToString("N1") hrs</td>
178
+ <td class="px-6 py-4"><span class="inline-flex rounded-full px-2.5 py-0.5 text-xs font-semibold @GetWorkloadBadge(emp.WorkloadLevel)">@emp.WorkloadLevel</span></td>
179
+ <td class="px-6 py-4"><span class="inline-flex rounded-full px-2.5 py-0.5 text-xs font-semibold @GetTrendBadge(emp.Trend)">@emp.Trend</span></td>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
  </tr>
181
  }
182
  }
183
  else
184
  {
185
  <tr>
186
+ <td colspan="7" class="px-6 py-12 text-center">
187
  <p class="text-sm text-slate-400">No employee data found matching criteria.</p>
188
  </td>
189
  </tr>
 
350
  {
351
  emp.FullName,
352
  emp.Username,
353
+ emp.AssignedIssues,
354
+ emp.OpenIssues,
355
+ emp.OverdueIssues,
356
+ emp.ActualHours,
357
+ emp.WorkloadLevel,
358
+ emp.Trend
359
  });
360
 
361
  var workbookBytes = SpreadsheetReportBuilder.BuildTableWorkbook(
362
+ "Employee Workload",
363
+ "Employee Workload & Performance Report",
364
+ new[] { "Employee", "Username", "Assigned Issues", "Open Issues", "Overdue Issues", "Actual Hours", "Workload", "Trend" },
365
  rows,
366
  new[]
367
  {
 
373
 
374
  await JS.InvokeVoidAsync(
375
  "downloadFileFromBase64",
376
+ $"EmployeeWorkloadPerformance_{DateTime.Today:yyyyMMdd}.xlsx",
377
  "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
378
  Convert.ToBase64String(workbookBytes));
379
  }
 
392
  {
393
  emp.FullName,
394
  emp.Username,
395
+ emp.AssignedIssues.ToString(),
396
+ emp.OpenIssues.ToString(),
397
+ emp.OverdueIssues.ToString(),
398
+ emp.ActualHours.ToString("N1"),
399
+ emp.WorkloadLevel,
400
+ emp.Trend
401
  });
402
 
403
  var pdfBytes = SimplePdfReportBuilder.BuildTableReport(
404
+ "Employee Workload & Performance Report",
405
  summaryLines,
406
+ new[] { "Employee", "Username", "Assigned Issues", "Open", "Overdue", "Hours", "Workload", "Trend" },
407
  rows);
408
 
409
  await JS.InvokeVoidAsync(
410
  "downloadFileFromBase64",
411
+ $"EmployeeWorkloadPerformance_{DateTime.Today:yyyyMMdd}.pdf",
412
  "application/pdf",
413
  Convert.ToBase64String(pdfBytes));
414
  }
415
+
416
+ private static string GetWorkloadBadge(string workload) => workload switch
417
+ {
418
+ "High" => "bg-rose-50 text-rose-700",
419
+ "Light" => "bg-slate-100 text-slate-600",
420
+ _ => "bg-emerald-50 text-emerald-700"
421
+ };
422
+
423
+ private static string GetTrendBadge(string trend) => trend switch
424
+ {
425
+ "Improving" => "bg-emerald-50 text-emerald-700",
426
+ "Declining" => "bg-rose-50 text-rose-700",
427
+ _ => "bg-blue-50 text-blue-700"
428
+ };
429
  }
TaskTrackingSystem.WebApp/Components/Pages/Features/Reports/IssueReport.razor ADDED
@@ -0,0 +1,325 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @page "/reports/issues"
2
+ @using Microsoft.AspNetCore.Authorization
3
+ @using TaskTrackingSystem.Shared
4
+ @using TaskTrackingSystem.Shared.Enums
5
+ @using TaskTrackingSystem.Shared.Models.Issue
6
+ @using TaskTrackingSystem.Shared.Models.Project
7
+ @rendermode @(new InteractiveServerRenderMode(prerender: false))
8
+ @attribute [Authorize]
9
+ @inject ApiClientService ApiClient
10
+ @inject IJSRuntime JS
11
+
12
+ <PageTitle>Issue Report</PageTitle>
13
+
14
+ <div class="space-y-6">
15
+ <div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
16
+ <div>
17
+ <h2 class="text-3xl font-bold tracking-tight text-slate-900">Issue Report</h2>
18
+ <p class="mt-1 text-slate-500">Track daily execution, backlog health, and issue-level effort.</p>
19
+ </div>
20
+ <div class="flex flex-wrap items-center gap-3">
21
+ <div class="inline-flex rounded-lg border border-slate-200 bg-white p-0.5 shadow-sm">
22
+ <button @onclick="() => SetView(false)" class="inline-flex items-center rounded-md px-3 py-1.5 text-xs font-semibold @(!isChartView ? "bg-violet-600 text-white" : "text-slate-600 hover:text-slate-900")">
23
+ <i data-lucide="list" class="mr-1 h-3.5 w-3.5"></i> List View
24
+ </button>
25
+ <button @onclick="() => SetView(true)" class="inline-flex items-center rounded-md px-3 py-1.5 text-xs font-semibold @(isChartView ? "bg-violet-600 text-white" : "text-slate-600 hover:text-slate-900")">
26
+ <i data-lucide="bar-chart-3" class="mr-1 h-3.5 w-3.5"></i> Chart View
27
+ </button>
28
+ </div>
29
+ <button @onclick="DownloadExcel" class="inline-flex items-center rounded-lg border border-slate-200 bg-white px-4 py-2 text-sm font-medium text-slate-700 shadow-sm hover:bg-slate-50">
30
+ <i data-lucide="download" class="mr-2 h-4 w-4"></i> Export Excel
31
+ </button>
32
+ <button @onclick="DownloadPdf" class="inline-flex items-center rounded-lg border border-slate-200 bg-white px-4 py-2 text-sm font-medium text-slate-700 shadow-sm hover:bg-slate-50">
33
+ <i data-lucide="file-text" class="mr-2 h-4 w-4"></i> Export PDF
34
+ </button>
35
+ </div>
36
+ </div>
37
+
38
+ <div class="rounded-xl border border-slate-200 bg-white p-4 shadow-sm">
39
+ <div class="flex flex-wrap items-end gap-3">
40
+ <div class="min-w-[220px] flex-1">
41
+ <label class="mb-1.5 block text-xs font-medium text-slate-500">Search</label>
42
+ <input @bind="searchInput" type="text" placeholder="Search issue, task, project, assignee..."
43
+ class="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-violet-600" />
44
+ </div>
45
+ <div>
46
+ <label class="mb-1.5 block text-xs font-medium text-slate-500">Project</label>
47
+ <select @bind="filterProjectId" class="rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-violet-600">
48
+ <option value="0">All Projects</option>
49
+ @foreach (var project in projects)
50
+ {
51
+ <option value="@project.Id">@project.Name</option>
52
+ }
53
+ </select>
54
+ </div>
55
+ <div>
56
+ <label class="mb-1.5 block text-xs font-medium text-slate-500">Status</label>
57
+ <select @bind="filterStatus" class="rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-violet-600">
58
+ <option value="0">All Statuses</option>
59
+ <option value="1">To Do</option>
60
+ <option value="2">In Progress</option>
61
+ <option value="3">Done</option>
62
+ <option value="overdue">Overdue</option>
63
+ <option value="blocked">Blocked</option>
64
+ </select>
65
+ </div>
66
+ <button @onclick="ApplyFilters" class="inline-flex items-center rounded-lg bg-violet-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-violet-700">
67
+ <i data-lucide="search" class="mr-2 h-4 w-4"></i> Search
68
+ </button>
69
+ <button @onclick="ResetFilters" class="inline-flex items-center rounded-lg border border-slate-200 bg-white px-4 py-2 text-sm font-medium text-slate-700 shadow-sm hover:bg-slate-50">
70
+ <i data-lucide="rotate-ccw" class="mr-2 h-4 w-4"></i> Reset
71
+ </button>
72
+ </div>
73
+ </div>
74
+
75
+ @if (isLoading)
76
+ {
77
+ <LoadingSpinner Message="Loading issue report..." />
78
+ }
79
+ else
80
+ {
81
+ <div class="grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4">
82
+ @MetricCard("Total Issues", FilteredIssues.Count().ToString(), "file-pen-line", "violet")
83
+ @MetricCard("Open Issues", OpenIssues.ToString(), "list-todo", "blue")
84
+ @MetricCard("Overdue Issues", OverdueIssues.ToString(), "alarm-clock", "rose")
85
+ @MetricCard("Blocked Issues", BlockedIssues.ToString(), "ban", "amber")
86
+ </div>
87
+
88
+ <div class="rounded-xl border border-slate-200 bg-white p-5 shadow-sm">
89
+ <h3 class="text-sm font-semibold text-slate-900">Key Insights</h3>
90
+ <div class="mt-4 grid grid-cols-1 gap-3 md:grid-cols-3">
91
+ @foreach (var insight in Insights)
92
+ {
93
+ <div class="rounded-lg border border-slate-100 bg-slate-50 px-3 py-2 text-sm text-slate-700">@insight</div>
94
+ }
95
+ </div>
96
+ </div>
97
+
98
+ <div class="rounded-xl border border-slate-200 bg-white p-5 shadow-sm">
99
+ @if (isChartView)
100
+ {
101
+ <div class="grid grid-cols-1 gap-5 md:grid-cols-2">
102
+ @BarChart("Issues by Status", StatusChart)
103
+ @BarChart("Issues by Priority", PriorityChart)
104
+ @BarChart("Issues by Employee", EmployeeChart)
105
+ @BarChart("Issues by Project", ProjectChart)
106
+ </div>
107
+ }
108
+ else
109
+ {
110
+ <div class="overflow-x-auto">
111
+ <table class="w-full">
112
+ <thead>
113
+ <tr class="border-b border-slate-100 bg-slate-50/60">
114
+ <th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-slate-500">Issue</th>
115
+ <th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-slate-500">Task / Project</th>
116
+ <th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-slate-500">Assignee</th>
117
+ <th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-slate-500">Status</th>
118
+ <th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-slate-500">Hours</th>
119
+ <th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-slate-500">Due</th>
120
+ <th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-slate-500">Risk</th>
121
+ </tr>
122
+ </thead>
123
+ <tbody class="divide-y divide-slate-100">
124
+ @foreach (var issue in PagedIssues)
125
+ {
126
+ <tr class="align-top hover:bg-slate-50/50">
127
+ <td class="px-4 py-3">
128
+ <button @onclick="() => ToggleExpanded(issue.Id)" class="text-left text-sm font-semibold text-slate-900 hover:text-violet-600">@issue.Title</button>
129
+ @if (expandedIssues.Contains(issue.Id))
130
+ {
131
+ <div class="mt-2 rounded-lg bg-slate-50 p-3 text-xs text-slate-600">
132
+ <div>@(string.IsNullOrWhiteSpace(issue.Description) ? "No description" : issue.Description)</div>
133
+ <div class="mt-1">Delay: @(string.IsNullOrWhiteSpace(issue.DelayReason) ? "None recorded" : issue.DelayReason)</div>
134
+ <div>Blocked by: @(string.IsNullOrWhiteSpace(issue.BlockedBy) ? "None" : issue.BlockedBy)</div>
135
+ </div>
136
+ }
137
+ </td>
138
+ <td class="px-4 py-3 text-sm text-slate-600"><div class="font-medium text-slate-800">@issue.TaskTitle</div><div class="text-xs text-slate-400">@issue.ProjectName</div></td>
139
+ <td class="px-4 py-3 text-sm text-slate-600">@(issue.AssignedToName ?? "Unassigned")</td>
140
+ <td class="px-4 py-3"><span class="rounded-full px-2 py-0.5 text-xs font-medium @GetStatusBadge(issue.StatusId)">@GetStatusLabel(issue.StatusId)</span></td>
141
+ <td class="px-4 py-3 text-sm text-slate-700">@((issue.EstimatedHours ?? 0).ToString("N1")) / @((issue.ActualHours ?? 0).ToString("N1"))</td>
142
+ <td class="px-4 py-3 text-sm @(IsOverdue(issue) ? "font-semibold text-rose-600" : "text-slate-600")">@DisplayFormats.Date(issue.DueDate)</td>
143
+ <td class="px-4 py-3">@RiskBadge(issue)</td>
144
+ </tr>
145
+ }
146
+ @if (!PagedIssues.Any())
147
+ {
148
+ <EmptyState ColSpan="7" Icon="file-pen-line" Title="No issues found" Subtitle="No issues matched the current report filters." />
149
+ }
150
+ </tbody>
151
+ </table>
152
+ </div>
153
+ <Pagination CurrentPage="currentPage" TotalPages="TotalPages" PageSize="pageSize" TotalCount="FilteredIssues.Count()" OnPageChanged="HandlePageChanged" OnPageSizeChanged="HandlePageSizeChanged" />
154
+ }
155
+ </div>
156
+ }
157
+ </div>
158
+
159
+ @code {
160
+ private bool isLoading = true;
161
+ private bool isChartView;
162
+ private List<IssueDto> issues = new();
163
+ private List<ProjectDto> projects = new();
164
+ private HashSet<long> expandedIssues = new();
165
+ private string searchInput = string.Empty;
166
+ private string appliedSearch = string.Empty;
167
+ private long filterProjectId;
168
+ private long appliedProjectId;
169
+ private string filterStatus = "0";
170
+ private string appliedStatus = "0";
171
+ private int currentPage = 1;
172
+ private int pageSize = 10;
173
+ private const string StorageKey = "tts.report.issues.filters";
174
+
175
+ private IEnumerable<IssueDto> FilteredIssues => issues.Where(MatchesFilters).OrderBy(i => i.DueDate).ThenBy(i => i.Title);
176
+ private IEnumerable<IssueDto> PagedIssues => FilteredIssues.Skip((currentPage - 1) * pageSize).Take(pageSize);
177
+ private int TotalPages => (int)Math.Ceiling((double)FilteredIssues.Count() / pageSize);
178
+ private int OpenIssues => FilteredIssues.Count(i => i.StatusId != AppTaskStatus.Done);
179
+ private int OverdueIssues => FilteredIssues.Count(IsOverdue);
180
+ private int BlockedIssues => FilteredIssues.Count(i => i.IsBlocked);
181
+
182
+ protected override async Task OnInitializedAsync()
183
+ {
184
+ var client = ApiClient.CreateClient();
185
+ try
186
+ {
187
+ var issuesTask = client.GetFromJsonAsync<Result<List<IssueDto>>>("Report/issues", Serialization.CaseInsensitive);
188
+ var projectsTask = client.GetFromJsonAsync<List<ProjectDto>>("Project", Serialization.CaseInsensitive);
189
+ await Task.WhenAll(issuesTask, projectsTask);
190
+ issues = issuesTask.Result?.Value ?? new();
191
+ projects = projectsTask.Result ?? new();
192
+ }
193
+ finally
194
+ {
195
+ isLoading = false;
196
+ }
197
+ }
198
+
199
+ protected override async Task OnAfterRenderAsync(bool firstRender)
200
+ {
201
+ await JS.InvokeVoidAsync("initIcons");
202
+ if (firstRender)
203
+ {
204
+ await LoadSavedFilters();
205
+ }
206
+ }
207
+
208
+ private bool MatchesFilters(IssueDto issue)
209
+ {
210
+ if (appliedProjectId > 0 && issue.ProjectId != appliedProjectId) return false;
211
+ if (appliedStatus == "overdue" && !IsOverdue(issue)) return false;
212
+ if (appliedStatus == "blocked" && !issue.IsBlocked) return false;
213
+ if (int.TryParse(appliedStatus, out var statusValue) && statusValue > 0 && (int)issue.StatusId != statusValue) return false;
214
+ if (!string.IsNullOrWhiteSpace(appliedSearch))
215
+ {
216
+ var s = appliedSearch.Trim();
217
+ return issue.Title.Contains(s, StringComparison.OrdinalIgnoreCase)
218
+ || issue.TaskTitle.Contains(s, StringComparison.OrdinalIgnoreCase)
219
+ || issue.ProjectName.Contains(s, StringComparison.OrdinalIgnoreCase)
220
+ || (issue.AssignedToName?.Contains(s, StringComparison.OrdinalIgnoreCase) == true);
221
+ }
222
+ return true;
223
+ }
224
+
225
+ private async Task ApplyFilters()
226
+ {
227
+ appliedSearch = searchInput;
228
+ appliedProjectId = filterProjectId;
229
+ appliedStatus = filterStatus;
230
+ currentPage = 1;
231
+ await SaveFilters();
232
+ }
233
+
234
+ private async Task ResetFilters()
235
+ {
236
+ searchInput = appliedSearch = string.Empty;
237
+ filterProjectId = appliedProjectId = 0;
238
+ filterStatus = appliedStatus = "0";
239
+ currentPage = 1;
240
+ await SaveFilters();
241
+ }
242
+
243
+ private void SetView(bool chartView) => isChartView = chartView;
244
+ private void ToggleExpanded(long id) { if (!expandedIssues.Remove(id)) expandedIssues.Add(id); }
245
+ private Task HandlePageChanged(int page) { currentPage = page; return Task.CompletedTask; }
246
+ private Task HandlePageSizeChanged(int size) { pageSize = size; currentPage = 1; return Task.CompletedTask; }
247
+
248
+ private async Task SaveFilters()
249
+ {
250
+ await JS.InvokeVoidAsync("localStorage.setItem", StorageKey, System.Text.Json.JsonSerializer.Serialize(new SavedFilter(appliedSearch, appliedProjectId, appliedStatus)));
251
+ }
252
+
253
+ private async Task LoadSavedFilters()
254
+ {
255
+ var json = await JS.InvokeAsync<string?>("localStorage.getItem", StorageKey);
256
+ if (string.IsNullOrWhiteSpace(json)) return;
257
+ var saved = System.Text.Json.JsonSerializer.Deserialize<SavedFilter>(json);
258
+ if (saved == null) return;
259
+ searchInput = appliedSearch = saved.Search;
260
+ filterProjectId = appliedProjectId = saved.ProjectId;
261
+ filterStatus = appliedStatus = saved.Status;
262
+ StateHasChanged();
263
+ }
264
+
265
+ private static bool IsOverdue(IssueDto issue) => issue.StatusId != AppTaskStatus.Done && issue.DueDate.Date < DateTime.Today;
266
+ private static string GetStatusLabel(AppTaskStatus status) => status == AppTaskStatus.InProgress ? "In Progress" : status == AppTaskStatus.Done ? "Done" : "To Do";
267
+ private static string GetStatusBadge(AppTaskStatus status) => status == AppTaskStatus.Done ? "bg-emerald-50 text-emerald-700" : status == AppTaskStatus.InProgress ? "bg-blue-50 text-blue-700" : "bg-slate-100 text-slate-700";
268
+ private static string GetPriorityLabel(TaskPriority priority) => priority == TaskPriority.High ? "High" : priority == TaskPriority.Medium ? "Medium" : "Low";
269
+ private static string EscalationLabel(int level) => level switch { 1 => "Team Lead", 2 => "PM", 3 => "Manager", _ => "None" };
270
+
271
+ private RenderFragment RiskBadge(IssueDto issue) => builder =>
272
+ {
273
+ var text = issue.IsBlocked ? "Blocked" : issue.EscalationLevel > 0 ? EscalationLabel(issue.EscalationLevel) : IsOverdue(issue) ? "Overdue" : "Clear";
274
+ var cls = issue.IsBlocked || IsOverdue(issue) ? "bg-rose-50 text-rose-700" : issue.EscalationLevel > 0 ? "bg-amber-50 text-amber-700" : "bg-emerald-50 text-emerald-700";
275
+ builder.OpenElement(0, "span");
276
+ builder.AddAttribute(1, "class", $"rounded-full px-2 py-0.5 text-xs font-semibold {cls}");
277
+ builder.AddContent(2, text);
278
+ builder.CloseElement();
279
+ };
280
+
281
+ private IReadOnlyList<(string Label, int Value)> StatusChart => new[] { ("To Do", FilteredIssues.Count(i => i.StatusId == AppTaskStatus.Todo)), ("In Progress", FilteredIssues.Count(i => i.StatusId == AppTaskStatus.InProgress)), ("Done", FilteredIssues.Count(i => i.StatusId == AppTaskStatus.Done)), ("Overdue", OverdueIssues) };
282
+ private IReadOnlyList<(string Label, int Value)> PriorityChart => Enum.GetValues<TaskPriority>().Select(p => (GetPriorityLabel(p), FilteredIssues.Count(i => i.PriorityId == p))).ToList();
283
+ private IReadOnlyList<(string Label, int Value)> EmployeeChart => FilteredIssues.GroupBy(i => i.AssignedToName ?? "Unassigned").OrderByDescending(g => g.Count()).Take(5).Select(g => (g.Key, g.Count())).ToList();
284
+ private IReadOnlyList<(string Label, int Value)> ProjectChart => FilteredIssues.GroupBy(i => i.ProjectName).OrderByDescending(g => g.Count()).Take(5).Select(g => (g.Key, g.Count())).ToList();
285
+ private IEnumerable<string> Insights => new[] { $"{OverdueIssues} overdue issues need attention.", $"{BlockedIssues} issues are blocked.", $"Actual hours are {FilteredIssues.Sum(i => i.ActualHours ?? 0):N1} against {FilteredIssues.Sum(i => i.EstimatedHours ?? 0):N1} estimated." };
286
+
287
+ private RenderFragment MetricCard(string label, string value, string icon, string color) => builder =>
288
+ {
289
+ builder.OpenElement(0, "div");
290
+ builder.AddAttribute(1, "class", $"rounded-xl border border-{color}-100 bg-{color}-50 p-5 shadow-sm");
291
+ builder.AddMarkupContent(2, $"<div class=\"flex items-center justify-between\"><div><p class=\"text-xs font-semibold uppercase tracking-wider text-{color}-600\">{label}</p><p class=\"mt-1 text-3xl font-bold text-{color}-700\">{value}</p></div><span class=\"flex h-11 w-11 items-center justify-center rounded-xl bg-white/70\"><i data-lucide=\"{icon}\" class=\"h-6 w-6 text-{color}-600\"></i></span></div>");
292
+ builder.CloseElement();
293
+ };
294
+
295
+ private RenderFragment BarChart(string title, IReadOnlyList<(string Label, int Value)> rows) => builder =>
296
+ {
297
+ var max = Math.Max(1, rows.Select(r => r.Value).DefaultIfEmpty(0).Max());
298
+ builder.OpenElement(0, "div");
299
+ builder.AddAttribute(1, "class", "rounded-xl border border-slate-100 bg-slate-50 p-4");
300
+ builder.AddMarkupContent(2, $"<h3 class=\"mb-4 text-sm font-semibold text-slate-900\">{title}</h3>");
301
+ var seq = 3;
302
+ foreach (var row in rows)
303
+ {
304
+ var width = row.Value * 100 / max;
305
+ builder.AddMarkupContent(seq++, $"<div class=\"mb-3\"><div class=\"mb-1 flex justify-between text-xs font-semibold text-slate-600\"><span>{row.Label}</span><span>{row.Value}</span></div><div class=\"h-2.5 rounded-full bg-white\"><div class=\"h-full rounded-full bg-violet-600\" style=\"width:{width}%\"></div></div></div>");
306
+ }
307
+ builder.CloseElement();
308
+ };
309
+
310
+ private async Task DownloadExcel()
311
+ {
312
+ var rows = FilteredIssues.Select(i => new object?[] { i.Title, i.TaskTitle, i.ProjectName, i.AssignedToName ?? "Unassigned", GetStatusLabel(i.StatusId), GetPriorityLabel(i.PriorityId), i.EstimatedHours ?? 0, i.ActualHours ?? 0, i.DueDate, i.IsBlocked ? "Blocked" : "Clear", EscalationLabel(i.EscalationLevel), i.DelayReason ?? "" });
313
+ var bytes = SpreadsheetReportBuilder.BuildTableWorkbook("Issue Report", "Issue Report", new[] { "Issue", "Task", "Project", "Assignee", "Status", "Priority", "Estimated", "Actual", "Due Date", "Blocked", "Escalation", "Delay Reason" }, rows, new[] { $"Generated: {DisplayFormats.Date(DateTime.Today)}" });
314
+ await JS.InvokeVoidAsync("downloadFileFromBase64", $"IssueReport_{DateTime.Today:yyyyMMdd}.xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", Convert.ToBase64String(bytes));
315
+ }
316
+
317
+ private async Task DownloadPdf()
318
+ {
319
+ var rows = FilteredIssues.Select(i => new[] { i.Title, i.TaskTitle, i.ProjectName, i.AssignedToName ?? "Unassigned", GetStatusLabel(i.StatusId), DisplayFormats.Date(i.DueDate), i.IsBlocked ? "Blocked" : "Clear" });
320
+ var bytes = SimplePdfReportBuilder.BuildTableReport("Issue Report", new[] { $"Generated: {DisplayFormats.Date(DateTime.Today)}" }, new[] { "Issue", "Task", "Project", "Assignee", "Status", "Due Date", "Risk" }, rows);
321
+ await JS.InvokeVoidAsync("downloadFileFromBase64", $"IssueReport_{DateTime.Today:yyyyMMdd}.pdf", "application/pdf", Convert.ToBase64String(bytes));
322
+ }
323
+
324
+ private sealed record SavedFilter(string Search, long ProjectId, string Status);
325
+ }
TaskTrackingSystem.WebApp/Components/Pages/Features/Reports/OverdueTasksReport.razor CHANGED
@@ -51,7 +51,7 @@
51
  <div class="rounded-xl border border-amber-100 bg-amber-50 p-5 shadow-sm">
52
  <div class="flex items-center justify-between">
53
  <div>
54
- <p class="text-xs font-semibold uppercase tracking-wider text-amber-500">Critical (High Priority)</p>
55
  <p class="mt-1 text-3xl font-bold text-amber-700">@criticalCount</p>
56
  </div>
57
  <span class="flex h-11 w-11 items-center justify-center rounded-xl bg-amber-100">
@@ -151,8 +151,8 @@
151
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Assignee</th>
152
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Due Date</th>
153
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Days Overdue</th>
154
- <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Priority</th>
155
- <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Status</th>
156
  </tr>
157
  </thead>
158
  <tbody class="divide-y divide-slate-100">
@@ -215,26 +215,13 @@
215
  </span>
216
  }
217
  </td>
 
 
 
218
  <td class="px-6 py-4">
219
- @{
220
- var priorityClass = task.PriorityName switch {
221
- "High" => "bg-rose-50 text-rose-700",
222
- "Medium" => "bg-amber-50 text-amber-700",
223
- _ => "bg-slate-100 text-slate-600"
224
- };
225
- }
226
- <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium @priorityClass">@task.PriorityName</span>
227
- </td>
228
- <td class="px-6 py-4">
229
- @{
230
- var statusClass = task.StatusName switch {
231
- "In Progress" => "bg-blue-50 text-blue-700",
232
- "Done" => "bg-emerald-50 text-emerald-700",
233
- _ => "bg-slate-100 text-slate-600"
234
- };
235
- }
236
- <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium @statusClass">@task.StatusName</span>
237
- </td>
238
  </tr>
239
  }
240
  }
@@ -320,7 +307,7 @@
320
  var now = DateTime.UtcNow;
321
 
322
  overdueCount = allOverdue.Count(t => t.DueDate < now);
323
- criticalCount = allOverdue.Count(t => t.PriorityName == "High");
324
  dueSoonCount = allOverdue.Count(t => t.DueDate >= now && t.DueDate <= now.AddDays(3));
325
  avgDaysOverdue = overdueCount > 0
326
  ? (int)allOverdue.Where(t => t.DueDate < now).Average(t => t.DaysOverdue)
@@ -443,4 +430,12 @@
443
  "application/pdf",
444
  Convert.ToBase64String(pdfBytes));
445
  }
 
 
 
 
 
 
 
 
446
  }
 
51
  <div class="rounded-xl border border-amber-100 bg-amber-50 p-5 shadow-sm">
52
  <div class="flex items-center justify-between">
53
  <div>
54
+ <p class="text-xs font-semibold uppercase tracking-wider text-amber-500">High Priority Overdue</p>
55
  <p class="mt-1 text-3xl font-bold text-amber-700">@criticalCount</p>
56
  </div>
57
  <span class="flex h-11 w-11 items-center justify-center rounded-xl bg-amber-100">
 
151
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Assignee</th>
152
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Due Date</th>
153
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Days Overdue</th>
154
+ <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Issues</th>
155
+ <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Delay / Escalation</th>
156
  </tr>
157
  </thead>
158
  <tbody class="divide-y divide-slate-100">
 
215
  </span>
216
  }
217
  </td>
218
+ <td class="px-6 py-4 text-sm text-slate-700">
219
+ <span class="font-semibold text-rose-600">@task.OverdueIssues</span> overdue
220
+ </td>
221
  <td class="px-6 py-4">
222
+ <div class="text-sm font-medium text-slate-800">@(string.IsNullOrWhiteSpace(task.DelayReason) ? "No reason recorded" : task.DelayReason)</div>
223
+ <div class="mt-1 text-xs text-slate-500">Blocked by: @(string.IsNullOrWhiteSpace(task.BlockedBy) ? "None" : task.BlockedBy) · Escalation: @GetEscalationLabel(task.EscalationLevel)</div>
224
+ </td>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
225
  </tr>
226
  }
227
  }
 
307
  var now = DateTime.UtcNow;
308
 
309
  overdueCount = allOverdue.Count(t => t.DueDate < now);
310
+ criticalCount = allOverdue.Count(t => t.PriorityName == "High" && t.DaysOverdue > 0);
311
  dueSoonCount = allOverdue.Count(t => t.DueDate >= now && t.DueDate <= now.AddDays(3));
312
  avgDaysOverdue = overdueCount > 0
313
  ? (int)allOverdue.Where(t => t.DueDate < now).Average(t => t.DaysOverdue)
 
430
  "application/pdf",
431
  Convert.ToBase64String(pdfBytes));
432
  }
433
+
434
+ private static string GetEscalationLabel(int level) => level switch
435
+ {
436
+ 1 => "Team Lead",
437
+ 2 => "PM",
438
+ 3 => "Manager",
439
+ _ => "None"
440
+ };
441
  }
TaskTrackingSystem.WebApp/Components/Pages/Features/Reports/ProjectProgressReport.razor CHANGED
@@ -19,7 +19,7 @@
19
  <div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
20
  <div>
21
  <h2 class="text-3xl font-bold tracking-tight text-slate-900">Project Progress</h2>
22
- <p class="text-slate-500 mt-1">View project completion status, timelines, and progress charts.</p>
23
  </div>
24
  <div class="flex items-center space-x-3">
25
  <div class="inline-flex rounded-lg border border-slate-200 bg-white p-0.5 shadow-sm">
@@ -154,7 +154,7 @@
154
  {
155
  <!-- Chart View: SVG Project Progress Bars -->
156
  <div class="rounded-xl border border-slate-200 bg-white p-6 shadow-sm">
157
- <h3 class="font-semibold text-slate-900 mb-4 font-sans text-md">Project Completion Chart</h3>
158
  @if (!allProjects.Any())
159
  {
160
  <p class="text-slate-400 text-center py-8">No data available.</p>
@@ -167,10 +167,10 @@
167
  <div>
168
  <div class="flex justify-between text-xs font-semibold text-slate-700 mb-1.5">
169
  <span>@proj.ProjectName</span>
170
- <span>@proj.CompletedTasks / @proj.TotalTasks tasks (@proj.Progress.ToString("F0")% completed)</span>
171
  </div>
172
  <div class="h-4 rounded-full bg-slate-100 overflow-hidden flex">
173
- <div class="h-full bg-emerald-500 rounded-full transition-all" style="width: @(Math.Min(proj.Progress, 100))%"></div>
174
  </div>
175
  </div>
176
  }
@@ -189,9 +189,10 @@
189
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Project Name</th>
190
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Start Date</th>
191
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">End Date</th>
192
- <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Total Tasks</th>
193
- <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Completed Tasks</th>
194
- <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Completeness</th>
 
195
  </tr>
196
  </thead>
197
  <tbody class="divide-y divide-slate-100">
@@ -205,23 +206,17 @@
205
  <td class="px-6 py-4 text-sm font-semibold text-slate-900">@proj.ProjectName</td>
206
  <td class="px-6 py-4 text-sm text-slate-600">@DisplayFormats.Date(proj.StartDate)</td>
207
  <td class="px-6 py-4 text-sm text-slate-600">@DisplayFormats.Date(proj.EndDate)</td>
208
- <td class="px-6 py-4 text-sm text-slate-900">@proj.TotalTasks</td>
209
- <td class="px-6 py-4 text-sm text-emerald-600">@proj.CompletedTasks</td>
210
- <td class="px-6 py-4">
211
- <div class="flex items-center space-x-3">
212
- <div class="flex-1 h-2.5 rounded-full bg-slate-100 overflow-hidden max-w-[120px]">
213
- <div class="h-full rounded-full bg-emerald-500 transition-all" style="width: @(Math.Min(proj.Progress, 100))%"></div>
214
- </div>
215
- <span class="text-sm font-semibold text-slate-700">@proj.Progress.ToString("F0")%</span>
216
- </div>
217
- </td>
218
  </tr>
219
  }
220
  }
221
  else
222
  {
223
  <tr>
224
- <td colspan="7" class="px-6 py-12 text-center">
225
  <p class="text-sm text-slate-400">No project data found matching criteria.</p>
226
  </td>
227
  </tr>
@@ -398,15 +393,17 @@
398
  proj.ProjectName,
399
  proj.StartDate,
400
  proj.EndDate,
401
- proj.TotalTasks,
402
- proj.CompletedTasks,
403
- $"{proj.Progress:F0}%"
 
 
404
  });
405
 
406
  var workbookBytes = SpreadsheetReportBuilder.BuildTableWorkbook(
407
  "Project Progress",
408
  "Project Progress Report",
409
- new[] { "Project", "Start Date", "End Date", "Total Tasks", "Completed Tasks", "Completeness" },
410
  rows,
411
  new[]
412
  {
@@ -438,15 +435,17 @@
438
  proj.ProjectName,
439
  DisplayFormats.Date(proj.StartDate),
440
  DisplayFormats.Date(proj.EndDate),
441
- proj.TotalTasks.ToString(),
442
- proj.CompletedTasks.ToString(),
443
- $"{proj.Progress:F0}%"
 
 
444
  });
445
 
446
  var pdfBytes = SimplePdfReportBuilder.BuildTableReport(
447
  "Project Progress Report",
448
  summaryLines,
449
- new[] { "Project", "Start Date", "End Date", "Total Tasks", "Completed Tasks", "Completeness" },
450
  rows);
451
 
452
  await JS.InvokeVoidAsync(
@@ -455,4 +454,11 @@
455
  "application/pdf",
456
  Convert.ToBase64String(pdfBytes));
457
  }
 
 
 
 
 
 
 
458
  }
 
19
  <div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
20
  <div>
21
  <h2 class="text-3xl font-bold tracking-tight text-slate-900">Project Progress</h2>
22
+ <p class="text-slate-500 mt-1">View project health using both planning tasks and daily execution issues.</p>
23
  </div>
24
  <div class="flex items-center space-x-3">
25
  <div class="inline-flex rounded-lg border border-slate-200 bg-white p-0.5 shadow-sm">
 
154
  {
155
  <!-- Chart View: SVG Project Progress Bars -->
156
  <div class="rounded-xl border border-slate-200 bg-white p-6 shadow-sm">
157
+ <h3 class="font-semibold text-slate-900 mb-4 font-sans text-md">Project Health Chart</h3>
158
  @if (!allProjects.Any())
159
  {
160
  <p class="text-slate-400 text-center py-8">No data available.</p>
 
167
  <div>
168
  <div class="flex justify-between text-xs font-semibold text-slate-700 mb-1.5">
169
  <span>@proj.ProjectName</span>
170
+ <span>@proj.OpenIssues open, @proj.OverdueIssues overdue, @proj.BlockedIssues blocked</span>
171
  </div>
172
  <div class="h-4 rounded-full bg-slate-100 overflow-hidden flex">
173
+ <div class="h-full @(proj.ProjectHealth == "Critical" ? "bg-rose-500" : proj.ProjectHealth == "At Risk" ? "bg-amber-500" : "bg-emerald-500") rounded-full transition-all" style="width: @(Math.Min(Math.Max(proj.OpenIssues + proj.OverdueIssues + proj.BlockedIssues, 1) * 10, 100))%"></div>
174
  </div>
175
  </div>
176
  }
 
189
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Project Name</th>
190
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Start Date</th>
191
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">End Date</th>
192
+ <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Open Issues</th>
193
+ <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Blocked / Overdue</th>
194
+ <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Health</th>
195
+ <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Last Activity</th>
196
  </tr>
197
  </thead>
198
  <tbody class="divide-y divide-slate-100">
 
206
  <td class="px-6 py-4 text-sm font-semibold text-slate-900">@proj.ProjectName</td>
207
  <td class="px-6 py-4 text-sm text-slate-600">@DisplayFormats.Date(proj.StartDate)</td>
208
  <td class="px-6 py-4 text-sm text-slate-600">@DisplayFormats.Date(proj.EndDate)</td>
209
+ <td class="px-6 py-4 text-sm text-slate-900">@proj.OpenIssues</td>
210
+ <td class="px-6 py-4 text-sm font-medium"><span class="text-amber-600">@proj.BlockedIssues</span> / <span class="text-rose-600">@proj.OverdueIssues</span></td>
211
+ <td class="px-6 py-4"><span class="inline-flex rounded-full px-2.5 py-0.5 text-xs font-semibold @GetProjectHealthBadge(proj.ProjectHealth)">@proj.ProjectHealth</span></td>
212
+ <td class="px-6 py-4 text-sm text-slate-600">@DisplayFormats.Date(proj.LastActivity)</td>
 
 
 
 
 
 
213
  </tr>
214
  }
215
  }
216
  else
217
  {
218
  <tr>
219
+ <td colspan="8" class="px-6 py-12 text-center">
220
  <p class="text-sm text-slate-400">No project data found matching criteria.</p>
221
  </td>
222
  </tr>
 
393
  proj.ProjectName,
394
  proj.StartDate,
395
  proj.EndDate,
396
+ proj.OpenIssues,
397
+ proj.OverdueIssues,
398
+ proj.BlockedIssues,
399
+ proj.ProjectHealth,
400
+ proj.ActualHours
401
  });
402
 
403
  var workbookBytes = SpreadsheetReportBuilder.BuildTableWorkbook(
404
  "Project Progress",
405
  "Project Progress Report",
406
+ new[] { "Project", "Start Date", "End Date", "Open Issues", "Overdue Issues", "Blocked Issues", "Health", "Actual Hours" },
407
  rows,
408
  new[]
409
  {
 
435
  proj.ProjectName,
436
  DisplayFormats.Date(proj.StartDate),
437
  DisplayFormats.Date(proj.EndDate),
438
+ proj.OpenIssues.ToString(),
439
+ proj.OverdueIssues.ToString(),
440
+ proj.BlockedIssues.ToString(),
441
+ proj.ProjectHealth,
442
+ proj.ActualHours.ToString("N1")
443
  });
444
 
445
  var pdfBytes = SimplePdfReportBuilder.BuildTableReport(
446
  "Project Progress Report",
447
  summaryLines,
448
+ new[] { "Project", "Start Date", "End Date", "Open", "Overdue", "Blocked", "Health", "Hours" },
449
  rows);
450
 
451
  await JS.InvokeVoidAsync(
 
454
  "application/pdf",
455
  Convert.ToBase64String(pdfBytes));
456
  }
457
+
458
+ private static string GetProjectHealthBadge(string health) => health switch
459
+ {
460
+ "Critical" => "bg-rose-50 text-rose-700",
461
+ "At Risk" => "bg-amber-50 text-amber-700",
462
+ _ => "bg-emerald-50 text-emerald-700"
463
+ };
464
  }
TaskTrackingSystem.WebApp/Components/Pages/Features/Reports/TaskReport.razor CHANGED
@@ -152,8 +152,37 @@
152
  </div>
153
  </div>
154
  }
 
 
 
 
 
 
 
 
 
 
 
 
155
  else
156
  {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
157
  @if (isChartView)
158
  {
159
  <!-- Chart View -->
@@ -250,6 +279,16 @@
250
  }
251
  </div>
252
  </div>
 
 
 
 
 
 
 
 
 
 
253
  </div>
254
  }
255
  else
@@ -264,8 +303,8 @@
264
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Project</th>
265
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Status</th>
266
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Priority</th>
267
- <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Assigned To</th>
268
- <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Due Date</th>
269
  </tr>
270
  </thead>
271
  <tbody class="divide-y divide-slate-100">
@@ -276,7 +315,9 @@
276
  var t = entry.t;
277
  <tr class="hover:bg-slate-50/50 transition-colors">
278
  <td class="px-6 py-3 text-sm font-semibold text-slate-500">@((currentPage - 1) * pageSize + entry.index + 1)</td>
279
- <td class="px-6 py-3 text-sm font-medium text-slate-900">@t.Title</td>
 
 
280
  <td class="px-6 py-3 text-sm text-slate-500">@t.ProjectName</td>
281
  <td class="px-6 py-3">
282
  <span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium @GetStatusBadge(t.StatusId)">@t.StatusName</span>
@@ -284,9 +325,24 @@
284
  <td class="px-6 py-3">
285
  <span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium @GetPriorityBadge(t.PriorityId)">@t.PriorityName</span>
286
  </td>
287
- <td class="px-6 py-3 text-sm text-slate-500">@(t.AssignedToUser ?? "Unassigned")</td>
288
- <td class="px-6 py-3 text-sm text-slate-500">@DisplayFormats.Date(t.DueDate)</td>
 
 
289
  </tr>
 
 
 
 
 
 
 
 
 
 
 
 
 
290
  }
291
  }
292
  else
@@ -327,8 +383,10 @@
327
  private bool isLoading = true;
328
  private bool isChartView = false;
329
  private List<TaskReportDto> taskReports = new();
 
330
  private PagedResult<TaskReportDto>? taskReportPage;
331
  private List<ProjectDto> projects = new();
 
332
 
333
  // Filters
334
  private string filterPeriod = "all";
@@ -399,6 +457,13 @@
399
 
400
  private IEnumerable<TaskReportDto> PagedList => taskReportPage?.Items ?? Enumerable.Empty<TaskReportDto>();
401
 
 
 
 
 
 
 
 
402
  protected override async Task OnInitializedAsync()
403
  {
404
  var client = ApiClient.CreateClient();
@@ -426,6 +491,14 @@
426
  isChartView = chartView;
427
  }
428
 
 
 
 
 
 
 
 
 
429
  private void ShowProjectSuggestions()
430
  {
431
  projectSearchQuery = projectSearchInput;
@@ -489,6 +562,7 @@
489
  var client = ApiClient.CreateClient();
490
  try
491
  {
 
492
  var query = BuildTaskReportQuery();
493
  var fullTask = client.GetFromJsonAsync<Result<List<TaskReportDto>>>(query);
494
  var pagedTask = client.GetFromJsonAsync<PagedResult<TaskReportDto>>(BuildTaskReportQuery(includePaging: true));
@@ -498,6 +572,10 @@
498
  {
499
  taskReports = fullTask.Result.Value;
500
  }
 
 
 
 
501
 
502
  if (pagedTask.Result != null)
503
  {
@@ -509,6 +587,9 @@
509
  catch (Exception ex)
510
  {
511
  Console.WriteLine($"Task report error: {ex.Message}");
 
 
 
512
  }
513
  finally
514
  {
@@ -616,4 +697,33 @@
616
  TaskPriority.High => "bg-red-100 text-red-700",
617
  _ => "bg-slate-100 text-slate-600"
618
  };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
619
  }
 
152
  </div>
153
  </div>
154
  }
155
+ else if (!string.IsNullOrWhiteSpace(errorMessage))
156
+ {
157
+ <div class="rounded-xl border border-rose-200 bg-rose-50 p-5 text-sm text-rose-700 shadow-sm">
158
+ <div class="flex items-start gap-3">
159
+ <i data-lucide="alert-triangle" class="mt-0.5 h-5 w-5 shrink-0"></i>
160
+ <div>
161
+ <p class="font-semibold">Task report could not load.</p>
162
+ <p class="mt-1">@errorMessage</p>
163
+ </div>
164
+ </div>
165
+ </div>
166
+ }
167
  else
168
  {
169
+ <div class="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 gap-4">
170
+ @TaskMetricCard("Total", FilteredList.Count().ToString(), "file-text", "violet")
171
+ @TaskMetricCard("Completed", FilteredList.Count(t => t.StatusId == AppTaskStatus.Done).ToString(), "check-circle-2", "emerald")
172
+ @TaskMetricCard("In Progress", FilteredList.Count(t => t.StatusId == AppTaskStatus.InProgress).ToString(), "timer", "blue")
173
+ @TaskMetricCard("Overdue", FilteredList.Count(t => t.DueDate.Date < DateTime.Today && t.StatusId != AppTaskStatus.Done).ToString(), "alarm-clock", "rose")
174
+ </div>
175
+
176
+ <div class="rounded-xl border border-slate-200 bg-white p-5 shadow-sm">
177
+ <h3 class="text-sm font-semibold text-slate-900">Key Insights</h3>
178
+ <div class="mt-3 grid grid-cols-1 gap-3 md:grid-cols-3">
179
+ @foreach (var insight in TaskInsights)
180
+ {
181
+ <div class="rounded-lg bg-slate-50 px-3 py-2 text-sm text-slate-700">@insight</div>
182
+ }
183
+ </div>
184
+ </div>
185
+
186
  @if (isChartView)
187
  {
188
  <!-- Chart View -->
 
279
  }
280
  </div>
281
  </div>
282
+
283
+ <div class="rounded-xl border border-slate-200 bg-white p-6 shadow-sm">
284
+ <h3 class="font-semibold text-slate-900 mb-4">Tasks by Project</h3>
285
+ @MiniBars(FilteredList.GroupBy(t => t.ProjectName).OrderByDescending(g => g.Count()).Take(5).Select(g => (g.Key, g.Count())).ToArray())
286
+ </div>
287
+
288
+ <div class="rounded-xl border border-slate-200 bg-white p-6 shadow-sm">
289
+ <h3 class="font-semibold text-slate-900 mb-4">Tasks with Most Issues</h3>
290
+ @MiniBars(FilteredList.OrderByDescending(t => t.OpenIssues + t.OverdueIssues).Take(5).Select(t => (t.Title, t.OpenIssues + t.OverdueIssues)).ToArray())
291
+ </div>
292
  </div>
293
  }
294
  else
 
303
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Project</th>
304
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Status</th>
305
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Priority</th>
306
+ <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Health</th>
307
+ <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Issues</th>
308
  </tr>
309
  </thead>
310
  <tbody class="divide-y divide-slate-100">
 
315
  var t = entry.t;
316
  <tr class="hover:bg-slate-50/50 transition-colors">
317
  <td class="px-6 py-3 text-sm font-semibold text-slate-500">@((currentPage - 1) * pageSize + entry.index + 1)</td>
318
+ <td class="px-6 py-3">
319
+ <button @onclick="() => ToggleExpanded(t.TaskId)" class="text-left text-sm font-medium text-slate-900 hover:text-violet-600">@t.Title</button>
320
+ </td>
321
  <td class="px-6 py-3 text-sm text-slate-500">@t.ProjectName</td>
322
  <td class="px-6 py-3">
323
  <span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium @GetStatusBadge(t.StatusId)">@t.StatusName</span>
 
325
  <td class="px-6 py-3">
326
  <span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium @GetPriorityBadge(t.PriorityId)">@t.PriorityName</span>
327
  </td>
328
+ <td class="px-6 py-3">
329
+ <span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-semibold @GetHealthBadge(t.TaskHealth)">@t.TaskHealth</span>
330
+ </td>
331
+ <td class="px-6 py-3 text-sm text-slate-600">@t.IssueSummary</td>
332
  </tr>
333
+ @if (expandedTasks.Contains(t.TaskId))
334
+ {
335
+ <tr class="bg-slate-50/60">
336
+ <td colspan="7" class="px-6 py-4">
337
+ <div class="grid grid-cols-1 gap-3 text-sm text-slate-600 md:grid-cols-4">
338
+ <div><span class="font-semibold text-slate-800">Assignee:</span> @(t.AssignedToUser ?? "Unassigned")</div>
339
+ <div><span class="font-semibold text-slate-800">Due:</span> @DisplayFormats.Date(t.DueDate)</div>
340
+ <div><span class="font-semibold text-slate-800">Open issues:</span> @t.OpenIssues</div>
341
+ <div><span class="font-semibold text-slate-800">Last activity:</span> @DisplayFormats.Date(t.LastActivity)</div>
342
+ </div>
343
+ </td>
344
+ </tr>
345
+ }
346
  }
347
  }
348
  else
 
383
  private bool isLoading = true;
384
  private bool isChartView = false;
385
  private List<TaskReportDto> taskReports = new();
386
+ private HashSet<long> expandedTasks = new();
387
  private PagedResult<TaskReportDto>? taskReportPage;
388
  private List<ProjectDto> projects = new();
389
+ private string? errorMessage;
390
 
391
  // Filters
392
  private string filterPeriod = "all";
 
457
 
458
  private IEnumerable<TaskReportDto> PagedList => taskReportPage?.Items ?? Enumerable.Empty<TaskReportDto>();
459
 
460
+ private IEnumerable<string> TaskInsights => new[]
461
+ {
462
+ $"{FilteredList.Count(t => t.TaskHealth == "Critical")} critical tasks need attention.",
463
+ $"{FilteredList.Sum(t => t.OpenIssues)} open issues are attached to reported tasks.",
464
+ $"{FilteredList.Count(t => t.OverdueIssues > 0)} tasks have overdue issues."
465
+ };
466
+
467
  protected override async Task OnInitializedAsync()
468
  {
469
  var client = ApiClient.CreateClient();
 
491
  isChartView = chartView;
492
  }
493
 
494
+ private void ToggleExpanded(long taskId)
495
+ {
496
+ if (!expandedTasks.Remove(taskId))
497
+ {
498
+ expandedTasks.Add(taskId);
499
+ }
500
+ }
501
+
502
  private void ShowProjectSuggestions()
503
  {
504
  projectSearchQuery = projectSearchInput;
 
562
  var client = ApiClient.CreateClient();
563
  try
564
  {
565
+ errorMessage = null;
566
  var query = BuildTaskReportQuery();
567
  var fullTask = client.GetFromJsonAsync<Result<List<TaskReportDto>>>(query);
568
  var pagedTask = client.GetFromJsonAsync<PagedResult<TaskReportDto>>(BuildTaskReportQuery(includePaging: true));
 
572
  {
573
  taskReports = fullTask.Result.Value;
574
  }
575
+ else if (fullTask.Result?.IsSuccess == false)
576
+ {
577
+ errorMessage = fullTask.Result.ErrorMessage ?? "The server returned an error while loading task report data.";
578
+ }
579
 
580
  if (pagedTask.Result != null)
581
  {
 
587
  catch (Exception ex)
588
  {
589
  Console.WriteLine($"Task report error: {ex.Message}");
590
+ errorMessage = ex.Message;
591
+ taskReports = new();
592
+ taskReportPage = null;
593
  }
594
  finally
595
  {
 
697
  TaskPriority.High => "bg-red-100 text-red-700",
698
  _ => "bg-slate-100 text-slate-600"
699
  };
700
+
701
+ private static string GetHealthBadge(string health) => health switch
702
+ {
703
+ "Critical" => "bg-rose-50 text-rose-700",
704
+ "At Risk" => "bg-amber-50 text-amber-700",
705
+ _ => "bg-emerald-50 text-emerald-700"
706
+ };
707
+
708
+ private RenderFragment TaskMetricCard(string label, string value, string icon, string color) => builder =>
709
+ {
710
+ builder.OpenElement(0, "div");
711
+ builder.AddAttribute(1, "class", $"rounded-xl border border-{color}-100 bg-{color}-50 p-5 shadow-sm");
712
+ builder.AddMarkupContent(2, $"<div class=\"flex items-center justify-between\"><div><p class=\"text-xs font-semibold uppercase tracking-wider text-{color}-600\">{label}</p><p class=\"mt-1 text-3xl font-bold text-{color}-700\">{value}</p></div><span class=\"flex h-11 w-11 items-center justify-center rounded-xl bg-white/70\"><i data-lucide=\"{icon}\" class=\"h-6 w-6 text-{color}-600\"></i></span></div>");
713
+ builder.CloseElement();
714
+ };
715
+
716
+ private RenderFragment MiniBars(IReadOnlyList<(string Label, int Value)> rows) => builder =>
717
+ {
718
+ var max = Math.Max(1, rows.Select(r => r.Value).DefaultIfEmpty(0).Max());
719
+ var seq = 0;
720
+ builder.OpenElement(seq++, "div");
721
+ builder.AddAttribute(seq++, "class", "space-y-3");
722
+ foreach (var row in rows)
723
+ {
724
+ var width = row.Value * 100 / max;
725
+ builder.AddMarkupContent(seq++, $"<div><div class=\"mb-1 flex justify-between text-xs font-semibold text-slate-600\"><span>{row.Label}</span><span>{row.Value}</span></div><div class=\"h-2.5 rounded-full bg-slate-100\"><div class=\"h-full rounded-full bg-violet-600\" style=\"width:{width}%\"></div></div></div>");
726
+ }
727
+ builder.CloseElement();
728
+ };
729
  }
TaskTrackingSystem.WebApp/Components/Pages/Features/Reports/TimeTrackingReport.razor CHANGED
@@ -22,6 +22,14 @@
22
  <p class="text-slate-500 mt-1">Review estimated hours, completion timelines, and resource effort distribution across issues.</p>
23
  </div>
24
  <div class="flex items-center space-x-3">
 
 
 
 
 
 
 
 
25
  <button @onclick="DownloadExcel" class="inline-flex items-center rounded-lg border border-slate-200 bg-white px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50 transition-colors shadow-sm">
26
  <i data-lucide="download" class="h-4 w-4 mr-2"></i> Export Excel
27
  </button>
@@ -49,8 +57,8 @@
49
  <div class="rounded-xl border border-emerald-100 bg-emerald-50 p-5 shadow-sm">
50
  <div class="flex items-center justify-between">
51
  <div>
52
- <p class="text-xs font-semibold uppercase tracking-wider text-emerald-500">Completed Effort</p>
53
- <p class="mt-1 text-3xl font-bold text-emerald-700">@completedHours.ToString("N1") hrs</p>
54
  </div>
55
  <span class="flex h-11 w-11 items-center justify-center rounded-xl bg-emerald-100">
56
  <i data-lucide="check-circle-2" class="h-6 w-6 text-emerald-600"></i>
@@ -60,8 +68,8 @@
60
  <div class="rounded-xl border border-amber-100 bg-amber-50 p-5 shadow-sm">
61
  <div class="flex items-center justify-between">
62
  <div>
63
- <p class="text-xs font-semibold uppercase tracking-wider text-amber-500">Active Effort (In-Flight)</p>
64
- <p class="mt-1 text-3xl font-bold text-amber-700">@activeHours.ToString("N1") hrs</p>
65
  </div>
66
  <span class="flex h-11 w-11 items-center justify-center rounded-xl bg-amber-100">
67
  <i data-lucide="timer" class="h-6 w-6 text-amber-600"></i>
@@ -71,8 +79,8 @@
71
  <div class="rounded-xl border border-rose-100 bg-rose-50 p-5 shadow-sm">
72
  <div class="flex items-center justify-between">
73
  <div>
74
- <p class="text-xs font-semibold uppercase tracking-wider text-rose-500">Overdue Effort</p>
75
- <p class="mt-1 text-3xl font-bold text-rose-700">@overdueHours.ToString("N1") hrs</p>
76
  </div>
77
  <span class="flex h-11 w-11 items-center justify-center rounded-xl bg-rose-100">
78
  <i data-lucide="alert-triangle" class="h-6 w-6 text-rose-600"></i>
@@ -137,6 +145,17 @@
137
  }
138
  else
139
  {
 
 
 
 
 
 
 
 
 
 
 
140
  <!-- Table -->
141
  <div class="rounded-xl border border-slate-200 bg-white shadow-sm overflow-hidden">
142
  <div class="px-6 py-4 border-b border-slate-100 flex items-center justify-between">
@@ -151,7 +170,8 @@
151
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Issue</th>
152
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Task &amp; Project</th>
153
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Assignee</th>
154
- <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Est. Hours</th>
 
155
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Due Date</th>
156
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Status</th>
157
  </tr>
@@ -195,6 +215,9 @@
195
  <td class="px-6 py-4 text-sm font-semibold text-slate-800">
196
  @((issue.EstimatedHours ?? 0).ToString("N1")) hrs
197
  </td>
 
 
 
198
  <td class="px-6 py-4 text-sm text-slate-600">
199
  @DisplayFormats.Date(issue.DueDate)
200
  </td>
@@ -213,7 +236,7 @@
213
  }
214
  else
215
  {
216
- <EmptyState ColSpan="7" Icon="clock" Title="No effort entries" Subtitle="No timesheet effort entries matched your filters." />
217
  }
218
  </tbody>
219
  </table>
@@ -226,11 +249,13 @@
226
  OnPageChanged="HandlePageChanged"
227
  OnPageSizeChanged="HandlePageSizeChanged" />
228
  </div>
 
229
  }
230
  </div>
231
 
232
  @code {
233
  private bool isLoading = true;
 
234
  private List<IssueDto> allIssues = new();
235
  private List<ProjectDto> allProjects = new();
236
 
@@ -254,9 +279,9 @@
254
 
255
  // Metrics
256
  private decimal totalHours = 0;
257
- private decimal completedHours = 0;
258
- private decimal activeHours = 0;
259
- private decimal overdueHours = 0;
260
 
261
  private int TotalPages => (int)Math.Ceiling((double)FilteredIssues.Count() / pageSize);
262
 
@@ -353,16 +378,38 @@
353
  await JS.InvokeVoidAsync("initIcons");
354
  }
355
 
 
 
 
 
 
356
  private void ComputeMetrics()
357
  {
358
  var today = DateTime.Today;
359
  var list = FilteredIssues.ToList();
360
  totalHours = list.Sum(i => i.EstimatedHours ?? 0);
361
- completedHours = list.Where(i => i.StatusId == AppTaskStatus.Done).Sum(i => i.EstimatedHours ?? 0);
362
- activeHours = list.Where(i => i.StatusId != AppTaskStatus.Done).Sum(i => i.EstimatedHours ?? 0);
363
- overdueHours = list.Where(i => i.StatusId != AppTaskStatus.Done && i.DueDate < today).Sum(i => i.EstimatedHours ?? 0);
364
  }
365
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
366
  private Task ApplyFilters()
367
  {
368
  appliedSearch = searchInput;
@@ -455,7 +502,7 @@
455
  $"Assigned To Me: {(filterAssignToMe ? "Yes" : "No")}",
456
  $"Assigned To My Team: {(filterAssignToMyTeam ? "Yes" : "No")}",
457
  $"Total Effort: {totalHours.ToString("N1")} hrs",
458
- $"Completed Effort: {completedHours.ToString("N1")} hrs",
459
  $"Generated: {DisplayFormats.Date(DateTime.Today)}"
460
  };
461
 
 
22
  <p class="text-slate-500 mt-1">Review estimated hours, completion timelines, and resource effort distribution across issues.</p>
23
  </div>
24
  <div class="flex items-center space-x-3">
25
+ <div class="inline-flex rounded-lg border border-slate-200 bg-white p-0.5 shadow-sm">
26
+ <button @onclick="() => SetView(false)" class="inline-flex items-center rounded-md px-3 py-1.5 text-xs font-semibold @(!isChartView ? "bg-violet-600 text-white" : "text-slate-600 hover:text-slate-900") transition-colors">
27
+ <i data-lucide="list" class="h-3.5 w-3.5 mr-1"></i> List View
28
+ </button>
29
+ <button @onclick="() => SetView(true)" class="inline-flex items-center rounded-md px-3 py-1.5 text-xs font-semibold @(isChartView ? "bg-violet-600 text-white" : "text-slate-600 hover:text-slate-900") transition-colors">
30
+ <i data-lucide="bar-chart-3" class="h-3.5 w-3.5 mr-1"></i> Chart View
31
+ </button>
32
+ </div>
33
  <button @onclick="DownloadExcel" class="inline-flex items-center rounded-lg border border-slate-200 bg-white px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50 transition-colors shadow-sm">
34
  <i data-lucide="download" class="h-4 w-4 mr-2"></i> Export Excel
35
  </button>
 
57
  <div class="rounded-xl border border-emerald-100 bg-emerald-50 p-5 shadow-sm">
58
  <div class="flex items-center justify-between">
59
  <div>
60
+ <p class="text-xs font-semibold uppercase tracking-wider text-emerald-500">Actual Hours</p>
61
+ <p class="mt-1 text-3xl font-bold text-emerald-700">@actualHours.ToString("N1") hrs</p>
62
  </div>
63
  <span class="flex h-11 w-11 items-center justify-center rounded-xl bg-emerald-100">
64
  <i data-lucide="check-circle-2" class="h-6 w-6 text-emerald-600"></i>
 
68
  <div class="rounded-xl border border-amber-100 bg-amber-50 p-5 shadow-sm">
69
  <div class="flex items-center justify-between">
70
  <div>
71
+ <p class="text-xs font-semibold uppercase tracking-wider text-amber-500">Variance</p>
72
+ <p class="mt-1 text-3xl font-bold text-amber-700">@varianceHours.ToString("+0.0;-0.0;0.0") hrs</p>
73
  </div>
74
  <span class="flex h-11 w-11 items-center justify-center rounded-xl bg-amber-100">
75
  <i data-lucide="timer" class="h-6 w-6 text-amber-600"></i>
 
79
  <div class="rounded-xl border border-rose-100 bg-rose-50 p-5 shadow-sm">
80
  <div class="flex items-center justify-between">
81
  <div>
82
+ <p class="text-xs font-semibold uppercase tracking-wider text-rose-500">Utilization</p>
83
+ <p class="mt-1 text-3xl font-bold text-rose-700">@utilizationPercent.ToString("N0")%</p>
84
  </div>
85
  <span class="flex h-11 w-11 items-center justify-center rounded-xl bg-rose-100">
86
  <i data-lucide="alert-triangle" class="h-6 w-6 text-rose-600"></i>
 
145
  }
146
  else
147
  {
148
+ @if (isChartView)
149
+ {
150
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
151
+ @EffortChart("Estimated vs Actual", new[] { ("Estimated", (double)totalHours), ("Actual", (double)actualHours) })
152
+ @EffortChart("Effort by Project", FilteredIssues.GroupBy(i => i.ProjectName).OrderByDescending(g => g.Sum(i => i.ActualHours ?? 0)).Take(5).Select(g => (g.Key, (double)g.Sum(i => i.ActualHours ?? 0))).ToArray())
153
+ @EffortChart("Effort by Employee", FilteredIssues.GroupBy(i => i.AssignedToName ?? "Unassigned").OrderByDescending(g => g.Sum(i => i.ActualHours ?? 0)).Take(5).Select(g => (g.Key, (double)g.Sum(i => i.ActualHours ?? 0))).ToArray())
154
+ @EffortChart("Estimate Accuracy", new[] { ("On / Under", (double)FilteredIssues.Count(i => (i.ActualHours ?? 0) <= (i.EstimatedHours ?? 0))), ("Over", (double)FilteredIssues.Count(i => (i.ActualHours ?? 0) > (i.EstimatedHours ?? 0))) })
155
+ </div>
156
+ }
157
+ else
158
+ {
159
  <!-- Table -->
160
  <div class="rounded-xl border border-slate-200 bg-white shadow-sm overflow-hidden">
161
  <div class="px-6 py-4 border-b border-slate-100 flex items-center justify-between">
 
170
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Issue</th>
171
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Task &amp; Project</th>
172
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Assignee</th>
173
+ <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Est. Hours</th>
174
+ <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Actual Hours</th>
175
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Due Date</th>
176
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Status</th>
177
  </tr>
 
215
  <td class="px-6 py-4 text-sm font-semibold text-slate-800">
216
  @((issue.EstimatedHours ?? 0).ToString("N1")) hrs
217
  </td>
218
+ <td class="px-6 py-4 text-sm font-semibold @(GetVariance(issue) > 0 ? "text-rose-600" : "text-emerald-600")">
219
+ @((issue.ActualHours ?? 0).ToString("N1")) hrs
220
+ </td>
221
  <td class="px-6 py-4 text-sm text-slate-600">
222
  @DisplayFormats.Date(issue.DueDate)
223
  </td>
 
236
  }
237
  else
238
  {
239
+ <EmptyState ColSpan="8" Icon="clock" Title="No effort entries" Subtitle="No timesheet effort entries matched your filters." />
240
  }
241
  </tbody>
242
  </table>
 
249
  OnPageChanged="HandlePageChanged"
250
  OnPageSizeChanged="HandlePageSizeChanged" />
251
  </div>
252
+ }
253
  }
254
  </div>
255
 
256
  @code {
257
  private bool isLoading = true;
258
+ private bool isChartView = false;
259
  private List<IssueDto> allIssues = new();
260
  private List<ProjectDto> allProjects = new();
261
 
 
279
 
280
  // Metrics
281
  private decimal totalHours = 0;
282
+ private decimal actualHours = 0;
283
+ private decimal varianceHours = 0;
284
+ private double utilizationPercent = 0;
285
 
286
  private int TotalPages => (int)Math.Ceiling((double)FilteredIssues.Count() / pageSize);
287
 
 
378
  await JS.InvokeVoidAsync("initIcons");
379
  }
380
 
381
+ private void SetView(bool chartView)
382
+ {
383
+ isChartView = chartView;
384
+ }
385
+
386
  private void ComputeMetrics()
387
  {
388
  var today = DateTime.Today;
389
  var list = FilteredIssues.ToList();
390
  totalHours = list.Sum(i => i.EstimatedHours ?? 0);
391
+ actualHours = list.Sum(i => i.ActualHours ?? 0);
392
+ varianceHours = actualHours - totalHours;
393
+ utilizationPercent = totalHours > 0 ? Math.Round((double)(actualHours / totalHours) * 100, 1) : 0;
394
  }
395
 
396
+ private static decimal GetVariance(IssueDto issue) => (issue.ActualHours ?? 0) - (issue.EstimatedHours ?? 0);
397
+
398
+ private RenderFragment EffortChart(string title, IReadOnlyList<(string Label, double Value)> rows) => builder =>
399
+ {
400
+ var max = Math.Max(1, rows.Select(r => r.Value).DefaultIfEmpty(0).Max());
401
+ builder.OpenElement(0, "div");
402
+ builder.AddAttribute(1, "class", "rounded-xl border border-slate-200 bg-white p-6 shadow-sm");
403
+ builder.AddMarkupContent(2, $"<h3 class=\"mb-4 text-sm font-semibold text-slate-900\">{title}</h3>");
404
+ var seq = 3;
405
+ foreach (var row in rows)
406
+ {
407
+ var width = row.Value * 100 / max;
408
+ builder.AddMarkupContent(seq++, $"<div class=\"mb-4\"><div class=\"mb-1 flex justify-between text-xs font-semibold text-slate-600\"><span>{row.Label}</span><span>{row.Value:N1}</span></div><div class=\"h-3 rounded-full bg-slate-100\"><div class=\"h-full rounded-full bg-violet-600\" style=\"width:{width}%\"></div></div></div>");
409
+ }
410
+ builder.CloseElement();
411
+ };
412
+
413
  private Task ApplyFilters()
414
  {
415
  appliedSearch = searchInput;
 
502
  $"Assigned To Me: {(filterAssignToMe ? "Yes" : "No")}",
503
  $"Assigned To My Team: {(filterAssignToMyTeam ? "Yes" : "No")}",
504
  $"Total Effort: {totalHours.ToString("N1")} hrs",
505
+ $"Actual Hours: {actualHours.ToString("N1")} hrs",
506
  $"Generated: {DisplayFormats.Date(DateTime.Today)}"
507
  };
508