summaryrefslogtreecommitdiff
path: root/internal/store/native_tasks.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/store/native_tasks.go')
-rw-r--r--internal/store/native_tasks.go88
1 files changed, 75 insertions, 13 deletions
diff --git a/internal/store/native_tasks.go b/internal/store/native_tasks.go
index 218675e..7789a0d 100644
--- a/internal/store/native_tasks.go
+++ b/internal/store/native_tasks.go
@@ -1,12 +1,21 @@
package store
import (
+ "database/sql"
"encoding/json"
+ "errors"
"time"
"task-dashboard/internal/models"
)
+// ErrNativeTaskNotFound is returned by CompleteNativeTask, UncompleteNativeTask,
+// and RescheduleNativeTask when no row matches the given id -- previously these
+// three silently reported success on a 0-row UPDATE (Exec's err is nil even when
+// no rows match), so a stale or wrong id from a caller looked identical to a real
+// completion: the HTTP response was 200, but nothing in the database changed.
+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(`
@@ -22,15 +31,37 @@ func (s *Store) GetNativeTasks() ([]models.Task, error) {
return scanNativeTasks(rows)
}
-// GetNativeTasksByDateRange returns non-completed native tasks due within the given range,
-// including overdue tasks (due before start) so they keep appearing until completed.
+// GetNativeTasksByDateRange returns non-completed native tasks due within the given range.
+// Overdue tasks (due before start) are deliberately excluded here -- BuildTimeline fetches
+// those separately via GetOverdueNativeTasks so callers that only want "in range" can use this
+// 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
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
+ `, start, end)
+ if err != nil {
+ return nil, err
+ }
+ defer func() { _ = rows.Close() }()
+ return scanNativeTasks(rows)
+}
+
+// GetOverdueNativeTasks returns non-completed native tasks whose due date is
+// before the given time. BuildTimeline calls this alongside
+// GetNativeTasksByDateRange, whose lower bound excludes anything due before
+// the requested range's start -- without this, a task overdue from a
+// previous day never gets fetched at all, so it never reaches
+// 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
+ FROM native_tasks
WHERE completed = 0 AND due_date IS NOT NULL AND due_date < ?
ORDER BY due_date ASC, priority DESC
- `, end)
+ `, before)
if err != nil {
return nil, err
}
@@ -81,31 +112,62 @@ func (s *Store) UpdateNativeTaskDescription(id, description string) error {
return err
}
-// CompleteNativeTask marks a task as completed.
+// CompleteNativeTask marks a task as completed. Returns ErrNativeTaskNotFound
+// if id doesn't match any row.
func (s *Store) CompleteNativeTask(id string) error {
- _, err := s.db.Exec(`
+ result, err := s.db.Exec(`
UPDATE native_tasks SET completed = 1, updated_at = CURRENT_TIMESTAMP WHERE id = ?
`, id)
- return err
+ if err != nil {
+ return err
+ }
+ return checkRowsAffected(result)
}
-// RescheduleNativeTask sets a new due date on a task.
+// RescheduleNativeTask sets a new due date on a task. Returns
+// ErrNativeTaskNotFound if id doesn't match any row.
func (s *Store) RescheduleNativeTask(id string, dueDate time.Time) error {
- _, err := s.db.Exec(`
+ result, err := s.db.Exec(`
UPDATE native_tasks SET due_date = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?
`, dueDate, id)
- return err
+ if err != nil {
+ return err
+ }
+ return checkRowsAffected(result)
}
-// UncompleteNativeTask marks a task as not completed.
+// UncompleteNativeTask marks a task as not completed. Returns
+// ErrNativeTaskNotFound if id doesn't match any row.
func (s *Store) UncompleteNativeTask(id string) error {
- _, err := s.db.Exec(`
+ result, err := s.db.Exec(`
UPDATE native_tasks SET completed = 0, updated_at = CURRENT_TIMESTAMP WHERE id = ?
`, id)
- return err
+ if err != nil {
+ return err
+ }
+ return checkRowsAffected(result)
+}
+
+// checkRowsAffected returns ErrNativeTaskNotFound if the update matched no
+// rows -- mirrors the RowsAffected() check already used in sqlite.go's
+// ApproveAgentSession/DenyAgentSession for the same "silent 0-row update"
+// class of bug.
+func checkRowsAffected(result sql.Result) error {
+ affected, err := result.RowsAffected()
+ if err != nil {
+ return err
+ }
+ if affected == 0 {
+ return ErrNativeTaskNotFound
+ }
+ return nil
}
-func scanNativeTasks(rows interface{ Next() bool; Scan(...interface{}) error; Err() error }) ([]models.Task, error) {
+func scanNativeTasks(rows interface {
+ Next() bool
+ Scan(...interface{}) error
+ Err() error
+}) ([]models.Task, error) {
var tasks []models.Task
for rows.Next() {
var t models.Task