package ticket import ( "strings" "unicode" ) // BoardList is a project-scoped view/filter over tickets by status. type BoardList string const ( BoardAll BoardList = "all" BoardBacklog BoardList = "backlog" BoardInProgress BoardList = "in_progress" BoardDone BoardList = "done" ) // AllBoardLists is the ordered picker list. var AllBoardLists = []BoardList{BoardAll, BoardBacklog, BoardInProgress, BoardDone} // Label returns a display name for the board list. func (b BoardList) Label() string { switch b { case BoardAll: return "All" case BoardBacklog: return "Backlog" case BoardInProgress: return "In Progress" case BoardDone: return "Done" default: return string(b) } } // Shortcut returns the board-menu hotkey (first letter of the label). func (b BoardList) Shortcut() rune { label := b.Label() if label == "" { return 0 } return unicode.ToLower([]rune(label)[0]) } // Index returns the position in AllBoardLists. func (b BoardList) Index() int { for i, bl := range AllBoardLists { if bl == b { return i } } return 0 } // ParseBoardList validates a board list id; invalid values become BoardAll. func ParseBoardList(s string) BoardList { b := BoardList(s) switch b { case BoardAll, BoardBacklog, BoardInProgress, BoardDone: return b default: return BoardAll } } // BoardFilter is a multi-select set of board lists (OR semantics). type BoardFilter []BoardList // ParseBoardLists parses a comma-separated list id string (legacy single values work). func ParseBoardLists(s string) BoardFilter { s = strings.TrimSpace(s) if s == "" { return BoardFilter{BoardAll} } parts := strings.Split(s, ",") var out BoardFilter seen := map[BoardList]bool{} for _, p := range parts { bl := ParseBoardList(strings.TrimSpace(p)) if seen[bl] { continue } seen[bl] = true out = append(out, bl) } return NormalizeFilter(out) } // FormatBoardLists serializes a filter for config persistence. func FormatBoardLists(f BoardFilter) string { f = NormalizeFilter(f) parts := make([]string, len(f)) for i, bl := range f { parts[i] = string(bl) } return strings.Join(parts, ",") } // NormalizeFilter ensures a non-empty filter; All alone if empty or only All. // If All is combined with others, All is dropped. func NormalizeFilter(f BoardFilter) BoardFilter { if len(f) == 0 { return BoardFilter{BoardAll} } hasAll := false var others BoardFilter seen := map[BoardList]bool{} for _, bl := range f { if bl == BoardAll { hasAll = true continue } if seen[bl] { continue } seen[bl] = true others = append(others, bl) } if len(others) > 0 { return others } if hasAll { return BoardFilter{BoardAll} } return BoardFilter{BoardAll} } // Contains reports whether bl is in the filter. func (f BoardFilter) Contains(bl BoardList) bool { for _, x := range f { if x == bl { return true } } return false } // Label returns a short header label for the active filter. func (f BoardFilter) Label() string { f = NormalizeFilter(f) if len(f) == 1 { return f[0].Label() } parts := make([]string, len(f)) for i, bl := range f { parts[i] = bl.Label() } return strings.Join(parts, "+") } // Matches reports whether a ticket status is visible under this filter. func (f BoardFilter) Matches(status Status) bool { f = NormalizeFilter(f) if f.Contains(BoardAll) { return true } for _, bl := range f { if bl.Matches(status) { return true } } return false } // DefaultStatus is used when creating a ticket under this filter. func (f BoardFilter) DefaultStatus() Status { f = NormalizeFilter(f) if len(f) == 1 { return f[0].DefaultStatus() } return StatusTodo } // Matches reports whether a ticket belongs on this board list. func (b BoardList) Matches(status Status) bool { switch b { case BoardAll: return true case BoardBacklog: return status == StatusTodo case BoardInProgress: return status == StatusInProgress case BoardDone: return status == StatusDone default: return true } } // DefaultStatus is the status used when creating a ticket on this list. func (b BoardList) DefaultStatus() Status { switch b { case BoardInProgress: return StatusInProgress case BoardDone: return StatusDone default: return StatusTodo } }