skyrama_tui/internal/ui/contracts.go
2026-06-04 20:51:20 +03:00

176 lines
4.5 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package ui
import (
"fmt"
"strings"
"time"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"skyrama-tui/internal/model"
"skyrama-tui/internal/service"
"skyrama-tui/internal/style"
)
// ─── Messages ─────────────────────────────────────────────────────────────────
type ContractsLoadedMsg struct{ Contracts []model.Contract }
type ContractsErrMsg struct{ Err error }
// ─── ContractsModel ───────────────────────────────────────────────────────────
type ContractsModel struct {
client *service.GameService
airportID uint32
contracts []model.Contract
cursor int
statusMsg string
focused bool
width int
height int
}
func NewContractsModel(client *service.GameService, airportID uint32) ContractsModel {
return ContractsModel{client: client, airportID: airportID}
}
func (m ContractsModel) Init() tea.Cmd { return m.load() }
func (m ContractsModel) Update(msg tea.Msg) (ContractsModel, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
m.width = msg.Width
m.height = msg.Height
case ContractsLoadedMsg:
m.contracts = msg.Contracts
if m.cursor >= len(m.contracts) {
m.cursor = clampMin(len(m.contracts)-1, 0)
}
case ContractsErrMsg:
m.statusMsg = style.Error.String() + msg.Err.Error()
case tea.KeyMsg:
if !m.focused {
break
}
switch msg.String() {
case "up", "k":
if m.cursor > 0 {
m.cursor--
}
case "down", "j":
if m.cursor < len(m.contracts)-1 {
m.cursor++
}
case "r":
m.statusMsg = ""
return m, m.load()
}
}
return m, nil
}
func (m ContractsModel) View() string {
header := style.PanelHeader.Render("📋 Active Contracts")
statusLine := ""
if m.statusMsg != "" {
statusLine = "\n" + m.statusMsg + "\n"
}
if len(m.contracts) == 0 {
return lipgloss.JoinVertical(lipgloss.Left,
header,
statusLine,
"",
style.Muted.Render("No active contracts right now."),
)
}
rows := make([]string, len(m.contracts))
for i, c := range m.contracts {
rows[i] = m.renderContract(c, i == m.cursor)
}
help := style.HelpKey.Render("↑↓ / jk") + style.HelpDesc.Render(" navigate ") +
style.HelpKey.Render("r") + style.HelpDesc.Render(" refresh")
return lipgloss.JoinVertical(lipgloss.Left,
header,
statusLine,
strings.Join(rows, "\n"),
"",
style.Muted.Render(help),
)
}
func (m ContractsModel) renderContract(c model.Contract, selected bool) string {
pct := c.ProgressPct()
barW := 24
bar := style.ResourceBar(int(c.CurrentAmountDelivered), int(c.RequiredAmount), barW, style.ColorGreen)
expiresIn := time.Until(c.ExpiresAt).Round(time.Minute)
urgency := style.Muted
if expiresIn < 30*time.Minute {
urgency = style.Error
} else if expiresIn < 2*time.Hour {
urgency = style.Warning
}
inner := lipgloss.JoinVertical(lipgloss.Left,
style.Bold.Render(c.Title),
style.Muted.Render(fmt.Sprintf("Deliver %d × %s", c.RequiredAmount, c.ProductName)),
lipgloss.JoinHorizontal(lipgloss.Top,
bar,
style.Muted.Render(fmt.Sprintf(" %d/%d (%d%%)",
c.CurrentAmountDelivered, c.RequiredAmount, pct)),
),
urgency.Render(fmt.Sprintf("⏰ Expires in %s", fmtContractDuration(expiresIn))),
)
if selected {
return style.PanelFocused.Render(inner)
}
return style.Panel.Render(inner)
}
// ─── Exported for parent AppModel ─────────────────────────────────────────────
// Load is the exported version for use by parent AppModel.
func (m ContractsModel) Load() tea.Cmd { return m.load() }
func (m ContractsModel) load() tea.Cmd {
client := m.client
airportID := m.airportID
return func() tea.Msg {
contracts, err := client.GetActiveContracts(airportID)
if err != nil {
return ContractsErrMsg{Err: err}
}
return ContractsLoadedMsg{Contracts: contracts}
}
}
// ─── Helpers ──────────────────────────────────────────────────────────────────
func clampMin(v, min int) int {
if v < min {
return min
}
return v
}
func fmtContractDuration(d time.Duration) string {
if d <= 0 {
return "expired"
}
h := int(d.Hours())
m := int(d.Minutes()) % 60
if h > 0 {
return fmt.Sprintf("%dh %dm", h, m)
}
return fmt.Sprintf("%dm", m)
}