summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-07-16 06:20:00 +0000
committerPeter Stone <thepeterstone@gmail.com>2026-07-16 06:20:00 +0000
commitc21f891954f98d6df3769d5063f9528240b5b20a (patch)
treefca0308099115476a2d93300717c5f370b1f8891
parent6b13197f479b98c323140d3e9d9998c32f9f6ac9 (diff)
Add estimated_minutes to native task read/write/recurrence paths
-rw-r--r--internal/store/native_tasks.go38
-rw-r--r--internal/store/native_tasks_test.go88
2 files changed, 113 insertions, 13 deletions
diff --git a/internal/store/native_tasks.go b/internal/store/native_tasks.go
index 1f898aa..5d03bde 100644
--- a/internal/store/native_tasks.go
+++ b/internal/store/native_tasks.go
@@ -24,7 +24,7 @@ var ErrNativeTaskNotFound = errors.New("native task not found")
func (s *Store) GetNativeTasks() ([]models.Task, error) {
rows, err := s.db.Query(`
SELECT id, content, description, project_name, project_id, due_date, priority, completed, labels, created_at,
- recurrence_freq, recurrence_interval, recurrence_weekdays, recurrence_series_id, next_occurrence_override
+ recurrence_freq, recurrence_interval, recurrence_weekdays, recurrence_series_id, next_occurrence_override, estimated_minutes
FROM native_tasks
WHERE completed = 0
ORDER BY CASE WHEN due_date IS NULL THEN 1 ELSE 0 END, due_date ASC, priority DESC
@@ -43,7 +43,7 @@ func (s *Store) GetNativeTasks() ([]models.Task, error) {
func (s *Store) GetNativeTasksByDateRange(start, end time.Time) ([]models.Task, error) {
rows, err := s.db.Query(`
SELECT id, content, description, project_name, project_id, due_date, priority, completed, labels, created_at,
- recurrence_freq, recurrence_interval, recurrence_weekdays, recurrence_series_id, next_occurrence_override
+ recurrence_freq, recurrence_interval, recurrence_weekdays, recurrence_series_id, next_occurrence_override, estimated_minutes
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
@@ -64,7 +64,7 @@ func (s *Store) GetNativeTasksByDateRange(start, end time.Time) ([]models.Task,
func (s *Store) GetOverdueNativeTasks(before time.Time) ([]models.Task, error) {
rows, err := s.db.Query(`
SELECT id, content, description, project_name, project_id, due_date, priority, completed, labels, created_at,
- recurrence_freq, recurrence_interval, recurrence_weekdays, recurrence_series_id, next_occurrence_override
+ recurrence_freq, recurrence_interval, recurrence_weekdays, recurrence_series_id, next_occurrence_override, estimated_minutes
FROM native_tasks
WHERE completed = 0 AND due_date IS NOT NULL AND due_date < ?
ORDER BY due_date ASC, priority DESC
@@ -80,7 +80,7 @@ func (s *Store) GetOverdueNativeTasks(before time.Time) ([]models.Task, error) {
func (s *Store) GetUndatedNativeTasks() ([]models.Task, error) {
rows, err := s.db.Query(`
SELECT id, content, description, project_name, project_id, due_date, priority, completed, labels, created_at,
- recurrence_freq, recurrence_interval, recurrence_weekdays, recurrence_series_id, next_occurrence_override
+ recurrence_freq, recurrence_interval, recurrence_weekdays, recurrence_series_id, next_occurrence_override, estimated_minutes
FROM native_tasks
WHERE completed = 0 AND due_date IS NULL
ORDER BY priority DESC, created_at ASC
@@ -96,7 +96,7 @@ func (s *Store) GetUndatedNativeTasks() ([]models.Task, error) {
func (s *Store) GetNativeTaskByID(id string) (*models.Task, error) {
rows, err := s.db.Query(`
SELECT id, content, description, project_name, project_id, due_date, priority, completed, labels, created_at,
- recurrence_freq, recurrence_interval, recurrence_weekdays, recurrence_series_id, next_occurrence_override
+ recurrence_freq, recurrence_interval, recurrence_weekdays, recurrence_series_id, next_occurrence_override, estimated_minutes
FROM native_tasks
WHERE id = ?
`, id)
@@ -118,9 +118,9 @@ func (s *Store) GetNativeTaskByID(id string) (*models.Task, error) {
func (s *Store) CreateNativeTask(task models.Task) error {
labelsJSON, _ := json.Marshal(task.Labels)
_, err := s.db.Exec(`
- INSERT INTO native_tasks (id, content, description, project_name, project_id, due_date, priority, labels, created_at, updated_at)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
- `, task.ID, task.Content, task.Description, task.ProjectName, task.ProjectID, task.DueDate, task.Priority, string(labelsJSON))
+ INSERT INTO native_tasks (id, content, description, project_name, project_id, due_date, priority, labels, estimated_minutes, created_at, updated_at)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
+ `, task.ID, task.Content, task.Description, task.ProjectName, task.ProjectID, task.DueDate, task.Priority, string(labelsJSON), task.EstimatedMinutes)
return err
}
@@ -211,6 +211,18 @@ func (s *Store) RescheduleNativeTask(id string, dueDate time.Time) error {
return checkRowsAffected(result)
}
+// SetTaskEstimate sets a task's estimated duration in minutes. Returns
+// ErrNativeTaskNotFound if id doesn't match any row.
+func (s *Store) SetTaskEstimate(id string, minutes int) error {
+ result, err := s.db.Exec(`
+ UPDATE native_tasks SET estimated_minutes = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?
+ `, minutes, id)
+ if err != nil {
+ return err
+ }
+ return checkRowsAffected(result)
+}
+
// UncompleteNativeTask marks a task as not completed. Returns
// ErrNativeTaskNotFound if id doesn't match any row.
func (s *Store) UncompleteNativeTask(id string) error {
@@ -245,17 +257,17 @@ func (s *Store) CreateNextIteration(old models.Task) error {
labelsJSON, _ := json.Marshal(old.Labels)
_, err := s.db.Exec(`
INSERT INTO native_tasks (
- id, content, description, project_name, project_id, due_date, priority, labels,
+ id, content, description, project_name, project_id, due_date, priority, labels, estimated_minutes,
recurrence_freq, recurrence_interval, recurrence_weekdays, recurrence_series_id, next_occurrence_override,
created_at, updated_at
)
- SELECT ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, '', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
+ SELECT ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, '', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
WHERE NOT EXISTS (
SELECT 1 FROM native_tasks
WHERE recurrence_series_id = ?
AND (due_date > ? OR (due_date = ? AND created_at > ?))
)
- `, newTaskID(), old.Content, old.Description, old.ProjectName, old.ProjectID, nextDue, old.Priority, string(labelsJSON),
+ `, newTaskID(), old.Content, old.Description, old.ProjectName, old.ProjectID, nextDue, old.Priority, string(labelsJSON), old.EstimatedMinutes,
old.RecurrenceFreq, old.RecurrenceInterval, formatWeekdays(old.RecurrenceWeekdays), old.RecurrenceSeriesID,
old.RecurrenceSeriesID, old.DueDate, old.DueDate, old.CreatedAt)
return err
@@ -322,7 +334,7 @@ func (s *Store) SetNextOccurrenceOverride(id string, date time.Time) error {
func (s *Store) GetSeriesNeedingNextIteration(now time.Time) ([]models.Task, error) {
rows, err := s.db.Query(`
SELECT id, content, description, project_name, project_id, due_date, priority, completed, labels, created_at,
- recurrence_freq, recurrence_interval, recurrence_weekdays, recurrence_series_id, next_occurrence_override
+ recurrence_freq, recurrence_interval, recurrence_weekdays, recurrence_series_id, next_occurrence_override, estimated_minutes
FROM native_tasks t1
WHERE recurrence_series_id != ''
AND due_date IS NOT NULL AND due_date <= ?
@@ -385,7 +397,7 @@ func scanNativeTasks(rows interface {
var nextOverrideStr string
if err := rows.Scan(
&t.ID, &t.Content, &t.Description, &t.ProjectName, &t.ProjectID, &dueDateStr, &t.Priority, &t.Completed, &labelsJSON, &t.CreatedAt,
- &t.RecurrenceFreq, &t.RecurrenceInterval, &weekdaysStr, &t.RecurrenceSeriesID, &nextOverrideStr,
+ &t.RecurrenceFreq, &t.RecurrenceInterval, &weekdaysStr, &t.RecurrenceSeriesID, &nextOverrideStr, &t.EstimatedMinutes,
); err != nil {
return nil, err
}
diff --git a/internal/store/native_tasks_test.go b/internal/store/native_tasks_test.go
index 1b4bdf2..00785c2 100644
--- a/internal/store/native_tasks_test.go
+++ b/internal/store/native_tasks_test.go
@@ -582,3 +582,91 @@ func TestCreateNextIteration_InheritsProjectIDAndLabels(t *testing.T) {
t.Errorf("next iteration Labels = %v, want [chore garden]", next.Labels)
}
}
+
+func TestCreateNativeTask_PersistsEstimatedMinutes(t *testing.T) {
+ s := newNativeTasksTestStore(t)
+
+ task := models.Task{ID: "t-est", Content: "Estimated task", EstimatedMinutes: 45}
+ if err := s.CreateNativeTask(task); err != nil {
+ t.Fatalf("CreateNativeTask: %v", err)
+ }
+
+ got, err := s.GetNativeTaskByID("t-est")
+ if err != nil {
+ t.Fatalf("GetNativeTaskByID: %v", err)
+ }
+ if got.EstimatedMinutes != 45 {
+ t.Errorf("EstimatedMinutes = %d, want 45", got.EstimatedMinutes)
+ }
+}
+
+func TestSetTaskEstimate_UpdatesMinutes(t *testing.T) {
+ s := newNativeTasksTestStore(t)
+
+ if err := s.SetTaskEstimate("real-1", 30); err != nil {
+ t.Fatalf("SetTaskEstimate: %v", err)
+ }
+
+ got, err := s.GetNativeTaskByID("real-1")
+ if err != nil {
+ t.Fatalf("GetNativeTaskByID: %v", err)
+ }
+ if got.EstimatedMinutes != 30 {
+ t.Errorf("EstimatedMinutes = %d, want 30", got.EstimatedMinutes)
+ }
+}
+
+func TestSetTaskEstimate_UnknownID_ReturnsErrNotFound(t *testing.T) {
+ s := newNativeTasksTestStore(t)
+
+ err := s.SetTaskEstimate("does-not-exist", 30)
+ if !errors.Is(err, ErrNativeTaskNotFound) {
+ t.Errorf("err = %v, want ErrNativeTaskNotFound", err)
+ }
+}
+
+func TestCreateNextIteration_CarriesEstimatedMinutesForward(t *testing.T) {
+ s := newNativeTasksTestStore(t)
+
+ due := time.Now()
+ old := models.Task{
+ ID: "t-series", Content: "Weekly review", DueDate: &due,
+ EstimatedMinutes: 60,
+ RecurrenceFreq: "weekly",
+ RecurrenceInterval: 1,
+ RecurrenceSeriesID: "series-1",
+ CreatedAt: time.Now().Add(-time.Hour),
+ }
+ if err := s.CreateNativeTask(old); err != nil {
+ t.Fatalf("CreateNativeTask: %v", err)
+ }
+ if _, err := s.db.Exec(`UPDATE native_tasks SET recurrence_freq = ?, recurrence_interval = ?, recurrence_series_id = ? WHERE id = ?`,
+ old.RecurrenceFreq, old.RecurrenceInterval, old.RecurrenceSeriesID, old.ID); err != nil {
+ t.Fatal(err)
+ }
+ stored, err := s.GetNativeTaskByID("t-series")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if err := s.CreateNextIteration(*stored); err != nil {
+ t.Fatalf("CreateNextIteration: %v", err)
+ }
+
+ series, err := s.GetSeriesNeedingNextIteration(time.Now().Add(365 * 24 * time.Hour))
+ if err != nil {
+ t.Fatal(err)
+ }
+ var next *models.Task
+ for i := range series {
+ if series[i].ID != "t-series" {
+ next = &series[i]
+ }
+ }
+ if next == nil {
+ t.Fatal("expected a next iteration row")
+ }
+ if next.EstimatedMinutes != 60 {
+ t.Errorf("EstimatedMinutes = %d, want 60 (carried forward)", next.EstimatedMinutes)
+ }
+}