using Microsoft.EntityFrameworkCore; using WorkClub.Application.Interfaces; using WorkClub.Application.Tasks.DTOs; using WorkClub.Domain.Entities; using WorkClub.Domain.Enums; using WorkClub.Infrastructure.Data; namespace WorkClub.Api.Services; public class TaskService { private readonly AppDbContext _context; private readonly ITenantProvider _tenantProvider; public TaskService(AppDbContext context, ITenantProvider tenantProvider) { _context = context; _tenantProvider = tenantProvider; } public async Task GetTasksAsync(string? statusFilter, int page, int pageSize, string? currentExternalUserId = null) { var query = _context.WorkItems.AsQueryable(); if (!string.IsNullOrEmpty(statusFilter)) { if (Enum.TryParse(statusFilter, ignoreCase: true, out var status)) { query = query.Where(w => w.Status == status); } } var total = await query.CountAsync(); var items = await query .OrderBy(w => w.CreatedAt) .Skip((page - 1) * pageSize) .Take(pageSize) .ToListAsync(); // Get current member ID for IsAssignedToMe check Guid? currentMemberId = null; if (currentExternalUserId != null) { var tenantId = _tenantProvider.GetTenantId(); currentMemberId = await _context.Members .Where(m => m.ExternalUserId == currentExternalUserId && m.TenantId == tenantId) .Select(m => m.Id) .FirstOrDefaultAsync(); } // Get all assignee IDs to fetch their names in bulk var assigneeIds = items.Where(w => w.AssigneeId.HasValue).Select(w => w.AssigneeId!.Value).Distinct().ToList(); var assigneeNames = await _context.Members .Where(m => assigneeIds.Contains(m.Id)) .Select(m => new { m.Id, m.DisplayName }) .ToDictionaryAsync(m => m.Id, m => m.DisplayName); var itemDtos = items.Select(w => new TaskListItemDto( w.Id, w.Title, w.Status.ToString(), w.AssigneeId, w.AssigneeId.HasValue && assigneeNames.TryGetValue(w.AssigneeId.Value, out var name) ? name : null, w.CreatedAt, currentMemberId != null && w.AssigneeId == currentMemberId )).ToList(); return new TaskListDto(itemDtos, total, page, pageSize); } public async Task GetTaskByIdAsync(Guid id, string? currentExternalUserId = null) { var workItem = await _context.WorkItems.FindAsync(id); if (workItem == null) return null; // Get current member ID for IsAssignedToMe check Guid? currentMemberId = null; if (currentExternalUserId != null) { var tenantId = _tenantProvider.GetTenantId(); currentMemberId = await _context.Members .Where(m => m.ExternalUserId == currentExternalUserId && m.TenantId == tenantId) .Select(m => m.Id) .FirstOrDefaultAsync(); } // Fetch assignee and creator names var memberIds = new List(); if (workItem.AssigneeId.HasValue) memberIds.Add(workItem.AssigneeId.Value); memberIds.Add(workItem.CreatedById); var memberNames = await _context.Members .Where(m => memberIds.Contains(m.Id)) .Select(m => new { m.Id, m.DisplayName }) .ToDictionaryAsync(m => m.Id, m => m.DisplayName); return new TaskDetailDto( workItem.Id, workItem.Title, workItem.Description, workItem.Status.ToString(), workItem.AssigneeId, workItem.AssigneeId.HasValue && memberNames.TryGetValue(workItem.AssigneeId.Value, out var assigneeName) ? assigneeName : null, workItem.CreatedById, memberNames.TryGetValue(workItem.CreatedById, out var createdByName) ? createdByName : null, workItem.ClubId, workItem.DueDate, workItem.CreatedAt, workItem.UpdatedAt, currentMemberId != null && workItem.AssigneeId == currentMemberId ); } public async Task<(TaskDetailDto? task, string? error)> CreateTaskAsync(CreateTaskRequest request, Guid createdById) { var tenantId = _tenantProvider.GetTenantId(); var workItem = new WorkItem { Id = Guid.NewGuid(), TenantId = tenantId, Title = request.Title, Description = request.Description, Status = WorkItemStatus.Open, ClubId = request.ClubId, AssigneeId = request.AssigneeId, DueDate = request.DueDate, CreatedById = createdById, CreatedAt = DateTimeOffset.UtcNow, UpdatedAt = DateTimeOffset.UtcNow }; _context.WorkItems.Add(workItem); await _context.SaveChangesAsync(); // Fetch creator and assignee names var memberIds = new List { createdById }; if (workItem.AssigneeId.HasValue) memberIds.Add(workItem.AssigneeId.Value); var memberNames = await _context.Members .Where(m => memberIds.Contains(m.Id)) .Select(m => new { m.Id, m.DisplayName }) .ToDictionaryAsync(m => m.Id, m => m.DisplayName); var dto = new TaskDetailDto( workItem.Id, workItem.Title, workItem.Description, workItem.Status.ToString(), workItem.AssigneeId, workItem.AssigneeId.HasValue && memberNames.TryGetValue(workItem.AssigneeId.Value, out var assigneeName) ? assigneeName : null, workItem.CreatedById, memberNames.TryGetValue(workItem.CreatedById, out var createdByName) ? createdByName : null, workItem.ClubId, workItem.DueDate, workItem.CreatedAt, workItem.UpdatedAt, false ); return (dto, null); } public async Task<(TaskDetailDto? task, string? error, bool isConflict)> UpdateTaskAsync(Guid id, UpdateTaskRequest request, string? currentExternalUserId = null) { var workItem = await _context.WorkItems.FindAsync(id); if (workItem == null) return (null, "Task not found", false); if (request.Title != null) workItem.Title = request.Title; if (request.Description != null) workItem.Description = request.Description; if (request.AssigneeId.HasValue) workItem.AssigneeId = request.AssigneeId; if (request.DueDate.HasValue) workItem.DueDate = request.DueDate; if (request.Status != null) { if (!Enum.TryParse(request.Status, ignoreCase: true, out var newStatus)) { return (null, $"Invalid status: {request.Status}", false); } if (!workItem.CanTransitionTo(newStatus)) { return (null, $"Cannot transition from {workItem.Status} to {newStatus}", false); } workItem.TransitionTo(newStatus); } workItem.UpdatedAt = DateTimeOffset.UtcNow; try { await _context.SaveChangesAsync(); } catch (DbUpdateConcurrencyException) { return (null, "Task was modified by another user. Please refresh and try again.", true); } // Get current member ID for IsAssignedToMe check Guid? currentMemberId = null; if (currentExternalUserId != null) { var tenantId = _tenantProvider.GetTenantId(); currentMemberId = await _context.Members .Where(m => m.ExternalUserId == currentExternalUserId && m.TenantId == tenantId) .Select(m => m.Id) .FirstOrDefaultAsync(); } // Fetch assignee and creator names var memberIds = new List(); if (workItem.AssigneeId.HasValue) memberIds.Add(workItem.AssigneeId.Value); memberIds.Add(workItem.CreatedById); var memberNames = await _context.Members .Where(m => memberIds.Contains(m.Id)) .Select(m => new { m.Id, m.DisplayName }) .ToDictionaryAsync(m => m.Id, m => m.DisplayName); var dto = new TaskDetailDto( workItem.Id, workItem.Title, workItem.Description, workItem.Status.ToString(), workItem.AssigneeId, workItem.AssigneeId.HasValue && memberNames.TryGetValue(workItem.AssigneeId.Value, out var assigneeName) ? assigneeName : null, workItem.CreatedById, memberNames.TryGetValue(workItem.CreatedById, out var createdByName) ? createdByName : null, workItem.ClubId, workItem.DueDate, workItem.CreatedAt, workItem.UpdatedAt, currentMemberId != null && workItem.AssigneeId == currentMemberId ); return (dto, null, false); } public async Task DeleteTaskAsync(Guid id) { var workItem = await _context.WorkItems.FindAsync(id); if (workItem == null) return false; _context.WorkItems.Remove(workItem); await _context.SaveChangesAsync(); return true; } public async Task<(bool success, string? error)> AssignToMeAsync(Guid taskId, string externalUserId) { var tenantId = _tenantProvider.GetTenantId(); var memberId = await _context.Members .Where(m => m.ExternalUserId == externalUserId && m.TenantId == tenantId) .Select(m => m.Id) .FirstOrDefaultAsync(); if (memberId == Guid.Empty) return (false, "User is not a member of this club"); var workItem = await _context.WorkItems.FindAsync(taskId); if (workItem == null) return (false, "Task not found"); if (workItem.AssigneeId.HasValue) return (false, "Task is already assigned"); workItem.AssigneeId = memberId; if (workItem.CanTransitionTo(WorkItemStatus.Assigned)) workItem.TransitionTo(WorkItemStatus.Assigned); workItem.UpdatedAt = DateTimeOffset.UtcNow; try { await _context.SaveChangesAsync(); } catch (DbUpdateConcurrencyException) { return (false, "Task was modified by another user"); } return (true, null); } public async Task<(bool success, string? error)> UnassignFromMeAsync(Guid taskId, string externalUserId) { var tenantId = _tenantProvider.GetTenantId(); var memberId = await _context.Members .Where(m => m.ExternalUserId == externalUserId && m.TenantId == tenantId) .Select(m => m.Id) .FirstOrDefaultAsync(); if (memberId == Guid.Empty) return (false, "User is not a member of this club"); var workItem = await _context.WorkItems.FindAsync(taskId); if (workItem == null) return (false, "Task not found"); if (workItem.AssigneeId != memberId) return (false, "Task is not assigned to you"); workItem.AssigneeId = null; if (workItem.Status == WorkItemStatus.Assigned || workItem.Status == WorkItemStatus.InProgress) { // Transition back to open if no longer assigned and not marked Review/Done workItem.Status = WorkItemStatus.Open; } workItem.UpdatedAt = DateTimeOffset.UtcNow; try { await _context.SaveChangesAsync(); } catch (DbUpdateConcurrencyException) { return (false, "Task was modified by another user"); } return (true, null); } }