package adminui import ( "fmt" "strconv" "strings" tea "github.com/charmbracelet/bubbletea" "skyrama-tui/internal/db" ) // ── Messages ────────────────────────────────────────────────────────────────── type factionLoadedMsg []db.AdminFaction type factionDoneMsg struct{ err error } // ── Field indices ───────────────────────────────────────────────────────────── const ( ffName = 0 ffContinent = 1 ffLeader = 2 ffBackground = 3 ffUser = 4 ffAirport = 5 ) // ── Model ───────────────────────────────────────────────────────────────────── type factionsTab struct { client *db.AdminClient items []db.AdminFaction cursor int inForm bool isEdit bool editID uint32 fields []field focusIdx int status string isErr bool w, h int } func newFactionsTab(client *db.AdminClient) *factionsTab { return &factionsTab{client: client} } func (t *factionsTab) InForm() bool { return t.inForm } func (t *factionsTab) Init() tea.Cmd { return t.load() } func (t *factionsTab) load() tea.Cmd { return func() tea.Msg { items, err := t.client.ListFactions() if err != nil { return factionDoneMsg{err} } return factionLoadedMsg(items) } } func (t *factionsTab) openForm() { t.fields = []field{ newTextField("Name", "e.g. Monsoon Alliance"), newEnumField("Continent", continentOptions), newTextField("Leader", "e.g. Mei-Lin Zhao"), newTextField("Background", "short bio"), newTextField("Owner User ID", "NPC user id"), newTextField("Central Airport ID", "central airport id"), } t.fields[ffName].input.Focus() t.focusIdx = ffName t.isEdit = false t.inForm = true t.status = "" } func (t *factionsTab) openEditForm(f db.AdminFaction) { t.fields = []field{ prefill("Name", f.Name), newEnumField("Continent", continentOptions), prefill("Leader", f.Leader), prefill("Background", f.Background), prefill("Owner User ID", strconv.Itoa(int(f.UserID))), prefill("Central Airport ID", strconv.Itoa(int(f.CentralAirportID))), } setEnumSel(&t.fields[ffContinent], f.Continent) t.fields[ffName].input.Focus() t.focusIdx = ffName t.isEdit = true t.editID = f.ID t.inForm = true t.status = "" } func (t *factionsTab) submit() tea.Cmd { name := strings.TrimSpace(t.fields[ffName].value()) continent := t.fields[ffContinent].value() leader := strings.TrimSpace(t.fields[ffLeader].value()) background := strings.TrimSpace(t.fields[ffBackground].value()) userStr := strings.TrimSpace(t.fields[ffUser].value()) airportStr := strings.TrimSpace(t.fields[ffAirport].value()) if name == "" || leader == "" { t.status = "name and leader are required" t.isErr = true return nil } userID, e1 := strconv.ParseInt(userStr, 10, 32) airportID, e2 := strconv.ParseUint(airportStr, 10, 32) if e1 != nil || e2 != nil { t.status = "owner user ID and central airport ID must be valid numbers" t.isErr = true return nil } if t.isEdit { id := t.editID return func() tea.Msg { return factionDoneMsg{t.client.UpdateFaction(id, name, continent, leader, background, int32(userID), uint32(airportID))} } } return func() tea.Msg { return factionDoneMsg{t.client.AddFaction(name, continent, leader, background, int32(userID), uint32(airportID))} } } func (t *factionsTab) 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 factionLoadedMsg: t.items = []db.AdminFaction(m) if t.cursor >= len(t.items) && t.cursor > 0 { t.cursor = len(t.items) - 1 } return t, nil case factionDoneMsg: 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 factionDoneMsg{t.client.DeleteFaction(id)} } } case "r": return t, t.load() } } return t, nil } func (t *factionsTab) View() string { var sb strings.Builder sb.WriteString(hdrSt.Render(" Factions") + "\n\n") if t.inForm { formTitle := "Add Faction" if t.isEdit { formTitle = fmt.Sprintf("Edit Faction #%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(" %-4s %-14s %-22s %-18s %-7s %s", "ID", "Continent", "Faction", "Leader", "UserID", "AirportID") sb.WriteString(dimSt.Render(header) + "\n") sb.WriteString(dimSt.Render(" "+strings.Repeat("─", 78)) + "\n") if len(t.items) == 0 { sb.WriteString(dimSt.Render(" (no factions — run sql/seed_factions.sql)") + "\n") } for i, f := range t.items { line := fmt.Sprintf(" %-4d %-14s %-22s %-18s %-7d %d", f.ID, f.Continent, f.Name, f.Leader, f.UserID, f.CentralAirportID) 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("a:add e:edit d:delete j/k:nav r:reload q:quit")) return sb.String() }