feat: enhance team planning and deployment
Test and publish / verify (push) Successful in 2m45s

This commit is contained in:
2026-07-29 12:09:21 +03:30
parent c3083b137f
commit eba6b3565c
10 changed files with 472 additions and 22 deletions
+159 -7
View File
@@ -103,6 +103,13 @@ type BoardTask struct {
CreatorName string
DueDate string
CreatedAt string
Tags []BoardTag
}
type BoardTag struct {
ID int64
Name string
Color string
}
type Store struct{ db *sql.DB }
@@ -196,6 +203,18 @@ CREATE TABLE IF NOT EXISTS board_tasks (
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_board_tasks_status ON board_tasks(status,created_at);
CREATE TABLE IF NOT EXISTS board_tags (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE COLLATE NOCASE,
color TEXT NOT NULL DEFAULT 'green',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS board_task_tags (
task_id INTEGER NOT NULL REFERENCES board_tasks(id) ON DELETE CASCADE,
tag_id INTEGER NOT NULL REFERENCES board_tags(id) ON DELETE CASCADE,
PRIMARY KEY(task_id,tag_id)
);
CREATE INDEX IF NOT EXISTS idx_board_task_tags_tag ON board_task_tags(tag_id,task_id);
`
if _, err := s.db.Exec(schema); err != nil {
return err
@@ -772,14 +791,14 @@ func validBoardStatus(status string) bool {
return status == "backlog" || status == "in_progress" || status == "blocked" || status == "done"
}
func (s *Store) CreateBoardTask(creatorID int64, title, description, status string, assigneeID *int64, dueDate string) error {
func (s *Store) CreateBoardTask(creatorID int64, title, description, status string, assigneeID *int64, dueDate string) (int64, error) {
title = strings.TrimSpace(title)
description = strings.TrimSpace(description)
if title == "" {
return errors.New("task title is required")
return 0, errors.New("task title is required")
}
if len([]rune(title)) > 160 || len([]rune(description)) > 4000 {
return errors.New("task title or description is too long")
return 0, errors.New("task title or description is too long")
}
if !validBoardStatus(status) {
status = "backlog"
@@ -792,9 +811,12 @@ func (s *Store) CreateBoardTask(creatorID int64, title, description, status stri
if dueDate != "" {
due = dueDate
}
_, err := s.db.Exec(`INSERT INTO board_tasks(title,description,status,assignee_id,creator_id,due_date)
result, err := s.db.Exec(`INSERT INTO board_tasks(title,description,status,assignee_id,creator_id,due_date)
VALUES(?,?,?,?,?,?)`, title, description, status, assignee, creatorID, due)
return err
if err != nil {
return 0, err
}
return result.LastInsertId()
}
func (s *Store) BoardTasks() ([]BoardTask, error) {
@@ -811,7 +833,6 @@ func (s *Store) BoardTasks() ([]BoardTask, error) {
if err != nil {
return nil, err
}
defer rows.Close()
var tasks []BoardTask
for rows.Next() {
var task BoardTask
@@ -824,7 +845,35 @@ func (s *Store) BoardTasks() ([]BoardTask, error) {
}
tasks = append(tasks, task)
}
return tasks, rows.Err()
if err := rows.Err(); err != nil {
rows.Close()
return nil, err
}
if err := rows.Close(); err != nil {
return nil, err
}
taskIndex := make(map[int64]int, len(tasks))
for index := range tasks {
taskIndex[tasks[index].ID] = index
}
tagRows, err := s.db.Query(`SELECT tt.task_id,t.id,t.name,t.color
FROM board_task_tags tt JOIN board_tags t ON t.id=tt.tag_id
ORDER BY t.name`)
if err != nil {
return nil, err
}
defer tagRows.Close()
for tagRows.Next() {
var taskID int64
var tag BoardTag
if err := tagRows.Scan(&taskID, &tag.ID, &tag.Name, &tag.Color); err != nil {
return nil, err
}
if index, ok := taskIndex[taskID]; ok {
tasks[index].Tags = append(tasks[index].Tags, tag)
}
}
return tasks, tagRows.Err()
}
func (s *Store) MoveBoardTask(id int64, status string) error {
@@ -859,3 +908,106 @@ func (s *Store) DeleteBoardTask(id, userID int64, admin bool) error {
}
return nil
}
func validTagColor(color string) bool {
switch color {
case "green", "blue", "red", "amber", "purple", "slate":
return true
default:
return false
}
}
func (s *Store) CreateBoardTag(name, color string) error {
name = strings.TrimSpace(name)
color = strings.TrimSpace(color)
if name == "" || len([]rune(name)) > 40 {
return errors.New("tag name must be between 1 and 40 characters")
}
if !validTagColor(color) {
return errors.New("choose a valid tag color")
}
_, err := s.db.Exec(`INSERT INTO board_tags(name,color) VALUES(?,?)`, name, color)
if err != nil && strings.Contains(strings.ToLower(err.Error()), "unique") {
return errors.New("a tag with that name already exists")
}
return err
}
func (s *Store) FindOrCreateBoardTag(name, color string) (BoardTag, error) {
name = strings.TrimSpace(name)
color = strings.TrimSpace(color)
if name == "" || len([]rune(name)) > 40 {
return BoardTag{}, errors.New("tag name must be between 1 and 40 characters")
}
if !validTagColor(color) {
return BoardTag{}, errors.New("choose a valid tag color")
}
if _, err := s.db.Exec(`INSERT INTO board_tags(name,color) VALUES(?,?)
ON CONFLICT(name) DO NOTHING`, name, color); err != nil {
return BoardTag{}, err
}
var tag BoardTag
err := s.db.QueryRow(`SELECT id,name,color FROM board_tags
WHERE name = ? COLLATE NOCASE`, name).Scan(&tag.ID, &tag.Name, &tag.Color)
return tag, err
}
func (s *Store) BoardTags() ([]BoardTag, error) {
rows, err := s.db.Query(`SELECT id,name,color FROM board_tags ORDER BY name`)
if err != nil {
return nil, err
}
defer rows.Close()
var tags []BoardTag
for rows.Next() {
var tag BoardTag
if err := rows.Scan(&tag.ID, &tag.Name, &tag.Color); err != nil {
return nil, err
}
tags = append(tags, tag)
}
return tags, rows.Err()
}
func (s *Store) SetBoardTaskTags(taskID int64, tagIDs []int64) error {
tx, err := s.db.Begin()
if err != nil {
return err
}
defer tx.Rollback()
var exists int
if err := tx.QueryRow(`SELECT COUNT(*) FROM board_tasks WHERE id=?`, taskID).Scan(&exists); err != nil {
return err
}
if exists == 0 {
return errors.New("task was not found")
}
if _, err := tx.Exec(`DELETE FROM board_task_tags WHERE task_id=?`, taskID); err != nil {
return err
}
seen := map[int64]bool{}
for _, tagID := range tagIDs {
if tagID < 1 || seen[tagID] {
continue
}
seen[tagID] = true
if _, err := tx.Exec(`INSERT INTO board_task_tags(task_id,tag_id)
SELECT ?,id FROM board_tags WHERE id=?`, taskID, tagID); err != nil {
return err
}
}
return tx.Commit()
}
func (s *Store) DeleteBoardTag(id int64) error {
result, err := s.db.Exec(`DELETE FROM board_tags WHERE id=?`, id)
if err != nil {
return err
}
count, _ := result.RowsAffected()
if count == 0 {
return errors.New("tag was not found")
}
return nil
}