using ClosedXML.Excel; using Microsoft.EntityFrameworkCore; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; using TaskTrackingSystem.Database.AppDbContextModels; using TaskTrackingSystem.Shared; using TaskTrackingSystem.Shared.Models.Report; using TaskTrackingSystem.Shared.Models.Issue; using TaskTrackingSystem.Shared.Enums; using TaskTrackingSystem.WebApi.Infrastructure; namespace TaskTrackingSystem.WebApi.Features.Report { public class ReportService { private readonly AppDbContext _db; private static readonly Dictionary StatusMap = new() { { AppTaskStatus.Todo, "To Do" }, { AppTaskStatus.InProgress, "In Progress" }, { AppTaskStatus.Done, "Done" } }; private static readonly Dictionary PriorityMap = new() { { TaskPriority.Low, "Low" }, { TaskPriority.Medium, "Medium" }, { TaskPriority.High, "High" } }; public ReportService(AppDbContext db) { _db = db; } public async Task>> GetIssuesReportAsync(long roleId, long currentUserId) { var isAdmin = await DataScopeAuthorization.IsAdminScopeAsync(_db, roleId); var isManager = await DataScopeAuthorization.IsManagerScopeAsync(_db, roleId); var list = await BuildAccessibleIssueQuery(isAdmin, isManager, currentUserId) .Include(i => i.Task) .ThenInclude(t => t.Project) .Include(i => i.AssignedToNavigation) .OrderBy(i => i.DueDate) .ThenBy(i => i.Title) .Select(i => new IssueDto { Id = i.Id, TaskId = i.TaskId, TaskTitle = i.Task.Title, ProjectId = i.Task.ProjectId, ProjectName = i.Task.Project.Name, Title = i.Title, Description = i.Description, AssignedTo = i.AssignedTo, AssignedToName = i.AssignedToNavigation != null ? i.AssignedToNavigation.FirstName + " " + i.AssignedToNavigation.LastName : null, EstimatedHours = i.EstimatedHours, ActualHours = i.ActualHours, DelayReason = i.DelayReason, IsBlocked = i.IsBlocked, BlockedBy = i.BlockedBy, EscalationLevel = i.EscalationLevel, StartDate = i.StartDate, DueDate = i.DueDate, StatusId = i.StatusId, PriorityId = i.PriorityId, CreatedAt = i.CreatedAt ?? DateTime.UtcNow, UpdatedAt = i.UpdatedAt }) .ToListAsync(); return Result>.Success(list); } private IQueryable BuildAccessibleTaskQuery(bool isAdmin, bool isManager, long currentUserId) { return _db.Tasks.Where(t => t.IsDeleted != true && !t.IsArchived); } private IQueryable BuildAccessibleProjectQuery(bool isAdmin, long currentUserId) { return _db.Projects.Where(p => p.IsDeleted != true); } private IQueryable BuildAccessibleUserQuery(bool isAdmin, long currentUserId) { return _db.Users.Where(u => !u.IsDeleted); } private IQueryable BuildAccessibleIssueQuery(bool isAdmin, bool isManager, long currentUserId) { var query = _db.Issues .Where(i => i.IsDeleted != true && i.Task.IsDeleted != true && i.Task.Project.IsDeleted != true); if (isAdmin) { return query; } if (isManager) { return query.Where(i => i.AssignedTo == currentUserId || i.CreatedBy == currentUserId || i.Task.Project.ProjectMembers.Any(pm => pm.UserId == currentUserId)); } return query.Where(i => i.AssignedTo == currentUserId || i.CreatedBy == currentUserId); } private static PagedResult MapPagedResult( PagedResult source, Func map) { return new PagedResult { Items = source.Items.Select(map).ToList(), TotalCount = source.TotalCount, Page = source.Page, PageSize = source.PageSize, TotalPages = source.TotalPages }; } private async Task> GetTeamUserIdsAsync(long currentUserId) { var myProjectIds = await _db.ProjectMembers .Where(pm => pm.UserId == currentUserId) .Select(pm => pm.ProjectId) .Distinct() .ToListAsync(); return await _db.ProjectMembers .Where(pm => myProjectIds.Contains(pm.ProjectId) && pm.UserId != currentUserId) .Select(pm => pm.UserId) .Distinct() .ToListAsync(); } private async Task> BuildTaskReportQueryAsync( bool isAdmin, bool isManager, long currentUserId, DateTime? startDate, DateTime? endDate, string? status, int? projectId, bool? assignedToMe, bool? assignedToMyTeam) { var query = BuildAccessibleTaskQuery(isAdmin, isManager, currentUserId) .Include(t => t.Project) .Include(t => t.AssignedToNavigation) .Include(t => t.AssignedByNavigation) .Where(t => t.IsDeleted != true); if (startDate.HasValue) query = query.Where(t => t.CreatedAt >= startDate.Value.Date); if (endDate.HasValue) query = query.Where(t => t.CreatedAt < endDate.Value.Date.AddDays(1)); if (projectId.HasValue) query = query.Where(t => t.ProjectId == projectId.Value); if (!string.IsNullOrWhiteSpace(status)) { var sl = status.Trim().ToLower(); if (sl == "uncompleted") { query = query.Where(t => t.StatusId != AppTaskStatus.Done); } else if (sl == "overdue") { query = query.Where(t => t.DueDate < DateTime.Today && t.StatusId != AppTaskStatus.Done); } else { AppTaskStatus? sid = sl switch { "to do" => AppTaskStatus.Todo, "in progress" => AppTaskStatus.InProgress, "done" => AppTaskStatus.Done, _ => null }; if (sid.HasValue) query = query.Where(t => t.StatusId == sid.Value); else if (Enum.TryParse(status, true, out var parsedStatus)) query = query.Where(t => t.StatusId == parsedStatus); } } if (assignedToMe == true || assignedToMyTeam == true) { var teamIds = assignedToMyTeam == true ? await GetTeamUserIdsAsync(currentUserId) : new List(); query = query.Where(t => (assignedToMe == true && t.AssignedTo == currentUserId) || (assignedToMyTeam == true && t.AssignedTo.HasValue && teamIds.Contains(t.AssignedTo.Value)) ); } return query; } private static TaskReportDto MapTaskReport(TaskTrackingSystem.Database.AppDbContextModels.Task t) { return new TaskReportDto { TaskId = t.Id, Title = t.Title, Description = t.Description, ProjectId = t.ProjectId, ProjectName = t.Project?.Name ?? string.Empty, StatusId = t.StatusId, StatusName = StatusMap.TryGetValue(t.StatusId, out var s) ? s : $"Status {t.StatusId}", PriorityId = t.PriorityId, PriorityName = PriorityMap.TryGetValue(t.PriorityId, out var p) ? p : $"Priority {t.PriorityId}", AssignedToUserId = t.AssignedTo, AssignedToUser = t.AssignedToNavigation != null ? $"{t.AssignedToNavigation.FirstName} {t.AssignedToNavigation.LastName}" : null, AssignedByUser = t.AssignedByNavigation != null ? $"{t.AssignedByNavigation.FirstName} {t.AssignedByNavigation.LastName}" : null, DueDate = t.DueDate, CreatedAt = t.CreatedAt ?? DateTime.UtcNow, LastActivity = t.UpdatedAt ?? t.CreatedAt ?? DateTime.UtcNow }; } private async System.Threading.Tasks.Task EnrichTaskReportsAsync(List reports) { if (reports.Count == 0) { return; } var today = DateTime.Today; var taskIds = reports.Select(r => r.TaskId).ToList(); var issues = await _db.Issues .Where(i => i.IsDeleted != true && taskIds.Contains(i.TaskId)) .Select(i => new { i.TaskId, i.Title, i.StatusId, i.PriorityId, i.DueDate, i.UpdatedAt, i.CreatedAt, i.IsBlocked }) .ToListAsync(); foreach (var report in reports) { var taskIssues = issues.Where(i => i.TaskId == report.TaskId).ToList(); report.OpenIssues = taskIssues.Count(i => i.StatusId != AppTaskStatus.Done); report.OverdueIssues = taskIssues.Count(i => i.StatusId != AppTaskStatus.Done && i.DueDate.Date < today); report.IssueSummary = taskIssues.Count == 0 ? "No issues" : $"{report.OpenIssues} open, {report.OverdueIssues} overdue, {taskIssues.Count(i => i.IsBlocked)} blocked"; report.LastActivity = taskIssues .Select(i => i.UpdatedAt ?? i.CreatedAt ?? report.LastActivity) .DefaultIfEmpty(report.LastActivity) .Max(); report.TaskHealth = report.StatusId == AppTaskStatus.Done ? "Healthy" : report.OverdueIssues > 0 || report.DueDate.Date < today || taskIssues.Any(i => i.IsBlocked) ? "Critical" : report.PriorityId == TaskPriority.High || report.DueDate.Date <= today.AddDays(3) ? "At Risk" : "Healthy"; } } private async System.Threading.Tasks.Task EnrichOverdueTaskReportsAsync(List reports) { if (reports.Count == 0) { return; } var today = DateTime.Today; var taskIds = reports.Select(r => r.TaskId).ToList(); var issues = await _db.Issues .Where(i => i.IsDeleted != true && taskIds.Contains(i.TaskId)) .Select(i => new { i.TaskId, i.StatusId, i.DueDate, i.DelayReason, i.IsBlocked, i.BlockedBy, i.EscalationLevel }) .ToListAsync(); foreach (var report in reports) { var taskIssues = issues.Where(i => i.TaskId == report.TaskId).ToList(); report.OverdueIssues = taskIssues.Count(i => i.StatusId != AppTaskStatus.Done && i.DueDate.Date < today); var riskIssue = taskIssues .OrderByDescending(i => i.IsBlocked) .ThenByDescending(i => i.EscalationLevel) .ThenBy(i => i.DueDate) .FirstOrDefault(); report.DelayReason = riskIssue?.DelayReason ?? (report.DaysOverdue > 0 ? "Past due date" : "High priority due soon"); report.BlockedBy = riskIssue?.BlockedBy ?? string.Empty; report.EscalationLevel = riskIssue?.EscalationLevel ?? 0; } } // ——— Legacy endpoints (kept for backward compatibility) ——— public async Task>> GetTasksReportAsync( DateTime? startDate, DateTime? endDate, string? status, int? projectId, long roleId, long currentUserId, bool? assignedToMe = null, bool? assignedToMyTeam = null) { var isAdmin = await DataScopeAuthorization.IsAdminScopeAsync(_db, roleId); var isManager = await DataScopeAuthorization.IsManagerScopeAsync(_db, roleId); var query = await BuildTaskReportQueryAsync(isAdmin, isManager, currentUserId, startDate, endDate, status, projectId, assignedToMe, assignedToMyTeam); var tasks = await query .OrderBy(t => t.DueDate) .ThenByDescending(t => t.CreatedAt ?? DateTime.UtcNow) .ToListAsync(); var list = tasks.Select(MapTaskReport).ToList(); await EnrichTaskReportsAsync(list); return Result>.Success(list); } public async Task> GetPagedTasksReportAsync( DateTime? startDate, DateTime? endDate, string? status, int? projectId, long roleId, long currentUserId, int page, int pageSize, bool? assignedToMe = null, bool? assignedToMyTeam = null) { var isAdmin = await DataScopeAuthorization.IsAdminScopeAsync(_db, roleId); var isManager = await DataScopeAuthorization.IsManagerScopeAsync(_db, roleId); var query = await BuildTaskReportQueryAsync(isAdmin, isManager, currentUserId, startDate, endDate, status, projectId, assignedToMe, assignedToMyTeam); var paged = await query .OrderBy(t => t.DueDate) .ThenByDescending(t => t.CreatedAt ?? DateTime.UtcNow) .ToPagedResultAsync(page, pageSize); var mapped = MapPagedResult(paged, MapTaskReport); await EnrichTaskReportsAsync(mapped.Items); return mapped; } public async Task>> GetUserProductivityReportAsync(long roleId, long currentUserId) { var isAdmin = await DataScopeAuthorization.IsAdminScopeAsync(_db, roleId); var isManager = await DataScopeAuthorization.IsManagerScopeAsync(_db, roleId); var users = await BuildAccessibleUserQuery(isAdmin, currentUserId).ToListAsync(); var tasks = await BuildAccessibleTaskQuery(isAdmin, isManager, currentUserId).ToListAsync(); var list = new List(); foreach (var user in users) { var userTasks = tasks.Where(t => t.AssignedTo == user.Id).ToList(); int total = userTasks.Count; var completedTasks = userTasks.Where(t => t.StatusId == AppTaskStatus.Done).ToList(); int done = completedTasks.Count; int onTimeCount = completedTasks.Count(t => (t.UpdatedAt ?? t.CreatedAt) <= t.DueDate); double onTimeDeliveryRate = done > 0 ? Math.Round(((double)onTimeCount / done) * 100, 2) : 0; list.Add(new UserProductivityDto { UserId = user.Id, Username = user.Username, FullName = $"{user.FirstName} {user.LastName}".Trim(), TotalAssignedTasks = total, CompletedTasksCount = done, EfficiencyRatio = total > 0 ? Math.Round(((double)done / total) * 100, 2) : 0, OnTimeDeliveryRate = onTimeDeliveryRate }); } return Result>.Success(list); } public async Task>> GetTaskStatusSummaryAsync( string? search, AppTaskStatus? statusId, long? projectId, long roleId, long currentUserId) { var isAdmin = await DataScopeAuthorization.IsAdminScopeAsync(_db, roleId); var isManager = await DataScopeAuthorization.IsManagerScopeAsync(_db, roleId); var query = BuildAccessibleTaskQuery(isAdmin, isManager, currentUserId) .Include(t => t.Project) .Include(t => t.AssignedToNavigation) .Where(t => t.IsDeleted != true); if (statusId.HasValue && Convert.ToInt64(statusId.Value) > 0) query = query.Where(t => t.StatusId == statusId.Value); if (projectId.HasValue && projectId > 0) query = query.Where(t => t.ProjectId == projectId.Value); var tasks = await query.OrderBy(t => t.DueDate).ThenByDescending(t => t.CreatedAt ?? DateTime.UtcNow).ToListAsync(); if (!string.IsNullOrWhiteSpace(search)) { var s = search.Trim().ToLower(); tasks = tasks.Where(t => t.Title.ToLower().Contains(s) || (t.Project?.Name.ToLower().Contains(s) == true) || (t.AssignedToNavigation != null && ($"{t.AssignedToNavigation.FirstName} {t.AssignedToNavigation.LastName}").ToLower().Contains(s)) ).ToList(); } var list = tasks.Select(t => new TaskStatusSummaryDto { TaskId = t.Id, Title = t.Title, ProjectName = t.Project?.Name ?? "-", StatusName = StatusMap.TryGetValue(t.StatusId, out var s) ? s : $"Status {t.StatusId}", PriorityName = PriorityMap.TryGetValue(t.PriorityId, out var p) ? p : $"Priority {t.PriorityId}", AssignedTo = t.AssignedToNavigation != null ? $"{t.AssignedToNavigation.FirstName} {t.AssignedToNavigation.LastName}" : null, DueDate = t.DueDate, CreatedAt = t.CreatedAt ?? DateTime.UtcNow }).ToList(); return Result>.Success(list); } public byte[] ExportTaskStatusSummaryToExcel(IEnumerable data) { using var wb = new XLWorkbook(); var ws = wb.Worksheets.Add("Task Status Summary"); var headers = new[] { "Task ID", "Title", "Project", "Status", "Priority", "Assigned To", "Due Date", "Created At", "Overdue" }; for (int i = 0; i < headers.Length; i++) { ws.Cell(1, i + 1).Value = headers[i]; ws.Cell(1, i + 1).Style.Font.Bold = true; ws.Cell(1, i + 1).Style.Fill.BackgroundColor = XLColor.FromHtml("#6D28D9"); ws.Cell(1, i + 1).Style.Font.FontColor = XLColor.White; } int row = 2; foreach (var item in data) { ws.Cell(row, 1).Value = item.TaskId; ws.Cell(row, 2).Value = item.Title; ws.Cell(row, 3).Value = item.ProjectName; ws.Cell(row, 4).Value = item.StatusName; ws.Cell(row, 5).Value = item.PriorityName; ws.Cell(row, 6).Value = item.AssignedTo ?? "Unassigned"; ws.Cell(row, 7).Value = DisplayFormats.Date(item.DueDate); ws.Cell(row, 8).Value = DisplayFormats.Date(item.CreatedAt); ws.Cell(row, 9).Value = item.IsOverdue ? "Yes" : "No"; if (item.IsOverdue) ws.Row(row).Style.Fill.BackgroundColor = XLColor.FromHtml("#FEE2E2"); row++; } ws.Columns().AdjustToContents(); using var ms = new MemoryStream(); wb.SaveAs(ms); return ms.ToArray(); } public async Task> GetPagedTeamProductivityAsync( string? search, long roleId, long currentUserId, int page, int pageSize) { var full = await GetTeamProductivityAsync(search, roleId, currentUserId); if (!full.IsSuccess || full.Value == null) { return new PagedResult(); } var ordered = full.Value.OrderByDescending(u => u.CompletionRate).ToList(); var normalizedPageSize = PaginationExtensions.NormalizePageSize(pageSize); var totalCount = ordered.Count; var totalPages = totalCount == 0 ? 0 : (int)Math.Ceiling(totalCount / (double)normalizedPageSize); var normalizedPage = Math.Max(page, 1); if (totalPages > 0 && normalizedPage > totalPages) normalizedPage = totalPages; var items = ordered .Skip((normalizedPage - 1) * normalizedPageSize) .Take(normalizedPageSize) .ToList(); return new PagedResult { Items = items, TotalCount = totalCount, Page = normalizedPage, PageSize = normalizedPageSize, TotalPages = totalPages }; } // ——— Report 2: Team Productivity ——— public async Task>> GetTeamProductivityAsync(string? search, long roleId, long currentUserId) { var isAdmin = await DataScopeAuthorization.IsAdminScopeAsync(_db, roleId); var isManager = await DataScopeAuthorization.IsManagerScopeAsync(_db, roleId); var users = await BuildAccessibleUserQuery(isAdmin, currentUserId).ToListAsync(); var allTasks = await BuildAccessibleTaskQuery(isAdmin, isManager, currentUserId) .Where(t => t.AssignedTo != null) .ToListAsync(); var now = DateTime.UtcNow; var list = users.Select(u => { var uTasks = allTasks.Where(t => t.AssignedTo == u.Id).ToList(); return new TeamProductivityReportDto { UserId = u.Id, FullName = $"{u.FirstName} {u.LastName}".Trim(), Username = u.Username, TotalAssigned = uTasks.Count, Completed = uTasks.Count(t => t.StatusId == AppTaskStatus.Done), InProgress = uTasks.Count(t => t.StatusId == AppTaskStatus.InProgress), ToDo = uTasks.Count(t => t.StatusId == AppTaskStatus.Todo), Overdue = uTasks.Count(t => t.DueDate < now && t.StatusId != AppTaskStatus.Done), CompletionRate = uTasks.Count > 0 ? Math.Round((double)uTasks.Count(t => t.StatusId == AppTaskStatus.Done) / uTasks.Count * 100, 1) : 0 }; }).ToList(); if (!string.IsNullOrWhiteSpace(search)) { var s = search.Trim().ToLower(); list = list.Where(u => u.FullName.ToLower().Contains(s) || u.Username.ToLower().Contains(s)).ToList(); } return Result>.Success(list.OrderByDescending(u => u.CompletionRate)); } public byte[] ExportTeamProductivityToExcel(IEnumerable data) { using var wb = new XLWorkbook(); var ws = wb.Worksheets.Add("Team Productivity"); var headers = new[] { "User ID", "Full Name", "Username", "Total Assigned", "Completed", "In Progress", "To Do", "Overdue", "Completion Rate (%)" }; for (int i = 0; i < headers.Length; i++) { ws.Cell(1, i + 1).Value = headers[i]; ws.Cell(1, i + 1).Style.Font.Bold = true; ws.Cell(1, i + 1).Style.Fill.BackgroundColor = XLColor.FromHtml("#059669"); ws.Cell(1, i + 1).Style.Font.FontColor = XLColor.White; } int row = 2; foreach (var item in data) { ws.Cell(row, 1).Value = item.UserId; ws.Cell(row, 2).Value = item.FullName; ws.Cell(row, 3).Value = item.Username; ws.Cell(row, 4).Value = item.TotalAssigned; ws.Cell(row, 5).Value = item.Completed; ws.Cell(row, 6).Value = item.InProgress; ws.Cell(row, 7).Value = item.ToDo; ws.Cell(row, 8).Value = item.Overdue; ws.Cell(row, 9).Value = item.CompletionRate; if (item.Overdue > 0) ws.Row(row).Style.Fill.BackgroundColor = XLColor.FromHtml("#FEF3C7"); row++; } ws.Columns().AdjustToContents(); using var ms = new MemoryStream(); wb.SaveAs(ms); return ms.ToArray(); } // ─── Report 3: Overdue & Critical Tasks ─────────────────────────────────── public async Task>> GetOverdueCriticalTasksAsync( string? search, long? projectId, long roleId, long currentUserId, int? priorityId = null, string? delayType = null, bool? assignedToMe = null, bool? assignedToMyTeam = null) { var isAdmin = await DataScopeAuthorization.IsAdminScopeAsync(_db, roleId); var isManager = await DataScopeAuthorization.IsManagerScopeAsync(_db, roleId); var now = DateTime.UtcNow; var today = DateTime.Today; var query = BuildAccessibleTaskQuery(isAdmin, isManager, currentUserId) .Include(t => t.Project) .Include(t => t.AssignedToNavigation) .Where(t => t.IsDeleted != true && t.StatusId != AppTaskStatus.Done && (t.DueDate < now || t.PriorityId == TaskPriority.High)); // overdue OR high priority if (projectId.HasValue && projectId > 0) query = query.Where(t => t.ProjectId == projectId.Value); if (priorityId.HasValue && priorityId > 0) query = query.Where(t => (int)t.PriorityId == priorityId.Value); switch (delayType?.Trim().ToLower()) { case "overdue": query = query.Where(t => t.DueDate.Date < today); break; case "soon": query = query.Where(t => t.DueDate.Date >= today && t.DueDate.Date <= today.AddDays(3)); break; } if (assignedToMe == true || assignedToMyTeam == true) { var teamIds = assignedToMyTeam == true ? await GetTeamUserIdsAsync(currentUserId) : new List(); query = query.Where(t => (assignedToMe == true && t.AssignedTo == currentUserId) || (assignedToMyTeam == true && t.AssignedTo.HasValue && teamIds.Contains(t.AssignedTo.Value)) ); } var tasks = await query.OrderBy(t => t.DueDate).ToListAsync(); if (!string.IsNullOrWhiteSpace(search)) { var s = search.Trim().ToLower(); tasks = tasks.Where(t => t.Title.ToLower().Contains(s) || (t.Project?.Name.ToLower().Contains(s) == true) ).ToList(); } var list = tasks.Select(t => new OverdueCriticalTaskDto { TaskId = t.Id, Title = t.Title, Description = t.Description, ProjectName = t.Project?.Name ?? "-", StatusName = StatusMap.TryGetValue(t.StatusId, out var s) ? s : $"Status {t.StatusId}", PriorityName = PriorityMap.TryGetValue(t.PriorityId, out var p) ? p : $"Priority {t.PriorityId}", AssignedTo = t.AssignedToNavigation != null ? $"{t.AssignedToNavigation.FirstName} {t.AssignedToNavigation.LastName}" : null, AssignedToUserId = t.AssignedTo, DueDate = t.DueDate, DaysOverdue = t.DueDate < now ? (int)(now - t.DueDate).TotalDays : 0, CreatedAt = t.CreatedAt ?? DateTime.UtcNow }).ToList(); await EnrichOverdueTaskReportsAsync(list); return Result>.Success(list); } public async Task> GetPagedOverdueCriticalTasksAsync( string? search, long? projectId, long roleId, long currentUserId, int page, int pageSize, int? priorityId = null, string? delayType = null, bool? assignedToMe = null, bool? assignedToMyTeam = null) { var isAdmin = await DataScopeAuthorization.IsAdminScopeAsync(_db, roleId); var isManager = await DataScopeAuthorization.IsManagerScopeAsync(_db, roleId); var now = DateTime.UtcNow; var today = DateTime.Today; var query = BuildAccessibleTaskQuery(isAdmin, isManager, currentUserId) .Include(t => t.Project) .Include(t => t.AssignedToNavigation) .Where(t => t.IsDeleted != true && t.StatusId != AppTaskStatus.Done && (t.DueDate < now || t.PriorityId == TaskPriority.High)); if (projectId.HasValue && projectId > 0) query = query.Where(t => t.ProjectId == projectId.Value); if (priorityId.HasValue && priorityId > 0) query = query.Where(t => (int)t.PriorityId == priorityId.Value); switch (delayType?.Trim().ToLower()) { case "overdue": query = query.Where(t => t.DueDate.Date < today); break; case "soon": query = query.Where(t => t.DueDate.Date >= today && t.DueDate.Date <= today.AddDays(3)); break; default: query = query.Where(t => t.DueDate.Date <= today.AddDays(3)); break; } if (assignedToMe == true || assignedToMyTeam == true) { var teamIds = assignedToMyTeam == true ? await GetTeamUserIdsAsync(currentUserId) : new List(); query = query.Where(t => (assignedToMe == true && t.AssignedTo == currentUserId) || (assignedToMyTeam == true && t.AssignedTo.HasValue && teamIds.Contains(t.AssignedTo.Value)) ); } if (!string.IsNullOrWhiteSpace(search)) { var s = search.Trim().ToLower(); query = query.Where(t => t.Title.ToLower().Contains(s) || (t.Project != null && t.Project.Name.ToLower().Contains(s))); } var paged = await query .OrderBy(t => t.DueDate) .ThenBy(t => t.Title) .ToPagedResultAsync(page, pageSize); var mapped = MapPagedResult(paged, t => new OverdueCriticalTaskDto { TaskId = t.Id, Title = t.Title, Description = t.Description, ProjectName = t.Project?.Name ?? "-", StatusName = StatusMap.TryGetValue(t.StatusId, out var s) ? s : $"Status {t.StatusId}", PriorityName = PriorityMap.TryGetValue(t.PriorityId, out var p) ? p : $"Priority {t.PriorityId}", AssignedTo = t.AssignedToNavigation != null ? $"{t.AssignedToNavigation.FirstName} {t.AssignedToNavigation.LastName}" : null, AssignedToUserId = t.AssignedTo, DueDate = t.DueDate, DaysOverdue = t.DueDate < now ? (int)(now - t.DueDate).TotalDays : 0, CreatedAt = t.CreatedAt ?? DateTime.UtcNow }); await EnrichOverdueTaskReportsAsync(mapped.Items); return mapped; } public byte[] ExportOverdueCriticalToExcel(IEnumerable data) { using var wb = new XLWorkbook(); var ws = wb.Worksheets.Add("Overdue & Critical Tasks"); var headers = new[] { "Task ID", "Title", "Project", "Status", "Priority", "Assigned To", "Due Date", "Days Overdue", "Created At" }; for (int i = 0; i < headers.Length; i++) { ws.Cell(1, i + 1).Value = headers[i]; ws.Cell(1, i + 1).Style.Font.Bold = true; ws.Cell(1, i + 1).Style.Fill.BackgroundColor = XLColor.FromHtml("#DC2626"); ws.Cell(1, i + 1).Style.Font.FontColor = XLColor.White; } int row = 2; foreach (var item in data) { ws.Cell(row, 1).Value = item.TaskId; ws.Cell(row, 2).Value = item.Title; ws.Cell(row, 3).Value = item.ProjectName; ws.Cell(row, 4).Value = item.StatusName; ws.Cell(row, 5).Value = item.PriorityName; ws.Cell(row, 6).Value = item.AssignedTo ?? "Unassigned"; ws.Cell(row, 7).Value = DisplayFormats.Date(item.DueDate); ws.Cell(row, 8).Value = item.DaysOverdue; ws.Cell(row, 9).Value = DisplayFormats.Date(item.CreatedAt); if (item.DaysOverdue > 0) ws.Row(row).Style.Fill.BackgroundColor = XLColor.FromHtml("#FEE2E2"); row++; } ws.Columns().AdjustToContents(); using var ms = new MemoryStream(); wb.SaveAs(ms); return ms.ToArray(); } public async Task>> GetEmployeeProductivityReportAsync( string? search, DateTime? startDate, DateTime? endDate, string? status, long roleId, long currentUserId, bool? assignedToMe = null, bool? assignedToMyTeam = null) { var isAdmin = await DataScopeAuthorization.IsAdminScopeAsync(_db, roleId); var isManager = await DataScopeAuthorization.IsManagerScopeAsync(_db, roleId); var users = await BuildAccessibleUserQuery(isAdmin, currentUserId).ToListAsync(); if (assignedToMe == true || assignedToMyTeam == true) { var teamIds = assignedToMyTeam == true ? await GetTeamUserIdsAsync(currentUserId) : new List(); users = users.Where(u => (assignedToMe == true && u.Id == currentUserId) || (assignedToMyTeam == true && teamIds.Contains(u.Id)) ).ToList(); } var tasks = await BuildAccessibleTaskQuery(isAdmin, isManager, currentUserId).ToListAsync(); var issues = await BuildAccessibleIssueQuery(isAdmin, isManager, currentUserId) .Include(i => i.Task) .ToListAsync(); if (startDate.HasValue) { tasks = tasks.Where(t => t.CreatedAt >= startDate.Value.Date).ToList(); issues = issues.Where(i => (i.CreatedAt ?? DateTime.UtcNow) >= startDate.Value.Date).ToList(); } if (endDate.HasValue) { tasks = tasks.Where(t => t.CreatedAt < endDate.Value.Date.AddDays(1)).ToList(); issues = issues.Where(i => (i.CreatedAt ?? DateTime.UtcNow) < endDate.Value.Date.AddDays(1)).ToList(); } if (!string.IsNullOrWhiteSpace(status)) { var sl = status.Trim().ToLower(); if (sl == "overdue") { tasks = tasks.Where(t => t.StatusId != AppTaskStatus.Done && t.DueDate < DateTime.Today).ToList(); issues = issues.Where(i => i.StatusId != AppTaskStatus.Done && i.DueDate.Date < DateTime.Today).ToList(); } else if (sl != "0") { if (Enum.TryParse(status, true, out var parsedStatus)) { tasks = tasks.Where(t => t.StatusId == parsedStatus).ToList(); issues = issues.Where(i => i.StatusId == parsedStatus).ToList(); } } } var list = new List(); foreach (var user in users) { var userTasks = tasks.Where(t => t.AssignedTo == user.Id).ToList(); var userIssues = issues.Where(i => i.AssignedTo == user.Id).ToList(); int total = userTasks.Count; var completedTasks = userTasks.Where(t => t.StatusId == AppTaskStatus.Done).ToList(); int done = completedTasks.Count; var completedIssues = userIssues.Where(i => i.StatusId == AppTaskStatus.Done).ToList(); int onTimeCount = completedTasks.Count(t => (t.UpdatedAt ?? t.CreatedAt) <= t.DueDate); var onTimeIssueCount = completedIssues.Count(i => (i.UpdatedAt ?? i.CreatedAt ?? DateTime.UtcNow) <= i.DueDate); double onTimeDeliveryRate = completedIssues.Count > 0 ? Math.Round(((double)onTimeIssueCount / completedIssues.Count) * 100, 2) : done > 0 ? Math.Round(((double)onTimeCount / done) * 100, 2) : 0; var openIssues = userIssues.Count(i => i.StatusId != AppTaskStatus.Done); var overdueIssues = userIssues.Count(i => i.StatusId != AppTaskStatus.Done && i.DueDate.Date < DateTime.Today); var completionRate = userIssues.Count > 0 ? Math.Round(((double)completedIssues.Count / userIssues.Count) * 100, 2) : 0; var avgCompletionDays = completedIssues.Count > 0 ? Math.Round(completedIssues.Average(i => Math.Max(0, ((i.UpdatedAt ?? i.CreatedAt ?? DateTime.UtcNow).Date - i.StartDate.Date).TotalDays)), 1) : 0; list.Add(new EmployeeProductivityReportDto { UserId = user.Id, Username = user.Username, FullName = $"{user.FirstName} {user.LastName}".Trim(), AssignedCount = total, CompletedCount = done, AssignedIssues = userIssues.Count, OpenIssues = openIssues, OverdueIssues = overdueIssues, ActualHours = userIssues.Sum(i => i.ActualHours ?? 0), WorkloadLevel = openIssues >= 10 || overdueIssues >= 3 ? "High" : openIssues <= 2 ? "Light" : "Balanced", AverageCompletionTimeDays = avgCompletionDays, Trend = overdueIssues > 0 ? "Declining" : completionRate >= 80 ? "Improving" : "Stable", CompletionRate = completionRate, Efficiency = completionRate, OnTimeDeliveryRate = onTimeDeliveryRate }); } if (!string.IsNullOrWhiteSpace(search)) { var s = search.Trim().ToLower(); list = list.Where(u => u.FullName.ToLower().Contains(s) || u.Username.ToLower().Contains(s)).ToList(); } return Result>.Success(list.OrderByDescending(u => u.OpenIssues).ThenByDescending(u => u.ActualHours)); } public async Task> GetPagedEmployeeProductivityReportAsync( string? search, DateTime? startDate, DateTime? endDate, string? status, long roleId, long currentUserId, int page, int pageSize, bool? assignedToMe = null, bool? assignedToMyTeam = null) { var full = await GetEmployeeProductivityReportAsync(search, startDate, endDate, status, roleId, currentUserId, assignedToMe, assignedToMyTeam); if (!full.IsSuccess || full.Value == null) { return new PagedResult(); } var ordered = full.Value.OrderByDescending(u => u.OpenIssues).ThenByDescending(u => u.ActualHours).ToList(); var normalizedPageSize = PaginationExtensions.NormalizePageSize(pageSize); var totalCount = ordered.Count; var totalPages = totalCount == 0 ? 0 : (int)Math.Ceiling(totalCount / (double)normalizedPageSize); var normalizedPage = Math.Max(page, 1); if (totalPages > 0 && normalizedPage > totalPages) normalizedPage = totalPages; var items = ordered .Skip((normalizedPage - 1) * normalizedPageSize) .Take(normalizedPageSize) .ToList(); return new PagedResult { Items = items, TotalCount = totalCount, Page = normalizedPage, PageSize = normalizedPageSize, TotalPages = totalPages }; } public async Task>> GetProjectProgressReportAsync( string? search, DateTime? startDate, DateTime? endDate, string? status, long roleId, long currentUserId, bool? assignedToMe = null, bool? assignedToMyTeam = null) { var isAdmin = await DataScopeAuthorization.IsAdminScopeAsync(_db, roleId); var isManager = await DataScopeAuthorization.IsManagerScopeAsync(_db, roleId); var projects = await BuildAccessibleProjectQuery(isAdmin, currentUserId).ToListAsync(); if (assignedToMe == true || assignedToMyTeam == true) { var myProjectIds = await _db.ProjectMembers .Where(pm => pm.UserId == currentUserId) .Select(pm => pm.ProjectId) .Distinct() .ToListAsync(); projects = projects.Where(p => myProjectIds.Contains(p.Id) || p.CreatedById == currentUserId).ToList(); } var tasks = await BuildAccessibleTaskQuery(isAdmin, isManager, currentUserId).ToListAsync(); var issues = await BuildAccessibleIssueQuery(isAdmin, isManager, currentUserId) .Include(i => i.Task) .ToListAsync(); var today = DateTime.Today; if (startDate.HasValue) projects = projects.Where(p => p.StartDate >= startDate.Value.Date).ToList(); if (endDate.HasValue) projects = projects.Where(p => p.StartDate < endDate.Value.Date.AddDays(1)).ToList(); var list = new List(); foreach (var project in projects) { var projectTasks = tasks.Where(t => t.ProjectId == project.Id).ToList(); var projectIssues = issues.Where(i => i.Task.ProjectId == project.Id).ToList(); if (!string.IsNullOrWhiteSpace(status)) { if (status.Equals("overdue", StringComparison.OrdinalIgnoreCase)) { projectTasks = projectTasks.Where(t => t.DueDate.Date < today && t.StatusId != AppTaskStatus.Done).ToList(); projectIssues = projectIssues.Where(i => i.DueDate.Date < today && i.StatusId != AppTaskStatus.Done).ToList(); } else if (status != "0" && Enum.TryParse(status, true, out var parsedStatus)) { projectTasks = projectTasks.Where(t => t.StatusId == parsedStatus).ToList(); projectIssues = projectIssues.Where(i => i.StatusId == parsedStatus).ToList(); } } int totalTasks = projectTasks.Count; int completedTasks = projectTasks.Count(t => t.StatusId == AppTaskStatus.Done); double progress = totalTasks > 0 ? ((double)completedTasks / totalTasks) * 100 : 0; double elapsedPct; if (today < project.StartDate) elapsedPct = 0; else if (today > project.EndDate) elapsedPct = 100; else { var totalDays = (project.EndDate - project.StartDate).TotalDays; var elapsedDays = (today - project.StartDate).TotalDays; elapsedPct = totalDays > 0 ? (elapsedDays / totalDays) * 100 : 100; } bool isAhead = progress > elapsedPct && progress > 0 && progress < 100; var openIssues = projectIssues.Count(i => i.StatusId != AppTaskStatus.Done); var overdueIssues = projectIssues.Count(i => i.StatusId != AppTaskStatus.Done && i.DueDate.Date < today); var blockedIssues = projectIssues.Count(i => i.IsBlocked); bool isAtRisk = (projectTasks.Any(t => t.StatusId != AppTaskStatus.Done && t.DueDate.Date < today)) || overdueIssues > 0 || blockedIssues > 0 || (project.EndDate.Date < today && progress < 100); var health = isAtRisk && (overdueIssues > 0 || blockedIssues > 0) ? "Critical" : isAtRisk ? "At Risk" : "Healthy"; var lastActivity = projectIssues .Select(i => i.UpdatedAt ?? i.CreatedAt ?? project.CreatedAt ?? DateTime.UtcNow) .Concat(projectTasks.Select(t => t.UpdatedAt ?? t.CreatedAt ?? DateTime.UtcNow)) .DefaultIfEmpty(project.CreatedAt ?? DateTime.UtcNow) .Max(); list.Add(new ProjectProgressReportDto { ProjectId = project.Id, ProjectName = project.Name, StartDate = project.StartDate, EndDate = project.EndDate, TotalTasks = totalTasks, CompletedTasks = completedTasks, OpenIssues = openIssues, OverdueIssues = overdueIssues, BlockedIssues = blockedIssues, Progress = progress, IsAhead = isAhead, IsAtRisk = isAtRisk, ProjectHealth = health, LastActivity = lastActivity, NextMilestone = project.EndDate.Date >= today ? project.EndDate : null, ActualHours = projectIssues.Sum(i => i.ActualHours ?? 0) }); } if (!string.IsNullOrWhiteSpace(search)) { var s = search.Trim().ToLower(); list = list.Where(p => p.ProjectName.ToLower().Contains(s)).ToList(); } return Result>.Success(list.OrderBy(p => p.ProjectName)); } public async Task> GetPagedProjectProgressReportAsync( string? search, DateTime? startDate, DateTime? endDate, string? status, long roleId, long currentUserId, int page, int pageSize, bool? assignedToMe = null, bool? assignedToMyTeam = null) { var full = await GetProjectProgressReportAsync(search, startDate, endDate, status, roleId, currentUserId, assignedToMe, assignedToMyTeam); if (!full.IsSuccess || full.Value == null) { return new PagedResult(); } var ordered = full.Value.OrderBy(p => p.ProjectName).ToList(); var normalizedPageSize = PaginationExtensions.NormalizePageSize(pageSize); var totalCount = ordered.Count; var totalPages = totalCount == 0 ? 0 : (int)Math.Ceiling(totalCount / (double)normalizedPageSize); var normalizedPage = Math.Max(page, 1); if (totalPages > 0 && normalizedPage > totalPages) normalizedPage = totalPages; var items = ordered .Skip((normalizedPage - 1) * normalizedPageSize) .Take(normalizedPageSize) .ToList(); return new PagedResult { Items = items, TotalCount = totalCount, Page = normalizedPage, PageSize = normalizedPageSize, TotalPages = totalPages }; } } }