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(` SELECT id, content, description, project_name, due_date, priority, completed, labels, created_at FROM native_tasks WHERE completed = 0 ORDER BY CASE WHEN due_date IS NULL THEN 1 ELSE 0 END, due_date ASC, priority DESC `) if err != nil { return nil, err } defer func() { _ = rows.Close() }() return scanNativeTasks(rows) } // GetNativeTasksByDateRange returns non-completed native tasks due within the given range. 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) } // 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 FROM native_tasks WHERE completed = 0 AND due_date IS NULL ORDER BY priority DESC, created_at ASC `) if err != nil { return nil, err } defer func() { _ = rows.Close() }() return scanNativeTasks(rows) } // CreateNativeTask inserts a new native task. 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, due_date, priority, labels, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) `, task.ID, task.Content, task.Description, task.ProjectName, task.DueDate, task.Priority, string(labelsJSON)) return err } // UpdateNativeTask updates a native task's content and description. func (s *Store) UpdateNativeTask(id, content, description string) error { _, err := s.db.Exec(` UPDATE native_tasks SET content = ?, description = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? `, content, description, id) return err } // CompleteNativeTask marks a task as completed. Returns ErrNativeTaskNotFound // if id doesn't match any row. func (s *Store) CompleteNativeTask(id string) error { result, err := s.db.Exec(` UPDATE native_tasks SET completed = 1, updated_at = CURRENT_TIMESTAMP WHERE id = ? `, id) if err != nil { return err } return checkRowsAffected(result) } // 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 { result, err := s.db.Exec(` UPDATE native_tasks SET due_date = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? `, dueDate, 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 { result, err := s.db.Exec(` UPDATE native_tasks SET completed = 0, updated_at = CURRENT_TIMESTAMP WHERE id = ? `, id) 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) { var tasks []models.Task for rows.Next() { 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 { return nil, err } if dueDateStr != nil { if parsed, err := time.Parse(time.RFC3339, *dueDateStr); err == nil { t.DueDate = &parsed } else if parsed, err := time.Parse("2006-01-02 15:04:05", *dueDateStr); err == nil { t.DueDate = &parsed } else if parsed, err := time.Parse("2006-01-02", *dueDateStr); err == nil { t.DueDate = &parsed } } if err := json.Unmarshal([]byte(labelsJSON), &t.Labels); err != nil { t.Labels = nil } tasks = append(tasks, t) } return tasks, rows.Err() }