package adminui import ( "fmt" "strconv" "strings" tea "github.com/charmbracelet/bubbletea" "skyrama-tui/internal/db" ) // ── Messages ────────────────────────────────────────────────────────────────── type cntryLoadedMsg []db.AdminCountry type cntryDoneMsg struct{ err error } // ── Field indices ───────────────────────────────────────────────────────────── const ( cfName = 0 cfContinent = 1 cfP1 = 2 cfP2 = 3 cfP3 = 4 ) var continentOptions = []string{ "void", "Africa", "Antarctica", "Asia", "Europe", "North America", "Oceania", "South America", } // ── Model ───────────────────────────────────────────────────────────────────── type countriesTab struct { client *db.AdminClient items []db.AdminCountry cursor int inForm bool isEdit bool editID uint16 fields []field focusIdx int status string isErr bool w, h int } func newCountriesTab(client *db.AdminClient) *countriesTab { return &countriesTab{client: client} } func (t *countriesTab) InForm() bool { return t.inForm } func (t *countriesTab) Init() tea.Cmd { return t.load() } func (t *countriesTab) load() tea.Cmd { return func() tea.Msg { items, err := t.client.ListCountries() if err != nil { return cntryDoneMsg{err} } return cntryLoadedMsg(items) } } func (t *countriesTab) openForm() { t.fields = []field{ newTextField("Name", "e.g. Turkey"), newEnumField("Continent", continentOptions), newTextField("Product 1 ID", "1"), newTextField("Product 2 ID", "2"), newTextField("Product 3 ID", "3"), } t.fields[cfName].input.Focus() t.focusIdx = cfName t.isEdit = false t.inForm = true t.status = "" } func (t *countriesTab) openEditForm(c db.AdminCountry) { t.fields = []field{ prefill("Name", c.Name), newEnumField("Continent", continentOptions), prefill("Product 1 ID", strconv.Itoa(int(c.P1))), prefill("Product 2 ID", strconv.Itoa(int(c.P2))), prefill("Product 3 ID", strconv.Itoa(int(c.P3))), } setEnumSel(&t.fields[cfContinent], c.Continent) t.fields[cfName].input.Focus() t.focusIdx = cfName t.isEdit = true t.editID = c.ID t.inForm = true t.status = "" } func (t *countriesTab) submit() tea.Cmd { name := strings.TrimSpace(t.fields[cfName].value()) continent := t.fields[cfContinent].value() p1s := strings.TrimSpace(t.fields[cfP1].value()) p2s := strings.TrimSpace(t.fields[cfP2].value()) p3s := strings.TrimSpace(t.fields[cfP3].value()) if name == "" { t.status = "name is required" t.isErr = true return nil } p1, e1 := strconv.ParseUint(p1s, 10, 16) p2, e2 := strconv.ParseUint(p2s, 10, 16) p3, e3 := strconv.ParseUint(p3s, 10, 16) if e1 != nil || e2 != nil || e3 != nil { t.status = "product IDs must be valid numbers" t.isErr = true return nil } if t.isEdit { id := t.editID return func() tea.Msg { return cntryDoneMsg{t.client.UpdateCountry(id, name, continent, uint16(p1), uint16(p2), uint16(p3))} } } return func() tea.Msg { return cntryDoneMsg{t.client.AddCountry(name, continent, uint16(p1), uint16(p2), uint16(p3))} } } func (t *countriesTab) 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 cntryLoadedMsg: t.items = []db.AdminCountry(m) if t.cursor >= len(t.items) && t.cursor > 0 { t.cursor = len(t.items) - 1 } return t, nil case cntryDoneMsg: 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 "r": return t, t.load() } } return t, nil } func (t *countriesTab) View() string { var sb strings.Builder sb.WriteString(hdrSt.Render(" Countries") + "\n\n") if t.inForm { formTitle := "Add Country" if t.isEdit { formTitle = fmt.Sprintf("Edit Country #%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 %-14s %s", "ID", "Name", "Continent", "Products (P1/P2/P3)") sb.WriteString(dimSt.Render(header) + "\n") sb.WriteString(dimSt.Render(" " + strings.Repeat("─", 60)) + "\n") if len(t.items) == 0 { sb.WriteString(dimSt.Render(" (no countries)") + "\n") } for i, c := range t.items { line := fmt.Sprintf(" %-5d %-20s %-14s %d / %d / %d", c.ID, c.Name, c.Continent, c.P1, c.P2, c.P3) 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 j/k:nav r:reload q:quit")) return sb.String() }