User commited on
Commit
ecb51fa
·
1 Parent(s): c7beb09

fix: adjust overall structure, clean out unnecessary fields, adjust table columns and layouts on task list and issue list

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. TaskTrackingSystem.Database/AppDbContextModels/AppDbContext.cs +0 -1
  2. TaskTrackingSystem.Shared/Models/Report/OverdueCriticalTaskDto.cs +1 -0
  3. TaskTrackingSystem.WebApi/Features/Audit/AuditLogController.cs +9 -2
  4. TaskTrackingSystem.WebApi/Features/Dashboard/DashboardController.cs +3 -3
  5. TaskTrackingSystem.WebApi/Features/Dashboard/DashboardService.cs +21 -23
  6. TaskTrackingSystem.WebApi/Features/Issue/IssueController.cs +7 -7
  7. TaskTrackingSystem.WebApi/Features/Issue/IssueService.cs +35 -33
  8. TaskTrackingSystem.WebApi/Features/Menu/MenuController.cs +1 -1
  9. TaskTrackingSystem.WebApi/Features/Notification/NotificationRealtimeService.cs +1 -6
  10. TaskTrackingSystem.WebApi/Features/Project/ProjectController.cs +9 -9
  11. TaskTrackingSystem.WebApi/Features/Project/ProjectService.cs +31 -27
  12. TaskTrackingSystem.WebApi/Features/Report/ReportController.cs +46 -48
  13. TaskTrackingSystem.WebApi/Features/Report/ReportService.cs +186 -94
  14. TaskTrackingSystem.WebApi/Features/Task/TaskController.cs +8 -8
  15. TaskTrackingSystem.WebApi/Features/Task/TaskDetailsController.cs +1 -1
  16. TaskTrackingSystem.WebApi/Features/Task/TaskService.cs +32 -35
  17. TaskTrackingSystem.WebApi/Features/User/UserController.cs +19 -0
  18. TaskTrackingSystem.WebApi/Features/User/UserService.cs +21 -0
  19. TaskTrackingSystem.WebApi/Features/UserDevice/UserDeviceService.cs +0 -18
  20. TaskTrackingSystem.WebApi/Features/UserDevice/UserDevicesController.cs +0 -12
  21. TaskTrackingSystem.WebApi/Infrastructure/ClaimsPrincipalExtensions.cs +1 -8
  22. TaskTrackingSystem.WebApi/Infrastructure/DataScopeAuthorization.cs +29 -0
  23. TaskTrackingSystem.WebApi/Infrastructure/PermissionAuthorizationService.cs +0 -5
  24. TaskTrackingSystem.WebApp/Components/App.razor +0 -1
  25. TaskTrackingSystem.WebApp/Components/Layout/Component.razor +0 -0
  26. TaskTrackingSystem.WebApp/Components/Layout/NavMenu.razor +4 -1
  27. TaskTrackingSystem.WebApp/Components/Pages/Component.razor +0 -0
  28. TaskTrackingSystem.WebApp/Components/Pages/{AuditLogs.razor → Features/Audit/AuditLogs.razor} +1 -1
  29. TaskTrackingSystem.WebApp/Components/Pages/{Login.razor → Features/Auth/Login.razor} +0 -0
  30. TaskTrackingSystem.WebApp/Components/Pages/{Register.razor → Features/Auth/Register.razor} +0 -0
  31. TaskTrackingSystem.WebApp/Components/Pages/{ResetPassword.razor → Features/Auth/ResetPassword.razor} +0 -0
  32. TaskTrackingSystem.WebApp/Components/Pages/{Error.razor → Features/Common/Error.razor} +0 -0
  33. TaskTrackingSystem.WebApp/Components/Pages/{EmployeeDashboard.razor → Features/Dashboard/EmployeeDashboard.razor} +1 -1
  34. TaskTrackingSystem.WebApp/Components/Pages/{Home.razor → Features/Dashboard/Home.razor} +22 -6
  35. TaskTrackingSystem.WebApp/Components/Pages/{ManagerDashboard.razor → Features/Dashboard/ManagerDashboard.razor} +1 -1
  36. TaskTrackingSystem.WebApp/Components/Pages/{AddIssue.razor → Features/Issues/AddIssue.razor} +0 -0
  37. TaskTrackingSystem.WebApp/Components/Pages/{Issues.razor → Features/Issues/Issues.razor} +44 -47
  38. TaskTrackingSystem.WebApp/Components/Pages/{ProjectAssign.razor → Features/Projects/ProjectAssign.razor} +0 -0
  39. TaskTrackingSystem.WebApp/Components/Pages/{Projects.razor → Features/Projects/Projects.razor} +0 -0
  40. TaskTrackingSystem.WebApp/Components/Pages/{EmployeeReport.razor → Features/Reports/EmployeeReport.razor} +76 -148
  41. TaskTrackingSystem.WebApp/Components/Pages/{OverdueTasksReport.razor → Features/Reports/OverdueTasksReport.razor} +99 -272
  42. TaskTrackingSystem.WebApp/Components/Pages/{ProjectProgressReport.razor → Features/Reports/ProjectProgressReport.razor} +74 -165
  43. TaskTrackingSystem.WebApp/Components/Pages/{TaskReport.razor → Features/Reports/TaskReport.razor} +23 -7
  44. TaskTrackingSystem.WebApp/Components/Pages/Features/Reports/TimeTrackingReport.razor +494 -0
  45. TaskTrackingSystem.WebApp/Components/Pages/{Roles.razor → Features/Roles/Roles.razor} +0 -0
  46. TaskTrackingSystem.WebApp/Components/Pages/{KanbanBoard.razor → Features/Tasks/KanbanBoard.razor} +0 -0
  47. TaskTrackingSystem.WebApp/Components/Pages/{TaskAssign.razor → Features/Tasks/TaskAssign.razor} +0 -0
  48. TaskTrackingSystem.WebApp/Components/Pages/{TaskDetails.razor → Features/Tasks/TaskDetails.razor} +0 -0
  49. TaskTrackingSystem.WebApp/Components/Pages/{Tasks.razor → Features/Tasks/Tasks.razor} +26 -12
  50. TaskTrackingSystem.WebApp/Components/Pages/{Users.razor → Features/Users/Users.razor} +0 -0
TaskTrackingSystem.Database/AppDbContextModels/AppDbContext.cs CHANGED
@@ -49,7 +49,6 @@ public partial class AppDbContext : DbContext
49
  {
50
  if (!optionsBuilder.IsConfigured)
51
  {
52
- #warning To protect potentially sensitive information in your connection string, you should move it out of source code. You can avoid scaffolding the connection string by using the Name= syntax to read it from configuration - see https://go.microsoft.com/fwlink/?linkid=2131148. For more guidance on storing connection strings, see https://go.microsoft.com/fwlink/?LinkId=723263.
53
  optionsBuilder.UseSqlServer("Server=.;Database=TTS;Trusted_Connection=True;TrustServerCertificate=True;Connect Timeout=5;");
54
  }
55
  }
 
49
  {
50
  if (!optionsBuilder.IsConfigured)
51
  {
 
52
  optionsBuilder.UseSqlServer("Server=.;Database=TTS;Trusted_Connection=True;TrustServerCertificate=True;Connect Timeout=5;");
53
  }
54
  }
TaskTrackingSystem.Shared/Models/Report/OverdueCriticalTaskDto.cs CHANGED
@@ -11,6 +11,7 @@ namespace TaskTrackingSystem.Shared.Models.Report
11
  public string StatusName { get; set; } = string.Empty;
12
  public string PriorityName { get; set; } = string.Empty;
13
  public string? AssignedTo { get; set; }
 
14
  public DateTime DueDate { get; set; }
15
  public int DaysOverdue { get; set; }
16
  public DateTime CreatedAt { get; set; }
 
11
  public string StatusName { get; set; } = string.Empty;
12
  public string PriorityName { get; set; } = string.Empty;
13
  public string? AssignedTo { get; set; }
14
+ public long? AssignedToUserId { get; set; }
15
  public DateTime DueDate { get; set; }
16
  public int DaysOverdue { get; set; }
17
  public DateTime CreatedAt { get; set; }
TaskTrackingSystem.WebApi/Features/Audit/AuditLogController.cs CHANGED
@@ -14,14 +14,16 @@ namespace TaskTrackingSystem.WebApi.Features.Audit
14
  {
15
  [Route("api/[controller]")]
16
  [ApiController]
17
- [Authorize(Roles = "Admin")]
18
  public class AuditLogController : ControllerBase
19
  {
20
  private readonly AppDbContext _db;
 
21
 
22
- public AuditLogController(AppDbContext db)
23
  {
24
  _db = db;
 
25
  }
26
 
27
  [HttpGet]
@@ -31,6 +33,11 @@ namespace TaskTrackingSystem.WebApi.Features.Audit
31
  [FromQuery] string? search,
32
  [FromQuery] PaginationQuery? paging = null)
33
  {
 
 
 
 
 
34
  var query = _db.AuditLogs
35
  .Include(a => a.User)
36
  .AsQueryable();
 
14
  {
15
  [Route("api/[controller]")]
16
  [ApiController]
17
+ [Authorize]
18
  public class AuditLogController : ControllerBase
19
  {
20
  private readonly AppDbContext _db;
21
+ private readonly PermissionAuthorizationService _permissionAuthorizationService;
22
 
23
+ public AuditLogController(AppDbContext db, PermissionAuthorizationService permissionAuthorizationService)
24
  {
25
  _db = db;
26
+ _permissionAuthorizationService = permissionAuthorizationService;
27
  }
28
 
29
  [HttpGet]
 
33
  [FromQuery] string? search,
34
  [FromQuery] PaginationQuery? paging = null)
35
  {
36
+ if (!await _permissionAuthorizationService.CanAccessAsync(User, "api/Role", "List"))
37
+ {
38
+ return Forbid();
39
+ }
40
+
41
  var query = _db.AuditLogs
42
  .Include(a => a.User)
43
  .AsQueryable();
TaskTrackingSystem.WebApi/Features/Dashboard/DashboardController.cs CHANGED
@@ -23,21 +23,21 @@ namespace TaskTrackingSystem.WebApi.Features.Dashboard
23
  [HttpGet("summary")]
24
  public async Task<ActionResult<Result<DashboardSummaryDto>>> GetSummary()
25
  {
26
- var result = await _dashboardService.GetSummaryAsync(User.GetRoleName(), User.GetUserId());
27
  return StatusCode(result.StatusCode, result);
28
  }
29
 
30
  [HttpGet("tasks-overview")]
31
  public async Task<ActionResult<Result<IEnumerable<TaskStatusOverviewDto>>>> GetTasksOverview()
32
  {
33
- var result = await _dashboardService.GetTasksOverviewAsync(User.GetRoleName(), User.GetUserId());
34
  return StatusCode(result.StatusCode, result);
35
  }
36
 
37
  [HttpGet("project-progress")]
38
  public async Task<ActionResult<Result<IEnumerable<ProjectProgressDto>>>> GetProjectProgress()
39
  {
40
- var result = await _dashboardService.GetProjectProgressAsync(User.GetRoleName(), User.GetUserId());
41
  return StatusCode(result.StatusCode, result);
42
  }
43
  }
 
23
  [HttpGet("summary")]
24
  public async Task<ActionResult<Result<DashboardSummaryDto>>> GetSummary()
25
  {
26
+ var result = await _dashboardService.GetSummaryAsync(User.GetRoleId(), User.GetUserId());
27
  return StatusCode(result.StatusCode, result);
28
  }
29
 
30
  [HttpGet("tasks-overview")]
31
  public async Task<ActionResult<Result<IEnumerable<TaskStatusOverviewDto>>>> GetTasksOverview()
32
  {
33
+ var result = await _dashboardService.GetTasksOverviewAsync(User.GetRoleId(), User.GetUserId());
34
  return StatusCode(result.StatusCode, result);
35
  }
36
 
37
  [HttpGet("project-progress")]
38
  public async Task<ActionResult<Result<IEnumerable<ProjectProgressDto>>>> GetProjectProgress()
39
  {
40
+ var result = await _dashboardService.GetProjectProgressAsync(User.GetRoleId(), User.GetUserId());
41
  return StatusCode(result.StatusCode, result);
42
  }
43
  }
TaskTrackingSystem.WebApi/Features/Dashboard/DashboardService.cs CHANGED
@@ -7,6 +7,7 @@ using TaskTrackingSystem.Database.AppDbContextModels;
7
  using TaskTrackingSystem.Shared;
8
  using TaskTrackingSystem.Shared.Enums;
9
  using TaskTrackingSystem.Shared.Models.Dashboard;
 
10
 
11
  namespace TaskTrackingSystem.WebApi.Features.Dashboard
12
  {
@@ -19,12 +20,15 @@ namespace TaskTrackingSystem.WebApi.Features.Dashboard
19
  _db = db;
20
  }
21
 
22
- public async Task<Result<DashboardSummaryDto>> GetSummaryAsync(string roleName, long currentUserId)
23
  {
24
- var projects = BuildAccessibleProjectQuery(roleName, currentUserId);
25
- var tasks = BuildAccessibleTaskQuery(roleName, currentUserId);
26
 
27
- var totalUsers = IsAdmin(roleName)
 
 
 
28
  ? await _db.Users.CountAsync(u => !u.IsDeleted)
29
  : await _db.Users
30
  .Where(u => !u.IsDeleted &&
@@ -46,9 +50,12 @@ namespace TaskTrackingSystem.WebApi.Features.Dashboard
46
  return Result<DashboardSummaryDto>.Success(summary);
47
  }
48
 
49
- public async Task<Result<IEnumerable<TaskStatusOverviewDto>>> GetTasksOverviewAsync(string roleName, long currentUserId)
50
  {
51
- var groupedTasks = await BuildAccessibleTaskQuery(roleName, currentUserId)
 
 
 
52
  .GroupBy(t => t.StatusId)
53
  .Select(g => new
54
  {
@@ -87,9 +94,10 @@ namespace TaskTrackingSystem.WebApi.Features.Dashboard
87
  return Result<IEnumerable<TaskStatusOverviewDto>>.Success(overview.OrderBy(o => o.StatusId));
88
  }
89
 
90
- public async Task<Result<IEnumerable<ProjectProgressDto>>> GetProjectProgressAsync(string roleName, long currentUserId)
91
  {
92
- var activeProjects = await BuildAccessibleProjectQuery(roleName, currentUserId).ToListAsync();
 
93
  var progressList = new List<ProjectProgressDto>();
94
 
95
  foreach (var project in activeProjects)
@@ -115,16 +123,16 @@ namespace TaskTrackingSystem.WebApi.Features.Dashboard
115
  return Result<IEnumerable<ProjectProgressDto>>.Success(progressList);
116
  }
117
 
118
- private IQueryable<TaskTrackingSystem.Database.AppDbContextModels.Task> BuildAccessibleTaskQuery(string roleName, long currentUserId)
119
  {
120
  var query = _db.Tasks.Where(t => t.IsDeleted != true && !t.IsArchived);
121
 
122
- if (IsAdmin(roleName))
123
  {
124
  return query;
125
  }
126
 
127
- if (IsManager(roleName))
128
  {
129
  return query.Where(t =>
130
  t.AssignedTo == currentUserId ||
@@ -137,11 +145,11 @@ namespace TaskTrackingSystem.WebApi.Features.Dashboard
137
  t.CreatedBy == currentUserId);
138
  }
139
 
140
- private IQueryable<TaskTrackingSystem.Database.AppDbContextModels.Project> BuildAccessibleProjectQuery(string roleName, long currentUserId)
141
  {
142
  var query = _db.Projects.Where(p => p.IsDeleted != true);
143
 
144
- if (IsAdmin(roleName))
145
  {
146
  return query;
147
  }
@@ -150,15 +158,5 @@ namespace TaskTrackingSystem.WebApi.Features.Dashboard
150
  p.CreatedById == currentUserId ||
151
  p.ProjectMembers.Any(pm => pm.UserId == currentUserId));
152
  }
153
-
154
- private static bool IsAdmin(string roleName)
155
- {
156
- return string.Equals(roleName, "Admin", StringComparison.OrdinalIgnoreCase);
157
- }
158
-
159
- private static bool IsManager(string roleName)
160
- {
161
- return string.Equals(roleName, "Manager", StringComparison.OrdinalIgnoreCase);
162
- }
163
  }
164
  }
 
7
  using TaskTrackingSystem.Shared;
8
  using TaskTrackingSystem.Shared.Enums;
9
  using TaskTrackingSystem.Shared.Models.Dashboard;
10
+ using TaskTrackingSystem.WebApi.Infrastructure;
11
 
12
  namespace TaskTrackingSystem.WebApi.Features.Dashboard
13
  {
 
20
  _db = db;
21
  }
22
 
23
+ public async Task<Result<DashboardSummaryDto>> GetSummaryAsync(long roleId, long currentUserId)
24
  {
25
+ var isAdmin = await DataScopeAuthorization.IsAdminScopeAsync(_db, roleId);
26
+ var isManager = await DataScopeAuthorization.IsManagerScopeAsync(_db, roleId);
27
 
28
+ var projects = BuildAccessibleProjectQuery(isAdmin, currentUserId);
29
+ var tasks = BuildAccessibleTaskQuery(isAdmin, isManager, currentUserId);
30
+
31
+ var totalUsers = isAdmin
32
  ? await _db.Users.CountAsync(u => !u.IsDeleted)
33
  : await _db.Users
34
  .Where(u => !u.IsDeleted &&
 
50
  return Result<DashboardSummaryDto>.Success(summary);
51
  }
52
 
53
+ public async Task<Result<IEnumerable<TaskStatusOverviewDto>>> GetTasksOverviewAsync(long roleId, long currentUserId)
54
  {
55
+ var isAdmin = await DataScopeAuthorization.IsAdminScopeAsync(_db, roleId);
56
+ var isManager = await DataScopeAuthorization.IsManagerScopeAsync(_db, roleId);
57
+
58
+ var groupedTasks = await BuildAccessibleTaskQuery(isAdmin, isManager, currentUserId)
59
  .GroupBy(t => t.StatusId)
60
  .Select(g => new
61
  {
 
94
  return Result<IEnumerable<TaskStatusOverviewDto>>.Success(overview.OrderBy(o => o.StatusId));
95
  }
96
 
97
+ public async Task<Result<IEnumerable<ProjectProgressDto>>> GetProjectProgressAsync(long roleId, long currentUserId)
98
  {
99
+ var isAdmin = await DataScopeAuthorization.IsAdminScopeAsync(_db, roleId);
100
+ var activeProjects = await BuildAccessibleProjectQuery(isAdmin, currentUserId).ToListAsync();
101
  var progressList = new List<ProjectProgressDto>();
102
 
103
  foreach (var project in activeProjects)
 
123
  return Result<IEnumerable<ProjectProgressDto>>.Success(progressList);
124
  }
125
 
126
+ private IQueryable<TaskTrackingSystem.Database.AppDbContextModels.Task> BuildAccessibleTaskQuery(bool isAdmin, bool isManager, long currentUserId)
127
  {
128
  var query = _db.Tasks.Where(t => t.IsDeleted != true && !t.IsArchived);
129
 
130
+ if (isAdmin)
131
  {
132
  return query;
133
  }
134
 
135
+ if (isManager)
136
  {
137
  return query.Where(t =>
138
  t.AssignedTo == currentUserId ||
 
145
  t.CreatedBy == currentUserId);
146
  }
147
 
148
+ private IQueryable<TaskTrackingSystem.Database.AppDbContextModels.Project> BuildAccessibleProjectQuery(bool isAdmin, long currentUserId)
149
  {
150
  var query = _db.Projects.Where(p => p.IsDeleted != true);
151
 
152
+ if (isAdmin)
153
  {
154
  return query;
155
  }
 
158
  p.CreatedById == currentUserId ||
159
  p.ProjectMembers.Any(pm => pm.UserId == currentUserId));
160
  }
 
 
 
 
 
 
 
 
 
 
161
  }
162
  }
TaskTrackingSystem.WebApi/Features/Issue/IssueController.cs CHANGED
@@ -37,14 +37,14 @@ namespace TaskTrackingSystem.WebApi.Features.Issue
37
  {
38
  if (paging == null || (!paging.Page.HasValue && !paging.Limit.HasValue))
39
  {
40
- var issues = await _issueService.GetAllIssuesAsync(User.GetRoleName(), User.GetUserId());
41
  return Ok(issues);
42
  }
43
 
44
  var page = PaginationExtensions.NormalizePage(paging.Page);
45
  var limit = PaginationExtensions.NormalizePageSize(paging.Limit ?? 0);
46
  var paged = await _issueService.GetPagedIssuesAsync(
47
- User.GetRoleName(),
48
  User.GetUserId(),
49
  search,
50
  taskId,
@@ -61,7 +61,7 @@ namespace TaskTrackingSystem.WebApi.Features.Issue
61
  [HttpGet("{id}")]
62
  public async Task<ActionResult<IssueDto>> GetIssue(long id)
63
  {
64
- var issue = await _issueService.GetIssueByIdAsync(id, User.GetRoleName(), User.GetUserId());
65
  if (issue == null)
66
  {
67
  return NotFound(new { message = $"Issue with ID {id} not found." });
@@ -73,7 +73,7 @@ namespace TaskTrackingSystem.WebApi.Features.Issue
73
  [HttpGet("task/{taskId}")]
74
  public async Task<ActionResult<IEnumerable<IssueDto>>> GetIssuesByTask(long taskId)
75
  {
76
- var issues = await _issueService.GetIssuesByTaskIdAsync(taskId, User.GetRoleName(), User.GetUserId());
77
  return Ok(issues);
78
  }
79
 
@@ -85,7 +85,7 @@ namespace TaskTrackingSystem.WebApi.Features.Issue
85
  return Forbid();
86
  }
87
 
88
- var result = await _issueService.CreateIssueAsync(dto, User.GetRoleName(), User.GetUserId());
89
  return StatusCode(result.StatusCode, result);
90
  }
91
 
@@ -97,7 +97,7 @@ namespace TaskTrackingSystem.WebApi.Features.Issue
97
  return Forbid();
98
  }
99
 
100
- var result = await _issueService.UpdateIssueAsync(id, dto, User.GetRoleName(), User.GetUserId());
101
  return StatusCode(result.StatusCode, result);
102
  }
103
 
@@ -109,7 +109,7 @@ namespace TaskTrackingSystem.WebApi.Features.Issue
109
  return Forbid();
110
  }
111
 
112
- var result = await _issueService.SoftDeleteIssueAsync(id, User.GetRoleName(), User.GetUserId());
113
  return StatusCode(result.StatusCode, result);
114
  }
115
  }
 
37
  {
38
  if (paging == null || (!paging.Page.HasValue && !paging.Limit.HasValue))
39
  {
40
+ var issues = await _issueService.GetAllIssuesAsync(User.GetRoleId(), User.GetUserId());
41
  return Ok(issues);
42
  }
43
 
44
  var page = PaginationExtensions.NormalizePage(paging.Page);
45
  var limit = PaginationExtensions.NormalizePageSize(paging.Limit ?? 0);
46
  var paged = await _issueService.GetPagedIssuesAsync(
47
+ User.GetRoleId(),
48
  User.GetUserId(),
49
  search,
50
  taskId,
 
61
  [HttpGet("{id}")]
62
  public async Task<ActionResult<IssueDto>> GetIssue(long id)
63
  {
64
+ var issue = await _issueService.GetIssueByIdAsync(id, User.GetRoleId(), User.GetUserId());
65
  if (issue == null)
66
  {
67
  return NotFound(new { message = $"Issue with ID {id} not found." });
 
73
  [HttpGet("task/{taskId}")]
74
  public async Task<ActionResult<IEnumerable<IssueDto>>> GetIssuesByTask(long taskId)
75
  {
76
+ var issues = await _issueService.GetIssuesByTaskIdAsync(taskId, User.GetRoleId(), User.GetUserId());
77
  return Ok(issues);
78
  }
79
 
 
85
  return Forbid();
86
  }
87
 
88
+ var result = await _issueService.CreateIssueAsync(dto, User.GetRoleId(), User.GetUserId());
89
  return StatusCode(result.StatusCode, result);
90
  }
91
 
 
97
  return Forbid();
98
  }
99
 
100
+ var result = await _issueService.UpdateIssueAsync(id, dto, User.GetRoleId(), User.GetUserId());
101
  return StatusCode(result.StatusCode, result);
102
  }
103
 
 
109
  return Forbid();
110
  }
111
 
112
+ var result = await _issueService.SoftDeleteIssueAsync(id, User.GetRoleId(), User.GetUserId());
113
  return StatusCode(result.StatusCode, result);
114
  }
115
  }
TaskTrackingSystem.WebApi/Features/Issue/IssueService.cs CHANGED
@@ -3,7 +3,6 @@ using System;
3
  using System.Collections.Generic;
4
  using System.Linq;
5
  using System.Threading.Tasks;
6
- using TaskTrackingSystem.Database;
7
  using TaskTrackingSystem.Database.AppDbContextModels;
8
  using TaskTrackingSystem.Shared;
9
  using TaskTrackingSystem.Shared.Enums;
@@ -22,16 +21,18 @@ namespace TaskTrackingSystem.WebApi.Features.Issue
22
  _db = db;
23
  }
24
 
25
- public async Task<IEnumerable<IssueDto>> GetAllIssuesAsync(string roleName, long currentUserId)
26
  {
27
- return await BuildAccessibleIssueQuery(roleName, currentUserId)
 
 
28
  .OrderByDescending(i => i.CreatedAt ?? DateTime.UtcNow)
29
  .Select(ToDtoProjection())
30
  .ToListAsync();
31
  }
32
 
33
  public async Task<PagedResult<IssueDto>> GetPagedIssuesAsync(
34
- string roleName,
35
  long currentUserId,
36
  string? search,
37
  long? taskId,
@@ -42,7 +43,9 @@ namespace TaskTrackingSystem.WebApi.Features.Issue
42
  int page,
43
  int pageSize)
44
  {
45
- var query = BuildAccessibleIssueQuery(roleName, currentUserId);
 
 
46
 
47
  if (assignedOnly)
48
  {
@@ -83,9 +86,13 @@ namespace TaskTrackingSystem.WebApi.Features.Issue
83
  .ToPagedResultAsync(page, pageSize);
84
  }
85
 
86
- public async Task<IssueDto?> GetIssueByIdAsync(long id, string roleName, long currentUserId)
 
 
87
  {
88
- var issue = await BuildAccessibleIssueQuery(roleName, currentUserId)
 
 
89
  .FirstOrDefaultAsync(i => i.Id == id);
90
 
91
  if (issue == null)
@@ -93,27 +100,29 @@ namespace TaskTrackingSystem.WebApi.Features.Issue
93
  return null;
94
  }
95
 
96
- return await BuildAccessibleIssueQuery(roleName, currentUserId)
97
  .Where(i => i.Id == id)
98
  .Select(ToDtoProjection())
99
  .FirstOrDefaultAsync();
100
  }
101
 
102
- public async Task<IEnumerable<IssueDto>> GetIssuesByTaskIdAsync(long taskId, string roleName, long currentUserId)
103
  {
104
- if (!await CanAccessTaskAsync(taskId, roleName, currentUserId))
 
105
  {
106
  return Array.Empty<IssueDto>();
107
  }
108
 
109
- return await BuildAccessibleIssueQuery(roleName, currentUserId)
 
110
  .Where(i => i.TaskId == taskId)
111
  .OrderByDescending(i => i.CreatedAt ?? DateTime.UtcNow)
112
  .Select(ToDtoProjection())
113
  .ToListAsync();
114
  }
115
 
116
- public async Task<Result<IssueDto>> CreateIssueAsync(CreateIssueDto dto, string roleName, long currentUserId)
117
  {
118
  if (string.IsNullOrWhiteSpace(dto.Title))
119
  {
@@ -129,7 +138,8 @@ namespace TaskTrackingSystem.WebApi.Features.Issue
129
  return Result<IssueDto>.Failure($"Task with ID {dto.TaskId} not found.", 404);
130
  }
131
 
132
- if (!await CanAccessTaskAsync(task.Id, roleName, currentUserId))
 
133
  {
134
  return Result<IssueDto>.Failure("You do not have access to this task.", 403);
135
  }
@@ -159,11 +169,11 @@ namespace TaskTrackingSystem.WebApi.Features.Issue
159
  _db.Issues.Add(issue);
160
  await _db.SaveChangesAsync();
161
 
162
- var result = await GetIssueByIdAsync(issue.Id, roleName, currentUserId);
163
  return Result<IssueDto>.Success(result!, 201);
164
  }
165
 
166
- public async Task<Result> UpdateIssueAsync(long id, UpdateIssueDto dto, string roleName, long currentUserId)
167
  {
168
  if (string.IsNullOrWhiteSpace(dto.Title))
169
  {
@@ -180,7 +190,8 @@ namespace TaskTrackingSystem.WebApi.Features.Issue
180
  return Result.Failure($"Issue with ID {id} not found.", 404);
181
  }
182
 
183
- if (!await CanAccessTaskAsync(issue.TaskId, roleName, currentUserId))
 
184
  {
185
  return Result.Failure("You do not have access to this task.", 403);
186
  }
@@ -207,7 +218,7 @@ namespace TaskTrackingSystem.WebApi.Features.Issue
207
  return Result.Success(200);
208
  }
209
 
210
- public async Task<Result> SoftDeleteIssueAsync(long id, string roleName, long currentUserId)
211
  {
212
  var issue = await _db.Issues
213
  .Include(i => i.Task)
@@ -219,7 +230,8 @@ namespace TaskTrackingSystem.WebApi.Features.Issue
219
  return Result.Failure($"Issue with ID {id} not found.", 404);
220
  }
221
 
222
- if (!await CanAccessTaskAsync(issue.TaskId, roleName, currentUserId))
 
223
  {
224
  return Result.Failure("You do not have access to this task.", 403);
225
  }
@@ -233,17 +245,17 @@ namespace TaskTrackingSystem.WebApi.Features.Issue
233
  return Result.Success(200);
234
  }
235
 
236
- private IQueryable<IssueEntity> BuildAccessibleIssueQuery(string roleName, long currentUserId)
237
  {
238
  var query = _db.Issues
239
  .Where(i => i.IsDeleted != true && i.Task.IsDeleted != true && i.Task.Project.IsDeleted != true);
240
 
241
- if (IsAdmin(roleName))
242
  {
243
  return query;
244
  }
245
 
246
- if (IsManager(roleName))
247
  {
248
  return query.Where(i =>
249
  i.AssignedTo == currentUserId ||
@@ -256,9 +268,9 @@ namespace TaskTrackingSystem.WebApi.Features.Issue
256
  i.CreatedBy == currentUserId);
257
  }
258
 
259
- private async Task<bool> CanAccessTaskAsync(long taskId, string roleName, long currentUserId)
260
  {
261
- if (IsAdmin(roleName))
262
  {
263
  return await _db.Tasks.AnyAsync(t => t.Id == taskId && t.IsDeleted != true && !t.IsArchived && t.Project.IsDeleted != true);
264
  }
@@ -278,16 +290,6 @@ namespace TaskTrackingSystem.WebApi.Features.Issue
278
  return await _db.ProjectMembers.AnyAsync(pm => pm.ProjectId == projectId && pm.UserId == userId);
279
  }
280
 
281
- private static bool IsAdmin(string roleName)
282
- {
283
- return roleName.Equals("Admin", StringComparison.OrdinalIgnoreCase);
284
- }
285
-
286
- private static bool IsManager(string roleName)
287
- {
288
- return roleName.Equals("Manager", StringComparison.OrdinalIgnoreCase);
289
- }
290
-
291
  private static System.Linq.Expressions.Expression<Func<IssueEntity, IssueDto>> ToDtoProjection()
292
  {
293
  return i => new IssueDto
 
3
  using System.Collections.Generic;
4
  using System.Linq;
5
  using System.Threading.Tasks;
 
6
  using TaskTrackingSystem.Database.AppDbContextModels;
7
  using TaskTrackingSystem.Shared;
8
  using TaskTrackingSystem.Shared.Enums;
 
21
  _db = db;
22
  }
23
 
24
+ public async Task<IEnumerable<IssueDto>> GetAllIssuesAsync(long roleId, long currentUserId)
25
  {
26
+ var isAdmin = await DataScopeAuthorization.IsAdminScopeAsync(_db, roleId);
27
+ var isManager = await DataScopeAuthorization.IsManagerScopeAsync(_db, roleId);
28
+ return await BuildAccessibleIssueQuery(isAdmin, isManager, currentUserId)
29
  .OrderByDescending(i => i.CreatedAt ?? DateTime.UtcNow)
30
  .Select(ToDtoProjection())
31
  .ToListAsync();
32
  }
33
 
34
  public async Task<PagedResult<IssueDto>> GetPagedIssuesAsync(
35
+ long roleId,
36
  long currentUserId,
37
  string? search,
38
  long? taskId,
 
43
  int page,
44
  int pageSize)
45
  {
46
+ var isAdmin = await DataScopeAuthorization.IsAdminScopeAsync(_db, roleId);
47
+ var isManager = await DataScopeAuthorization.IsManagerScopeAsync(_db, roleId);
48
+ var query = BuildAccessibleIssueQuery(isAdmin, isManager, currentUserId);
49
 
50
  if (assignedOnly)
51
  {
 
86
  .ToPagedResultAsync(page, pageSize);
87
  }
88
 
89
+
90
+
91
+ public async Task<IssueDto?> GetIssueByIdAsync(long id, long roleId, long currentUserId)
92
  {
93
+ var isAdmin = await DataScopeAuthorization.IsAdminScopeAsync(_db, roleId);
94
+ var isManager = await DataScopeAuthorization.IsManagerScopeAsync(_db, roleId);
95
+ var issue = await BuildAccessibleIssueQuery(isAdmin, isManager, currentUserId)
96
  .FirstOrDefaultAsync(i => i.Id == id);
97
 
98
  if (issue == null)
 
100
  return null;
101
  }
102
 
103
+ return await BuildAccessibleIssueQuery(isAdmin, isManager, currentUserId)
104
  .Where(i => i.Id == id)
105
  .Select(ToDtoProjection())
106
  .FirstOrDefaultAsync();
107
  }
108
 
109
+ public async Task<IEnumerable<IssueDto>> GetIssuesByTaskIdAsync(long taskId, long roleId, long currentUserId)
110
  {
111
+ var isAdmin = await DataScopeAuthorization.IsAdminScopeAsync(_db, roleId);
112
+ if (!await CanAccessTaskAsync(taskId, isAdmin, currentUserId))
113
  {
114
  return Array.Empty<IssueDto>();
115
  }
116
 
117
+ var isManager = await DataScopeAuthorization.IsManagerScopeAsync(_db, roleId);
118
+ return await BuildAccessibleIssueQuery(isAdmin, isManager, currentUserId)
119
  .Where(i => i.TaskId == taskId)
120
  .OrderByDescending(i => i.CreatedAt ?? DateTime.UtcNow)
121
  .Select(ToDtoProjection())
122
  .ToListAsync();
123
  }
124
 
125
+ public async Task<Result<IssueDto>> CreateIssueAsync(CreateIssueDto dto, long roleId, long currentUserId)
126
  {
127
  if (string.IsNullOrWhiteSpace(dto.Title))
128
  {
 
138
  return Result<IssueDto>.Failure($"Task with ID {dto.TaskId} not found.", 404);
139
  }
140
 
141
+ var isAdmin = await DataScopeAuthorization.IsAdminScopeAsync(_db, roleId);
142
+ if (!await CanAccessTaskAsync(task.Id, isAdmin, currentUserId))
143
  {
144
  return Result<IssueDto>.Failure("You do not have access to this task.", 403);
145
  }
 
169
  _db.Issues.Add(issue);
170
  await _db.SaveChangesAsync();
171
 
172
+ var result = await GetIssueByIdAsync(issue.Id, roleId, currentUserId);
173
  return Result<IssueDto>.Success(result!, 201);
174
  }
175
 
176
+ public async Task<Result> UpdateIssueAsync(long id, UpdateIssueDto dto, long roleId, long currentUserId)
177
  {
178
  if (string.IsNullOrWhiteSpace(dto.Title))
179
  {
 
190
  return Result.Failure($"Issue with ID {id} not found.", 404);
191
  }
192
 
193
+ var isAdmin = await DataScopeAuthorization.IsAdminScopeAsync(_db, roleId);
194
+ if (!await CanAccessTaskAsync(issue.TaskId, isAdmin, currentUserId))
195
  {
196
  return Result.Failure("You do not have access to this task.", 403);
197
  }
 
218
  return Result.Success(200);
219
  }
220
 
221
+ public async Task<Result> SoftDeleteIssueAsync(long id, long roleId, long currentUserId)
222
  {
223
  var issue = await _db.Issues
224
  .Include(i => i.Task)
 
230
  return Result.Failure($"Issue with ID {id} not found.", 404);
231
  }
232
 
233
+ var isAdmin = await DataScopeAuthorization.IsAdminScopeAsync(_db, roleId);
234
+ if (!await CanAccessTaskAsync(issue.TaskId, isAdmin, currentUserId))
235
  {
236
  return Result.Failure("You do not have access to this task.", 403);
237
  }
 
245
  return Result.Success(200);
246
  }
247
 
248
+ private IQueryable<IssueEntity> BuildAccessibleIssueQuery(bool isAdmin, bool isManager, long currentUserId)
249
  {
250
  var query = _db.Issues
251
  .Where(i => i.IsDeleted != true && i.Task.IsDeleted != true && i.Task.Project.IsDeleted != true);
252
 
253
+ if (isAdmin)
254
  {
255
  return query;
256
  }
257
 
258
+ if (isManager)
259
  {
260
  return query.Where(i =>
261
  i.AssignedTo == currentUserId ||
 
268
  i.CreatedBy == currentUserId);
269
  }
270
 
271
+ private async Task<bool> CanAccessTaskAsync(long taskId, bool isAdmin, long currentUserId)
272
  {
273
+ if (isAdmin)
274
  {
275
  return await _db.Tasks.AnyAsync(t => t.Id == taskId && t.IsDeleted != true && !t.IsArchived && t.Project.IsDeleted != true);
276
  }
 
290
  return await _db.ProjectMembers.AnyAsync(pm => pm.ProjectId == projectId && pm.UserId == userId);
291
  }
292
 
 
 
 
 
 
 
 
 
 
 
293
  private static System.Linq.Expressions.Expression<Func<IssueEntity, IssueDto>> ToDtoProjection()
294
  {
295
  return i => new IssueDto
TaskTrackingSystem.WebApi/Features/Menu/MenuController.cs CHANGED
@@ -49,7 +49,7 @@ namespace TaskTrackingSystem.WebApi.Features.Menu
49
  {
50
  var canCreate = await _permissionAuthorizationService.CanAccessAsync(User, "api/Role", "Create");
51
  var canUpdate = await _permissionAuthorizationService.CanAccessAsync(User, "api/Role", "Update");
52
- if (!canCreate && !canUpdate && !User.IsInRole("Admin"))
53
  {
54
  return Forbid();
55
  }
 
49
  {
50
  var canCreate = await _permissionAuthorizationService.CanAccessAsync(User, "api/Role", "Create");
51
  var canUpdate = await _permissionAuthorizationService.CanAccessAsync(User, "api/Role", "Update");
52
+ if (!canCreate && !canUpdate)
53
  {
54
  return Forbid();
55
  }
TaskTrackingSystem.WebApi/Features/Notification/NotificationRealtimeService.cs CHANGED
@@ -12,12 +12,7 @@ public class NotificationRealtimeService
12
  _hubContext = hubContext;
13
  }
14
 
15
- public global::System.Threading.Tasks.Task SendCreatedAsync(long recipientId, NotificationDto notification, int unreadCount)
16
- {
17
- return SendCreatedInternalAsync(recipientId, notification, unreadCount);
18
- }
19
-
20
- private async global::System.Threading.Tasks.Task SendCreatedInternalAsync(long recipientId, NotificationDto notification, int unreadCount)
21
  {
22
  var group = _hubContext.Clients.Group(NotificationHub.GetUserGroupName(recipientId));
23
  await group.SendAsync("NotificationCreated", notification);
 
12
  _hubContext = hubContext;
13
  }
14
 
15
+ public async global::System.Threading.Tasks.Task SendCreatedAsync(long recipientId, NotificationDto notification, int unreadCount)
 
 
 
 
 
16
  {
17
  var group = _hubContext.Clients.Group(NotificationHub.GetUserGroupName(recipientId));
18
  await group.SendAsync("NotificationCreated", notification);
TaskTrackingSystem.WebApi/Features/Project/ProjectController.cs CHANGED
@@ -32,20 +32,20 @@ namespace TaskTrackingSystem.WebApi.Features.Project
32
  {
33
  if (paging == null || (!paging.Page.HasValue && !paging.Limit.HasValue))
34
  {
35
- var projects = await _projectService.GetAllProjectsAsync(User.GetRoleName(), User.GetUserId());
36
  return Ok(projects);
37
  }
38
 
39
  var page = PaginationExtensions.NormalizePage(paging.Page);
40
  var limit = PaginationExtensions.NormalizePageSize(paging.Limit ?? 0);
41
- var paged = await _projectService.GetPagedProjectsAsync(User.GetRoleName(), User.GetUserId(), search, page, limit);
42
  return Ok(paged);
43
  }
44
 
45
  [HttpGet("{id}")]
46
  public async Task<ActionResult<ProjectDto>> GetProject(long id)
47
  {
48
- var project = await _projectService.GetProjectByIdAsync(id, User.GetRoleName(), User.GetUserId());
49
  if (project == null)
50
  {
51
  return NotFound(new { message = $"Project with ID {id} not found." });
@@ -75,7 +75,7 @@ namespace TaskTrackingSystem.WebApi.Features.Project
75
  }
76
 
77
  var currentUserId = User.GetUserId();
78
- var result = await _projectService.UpdateProjectAsync(id, updateProjectDto, User.GetRoleName(), currentUserId);
79
  return StatusCode(result.StatusCode, result);
80
  }
81
 
@@ -87,14 +87,14 @@ namespace TaskTrackingSystem.WebApi.Features.Project
87
  return Forbid();
88
  }
89
 
90
- var result = await _projectService.SoftDeleteProjectAsync(id, User.GetRoleName(), User.GetUserId());
91
  return StatusCode(result.StatusCode, result);
92
  }
93
 
94
  [HttpGet("{id}/members")]
95
  public async Task<ActionResult<Result<IEnumerable<UserDto>>>> GetProjectMembers(long id)
96
  {
97
- var result = await _projectService.GetProjectMembersAsync(id, User.GetRoleName(), User.GetUserId());
98
  return StatusCode(result.StatusCode, result);
99
  }
100
 
@@ -106,7 +106,7 @@ namespace TaskTrackingSystem.WebApi.Features.Project
106
  return Forbid();
107
  }
108
 
109
- var result = await _projectService.AssignMembersToProjectAsync(id, dto, User.GetRoleName(), User.GetUserId());
110
  if (!result.IsSuccess)
111
  {
112
  return StatusCode(result.StatusCode, new { message = result.ErrorMessage });
@@ -122,7 +122,7 @@ namespace TaskTrackingSystem.WebApi.Features.Project
122
  return Forbid();
123
  }
124
 
125
- var result = await _projectService.RemoveMemberFromProjectAsync(id, userId, User.GetRoleName(), User.GetUserId());
126
  if (!result.IsSuccess)
127
  {
128
  return StatusCode(result.StatusCode, new { message = result.ErrorMessage });
@@ -133,7 +133,7 @@ namespace TaskTrackingSystem.WebApi.Features.Project
133
  [HttpGet("{id}/tasks")]
134
  public async Task<ActionResult<Result<IEnumerable<TaskDto>>>> GetProjectTasks(long id)
135
  {
136
- var result = await _projectService.GetProjectTasksAsync(id, User.GetRoleName(), User.GetUserId());
137
  return StatusCode(result.StatusCode, result);
138
  }
139
  }
 
32
  {
33
  if (paging == null || (!paging.Page.HasValue && !paging.Limit.HasValue))
34
  {
35
+ var projects = await _projectService.GetAllProjectsAsync(User.GetRoleId(), User.GetUserId());
36
  return Ok(projects);
37
  }
38
 
39
  var page = PaginationExtensions.NormalizePage(paging.Page);
40
  var limit = PaginationExtensions.NormalizePageSize(paging.Limit ?? 0);
41
+ var paged = await _projectService.GetPagedProjectsAsync(User.GetRoleId(), User.GetUserId(), search, page, limit);
42
  return Ok(paged);
43
  }
44
 
45
  [HttpGet("{id}")]
46
  public async Task<ActionResult<ProjectDto>> GetProject(long id)
47
  {
48
+ var project = await _projectService.GetProjectByIdAsync(id, User.GetRoleId(), User.GetUserId());
49
  if (project == null)
50
  {
51
  return NotFound(new { message = $"Project with ID {id} not found." });
 
75
  }
76
 
77
  var currentUserId = User.GetUserId();
78
+ var result = await _projectService.UpdateProjectAsync(id, updateProjectDto, User.GetRoleId(), currentUserId);
79
  return StatusCode(result.StatusCode, result);
80
  }
81
 
 
87
  return Forbid();
88
  }
89
 
90
+ var result = await _projectService.SoftDeleteProjectAsync(id, User.GetRoleId(), User.GetUserId());
91
  return StatusCode(result.StatusCode, result);
92
  }
93
 
94
  [HttpGet("{id}/members")]
95
  public async Task<ActionResult<Result<IEnumerable<UserDto>>>> GetProjectMembers(long id)
96
  {
97
+ var result = await _projectService.GetProjectMembersAsync(id, User.GetRoleId(), User.GetUserId());
98
  return StatusCode(result.StatusCode, result);
99
  }
100
 
 
106
  return Forbid();
107
  }
108
 
109
+ var result = await _projectService.AssignMembersToProjectAsync(id, dto, User.GetRoleId(), User.GetUserId());
110
  if (!result.IsSuccess)
111
  {
112
  return StatusCode(result.StatusCode, new { message = result.ErrorMessage });
 
122
  return Forbid();
123
  }
124
 
125
+ var result = await _projectService.RemoveMemberFromProjectAsync(id, userId, User.GetRoleId(), User.GetUserId());
126
  if (!result.IsSuccess)
127
  {
128
  return StatusCode(result.StatusCode, new { message = result.ErrorMessage });
 
133
  [HttpGet("{id}/tasks")]
134
  public async Task<ActionResult<Result<IEnumerable<TaskDto>>>> GetProjectTasks(long id)
135
  {
136
+ var result = await _projectService.GetProjectTasksAsync(id, User.GetRoleId(), User.GetUserId());
137
  return StatusCode(result.StatusCode, result);
138
  }
139
  }
TaskTrackingSystem.WebApi/Features/Project/ProjectService.cs CHANGED
@@ -31,9 +31,10 @@ namespace TaskTrackingSystem.WebApi.Features.Project
31
  : null;
32
  }
33
 
34
- public async Task<IEnumerable<ProjectDto>> GetAllProjectsAsync(string roleName, long currentUserId)
35
  {
36
- return await BuildAccessibleProjectQuery(roleName, currentUserId)
 
37
  .OrderBy(p => p.EndDate)
38
  .ThenByDescending(p => p.CreatedAt ?? DateTime.UtcNow)
39
  .Select(p => new ProjectDto
@@ -49,9 +50,10 @@ namespace TaskTrackingSystem.WebApi.Features.Project
49
  }).ToListAsync();
50
  }
51
 
52
- public async Task<PagedResult<ProjectDto>> GetPagedProjectsAsync(string roleName, long currentUserId, string? search, int page, int pageSize)
53
  {
54
- var query = BuildAccessibleProjectQuery(roleName, currentUserId);
 
55
 
56
  if (!string.IsNullOrWhiteSpace(search))
57
  {
@@ -78,9 +80,10 @@ namespace TaskTrackingSystem.WebApi.Features.Project
78
  .ToPagedResultAsync(page, pageSize);
79
  }
80
 
81
- public async Task<ProjectDto?> GetProjectByIdAsync(long id, string roleName, long currentUserId)
82
  {
83
- var project = await BuildAccessibleProjectQuery(roleName, currentUserId)
 
84
  .FirstOrDefaultAsync(p => p.Id == id);
85
 
86
  if (project == null)
@@ -139,7 +142,7 @@ namespace TaskTrackingSystem.WebApi.Features.Project
139
  return Result<ProjectDto>.Success(resultDto, 201);
140
  }
141
 
142
- public async Task<Result> UpdateProjectAsync(long id, UpdateProjectDto dto, string roleName, long currentUserId)
143
  {
144
  if (string.IsNullOrWhiteSpace(dto.Name))
145
  {
@@ -152,7 +155,8 @@ namespace TaskTrackingSystem.WebApi.Features.Project
152
  return Result.Failure(ResultMessages.ProjectNotFound(id), 404);
153
  }
154
 
155
- if (!await IsProjectAccessibleAsync(id, roleName, currentUserId))
 
156
  {
157
  return Result.Failure("You do not have access to this project.", 403);
158
  }
@@ -170,7 +174,7 @@ namespace TaskTrackingSystem.WebApi.Features.Project
170
  return Result.Success(200);
171
  }
172
 
173
- public async Task<Result> SoftDeleteProjectAsync(long id, string roleName, long currentUserId)
174
  {
175
  var project = await _db.Projects.FirstOrDefaultAsync(p => p.Id == id && p.IsDeleted != true);
176
  if (project == null)
@@ -178,7 +182,8 @@ namespace TaskTrackingSystem.WebApi.Features.Project
178
  return Result.Failure(ResultMessages.ProjectNotFound(id), 404);
179
  }
180
 
181
- if (!await IsProjectAccessibleAsync(id, roleName, currentUserId))
 
182
  {
183
  return Result.Failure("You do not have access to this project.", 403);
184
  }
@@ -205,9 +210,10 @@ namespace TaskTrackingSystem.WebApi.Features.Project
205
  return Result.Success(200);
206
  }
207
 
208
- public async Task<Result<IEnumerable<UserDto>>> GetProjectMembersAsync(long projectId, string roleName, long currentUserId)
209
  {
210
- if (!await IsProjectAccessibleAsync(projectId, roleName, currentUserId))
 
211
  {
212
  return Result<IEnumerable<UserDto>>.Failure("You do not have access to this project.", 403);
213
  }
@@ -234,7 +240,7 @@ namespace TaskTrackingSystem.WebApi.Features.Project
234
  return Result<IEnumerable<UserDto>>.Success(members);
235
  }
236
 
237
- public async Task<Result> AssignMembersToProjectAsync(long projectId, AssignMembersDto dto, string roleName, long currentUserId)
238
  {
239
  var projectExists = await _db.Projects.AnyAsync(p => p.Id == projectId && p.IsDeleted != true);
240
  if (!projectExists)
@@ -242,7 +248,8 @@ namespace TaskTrackingSystem.WebApi.Features.Project
242
  return Result.Failure(ResultMessages.ProjectNotFound(projectId), 404);
243
  }
244
 
245
- if (!await IsProjectAccessibleAsync(projectId, roleName, currentUserId))
 
246
  {
247
  return Result.Failure("You do not have access to this project.", 403);
248
  }
@@ -301,7 +308,7 @@ namespace TaskTrackingSystem.WebApi.Features.Project
301
  return Result.Success(200);
302
  }
303
 
304
- public async Task<Result> RemoveMemberFromProjectAsync(long projectId, long userId, string roleName, long currentUserId)
305
  {
306
  var projectExists = await _db.Projects.AnyAsync(p => p.Id == projectId && p.IsDeleted != true);
307
  if (!projectExists)
@@ -309,7 +316,8 @@ namespace TaskTrackingSystem.WebApi.Features.Project
309
  return Result.Failure(ResultMessages.ProjectNotFound(projectId), 404);
310
  }
311
 
312
- if (!await IsProjectAccessibleAsync(projectId, roleName, currentUserId))
 
313
  {
314
  return Result.Failure("You do not have access to this project.", 403);
315
  }
@@ -328,9 +336,10 @@ namespace TaskTrackingSystem.WebApi.Features.Project
328
  return Result.Success(204);
329
  }
330
 
331
- public async Task<Result<IEnumerable<TaskDto>>> GetProjectTasksAsync(long projectId, string roleName, long currentUserId)
332
  {
333
- if (!await IsProjectAccessibleAsync(projectId, roleName, currentUserId))
 
334
  {
335
  return Result<IEnumerable<TaskDto>>.Failure("You do not have access to this project.", 403);
336
  }
@@ -357,11 +366,11 @@ namespace TaskTrackingSystem.WebApi.Features.Project
357
  return Result<IEnumerable<TaskDto>>.Success(tasks);
358
  }
359
 
360
- private IQueryable<TaskTrackingSystem.Database.AppDbContextModels.Project> BuildAccessibleProjectQuery(string roleName, long currentUserId)
361
  {
362
  var query = _db.Projects.Where(p => p.IsDeleted != true);
363
 
364
- if (IsAdmin(roleName))
365
  {
366
  return query;
367
  }
@@ -371,9 +380,9 @@ namespace TaskTrackingSystem.WebApi.Features.Project
371
  p.ProjectMembers.Any(pm => pm.UserId == currentUserId));
372
  }
373
 
374
- private async Task<bool> IsProjectAccessibleAsync(long projectId, string roleName, long currentUserId)
375
  {
376
- if (IsAdmin(roleName))
377
  {
378
  return await _db.Projects.AnyAsync(p => p.Id == projectId && p.IsDeleted != true);
379
  }
@@ -383,10 +392,5 @@ namespace TaskTrackingSystem.WebApi.Features.Project
383
  p.IsDeleted != true &&
384
  (p.CreatedById == currentUserId || p.ProjectMembers.Any(pm => pm.UserId == currentUserId)));
385
  }
386
-
387
- private static bool IsAdmin(string roleName)
388
- {
389
- return roleName.Equals("Admin", StringComparison.OrdinalIgnoreCase);
390
- }
391
  }
392
  }
 
31
  : null;
32
  }
33
 
34
+ public async Task<IEnumerable<ProjectDto>> GetAllProjectsAsync(long roleId, long currentUserId)
35
  {
36
+ var isAdmin = await DataScopeAuthorization.IsAdminScopeAsync(_db, roleId);
37
+ return await BuildAccessibleProjectQuery(isAdmin, currentUserId)
38
  .OrderBy(p => p.EndDate)
39
  .ThenByDescending(p => p.CreatedAt ?? DateTime.UtcNow)
40
  .Select(p => new ProjectDto
 
50
  }).ToListAsync();
51
  }
52
 
53
+ public async Task<PagedResult<ProjectDto>> GetPagedProjectsAsync(long roleId, long currentUserId, string? search, int page, int pageSize)
54
  {
55
+ var isAdmin = await DataScopeAuthorization.IsAdminScopeAsync(_db, roleId);
56
+ var query = BuildAccessibleProjectQuery(isAdmin, currentUserId);
57
 
58
  if (!string.IsNullOrWhiteSpace(search))
59
  {
 
80
  .ToPagedResultAsync(page, pageSize);
81
  }
82
 
83
+ public async Task<ProjectDto?> GetProjectByIdAsync(long id, long roleId, long currentUserId)
84
  {
85
+ var isAdmin = await DataScopeAuthorization.IsAdminScopeAsync(_db, roleId);
86
+ var project = await BuildAccessibleProjectQuery(isAdmin, currentUserId)
87
  .FirstOrDefaultAsync(p => p.Id == id);
88
 
89
  if (project == null)
 
142
  return Result<ProjectDto>.Success(resultDto, 201);
143
  }
144
 
145
+ public async Task<Result> UpdateProjectAsync(long id, UpdateProjectDto dto, long roleId, long currentUserId)
146
  {
147
  if (string.IsNullOrWhiteSpace(dto.Name))
148
  {
 
155
  return Result.Failure(ResultMessages.ProjectNotFound(id), 404);
156
  }
157
 
158
+ var isAdmin = await DataScopeAuthorization.IsAdminScopeAsync(_db, roleId);
159
+ if (!await IsProjectAccessibleAsync(id, isAdmin, currentUserId))
160
  {
161
  return Result.Failure("You do not have access to this project.", 403);
162
  }
 
174
  return Result.Success(200);
175
  }
176
 
177
+ public async Task<Result> SoftDeleteProjectAsync(long id, long roleId, long currentUserId)
178
  {
179
  var project = await _db.Projects.FirstOrDefaultAsync(p => p.Id == id && p.IsDeleted != true);
180
  if (project == null)
 
182
  return Result.Failure(ResultMessages.ProjectNotFound(id), 404);
183
  }
184
 
185
+ var isAdmin = await DataScopeAuthorization.IsAdminScopeAsync(_db, roleId);
186
+ if (!await IsProjectAccessibleAsync(id, isAdmin, currentUserId))
187
  {
188
  return Result.Failure("You do not have access to this project.", 403);
189
  }
 
210
  return Result.Success(200);
211
  }
212
 
213
+ public async Task<Result<IEnumerable<UserDto>>> GetProjectMembersAsync(long projectId, long roleId, long currentUserId)
214
  {
215
+ var isAdmin = await DataScopeAuthorization.IsAdminScopeAsync(_db, roleId);
216
+ if (!await IsProjectAccessibleAsync(projectId, isAdmin, currentUserId))
217
  {
218
  return Result<IEnumerable<UserDto>>.Failure("You do not have access to this project.", 403);
219
  }
 
240
  return Result<IEnumerable<UserDto>>.Success(members);
241
  }
242
 
243
+ public async Task<Result> AssignMembersToProjectAsync(long projectId, AssignMembersDto dto, long roleId, long currentUserId)
244
  {
245
  var projectExists = await _db.Projects.AnyAsync(p => p.Id == projectId && p.IsDeleted != true);
246
  if (!projectExists)
 
248
  return Result.Failure(ResultMessages.ProjectNotFound(projectId), 404);
249
  }
250
 
251
+ var isAdmin = await DataScopeAuthorization.IsAdminScopeAsync(_db, roleId);
252
+ if (!await IsProjectAccessibleAsync(projectId, isAdmin, currentUserId))
253
  {
254
  return Result.Failure("You do not have access to this project.", 403);
255
  }
 
308
  return Result.Success(200);
309
  }
310
 
311
+ public async Task<Result> RemoveMemberFromProjectAsync(long projectId, long userId, long roleId, long currentUserId)
312
  {
313
  var projectExists = await _db.Projects.AnyAsync(p => p.Id == projectId && p.IsDeleted != true);
314
  if (!projectExists)
 
316
  return Result.Failure(ResultMessages.ProjectNotFound(projectId), 404);
317
  }
318
 
319
+ var isAdmin = await DataScopeAuthorization.IsAdminScopeAsync(_db, roleId);
320
+ if (!await IsProjectAccessibleAsync(projectId, isAdmin, currentUserId))
321
  {
322
  return Result.Failure("You do not have access to this project.", 403);
323
  }
 
336
  return Result.Success(204);
337
  }
338
 
339
+ public async Task<Result<IEnumerable<TaskDto>>> GetProjectTasksAsync(long projectId, long roleId, long currentUserId)
340
  {
341
+ var isAdmin = await DataScopeAuthorization.IsAdminScopeAsync(_db, roleId);
342
+ if (!await IsProjectAccessibleAsync(projectId, isAdmin, currentUserId))
343
  {
344
  return Result<IEnumerable<TaskDto>>.Failure("You do not have access to this project.", 403);
345
  }
 
366
  return Result<IEnumerable<TaskDto>>.Success(tasks);
367
  }
368
 
369
+ private IQueryable<TaskTrackingSystem.Database.AppDbContextModels.Project> BuildAccessibleProjectQuery(bool isAdmin, long currentUserId)
370
  {
371
  var query = _db.Projects.Where(p => p.IsDeleted != true);
372
 
373
+ if (isAdmin)
374
  {
375
  return query;
376
  }
 
380
  p.ProjectMembers.Any(pm => pm.UserId == currentUserId));
381
  }
382
 
383
+ private async Task<bool> IsProjectAccessibleAsync(long projectId, bool isAdmin, long currentUserId)
384
  {
385
+ if (isAdmin)
386
  {
387
  return await _db.Projects.AnyAsync(p => p.Id == projectId && p.IsDeleted != true);
388
  }
 
392
  p.IsDeleted != true &&
393
  (p.CreatedById == currentUserId || p.ProjectMembers.Any(pm => pm.UserId == currentUserId)));
394
  }
 
 
 
 
 
395
  }
396
  }
TaskTrackingSystem.WebApi/Features/Report/ReportController.cs CHANGED
@@ -8,6 +8,7 @@ using System.Threading.Tasks;
8
  using TaskTrackingSystem.Shared;
9
  using TaskTrackingSystem.Shared.Enums;
10
  using TaskTrackingSystem.Shared.Models.Report;
 
11
  using TaskTrackingSystem.WebApi.Infrastructure;
12
 
13
  namespace TaskTrackingSystem.WebApi.Features.Report
@@ -24,6 +25,14 @@ namespace TaskTrackingSystem.WebApi.Features.Report
24
  _reportService = reportService;
25
  }
26
 
 
 
 
 
 
 
 
 
27
  [HttpGet("tasks")]
28
  [ProducesResponseType(typeof(PagedResult<TaskReportDto>), StatusCodes.Status200OK)]
29
  [ProducesResponseType(StatusCodes.Status401Unauthorized)]
@@ -32,25 +41,21 @@ namespace TaskTrackingSystem.WebApi.Features.Report
32
  [FromQuery] DateTime? endDate,
33
  [FromQuery] string? status,
34
  [FromQuery] int? projectId,
 
 
35
  [FromQuery] PaginationQuery? paging = null)
36
  {
37
  if (paging == null || (!paging.Page.HasValue && !paging.Limit.HasValue))
38
  {
39
- var full = await _reportService.GetTasksReportAsync(startDate, endDate, status, projectId, User.GetRoleName(), User.GetUserId());
 
40
  return StatusCode(full.StatusCode, full);
41
  }
42
 
43
  var page = PaginationExtensions.NormalizePage(paging.Page);
44
  var limit = PaginationExtensions.NormalizePageSize(paging.Limit ?? 0);
45
  var result = await _reportService.GetPagedTasksReportAsync(
46
- startDate,
47
- endDate,
48
- status,
49
- projectId,
50
- User.GetRoleName(),
51
- User.GetUserId(),
52
- page,
53
- limit);
54
  return Ok(result);
55
  }
56
 
@@ -60,7 +65,7 @@ namespace TaskTrackingSystem.WebApi.Features.Report
60
  public async Task<ActionResult<PagedResult<UserProductivityDto>>> GetUserProductivityReport(
61
  [FromQuery] PaginationQuery? paging = null)
62
  {
63
- var users = await _reportService.GetUserProductivityReportAsync(User.GetRoleName(), User.GetUserId());
64
  if (!users.IsSuccess || users.Value == null)
65
  {
66
  return StatusCode(users.StatusCode, new PagedResult<UserProductivityDto>());
@@ -84,7 +89,7 @@ namespace TaskTrackingSystem.WebApi.Features.Report
84
  [FromQuery] AppTaskStatus? statusId,
85
  [FromQuery] long? projectId)
86
  {
87
- var result = await _reportService.GetTaskStatusSummaryAsync(search, statusId, projectId, User.GetRoleName(), User.GetUserId());
88
  return StatusCode(result.StatusCode, result);
89
  }
90
 
@@ -94,7 +99,7 @@ namespace TaskTrackingSystem.WebApi.Features.Report
94
  [FromQuery] AppTaskStatus? statusId,
95
  [FromQuery] long? projectId)
96
  {
97
- var result = await _reportService.GetTaskStatusSummaryAsync(search, statusId, projectId, User.GetRoleName(), User.GetUserId());
98
  if (!result.IsSuccess || result.Value == null)
99
  return BadRequest(new { message = result.ErrorMessage });
100
  var bytes = _reportService.ExportTaskStatusSummaryToExcel(result.Value);
@@ -111,20 +116,20 @@ namespace TaskTrackingSystem.WebApi.Features.Report
111
  {
112
  if (paging == null || (!paging.Page.HasValue && !paging.Limit.HasValue))
113
  {
114
- var full = await _reportService.GetTeamProductivityAsync(search, User.GetRoleName(), User.GetUserId());
115
  return StatusCode(full.StatusCode, full);
116
  }
117
 
118
  var page = PaginationExtensions.NormalizePage(paging?.Page);
119
  var limit = PaginationExtensions.NormalizePageSize(paging?.Limit ?? 0);
120
- var result = await _reportService.GetPagedTeamProductivityAsync(search, User.GetRoleName(), User.GetUserId(), page, limit);
121
  return Ok(result);
122
  }
123
 
124
  [HttpGet("team-productivity/excel")]
125
  public async Task<IActionResult> DownloadTeamProductivityExcel([FromQuery] string? search)
126
  {
127
- var result = await _reportService.GetTeamProductivityAsync(search, User.GetRoleName(), User.GetUserId());
128
  if (!result.IsSuccess || result.Value == null)
129
  return BadRequest(new { message = result.ErrorMessage });
130
  var bytes = _reportService.ExportTeamProductivityToExcel(result.Value);
@@ -140,34 +145,33 @@ namespace TaskTrackingSystem.WebApi.Features.Report
140
  [FromQuery] long? projectId,
141
  [FromQuery] int? priorityId,
142
  [FromQuery] string? delayType,
 
 
143
  [FromQuery] PaginationQuery? paging = null)
144
  {
145
  if (paging == null || (!paging.Page.HasValue && !paging.Limit.HasValue))
146
  {
147
- var full = await _reportService.GetOverdueCriticalTasksAsync(search, projectId, User.GetRoleName(), User.GetUserId(), priorityId, delayType);
 
148
  return StatusCode(full.StatusCode, full);
149
  }
150
 
151
  var page = PaginationExtensions.NormalizePage(paging?.Page);
152
  var limit = PaginationExtensions.NormalizePageSize(paging?.Limit ?? 0);
153
  var result = await _reportService.GetPagedOverdueCriticalTasksAsync(
154
- search,
155
- projectId,
156
- User.GetRoleName(),
157
- User.GetUserId(),
158
- page,
159
- limit,
160
- priorityId,
161
- delayType);
162
  return Ok(result);
163
  }
164
 
165
  [HttpGet("overdue-critical/excel")]
166
  public async Task<IActionResult> DownloadOverdueCriticalExcel(
167
  [FromQuery] string? search,
168
- [FromQuery] long? projectId)
 
 
169
  {
170
- var result = await _reportService.GetOverdueCriticalTasksAsync(search, projectId, User.GetRoleName(), User.GetUserId());
 
171
  if (!result.IsSuccess || result.Value == null)
172
  return BadRequest(new { message = result.ErrorMessage });
173
  var bytes = _reportService.ExportOverdueCriticalToExcel(result.Value);
@@ -183,24 +187,21 @@ namespace TaskTrackingSystem.WebApi.Features.Report
183
  [FromQuery] DateTime? startDate,
184
  [FromQuery] DateTime? endDate,
185
  [FromQuery] string? status,
 
 
186
  [FromQuery] PaginationQuery? paging = null)
187
  {
188
  if (paging == null || (!paging.Page.HasValue && !paging.Limit.HasValue))
189
  {
190
- var reportData = await _reportService.GetEmployeeProductivityReportAsync(search, startDate, endDate, status, User.GetRoleName(), User.GetUserId());
 
191
  return StatusCode(reportData.StatusCode, reportData);
192
  }
193
 
194
- var reportDataPaged = await _reportService.GetEmployeeProductivityReportAsync(search, startDate, endDate, status, User.GetRoleName(), User.GetUserId());
195
- if (!reportDataPaged.IsSuccess || reportDataPaged.Value == null)
196
- {
197
- return StatusCode(reportDataPaged.StatusCode, new PagedResult<EmployeeProductivityReportDto>());
198
- }
199
-
200
- var page = PaginationExtensions.NormalizePage(paging?.Page);
201
- var limit = PaginationExtensions.NormalizePageSize(paging?.Limit ?? 0);
202
- var paged = await reportDataPaged.Value.OrderByDescending(u => u.Efficiency).AsQueryable().ToPagedResultAsync(page, limit);
203
- return Ok(paged);
204
  }
205
 
206
  [HttpGet("project-progress")]
@@ -211,24 +212,21 @@ namespace TaskTrackingSystem.WebApi.Features.Report
211
  [FromQuery] DateTime? startDate,
212
  [FromQuery] DateTime? endDate,
213
  [FromQuery] string? status,
 
 
214
  [FromQuery] PaginationQuery? paging = null)
215
  {
216
  if (paging == null || (!paging.Page.HasValue && !paging.Limit.HasValue))
217
  {
218
- var reportData = await _reportService.GetProjectProgressReportAsync(search, startDate, endDate, status, User.GetRoleName(), User.GetUserId());
 
219
  return StatusCode(reportData.StatusCode, reportData);
220
  }
221
 
222
- var reportDataPaged = await _reportService.GetProjectProgressReportAsync(search, startDate, endDate, status, User.GetRoleName(), User.GetUserId());
223
- if (!reportDataPaged.IsSuccess || reportDataPaged.Value == null)
224
- {
225
- return StatusCode(reportDataPaged.StatusCode, new PagedResult<ProjectProgressReportDto>());
226
- }
227
-
228
- var page = PaginationExtensions.NormalizePage(paging?.Page);
229
- var limit = PaginationExtensions.NormalizePageSize(paging?.Limit ?? 0);
230
- var paged = await reportDataPaged.Value.OrderBy(p => p.ProjectName).AsQueryable().ToPagedResultAsync(page, limit);
231
- return Ok(paged);
232
  }
233
  }
234
  }
 
8
  using TaskTrackingSystem.Shared;
9
  using TaskTrackingSystem.Shared.Enums;
10
  using TaskTrackingSystem.Shared.Models.Report;
11
+ using TaskTrackingSystem.Shared.Models.Issue;
12
  using TaskTrackingSystem.WebApi.Infrastructure;
13
 
14
  namespace TaskTrackingSystem.WebApi.Features.Report
 
25
  _reportService = reportService;
26
  }
27
 
28
+ [HttpGet("issues")]
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
+
36
  [HttpGet("tasks")]
37
  [ProducesResponseType(typeof(PagedResult<TaskReportDto>), StatusCodes.Status200OK)]
38
  [ProducesResponseType(StatusCodes.Status401Unauthorized)]
 
41
  [FromQuery] DateTime? endDate,
42
  [FromQuery] string? status,
43
  [FromQuery] int? projectId,
44
+ [FromQuery] bool? assignedToMe,
45
+ [FromQuery] bool? assignedToMyTeam,
46
  [FromQuery] PaginationQuery? paging = null)
47
  {
48
  if (paging == null || (!paging.Page.HasValue && !paging.Limit.HasValue))
49
  {
50
+ var full = await _reportService.GetTasksReportAsync(
51
+ startDate, endDate, status, projectId, User.GetRoleId(), User.GetUserId(), assignedToMe, assignedToMyTeam);
52
  return StatusCode(full.StatusCode, full);
53
  }
54
 
55
  var page = PaginationExtensions.NormalizePage(paging.Page);
56
  var limit = PaginationExtensions.NormalizePageSize(paging.Limit ?? 0);
57
  var result = await _reportService.GetPagedTasksReportAsync(
58
+ startDate, endDate, status, projectId, User.GetRoleId(), User.GetUserId(), page, limit, assignedToMe, assignedToMyTeam);
 
 
 
 
 
 
 
59
  return Ok(result);
60
  }
61
 
 
65
  public async Task<ActionResult<PagedResult<UserProductivityDto>>> GetUserProductivityReport(
66
  [FromQuery] PaginationQuery? paging = null)
67
  {
68
+ var users = await _reportService.GetUserProductivityReportAsync(User.GetRoleId(), User.GetUserId());
69
  if (!users.IsSuccess || users.Value == null)
70
  {
71
  return StatusCode(users.StatusCode, new PagedResult<UserProductivityDto>());
 
89
  [FromQuery] AppTaskStatus? statusId,
90
  [FromQuery] long? projectId)
91
  {
92
+ var result = await _reportService.GetTaskStatusSummaryAsync(search, statusId, projectId, User.GetRoleId(), User.GetUserId());
93
  return StatusCode(result.StatusCode, result);
94
  }
95
 
 
99
  [FromQuery] AppTaskStatus? statusId,
100
  [FromQuery] long? projectId)
101
  {
102
+ var result = await _reportService.GetTaskStatusSummaryAsync(search, statusId, projectId, User.GetRoleId(), User.GetUserId());
103
  if (!result.IsSuccess || result.Value == null)
104
  return BadRequest(new { message = result.ErrorMessage });
105
  var bytes = _reportService.ExportTaskStatusSummaryToExcel(result.Value);
 
116
  {
117
  if (paging == null || (!paging.Page.HasValue && !paging.Limit.HasValue))
118
  {
119
+ var full = await _reportService.GetTeamProductivityAsync(search, User.GetRoleId(), User.GetUserId());
120
  return StatusCode(full.StatusCode, full);
121
  }
122
 
123
  var page = PaginationExtensions.NormalizePage(paging?.Page);
124
  var limit = PaginationExtensions.NormalizePageSize(paging?.Limit ?? 0);
125
+ var result = await _reportService.GetPagedTeamProductivityAsync(search, User.GetRoleId(), User.GetUserId(), page, limit);
126
  return Ok(result);
127
  }
128
 
129
  [HttpGet("team-productivity/excel")]
130
  public async Task<IActionResult> DownloadTeamProductivityExcel([FromQuery] string? search)
131
  {
132
+ var result = await _reportService.GetTeamProductivityAsync(search, User.GetRoleId(), User.GetUserId());
133
  if (!result.IsSuccess || result.Value == null)
134
  return BadRequest(new { message = result.ErrorMessage });
135
  var bytes = _reportService.ExportTeamProductivityToExcel(result.Value);
 
145
  [FromQuery] long? projectId,
146
  [FromQuery] int? priorityId,
147
  [FromQuery] string? delayType,
148
+ [FromQuery] bool? assignedToMe,
149
+ [FromQuery] bool? assignedToMyTeam,
150
  [FromQuery] PaginationQuery? paging = null)
151
  {
152
  if (paging == null || (!paging.Page.HasValue && !paging.Limit.HasValue))
153
  {
154
+ var full = await _reportService.GetOverdueCriticalTasksAsync(
155
+ search, projectId, User.GetRoleId(), User.GetUserId(), priorityId, delayType, assignedToMe, assignedToMyTeam);
156
  return StatusCode(full.StatusCode, full);
157
  }
158
 
159
  var page = PaginationExtensions.NormalizePage(paging?.Page);
160
  var limit = PaginationExtensions.NormalizePageSize(paging?.Limit ?? 0);
161
  var result = await _reportService.GetPagedOverdueCriticalTasksAsync(
162
+ search, projectId, User.GetRoleId(), User.GetUserId(), page, limit, priorityId, delayType, assignedToMe, assignedToMyTeam);
 
 
 
 
 
 
 
163
  return Ok(result);
164
  }
165
 
166
  [HttpGet("overdue-critical/excel")]
167
  public async Task<IActionResult> DownloadOverdueCriticalExcel(
168
  [FromQuery] string? search,
169
+ [FromQuery] long? projectId,
170
+ [FromQuery] bool? assignedToMe,
171
+ [FromQuery] bool? assignedToMyTeam)
172
  {
173
+ var result = await _reportService.GetOverdueCriticalTasksAsync(
174
+ search, projectId, User.GetRoleId(), User.GetUserId(), assignedToMe: assignedToMe, assignedToMyTeam: assignedToMyTeam);
175
  if (!result.IsSuccess || result.Value == null)
176
  return BadRequest(new { message = result.ErrorMessage });
177
  var bytes = _reportService.ExportOverdueCriticalToExcel(result.Value);
 
187
  [FromQuery] DateTime? startDate,
188
  [FromQuery] DateTime? endDate,
189
  [FromQuery] string? status,
190
+ [FromQuery] bool? assignedToMe,
191
+ [FromQuery] bool? assignedToMyTeam,
192
  [FromQuery] PaginationQuery? paging = null)
193
  {
194
  if (paging == null || (!paging.Page.HasValue && !paging.Limit.HasValue))
195
  {
196
+ var reportData = await _reportService.GetEmployeeProductivityReportAsync(
197
+ search, startDate, endDate, status, User.GetRoleId(), User.GetUserId(), assignedToMe, assignedToMyTeam);
198
  return StatusCode(reportData.StatusCode, reportData);
199
  }
200
 
201
+ var reportDataPaged = await _reportService.GetPagedEmployeeProductivityReportAsync(
202
+ search, startDate, endDate, status, User.GetRoleId(), User.GetUserId(),
203
+ paging.Page ?? 1, paging.Limit ?? 10, assignedToMe, assignedToMyTeam);
204
+ return Ok(reportDataPaged);
 
 
 
 
 
 
205
  }
206
 
207
  [HttpGet("project-progress")]
 
212
  [FromQuery] DateTime? startDate,
213
  [FromQuery] DateTime? endDate,
214
  [FromQuery] string? status,
215
+ [FromQuery] bool? assignedToMe,
216
+ [FromQuery] bool? assignedToMyTeam,
217
  [FromQuery] PaginationQuery? paging = null)
218
  {
219
  if (paging == null || (!paging.Page.HasValue && !paging.Limit.HasValue))
220
  {
221
+ var reportData = await _reportService.GetProjectProgressReportAsync(
222
+ search, startDate, endDate, status, User.GetRoleId(), User.GetUserId(), assignedToMe, assignedToMyTeam);
223
  return StatusCode(reportData.StatusCode, reportData);
224
  }
225
 
226
+ var reportDataPaged = await _reportService.GetPagedProjectProgressReportAsync(
227
+ search, startDate, endDate, status, User.GetRoleId(), User.GetUserId(),
228
+ paging.Page ?? 1, paging.Limit ?? 10, assignedToMe, assignedToMyTeam);
229
+ return Ok(reportDataPaged);
 
 
 
 
 
 
230
  }
231
  }
232
  }
TaskTrackingSystem.WebApi/Features/Report/ReportService.cs CHANGED
@@ -8,6 +8,7 @@ using System.Threading.Tasks;
8
  using TaskTrackingSystem.Database.AppDbContextModels;
9
  using TaskTrackingSystem.Shared;
10
  using TaskTrackingSystem.Shared.Models.Report;
 
11
  using TaskTrackingSystem.Shared.Enums;
12
  using TaskTrackingSystem.WebApi.Infrastructure;
13
 
@@ -24,61 +25,54 @@ namespace TaskTrackingSystem.WebApi.Features.Report
24
  _db = db;
25
  }
26
 
27
- private static bool IsAdmin(string roleName)
28
  {
29
- return roleName.Equals("Admin", StringComparison.OrdinalIgnoreCase);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  }
31
 
32
- private IQueryable<TaskTrackingSystem.Database.AppDbContextModels.Task> BuildAccessibleTaskQuery(string roleName, long currentUserId)
33
  {
34
- var query = _db.Tasks.Where(t => t.IsDeleted != true && !t.IsArchived);
35
-
36
- if (IsAdmin(roleName))
37
- {
38
- return query;
39
- }
40
-
41
- if (roleName.Equals("Manager", StringComparison.OrdinalIgnoreCase))
42
- {
43
- return query.Where(t =>
44
- t.AssignedTo == currentUserId ||
45
- t.CreatedBy == currentUserId ||
46
- t.Project.ProjectMembers.Any(pm => pm.UserId == currentUserId));
47
- }
48
-
49
- return query.Where(t =>
50
- t.AssignedTo == currentUserId ||
51
- t.CreatedBy == currentUserId);
52
  }
53
 
54
- private IQueryable<TaskTrackingSystem.Database.AppDbContextModels.Project> BuildAccessibleProjectQuery(string roleName, long currentUserId)
55
  {
56
- var query = _db.Projects.Where(p => p.IsDeleted != true);
57
-
58
- if (IsAdmin(roleName))
59
- {
60
- return query;
61
- }
62
-
63
- return query.Where(p =>
64
- p.CreatedById == currentUserId ||
65
- p.ProjectMembers.Any(pm => pm.UserId == currentUserId));
66
  }
67
 
68
- private IQueryable<TaskTrackingSystem.Database.AppDbContextModels.User> BuildAccessibleUserQuery(string roleName, long currentUserId)
69
  {
70
- var users = _db.Users.Where(u => !u.IsDeleted);
71
-
72
- if (IsAdmin(roleName))
73
- {
74
- return users;
75
- }
76
-
77
- var accessibleUserIds = BuildAccessibleProjectQuery(roleName, currentUserId)
78
- .SelectMany(p => p.ProjectMembers.Select(pm => pm.UserId))
79
- .Distinct();
80
-
81
- return users.Where(u => u.Id == currentUserId || accessibleUserIds.Contains(u.Id));
82
  }
83
 
84
  private static PagedResult<TDestination> MapPagedResult<TSource, TDestination>(
@@ -95,15 +89,33 @@ namespace TaskTrackingSystem.WebApi.Features.Report
95
  };
96
  }
97
 
98
- private IQueryable<TaskTrackingSystem.Database.AppDbContextModels.Task> BuildTaskReportQuery(
99
- string roleName,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
  long currentUserId,
101
  DateTime? startDate,
102
  DateTime? endDate,
103
  string? status,
104
- int? projectId)
 
 
105
  {
106
- var query = BuildAccessibleTaskQuery(roleName, currentUserId)
107
  .Include(t => t.Project)
108
  .Include(t => t.AssignedToNavigation)
109
  .Include(t => t.AssignedByNavigation)
@@ -146,6 +158,15 @@ namespace TaskTrackingSystem.WebApi.Features.Report
146
  }
147
  }
148
 
 
 
 
 
 
 
 
 
 
149
  return query;
150
  }
151
 
@@ -170,12 +191,16 @@ namespace TaskTrackingSystem.WebApi.Features.Report
170
  };
171
  }
172
 
173
- // ─── Legacy endpoints (kept for backward compatibility) ───────────────────
174
 
175
  public async Task<Result<IEnumerable<TaskReportDto>>> GetTasksReportAsync(
176
- DateTime? startDate, DateTime? endDate, string? status, int? projectId, string roleName, long currentUserId)
 
177
  {
178
- var tasks = await BuildTaskReportQuery(roleName, currentUserId, startDate, endDate, status, projectId)
 
 
 
179
  .OrderBy(t => t.DueDate)
180
  .ThenByDescending(t => t.CreatedAt ?? DateTime.UtcNow)
181
  .ToListAsync();
@@ -188,12 +213,17 @@ namespace TaskTrackingSystem.WebApi.Features.Report
188
  DateTime? endDate,
189
  string? status,
190
  int? projectId,
191
- string roleName,
192
  long currentUserId,
193
  int page,
194
- int pageSize)
 
 
195
  {
196
- var paged = await BuildTaskReportQuery(roleName, currentUserId, startDate, endDate, status, projectId)
 
 
 
197
  .OrderBy(t => t.DueDate)
198
  .ThenByDescending(t => t.CreatedAt ?? DateTime.UtcNow)
199
  .ToPagedResultAsync(page, pageSize);
@@ -201,23 +231,21 @@ namespace TaskTrackingSystem.WebApi.Features.Report
201
  return MapPagedResult(paged, MapTaskReport);
202
  }
203
 
204
- public async Task<Result<IEnumerable<UserProductivityDto>>> GetUserProductivityReportAsync(string roleName, long currentUserId)
205
  {
206
- var users = await BuildAccessibleUserQuery(roleName, currentUserId).ToListAsync();
207
- var tasks = await BuildAccessibleTaskQuery(roleName, currentUserId).ToListAsync();
 
 
208
  var list = new List<UserProductivityDto>();
209
  foreach (var user in users)
210
  {
211
  var userTasks = tasks.Where(t => t.AssignedTo == user.Id).ToList();
212
-
213
  int total = userTasks.Count;
214
  var completedTasks = userTasks.Where(t => t.StatusId == AppTaskStatus.Done).ToList();
215
  int done = completedTasks.Count;
216
-
217
- int onTimeCount = completedTasks.Count(t =>
218
- (t.UpdatedAt ?? t.CreatedAt) <= t.DueDate
219
- );
220
-
221
  double onTimeDeliveryRate = done > 0 ? Math.Round(((double)onTimeCount / done) * 100, 2) : 0;
222
 
223
  list.Add(new UserProductivityDto
@@ -234,12 +262,12 @@ namespace TaskTrackingSystem.WebApi.Features.Report
234
  return Result<IEnumerable<UserProductivityDto>>.Success(list);
235
  }
236
 
237
- // ─── Report 1: Task Status Summary ────────────────────────────────────────
238
-
239
  public async Task<Result<IEnumerable<TaskStatusSummaryDto>>> GetTaskStatusSummaryAsync(
240
- string? search, AppTaskStatus? statusId, long? projectId, string roleName, long currentUserId)
241
  {
242
- var query = BuildAccessibleTaskQuery(roleName, currentUserId)
 
 
243
  .Include(t => t.Project)
244
  .Include(t => t.AssignedToNavigation)
245
  .Where(t => t.IsDeleted != true);
@@ -312,12 +340,12 @@ namespace TaskTrackingSystem.WebApi.Features.Report
312
 
313
  public async Task<PagedResult<TeamProductivityReportDto>> GetPagedTeamProductivityAsync(
314
  string? search,
315
- string roleName,
316
  long currentUserId,
317
  int page,
318
  int pageSize)
319
  {
320
- var full = await GetTeamProductivityAsync(search, roleName, currentUserId);
321
  if (!full.IsSuccess || full.Value == null)
322
  {
323
  return new PagedResult<TeamProductivityReportDto>();
@@ -346,12 +374,14 @@ namespace TaskTrackingSystem.WebApi.Features.Report
346
  };
347
  }
348
 
349
- // ─── Report 2: Team Productivity ──────────────────────────────────────────
350
 
351
- public async Task<Result<IEnumerable<TeamProductivityReportDto>>> GetTeamProductivityAsync(string? search, string roleName, long currentUserId)
352
  {
353
- var users = await BuildAccessibleUserQuery(roleName, currentUserId).ToListAsync();
354
- var allTasks = await BuildAccessibleTaskQuery(roleName, currentUserId)
 
 
355
  .Where(t => t.AssignedTo != null)
356
  .ToListAsync();
357
  var now = DateTime.UtcNow;
@@ -420,14 +450,18 @@ namespace TaskTrackingSystem.WebApi.Features.Report
420
  public async Task<Result<IEnumerable<OverdueCriticalTaskDto>>> GetOverdueCriticalTasksAsync(
421
  string? search,
422
  long? projectId,
423
- string roleName,
424
  long currentUserId,
425
  int? priorityId = null,
426
- string? delayType = null)
 
 
427
  {
 
 
428
  var now = DateTime.UtcNow;
429
  var today = DateTime.Today;
430
- var query = BuildAccessibleTaskQuery(roleName, currentUserId)
431
  .Include(t => t.Project)
432
  .Include(t => t.AssignedToNavigation)
433
  .Where(t => t.IsDeleted != true && t.StatusId != AppTaskStatus.Done &&
@@ -449,6 +483,15 @@ namespace TaskTrackingSystem.WebApi.Features.Report
449
  break;
450
  }
451
 
 
 
 
 
 
 
 
 
 
452
  var tasks = await query.OrderBy(t => t.DueDate).ToListAsync();
453
 
454
  if (!string.IsNullOrWhiteSpace(search))
@@ -469,6 +512,7 @@ namespace TaskTrackingSystem.WebApi.Features.Report
469
  StatusName = StatusMap.TryGetValue(t.StatusId, out var s) ? s : $"Status {t.StatusId}",
470
  PriorityName = PriorityMap.TryGetValue(t.PriorityId, out var p) ? p : $"Priority {t.PriorityId}",
471
  AssignedTo = t.AssignedToNavigation != null ? $"{t.AssignedToNavigation.FirstName} {t.AssignedToNavigation.LastName}" : null,
 
472
  DueDate = t.DueDate,
473
  DaysOverdue = t.DueDate < now ? (int)(now - t.DueDate).TotalDays : 0,
474
  CreatedAt = t.CreatedAt ?? DateTime.UtcNow
@@ -480,16 +524,20 @@ namespace TaskTrackingSystem.WebApi.Features.Report
480
  public async Task<PagedResult<OverdueCriticalTaskDto>> GetPagedOverdueCriticalTasksAsync(
481
  string? search,
482
  long? projectId,
483
- string roleName,
484
  long currentUserId,
485
  int page,
486
  int pageSize,
487
  int? priorityId = null,
488
- string? delayType = null)
 
 
489
  {
 
 
490
  var now = DateTime.UtcNow;
491
  var today = DateTime.Today;
492
- var query = BuildAccessibleTaskQuery(roleName, currentUserId)
493
  .Include(t => t.Project)
494
  .Include(t => t.AssignedToNavigation)
495
  .Where(t => t.IsDeleted != true && t.StatusId != AppTaskStatus.Done &&
@@ -514,6 +562,15 @@ namespace TaskTrackingSystem.WebApi.Features.Report
514
  break;
515
  }
516
 
 
 
 
 
 
 
 
 
 
517
  if (!string.IsNullOrWhiteSpace(search))
518
  {
519
  var s = search.Trim().ToLower();
@@ -536,6 +593,7 @@ namespace TaskTrackingSystem.WebApi.Features.Report
536
  StatusName = StatusMap.TryGetValue(t.StatusId, out var s) ? s : $"Status {t.StatusId}",
537
  PriorityName = PriorityMap.TryGetValue(t.PriorityId, out var p) ? p : $"Priority {t.PriorityId}",
538
  AssignedTo = t.AssignedToNavigation != null ? $"{t.AssignedToNavigation.FirstName} {t.AssignedToNavigation.LastName}" : null,
 
539
  DueDate = t.DueDate,
540
  DaysOverdue = t.DueDate < now ? (int)(now - t.DueDate).TotalDays : 0,
541
  CreatedAt = t.CreatedAt ?? DateTime.UtcNow
@@ -582,11 +640,25 @@ namespace TaskTrackingSystem.WebApi.Features.Report
582
  DateTime? startDate,
583
  DateTime? endDate,
584
  string? status,
585
- string roleName,
586
- long currentUserId)
 
 
587
  {
588
- var users = await BuildAccessibleUserQuery(roleName, currentUserId).ToListAsync();
589
- var tasks = await BuildAccessibleTaskQuery(roleName, currentUserId).ToListAsync();
 
 
 
 
 
 
 
 
 
 
 
 
590
 
591
  if (startDate.HasValue)
592
  tasks = tasks.Where(t => t.CreatedAt >= startDate.Value.Date).ToList();
@@ -647,12 +719,14 @@ namespace TaskTrackingSystem.WebApi.Features.Report
647
  DateTime? startDate,
648
  DateTime? endDate,
649
  string? status,
650
- string roleName,
651
  long currentUserId,
652
  int page,
653
- int pageSize)
 
 
654
  {
655
- var full = await GetEmployeeProductivityReportAsync(search, startDate, endDate, status, roleName, currentUserId);
656
  if (!full.IsSuccess || full.Value == null)
657
  {
658
  return new PagedResult<EmployeeProductivityReportDto>();
@@ -686,11 +760,27 @@ namespace TaskTrackingSystem.WebApi.Features.Report
686
  DateTime? startDate,
687
  DateTime? endDate,
688
  string? status,
689
- string roleName,
690
- long currentUserId)
 
 
691
  {
692
- var projects = await BuildAccessibleProjectQuery(roleName, currentUserId).ToListAsync();
693
- var tasks = await BuildAccessibleTaskQuery(roleName, currentUserId).ToListAsync();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
694
  var today = DateTime.Today;
695
 
696
  if (startDate.HasValue)
@@ -762,12 +852,14 @@ namespace TaskTrackingSystem.WebApi.Features.Report
762
  DateTime? startDate,
763
  DateTime? endDate,
764
  string? status,
765
- string roleName,
766
  long currentUserId,
767
  int page,
768
- int pageSize)
 
 
769
  {
770
- var full = await GetProjectProgressReportAsync(search, startDate, endDate, status, roleName, currentUserId);
771
  if (!full.IsSuccess || full.Value == null)
772
  {
773
  return new PagedResult<ProjectProgressReportDto>();
 
8
  using TaskTrackingSystem.Database.AppDbContextModels;
9
  using TaskTrackingSystem.Shared;
10
  using TaskTrackingSystem.Shared.Models.Report;
11
+ using TaskTrackingSystem.Shared.Models.Issue;
12
  using TaskTrackingSystem.Shared.Enums;
13
  using TaskTrackingSystem.WebApi.Infrastructure;
14
 
 
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,
39
+ TaskId = i.TaskId,
40
+ TaskTitle = i.Task.Title,
41
+ ProjectId = i.Task.ProjectId,
42
+ ProjectName = i.Task.Project.Name,
43
+ Title = i.Title,
44
+ Description = i.Description,
45
+ AssignedTo = i.AssignedTo,
46
+ AssignedToName = i.AssignedToNavigation != null
47
+ ? i.AssignedToNavigation.FirstName + " " + i.AssignedToNavigation.LastName
48
+ : null,
49
+ EstimatedHours = i.EstimatedHours,
50
+ ActualHours = i.ActualHours,
51
+ StartDate = i.StartDate,
52
+ DueDate = i.DueDate,
53
+ StatusId = i.StatusId,
54
+ PriorityId = i.PriorityId,
55
+ CreatedAt = i.CreatedAt ?? DateTime.UtcNow,
56
+ UpdatedAt = i.UpdatedAt
57
+ })
58
+ .ToListAsync();
59
+
60
+ return Result<List<IssueDto>>.Success(list);
61
  }
62
 
63
+ private IQueryable<TaskTrackingSystem.Database.AppDbContextModels.Task> BuildAccessibleTaskQuery(bool isAdmin, bool isManager, long currentUserId)
64
  {
65
+ return _db.Tasks.Where(t => t.IsDeleted != true && !t.IsArchived);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  }
67
 
68
+ private IQueryable<TaskTrackingSystem.Database.AppDbContextModels.Project> BuildAccessibleProjectQuery(bool isAdmin, long currentUserId)
69
  {
70
+ return _db.Projects.Where(p => p.IsDeleted != true);
 
 
 
 
 
 
 
 
 
71
  }
72
 
73
+ private IQueryable<TaskTrackingSystem.Database.AppDbContextModels.User> BuildAccessibleUserQuery(bool isAdmin, long currentUserId)
74
  {
75
+ return _db.Users.Where(u => !u.IsDeleted);
 
 
 
 
 
 
 
 
 
 
 
76
  }
77
 
78
  private static PagedResult<TDestination> MapPagedResult<TSource, TDestination>(
 
89
  };
90
  }
91
 
92
+ private async Task<List<long>> GetTeamUserIdsAsync(long currentUserId)
93
+ {
94
+ var myProjectIds = await _db.ProjectMembers
95
+ .Where(pm => pm.UserId == currentUserId)
96
+ .Select(pm => pm.ProjectId)
97
+ .Distinct()
98
+ .ToListAsync();
99
+
100
+ return await _db.ProjectMembers
101
+ .Where(pm => myProjectIds.Contains(pm.ProjectId) && pm.UserId != currentUserId)
102
+ .Select(pm => pm.UserId)
103
+ .Distinct()
104
+ .ToListAsync();
105
+ }
106
+
107
+ private async Task<IQueryable<TaskTrackingSystem.Database.AppDbContextModels.Task>> BuildTaskReportQueryAsync(
108
+ bool isAdmin,
109
+ bool isManager,
110
  long currentUserId,
111
  DateTime? startDate,
112
  DateTime? endDate,
113
  string? status,
114
+ int? projectId,
115
+ bool? assignedToMe,
116
+ bool? assignedToMyTeam)
117
  {
118
+ var query = BuildAccessibleTaskQuery(isAdmin, isManager, currentUserId)
119
  .Include(t => t.Project)
120
  .Include(t => t.AssignedToNavigation)
121
  .Include(t => t.AssignedByNavigation)
 
158
  }
159
  }
160
 
161
+ if (assignedToMe == true || assignedToMyTeam == true)
162
+ {
163
+ var teamIds = assignedToMyTeam == true ? await GetTeamUserIdsAsync(currentUserId) : new List<long>();
164
+ query = query.Where(t =>
165
+ (assignedToMe == true && t.AssignedTo == currentUserId) ||
166
+ (assignedToMyTeam == true && t.AssignedTo.HasValue && teamIds.Contains(t.AssignedTo.Value))
167
+ );
168
+ }
169
+
170
  return query;
171
  }
172
 
 
191
  };
192
  }
193
 
194
+ // ——— Legacy endpoints (kept for backward compatibility) ———
195
 
196
  public async Task<Result<IEnumerable<TaskReportDto>>> GetTasksReportAsync(
197
+ DateTime? startDate, DateTime? endDate, string? status, int? projectId, long roleId, long currentUserId,
198
+ bool? assignedToMe = null, bool? assignedToMyTeam = null)
199
  {
200
+ var isAdmin = await DataScopeAuthorization.IsAdminScopeAsync(_db, roleId);
201
+ var isManager = await DataScopeAuthorization.IsManagerScopeAsync(_db, roleId);
202
+ var query = await BuildTaskReportQueryAsync(isAdmin, isManager, currentUserId, startDate, endDate, status, projectId, assignedToMe, assignedToMyTeam);
203
+ var tasks = await query
204
  .OrderBy(t => t.DueDate)
205
  .ThenByDescending(t => t.CreatedAt ?? DateTime.UtcNow)
206
  .ToListAsync();
 
213
  DateTime? endDate,
214
  string? status,
215
  int? projectId,
216
+ long roleId,
217
  long currentUserId,
218
  int page,
219
+ int pageSize,
220
+ bool? assignedToMe = null,
221
+ bool? assignedToMyTeam = null)
222
  {
223
+ var isAdmin = await DataScopeAuthorization.IsAdminScopeAsync(_db, roleId);
224
+ var isManager = await DataScopeAuthorization.IsManagerScopeAsync(_db, roleId);
225
+ var query = await BuildTaskReportQueryAsync(isAdmin, isManager, currentUserId, startDate, endDate, status, projectId, assignedToMe, assignedToMyTeam);
226
+ var paged = await query
227
  .OrderBy(t => t.DueDate)
228
  .ThenByDescending(t => t.CreatedAt ?? DateTime.UtcNow)
229
  .ToPagedResultAsync(page, pageSize);
 
231
  return MapPagedResult(paged, MapTaskReport);
232
  }
233
 
234
+ public async Task<Result<IEnumerable<UserProductivityDto>>> GetUserProductivityReportAsync(long roleId, long currentUserId)
235
  {
236
+ var isAdmin = await DataScopeAuthorization.IsAdminScopeAsync(_db, roleId);
237
+ var isManager = await DataScopeAuthorization.IsManagerScopeAsync(_db, roleId);
238
+ var users = await BuildAccessibleUserQuery(isAdmin, currentUserId).ToListAsync();
239
+ var tasks = await BuildAccessibleTaskQuery(isAdmin, isManager, currentUserId).ToListAsync();
240
  var list = new List<UserProductivityDto>();
241
  foreach (var user in users)
242
  {
243
  var userTasks = tasks.Where(t => t.AssignedTo == user.Id).ToList();
 
244
  int total = userTasks.Count;
245
  var completedTasks = userTasks.Where(t => t.StatusId == AppTaskStatus.Done).ToList();
246
  int done = completedTasks.Count;
247
+
248
+ int onTimeCount = completedTasks.Count(t => (t.UpdatedAt ?? t.CreatedAt) <= t.DueDate);
 
 
 
249
  double onTimeDeliveryRate = done > 0 ? Math.Round(((double)onTimeCount / done) * 100, 2) : 0;
250
 
251
  list.Add(new UserProductivityDto
 
262
  return Result<IEnumerable<UserProductivityDto>>.Success(list);
263
  }
264
 
 
 
265
  public async Task<Result<IEnumerable<TaskStatusSummaryDto>>> GetTaskStatusSummaryAsync(
266
+ string? search, AppTaskStatus? statusId, long? projectId, long roleId, long currentUserId)
267
  {
268
+ var isAdmin = await DataScopeAuthorization.IsAdminScopeAsync(_db, roleId);
269
+ var isManager = await DataScopeAuthorization.IsManagerScopeAsync(_db, roleId);
270
+ var query = BuildAccessibleTaskQuery(isAdmin, isManager, currentUserId)
271
  .Include(t => t.Project)
272
  .Include(t => t.AssignedToNavigation)
273
  .Where(t => t.IsDeleted != true);
 
340
 
341
  public async Task<PagedResult<TeamProductivityReportDto>> GetPagedTeamProductivityAsync(
342
  string? search,
343
+ long roleId,
344
  long currentUserId,
345
  int page,
346
  int pageSize)
347
  {
348
+ var full = await GetTeamProductivityAsync(search, roleId, currentUserId);
349
  if (!full.IsSuccess || full.Value == null)
350
  {
351
  return new PagedResult<TeamProductivityReportDto>();
 
374
  };
375
  }
376
 
377
+ // ——— Report 2: Team Productivity ———
378
 
379
+ public async Task<Result<IEnumerable<TeamProductivityReportDto>>> GetTeamProductivityAsync(string? search, long roleId, long currentUserId)
380
  {
381
+ var isAdmin = await DataScopeAuthorization.IsAdminScopeAsync(_db, roleId);
382
+ var isManager = await DataScopeAuthorization.IsManagerScopeAsync(_db, roleId);
383
+ var users = await BuildAccessibleUserQuery(isAdmin, currentUserId).ToListAsync();
384
+ var allTasks = await BuildAccessibleTaskQuery(isAdmin, isManager, currentUserId)
385
  .Where(t => t.AssignedTo != null)
386
  .ToListAsync();
387
  var now = DateTime.UtcNow;
 
450
  public async Task<Result<IEnumerable<OverdueCriticalTaskDto>>> GetOverdueCriticalTasksAsync(
451
  string? search,
452
  long? projectId,
453
+ long roleId,
454
  long currentUserId,
455
  int? priorityId = null,
456
+ string? delayType = null,
457
+ bool? assignedToMe = null,
458
+ bool? assignedToMyTeam = null)
459
  {
460
+ var isAdmin = await DataScopeAuthorization.IsAdminScopeAsync(_db, roleId);
461
+ var isManager = await DataScopeAuthorization.IsManagerScopeAsync(_db, roleId);
462
  var now = DateTime.UtcNow;
463
  var today = DateTime.Today;
464
+ var query = BuildAccessibleTaskQuery(isAdmin, isManager, currentUserId)
465
  .Include(t => t.Project)
466
  .Include(t => t.AssignedToNavigation)
467
  .Where(t => t.IsDeleted != true && t.StatusId != AppTaskStatus.Done &&
 
483
  break;
484
  }
485
 
486
+ if (assignedToMe == true || assignedToMyTeam == true)
487
+ {
488
+ var teamIds = assignedToMyTeam == true ? await GetTeamUserIdsAsync(currentUserId) : new List<long>();
489
+ query = query.Where(t =>
490
+ (assignedToMe == true && t.AssignedTo == currentUserId) ||
491
+ (assignedToMyTeam == true && t.AssignedTo.HasValue && teamIds.Contains(t.AssignedTo.Value))
492
+ );
493
+ }
494
+
495
  var tasks = await query.OrderBy(t => t.DueDate).ToListAsync();
496
 
497
  if (!string.IsNullOrWhiteSpace(search))
 
512
  StatusName = StatusMap.TryGetValue(t.StatusId, out var s) ? s : $"Status {t.StatusId}",
513
  PriorityName = PriorityMap.TryGetValue(t.PriorityId, out var p) ? p : $"Priority {t.PriorityId}",
514
  AssignedTo = t.AssignedToNavigation != null ? $"{t.AssignedToNavigation.FirstName} {t.AssignedToNavigation.LastName}" : null,
515
+ AssignedToUserId = t.AssignedTo,
516
  DueDate = t.DueDate,
517
  DaysOverdue = t.DueDate < now ? (int)(now - t.DueDate).TotalDays : 0,
518
  CreatedAt = t.CreatedAt ?? DateTime.UtcNow
 
524
  public async Task<PagedResult<OverdueCriticalTaskDto>> GetPagedOverdueCriticalTasksAsync(
525
  string? search,
526
  long? projectId,
527
+ long roleId,
528
  long currentUserId,
529
  int page,
530
  int pageSize,
531
  int? priorityId = null,
532
+ string? delayType = null,
533
+ bool? assignedToMe = null,
534
+ bool? assignedToMyTeam = null)
535
  {
536
+ var isAdmin = await DataScopeAuthorization.IsAdminScopeAsync(_db, roleId);
537
+ var isManager = await DataScopeAuthorization.IsManagerScopeAsync(_db, roleId);
538
  var now = DateTime.UtcNow;
539
  var today = DateTime.Today;
540
+ var query = BuildAccessibleTaskQuery(isAdmin, isManager, currentUserId)
541
  .Include(t => t.Project)
542
  .Include(t => t.AssignedToNavigation)
543
  .Where(t => t.IsDeleted != true && t.StatusId != AppTaskStatus.Done &&
 
562
  break;
563
  }
564
 
565
+ if (assignedToMe == true || assignedToMyTeam == true)
566
+ {
567
+ var teamIds = assignedToMyTeam == true ? await GetTeamUserIdsAsync(currentUserId) : new List<long>();
568
+ query = query.Where(t =>
569
+ (assignedToMe == true && t.AssignedTo == currentUserId) ||
570
+ (assignedToMyTeam == true && t.AssignedTo.HasValue && teamIds.Contains(t.AssignedTo.Value))
571
+ );
572
+ }
573
+
574
  if (!string.IsNullOrWhiteSpace(search))
575
  {
576
  var s = search.Trim().ToLower();
 
593
  StatusName = StatusMap.TryGetValue(t.StatusId, out var s) ? s : $"Status {t.StatusId}",
594
  PriorityName = PriorityMap.TryGetValue(t.PriorityId, out var p) ? p : $"Priority {t.PriorityId}",
595
  AssignedTo = t.AssignedToNavigation != null ? $"{t.AssignedToNavigation.FirstName} {t.AssignedToNavigation.LastName}" : null,
596
+ AssignedToUserId = t.AssignedTo,
597
  DueDate = t.DueDate,
598
  DaysOverdue = t.DueDate < now ? (int)(now - t.DueDate).TotalDays : 0,
599
  CreatedAt = t.CreatedAt ?? DateTime.UtcNow
 
640
  DateTime? startDate,
641
  DateTime? endDate,
642
  string? status,
643
+ long roleId,
644
+ long currentUserId,
645
+ bool? assignedToMe = null,
646
+ bool? assignedToMyTeam = null)
647
  {
648
+ var isAdmin = await DataScopeAuthorization.IsAdminScopeAsync(_db, roleId);
649
+ var isManager = await DataScopeAuthorization.IsManagerScopeAsync(_db, roleId);
650
+ var users = await BuildAccessibleUserQuery(isAdmin, currentUserId).ToListAsync();
651
+
652
+ if (assignedToMe == true || assignedToMyTeam == true)
653
+ {
654
+ var teamIds = assignedToMyTeam == true ? await GetTeamUserIdsAsync(currentUserId) : new List<long>();
655
+ users = users.Where(u =>
656
+ (assignedToMe == true && u.Id == currentUserId) ||
657
+ (assignedToMyTeam == true && teamIds.Contains(u.Id))
658
+ ).ToList();
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();
 
719
  DateTime? startDate,
720
  DateTime? endDate,
721
  string? status,
722
+ long roleId,
723
  long currentUserId,
724
  int page,
725
+ int pageSize,
726
+ bool? assignedToMe = null,
727
+ bool? assignedToMyTeam = null)
728
  {
729
+ var full = await GetEmployeeProductivityReportAsync(search, startDate, endDate, status, roleId, currentUserId, assignedToMe, assignedToMyTeam);
730
  if (!full.IsSuccess || full.Value == null)
731
  {
732
  return new PagedResult<EmployeeProductivityReportDto>();
 
760
  DateTime? startDate,
761
  DateTime? endDate,
762
  string? status,
763
+ long roleId,
764
+ long currentUserId,
765
+ bool? assignedToMe = null,
766
+ bool? assignedToMyTeam = null)
767
  {
768
+ var isAdmin = await DataScopeAuthorization.IsAdminScopeAsync(_db, roleId);
769
+ var isManager = await DataScopeAuthorization.IsManagerScopeAsync(_db, roleId);
770
+ var projects = await BuildAccessibleProjectQuery(isAdmin, currentUserId).ToListAsync();
771
+
772
+ if (assignedToMe == true || assignedToMyTeam == true)
773
+ {
774
+ var myProjectIds = await _db.ProjectMembers
775
+ .Where(pm => pm.UserId == currentUserId)
776
+ .Select(pm => pm.ProjectId)
777
+ .Distinct()
778
+ .ToListAsync();
779
+
780
+ projects = projects.Where(p => myProjectIds.Contains(p.Id) || p.CreatedById == currentUserId).ToList();
781
+ }
782
+
783
+ var tasks = await BuildAccessibleTaskQuery(isAdmin, isManager, currentUserId).ToListAsync();
784
  var today = DateTime.Today;
785
 
786
  if (startDate.HasValue)
 
852
  DateTime? startDate,
853
  DateTime? endDate,
854
  string? status,
855
+ long roleId,
856
  long currentUserId,
857
  int page,
858
+ int pageSize,
859
+ bool? assignedToMe = null,
860
+ bool? assignedToMyTeam = null)
861
  {
862
+ var full = await GetProjectProgressReportAsync(search, startDate, endDate, status, roleId, currentUserId, assignedToMe, assignedToMyTeam);
863
  if (!full.IsSuccess || full.Value == null)
864
  {
865
  return new PagedResult<ProjectProgressReportDto>();
TaskTrackingSystem.WebApi/Features/Task/TaskController.cs CHANGED
@@ -36,14 +36,14 @@ namespace TaskTrackingSystem.WebApi.Features.Task
36
  {
37
  if (paging == null || (!paging.Page.HasValue && !paging.Limit.HasValue))
38
  {
39
- var tasks = await _taskService.GetAllTasksAsync(User.GetRoleName(), User.GetUserId());
40
  return Ok(tasks);
41
  }
42
 
43
  var page = PaginationExtensions.NormalizePage(paging.Page);
44
  var limit = PaginationExtensions.NormalizePageSize(paging.Limit ?? 0);
45
  var paged = await _taskService.GetPagedTasksAsync(
46
- User.GetRoleName(),
47
  User.GetUserId(),
48
  search,
49
  projectId,
@@ -59,14 +59,14 @@ namespace TaskTrackingSystem.WebApi.Features.Task
59
  [HttpGet("archived")]
60
  public async Task<ActionResult<IEnumerable<TaskDto>>> GetArchivedTasks()
61
  {
62
- var tasks = await _taskService.GetArchivedTasksAsync(User.GetRoleName(), User.GetUserId());
63
  return Ok(tasks);
64
  }
65
 
66
  [HttpGet("{id}")]
67
  public async Task<ActionResult<TaskDto>> GetTask(long id)
68
  {
69
- var task = await _taskService.GetTaskByIdAsync(id, User.GetRoleName(), User.GetUserId());
70
  if (task == null)
71
  {
72
  return NotFound(new { message = $"Task with ID {id} not found." });
@@ -83,7 +83,7 @@ namespace TaskTrackingSystem.WebApi.Features.Task
83
  }
84
 
85
  var currentUserId = User.GetUserId();
86
- var result = await _taskService.CreateTaskAsync(createTaskDto, User.GetRoleName(), currentUserId);
87
  return StatusCode(result.StatusCode, result);
88
  }
89
 
@@ -115,14 +115,14 @@ namespace TaskTrackingSystem.WebApi.Features.Task
115
  [HttpGet("/api/User/{userId}/tasks")]
116
  public async Task<ActionResult<Result<IEnumerable<TaskDto>>>> GetUserTasks(long userId)
117
  {
118
- var result = await _taskService.GetTasksByUserIdAsync(userId, User.GetRoleName(), User.GetUserId());
119
  return StatusCode(result.StatusCode, result);
120
  }
121
 
122
  [HttpPatch("{id}/status")]
123
  public async Task<IActionResult> UpdateTaskStatus(long id, [FromQuery] AppTaskStatus statusId)
124
  {
125
- var result = await _taskService.UpdateTaskStatusAsync(id, statusId, User.GetRoleName(), User.GetUserId());
126
  if (!result.IsSuccess)
127
  {
128
  return StatusCode(result.StatusCode, new { message = result.ErrorMessage });
@@ -138,7 +138,7 @@ namespace TaskTrackingSystem.WebApi.Features.Task
138
  return Forbid();
139
  }
140
 
141
- var result = await _taskService.ArchiveDoneTasksAsync(projectId, User.GetRoleName(), User.GetUserId());
142
  return StatusCode(result.StatusCode, result);
143
  }
144
  }
 
36
  {
37
  if (paging == null || (!paging.Page.HasValue && !paging.Limit.HasValue))
38
  {
39
+ var tasks = await _taskService.GetAllTasksAsync(User.GetRoleId(), User.GetUserId());
40
  return Ok(tasks);
41
  }
42
 
43
  var page = PaginationExtensions.NormalizePage(paging.Page);
44
  var limit = PaginationExtensions.NormalizePageSize(paging.Limit ?? 0);
45
  var paged = await _taskService.GetPagedTasksAsync(
46
+ User.GetRoleId(),
47
  User.GetUserId(),
48
  search,
49
  projectId,
 
59
  [HttpGet("archived")]
60
  public async Task<ActionResult<IEnumerable<TaskDto>>> GetArchivedTasks()
61
  {
62
+ var tasks = await _taskService.GetArchivedTasksAsync(User.GetRoleId(), User.GetUserId());
63
  return Ok(tasks);
64
  }
65
 
66
  [HttpGet("{id}")]
67
  public async Task<ActionResult<TaskDto>> GetTask(long id)
68
  {
69
+ var task = await _taskService.GetTaskByIdAsync(id, User.GetRoleId(), User.GetUserId());
70
  if (task == null)
71
  {
72
  return NotFound(new { message = $"Task with ID {id} not found." });
 
83
  }
84
 
85
  var currentUserId = User.GetUserId();
86
+ var result = await _taskService.CreateTaskAsync(createTaskDto, User.GetRoleId(), currentUserId);
87
  return StatusCode(result.StatusCode, result);
88
  }
89
 
 
115
  [HttpGet("/api/User/{userId}/tasks")]
116
  public async Task<ActionResult<Result<IEnumerable<TaskDto>>>> GetUserTasks(long userId)
117
  {
118
+ var result = await _taskService.GetTasksByUserIdAsync(userId, User.GetRoleId(), User.GetUserId());
119
  return StatusCode(result.StatusCode, result);
120
  }
121
 
122
  [HttpPatch("{id}/status")]
123
  public async Task<IActionResult> UpdateTaskStatus(long id, [FromQuery] AppTaskStatus statusId)
124
  {
125
+ var result = await _taskService.UpdateTaskStatusAsync(id, statusId, User.GetRoleId(), User.GetUserId());
126
  if (!result.IsSuccess)
127
  {
128
  return StatusCode(result.StatusCode, new { message = result.ErrorMessage });
 
138
  return Forbid();
139
  }
140
 
141
+ var result = await _taskService.ArchiveDoneTasksAsync(projectId, User.GetRoleId(), User.GetUserId());
142
  return StatusCode(result.StatusCode, result);
143
  }
144
  }
TaskTrackingSystem.WebApi/Features/Task/TaskDetailsController.cs CHANGED
@@ -125,7 +125,7 @@ namespace TaskTrackingSystem.WebApi.Features.Task
125
  }
126
 
127
  var userId = User.GetUserId();
128
- var isAdmin = User.IsAdmin();
129
 
130
  // Only comment creator or Admin can delete
131
  if (comment.UserId != userId && !isAdmin)
 
125
  }
126
 
127
  var userId = User.GetUserId();
128
+ var isAdmin = await DataScopeAuthorization.IsAdminScopeAsync(_db, User.GetRoleId());
129
 
130
  // Only comment creator or Admin can delete
131
  if (comment.UserId != userId && !isAdmin)
TaskTrackingSystem.WebApi/Features/Task/TaskService.cs CHANGED
@@ -30,9 +30,10 @@ namespace TaskTrackingSystem.WebApi.Features.Task
30
  : null;
31
  }
32
 
33
- public async Task<IEnumerable<TaskDto>> GetAllTasksAsync(string roleName, long currentUserId)
34
  {
35
- return await BuildAccessibleTaskQuery(roleName, currentUserId)
 
36
  .OrderBy(t => t.DueDate)
37
  .ThenByDescending(t => t.CreatedAt ?? DateTime.UtcNow)
38
  .Select(t => new TaskDto
@@ -54,7 +55,7 @@ namespace TaskTrackingSystem.WebApi.Features.Task
54
  }
55
 
56
  public async Task<PagedResult<TaskDto>> GetPagedTasksAsync(
57
- string roleName,
58
  long currentUserId,
59
  string? search,
60
  long? projectId,
@@ -64,7 +65,7 @@ namespace TaskTrackingSystem.WebApi.Features.Task
64
  int page,
65
  int pageSize)
66
  {
67
- var query = BuildAccessibleTaskQuery(roleName, currentUserId);
68
 
69
  if (assignedOnly)
70
  {
@@ -115,9 +116,10 @@ namespace TaskTrackingSystem.WebApi.Features.Task
115
  .ToPagedResultAsync(page, pageSize);
116
  }
117
 
118
- public async Task<IEnumerable<TaskDto>> GetArchivedTasksAsync(string roleName, long currentUserId)
119
  {
120
- return await BuildAccessibleTaskQuery(roleName, currentUserId, includeArchived: true)
 
121
  .Where(t => t.IsArchived)
122
  .OrderByDescending(t => t.UpdatedAt ?? t.CreatedAt ?? DateTime.UtcNow)
123
  .Select(t => new TaskDto
@@ -138,10 +140,10 @@ namespace TaskTrackingSystem.WebApi.Features.Task
138
  .ToListAsync();
139
  }
140
 
141
- public async Task<TaskDto?> GetTaskByIdAsync(long id, string roleName, long currentUserId)
142
  {
143
- var task = await BuildAccessibleTaskQuery(roleName, currentUserId)
144
- .FirstOrDefaultAsync(t => t.Id == id);
145
 
146
  if (task == null)
147
  {
@@ -165,7 +167,7 @@ namespace TaskTrackingSystem.WebApi.Features.Task
165
  };
166
  }
167
 
168
- public async Task<Result<TaskDto>> CreateTaskAsync(CreateTaskDto dto, string roleName, long currentUserId)
169
  {
170
  if (string.IsNullOrWhiteSpace(dto.Title))
171
  {
@@ -183,7 +185,7 @@ namespace TaskTrackingSystem.WebApi.Features.Task
183
  return Result<TaskDto>.Failure(ResultMessages.ProjectNotFound(dto.ProjectId), 404);
184
  }
185
 
186
- if (!await CanAccessProjectAsync(dto.ProjectId, roleName, currentUserId))
187
  {
188
  return Result<TaskDto>.Failure("You do not have access to this project.", 403);
189
  }
@@ -299,7 +301,7 @@ namespace TaskTrackingSystem.WebApi.Features.Task
299
  return Result.Success(200);
300
  }
301
 
302
- public async Task<Result<IEnumerable<TaskDto>>> GetTasksByUserIdAsync(long userId, string roleName, long currentUserId)
303
  {
304
  var userExists = await _db.Users.AnyAsync(u => u.Id == userId && !u.IsDeleted);
305
  if (!userExists)
@@ -307,7 +309,8 @@ namespace TaskTrackingSystem.WebApi.Features.Task
307
  return Result<IEnumerable<TaskDto>>.Failure(ResultMessages.UserNotFound(userId), 404);
308
  }
309
 
310
- var tasks = await BuildAccessibleTaskQuery(roleName, currentUserId)
 
311
  .Where(t => t.AssignedTo == userId)
312
  .Select(t => new TaskDto
313
  {
@@ -329,7 +332,7 @@ namespace TaskTrackingSystem.WebApi.Features.Task
329
  return Result<IEnumerable<TaskDto>>.Success(tasks);
330
  }
331
 
332
- public async Task<Result> UpdateTaskStatusAsync(long id, AppTaskStatus statusId, string roleName, long currentUserId)
333
  {
334
  var task = await _db.Tasks.FirstOrDefaultAsync(t => t.Id == id && t.IsDeleted != true && !t.IsArchived);
335
  if (task == null)
@@ -354,7 +357,7 @@ namespace TaskTrackingSystem.WebApi.Features.Task
354
  return Result.Success(200);
355
  }
356
 
357
- public async Task<Result<int>> ArchiveDoneTasksAsync(long? projectId, string roleName, long currentUserId)
358
  {
359
  if (projectId.HasValue && projectId.Value > 0)
360
  {
@@ -364,21 +367,22 @@ namespace TaskTrackingSystem.WebApi.Features.Task
364
  return Result<int>.Failure(ResultMessages.ProjectNotFound(projectId.Value), 404);
365
  }
366
 
367
- if (!await CanAccessProjectAsync(projectId.Value, roleName, currentUserId))
368
  {
369
  return Result<int>.Failure("You do not have access to this project.", 403);
370
  }
371
  }
372
 
373
- var query = BuildAccessibleTaskQuery(roleName, currentUserId)
374
- .Where(t => t.StatusId == AppTaskStatus.Done);
 
 
375
 
376
  if (projectId.HasValue && projectId.Value > 0)
377
  {
378
- query = query.Where(t => t.ProjectId == projectId.Value);
379
  }
380
 
381
- var tasksToArchive = await query.ToListAsync();
382
  foreach (var task in tasksToArchive)
383
  {
384
  task.IsArchived = true;
@@ -395,16 +399,18 @@ namespace TaskTrackingSystem.WebApi.Features.Task
395
  return Result<int>.Success(tasksToArchive.Count);
396
  }
397
 
398
- private IQueryable<TaskTrackingSystem.Database.AppDbContextModels.Task> BuildAccessibleTaskQuery(string roleName, long currentUserId, bool includeArchived = false)
399
  {
400
  var query = _db.Tasks.Where(t => t.IsDeleted != true && (includeArchived || !t.IsArchived));
401
 
402
- if (IsAdmin(roleName))
 
403
  {
404
  return query;
405
  }
406
 
407
- if (IsManager(roleName))
 
408
  {
409
  return query.Where(t =>
410
  t.AssignedTo == currentUserId ||
@@ -417,9 +423,10 @@ namespace TaskTrackingSystem.WebApi.Features.Task
417
  t.CreatedBy == currentUserId);
418
  }
419
 
420
- private async Task<bool> CanAccessProjectAsync(long projectId, string roleName, long currentUserId)
421
  {
422
- if (IsAdmin(roleName))
 
423
  {
424
  return await _db.Projects.AnyAsync(p => p.Id == projectId && p.IsDeleted != true);
425
  }
@@ -434,15 +441,5 @@ namespace TaskTrackingSystem.WebApi.Features.Task
434
  {
435
  return await _db.ProjectMembers.AnyAsync(pm => pm.ProjectId == projectId && pm.UserId == userId);
436
  }
437
-
438
- private static bool IsAdmin(string roleName)
439
- {
440
- return roleName.Equals("Admin", StringComparison.OrdinalIgnoreCase);
441
- }
442
-
443
- private static bool IsManager(string roleName)
444
- {
445
- return roleName.Equals("Manager", StringComparison.OrdinalIgnoreCase);
446
- }
447
  }
448
  }
 
30
  : null;
31
  }
32
 
33
+ public async Task<IEnumerable<TaskDto>> GetAllTasksAsync(long roleId, long currentUserId)
34
  {
35
+ var query = await BuildAccessibleTaskQueryAsync(roleId, currentUserId);
36
+ return await query
37
  .OrderBy(t => t.DueDate)
38
  .ThenByDescending(t => t.CreatedAt ?? DateTime.UtcNow)
39
  .Select(t => new TaskDto
 
55
  }
56
 
57
  public async Task<PagedResult<TaskDto>> GetPagedTasksAsync(
58
+ long roleId,
59
  long currentUserId,
60
  string? search,
61
  long? projectId,
 
65
  int page,
66
  int pageSize)
67
  {
68
+ var query = await BuildAccessibleTaskQueryAsync(roleId, currentUserId);
69
 
70
  if (assignedOnly)
71
  {
 
116
  .ToPagedResultAsync(page, pageSize);
117
  }
118
 
119
+ public async Task<IEnumerable<TaskDto>> GetArchivedTasksAsync(long roleId, long currentUserId)
120
  {
121
+ var query = await BuildAccessibleTaskQueryAsync(roleId, currentUserId, includeArchived: true);
122
+ return await query
123
  .Where(t => t.IsArchived)
124
  .OrderByDescending(t => t.UpdatedAt ?? t.CreatedAt ?? DateTime.UtcNow)
125
  .Select(t => new TaskDto
 
140
  .ToListAsync();
141
  }
142
 
143
+ public async Task<TaskDto?> GetTaskByIdAsync(long id, long roleId, long currentUserId)
144
  {
145
+ var query = await BuildAccessibleTaskQueryAsync(roleId, currentUserId);
146
+ var task = await query.FirstOrDefaultAsync(t => t.Id == id);
147
 
148
  if (task == null)
149
  {
 
167
  };
168
  }
169
 
170
+ public async Task<Result<TaskDto>> CreateTaskAsync(CreateTaskDto dto, long roleId, long currentUserId)
171
  {
172
  if (string.IsNullOrWhiteSpace(dto.Title))
173
  {
 
185
  return Result<TaskDto>.Failure(ResultMessages.ProjectNotFound(dto.ProjectId), 404);
186
  }
187
 
188
+ if (!await CanAccessProjectAsync(dto.ProjectId, roleId, currentUserId))
189
  {
190
  return Result<TaskDto>.Failure("You do not have access to this project.", 403);
191
  }
 
301
  return Result.Success(200);
302
  }
303
 
304
+ public async Task<Result<IEnumerable<TaskDto>>> GetTasksByUserIdAsync(long userId, long roleId, long currentUserId)
305
  {
306
  var userExists = await _db.Users.AnyAsync(u => u.Id == userId && !u.IsDeleted);
307
  if (!userExists)
 
309
  return Result<IEnumerable<TaskDto>>.Failure(ResultMessages.UserNotFound(userId), 404);
310
  }
311
 
312
+ var query = await BuildAccessibleTaskQueryAsync(roleId, currentUserId);
313
+ var tasks = await query
314
  .Where(t => t.AssignedTo == userId)
315
  .Select(t => new TaskDto
316
  {
 
332
  return Result<IEnumerable<TaskDto>>.Success(tasks);
333
  }
334
 
335
+ public async Task<Result> UpdateTaskStatusAsync(long id, AppTaskStatus statusId, long roleId, long currentUserId)
336
  {
337
  var task = await _db.Tasks.FirstOrDefaultAsync(t => t.Id == id && t.IsDeleted != true && !t.IsArchived);
338
  if (task == null)
 
357
  return Result.Success(200);
358
  }
359
 
360
+ public async Task<Result<int>> ArchiveDoneTasksAsync(long? projectId, long roleId, long currentUserId)
361
  {
362
  if (projectId.HasValue && projectId.Value > 0)
363
  {
 
367
  return Result<int>.Failure(ResultMessages.ProjectNotFound(projectId.Value), 404);
368
  }
369
 
370
+ if (!await CanAccessProjectAsync(projectId.Value, roleId, currentUserId))
371
  {
372
  return Result<int>.Failure("You do not have access to this project.", 403);
373
  }
374
  }
375
 
376
+ var query = await BuildAccessibleTaskQueryAsync(roleId, currentUserId);
377
+ var tasksToArchive = await query
378
+ .Where(t => t.StatusId == AppTaskStatus.Done)
379
+ .ToListAsync();
380
 
381
  if (projectId.HasValue && projectId.Value > 0)
382
  {
383
+ tasksToArchive = tasksToArchive.Where(t => t.ProjectId == projectId.Value).ToList();
384
  }
385
 
 
386
  foreach (var task in tasksToArchive)
387
  {
388
  task.IsArchived = true;
 
399
  return Result<int>.Success(tasksToArchive.Count);
400
  }
401
 
402
+ private async Task<IQueryable<TaskTrackingSystem.Database.AppDbContextModels.Task>> BuildAccessibleTaskQueryAsync(long roleId, long currentUserId, bool includeArchived = false)
403
  {
404
  var query = _db.Tasks.Where(t => t.IsDeleted != true && (includeArchived || !t.IsArchived));
405
 
406
+ var isAdmin = await DataScopeAuthorization.IsAdminScopeAsync(_db, roleId);
407
+ if (isAdmin)
408
  {
409
  return query;
410
  }
411
 
412
+ var isManager = await DataScopeAuthorization.IsManagerScopeAsync(_db, roleId);
413
+ if (isManager)
414
  {
415
  return query.Where(t =>
416
  t.AssignedTo == currentUserId ||
 
423
  t.CreatedBy == currentUserId);
424
  }
425
 
426
+ private async Task<bool> CanAccessProjectAsync(long projectId, long roleId, long currentUserId)
427
  {
428
+ var isAdmin = await DataScopeAuthorization.IsAdminScopeAsync(_db, roleId);
429
+ if (isAdmin)
430
  {
431
  return await _db.Projects.AnyAsync(p => p.Id == projectId && p.IsDeleted != true);
432
  }
 
441
  {
442
  return await _db.ProjectMembers.AnyAsync(pm => pm.ProjectId == projectId && pm.UserId == userId);
443
  }
 
 
 
 
 
 
 
 
 
 
444
  }
445
  }
TaskTrackingSystem.WebApi/Features/User/UserController.cs CHANGED
@@ -43,6 +43,25 @@ namespace TaskTrackingSystem.WebApi.Features.User
43
  return Ok(paged);
44
  }
45
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  [HttpGet("{id}")]
47
  public async Task<ActionResult<UserDto>> GetUser(long id)
48
  {
 
43
  return Ok(paged);
44
  }
45
 
46
+ /// <summary>Returns the user IDs of all colleagues who share any project with the current user.</summary>
47
+ [HttpGet("my-team-user-ids")]
48
+ public async Task<ActionResult<List<long>>> GetMyTeamUserIds()
49
+ {
50
+ var currentUserId = User.GetUserId();
51
+ var myProjectIds = await _userService.GetMyProjectIdsAsync(currentUserId);
52
+ var teamUserIds = await _userService.GetTeamUserIdsAsync(myProjectIds, currentUserId);
53
+ return Ok(teamUserIds);
54
+ }
55
+
56
+ /// <summary>Returns all project IDs the current user is a member of.</summary>
57
+ [HttpGet("my-project-ids")]
58
+ public async Task<ActionResult<List<long>>> GetMyProjectIds()
59
+ {
60
+ var currentUserId = User.GetUserId();
61
+ var projectIds = await _userService.GetMyProjectIdsAsync(currentUserId);
62
+ return Ok(projectIds);
63
+ }
64
+
65
  [HttpGet("{id}")]
66
  public async Task<ActionResult<UserDto>> GetUser(long id)
67
  {
TaskTrackingSystem.WebApi/Features/User/UserService.cs CHANGED
@@ -221,5 +221,26 @@ namespace TaskTrackingSystem.WebApi.Features.User
221
 
222
  return Result.Success(200);
223
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
224
  }
225
  }
 
221
 
222
  return Result.Success(200);
223
  }
224
+
225
+ public async Task<List<long>> GetMyProjectIdsAsync(long currentUserId)
226
+ {
227
+ return await _db.ProjectMembers
228
+ .Where(pm => pm.UserId == currentUserId)
229
+ .Select(pm => pm.ProjectId)
230
+ .Distinct()
231
+ .ToListAsync();
232
+ }
233
+
234
+ public async Task<List<long>> GetTeamUserIdsAsync(List<long> projectIds, long currentUserId)
235
+ {
236
+ if (projectIds == null || projectIds.Count == 0)
237
+ return new List<long>();
238
+
239
+ return await _db.ProjectMembers
240
+ .Where(pm => projectIds.Contains(pm.ProjectId) && pm.UserId != currentUserId)
241
+ .Select(pm => pm.UserId)
242
+ .Distinct()
243
+ .ToListAsync();
244
+ }
245
  }
246
  }
TaskTrackingSystem.WebApi/Features/UserDevice/UserDeviceService.cs CHANGED
@@ -45,22 +45,4 @@ public class UserDeviceService
45
  return Result.Success();
46
  }
47
 
48
- public async Task<Result> RemoveTokenAsync(long userId, string fcmToken)
49
- {
50
- var token = fcmToken.Trim();
51
- if (string.IsNullOrWhiteSpace(token))
52
- {
53
- return Result.Failure("FCM token is required.", 400);
54
- }
55
-
56
- var device = await _db.UserDevices.FirstOrDefaultAsync(x => x.UserId == userId && x.FcmToken == token);
57
- if (device == null)
58
- {
59
- return Result.Success(204);
60
- }
61
-
62
- _db.UserDevices.Remove(device);
63
- await _db.SaveChangesAsync();
64
- return Result.Success(204);
65
- }
66
  }
 
45
  return Result.Success();
46
  }
47
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
  }
TaskTrackingSystem.WebApi/Features/UserDevice/UserDevicesController.cs CHANGED
@@ -31,16 +31,4 @@ public class UserDevicesController : ControllerBase
31
  return StatusCode(result.StatusCode, result);
32
  }
33
 
34
- [HttpPost("unregister")]
35
- public async Task<ActionResult<Result>> Unregister([FromBody] RegisterDeviceTokenDto dto)
36
- {
37
- var userId = User.GetUserId();
38
- if (userId <= 0)
39
- {
40
- return Unauthorized(Result.Failure("User is not authenticated.", 401));
41
- }
42
-
43
- var result = await _userDeviceService.RemoveTokenAsync(userId, dto.FcmToken);
44
- return StatusCode(result.StatusCode, result);
45
- }
46
  }
 
31
  return StatusCode(result.StatusCode, result);
32
  }
33
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  }
TaskTrackingSystem.WebApi/Infrastructure/ClaimsPrincipalExtensions.cs CHANGED
@@ -21,13 +21,6 @@ public static class ClaimsPrincipalExtensions
21
  return user.FindFirstValue(ClaimTypes.Role) ?? string.Empty;
22
  }
23
 
24
- public static bool IsAdmin(this ClaimsPrincipal user)
25
- {
26
- return user.IsInRole("Admin");
27
- }
28
 
29
- public static bool IsManager(this ClaimsPrincipal user)
30
- {
31
- return user.IsInRole("Manager");
32
- }
33
  }
 
21
  return user.FindFirstValue(ClaimTypes.Role) ?? string.Empty;
22
  }
23
 
 
 
 
 
24
 
25
+
 
 
 
26
  }
TaskTrackingSystem.WebApi/Infrastructure/DataScopeAuthorization.cs ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using Microsoft.EntityFrameworkCore;
2
+ using System.Threading.Tasks;
3
+ using TaskTrackingSystem.Database.AppDbContextModels;
4
+
5
+ namespace TaskTrackingSystem.WebApi.Infrastructure
6
+ {
7
+ public static class DataScopeAuthorization
8
+ {
9
+ public static async Task<bool> IsAdminScopeAsync(AppDbContext db, long roleId)
10
+ {
11
+ if (roleId <= 0) return false;
12
+ return await db.RolePermissions.AnyAsync(rp =>
13
+ rp.RoleId == roleId &&
14
+ !rp.IsDeleted &&
15
+ rp.Permission.PermissionCode == "Roles_List" &&
16
+ !rp.Permission.IsDeleted);
17
+ }
18
+
19
+ public static async Task<bool> IsManagerScopeAsync(AppDbContext db, long roleId)
20
+ {
21
+ if (roleId <= 0) return false;
22
+ return await db.RolePermissions.AnyAsync(rp =>
23
+ rp.RoleId == roleId &&
24
+ !rp.IsDeleted &&
25
+ rp.Permission.PermissionCode == "Projects_Create" &&
26
+ !rp.Permission.IsDeleted);
27
+ }
28
+ }
29
+ }
TaskTrackingSystem.WebApi/Infrastructure/PermissionAuthorizationService.cs CHANGED
@@ -20,11 +20,6 @@ public class PermissionAuthorizationService
20
  return false;
21
  }
22
 
23
- if (user.IsInRole("Admin"))
24
- {
25
- return true;
26
- }
27
-
28
  var roleId = await ResolveRoleIdAsync(user);
29
  if (roleId <= 0)
30
  {
 
20
  return false;
21
  }
22
 
 
 
 
 
 
23
  var roleId = await ResolveRoleIdAsync(user);
24
  if (roleId <= 0)
25
  {
TaskTrackingSystem.WebApp/Components/App.razor CHANGED
@@ -5,7 +5,6 @@
5
  <meta charset="utf-8" />
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
  <base href="/" />
8
- <link rel="stylesheet" href="bootstrap/bootstrap.min.css" />
9
  <link rel="stylesheet" href="app.css" />
10
  <link rel="stylesheet" href="TaskTrackingSystem.WebApp.styles.css" />
11
  <link rel="icon" type="image/png" href="favicon.png" />
 
5
  <meta charset="utf-8" />
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
  <base href="/" />
 
8
  <link rel="stylesheet" href="app.css" />
9
  <link rel="stylesheet" href="TaskTrackingSystem.WebApp.styles.css" />
10
  <link rel="icon" type="image/png" href="favicon.png" />
TaskTrackingSystem.WebApp/Components/Layout/Component.razor DELETED
File without changes
TaskTrackingSystem.WebApp/Components/Layout/NavMenu.razor CHANGED
@@ -81,7 +81,8 @@
81
 
82
  if (user.Identity?.IsAuthenticated == true)
83
  {
84
- isAdmin = user.IsInRole("Admin");
 
85
  var rawMenus = await MenuAuthorization.GetUserMenusAsync(user);
86
  menus = ProcessMenus(rawMenus);
87
  BuildUrlHierarchy(menus);
@@ -127,6 +128,8 @@
127
 
128
  if (user.Identity?.IsAuthenticated == true)
129
  {
 
 
130
  var freshMenus = await MenuAuthorization.GetUserMenusAsync(user);
131
  var processedMenus = ProcessMenus(freshMenus);
132
 
 
81
 
82
  if (user.Identity?.IsAuthenticated == true)
83
  {
84
+ var codes = await MenuAuthorization.GetCurrentAccessCodesAsync(user);
85
+ isAdmin = codes.Contains("Roles_List");
86
  var rawMenus = await MenuAuthorization.GetUserMenusAsync(user);
87
  menus = ProcessMenus(rawMenus);
88
  BuildUrlHierarchy(menus);
 
128
 
129
  if (user.Identity?.IsAuthenticated == true)
130
  {
131
+ var codes = await MenuAuthorization.GetCurrentAccessCodesAsync(user);
132
+ isAdmin = codes.Contains("Roles_List");
133
  var freshMenus = await MenuAuthorization.GetUserMenusAsync(user);
134
  var processedMenus = ProcessMenus(freshMenus);
135
 
TaskTrackingSystem.WebApp/Components/Pages/Component.razor DELETED
File without changes
TaskTrackingSystem.WebApp/Components/Pages/{AuditLogs.razor → Features/Audit/AuditLogs.razor} RENAMED
@@ -4,7 +4,7 @@
4
  @using TaskTrackingSystem.Shared.Models.AuditLog
5
  @using TaskTrackingSystem.WebApp.Components.Partial
6
  @rendermode @(new InteractiveServerRenderMode(prerender: false))
7
- @attribute [Authorize(Roles = "Admin")]
8
  @inject ApiClientService ApiClient
9
  @inject IJSRuntime JS
10
  @inject AuthenticationStateProvider AuthStateProvider
 
4
  @using TaskTrackingSystem.Shared.Models.AuditLog
5
  @using TaskTrackingSystem.WebApp.Components.Partial
6
  @rendermode @(new InteractiveServerRenderMode(prerender: false))
7
+ @attribute [Authorize]
8
  @inject ApiClientService ApiClient
9
  @inject IJSRuntime JS
10
  @inject AuthenticationStateProvider AuthStateProvider
TaskTrackingSystem.WebApp/Components/Pages/{Login.razor → Features/Auth/Login.razor} RENAMED
File without changes
TaskTrackingSystem.WebApp/Components/Pages/{Register.razor → Features/Auth/Register.razor} RENAMED
File without changes
TaskTrackingSystem.WebApp/Components/Pages/{ResetPassword.razor → Features/Auth/ResetPassword.razor} RENAMED
File without changes
TaskTrackingSystem.WebApp/Components/Pages/{Error.razor → Features/Common/Error.razor} RENAMED
File without changes
TaskTrackingSystem.WebApp/Components/Pages/{EmployeeDashboard.razor → Features/Dashboard/EmployeeDashboard.razor} RENAMED
@@ -2,7 +2,7 @@
2
  @using Microsoft.AspNetCore.Authorization
3
  @using Microsoft.AspNetCore.Components.Authorization
4
  @rendermode @(new InteractiveServerRenderMode(prerender: false))
5
- @attribute [Authorize(Roles = "Employee")]
6
  @inject ApiClientService ApiClient
7
  @inject NavigationManager Navigation
8
  @inject IJSRuntime JS
 
2
  @using Microsoft.AspNetCore.Authorization
3
  @using Microsoft.AspNetCore.Components.Authorization
4
  @rendermode @(new InteractiveServerRenderMode(prerender: false))
5
+ @attribute [Authorize]
6
  @inject ApiClientService ApiClient
7
  @inject NavigationManager Navigation
8
  @inject IJSRuntime JS
TaskTrackingSystem.WebApp/Components/Pages/{Home.razor → Features/Dashboard/Home.razor} RENAMED
@@ -9,11 +9,12 @@
9
  @using TaskTrackingSystem.Shared.Models.User
10
  @using TaskTrackingSystem.Shared.Models.Dashboard
11
  @rendermode @(new InteractiveServerRenderMode(prerender: false))
12
- @attribute [Microsoft.AspNetCore.Authorization.Authorize(Roles = "Admin")]
13
  @inject ApiClientService ApiClient
14
  @inject IJSRuntime JS
15
  @inject AuthenticationStateProvider AuthStateProvider
16
  @inject MenuAuthorizationService MenuAuthorization
 
17
 
18
  <PageTitle>Dashboard</PageTitle>
19
 
@@ -364,7 +365,22 @@
364
  protected override async Task OnInitializedAsync()
365
  {
366
  var authState = await AuthStateProvider.GetAuthenticationStateAsync();
367
- canCreateTask = authState.User.IsInRole("Admin") || await MenuAuthorization.HasAccessCodeAsync(authState.User, "Tasks_Create");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
368
  await LoadDashboardData();
369
  }
370
 
@@ -612,7 +628,7 @@
612
 
613
  // 2. Adjust total so it maps 100% cleanly to the math displayed
614
  var total = Math.Max(TotalTasksCount, 1);
615
- var circumference = 2 * Math.PI * 74; // 2πr for r=74
616
  var slices = new List<DistributionSlice>();
617
  var cumulativeArc = 0.0;
618
 
@@ -620,7 +636,7 @@
620
  {
621
  if (rawCounts[i] <= 0)
622
  {
623
- // Arc length 0 with a full-circumference gap invisible.
624
  // Avoids stroke-linecap="round" rendering a phantom dot.
625
  slices.Add(new DistributionSlice(GetDistributionLabel(i), 0, colors[i], $"0 {circumference:0.##}", 0));
626
  continue;
@@ -725,7 +741,7 @@
725
  }
726
 
727
  var first = months.First();
728
- var n = months.Count; // total columns each flex-1 month slot = 1/n of bar width
729
 
730
  return FeaturedProjects.Select(project =>
731
  {
@@ -743,7 +759,7 @@
743
 
744
  return new TimelineRow(
745
  project.Name,
746
- $"{DisplayFormats.Date(project.StartDate)} {DisplayFormats.Date(project.EndDate)}",
747
  GetProjectColor(project.Id),
748
  left,
749
  width,
 
9
  @using TaskTrackingSystem.Shared.Models.User
10
  @using TaskTrackingSystem.Shared.Models.Dashboard
11
  @rendermode @(new InteractiveServerRenderMode(prerender: false))
12
+ @attribute [Microsoft.AspNetCore.Authorization.Authorize]
13
  @inject ApiClientService ApiClient
14
  @inject IJSRuntime JS
15
  @inject AuthenticationStateProvider AuthStateProvider
16
  @inject MenuAuthorizationService MenuAuthorization
17
+ @inject NavigationManager NavigationManager
18
 
19
  <PageTitle>Dashboard</PageTitle>
20
 
 
365
  protected override async Task OnInitializedAsync()
366
  {
367
  var authState = await AuthStateProvider.GetAuthenticationStateAsync();
368
+ var user = authState.User;
369
+
370
+ var codes = await MenuAuthorization.GetCurrentAccessCodesAsync(user);
371
+
372
+ if (codes.Contains("Projects_Create") && !codes.Contains("Roles_List"))
373
+ {
374
+ NavigationManager.NavigateTo("/dashboard/manager");
375
+ return;
376
+ }
377
+ else if (!codes.Contains("Projects_Create") && !codes.Contains("Roles_List"))
378
+ {
379
+ NavigationManager.NavigateTo("/dashboard/employee");
380
+ return;
381
+ }
382
+
383
+ canCreateTask = codes.Contains("Tasks_Create");
384
  await LoadDashboardData();
385
  }
386
 
 
628
 
629
  // 2. Adjust total so it maps 100% cleanly to the math displayed
630
  var total = Math.Max(TotalTasksCount, 1);
631
+ var circumference = 2 * Math.PI * 74; // 2πr for r=74
632
  var slices = new List<DistributionSlice>();
633
  var cumulativeArc = 0.0;
634
 
 
636
  {
637
  if (rawCounts[i] <= 0)
638
  {
639
+ // Arc length 0 with a full-circumference gap → invisible.
640
  // Avoids stroke-linecap="round" rendering a phantom dot.
641
  slices.Add(new DistributionSlice(GetDistributionLabel(i), 0, colors[i], $"0 {circumference:0.##}", 0));
642
  continue;
 
741
  }
742
 
743
  var first = months.First();
744
+ var n = months.Count; // total columns ΓÇö each flex-1 month slot = 1/n of bar width
745
 
746
  return FeaturedProjects.Select(project =>
747
  {
 
759
 
760
  return new TimelineRow(
761
  project.Name,
762
+ $"{DisplayFormats.Date(project.StartDate)} ΓÇô {DisplayFormats.Date(project.EndDate)}",
763
  GetProjectColor(project.Id),
764
  left,
765
  width,
TaskTrackingSystem.WebApp/Components/Pages/{ManagerDashboard.razor → Features/Dashboard/ManagerDashboard.razor} RENAMED
@@ -1,7 +1,7 @@
1
  @page "/dashboard/manager"
2
  @using Microsoft.AspNetCore.Authorization
3
  @rendermode @(new InteractiveServerRenderMode(prerender: false))
4
- @attribute [Authorize(Roles = "Manager")]
5
  @inject ApiClientService ApiClient
6
  @inject NavigationManager Navigation
7
  @inject IJSRuntime JS
 
1
  @page "/dashboard/manager"
2
  @using Microsoft.AspNetCore.Authorization
3
  @rendermode @(new InteractiveServerRenderMode(prerender: false))
4
+ @attribute [Authorize]
5
  @inject ApiClientService ApiClient
6
  @inject NavigationManager Navigation
7
  @inject IJSRuntime JS
TaskTrackingSystem.WebApp/Components/Pages/{AddIssue.razor → Features/Issues/AddIssue.razor} RENAMED
File without changes
TaskTrackingSystem.WebApp/Components/Pages/{Issues.razor → Features/Issues/Issues.razor} RENAMED
@@ -100,72 +100,68 @@
100
  </div>
101
 
102
  <div class="rounded-xl border border-slate-200 bg-white shadow-sm overflow-hidden">
103
- <div class="overflow-x-auto">
104
- <table class="w-full min-w-[1200px]">
 
 
 
 
 
 
 
 
 
 
105
  <thead>
106
  <tr class="border-b border-slate-100 bg-slate-50/60">
107
- <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">No.</th>
108
- <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Issue</th>
109
- <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Task</th>
110
- <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Project</th>
111
- <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Hours</th>
112
- <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Status</th>
113
- <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Priority</th>
114
- <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Dates</th>
115
- <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Assignee</th>
116
- <th class="px-6 py-3 text-right text-xs font-semibold text-slate-500 uppercase tracking-wider">Actions</th>
117
  </tr>
118
  </thead>
119
- <tbody class="divide-y divide-slate-100">
120
  @if (issues.Any())
121
  {
122
  @foreach (var entry in issues.Select((issue, index) => (issue, index)))
123
  {
124
  var issue = entry.issue;
125
- <tr class="hover:bg-slate-50/50 transition-colors">
126
- <td class="px-6 py-4 text-sm font-semibold text-slate-500">@((currentPage - 1) * pageSize + entry.index + 1)</td>
127
- <td class="px-6 py-4">
128
- <button @onclick="() => OpenEditModal(issue)" class="text-sm font-semibold text-slate-900 hover:text-violet-600 transition-colors">
 
 
 
 
129
  @issue.Title
130
  </button>
131
- @if (!string.IsNullOrWhiteSpace(issue.Description))
132
- {
133
- <p class="mt-0.5 max-w-xs truncate text-xs text-slate-400">@issue.Description</p>
134
- }
135
  </td>
136
- <td class="px-6 py-4 text-sm text-slate-700">
137
- <a href="@($"/tasks/{issue.TaskId}/details")" class="font-medium text-violet-700 hover:text-violet-800 transition-colors">
138
- @issue.TaskTitle
139
- </a>
140
- </td>
141
- <td class="px-6 py-4 text-sm text-slate-600">@issue.ProjectName</td>
142
- <td class="px-6 py-4 text-sm text-slate-600">
143
- <div class="flex flex-col">
144
- <span>@(issue.ActualHours?.ToString("0.##") ?? "-") hr</span>
145
- <span class="text-xs text-slate-400">@issue.StartDate.ToString("dd MMM yyyy")</span>
146
- </div>
147
  </td>
148
- <td class="px-6 py-4">
149
  <span class="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium @GetStatusBadge(issue.StatusId)">
150
  @GetStatusLabel(issue.StatusId)
151
  </span>
152
  </td>
153
- <td class="px-6 py-4">
154
  <span class="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium @GetPriorityBadge(issue.PriorityId)">
155
  @GetPriorityLabel(issue.PriorityId)
156
  </span>
157
  </td>
158
- <td class="px-6 py-4 text-sm text-slate-600">
159
- <div class="flex flex-col">
160
- <span>@issue.StartDate.ToString("dd MMM yyyy")</span>
161
- <span class="text-xs text-slate-400">Due @issue.DueDate.ToString("dd MMM yyyy")</span>
162
- </div>
163
  </td>
164
- <td class="px-6 py-4 text-sm text-slate-600">
165
  @(!string.IsNullOrWhiteSpace(issue.AssignedToName) ? issue.AssignedToName : "Unassigned")
166
  </td>
167
- <td class="px-6 py-4 text-right">
168
- <div class="flex items-center justify-end space-x-2">
169
  @if (canEditIssue)
170
  {
171
  <button @onclick="() => OpenEditModal(issue)" class="rounded-lg p-1.5 text-slate-400 hover:bg-amber-50 hover:text-amber-600 transition-all" title="Edit">
@@ -182,11 +178,11 @@
182
  </td>
183
  </tr>
184
  }
185
- }
186
- else
187
- {
188
- <EmptyState ColSpan="10" Icon="file-pen-line" Title="No issues yet" Subtitle="Create your first issue to start tracking daily work." />
189
- }
190
  </tbody>
191
  </table>
192
  </div>
@@ -380,6 +376,7 @@
380
  : "Unknown project";
381
 
382
  private IEnumerable<UserDto> filteredProjectMembers => projectMembers.Where(u => u.IsActive);
 
383
 
384
  protected override async Task OnInitializedAsync()
385
  {
 
100
  </div>
101
 
102
  <div class="rounded-xl border border-slate-200 bg-white shadow-sm overflow-hidden">
103
+ <div class="overflow-hidden">
104
+ <table class="w-full table-fixed">
105
+ <colgroup>
106
+ <col class="w-[12%]" />
107
+ <col class="w-[24%]" />
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>
116
  <tr class="border-b border-slate-100 bg-slate-50/60">
117
+ <th class="px-5 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Task Code</th>
118
+ <th class="px-5 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Issue</th>
119
+ <th class="px-5 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Hours</th>
120
+ <th class="px-5 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Status</th>
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>
127
+ <tbody class="divide-y divide-slate-100 text-[13px]">
128
  @if (issues.Any())
129
  {
130
  @foreach (var entry in issues.Select((issue, index) => (issue, index)))
131
  {
132
  var issue = entry.issue;
133
+ <tr class="hover:bg-slate-50/50 transition-colors align-top">
134
+ <td class="px-5 py-4 text-left whitespace-nowrap">
135
+ <span class="font-mono text-[11px] font-semibold tracking-wide text-indigo-700">
136
+ @GetTaskCode(issue.TaskId)
137
+ </span>
138
+ </td>
139
+ <td class="px-5 py-4 text-left">
140
+ <button @onclick="() => OpenEditModal(issue)" class="min-w-0 text-left text-sm font-semibold leading-5 text-slate-900 hover:text-violet-600 transition-colors break-words">
141
  @issue.Title
142
  </button>
 
 
 
 
143
  </td>
144
+ <td class="px-5 py-4 text-slate-600 whitespace-nowrap">
145
+ <span class="font-medium text-slate-700">@(issue.ActualHours?.ToString("0.##") ?? "-") hr</span>
 
 
 
 
 
 
 
 
 
146
  </td>
147
+ <td class="px-5 py-4">
148
  <span class="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium @GetStatusBadge(issue.StatusId)">
149
  @GetStatusLabel(issue.StatusId)
150
  </span>
151
  </td>
152
+ <td class="px-5 py-4">
153
  <span class="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium @GetPriorityBadge(issue.PriorityId)">
154
  @GetPriorityLabel(issue.PriorityId)
155
  </span>
156
  </td>
157
+ <td class="px-5 py-4 text-sm text-slate-600 whitespace-nowrap">
158
+ <span class="font-medium text-slate-700">Due: @issue.DueDate.ToString("MMM d, yyyy")</span>
 
 
 
159
  </td>
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)
166
  {
167
  <button @onclick="() => OpenEditModal(issue)" class="rounded-lg p-1.5 text-slate-400 hover:bg-amber-50 hover:text-amber-600 transition-all" title="Edit">
 
178
  </td>
179
  </tr>
180
  }
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>
188
  </div>
 
376
  : "Unknown project";
377
 
378
  private IEnumerable<UserDto> filteredProjectMembers => projectMembers.Where(u => u.IsActive);
379
+ private static string GetTaskCode(long taskId) => $"TSK-{taskId:D4}";
380
 
381
  protected override async Task OnInitializedAsync()
382
  {
TaskTrackingSystem.WebApp/Components/Pages/{ProjectAssign.razor → Features/Projects/ProjectAssign.razor} RENAMED
File without changes
TaskTrackingSystem.WebApp/Components/Pages/{Projects.razor → Features/Projects/Projects.razor} RENAMED
File without changes
TaskTrackingSystem.WebApp/Components/Pages/{EmployeeReport.razor → Features/Reports/EmployeeReport.razor} RENAMED
@@ -83,6 +83,17 @@
83
  <button @onclick="ResetAllFilters" 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">
84
  <i data-lucide="rotate-ccw" class="h-4 w-4 mr-2 text-slate-500"></i> Reset
85
  </button>
 
 
 
 
 
 
 
 
 
 
 
86
  </div>
87
  </div>
88
 
@@ -103,14 +114,14 @@
103
  <!-- Chart View: SVG Horizontal Bar Chart showing Efficiency -->
104
  <div class="rounded-xl border border-slate-200 bg-white p-6 shadow-sm">
105
  <h3 class="font-semibold text-slate-900 mb-4 font-sans text-md">Employee Productivity Chart</h3>
106
- @if (!FilteredList.Any())
107
  {
108
  <p class="text-slate-400 text-center py-8">No data available.</p>
109
  }
110
  else
111
  {
112
  <div class="space-y-4 max-w-2xl">
113
- @foreach (var emp in FilteredList)
114
  {
115
  <div>
116
  <div class="flex justify-between text-xs font-semibold text-slate-700 mb-1.5">
@@ -142,9 +153,9 @@
142
  </tr>
143
  </thead>
144
  <tbody class="divide-y divide-slate-100">
145
- @if (PagedList.Any())
146
  {
147
- @foreach (var entry in PagedList.Select((emp, index) => (emp, index)))
148
  {
149
  var emp = entry.emp;
150
  <tr class="hover:bg-slate-50/50 transition-colors">
@@ -196,12 +207,12 @@
196
  </table>
197
 
198
  <!-- Pagination -->
199
- <Pagination CurrentPage="currentPage"
200
- TotalPages="TotalPages"
201
- PageSize="pageSize"
202
- TotalCount="TotalCount"
203
- OnPageChanged="HandlePageChanged"
204
- OnPageSizeChanged="HandlePageSizeChanged" />
205
  </div>
206
  }
207
  }
@@ -210,9 +221,8 @@
210
  @code {
211
  private bool isLoading = true;
212
  private bool isChartView = false;
213
- private List<UserDto> users = new();
214
- private List<TaskDto> tasks = new();
215
- private List<EmployeeMetric> displayList = new();
216
  private PagedResult<EmployeeProductivityReportDto>? reportPage;
217
 
218
  // Filters
@@ -220,6 +230,8 @@
220
  private DateTime? filterStartDate;
221
  private DateTime? filterEndDate;
222
  private string filterStatus = "0";
 
 
223
 
224
  // Search and Pagination
225
  private string searchInput = "";
@@ -227,13 +239,31 @@
227
  private int currentPage = 1;
228
  private int pageSize = 10;
229
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
230
  private async Task ApplyAllFilters()
231
  {
232
  ApplyPeriodPreset();
233
  searchQuery = searchInput;
234
  currentPage = 1;
235
- CalculateMetrics();
236
- await LoadEmployeeReportPage();
237
  }
238
 
239
  private async Task ResetAllFilters()
@@ -244,77 +274,23 @@
244
  filterStartDate = null;
245
  filterEndDate = null;
246
  filterStatus = "0";
 
 
247
  currentPage = 1;
248
- CalculateMetrics();
249
- await LoadEmployeeReportPage();
250
  }
251
 
252
  private async Task HandlePageChanged(int page)
253
  {
254
  currentPage = page;
255
- await LoadEmployeeReportPage();
256
  }
257
 
258
  private async Task HandlePageSizeChanged(int size)
259
  {
260
  pageSize = size;
261
  currentPage = 1;
262
- await LoadEmployeeReportPage();
263
- }
264
-
265
- private IEnumerable<EmployeeMetric> FilteredList =>
266
- string.IsNullOrWhiteSpace(searchQuery)
267
- ? displayList
268
- : displayList.Where(e => e.FullName.Contains(searchQuery, StringComparison.OrdinalIgnoreCase)
269
- || e.Username.Contains(searchQuery, StringComparison.OrdinalIgnoreCase));
270
-
271
- private int TotalPages => reportPage?.TotalPages ?? 0;
272
-
273
- private int TotalCount => reportPage?.TotalCount ?? 0;
274
-
275
- private IEnumerable<EmployeeMetric> PagedList =>
276
- reportPage?.Items.Select(x => new EmployeeMetric
277
- {
278
- FullName = x.FullName,
279
- Username = x.Username,
280
- AssignedCount = x.AssignedCount,
281
- CompletedCount = x.CompletedCount,
282
- Efficiency = x.Efficiency,
283
- OnTimeDeliveryRate = x.OnTimeDeliveryRate
284
- }) ?? Enumerable.Empty<EmployeeMetric>();
285
-
286
- protected override async Task OnInitializedAsync()
287
- {
288
- var client = ApiClient.CreateClient();
289
- try
290
- {
291
- var usersTask = client.GetFromJsonAsync<List<UserDto>>("User");
292
- var tasksTask = client.GetFromJsonAsync<List<TaskDto>>("Task");
293
- await Task.WhenAll(usersTask, tasksTask);
294
-
295
- users = usersTask.Result ?? new();
296
- tasks = tasksTask.Result ?? new();
297
- CalculateMetrics();
298
- await LoadEmployeeReportPage();
299
- }
300
- catch (Exception ex)
301
- {
302
- Console.WriteLine($"Error initializing: {ex.Message}");
303
- }
304
- finally
305
- {
306
- isLoading = false;
307
- }
308
- }
309
-
310
- protected override async Task OnAfterRenderAsync(bool firstRender)
311
- {
312
- await JS.InvokeVoidAsync("initIcons");
313
- }
314
-
315
- private void SetView(bool chartView)
316
- {
317
- isChartView = chartView;
318
  }
319
 
320
  private void ApplyPeriodPreset()
@@ -342,58 +318,7 @@
342
  }
343
  }
344
 
345
- private void CalculateMetrics()
346
- {
347
- var metrics = new List<EmployeeMetric>();
348
-
349
- foreach (var u in users)
350
- {
351
- // Filter user's tasks
352
- var userTasks = tasks.Where(t => t.AssignedTo == u.Id).ToList();
353
-
354
- // Apply Period Filter
355
- if (filterStartDate.HasValue)
356
- {
357
- userTasks = userTasks.Where(t => t.CreatedAt >= filterStartDate.Value).ToList();
358
- }
359
- if (filterEndDate.HasValue)
360
- {
361
- userTasks = userTasks.Where(t => t.CreatedAt <= filterEndDate.Value.AddDays(1)).ToList();
362
- }
363
-
364
- // Apply Status Filter if matching
365
- if (filterStatus == "overdue")
366
- {
367
- userTasks = userTasks.Where(t => t.DueDate.Date < DateTime.Today && t.StatusId != AppTaskStatus.Done).ToList();
368
- }
369
- else if (filterStatus != "0" && Enum.TryParse<AppTaskStatus>(filterStatus, out var parsedStatus))
370
- {
371
- userTasks = userTasks.Where(t => t.StatusId == parsedStatus).ToList();
372
- }
373
-
374
- int assignedCount = userTasks.Count;
375
- int completedCount = userTasks.Count(t => t.StatusId == AppTaskStatus.Done); // Done
376
- double efficiency = assignedCount > 0 ? ((double)completedCount / assignedCount) * 100 : 0;
377
-
378
- var completedTasksList = userTasks.Where(t => t.StatusId == AppTaskStatus.Done).ToList();
379
- int onTimeCompletedCount = completedTasksList.Count(t => t.CompletedAt.HasValue && t.CompletedAt.Value.Date <= t.DueDate.Date);
380
- double onTimeDeliveryRate = completedCount > 0 ? ((double)onTimeCompletedCount / completedCount) * 100 : 0;
381
-
382
- metrics.Add(new EmployeeMetric
383
- {
384
- FullName = $"{u.FirstName} {u.LastName}".Trim(),
385
- Username = u.Username,
386
- AssignedCount = assignedCount,
387
- CompletedCount = completedCount,
388
- Efficiency = efficiency,
389
- OnTimeDeliveryRate = onTimeDeliveryRate
390
- });
391
- }
392
-
393
- displayList = metrics.OrderBy(m => m.FullName).ToList();
394
- }
395
-
396
- private async Task LoadEmployeeReportPage()
397
  {
398
  var client = ApiClient.CreateClient();
399
  try
@@ -402,28 +327,41 @@
402
  if (!string.IsNullOrWhiteSpace(searchQuery)) parts.Add($"search={Uri.EscapeDataString(searchQuery)}");
403
  if (filterStartDate.HasValue) parts.Add($"startDate={filterStartDate:yyyy-MM-dd}");
404
  if (filterEndDate.HasValue) parts.Add($"endDate={filterEndDate:yyyy-MM-dd}");
405
- if (!string.IsNullOrWhiteSpace(filterStatus)) parts.Add($"status={Uri.EscapeDataString(filterStatus)}");
406
- parts.Add($"page={currentPage}");
407
- parts.Add($"limit={pageSize}");
 
 
 
 
 
 
 
 
408
 
409
- var url = "Report/employee-productivity" + (parts.Count > 0 ? "?" + string.Join("&", parts) : "");
410
- var result = await client.GetFromJsonAsync<PagedResult<EmployeeProductivityReportDto>>(url, Serialization.CaseInsensitive);
411
- if (result != null)
 
 
 
 
412
  {
413
- reportPage = result;
414
- currentPage = result.Page;
415
- pageSize = result.PageSize;
 
416
  }
417
  }
418
  catch (Exception ex)
419
  {
420
- Console.WriteLine($"Employee report paging error: {ex.Message}");
421
  }
422
  }
423
 
424
  private async Task DownloadExcel()
425
  {
426
- var rows = FilteredList.Select(emp => new object?[]
427
  {
428
  emp.FullName,
429
  emp.Username,
@@ -463,7 +401,7 @@
463
  $"Generated: {DisplayFormats.Date(DateTime.Today)}"
464
  };
465
 
466
- var rows = FilteredList.Select(emp => new[]
467
  {
468
  emp.FullName,
469
  emp.Username,
@@ -485,14 +423,4 @@
485
  "application/pdf",
486
  Convert.ToBase64String(pdfBytes));
487
  }
488
-
489
- private class EmployeeMetric
490
- {
491
- public string FullName { get; set; } = string.Empty;
492
- public string Username { get; set; } = string.Empty;
493
- public int AssignedCount { get; set; }
494
- public int CompletedCount { get; set; }
495
- public double Efficiency { get; set; }
496
- public double OnTimeDeliveryRate { get; set; }
497
- }
498
  }
 
83
  <button @onclick="ResetAllFilters" 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">
84
  <i data-lucide="rotate-ccw" class="h-4 w-4 mr-2 text-slate-500"></i> Reset
85
  </button>
86
+
87
+ <div class="flex flex-wrap items-center gap-6 w-full border-t border-slate-100 pt-3 mt-1">
88
+ <label class="flex items-center space-x-2 text-sm font-medium text-slate-700 cursor-pointer select-none">
89
+ <input type="checkbox" @bind="filterAssignToMe" class="h-4 w-4 rounded border-slate-300 text-violet-600 focus:ring-violet-500" />
90
+ <span>Assigned to me</span>
91
+ </label>
92
+ <label class="flex items-center space-x-2 text-sm font-medium text-slate-700 cursor-pointer select-none">
93
+ <input type="checkbox" @bind="filterAssignToMyTeam" class="h-4 w-4 rounded border-slate-300 text-violet-600 focus:ring-violet-500" />
94
+ <span>Assigned to my team</span>
95
+ </label>
96
+ </div>
97
  </div>
98
  </div>
99
 
 
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>
120
  }
121
  else
122
  {
123
  <div class="space-y-4 max-w-2xl">
124
+ @foreach (var emp in allEmployees)
125
  {
126
  <div>
127
  <div class="flex justify-between text-xs font-semibold text-slate-700 mb-1.5">
 
153
  </tr>
154
  </thead>
155
  <tbody class="divide-y divide-slate-100">
156
+ @if (pagedEmployees.Any())
157
  {
158
+ @foreach (var entry in pagedEmployees.Select((emp, index) => (emp, index)))
159
  {
160
  var emp = entry.emp;
161
  <tr class="hover:bg-slate-50/50 transition-colors">
 
207
  </table>
208
 
209
  <!-- Pagination -->
210
+ <Pagination CurrentPage="currentPage"
211
+ TotalPages="TotalPages"
212
+ PageSize="pageSize"
213
+ TotalCount="TotalCount"
214
+ OnPageChanged="HandlePageChanged"
215
+ OnPageSizeChanged="HandlePageSizeChanged" />
216
  </div>
217
  }
218
  }
 
221
  @code {
222
  private bool isLoading = true;
223
  private bool isChartView = false;
224
+ private List<EmployeeProductivityReportDto> allEmployees = new();
225
+ private List<EmployeeProductivityReportDto> pagedEmployees = new();
 
226
  private PagedResult<EmployeeProductivityReportDto>? reportPage;
227
 
228
  // Filters
 
230
  private DateTime? filterStartDate;
231
  private DateTime? filterEndDate;
232
  private string filterStatus = "0";
233
+ private bool filterAssignToMe = false;
234
+ private bool filterAssignToMyTeam = false;
235
 
236
  // Search and Pagination
237
  private string searchInput = "";
 
239
  private int currentPage = 1;
240
  private int pageSize = 10;
241
 
242
+ private int TotalPages => reportPage?.TotalPages ?? 0;
243
+ private int TotalCount => reportPage?.TotalCount ?? 0;
244
+
245
+ protected override async Task OnInitializedAsync()
246
+ {
247
+ await LoadEmployeeReportData();
248
+ isLoading = false;
249
+ }
250
+
251
+ protected override async Task OnAfterRenderAsync(bool firstRender)
252
+ {
253
+ await JS.InvokeVoidAsync("initIcons");
254
+ }
255
+
256
+ private void SetView(bool chartView)
257
+ {
258
+ isChartView = chartView;
259
+ }
260
+
261
  private async Task ApplyAllFilters()
262
  {
263
  ApplyPeriodPreset();
264
  searchQuery = searchInput;
265
  currentPage = 1;
266
+ await LoadEmployeeReportData();
 
267
  }
268
 
269
  private async Task ResetAllFilters()
 
274
  filterStartDate = null;
275
  filterEndDate = null;
276
  filterStatus = "0";
277
+ filterAssignToMe = false;
278
+ filterAssignToMyTeam = false;
279
  currentPage = 1;
280
+ await LoadEmployeeReportData();
 
281
  }
282
 
283
  private async Task HandlePageChanged(int page)
284
  {
285
  currentPage = page;
286
+ await LoadEmployeeReportData();
287
  }
288
 
289
  private async Task HandlePageSizeChanged(int size)
290
  {
291
  pageSize = size;
292
  currentPage = 1;
293
+ await LoadEmployeeReportData();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
294
  }
295
 
296
  private void ApplyPeriodPreset()
 
318
  }
319
  }
320
 
321
+ private async Task LoadEmployeeReportData()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
322
  {
323
  var client = ApiClient.CreateClient();
324
  try
 
327
  if (!string.IsNullOrWhiteSpace(searchQuery)) parts.Add($"search={Uri.EscapeDataString(searchQuery)}");
328
  if (filterStartDate.HasValue) parts.Add($"startDate={filterStartDate:yyyy-MM-dd}");
329
  if (filterEndDate.HasValue) parts.Add($"endDate={filterEndDate:yyyy-MM-dd}");
330
+ if (!string.IsNullOrWhiteSpace(filterStatus) && filterStatus != "0") parts.Add($"status={Uri.EscapeDataString(filterStatus)}");
331
+ if (filterAssignToMe) parts.Add("assignedToMe=true");
332
+ if (filterAssignToMyTeam) parts.Add("assignedToMyTeam=true");
333
+
334
+ // 1. Fetch full matching list for chart view and exports
335
+ var fullUrl = "Report/employee-productivity" + (parts.Count > 0 ? "?" + string.Join("&", parts) : "");
336
+ var fullResult = await client.GetFromJsonAsync<Result<List<EmployeeProductivityReportDto>>>(fullUrl, Serialization.CaseInsensitive);
337
+ if (fullResult != null && fullResult.IsSuccess && fullResult.Value != null)
338
+ {
339
+ allEmployees = fullResult.Value;
340
+ }
341
 
342
+ // 2. Fetch paged list for table pagination
343
+ var pagedParts = new List<string>(parts);
344
+ pagedParts.Add($"page={currentPage}");
345
+ pagedParts.Add($"limit={pageSize}");
346
+ var pagedUrl = "Report/employee-productivity" + (pagedParts.Count > 0 ? "?" + string.Join("&", pagedParts) : "");
347
+ var pagedResult = await client.GetFromJsonAsync<PagedResult<EmployeeProductivityReportDto>>(pagedUrl, Serialization.CaseInsensitive);
348
+ if (pagedResult != null)
349
  {
350
+ reportPage = pagedResult;
351
+ pagedEmployees = pagedResult.Items.ToList();
352
+ currentPage = pagedResult.Page;
353
+ pageSize = pagedResult.PageSize;
354
  }
355
  }
356
  catch (Exception ex)
357
  {
358
+ Console.WriteLine($"Error loading employee report: {ex.Message}");
359
  }
360
  }
361
 
362
  private async Task DownloadExcel()
363
  {
364
+ var rows = allEmployees.Select(emp => new object?[]
365
  {
366
  emp.FullName,
367
  emp.Username,
 
401
  $"Generated: {DisplayFormats.Date(DateTime.Today)}"
402
  };
403
 
404
+ var rows = allEmployees.Select(emp => new[]
405
  {
406
  emp.FullName,
407
  emp.Username,
 
423
  "application/pdf",
424
  Convert.ToBase64String(pdfBytes));
425
  }
 
 
 
 
 
 
 
 
 
 
426
  }
TaskTrackingSystem.WebApp/Components/Pages/{OverdueTasksReport.razor → Features/Reports/OverdueTasksReport.razor} RENAMED
@@ -115,6 +115,17 @@
115
  <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-600 hover:bg-slate-50 transition-colors shadow-sm">
116
  <i data-lucide="rotate-ccw" class="h-4 w-4 mr-2"></i> Reset
117
  </button>
 
 
 
 
 
 
 
 
 
 
 
118
  </div>
119
  </div>
120
 
@@ -128,7 +139,7 @@
128
  <div class="rounded-xl border border-slate-200 bg-white shadow-sm overflow-hidden">
129
  <div class="px-6 py-4 border-b border-slate-100 flex items-center justify-between">
130
  <h3 class="text-sm font-semibold text-slate-700">
131
- Showing <span class="text-rose-600 font-bold">@FilteredTasks.Count()</span> delayed / overdue task(s)
132
  </h3>
133
  </div>
134
  <table class="w-full">
@@ -145,14 +156,11 @@
145
  </tr>
146
  </thead>
147
  <tbody class="divide-y divide-slate-100">
148
- @if (PagedTasks.Any())
149
  {
150
- @foreach (var entry in PagedTasks.Select((task, index) => (task, index)))
151
  {
152
  var task = entry.task;
153
- var daysOverdue = (DateTime.Today - task.DueDate).Days;
154
- var assignee = allUsers.FirstOrDefault(u => u.Id == task.AssignedTo);
155
- var project = allProjects.FirstOrDefault(p => p.Id == task.ProjectId);
156
  <tr class="hover:bg-slate-50/50 transition-colors">
157
  <td class="px-6 py-4 text-sm font-semibold text-slate-500 align-top">@((currentPage - 1) * pageSize + entry.index + 1)</td>
158
  <td class="px-6 py-4">
@@ -167,17 +175,17 @@
167
  <td class="px-6 py-4">
168
  <span class="inline-flex items-center gap-1 text-sm text-slate-600">
169
  <i data-lucide="folder" class="h-3.5 w-3.5 text-violet-500"></i>
170
- @(project?.Name ?? "—")
171
  </span>
172
  </td>
173
  <td class="px-6 py-4">
174
- @if (assignee != null)
175
  {
176
  <div class="flex items-center gap-2">
177
  <span class="flex h-7 w-7 items-center justify-center rounded-full bg-violet-100 text-violet-700 text-xs font-bold">
178
- @(assignee.FirstName.Length > 0 ? assignee.FirstName[0].ToString().ToUpper() : "")@(assignee.LastName.Length > 0 ? assignee.LastName[0].ToString().ToUpper() : "")
179
  </span>
180
- <span class="text-sm text-slate-700">@assignee.FirstName @assignee.LastName</span>
181
  </div>
182
  }
183
  else
@@ -186,21 +194,21 @@
186
  }
187
  </td>
188
  <td class="px-6 py-4">
189
- <span class="text-sm font-medium @(daysOverdue > 0 ? "text-rose-600" : "text-amber-600")">
190
  @DisplayFormats.Date(task.DueDate)
191
  </span>
192
  </td>
193
  <td class="px-6 py-4">
194
- @if (daysOverdue > 0)
195
  {
196
  <span class="inline-flex items-center gap-1 rounded-full bg-rose-50 px-2.5 py-1 text-xs font-semibold text-rose-700">
197
  <i data-lucide="alert-triangle" class="h-3 w-3"></i>
198
- @daysOverdue day@(daysOverdue != 1 ? "s" : "")
199
  </span>
200
  }
201
  else
202
  {
203
- var daysDue = Math.Abs(daysOverdue);
204
  <span class="inline-flex items-center gap-1 rounded-full bg-amber-50 px-2.5 py-1 text-xs font-semibold text-amber-700">
205
  <i data-lucide="clock" class="h-3 w-3"></i>
206
  Due in @daysDue day@(daysDue != 1 ? "s" : "")
@@ -209,23 +217,23 @@
209
  </td>
210
  <td class="px-6 py-4">
211
  @{
212
- var (priorityLabel, priorityClass) = task.PriorityId switch {
213
- TaskPriority.High => ("High", "bg-rose-50 text-rose-700"),
214
- TaskPriority.Medium => ("Medium", "bg-amber-50 text-amber-700"),
215
- _ => ("Low", "bg-slate-100 text-slate-600")
216
  };
217
  }
218
- <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium @priorityClass">@priorityLabel</span>
219
  </td>
220
  <td class="px-6 py-4">
221
  @{
222
- var (statusLabel, statusClass) = task.StatusId switch {
223
- AppTaskStatus.InProgress => ("In Progress", "bg-blue-50 text-blue-700"),
224
- AppTaskStatus.Done => ("Done", "bg-emerald-50 text-emerald-700"),
225
- _ => ("To Do", "bg-slate-100 text-slate-600")
226
  };
227
  }
228
- <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium @statusClass">@statusLabel</span>
229
  </td>
230
  </tr>
231
  }
@@ -241,53 +249,18 @@
241
  <Pagination CurrentPage="currentPage"
242
  TotalPages="TotalPages"
243
  PageSize="pageSize"
244
- TotalCount="FilteredTasks.Count()"
245
  OnPageChanged="HandlePageChanged"
246
  OnPageSizeChanged="HandlePageSizeChanged" />
247
  </div>
248
  }
249
-
250
- @if (isAdjustModalOpen && selectedTaskForAdjustment != null)
251
- {
252
- <div class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-slate-900/40 backdrop-blur-sm transition-opacity">
253
- <div class="relative w-full max-w-md rounded-2xl bg-white p-6 shadow-xl border border-slate-100 animate-in fade-in zoom-in duration-200">
254
- <h3 class="text-lg font-bold text-slate-900 mb-2 font-sans">Adjust Due Date</h3>
255
- <p class="text-sm text-slate-500 mb-4 font-sans">Update the due date for task: <span class="font-semibold text-slate-700">@selectedTaskForAdjustment.Title</span></p>
256
-
257
- <div class="space-y-4">
258
- <div>
259
- <label class="block text-xs font-medium text-slate-500 mb-1.5 font-sans">New Due Date</label>
260
- <input @bind="adjustedDueDate" type="date" 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" />
261
- </div>
262
-
263
- @if (!string.IsNullOrEmpty(adjustError))
264
- {
265
- <div class="rounded-lg bg-rose-50 p-3 text-xs font-medium text-rose-700 font-sans">@adjustError</div>
266
- }
267
-
268
- <div class="flex items-center justify-end space-x-3 pt-2">
269
- <button @onclick="CloseAdjustModal" class="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 font-sans">
270
- Cancel
271
- </button>
272
- <button @onclick="SaveDueDateAdjustment" disabled="@isAdjusting" class="rounded-lg bg-violet-600 px-4 py-2 text-sm font-medium text-white hover:bg-violet-700 transition-colors disabled:opacity-50 shadow-sm flex items-center font-sans">
273
- @if (isAdjusting)
274
- {
275
- <svg class="animate-spin -ml-1 mr-2 h-4 w-4 text-white" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path></svg>
276
- }
277
- Save Changes
278
- </button>
279
- </div>
280
- </div>
281
- </div>
282
- </div>
283
- }
284
  </div>
285
 
286
  @code {
287
  private bool isLoading = true;
288
- private List<TaskDto> allTasks = new();
289
- private List<UserDto> allUsers = new();
290
- private List<ProjectDto> allProjects = new();
291
 
292
  // Filters
293
  private string searchInput = "";
@@ -296,6 +269,8 @@
296
  private int appliedPriority = 0;
297
  private string filterDelayType = "all";
298
  private string appliedDelayType = "all";
 
 
299
 
300
  // Pagination
301
  private int currentPage = 1;
@@ -307,70 +282,15 @@
307
  private int dueSoonCount = 0;
308
  private int avgDaysOverdue = 0;
309
  private bool canUpdateTask = false;
310
- private List<OverdueCriticalTaskDto> overdueReportTasks = new();
311
- private PagedResult<OverdueCriticalTaskDto>? overdueReportPage;
312
-
313
- private IEnumerable<TaskDto> FilteredTasks
314
- {
315
- get
316
- {
317
- var today = DateTime.Today;
318
- var result = allTasks.AsEnumerable();
319
-
320
- // Only show non-completed tasks that are overdue or due soon
321
- result = result.Where(t => t.StatusId != AppTaskStatus.Done); // exclude Done
322
-
323
- if (appliedDelayType == "overdue")
324
- result = result.Where(t => t.DueDate < today);
325
- else if (appliedDelayType == "soon")
326
- result = result.Where(t => t.DueDate >= today && t.DueDate <= today.AddDays(3));
327
- else
328
- result = result.Where(t => t.DueDate <= today.AddDays(3)); // overdue + due within 3 days
329
 
330
- if (!string.IsNullOrWhiteSpace(appliedSearch))
331
- {
332
- var q = appliedSearch.Trim().ToLower();
333
- result = result.Where(t =>
334
- t.Title.ToLower().Contains(q) ||
335
- (t.Description != null && t.Description.ToLower().Contains(q)) ||
336
- (allUsers.FirstOrDefault(u => u.Id == t.AssignedTo) is { } usr &&
337
- ($"{usr.FirstName} {usr.LastName}").ToLower().Contains(q)) ||
338
- (allProjects.FirstOrDefault(p => p.Id == t.ProjectId) is { } prj &&
339
- prj.Name.ToLower().Contains(q))
340
- );
341
- }
342
-
343
- if (appliedPriority > 0)
344
- result = result.Where(t => (int)t.PriorityId == appliedPriority);
345
-
346
- return result.OrderBy(t => t.Title).ThenBy(t => t.DueDate);
347
- }
348
- }
349
-
350
- private int TotalPages => (int)Math.Ceiling((double)FilteredTasks.Count() / pageSize);
351
- private IEnumerable<TaskDto> PagedTasks => FilteredTasks.Skip((currentPage - 1) * pageSize).Take(pageSize);
352
 
353
  protected override async Task OnInitializedAsync()
354
  {
355
  var authState = await AuthStateProvider.GetAuthenticationStateAsync();
356
- canUpdateTask = authState.User.IsInRole("Admin") || await MenuAuthorization.HasAccessCodeAsync(authState.User, "Tasks_Update");
357
- var client = ApiClient.CreateClient();
358
- try
359
- {
360
- var tasksTask = client.GetFromJsonAsync<List<TaskDto>>("Task", Serialization.CaseInsensitive);
361
- var usersTask = client.GetFromJsonAsync<List<UserDto>>("User", Serialization.CaseInsensitive);
362
- var projectsTask = client.GetFromJsonAsync<List<ProjectDto>>("Project", Serialization.CaseInsensitive);
363
- await System.Threading.Tasks.Task.WhenAll(tasksTask, usersTask, projectsTask);
364
-
365
- allTasks = tasksTask.Result ?? new();
366
- allUsers = usersTask.Result ?? new();
367
- allProjects = projectsTask.Result ?? new();
368
-
369
- ComputeSummary();
370
- await LoadOverdueReport();
371
- }
372
- catch (Exception ex) { Console.WriteLine($"Load error: {ex.Message}"); }
373
- finally { isLoading = false; }
374
  }
375
 
376
  protected override async Task OnAfterRenderAsync(bool firstRender)
@@ -378,19 +298,6 @@
378
  await JS.InvokeVoidAsync("initIcons");
379
  }
380
 
381
- private void ComputeSummary()
382
- {
383
- var today = DateTime.Today;
384
- var activeTasks = allTasks.Where(t => t.StatusId != AppTaskStatus.Done).ToList();
385
- var overdueTasks = activeTasks.Where(t => t.DueDate < today).ToList();
386
- overdueCount = overdueTasks.Count;
387
- criticalCount = overdueTasks.Count(t => t.PriorityId == TaskPriority.High);
388
- dueSoonCount = activeTasks.Count(t => t.DueDate >= today && t.DueDate <= today.AddDays(3));
389
- avgDaysOverdue = overdueTasks.Any()
390
- ? (int)overdueTasks.Average(t => (today - t.DueDate).Days)
391
- : 0;
392
- }
393
-
394
  private async Task LoadOverdueReport()
395
  {
396
  var client = ApiClient.CreateClient();
@@ -400,22 +307,43 @@
400
  if (!string.IsNullOrWhiteSpace(appliedSearch)) parts.Add($"search={Uri.EscapeDataString(appliedSearch)}");
401
  if (appliedPriority > 0) parts.Add($"priorityId={appliedPriority}");
402
  if (!string.IsNullOrWhiteSpace(appliedDelayType)) parts.Add($"delayType={Uri.EscapeDataString(appliedDelayType)}");
403
- parts.Add($"page={currentPage}");
404
- parts.Add($"limit={pageSize}");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
405
 
406
- var url = "Report/overdue-critical" + (parts.Count > 0 ? "?" + string.Join("&", parts) : "");
407
- var result = await client.GetFromJsonAsync<PagedResult<OverdueCriticalTaskDto>>(url, Serialization.CaseInsensitive);
408
- if (result != null)
 
 
 
 
409
  {
410
- overdueReportPage = result;
411
- overdueReportTasks = result.Items.ToList();
412
- currentPage = result.Page;
413
- pageSize = result.PageSize;
414
  }
415
  }
416
  catch (Exception ex)
417
  {
418
- Console.WriteLine($"Overdue report paging error: {ex.Message}");
419
  }
420
  }
421
 
@@ -436,6 +364,8 @@
436
  appliedPriority = 0;
437
  filterDelayType = "all";
438
  appliedDelayType = "all";
 
 
439
  currentPage = 1;
440
  await LoadOverdueReport();
441
  }
@@ -455,44 +385,26 @@
455
 
456
  private async Task DownloadExcel()
457
  {
458
- var rows = FilteredTasks.Select(task =>
 
459
  {
460
- var daysOver = (DateTime.Today - task.DueDate).Days;
461
- var assignee = allUsers.FirstOrDefault(u => u.Id == task.AssignedTo);
462
- var project = allProjects.FirstOrDefault(p => p.Id == task.ProjectId);
463
- var priority = task.PriorityId switch { TaskPriority.High => "High", TaskPriority.Medium => "Medium", _ => "Low" };
464
- var status = task.StatusId switch { AppTaskStatus.InProgress => "In Progress", AppTaskStatus.Done => "Done", _ => "To Do" };
465
-
466
- return new object?[]
467
- {
468
- task.Title,
469
- project?.Name ?? string.Empty,
470
- assignee != null ? $"{assignee.FirstName} {assignee.LastName}" : "Unassigned",
471
- task.DueDate,
472
- daysOver,
473
- priority,
474
- status
475
- };
476
- });
477
-
478
- var workbookBytes = SpreadsheetReportBuilder.BuildTableWorkbook(
479
- "Overdue Tasks",
480
- "Overdue & Delayed Tasks Report",
481
- new[] { "Task", "Project", "Assignee", "Due Date", "Days Overdue", "Priority", "Status" },
482
- rows,
483
- new[]
484
  {
485
- $"Search: {(string.IsNullOrWhiteSpace(appliedSearch) ? "All tasks" : appliedSearch)}",
486
- $"Priority: {appliedPriority switch { 0 => "All", 1 => "Low", 2 => "Medium", 3 => "High", _ => appliedPriority.ToString() }}",
487
- $"Delay Type: {appliedDelayType switch { "all" => "All delayed tasks", "overdue" => "Overdue", "soon" => "Due within 3 days", _ => appliedDelayType }}",
488
- $"Generated: {DisplayFormats.Date(DateTime.Today)}"
489
- });
490
-
491
- await JS.InvokeVoidAsync(
492
- "downloadFileFromBase64",
493
- $"OverdueTasksReport_{DateTime.Today:yyyyMMdd}.xlsx",
494
- "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
495
- Convert.ToBase64String(workbookBytes));
496
  }
497
 
498
  private async Task DownloadPdf()
@@ -505,22 +417,17 @@
505
  $"Generated: {DisplayFormats.Date(DateTime.Today)}"
506
  };
507
 
508
- var rows = FilteredTasks.Select(task =>
509
  {
510
- var daysOver = (DateTime.Today - task.DueDate).Days;
511
- var assignee = allUsers.FirstOrDefault(u => u.Id == task.AssignedTo);
512
- var project = allProjects.FirstOrDefault(p => p.Id == task.ProjectId);
513
- var priority = task.PriorityId switch { TaskPriority.High => "High", TaskPriority.Medium => "Medium", _ => "Low" };
514
- var status = task.StatusId switch { AppTaskStatus.InProgress => "In Progress", AppTaskStatus.Done => "Done", _ => "To Do" };
515
  return new[]
516
  {
517
  task.Title,
518
- project?.Name ?? string.Empty,
519
- assignee != null ? $"{assignee.FirstName} {assignee.LastName}" : "Unassigned",
520
  DisplayFormats.Date(task.DueDate),
521
- daysOver.ToString(),
522
- priority,
523
- status
524
  };
525
  });
526
 
@@ -536,84 +443,4 @@
536
  "application/pdf",
537
  Convert.ToBase64String(pdfBytes));
538
  }
539
-
540
- // Adjust Due Date modal variables and methods
541
- private bool isAdjustModalOpen = false;
542
- private TaskDto? selectedTaskForAdjustment;
543
- private DateTime adjustedDueDate = DateTime.Today;
544
- private string adjustError = string.Empty;
545
- private bool isAdjusting = false;
546
-
547
- private void RemindAssignee(TaskDto task)
548
- {
549
- var assigneeName = allUsers.FirstOrDefault(u => u.Id == task.AssignedTo) is { } usr ? $"{usr.FirstName} {usr.LastName}" : "Unassigned";
550
- Console.WriteLine($"[REMINDER PLACEHOLDER] Sent reminder notification to assignee '{assigneeName}' for overdue task ID {task.Id} ('{task.Title}')");
551
- }
552
-
553
- private void OpenAdjustDueDateModal(TaskDto task)
554
- {
555
- selectedTaskForAdjustment = task;
556
- adjustedDueDate = task.DueDate;
557
- adjustError = string.Empty;
558
- isAdjustModalOpen = true;
559
- }
560
-
561
- private void CloseAdjustModal()
562
- {
563
- isAdjustModalOpen = false;
564
- selectedTaskForAdjustment = null;
565
- adjustError = string.Empty;
566
- }
567
-
568
- private async Task SaveDueDateAdjustment()
569
- {
570
- if (!canUpdateTask)
571
- {
572
- return;
573
- }
574
-
575
- if (selectedTaskForAdjustment == null) return;
576
-
577
- isAdjusting = true;
578
- adjustError = string.Empty;
579
- var client = ApiClient.CreateClient();
580
- try
581
- {
582
- var updateDto = new UpdateTaskDto
583
- {
584
- Title = selectedTaskForAdjustment.Title,
585
- Description = selectedTaskForAdjustment.Description,
586
- StatusId = selectedTaskForAdjustment.StatusId,
587
- PriorityId = selectedTaskForAdjustment.PriorityId,
588
- AssignedTo = selectedTaskForAdjustment.AssignedTo,
589
- AssignedBy = selectedTaskForAdjustment.AssignedBy,
590
- DueDate = adjustedDueDate
591
- };
592
-
593
- var response = await client.PutAsJsonAsync($"Task/{selectedTaskForAdjustment.Id}", updateDto);
594
- if (response.IsSuccessStatusCode)
595
- {
596
- var t = allTasks.FirstOrDefault(x => x.Id == selectedTaskForAdjustment.Id);
597
- if (t != null)
598
- {
599
- t.DueDate = adjustedDueDate;
600
- }
601
- ComputeSummary();
602
- CloseAdjustModal();
603
- }
604
- else
605
- {
606
- var errorRes = await response.Content.ReadFromJsonAsync<Result>();
607
- adjustError = errorRes?.ErrorMessage ?? "Failed to update due date. Please try again.";
608
- }
609
- }
610
- catch (Exception ex)
611
- {
612
- adjustError = $"Error: {ex.Message}";
613
- }
614
- finally
615
- {
616
- isAdjusting = false;
617
- }
618
- }
619
  }
 
115
  <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-600 hover:bg-slate-50 transition-colors shadow-sm">
116
  <i data-lucide="rotate-ccw" class="h-4 w-4 mr-2"></i> Reset
117
  </button>
118
+
119
+ <div class="flex flex-wrap items-center gap-6 w-full border-t border-slate-100 pt-3 mt-1">
120
+ <label class="flex items-center space-x-2 text-sm font-medium text-slate-700 cursor-pointer select-none">
121
+ <input type="checkbox" @bind="filterAssignToMe" class="h-4 w-4 rounded border-slate-300 text-violet-600 focus:ring-violet-500" />
122
+ <span>Assigned to me</span>
123
+ </label>
124
+ <label class="flex items-center space-x-2 text-sm font-medium text-slate-700 cursor-pointer select-none">
125
+ <input type="checkbox" @bind="filterAssignToMyTeam" class="h-4 w-4 rounded border-slate-300 text-violet-600 focus:ring-violet-500" />
126
+ <span>Assigned to my team</span>
127
+ </label>
128
+ </div>
129
  </div>
130
  </div>
131
 
 
139
  <div class="rounded-xl border border-slate-200 bg-white shadow-sm overflow-hidden">
140
  <div class="px-6 py-4 border-b border-slate-100 flex items-center justify-between">
141
  <h3 class="text-sm font-semibold text-slate-700">
142
+ Showing <span class="text-rose-600 font-bold">@(overdueReportPage?.TotalCount ?? 0)</span> delayed / overdue task(s)
143
  </h3>
144
  </div>
145
  <table class="w-full">
 
156
  </tr>
157
  </thead>
158
  <tbody class="divide-y divide-slate-100">
159
+ @if (overdueReportTasks.Any())
160
  {
161
+ @foreach (var entry in overdueReportTasks.Select((task, index) => (task, index)))
162
  {
163
  var task = entry.task;
 
 
 
164
  <tr class="hover:bg-slate-50/50 transition-colors">
165
  <td class="px-6 py-4 text-sm font-semibold text-slate-500 align-top">@((currentPage - 1) * pageSize + entry.index + 1)</td>
166
  <td class="px-6 py-4">
 
175
  <td class="px-6 py-4">
176
  <span class="inline-flex items-center gap-1 text-sm text-slate-600">
177
  <i data-lucide="folder" class="h-3.5 w-3.5 text-violet-500"></i>
178
+ @task.ProjectName
179
  </span>
180
  </td>
181
  <td class="px-6 py-4">
182
+ @if (!string.IsNullOrEmpty(task.AssignedTo))
183
  {
184
  <div class="flex items-center gap-2">
185
  <span class="flex h-7 w-7 items-center justify-center rounded-full bg-violet-100 text-violet-700 text-xs font-bold">
186
+ @(task.AssignedTo.Length > 0 ? task.AssignedTo[0].ToString().ToUpper() : "")
187
  </span>
188
+ <span class="text-sm text-slate-700">@task.AssignedTo</span>
189
  </div>
190
  }
191
  else
 
194
  }
195
  </td>
196
  <td class="px-6 py-4">
197
+ <span class="text-sm font-medium @(task.DaysOverdue > 0 ? "text-rose-600" : "text-amber-600")">
198
  @DisplayFormats.Date(task.DueDate)
199
  </span>
200
  </td>
201
  <td class="px-6 py-4">
202
+ @if (task.DaysOverdue > 0)
203
  {
204
  <span class="inline-flex items-center gap-1 rounded-full bg-rose-50 px-2.5 py-1 text-xs font-semibold text-rose-700">
205
  <i data-lucide="alert-triangle" class="h-3 w-3"></i>
206
+ @task.DaysOverdue day@(task.DaysOverdue != 1 ? "s" : "")
207
  </span>
208
  }
209
  else
210
  {
211
+ var daysDue = Math.Abs(task.DaysOverdue);
212
  <span class="inline-flex items-center gap-1 rounded-full bg-amber-50 px-2.5 py-1 text-xs font-semibold text-amber-700">
213
  <i data-lucide="clock" class="h-3 w-3"></i>
214
  Due in @daysDue day@(daysDue != 1 ? "s" : "")
 
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
  }
 
249
  <Pagination CurrentPage="currentPage"
250
  TotalPages="TotalPages"
251
  PageSize="pageSize"
252
+ TotalCount="@(overdueReportPage?.TotalCount ?? 0)"
253
  OnPageChanged="HandlePageChanged"
254
  OnPageSizeChanged="HandlePageSizeChanged" />
255
  </div>
256
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
257
  </div>
258
 
259
  @code {
260
  private bool isLoading = true;
261
+ private List<OverdueCriticalTaskDto> allOverdue = new();
262
+ private List<OverdueCriticalTaskDto> overdueReportTasks = new();
263
+ private PagedResult<OverdueCriticalTaskDto>? overdueReportPage;
264
 
265
  // Filters
266
  private string searchInput = "";
 
269
  private int appliedPriority = 0;
270
  private string filterDelayType = "all";
271
  private string appliedDelayType = "all";
272
+ private bool filterAssignToMe = false;
273
+ private bool filterAssignToMyTeam = false;
274
 
275
  // Pagination
276
  private int currentPage = 1;
 
282
  private int dueSoonCount = 0;
283
  private int avgDaysOverdue = 0;
284
  private bool canUpdateTask = false;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
285
 
286
+ private int TotalPages => overdueReportPage?.TotalPages ?? 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
287
 
288
  protected override async Task OnInitializedAsync()
289
  {
290
  var authState = await AuthStateProvider.GetAuthenticationStateAsync();
291
+ canUpdateTask = await MenuAuthorization.HasAccessCodeAsync(authState.User, "Tasks_Update");
292
+ await LoadOverdueReport();
293
+ isLoading = false;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
294
  }
295
 
296
  protected override async Task OnAfterRenderAsync(bool firstRender)
 
298
  await JS.InvokeVoidAsync("initIcons");
299
  }
300
 
 
 
 
 
 
 
 
 
 
 
 
 
 
301
  private async Task LoadOverdueReport()
302
  {
303
  var client = ApiClient.CreateClient();
 
307
  if (!string.IsNullOrWhiteSpace(appliedSearch)) parts.Add($"search={Uri.EscapeDataString(appliedSearch)}");
308
  if (appliedPriority > 0) parts.Add($"priorityId={appliedPriority}");
309
  if (!string.IsNullOrWhiteSpace(appliedDelayType)) parts.Add($"delayType={Uri.EscapeDataString(appliedDelayType)}");
310
+ if (filterAssignToMe) parts.Add("assignedToMe=true");
311
+ if (filterAssignToMyTeam) parts.Add("assignedToMyTeam=true");
312
+
313
+ // 1. Fetch full matching list once to compute summary cards and for exports
314
+ var fullUrl = "Report/overdue-critical" + (parts.Count > 0 ? "?" + string.Join("&", parts) : "");
315
+ var fullResult = await client.GetFromJsonAsync<Result<List<OverdueCriticalTaskDto>>>(fullUrl, Serialization.CaseInsensitive);
316
+ if (fullResult != null && fullResult.IsSuccess && fullResult.Value != null)
317
+ {
318
+ allOverdue = fullResult.Value;
319
+ var today = DateTime.Today;
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)
327
+ : 0;
328
+ }
329
 
330
+ // 2. Fetch paged list for the table view
331
+ var pagedParts = new List<string>(parts);
332
+ pagedParts.Add($"page={currentPage}");
333
+ pagedParts.Add($"limit={pageSize}");
334
+ var pagedUrl = "Report/overdue-critical" + (pagedParts.Count > 0 ? "?" + string.Join("&", pagedParts) : "");
335
+ var pagedResult = await client.GetFromJsonAsync<PagedResult<OverdueCriticalTaskDto>>(pagedUrl, Serialization.CaseInsensitive);
336
+ if (pagedResult != null)
337
  {
338
+ overdueReportPage = pagedResult;
339
+ overdueReportTasks = pagedResult.Items.ToList();
340
+ currentPage = pagedResult.Page;
341
+ pageSize = pagedResult.PageSize;
342
  }
343
  }
344
  catch (Exception ex)
345
  {
346
+ Console.WriteLine($"Overdue report loading error: {ex.Message}");
347
  }
348
  }
349
 
 
364
  appliedPriority = 0;
365
  filterDelayType = "all";
366
  appliedDelayType = "all";
367
+ filterAssignToMe = false;
368
+ filterAssignToMyTeam = false;
369
  currentPage = 1;
370
  await LoadOverdueReport();
371
  }
 
385
 
386
  private async Task DownloadExcel()
387
  {
388
+ var client = ApiClient.CreateClient();
389
+ try
390
  {
391
+ var parts = new List<string>();
392
+ if (!string.IsNullOrWhiteSpace(appliedSearch)) parts.Add($"search={Uri.EscapeDataString(appliedSearch)}");
393
+ if (appliedPriority > 0) parts.Add($"priorityId={appliedPriority}");
394
+
395
+ var url = "Report/overdue-critical/excel" + (parts.Count > 0 ? "?" + string.Join("&", parts) : "");
396
+ var response = await client.GetAsync(url);
397
+ if (response.IsSuccessStatusCode)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
398
  {
399
+ var bytes = await response.Content.ReadAsByteArrayAsync();
400
+ await JS.InvokeVoidAsync(
401
+ "downloadFileFromBase64",
402
+ $"OverdueTasksReport_{DateTime.Today:yyyyMMdd}.xlsx",
403
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
404
+ Convert.ToBase64String(bytes));
405
+ }
406
+ }
407
+ catch (Exception ex) { Console.WriteLine($"Excel download error: {ex.Message}"); }
 
 
408
  }
409
 
410
  private async Task DownloadPdf()
 
417
  $"Generated: {DisplayFormats.Date(DateTime.Today)}"
418
  };
419
 
420
+ var rows = allOverdue.Select(task =>
421
  {
 
 
 
 
 
422
  return new[]
423
  {
424
  task.Title,
425
+ task.ProjectName,
426
+ task.AssignedTo ?? "Unassigned",
427
  DisplayFormats.Date(task.DueDate),
428
+ task.DaysOverdue.ToString(),
429
+ task.PriorityName,
430
+ task.StatusName
431
  };
432
  });
433
 
 
443
  "application/pdf",
444
  Convert.ToBase64String(pdfBytes));
445
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
446
  }
TaskTrackingSystem.WebApp/Components/Pages/{ProjectProgressReport.razor → Features/Reports/ProjectProgressReport.razor} RENAMED
@@ -85,6 +85,17 @@
85
  <button @onclick="ResetAllFilters" 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">
86
  <i data-lucide="rotate-ccw" class="h-4 w-4 mr-2 text-slate-500"></i> Reset
87
  </button>
 
 
 
 
 
 
 
 
 
 
 
88
  </div>
89
  </div>
90
 
@@ -144,14 +155,14 @@
144
  <!-- Chart View: SVG Project Progress Bars -->
145
  <div class="rounded-xl border border-slate-200 bg-white p-6 shadow-sm">
146
  <h3 class="font-semibold text-slate-900 mb-4 font-sans text-md">Project Completion Chart</h3>
147
- @if (!FilteredList.Any())
148
  {
149
  <p class="text-slate-400 text-center py-8">No data available.</p>
150
  }
151
  else
152
  {
153
  <div class="space-y-4 max-w-2xl">
154
- @foreach (var proj in FilteredList)
155
  {
156
  <div>
157
  <div class="flex justify-between text-xs font-semibold text-slate-700 mb-1.5">
@@ -184,9 +195,9 @@
184
  </tr>
185
  </thead>
186
  <tbody class="divide-y divide-slate-100">
187
- @if (PagedList.Any())
188
  {
189
- @foreach (var entry in PagedList.Select((proj, index) => (proj, index)))
190
  {
191
  var proj = entry.proj;
192
  <tr class="hover:bg-slate-50/50 transition-colors">
@@ -233,9 +244,8 @@
233
  @code {
234
  private bool isLoading = true;
235
  private bool isChartView = false;
236
- private List<ProjectDto> projects = new();
237
- private List<TaskDto> tasks = new();
238
- private List<ProjectMetric> displayList = new();
239
  private PagedResult<ProjectProgressReportDto>? reportPage;
240
 
241
  // Filters
@@ -243,6 +253,8 @@
243
  private DateTime? filterStartDate;
244
  private DateTime? filterEndDate;
245
  private string filterStatus = "0";
 
 
246
 
247
  // Search and Pagination
248
  private string searchInput = "";
@@ -255,13 +267,31 @@
255
  private int aheadOfScheduleCount = 0;
256
  private int atRiskCount = 0;
257
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
258
  private async Task ApplyAllFilters()
259
  {
260
  ApplyPeriodPreset();
261
  searchQuery = searchInput;
262
  currentPage = 1;
263
- CalculateMetrics();
264
- await LoadProjectReportPage();
265
  }
266
 
267
  private async Task ResetAllFilters()
@@ -272,78 +302,23 @@
272
  filterStartDate = null;
273
  filterEndDate = null;
274
  filterStatus = "0";
 
 
275
  currentPage = 1;
276
- CalculateMetrics();
277
- await LoadProjectReportPage();
278
  }
279
 
280
  private async Task HandlePageChanged(int page)
281
  {
282
  currentPage = page;
283
- await LoadProjectReportPage();
284
  }
285
 
286
  private async Task HandlePageSizeChanged(int size)
287
  {
288
  pageSize = size;
289
  currentPage = 1;
290
- await LoadProjectReportPage();
291
- }
292
-
293
- private IEnumerable<ProjectMetric> FilteredList =>
294
- string.IsNullOrWhiteSpace(searchQuery)
295
- ? displayList
296
- : displayList.Where(p => p.ProjectName.Contains(searchQuery, StringComparison.OrdinalIgnoreCase));
297
-
298
- private int TotalPages => reportPage?.TotalPages ?? 0;
299
-
300
- private int TotalCount => reportPage?.TotalCount ?? 0;
301
-
302
- private IEnumerable<ProjectMetric> PagedList =>
303
- reportPage?.Items.Select(x => new ProjectMetric
304
- {
305
- ProjectName = x.ProjectName,
306
- StartDate = x.StartDate,
307
- EndDate = x.EndDate,
308
- TotalTasks = x.TotalTasks,
309
- CompletedTasks = x.CompletedTasks,
310
- Progress = x.Progress,
311
- IsAhead = x.IsAhead,
312
- IsAtRisk = x.IsAtRisk
313
- }) ?? Enumerable.Empty<ProjectMetric>();
314
-
315
- protected override async Task OnInitializedAsync()
316
- {
317
- var client = ApiClient.CreateClient();
318
- try
319
- {
320
- var projectsTask = client.GetFromJsonAsync<List<ProjectDto>>("Project");
321
- var tasksTask = client.GetFromJsonAsync<List<TaskDto>>("Task");
322
- await Task.WhenAll(projectsTask, tasksTask);
323
-
324
- projects = projectsTask.Result ?? new();
325
- tasks = tasksTask.Result ?? new();
326
- CalculateMetrics();
327
- await LoadProjectReportPage();
328
- }
329
- catch (Exception ex)
330
- {
331
- Console.WriteLine($"Error initializing: {ex.Message}");
332
- }
333
- finally
334
- {
335
- isLoading = false;
336
- }
337
- }
338
-
339
- protected override async Task OnAfterRenderAsync(bool firstRender)
340
- {
341
- await JS.InvokeVoidAsync("initIcons");
342
- }
343
-
344
- private void SetView(bool chartView)
345
- {
346
- isChartView = chartView;
347
  }
348
 
349
  private void ApplyPeriodPreset()
@@ -371,78 +346,7 @@
371
  }
372
  }
373
 
374
- private void CalculateMetrics()
375
- {
376
- var metrics = new List<ProjectMetric>();
377
- var filteredProjects = projects.ToList();
378
-
379
- // Apply period filter to projects by StartDate
380
- if (filterStartDate.HasValue)
381
- {
382
- filteredProjects = filteredProjects.Where(p => p.StartDate >= filterStartDate.Value).ToList();
383
- }
384
- if (filterEndDate.HasValue)
385
- {
386
- filteredProjects = filteredProjects.Where(p => p.StartDate <= filterEndDate.Value.AddDays(1)).ToList();
387
- }
388
-
389
- var today = DateTime.Today;
390
-
391
- foreach (var p in filteredProjects)
392
- {
393
- var projectTasks = tasks.Where(t => t.ProjectId == p.Id).ToList();
394
-
395
- // Apply filter status on tasks if matching
396
- if (filterStatus == "overdue")
397
- {
398
- projectTasks = projectTasks.Where(t => t.DueDate.Date < today && t.StatusId != AppTaskStatus.Done).ToList();
399
- }
400
- else if (filterStatus != "0" && Enum.TryParse<AppTaskStatus>(filterStatus, out var parsedStatus))
401
- {
402
- projectTasks = projectTasks.Where(t => t.StatusId == parsedStatus).ToList();
403
- }
404
-
405
- int totalTasks = projectTasks.Count;
406
- int completedTasks = projectTasks.Count(t => t.StatusId == AppTaskStatus.Done); // Done
407
- double progress = totalTasks > 0 ? ((double)completedTasks / totalTasks) * 100 : 0;
408
-
409
- // Ahead of schedule calculation
410
- double elapsedPct;
411
- if (today < p.StartDate) elapsedPct = 0;
412
- else if (today > p.EndDate) elapsedPct = 100;
413
- else
414
- {
415
- var totalDays = (p.EndDate - p.StartDate).TotalDays;
416
- var elapsedDays = (today - p.StartDate).TotalDays;
417
- elapsedPct = totalDays > 0 ? (elapsedDays / totalDays) * 100 : 100;
418
- }
419
- bool isAhead = progress > elapsedPct && progress > 0 && progress < 100;
420
-
421
- // At risk calculation
422
- bool isAtRisk = (projectTasks.Any(t => t.StatusId != AppTaskStatus.Done && t.DueDate.Date < today)) || (p.EndDate.Date < today && progress < 100);
423
-
424
- metrics.Add(new ProjectMetric
425
- {
426
- ProjectName = p.Name,
427
- StartDate = p.StartDate,
428
- EndDate = p.EndDate,
429
- TotalTasks = totalTasks,
430
- CompletedTasks = completedTasks,
431
- Progress = progress,
432
- IsAhead = isAhead,
433
- IsAtRisk = isAtRisk
434
- });
435
- }
436
-
437
- displayList = metrics.OrderBy(m => m.ProjectName).ToList();
438
-
439
- // Calculate card statistics
440
- totalActiveProjects = displayList.Count;
441
- aheadOfScheduleCount = displayList.Count(m => m.IsAhead);
442
- atRiskCount = displayList.Count(m => m.IsAtRisk);
443
- }
444
-
445
- private async Task LoadProjectReportPage()
446
  {
447
  var client = ApiClient.CreateClient();
448
  try
@@ -451,28 +355,45 @@
451
  if (!string.IsNullOrWhiteSpace(searchQuery)) parts.Add($"search={Uri.EscapeDataString(searchQuery)}");
452
  if (filterStartDate.HasValue) parts.Add($"startDate={filterStartDate:yyyy-MM-dd}");
453
  if (filterEndDate.HasValue) parts.Add($"endDate={filterEndDate:yyyy-MM-dd}");
454
- if (!string.IsNullOrWhiteSpace(filterStatus)) parts.Add($"status={Uri.EscapeDataString(filterStatus)}");
455
- parts.Add($"page={currentPage}");
456
- parts.Add($"limit={pageSize}");
 
 
 
 
 
 
 
 
 
 
 
 
457
 
458
- var url = "Report/project-progress" + (parts.Count > 0 ? "?" + string.Join("&", parts) : "");
459
- var result = await client.GetFromJsonAsync<PagedResult<ProjectProgressReportDto>>(url, Serialization.CaseInsensitive);
460
- if (result != null)
 
 
 
 
461
  {
462
- reportPage = result;
463
- currentPage = result.Page;
464
- pageSize = result.PageSize;
 
465
  }
466
  }
467
  catch (Exception ex)
468
  {
469
- Console.WriteLine($"Project report paging error: {ex.Message}");
470
  }
471
  }
472
 
473
  private async Task DownloadExcel()
474
  {
475
- var rows = FilteredList.Select(proj => new object?[]
476
  {
477
  proj.ProjectName,
478
  proj.StartDate,
@@ -512,7 +433,7 @@
512
  $"Generated: {DisplayFormats.Date(DateTime.Today)}"
513
  };
514
 
515
- var rows = FilteredList.Select(proj => new[]
516
  {
517
  proj.ProjectName,
518
  DisplayFormats.Date(proj.StartDate),
@@ -534,16 +455,4 @@
534
  "application/pdf",
535
  Convert.ToBase64String(pdfBytes));
536
  }
537
-
538
- private class ProjectMetric
539
- {
540
- public string ProjectName { get; set; } = string.Empty;
541
- public DateTime StartDate { get; set; }
542
- public DateTime EndDate { get; set; }
543
- public int TotalTasks { get; set; }
544
- public int CompletedTasks { get; set; }
545
- public double Progress { get; set; }
546
- public bool IsAhead { get; set; }
547
- public bool IsAtRisk { get; set; }
548
- }
549
  }
 
85
  <button @onclick="ResetAllFilters" 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">
86
  <i data-lucide="rotate-ccw" class="h-4 w-4 mr-2 text-slate-500"></i> Reset
87
  </button>
88
+
89
+ <div class="flex flex-wrap items-center gap-6 w-full border-t border-slate-100 pt-3 mt-1">
90
+ <label class="flex items-center space-x-2 text-sm font-medium text-slate-700 cursor-pointer select-none">
91
+ <input type="checkbox" @bind="filterAssignToMe" class="h-4 w-4 rounded border-slate-300 text-violet-600 focus:ring-violet-500" />
92
+ <span>Assigned to me</span>
93
+ </label>
94
+ <label class="flex items-center space-x-2 text-sm font-medium text-slate-700 cursor-pointer select-none">
95
+ <input type="checkbox" @bind="filterAssignToMyTeam" class="h-4 w-4 rounded border-slate-300 text-violet-600 focus:ring-violet-500" />
96
+ <span>Assigned to my team</span>
97
+ </label>
98
+ </div>
99
  </div>
100
  </div>
101
 
 
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>
161
  }
162
  else
163
  {
164
  <div class="space-y-4 max-w-2xl">
165
+ @foreach (var proj in allProjects)
166
  {
167
  <div>
168
  <div class="flex justify-between text-xs font-semibold text-slate-700 mb-1.5">
 
195
  </tr>
196
  </thead>
197
  <tbody class="divide-y divide-slate-100">
198
+ @if (pagedProjects.Any())
199
  {
200
+ @foreach (var entry in pagedProjects.Select((proj, index) => (proj, index)))
201
  {
202
  var proj = entry.proj;
203
  <tr class="hover:bg-slate-50/50 transition-colors">
 
244
  @code {
245
  private bool isLoading = true;
246
  private bool isChartView = false;
247
+ private List<ProjectProgressReportDto> allProjects = new();
248
+ private List<ProjectProgressReportDto> pagedProjects = new();
 
249
  private PagedResult<ProjectProgressReportDto>? reportPage;
250
 
251
  // Filters
 
253
  private DateTime? filterStartDate;
254
  private DateTime? filterEndDate;
255
  private string filterStatus = "0";
256
+ private bool filterAssignToMe = false;
257
+ private bool filterAssignToMyTeam = false;
258
 
259
  // Search and Pagination
260
  private string searchInput = "";
 
267
  private int aheadOfScheduleCount = 0;
268
  private int atRiskCount = 0;
269
 
270
+ private int TotalPages => reportPage?.TotalPages ?? 0;
271
+ private int TotalCount => reportPage?.TotalCount ?? 0;
272
+
273
+ protected override async Task OnInitializedAsync()
274
+ {
275
+ await LoadProjectReportData();
276
+ isLoading = false;
277
+ }
278
+
279
+ protected override async Task OnAfterRenderAsync(bool firstRender)
280
+ {
281
+ await JS.InvokeVoidAsync("initIcons");
282
+ }
283
+
284
+ private void SetView(bool chartView)
285
+ {
286
+ isChartView = chartView;
287
+ }
288
+
289
  private async Task ApplyAllFilters()
290
  {
291
  ApplyPeriodPreset();
292
  searchQuery = searchInput;
293
  currentPage = 1;
294
+ await LoadProjectReportData();
 
295
  }
296
 
297
  private async Task ResetAllFilters()
 
302
  filterStartDate = null;
303
  filterEndDate = null;
304
  filterStatus = "0";
305
+ filterAssignToMe = false;
306
+ filterAssignToMyTeam = false;
307
  currentPage = 1;
308
+ await LoadProjectReportData();
 
309
  }
310
 
311
  private async Task HandlePageChanged(int page)
312
  {
313
  currentPage = page;
314
+ await LoadProjectReportData();
315
  }
316
 
317
  private async Task HandlePageSizeChanged(int size)
318
  {
319
  pageSize = size;
320
  currentPage = 1;
321
+ await LoadProjectReportData();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
322
  }
323
 
324
  private void ApplyPeriodPreset()
 
346
  }
347
  }
348
 
349
+ private async Task LoadProjectReportData()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
350
  {
351
  var client = ApiClient.CreateClient();
352
  try
 
355
  if (!string.IsNullOrWhiteSpace(searchQuery)) parts.Add($"search={Uri.EscapeDataString(searchQuery)}");
356
  if (filterStartDate.HasValue) parts.Add($"startDate={filterStartDate:yyyy-MM-dd}");
357
  if (filterEndDate.HasValue) parts.Add($"endDate={filterEndDate:yyyy-MM-dd}");
358
+ if (!string.IsNullOrWhiteSpace(filterStatus) && filterStatus != "0") parts.Add($"status={Uri.EscapeDataString(filterStatus)}");
359
+ if (filterAssignToMe) parts.Add("assignedToMe=true");
360
+ if (filterAssignToMyTeam) parts.Add("assignedToMyTeam=true");
361
+
362
+ // 1. Fetch full matching list for chart view, cards summary, and exports
363
+ var fullUrl = "Report/project-progress" + (parts.Count > 0 ? "?" + string.Join("&", parts) : "");
364
+ var fullResult = await client.GetFromJsonAsync<Result<List<ProjectProgressReportDto>>>(fullUrl, Serialization.CaseInsensitive);
365
+ if (fullResult != null && fullResult.IsSuccess && fullResult.Value != null)
366
+ {
367
+ allProjects = fullResult.Value;
368
+
369
+ totalActiveProjects = allProjects.Count;
370
+ aheadOfScheduleCount = allProjects.Count(m => m.IsAhead);
371
+ atRiskCount = allProjects.Count(m => m.IsAtRisk);
372
+ }
373
 
374
+ // 2. Fetch paged list for table pagination
375
+ var pagedParts = new List<string>(parts);
376
+ pagedParts.Add($"page={currentPage}");
377
+ pagedParts.Add($"limit={pageSize}");
378
+ var pagedUrl = "Report/project-progress" + (pagedParts.Count > 0 ? "?" + string.Join("&", pagedParts) : "");
379
+ var pagedResult = await client.GetFromJsonAsync<PagedResult<ProjectProgressReportDto>>(pagedUrl, Serialization.CaseInsensitive);
380
+ if (pagedResult != null)
381
  {
382
+ reportPage = pagedResult;
383
+ pagedProjects = pagedResult.Items.ToList();
384
+ currentPage = pagedResult.Page;
385
+ pageSize = pagedResult.PageSize;
386
  }
387
  }
388
  catch (Exception ex)
389
  {
390
+ Console.WriteLine($"Error loading project progress report: {ex.Message}");
391
  }
392
  }
393
 
394
  private async Task DownloadExcel()
395
  {
396
+ var rows = allProjects.Select(proj => new object?[]
397
  {
398
  proj.ProjectName,
399
  proj.StartDate,
 
433
  $"Generated: {DisplayFormats.Date(DateTime.Today)}"
434
  };
435
 
436
+ var rows = allProjects.Select(proj => new[]
437
  {
438
  proj.ProjectName,
439
  DisplayFormats.Date(proj.StartDate),
 
455
  "application/pdf",
456
  Convert.ToBase64String(pdfBytes));
457
  }
 
 
 
 
 
 
 
 
 
 
 
 
458
  }
TaskTrackingSystem.WebApp/Components/Pages/{TaskReport.razor → Features/Reports/TaskReport.razor} RENAMED
@@ -129,10 +129,20 @@
129
  <button @onclick="ResetAllFilters" 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">
130
  <i data-lucide="rotate-ccw" class="h-4 w-4 mr-2 text-slate-500"></i> Reset
131
  </button>
 
 
 
 
 
 
 
 
 
 
 
132
  </div>
133
  </div>
134
 
135
- <!-- Content Area -->
136
  @if (isLoading)
137
  {
138
  <div class="flex items-center justify-center py-20">
@@ -302,12 +312,12 @@
302
  </table>
303
 
304
  <!-- Pagination -->
305
- <Pagination CurrentPage="currentPage"
306
- TotalPages="TotalPages"
307
- PageSize="pageSize"
308
- TotalCount="taskReportPage?.TotalCount ?? 0"
309
- OnPageChanged="HandlePageChanged"
310
- OnPageSizeChanged="HandlePageSizeChanged" />
311
  </div>
312
  }
313
  }
@@ -329,6 +339,8 @@
329
  private string projectSearchInput = string.Empty;
330
  private string projectSearchQuery = string.Empty;
331
  private bool showProjectSuggestions;
 
 
332
 
333
  // Search and Pagination
334
  private string searchInput = "";
@@ -362,6 +374,8 @@
362
  projectSearchInput = string.Empty;
363
  projectSearchQuery = string.Empty;
364
  showProjectSuggestions = false;
 
 
365
  currentPage = 1;
366
  await LoadTaskReport();
367
  }
@@ -510,6 +524,8 @@
510
  if (filterEndDate.HasValue) parts.Add($"endDate={filterEndDate:yyyy-MM-dd}");
511
  if (!string.IsNullOrEmpty(filterStatus)) parts.Add($"status={Uri.EscapeDataString(filterStatus)}");
512
  if (filterProjectId > 0) parts.Add($"projectId={filterProjectId}");
 
 
513
  if (includePaging)
514
  {
515
  parts.Add($"page={currentPage}");
 
129
  <button @onclick="ResetAllFilters" 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">
130
  <i data-lucide="rotate-ccw" class="h-4 w-4 mr-2 text-slate-500"></i> Reset
131
  </button>
132
+
133
+ <div class="flex flex-wrap items-center gap-6 w-full border-t border-slate-100 pt-3 mt-1">
134
+ <label class="flex items-center space-x-2 text-sm font-medium text-slate-700 cursor-pointer select-none">
135
+ <input type="checkbox" @bind="filterAssignToMe" class="h-4 w-4 rounded border-slate-300 text-violet-600 focus:ring-violet-500" />
136
+ <span>Assigned to me</span>
137
+ </label>
138
+ <label class="flex items-center space-x-2 text-sm font-medium text-slate-700 cursor-pointer select-none">
139
+ <input type="checkbox" @bind="filterAssignToMyTeam" class="h-4 w-4 rounded border-slate-300 text-violet-600 focus:ring-violet-500" />
140
+ <span>Assigned to my team</span>
141
+ </label>
142
+ </div>
143
  </div>
144
  </div>
145
 
 
146
  @if (isLoading)
147
  {
148
  <div class="flex items-center justify-center py-20">
 
312
  </table>
313
 
314
  <!-- Pagination -->
315
+ <Pagination CurrentPage="currentPage"
316
+ TotalPages="TotalPages"
317
+ PageSize="pageSize"
318
+ TotalCount="taskReportPage?.TotalCount ?? 0"
319
+ OnPageChanged="HandlePageChanged"
320
+ OnPageSizeChanged="HandlePageSizeChanged" />
321
  </div>
322
  }
323
  }
 
339
  private string projectSearchInput = string.Empty;
340
  private string projectSearchQuery = string.Empty;
341
  private bool showProjectSuggestions;
342
+ private bool filterAssignToMe = false;
343
+ private bool filterAssignToMyTeam = false;
344
 
345
  // Search and Pagination
346
  private string searchInput = "";
 
374
  projectSearchInput = string.Empty;
375
  projectSearchQuery = string.Empty;
376
  showProjectSuggestions = false;
377
+ filterAssignToMe = false;
378
+ filterAssignToMyTeam = false;
379
  currentPage = 1;
380
  await LoadTaskReport();
381
  }
 
524
  if (filterEndDate.HasValue) parts.Add($"endDate={filterEndDate:yyyy-MM-dd}");
525
  if (!string.IsNullOrEmpty(filterStatus)) parts.Add($"status={Uri.EscapeDataString(filterStatus)}");
526
  if (filterProjectId > 0) parts.Add($"projectId={filterProjectId}");
527
+ if (filterAssignToMe) parts.Add("assignedToMe=true");
528
+ if (filterAssignToMyTeam) parts.Add("assignedToMyTeam=true");
529
  if (includePaging)
530
  {
531
  parts.Add($"page={currentPage}");
TaskTrackingSystem.WebApp/Components/Pages/Features/Reports/TimeTrackingReport.razor ADDED
@@ -0,0 +1,494 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @page "/reports/timesheet"
2
+ @using Microsoft.AspNetCore.Authorization
3
+ @using TaskTrackingSystem.Shared
4
+ @using TaskTrackingSystem.Shared.Models.Issue
5
+ @using TaskTrackingSystem.Shared.Models.User
6
+ @using TaskTrackingSystem.Shared.Models.Project
7
+ @using System.Text
8
+ @rendermode @(new InteractiveServerRenderMode(prerender: false))
9
+ @attribute [Authorize]
10
+ @inject ApiClientService ApiClient
11
+ @inject IJSRuntime JS
12
+ @inject AuthenticationStateProvider AuthStateProvider
13
+ @inject MenuAuthorizationService MenuAuthorization
14
+
15
+ <PageTitle>Time &amp; Effort Tracking Report</PageTitle>
16
+
17
+ <div class="space-y-6">
18
+ <!-- Page Header -->
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">Time &amp; Effort Tracking</h2>
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>
28
+ <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 hover:bg-slate-50 transition-colors shadow-sm">
29
+ <i data-lucide="file-text" class="h-4 w-4 mr-2"></i> Export PDF
30
+ </button>
31
+ </div>
32
+ </div>
33
+
34
+ <!-- Stats summary -->
35
+ @if (!isLoading)
36
+ {
37
+ <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
38
+ <div class="rounded-xl border border-violet-100 bg-violet-50 p-5 shadow-sm">
39
+ <div class="flex items-center justify-between">
40
+ <div>
41
+ <p class="text-xs font-semibold uppercase tracking-wider text-violet-500">Total Estimated Effort</p>
42
+ <p class="mt-1 text-3xl font-bold text-violet-700">@totalHours.ToString("N1") hrs</p>
43
+ </div>
44
+ <span class="flex h-11 w-11 items-center justify-center rounded-xl bg-violet-100">
45
+ <i data-lucide="clock" class="h-6 w-6 text-violet-600"></i>
46
+ </span>
47
+ </div>
48
+ </div>
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>
57
+ </span>
58
+ </div>
59
+ </div>
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>
68
+ </span>
69
+ </div>
70
+ </div>
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>
79
+ </span>
80
+ </div>
81
+ </div>
82
+ </div>
83
+ }
84
+
85
+ <!-- Filters -->
86
+ <div class="rounded-xl border border-slate-200 bg-white p-6 shadow-sm">
87
+ <div class="flex flex-col gap-4">
88
+ <div class="flex flex-wrap items-end gap-4">
89
+ <div class="flex-1 min-w-[200px]">
90
+ <label class="block text-xs font-medium text-slate-500 mb-1.5">Search Issues / Assignee</label>
91
+ <input @bind="searchInput" type="text" placeholder="Search issue title, description or assignee..."
92
+ 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" />
93
+ </div>
94
+ <div>
95
+ <label class="block text-xs font-medium text-slate-500 mb-1.5">Project</label>
96
+ <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 focus:border-transparent transition-all">
97
+ <option value="0">All Projects</option>
98
+ @foreach (var proj in allProjects)
99
+ {
100
+ <option value="@proj.Id">@proj.Name</option>
101
+ }
102
+ </select>
103
+ </div>
104
+ <div>
105
+ <label class="block text-xs font-medium text-slate-500 mb-1.5">Issue Status</label>
106
+ <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 focus:border-transparent transition-all">
107
+ <option value="0">All Statuses</option>
108
+ <option value="Todo">To Do</option>
109
+ <option value="InProgress">In Progress</option>
110
+ <option value="Done">Completed</option>
111
+ </select>
112
+ </div>
113
+ <button @onclick="ApplyFilters" class="inline-flex items-center rounded-lg bg-violet-600 px-5 py-2 text-sm font-medium text-white hover:bg-violet-700 transition-colors shadow-sm">
114
+ <i data-lucide="search" class="h-4 w-4 mr-2"></i> Search
115
+ </button>
116
+ <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-600 hover:bg-slate-50 transition-colors shadow-sm">
117
+ <i data-lucide="rotate-ccw" class="h-4 w-4 mr-2"></i> Reset
118
+ </button>
119
+ </div>
120
+
121
+ <div class="flex flex-wrap items-center gap-6 border-t border-slate-100 pt-3">
122
+ <label class="flex items-center space-x-2 text-sm font-medium text-slate-700 cursor-pointer select-none">
123
+ <input type="checkbox" @bind="filterAssignToMe" class="h-4 w-4 rounded border-slate-300 text-violet-600 focus:ring-violet-500" />
124
+ <span>Assigned to me</span>
125
+ </label>
126
+ <label class="flex items-center space-x-2 text-sm font-medium text-slate-700 cursor-pointer select-none">
127
+ <input type="checkbox" @bind="filterAssignToMyTeam" class="h-4 w-4 rounded border-slate-300 text-violet-600 focus:ring-violet-500" />
128
+ <span>Assigned to my team</span>
129
+ </label>
130
+ </div>
131
+ </div>
132
+ </div>
133
+
134
+ @if (isLoading)
135
+ {
136
+ <LoadingSpinner Message="Loading timesheet data..." />
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">
143
+ <h3 class="text-sm font-semibold text-slate-700">
144
+ Showing <span class="text-violet-600 font-bold">@FilteredIssues.Count()</span> timesheet issue entry(s)
145
+ </h3>
146
+ </div>
147
+ <table class="w-full">
148
+ <thead>
149
+ <tr class="border-b border-slate-100 bg-slate-50/60">
150
+ <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">No.</th>
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>
158
+ </thead>
159
+ <tbody class="divide-y divide-slate-100">
160
+ @if (PagedIssues.Any())
161
+ {
162
+ @foreach (var entry in PagedIssues.Select((issue, index) => (issue, index)))
163
+ {
164
+ var issue = entry.issue;
165
+ <tr class="hover:bg-slate-50/50 transition-colors">
166
+ <td class="px-6 py-4 text-sm font-semibold text-slate-500">@((currentPage - 1) * pageSize + entry.index + 1)</td>
167
+ <td class="px-6 py-4">
168
+ <div>
169
+ <p class="text-sm font-semibold text-slate-900">@issue.Title</p>
170
+ @if (!string.IsNullOrEmpty(issue.Description))
171
+ {
172
+ <p class="text-xs text-slate-400 mt-0.5 truncate max-w-[200px]">@issue.Description</p>
173
+ }
174
+ </div>
175
+ </td>
176
+ <td class="px-6 py-4">
177
+ <div class="text-sm text-slate-800 font-medium">@issue.TaskTitle</div>
178
+ <div class="text-xs text-slate-400 mt-0.5">@issue.ProjectName</div>
179
+ </td>
180
+ <td class="px-6 py-4">
181
+ @if (!string.IsNullOrEmpty(issue.AssignedToName))
182
+ {
183
+ <div class="flex items-center gap-2">
184
+ <span class="flex h-7 w-7 items-center justify-center rounded-full bg-violet-100 text-violet-700 text-xs font-bold">
185
+ @(issue.AssignedToName.Length > 0 ? issue.AssignedToName[0].ToString().ToUpper() : "")
186
+ </span>
187
+ <span class="text-sm text-slate-700">@issue.AssignedToName</span>
188
+ </div>
189
+ }
190
+ else
191
+ {
192
+ <span class="text-xs text-slate-400">Unassigned</span>
193
+ }
194
+ </td>
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>
201
+ <td class="px-6 py-4">
202
+ @{
203
+ var (statusLabel, statusClass) = issue.StatusId switch {
204
+ AppTaskStatus.InProgress => ("In Progress", "bg-blue-50 text-blue-700"),
205
+ AppTaskStatus.Done => ("Completed", "bg-emerald-50 text-emerald-700"),
206
+ _ => ("To Do", "bg-slate-100 text-slate-600")
207
+ };
208
+ }
209
+ <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium @statusClass">@statusLabel</span>
210
+ </td>
211
+ </tr>
212
+ }
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>
220
+
221
+ <!-- Pagination -->
222
+ <Pagination CurrentPage="currentPage"
223
+ TotalPages="TotalPages"
224
+ PageSize="pageSize"
225
+ TotalCount="FilteredIssues.Count()"
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
+
237
+ // Filters
238
+ private string searchInput = "";
239
+ private string appliedSearch = "";
240
+ private long filterProjectId = 0;
241
+ private long appliedProjectId = 0;
242
+ private string filterStatus = "0";
243
+ private string appliedStatus = "0";
244
+ private bool filterAssignToMe = false;
245
+ private bool filterAssignToMyTeam = false;
246
+
247
+ // Current User & Team
248
+ private long currentUserId = 0;
249
+ private List<long> myTeamUserIds = new();
250
+
251
+ // Pagination
252
+ private int currentPage = 1;
253
+ private int pageSize = 10;
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
+
263
+ private IEnumerable<IssueDto> FilteredIssues
264
+ {
265
+ get
266
+ {
267
+ var result = allIssues.AsEnumerable();
268
+
269
+ if (appliedProjectId > 0)
270
+ result = result.Where(i => i.ProjectId == appliedProjectId);
271
+
272
+ if (appliedStatus != "0" && Enum.TryParse<AppTaskStatus>(appliedStatus, out var status))
273
+ result = result.Where(i => i.StatusId == status);
274
+
275
+ if (!string.IsNullOrWhiteSpace(appliedSearch))
276
+ {
277
+ var q = appliedSearch.Trim().ToLower();
278
+ result = result.Where(i =>
279
+ i.Title.ToLower().Contains(q) ||
280
+ (i.Description != null && i.Description.ToLower().Contains(q)) ||
281
+ (i.AssignedToName != null && i.AssignedToName.ToLower().Contains(q)) ||
282
+ i.TaskTitle.ToLower().Contains(q)
283
+ );
284
+ }
285
+
286
+ if (filterAssignToMe || filterAssignToMyTeam)
287
+ {
288
+ result = result.Where(i =>
289
+ (filterAssignToMe && i.AssignedTo == currentUserId) ||
290
+ (filterAssignToMyTeam && i.AssignedTo.HasValue && myTeamUserIds.Contains(i.AssignedTo.Value))
291
+ );
292
+ }
293
+
294
+ return result.OrderBy(i => i.Title);
295
+ }
296
+ }
297
+
298
+ private IEnumerable<IssueDto> PagedIssues => FilteredIssues.Skip((currentPage - 1) * pageSize).Take(pageSize);
299
+
300
+ protected override async Task OnInitializedAsync()
301
+ {
302
+ var client = ApiClient.CreateClient();
303
+ try
304
+ {
305
+ var issuesTask = client.GetFromJsonAsync<Result<List<IssueDto>>>("Report/issues", Serialization.CaseInsensitive);
306
+ var projectsTask = client.GetFromJsonAsync<List<ProjectDto>>("Project", Serialization.CaseInsensitive);
307
+
308
+ // Get current user ID
309
+ var authState = await AuthStateProvider.GetAuthenticationStateAsync();
310
+ var user = authState.User;
311
+ if (user.Identity?.IsAuthenticated == true)
312
+ {
313
+ var idStr = user.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
314
+ if (long.TryParse(idStr, out var parsedId))
315
+ {
316
+ currentUserId = parsedId;
317
+ }
318
+ }
319
+
320
+ // Get team IDs
321
+ try
322
+ {
323
+ var teamTask = client.GetFromJsonAsync<List<long>>("User/my-team-user-ids", Serialization.CaseInsensitive);
324
+ await Task.WhenAll(issuesTask, projectsTask, teamTask);
325
+ myTeamUserIds = teamTask.Result ?? new();
326
+ }
327
+ catch
328
+ {
329
+ await Task.WhenAll(issuesTask, projectsTask);
330
+ }
331
+
332
+ var issuesResult = issuesTask.Result;
333
+ if (issuesResult?.IsSuccess == true && issuesResult.Value != null)
334
+ {
335
+ allIssues = issuesResult.Value;
336
+ }
337
+ allProjects = projectsTask.Result ?? new();
338
+
339
+ ComputeMetrics();
340
+ }
341
+ catch (Exception ex)
342
+ {
343
+ Console.WriteLine($"Error fetching timesheet data: {ex.Message}");
344
+ }
345
+ finally
346
+ {
347
+ isLoading = false;
348
+ }
349
+ }
350
+
351
+ protected override async Task OnAfterRenderAsync(bool firstRender)
352
+ {
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;
369
+ appliedProjectId = filterProjectId;
370
+ appliedStatus = filterStatus;
371
+ currentPage = 1;
372
+ ComputeMetrics();
373
+ return Task.CompletedTask;
374
+ }
375
+
376
+ private Task ResetFilters()
377
+ {
378
+ searchInput = "";
379
+ appliedSearch = "";
380
+ filterProjectId = 0;
381
+ appliedProjectId = 0;
382
+ filterStatus = "0";
383
+ appliedStatus = "0";
384
+ filterAssignToMe = false;
385
+ filterAssignToMyTeam = false;
386
+ currentPage = 1;
387
+ ComputeMetrics();
388
+ return Task.CompletedTask;
389
+ }
390
+
391
+ private Task HandlePageChanged(int page)
392
+ {
393
+ currentPage = page;
394
+ return Task.CompletedTask;
395
+ }
396
+
397
+ private Task HandlePageSizeChanged(int size)
398
+ {
399
+ pageSize = size;
400
+ currentPage = 1;
401
+ return Task.CompletedTask;
402
+ }
403
+
404
+ private async Task DownloadExcel()
405
+ {
406
+ var rows = FilteredIssues.Select(issue =>
407
+ {
408
+ var statusLabel = issue.StatusId switch {
409
+ AppTaskStatus.InProgress => "In Progress",
410
+ AppTaskStatus.Done => "Completed",
411
+ _ => "To Do"
412
+ };
413
+
414
+ return new object?[]
415
+ {
416
+ issue.Title,
417
+ issue.TaskTitle,
418
+ issue.ProjectName,
419
+ issue.AssignedToName ?? "Unassigned",
420
+ issue.EstimatedHours ?? 0,
421
+ issue.DueDate,
422
+ statusLabel
423
+ };
424
+ });
425
+
426
+ var workbookBytes = SpreadsheetReportBuilder.BuildTableWorkbook(
427
+ "Time Effort Tracking",
428
+ "Timesheet Effort Report",
429
+ new[] { "Issue", "Task", "Project", "Assignee", "Estimated Effort (Hours)", "Due Date", "Status" },
430
+ rows,
431
+ new[]
432
+ {
433
+ $"Search: {(string.IsNullOrWhiteSpace(appliedSearch) ? "All" : appliedSearch)}",
434
+ $"Project: {(appliedProjectId > 0 ? allProjects.FirstOrDefault(p => p.Id == appliedProjectId)?.Name ?? "All" : "All")}",
435
+ $"Status: {(appliedStatus != "0" ? appliedStatus : "All")}",
436
+ $"Assigned To Me: {(filterAssignToMe ? "Yes" : "No")}",
437
+ $"Assigned To My Team: {(filterAssignToMyTeam ? "Yes" : "No")}",
438
+ $"Generated: {DisplayFormats.Date(DateTime.Today)}"
439
+ });
440
+
441
+ await JS.InvokeVoidAsync(
442
+ "downloadFileFromBase64",
443
+ $"TimesheetEffortReport_{DateTime.Today:yyyyMMdd}.xlsx",
444
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
445
+ Convert.ToBase64String(workbookBytes));
446
+ }
447
+
448
+ private async Task DownloadPdf()
449
+ {
450
+ var summaryLines = new List<string>
451
+ {
452
+ $"Search: {(string.IsNullOrWhiteSpace(appliedSearch) ? "All" : appliedSearch)}",
453
+ $"Project: {(appliedProjectId > 0 ? allProjects.FirstOrDefault(p => p.Id == appliedProjectId)?.Name ?? "All" : "All")}",
454
+ $"Status: {(appliedStatus != "0" ? appliedStatus : "All")}",
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
+
462
+ var rows = FilteredIssues.Select(issue =>
463
+ {
464
+ var statusLabel = issue.StatusId switch {
465
+ AppTaskStatus.InProgress => "In Progress",
466
+ AppTaskStatus.Done => "Completed",
467
+ _ => "To Do"
468
+ };
469
+
470
+ return new[]
471
+ {
472
+ issue.Title,
473
+ issue.TaskTitle,
474
+ issue.ProjectName,
475
+ issue.AssignedToName ?? "Unassigned",
476
+ (issue.EstimatedHours ?? 0).ToString("N1") + " hrs",
477
+ DisplayFormats.Date(issue.DueDate),
478
+ statusLabel
479
+ };
480
+ });
481
+
482
+ var pdfBytes = SimplePdfReportBuilder.BuildTableReport(
483
+ "Timesheet Effort Tracking Report",
484
+ summaryLines,
485
+ new[] { "Issue", "Task", "Project", "Assignee", "Est. Effort", "Due Date", "Status" },
486
+ rows);
487
+
488
+ await JS.InvokeVoidAsync(
489
+ "downloadFileFromBase64",
490
+ $"TimesheetEffortReport_{DateTime.Today:yyyyMMdd}.pdf",
491
+ "application/pdf",
492
+ Convert.ToBase64String(pdfBytes));
493
+ }
494
+ }
TaskTrackingSystem.WebApp/Components/Pages/{Roles.razor → Features/Roles/Roles.razor} RENAMED
File without changes
TaskTrackingSystem.WebApp/Components/Pages/{KanbanBoard.razor → Features/Tasks/KanbanBoard.razor} RENAMED
File without changes
TaskTrackingSystem.WebApp/Components/Pages/{TaskAssign.razor → Features/Tasks/TaskAssign.razor} RENAMED
File without changes
TaskTrackingSystem.WebApp/Components/Pages/{TaskDetails.razor → Features/Tasks/TaskDetails.razor} RENAMED
File without changes
TaskTrackingSystem.WebApp/Components/Pages/{Tasks.razor → Features/Tasks/Tasks.razor} RENAMED
@@ -82,11 +82,21 @@
82
 
83
  <!-- Tasks Table (List View) -->
84
  <div class="rounded-xl border border-slate-200 bg-white shadow-sm overflow-hidden mt-4">
85
- <table class="w-full">
 
 
 
 
 
 
 
 
 
 
86
  <thead>
87
  <tr class="border-b border-slate-100 bg-slate-50/60">
88
- <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">No.</th>
89
- <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Title</th>
90
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Status</th>
91
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Priority</th>
92
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Due Date</th>
@@ -102,30 +112,32 @@
102
  {
103
  var task = entry.task;
104
  <tr class="hover:bg-slate-50/50 transition-colors">
105
- <td class="px-6 py-4 text-sm font-semibold text-slate-500">@((currentPage - 1) * pageSize + entry.index + 1)</td>
106
- <td class="px-6 py-4">
 
 
107
  <a href="/tasks/@task.Id/details" class="text-sm font-semibold text-slate-900 hover:text-violet-600 transition-colors">@task.Title</a>
108
  @if (!string.IsNullOrEmpty(task.Description))
109
  {
110
- <p class="text-xs text-slate-400 mt-0.5 truncate max-w-xs">@task.Description</p>
111
  }
112
  </td>
113
- <td class="px-6 py-4">
114
  <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium @GetStatusBadge(task.StatusId)">
115
  @GetStatusLabel(task.StatusId)
116
  </span>
117
  </td>
118
- <td class="px-6 py-4">
119
  <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium @GetPriorityBadge(task.PriorityId)">
120
  @GetPriorityLabel(task.PriorityId)
121
  </span>
122
  </td>
123
- <td class="px-6 py-4 text-sm text-slate-600">@DisplayFormats.Date(task.DueDate)</td>
124
- <td class="px-6 py-4 text-sm text-slate-500">
125
  @{ var proj = projects.FirstOrDefault(p => p.Id == task.ProjectId); }
126
  @(proj?.Name ?? $"#{task.ProjectId}")
127
  </td>
128
- <td class="px-6 py-4 text-sm text-slate-500 font-medium">
129
  @{ var user = allUsers.FirstOrDefault(u => u.Id == task.AssignedTo); }
130
  @if (user != null)
131
  {
@@ -162,7 +174,7 @@
162
  }
163
  else
164
  {
165
- <EmptyState ColSpan="7" Icon="check-square" Title="No tasks yet" Subtitle="Create your first task to get started." />
166
  }
167
  </tbody>
168
  </table>
@@ -967,6 +979,8 @@
967
  _ => "Unknown"
968
  };
969
 
 
 
970
  private string GetPriorityBadge(TaskPriority priorityId) => priorityId switch
971
  {
972
  TaskPriority.Low => "bg-slate-100 text-slate-600",
 
82
 
83
  <!-- Tasks Table (List View) -->
84
  <div class="rounded-xl border border-slate-200 bg-white shadow-sm overflow-hidden mt-4">
85
+ <table class="w-full table-fixed">
86
+ <colgroup>
87
+ <col class="w-[12%]" />
88
+ <col class="w-[22%]" />
89
+ <col class="w-[11%]" />
90
+ <col class="w-[11%]" />
91
+ <col class="w-[14%]" />
92
+ <col class="w-[16%]" />
93
+ <col class="w-[14%]" />
94
+ <col class="w-[10%]" />
95
+ </colgroup>
96
  <thead>
97
  <tr class="border-b border-slate-100 bg-slate-50/60">
98
+ <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Task Code</th>
99
+ <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Task</th>
100
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Status</th>
101
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Priority</th>
102
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Due Date</th>
 
112
  {
113
  var task = entry.task;
114
  <tr class="hover:bg-slate-50/50 transition-colors">
115
+ <td class="px-6 py-4 text-left">
116
+ <span class="font-mono text-[11px] font-semibold text-indigo-700">@GetTaskCode(task.Id)</span>
117
+ </td>
118
+ <td class="px-6 py-4 text-left">
119
  <a href="/tasks/@task.Id/details" class="text-sm font-semibold text-slate-900 hover:text-violet-600 transition-colors">@task.Title</a>
120
  @if (!string.IsNullOrEmpty(task.Description))
121
  {
122
+ <p class="mt-0.5 text-xs text-slate-400 truncate max-w-full">@task.Description</p>
123
  }
124
  </td>
125
+ <td class="px-6 py-4 text-left">
126
  <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium @GetStatusBadge(task.StatusId)">
127
  @GetStatusLabel(task.StatusId)
128
  </span>
129
  </td>
130
+ <td class="px-6 py-4 text-left">
131
  <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium @GetPriorityBadge(task.PriorityId)">
132
  @GetPriorityLabel(task.PriorityId)
133
  </span>
134
  </td>
135
+ <td class="px-6 py-4 text-sm text-slate-600 text-left whitespace-nowrap">@DisplayFormats.Date(task.DueDate)</td>
136
+ <td class="px-6 py-4 text-sm text-slate-500 text-left break-words">
137
  @{ var proj = projects.FirstOrDefault(p => p.Id == task.ProjectId); }
138
  @(proj?.Name ?? $"#{task.ProjectId}")
139
  </td>
140
+ <td class="px-6 py-4 text-sm text-slate-500 font-medium text-left">
141
  @{ var user = allUsers.FirstOrDefault(u => u.Id == task.AssignedTo); }
142
  @if (user != null)
143
  {
 
174
  }
175
  else
176
  {
177
+ <EmptyState ColSpan="8" Icon="check-square" Title="No tasks yet" Subtitle="Create your first task to get started." />
178
  }
179
  </tbody>
180
  </table>
 
979
  _ => "Unknown"
980
  };
981
 
982
+ private static string GetTaskCode(long taskId) => $"TSK-{taskId:D4}";
983
+
984
  private string GetPriorityBadge(TaskPriority priorityId) => priorityId switch
985
  {
986
  TaskPriority.Low => "bg-slate-100 text-slate-600",
TaskTrackingSystem.WebApp/Components/Pages/{Users.razor → Features/Users/Users.razor} RENAMED
File without changes