Files
honeyDueAPI/docs/TASK_KANBAN_CATEGORIZATION.md
Trey t 215e7c895d wip
2026-02-18 10:54:18 -06:00

9.8 KiB

Task Kanban Categorization

This document explains how tasks are categorized into kanban columns in the Casera application.

Note: The categorization chain still computes cancelled_tasks, but the kanban board response intentionally hides cancelled/archived tasks and returns only 5 visible columns.

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.InProgress == true

Result: in_progress_tasks

Tasks marked as "In Progress" are grouped together regardless of their due date. This allows users to see what's actively being worked on.

if task.InProgress {
    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 in_progress flag MUST be reset to false after completion
  3. Otherwise, the task stays in "In Progress" instead of moving to "Upcoming"

This is handled automatically in TaskService.CreateCompletion():

if isRecurringTask {
    task.InProgress = false
}

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
    InProgress: false,
}

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 InProgress: true 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 InProgress to false

Task showing overdue but due date looks correct?

The system uses UTC times. Ensure dates are compared in UTC:

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