package handlers import ( "mime/multipart" "net/http" "strconv" "strings" "time" "github.com/labstack/echo/v4" "github.com/shopspring/decimal" "github.com/treytartt/casera-api/internal/apperrors" "github.com/treytartt/casera-api/internal/dto/requests" "github.com/treytartt/casera-api/internal/middleware" "github.com/treytartt/casera-api/internal/models" "github.com/treytartt/casera-api/internal/services" ) // TaskHandler handles task-related HTTP requests type TaskHandler struct { taskService *services.TaskService storageService *services.StorageService } // NewTaskHandler creates a new task handler func NewTaskHandler(taskService *services.TaskService, storageService *services.StorageService) *TaskHandler { return &TaskHandler{ taskService: taskService, storageService: storageService, } } // ListTasks handles GET /api/tasks/ func (h *TaskHandler) ListTasks(c echo.Context) error { user := c.Get(middleware.AuthUserKey).(*models.User) userNow := middleware.GetUserNow(c) // Auto-capture timezone from header for background job calculations (e.g., daily digest) // This runs in a goroutine to avoid blocking the response if tzHeader := c.Request().Header.Get("X-Timezone"); tzHeader != "" { go h.taskService.UpdateUserTimezone(user.ID, tzHeader) } response, err := h.taskService.ListTasks(user.ID, userNow) if err != nil { return err } return c.JSON(http.StatusOK, response) } // GetTask handles GET /api/tasks/:id/ func (h *TaskHandler) GetTask(c echo.Context) error { user := c.Get(middleware.AuthUserKey).(*models.User) taskID, err := strconv.ParseUint(c.Param("id"), 10, 32) if err != nil { return apperrors.BadRequest("error.invalid_task_id") } response, err := h.taskService.GetTask(uint(taskID), user.ID) if err != nil { return err } return c.JSON(http.StatusOK, response) } // GetTasksByResidence handles GET /api/tasks/by-residence/:residence_id/ func (h *TaskHandler) GetTasksByResidence(c echo.Context) error { user := c.Get(middleware.AuthUserKey).(*models.User) userNow := middleware.GetUserNow(c) residenceID, err := strconv.ParseUint(c.Param("residence_id"), 10, 32) if err != nil { return apperrors.BadRequest("error.invalid_residence_id") } daysThreshold := 30 if d := c.QueryParam("days_threshold"); d != "" { if parsed, err := strconv.Atoi(d); err == nil { daysThreshold = parsed } } response, err := h.taskService.GetTasksByResidence(uint(residenceID), user.ID, daysThreshold, userNow) if err != nil { return err } return c.JSON(http.StatusOK, response) } // CreateTask handles POST /api/tasks/ func (h *TaskHandler) CreateTask(c echo.Context) error { user := c.Get(middleware.AuthUserKey).(*models.User) userNow := middleware.GetUserNow(c) var req requests.CreateTaskRequest if err := c.Bind(&req); err != nil { return apperrors.BadRequest("error.invalid_request") } response, err := h.taskService.CreateTask(&req, user.ID, userNow) if err != nil { return err } return c.JSON(http.StatusCreated, response) } // UpdateTask handles PUT/PATCH /api/tasks/:id/ func (h *TaskHandler) UpdateTask(c echo.Context) error { user := c.Get(middleware.AuthUserKey).(*models.User) userNow := middleware.GetUserNow(c) taskID, err := strconv.ParseUint(c.Param("id"), 10, 32) if err != nil { return apperrors.BadRequest("error.invalid_task_id") } var req requests.UpdateTaskRequest if err := c.Bind(&req); err != nil { return apperrors.BadRequest("error.invalid_request") } response, err := h.taskService.UpdateTask(uint(taskID), user.ID, &req, userNow) if err != nil { return err } return c.JSON(http.StatusOK, response) } // DeleteTask handles DELETE /api/tasks/:id/ func (h *TaskHandler) DeleteTask(c echo.Context) error { user := c.Get(middleware.AuthUserKey).(*models.User) taskID, err := strconv.ParseUint(c.Param("id"), 10, 32) if err != nil { return apperrors.BadRequest("error.invalid_task_id") } response, err := h.taskService.DeleteTask(uint(taskID), user.ID) if err != nil { return err } return c.JSON(http.StatusOK, response) } // MarkInProgress handles POST /api/tasks/:id/mark-in-progress/ func (h *TaskHandler) MarkInProgress(c echo.Context) error { user := c.Get(middleware.AuthUserKey).(*models.User) userNow := middleware.GetUserNow(c) taskID, err := strconv.ParseUint(c.Param("id"), 10, 32) if err != nil { return apperrors.BadRequest("error.invalid_task_id") } response, err := h.taskService.MarkInProgress(uint(taskID), user.ID, userNow) if err != nil { return err } return c.JSON(http.StatusOK, response) } // CancelTask handles POST /api/tasks/:id/cancel/ func (h *TaskHandler) CancelTask(c echo.Context) error { user := c.Get(middleware.AuthUserKey).(*models.User) userNow := middleware.GetUserNow(c) taskID, err := strconv.ParseUint(c.Param("id"), 10, 32) if err != nil { return apperrors.BadRequest("error.invalid_task_id") } response, err := h.taskService.CancelTask(uint(taskID), user.ID, userNow) if err != nil { return err } return c.JSON(http.StatusOK, response) } // UncancelTask handles POST /api/tasks/:id/uncancel/ func (h *TaskHandler) UncancelTask(c echo.Context) error { user := c.Get(middleware.AuthUserKey).(*models.User) userNow := middleware.GetUserNow(c) taskID, err := strconv.ParseUint(c.Param("id"), 10, 32) if err != nil { return apperrors.BadRequest("error.invalid_task_id") } response, err := h.taskService.UncancelTask(uint(taskID), user.ID, userNow) if err != nil { return err } return c.JSON(http.StatusOK, response) } // ArchiveTask handles POST /api/tasks/:id/archive/ func (h *TaskHandler) ArchiveTask(c echo.Context) error { user := c.Get(middleware.AuthUserKey).(*models.User) userNow := middleware.GetUserNow(c) taskID, err := strconv.ParseUint(c.Param("id"), 10, 32) if err != nil { return apperrors.BadRequest("error.invalid_task_id") } response, err := h.taskService.ArchiveTask(uint(taskID), user.ID, userNow) if err != nil { return err } return c.JSON(http.StatusOK, response) } // UnarchiveTask handles POST /api/tasks/:id/unarchive/ func (h *TaskHandler) UnarchiveTask(c echo.Context) error { user := c.Get(middleware.AuthUserKey).(*models.User) userNow := middleware.GetUserNow(c) taskID, err := strconv.ParseUint(c.Param("id"), 10, 32) if err != nil { return apperrors.BadRequest("error.invalid_task_id") } response, err := h.taskService.UnarchiveTask(uint(taskID), user.ID, userNow) if err != nil { return err } return c.JSON(http.StatusOK, response) } // QuickComplete handles POST /api/tasks/:id/quick-complete/ // Lightweight endpoint for widget - just returns 200 OK on success func (h *TaskHandler) QuickComplete(c echo.Context) error { user := c.Get(middleware.AuthUserKey).(*models.User) taskID, err := strconv.ParseUint(c.Param("id"), 10, 32) if err != nil { return apperrors.BadRequest("error.invalid_task_id") } err = h.taskService.QuickComplete(uint(taskID), user.ID) if err != nil { return err } return c.NoContent(http.StatusOK) } // === Task Completions === // GetTaskCompletions handles GET /api/tasks/:id/completions/ func (h *TaskHandler) GetTaskCompletions(c echo.Context) error { user := c.Get(middleware.AuthUserKey).(*models.User) taskID, err := strconv.ParseUint(c.Param("id"), 10, 32) if err != nil { return apperrors.BadRequest("error.invalid_task_id") } response, err := h.taskService.GetCompletionsByTask(uint(taskID), user.ID) if err != nil { return err } return c.JSON(http.StatusOK, response) } // ListCompletions handles GET /api/task-completions/ func (h *TaskHandler) ListCompletions(c echo.Context) error { user := c.Get(middleware.AuthUserKey).(*models.User) response, err := h.taskService.ListCompletions(user.ID) if err != nil { return err } return c.JSON(http.StatusOK, response) } // GetCompletion handles GET /api/task-completions/:id/ func (h *TaskHandler) GetCompletion(c echo.Context) error { user := c.Get(middleware.AuthUserKey).(*models.User) completionID, err := strconv.ParseUint(c.Param("id"), 10, 32) if err != nil { return apperrors.BadRequest("error.invalid_completion_id") } response, err := h.taskService.GetCompletion(uint(completionID), user.ID) if err != nil { return err } return c.JSON(http.StatusOK, response) } // CreateCompletion handles POST /api/task-completions/ // Supports both JSON and multipart form data (for image uploads) func (h *TaskHandler) CreateCompletion(c echo.Context) error { user := c.Get(middleware.AuthUserKey).(*models.User) userNow := middleware.GetUserNow(c) var req requests.CreateTaskCompletionRequest contentType := c.Request().Header.Get("Content-Type") // Check if this is a multipart form request (image upload) if strings.HasPrefix(contentType, "multipart/form-data") { // Parse multipart form if err := c.Request().ParseMultipartForm(32 << 20); err != nil { // 32MB max return apperrors.BadRequest("error.failed_to_parse_form") } // Parse task_id (required) taskIDStr := c.FormValue("task_id") if taskIDStr == "" { return apperrors.BadRequest("error.task_id_required") } taskID, err := strconv.ParseUint(taskIDStr, 10, 32) if err != nil { return apperrors.BadRequest("error.invalid_task_id_value") } req.TaskID = uint(taskID) // Parse notes (optional) req.Notes = c.FormValue("notes") // Parse actual_cost (optional) if costStr := c.FormValue("actual_cost"); costStr != "" { cost, err := decimal.NewFromString(costStr) if err == nil { req.ActualCost = &cost } } // Parse completed_at (optional) if completedAtStr := c.FormValue("completed_at"); completedAtStr != "" { if t, err := time.Parse(time.RFC3339, completedAtStr); err == nil { req.CompletedAt = &t } } // Handle image upload (look for "images" or "image" or "photo" field) var imageFile interface{} for _, fieldName := range []string{"images", "image", "photo"} { if file, err := c.FormFile(fieldName); err == nil { imageFile = file break } } if imageFile != nil { file := imageFile.(*multipart.FileHeader) if h.storageService != nil { result, err := h.storageService.Upload(file, "completions") if err != nil { return apperrors.BadRequest("error.failed_to_upload_image") } req.ImageURLs = append(req.ImageURLs, result.URL) } } } else { // Standard JSON request if err := c.Bind(&req); err != nil { return apperrors.BadRequest("error.invalid_request") } } response, err := h.taskService.CreateCompletion(&req, user.ID, userNow) if err != nil { return err } return c.JSON(http.StatusCreated, response) } // DeleteCompletion handles DELETE /api/task-completions/:id/ func (h *TaskHandler) DeleteCompletion(c echo.Context) error { user := c.Get(middleware.AuthUserKey).(*models.User) completionID, err := strconv.ParseUint(c.Param("id"), 10, 32) if err != nil { return apperrors.BadRequest("error.invalid_completion_id") } response, err := h.taskService.DeleteCompletion(uint(completionID), user.ID) if err != nil { return err } return c.JSON(http.StatusOK, response) } // === Lookups === // GetCategories handles GET /api/tasks/categories/ func (h *TaskHandler) GetCategories(c echo.Context) error { categories, err := h.taskService.GetCategories() if err != nil { return err } return c.JSON(http.StatusOK, categories) } // GetPriorities handles GET /api/tasks/priorities/ func (h *TaskHandler) GetPriorities(c echo.Context) error { priorities, err := h.taskService.GetPriorities() if err != nil { return err } return c.JSON(http.StatusOK, priorities) } // GetFrequencies handles GET /api/tasks/frequencies/ func (h *TaskHandler) GetFrequencies(c echo.Context) error { frequencies, err := h.taskService.GetFrequencies() if err != nil { return err } return c.JSON(http.StatusOK, frequencies) }