using Nexus.Api.Data; using Nexus.Api.DTOs; using Nexus.Api.Repositories; namespace Nexus.Api.Services; public sealed class TaskService( ITaskRepository taskRepo, IActivityRepository activityRepo) : ITaskService { public async Task> GetAllAsync(CancellationToken ct = default) => await taskRepo.GetAllAsync(ct); public async Task GetByIdAsync(Guid id, CancellationToken ct = default) => await taskRepo.GetByIdAsync(id, ct); public async Task> GetPendingApprovalAsync(CancellationToken ct = default) => await taskRepo.GetPendingApprovalAsync(ct); public async Task CreateAsync(CreateTaskRequest request, CancellationToken ct = default) { var task = new WorkTask { Title = request.Title.Trim(), Priority = string.IsNullOrWhiteSpace(request.Priority) ? "Normal" : request.Priority.Trim(), ProjectId = request.ProjectId }; await taskRepo.AddAsync(task, ct); await activityRepo.AddAsync(new ActivityEvent { Type = "task", Message = $"Task {task.Title} created" }, ct); return task; } public async Task ApproveAsync(Guid id, CancellationToken ct = default) { var task = await taskRepo.GetByIdAsync(id, ct); if (task is null) return new TaskOperationResult(TaskOperationOutcome.NotFound); if (!TaskStateHelper.IsInProgressOrBlocked(task.State)) return new TaskOperationResult(TaskOperationOutcome.InvalidState, task); task.State = TaskStateHelper.ToStateString(TaskState.Done); await taskRepo.UpdateAsync(task, ct); await activityRepo.AddAsync(new ActivityEvent { Type = "task", Message = $"Task {task.Title} approved" }, ct); return new TaskOperationResult(TaskOperationOutcome.Success, task); } public async Task RejectAsync(Guid id, CancellationToken ct = default) { var task = await taskRepo.GetByIdAsync(id, ct); if (task is null) return new TaskOperationResult(TaskOperationOutcome.NotFound); if (!TaskStateHelper.IsInProgressOrBlocked(task.State)) return new TaskOperationResult(TaskOperationOutcome.InvalidState, task); task.State = TaskStateHelper.ToStateString(TaskState.Backlog); await taskRepo.UpdateAsync(task, ct); await activityRepo.AddAsync(new ActivityEvent { Type = "task", Message = $"Task {task.Title} rejected, returned to backlog" }, ct); return new TaskOperationResult(TaskOperationOutcome.Success, task); } public async Task UpdateStateAsync(Guid id, string state, CancellationToken ct = default) { var canonical = TaskStateHelper.AllStates.FirstOrDefault(s => s.Equals(state, StringComparison.OrdinalIgnoreCase)); if (canonical is null) return new TaskOperationResult(TaskOperationOutcome.InvalidState); var task = await taskRepo.GetByIdAsync(id, ct); if (task is null) return new TaskOperationResult(TaskOperationOutcome.NotFound); task.State = canonical; await taskRepo.UpdateAsync(task, ct); await activityRepo.AddAsync(new ActivityEvent { Type = "task", Message = $"Task {task.Title} moved to {task.State}" }, ct); return new TaskOperationResult(TaskOperationOutcome.Success, task); } public async Task UpdateAsync(Guid id, UpdateTaskRequest request, CancellationToken ct = default) { var task = await taskRepo.GetByIdAsync(id, ct); if (task is null) return new TaskOperationResult(TaskOperationOutcome.NotFound); if (!string.IsNullOrWhiteSpace(request.Title)) task.Title = request.Title.Trim(); if (!string.IsNullOrWhiteSpace(request.Priority)) task.Priority = request.Priority.Trim(); if (request.ProjectId.HasValue) task.ProjectId = request.ProjectId.Value == Guid.Empty ? null : request.ProjectId; await taskRepo.UpdateAsync(task, ct); await activityRepo.AddAsync(new ActivityEvent { Type = "task", Message = $"Task {task.Title} updated" }, ct); return new TaskOperationResult(TaskOperationOutcome.Success, task); } public async Task DeleteAsync(Guid id, CancellationToken ct = default) { var task = await taskRepo.GetByIdAsync(id, ct); if (task is null) return new TaskOperationResult(TaskOperationOutcome.NotFound); if (!TaskStateHelper.IsDoneOrBacklog(task.State)) return new TaskOperationResult(TaskOperationOutcome.InvalidState, task); await activityRepo.AddAsync(new ActivityEvent { Type = "task", Message = $"Task {task.Title} deleted" }, ct); await taskRepo.DeleteAsync(task, ct); return new TaskOperationResult(TaskOperationOutcome.Success); } // ── Dashboard-facing operations ── public async Task> GetOpenAsync(CancellationToken ct = default) { var all = await taskRepo.GetAllAsync(ct); return all.Where(t => !string.Equals(t.State, "Done", StringComparison.OrdinalIgnoreCase)) .OrderByDescending(t => t.CreatedAt) .ToList(); } public async Task CreateDashboardTaskAsync( string title, string? detail, string? source, string? priority, string? assignedTo, CancellationToken ct = default) { var task = new WorkTask { Title = title.Trim(), Detail = detail?.Trim(), Source = string.IsNullOrWhiteSpace(source) ? "bao" : source.Trim(), Priority = string.IsNullOrWhiteSpace(priority) ? "Normal" : priority.Trim(), AssignedTo = assignedTo?.Trim() }; await taskRepo.AddAsync(task, ct); await activityRepo.AddAsync(new ActivityEvent { Type = "task", Message = $"Task \"{task.Title}\" created ({task.Source})" }, ct); return task; } public async Task UpdateDashboardTaskAsync( Guid id, string? title, string? detail, string? source, string? priority, string? assignedTo, CancellationToken ct = default) { var task = await taskRepo.GetByIdAsync(id, ct); if (task is null) return new TaskOperationResult(TaskOperationOutcome.NotFound); if (!string.IsNullOrWhiteSpace(title)) task.Title = title.Trim(); if (detail is not null) task.Detail = string.IsNullOrWhiteSpace(detail) ? null : detail.Trim(); if (!string.IsNullOrWhiteSpace(source)) task.Source = source.Trim(); if (!string.IsNullOrWhiteSpace(priority)) task.Priority = priority.Trim(); if (assignedTo is not null) task.AssignedTo = string.IsNullOrWhiteSpace(assignedTo) ? null : assignedTo.Trim(); await taskRepo.UpdateAsync(task, ct); await activityRepo.AddAsync(new ActivityEvent { Type = "task", Message = $"Task \"{task.Title}\" updated" }, ct); return new TaskOperationResult(TaskOperationOutcome.Success, task); } public async Task UpdateStatusAsync(Guid id, string status, CancellationToken ct = default) { if (!TaskStateHelper.IsValidState(status)) return new TaskOperationResult(TaskOperationOutcome.InvalidState); var task = await taskRepo.GetByIdAsync(id, ct); if (task is null) return new TaskOperationResult(TaskOperationOutcome.NotFound); var canonical = TaskStateHelper.AllStates.First(s => s.Equals(status, StringComparison.OrdinalIgnoreCase)); task.State = canonical; await taskRepo.UpdateAsync(task, ct); await activityRepo.AddAsync(new ActivityEvent { Type = "task", Message = $"Task \"{task.Title}\" → {canonical}" }, ct); return new TaskOperationResult(TaskOperationOutcome.Success, task); } public async Task CompleteViaQueueAsync(Guid id, CancellationToken ct = default) { var task = await taskRepo.GetByIdAsync(id, ct); if (task is null) return new TaskOperationResult(TaskOperationOutcome.NotFound); task.State = "Done"; await taskRepo.UpdateAsync(task, ct); await activityRepo.AddAsync(new ActivityEvent { Type = "task", Message = $"Task \"{task.Title}\" completed via queue" }, ct); return new TaskOperationResult(TaskOperationOutcome.Success, task); } public async Task CyclePriorityAsync(Guid id, CancellationToken ct = default) { var task = await taskRepo.GetByIdAsync(id, ct); if (task is null) return new TaskOperationResult(TaskOperationOutcome.NotFound); task.Priority = task.Priority.ToLowerInvariant() switch { "high" => "Medium", "medium" => "Low", "low" => "High", _ => "Medium" }; await taskRepo.UpdateAsync(task, ct); await activityRepo.AddAsync(new ActivityEvent { Type = "task", Message = $"Task \"{task.Title}\" priority → {task.Priority}" }, ct); return new TaskOperationResult(TaskOperationOutcome.Success, task); } }