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:
@@ -295,6 +295,31 @@ func (h *TaskHandler) UnarchiveTask(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, response)
|
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 ===
|
// === Task Completions ===
|
||||||
|
|
||||||
// GetTaskCompletions handles GET /api/tasks/:id/completions/
|
// GetTaskCompletions handles GET /api/tasks/:id/completions/
|
||||||
|
|||||||
@@ -298,6 +298,7 @@ func setupTaskRoutes(api *gin.RouterGroup, taskHandler *handlers.TaskHandler) {
|
|||||||
tasks.POST("/:id/uncancel/", taskHandler.UncancelTask)
|
tasks.POST("/:id/uncancel/", taskHandler.UncancelTask)
|
||||||
tasks.POST("/:id/archive/", taskHandler.ArchiveTask)
|
tasks.POST("/:id/archive/", taskHandler.ArchiveTask)
|
||||||
tasks.POST("/:id/unarchive/", taskHandler.UnarchiveTask)
|
tasks.POST("/:id/unarchive/", taskHandler.UnarchiveTask)
|
||||||
|
tasks.POST("/:id/quick-complete/", taskHandler.QuickComplete)
|
||||||
tasks.GET("/:id/completions/", taskHandler.GetTaskCompletions)
|
tasks.GET("/:id/completions/", taskHandler.GetTaskCompletions)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -593,6 +593,65 @@ func (s *TaskService) CreateCompletion(req *requests.CreateTaskCompletionRequest
|
|||||||
}, nil
|
}, 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
|
// sendTaskCompletedNotification sends notifications when a task is completed
|
||||||
func (s *TaskService) sendTaskCompletedNotification(task *models.Task, completion *models.TaskCompletion) {
|
func (s *TaskService) sendTaskCompletedNotification(task *models.Task, completion *models.TaskCompletion) {
|
||||||
// Get all users with access to this residence
|
// Get all users with access to this residence
|
||||||
|
|||||||
Reference in New Issue
Block a user