diff --git a/db/queries/tap.sql b/db/queries/tap.sql index 36072da..742aa7f 100644 --- a/db/queries/tap.sql +++ b/db/queries/tap.sql @@ -50,7 +50,7 @@ FROM tap GROUP BY category; -- name: GetOrderCountByCategorySinceOrderID :many -SELECT category, COUNT(*) +SELECT category, COUNT(*), CAST(MAX(order_created_at) AS INTEGER) AS latest_order_created_at FROM tap WHERE order_id >= ? GROUP BY category; diff --git a/internal/pkg/db/sqlc/tap.sql.go b/internal/pkg/db/sqlc/tap.sql.go index 66dfd70..33664e2 100644 --- a/internal/pkg/db/sqlc/tap.sql.go +++ b/internal/pkg/db/sqlc/tap.sql.go @@ -148,15 +148,16 @@ func (q *Queries) GetOrderCount(ctx context.Context) ([]GetOrderCountRow, error) } const getOrderCountByCategorySinceOrderID = `-- name: GetOrderCountByCategorySinceOrderID :many -SELECT category, COUNT(*) +SELECT category, COUNT(*), CAST(MAX(order_created_at) AS INTEGER) AS latest_order_created_at FROM tap WHERE order_id >= ? GROUP BY category ` type GetOrderCountByCategorySinceOrderIDRow struct { - Category string - Count int64 + Category string + Count int64 + LatestOrderCreatedAt int64 } func (q *Queries) GetOrderCountByCategorySinceOrderID(ctx context.Context, orderID int64) ([]GetOrderCountByCategorySinceOrderIDRow, error) { @@ -168,7 +169,7 @@ func (q *Queries) GetOrderCountByCategorySinceOrderID(ctx context.Context, order var items []GetOrderCountByCategorySinceOrderIDRow for rows.Next() { var i GetOrderCountByCategorySinceOrderIDRow - if err := rows.Scan(&i.Category, &i.Count); err != nil { + if err := rows.Scan(&i.Category, &i.Count, &i.LatestOrderCreatedAt); err != nil { return nil, err } items = append(items, i) diff --git a/ui/view/tap/style.go b/ui/view/tap/style.go new file mode 100644 index 0000000..d72b0e4 --- /dev/null +++ b/ui/view/tap/style.go @@ -0,0 +1,50 @@ +package tap + +import "github.com/charmbracelet/lipgloss" + +var base = lipgloss.NewStyle() + +// Width +var ( + widthAmount = 5 + widthCategory = 8 + widthLast = 13 +) + +// Margin +var mStats = 2 + +// Barchart +var ( + widthBar = 40 + heightBar = 20 +) + +// Colors +var ( + cMate = lipgloss.Color("#D27D2D") + cSoft = lipgloss.Color("#ADD8E6") + cBeer = lipgloss.Color("#F9B116") + cFood = lipgloss.Color("#00ff00") + + cBorder = lipgloss.Color("#383838") + cStatsTitle = lipgloss.Color("#EE4B2B") +) + +// Styles Chart +var ( + sMate = base.Foreground(cMate) + sSoft = base.Foreground(cSoft) + sBeer = base.Foreground(cBeer) + sFood = base.Foreground(cFood) + sUnknown = base +) + +// Styles stats +var ( + sStats = base.Border(lipgloss.NormalBorder(), false, false, false, true).BorderForeground(cBorder).MarginLeft(mStats).PaddingLeft(mStats) + sStatsTitle = base.Foreground(cStatsTitle).Bold(true).Width(widthAmount+widthCategory+widthLast).Border(lipgloss.NormalBorder(), false, false, true, false).BorderForeground(cBorder) + sStatsAmount = base.Width(widthAmount).Bold(true) + sStatsCategory = base.Width(widthCategory) + sStatsLast = base.Width(widthLast).Align(lipgloss.Right).Italic(true).Faint(true) +) diff --git a/ui/view/tap/tap.go b/ui/view/tap/tap.go index 5278d6a..b46ce69 100644 --- a/ui/view/tap/tap.go +++ b/ui/view/tap/tap.go @@ -4,8 +4,9 @@ package tap import ( "context" "database/sql" + "slices" + "time" - "github.com/NimbleMarkets/ntcharts/barchart" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/zeusWPI/scc/internal/pkg/db" @@ -13,14 +14,27 @@ import ( "github.com/zeusWPI/scc/ui/view" ) +type category string + +const ( + mate category = "Mate" + soft category = "Soft" + beer category = "Beer" + food category = "Food" +) + +var categoryToStyle = map[category]lipgloss.Style{ + mate: sMate, + soft: sSoft, + beer: sBeer, + food: sFood, +} + // Model represents the tap model type Model struct { db *db.DB lastOrderID int64 - mate float64 - soft float64 - beer float64 - food float64 + items []tapItem } // Msg represents a tap message @@ -30,15 +44,9 @@ type Msg struct { } type tapItem struct { - category string - amount float64 -} - -var tapCategoryColor = map[string]lipgloss.Color{ - "Mate": lipgloss.Color("208"), - "Soft": lipgloss.Color("86"), - "Beer": lipgloss.Color("160"), - "Food": lipgloss.Color("40"), + category category + amount int + last time.Time } // NewModel creates a new tap model @@ -51,25 +59,38 @@ func (m *Model) Init() tea.Cmd { return nil } +// Name returns the name of the view +func (m *Model) Name() string { + return "Tap" +} + // Update updates the tap model func (m *Model) Update(msg tea.Msg) (view.View, tea.Cmd) { switch msg := msg.(type) { case Msg: m.lastOrderID = msg.lastOrderID - for _, msg := range msg.items { - switch msg.category { - case "Mate": - m.mate += msg.amount - case "Soft": - m.soft += msg.amount - case "Beer": - m.beer += msg.amount - case "Food": - m.food += msg.amount + for _, msgItem := range msg.items { + found := false + for i, item := range m.items { + if item.category == msgItem.category { + m.items[i].amount += msgItem.amount + m.items[i].last = msgItem.last + found = true + break + } + } + + if !found { + m.items = append(m.items, msgItem) } } + // Sort to display bars in order + slices.SortFunc(m.items, func(i, j tapItem) int { + return j.amount - i.amount + }) + return m, nil } @@ -78,45 +99,15 @@ func (m *Model) Update(msg tea.Msg) (view.View, tea.Cmd) { // View returns the tap view func (m *Model) View() string { - chart := barchart.New(20, 20) - - barMate := barchart.BarData{ - Label: "Mate", - Values: []barchart.BarValue{{ - Name: "Mate", - Value: m.mate, - Style: lipgloss.NewStyle().Foreground(tapCategoryColor["Mate"]), - }}, - } - barSoft := barchart.BarData{ - Label: "Soft", - Values: []barchart.BarValue{{ - Name: "Soft", - Value: m.soft, - Style: lipgloss.NewStyle().Foreground(tapCategoryColor["Soft"]), - }}, - } - barBeer := barchart.BarData{ - Label: "Beer", - Values: []barchart.BarValue{{ - Name: "Beer", - Value: m.beer, - Style: lipgloss.NewStyle().Foreground(tapCategoryColor["Beer"]), - }}, - } - barFood := barchart.BarData{ - Label: "Food", - Values: []barchart.BarValue{{ - Name: "Food", - Value: m.food, - Style: lipgloss.NewStyle().Foreground(tapCategoryColor["Food"]), - }}, - } + chart := m.viewChart() + stats := m.viewStats() - chart.PushAll([]barchart.BarData{barMate, barSoft, barBeer, barFood}) - chart.Draw() + // Give them same height + stats = sStats.Height(lipgloss.Height(chart)).Render(stats) - return chart.View() + // Join them together + view := lipgloss.JoinHorizontal(lipgloss.Top, chart, stats) + return view } // GetUpdateDatas returns all the update functions for the tap model @@ -140,45 +131,39 @@ func updateOrders(view view.View) (tea.Msg, error) { if err == sql.ErrNoRows { err = nil } - return Msg{lastOrderID: lastOrderID, items: []tapItem{}}, err + return nil, err } if order.OrderID <= lastOrderID { - return Msg{lastOrderID: lastOrderID, items: []tapItem{}}, nil + return nil, nil } orders, err := m.db.Queries.GetOrderCountByCategorySinceOrderID(context.Background(), lastOrderID) if err != nil { - return Msg{lastOrderID: lastOrderID, items: []tapItem{}}, err + return nil, err } - mate, soft, beer, food := 0.0, 0.0, 0.0, 0.0 + counts := make(map[category]tapItem) + for _, order := range orders { - switch order.Category { - case "Mate": - mate += float64(order.Count) - case "Soft": - soft += float64(order.Count) - case "Beer": - beer += float64(order.Count) - case "Food": - food += float64(order.Count) + if entry, ok := counts[category(order.Category)]; ok { + entry.amount += int(order.Count) + counts[category(order.Category)] = entry + continue } - } - messages := make([]tapItem, 0, 4) - if mate > 0 { - messages = append(messages, tapItem{"Mate", mate}) - } - if soft > 0 { - messages = append(messages, tapItem{"Soft", soft}) - } - if beer > 0 { - messages = append(messages, tapItem{"Beer", beer}) + counts[category(order.Category)] = tapItem{ + category: category(order.Category), + amount: int(order.Count), + last: time.Unix(order.LatestOrderCreatedAt, 0), + } } - if food > 0 { - messages = append(messages, tapItem{"Food", food}) + + items := make([]tapItem, 0, len(counts)) + + for _, v := range counts { + items = append(items, v) } - return Msg{lastOrderID: order.OrderID, items: messages}, err + return Msg{lastOrderID: order.OrderID, items: items}, nil } diff --git a/ui/view/tap/view.go b/ui/view/tap/view.go new file mode 100644 index 0000000..4c4a850 --- /dev/null +++ b/ui/view/tap/view.go @@ -0,0 +1,55 @@ +package tap + +import ( + "strconv" + + "github.com/NimbleMarkets/ntcharts/barchart" + "github.com/charmbracelet/lipgloss" +) + +func (m *Model) viewChart() string { + chart := barchart.New(widthBar, heightBar) + bars := make([]barchart.BarData, 0, len(m.items)) + + for _, item := range m.items { + style, ok := categoryToStyle[item.category] + if !ok { + style = sUnknown + } + + bars = append(bars, barchart.BarData{ + Label: string(item.category), + Values: []barchart.BarValue{{ + Name: string(item.category), + Value: float64(item.amount), + Style: style, + }}, + }) + } + + chart.PushAll(bars) + chart.Draw() + + return chart.View() +} + +func (m *Model) viewStats() string { + rows := make([]string, 0, len(m.items)) + + for _, item := range m.items { + amount := sStatsAmount.Render(strconv.Itoa(item.amount)) + category := sStatsCategory.Inherit(categoryToStyle[item.category]).Render(string(item.category)) + last := sStatsLast.Render(item.last.Format("02/01 15:04")) + + text := lipgloss.JoinHorizontal(lipgloss.Top, amount, category, last) + rows = append(rows, text) + } + + view := lipgloss.JoinVertical(lipgloss.Left, rows...) + + // Add title + title := sStatsTitle.Render("Leaderboard") + view = lipgloss.JoinVertical(lipgloss.Left, title, view) + + return view +}