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