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>
311 lines
9.8 KiB
Markdown
311 lines
9.8 KiB
Markdown
# 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.
|
|
|
|
```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.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.
|
|
|
|
```go
|
|
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.
|
|
|
|
```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 `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()`:
|
|
|
|
```go
|
|
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
|
|
|
|
```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/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
|
|
|
|
```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 `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:
|
|
```go
|
|
now := time.Now().UTC()
|
|
```
|