package adminui import ( "fmt" "strconv" "strings" tea "github.com/charmbracelet/bubbletea" "skyrama-tui/internal/db" ) // ── Messages ────────────────────────────────────────────────────────────────── type bldgLoadedMsg []db.AdminBuilding type bldgDoneMsg struct{ err error } // ── Field indices ───────────────────────────────────────────────────────────── const ( bfName = 0 bfMinLevel = 1 bfCost = 2 bfType = 3 // storage bfCapacity = 4 bfStorType = 5 bfStorSubtype = 6 // operation bfOpType = 7 bfOpSize = 8 // passive bfPassiveType = 9 bfPassiveRate = 10 ) // ── Model ───────────────────────────────────────────────────────────────────── type buildingsTab struct { client *db.AdminClient items []db.AdminBuilding cursor int inForm bool isEdit bool editID uint32 fields []field focusIdx int status string isErr bool w, h int } func newBuildingsTab(client *db.AdminClient) *buildingsTab { return &buildingsTab{client: client} } func (t *buildingsTab) InForm() bool { return t.inForm } func (t *buildingsTab) Init() tea.Cmd { return t.load() } func (t *buildingsTab) load() tea.Cmd { return func() tea.Msg { items, err := t.client.ListBuildings() if err != nil { return bldgDoneMsg{err} } return bldgLoadedMsg(items) } } func (t *buildingsTab) initFields() { t.fields = []field{ newTextField("Name", "e.g. Small Hangar"), newTextField("Min Level", "1"), newTextField("Cost (cash)", "100"), newEnumField("Type", []string{"none", "storage", "operation", "passive"}), // storage newTextField("Capacity", "10"), newEnumField("Storage Type", []string{"fuel", "plane", "product"}), newEnumField("Storage Subtype", []string{"none", "airplane", "helicopter", "seaplane", "spaceship"}), // operation newEnumField("Op Type", []string{"runway", "service", "production"}), newEnumField("Op Size", []string{"small", "medium", "large", "huge"}), // passive newEnumField("Passive Type", []string{"passengers", "fuel"}), newTextField("Passive Rate", "10"), } } func (t *buildingsTab) openForm() { t.initFields() t.fields[bfName].input.Focus() t.focusIdx = bfName t.isEdit = false t.updateHidden() t.inForm = true t.status = "" } func (t *buildingsTab) openEditForm(b db.AdminBuilding) { t.initFields() t.fields[bfName].input.SetValue(b.Name) t.fields[bfMinLevel].input.SetValue(strconv.Itoa(int(b.MinLevel))) t.fields[bfCost].input.SetValue(strconv.Itoa(int(b.Cost))) setEnumSel(&t.fields[bfType], b.BuildingType) t.fields[bfCapacity].input.SetValue(strconv.Itoa(b.Capacity)) setEnumSel(&t.fields[bfStorType], b.StorageType) setEnumSel(&t.fields[bfStorSubtype], b.StorageSubtype) setEnumSel(&t.fields[bfOpType], b.OperationType) setEnumSel(&t.fields[bfOpSize], b.OperationSize) setEnumSel(&t.fields[bfPassiveType], b.PassiveType) t.fields[bfPassiveRate].input.SetValue(strconv.Itoa(b.PassiveRate)) t.fields[bfName].input.Focus() t.focusIdx = bfName t.isEdit = true t.editID = b.ID t.updateHidden() t.inForm = true t.status = "" } // updateHidden shows/hides type-specific fields based on the type enum selection. func (t *buildingsTab) updateHidden() { btype := t.fields[bfType].options[t.fields[bfType].sel] for i := range t.fields { switch i { case bfCapacity, bfStorType, bfStorSubtype: t.fields[i].hidden = btype != "storage" case bfOpType, bfOpSize: t.fields[i].hidden = btype != "operation" case bfPassiveType, bfPassiveRate: t.fields[i].hidden = btype != "passive" } } } func (t *buildingsTab) submit() tea.Cmd { name := strings.TrimSpace(t.fields[bfName].value()) if name == "" { t.status = "name is required" t.isErr = true return nil } lvl, err := strconv.ParseUint(strings.TrimSpace(t.fields[bfMinLevel].value()), 10, 32) if err != nil { t.status = "min level must be a number" t.isErr = true return nil } cost, err := strconv.ParseUint(strings.TrimSpace(t.fields[bfCost].value()), 10, 32) if err != nil { t.status = "cost must be a number" t.isErr = true return nil } btype := t.fields[bfType].value() p := db.AddBuildingParams{ Name: name, MinLevel: uint32(lvl), Cost: uint32(cost), BuildingType: btype, } switch btype { case "storage": cap, err := strconv.Atoi(strings.TrimSpace(t.fields[bfCapacity].value())) if err != nil || cap <= 0 { t.status = "capacity must be a positive number" t.isErr = true return nil } p.Capacity = cap p.StorageType = t.fields[bfStorType].value() p.StorageSubtype = t.fields[bfStorSubtype].value() case "operation": p.OperationType = t.fields[bfOpType].value() p.OperationSize = t.fields[bfOpSize].value() case "passive": rate, err := strconv.Atoi(strings.TrimSpace(t.fields[bfPassiveRate].value())) if err != nil { t.status = "passive rate must be a number" t.isErr = true return nil } p.PassiveType = t.fields[bfPassiveType].value() p.PassiveRate = rate } if t.isEdit { id := t.editID return func() tea.Msg { return bldgDoneMsg{t.client.UpdateBuilding(id, p)} } } return func() tea.Msg { return bldgDoneMsg{t.client.AddBuilding(p)} } } func (t *buildingsTab) 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 bldgLoadedMsg: t.items = []db.AdminBuilding(m) if t.cursor >= len(t.items) && t.cursor > 0 { t.cursor = len(t.items) - 1 } return t, nil case bldgDoneMsg: 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: prevType := t.fields[bfType].sel var cmd tea.Cmd t.focusIdx, cmd = handleFormKey(t.fields, t.focusIdx, m) // If type enum changed, update field visibility if t.fields[bfType].sel != prevType { t.updateHidden() // If current focus is now hidden, jump to next visible if t.focusIdx < len(t.fields) && t.fields[t.focusIdx].hidden { t.focusIdx = nextVisible(t.fields, t.focusIdx) applyFocus(t.fields, t.focusIdx) } } 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 bldgDoneMsg{t.client.DeleteBuilding(id)} } } case "r": return t, t.load() } } return t, nil } func (t *buildingsTab) View() string { var sb strings.Builder sb.WriteString(hdrSt.Render(" Buildings") + "\n\n") if t.inForm { formTitle := "Add Building" if t.isEdit { formTitle = fmt.Sprintf("Edit Building #%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 %-24s %-12s %-8s %s", "ID", "Name", "Type", "Cost", "Details") sb.WriteString(dimSt.Render(header) + "\n") sb.WriteString(dimSt.Render(" "+strings.Repeat("─", 65)) + "\n") if len(t.items) == 0 { sb.WriteString(dimSt.Render(" (no buildings — press a to add)") + "\n") } for i, b := range t.items { line := fmt.Sprintf(" %-5d %-24s %-12s %-8d %s", b.ID, b.Name, b.BuildingType, b.Cost, b.Details) 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() }