skyrama_tui/internal/adminui/planes.go
2026-06-04 20:51:20 +03:00

334 lines
9 KiB
Go

package adminui
import (
"fmt"
"strconv"
"strings"
tea "github.com/charmbracelet/bubbletea"
"skyrama-tui/internal/db"
)
// ── Messages ──────────────────────────────────────────────────────────────────
type planeLoadedMsg []db.AdminPlane
type planeDoneMsg struct{ err error }
// ── Field indices ─────────────────────────────────────────────────────────────
const (
pfName = 0
pfSize = 1
pfType = 2
pfOpType = 3
pfMinLevel = 4
pfTravelSec = 5
pfFuelCost = 6
pfCashCost = 7
pfMaxPax = 8
pfBuyCost = 9
pfXP = 10
pfCashRew = 11
pfCargoRew = 12
pfBoardingTime = 13
pfFuelingTime = 14
)
// ── Model ─────────────────────────────────────────────────────────────────────
type planesTab struct {
client *db.AdminClient
items []db.AdminPlane
cursor int
inForm bool
isEdit bool
editID uint32
fields []field
focusIdx int
status string
isErr bool
w, h int
}
func newPlanesTab(client *db.AdminClient) *planesTab {
return &planesTab{client: client}
}
func (t *planesTab) InForm() bool { return t.inForm }
func (t *planesTab) Init() tea.Cmd { return t.load() }
func (t *planesTab) load() tea.Cmd {
return func() tea.Msg {
items, err := t.client.ListPlanes()
if err != nil {
return planeDoneMsg{err}
}
return planeLoadedMsg(items)
}
}
func (t *planesTab) initFields() []field {
return []field{
newTextField("Name", "e.g. Cessna 172"),
newEnumField("Aircraft Size", []string{"small", "medium", "large", "huge"}),
newEnumField("Aircraft Type", []string{"plane", "helicopter", "seaplane", "spaceship"}),
newEnumField("Operation Type", []string{"passenger", "cargo"}),
newTextField("Min Level", "1"),
newTextField("Travel Time (sec)", "300"),
newTextField("Fuel Cost", "50"),
newTextField("Cash Cost", "10"),
newTextField("Max Passengers (0=null)", "0"),
newTextField("Buy Cost (cash)", "1000"),
newTextField("XP Reward", "20"),
newTextField("Cash Reward (0=null)", "50"),
newTextField("Cargo Reward (0=null)", "0"),
newTextField("Boarding Time (sec, 0=skip)", "0"),
newTextField("Fueling Time (sec, 0=skip)", "0"),
}
}
func (t *planesTab) openForm() {
t.fields = t.initFields()
t.fields[pfName].input.Focus()
t.focusIdx = pfName
t.isEdit = false
t.inForm = true
t.status = ""
}
func (t *planesTab) openEditForm(p db.AdminPlane) {
t.fields = t.initFields()
t.fields[pfName].input.SetValue(p.Name)
setEnumSel(&t.fields[pfSize], p.Size)
setEnumSel(&t.fields[pfType], p.AircraftType)
setEnumSel(&t.fields[pfOpType], p.OperationType)
t.fields[pfMinLevel].input.SetValue(strconv.Itoa(int(p.MinLevel)))
t.fields[pfTravelSec].input.SetValue(strconv.Itoa(int(p.TravelSec)))
t.fields[pfFuelCost].input.SetValue(strconv.Itoa(int(p.FuelCost)))
t.fields[pfCashCost].input.SetValue(strconv.Itoa(int(p.CashCost)))
t.fields[pfMaxPax].input.SetValue(strconv.Itoa(p.MaxPassengers))
t.fields[pfBuyCost].input.SetValue(strconv.Itoa(int(p.BuyCost)))
t.fields[pfXP].input.SetValue(strconv.Itoa(int(p.XP)))
t.fields[pfCashRew].input.SetValue(strconv.Itoa(p.CashReward))
t.fields[pfCargoRew].input.SetValue(strconv.Itoa(p.CargoReward))
t.fields[pfBoardingTime].input.SetValue(strconv.Itoa(int(p.BoardingTimeSec)))
t.fields[pfFuelingTime].input.SetValue(strconv.Itoa(int(p.FuelingTimeSec)))
t.fields[pfName].input.Focus()
t.focusIdx = pfName
t.isEdit = true
t.editID = p.ID
t.inForm = true
t.status = ""
}
func mustInt(s string) (int, error) {
return strconv.Atoi(strings.TrimSpace(s))
}
func (t *planesTab) submit() tea.Cmd {
name := strings.TrimSpace(t.fields[pfName].value())
if name == "" {
t.status = "name is required"
t.isErr = true
return nil
}
lvl, err := mustInt(t.fields[pfMinLevel].value())
if err != nil {
t.status = "min level must be a number"
t.isErr = true
return nil
}
travelSec, err := mustInt(t.fields[pfTravelSec].value())
if err != nil || travelSec <= 0 {
t.status = "travel time must be a positive number"
t.isErr = true
return nil
}
fuelCost, err := mustInt(t.fields[pfFuelCost].value())
if err != nil {
t.status = "fuel cost must be a number"
t.isErr = true
return nil
}
cashCost, err := mustInt(t.fields[pfCashCost].value())
if err != nil {
t.status = "cash cost must be a number"
t.isErr = true
return nil
}
maxPax, err := mustInt(t.fields[pfMaxPax].value())
if err != nil {
t.status = "max passengers must be a number (0 for none)"
t.isErr = true
return nil
}
buyCost, err := mustInt(t.fields[pfBuyCost].value())
if err != nil {
t.status = "buy cost must be a number"
t.isErr = true
return nil
}
xp, err := mustInt(t.fields[pfXP].value())
if err != nil {
t.status = "XP reward must be a number"
t.isErr = true
return nil
}
cashRew, err := mustInt(t.fields[pfCashRew].value())
if err != nil {
t.status = "cash reward must be a number (0 for none)"
t.isErr = true
return nil
}
cargoRew, err := mustInt(t.fields[pfCargoRew].value())
if err != nil {
t.status = "cargo reward must be a number (0 for none)"
t.isErr = true
return nil
}
boardingTime, err := mustInt(t.fields[pfBoardingTime].value())
if err != nil {
t.status = "boarding time must be a number"
t.isErr = true
return nil
}
fuelingTime, err := mustInt(t.fields[pfFuelingTime].value())
if err != nil {
t.status = "fueling time must be a number"
t.isErr = true
return nil
}
p := db.AddPlaneParams{
Name: name,
Size: t.fields[pfSize].value(),
AircraftType: t.fields[pfType].value(),
OperationType: t.fields[pfOpType].value(),
MinLevel: uint32(lvl),
FuelCost: uint32(fuelCost),
CashCost: uint32(cashCost),
MaxPassengers: maxPax,
TravelSec: uint32(travelSec),
BuyCost: uint32(buyCost),
XP: uint32(xp),
CashReward: cashRew,
CargoReward: cargoRew,
BoardingTimeSec: boardingTime,
FuelingTimeSec: fuelingTime,
}
if t.isEdit {
id := t.editID
return func() tea.Msg { return planeDoneMsg{t.client.UpdatePlane(id, p)} }
}
return func() tea.Msg { return planeDoneMsg{t.client.AddPlane(p)} }
}
func (t *planesTab) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch m := msg.(type) {
case tea.WindowSizeMsg:
t.w, t.h = m.Width, m.Height
return t, nil
case planeLoadedMsg:
t.items = []db.AdminPlane(m)
if t.cursor >= len(t.items) && t.cursor > 0 {
t.cursor = len(t.items) - 1
}
return t, nil
case planeDoneMsg:
if m.err != nil {
t.status = m.err.Error()
t.isErr = true
return t, nil
}
t.inForm = false
t.status = "saved"
t.isErr = false
return t, t.load()
case tea.KeyMsg:
if t.inForm {
switch m.String() {
case "esc":
t.inForm = false
t.status = ""
case "ctrl+s":
return t, t.submit()
default:
var cmd tea.Cmd
t.focusIdx, cmd = handleFormKey(t.fields, t.focusIdx, m)
return t, cmd
}
return t, nil
}
switch m.String() {
case "q":
return t, tea.Quit
case "j", "down":
if t.cursor < len(t.items)-1 {
t.cursor++
}
case "k", "up":
if t.cursor > 0 {
t.cursor--
}
case "a":
t.openForm()
case "e":
if len(t.items) > 0 {
t.openEditForm(t.items[t.cursor])
}
case "d":
if len(t.items) > 0 {
id := t.items[t.cursor].ID
return t, func() tea.Msg {
return planeDoneMsg{t.client.DeletePlane(id)}
}
}
case "r":
return t, t.load()
}
}
return t, nil
}
func (t *planesTab) View() string {
var sb strings.Builder
sb.WriteString(hdrSt.Render(" Planes") + "\n\n")
if t.inForm {
formTitle := "Add Plane"
if t.isEdit {
formTitle = fmt.Sprintf("Edit Plane #%d", t.editID)
}
sb.WriteString(titleSt.Render(" "+formTitle) + "\n\n")
sb.WriteString(renderForm(t.fields, t.focusIdx))
sb.WriteString("\n " + renderStatus(t.status, t.isErr))
sb.WriteString("\n\n " + dimSt.Render(helpForm))
return sb.String()
}
header := fmt.Sprintf(" %-5s %-20s %-8s %-12s %-10s %-5s %-8s %s",
"ID", "Name", "Size", "Type", "Op", "Lvl", "Travel", "Buy$")
sb.WriteString(dimSt.Render(header) + "\n")
sb.WriteString(dimSt.Render(" "+strings.Repeat("─", 80)) + "\n")
if len(t.items) == 0 {
sb.WriteString(dimSt.Render(" (no planes — press a to add)") + "\n")
}
for i, p := range t.items {
travelFmt := fmt.Sprintf("%ds", p.TravelSec)
line := fmt.Sprintf(" %-5d %-20s %-8s %-12s %-10s %-5d %-8s %d",
p.ID, p.Name, p.Size, p.AircraftType, p.OperationType, p.MinLevel, travelFmt, p.BuyCost)
if i == t.cursor {
sb.WriteString(cursorSt.Render("▸") + activeTS.Render(line[1:]) + "\n")
} else {
sb.WriteString(line + "\n")
}
}
sb.WriteString("\n " + renderStatus(t.status, t.isErr))
sb.WriteString("\n\n " + dimSt.Render(helpList))
return sb.String()
}