package sqlite import ( "database/sql" "fmt" "strings" "time" "github.com/google/uuid" ) // ProjectGroup is a folder for organizing projects in the UI tree. type ProjectGroup struct { ID string `json:"id"` Name string `json:"name"` SortOrder int `json:"sortOrder"` } // ListProjectGroups returns all groups sorted by sort_order then name. func (db *DB) ListProjectGroups() ([]ProjectGroup, error) { rows, err := db.SQL.Query(` SELECT id, name, sort_order FROM project_groups ORDER BY sort_order ASC, name COLLATE NOCASE`) if err != nil { return nil, fmt.Errorf("list project groups: %w", err) } defer rows.Close() var out []ProjectGroup for rows.Next() { var g ProjectGroup if err := rows.Scan(&g.ID, &g.Name, &g.SortOrder); err != nil { return nil, err } out = append(out, g) } if out == nil { out = []ProjectGroup{} } return out, rows.Err() } // CreateProjectGroup inserts a new group. func (db *DB) CreateProjectGroup(name string) (ProjectGroup, error) { name = strings.TrimSpace(name) if name == "" { return ProjectGroup{}, fmt.Errorf("name is required") } id := uuid.NewString() var maxOrd sql.NullInt64 _ = db.SQL.QueryRow(`SELECT MAX(sort_order) FROM project_groups`).Scan(&maxOrd) ord := 0 if maxOrd.Valid { ord = int(maxOrd.Int64) + 1 } now := formatTime(time.Now().UTC()) _, err := db.SQL.Exec(`INSERT INTO project_groups(id, name, sort_order, created_at) VALUES(?, ?, ?, ?)`, id, name, ord, now) if err != nil { return ProjectGroup{}, fmt.Errorf("create project group: %w", err) } return ProjectGroup{ID: id, Name: name, SortOrder: ord}, nil } // RenameProjectGroup updates the group name. func (db *DB) RenameProjectGroup(id, name string) error { name = strings.TrimSpace(name) if id == "" || name == "" { return fmt.Errorf("id and name are required") } res, err := db.SQL.Exec(`UPDATE project_groups SET name = ? WHERE id = ?`, name, id) if err != nil { return err } n, _ := res.RowsAffected() if n == 0 { return fmt.Errorf("group %s: not found", id) } return nil } // DeleteProjectGroup removes a group; projects in it become ungrouped. func (db *DB) DeleteProjectGroup(id string) error { if id == "" { return fmt.Errorf("id is required") } tx, err := db.SQL.Begin() if err != nil { return err } defer func() { _ = tx.Rollback() }() if _, err := tx.Exec(`UPDATE projects SET group_id = NULL WHERE group_id = ?`, id); err != nil { return fmt.Errorf("ungroup projects: %w", err) } res, err := tx.Exec(`DELETE FROM project_groups WHERE id = ?`, id) if err != nil { return fmt.Errorf("delete group %s: %w", id, err) } n, _ := res.RowsAffected() if n == 0 { return fmt.Errorf("group %s: not found", id) } return tx.Commit() } // GetProjectGroup returns one group. func (db *DB) GetProjectGroup(id string) (ProjectGroup, error) { var g ProjectGroup err := db.SQL.QueryRow(`SELECT id, name, sort_order FROM project_groups WHERE id = ?`, id). Scan(&g.ID, &g.Name, &g.SortOrder) if err == sql.ErrNoRows { return ProjectGroup{}, fmt.Errorf("group %s: not found", id) } if err != nil { return ProjectGroup{}, err } return g, nil }