package handlers import ( "errors" "mime/multipart" "net/http" "strconv" "strings" "time" "github.com/gin-gonic/gin" "github.com/shopspring/decimal" "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 *gin.Context) { user := c.MustGet(middleware.AuthUserKey).(*models.User) response, err := h.taskService.ListTasks(user.ID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, response) } // GetTask handles GET /api/tasks/:id/ func (h *TaskHandler) GetTask(c *gin.Context) { user := c.MustGet(middleware.AuthUserKey).(*models.User) taskID, err := strconv.ParseUint(c.Param("id"), 10, 32) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid task ID"}) return } response, err := h.taskService.GetTask(uint(taskID), user.ID) if err != nil { switch { case errors.Is(err, services.ErrTaskNotFound): c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) case errors.Is(err, services.ErrTaskAccessDenied): c.JSON(http.StatusForbidden, gin.H{"error": err.Error()}) default: c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) } return } c.JSON(http.StatusOK, response) } // GetTasksByResidence handles GET /api/tasks/by-residence/:residence_id/ func (h *TaskHandler) GetTasksByResidence(c *gin.Context) { user := c.MustGet(middleware.AuthUserKey).(*models.User) residenceID, err := strconv.ParseUint(c.Param("residence_id"), 10, 32) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid residence ID"}) return } daysThreshold := 30 if d := c.Query("days_threshold"); d != "" { if parsed, err := strconv.Atoi(d); err == nil { daysThreshold = parsed } } response, err := h.taskService.GetTasksByResidence(uint(residenceID), user.ID, daysThreshold) if err != nil { switch { case errors.Is(err, services.ErrResidenceAccessDenied): c.JSON(http.StatusForbidden, gin.H{"error": err.Error()}) default: c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) } return } c.JSON(http.StatusOK, response) } // CreateTask handles POST /api/tasks/ func (h *TaskHandler) CreateTask(c *gin.Context) { user := c.MustGet(middleware.AuthUserKey).(*models.User) var req requests.CreateTaskRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } response, err := h.taskService.CreateTask(&req, user.ID) if err != nil { if errors.Is(err, services.ErrResidenceAccessDenied) { c.JSON(http.StatusForbidden, gin.H{"error": err.Error()}) return } c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusCreated, response) } // UpdateTask handles PUT/PATCH /api/tasks/:id/ func (h *TaskHandler) UpdateTask(c *gin.Context) { user := c.MustGet(middleware.AuthUserKey).(*models.User) taskID, err := strconv.ParseUint(c.Param("id"), 10, 32) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid task ID"}) return } var req requests.UpdateTaskRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } response, err := h.taskService.UpdateTask(uint(taskID), user.ID, &req) if err != nil { switch { case errors.Is(err, services.ErrTaskNotFound): c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) case errors.Is(err, services.ErrTaskAccessDenied): c.JSON(http.StatusForbidden, gin.H{"error": err.Error()}) default: c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) } return } c.JSON(http.StatusOK, response) } // DeleteTask handles DELETE /api/tasks/:id/ func (h *TaskHandler) DeleteTask(c *gin.Context) { user := c.MustGet(middleware.AuthUserKey).(*models.User) taskID, err := strconv.ParseUint(c.Param("id"), 10, 32) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid task ID"}) return } err = h.taskService.DeleteTask(uint(taskID), user.ID) if err != nil { switch { case errors.Is(err, services.ErrTaskNotFound): c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) case errors.Is(err, services.ErrTaskAccessDenied): c.JSON(http.StatusForbidden, gin.H{"error": err.Error()}) default: c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) } return } c.JSON(http.StatusOK, gin.H{"message": "Task deleted successfully"}) } // MarkInProgress handles POST /api/tasks/:id/mark-in-progress/ func (h *TaskHandler) MarkInProgress(c *gin.Context) { user := c.MustGet(middleware.AuthUserKey).(*models.User) taskID, err := strconv.ParseUint(c.Param("id"), 10, 32) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid task ID"}) return } response, err := h.taskService.MarkInProgress(uint(taskID), user.ID) if err != nil { switch { case errors.Is(err, services.ErrTaskNotFound): c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) case errors.Is(err, services.ErrTaskAccessDenied): c.JSON(http.StatusForbidden, gin.H{"error": err.Error()}) default: c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) } return } c.JSON(http.StatusOK, gin.H{"message": "Task marked as in progress", "task": response}) } // CancelTask handles POST /api/tasks/:id/cancel/ func (h *TaskHandler) CancelTask(c *gin.Context) { user := c.MustGet(middleware.AuthUserKey).(*models.User) taskID, err := strconv.ParseUint(c.Param("id"), 10, 32) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid task ID"}) return } response, err := h.taskService.CancelTask(uint(taskID), user.ID) if err != nil { switch { case errors.Is(err, services.ErrTaskNotFound): c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) case errors.Is(err, services.ErrTaskAccessDenied): c.JSON(http.StatusForbidden, gin.H{"error": err.Error()}) case errors.Is(err, services.ErrTaskAlreadyCancelled): c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) default: c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) } return } c.JSON(http.StatusOK, gin.H{"message": "Task cancelled", "task": response}) } // UncancelTask handles POST /api/tasks/:id/uncancel/ func (h *TaskHandler) UncancelTask(c *gin.Context) { user := c.MustGet(middleware.AuthUserKey).(*models.User) taskID, err := strconv.ParseUint(c.Param("id"), 10, 32) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid task ID"}) return } response, err := h.taskService.UncancelTask(uint(taskID), user.ID) if err != nil { switch { case errors.Is(err, services.ErrTaskNotFound): c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) case errors.Is(err, services.ErrTaskAccessDenied): c.JSON(http.StatusForbidden, gin.H{"error": err.Error()}) default: c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) } return } c.JSON(http.StatusOK, gin.H{"message": "Task uncancelled", "task": response}) } // ArchiveTask handles POST /api/tasks/:id/archive/ func (h *TaskHandler) ArchiveTask(c *gin.Context) { user := c.MustGet(middleware.AuthUserKey).(*models.User) taskID, err := strconv.ParseUint(c.Param("id"), 10, 32) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid task ID"}) return } response, err := h.taskService.ArchiveTask(uint(taskID), user.ID) if err != nil { switch { case errors.Is(err, services.ErrTaskNotFound): c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) case errors.Is(err, services.ErrTaskAccessDenied): c.JSON(http.StatusForbidden, gin.H{"error": err.Error()}) case errors.Is(err, services.ErrTaskAlreadyArchived): c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) default: c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) } return } c.JSON(http.StatusOK, gin.H{"message": "Task archived", "task": response}) } // UnarchiveTask handles POST /api/tasks/:id/unarchive/ func (h *TaskHandler) UnarchiveTask(c *gin.Context) { user := c.MustGet(middleware.AuthUserKey).(*models.User) taskID, err := strconv.ParseUint(c.Param("id"), 10, 32) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid task ID"}) return } response, err := h.taskService.UnarchiveTask(uint(taskID), user.ID) if err != nil { switch { case errors.Is(err, services.ErrTaskNotFound): c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) case errors.Is(err, services.ErrTaskAccessDenied): c.JSON(http.StatusForbidden, gin.H{"error": err.Error()}) default: c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) } return } c.JSON(http.StatusOK, gin.H{"message": "Task unarchived", "task": response}) } // === Task Completions === // GetTaskCompletions handles GET /api/tasks/:id/completions/ func (h *TaskHandler) GetTaskCompletions(c *gin.Context) { user := c.MustGet(middleware.AuthUserKey).(*models.User) taskID, err := strconv.ParseUint(c.Param("id"), 10, 32) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid task ID"}) return } response, err := h.taskService.GetCompletionsByTask(uint(taskID), user.ID) if err != nil { switch { case errors.Is(err, services.ErrTaskNotFound): c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) case errors.Is(err, services.ErrTaskAccessDenied): c.JSON(http.StatusForbidden, gin.H{"error": err.Error()}) default: c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) } return } c.JSON(http.StatusOK, response) } // ListCompletions handles GET /api/task-completions/ func (h *TaskHandler) ListCompletions(c *gin.Context) { user := c.MustGet(middleware.AuthUserKey).(*models.User) response, err := h.taskService.ListCompletions(user.ID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, response) } // GetCompletion handles GET /api/task-completions/:id/ func (h *TaskHandler) GetCompletion(c *gin.Context) { user := c.MustGet(middleware.AuthUserKey).(*models.User) completionID, err := strconv.ParseUint(c.Param("id"), 10, 32) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid completion ID"}) return } response, err := h.taskService.GetCompletion(uint(completionID), user.ID) if err != nil { switch { case errors.Is(err, services.ErrCompletionNotFound): c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) case errors.Is(err, services.ErrTaskAccessDenied): c.JSON(http.StatusForbidden, gin.H{"error": err.Error()}) default: c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) } 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 *gin.Context) { user := c.MustGet(middleware.AuthUserKey).(*models.User) var req requests.CreateTaskCompletionRequest contentType := c.GetHeader("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 c.JSON(http.StatusBadRequest, gin.H{"error": "failed to parse multipart form: " + err.Error()}) return } // Parse task_id (required) taskIDStr := c.PostForm("task_id") if taskIDStr == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "task_id is required"}) return } taskID, err := strconv.ParseUint(taskIDStr, 10, 32) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid task_id"}) return } req.TaskID = uint(taskID) // Parse notes (optional) req.Notes = c.PostForm("notes") // Parse actual_cost (optional) if costStr := c.PostForm("actual_cost"); costStr != "" { cost, err := decimal.NewFromString(costStr) if err == nil { req.ActualCost = &cost } } // Parse completed_at (optional) if completedAtStr := c.PostForm("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 { c.JSON(http.StatusBadRequest, gin.H{"error": "failed to upload image: " + err.Error()}) return } req.ImageURLs = append(req.ImageURLs, result.URL) } } } else { // Standard JSON request if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } } response, err := h.taskService.CreateCompletion(&req, user.ID) if err != nil { switch { case errors.Is(err, services.ErrTaskNotFound): c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) case errors.Is(err, services.ErrTaskAccessDenied): c.JSON(http.StatusForbidden, gin.H{"error": err.Error()}) default: c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) } return } c.JSON(http.StatusCreated, response) } // DeleteCompletion handles DELETE /api/task-completions/:id/ func (h *TaskHandler) DeleteCompletion(c *gin.Context) { user := c.MustGet(middleware.AuthUserKey).(*models.User) completionID, err := strconv.ParseUint(c.Param("id"), 10, 32) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid completion ID"}) return } err = h.taskService.DeleteCompletion(uint(completionID), user.ID) if err != nil { switch { case errors.Is(err, services.ErrCompletionNotFound): c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) case errors.Is(err, services.ErrTaskAccessDenied): c.JSON(http.StatusForbidden, gin.H{"error": err.Error()}) default: c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) } return } c.JSON(http.StatusOK, gin.H{"message": "Completion deleted successfully"}) } // === Lookups === // GetCategories handles GET /api/tasks/categories/ func (h *TaskHandler) GetCategories(c *gin.Context) { categories, err := h.taskService.GetCategories() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, categories) } // GetPriorities handles GET /api/tasks/priorities/ func (h *TaskHandler) GetPriorities(c *gin.Context) { priorities, err := h.taskService.GetPriorities() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, priorities) } // GetStatuses handles GET /api/tasks/statuses/ func (h *TaskHandler) GetStatuses(c *gin.Context) { statuses, err := h.taskService.GetStatuses() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, statuses) } // GetFrequencies handles GET /api/tasks/frequencies/ func (h *TaskHandler) GetFrequencies(c *gin.Context) { frequencies, err := h.taskService.GetFrequencies() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, frequencies) }