phase 0.3

This commit is contained in:
portakal 2026-06-04 20:51:20 +03:00
commit 4a94bfc4e7
52 changed files with 11653 additions and 0 deletions

176
internal/ui/contracts.go Normal file
View file

@ -0,0 +1,176 @@
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)
}