diff options
| author | Peter Stone <thepeterstone@gmail.com> | 2026-07-14 20:11:31 +0000 |
|---|---|---|
| committer | Peter Stone <thepeterstone@gmail.com> | 2026-07-16 02:44:02 +0000 |
| commit | 0baf1134ea2d415aa55079ff98476afd0acf5811 (patch) | |
| tree | 715f830a792d63af1a83998a166638cdb1bc696a /internal/store | |
| parent | 7605e59f6d4524f4a0bf5d573916c8728ad0bfdb (diff) | |
feat(tasks): add recurrence columns and model fields for native tasks
recurrence_series_id != "" is the real recurring indicator (IsRecurring
predates this feature and is never set). Adds parseWeekdays/formatWeekdays
for the comma-separated weekday-list column, and threads the five new
columns through scanNativeTasks and all four existing SELECT queries.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VTUSAEKfsPc6WGDq45yPHD
Diffstat (limited to 'internal/store')
| -rw-r--r-- | internal/store/native_tasks.go | 55 | ||||
| -rw-r--r-- | internal/store/native_tasks_test.go | 76 |
2 files changed, 125 insertions, 6 deletions
diff --git a/internal/store/native_tasks.go b/internal/store/native_tasks.go index 7789a0d..c0a7a61 100644 --- a/internal/store/native_tasks.go +++ b/internal/store/native_tasks.go @@ -4,6 +4,8 @@ import ( "database/sql" "encoding/json" "errors" + "strconv" + "strings" "time" "task-dashboard/internal/models" @@ -19,7 +21,8 @@ var ErrNativeTaskNotFound = errors.New("native task not found") // GetNativeTasks returns all non-completed native tasks. func (s *Store) GetNativeTasks() ([]models.Task, error) { rows, err := s.db.Query(` - SELECT id, content, description, project_name, due_date, priority, completed, labels, created_at + SELECT id, content, description, project_name, due_date, priority, completed, labels, created_at, + recurrence_freq, recurrence_interval, recurrence_weekdays, recurrence_series_id, next_occurrence_override FROM native_tasks WHERE completed = 0 ORDER BY CASE WHEN due_date IS NULL THEN 1 ELSE 0 END, due_date ASC, priority DESC @@ -37,7 +40,8 @@ func (s *Store) GetNativeTasks() ([]models.Task, error) { // without double-counting against that separate fetch. func (s *Store) GetNativeTasksByDateRange(start, end time.Time) ([]models.Task, error) { rows, err := s.db.Query(` - SELECT id, content, description, project_name, due_date, priority, completed, labels, created_at + SELECT id, content, description, project_name, due_date, priority, completed, labels, created_at, + recurrence_freq, recurrence_interval, recurrence_weekdays, recurrence_series_id, next_occurrence_override FROM native_tasks WHERE completed = 0 AND due_date IS NOT NULL AND due_date >= ? AND due_date < ? ORDER BY due_date ASC, priority DESC @@ -57,7 +61,8 @@ func (s *Store) GetNativeTasksByDateRange(start, end time.Time) ([]models.Task, // ComputeDaySection to be marked IsOverdue. func (s *Store) GetOverdueNativeTasks(before time.Time) ([]models.Task, error) { rows, err := s.db.Query(` - SELECT id, content, description, project_name, due_date, priority, completed, labels, created_at + SELECT id, content, description, project_name, due_date, priority, completed, labels, created_at, + recurrence_freq, recurrence_interval, recurrence_weekdays, recurrence_series_id, next_occurrence_override FROM native_tasks WHERE completed = 0 AND due_date IS NOT NULL AND due_date < ? ORDER BY due_date ASC, priority DESC @@ -72,7 +77,8 @@ func (s *Store) GetOverdueNativeTasks(before time.Time) ([]models.Task, error) { // GetUndatedNativeTasks returns non-completed native tasks with no due date. func (s *Store) GetUndatedNativeTasks() ([]models.Task, error) { rows, err := s.db.Query(` - SELECT id, content, description, project_name, due_date, priority, completed, labels, created_at + SELECT id, content, description, project_name, due_date, priority, completed, labels, created_at, + recurrence_freq, recurrence_interval, recurrence_weekdays, recurrence_series_id, next_occurrence_override FROM native_tasks WHERE completed = 0 AND due_date IS NULL ORDER BY priority DESC, created_at ASC @@ -173,7 +179,12 @@ func scanNativeTasks(rows interface { var t models.Task var labelsJSON string var dueDateStr *string - if err := rows.Scan(&t.ID, &t.Content, &t.Description, &t.ProjectName, &dueDateStr, &t.Priority, &t.Completed, &labelsJSON, &t.CreatedAt); err != nil { + var weekdaysStr string + var nextOverrideStr string + if err := rows.Scan( + &t.ID, &t.Content, &t.Description, &t.ProjectName, &dueDateStr, &t.Priority, &t.Completed, &labelsJSON, &t.CreatedAt, + &t.RecurrenceFreq, &t.RecurrenceInterval, &weekdaysStr, &t.RecurrenceSeriesID, &nextOverrideStr, + ); err != nil { return nil, err } if dueDateStr != nil { @@ -188,7 +199,41 @@ func scanNativeTasks(rows interface { if err := json.Unmarshal([]byte(labelsJSON), &t.Labels); err != nil { t.Labels = nil } + t.RecurrenceWeekdays = parseWeekdays(weekdaysStr) + if nextOverrideStr != "" { + if parsed, err := time.Parse("2006-01-02", nextOverrideStr); err == nil { + t.NextOccurrenceOverride = &parsed + } + } tasks = append(tasks, t) } return tasks, rows.Err() } + +// parseWeekdays parses a comma-separated list of 0-6 ints (e.g. "1,3,5"), +// returning nil for an empty string. +func parseWeekdays(s string) []int { + if s == "" { + return nil + } + parts := strings.Split(s, ",") + weekdays := make([]int, 0, len(parts)) + for _, p := range parts { + if n, err := strconv.Atoi(strings.TrimSpace(p)); err == nil { + weekdays = append(weekdays, n) + } + } + return weekdays +} + +// formatWeekdays is the inverse of parseWeekdays. +func formatWeekdays(weekdays []int) string { + if len(weekdays) == 0 { + return "" + } + strs := make([]string, len(weekdays)) + for i, d := range weekdays { + strs[i] = strconv.Itoa(d) + } + return strings.Join(strs, ",") +} diff --git a/internal/store/native_tasks_test.go b/internal/store/native_tasks_test.go index 5c11d3a..0a87bfa 100644 --- a/internal/store/native_tasks_test.go +++ b/internal/store/native_tasks_test.go @@ -7,6 +7,8 @@ import ( "testing" "time" + "task-dashboard/internal/models" + _ "github.com/mattn/go-sqlite3" ) @@ -33,7 +35,12 @@ func newNativeTasksTestStore(t *testing.T) *Store { completed BOOLEAN DEFAULT 0, labels TEXT DEFAULT '[]', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, + recurrence_freq TEXT DEFAULT '', + recurrence_interval INTEGER DEFAULT 1, + recurrence_weekdays TEXT DEFAULT '', + recurrence_series_id TEXT DEFAULT '', + next_occurrence_override TEXT DEFAULT '' ) `); err != nil { t.Fatal(err) @@ -94,3 +101,70 @@ func TestRescheduleNativeTask_UnknownID_ReturnsErrNotFound(t *testing.T) { t.Fatalf("expected ErrNativeTaskNotFound, got %v", err) } } + +func TestGetNativeTasks_ParsesRecurrenceFields(t *testing.T) { + s := newNativeTasksTestStore(t) + if _, err := s.db.Exec(` + INSERT INTO native_tasks (id, content, due_date, recurrence_freq, recurrence_interval, recurrence_weekdays, recurrence_series_id, next_occurrence_override) + VALUES ('rec-1', 'Recurring task', '2026-07-13', 'weekly', 2, '1,3,5', 'series-abc', '2026-07-27') + `); err != nil { + t.Fatal(err) + } + + tasks, err := s.GetNativeTasks() + if err != nil { + t.Fatalf("GetNativeTasks: %v", err) + } + + var found *models.Task + for i := range tasks { + if tasks[i].ID == "rec-1" { + found = &tasks[i] + } + } + if found == nil { + t.Fatal("expected to find rec-1") + } + if found.RecurrenceFreq != "weekly" { + t.Errorf("RecurrenceFreq = %q, want %q", found.RecurrenceFreq, "weekly") + } + if found.RecurrenceInterval != 2 { + t.Errorf("RecurrenceInterval = %d, want 2", found.RecurrenceInterval) + } + if len(found.RecurrenceWeekdays) != 3 || found.RecurrenceWeekdays[0] != 1 || found.RecurrenceWeekdays[1] != 3 || found.RecurrenceWeekdays[2] != 5 { + t.Errorf("RecurrenceWeekdays = %v, want [1 3 5]", found.RecurrenceWeekdays) + } + if found.RecurrenceSeriesID != "series-abc" { + t.Errorf("RecurrenceSeriesID = %q, want %q", found.RecurrenceSeriesID, "series-abc") + } + if found.NextOccurrenceOverride == nil || found.NextOccurrenceOverride.Format("2006-01-02") != "2026-07-27" { + t.Errorf("NextOccurrenceOverride = %v, want 2026-07-27", found.NextOccurrenceOverride) + } +} + +func TestGetNativeTasks_NonRecurringTask_HasEmptyRecurrenceFields(t *testing.T) { + s := newNativeTasksTestStore(t) + // "real-1" (inserted by newNativeTasksTestStore) has no recurrence columns set. + tasks, err := s.GetNativeTasks() + if err != nil { + t.Fatalf("GetNativeTasks: %v", err) + } + var found *models.Task + for i := range tasks { + if tasks[i].ID == "real-1" { + found = &tasks[i] + } + } + if found == nil { + t.Fatal("expected to find real-1") + } + if found.RecurrenceSeriesID != "" { + t.Errorf("expected empty RecurrenceSeriesID for non-recurring task, got %q", found.RecurrenceSeriesID) + } + if found.RecurrenceWeekdays != nil { + t.Errorf("expected nil RecurrenceWeekdays for non-recurring task, got %v", found.RecurrenceWeekdays) + } + if found.NextOccurrenceOverride != nil { + t.Errorf("expected nil NextOccurrenceOverride for non-recurring task, got %v", found.NextOccurrenceOverride) + } +} |
