Add actionable push notifications and fix recurring task completion

Features:
- Add task action buttons to push notifications (complete, view, cancel, etc.)
- Add button types logic for different task states (overdue, in_progress, etc.)
- Implement Chain of Responsibility pattern for task categorization
- Add comprehensive kanban categorization documentation

Fixes:
- Reset recurring task status to Pending after completion so tasks appear
  in correct kanban column (was staying in "In Progress")
- Fix PostgreSQL EXTRACT function error in overdue notifications query
- Update seed data to properly set next_due_date for recurring tasks

Admin:
- Add tasks list to residence detail page
- Fix task edit page to properly handle all fields

🤖 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-05 14:23:14 -06:00
parent bbf3999c79
commit 1b06c0639c
22 changed files with 2715 additions and 142 deletions

View File

@@ -3,7 +3,6 @@ package services
import (
"context"
"errors"
"fmt"
"time"
"github.com/rs/zerolog/log"
@@ -142,6 +141,7 @@ func (s *TaskService) CreateTask(req *requests.CreateTaskRequest, userID uint) (
return nil, ErrResidenceAccessDenied
}
dueDate := req.DueDate.ToTimePtr()
task := &models.Task{
ResidenceID: req.ResidenceID,
CreatedByID: userID,
@@ -152,7 +152,8 @@ func (s *TaskService) CreateTask(req *requests.CreateTaskRequest, userID uint) (
StatusID: req.StatusID,
FrequencyID: req.FrequencyID,
AssignedToID: req.AssignedToID,
DueDate: req.DueDate.ToTimePtr(),
DueDate: dueDate,
NextDueDate: dueDate, // Initialize next_due_date to due_date
EstimatedCost: req.EstimatedCost,
ContractorID: req.ContractorID,
}
@@ -213,7 +214,13 @@ func (s *TaskService) UpdateTask(taskID, userID uint, req *requests.UpdateTaskRe
task.AssignedToID = req.AssignedToID
}
if req.DueDate != nil {
task.DueDate = req.DueDate.ToTimePtr()
newDueDate := req.DueDate.ToTimePtr()
task.DueDate = newDueDate
// Also update NextDueDate if the task doesn't have completions yet
// (if it has completions, NextDueDate should be managed by completion logic)
if len(task.Completions) == 0 {
task.NextDueDate = newDueDate
}
}
if req.EstimatedCost != nil {
task.EstimatedCost = req.EstimatedCost
@@ -482,6 +489,27 @@ func (s *TaskService) CreateCompletion(req *requests.CreateTaskCompletionRequest
return nil, err
}
// Update next_due_date and status based on frequency
// - If frequency is "Once" (days = nil or 0), set next_due_date to nil
// - If frequency is recurring, calculate next_due_date = completion_date + frequency_days
// and reset status to "Pending" so task shows in correct kanban column
if task.Frequency == nil || task.Frequency.Days == nil || *task.Frequency.Days == 0 {
// One-time task - clear next_due_date since it's completed
task.NextDueDate = nil
} 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) so task appears in upcoming/due_soon
// instead of staying in "In Progress" column
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 completion")
}
// Create images if provided
for _, imageURL := range req.ImageURLs {
if imageURL != "" {
@@ -539,15 +567,6 @@ func (s *TaskService) sendTaskCompletedNotification(task *models.Task, completio
completedByName = completion.CompletedBy.GetFullName()
}
title := "Task Completed"
body := fmt.Sprintf("%s completed: %s", completedByName, task.Title)
data := map[string]interface{}{
"task_id": task.ID,
"residence_id": task.ResidenceID,
"completion_id": completion.ID,
}
// Notify all users
for _, user := range users {
isCompleter := user.ID == completion.CompletedByID
@@ -556,13 +575,11 @@ func (s *TaskService) sendTaskCompletedNotification(task *models.Task, completio
if !isCompleter && s.notificationService != nil {
go func(userID uint) {
ctx := context.Background()
if err := s.notificationService.CreateAndSendNotification(
if err := s.notificationService.CreateAndSendTaskNotification(
ctx,
userID,
models.NotificationTaskCompleted,
title,
body,
data,
task,
); err != nil {
log.Error().Err(err).Uint("user_id", userID).Uint("task_id", task.ID).Msg("Failed to send task completion push notification")
}