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