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() }