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:
@@ -95,6 +95,7 @@ type TaskResponse struct {
|
||||
FrequencyID *uint `json:"frequency_id"`
|
||||
Frequency *TaskFrequencyResponse `json:"frequency,omitempty"`
|
||||
DueDate *time.Time `json:"due_date"`
|
||||
NextDueDate *time.Time `json:"next_due_date"` // For recurring tasks, updated after each completion
|
||||
EstimatedCost *decimal.Decimal `json:"estimated_cost"`
|
||||
ActualCost *decimal.Decimal `json:"actual_cost"`
|
||||
ContractorID *uint `json:"contractor_id"`
|
||||
@@ -229,7 +230,13 @@ func NewTaskCompletionResponse(c *models.TaskCompletion) TaskCompletionResponse
|
||||
}
|
||||
|
||||
// NewTaskResponse creates a TaskResponse from a Task model
|
||||
// Always includes kanban_column using default 30-day threshold
|
||||
func NewTaskResponse(t *models.Task) TaskResponse {
|
||||
return NewTaskResponseWithThreshold(t, 30)
|
||||
}
|
||||
|
||||
// NewTaskResponseWithThreshold creates a TaskResponse with a custom days threshold for kanban column
|
||||
func NewTaskResponseWithThreshold(t *models.Task, daysThreshold int) TaskResponse {
|
||||
resp := TaskResponse{
|
||||
ID: t.ID,
|
||||
ResidenceID: t.ResidenceID,
|
||||
@@ -242,6 +249,7 @@ func NewTaskResponse(t *models.Task) TaskResponse {
|
||||
FrequencyID: t.FrequencyID,
|
||||
AssignedToID: t.AssignedToID,
|
||||
DueDate: t.DueDate,
|
||||
NextDueDate: t.NextDueDate,
|
||||
EstimatedCost: t.EstimatedCost,
|
||||
ActualCost: t.ActualCost,
|
||||
ContractorID: t.ContractorID,
|
||||
@@ -249,6 +257,7 @@ func NewTaskResponse(t *models.Task) TaskResponse {
|
||||
IsArchived: t.IsArchived,
|
||||
ParentTaskID: t.ParentTaskID,
|
||||
CompletionCount: len(t.Completions),
|
||||
KanbanColumn: DetermineKanbanColumn(t, daysThreshold),
|
||||
CreatedAt: t.CreatedAt,
|
||||
UpdatedAt: t.UpdatedAt,
|
||||
}
|
||||
@@ -348,17 +357,23 @@ func NewTaskCompletionWithTaskResponse(c *models.TaskCompletion, task *models.Ta
|
||||
resp := NewTaskCompletionResponse(c)
|
||||
|
||||
if task != nil {
|
||||
taskResp := NewTaskResponse(task)
|
||||
taskResp.KanbanColumn = DetermineKanbanColumn(task, daysThreshold)
|
||||
taskResp := NewTaskResponseWithThreshold(task, daysThreshold)
|
||||
resp.Task = &taskResp
|
||||
}
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
// DetermineKanbanColumn determines which kanban column a task belongs to
|
||||
// Uses the same logic as task_repo.go GetKanbanData
|
||||
// DetermineKanbanColumn determines which kanban column a task belongs to.
|
||||
// This is a wrapper around the Chain of Responsibility implementation in
|
||||
// internal/task/categorization package. See that package for detailed
|
||||
// documentation on the categorization logic.
|
||||
//
|
||||
// Deprecated: Use categorization.DetermineKanbanColumn directly for new code.
|
||||
func DetermineKanbanColumn(task *models.Task, daysThreshold int) string {
|
||||
// Import would cause circular dependency, so we replicate the logic here
|
||||
// for backwards compatibility. The authoritative implementation is in
|
||||
// internal/task/categorization/chain.go
|
||||
if daysThreshold <= 0 {
|
||||
daysThreshold = 30 // Default
|
||||
}
|
||||
@@ -366,31 +381,37 @@ func DetermineKanbanColumn(task *models.Task, daysThreshold int) string {
|
||||
now := time.Now().UTC()
|
||||
threshold := now.AddDate(0, 0, daysThreshold)
|
||||
|
||||
// Priority order (same as GetKanbanData):
|
||||
// 1. Cancelled
|
||||
// Priority order (Chain of Responsibility):
|
||||
// 1. Cancelled (highest priority)
|
||||
if task.IsCancelled {
|
||||
return "cancelled_tasks"
|
||||
}
|
||||
|
||||
// 2. Completed (has completions)
|
||||
if len(task.Completions) > 0 {
|
||||
// 2. Completed (one-time task with nil next_due_date and has completions)
|
||||
if task.NextDueDate == nil && len(task.Completions) > 0 {
|
||||
return "completed_tasks"
|
||||
}
|
||||
|
||||
// 3. In Progress
|
||||
// 3. In Progress (status check)
|
||||
if task.Status != nil && task.Status.Name == "In Progress" {
|
||||
return "in_progress_tasks"
|
||||
}
|
||||
|
||||
// 4. Due date based
|
||||
if task.DueDate != nil {
|
||||
if task.DueDate.Before(now) {
|
||||
// 4. Overdue (next_due_date or due_date is in the past)
|
||||
effectiveDate := task.NextDueDate
|
||||
if effectiveDate == nil {
|
||||
effectiveDate = task.DueDate
|
||||
}
|
||||
if effectiveDate != nil {
|
||||
if effectiveDate.Before(now) {
|
||||
return "overdue_tasks"
|
||||
} else if task.DueDate.Before(threshold) {
|
||||
}
|
||||
// 5. Due Soon (within threshold)
|
||||
if effectiveDate.Before(threshold) {
|
||||
return "due_soon_tasks"
|
||||
}
|
||||
}
|
||||
|
||||
// Default: upcoming
|
||||
// 6. Upcoming (default/fallback)
|
||||
return "upcoming_tasks"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user