using Microsoft.EntityFrameworkCore; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using TaskTrackingSystem.Database.AppDbContextModels; using TaskTrackingSystem.Shared; using TaskTrackingSystem.Shared.Enums; using TaskTrackingSystem.Shared.Models.Issue; using TaskTrackingSystem.WebApi.Infrastructure; using IssueEntity = TaskTrackingSystem.Database.AppDbContextModels.Issue; namespace TaskTrackingSystem.WebApi.Features.Issue { public class IssueService { private readonly AppDbContext _db; public IssueService(AppDbContext db) { _db = db; } public async Task> GetAllIssuesAsync(long roleId, long currentUserId) { var isAdmin = await DataScopeAuthorization.IsAdminScopeAsync(_db, roleId); var isManager = await DataScopeAuthorization.IsManagerScopeAsync(_db, roleId); return await BuildAccessibleIssueQuery(isAdmin, isManager, currentUserId) .OrderByDescending(i => i.CreatedAt ?? DateTime.UtcNow) .Select(ToDtoProjection()) .ToListAsync(); } public async Task> GetPagedIssuesAsync( long roleId, long currentUserId, string? search, long? taskId, long? projectId, AppTaskStatus? statusId, TaskPriority? priorityId, bool assignedOnly, int page, int pageSize) { var isAdmin = await DataScopeAuthorization.IsAdminScopeAsync(_db, roleId); var isManager = await DataScopeAuthorization.IsManagerScopeAsync(_db, roleId); var query = BuildAccessibleIssueQuery(isAdmin, isManager, currentUserId); if (assignedOnly) { query = query.Where(i => i.AssignedTo == currentUserId); } if (!string.IsNullOrWhiteSpace(search)) { var searchTerm = search.Trim().ToLower(); query = query.Where(i => (i.Title != null && i.Title.ToLower().Contains(searchTerm)) || (i.Description != null && i.Description.ToLower().Contains(searchTerm))); } if (taskId.HasValue && taskId.Value > 0) { query = query.Where(i => i.TaskId == taskId.Value); } if (projectId.HasValue && projectId.Value > 0) { query = query.Where(i => i.Task.ProjectId == projectId.Value); } if (statusId.HasValue) { query = query.Where(i => i.StatusId == statusId.Value); } if (priorityId.HasValue) { query = query.Where(i => i.PriorityId == priorityId.Value); } return await query .OrderByDescending(i => i.CreatedAt ?? DateTime.UtcNow) .Select(ToDtoProjection()) .ToPagedResultAsync(page, pageSize); } public async Task GetIssueByIdAsync(long id, long roleId, long currentUserId) { var isAdmin = await DataScopeAuthorization.IsAdminScopeAsync(_db, roleId); var isManager = await DataScopeAuthorization.IsManagerScopeAsync(_db, roleId); var issue = await BuildAccessibleIssueQuery(isAdmin, isManager, currentUserId) .FirstOrDefaultAsync(i => i.Id == id); if (issue == null) { return null; } return await BuildAccessibleIssueQuery(isAdmin, isManager, currentUserId) .Where(i => i.Id == id) .Select(ToDtoProjection()) .FirstOrDefaultAsync(); } public async Task> GetIssuesByTaskIdAsync(long taskId, long roleId, long currentUserId) { var isAdmin = await DataScopeAuthorization.IsAdminScopeAsync(_db, roleId); if (!await CanAccessTaskAsync(taskId, isAdmin, currentUserId)) { return Array.Empty(); } var isManager = await DataScopeAuthorization.IsManagerScopeAsync(_db, roleId); return await BuildAccessibleIssueQuery(isAdmin, isManager, currentUserId) .Where(i => i.TaskId == taskId) .OrderByDescending(i => i.CreatedAt ?? DateTime.UtcNow) .Select(ToDtoProjection()) .ToListAsync(); } public async Task> CreateIssueAsync(CreateIssueDto dto, long roleId, long currentUserId) { if (string.IsNullOrWhiteSpace(dto.Title)) { return Result.Failure("Issue title is required.", 400); } var task = await _db.Tasks .Include(t => t.Project) .FirstOrDefaultAsync(t => t.Id == dto.TaskId && t.IsDeleted != true && !t.IsArchived && t.Project.IsDeleted != true); if (task == null) { return Result.Failure($"Task with ID {dto.TaskId} not found.", 404); } var isAdmin = await DataScopeAuthorization.IsAdminScopeAsync(_db, roleId); if (!await CanAccessTaskAsync(task.Id, isAdmin, currentUserId)) { return Result.Failure("You do not have access to this task.", 403); } if (dto.AssignedTo.HasValue && !await IsTaskMemberAsync(task.ProjectId, dto.AssignedTo.Value)) { return Result.Failure("Issue assignee must belong to the selected project.", 400); } var issue = new IssueEntity { TaskId = dto.TaskId, Title = dto.Title.Trim(), Description = dto.Description, AssignedTo = dto.AssignedTo, EstimatedHours = dto.EstimatedHours, ActualHours = dto.ActualHours, DelayReason = dto.DelayReason, IsBlocked = dto.IsBlocked, BlockedBy = dto.BlockedBy, EscalationLevel = dto.EscalationLevel, StartDate = dto.StartDate, DueDate = dto.DueDate, StatusId = dto.StatusId == 0 ? AppTaskStatus.Todo : dto.StatusId, PriorityId = dto.PriorityId == 0 ? TaskPriority.Medium : dto.PriorityId, CreatedAt = DateTime.UtcNow, CreatedBy = currentUserId, IsDeleted = false }; _db.Issues.Add(issue); await _db.SaveChangesAsync(); var result = await GetIssueByIdAsync(issue.Id, roleId, currentUserId); return Result.Success(result!, 201); } public async Task UpdateIssueAsync(long id, UpdateIssueDto dto, long roleId, long currentUserId) { if (string.IsNullOrWhiteSpace(dto.Title)) { return Result.Failure("Issue title is required.", 400); } var issue = await _db.Issues .Include(i => i.Task) .ThenInclude(t => t.Project) .FirstOrDefaultAsync(i => i.Id == id && i.IsDeleted != true); if (issue == null) { return Result.Failure($"Issue with ID {id} not found.", 404); } var isAdmin = await DataScopeAuthorization.IsAdminScopeAsync(_db, roleId); if (!await CanAccessTaskAsync(issue.TaskId, isAdmin, currentUserId)) { return Result.Failure("You do not have access to this task.", 403); } if (dto.AssignedTo.HasValue && !await IsTaskMemberAsync(issue.Task.ProjectId, dto.AssignedTo.Value)) { return Result.Failure("Issue assignee must belong to the selected project.", 400); } issue.Title = dto.Title.Trim(); issue.Description = dto.Description; issue.AssignedTo = dto.AssignedTo; issue.EstimatedHours = dto.EstimatedHours; issue.ActualHours = dto.ActualHours; issue.DelayReason = dto.DelayReason; issue.IsBlocked = dto.IsBlocked; issue.BlockedBy = dto.BlockedBy; issue.EscalationLevel = dto.EscalationLevel; issue.StartDate = dto.StartDate; issue.DueDate = dto.DueDate; issue.StatusId = dto.StatusId; issue.PriorityId = dto.PriorityId; issue.UpdatedAt = DateTime.UtcNow; issue.UpdatedBy = currentUserId; _db.Issues.Update(issue); await _db.SaveChangesAsync(); return Result.Success(200); } public async Task SoftDeleteIssueAsync(long id, long roleId, long currentUserId) { var issue = await _db.Issues .Include(i => i.Task) .ThenInclude(t => t.Project) .FirstOrDefaultAsync(i => i.Id == id && i.IsDeleted != true); if (issue == null) { return Result.Failure($"Issue with ID {id} not found.", 404); } var isAdmin = await DataScopeAuthorization.IsAdminScopeAsync(_db, roleId); if (!await CanAccessTaskAsync(issue.TaskId, isAdmin, currentUserId)) { return Result.Failure("You do not have access to this task.", 403); } issue.IsDeleted = true; issue.UpdatedAt = DateTime.UtcNow; issue.UpdatedBy = currentUserId; _db.Issues.Update(issue); await _db.SaveChangesAsync(); return Result.Success(200); } 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 async Task CanAccessTaskAsync(long taskId, bool isAdmin, long currentUserId) { if (isAdmin) { return await _db.Tasks.AnyAsync(t => t.Id == taskId && t.IsDeleted != true && !t.IsArchived && t.Project.IsDeleted != true); } return await _db.Tasks.AnyAsync(t => t.Id == taskId && t.IsDeleted != true && !t.IsArchived && t.Project.IsDeleted != true && (t.AssignedTo == currentUserId || t.CreatedBy == currentUserId || t.Project.ProjectMembers.Any(pm => pm.UserId == currentUserId))); } private async Task IsTaskMemberAsync(long projectId, long userId) { return await _db.ProjectMembers.AnyAsync(pm => pm.ProjectId == projectId && pm.UserId == userId); } private static System.Linq.Expressions.Expression> ToDtoProjection() { return 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 }; } } }