diff --git a/TaskTrackingSystem.Database/AppDbContextModels/AppDbContext.cs b/TaskTrackingSystem.Database/AppDbContextModels/AppDbContext.cs index ec3648c1232075a380634bb2808d6d09e39bfdad..2aab829f1c4435b64b211a215ebef14804f4afb6 100644 --- a/TaskTrackingSystem.Database/AppDbContextModels/AppDbContext.cs +++ b/TaskTrackingSystem.Database/AppDbContextModels/AppDbContext.cs @@ -49,7 +49,6 @@ public partial class AppDbContext : DbContext { if (!optionsBuilder.IsConfigured) { -#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. optionsBuilder.UseSqlServer("Server=.;Database=TTS;Trusted_Connection=True;TrustServerCertificate=True;Connect Timeout=5;"); } } diff --git a/TaskTrackingSystem.Shared/Models/Report/OverdueCriticalTaskDto.cs b/TaskTrackingSystem.Shared/Models/Report/OverdueCriticalTaskDto.cs index e378ee7e1a6d0ad7d8852f95f8d96fde705e48dc..883c002a3da8c71e6aece783b1d82a201de86ab7 100644 --- a/TaskTrackingSystem.Shared/Models/Report/OverdueCriticalTaskDto.cs +++ b/TaskTrackingSystem.Shared/Models/Report/OverdueCriticalTaskDto.cs @@ -11,6 +11,7 @@ namespace TaskTrackingSystem.Shared.Models.Report public string StatusName { get; set; } = string.Empty; public string PriorityName { get; set; } = string.Empty; public string? AssignedTo { get; set; } + public long? AssignedToUserId { get; set; } public DateTime DueDate { get; set; } public int DaysOverdue { get; set; } public DateTime CreatedAt { get; set; } diff --git a/TaskTrackingSystem.WebApi/Features/Audit/AuditLogController.cs b/TaskTrackingSystem.WebApi/Features/Audit/AuditLogController.cs index 3acc2b544963df6e55fa896c4385834cec90c3fb..71ae87df2efc2f3219f1c9e727d160bbffda401d 100644 --- a/TaskTrackingSystem.WebApi/Features/Audit/AuditLogController.cs +++ b/TaskTrackingSystem.WebApi/Features/Audit/AuditLogController.cs @@ -14,14 +14,16 @@ namespace TaskTrackingSystem.WebApi.Features.Audit { [Route("api/[controller]")] [ApiController] - [Authorize(Roles = "Admin")] + [Authorize] public class AuditLogController : ControllerBase { private readonly AppDbContext _db; + private readonly PermissionAuthorizationService _permissionAuthorizationService; - public AuditLogController(AppDbContext db) + public AuditLogController(AppDbContext db, PermissionAuthorizationService permissionAuthorizationService) { _db = db; + _permissionAuthorizationService = permissionAuthorizationService; } [HttpGet] @@ -31,6 +33,11 @@ namespace TaskTrackingSystem.WebApi.Features.Audit [FromQuery] string? search, [FromQuery] PaginationQuery? paging = null) { + if (!await _permissionAuthorizationService.CanAccessAsync(User, "api/Role", "List")) + { + return Forbid(); + } + var query = _db.AuditLogs .Include(a => a.User) .AsQueryable(); diff --git a/TaskTrackingSystem.WebApi/Features/Dashboard/DashboardController.cs b/TaskTrackingSystem.WebApi/Features/Dashboard/DashboardController.cs index 5246fab72ab1d61c276212270c69fc8ea9660f9d..6a4e4a1dad80acb7f2d077fd17567eba785ac599 100644 --- a/TaskTrackingSystem.WebApi/Features/Dashboard/DashboardController.cs +++ b/TaskTrackingSystem.WebApi/Features/Dashboard/DashboardController.cs @@ -23,21 +23,21 @@ namespace TaskTrackingSystem.WebApi.Features.Dashboard [HttpGet("summary")] public async Task>> GetSummary() { - var result = await _dashboardService.GetSummaryAsync(User.GetRoleName(), User.GetUserId()); + var result = await _dashboardService.GetSummaryAsync(User.GetRoleId(), User.GetUserId()); return StatusCode(result.StatusCode, result); } [HttpGet("tasks-overview")] public async Task>>> GetTasksOverview() { - var result = await _dashboardService.GetTasksOverviewAsync(User.GetRoleName(), User.GetUserId()); + var result = await _dashboardService.GetTasksOverviewAsync(User.GetRoleId(), User.GetUserId()); return StatusCode(result.StatusCode, result); } [HttpGet("project-progress")] public async Task>>> GetProjectProgress() { - var result = await _dashboardService.GetProjectProgressAsync(User.GetRoleName(), User.GetUserId()); + var result = await _dashboardService.GetProjectProgressAsync(User.GetRoleId(), User.GetUserId()); return StatusCode(result.StatusCode, result); } } diff --git a/TaskTrackingSystem.WebApi/Features/Dashboard/DashboardService.cs b/TaskTrackingSystem.WebApi/Features/Dashboard/DashboardService.cs index d81a4121ddf8cf35b13bd4c920a6d06f6ca5f2e7..7b3d08b065a23cb6f08e2747fde416615226c561 100644 --- a/TaskTrackingSystem.WebApi/Features/Dashboard/DashboardService.cs +++ b/TaskTrackingSystem.WebApi/Features/Dashboard/DashboardService.cs @@ -7,6 +7,7 @@ using TaskTrackingSystem.Database.AppDbContextModels; using TaskTrackingSystem.Shared; using TaskTrackingSystem.Shared.Enums; using TaskTrackingSystem.Shared.Models.Dashboard; +using TaskTrackingSystem.WebApi.Infrastructure; namespace TaskTrackingSystem.WebApi.Features.Dashboard { @@ -19,12 +20,15 @@ namespace TaskTrackingSystem.WebApi.Features.Dashboard _db = db; } - public async Task> GetSummaryAsync(string roleName, long currentUserId) + public async Task> GetSummaryAsync(long roleId, long currentUserId) { - var projects = BuildAccessibleProjectQuery(roleName, currentUserId); - var tasks = BuildAccessibleTaskQuery(roleName, currentUserId); + var isAdmin = await DataScopeAuthorization.IsAdminScopeAsync(_db, roleId); + var isManager = await DataScopeAuthorization.IsManagerScopeAsync(_db, roleId); - var totalUsers = IsAdmin(roleName) + var projects = BuildAccessibleProjectQuery(isAdmin, currentUserId); + var tasks = BuildAccessibleTaskQuery(isAdmin, isManager, currentUserId); + + var totalUsers = isAdmin ? await _db.Users.CountAsync(u => !u.IsDeleted) : await _db.Users .Where(u => !u.IsDeleted && @@ -46,9 +50,12 @@ namespace TaskTrackingSystem.WebApi.Features.Dashboard return Result.Success(summary); } - public async Task>> GetTasksOverviewAsync(string roleName, long currentUserId) + public async Task>> GetTasksOverviewAsync(long roleId, long currentUserId) { - var groupedTasks = await BuildAccessibleTaskQuery(roleName, currentUserId) + var isAdmin = await DataScopeAuthorization.IsAdminScopeAsync(_db, roleId); + var isManager = await DataScopeAuthorization.IsManagerScopeAsync(_db, roleId); + + var groupedTasks = await BuildAccessibleTaskQuery(isAdmin, isManager, currentUserId) .GroupBy(t => t.StatusId) .Select(g => new { @@ -87,9 +94,10 @@ namespace TaskTrackingSystem.WebApi.Features.Dashboard return Result>.Success(overview.OrderBy(o => o.StatusId)); } - public async Task>> GetProjectProgressAsync(string roleName, long currentUserId) + public async Task>> GetProjectProgressAsync(long roleId, long currentUserId) { - var activeProjects = await BuildAccessibleProjectQuery(roleName, currentUserId).ToListAsync(); + var isAdmin = await DataScopeAuthorization.IsAdminScopeAsync(_db, roleId); + var activeProjects = await BuildAccessibleProjectQuery(isAdmin, currentUserId).ToListAsync(); var progressList = new List(); foreach (var project in activeProjects) @@ -115,16 +123,16 @@ namespace TaskTrackingSystem.WebApi.Features.Dashboard return Result>.Success(progressList); } - private IQueryable BuildAccessibleTaskQuery(string roleName, long currentUserId) + private IQueryable BuildAccessibleTaskQuery(bool isAdmin, bool isManager, long currentUserId) { var query = _db.Tasks.Where(t => t.IsDeleted != true && !t.IsArchived); - if (IsAdmin(roleName)) + if (isAdmin) { return query; } - if (IsManager(roleName)) + if (isManager) { return query.Where(t => t.AssignedTo == currentUserId || @@ -137,11 +145,11 @@ namespace TaskTrackingSystem.WebApi.Features.Dashboard t.CreatedBy == currentUserId); } - private IQueryable BuildAccessibleProjectQuery(string roleName, long currentUserId) + private IQueryable BuildAccessibleProjectQuery(bool isAdmin, long currentUserId) { var query = _db.Projects.Where(p => p.IsDeleted != true); - if (IsAdmin(roleName)) + if (isAdmin) { return query; } @@ -150,15 +158,5 @@ namespace TaskTrackingSystem.WebApi.Features.Dashboard p.CreatedById == currentUserId || p.ProjectMembers.Any(pm => pm.UserId == currentUserId)); } - - private static bool IsAdmin(string roleName) - { - return string.Equals(roleName, "Admin", StringComparison.OrdinalIgnoreCase); - } - - private static bool IsManager(string roleName) - { - return string.Equals(roleName, "Manager", StringComparison.OrdinalIgnoreCase); - } } } diff --git a/TaskTrackingSystem.WebApi/Features/Issue/IssueController.cs b/TaskTrackingSystem.WebApi/Features/Issue/IssueController.cs index 9821033f5a5a941323e4f76f323bbdb1b815f808..f2d65643ee91dfdedf1611f22aba25955d518ca9 100644 --- a/TaskTrackingSystem.WebApi/Features/Issue/IssueController.cs +++ b/TaskTrackingSystem.WebApi/Features/Issue/IssueController.cs @@ -37,14 +37,14 @@ namespace TaskTrackingSystem.WebApi.Features.Issue { if (paging == null || (!paging.Page.HasValue && !paging.Limit.HasValue)) { - var issues = await _issueService.GetAllIssuesAsync(User.GetRoleName(), User.GetUserId()); + var issues = await _issueService.GetAllIssuesAsync(User.GetRoleId(), User.GetUserId()); return Ok(issues); } var page = PaginationExtensions.NormalizePage(paging.Page); var limit = PaginationExtensions.NormalizePageSize(paging.Limit ?? 0); var paged = await _issueService.GetPagedIssuesAsync( - User.GetRoleName(), + User.GetRoleId(), User.GetUserId(), search, taskId, @@ -61,7 +61,7 @@ namespace TaskTrackingSystem.WebApi.Features.Issue [HttpGet("{id}")] public async Task> GetIssue(long id) { - var issue = await _issueService.GetIssueByIdAsync(id, User.GetRoleName(), User.GetUserId()); + var issue = await _issueService.GetIssueByIdAsync(id, User.GetRoleId(), User.GetUserId()); if (issue == null) { return NotFound(new { message = $"Issue with ID {id} not found." }); @@ -73,7 +73,7 @@ namespace TaskTrackingSystem.WebApi.Features.Issue [HttpGet("task/{taskId}")] public async Task>> GetIssuesByTask(long taskId) { - var issues = await _issueService.GetIssuesByTaskIdAsync(taskId, User.GetRoleName(), User.GetUserId()); + var issues = await _issueService.GetIssuesByTaskIdAsync(taskId, User.GetRoleId(), User.GetUserId()); return Ok(issues); } @@ -85,7 +85,7 @@ namespace TaskTrackingSystem.WebApi.Features.Issue return Forbid(); } - var result = await _issueService.CreateIssueAsync(dto, User.GetRoleName(), User.GetUserId()); + var result = await _issueService.CreateIssueAsync(dto, User.GetRoleId(), User.GetUserId()); return StatusCode(result.StatusCode, result); } @@ -97,7 +97,7 @@ namespace TaskTrackingSystem.WebApi.Features.Issue return Forbid(); } - var result = await _issueService.UpdateIssueAsync(id, dto, User.GetRoleName(), User.GetUserId()); + var result = await _issueService.UpdateIssueAsync(id, dto, User.GetRoleId(), User.GetUserId()); return StatusCode(result.StatusCode, result); } @@ -109,7 +109,7 @@ namespace TaskTrackingSystem.WebApi.Features.Issue return Forbid(); } - var result = await _issueService.SoftDeleteIssueAsync(id, User.GetRoleName(), User.GetUserId()); + var result = await _issueService.SoftDeleteIssueAsync(id, User.GetRoleId(), User.GetUserId()); return StatusCode(result.StatusCode, result); } } diff --git a/TaskTrackingSystem.WebApi/Features/Issue/IssueService.cs b/TaskTrackingSystem.WebApi/Features/Issue/IssueService.cs index 7130728cfa99653fdf4f91201c061d5310521a49..b7bd2d5b4c877de6d5e87d64df957c6bc35d5b81 100644 --- a/TaskTrackingSystem.WebApi/Features/Issue/IssueService.cs +++ b/TaskTrackingSystem.WebApi/Features/Issue/IssueService.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using TaskTrackingSystem.Database; using TaskTrackingSystem.Database.AppDbContextModels; using TaskTrackingSystem.Shared; using TaskTrackingSystem.Shared.Enums; @@ -22,16 +21,18 @@ namespace TaskTrackingSystem.WebApi.Features.Issue _db = db; } - public async Task> GetAllIssuesAsync(string roleName, long currentUserId) + public async Task> GetAllIssuesAsync(long roleId, long currentUserId) { - return await BuildAccessibleIssueQuery(roleName, currentUserId) + var isAdmin = await DataScopeAuthorization.IsAdminScopeAsync(_db, roleId); + var isManager = await DataScopeAuthorization.IsManagerScopeAsync(_db, roleId); + return await BuildAccessibleIssueQuery(isAdmin, isManager, currentUserId) .OrderByDescending(i => i.CreatedAt ?? DateTime.UtcNow) .Select(ToDtoProjection()) .ToListAsync(); } public async Task> GetPagedIssuesAsync( - string roleName, + long roleId, long currentUserId, string? search, long? taskId, @@ -42,7 +43,9 @@ namespace TaskTrackingSystem.WebApi.Features.Issue int page, int pageSize) { - var query = BuildAccessibleIssueQuery(roleName, currentUserId); + var isAdmin = await DataScopeAuthorization.IsAdminScopeAsync(_db, roleId); + var isManager = await DataScopeAuthorization.IsManagerScopeAsync(_db, roleId); + var query = BuildAccessibleIssueQuery(isAdmin, isManager, currentUserId); if (assignedOnly) { @@ -83,9 +86,13 @@ namespace TaskTrackingSystem.WebApi.Features.Issue .ToPagedResultAsync(page, pageSize); } - public async Task GetIssueByIdAsync(long id, string roleName, long currentUserId) + + + public async Task GetIssueByIdAsync(long id, long roleId, long currentUserId) { - var issue = await BuildAccessibleIssueQuery(roleName, currentUserId) + var isAdmin = await DataScopeAuthorization.IsAdminScopeAsync(_db, roleId); + var isManager = await DataScopeAuthorization.IsManagerScopeAsync(_db, roleId); + var issue = await BuildAccessibleIssueQuery(isAdmin, isManager, currentUserId) .FirstOrDefaultAsync(i => i.Id == id); if (issue == null) @@ -93,27 +100,29 @@ namespace TaskTrackingSystem.WebApi.Features.Issue return null; } - return await BuildAccessibleIssueQuery(roleName, currentUserId) + return await BuildAccessibleIssueQuery(isAdmin, isManager, currentUserId) .Where(i => i.Id == id) .Select(ToDtoProjection()) .FirstOrDefaultAsync(); } - public async Task> GetIssuesByTaskIdAsync(long taskId, string roleName, long currentUserId) + public async Task> GetIssuesByTaskIdAsync(long taskId, long roleId, long currentUserId) { - if (!await CanAccessTaskAsync(taskId, roleName, currentUserId)) + var isAdmin = await DataScopeAuthorization.IsAdminScopeAsync(_db, roleId); + if (!await CanAccessTaskAsync(taskId, isAdmin, currentUserId)) { return Array.Empty(); } - return await BuildAccessibleIssueQuery(roleName, currentUserId) + var isManager = await DataScopeAuthorization.IsManagerScopeAsync(_db, roleId); + return await BuildAccessibleIssueQuery(isAdmin, isManager, currentUserId) .Where(i => i.TaskId == taskId) .OrderByDescending(i => i.CreatedAt ?? DateTime.UtcNow) .Select(ToDtoProjection()) .ToListAsync(); } - public async Task> CreateIssueAsync(CreateIssueDto dto, string roleName, long currentUserId) + public async Task> CreateIssueAsync(CreateIssueDto dto, long roleId, long currentUserId) { if (string.IsNullOrWhiteSpace(dto.Title)) { @@ -129,7 +138,8 @@ namespace TaskTrackingSystem.WebApi.Features.Issue return Result.Failure($"Task with ID {dto.TaskId} not found.", 404); } - if (!await CanAccessTaskAsync(task.Id, roleName, currentUserId)) + var isAdmin = await DataScopeAuthorization.IsAdminScopeAsync(_db, roleId); + if (!await CanAccessTaskAsync(task.Id, isAdmin, currentUserId)) { return Result.Failure("You do not have access to this task.", 403); } @@ -159,11 +169,11 @@ namespace TaskTrackingSystem.WebApi.Features.Issue _db.Issues.Add(issue); await _db.SaveChangesAsync(); - var result = await GetIssueByIdAsync(issue.Id, roleName, currentUserId); + var result = await GetIssueByIdAsync(issue.Id, roleId, currentUserId); return Result.Success(result!, 201); } - public async Task UpdateIssueAsync(long id, UpdateIssueDto dto, string roleName, long currentUserId) + public async Task UpdateIssueAsync(long id, UpdateIssueDto dto, long roleId, long currentUserId) { if (string.IsNullOrWhiteSpace(dto.Title)) { @@ -180,7 +190,8 @@ namespace TaskTrackingSystem.WebApi.Features.Issue return Result.Failure($"Issue with ID {id} not found.", 404); } - if (!await CanAccessTaskAsync(issue.TaskId, roleName, currentUserId)) + var isAdmin = await DataScopeAuthorization.IsAdminScopeAsync(_db, roleId); + if (!await CanAccessTaskAsync(issue.TaskId, isAdmin, currentUserId)) { return Result.Failure("You do not have access to this task.", 403); } @@ -207,7 +218,7 @@ namespace TaskTrackingSystem.WebApi.Features.Issue return Result.Success(200); } - public async Task SoftDeleteIssueAsync(long id, string roleName, long currentUserId) + public async Task SoftDeleteIssueAsync(long id, long roleId, long currentUserId) { var issue = await _db.Issues .Include(i => i.Task) @@ -219,7 +230,8 @@ namespace TaskTrackingSystem.WebApi.Features.Issue return Result.Failure($"Issue with ID {id} not found.", 404); } - if (!await CanAccessTaskAsync(issue.TaskId, roleName, currentUserId)) + var isAdmin = await DataScopeAuthorization.IsAdminScopeAsync(_db, roleId); + if (!await CanAccessTaskAsync(issue.TaskId, isAdmin, currentUserId)) { return Result.Failure("You do not have access to this task.", 403); } @@ -233,17 +245,17 @@ namespace TaskTrackingSystem.WebApi.Features.Issue return Result.Success(200); } - private IQueryable BuildAccessibleIssueQuery(string roleName, long currentUserId) + private IQueryable BuildAccessibleIssueQuery(bool isAdmin, bool isManager, long currentUserId) { var query = _db.Issues .Where(i => i.IsDeleted != true && i.Task.IsDeleted != true && i.Task.Project.IsDeleted != true); - if (IsAdmin(roleName)) + if (isAdmin) { return query; } - if (IsManager(roleName)) + if (isManager) { return query.Where(i => i.AssignedTo == currentUserId || @@ -256,9 +268,9 @@ namespace TaskTrackingSystem.WebApi.Features.Issue i.CreatedBy == currentUserId); } - private async Task CanAccessTaskAsync(long taskId, string roleName, long currentUserId) + private async Task CanAccessTaskAsync(long taskId, bool isAdmin, long currentUserId) { - if (IsAdmin(roleName)) + if (isAdmin) { return await _db.Tasks.AnyAsync(t => t.Id == taskId && t.IsDeleted != true && !t.IsArchived && t.Project.IsDeleted != true); } @@ -278,16 +290,6 @@ namespace TaskTrackingSystem.WebApi.Features.Issue return await _db.ProjectMembers.AnyAsync(pm => pm.ProjectId == projectId && pm.UserId == userId); } - private static bool IsAdmin(string roleName) - { - return roleName.Equals("Admin", StringComparison.OrdinalIgnoreCase); - } - - private static bool IsManager(string roleName) - { - return roleName.Equals("Manager", StringComparison.OrdinalIgnoreCase); - } - private static System.Linq.Expressions.Expression> ToDtoProjection() { return i => new IssueDto diff --git a/TaskTrackingSystem.WebApi/Features/Menu/MenuController.cs b/TaskTrackingSystem.WebApi/Features/Menu/MenuController.cs index f6ef37d31fc16f845408a58d4eab227dbef172dc..00df521a9ea8a0cb07a3ce561bbd13bbcf9df812 100644 --- a/TaskTrackingSystem.WebApi/Features/Menu/MenuController.cs +++ b/TaskTrackingSystem.WebApi/Features/Menu/MenuController.cs @@ -49,7 +49,7 @@ namespace TaskTrackingSystem.WebApi.Features.Menu { var canCreate = await _permissionAuthorizationService.CanAccessAsync(User, "api/Role", "Create"); var canUpdate = await _permissionAuthorizationService.CanAccessAsync(User, "api/Role", "Update"); - if (!canCreate && !canUpdate && !User.IsInRole("Admin")) + if (!canCreate && !canUpdate) { return Forbid(); } diff --git a/TaskTrackingSystem.WebApi/Features/Notification/NotificationRealtimeService.cs b/TaskTrackingSystem.WebApi/Features/Notification/NotificationRealtimeService.cs index 144ff26fb549b1f7970d22f606051d2f15b7d338..a9590df1c49d18c75e1495ac27c767991f461c31 100644 --- a/TaskTrackingSystem.WebApi/Features/Notification/NotificationRealtimeService.cs +++ b/TaskTrackingSystem.WebApi/Features/Notification/NotificationRealtimeService.cs @@ -12,12 +12,7 @@ public class NotificationRealtimeService _hubContext = hubContext; } - public global::System.Threading.Tasks.Task SendCreatedAsync(long recipientId, NotificationDto notification, int unreadCount) - { - return SendCreatedInternalAsync(recipientId, notification, unreadCount); - } - - private async global::System.Threading.Tasks.Task SendCreatedInternalAsync(long recipientId, NotificationDto notification, int unreadCount) + public async global::System.Threading.Tasks.Task SendCreatedAsync(long recipientId, NotificationDto notification, int unreadCount) { var group = _hubContext.Clients.Group(NotificationHub.GetUserGroupName(recipientId)); await group.SendAsync("NotificationCreated", notification); diff --git a/TaskTrackingSystem.WebApi/Features/Project/ProjectController.cs b/TaskTrackingSystem.WebApi/Features/Project/ProjectController.cs index fa2c9ea723564fe5187539be27847ea1ac25251e..327191af0b300080221fc3489b9bf4e95d5c5c52 100644 --- a/TaskTrackingSystem.WebApi/Features/Project/ProjectController.cs +++ b/TaskTrackingSystem.WebApi/Features/Project/ProjectController.cs @@ -32,20 +32,20 @@ namespace TaskTrackingSystem.WebApi.Features.Project { if (paging == null || (!paging.Page.HasValue && !paging.Limit.HasValue)) { - var projects = await _projectService.GetAllProjectsAsync(User.GetRoleName(), User.GetUserId()); + var projects = await _projectService.GetAllProjectsAsync(User.GetRoleId(), User.GetUserId()); return Ok(projects); } var page = PaginationExtensions.NormalizePage(paging.Page); var limit = PaginationExtensions.NormalizePageSize(paging.Limit ?? 0); - var paged = await _projectService.GetPagedProjectsAsync(User.GetRoleName(), User.GetUserId(), search, page, limit); + var paged = await _projectService.GetPagedProjectsAsync(User.GetRoleId(), User.GetUserId(), search, page, limit); return Ok(paged); } [HttpGet("{id}")] public async Task> GetProject(long id) { - var project = await _projectService.GetProjectByIdAsync(id, User.GetRoleName(), User.GetUserId()); + var project = await _projectService.GetProjectByIdAsync(id, User.GetRoleId(), User.GetUserId()); if (project == null) { return NotFound(new { message = $"Project with ID {id} not found." }); @@ -75,7 +75,7 @@ namespace TaskTrackingSystem.WebApi.Features.Project } var currentUserId = User.GetUserId(); - var result = await _projectService.UpdateProjectAsync(id, updateProjectDto, User.GetRoleName(), currentUserId); + var result = await _projectService.UpdateProjectAsync(id, updateProjectDto, User.GetRoleId(), currentUserId); return StatusCode(result.StatusCode, result); } @@ -87,14 +87,14 @@ namespace TaskTrackingSystem.WebApi.Features.Project return Forbid(); } - var result = await _projectService.SoftDeleteProjectAsync(id, User.GetRoleName(), User.GetUserId()); + var result = await _projectService.SoftDeleteProjectAsync(id, User.GetRoleId(), User.GetUserId()); return StatusCode(result.StatusCode, result); } [HttpGet("{id}/members")] public async Task>>> GetProjectMembers(long id) { - var result = await _projectService.GetProjectMembersAsync(id, User.GetRoleName(), User.GetUserId()); + var result = await _projectService.GetProjectMembersAsync(id, User.GetRoleId(), User.GetUserId()); return StatusCode(result.StatusCode, result); } @@ -106,7 +106,7 @@ namespace TaskTrackingSystem.WebApi.Features.Project return Forbid(); } - var result = await _projectService.AssignMembersToProjectAsync(id, dto, User.GetRoleName(), User.GetUserId()); + var result = await _projectService.AssignMembersToProjectAsync(id, dto, User.GetRoleId(), User.GetUserId()); if (!result.IsSuccess) { return StatusCode(result.StatusCode, new { message = result.ErrorMessage }); @@ -122,7 +122,7 @@ namespace TaskTrackingSystem.WebApi.Features.Project return Forbid(); } - var result = await _projectService.RemoveMemberFromProjectAsync(id, userId, User.GetRoleName(), User.GetUserId()); + var result = await _projectService.RemoveMemberFromProjectAsync(id, userId, User.GetRoleId(), User.GetUserId()); if (!result.IsSuccess) { return StatusCode(result.StatusCode, new { message = result.ErrorMessage }); @@ -133,7 +133,7 @@ namespace TaskTrackingSystem.WebApi.Features.Project [HttpGet("{id}/tasks")] public async Task>>> GetProjectTasks(long id) { - var result = await _projectService.GetProjectTasksAsync(id, User.GetRoleName(), User.GetUserId()); + var result = await _projectService.GetProjectTasksAsync(id, User.GetRoleId(), User.GetUserId()); return StatusCode(result.StatusCode, result); } } diff --git a/TaskTrackingSystem.WebApi/Features/Project/ProjectService.cs b/TaskTrackingSystem.WebApi/Features/Project/ProjectService.cs index 5734e37d35102b93022061550d80fcba73a5c472..83f5aa21c08ed14884fa40b1f2bed1b0d617d78e 100644 --- a/TaskTrackingSystem.WebApi/Features/Project/ProjectService.cs +++ b/TaskTrackingSystem.WebApi/Features/Project/ProjectService.cs @@ -31,9 +31,10 @@ namespace TaskTrackingSystem.WebApi.Features.Project : null; } - public async Task> GetAllProjectsAsync(string roleName, long currentUserId) + public async Task> GetAllProjectsAsync(long roleId, long currentUserId) { - return await BuildAccessibleProjectQuery(roleName, currentUserId) + var isAdmin = await DataScopeAuthorization.IsAdminScopeAsync(_db, roleId); + return await BuildAccessibleProjectQuery(isAdmin, currentUserId) .OrderBy(p => p.EndDate) .ThenByDescending(p => p.CreatedAt ?? DateTime.UtcNow) .Select(p => new ProjectDto @@ -49,9 +50,10 @@ namespace TaskTrackingSystem.WebApi.Features.Project }).ToListAsync(); } - public async Task> GetPagedProjectsAsync(string roleName, long currentUserId, string? search, int page, int pageSize) + public async Task> GetPagedProjectsAsync(long roleId, long currentUserId, string? search, int page, int pageSize) { - var query = BuildAccessibleProjectQuery(roleName, currentUserId); + var isAdmin = await DataScopeAuthorization.IsAdminScopeAsync(_db, roleId); + var query = BuildAccessibleProjectQuery(isAdmin, currentUserId); if (!string.IsNullOrWhiteSpace(search)) { @@ -78,9 +80,10 @@ namespace TaskTrackingSystem.WebApi.Features.Project .ToPagedResultAsync(page, pageSize); } - public async Task GetProjectByIdAsync(long id, string roleName, long currentUserId) + public async Task GetProjectByIdAsync(long id, long roleId, long currentUserId) { - var project = await BuildAccessibleProjectQuery(roleName, currentUserId) + var isAdmin = await DataScopeAuthorization.IsAdminScopeAsync(_db, roleId); + var project = await BuildAccessibleProjectQuery(isAdmin, currentUserId) .FirstOrDefaultAsync(p => p.Id == id); if (project == null) @@ -139,7 +142,7 @@ namespace TaskTrackingSystem.WebApi.Features.Project return Result.Success(resultDto, 201); } - public async Task UpdateProjectAsync(long id, UpdateProjectDto dto, string roleName, long currentUserId) + public async Task UpdateProjectAsync(long id, UpdateProjectDto dto, long roleId, long currentUserId) { if (string.IsNullOrWhiteSpace(dto.Name)) { @@ -152,7 +155,8 @@ namespace TaskTrackingSystem.WebApi.Features.Project return Result.Failure(ResultMessages.ProjectNotFound(id), 404); } - if (!await IsProjectAccessibleAsync(id, roleName, currentUserId)) + var isAdmin = await DataScopeAuthorization.IsAdminScopeAsync(_db, roleId); + if (!await IsProjectAccessibleAsync(id, isAdmin, currentUserId)) { return Result.Failure("You do not have access to this project.", 403); } @@ -170,7 +174,7 @@ namespace TaskTrackingSystem.WebApi.Features.Project return Result.Success(200); } - public async Task SoftDeleteProjectAsync(long id, string roleName, long currentUserId) + public async Task SoftDeleteProjectAsync(long id, long roleId, long currentUserId) { var project = await _db.Projects.FirstOrDefaultAsync(p => p.Id == id && p.IsDeleted != true); if (project == null) @@ -178,7 +182,8 @@ namespace TaskTrackingSystem.WebApi.Features.Project return Result.Failure(ResultMessages.ProjectNotFound(id), 404); } - if (!await IsProjectAccessibleAsync(id, roleName, currentUserId)) + var isAdmin = await DataScopeAuthorization.IsAdminScopeAsync(_db, roleId); + if (!await IsProjectAccessibleAsync(id, isAdmin, currentUserId)) { return Result.Failure("You do not have access to this project.", 403); } @@ -205,9 +210,10 @@ namespace TaskTrackingSystem.WebApi.Features.Project return Result.Success(200); } - public async Task>> GetProjectMembersAsync(long projectId, string roleName, long currentUserId) + public async Task>> GetProjectMembersAsync(long projectId, long roleId, long currentUserId) { - if (!await IsProjectAccessibleAsync(projectId, roleName, currentUserId)) + var isAdmin = await DataScopeAuthorization.IsAdminScopeAsync(_db, roleId); + if (!await IsProjectAccessibleAsync(projectId, isAdmin, currentUserId)) { return Result>.Failure("You do not have access to this project.", 403); } @@ -234,7 +240,7 @@ namespace TaskTrackingSystem.WebApi.Features.Project return Result>.Success(members); } - public async Task AssignMembersToProjectAsync(long projectId, AssignMembersDto dto, string roleName, long currentUserId) + public async Task AssignMembersToProjectAsync(long projectId, AssignMembersDto dto, long roleId, long currentUserId) { var projectExists = await _db.Projects.AnyAsync(p => p.Id == projectId && p.IsDeleted != true); if (!projectExists) @@ -242,7 +248,8 @@ namespace TaskTrackingSystem.WebApi.Features.Project return Result.Failure(ResultMessages.ProjectNotFound(projectId), 404); } - if (!await IsProjectAccessibleAsync(projectId, roleName, currentUserId)) + var isAdmin = await DataScopeAuthorization.IsAdminScopeAsync(_db, roleId); + if (!await IsProjectAccessibleAsync(projectId, isAdmin, currentUserId)) { return Result.Failure("You do not have access to this project.", 403); } @@ -301,7 +308,7 @@ namespace TaskTrackingSystem.WebApi.Features.Project return Result.Success(200); } - public async Task RemoveMemberFromProjectAsync(long projectId, long userId, string roleName, long currentUserId) + public async Task RemoveMemberFromProjectAsync(long projectId, long userId, long roleId, long currentUserId) { var projectExists = await _db.Projects.AnyAsync(p => p.Id == projectId && p.IsDeleted != true); if (!projectExists) @@ -309,7 +316,8 @@ namespace TaskTrackingSystem.WebApi.Features.Project return Result.Failure(ResultMessages.ProjectNotFound(projectId), 404); } - if (!await IsProjectAccessibleAsync(projectId, roleName, currentUserId)) + var isAdmin = await DataScopeAuthorization.IsAdminScopeAsync(_db, roleId); + if (!await IsProjectAccessibleAsync(projectId, isAdmin, currentUserId)) { return Result.Failure("You do not have access to this project.", 403); } @@ -328,9 +336,10 @@ namespace TaskTrackingSystem.WebApi.Features.Project return Result.Success(204); } - public async Task>> GetProjectTasksAsync(long projectId, string roleName, long currentUserId) + public async Task>> GetProjectTasksAsync(long projectId, long roleId, long currentUserId) { - if (!await IsProjectAccessibleAsync(projectId, roleName, currentUserId)) + var isAdmin = await DataScopeAuthorization.IsAdminScopeAsync(_db, roleId); + if (!await IsProjectAccessibleAsync(projectId, isAdmin, currentUserId)) { return Result>.Failure("You do not have access to this project.", 403); } @@ -357,11 +366,11 @@ namespace TaskTrackingSystem.WebApi.Features.Project return Result>.Success(tasks); } - private IQueryable BuildAccessibleProjectQuery(string roleName, long currentUserId) + private IQueryable BuildAccessibleProjectQuery(bool isAdmin, long currentUserId) { var query = _db.Projects.Where(p => p.IsDeleted != true); - if (IsAdmin(roleName)) + if (isAdmin) { return query; } @@ -371,9 +380,9 @@ namespace TaskTrackingSystem.WebApi.Features.Project p.ProjectMembers.Any(pm => pm.UserId == currentUserId)); } - private async Task IsProjectAccessibleAsync(long projectId, string roleName, long currentUserId) + private async Task IsProjectAccessibleAsync(long projectId, bool isAdmin, long currentUserId) { - if (IsAdmin(roleName)) + if (isAdmin) { return await _db.Projects.AnyAsync(p => p.Id == projectId && p.IsDeleted != true); } @@ -383,10 +392,5 @@ namespace TaskTrackingSystem.WebApi.Features.Project p.IsDeleted != true && (p.CreatedById == currentUserId || p.ProjectMembers.Any(pm => pm.UserId == currentUserId))); } - - private static bool IsAdmin(string roleName) - { - return roleName.Equals("Admin", StringComparison.OrdinalIgnoreCase); - } } } diff --git a/TaskTrackingSystem.WebApi/Features/Report/ReportController.cs b/TaskTrackingSystem.WebApi/Features/Report/ReportController.cs index 38e5439dda11c042c599a8c4c953a2d3310a2947..c12686f75376db29a345dc400948b53b83ff5c8b 100644 --- a/TaskTrackingSystem.WebApi/Features/Report/ReportController.cs +++ b/TaskTrackingSystem.WebApi/Features/Report/ReportController.cs @@ -8,6 +8,7 @@ using System.Threading.Tasks; using TaskTrackingSystem.Shared; using TaskTrackingSystem.Shared.Enums; using TaskTrackingSystem.Shared.Models.Report; +using TaskTrackingSystem.Shared.Models.Issue; using TaskTrackingSystem.WebApi.Infrastructure; namespace TaskTrackingSystem.WebApi.Features.Report @@ -24,6 +25,14 @@ namespace TaskTrackingSystem.WebApi.Features.Report _reportService = reportService; } + [HttpGet("issues")] + [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] + public async Task>>> GetIssuesReport() + { + var result = await _reportService.GetIssuesReportAsync(); + return StatusCode(result.StatusCode, result); + } + [HttpGet("tasks")] [ProducesResponseType(typeof(PagedResult), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] @@ -32,25 +41,21 @@ namespace TaskTrackingSystem.WebApi.Features.Report [FromQuery] DateTime? endDate, [FromQuery] string? status, [FromQuery] int? projectId, + [FromQuery] bool? assignedToMe, + [FromQuery] bool? assignedToMyTeam, [FromQuery] PaginationQuery? paging = null) { if (paging == null || (!paging.Page.HasValue && !paging.Limit.HasValue)) { - var full = await _reportService.GetTasksReportAsync(startDate, endDate, status, projectId, User.GetRoleName(), User.GetUserId()); + var full = await _reportService.GetTasksReportAsync( + startDate, endDate, status, projectId, User.GetRoleId(), User.GetUserId(), assignedToMe, assignedToMyTeam); return StatusCode(full.StatusCode, full); } var page = PaginationExtensions.NormalizePage(paging.Page); var limit = PaginationExtensions.NormalizePageSize(paging.Limit ?? 0); var result = await _reportService.GetPagedTasksReportAsync( - startDate, - endDate, - status, - projectId, - User.GetRoleName(), - User.GetUserId(), - page, - limit); + startDate, endDate, status, projectId, User.GetRoleId(), User.GetUserId(), page, limit, assignedToMe, assignedToMyTeam); return Ok(result); } @@ -60,7 +65,7 @@ namespace TaskTrackingSystem.WebApi.Features.Report public async Task>> GetUserProductivityReport( [FromQuery] PaginationQuery? paging = null) { - var users = await _reportService.GetUserProductivityReportAsync(User.GetRoleName(), User.GetUserId()); + var users = await _reportService.GetUserProductivityReportAsync(User.GetRoleId(), User.GetUserId()); if (!users.IsSuccess || users.Value == null) { return StatusCode(users.StatusCode, new PagedResult()); @@ -84,7 +89,7 @@ namespace TaskTrackingSystem.WebApi.Features.Report [FromQuery] AppTaskStatus? statusId, [FromQuery] long? projectId) { - var result = await _reportService.GetTaskStatusSummaryAsync(search, statusId, projectId, User.GetRoleName(), User.GetUserId()); + var result = await _reportService.GetTaskStatusSummaryAsync(search, statusId, projectId, User.GetRoleId(), User.GetUserId()); return StatusCode(result.StatusCode, result); } @@ -94,7 +99,7 @@ namespace TaskTrackingSystem.WebApi.Features.Report [FromQuery] AppTaskStatus? statusId, [FromQuery] long? projectId) { - var result = await _reportService.GetTaskStatusSummaryAsync(search, statusId, projectId, User.GetRoleName(), User.GetUserId()); + var result = await _reportService.GetTaskStatusSummaryAsync(search, statusId, projectId, User.GetRoleId(), User.GetUserId()); if (!result.IsSuccess || result.Value == null) return BadRequest(new { message = result.ErrorMessage }); var bytes = _reportService.ExportTaskStatusSummaryToExcel(result.Value); @@ -111,20 +116,20 @@ namespace TaskTrackingSystem.WebApi.Features.Report { if (paging == null || (!paging.Page.HasValue && !paging.Limit.HasValue)) { - var full = await _reportService.GetTeamProductivityAsync(search, User.GetRoleName(), User.GetUserId()); + var full = await _reportService.GetTeamProductivityAsync(search, User.GetRoleId(), User.GetUserId()); return StatusCode(full.StatusCode, full); } var page = PaginationExtensions.NormalizePage(paging?.Page); var limit = PaginationExtensions.NormalizePageSize(paging?.Limit ?? 0); - var result = await _reportService.GetPagedTeamProductivityAsync(search, User.GetRoleName(), User.GetUserId(), page, limit); + var result = await _reportService.GetPagedTeamProductivityAsync(search, User.GetRoleId(), User.GetUserId(), page, limit); return Ok(result); } [HttpGet("team-productivity/excel")] public async Task DownloadTeamProductivityExcel([FromQuery] string? search) { - var result = await _reportService.GetTeamProductivityAsync(search, User.GetRoleName(), User.GetUserId()); + var result = await _reportService.GetTeamProductivityAsync(search, User.GetRoleId(), User.GetUserId()); if (!result.IsSuccess || result.Value == null) return BadRequest(new { message = result.ErrorMessage }); var bytes = _reportService.ExportTeamProductivityToExcel(result.Value); @@ -140,34 +145,33 @@ namespace TaskTrackingSystem.WebApi.Features.Report [FromQuery] long? projectId, [FromQuery] int? priorityId, [FromQuery] string? delayType, + [FromQuery] bool? assignedToMe, + [FromQuery] bool? assignedToMyTeam, [FromQuery] PaginationQuery? paging = null) { if (paging == null || (!paging.Page.HasValue && !paging.Limit.HasValue)) { - var full = await _reportService.GetOverdueCriticalTasksAsync(search, projectId, User.GetRoleName(), User.GetUserId(), priorityId, delayType); + var full = await _reportService.GetOverdueCriticalTasksAsync( + search, projectId, User.GetRoleId(), User.GetUserId(), priorityId, delayType, assignedToMe, assignedToMyTeam); return StatusCode(full.StatusCode, full); } var page = PaginationExtensions.NormalizePage(paging?.Page); var limit = PaginationExtensions.NormalizePageSize(paging?.Limit ?? 0); var result = await _reportService.GetPagedOverdueCriticalTasksAsync( - search, - projectId, - User.GetRoleName(), - User.GetUserId(), - page, - limit, - priorityId, - delayType); + search, projectId, User.GetRoleId(), User.GetUserId(), page, limit, priorityId, delayType, assignedToMe, assignedToMyTeam); return Ok(result); } [HttpGet("overdue-critical/excel")] public async Task DownloadOverdueCriticalExcel( [FromQuery] string? search, - [FromQuery] long? projectId) + [FromQuery] long? projectId, + [FromQuery] bool? assignedToMe, + [FromQuery] bool? assignedToMyTeam) { - var result = await _reportService.GetOverdueCriticalTasksAsync(search, projectId, User.GetRoleName(), User.GetUserId()); + var result = await _reportService.GetOverdueCriticalTasksAsync( + search, projectId, User.GetRoleId(), User.GetUserId(), assignedToMe: assignedToMe, assignedToMyTeam: assignedToMyTeam); if (!result.IsSuccess || result.Value == null) return BadRequest(new { message = result.ErrorMessage }); var bytes = _reportService.ExportOverdueCriticalToExcel(result.Value); @@ -183,24 +187,21 @@ namespace TaskTrackingSystem.WebApi.Features.Report [FromQuery] DateTime? startDate, [FromQuery] DateTime? endDate, [FromQuery] string? status, + [FromQuery] bool? assignedToMe, + [FromQuery] bool? assignedToMyTeam, [FromQuery] PaginationQuery? paging = null) { if (paging == null || (!paging.Page.HasValue && !paging.Limit.HasValue)) { - var reportData = await _reportService.GetEmployeeProductivityReportAsync(search, startDate, endDate, status, User.GetRoleName(), User.GetUserId()); + var reportData = await _reportService.GetEmployeeProductivityReportAsync( + search, startDate, endDate, status, User.GetRoleId(), User.GetUserId(), assignedToMe, assignedToMyTeam); return StatusCode(reportData.StatusCode, reportData); } - var reportDataPaged = await _reportService.GetEmployeeProductivityReportAsync(search, startDate, endDate, status, User.GetRoleName(), User.GetUserId()); - if (!reportDataPaged.IsSuccess || reportDataPaged.Value == null) - { - return StatusCode(reportDataPaged.StatusCode, new PagedResult()); - } - - var page = PaginationExtensions.NormalizePage(paging?.Page); - var limit = PaginationExtensions.NormalizePageSize(paging?.Limit ?? 0); - var paged = await reportDataPaged.Value.OrderByDescending(u => u.Efficiency).AsQueryable().ToPagedResultAsync(page, limit); - return Ok(paged); + var reportDataPaged = await _reportService.GetPagedEmployeeProductivityReportAsync( + search, startDate, endDate, status, User.GetRoleId(), User.GetUserId(), + paging.Page ?? 1, paging.Limit ?? 10, assignedToMe, assignedToMyTeam); + return Ok(reportDataPaged); } [HttpGet("project-progress")] @@ -211,24 +212,21 @@ namespace TaskTrackingSystem.WebApi.Features.Report [FromQuery] DateTime? startDate, [FromQuery] DateTime? endDate, [FromQuery] string? status, + [FromQuery] bool? assignedToMe, + [FromQuery] bool? assignedToMyTeam, [FromQuery] PaginationQuery? paging = null) { if (paging == null || (!paging.Page.HasValue && !paging.Limit.HasValue)) { - var reportData = await _reportService.GetProjectProgressReportAsync(search, startDate, endDate, status, User.GetRoleName(), User.GetUserId()); + var reportData = await _reportService.GetProjectProgressReportAsync( + search, startDate, endDate, status, User.GetRoleId(), User.GetUserId(), assignedToMe, assignedToMyTeam); return StatusCode(reportData.StatusCode, reportData); } - var reportDataPaged = await _reportService.GetProjectProgressReportAsync(search, startDate, endDate, status, User.GetRoleName(), User.GetUserId()); - if (!reportDataPaged.IsSuccess || reportDataPaged.Value == null) - { - return StatusCode(reportDataPaged.StatusCode, new PagedResult()); - } - - var page = PaginationExtensions.NormalizePage(paging?.Page); - var limit = PaginationExtensions.NormalizePageSize(paging?.Limit ?? 0); - var paged = await reportDataPaged.Value.OrderBy(p => p.ProjectName).AsQueryable().ToPagedResultAsync(page, limit); - return Ok(paged); + var reportDataPaged = await _reportService.GetPagedProjectProgressReportAsync( + search, startDate, endDate, status, User.GetRoleId(), User.GetUserId(), + paging.Page ?? 1, paging.Limit ?? 10, assignedToMe, assignedToMyTeam); + return Ok(reportDataPaged); } } } diff --git a/TaskTrackingSystem.WebApi/Features/Report/ReportService.cs b/TaskTrackingSystem.WebApi/Features/Report/ReportService.cs index 11a80b27dbd4dfe78cceb5ee98e1f1ce1403c6ca..e9c148131e549fc32c51774d9933d1554b3d31cd 100644 --- a/TaskTrackingSystem.WebApi/Features/Report/ReportService.cs +++ b/TaskTrackingSystem.WebApi/Features/Report/ReportService.cs @@ -8,6 +8,7 @@ using System.Threading.Tasks; using TaskTrackingSystem.Database.AppDbContextModels; using TaskTrackingSystem.Shared; using TaskTrackingSystem.Shared.Models.Report; +using TaskTrackingSystem.Shared.Models.Issue; using TaskTrackingSystem.Shared.Enums; using TaskTrackingSystem.WebApi.Infrastructure; @@ -24,61 +25,54 @@ namespace TaskTrackingSystem.WebApi.Features.Report _db = db; } - private static bool IsAdmin(string roleName) + public async Task>> GetIssuesReportAsync() { - return roleName.Equals("Admin", StringComparison.OrdinalIgnoreCase); + var list = await _db.Issues + .Where(i => i.IsDeleted != true && i.Task.IsDeleted != true && i.Task.Project.IsDeleted != true) + .Include(i => i.Task) + .ThenInclude(t => t.Project) + .Include(i => i.AssignedToNavigation) + .OrderBy(i => i.Title) + .Select(i => new IssueDto + { + Id = i.Id, + TaskId = i.TaskId, + TaskTitle = i.Task.Title, + ProjectId = i.Task.ProjectId, + ProjectName = i.Task.Project.Name, + Title = i.Title, + Description = i.Description, + AssignedTo = i.AssignedTo, + AssignedToName = i.AssignedToNavigation != null + ? i.AssignedToNavigation.FirstName + " " + i.AssignedToNavigation.LastName + : null, + EstimatedHours = i.EstimatedHours, + ActualHours = i.ActualHours, + StartDate = i.StartDate, + DueDate = i.DueDate, + StatusId = i.StatusId, + PriorityId = i.PriorityId, + CreatedAt = i.CreatedAt ?? DateTime.UtcNow, + UpdatedAt = i.UpdatedAt + }) + .ToListAsync(); + + return Result>.Success(list); } - private IQueryable BuildAccessibleTaskQuery(string roleName, long currentUserId) + private IQueryable BuildAccessibleTaskQuery(bool isAdmin, bool isManager, long currentUserId) { - var query = _db.Tasks.Where(t => t.IsDeleted != true && !t.IsArchived); - - if (IsAdmin(roleName)) - { - return query; - } - - if (roleName.Equals("Manager", StringComparison.OrdinalIgnoreCase)) - { - return query.Where(t => - t.AssignedTo == currentUserId || - t.CreatedBy == currentUserId || - t.Project.ProjectMembers.Any(pm => pm.UserId == currentUserId)); - } - - return query.Where(t => - t.AssignedTo == currentUserId || - t.CreatedBy == currentUserId); + return _db.Tasks.Where(t => t.IsDeleted != true && !t.IsArchived); } - private IQueryable BuildAccessibleProjectQuery(string roleName, long currentUserId) + private IQueryable BuildAccessibleProjectQuery(bool isAdmin, long currentUserId) { - var query = _db.Projects.Where(p => p.IsDeleted != true); - - if (IsAdmin(roleName)) - { - return query; - } - - return query.Where(p => - p.CreatedById == currentUserId || - p.ProjectMembers.Any(pm => pm.UserId == currentUserId)); + return _db.Projects.Where(p => p.IsDeleted != true); } - private IQueryable BuildAccessibleUserQuery(string roleName, long currentUserId) + private IQueryable BuildAccessibleUserQuery(bool isAdmin, long currentUserId) { - var users = _db.Users.Where(u => !u.IsDeleted); - - if (IsAdmin(roleName)) - { - return users; - } - - var accessibleUserIds = BuildAccessibleProjectQuery(roleName, currentUserId) - .SelectMany(p => p.ProjectMembers.Select(pm => pm.UserId)) - .Distinct(); - - return users.Where(u => u.Id == currentUserId || accessibleUserIds.Contains(u.Id)); + return _db.Users.Where(u => !u.IsDeleted); } private static PagedResult MapPagedResult( @@ -95,15 +89,33 @@ namespace TaskTrackingSystem.WebApi.Features.Report }; } - private IQueryable BuildTaskReportQuery( - string roleName, + private async Task> GetTeamUserIdsAsync(long currentUserId) + { + var myProjectIds = await _db.ProjectMembers + .Where(pm => pm.UserId == currentUserId) + .Select(pm => pm.ProjectId) + .Distinct() + .ToListAsync(); + + return await _db.ProjectMembers + .Where(pm => myProjectIds.Contains(pm.ProjectId) && pm.UserId != currentUserId) + .Select(pm => pm.UserId) + .Distinct() + .ToListAsync(); + } + + private async Task> BuildTaskReportQueryAsync( + bool isAdmin, + bool isManager, long currentUserId, DateTime? startDate, DateTime? endDate, string? status, - int? projectId) + int? projectId, + bool? assignedToMe, + bool? assignedToMyTeam) { - var query = BuildAccessibleTaskQuery(roleName, currentUserId) + var query = BuildAccessibleTaskQuery(isAdmin, isManager, currentUserId) .Include(t => t.Project) .Include(t => t.AssignedToNavigation) .Include(t => t.AssignedByNavigation) @@ -146,6 +158,15 @@ namespace TaskTrackingSystem.WebApi.Features.Report } } + if (assignedToMe == true || assignedToMyTeam == true) + { + var teamIds = assignedToMyTeam == true ? await GetTeamUserIdsAsync(currentUserId) : new List(); + query = query.Where(t => + (assignedToMe == true && t.AssignedTo == currentUserId) || + (assignedToMyTeam == true && t.AssignedTo.HasValue && teamIds.Contains(t.AssignedTo.Value)) + ); + } + return query; } @@ -170,12 +191,16 @@ namespace TaskTrackingSystem.WebApi.Features.Report }; } - // ─── Legacy endpoints (kept for backward compatibility) ─────────────────── + // ——— Legacy endpoints (kept for backward compatibility) ——— public async Task>> GetTasksReportAsync( - DateTime? startDate, DateTime? endDate, string? status, int? projectId, string roleName, long currentUserId) + DateTime? startDate, DateTime? endDate, string? status, int? projectId, long roleId, long currentUserId, + bool? assignedToMe = null, bool? assignedToMyTeam = null) { - var tasks = await BuildTaskReportQuery(roleName, currentUserId, startDate, endDate, status, projectId) + var isAdmin = await DataScopeAuthorization.IsAdminScopeAsync(_db, roleId); + var isManager = await DataScopeAuthorization.IsManagerScopeAsync(_db, roleId); + var query = await BuildTaskReportQueryAsync(isAdmin, isManager, currentUserId, startDate, endDate, status, projectId, assignedToMe, assignedToMyTeam); + var tasks = await query .OrderBy(t => t.DueDate) .ThenByDescending(t => t.CreatedAt ?? DateTime.UtcNow) .ToListAsync(); @@ -188,12 +213,17 @@ namespace TaskTrackingSystem.WebApi.Features.Report DateTime? endDate, string? status, int? projectId, - string roleName, + long roleId, long currentUserId, int page, - int pageSize) + int pageSize, + bool? assignedToMe = null, + bool? assignedToMyTeam = null) { - var paged = await BuildTaskReportQuery(roleName, currentUserId, startDate, endDate, status, projectId) + var isAdmin = await DataScopeAuthorization.IsAdminScopeAsync(_db, roleId); + var isManager = await DataScopeAuthorization.IsManagerScopeAsync(_db, roleId); + var query = await BuildTaskReportQueryAsync(isAdmin, isManager, currentUserId, startDate, endDate, status, projectId, assignedToMe, assignedToMyTeam); + var paged = await query .OrderBy(t => t.DueDate) .ThenByDescending(t => t.CreatedAt ?? DateTime.UtcNow) .ToPagedResultAsync(page, pageSize); @@ -201,23 +231,21 @@ namespace TaskTrackingSystem.WebApi.Features.Report return MapPagedResult(paged, MapTaskReport); } - public async Task>> GetUserProductivityReportAsync(string roleName, long currentUserId) + public async Task>> GetUserProductivityReportAsync(long roleId, long currentUserId) { - var users = await BuildAccessibleUserQuery(roleName, currentUserId).ToListAsync(); - var tasks = await BuildAccessibleTaskQuery(roleName, currentUserId).ToListAsync(); + var isAdmin = await DataScopeAuthorization.IsAdminScopeAsync(_db, roleId); + var isManager = await DataScopeAuthorization.IsManagerScopeAsync(_db, roleId); + var users = await BuildAccessibleUserQuery(isAdmin, currentUserId).ToListAsync(); + var tasks = await BuildAccessibleTaskQuery(isAdmin, isManager, currentUserId).ToListAsync(); var list = new List(); foreach (var user in users) { var userTasks = tasks.Where(t => t.AssignedTo == user.Id).ToList(); - int total = userTasks.Count; var completedTasks = userTasks.Where(t => t.StatusId == AppTaskStatus.Done).ToList(); int done = completedTasks.Count; - - int onTimeCount = completedTasks.Count(t => - (t.UpdatedAt ?? t.CreatedAt) <= t.DueDate - ); - + + int onTimeCount = completedTasks.Count(t => (t.UpdatedAt ?? t.CreatedAt) <= t.DueDate); double onTimeDeliveryRate = done > 0 ? Math.Round(((double)onTimeCount / done) * 100, 2) : 0; list.Add(new UserProductivityDto @@ -234,12 +262,12 @@ namespace TaskTrackingSystem.WebApi.Features.Report return Result>.Success(list); } - // ─── Report 1: Task Status Summary ──────────────────────────────────────── - public async Task>> GetTaskStatusSummaryAsync( - string? search, AppTaskStatus? statusId, long? projectId, string roleName, long currentUserId) + string? search, AppTaskStatus? statusId, long? projectId, long roleId, long currentUserId) { - var query = BuildAccessibleTaskQuery(roleName, currentUserId) + var isAdmin = await DataScopeAuthorization.IsAdminScopeAsync(_db, roleId); + var isManager = await DataScopeAuthorization.IsManagerScopeAsync(_db, roleId); + var query = BuildAccessibleTaskQuery(isAdmin, isManager, currentUserId) .Include(t => t.Project) .Include(t => t.AssignedToNavigation) .Where(t => t.IsDeleted != true); @@ -312,12 +340,12 @@ namespace TaskTrackingSystem.WebApi.Features.Report public async Task> GetPagedTeamProductivityAsync( string? search, - string roleName, + long roleId, long currentUserId, int page, int pageSize) { - var full = await GetTeamProductivityAsync(search, roleName, currentUserId); + var full = await GetTeamProductivityAsync(search, roleId, currentUserId); if (!full.IsSuccess || full.Value == null) { return new PagedResult(); @@ -346,12 +374,14 @@ namespace TaskTrackingSystem.WebApi.Features.Report }; } - // ─── Report 2: Team Productivity ────────────────────────────────────────── + // ——— Report 2: Team Productivity ——— - public async Task>> GetTeamProductivityAsync(string? search, string roleName, long currentUserId) + public async Task>> GetTeamProductivityAsync(string? search, long roleId, long currentUserId) { - var users = await BuildAccessibleUserQuery(roleName, currentUserId).ToListAsync(); - var allTasks = await BuildAccessibleTaskQuery(roleName, currentUserId) + var isAdmin = await DataScopeAuthorization.IsAdminScopeAsync(_db, roleId); + var isManager = await DataScopeAuthorization.IsManagerScopeAsync(_db, roleId); + var users = await BuildAccessibleUserQuery(isAdmin, currentUserId).ToListAsync(); + var allTasks = await BuildAccessibleTaskQuery(isAdmin, isManager, currentUserId) .Where(t => t.AssignedTo != null) .ToListAsync(); var now = DateTime.UtcNow; @@ -420,14 +450,18 @@ namespace TaskTrackingSystem.WebApi.Features.Report public async Task>> GetOverdueCriticalTasksAsync( string? search, long? projectId, - string roleName, + long roleId, long currentUserId, int? priorityId = null, - string? delayType = null) + string? delayType = null, + bool? assignedToMe = null, + bool? assignedToMyTeam = null) { + var isAdmin = await DataScopeAuthorization.IsAdminScopeAsync(_db, roleId); + var isManager = await DataScopeAuthorization.IsManagerScopeAsync(_db, roleId); var now = DateTime.UtcNow; var today = DateTime.Today; - var query = BuildAccessibleTaskQuery(roleName, currentUserId) + var query = BuildAccessibleTaskQuery(isAdmin, isManager, currentUserId) .Include(t => t.Project) .Include(t => t.AssignedToNavigation) .Where(t => t.IsDeleted != true && t.StatusId != AppTaskStatus.Done && @@ -449,6 +483,15 @@ namespace TaskTrackingSystem.WebApi.Features.Report break; } + if (assignedToMe == true || assignedToMyTeam == true) + { + var teamIds = assignedToMyTeam == true ? await GetTeamUserIdsAsync(currentUserId) : new List(); + query = query.Where(t => + (assignedToMe == true && t.AssignedTo == currentUserId) || + (assignedToMyTeam == true && t.AssignedTo.HasValue && teamIds.Contains(t.AssignedTo.Value)) + ); + } + var tasks = await query.OrderBy(t => t.DueDate).ToListAsync(); if (!string.IsNullOrWhiteSpace(search)) @@ -469,6 +512,7 @@ namespace TaskTrackingSystem.WebApi.Features.Report StatusName = StatusMap.TryGetValue(t.StatusId, out var s) ? s : $"Status {t.StatusId}", PriorityName = PriorityMap.TryGetValue(t.PriorityId, out var p) ? p : $"Priority {t.PriorityId}", AssignedTo = t.AssignedToNavigation != null ? $"{t.AssignedToNavigation.FirstName} {t.AssignedToNavigation.LastName}" : null, + AssignedToUserId = t.AssignedTo, DueDate = t.DueDate, DaysOverdue = t.DueDate < now ? (int)(now - t.DueDate).TotalDays : 0, CreatedAt = t.CreatedAt ?? DateTime.UtcNow @@ -480,16 +524,20 @@ namespace TaskTrackingSystem.WebApi.Features.Report public async Task> GetPagedOverdueCriticalTasksAsync( string? search, long? projectId, - string roleName, + long roleId, long currentUserId, int page, int pageSize, int? priorityId = null, - string? delayType = null) + string? delayType = null, + bool? assignedToMe = null, + bool? assignedToMyTeam = null) { + var isAdmin = await DataScopeAuthorization.IsAdminScopeAsync(_db, roleId); + var isManager = await DataScopeAuthorization.IsManagerScopeAsync(_db, roleId); var now = DateTime.UtcNow; var today = DateTime.Today; - var query = BuildAccessibleTaskQuery(roleName, currentUserId) + var query = BuildAccessibleTaskQuery(isAdmin, isManager, currentUserId) .Include(t => t.Project) .Include(t => t.AssignedToNavigation) .Where(t => t.IsDeleted != true && t.StatusId != AppTaskStatus.Done && @@ -514,6 +562,15 @@ namespace TaskTrackingSystem.WebApi.Features.Report break; } + if (assignedToMe == true || assignedToMyTeam == true) + { + var teamIds = assignedToMyTeam == true ? await GetTeamUserIdsAsync(currentUserId) : new List(); + query = query.Where(t => + (assignedToMe == true && t.AssignedTo == currentUserId) || + (assignedToMyTeam == true && t.AssignedTo.HasValue && teamIds.Contains(t.AssignedTo.Value)) + ); + } + if (!string.IsNullOrWhiteSpace(search)) { var s = search.Trim().ToLower(); @@ -536,6 +593,7 @@ namespace TaskTrackingSystem.WebApi.Features.Report StatusName = StatusMap.TryGetValue(t.StatusId, out var s) ? s : $"Status {t.StatusId}", PriorityName = PriorityMap.TryGetValue(t.PriorityId, out var p) ? p : $"Priority {t.PriorityId}", AssignedTo = t.AssignedToNavigation != null ? $"{t.AssignedToNavigation.FirstName} {t.AssignedToNavigation.LastName}" : null, + AssignedToUserId = t.AssignedTo, DueDate = t.DueDate, DaysOverdue = t.DueDate < now ? (int)(now - t.DueDate).TotalDays : 0, CreatedAt = t.CreatedAt ?? DateTime.UtcNow @@ -582,11 +640,25 @@ namespace TaskTrackingSystem.WebApi.Features.Report DateTime? startDate, DateTime? endDate, string? status, - string roleName, - long currentUserId) + long roleId, + long currentUserId, + bool? assignedToMe = null, + bool? assignedToMyTeam = null) { - var users = await BuildAccessibleUserQuery(roleName, currentUserId).ToListAsync(); - var tasks = await BuildAccessibleTaskQuery(roleName, currentUserId).ToListAsync(); + var isAdmin = await DataScopeAuthorization.IsAdminScopeAsync(_db, roleId); + var isManager = await DataScopeAuthorization.IsManagerScopeAsync(_db, roleId); + var users = await BuildAccessibleUserQuery(isAdmin, currentUserId).ToListAsync(); + + if (assignedToMe == true || assignedToMyTeam == true) + { + var teamIds = assignedToMyTeam == true ? await GetTeamUserIdsAsync(currentUserId) : new List(); + users = users.Where(u => + (assignedToMe == true && u.Id == currentUserId) || + (assignedToMyTeam == true && teamIds.Contains(u.Id)) + ).ToList(); + } + + var tasks = await BuildAccessibleTaskQuery(isAdmin, isManager, currentUserId).ToListAsync(); if (startDate.HasValue) tasks = tasks.Where(t => t.CreatedAt >= startDate.Value.Date).ToList(); @@ -647,12 +719,14 @@ namespace TaskTrackingSystem.WebApi.Features.Report DateTime? startDate, DateTime? endDate, string? status, - string roleName, + long roleId, long currentUserId, int page, - int pageSize) + int pageSize, + bool? assignedToMe = null, + bool? assignedToMyTeam = null) { - var full = await GetEmployeeProductivityReportAsync(search, startDate, endDate, status, roleName, currentUserId); + var full = await GetEmployeeProductivityReportAsync(search, startDate, endDate, status, roleId, currentUserId, assignedToMe, assignedToMyTeam); if (!full.IsSuccess || full.Value == null) { return new PagedResult(); @@ -686,11 +760,27 @@ namespace TaskTrackingSystem.WebApi.Features.Report DateTime? startDate, DateTime? endDate, string? status, - string roleName, - long currentUserId) + long roleId, + long currentUserId, + bool? assignedToMe = null, + bool? assignedToMyTeam = null) { - var projects = await BuildAccessibleProjectQuery(roleName, currentUserId).ToListAsync(); - var tasks = await BuildAccessibleTaskQuery(roleName, currentUserId).ToListAsync(); + var isAdmin = await DataScopeAuthorization.IsAdminScopeAsync(_db, roleId); + var isManager = await DataScopeAuthorization.IsManagerScopeAsync(_db, roleId); + var projects = await BuildAccessibleProjectQuery(isAdmin, currentUserId).ToListAsync(); + + if (assignedToMe == true || assignedToMyTeam == true) + { + var myProjectIds = await _db.ProjectMembers + .Where(pm => pm.UserId == currentUserId) + .Select(pm => pm.ProjectId) + .Distinct() + .ToListAsync(); + + projects = projects.Where(p => myProjectIds.Contains(p.Id) || p.CreatedById == currentUserId).ToList(); + } + + var tasks = await BuildAccessibleTaskQuery(isAdmin, isManager, currentUserId).ToListAsync(); var today = DateTime.Today; if (startDate.HasValue) @@ -762,12 +852,14 @@ namespace TaskTrackingSystem.WebApi.Features.Report DateTime? startDate, DateTime? endDate, string? status, - string roleName, + long roleId, long currentUserId, int page, - int pageSize) + int pageSize, + bool? assignedToMe = null, + bool? assignedToMyTeam = null) { - var full = await GetProjectProgressReportAsync(search, startDate, endDate, status, roleName, currentUserId); + var full = await GetProjectProgressReportAsync(search, startDate, endDate, status, roleId, currentUserId, assignedToMe, assignedToMyTeam); if (!full.IsSuccess || full.Value == null) { return new PagedResult(); diff --git a/TaskTrackingSystem.WebApi/Features/Task/TaskController.cs b/TaskTrackingSystem.WebApi/Features/Task/TaskController.cs index 934f5af6723879d5427545573e25e4d1f202b0c4..1f70ff2f06b99dd08ca86ed619502485adf4df32 100644 --- a/TaskTrackingSystem.WebApi/Features/Task/TaskController.cs +++ b/TaskTrackingSystem.WebApi/Features/Task/TaskController.cs @@ -36,14 +36,14 @@ namespace TaskTrackingSystem.WebApi.Features.Task { if (paging == null || (!paging.Page.HasValue && !paging.Limit.HasValue)) { - var tasks = await _taskService.GetAllTasksAsync(User.GetRoleName(), User.GetUserId()); + var tasks = await _taskService.GetAllTasksAsync(User.GetRoleId(), User.GetUserId()); return Ok(tasks); } var page = PaginationExtensions.NormalizePage(paging.Page); var limit = PaginationExtensions.NormalizePageSize(paging.Limit ?? 0); var paged = await _taskService.GetPagedTasksAsync( - User.GetRoleName(), + User.GetRoleId(), User.GetUserId(), search, projectId, @@ -59,14 +59,14 @@ namespace TaskTrackingSystem.WebApi.Features.Task [HttpGet("archived")] public async Task>> GetArchivedTasks() { - var tasks = await _taskService.GetArchivedTasksAsync(User.GetRoleName(), User.GetUserId()); + var tasks = await _taskService.GetArchivedTasksAsync(User.GetRoleId(), User.GetUserId()); return Ok(tasks); } [HttpGet("{id}")] public async Task> GetTask(long id) { - var task = await _taskService.GetTaskByIdAsync(id, User.GetRoleName(), User.GetUserId()); + var task = await _taskService.GetTaskByIdAsync(id, User.GetRoleId(), User.GetUserId()); if (task == null) { return NotFound(new { message = $"Task with ID {id} not found." }); @@ -83,7 +83,7 @@ namespace TaskTrackingSystem.WebApi.Features.Task } var currentUserId = User.GetUserId(); - var result = await _taskService.CreateTaskAsync(createTaskDto, User.GetRoleName(), currentUserId); + var result = await _taskService.CreateTaskAsync(createTaskDto, User.GetRoleId(), currentUserId); return StatusCode(result.StatusCode, result); } @@ -115,14 +115,14 @@ namespace TaskTrackingSystem.WebApi.Features.Task [HttpGet("/api/User/{userId}/tasks")] public async Task>>> GetUserTasks(long userId) { - var result = await _taskService.GetTasksByUserIdAsync(userId, User.GetRoleName(), User.GetUserId()); + var result = await _taskService.GetTasksByUserIdAsync(userId, User.GetRoleId(), User.GetUserId()); return StatusCode(result.StatusCode, result); } [HttpPatch("{id}/status")] public async Task UpdateTaskStatus(long id, [FromQuery] AppTaskStatus statusId) { - var result = await _taskService.UpdateTaskStatusAsync(id, statusId, User.GetRoleName(), User.GetUserId()); + var result = await _taskService.UpdateTaskStatusAsync(id, statusId, User.GetRoleId(), User.GetUserId()); if (!result.IsSuccess) { return StatusCode(result.StatusCode, new { message = result.ErrorMessage }); @@ -138,7 +138,7 @@ namespace TaskTrackingSystem.WebApi.Features.Task return Forbid(); } - var result = await _taskService.ArchiveDoneTasksAsync(projectId, User.GetRoleName(), User.GetUserId()); + var result = await _taskService.ArchiveDoneTasksAsync(projectId, User.GetRoleId(), User.GetUserId()); return StatusCode(result.StatusCode, result); } } diff --git a/TaskTrackingSystem.WebApi/Features/Task/TaskDetailsController.cs b/TaskTrackingSystem.WebApi/Features/Task/TaskDetailsController.cs index 0de02777558fb9501ef9de26a82ead1a8096c057..4d6b486c4af13bfc75bd0d152a3dc2b306078572 100644 --- a/TaskTrackingSystem.WebApi/Features/Task/TaskDetailsController.cs +++ b/TaskTrackingSystem.WebApi/Features/Task/TaskDetailsController.cs @@ -125,7 +125,7 @@ namespace TaskTrackingSystem.WebApi.Features.Task } var userId = User.GetUserId(); - var isAdmin = User.IsAdmin(); + var isAdmin = await DataScopeAuthorization.IsAdminScopeAsync(_db, User.GetRoleId()); // Only comment creator or Admin can delete if (comment.UserId != userId && !isAdmin) diff --git a/TaskTrackingSystem.WebApi/Features/Task/TaskService.cs b/TaskTrackingSystem.WebApi/Features/Task/TaskService.cs index 060b410a1d3347147f2dbbd5ea111486758bf6f8..f4944d5fe1526e124370be0e6639c37e03e5bf8c 100644 --- a/TaskTrackingSystem.WebApi/Features/Task/TaskService.cs +++ b/TaskTrackingSystem.WebApi/Features/Task/TaskService.cs @@ -30,9 +30,10 @@ namespace TaskTrackingSystem.WebApi.Features.Task : null; } - public async Task> GetAllTasksAsync(string roleName, long currentUserId) + public async Task> GetAllTasksAsync(long roleId, long currentUserId) { - return await BuildAccessibleTaskQuery(roleName, currentUserId) + var query = await BuildAccessibleTaskQueryAsync(roleId, currentUserId); + return await query .OrderBy(t => t.DueDate) .ThenByDescending(t => t.CreatedAt ?? DateTime.UtcNow) .Select(t => new TaskDto @@ -54,7 +55,7 @@ namespace TaskTrackingSystem.WebApi.Features.Task } public async Task> GetPagedTasksAsync( - string roleName, + long roleId, long currentUserId, string? search, long? projectId, @@ -64,7 +65,7 @@ namespace TaskTrackingSystem.WebApi.Features.Task int page, int pageSize) { - var query = BuildAccessibleTaskQuery(roleName, currentUserId); + var query = await BuildAccessibleTaskQueryAsync(roleId, currentUserId); if (assignedOnly) { @@ -115,9 +116,10 @@ namespace TaskTrackingSystem.WebApi.Features.Task .ToPagedResultAsync(page, pageSize); } - public async Task> GetArchivedTasksAsync(string roleName, long currentUserId) + public async Task> GetArchivedTasksAsync(long roleId, long currentUserId) { - return await BuildAccessibleTaskQuery(roleName, currentUserId, includeArchived: true) + var query = await BuildAccessibleTaskQueryAsync(roleId, currentUserId, includeArchived: true); + return await query .Where(t => t.IsArchived) .OrderByDescending(t => t.UpdatedAt ?? t.CreatedAt ?? DateTime.UtcNow) .Select(t => new TaskDto @@ -138,10 +140,10 @@ namespace TaskTrackingSystem.WebApi.Features.Task .ToListAsync(); } - public async Task GetTaskByIdAsync(long id, string roleName, long currentUserId) + public async Task GetTaskByIdAsync(long id, long roleId, long currentUserId) { - var task = await BuildAccessibleTaskQuery(roleName, currentUserId) - .FirstOrDefaultAsync(t => t.Id == id); + var query = await BuildAccessibleTaskQueryAsync(roleId, currentUserId); + var task = await query.FirstOrDefaultAsync(t => t.Id == id); if (task == null) { @@ -165,7 +167,7 @@ namespace TaskTrackingSystem.WebApi.Features.Task }; } - public async Task> CreateTaskAsync(CreateTaskDto dto, string roleName, long currentUserId) + public async Task> CreateTaskAsync(CreateTaskDto dto, long roleId, long currentUserId) { if (string.IsNullOrWhiteSpace(dto.Title)) { @@ -183,7 +185,7 @@ namespace TaskTrackingSystem.WebApi.Features.Task return Result.Failure(ResultMessages.ProjectNotFound(dto.ProjectId), 404); } - if (!await CanAccessProjectAsync(dto.ProjectId, roleName, currentUserId)) + if (!await CanAccessProjectAsync(dto.ProjectId, roleId, currentUserId)) { return Result.Failure("You do not have access to this project.", 403); } @@ -299,7 +301,7 @@ namespace TaskTrackingSystem.WebApi.Features.Task return Result.Success(200); } - public async Task>> GetTasksByUserIdAsync(long userId, string roleName, long currentUserId) + public async Task>> GetTasksByUserIdAsync(long userId, long roleId, long currentUserId) { var userExists = await _db.Users.AnyAsync(u => u.Id == userId && !u.IsDeleted); if (!userExists) @@ -307,7 +309,8 @@ namespace TaskTrackingSystem.WebApi.Features.Task return Result>.Failure(ResultMessages.UserNotFound(userId), 404); } - var tasks = await BuildAccessibleTaskQuery(roleName, currentUserId) + var query = await BuildAccessibleTaskQueryAsync(roleId, currentUserId); + var tasks = await query .Where(t => t.AssignedTo == userId) .Select(t => new TaskDto { @@ -329,7 +332,7 @@ namespace TaskTrackingSystem.WebApi.Features.Task return Result>.Success(tasks); } - public async Task UpdateTaskStatusAsync(long id, AppTaskStatus statusId, string roleName, long currentUserId) + public async Task UpdateTaskStatusAsync(long id, AppTaskStatus statusId, long roleId, long currentUserId) { var task = await _db.Tasks.FirstOrDefaultAsync(t => t.Id == id && t.IsDeleted != true && !t.IsArchived); if (task == null) @@ -354,7 +357,7 @@ namespace TaskTrackingSystem.WebApi.Features.Task return Result.Success(200); } - public async Task> ArchiveDoneTasksAsync(long? projectId, string roleName, long currentUserId) + public async Task> ArchiveDoneTasksAsync(long? projectId, long roleId, long currentUserId) { if (projectId.HasValue && projectId.Value > 0) { @@ -364,21 +367,22 @@ namespace TaskTrackingSystem.WebApi.Features.Task return Result.Failure(ResultMessages.ProjectNotFound(projectId.Value), 404); } - if (!await CanAccessProjectAsync(projectId.Value, roleName, currentUserId)) + if (!await CanAccessProjectAsync(projectId.Value, roleId, currentUserId)) { return Result.Failure("You do not have access to this project.", 403); } } - var query = BuildAccessibleTaskQuery(roleName, currentUserId) - .Where(t => t.StatusId == AppTaskStatus.Done); + var query = await BuildAccessibleTaskQueryAsync(roleId, currentUserId); + var tasksToArchive = await query + .Where(t => t.StatusId == AppTaskStatus.Done) + .ToListAsync(); if (projectId.HasValue && projectId.Value > 0) { - query = query.Where(t => t.ProjectId == projectId.Value); + tasksToArchive = tasksToArchive.Where(t => t.ProjectId == projectId.Value).ToList(); } - var tasksToArchive = await query.ToListAsync(); foreach (var task in tasksToArchive) { task.IsArchived = true; @@ -395,16 +399,18 @@ namespace TaskTrackingSystem.WebApi.Features.Task return Result.Success(tasksToArchive.Count); } - private IQueryable BuildAccessibleTaskQuery(string roleName, long currentUserId, bool includeArchived = false) + private async Task> BuildAccessibleTaskQueryAsync(long roleId, long currentUserId, bool includeArchived = false) { var query = _db.Tasks.Where(t => t.IsDeleted != true && (includeArchived || !t.IsArchived)); - if (IsAdmin(roleName)) + var isAdmin = await DataScopeAuthorization.IsAdminScopeAsync(_db, roleId); + if (isAdmin) { return query; } - if (IsManager(roleName)) + var isManager = await DataScopeAuthorization.IsManagerScopeAsync(_db, roleId); + if (isManager) { return query.Where(t => t.AssignedTo == currentUserId || @@ -417,9 +423,10 @@ namespace TaskTrackingSystem.WebApi.Features.Task t.CreatedBy == currentUserId); } - private async Task CanAccessProjectAsync(long projectId, string roleName, long currentUserId) + private async Task CanAccessProjectAsync(long projectId, long roleId, long currentUserId) { - if (IsAdmin(roleName)) + var isAdmin = await DataScopeAuthorization.IsAdminScopeAsync(_db, roleId); + if (isAdmin) { return await _db.Projects.AnyAsync(p => p.Id == projectId && p.IsDeleted != true); } @@ -434,15 +441,5 @@ namespace TaskTrackingSystem.WebApi.Features.Task { return await _db.ProjectMembers.AnyAsync(pm => pm.ProjectId == projectId && pm.UserId == userId); } - - private static bool IsAdmin(string roleName) - { - return roleName.Equals("Admin", StringComparison.OrdinalIgnoreCase); - } - - private static bool IsManager(string roleName) - { - return roleName.Equals("Manager", StringComparison.OrdinalIgnoreCase); - } } } diff --git a/TaskTrackingSystem.WebApi/Features/User/UserController.cs b/TaskTrackingSystem.WebApi/Features/User/UserController.cs index e6339a0c28bb690f1d63e76883159119d15e3463..ec413d5e56dfd88051bd76499d2ec2cf933d0ac2 100644 --- a/TaskTrackingSystem.WebApi/Features/User/UserController.cs +++ b/TaskTrackingSystem.WebApi/Features/User/UserController.cs @@ -43,6 +43,25 @@ namespace TaskTrackingSystem.WebApi.Features.User return Ok(paged); } + /// Returns the user IDs of all colleagues who share any project with the current user. + [HttpGet("my-team-user-ids")] + public async Task>> GetMyTeamUserIds() + { + var currentUserId = User.GetUserId(); + var myProjectIds = await _userService.GetMyProjectIdsAsync(currentUserId); + var teamUserIds = await _userService.GetTeamUserIdsAsync(myProjectIds, currentUserId); + return Ok(teamUserIds); + } + + /// Returns all project IDs the current user is a member of. + [HttpGet("my-project-ids")] + public async Task>> GetMyProjectIds() + { + var currentUserId = User.GetUserId(); + var projectIds = await _userService.GetMyProjectIdsAsync(currentUserId); + return Ok(projectIds); + } + [HttpGet("{id}")] public async Task> GetUser(long id) { diff --git a/TaskTrackingSystem.WebApi/Features/User/UserService.cs b/TaskTrackingSystem.WebApi/Features/User/UserService.cs index 7b9dfa66b4fca1a289c86c3317b9b5dceb6f644f..ad831f56878ae329aafb80fe3acd51cfa8ce726c 100644 --- a/TaskTrackingSystem.WebApi/Features/User/UserService.cs +++ b/TaskTrackingSystem.WebApi/Features/User/UserService.cs @@ -221,5 +221,26 @@ namespace TaskTrackingSystem.WebApi.Features.User return Result.Success(200); } + + public async Task> GetMyProjectIdsAsync(long currentUserId) + { + return await _db.ProjectMembers + .Where(pm => pm.UserId == currentUserId) + .Select(pm => pm.ProjectId) + .Distinct() + .ToListAsync(); + } + + public async Task> GetTeamUserIdsAsync(List projectIds, long currentUserId) + { + if (projectIds == null || projectIds.Count == 0) + return new List(); + + return await _db.ProjectMembers + .Where(pm => projectIds.Contains(pm.ProjectId) && pm.UserId != currentUserId) + .Select(pm => pm.UserId) + .Distinct() + .ToListAsync(); + } } } diff --git a/TaskTrackingSystem.WebApi/Features/UserDevice/UserDeviceService.cs b/TaskTrackingSystem.WebApi/Features/UserDevice/UserDeviceService.cs index 9b3029af56f6ce251241e05726cf7cfef61cd6cb..585ea2112a7e2055259874d092a666be88b5b1ae 100644 --- a/TaskTrackingSystem.WebApi/Features/UserDevice/UserDeviceService.cs +++ b/TaskTrackingSystem.WebApi/Features/UserDevice/UserDeviceService.cs @@ -45,22 +45,4 @@ public class UserDeviceService return Result.Success(); } - public async Task RemoveTokenAsync(long userId, string fcmToken) - { - var token = fcmToken.Trim(); - if (string.IsNullOrWhiteSpace(token)) - { - return Result.Failure("FCM token is required.", 400); - } - - var device = await _db.UserDevices.FirstOrDefaultAsync(x => x.UserId == userId && x.FcmToken == token); - if (device == null) - { - return Result.Success(204); - } - - _db.UserDevices.Remove(device); - await _db.SaveChangesAsync(); - return Result.Success(204); - } } diff --git a/TaskTrackingSystem.WebApi/Features/UserDevice/UserDevicesController.cs b/TaskTrackingSystem.WebApi/Features/UserDevice/UserDevicesController.cs index 25183d1966b7953808ee17f8e4291e98da41ea6b..ad2425846fd33c2bb8983b04f5e21264a2b13630 100644 --- a/TaskTrackingSystem.WebApi/Features/UserDevice/UserDevicesController.cs +++ b/TaskTrackingSystem.WebApi/Features/UserDevice/UserDevicesController.cs @@ -31,16 +31,4 @@ public class UserDevicesController : ControllerBase return StatusCode(result.StatusCode, result); } - [HttpPost("unregister")] - public async Task> Unregister([FromBody] RegisterDeviceTokenDto dto) - { - var userId = User.GetUserId(); - if (userId <= 0) - { - return Unauthorized(Result.Failure("User is not authenticated.", 401)); - } - - var result = await _userDeviceService.RemoveTokenAsync(userId, dto.FcmToken); - return StatusCode(result.StatusCode, result); - } } diff --git a/TaskTrackingSystem.WebApi/Infrastructure/ClaimsPrincipalExtensions.cs b/TaskTrackingSystem.WebApi/Infrastructure/ClaimsPrincipalExtensions.cs index 9eeb35e3594b392ac8cf45fd38a443ff65373e19..e41b69cbb0838e04e25b64bc5fb0ca3c04d0e841 100644 --- a/TaskTrackingSystem.WebApi/Infrastructure/ClaimsPrincipalExtensions.cs +++ b/TaskTrackingSystem.WebApi/Infrastructure/ClaimsPrincipalExtensions.cs @@ -21,13 +21,6 @@ public static class ClaimsPrincipalExtensions return user.FindFirstValue(ClaimTypes.Role) ?? string.Empty; } - public static bool IsAdmin(this ClaimsPrincipal user) - { - return user.IsInRole("Admin"); - } - public static bool IsManager(this ClaimsPrincipal user) - { - return user.IsInRole("Manager"); - } + } diff --git a/TaskTrackingSystem.WebApi/Infrastructure/DataScopeAuthorization.cs b/TaskTrackingSystem.WebApi/Infrastructure/DataScopeAuthorization.cs new file mode 100644 index 0000000000000000000000000000000000000000..3cea63cdb57f27ca8e2b2db97e20927d069bab27 --- /dev/null +++ b/TaskTrackingSystem.WebApi/Infrastructure/DataScopeAuthorization.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore; +using System.Threading.Tasks; +using TaskTrackingSystem.Database.AppDbContextModels; + +namespace TaskTrackingSystem.WebApi.Infrastructure +{ + public static class DataScopeAuthorization + { + public static async Task IsAdminScopeAsync(AppDbContext db, long roleId) + { + if (roleId <= 0) return false; + return await db.RolePermissions.AnyAsync(rp => + rp.RoleId == roleId && + !rp.IsDeleted && + rp.Permission.PermissionCode == "Roles_List" && + !rp.Permission.IsDeleted); + } + + public static async Task IsManagerScopeAsync(AppDbContext db, long roleId) + { + if (roleId <= 0) return false; + return await db.RolePermissions.AnyAsync(rp => + rp.RoleId == roleId && + !rp.IsDeleted && + rp.Permission.PermissionCode == "Projects_Create" && + !rp.Permission.IsDeleted); + } + } +} diff --git a/TaskTrackingSystem.WebApi/Infrastructure/PermissionAuthorizationService.cs b/TaskTrackingSystem.WebApi/Infrastructure/PermissionAuthorizationService.cs index 8ecc726a6d1195d5a831b007fd9b3a99ad2ff61b..961695fc4da08d9e6e56334756aea9f530810dda 100644 --- a/TaskTrackingSystem.WebApi/Infrastructure/PermissionAuthorizationService.cs +++ b/TaskTrackingSystem.WebApi/Infrastructure/PermissionAuthorizationService.cs @@ -20,11 +20,6 @@ public class PermissionAuthorizationService return false; } - if (user.IsInRole("Admin")) - { - return true; - } - var roleId = await ResolveRoleIdAsync(user); if (roleId <= 0) { diff --git a/TaskTrackingSystem.WebApp/Components/App.razor b/TaskTrackingSystem.WebApp/Components/App.razor index 658a4d9017eb09a0a7527ba7dbd72cb3a6914e41..3318d80f6c97ca0595c90cadf029bcdc8328f697 100644 --- a/TaskTrackingSystem.WebApp/Components/App.razor +++ b/TaskTrackingSystem.WebApp/Components/App.razor @@ -5,7 +5,6 @@ - diff --git a/TaskTrackingSystem.WebApp/Components/Layout/Component.razor b/TaskTrackingSystem.WebApp/Components/Layout/Component.razor deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/TaskTrackingSystem.WebApp/Components/Layout/NavMenu.razor b/TaskTrackingSystem.WebApp/Components/Layout/NavMenu.razor index e9e6cac740962079221f40cff219e9cd240bb349..2b95af3425c9c1da5e23eed001510ab1f2e2bfc1 100644 --- a/TaskTrackingSystem.WebApp/Components/Layout/NavMenu.razor +++ b/TaskTrackingSystem.WebApp/Components/Layout/NavMenu.razor @@ -81,7 +81,8 @@ if (user.Identity?.IsAuthenticated == true) { - isAdmin = user.IsInRole("Admin"); + var codes = await MenuAuthorization.GetCurrentAccessCodesAsync(user); + isAdmin = codes.Contains("Roles_List"); var rawMenus = await MenuAuthorization.GetUserMenusAsync(user); menus = ProcessMenus(rawMenus); BuildUrlHierarchy(menus); @@ -127,6 +128,8 @@ if (user.Identity?.IsAuthenticated == true) { + var codes = await MenuAuthorization.GetCurrentAccessCodesAsync(user); + isAdmin = codes.Contains("Roles_List"); var freshMenus = await MenuAuthorization.GetUserMenusAsync(user); var processedMenus = ProcessMenus(freshMenus); diff --git a/TaskTrackingSystem.WebApp/Components/Pages/Component.razor b/TaskTrackingSystem.WebApp/Components/Pages/Component.razor deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/TaskTrackingSystem.WebApp/Components/Pages/AuditLogs.razor b/TaskTrackingSystem.WebApp/Components/Pages/Features/Audit/AuditLogs.razor similarity index 99% rename from TaskTrackingSystem.WebApp/Components/Pages/AuditLogs.razor rename to TaskTrackingSystem.WebApp/Components/Pages/Features/Audit/AuditLogs.razor index 331c39022ca42d1bf7e6dd63aefb010640624a61..1486c35d47f4242c0afb86325f5129b534ac5d45 100644 --- a/TaskTrackingSystem.WebApp/Components/Pages/AuditLogs.razor +++ b/TaskTrackingSystem.WebApp/Components/Pages/Features/Audit/AuditLogs.razor @@ -4,7 +4,7 @@ @using TaskTrackingSystem.Shared.Models.AuditLog @using TaskTrackingSystem.WebApp.Components.Partial @rendermode @(new InteractiveServerRenderMode(prerender: false)) -@attribute [Authorize(Roles = "Admin")] +@attribute [Authorize] @inject ApiClientService ApiClient @inject IJSRuntime JS @inject AuthenticationStateProvider AuthStateProvider diff --git a/TaskTrackingSystem.WebApp/Components/Pages/Login.razor b/TaskTrackingSystem.WebApp/Components/Pages/Features/Auth/Login.razor similarity index 100% rename from TaskTrackingSystem.WebApp/Components/Pages/Login.razor rename to TaskTrackingSystem.WebApp/Components/Pages/Features/Auth/Login.razor diff --git a/TaskTrackingSystem.WebApp/Components/Pages/Register.razor b/TaskTrackingSystem.WebApp/Components/Pages/Features/Auth/Register.razor similarity index 100% rename from TaskTrackingSystem.WebApp/Components/Pages/Register.razor rename to TaskTrackingSystem.WebApp/Components/Pages/Features/Auth/Register.razor diff --git a/TaskTrackingSystem.WebApp/Components/Pages/ResetPassword.razor b/TaskTrackingSystem.WebApp/Components/Pages/Features/Auth/ResetPassword.razor similarity index 100% rename from TaskTrackingSystem.WebApp/Components/Pages/ResetPassword.razor rename to TaskTrackingSystem.WebApp/Components/Pages/Features/Auth/ResetPassword.razor diff --git a/TaskTrackingSystem.WebApp/Components/Pages/Error.razor b/TaskTrackingSystem.WebApp/Components/Pages/Features/Common/Error.razor similarity index 100% rename from TaskTrackingSystem.WebApp/Components/Pages/Error.razor rename to TaskTrackingSystem.WebApp/Components/Pages/Features/Common/Error.razor diff --git a/TaskTrackingSystem.WebApp/Components/Pages/EmployeeDashboard.razor b/TaskTrackingSystem.WebApp/Components/Pages/Features/Dashboard/EmployeeDashboard.razor similarity index 99% rename from TaskTrackingSystem.WebApp/Components/Pages/EmployeeDashboard.razor rename to TaskTrackingSystem.WebApp/Components/Pages/Features/Dashboard/EmployeeDashboard.razor index ce35d91c8ced88de0bed5d7e6721cb23f62000d3..67707a007940b738e3b832c1f091230c2b01d493 100644 --- a/TaskTrackingSystem.WebApp/Components/Pages/EmployeeDashboard.razor +++ b/TaskTrackingSystem.WebApp/Components/Pages/Features/Dashboard/EmployeeDashboard.razor @@ -2,7 +2,7 @@ @using Microsoft.AspNetCore.Authorization @using Microsoft.AspNetCore.Components.Authorization @rendermode @(new InteractiveServerRenderMode(prerender: false)) -@attribute [Authorize(Roles = "Employee")] +@attribute [Authorize] @inject ApiClientService ApiClient @inject NavigationManager Navigation @inject IJSRuntime JS diff --git a/TaskTrackingSystem.WebApp/Components/Pages/Home.razor b/TaskTrackingSystem.WebApp/Components/Pages/Features/Dashboard/Home.razor similarity index 97% rename from TaskTrackingSystem.WebApp/Components/Pages/Home.razor rename to TaskTrackingSystem.WebApp/Components/Pages/Features/Dashboard/Home.razor index f8eaf8cfdf58c404a00234c7f6c744f854f1879b..59eff639098a837f2bfdfe66bafbe6eefec8acc5 100644 --- a/TaskTrackingSystem.WebApp/Components/Pages/Home.razor +++ b/TaskTrackingSystem.WebApp/Components/Pages/Features/Dashboard/Home.razor @@ -9,11 +9,12 @@ @using TaskTrackingSystem.Shared.Models.User @using TaskTrackingSystem.Shared.Models.Dashboard @rendermode @(new InteractiveServerRenderMode(prerender: false)) -@attribute [Microsoft.AspNetCore.Authorization.Authorize(Roles = "Admin")] +@attribute [Microsoft.AspNetCore.Authorization.Authorize] @inject ApiClientService ApiClient @inject IJSRuntime JS @inject AuthenticationStateProvider AuthStateProvider @inject MenuAuthorizationService MenuAuthorization +@inject NavigationManager NavigationManager Dashboard @@ -364,7 +365,22 @@ protected override async Task OnInitializedAsync() { var authState = await AuthStateProvider.GetAuthenticationStateAsync(); - canCreateTask = authState.User.IsInRole("Admin") || await MenuAuthorization.HasAccessCodeAsync(authState.User, "Tasks_Create"); + var user = authState.User; + + var codes = await MenuAuthorization.GetCurrentAccessCodesAsync(user); + + if (codes.Contains("Projects_Create") && !codes.Contains("Roles_List")) + { + NavigationManager.NavigateTo("/dashboard/manager"); + return; + } + else if (!codes.Contains("Projects_Create") && !codes.Contains("Roles_List")) + { + NavigationManager.NavigateTo("/dashboard/employee"); + return; + } + + canCreateTask = codes.Contains("Tasks_Create"); await LoadDashboardData(); } @@ -612,7 +628,7 @@ // 2. Adjust total so it maps 100% cleanly to the math displayed var total = Math.Max(TotalTasksCount, 1); - var circumference = 2 * Math.PI * 74; // 2πr for r=74 + var circumference = 2 * Math.PI * 74; // 2╧Çr for r=74 var slices = new List(); var cumulativeArc = 0.0; @@ -620,7 +636,7 @@ { if (rawCounts[i] <= 0) { - // Arc length 0 with a full-circumference gap → invisible. + // Arc length 0 with a full-circumference gap ΓåÆ invisible. // Avoids stroke-linecap="round" rendering a phantom dot. slices.Add(new DistributionSlice(GetDistributionLabel(i), 0, colors[i], $"0 {circumference:0.##}", 0)); continue; @@ -725,7 +741,7 @@ } var first = months.First(); - var n = months.Count; // total columns — each flex-1 month slot = 1/n of bar width + var n = months.Count; // total columns ΓÇö each flex-1 month slot = 1/n of bar width return FeaturedProjects.Select(project => { @@ -743,7 +759,7 @@ return new TimelineRow( project.Name, - $"{DisplayFormats.Date(project.StartDate)} – {DisplayFormats.Date(project.EndDate)}", + $"{DisplayFormats.Date(project.StartDate)} ΓÇô {DisplayFormats.Date(project.EndDate)}", GetProjectColor(project.Id), left, width, diff --git a/TaskTrackingSystem.WebApp/Components/Pages/ManagerDashboard.razor b/TaskTrackingSystem.WebApp/Components/Pages/Features/Dashboard/ManagerDashboard.razor similarity index 99% rename from TaskTrackingSystem.WebApp/Components/Pages/ManagerDashboard.razor rename to TaskTrackingSystem.WebApp/Components/Pages/Features/Dashboard/ManagerDashboard.razor index 17b812fb4dda6e28db338057ffd651d8fbae6b42..883b28f9ced681c745d99e45071881ba44fff93e 100644 --- a/TaskTrackingSystem.WebApp/Components/Pages/ManagerDashboard.razor +++ b/TaskTrackingSystem.WebApp/Components/Pages/Features/Dashboard/ManagerDashboard.razor @@ -1,7 +1,7 @@ @page "/dashboard/manager" @using Microsoft.AspNetCore.Authorization @rendermode @(new InteractiveServerRenderMode(prerender: false)) -@attribute [Authorize(Roles = "Manager")] +@attribute [Authorize] @inject ApiClientService ApiClient @inject NavigationManager Navigation @inject IJSRuntime JS diff --git a/TaskTrackingSystem.WebApp/Components/Pages/AddIssue.razor b/TaskTrackingSystem.WebApp/Components/Pages/Features/Issues/AddIssue.razor similarity index 100% rename from TaskTrackingSystem.WebApp/Components/Pages/AddIssue.razor rename to TaskTrackingSystem.WebApp/Components/Pages/Features/Issues/AddIssue.razor diff --git a/TaskTrackingSystem.WebApp/Components/Pages/Issues.razor b/TaskTrackingSystem.WebApp/Components/Pages/Features/Issues/Issues.razor similarity index 89% rename from TaskTrackingSystem.WebApp/Components/Pages/Issues.razor rename to TaskTrackingSystem.WebApp/Components/Pages/Features/Issues/Issues.razor index 61a637a51b61b555d0fbdcfc7ac635c6cbe382ae..d5f83e39129cd0b1e892aef01b2d0240ce8c8392 100644 --- a/TaskTrackingSystem.WebApp/Components/Pages/Issues.razor +++ b/TaskTrackingSystem.WebApp/Components/Pages/Features/Issues/Issues.razor @@ -100,72 +100,68 @@
-
- +
+
+ + + + + + + + + + - - - - - - - - - - + + + + + + + + - + @if (issues.Any()) { @foreach (var entry in issues.Select((issue, index) => (issue, index))) { var issue = entry.issue; - - - + + - - - - - - - - } - } - else - { - - } + } + else + { + + }
No.IssueTaskProjectHoursStatusPriorityDatesAssigneeActionsTask CodeIssueHoursStatusPriorityDatesAssigneeActions
@((currentPage - 1) * pageSize + entry.index + 1) -
+ + @GetTaskCode(issue.TaskId) + + + - @if (!string.IsNullOrWhiteSpace(issue.Description)) - { -

@issue.Description

- }
- - @issue.TaskTitle - - @issue.ProjectName -
- @(issue.ActualHours?.ToString("0.##") ?? "-") hr - @issue.StartDate.ToString("dd MMM yyyy") -
+
+ @(issue.ActualHours?.ToString("0.##") ?? "-") hr + @GetStatusLabel(issue.StatusId) + @GetPriorityLabel(issue.PriorityId) -
- @issue.StartDate.ToString("dd MMM yyyy") - Due @issue.DueDate.ToString("dd MMM yyyy") -
+
+ Due: @issue.DueDate.ToString("MMM d, yyyy") + @(!string.IsNullOrWhiteSpace(issue.AssignedToName) ? issue.AssignedToName : "Unassigned") -
+
+
@if (canEditIssue) {
@@ -380,6 +376,7 @@ : "Unknown project"; private IEnumerable filteredProjectMembers => projectMembers.Where(u => u.IsActive); + private static string GetTaskCode(long taskId) => $"TSK-{taskId:D4}"; protected override async Task OnInitializedAsync() { diff --git a/TaskTrackingSystem.WebApp/Components/Pages/ProjectAssign.razor b/TaskTrackingSystem.WebApp/Components/Pages/Features/Projects/ProjectAssign.razor similarity index 100% rename from TaskTrackingSystem.WebApp/Components/Pages/ProjectAssign.razor rename to TaskTrackingSystem.WebApp/Components/Pages/Features/Projects/ProjectAssign.razor diff --git a/TaskTrackingSystem.WebApp/Components/Pages/Projects.razor b/TaskTrackingSystem.WebApp/Components/Pages/Features/Projects/Projects.razor similarity index 100% rename from TaskTrackingSystem.WebApp/Components/Pages/Projects.razor rename to TaskTrackingSystem.WebApp/Components/Pages/Features/Projects/Projects.razor diff --git a/TaskTrackingSystem.WebApp/Components/Pages/EmployeeReport.razor b/TaskTrackingSystem.WebApp/Components/Pages/Features/Reports/EmployeeReport.razor similarity index 75% rename from TaskTrackingSystem.WebApp/Components/Pages/EmployeeReport.razor rename to TaskTrackingSystem.WebApp/Components/Pages/Features/Reports/EmployeeReport.razor index 66d05d689b80c4c862e83e79dbb398d3a0635c64..a1f02734258929376742dd176e333395d24469c2 100644 --- a/TaskTrackingSystem.WebApp/Components/Pages/EmployeeReport.razor +++ b/TaskTrackingSystem.WebApp/Components/Pages/Features/Reports/EmployeeReport.razor @@ -83,6 +83,17 @@ + +
+ + +
@@ -103,14 +114,14 @@

Employee Productivity Chart

- @if (!FilteredList.Any()) + @if (!allEmployees.Any()) {

No data available.

} else {
- @foreach (var emp in FilteredList) + @foreach (var emp in allEmployees) {
@@ -142,9 +153,9 @@ - @if (PagedList.Any()) + @if (pagedEmployees.Any()) { - @foreach (var entry in PagedList.Select((emp, index) => (emp, index))) + @foreach (var entry in pagedEmployees.Select((emp, index) => (emp, index))) { var emp = entry.emp; @@ -196,12 +207,12 @@ - +
} } @@ -210,9 +221,8 @@ @code { private bool isLoading = true; private bool isChartView = false; - private List users = new(); - private List tasks = new(); - private List displayList = new(); + private List allEmployees = new(); + private List pagedEmployees = new(); private PagedResult? reportPage; // Filters @@ -220,6 +230,8 @@ private DateTime? filterStartDate; private DateTime? filterEndDate; private string filterStatus = "0"; + private bool filterAssignToMe = false; + private bool filterAssignToMyTeam = false; // Search and Pagination private string searchInput = ""; @@ -227,13 +239,31 @@ private int currentPage = 1; private int pageSize = 10; + private int TotalPages => reportPage?.TotalPages ?? 0; + private int TotalCount => reportPage?.TotalCount ?? 0; + + protected override async Task OnInitializedAsync() + { + await LoadEmployeeReportData(); + isLoading = false; + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await JS.InvokeVoidAsync("initIcons"); + } + + private void SetView(bool chartView) + { + isChartView = chartView; + } + private async Task ApplyAllFilters() { ApplyPeriodPreset(); searchQuery = searchInput; currentPage = 1; - CalculateMetrics(); - await LoadEmployeeReportPage(); + await LoadEmployeeReportData(); } private async Task ResetAllFilters() @@ -244,77 +274,23 @@ filterStartDate = null; filterEndDate = null; filterStatus = "0"; + filterAssignToMe = false; + filterAssignToMyTeam = false; currentPage = 1; - CalculateMetrics(); - await LoadEmployeeReportPage(); + await LoadEmployeeReportData(); } private async Task HandlePageChanged(int page) { currentPage = page; - await LoadEmployeeReportPage(); + await LoadEmployeeReportData(); } private async Task HandlePageSizeChanged(int size) { pageSize = size; currentPage = 1; - await LoadEmployeeReportPage(); - } - - private IEnumerable FilteredList => - string.IsNullOrWhiteSpace(searchQuery) - ? displayList - : displayList.Where(e => e.FullName.Contains(searchQuery, StringComparison.OrdinalIgnoreCase) - || e.Username.Contains(searchQuery, StringComparison.OrdinalIgnoreCase)); - - private int TotalPages => reportPage?.TotalPages ?? 0; - - private int TotalCount => reportPage?.TotalCount ?? 0; - - private IEnumerable PagedList => - reportPage?.Items.Select(x => new EmployeeMetric - { - FullName = x.FullName, - Username = x.Username, - AssignedCount = x.AssignedCount, - CompletedCount = x.CompletedCount, - Efficiency = x.Efficiency, - OnTimeDeliveryRate = x.OnTimeDeliveryRate - }) ?? Enumerable.Empty(); - - protected override async Task OnInitializedAsync() - { - var client = ApiClient.CreateClient(); - try - { - var usersTask = client.GetFromJsonAsync>("User"); - var tasksTask = client.GetFromJsonAsync>("Task"); - await Task.WhenAll(usersTask, tasksTask); - - users = usersTask.Result ?? new(); - tasks = tasksTask.Result ?? new(); - CalculateMetrics(); - await LoadEmployeeReportPage(); - } - catch (Exception ex) - { - Console.WriteLine($"Error initializing: {ex.Message}"); - } - finally - { - isLoading = false; - } - } - - protected override async Task OnAfterRenderAsync(bool firstRender) - { - await JS.InvokeVoidAsync("initIcons"); - } - - private void SetView(bool chartView) - { - isChartView = chartView; + await LoadEmployeeReportData(); } private void ApplyPeriodPreset() @@ -342,58 +318,7 @@ } } - private void CalculateMetrics() - { - var metrics = new List(); - - foreach (var u in users) - { - // Filter user's tasks - var userTasks = tasks.Where(t => t.AssignedTo == u.Id).ToList(); - - // Apply Period Filter - if (filterStartDate.HasValue) - { - userTasks = userTasks.Where(t => t.CreatedAt >= filterStartDate.Value).ToList(); - } - if (filterEndDate.HasValue) - { - userTasks = userTasks.Where(t => t.CreatedAt <= filterEndDate.Value.AddDays(1)).ToList(); - } - - // Apply Status Filter if matching - if (filterStatus == "overdue") - { - userTasks = userTasks.Where(t => t.DueDate.Date < DateTime.Today && t.StatusId != AppTaskStatus.Done).ToList(); - } - else if (filterStatus != "0" && Enum.TryParse(filterStatus, out var parsedStatus)) - { - userTasks = userTasks.Where(t => t.StatusId == parsedStatus).ToList(); - } - - int assignedCount = userTasks.Count; - int completedCount = userTasks.Count(t => t.StatusId == AppTaskStatus.Done); // Done - double efficiency = assignedCount > 0 ? ((double)completedCount / assignedCount) * 100 : 0; - - var completedTasksList = userTasks.Where(t => t.StatusId == AppTaskStatus.Done).ToList(); - int onTimeCompletedCount = completedTasksList.Count(t => t.CompletedAt.HasValue && t.CompletedAt.Value.Date <= t.DueDate.Date); - double onTimeDeliveryRate = completedCount > 0 ? ((double)onTimeCompletedCount / completedCount) * 100 : 0; - - metrics.Add(new EmployeeMetric - { - FullName = $"{u.FirstName} {u.LastName}".Trim(), - Username = u.Username, - AssignedCount = assignedCount, - CompletedCount = completedCount, - Efficiency = efficiency, - OnTimeDeliveryRate = onTimeDeliveryRate - }); - } - - displayList = metrics.OrderBy(m => m.FullName).ToList(); - } - - private async Task LoadEmployeeReportPage() + private async Task LoadEmployeeReportData() { var client = ApiClient.CreateClient(); try @@ -402,28 +327,41 @@ if (!string.IsNullOrWhiteSpace(searchQuery)) parts.Add($"search={Uri.EscapeDataString(searchQuery)}"); if (filterStartDate.HasValue) parts.Add($"startDate={filterStartDate:yyyy-MM-dd}"); if (filterEndDate.HasValue) parts.Add($"endDate={filterEndDate:yyyy-MM-dd}"); - if (!string.IsNullOrWhiteSpace(filterStatus)) parts.Add($"status={Uri.EscapeDataString(filterStatus)}"); - parts.Add($"page={currentPage}"); - parts.Add($"limit={pageSize}"); + if (!string.IsNullOrWhiteSpace(filterStatus) && filterStatus != "0") parts.Add($"status={Uri.EscapeDataString(filterStatus)}"); + if (filterAssignToMe) parts.Add("assignedToMe=true"); + if (filterAssignToMyTeam) parts.Add("assignedToMyTeam=true"); + + // 1. Fetch full matching list for chart view and exports + var fullUrl = "Report/employee-productivity" + (parts.Count > 0 ? "?" + string.Join("&", parts) : ""); + var fullResult = await client.GetFromJsonAsync>>(fullUrl, Serialization.CaseInsensitive); + if (fullResult != null && fullResult.IsSuccess && fullResult.Value != null) + { + allEmployees = fullResult.Value; + } - var url = "Report/employee-productivity" + (parts.Count > 0 ? "?" + string.Join("&", parts) : ""); - var result = await client.GetFromJsonAsync>(url, Serialization.CaseInsensitive); - if (result != null) + // 2. Fetch paged list for table pagination + var pagedParts = new List(parts); + pagedParts.Add($"page={currentPage}"); + pagedParts.Add($"limit={pageSize}"); + var pagedUrl = "Report/employee-productivity" + (pagedParts.Count > 0 ? "?" + string.Join("&", pagedParts) : ""); + var pagedResult = await client.GetFromJsonAsync>(pagedUrl, Serialization.CaseInsensitive); + if (pagedResult != null) { - reportPage = result; - currentPage = result.Page; - pageSize = result.PageSize; + reportPage = pagedResult; + pagedEmployees = pagedResult.Items.ToList(); + currentPage = pagedResult.Page; + pageSize = pagedResult.PageSize; } } catch (Exception ex) { - Console.WriteLine($"Employee report paging error: {ex.Message}"); + Console.WriteLine($"Error loading employee report: {ex.Message}"); } } private async Task DownloadExcel() { - var rows = FilteredList.Select(emp => new object?[] + var rows = allEmployees.Select(emp => new object?[] { emp.FullName, emp.Username, @@ -463,7 +401,7 @@ $"Generated: {DisplayFormats.Date(DateTime.Today)}" }; - var rows = FilteredList.Select(emp => new[] + var rows = allEmployees.Select(emp => new[] { emp.FullName, emp.Username, @@ -485,14 +423,4 @@ "application/pdf", Convert.ToBase64String(pdfBytes)); } - - private class EmployeeMetric - { - public string FullName { get; set; } = string.Empty; - public string Username { get; set; } = string.Empty; - public int AssignedCount { get; set; } - public int CompletedCount { get; set; } - public double Efficiency { get; set; } - public double OnTimeDeliveryRate { get; set; } - } } diff --git a/TaskTrackingSystem.WebApp/Components/Pages/OverdueTasksReport.razor b/TaskTrackingSystem.WebApp/Components/Pages/Features/Reports/OverdueTasksReport.razor similarity index 55% rename from TaskTrackingSystem.WebApp/Components/Pages/OverdueTasksReport.razor rename to TaskTrackingSystem.WebApp/Components/Pages/Features/Reports/OverdueTasksReport.razor index 482fd2088b23659a28c7513f00cc49f4635c3af6..1e994718f12027fd17a82469cd623b5a297e27a3 100644 --- a/TaskTrackingSystem.WebApp/Components/Pages/OverdueTasksReport.razor +++ b/TaskTrackingSystem.WebApp/Components/Pages/Features/Reports/OverdueTasksReport.razor @@ -115,6 +115,17 @@ + +
+ + +
@@ -128,7 +139,7 @@

- Showing @FilteredTasks.Count() delayed / overdue task(s) + Showing @(overdueReportPage?.TotalCount ?? 0) delayed / overdue task(s)

@@ -145,14 +156,11 @@ - @if (PagedTasks.Any()) + @if (overdueReportTasks.Any()) { - @foreach (var entry in PagedTasks.Select((task, index) => (task, index))) + @foreach (var entry in overdueReportTasks.Select((task, index) => (task, index))) { var task = entry.task; - var daysOverdue = (DateTime.Today - task.DueDate).Days; - var assignee = allUsers.FirstOrDefault(u => u.Id == task.AssignedTo); - var project = allProjects.FirstOrDefault(p => p.Id == task.ProjectId); } @@ -241,53 +249,18 @@ } - - @if (isAdjustModalOpen && selectedTaskForAdjustment != null) - { -
-
-

Adjust Due Date

-

Update the due date for task: @selectedTaskForAdjustment.Title

- -
-
- - -
- - @if (!string.IsNullOrEmpty(adjustError)) - { -
@adjustError
- } - -
- - -
-
-
-
- } @code { private bool isLoading = true; - private List allTasks = new(); - private List allUsers = new(); - private List allProjects = new(); + private List allOverdue = new(); + private List overdueReportTasks = new(); + private PagedResult? overdueReportPage; // Filters private string searchInput = ""; @@ -296,6 +269,8 @@ private int appliedPriority = 0; private string filterDelayType = "all"; private string appliedDelayType = "all"; + private bool filterAssignToMe = false; + private bool filterAssignToMyTeam = false; // Pagination private int currentPage = 1; @@ -307,70 +282,15 @@ private int dueSoonCount = 0; private int avgDaysOverdue = 0; private bool canUpdateTask = false; - private List overdueReportTasks = new(); - private PagedResult? overdueReportPage; - - private IEnumerable FilteredTasks - { - get - { - var today = DateTime.Today; - var result = allTasks.AsEnumerable(); - - // Only show non-completed tasks that are overdue or due soon - result = result.Where(t => t.StatusId != AppTaskStatus.Done); // exclude Done - - if (appliedDelayType == "overdue") - result = result.Where(t => t.DueDate < today); - else if (appliedDelayType == "soon") - result = result.Where(t => t.DueDate >= today && t.DueDate <= today.AddDays(3)); - else - result = result.Where(t => t.DueDate <= today.AddDays(3)); // overdue + due within 3 days - if (!string.IsNullOrWhiteSpace(appliedSearch)) - { - var q = appliedSearch.Trim().ToLower(); - result = result.Where(t => - t.Title.ToLower().Contains(q) || - (t.Description != null && t.Description.ToLower().Contains(q)) || - (allUsers.FirstOrDefault(u => u.Id == t.AssignedTo) is { } usr && - ($"{usr.FirstName} {usr.LastName}").ToLower().Contains(q)) || - (allProjects.FirstOrDefault(p => p.Id == t.ProjectId) is { } prj && - prj.Name.ToLower().Contains(q)) - ); - } - - if (appliedPriority > 0) - result = result.Where(t => (int)t.PriorityId == appliedPriority); - - return result.OrderBy(t => t.Title).ThenBy(t => t.DueDate); - } - } - - private int TotalPages => (int)Math.Ceiling((double)FilteredTasks.Count() / pageSize); - private IEnumerable PagedTasks => FilteredTasks.Skip((currentPage - 1) * pageSize).Take(pageSize); + private int TotalPages => overdueReportPage?.TotalPages ?? 0; protected override async Task OnInitializedAsync() { var authState = await AuthStateProvider.GetAuthenticationStateAsync(); - canUpdateTask = authState.User.IsInRole("Admin") || await MenuAuthorization.HasAccessCodeAsync(authState.User, "Tasks_Update"); - var client = ApiClient.CreateClient(); - try - { - var tasksTask = client.GetFromJsonAsync>("Task", Serialization.CaseInsensitive); - var usersTask = client.GetFromJsonAsync>("User", Serialization.CaseInsensitive); - var projectsTask = client.GetFromJsonAsync>("Project", Serialization.CaseInsensitive); - await System.Threading.Tasks.Task.WhenAll(tasksTask, usersTask, projectsTask); - - allTasks = tasksTask.Result ?? new(); - allUsers = usersTask.Result ?? new(); - allProjects = projectsTask.Result ?? new(); - - ComputeSummary(); - await LoadOverdueReport(); - } - catch (Exception ex) { Console.WriteLine($"Load error: {ex.Message}"); } - finally { isLoading = false; } + canUpdateTask = await MenuAuthorization.HasAccessCodeAsync(authState.User, "Tasks_Update"); + await LoadOverdueReport(); + isLoading = false; } protected override async Task OnAfterRenderAsync(bool firstRender) @@ -378,19 +298,6 @@ await JS.InvokeVoidAsync("initIcons"); } - private void ComputeSummary() - { - var today = DateTime.Today; - var activeTasks = allTasks.Where(t => t.StatusId != AppTaskStatus.Done).ToList(); - var overdueTasks = activeTasks.Where(t => t.DueDate < today).ToList(); - overdueCount = overdueTasks.Count; - criticalCount = overdueTasks.Count(t => t.PriorityId == TaskPriority.High); - dueSoonCount = activeTasks.Count(t => t.DueDate >= today && t.DueDate <= today.AddDays(3)); - avgDaysOverdue = overdueTasks.Any() - ? (int)overdueTasks.Average(t => (today - t.DueDate).Days) - : 0; - } - private async Task LoadOverdueReport() { var client = ApiClient.CreateClient(); @@ -400,22 +307,43 @@ if (!string.IsNullOrWhiteSpace(appliedSearch)) parts.Add($"search={Uri.EscapeDataString(appliedSearch)}"); if (appliedPriority > 0) parts.Add($"priorityId={appliedPriority}"); if (!string.IsNullOrWhiteSpace(appliedDelayType)) parts.Add($"delayType={Uri.EscapeDataString(appliedDelayType)}"); - parts.Add($"page={currentPage}"); - parts.Add($"limit={pageSize}"); + if (filterAssignToMe) parts.Add("assignedToMe=true"); + if (filterAssignToMyTeam) parts.Add("assignedToMyTeam=true"); + + // 1. Fetch full matching list once to compute summary cards and for exports + var fullUrl = "Report/overdue-critical" + (parts.Count > 0 ? "?" + string.Join("&", parts) : ""); + var fullResult = await client.GetFromJsonAsync>>(fullUrl, Serialization.CaseInsensitive); + if (fullResult != null && fullResult.IsSuccess && fullResult.Value != null) + { + allOverdue = fullResult.Value; + var today = DateTime.Today; + var now = DateTime.UtcNow; + + overdueCount = allOverdue.Count(t => t.DueDate < now); + criticalCount = allOverdue.Count(t => t.PriorityName == "High"); + dueSoonCount = allOverdue.Count(t => t.DueDate >= now && t.DueDate <= now.AddDays(3)); + avgDaysOverdue = overdueCount > 0 + ? (int)allOverdue.Where(t => t.DueDate < now).Average(t => t.DaysOverdue) + : 0; + } - var url = "Report/overdue-critical" + (parts.Count > 0 ? "?" + string.Join("&", parts) : ""); - var result = await client.GetFromJsonAsync>(url, Serialization.CaseInsensitive); - if (result != null) + // 2. Fetch paged list for the table view + var pagedParts = new List(parts); + pagedParts.Add($"page={currentPage}"); + pagedParts.Add($"limit={pageSize}"); + var pagedUrl = "Report/overdue-critical" + (pagedParts.Count > 0 ? "?" + string.Join("&", pagedParts) : ""); + var pagedResult = await client.GetFromJsonAsync>(pagedUrl, Serialization.CaseInsensitive); + if (pagedResult != null) { - overdueReportPage = result; - overdueReportTasks = result.Items.ToList(); - currentPage = result.Page; - pageSize = result.PageSize; + overdueReportPage = pagedResult; + overdueReportTasks = pagedResult.Items.ToList(); + currentPage = pagedResult.Page; + pageSize = pagedResult.PageSize; } } catch (Exception ex) { - Console.WriteLine($"Overdue report paging error: {ex.Message}"); + Console.WriteLine($"Overdue report loading error: {ex.Message}"); } } @@ -436,6 +364,8 @@ appliedPriority = 0; filterDelayType = "all"; appliedDelayType = "all"; + filterAssignToMe = false; + filterAssignToMyTeam = false; currentPage = 1; await LoadOverdueReport(); } @@ -455,44 +385,26 @@ private async Task DownloadExcel() { - var rows = FilteredTasks.Select(task => + var client = ApiClient.CreateClient(); + try { - var daysOver = (DateTime.Today - task.DueDate).Days; - var assignee = allUsers.FirstOrDefault(u => u.Id == task.AssignedTo); - var project = allProjects.FirstOrDefault(p => p.Id == task.ProjectId); - var priority = task.PriorityId switch { TaskPriority.High => "High", TaskPriority.Medium => "Medium", _ => "Low" }; - var status = task.StatusId switch { AppTaskStatus.InProgress => "In Progress", AppTaskStatus.Done => "Done", _ => "To Do" }; - - return new object?[] - { - task.Title, - project?.Name ?? string.Empty, - assignee != null ? $"{assignee.FirstName} {assignee.LastName}" : "Unassigned", - task.DueDate, - daysOver, - priority, - status - }; - }); - - var workbookBytes = SpreadsheetReportBuilder.BuildTableWorkbook( - "Overdue Tasks", - "Overdue & Delayed Tasks Report", - new[] { "Task", "Project", "Assignee", "Due Date", "Days Overdue", "Priority", "Status" }, - rows, - new[] + var parts = new List(); + if (!string.IsNullOrWhiteSpace(appliedSearch)) parts.Add($"search={Uri.EscapeDataString(appliedSearch)}"); + if (appliedPriority > 0) parts.Add($"priorityId={appliedPriority}"); + + var url = "Report/overdue-critical/excel" + (parts.Count > 0 ? "?" + string.Join("&", parts) : ""); + var response = await client.GetAsync(url); + if (response.IsSuccessStatusCode) { - $"Search: {(string.IsNullOrWhiteSpace(appliedSearch) ? "All tasks" : appliedSearch)}", - $"Priority: {appliedPriority switch { 0 => "All", 1 => "Low", 2 => "Medium", 3 => "High", _ => appliedPriority.ToString() }}", - $"Delay Type: {appliedDelayType switch { "all" => "All delayed tasks", "overdue" => "Overdue", "soon" => "Due within 3 days", _ => appliedDelayType }}", - $"Generated: {DisplayFormats.Date(DateTime.Today)}" - }); - - await JS.InvokeVoidAsync( - "downloadFileFromBase64", - $"OverdueTasksReport_{DateTime.Today:yyyyMMdd}.xlsx", - "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", - Convert.ToBase64String(workbookBytes)); + var bytes = await response.Content.ReadAsByteArrayAsync(); + await JS.InvokeVoidAsync( + "downloadFileFromBase64", + $"OverdueTasksReport_{DateTime.Today:yyyyMMdd}.xlsx", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + Convert.ToBase64String(bytes)); + } + } + catch (Exception ex) { Console.WriteLine($"Excel download error: {ex.Message}"); } } private async Task DownloadPdf() @@ -505,22 +417,17 @@ $"Generated: {DisplayFormats.Date(DateTime.Today)}" }; - var rows = FilteredTasks.Select(task => + var rows = allOverdue.Select(task => { - var daysOver = (DateTime.Today - task.DueDate).Days; - var assignee = allUsers.FirstOrDefault(u => u.Id == task.AssignedTo); - var project = allProjects.FirstOrDefault(p => p.Id == task.ProjectId); - var priority = task.PriorityId switch { TaskPriority.High => "High", TaskPriority.Medium => "Medium", _ => "Low" }; - var status = task.StatusId switch { AppTaskStatus.InProgress => "In Progress", AppTaskStatus.Done => "Done", _ => "To Do" }; return new[] { task.Title, - project?.Name ?? string.Empty, - assignee != null ? $"{assignee.FirstName} {assignee.LastName}" : "Unassigned", + task.ProjectName, + task.AssignedTo ?? "Unassigned", DisplayFormats.Date(task.DueDate), - daysOver.ToString(), - priority, - status + task.DaysOverdue.ToString(), + task.PriorityName, + task.StatusName }; }); @@ -536,84 +443,4 @@ "application/pdf", Convert.ToBase64String(pdfBytes)); } - - // Adjust Due Date modal variables and methods - private bool isAdjustModalOpen = false; - private TaskDto? selectedTaskForAdjustment; - private DateTime adjustedDueDate = DateTime.Today; - private string adjustError = string.Empty; - private bool isAdjusting = false; - - private void RemindAssignee(TaskDto task) - { - var assigneeName = allUsers.FirstOrDefault(u => u.Id == task.AssignedTo) is { } usr ? $"{usr.FirstName} {usr.LastName}" : "Unassigned"; - Console.WriteLine($"[REMINDER PLACEHOLDER] Sent reminder notification to assignee '{assigneeName}' for overdue task ID {task.Id} ('{task.Title}')"); - } - - private void OpenAdjustDueDateModal(TaskDto task) - { - selectedTaskForAdjustment = task; - adjustedDueDate = task.DueDate; - adjustError = string.Empty; - isAdjustModalOpen = true; - } - - private void CloseAdjustModal() - { - isAdjustModalOpen = false; - selectedTaskForAdjustment = null; - adjustError = string.Empty; - } - - private async Task SaveDueDateAdjustment() - { - if (!canUpdateTask) - { - return; - } - - if (selectedTaskForAdjustment == null) return; - - isAdjusting = true; - adjustError = string.Empty; - var client = ApiClient.CreateClient(); - try - { - var updateDto = new UpdateTaskDto - { - Title = selectedTaskForAdjustment.Title, - Description = selectedTaskForAdjustment.Description, - StatusId = selectedTaskForAdjustment.StatusId, - PriorityId = selectedTaskForAdjustment.PriorityId, - AssignedTo = selectedTaskForAdjustment.AssignedTo, - AssignedBy = selectedTaskForAdjustment.AssignedBy, - DueDate = adjustedDueDate - }; - - var response = await client.PutAsJsonAsync($"Task/{selectedTaskForAdjustment.Id}", updateDto); - if (response.IsSuccessStatusCode) - { - var t = allTasks.FirstOrDefault(x => x.Id == selectedTaskForAdjustment.Id); - if (t != null) - { - t.DueDate = adjustedDueDate; - } - ComputeSummary(); - CloseAdjustModal(); - } - else - { - var errorRes = await response.Content.ReadFromJsonAsync(); - adjustError = errorRes?.ErrorMessage ?? "Failed to update due date. Please try again."; - } - } - catch (Exception ex) - { - adjustError = $"Error: {ex.Message}"; - } - finally - { - isAdjusting = false; - } - } } diff --git a/TaskTrackingSystem.WebApp/Components/Pages/ProjectProgressReport.razor b/TaskTrackingSystem.WebApp/Components/Pages/Features/Reports/ProjectProgressReport.razor similarity index 76% rename from TaskTrackingSystem.WebApp/Components/Pages/ProjectProgressReport.razor rename to TaskTrackingSystem.WebApp/Components/Pages/Features/Reports/ProjectProgressReport.razor index 5de3ef19629b2daba3cc67b2588aba2c2b5e999c..8df7b18d85d04693d8f3b9ca932a5bd87a9fe7df 100644 --- a/TaskTrackingSystem.WebApp/Components/Pages/ProjectProgressReport.razor +++ b/TaskTrackingSystem.WebApp/Components/Pages/Features/Reports/ProjectProgressReport.razor @@ -85,6 +85,17 @@ + +
+ + +
@@ -144,14 +155,14 @@

Project Completion Chart

- @if (!FilteredList.Any()) + @if (!allProjects.Any()) {

No data available.

} else {
- @foreach (var proj in FilteredList) + @foreach (var proj in allProjects) {
@@ -184,9 +195,9 @@
- @if (PagedList.Any()) + @if (pagedProjects.Any()) { - @foreach (var entry in PagedList.Select((proj, index) => (proj, index))) + @foreach (var entry in pagedProjects.Select((proj, index) => (proj, index))) { var proj = entry.proj; @@ -233,9 +244,8 @@ @code { private bool isLoading = true; private bool isChartView = false; - private List projects = new(); - private List tasks = new(); - private List displayList = new(); + private List allProjects = new(); + private List pagedProjects = new(); private PagedResult? reportPage; // Filters @@ -243,6 +253,8 @@ private DateTime? filterStartDate; private DateTime? filterEndDate; private string filterStatus = "0"; + private bool filterAssignToMe = false; + private bool filterAssignToMyTeam = false; // Search and Pagination private string searchInput = ""; @@ -255,13 +267,31 @@ private int aheadOfScheduleCount = 0; private int atRiskCount = 0; + private int TotalPages => reportPage?.TotalPages ?? 0; + private int TotalCount => reportPage?.TotalCount ?? 0; + + protected override async Task OnInitializedAsync() + { + await LoadProjectReportData(); + isLoading = false; + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await JS.InvokeVoidAsync("initIcons"); + } + + private void SetView(bool chartView) + { + isChartView = chartView; + } + private async Task ApplyAllFilters() { ApplyPeriodPreset(); searchQuery = searchInput; currentPage = 1; - CalculateMetrics(); - await LoadProjectReportPage(); + await LoadProjectReportData(); } private async Task ResetAllFilters() @@ -272,78 +302,23 @@ filterStartDate = null; filterEndDate = null; filterStatus = "0"; + filterAssignToMe = false; + filterAssignToMyTeam = false; currentPage = 1; - CalculateMetrics(); - await LoadProjectReportPage(); + await LoadProjectReportData(); } private async Task HandlePageChanged(int page) { currentPage = page; - await LoadProjectReportPage(); + await LoadProjectReportData(); } private async Task HandlePageSizeChanged(int size) { pageSize = size; currentPage = 1; - await LoadProjectReportPage(); - } - - private IEnumerable FilteredList => - string.IsNullOrWhiteSpace(searchQuery) - ? displayList - : displayList.Where(p => p.ProjectName.Contains(searchQuery, StringComparison.OrdinalIgnoreCase)); - - private int TotalPages => reportPage?.TotalPages ?? 0; - - private int TotalCount => reportPage?.TotalCount ?? 0; - - private IEnumerable PagedList => - reportPage?.Items.Select(x => new ProjectMetric - { - ProjectName = x.ProjectName, - StartDate = x.StartDate, - EndDate = x.EndDate, - TotalTasks = x.TotalTasks, - CompletedTasks = x.CompletedTasks, - Progress = x.Progress, - IsAhead = x.IsAhead, - IsAtRisk = x.IsAtRisk - }) ?? Enumerable.Empty(); - - protected override async Task OnInitializedAsync() - { - var client = ApiClient.CreateClient(); - try - { - var projectsTask = client.GetFromJsonAsync>("Project"); - var tasksTask = client.GetFromJsonAsync>("Task"); - await Task.WhenAll(projectsTask, tasksTask); - - projects = projectsTask.Result ?? new(); - tasks = tasksTask.Result ?? new(); - CalculateMetrics(); - await LoadProjectReportPage(); - } - catch (Exception ex) - { - Console.WriteLine($"Error initializing: {ex.Message}"); - } - finally - { - isLoading = false; - } - } - - protected override async Task OnAfterRenderAsync(bool firstRender) - { - await JS.InvokeVoidAsync("initIcons"); - } - - private void SetView(bool chartView) - { - isChartView = chartView; + await LoadProjectReportData(); } private void ApplyPeriodPreset() @@ -371,78 +346,7 @@ } } - private void CalculateMetrics() - { - var metrics = new List(); - var filteredProjects = projects.ToList(); - - // Apply period filter to projects by StartDate - if (filterStartDate.HasValue) - { - filteredProjects = filteredProjects.Where(p => p.StartDate >= filterStartDate.Value).ToList(); - } - if (filterEndDate.HasValue) - { - filteredProjects = filteredProjects.Where(p => p.StartDate <= filterEndDate.Value.AddDays(1)).ToList(); - } - - var today = DateTime.Today; - - foreach (var p in filteredProjects) - { - var projectTasks = tasks.Where(t => t.ProjectId == p.Id).ToList(); - - // Apply filter status on tasks if matching - if (filterStatus == "overdue") - { - projectTasks = projectTasks.Where(t => t.DueDate.Date < today && t.StatusId != AppTaskStatus.Done).ToList(); - } - else if (filterStatus != "0" && Enum.TryParse(filterStatus, out var parsedStatus)) - { - projectTasks = projectTasks.Where(t => t.StatusId == parsedStatus).ToList(); - } - - int totalTasks = projectTasks.Count; - int completedTasks = projectTasks.Count(t => t.StatusId == AppTaskStatus.Done); // Done - double progress = totalTasks > 0 ? ((double)completedTasks / totalTasks) * 100 : 0; - - // Ahead of schedule calculation - double elapsedPct; - if (today < p.StartDate) elapsedPct = 0; - else if (today > p.EndDate) elapsedPct = 100; - else - { - var totalDays = (p.EndDate - p.StartDate).TotalDays; - var elapsedDays = (today - p.StartDate).TotalDays; - elapsedPct = totalDays > 0 ? (elapsedDays / totalDays) * 100 : 100; - } - bool isAhead = progress > elapsedPct && progress > 0 && progress < 100; - - // At risk calculation - bool isAtRisk = (projectTasks.Any(t => t.StatusId != AppTaskStatus.Done && t.DueDate.Date < today)) || (p.EndDate.Date < today && progress < 100); - - metrics.Add(new ProjectMetric - { - ProjectName = p.Name, - StartDate = p.StartDate, - EndDate = p.EndDate, - TotalTasks = totalTasks, - CompletedTasks = completedTasks, - Progress = progress, - IsAhead = isAhead, - IsAtRisk = isAtRisk - }); - } - - displayList = metrics.OrderBy(m => m.ProjectName).ToList(); - - // Calculate card statistics - totalActiveProjects = displayList.Count; - aheadOfScheduleCount = displayList.Count(m => m.IsAhead); - atRiskCount = displayList.Count(m => m.IsAtRisk); - } - - private async Task LoadProjectReportPage() + private async Task LoadProjectReportData() { var client = ApiClient.CreateClient(); try @@ -451,28 +355,45 @@ if (!string.IsNullOrWhiteSpace(searchQuery)) parts.Add($"search={Uri.EscapeDataString(searchQuery)}"); if (filterStartDate.HasValue) parts.Add($"startDate={filterStartDate:yyyy-MM-dd}"); if (filterEndDate.HasValue) parts.Add($"endDate={filterEndDate:yyyy-MM-dd}"); - if (!string.IsNullOrWhiteSpace(filterStatus)) parts.Add($"status={Uri.EscapeDataString(filterStatus)}"); - parts.Add($"page={currentPage}"); - parts.Add($"limit={pageSize}"); + if (!string.IsNullOrWhiteSpace(filterStatus) && filterStatus != "0") parts.Add($"status={Uri.EscapeDataString(filterStatus)}"); + if (filterAssignToMe) parts.Add("assignedToMe=true"); + if (filterAssignToMyTeam) parts.Add("assignedToMyTeam=true"); + + // 1. Fetch full matching list for chart view, cards summary, and exports + var fullUrl = "Report/project-progress" + (parts.Count > 0 ? "?" + string.Join("&", parts) : ""); + var fullResult = await client.GetFromJsonAsync>>(fullUrl, Serialization.CaseInsensitive); + if (fullResult != null && fullResult.IsSuccess && fullResult.Value != null) + { + allProjects = fullResult.Value; + + totalActiveProjects = allProjects.Count; + aheadOfScheduleCount = allProjects.Count(m => m.IsAhead); + atRiskCount = allProjects.Count(m => m.IsAtRisk); + } - var url = "Report/project-progress" + (parts.Count > 0 ? "?" + string.Join("&", parts) : ""); - var result = await client.GetFromJsonAsync>(url, Serialization.CaseInsensitive); - if (result != null) + // 2. Fetch paged list for table pagination + var pagedParts = new List(parts); + pagedParts.Add($"page={currentPage}"); + pagedParts.Add($"limit={pageSize}"); + var pagedUrl = "Report/project-progress" + (pagedParts.Count > 0 ? "?" + string.Join("&", pagedParts) : ""); + var pagedResult = await client.GetFromJsonAsync>(pagedUrl, Serialization.CaseInsensitive); + if (pagedResult != null) { - reportPage = result; - currentPage = result.Page; - pageSize = result.PageSize; + reportPage = pagedResult; + pagedProjects = pagedResult.Items.ToList(); + currentPage = pagedResult.Page; + pageSize = pagedResult.PageSize; } } catch (Exception ex) { - Console.WriteLine($"Project report paging error: {ex.Message}"); + Console.WriteLine($"Error loading project progress report: {ex.Message}"); } } private async Task DownloadExcel() { - var rows = FilteredList.Select(proj => new object?[] + var rows = allProjects.Select(proj => new object?[] { proj.ProjectName, proj.StartDate, @@ -512,7 +433,7 @@ $"Generated: {DisplayFormats.Date(DateTime.Today)}" }; - var rows = FilteredList.Select(proj => new[] + var rows = allProjects.Select(proj => new[] { proj.ProjectName, DisplayFormats.Date(proj.StartDate), @@ -534,16 +455,4 @@ "application/pdf", Convert.ToBase64String(pdfBytes)); } - - private class ProjectMetric - { - public string ProjectName { get; set; } = string.Empty; - public DateTime StartDate { get; set; } - public DateTime EndDate { get; set; } - public int TotalTasks { get; set; } - public int CompletedTasks { get; set; } - public double Progress { get; set; } - public bool IsAhead { get; set; } - public bool IsAtRisk { get; set; } - } } diff --git a/TaskTrackingSystem.WebApp/Components/Pages/TaskReport.razor b/TaskTrackingSystem.WebApp/Components/Pages/Features/Reports/TaskReport.razor similarity index 95% rename from TaskTrackingSystem.WebApp/Components/Pages/TaskReport.razor rename to TaskTrackingSystem.WebApp/Components/Pages/Features/Reports/TaskReport.razor index c21de7393636e46be4766339096da38fa42e4c33..75be181b940a5768d02630fcd79049f232e930fb 100644 --- a/TaskTrackingSystem.WebApp/Components/Pages/TaskReport.razor +++ b/TaskTrackingSystem.WebApp/Components/Pages/Features/Reports/TaskReport.razor @@ -129,10 +129,20 @@ + +
+ + +
- @if (isLoading) {
@@ -302,12 +312,12 @@
@((currentPage - 1) * pageSize + entry.index + 1) @@ -167,17 +175,17 @@ - @(project?.Name ?? "—") + @task.ProjectName - @if (assignee != null) + @if (!string.IsNullOrEmpty(task.AssignedTo)) {
- @(assignee.FirstName.Length > 0 ? assignee.FirstName[0].ToString().ToUpper() : "")@(assignee.LastName.Length > 0 ? assignee.LastName[0].ToString().ToUpper() : "") + @(task.AssignedTo.Length > 0 ? task.AssignedTo[0].ToString().ToUpper() : "") - @assignee.FirstName @assignee.LastName + @task.AssignedTo
} else @@ -186,21 +194,21 @@ }
- + @DisplayFormats.Date(task.DueDate) - @if (daysOverdue > 0) + @if (task.DaysOverdue > 0) { - @daysOverdue day@(daysOverdue != 1 ? "s" : "") + @task.DaysOverdue day@(task.DaysOverdue != 1 ? "s" : "") } else { - var daysDue = Math.Abs(daysOverdue); + var daysDue = Math.Abs(task.DaysOverdue); Due in @daysDue day@(daysDue != 1 ? "s" : "") @@ -209,23 +217,23 @@ @{ - var (priorityLabel, priorityClass) = task.PriorityId switch { - TaskPriority.High => ("High", "bg-rose-50 text-rose-700"), - TaskPriority.Medium => ("Medium", "bg-amber-50 text-amber-700"), - _ => ("Low", "bg-slate-100 text-slate-600") + var priorityClass = task.PriorityName switch { + "High" => "bg-rose-50 text-rose-700", + "Medium" => "bg-amber-50 text-amber-700", + _ => "bg-slate-100 text-slate-600" }; } - @priorityLabel + @task.PriorityName @{ - var (statusLabel, statusClass) = task.StatusId switch { - AppTaskStatus.InProgress => ("In Progress", "bg-blue-50 text-blue-700"), - AppTaskStatus.Done => ("Done", "bg-emerald-50 text-emerald-700"), - _ => ("To Do", "bg-slate-100 text-slate-600") + var statusClass = task.StatusName switch { + "In Progress" => "bg-blue-50 text-blue-700", + "Done" => "bg-emerald-50 text-emerald-700", + _ => "bg-slate-100 text-slate-600" }; } - @statusLabel + @task.StatusName
- +
} } @@ -329,6 +339,8 @@ private string projectSearchInput = string.Empty; private string projectSearchQuery = string.Empty; private bool showProjectSuggestions; + private bool filterAssignToMe = false; + private bool filterAssignToMyTeam = false; // Search and Pagination private string searchInput = ""; @@ -362,6 +374,8 @@ projectSearchInput = string.Empty; projectSearchQuery = string.Empty; showProjectSuggestions = false; + filterAssignToMe = false; + filterAssignToMyTeam = false; currentPage = 1; await LoadTaskReport(); } @@ -510,6 +524,8 @@ if (filterEndDate.HasValue) parts.Add($"endDate={filterEndDate:yyyy-MM-dd}"); if (!string.IsNullOrEmpty(filterStatus)) parts.Add($"status={Uri.EscapeDataString(filterStatus)}"); if (filterProjectId > 0) parts.Add($"projectId={filterProjectId}"); + if (filterAssignToMe) parts.Add("assignedToMe=true"); + if (filterAssignToMyTeam) parts.Add("assignedToMyTeam=true"); if (includePaging) { parts.Add($"page={currentPage}"); diff --git a/TaskTrackingSystem.WebApp/Components/Pages/Features/Reports/TimeTrackingReport.razor b/TaskTrackingSystem.WebApp/Components/Pages/Features/Reports/TimeTrackingReport.razor new file mode 100644 index 0000000000000000000000000000000000000000..0b11420911deedbc7f408845762a4b1d53d270db --- /dev/null +++ b/TaskTrackingSystem.WebApp/Components/Pages/Features/Reports/TimeTrackingReport.razor @@ -0,0 +1,494 @@ +@page "/reports/timesheet" +@using Microsoft.AspNetCore.Authorization +@using TaskTrackingSystem.Shared +@using TaskTrackingSystem.Shared.Models.Issue +@using TaskTrackingSystem.Shared.Models.User +@using TaskTrackingSystem.Shared.Models.Project +@using System.Text +@rendermode @(new InteractiveServerRenderMode(prerender: false)) +@attribute [Authorize] +@inject ApiClientService ApiClient +@inject IJSRuntime JS +@inject AuthenticationStateProvider AuthStateProvider +@inject MenuAuthorizationService MenuAuthorization + +Time & Effort Tracking Report + +
+ +
+
+

Time & Effort Tracking

+

Review estimated hours, completion timelines, and resource effort distribution across issues.

+
+
+ + +
+
+ + + @if (!isLoading) + { +
+
+
+
+

Total Estimated Effort

+

@totalHours.ToString("N1") hrs

+
+ + + +
+
+
+
+
+

Completed Effort

+

@completedHours.ToString("N1") hrs

+
+ + + +
+
+
+
+
+

Active Effort (In-Flight)

+

@activeHours.ToString("N1") hrs

+
+ + + +
+
+
+
+
+

Overdue Effort

+

@overdueHours.ToString("N1") hrs

+
+ + + +
+
+
+ } + + +
+
+
+
+ + +
+
+ + +
+
+ + +
+ + +
+ +
+ + +
+
+
+ + @if (isLoading) + { + + } + else + { + +
+
+

+ Showing @FilteredIssues.Count() timesheet issue entry(s) +

+
+ + + + + + + + + + + + + + @if (PagedIssues.Any()) + { + @foreach (var entry in PagedIssues.Select((issue, index) => (issue, index))) + { + var issue = entry.issue; + + + + + + + + + + } + } + else + { + + } + +
No.IssueTask & ProjectAssigneeEst. HoursDue DateStatus
@((currentPage - 1) * pageSize + entry.index + 1) +
+

@issue.Title

+ @if (!string.IsNullOrEmpty(issue.Description)) + { +

@issue.Description

+ } +
+
+
@issue.TaskTitle
+
@issue.ProjectName
+
+ @if (!string.IsNullOrEmpty(issue.AssignedToName)) + { +
+ + @(issue.AssignedToName.Length > 0 ? issue.AssignedToName[0].ToString().ToUpper() : "") + + @issue.AssignedToName +
+ } + else + { + Unassigned + } +
+ @((issue.EstimatedHours ?? 0).ToString("N1")) hrs + + @DisplayFormats.Date(issue.DueDate) + + @{ + var (statusLabel, statusClass) = issue.StatusId switch { + AppTaskStatus.InProgress => ("In Progress", "bg-blue-50 text-blue-700"), + AppTaskStatus.Done => ("Completed", "bg-emerald-50 text-emerald-700"), + _ => ("To Do", "bg-slate-100 text-slate-600") + }; + } + @statusLabel +
+ + + +
+ } +
+ +@code { + private bool isLoading = true; + private List allIssues = new(); + private List allProjects = new(); + + // Filters + private string searchInput = ""; + private string appliedSearch = ""; + private long filterProjectId = 0; + private long appliedProjectId = 0; + private string filterStatus = "0"; + private string appliedStatus = "0"; + private bool filterAssignToMe = false; + private bool filterAssignToMyTeam = false; + + // Current User & Team + private long currentUserId = 0; + private List myTeamUserIds = new(); + + // Pagination + private int currentPage = 1; + private int pageSize = 10; + + // Metrics + private decimal totalHours = 0; + private decimal completedHours = 0; + private decimal activeHours = 0; + private decimal overdueHours = 0; + + private int TotalPages => (int)Math.Ceiling((double)FilteredIssues.Count() / pageSize); + + private IEnumerable FilteredIssues + { + get + { + var result = allIssues.AsEnumerable(); + + if (appliedProjectId > 0) + result = result.Where(i => i.ProjectId == appliedProjectId); + + if (appliedStatus != "0" && Enum.TryParse(appliedStatus, out var status)) + result = result.Where(i => i.StatusId == status); + + if (!string.IsNullOrWhiteSpace(appliedSearch)) + { + var q = appliedSearch.Trim().ToLower(); + result = result.Where(i => + i.Title.ToLower().Contains(q) || + (i.Description != null && i.Description.ToLower().Contains(q)) || + (i.AssignedToName != null && i.AssignedToName.ToLower().Contains(q)) || + i.TaskTitle.ToLower().Contains(q) + ); + } + + if (filterAssignToMe || filterAssignToMyTeam) + { + result = result.Where(i => + (filterAssignToMe && i.AssignedTo == currentUserId) || + (filterAssignToMyTeam && i.AssignedTo.HasValue && myTeamUserIds.Contains(i.AssignedTo.Value)) + ); + } + + return result.OrderBy(i => i.Title); + } + } + + private IEnumerable PagedIssues => FilteredIssues.Skip((currentPage - 1) * pageSize).Take(pageSize); + + protected override async Task OnInitializedAsync() + { + var client = ApiClient.CreateClient(); + try + { + var issuesTask = client.GetFromJsonAsync>>("Report/issues", Serialization.CaseInsensitive); + var projectsTask = client.GetFromJsonAsync>("Project", Serialization.CaseInsensitive); + + // Get current user ID + var authState = await AuthStateProvider.GetAuthenticationStateAsync(); + var user = authState.User; + if (user.Identity?.IsAuthenticated == true) + { + var idStr = user.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value; + if (long.TryParse(idStr, out var parsedId)) + { + currentUserId = parsedId; + } + } + + // Get team IDs + try + { + var teamTask = client.GetFromJsonAsync>("User/my-team-user-ids", Serialization.CaseInsensitive); + await Task.WhenAll(issuesTask, projectsTask, teamTask); + myTeamUserIds = teamTask.Result ?? new(); + } + catch + { + await Task.WhenAll(issuesTask, projectsTask); + } + + var issuesResult = issuesTask.Result; + if (issuesResult?.IsSuccess == true && issuesResult.Value != null) + { + allIssues = issuesResult.Value; + } + allProjects = projectsTask.Result ?? new(); + + ComputeMetrics(); + } + catch (Exception ex) + { + Console.WriteLine($"Error fetching timesheet data: {ex.Message}"); + } + finally + { + isLoading = false; + } + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await JS.InvokeVoidAsync("initIcons"); + } + + private void ComputeMetrics() + { + var today = DateTime.Today; + var list = FilteredIssues.ToList(); + totalHours = list.Sum(i => i.EstimatedHours ?? 0); + completedHours = list.Where(i => i.StatusId == AppTaskStatus.Done).Sum(i => i.EstimatedHours ?? 0); + activeHours = list.Where(i => i.StatusId != AppTaskStatus.Done).Sum(i => i.EstimatedHours ?? 0); + overdueHours = list.Where(i => i.StatusId != AppTaskStatus.Done && i.DueDate < today).Sum(i => i.EstimatedHours ?? 0); + } + + private Task ApplyFilters() + { + appliedSearch = searchInput; + appliedProjectId = filterProjectId; + appliedStatus = filterStatus; + currentPage = 1; + ComputeMetrics(); + return Task.CompletedTask; + } + + private Task ResetFilters() + { + searchInput = ""; + appliedSearch = ""; + filterProjectId = 0; + appliedProjectId = 0; + filterStatus = "0"; + appliedStatus = "0"; + filterAssignToMe = false; + filterAssignToMyTeam = false; + currentPage = 1; + ComputeMetrics(); + return Task.CompletedTask; + } + + private Task HandlePageChanged(int page) + { + currentPage = page; + return Task.CompletedTask; + } + + private Task HandlePageSizeChanged(int size) + { + pageSize = size; + currentPage = 1; + return Task.CompletedTask; + } + + private async Task DownloadExcel() + { + var rows = FilteredIssues.Select(issue => + { + var statusLabel = issue.StatusId switch { + AppTaskStatus.InProgress => "In Progress", + AppTaskStatus.Done => "Completed", + _ => "To Do" + }; + + return new object?[] + { + issue.Title, + issue.TaskTitle, + issue.ProjectName, + issue.AssignedToName ?? "Unassigned", + issue.EstimatedHours ?? 0, + issue.DueDate, + statusLabel + }; + }); + + var workbookBytes = SpreadsheetReportBuilder.BuildTableWorkbook( + "Time Effort Tracking", + "Timesheet Effort Report", + new[] { "Issue", "Task", "Project", "Assignee", "Estimated Effort (Hours)", "Due Date", "Status" }, + rows, + new[] + { + $"Search: {(string.IsNullOrWhiteSpace(appliedSearch) ? "All" : appliedSearch)}", + $"Project: {(appliedProjectId > 0 ? allProjects.FirstOrDefault(p => p.Id == appliedProjectId)?.Name ?? "All" : "All")}", + $"Status: {(appliedStatus != "0" ? appliedStatus : "All")}", + $"Assigned To Me: {(filterAssignToMe ? "Yes" : "No")}", + $"Assigned To My Team: {(filterAssignToMyTeam ? "Yes" : "No")}", + $"Generated: {DisplayFormats.Date(DateTime.Today)}" + }); + + await JS.InvokeVoidAsync( + "downloadFileFromBase64", + $"TimesheetEffortReport_{DateTime.Today:yyyyMMdd}.xlsx", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + Convert.ToBase64String(workbookBytes)); + } + + private async Task DownloadPdf() + { + var summaryLines = new List + { + $"Search: {(string.IsNullOrWhiteSpace(appliedSearch) ? "All" : appliedSearch)}", + $"Project: {(appliedProjectId > 0 ? allProjects.FirstOrDefault(p => p.Id == appliedProjectId)?.Name ?? "All" : "All")}", + $"Status: {(appliedStatus != "0" ? appliedStatus : "All")}", + $"Assigned To Me: {(filterAssignToMe ? "Yes" : "No")}", + $"Assigned To My Team: {(filterAssignToMyTeam ? "Yes" : "No")}", + $"Total Effort: {totalHours.ToString("N1")} hrs", + $"Completed Effort: {completedHours.ToString("N1")} hrs", + $"Generated: {DisplayFormats.Date(DateTime.Today)}" + }; + + var rows = FilteredIssues.Select(issue => + { + var statusLabel = issue.StatusId switch { + AppTaskStatus.InProgress => "In Progress", + AppTaskStatus.Done => "Completed", + _ => "To Do" + }; + + return new[] + { + issue.Title, + issue.TaskTitle, + issue.ProjectName, + issue.AssignedToName ?? "Unassigned", + (issue.EstimatedHours ?? 0).ToString("N1") + " hrs", + DisplayFormats.Date(issue.DueDate), + statusLabel + }; + }); + + var pdfBytes = SimplePdfReportBuilder.BuildTableReport( + "Timesheet Effort Tracking Report", + summaryLines, + new[] { "Issue", "Task", "Project", "Assignee", "Est. Effort", "Due Date", "Status" }, + rows); + + await JS.InvokeVoidAsync( + "downloadFileFromBase64", + $"TimesheetEffortReport_{DateTime.Today:yyyyMMdd}.pdf", + "application/pdf", + Convert.ToBase64String(pdfBytes)); + } +} diff --git a/TaskTrackingSystem.WebApp/Components/Pages/Roles.razor b/TaskTrackingSystem.WebApp/Components/Pages/Features/Roles/Roles.razor similarity index 100% rename from TaskTrackingSystem.WebApp/Components/Pages/Roles.razor rename to TaskTrackingSystem.WebApp/Components/Pages/Features/Roles/Roles.razor diff --git a/TaskTrackingSystem.WebApp/Components/Pages/KanbanBoard.razor b/TaskTrackingSystem.WebApp/Components/Pages/Features/Tasks/KanbanBoard.razor similarity index 100% rename from TaskTrackingSystem.WebApp/Components/Pages/KanbanBoard.razor rename to TaskTrackingSystem.WebApp/Components/Pages/Features/Tasks/KanbanBoard.razor diff --git a/TaskTrackingSystem.WebApp/Components/Pages/TaskAssign.razor b/TaskTrackingSystem.WebApp/Components/Pages/Features/Tasks/TaskAssign.razor similarity index 100% rename from TaskTrackingSystem.WebApp/Components/Pages/TaskAssign.razor rename to TaskTrackingSystem.WebApp/Components/Pages/Features/Tasks/TaskAssign.razor diff --git a/TaskTrackingSystem.WebApp/Components/Pages/TaskDetails.razor b/TaskTrackingSystem.WebApp/Components/Pages/Features/Tasks/TaskDetails.razor similarity index 100% rename from TaskTrackingSystem.WebApp/Components/Pages/TaskDetails.razor rename to TaskTrackingSystem.WebApp/Components/Pages/Features/Tasks/TaskDetails.razor diff --git a/TaskTrackingSystem.WebApp/Components/Pages/Tasks.razor b/TaskTrackingSystem.WebApp/Components/Pages/Features/Tasks/Tasks.razor similarity index 96% rename from TaskTrackingSystem.WebApp/Components/Pages/Tasks.razor rename to TaskTrackingSystem.WebApp/Components/Pages/Features/Tasks/Tasks.razor index f56e56aab916f93ec464cb7c1d6c3a44f0784c73..85fca14485ed7a6dbffeef7320e6f867e64133a3 100644 --- a/TaskTrackingSystem.WebApp/Components/Pages/Tasks.razor +++ b/TaskTrackingSystem.WebApp/Components/Pages/Features/Tasks/Tasks.razor @@ -82,11 +82,21 @@
- +
+ + + + + + + + + + - - + + @@ -102,30 +112,32 @@ { var task = entry.task; - - + - - - - + -
No.TitleTask CodeTask Status Priority Due Date
@((currentPage - 1) * pageSize + entry.index + 1) + + @GetTaskCode(task.Id) + @task.Title @if (!string.IsNullOrEmpty(task.Description)) { -

@task.Description

+

@task.Description

}
+ @GetStatusLabel(task.StatusId) + @GetPriorityLabel(task.PriorityId) @DisplayFormats.Date(task.DueDate) + @DisplayFormats.Date(task.DueDate) @{ var proj = projects.FirstOrDefault(p => p.Id == task.ProjectId); } @(proj?.Name ?? $"#{task.ProjectId}") + @{ var user = allUsers.FirstOrDefault(u => u.Id == task.AssignedTo); } @if (user != null) { @@ -162,7 +174,7 @@ } else { - + }
@@ -967,6 +979,8 @@ _ => "Unknown" }; + private static string GetTaskCode(long taskId) => $"TSK-{taskId:D4}"; + private string GetPriorityBadge(TaskPriority priorityId) => priorityId switch { TaskPriority.Low => "bg-slate-100 text-slate-600", diff --git a/TaskTrackingSystem.WebApp/Components/Pages/Users.razor b/TaskTrackingSystem.WebApp/Components/Pages/Features/Users/Users.razor similarity index 100% rename from TaskTrackingSystem.WebApp/Components/Pages/Users.razor rename to TaskTrackingSystem.WebApp/Components/Pages/Features/Users/Users.razor diff --git a/TaskTrackingSystem.WebApp/Components/_Imports.razor b/TaskTrackingSystem.WebApp/Components/_Imports.razor index 115643dac2fab09ef685ecccbeffae07187f9eb4..c524df8241f6c81f28e8419663edb1d606a67415 100644 --- a/TaskTrackingSystem.WebApp/Components/_Imports.razor +++ b/TaskTrackingSystem.WebApp/Components/_Imports.razor @@ -22,3 +22,14 @@ @using Microsoft.AspNetCore.Components.Authorization @using Microsoft.AspNetCore.SignalR.Client @using TaskTrackingSystem.Shared.Enums +@using TaskTrackingSystem.WebApp.Components.Pages.Features.Dashboard +@using TaskTrackingSystem.WebApp.Components.Pages.Features.Tasks +@using TaskTrackingSystem.WebApp.Components.Pages.Features.Projects +@using TaskTrackingSystem.WebApp.Components.Pages.Features.Issues +@using TaskTrackingSystem.WebApp.Components.Pages.Features.Reports +@using TaskTrackingSystem.WebApp.Components.Pages.Features.Roles +@using TaskTrackingSystem.WebApp.Components.Pages.Features.Users +@using TaskTrackingSystem.WebApp.Components.Pages.Features.Auth +@using TaskTrackingSystem.WebApp.Components.Pages.Features.Audit +@using TaskTrackingSystem.WebApp.Components.Pages.Features.Common + diff --git a/TaskTrackingSystem.WebApp/MenuAuthorizationService.cs b/TaskTrackingSystem.WebApp/MenuAuthorizationService.cs index ac89a2c5cec0c3f6b19fe100e190aa67cd23efbc..aa903704b8160b88addc5bca0e502403b1cfc4ba 100644 --- a/TaskTrackingSystem.WebApp/MenuAuthorizationService.cs +++ b/TaskTrackingSystem.WebApp/MenuAuthorizationService.cs @@ -67,7 +67,7 @@ public class MenuAuthorizationService var cleanRelative = relativePath.Split('?')[0].Trim('/'); if (cleanRelative.Equals("audit-logs", StringComparison.OrdinalIgnoreCase)) { - return user.IsInRole("Admin"); + return _sessionState.CachedAccessCodes != null && _sessionState.CachedAccessCodes.Contains("Roles_List"); } var roleKey = GetRoleCacheKey(user);