feat: task system
All checks were successful
Build and Push App Image / build-and-push (push) Successful in 1m55s

This commit is contained in:
domrichardson
2026-03-27 16:33:11 +00:00
parent d793b5ccf2
commit 1b336299ee
15 changed files with 3876 additions and 17 deletions

View File

@@ -0,0 +1,398 @@
package handlers
import (
"encoding/json"
"net/http"
"strings"
"gitea.hostxtra.co.uk/mrhid6/notely/backend/internal/application/dto"
"gitea.hostxtra.co.uk/mrhid6/notely/backend/internal/application/services"
"github.com/gorilla/mux"
"go.mongodb.org/mongo-driver/v2/bson"
)
// TaskHandler handles task and task status endpoints.
type TaskHandler struct {
taskService *services.TaskService
}
// NewTaskHandler creates a task handler.
func NewTaskHandler(taskService *services.TaskService) *TaskHandler {
return &TaskHandler{taskService: taskService}
}
func parseIDsFromRequest(r *http.Request) (bson.ObjectID, bson.ObjectID, error) {
userID, err := getUserObjectID(r)
if err != nil {
return bson.NilObjectID, bson.NilObjectID, err
}
spaceID, err := bson.ObjectIDFromHex(mux.Vars(r)["spaceId"])
if err != nil {
return bson.NilObjectID, bson.NilObjectID, err
}
return userID, spaceID, nil
}
func (h *TaskHandler) CreateTask(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.CreateTaskRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid request body", http.StatusBadRequest)
return
}
task, err := h.taskService.CreateTask(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(task)
}
func (h *TaskHandler) ListTasks(w http.ResponseWriter, r *http.Request) {
userID, spaceID, err := parseIDsFromRequest(r)
if err != nil {
http.Error(w, "invalid request", http.StatusBadRequest)
return
}
categoryID := strings.TrimSpace(r.URL.Query().Get("categoryId"))
statusID := strings.TrimSpace(r.URL.Query().Get("statusId"))
parentTaskID := strings.TrimSpace(r.URL.Query().Get("parentTaskId"))
categoryFilter := &categoryID
statusFilter := &statusID
parentFilter := &parentTaskID
if categoryID == "" {
categoryFilter = nil
}
if statusID == "" {
statusFilter = nil
}
if parentTaskID == "" {
parentFilter = nil
}
tasks, err := h.taskService.ListTasks(r.Context(), spaceID, userID, categoryFilter, statusFilter, parentFilter)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(tasks)
}
func (h *TaskHandler) SearchTasks(w http.ResponseWriter, r *http.Request) {
userID, spaceID, err := parseIDsFromRequest(r)
if err != nil {
http.Error(w, "invalid request", http.StatusBadRequest)
return
}
query := r.URL.Query().Get("q")
tasks, err := h.taskService.SearchTasks(r.Context(), spaceID, userID, query)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(tasks)
}
func (h *TaskHandler) GetTask(w http.ResponseWriter, r *http.Request) {
userID, spaceID, err := parseIDsFromRequest(r)
if err != nil {
http.Error(w, "invalid request", http.StatusBadRequest)
return
}
taskID, err := bson.ObjectIDFromHex(mux.Vars(r)["taskId"])
if err != nil {
http.Error(w, "invalid task id", http.StatusBadRequest)
return
}
task, err := h.taskService.GetTaskByID(r.Context(), spaceID, taskID, userID)
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(task)
}
func (h *TaskHandler) UpdateTask(w http.ResponseWriter, r *http.Request) {
userID, spaceID, err := parseIDsFromRequest(r)
if err != nil {
http.Error(w, "invalid request", http.StatusBadRequest)
return
}
taskID, err := bson.ObjectIDFromHex(mux.Vars(r)["taskId"])
if err != nil {
http.Error(w, "invalid task id", http.StatusBadRequest)
return
}
var req dto.UpdateTaskRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid request body", http.StatusBadRequest)
return
}
task, err := h.taskService.UpdateTask(r.Context(), spaceID, taskID, userID, &req)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(task)
}
func (h *TaskHandler) DeleteTask(w http.ResponseWriter, r *http.Request) {
userID, spaceID, err := parseIDsFromRequest(r)
if err != nil {
http.Error(w, "invalid request", http.StatusBadRequest)
return
}
taskID, err := bson.ObjectIDFromHex(mux.Vars(r)["taskId"])
if err != nil {
http.Error(w, "invalid task id", http.StatusBadRequest)
return
}
if err := h.taskService.DeleteTask(r.Context(), spaceID, taskID, userID); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
w.WriteHeader(http.StatusNoContent)
}
func (h *TaskHandler) TransitionTaskStatus(w http.ResponseWriter, r *http.Request) {
userID, spaceID, err := parseIDsFromRequest(r)
if err != nil {
http.Error(w, "invalid request", http.StatusBadRequest)
return
}
taskID, err := bson.ObjectIDFromHex(mux.Vars(r)["taskId"])
if err != nil {
http.Error(w, "invalid task id", http.StatusBadRequest)
return
}
var req dto.TaskTransitionRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid request body", http.StatusBadRequest)
return
}
task, err := h.taskService.TransitionTaskStatus(r.Context(), spaceID, taskID, userID, req.Direction)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(task)
}
func (h *TaskHandler) LinkTaskNote(w http.ResponseWriter, r *http.Request) {
userID, spaceID, err := parseIDsFromRequest(r)
if err != nil {
http.Error(w, "invalid request", http.StatusBadRequest)
return
}
taskID, err := bson.ObjectIDFromHex(mux.Vars(r)["taskId"])
if err != nil {
http.Error(w, "invalid task id", http.StatusBadRequest)
return
}
var req dto.LinkTaskNoteRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid request body", http.StatusBadRequest)
return
}
noteID, err := bson.ObjectIDFromHex(req.NoteID)
if err != nil {
http.Error(w, "invalid note id", http.StatusBadRequest)
return
}
task, err := h.taskService.LinkNoteToTask(r.Context(), spaceID, taskID, noteID, userID)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(task)
}
func (h *TaskHandler) UnlinkTaskNote(w http.ResponseWriter, r *http.Request) {
userID, spaceID, err := parseIDsFromRequest(r)
if err != nil {
http.Error(w, "invalid request", http.StatusBadRequest)
return
}
taskID, err := bson.ObjectIDFromHex(mux.Vars(r)["taskId"])
if err != nil {
http.Error(w, "invalid task id", http.StatusBadRequest)
return
}
noteID, err := bson.ObjectIDFromHex(mux.Vars(r)["noteId"])
if err != nil {
http.Error(w, "invalid note id", http.StatusBadRequest)
return
}
task, err := h.taskService.UnlinkNoteFromTask(r.Context(), spaceID, taskID, noteID, userID)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(task)
}
func (h *TaskHandler) ListTasksByNote(w http.ResponseWriter, r *http.Request) {
userID, spaceID, err := parseIDsFromRequest(r)
if err != nil {
http.Error(w, "invalid request", http.StatusBadRequest)
return
}
noteID, err := bson.ObjectIDFromHex(mux.Vars(r)["noteId"])
if err != nil {
http.Error(w, "invalid note id", http.StatusBadRequest)
return
}
tasks, err := h.taskService.ListTasksLinkedToNote(r.Context(), spaceID, noteID, userID)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(tasks)
}
func (h *TaskHandler) ListStatuses(w http.ResponseWriter, r *http.Request) {
userID, spaceID, err := parseIDsFromRequest(r)
if err != nil {
http.Error(w, "invalid request", http.StatusBadRequest)
return
}
statuses, err := h.taskService.ListStatuses(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(statuses)
}
func (h *TaskHandler) CreateStatus(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.CreateTaskStatusRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid request body", http.StatusBadRequest)
return
}
status, err := h.taskService.CreateStatus(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(status)
}
func (h *TaskHandler) UpdateStatus(w http.ResponseWriter, r *http.Request) {
userID, spaceID, err := parseIDsFromRequest(r)
if err != nil {
http.Error(w, "invalid request", http.StatusBadRequest)
return
}
statusID, err := bson.ObjectIDFromHex(mux.Vars(r)["statusId"])
if err != nil {
http.Error(w, "invalid status id", http.StatusBadRequest)
return
}
var req dto.UpdateTaskStatusRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid request body", http.StatusBadRequest)
return
}
status, err := h.taskService.UpdateStatus(r.Context(), spaceID, statusID, userID, &req)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(status)
}
func (h *TaskHandler) DeleteStatus(w http.ResponseWriter, r *http.Request) {
userID, spaceID, err := parseIDsFromRequest(r)
if err != nil {
http.Error(w, "invalid request", http.StatusBadRequest)
return
}
statusID, err := bson.ObjectIDFromHex(mux.Vars(r)["statusId"])
if err != nil {
http.Error(w, "invalid status id", http.StatusBadRequest)
return
}
if err := h.taskService.DeleteStatus(r.Context(), spaceID, statusID, userID); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
w.WriteHeader(http.StatusNoContent)
}
func (h *TaskHandler) ReorderStatuses(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.ReorderTaskStatusesRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid request body", http.StatusBadRequest)
return
}
statuses, err := h.taskService.ReorderStatuses(r.Context(), spaceID, userID, req.OrderedStatusIDs)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(statuses)
}