This commit is contained in:
+140
-4
@@ -105,6 +105,8 @@ type BoardTask struct {
|
||||
ArchivedAt string
|
||||
Assignees []User
|
||||
Tags []BoardTag
|
||||
Todos []BoardTodo
|
||||
TodoDone int
|
||||
}
|
||||
|
||||
type BoardTag struct {
|
||||
@@ -113,6 +115,13 @@ type BoardTag struct {
|
||||
Color string
|
||||
}
|
||||
|
||||
type BoardTodo struct {
|
||||
ID int64
|
||||
TaskID int64
|
||||
Body string
|
||||
Completed bool
|
||||
}
|
||||
|
||||
type Store struct{ db *sql.DB }
|
||||
|
||||
func OpenStore(path string) (*Store, error) {
|
||||
@@ -230,7 +239,7 @@ CREATE TABLE IF NOT EXISTS board_tasks (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
title TEXT NOT NULL,
|
||||
description TEXT NOT NULL DEFAULT '',
|
||||
status TEXT NOT NULL DEFAULT 'backlog' CHECK(status IN ('backlog','in_progress','blocked','done')),
|
||||
status TEXT NOT NULL DEFAULT 'backlog' CHECK(status IN ('backlog','todo','in_progress','blocked','done')),
|
||||
importance TEXT NOT NULL DEFAULT 'normal' CHECK(importance IN ('low','normal','high','urgent')),
|
||||
assignee_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
|
||||
creator_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
@@ -245,6 +254,14 @@ CREATE TABLE IF NOT EXISTS board_task_assignees (
|
||||
PRIMARY KEY(task_id,user_id)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_board_task_assignees_user ON board_task_assignees(user_id,task_id);
|
||||
CREATE TABLE IF NOT EXISTS board_task_todos (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
task_id INTEGER NOT NULL REFERENCES board_tasks(id) ON DELETE CASCADE,
|
||||
body TEXT NOT NULL,
|
||||
completed INTEGER NOT NULL DEFAULT 0 CHECK(completed IN (0,1)),
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_board_task_todos_task ON board_task_todos(task_id,completed,created_at);
|
||||
CREATE TABLE IF NOT EXISTS board_tags (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL UNIQUE COLLATE NOCASE,
|
||||
@@ -270,6 +287,9 @@ CREATE INDEX IF NOT EXISTS idx_board_task_tags_tag ON board_task_tags(tag_id,tas
|
||||
if err := s.ensureColumn("board_tasks", "importance", "importance TEXT NOT NULL DEFAULT 'normal' CHECK(importance IN ('low','normal','high','urgent'))"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.ensureBoardTaskTodoStatus(); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := s.db.Exec(`INSERT OR IGNORE INTO board_task_assignees(task_id,user_id)
|
||||
SELECT id,assignee_id FROM board_tasks WHERE assignee_id IS NOT NULL`); err != nil {
|
||||
return err
|
||||
@@ -297,6 +317,57 @@ CREATE INDEX IF NOT EXISTS idx_board_task_tags_tag ON board_task_tags(tag_id,tas
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store) ensureBoardTaskTodoStatus() error {
|
||||
var tableSQL string
|
||||
err := s.db.QueryRow(`SELECT COALESCE(sql,'') FROM sqlite_master WHERE type='table' AND name='board_tasks'`).Scan(&tableSQL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if strings.Contains(strings.ToLower(tableSQL), "'todo'") {
|
||||
return nil
|
||||
}
|
||||
if _, err := s.db.Exec(`PRAGMA foreign_keys=OFF`); err != nil {
|
||||
return err
|
||||
}
|
||||
restoreForeignKeys := func() { _, _ = s.db.Exec(`PRAGMA foreign_keys=ON`) }
|
||||
if _, err := s.db.Exec(`CREATE TABLE board_tasks_migrated (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
title TEXT NOT NULL,
|
||||
description TEXT NOT NULL DEFAULT '',
|
||||
status TEXT NOT NULL DEFAULT 'backlog' CHECK(status IN ('backlog','todo','in_progress','blocked','done')),
|
||||
importance TEXT NOT NULL DEFAULT 'normal' CHECK(importance IN ('low','normal','high','urgent')),
|
||||
assignee_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
|
||||
creator_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
due_date TEXT,
|
||||
archived_at DATETIME,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
)`); err != nil {
|
||||
restoreForeignKeys()
|
||||
return err
|
||||
}
|
||||
if _, err := s.db.Exec(`INSERT INTO board_tasks_migrated(id,title,description,status,importance,assignee_id,creator_id,due_date,archived_at,created_at)
|
||||
SELECT id,title,description,status,importance,assignee_id,creator_id,due_date,archived_at,created_at FROM board_tasks`); err != nil {
|
||||
_, _ = s.db.Exec(`DROP TABLE board_tasks_migrated`)
|
||||
restoreForeignKeys()
|
||||
return err
|
||||
}
|
||||
if _, err := s.db.Exec(`DROP TABLE board_tasks`); err != nil {
|
||||
_, _ = s.db.Exec(`DROP TABLE board_tasks_migrated`)
|
||||
restoreForeignKeys()
|
||||
return err
|
||||
}
|
||||
if _, err := s.db.Exec(`ALTER TABLE board_tasks_migrated RENAME TO board_tasks`); err != nil {
|
||||
restoreForeignKeys()
|
||||
return err
|
||||
}
|
||||
if _, err := s.db.Exec(`CREATE INDEX IF NOT EXISTS idx_board_tasks_status ON board_tasks(status,created_at)`); err != nil {
|
||||
restoreForeignKeys()
|
||||
return err
|
||||
}
|
||||
restoreForeignKeys()
|
||||
return nil
|
||||
}
|
||||
|
||||
func hashPassword(password string) (string, error) {
|
||||
salt := make([]byte, 16)
|
||||
if _, err := rand.Read(salt); err != nil {
|
||||
@@ -927,7 +998,7 @@ func (s *Store) ReportSummary(start, end string) ([]PersonReportSummary, error)
|
||||
}
|
||||
|
||||
func validBoardStatus(status string) bool {
|
||||
return status == "backlog" || status == "in_progress" || status == "blocked" || status == "done"
|
||||
return status == "backlog" || status == "todo" || status == "in_progress" || status == "blocked" || status == "done"
|
||||
}
|
||||
|
||||
func validBoardImportance(importance string) bool {
|
||||
@@ -994,7 +1065,7 @@ func (s *Store) boardTasks(archived bool) ([]BoardTask, error) {
|
||||
} else {
|
||||
query += ` WHERE t.archived_at IS NULL
|
||||
ORDER BY CASE t.status
|
||||
WHEN 'backlog' THEN 0 WHEN 'in_progress' THEN 1 WHEN 'blocked' THEN 2 ELSE 3 END,
|
||||
WHEN 'backlog' THEN 0 WHEN 'todo' THEN 1 WHEN 'in_progress' THEN 2 WHEN 'blocked' THEN 3 ELSE 4 END,
|
||||
t.created_at DESC`
|
||||
}
|
||||
rows, err := s.db.Query(query)
|
||||
@@ -1069,7 +1140,28 @@ func (s *Store) boardTasks(archived bool) ([]BoardTask, error) {
|
||||
tasks[index].Tags = append(tasks[index].Tags, tag)
|
||||
}
|
||||
}
|
||||
return tasks, tagRows.Err()
|
||||
if err := tagRows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
todoRows, err := s.db.Query(`SELECT id,task_id,body,completed FROM board_task_todos
|
||||
ORDER BY completed,created_at,id`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer todoRows.Close()
|
||||
for todoRows.Next() {
|
||||
var todo BoardTodo
|
||||
if err := todoRows.Scan(&todo.ID, &todo.TaskID, &todo.Body, &todo.Completed); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if index, ok := taskIndex[todo.TaskID]; ok {
|
||||
tasks[index].Todos = append(tasks[index].Todos, todo)
|
||||
if todo.Completed {
|
||||
tasks[index].TodoDone++
|
||||
}
|
||||
}
|
||||
}
|
||||
return tasks, todoRows.Err()
|
||||
}
|
||||
|
||||
type sqlExecutor interface {
|
||||
@@ -1117,6 +1209,50 @@ func (s *Store) SetBoardTaskDetails(taskID int64, assigneeIDs []int64, importanc
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func (s *Store) CreateBoardTaskTodo(taskID int64, body string) error {
|
||||
body = strings.TrimSpace(body)
|
||||
if body == "" || len([]rune(body)) > 240 {
|
||||
return errors.New("to-do item must be between 1 and 240 characters")
|
||||
}
|
||||
result, err := s.db.Exec(`INSERT INTO board_task_todos(task_id,body) SELECT id,? FROM board_tasks WHERE id=?`, body, taskID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
count, _ := result.RowsAffected()
|
||||
if count == 0 {
|
||||
return errors.New("task was not found")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store) SetBoardTaskTodoCompleted(taskID, todoID int64, completed bool) error {
|
||||
value := 0
|
||||
if completed {
|
||||
value = 1
|
||||
}
|
||||
result, err := s.db.Exec(`UPDATE board_task_todos SET completed=? WHERE id=? AND task_id=?`, value, todoID, taskID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
count, _ := result.RowsAffected()
|
||||
if count == 0 {
|
||||
return errors.New("to-do item was not found")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store) DeleteBoardTaskTodo(taskID, todoID int64) error {
|
||||
result, err := s.db.Exec(`DELETE FROM board_task_todos WHERE id=? AND task_id=?`, todoID, taskID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
count, _ := result.RowsAffected()
|
||||
if count == 0 {
|
||||
return errors.New("to-do item was not found")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store) MoveBoardTask(id int64, status string) error {
|
||||
if !validBoardStatus(status) {
|
||||
return errors.New("invalid board column")
|
||||
|
||||
Reference in New Issue
Block a user