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

@@ -0,0 +1,308 @@
# Task Kanban Categorization
This document explains how tasks are categorized into kanban columns in the Casera application.
## Overview
The task categorization system uses the **Chain of Responsibility** design pattern to determine which kanban column a task belongs to. Each handler in the chain evaluates the task against specific criteria, and if matched, returns the appropriate column. If not matched, the task is passed to the next handler.
## Architecture
### Design Pattern: Chain of Responsibility
```
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Cancelled │───▶│ Completed │───▶│ In Progress │───▶│ Overdue │───▶│ Due Soon │───▶│ Upcoming │
│ Handler │ │ Handler │ │ Handler │ │ Handler │ │ Handler │ │ Handler │
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
│ │ │ │ │ │
▼ ▼ ▼ ▼ ▼ ▼
"cancelled_tasks" "completed_tasks" "in_progress_tasks" "overdue_tasks" "due_soon_tasks" "upcoming_tasks"
```
### Source Files
- **Chain Implementation**: `internal/task/categorization/chain.go`
- **Tests**: `internal/task/categorization/chain_test.go`
- **Legacy Wrapper**: `internal/dto/responses/task.go` (`DetermineKanbanColumn`)
- **Repository Usage**: `internal/repositories/task_repo.go` (`GetKanbanData`)
## Kanban Columns
The system supports 6 kanban columns, evaluated in strict priority order:
| Priority | Column Name | Display Name | Description |
|----------|-------------|--------------|-------------|
| 1 | `cancelled_tasks` | Cancelled | Tasks that have been cancelled |
| 2 | `completed_tasks` | Completed | One-time tasks that are done |
| 3 | `in_progress_tasks` | In Progress | Tasks currently being worked on |
| 4 | `overdue_tasks` | Overdue | Tasks past their due date |
| 5 | `due_soon_tasks` | Due Soon | Tasks due within the threshold (default: 30 days) |
| 6 | `upcoming_tasks` | Upcoming | All other active tasks |
## Categorization Logic (Step by Step)
### Step 1: Cancelled Check (Highest Priority)
**Handler**: `CancelledHandler`
**Condition**: `task.IsCancelled == true`
**Result**: `cancelled_tasks`
A cancelled task always goes to the cancelled column, regardless of any other attributes. This is the first check because cancellation represents a terminal state that overrides all other considerations.
```go
if task.IsCancelled {
return "cancelled_tasks"
}
```
### Step 2: Completed Check
**Handler**: `CompletedHandler`
**Condition**: `task.NextDueDate == nil && len(task.Completions) > 0`
**Result**: `completed_tasks`
A task is considered "completed" when:
1. It has at least one completion record (someone marked it done)
2. AND it has no `NextDueDate` (meaning it's a one-time task)
**Important**: Recurring tasks with completions but with a `NextDueDate` set are NOT considered completed - they continue in their active cycle.
```go
if task.NextDueDate == nil && len(task.Completions) > 0 {
return "completed_tasks"
}
```
### Step 3: In Progress Check
**Handler**: `InProgressHandler`
**Condition**: `task.Status != nil && task.Status.Name == "In Progress"`
**Result**: `in_progress_tasks`
Tasks with the "In Progress" status are grouped together regardless of their due date. This allows users to see what's actively being worked on.
```go
if task.Status != nil && task.Status.Name == "In Progress" {
return "in_progress_tasks"
}
```
### Step 4: Overdue Check
**Handler**: `OverdueHandler`
**Condition**: `effectiveDate.Before(now)`
**Result**: `overdue_tasks`
The "effective date" is determined as:
1. `NextDueDate` (preferred - used for recurring tasks after completion)
2. `DueDate` (fallback - used for initial scheduling)
If the effective date is in the past, the task is overdue.
```go
effectiveDate := task.NextDueDate
if effectiveDate == nil {
effectiveDate = task.DueDate
}
if effectiveDate != nil && effectiveDate.Before(now) {
return "overdue_tasks"
}
```
### Step 5: Due Soon Check
**Handler**: `DueSoonHandler`
**Condition**: `effectiveDate.Before(threshold)`
**Result**: `due_soon_tasks`
Where `threshold = now + daysThreshold` (default 30 days)
Tasks due within the threshold period are considered "due soon" and need attention.
```go
threshold := now.AddDate(0, 0, daysThreshold)
if effectiveDate != nil && effectiveDate.Before(threshold) {
return "due_soon_tasks"
}
```
### Step 6: Upcoming (Default)
**Handler**: `UpcomingHandler`
**Condition**: None (catches all remaining tasks)
**Result**: `upcoming_tasks`
Any task that doesn't match the above criteria falls into "upcoming". This includes:
- Tasks with due dates beyond the threshold
- Tasks with no due date set
- Future scheduled tasks
```go
return "upcoming_tasks"
```
## Key Concepts
### DueDate vs NextDueDate
| Field | Purpose | When Set |
|-------|---------|----------|
| `DueDate` | Original/initial due date | When task is created |
| `NextDueDate` | Next occurrence for recurring tasks | After each completion |
**For recurring tasks**: `NextDueDate` takes precedence over `DueDate` for categorization. After completing a recurring task:
1. A new completion record is created
2. `NextDueDate` is calculated as `completionDate + frequencyDays`
3. Status is reset to "Pending"
4. Task moves to the appropriate column based on the new `NextDueDate`
### One-Time vs Recurring Tasks
**One-Time Tasks** (frequency = "Once" or no frequency):
- When completed: `NextDueDate` is set to `nil`
- Categorization: Goes to `completed_tasks` column
- Status: Remains as-is (usually "Completed" or last status)
**Recurring Tasks** (weekly, monthly, annually, etc.):
- When completed: `NextDueDate` is set to `completionDate + frequencyDays`
- Categorization: Based on new `NextDueDate` (could be upcoming, due_soon, etc.)
- Status: **Reset to "Pending"** to allow proper column placement
### The "In Progress" Gotcha
**Important**: Tasks with "In Progress" status will stay in the `in_progress_tasks` column even if they're overdue!
This is by design - "In Progress" indicates active work, so the task should be visible there. However, this means:
1. If a recurring task is marked "In Progress" and then completed
2. The status MUST be reset to "Pending" after completion
3. Otherwise, the task stays in "In Progress" instead of moving to "Upcoming"
This is handled automatically in `TaskService.CreateCompletion()`:
```go
if isRecurringTask {
pendingStatusID := uint(1)
task.StatusID = &pendingStatusID
}
```
## Configuration
### Days Threshold
The `daysThreshold` parameter controls what counts as "due soon":
- Default: 30 days
- Configurable per-request via query parameter
```go
// Example: Consider tasks due within 7 days as "due soon"
chain.Categorize(task, 7)
```
## Column Metadata
Each column has associated metadata for UI rendering:
```go
{
Name: "overdue_tasks",
DisplayName: "Overdue",
ButtonTypes: []string{"edit", "complete", "cancel", "mark_in_progress"},
Icons: map[string]string{"ios": "exclamationmark.triangle", "android": "Warning"},
Color: "#FF3B30",
}
```
### Button Types by Column
| Column | Available Actions |
|--------|-------------------|
| `overdue_tasks` | edit, complete, cancel, mark_in_progress |
| `in_progress_tasks` | edit, complete, cancel |
| `due_soon_tasks` | edit, complete, cancel, mark_in_progress |
| `upcoming_tasks` | edit, complete, cancel, mark_in_progress |
| `completed_tasks` | (none - read-only) |
| `cancelled_tasks` | uncancel, delete |
## Usage Examples
### Basic Categorization
```go
import "github.com/treytartt/casera-api/internal/task/categorization"
task := &models.Task{
DueDate: time.Now().AddDate(0, 0, 15), // 15 days from now
Status: &models.TaskStatus{Name: "Pending"},
}
column := categorization.DetermineKanbanColumn(task, 30)
// Returns: "due_soon_tasks"
```
### Categorize Multiple Tasks
```go
tasks := []models.Task{...}
columns := categorization.CategorizeTasksIntoColumns(tasks, 30)
// Returns map[KanbanColumn][]models.Task with tasks organized by column
```
### Custom Chain (Advanced)
```go
chain := categorization.NewChain()
ctx := categorization.NewContext(task, 14) // 14-day threshold
column := chain.CategorizeWithContext(ctx)
```
## Testing
The categorization logic is thoroughly tested in `chain_test.go`:
```bash
go test ./internal/task/categorization/... -v
```
Key test scenarios:
- Each handler's matching criteria
- Priority order between handlers
- Recurring task lifecycle
- Edge cases (nil dates, empty completions, etc.)
## Troubleshooting
### Task stuck in wrong column?
1. **Check `IsCancelled`**: Cancelled takes highest priority
2. **Check `NextDueDate`**: For recurring tasks, this determines placement
3. **Check `Status`**: "In Progress" overrides date-based categorization
4. **Check `Completions`**: Empty + nil NextDueDate = upcoming, not completed
### Recurring task not moving to Upcoming after completion?
Verify that `CreateCompletion` is:
1. Setting `NextDueDate` correctly
2. Resetting `StatusID` to Pending (1)
### Task showing overdue but due date looks correct?
The system uses UTC times. Ensure dates are compared in UTC:
```go
now := time.Now().UTC()
```