Add quick-complete endpoint for iOS widget task completion

- Add lightweight POST /api/tasks/:id/quick-complete/ endpoint
- Creates task completion with minimal processing for widget use
- Returns only 200 OK on success (no response body)
- Updates task status and next_due_date based on frequency
- Sends completion notification asynchronously

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Trey t
2025-12-08 12:02:44 -06:00
parent 1a48fbfb20
commit e152a6308a
3 changed files with 85 additions and 0 deletions

View File

@@ -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/

View File

@@ -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)
}

View File

@@ -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