summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
Diffstat (limited to 'internal')
-rw-r--r--internal/models/types.go9
-rw-r--r--internal/store/native_tasks.go55
-rw-r--r--internal/store/native_tasks_test.go76
3 files changed, 134 insertions, 6 deletions
diff --git a/internal/models/types.go b/internal/models/types.go
index 6f8c405..f74d264 100644
--- a/internal/models/types.go
+++ b/internal/models/types.go
@@ -20,6 +20,15 @@ type Task struct {
URL string `json:"url"`
CreatedAt time.Time `json:"created_at"`
IsRecurring bool `json:"is_recurring"`
+
+ // Recurrence (doot-native tasks only). RecurrenceSeriesID != "" is the
+ // real "is this task recurring" indicator -- IsRecurring above predates
+ // this feature and is never set by it.
+ RecurrenceFreq string `json:"recurrence_freq,omitempty"`
+ RecurrenceInterval int `json:"recurrence_interval,omitempty"`
+ RecurrenceWeekdays []int `json:"recurrence_weekdays,omitempty"`
+ RecurrenceSeriesID string `json:"recurrence_series_id,omitempty"`
+ NextOccurrenceOverride *time.Time `json:"next_occurrence_override,omitempty"`
}
// Meal represents a meal from PlanToEat
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)
+ }
+}