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

@@ -135,6 +135,7 @@ func main() {
db.MembershipRepo,
db.NoteRepo,
db.CategoryRepo,
db.TaskListRepo,
db.UserRepo,
permissionService,
)
@@ -151,6 +152,7 @@ func main() {
categoryService := services.NewCategoryService(
db.CategoryRepo,
db.TaskListRepo,
db.MembershipRepo,
db.NoteRepo,
permissionService,
@@ -158,6 +160,7 @@ func main() {
taskService := services.NewTaskService(
db.TaskRepo,
db.TaskListRepo,
db.TaskStatusRepo,
db.NoteRepo,
db.CategoryRepo,
@@ -269,6 +272,11 @@ func main() {
api.HandleFunc("/spaces/{spaceId}/categories/{categoryId}/move", categoryHandler.MoveCategory).Methods("PATCH")
// Task endpoints
api.HandleFunc("/spaces/{spaceId}/task-lists", taskHandler.ListTaskLists).Methods("GET")
api.HandleFunc("/spaces/{spaceId}/task-lists", taskHandler.CreateTaskList).Methods("POST")
api.HandleFunc("/spaces/{spaceId}/task-lists/{taskListId}", taskHandler.UpdateTaskList).Methods("PUT")
api.HandleFunc("/spaces/{spaceId}/task-lists/{taskListId}", taskHandler.DeleteTaskList).Methods("DELETE")
api.HandleFunc("/spaces/{spaceId}/tasks", taskHandler.ListTasks).Methods("GET")
api.HandleFunc("/spaces/{spaceId}/tasks", taskHandler.CreateTask).Methods("POST")
api.HandleFunc("/spaces/{spaceId}/tasks/search", taskHandler.SearchTasks).Methods("GET")

View File

@@ -430,6 +430,7 @@ type CategoryTreeDTO struct {
*CategoryDTO
Subcategories []*CategoryTreeDTO `json:"subcategories"`
Notes []*NoteListItemDTO `json:"notes"`
TaskLists []*TaskListDTO `json:"task_lists"`
}
// NewCategoryDTO creates a DTO from a category entity
@@ -458,7 +459,7 @@ func NewCategoryDTO(category *entities.Category) *CategoryDTO {
type CreateTaskRequest struct {
Title string `json:"title" validate:"required,min=1,max=255"`
Description string `json:"description" validate:"max=2000"`
CategoryID *string `json:"category_id,omitempty"`
TaskListID string `json:"task_list_id" validate:"required"`
StatusID string `json:"status_id" validate:"required"`
ParentTaskID *string `json:"parent_task_id,omitempty"`
NoteLinks []string `json:"note_links"`
@@ -468,7 +469,7 @@ type CreateTaskRequest struct {
type UpdateTaskRequest struct {
Title *string `json:"title,omitempty"`
Description *string `json:"description,omitempty"`
CategoryID *string `json:"category_id,omitempty"`
TaskListID *string `json:"task_list_id,omitempty"`
StatusID *string `json:"status_id,omitempty"`
ParentTaskID *string `json:"parent_task_id,omitempty"`
NoteLinks []string `json:"note_links,omitempty"`
@@ -490,7 +491,7 @@ type TaskDTO struct {
SpaceID string `json:"space_id"`
Title string `json:"title"`
Description string `json:"description"`
CategoryID *string `json:"category_id,omitempty"`
TaskListID string `json:"task_list_id"`
StatusID string `json:"status_id"`
ParentTaskID *string `json:"parent_task_id,omitempty"`
Depth int `json:"depth"`
@@ -538,14 +539,35 @@ type TaskStatusDTO struct {
UpdatedAt string `json:"updated_at"`
}
// CreateTaskListRequest represents task list creation input.
type CreateTaskListRequest struct {
Name string `json:"name" validate:"required,min=1,max=120"`
Description string `json:"description" validate:"max=500"`
CategoryID *string `json:"category_id,omitempty"`
}
// UpdateTaskListRequest represents task list update input.
type UpdateTaskListRequest struct {
Name *string `json:"name,omitempty"`
Description *string `json:"description,omitempty"`
CategoryID *string `json:"category_id,omitempty"`
}
// TaskListDTO represents a task list in API responses.
type TaskListDTO struct {
ID string `json:"id"`
SpaceID string `json:"space_id"`
CategoryID *string `json:"category_id,omitempty"`
Name string `json:"name"`
Description string `json:"description"`
CreatedBy string `json:"created_by"`
UpdatedBy string `json:"updated_by"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
// NewTaskDTO creates a DTO from a task entity.
func NewTaskDTO(task *entities.Task) *TaskDTO {
var categoryID *string
if task.CategoryID != nil {
id := task.CategoryID.Hex()
categoryID = &id
}
var parentTaskID *string
if task.ParentTaskID != nil {
id := task.ParentTaskID.Hex()
@@ -562,7 +584,7 @@ func NewTaskDTO(task *entities.Task) *TaskDTO {
SpaceID: task.SpaceID.Hex(),
Title: task.Title,
Description: task.Description,
CategoryID: categoryID,
TaskListID: task.TaskListID.Hex(),
StatusID: task.StatusID.Hex(),
ParentTaskID: parentTaskID,
Depth: task.Depth,
@@ -574,6 +596,27 @@ func NewTaskDTO(task *entities.Task) *TaskDTO {
}
}
// NewTaskListDTO creates a DTO from a task list entity.
func NewTaskListDTO(taskList *entities.TaskList) *TaskListDTO {
var categoryID *string
if taskList.CategoryID != nil {
id := taskList.CategoryID.Hex()
categoryID = &id
}
return &TaskListDTO{
ID: taskList.ID.Hex(),
SpaceID: taskList.SpaceID.Hex(),
CategoryID: categoryID,
Name: taskList.Name,
Description: taskList.Description,
CreatedBy: taskList.CreatedBy.Hex(),
UpdatedBy: taskList.UpdatedBy.Hex(),
CreatedAt: taskList.CreatedAt.Format("2006-01-02T15:04:05Z"),
UpdatedAt: taskList.UpdatedAt.Format("2006-01-02T15:04:05Z"),
}
}
// NewTaskStatusDTO creates a DTO from a task status entity.
func NewTaskStatusDTO(status *entities.TaskStatus) *TaskStatusDTO {
return &TaskStatusDTO{

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

View File

@@ -14,7 +14,7 @@ type Task struct {
SpaceID bson.ObjectID `bson:"space_id"`
Title string `bson:"title"`
Description string `bson:"description"`
CategoryID *bson.ObjectID `bson:"category_id,omitempty"`
TaskListID bson.ObjectID `bson:"task_list_id"`
StatusID bson.ObjectID `bson:"status_id"`
ParentTaskID *bson.ObjectID `bson:"parent_task_id,omitempty"`
Depth int `bson:"depth"`
@@ -35,3 +35,16 @@ type TaskStatus struct {
CreatedAt time.Time `bson:"created_at"`
UpdatedAt time.Time `bson:"updated_at"`
}
// TaskList groups tasks under a named list that can be attached to a category.
type TaskList struct {
ID bson.ObjectID `bson:"_id,omitempty"`
SpaceID bson.ObjectID `bson:"space_id"`
CategoryID *bson.ObjectID `bson:"category_id,omitempty"`
Name string `bson:"name"`
Description string `bson:"description,omitempty"`
CreatedBy bson.ObjectID `bson:"created_by"`
UpdatedBy bson.ObjectID `bson:"updated_by"`
CreatedAt time.Time `bson:"created_at"`
UpdatedAt time.Time `bson:"updated_at"`
}

View File

@@ -229,6 +229,17 @@ type TaskRepository interface {
CountChildren(ctx context.Context, parentTaskID bson.ObjectID) (int64, error)
}
// TaskListRepository defines task list operations.
type TaskListRepository interface {
CreateTaskList(ctx context.Context, list *entities.TaskList) error
GetTaskListByID(ctx context.Context, id bson.ObjectID) (*entities.TaskList, error)
ListTaskLists(ctx context.Context, spaceID bson.ObjectID) ([]*entities.TaskList, error)
ListTaskListsByCategory(ctx context.Context, spaceID bson.ObjectID, categoryID bson.ObjectID) ([]*entities.TaskList, error)
UpdateTaskList(ctx context.Context, list *entities.TaskList) error
DeleteTaskList(ctx context.Context, id bson.ObjectID) error
DeleteTaskListsBySpaceID(ctx context.Context, spaceID bson.ObjectID) error
}
// TaskStatusRepository defines task status operations
type TaskStatusRepository interface {
CreateStatus(ctx context.Context, status *entities.TaskStatus) error

View File

@@ -16,6 +16,7 @@ type Database struct {
MembershipRepo *MembershipRepository
NoteRepo *NoteRepository
CategoryRepo *CategoryRepository
TaskListRepo *TaskListRepository
TaskRepo *TaskRepository
TaskStatusRepo *TaskStatusRepository
RevisionRepo *NoteRevisionRepository
@@ -49,6 +50,7 @@ func NewDatabase(ctx context.Context, mongoURL string) (*Database, error) {
MembershipRepo: NewMembershipRepository(db),
NoteRepo: NewNoteRepository(db),
CategoryRepo: NewCategoryRepository(db),
TaskListRepo: NewTaskListRepository(db),
TaskRepo: NewTaskRepository(db),
TaskStatusRepo: NewTaskStatusRepository(db),
RevisionRepo: NewNoteRevisionRepository(db),
@@ -87,6 +89,9 @@ func (d *Database) EnsureIndexes(ctx context.Context) error {
if err := d.TaskRepo.EnsureIndexes(ctx); err != nil {
return err
}
if err := d.TaskListRepo.EnsureIndexes(ctx); err != nil {
return err
}
if err := d.TaskStatusRepo.EnsureIndexes(ctx); err != nil {
return err
}

View File

@@ -108,13 +108,95 @@ func (r *TaskRepository) EnsureIndexes(ctx context.Context) error {
_, err := r.collection.Indexes().CreateMany(ctx, []mongo.IndexModel{
{Keys: bson.D{{Key: "space_id", Value: 1}, {Key: "updated_at", Value: -1}}},
{Keys: bson.D{{Key: "space_id", Value: 1}, {Key: "status_id", Value: 1}}},
{Keys: bson.D{{Key: "space_id", Value: 1}, {Key: "category_id", Value: 1}}},
{Keys: bson.D{{Key: "space_id", Value: 1}, {Key: "task_list_id", Value: 1}}},
{Keys: bson.D{{Key: "space_id", Value: 1}, {Key: "parent_task_id", Value: 1}}},
{Keys: bson.D{{Key: "space_id", Value: 1}, {Key: "note_links", Value: 1}}},
})
return err
}
// TaskListRepository implements task list data access.
type TaskListRepository struct {
collection *mongo.Collection
}
// NewTaskListRepository creates a new task list repository.
func NewTaskListRepository(db *mongo.Database) *TaskListRepository {
return &TaskListRepository{collection: db.Collection("task_lists")}
}
func (r *TaskListRepository) CreateTaskList(ctx context.Context, list *entities.TaskList) error {
list.ID = bson.NewObjectID()
list.CreatedAt = time.Now()
list.UpdatedAt = time.Now()
_, err := r.collection.InsertOne(ctx, list)
return err
}
func (r *TaskListRepository) GetTaskListByID(ctx context.Context, id bson.ObjectID) (*entities.TaskList, error) {
var list entities.TaskList
err := r.collection.FindOne(ctx, bson.M{"_id": id}).Decode(&list)
if err != nil {
if err == mongo.ErrNoDocuments {
return nil, errors.New("task list not found")
}
return nil, err
}
return &list, nil
}
func (r *TaskListRepository) ListTaskLists(ctx context.Context, spaceID bson.ObjectID) ([]*entities.TaskList, error) {
cursor, err := r.collection.Find(ctx, bson.M{"space_id": spaceID}, options.Find().SetSort(bson.D{{Key: "name", Value: 1}}))
if err != nil {
return nil, err
}
defer cursor.Close(ctx)
var lists []*entities.TaskList
if err := cursor.All(ctx, &lists); err != nil {
return nil, err
}
return lists, nil
}
func (r *TaskListRepository) ListTaskListsByCategory(ctx context.Context, spaceID bson.ObjectID, categoryID bson.ObjectID) ([]*entities.TaskList, error) {
cursor, err := r.collection.Find(ctx, bson.M{"space_id": spaceID, "category_id": categoryID}, options.Find().SetSort(bson.D{{Key: "name", Value: 1}}))
if err != nil {
return nil, err
}
defer cursor.Close(ctx)
var lists []*entities.TaskList
if err := cursor.All(ctx, &lists); err != nil {
return nil, err
}
return lists, nil
}
func (r *TaskListRepository) UpdateTaskList(ctx context.Context, list *entities.TaskList) error {
list.UpdatedAt = time.Now()
_, err := r.collection.ReplaceOne(ctx, bson.M{"_id": list.ID}, list)
return err
}
func (r *TaskListRepository) DeleteTaskList(ctx context.Context, id bson.ObjectID) error {
_, err := r.collection.DeleteOne(ctx, bson.M{"_id": id})
return err
}
func (r *TaskListRepository) DeleteTaskListsBySpaceID(ctx context.Context, spaceID bson.ObjectID) error {
_, err := r.collection.DeleteMany(ctx, bson.M{"space_id": spaceID})
return err
}
func (r *TaskListRepository) EnsureIndexes(ctx context.Context) error {
_, err := r.collection.Indexes().CreateMany(ctx, []mongo.IndexModel{
{Keys: bson.D{{Key: "space_id", Value: 1}, {Key: "name", Value: 1}}, Options: options.Index().SetUnique(true)},
{Keys: bson.D{{Key: "space_id", Value: 1}, {Key: "category_id", Value: 1}}},
})
return err
}
// TaskStatusRepository implements task status data access.
type TaskStatusRepository struct {
collection *mongo.Collection

View File

@@ -64,15 +64,15 @@ func (h *TaskHandler) ListTasks(w http.ResponseWriter, r *http.Request) {
return
}
categoryID := strings.TrimSpace(r.URL.Query().Get("categoryId"))
taskListID := strings.TrimSpace(r.URL.Query().Get("taskListId"))
statusID := strings.TrimSpace(r.URL.Query().Get("statusId"))
parentTaskID := strings.TrimSpace(r.URL.Query().Get("parentTaskId"))
categoryFilter := &categoryID
taskListFilter := &taskListID
statusFilter := &statusID
parentFilter := &parentTaskID
if categoryID == "" {
categoryFilter = nil
if taskListID == "" {
taskListFilter = nil
}
if statusID == "" {
statusFilter = nil
@@ -81,7 +81,7 @@ func (h *TaskHandler) ListTasks(w http.ResponseWriter, r *http.Request) {
parentFilter = nil
}
tasks, err := h.taskService.ListTasks(r.Context(), spaceID, userID, categoryFilter, statusFilter, parentFilter)
tasks, err := h.taskService.ListTasks(r.Context(), spaceID, userID, taskListFilter, statusFilter, parentFilter)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
@@ -91,6 +91,94 @@ func (h *TaskHandler) ListTasks(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(tasks)
}
func (h *TaskHandler) ListTaskLists(w http.ResponseWriter, r *http.Request) {
userID, spaceID, err := parseIDsFromRequest(r)
if err != nil {
http.Error(w, "invalid request", http.StatusBadRequest)
return
}
lists, err := h.taskService.ListTaskLists(r.Context(), spaceID, userID)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(lists)
}
func (h *TaskHandler) CreateTaskList(w http.ResponseWriter, r *http.Request) {
userID, spaceID, err := parseIDsFromRequest(r)
if err != nil {
http.Error(w, "invalid request", http.StatusBadRequest)
return
}
var req dto.CreateTaskListRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid request body", http.StatusBadRequest)
return
}
list, err := h.taskService.CreateTaskList(r.Context(), spaceID, userID, &req)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(list)
}
func (h *TaskHandler) UpdateTaskList(w http.ResponseWriter, r *http.Request) {
userID, spaceID, err := parseIDsFromRequest(r)
if err != nil {
http.Error(w, "invalid request", http.StatusBadRequest)
return
}
taskListID, err := bson.ObjectIDFromHex(mux.Vars(r)["taskListId"])
if err != nil {
http.Error(w, "invalid task list id", http.StatusBadRequest)
return
}
var req dto.UpdateTaskListRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid request body", http.StatusBadRequest)
return
}
list, err := h.taskService.UpdateTaskList(r.Context(), spaceID, taskListID, userID, &req)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(list)
}
func (h *TaskHandler) DeleteTaskList(w http.ResponseWriter, r *http.Request) {
userID, spaceID, err := parseIDsFromRequest(r)
if err != nil {
http.Error(w, "invalid request", http.StatusBadRequest)
return
}
taskListID, err := bson.ObjectIDFromHex(mux.Vars(r)["taskListId"])
if err != nil {
http.Error(w, "invalid task list id", http.StatusBadRequest)
return
}
if err := h.taskService.DeleteTaskList(r.Context(), spaceID, taskListID, userID); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
w.WriteHeader(http.StatusNoContent)
}
func (h *TaskHandler) SearchTasks(w http.ResponseWriter, r *http.Request) {
userID, spaceID, err := parseIDsFromRequest(r)
if err != nil {