feat: Created task lists that work in categories
All checks were successful
Build and Push App Image / build-and-push (push) Successful in 1m20s

This commit is contained in:
domrichardson
2026-03-29 16:14:23 +01:00
parent a1dd2f2c00
commit b9ca845b9c
22 changed files with 1000 additions and 249 deletions

View File

@@ -15,6 +15,7 @@ import (
// CategoryService handles category operations
type CategoryService struct {
categoryRepo repositories.CategoryRepository
taskListRepo repositories.TaskListRepository
membershipRepo repositories.MembershipRepository
noteRepo repositories.NoteRepository
permissionService *PermissionService
@@ -23,12 +24,14 @@ type CategoryService struct {
// NewCategoryService creates a new category service
func NewCategoryService(
categoryRepo repositories.CategoryRepository,
taskListRepo repositories.TaskListRepository,
membershipRepo repositories.MembershipRepository,
noteRepo repositories.NoteRepository,
permissionService *PermissionService,
) *CategoryService {
return &CategoryService{
categoryRepo: categoryRepo,
taskListRepo: taskListRepo,
membershipRepo: membershipRepo,
noteRepo: noteRepo,
permissionService: permissionService,
@@ -134,6 +137,14 @@ func (s *CategoryService) buildCategoryTree(ctx context.Context, category *entit
}
}
// Get task lists in this category
taskLists, err := s.taskListRepo.ListTaskListsByCategory(ctx, spaceID, category.ID)
if err == nil {
for _, taskList := range taskLists {
tree.TaskLists = append(tree.TaskLists, dto.NewTaskListDTO(taskList))
}
}
return tree, nil
}

View File

@@ -17,6 +17,7 @@ type SpaceService struct {
membershipRepo repositories.MembershipRepository
noteRepo repositories.NoteRepository
categoryRepo repositories.CategoryRepository
taskListRepo repositories.TaskListRepository
userRepo repositories.UserRepository
permissionService *PermissionService
}
@@ -27,6 +28,7 @@ func NewSpaceService(
membershipRepo repositories.MembershipRepository,
noteRepo repositories.NoteRepository,
categoryRepo repositories.CategoryRepository,
taskListRepo repositories.TaskListRepository,
userRepo repositories.UserRepository,
permissionService *PermissionService,
) *SpaceService {
@@ -35,6 +37,7 @@ func NewSpaceService(
membershipRepo: membershipRepo,
noteRepo: noteRepo,
categoryRepo: categoryRepo,
taskListRepo: taskListRepo,
userRepo: userRepo,
permissionService: permissionService,
}
@@ -180,6 +183,9 @@ func (s *SpaceService) DeleteSpace(ctx context.Context, spaceID, userID bson.Obj
if err := s.categoryRepo.DeleteCategoriesBySpaceID(ctx, spaceID); err != nil {
return err
}
if err := s.taskListRepo.DeleteTaskListsBySpaceID(ctx, spaceID); err != nil {
return err
}
if err := s.membershipRepo.DeleteMembershipsBySpaceID(ctx, spaceID); err != nil {
return err
}

View File

@@ -17,6 +17,7 @@ import (
// TaskService handles task and task status operations.
type TaskService struct {
taskRepo repositories.TaskRepository
taskListRepo repositories.TaskListRepository
taskStatusRepo repositories.TaskStatusRepository
noteRepo repositories.NoteRepository
categoryRepo repositories.CategoryRepository
@@ -27,6 +28,7 @@ type TaskService struct {
// NewTaskService creates a task service.
func NewTaskService(
taskRepo repositories.TaskRepository,
taskListRepo repositories.TaskListRepository,
taskStatusRepo repositories.TaskStatusRepository,
noteRepo repositories.NoteRepository,
categoryRepo repositories.CategoryRepository,
@@ -35,6 +37,7 @@ func NewTaskService(
) *TaskService {
return &TaskService{
taskRepo: taskRepo,
taskListRepo: taskListRepo,
taskStatusRepo: taskStatusRepo,
noteRepo: noteRepo,
categoryRepo: categoryRepo,
@@ -121,13 +124,10 @@ func toObjectIDs(hexIDs []string) ([]bson.ObjectID, error) {
return result, nil
}
func (s *TaskService) validateCategory(ctx context.Context, spaceID bson.ObjectID, categoryID *bson.ObjectID) error {
if categoryID == nil {
return nil
}
category, err := s.categoryRepo.GetCategoryByID(ctx, *categoryID)
if err != nil || category.SpaceID != spaceID {
return errors.New("invalid category")
func (s *TaskService) validateTaskList(ctx context.Context, spaceID, taskListID bson.ObjectID) error {
taskList, err := s.taskListRepo.GetTaskListByID(ctx, taskListID)
if err != nil || taskList.SpaceID != spaceID {
return errors.New("invalid task list")
}
return nil
}
@@ -209,14 +209,6 @@ func (s *TaskService) CreateTask(ctx context.Context, spaceID, userID bson.Objec
return nil, err
}
categoryID, err := toObjectIDPtr(req.CategoryID)
if err != nil {
return nil, errors.New("invalid category")
}
if err := s.validateCategory(ctx, spaceID, categoryID); err != nil {
return nil, err
}
parentTaskID, err := toObjectIDPtr(req.ParentTaskID)
if err != nil {
return nil, errors.New("invalid parent task")
@@ -226,6 +218,14 @@ func (s *TaskService) CreateTask(ctx context.Context, spaceID, userID bson.Objec
return nil, err
}
taskListID, err := bson.ObjectIDFromHex(strings.TrimSpace(req.TaskListID))
if err != nil {
return nil, errors.New("invalid task list")
}
if err := s.validateTaskList(ctx, spaceID, taskListID); err != nil {
return nil, err
}
noteLinks, err := toObjectIDs(req.NoteLinks)
if err != nil {
return nil, err
@@ -258,11 +258,19 @@ func (s *TaskService) CreateTask(ctx context.Context, spaceID, userID bson.Objec
statusID = parsedStatusID
}
if parentTaskID != nil {
parent, parentErr := s.taskRepo.GetTaskByID(ctx, *parentTaskID)
if parentErr != nil || parent.SpaceID != spaceID {
return nil, errors.New("invalid parent task")
}
taskListID = parent.TaskListID
}
task := &entities.Task{
SpaceID: spaceID,
Title: strings.TrimSpace(req.Title),
Description: strings.TrimSpace(req.Description),
CategoryID: categoryID,
TaskListID: taskListID,
StatusID: statusID,
ParentTaskID: parentTaskID,
Depth: depth,
@@ -318,7 +326,7 @@ func (s *TaskService) GetTaskByID(ctx context.Context, spaceID, taskID, userID b
func (s *TaskService) ListTasks(
ctx context.Context,
spaceID, userID bson.ObjectID,
categoryID, statusID, parentTaskID *string,
taskListID, statusID, parentTaskID *string,
) ([]*dto.TaskDTO, error) {
if err := s.requireSpaceAccess(ctx, userID, spaceID); err != nil {
return nil, err
@@ -328,12 +336,12 @@ func (s *TaskService) ListTasks(
}
filters := map[string]any{}
if categoryID != nil && strings.TrimSpace(*categoryID) != "" {
id, err := bson.ObjectIDFromHex(*categoryID)
if taskListID != nil && strings.TrimSpace(*taskListID) != "" {
id, err := bson.ObjectIDFromHex(*taskListID)
if err != nil {
return nil, errors.New("invalid category filter")
return nil, errors.New("invalid task list filter")
}
filters["category_id"] = id
filters["task_list_id"] = id
}
if statusID != nil && strings.TrimSpace(*statusID) != "" {
id, err := bson.ObjectIDFromHex(*statusID)
@@ -453,19 +461,21 @@ func (s *TaskService) UpdateTask(ctx context.Context, spaceID, taskID, userID bs
task.Description = strings.TrimSpace(*req.Description)
}
if req.CategoryID != nil {
if strings.TrimSpace(*req.CategoryID) == "" {
task.CategoryID = nil
} else {
categoryID, parseErr := bson.ObjectIDFromHex(*req.CategoryID)
if parseErr != nil {
return nil, errors.New("invalid category")
}
task.CategoryID = &categoryID
if req.TaskListID != nil {
if strings.TrimSpace(*req.TaskListID) == "" {
return nil, errors.New("task list is required")
}
if err := s.validateCategory(ctx, spaceID, task.CategoryID); err != nil {
taskListID, parseErr := bson.ObjectIDFromHex(*req.TaskListID)
if parseErr != nil {
return nil, errors.New("invalid task list")
}
if task.ParentTaskID != nil {
return nil, errors.New("subtasks inherit task list from parent")
}
if err := s.validateTaskList(ctx, spaceID, taskListID); err != nil {
return nil, err
}
task.TaskListID = taskListID
}
if req.ParentTaskID != nil {
@@ -486,6 +496,11 @@ func (s *TaskService) UpdateTask(ctx context.Context, spaceID, taskID, userID bs
}
task.ParentTaskID = &parentID
task.Depth = depth
parentTask, parentErr := s.taskRepo.GetTaskByID(ctx, parentID)
if parentErr != nil || parentTask.SpaceID != spaceID {
return nil, errors.New("invalid parent task")
}
task.TaskListID = parentTask.TaskListID
}
}
@@ -670,6 +685,144 @@ func (s *TaskService) UnlinkNoteFromTask(ctx context.Context, spaceID, taskID, n
return dto.NewTaskDTO(task), nil
}
func (s *TaskService) ListTaskLists(ctx context.Context, spaceID, userID bson.ObjectID) ([]*dto.TaskListDTO, error) {
if err := s.requireSpaceAccess(ctx, userID, spaceID); err != nil {
return nil, err
}
lists, err := s.taskListRepo.ListTaskLists(ctx, spaceID)
if err != nil {
return nil, err
}
result := make([]*dto.TaskListDTO, 0, len(lists))
for _, list := range lists {
result = append(result, dto.NewTaskListDTO(list))
}
return result, nil
}
func (s *TaskService) CreateTaskList(ctx context.Context, spaceID, userID bson.ObjectID, req *dto.CreateTaskListRequest) (*dto.TaskListDTO, error) {
if err := s.requireSpaceAccess(ctx, userID, spaceID); err != nil {
return nil, err
}
hasPermission, err := s.hasTaskPermission(ctx, userID, spaceID, "tasks.create")
if err != nil {
return nil, err
}
if !hasPermission {
return nil, errors.New("insufficient permissions")
}
name := strings.TrimSpace(req.Name)
if name == "" {
return nil, errors.New("task list name is required")
}
var categoryID *bson.ObjectID
if req.CategoryID != nil && strings.TrimSpace(*req.CategoryID) != "" {
id, parseErr := bson.ObjectIDFromHex(*req.CategoryID)
if parseErr != nil {
return nil, errors.New("invalid category")
}
category, categoryErr := s.categoryRepo.GetCategoryByID(ctx, id)
if categoryErr != nil || category.SpaceID != spaceID {
return nil, errors.New("invalid category")
}
categoryID = &id
}
list := &entities.TaskList{
SpaceID: spaceID,
CategoryID: categoryID,
Name: name,
Description: strings.TrimSpace(req.Description),
CreatedBy: userID,
UpdatedBy: userID,
}
if err := s.taskListRepo.CreateTaskList(ctx, list); err != nil {
return nil, err
}
return dto.NewTaskListDTO(list), nil
}
func (s *TaskService) UpdateTaskList(ctx context.Context, spaceID, taskListID, userID bson.ObjectID, req *dto.UpdateTaskListRequest) (*dto.TaskListDTO, error) {
if err := s.requireSpaceAccess(ctx, userID, spaceID); err != nil {
return nil, err
}
hasPermission, err := s.hasTaskPermission(ctx, userID, spaceID, "tasks.edit")
if err != nil {
return nil, err
}
if !hasPermission {
return nil, errors.New("insufficient permissions")
}
list, err := s.taskListRepo.GetTaskListByID(ctx, taskListID)
if err != nil || list.SpaceID != spaceID {
return nil, errors.New("task list not found")
}
if req.Name != nil {
name := strings.TrimSpace(*req.Name)
if name == "" {
return nil, errors.New("task list name is required")
}
list.Name = name
}
if req.Description != nil {
list.Description = strings.TrimSpace(*req.Description)
}
if req.CategoryID != nil {
if strings.TrimSpace(*req.CategoryID) == "" {
list.CategoryID = nil
} else {
categoryID, parseErr := bson.ObjectIDFromHex(*req.CategoryID)
if parseErr != nil {
return nil, errors.New("invalid category")
}
category, categoryErr := s.categoryRepo.GetCategoryByID(ctx, categoryID)
if categoryErr != nil || category.SpaceID != spaceID {
return nil, errors.New("invalid category")
}
list.CategoryID = &categoryID
}
}
list.UpdatedBy = userID
if err := s.taskListRepo.UpdateTaskList(ctx, list); err != nil {
return nil, err
}
return dto.NewTaskListDTO(list), nil
}
func (s *TaskService) DeleteTaskList(ctx context.Context, spaceID, taskListID, userID bson.ObjectID) error {
if err := s.requireSpaceAccess(ctx, userID, spaceID); err != nil {
return err
}
hasPermission, err := s.hasTaskPermission(ctx, userID, spaceID, "tasks.delete")
if err != nil {
return err
}
if !hasPermission {
return errors.New("insufficient permissions")
}
list, err := s.taskListRepo.GetTaskListByID(ctx, taskListID)
if err != nil || list.SpaceID != spaceID {
return errors.New("task list not found")
}
tasks, err := s.taskRepo.ListTasks(ctx, spaceID, map[string]any{"task_list_id": taskListID})
if err != nil {
return err
}
if len(tasks) > 0 {
return errors.New("cannot delete task list with tasks")
}
return s.taskListRepo.DeleteTaskList(ctx, taskListID)
}
func (s *TaskService) ListStatuses(ctx context.Context, spaceID, userID bson.ObjectID) ([]*dto.TaskStatusDTO, error) {
if err := s.requireSpaceAccess(ctx, userID, spaceID); err != nil {
return nil, err