User
fix: reports
4671199
Raw
History Blame Contribute Delete
50.6 kB
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<AppTaskStatus, string> StatusMap = new() { { AppTaskStatus.Todo, "To Do" }, { AppTaskStatus.InProgress, "In Progress" }, { AppTaskStatus.Done, "Done" } };
private static readonly Dictionary<TaskPriority, string> PriorityMap = new() { { TaskPriority.Low, "Low" }, { TaskPriority.Medium, "Medium" }, { TaskPriority.High, "High" } };
public ReportService(AppDbContext db)
{
_db = db;
}
public async Task<Result<List<IssueDto>>> 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<List<IssueDto>>.Success(list);
}
private IQueryable<TaskTrackingSystem.Database.AppDbContextModels.Task> BuildAccessibleTaskQuery(bool isAdmin, bool isManager, long currentUserId)
{
return _db.Tasks.Where(t => t.IsDeleted != true && !t.IsArchived);
}
private IQueryable<TaskTrackingSystem.Database.AppDbContextModels.Project> BuildAccessibleProjectQuery(bool isAdmin, long currentUserId)
{
return _db.Projects.Where(p => p.IsDeleted != true);
}
private IQueryable<TaskTrackingSystem.Database.AppDbContextModels.User> BuildAccessibleUserQuery(bool isAdmin, long currentUserId)
{
return _db.Users.Where(u => !u.IsDeleted);
}
private IQueryable<TaskTrackingSystem.Database.AppDbContextModels.Issue> 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<TDestination> MapPagedResult<TSource, TDestination>(
PagedResult<TSource> source,
Func<TSource, TDestination> map)
{
return new PagedResult<TDestination>
{
Items = source.Items.Select(map).ToList(),
TotalCount = source.TotalCount,
Page = source.Page,
PageSize = source.PageSize,
TotalPages = source.TotalPages
};
}
private async Task<List<long>> 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<IQueryable<TaskTrackingSystem.Database.AppDbContextModels.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<AppTaskStatus>(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<long>();
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<TaskReportDto> 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<OverdueCriticalTaskDto> 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<Result<IEnumerable<TaskReportDto>>> 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<IEnumerable<TaskReportDto>>.Success(list);
}
public async Task<PagedResult<TaskReportDto>> 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<Result<IEnumerable<UserProductivityDto>>> 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<UserProductivityDto>();
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<IEnumerable<UserProductivityDto>>.Success(list);
}
public async Task<Result<IEnumerable<TaskStatusSummaryDto>>> 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<IEnumerable<TaskStatusSummaryDto>>.Success(list);
}
public byte[] ExportTaskStatusSummaryToExcel(IEnumerable<TaskStatusSummaryDto> 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<PagedResult<TeamProductivityReportDto>> 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<TeamProductivityReportDto>();
}
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<TeamProductivityReportDto>
{
Items = items,
TotalCount = totalCount,
Page = normalizedPage,
PageSize = normalizedPageSize,
TotalPages = totalPages
};
}
// ——— Report 2: Team Productivity ———
public async Task<Result<IEnumerable<TeamProductivityReportDto>>> 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<IEnumerable<TeamProductivityReportDto>>.Success(list.OrderByDescending(u => u.CompletionRate));
}
public byte[] ExportTeamProductivityToExcel(IEnumerable<TeamProductivityReportDto> 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<Result<IEnumerable<OverdueCriticalTaskDto>>> 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<long>();
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<IEnumerable<OverdueCriticalTaskDto>>.Success(list);
}
public async Task<PagedResult<OverdueCriticalTaskDto>> 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<long>();
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<OverdueCriticalTaskDto> 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<Result<IEnumerable<EmployeeProductivityReportDto>>> 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<long>();
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<AppTaskStatus>(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<EmployeeProductivityReportDto>();
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<IEnumerable<EmployeeProductivityReportDto>>.Success(list.OrderByDescending(u => u.OpenIssues).ThenByDescending(u => u.ActualHours));
}
public async Task<PagedResult<EmployeeProductivityReportDto>> 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<EmployeeProductivityReportDto>();
}
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<EmployeeProductivityReportDto>
{
Items = items,
TotalCount = totalCount,
Page = normalizedPage,
PageSize = normalizedPageSize,
TotalPages = totalPages
};
}
public async Task<Result<IEnumerable<ProjectProgressReportDto>>> 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<ProjectProgressReportDto>();
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<AppTaskStatus>(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<IEnumerable<ProjectProgressReportDto>>.Success(list.OrderBy(p => p.ProjectName));
}
public async Task<PagedResult<ProjectProgressReportDto>> 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<ProjectProgressReportDto>();
}
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<ProjectProgressReportDto>
{
Items = items,
TotalCount = totalCount,
Page = normalizedPage,
PageSize = normalizedPageSize,
TotalPages = totalPages
};
}
}
}