package handlers import ( "encoding/json" "fmt" "net/http" "time" "git.jnss.me/joakim/opal/internal/engine" "github.com/go-chi/chi/v5" "github.com/google/uuid" ) // Response helpers (avoiding import cycle) func jsonResponse(w http.ResponseWriter, status int, data interface{}) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) json.NewEncoder(w).Encode(map[string]interface{}{ "success": status >= 200 && status < 300, "data": data, }) } func errorResponse(w http.ResponseWriter, status int, message string) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) json.NewEncoder(w).Encode(map[string]interface{}{ "success": false, "error": message, }) } // ListTasks returns tasks based on filter query parameters func ListTasks(w http.ResponseWriter, r *http.Request) { query := r.URL.Query() // Build filter from query params filter := engine.NewFilter() // Status filter if status := query.Get("status"); status != "" { filter.Attributes["status"] = status } // Project filter if project := query.Get("project"); project != "" { filter.Attributes["project"] = project } // Priority filter if priority := query.Get("priority"); priority != "" { filter.Attributes["priority"] = priority } // Tag filters if tags := query["tag"]; len(tags) > 0 { filter.IncludeTags = tags } // Get tasks tasks, err := engine.GetTasks(filter) if err != nil { errorResponse(w, http.StatusInternalServerError, err.Error()) return } jsonResponse(w, http.StatusOK, tasks) } // CreateTaskRequest represents the request body for creating a task type CreateTaskRequest struct { Description string `json:"description"` Tags []string `json:"tags,omitempty"` Project *string `json:"project,omitempty"` Priority *string `json:"priority,omitempty"` Due *int64 `json:"due,omitempty"` Scheduled *int64 `json:"scheduled,omitempty"` Wait *int64 `json:"wait,omitempty"` Until *int64 `json:"until,omitempty"` Recurrence *string `json:"recurrence,omitempty"` } // CreateTask creates a new task func CreateTask(w http.ResponseWriter, r *http.Request) { var req CreateTaskRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { errorResponse(w, http.StatusBadRequest, "invalid request body") return } if req.Description == "" { errorResponse(w, http.StatusBadRequest, "description is required") return } // Build modifier from request mod := engine.NewModifier() mod.AddTags = req.Tags if req.Project != nil { mod.SetAttributes["project"] = req.Project } if req.Priority != nil { mod.SetAttributes["priority"] = req.Priority } if req.Due != nil { dueStr := fmt.Sprintf("%d", *req.Due) mod.SetAttributes["due"] = &dueStr } if req.Scheduled != nil { scheduledStr := fmt.Sprintf("%d", *req.Scheduled) mod.SetAttributes["scheduled"] = &scheduledStr } if req.Wait != nil { waitStr := fmt.Sprintf("%d", *req.Wait) mod.SetAttributes["wait"] = &waitStr } if req.Until != nil { untilStr := fmt.Sprintf("%d", *req.Until) mod.SetAttributes["until"] = &untilStr } if req.Recurrence != nil { mod.SetAttributes["recurrence"] = req.Recurrence } // Create task task, err := engine.CreateTaskWithModifier(req.Description, mod) if err != nil { errorResponse(w, http.StatusInternalServerError, err.Error()) return } jsonResponse(w, http.StatusCreated, task) } // GetTask returns a single task by UUID func GetTask(w http.ResponseWriter, r *http.Request) { uuidStr := chi.URLParam(r, "uuid") taskUUID, err := uuid.Parse(uuidStr) if err != nil { errorResponse(w, http.StatusBadRequest, "invalid UUID") return } task, err := engine.GetTask(taskUUID) if err != nil { errorResponse(w, http.StatusNotFound, "task not found") return } jsonResponse(w, http.StatusOK, task) } // UpdateTaskRequest represents the request body for updating a task type UpdateTaskRequest struct { Description *string `json:"description,omitempty"` Status *string `json:"status,omitempty"` Priority *string `json:"priority,omitempty"` Project *string `json:"project,omitempty"` Due *int64 `json:"due,omitempty"` Scheduled *int64 `json:"scheduled,omitempty"` Wait *int64 `json:"wait,omitempty"` Until *int64 `json:"until,omitempty"` Start *int64 `json:"start,omitempty"` Recurrence *string `json:"recurrence,omitempty"` } // UpdateTask updates an existing task func UpdateTask(w http.ResponseWriter, r *http.Request) { uuidStr := chi.URLParam(r, "uuid") taskUUID, err := uuid.Parse(uuidStr) if err != nil { errorResponse(w, http.StatusBadRequest, "invalid UUID") return } task, err := engine.GetTask(taskUUID) if err != nil { errorResponse(w, http.StatusNotFound, "task not found") return } var req UpdateTaskRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { errorResponse(w, http.StatusBadRequest, "invalid request body") return } // Build modifier mod := engine.NewModifier() if req.Description != nil { mod.SetAttributes["description"] = req.Description } if req.Status != nil { mod.SetAttributes["status"] = req.Status } if req.Priority != nil { mod.SetAttributes["priority"] = req.Priority } if req.Project != nil { mod.SetAttributes["project"] = req.Project } if req.Due != nil { dueStr := fmt.Sprintf("%d", *req.Due) mod.SetAttributes["due"] = &dueStr } if req.Scheduled != nil { scheduledStr := fmt.Sprintf("%d", *req.Scheduled) mod.SetAttributes["scheduled"] = &scheduledStr } if req.Wait != nil { waitStr := fmt.Sprintf("%d", *req.Wait) mod.SetAttributes["wait"] = &waitStr } if req.Until != nil { untilStr := fmt.Sprintf("%d", *req.Until) mod.SetAttributes["until"] = &untilStr } if req.Start != nil { t := time.Unix(*req.Start, 0) task.Start = &t } if req.Recurrence != nil { mod.SetAttributes["recurrence"] = req.Recurrence } // Apply modifier if err := mod.Apply(task); err != nil { errorResponse(w, http.StatusBadRequest, err.Error()) return } jsonResponse(w, http.StatusOK, task) } // DeleteTask deletes a task func DeleteTask(w http.ResponseWriter, r *http.Request) { uuidStr := chi.URLParam(r, "uuid") taskUUID, err := uuid.Parse(uuidStr) if err != nil { errorResponse(w, http.StatusBadRequest, "invalid UUID") return } task, err := engine.GetTask(taskUUID) if err != nil { errorResponse(w, http.StatusNotFound, "task not found") return } // Check for permanent delete flag permanent := r.URL.Query().Get("permanent") == "true" if err := task.Delete(permanent); err != nil { errorResponse(w, http.StatusInternalServerError, err.Error()) return } jsonResponse(w, http.StatusOK, map[string]string{"message": "task deleted"}) } // CompleteTask marks a task as completed func CompleteTask(w http.ResponseWriter, r *http.Request) { uuidStr := chi.URLParam(r, "uuid") taskUUID, err := uuid.Parse(uuidStr) if err != nil { errorResponse(w, http.StatusBadRequest, "invalid UUID") return } task, err := engine.GetTask(taskUUID) if err != nil { errorResponse(w, http.StatusNotFound, "task not found") return } if err := task.Complete(); err != nil { errorResponse(w, http.StatusInternalServerError, err.Error()) return } jsonResponse(w, http.StatusOK, task) } // StartTask sets the start time for a task func StartTask(w http.ResponseWriter, r *http.Request) { uuidStr := chi.URLParam(r, "uuid") taskUUID, err := uuid.Parse(uuidStr) if err != nil { errorResponse(w, http.StatusBadRequest, "invalid UUID") return } task, err := engine.GetTask(taskUUID) if err != nil { errorResponse(w, http.StatusNotFound, "task not found") return } if err := task.StartTask(); err != nil { errorResponse(w, http.StatusInternalServerError, err.Error()) return } jsonResponse(w, http.StatusOK, task) } // StopTask clears the start time for a task func StopTask(w http.ResponseWriter, r *http.Request) { uuidStr := chi.URLParam(r, "uuid") taskUUID, err := uuid.Parse(uuidStr) if err != nil { errorResponse(w, http.StatusBadRequest, "invalid UUID") return } task, err := engine.GetTask(taskUUID) if err != nil { errorResponse(w, http.StatusNotFound, "task not found") return } if err := task.StopTask(); err != nil { errorResponse(w, http.StatusInternalServerError, err.Error()) return } jsonResponse(w, http.StatusOK, task) } // GetTaskTags returns all tags for a task func GetTaskTags(w http.ResponseWriter, r *http.Request) { uuidStr := chi.URLParam(r, "uuid") taskUUID, err := uuid.Parse(uuidStr) if err != nil { errorResponse(w, http.StatusBadRequest, "invalid UUID") return } task, err := engine.GetTask(taskUUID) if err != nil { errorResponse(w, http.StatusNotFound, "task not found") return } jsonResponse(w, http.StatusOK, task.Tags) } // AddTaskTagRequest represents the request to add a tag type AddTaskTagRequest struct { Tag string `json:"tag"` } // AddTaskTag adds a tag to a task func AddTaskTag(w http.ResponseWriter, r *http.Request) { uuidStr := chi.URLParam(r, "uuid") taskUUID, err := uuid.Parse(uuidStr) if err != nil { errorResponse(w, http.StatusBadRequest, "invalid UUID") return } task, err := engine.GetTask(taskUUID) if err != nil { errorResponse(w, http.StatusNotFound, "task not found") return } var req AddTaskTagRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { errorResponse(w, http.StatusBadRequest, "invalid request body") return } if req.Tag == "" { errorResponse(w, http.StatusBadRequest, "tag is required") return } if err := task.AddTag(req.Tag); err != nil { errorResponse(w, http.StatusInternalServerError, err.Error()) return } jsonResponse(w, http.StatusOK, task) } // RemoveTaskTag removes a tag from a task func RemoveTaskTag(w http.ResponseWriter, r *http.Request) { uuidStr := chi.URLParam(r, "uuid") tag := chi.URLParam(r, "tag") taskUUID, err := uuid.Parse(uuidStr) if err != nil { errorResponse(w, http.StatusBadRequest, "invalid UUID") return } task, err := engine.GetTask(taskUUID) if err != nil { errorResponse(w, http.StatusNotFound, "task not found") return } if err := task.RemoveTag(tag); err != nil { errorResponse(w, http.StatusInternalServerError, err.Error()) return } jsonResponse(w, http.StatusOK, task) }