Files
honeyDueAPI/docs/TASK_KANBAN_CATEGORIZATION.md
Trey t 1b06c0639c 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>
2025-12-05 14:23:14 -06:00

9.8 KiB

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.

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.

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.

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.

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.

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
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():

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
// Example: Consider tasks due within 7 days as "due soon"
chain.Categorize(task, 7)

Column Metadata

Each column has associated metadata for UI rendering:

{
    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

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

tasks := []models.Task{...}
columns := categorization.CategorizeTasksIntoColumns(tasks, 30)

// Returns map[KanbanColumn][]models.Task with tasks organized by column

Custom Chain (Advanced)

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:

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:

now := time.Now().UTC()