Total rebrand across all Go API source files: - Go module path: casera-api -> honeydue-api - All imports updated (130+ files) - Docker: containers, images, networks renamed - Email templates: support email, noreply, icon URL - Domains: casera.app/mycrib.treytartt.com -> honeyDue.treytartt.com - Bundle IDs: com.tt.casera -> com.tt.honeyDue - IAP product IDs updated - Landing page, admin panel, config defaults - Seeds, CI workflows, Makefile, docs - Database table names preserved (no migration needed) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
9.8 KiB
Task Kanban Categorization
This document explains how tasks are categorized into kanban columns in the honeyDue 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:
- It has at least one completion record (someone marked it done)
- 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:
NextDueDate(preferred - used for recurring tasks after completion)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:
- A new completion record is created
NextDueDateis calculated ascompletionDate + frequencyDays- Status is reset to "Pending"
- 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:
NextDueDateis set tonil - Categorization: Goes to
completed_taskscolumn - Status: Remains as-is (usually "Completed" or last status)
Recurring Tasks (weekly, monthly, annually, etc.):
- When completed:
NextDueDateis set tocompletionDate + 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:
- If a recurring task is marked "In Progress" and then completed
- The
in_progressflag MUST be reset tofalseafter completion - 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/honeydue-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?
- Check
IsCancelled: Cancelled takes highest priority - Check
NextDueDate: For recurring tasks, this determines placement - Check
InProgress:trueoverrides date-based categorization - Check
Completions: Empty + nil NextDueDate = upcoming, not completed
Recurring task not moving to Upcoming after completion?
Verify that CreateCompletion is:
- Setting
NextDueDatecorrectly - Resetting
InProgresstofalse
Task showing overdue but due date looks correct?
The system uses UTC times. Ensure dates are compared in UTC:
now := time.Now().UTC()