112 lines
3.9 KiB
Go
112 lines
3.9 KiB
Go
package mujica
|
|
|
|
import (
|
|
"fmt"
|
|
"slices"
|
|
|
|
"github.com/egoist/mygo/ui"
|
|
|
|
"mujicaui/internal"
|
|
"mujicaui/theme"
|
|
)
|
|
|
|
// Sprint is one iteration: its name, goal and inclusive dates.
|
|
type Sprint struct {
|
|
Name string
|
|
Goal string
|
|
Start, End Date
|
|
}
|
|
|
|
// SprintBoardOptions configure a SprintBoard.
|
|
type SprintBoardOptions struct {
|
|
Board KanbanBoardOptions
|
|
// Today counts the days left; zero uses the system clock.
|
|
Today Date
|
|
}
|
|
|
|
// SprintBoard combines iteration information, completion and a task board.
|
|
func SprintBoard(c *ui.Context, sprint Sprint, tasks *[]Task, opts SprintBoardOptions) KanbanResult {
|
|
datetimeCheckDate("SprintBoard Start", sprint.Start)
|
|
datetimeCheckDate("SprintBoard End", sprint.End)
|
|
if sprint.Start.IsZero() || sprint.End.Compare(sprint.Start) < 0 {
|
|
panic(fmt.Sprintf("mujica: SprintBoard dates %v..%v are not a span", sprint.Start, sprint.End))
|
|
}
|
|
k := Tokens(c)
|
|
d := Density(c)
|
|
today := datetimeToday(c, opts.Today)
|
|
var r KanbanResult
|
|
col := ui.Column(c).Gap(d.Unit() * 4).Grow(1).MinHeight(0).Role(ui.RoleGroup).Label(sprint.Name)
|
|
cache := ui.Local(col, "sprint-counts", func() sprintCache { return sprintCache{} })
|
|
var first *Task
|
|
if len(*tasks) > 0 {
|
|
first = &(*tasks)[0]
|
|
}
|
|
if !cache.ready || cache.first != first || cache.n != len(*tasks) || cache.version != opts.Board.Version || !slices.Equal(cache.statuses, opts.Board.Statuses) {
|
|
*cache = sprintCache{first: first, n: len(*tasks), version: opts.Board.Version, statuses: slices.Clone(opts.Board.Statuses), ready: true}
|
|
for _, t := range *tasks {
|
|
if g := projectStatus(opts.Board.Statuses, t.Status).Group; g != StatusCanceled {
|
|
cache.total++
|
|
if g == StatusCompleted {
|
|
cache.done++
|
|
}
|
|
}
|
|
}
|
|
}
|
|
done, total := cache.done, cache.total
|
|
col.Children(func() {
|
|
ui.Row(c).Gap(d.Unit() * 6).AlignItems(ui.Center).Wrap().Children(func() {
|
|
ui.Column(c).Gap(d.Unit()).Grow(1).Children(func() {
|
|
ui.Text(c, sprint.Name).Font(theme.SerifFont).FontSize(FontSize(c, theme.H2Size)).TextColor(k.Text)
|
|
if sprint.Goal != "" {
|
|
ui.Text(c, sprint.Goal).TextColor(k.TextMuted)
|
|
}
|
|
ui.Text(c, weekViewTitle(c, sprint.Start, sprint.End)).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted)
|
|
})
|
|
sprintStat(c, sprintDaysLeft(c, sprint, today), msg(c, "sprint_board.time"))
|
|
frac := 0.0
|
|
if total > 0 {
|
|
frac = float64(done) / float64(total)
|
|
}
|
|
ui.Column(c).Width(d.ControlHeight() * 5).Gap(d.Unit()).Children(func() {
|
|
ui.Text(c, fmt.Sprintf(msg(c, "sprint_board.done"), done, total)).TextColor(k.Text).FontFeatures("tnum")
|
|
Progress(c, frac, ProgressOptions{Label: msg(c, "sprint_board.progress")})
|
|
})
|
|
})
|
|
ui.Box(c).Height(12).FillWidth().Draw(func(p *ui.Painter, rr ui.Rect) { internal.RuleDiamond(p, rr, 8, 6, k.Ornament) })
|
|
r = KanbanBoard(c, tasks, opts.Board)
|
|
})
|
|
if _, moved := r.Moved(); moved {
|
|
cache.ready = false
|
|
c.Invalidate()
|
|
}
|
|
r.Element = col
|
|
return r
|
|
}
|
|
|
|
// sprintDaysLeft formats remaining time relative to the iteration.
|
|
func sprintDaysLeft(c *ui.Context, s Sprint, today Date) string {
|
|
switch {
|
|
case today.Compare(s.Start) < 0:
|
|
return calendarCount(c, "sprint_board.starts_in", int(s.Start.civil().Sub(today.civil()).Hours()/24))
|
|
case today.Compare(s.End) > 0:
|
|
return msg(c, "sprint_board.ended")
|
|
}
|
|
return calendarCount(c, "sprint_board.left", int(s.End.civil().Sub(today.civil()).Hours()/24)+1)
|
|
}
|
|
|
|
// sprintStat is a figure above its caption.
|
|
func sprintStat(c *ui.Context, value, caption string) {
|
|
k := Tokens(c)
|
|
ui.Column(c).Gap(2).Role(ui.RoleStatus).Label(caption + ": " + value).Children(func() {
|
|
ui.Text(c, value).Font(theme.SerifFont).FontSize(FontSize(c, theme.H3Size)).TextColor(k.AccentText)
|
|
ui.Text(c, caption).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted)
|
|
})
|
|
}
|
|
|
|
type sprintCache struct {
|
|
first *Task
|
|
n, version, done, total int
|
|
statuses []WorkflowStatus
|
|
ready bool
|
|
}
|