diff --git a/internal/handlers/task_handler.go b/internal/handlers/task_handler.go index 3b0ae3d..2bbcd6c 100644 --- a/internal/handlers/task_handler.go +++ b/internal/handlers/task_handler.go @@ -295,6 +295,31 @@ func (h *TaskHandler) UnarchiveTask(c *gin.Context) { 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 *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": i18n.LocalizedMessage(c, "error.invalid_task_id")}) + return + } + + err = h.taskService.QuickComplete(uint(taskID), user.ID) + if err != nil { + switch { + case errors.Is(err, services.ErrTaskNotFound): + c.JSON(http.StatusNotFound, gin.H{"error": i18n.LocalizedMessage(c, "error.task_not_found")}) + case errors.Is(err, services.ErrTaskAccessDenied): + c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.task_access_denied")}) + default: + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + } + return + } + c.Status(http.StatusOK) +} + // === Task Completions === // GetTaskCompletions handles GET /api/tasks/:id/completions/ diff --git a/internal/router/router.go b/internal/router/router.go index 8beeb59..00370b0 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -298,6 +298,7 @@ func setupTaskRoutes(api *gin.RouterGroup, taskHandler *handlers.TaskHandler) { tasks.POST("/:id/uncancel/", taskHandler.UncancelTask) tasks.POST("/:id/archive/", taskHandler.ArchiveTask) tasks.POST("/:id/unarchive/", taskHandler.UnarchiveTask) + tasks.POST("/:id/quick-complete/", taskHandler.QuickComplete) tasks.GET("/:id/completions/", taskHandler.GetTaskCompletions) } diff --git a/internal/services/task_service.go b/internal/services/task_service.go index 9d5aea7..c86b3b9 100644 --- a/internal/services/task_service.go +++ b/internal/services/task_service.go @@ -593,6 +593,65 @@ func (s *TaskService) CreateCompletion(req *requests.CreateTaskCompletionRequest }, nil } +// QuickComplete creates a minimal task completion (for widget use) +// Returns only success/error, no response body +func (s *TaskService) QuickComplete(taskID uint, userID uint) error { + // Get the task + task, err := s.taskRepo.FindByID(taskID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return ErrTaskNotFound + } + return err + } + + // Check access + hasAccess, err := s.residenceRepo.HasAccess(task.ResidenceID, userID) + if err != nil { + return err + } + if !hasAccess { + return ErrTaskAccessDenied + } + + completedAt := time.Now().UTC() + + completion := &models.TaskCompletion{ + TaskID: taskID, + CompletedByID: userID, + CompletedAt: completedAt, + Notes: "Completed from widget", + } + + if err := s.taskRepo.CreateCompletion(completion); err != nil { + return err + } + + // Update next_due_date and status based on frequency + if task.Frequency == nil || task.Frequency.Days == nil || *task.Frequency.Days == 0 { + // One-time task - clear next_due_date and set status to "Completed" (ID=3) + task.NextDueDate = nil + completedStatusID := uint(3) + task.StatusID = &completedStatusID + } else { + // Recurring task - calculate next due date from completion date + frequency + nextDue := completedAt.AddDate(0, 0, *task.Frequency.Days) + task.NextDueDate = &nextDue + + // Reset status to "Pending" (ID=1) + pendingStatusID := uint(1) + task.StatusID = &pendingStatusID + } + if err := s.taskRepo.Update(task); err != nil { + log.Error().Err(err).Uint("task_id", task.ID).Msg("Failed to update task after quick completion") + } + + // Send notification (fire and forget) + go s.sendTaskCompletedNotification(task, completion) + + return nil +} + // sendTaskCompletedNotification sends notifications when a task is completed func (s *TaskService) sendTaskCompletedNotification(task *models.Task, completion *models.TaskCompletion) { // Get all users with access to this residence