skyrama_tui/internal/adminui/airports.go

275 lines
7.3 KiB
Go
Raw Permalink Normal View History

2026-06-04 20:51:20 +03:00
package adminui
import (
"fmt"
"strconv"
"strings"
"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
"skyrama-tui/internal/db"
)
// ── Messages ──────────────────────────────────────────────────────────────────
type airportLoadedMsg []db.AdminAirport
type airportDoneMsg struct{ err error }
// ── Action mode ───────────────────────────────────────────────────────────────
type airportAction int
const (
airportActionNone airportAction = iota
airportActionGiveCash
airportActionGiveBuilding
)
// ── Model ─────────────────────────────────────────────────────────────────────
// field indices for airport edit form
const (
aeqCash = 0
aeqLevel = 1
aeqMaxFuel = 2
aeqMaxPax = 3
)
type airportsTab struct {
client *db.AdminClient
items []db.AdminAirport
cursor int
// simple single-input actions (give cash / give building)
action airportAction
input textinput.Model
// full edit form
inForm bool
editID uint32
fields []field
focusIdx int
status string
isErr bool
w, h int
}
func newAirportsTab(client *db.AdminClient) *airportsTab {
ti := textinput.New()
ti.CharLimit = 20
return &airportsTab{client: client, input: ti}
}
func (t *airportsTab) InForm() bool { return t.action != airportActionNone || t.inForm }
func (t *airportsTab) Init() tea.Cmd { return t.load() }
func (t *airportsTab) load() tea.Cmd {
return func() tea.Msg {
items, err := t.client.ListAirports()
if err != nil {
return airportDoneMsg{err}
}
return airportLoadedMsg(items)
}
}
func (t *airportsTab) openEditForm(a db.AdminAirport) {
t.fields = []field{
prefill("Cash (set)", strconv.FormatUint(a.Cash, 10)),
prefill("Player Level", strconv.Itoa(int(a.Level))),
prefill("Max Fuel Capacity", strconv.Itoa(int(a.MaxFuel))),
prefill("Max Passenger Cap", strconv.Itoa(int(a.MaxPax))),
}
t.fields[aeqCash].input.Focus()
t.focusIdx = aeqCash
t.editID = a.ID
t.inForm = true
t.status = ""
}
func (t *airportsTab) submitEdit() tea.Cmd {
cash, e1 := strconv.ParseUint(strings.TrimSpace(t.fields[aeqCash].value()), 10, 64)
level, e2 := strconv.ParseUint(strings.TrimSpace(t.fields[aeqLevel].value()), 10, 32)
maxFuel, e3 := strconv.ParseUint(strings.TrimSpace(t.fields[aeqMaxFuel].value()), 10, 32)
maxPax, e4 := strconv.ParseUint(strings.TrimSpace(t.fields[aeqMaxPax].value()), 10, 32)
if e1 != nil || e2 != nil || e3 != nil || e4 != nil {
t.status = "all fields must be valid numbers"
t.isErr = true
return nil
}
id := t.editID
return func() tea.Msg {
return airportDoneMsg{t.client.UpdateAirport(id, cash, uint32(level), uint32(maxFuel), uint32(maxPax))}
}
}
func (t *airportsTab) openAction(a airportAction, placeholder string) {
t.action = a
t.input.Reset()
t.input.Placeholder = placeholder
t.input.Focus()
t.status = ""
}
func (t *airportsTab) submitAction() tea.Cmd {
if len(t.items) == 0 {
return nil
}
airport := t.items[t.cursor]
raw := strings.TrimSpace(t.input.Value())
switch t.action {
case airportActionGiveCash:
amount, err := strconv.ParseUint(raw, 10, 64)
if err != nil || amount == 0 {
t.status = "enter a positive cash amount"
t.isErr = true
return nil
}
id := airport.ID
return func() tea.Msg {
return airportDoneMsg{t.client.GiveAirportCash(id, amount)}
}
case airportActionGiveBuilding:
bID, err := strconv.ParseUint(raw, 10, 32)
if err != nil || bID == 0 {
t.status = "enter a valid building ID"
t.isErr = true
return nil
}
id := airport.ID
return func() tea.Msg {
return airportDoneMsg{t.client.GiveAirportBuilding(id, uint32(bID))}
}
}
return nil
}
func (t *airportsTab) 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 airportLoadedMsg:
t.items = []db.AdminAirport(m)
if t.cursor >= len(t.items) && t.cursor > 0 {
t.cursor = len(t.items) - 1
}
return t, nil
case airportDoneMsg:
if m.err != nil {
t.status = m.err.Error()
t.isErr = true
return t, nil
}
t.action = airportActionNone
t.inForm = false
t.status = "done"
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.submitEdit()
default:
var cmd tea.Cmd
t.focusIdx, cmd = handleFormKey(t.fields, t.focusIdx, m)
return t, cmd
}
return t, nil
}
if t.action != airportActionNone {
switch m.String() {
case "esc":
t.action = airportActionNone
t.status = ""
case "enter", "ctrl+s":
return t, t.submitAction()
default:
var cmd tea.Cmd
t.input, cmd = t.input.Update(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 "e":
if len(t.items) > 0 {
t.openEditForm(t.items[t.cursor])
}
case "g":
t.openAction(airportActionGiveCash, "amount, e.g. 5000")
case "b":
t.openAction(airportActionGiveBuilding, "building ID, e.g. 3")
case "r":
return t, t.load()
}
}
return t, nil
}
func (t *airportsTab) View() string {
var sb strings.Builder
sb.WriteString(hdrSt.Render(" Airports") + "\n\n")
if t.inForm {
sb.WriteString(titleSt.Render(fmt.Sprintf(" Edit Airport #%d", t.editID)) + "\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()
}
if t.action != airportActionNone {
if len(t.items) > 0 {
ap := t.items[t.cursor]
sb.WriteString(fmt.Sprintf(" Selected: %s (owner: %s, cash: %d)\n\n",
titleSt.Render(ap.Name), ap.Username, ap.Cash))
}
label := "Give Cash Amount:"
if t.action == airportActionGiveBuilding {
label = "Building ID to add:"
}
sb.WriteString(" " + labelSt.Render(label) + " " + t.input.View() + "\n")
sb.WriteString("\n " + renderStatus(t.status, t.isErr))
sb.WriteString("\n\n " + dimSt.Render("enter:confirm esc:cancel"))
return sb.String()
}
header := fmt.Sprintf(" %-5s %-20s %-16s %-12s %-8s %s",
"ID", "Airport Name", "Owner", "Cash", "Fuel", "Lvl")
sb.WriteString(dimSt.Render(header) + "\n")
sb.WriteString(dimSt.Render(" "+strings.Repeat("─", 70)) + "\n")
if len(t.items) == 0 {
sb.WriteString(dimSt.Render(" (no airports)") + "\n")
}
for i, a := range t.items {
line := fmt.Sprintf(" %-5d %-20s %-16s %-12d %-8d %d",
a.ID, a.Name, a.Username, a.Cash, a.Fuel, a.Level)
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("e:edit g:give cash b:give building j/k:nav r:reload q:quit"))
return sb.String()
}