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; } private static DateTime ToUtcDate(DateTime value) { return value.Kind == DateTimeKind.Utc ? value : DateTime.SpecifyKind(value.Date, DateTimeKind.Utc); } 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(); var searchPattern = $"%{searchTerm}%"; query = query.Where(i => EF.Functions.ILike(i.Title ?? string.Empty, searchPattern) || EF.Functions.ILike(i.TitleMy ?? string.Empty, searchPattern) || EF.Functions.ILike(i.Description ?? string.Empty, searchPattern) || EF.Functions.ILike(i.DescriptionMy ?? string.Empty, searchPattern) || EF.Functions.ILike(i.DelayReason ?? string.Empty, searchPattern) || EF.Functions.ILike(i.DelayReasonMy ?? string.Empty, searchPattern) || EF.Functions.ILike(i.BlockedBy ?? string.Empty, searchPattern) || EF.Functions.ILike(i.BlockedByMy ?? string.Empty, searchPattern) || (i.AssignedToNavigation != null && (EF.Functions.ILike(i.AssignedToNavigation.FirstName ?? string.Empty, searchPattern) || EF.Functions.ILike(i.AssignedToNavigation.LastName ?? string.Empty, searchPattern) || EF.Functions.ILike((i.AssignedToNavigation.FirstName ?? string.Empty) + " " + (i.AssignedToNavigation.LastName ?? string.Empty), searchPattern) || EF.Functions.ILike((i.AssignedToNavigation.FirstName ?? string.Empty) + (i.AssignedToNavigation.LastName ?? string.Empty), searchPattern) || EF.Functions.ILike(i.AssignedToNavigation.FirstNameMy ?? string.Empty, searchPattern) || EF.Functions.ILike(i.AssignedToNavigation.LastNameMy ?? string.Empty, searchPattern) || EF.Functions.ILike((i.AssignedToNavigation.FirstNameMy ?? string.Empty) + " " + (i.AssignedToNavigation.LastNameMy ?? string.Empty), searchPattern) || EF.Functions.ILike((i.AssignedToNavigation.FirstNameMy ?? string.Empty) + (i.AssignedToNavigation.LastNameMy ?? string.Empty), searchPattern) || EF.Functions.ILike(i.AssignedToNavigation.Username ?? string.Empty, searchPattern) || EF.Functions.ILike(i.AssignedToNavigation.Email ?? string.Empty, searchPattern)))); } 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) && string.IsNullOrWhiteSpace(dto.TitleMy)) { return Result.Failure("Issue title is required.", 400); } if (dto.StartDate == default || dto.DueDate == default) { return Result.Failure("Issue start and due dates are required.", 400); } if (dto.DueDate.Date < dto.StartDate.Date) { return Result.Failure("Issue due date cannot be earlier than the start date.", 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() ?? string.Empty, TitleMy = dto.TitleMy, Description = dto.Description, DescriptionMy = dto.DescriptionMy, AssignedTo = dto.AssignedTo, EstimatedHours = dto.EstimatedHours, ActualHours = dto.ActualHours, DelayReason = dto.DelayReason, DelayReasonMy = dto.DelayReasonMy, IsBlocked = dto.IsBlocked, BlockedBy = dto.BlockedBy, BlockedByMy = dto.BlockedByMy, EscalationLevel = dto.EscalationLevel, StartDate = ToUtcDate(dto.StartDate), DueDate = ToUtcDate(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) && string.IsNullOrWhiteSpace(dto.TitleMy)) { 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() ?? string.Empty; issue.TitleMy = dto.TitleMy; issue.Description = dto.Description; issue.DescriptionMy = dto.DescriptionMy; issue.AssignedTo = dto.AssignedTo; issue.EstimatedHours = dto.EstimatedHours; issue.ActualHours = dto.ActualHours; issue.DelayReason = dto.DelayReason; issue.DelayReasonMy = dto.DelayReasonMy; issue.IsBlocked = dto.IsBlocked; issue.BlockedBy = dto.BlockedBy; issue.BlockedByMy = dto.BlockedByMy; issue.EscalationLevel = dto.EscalationLevel; issue.StartDate = ToUtcDate(dto.StartDate); issue.DueDate = ToUtcDate(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, TaskTitleMy = i.Task.TitleMy, ProjectId = i.Task.ProjectId, ProjectName = i.Task.Project.Name, ProjectNameMy = i.Task.Project.NameMy, Title = i.Title, TitleMy = i.TitleMy, Description = i.Description, DescriptionMy = i.DescriptionMy, AssignedTo = i.AssignedTo, AssignedToName = i.AssignedToNavigation != null ? i.AssignedToNavigation.FirstName + " " + i.AssignedToNavigation.LastName : null, AssignedToNameMy = i.AssignedToNavigation != null ? i.AssignedToNavigation.FirstNameMy + " " + i.AssignedToNavigation.LastNameMy : null, EstimatedHours = i.EstimatedHours, ActualHours = i.ActualHours, DelayReason = i.DelayReason, DelayReasonMy = i.DelayReasonMy, IsBlocked = i.IsBlocked, BlockedBy = i.BlockedBy, BlockedByMy = i.BlockedByMy, EscalationLevel = i.EscalationLevel, StartDate = i.StartDate, DueDate = i.DueDate, StatusId = i.StatusId, PriorityId = i.PriorityId, CreatedAt = i.CreatedAt ?? DateTime.UtcNow, UpdatedAt = i.UpdatedAt }; } } }