diff --git a/db/queries/tap.sql b/db/queries/tap.sql index 6d78deb..2ffb924 100644 --- a/db/queries/tap.sql +++ b/db/queries/tap.sql @@ -36,13 +36,19 @@ SELECT * FROM tap WHERE category = ?; --- name: GetLastOrder :one +-- name: GetLastOrderByOrderID :one SELECT * FROM tap -ORDER BY id DESC +ORDER BY order_id DESC LIMIT 1; -- name: GetOrderCount :many SELECT category, COUNT(*) FROM tap GROUP BY category; + +-- name: GetOrderCountByCategorySinceOrderID :many +SELECT category, COUNT(*) +FROM tap +WHERE order_id >= ? +GROUP BY category; diff --git a/internal/cmd/tui.go b/internal/cmd/tui.go index 73400e0..cdcdf21 100644 --- a/internal/cmd/tui.go +++ b/internal/cmd/tui.go @@ -2,16 +2,37 @@ package cmd import ( + "fmt" + "os" + tea "github.com/charmbracelet/bubbletea" "github.com/zeusWPI/scc/internal/pkg/db" + "github.com/zeusWPI/scc/pkg/util" tui "github.com/zeusWPI/scc/ui" + "github.com/zeusWPI/scc/ui/screen" ) +var screens = map[string]func(*db.DB) tea.Model{ + "cammie": screen.NewCammie, +} + // TUI starts the terminal user interface -func TUI(db *db.DB) *tea.Program { - tui := tui.New(db) +func TUI(db *db.DB) (*tea.Program, error) { + args := os.Args + if len(args) < 2 { + return nil, fmt.Errorf("No screen specified. Options are %v", util.Keys(screens)) + } + + screen := args[1] + + val, ok := screens[screen] + if !ok { + return nil, fmt.Errorf("Screen %s not found. Options are %v", screen, util.Keys(screens)) + } + + tui := tui.New(val(db)) program := tea.NewProgram(tui) - return program + return program, nil } diff --git a/internal/pkg/db/sqlc/tap.sql.go b/internal/pkg/db/sqlc/tap.sql.go index 5096e03..775c7ab 100644 --- a/internal/pkg/db/sqlc/tap.sql.go +++ b/internal/pkg/db/sqlc/tap.sql.go @@ -92,15 +92,15 @@ func (q *Queries) GetAllTaps(ctx context.Context) ([]Tap, error) { return items, nil } -const getLastOrder = `-- name: GetLastOrder :one +const getLastOrderByOrderID = `-- name: GetLastOrderByOrderID :one SELECT id, order_id, order_created_at, name, category, created_at FROM tap -ORDER BY id DESC +ORDER BY order_id DESC LIMIT 1 ` -func (q *Queries) GetLastOrder(ctx context.Context) (Tap, error) { - row := q.db.QueryRowContext(ctx, getLastOrder) +func (q *Queries) GetLastOrderByOrderID(ctx context.Context) (Tap, error) { + row := q.db.QueryRowContext(ctx, getLastOrderByOrderID) var i Tap err := row.Scan( &i.ID, @@ -147,6 +147,41 @@ func (q *Queries) GetOrderCount(ctx context.Context) ([]GetOrderCountRow, error) return items, nil } +const getOrderCountByCategorySinceOrderID = `-- name: GetOrderCountByCategorySinceOrderID :many +SELECT category, COUNT(*) +FROM tap +WHERE order_id >= ? +GROUP BY category +` + +type GetOrderCountByCategorySinceOrderIDRow struct { + Category string + Count int64 +} + +func (q *Queries) GetOrderCountByCategorySinceOrderID(ctx context.Context, orderID int64) ([]GetOrderCountByCategorySinceOrderIDRow, error) { + rows, err := q.db.QueryContext(ctx, getOrderCountByCategorySinceOrderID, orderID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetOrderCountByCategorySinceOrderIDRow + for rows.Next() { + var i GetOrderCountByCategorySinceOrderIDRow + if err := rows.Scan(&i.Category, &i.Count); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const getTapByCategory = `-- name: GetTapByCategory :many SELECT id, order_id, order_created_at, name, category, created_at FROM tap diff --git a/internal/pkg/tap/tap.go b/internal/pkg/tap/tap.go index a4d50f8..59aecad 100644 --- a/internal/pkg/tap/tap.go +++ b/internal/pkg/tap/tap.go @@ -44,7 +44,7 @@ func New(db *db.DB) *Tap { // Update gets all new orders from tap func (t *Tap) Update() error { // Get latest order - lastOrder, err := t.db.Queries.GetLastOrder(context.Background()) + lastOrder, err := t.db.Queries.GetLastOrderByOrderID(context.Background()) if err != nil { if err != sql.ErrNoRows { return err diff --git a/makefile b/makefile index a70118f..671e929 100644 --- a/makefile +++ b/makefile @@ -5,6 +5,13 @@ build: clean backend tui run: backend tui @./backend & ./tui +run-backend: backend + @./backend + +run-tui: tui + @read -p "Enter screen name: " screen; \ + ./tui $$screen + backend: @[ -f backend ] || (echo "Building backend..." && go build -o backend cmd/backend/backend.go) diff --git a/pkg/logger/logger.go b/pkg/logger/logger.go index b38ea9a..22f8fca 100644 --- a/pkg/logger/logger.go +++ b/pkg/logger/logger.go @@ -2,6 +2,7 @@ package logger import ( + "fmt" "os" "github.com/zeusWPI/scc/pkg/config" @@ -9,7 +10,7 @@ import ( ) // New returns a new logger instance -func New() (*zap.Logger, error) { +func New(logFile string) (*zap.Logger, error) { // Create logs directory err := os.Mkdir("logs", os.ModePerm) if err != nil && !os.IsExist(err) { @@ -24,8 +25,8 @@ func New() (*zap.Logger, error) { } else { zapConfig = zap.NewProductionConfig() } - zapConfig.OutputPaths = []string{"logs/scc.log"} - zapConfig.ErrorOutputPaths = []string{"logs/scc.log"} + zapConfig.OutputPaths = []string{fmt.Sprintf("logs/%s.log", logFile)} + zapConfig.ErrorOutputPaths = []string{fmt.Sprintf("logs/%s.log", logFile)} logger := zap.Must(zapConfig.Build()) diff --git a/pkg/util/map.go b/pkg/util/map.go new file mode 100644 index 0000000..5e45f39 --- /dev/null +++ b/pkg/util/map.go @@ -0,0 +1,10 @@ +package util + +// Keys returns the keys of a map +func Keys[T comparable, U any](m map[T]U) []T { + keys := make([]T, 0, len(m)) + for k := range m { + keys = append(keys, k) + } + return keys +} diff --git a/ui/screen/cammie.go b/ui/screen/cammie.go new file mode 100644 index 0000000..8c96472 --- /dev/null +++ b/ui/screen/cammie.go @@ -0,0 +1,32 @@ +// Package screen provides difference screens for the tui +package screen + +import ( + tea "github.com/charmbracelet/bubbletea" + "github.com/zeusWPI/scc/internal/pkg/db" +) + +// Cammie represents the cammie screen +type Cammie struct { + db *db.DB +} + +// NewCammie creates a new cammie screen +func NewCammie(db *db.DB) tea.Model { + return &Cammie{db: db} +} + +// Init initializes the cammie screen +func (c *Cammie) Init() tea.Cmd { + return nil +} + +// Update updates the cammie screen +func (c *Cammie) Update(_ tea.Msg) (tea.Model, tea.Cmd) { + return c, nil +} + +// View returns the cammie screen view +func (c *Cammie) View() string { + return "" +} diff --git a/ui/tui.go b/ui/tui.go index 664794a..146dc90 100644 --- a/ui/tui.go +++ b/ui/tui.go @@ -3,39 +3,33 @@ package tui import ( tea "github.com/charmbracelet/bubbletea" - "github.com/zeusWPI/scc/internal/pkg/db" - "github.com/zeusWPI/scc/ui/views" "go.uber.org/zap" ) // TUI represent a terminal instance type TUI struct { - db *db.DB - tap tea.Model + screen tea.Model } -// New creates a new tty instance -func New(db *db.DB) *TUI { - return &TUI{ - db: db, - tap: views.NewTapModel(db), - } +// New creates a new tui instance +func New(screen tea.Model) *TUI { + return &TUI{screen: screen} } -// Init initializes the tty +// Init initializes the tui func (t *TUI) Init() tea.Cmd { - return tea.Batch(tea.EnterAltScreen, t.tap.Init()) + return tea.Batch(tea.EnterAltScreen, t.screen.Init()) } -// Update updates the tty +// Update updates the tui func (t *TUI) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmds []tea.Cmd - tapModel, tapCmd := t.tap.Update(msg) - if tapCmd != nil { - cmds = append(cmds, tapCmd) + model, cmd := t.screen.Update(msg) + if cmd != nil { + cmds = append(cmds, cmd) } - t.tap = tapModel + t.screen = model switch msg := msg.(type) { case tea.KeyMsg: @@ -50,7 +44,7 @@ func (t *TUI) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return t, tea.Batch(cmds...) } -// View returns the tty view +// View returns the ttuity view func (t *TUI) View() string { - return t.tap.View() + return t.screen.View() } diff --git a/ui/view/tap.go b/ui/view/tap.go new file mode 100644 index 0000000..f5aac2f --- /dev/null +++ b/ui/view/tap.go @@ -0,0 +1,164 @@ +// Package view contains all the different views for the tui +package view + +import ( + "context" + "time" + + "github.com/NimbleMarkets/ntcharts/barchart" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/zeusWPI/scc/internal/pkg/db" + "go.uber.org/zap" +) + +// TapModel represents the tap model +type TapModel struct { + db *db.DB + lastOrderID int64 + mate float64 + soft float64 + beer float64 + food float64 +} + +// TapMessage represents a tap message +type TapMessage struct { + lastOrderID int64 + items []tapItem +} + +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"), +} + +// NewTapModel creates a new tap model +func NewTapModel(db *db.DB) *TapModel { + return &TapModel{db: db, lastOrderID: -1} +} + +// Init initializes the tap model +func (t *TapModel) Init() tea.Cmd { + return updateOrders(t.db, t.lastOrderID) +} + +// Update updates the tap model +func (t *TapModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + t.lastOrderID = msg.(TapMessage).lastOrderID + + for _, msg := range msg.(TapMessage).items { + switch msg.category { + case "Mate": + t.mate += msg.amount + case "Soft": + t.soft += msg.amount + case "Beer": + t.beer += msg.amount + case "Food": + t.food += msg.amount + } + } + + return t, updateOrders(t.db, t.lastOrderID) +} + +// View returns the tap view +func (t *TapModel) View() string { + chart := barchart.New(20, 20) + + barMate := barchart.BarData{ + Label: "Mate", + Values: []barchart.BarValue{{ + Name: "Item1", + Value: t.mate, + Style: lipgloss.NewStyle().Foreground(tapCategoryColor["Mate"]), + }}, + } + barSoft := barchart.BarData{ + Label: "Soft", + Values: []barchart.BarValue{{ + Name: "Item1", + Value: t.soft, + Style: lipgloss.NewStyle().Foreground(tapCategoryColor["Soft"]), + }}, + } + barBeer := barchart.BarData{ + Label: "Beer", + Values: []barchart.BarValue{{ + Name: "Item1", + Value: t.beer, + Style: lipgloss.NewStyle().Foreground(tapCategoryColor["Beer"]), + }}, + } + barFood := barchart.BarData{ + Label: "Food", + Values: []barchart.BarValue{{ + Name: "Item1", + Value: t.food, + Style: lipgloss.NewStyle().Foreground(tapCategoryColor["Food"]), + }}, + } + + chart.PushAll([]barchart.BarData{barMate, barSoft, barBeer, barFood}) + chart.Draw() + + return chart.View() +} + +func updateOrders(db *db.DB, lastOrderID int64) tea.Cmd { + return tea.Tick(time.Second, func(_ time.Time) tea.Msg { + order, err := db.Queries.GetLastOrderByOrderID(context.Background()) + if err != nil { + zap.S().Error("DB: Failed to get last order", err) + return nil + } + + if order.OrderID <= lastOrderID { + return nil + } + + orders, err := db.Queries.GetOrderCountByCategorySinceOrderID(context.Background(), lastOrderID) + if err != nil { + zap.S().Error("DB: Failed to get tap orders", err) + return nil + } + + mate, soft, beer, food := 0.0, 0.0, 0.0, 0.0 + 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) + } + } + + 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}) + } + if food > 0 { + messages = append(messages, tapItem{"Food", food}) + } + + return TapMessage{lastOrderID: order.OrderID, items: messages} + }) +}