package adminui import ( "fmt" "strconv" "strings" tea "github.com/charmbracelet/bubbletea" "skyrama-tui/internal/db" ) // ── Messages ────────────────────────────────────────────────────────────────── type userLoadedMsg []db.AdminUser type userDoneMsg struct{ err error } // ── Field indices ───────────────────────────────────────────────────────────── const ( ufUsername = 0 ufPassword = 1 ufAirportName = 2 ufCountryID = 3 ) // ── Model ───────────────────────────────────────────────────────────────────── type usersTab struct { client *db.AdminClient items []db.AdminUser cursor int inForm bool isEdit bool editUsername string fields []field focusIdx int status string isErr bool w, h int } func newUsersTab(client *db.AdminClient) *usersTab { return &usersTab{client: client} } func (t *usersTab) InForm() bool { return t.inForm } func (t *usersTab) Init() tea.Cmd { return t.load() } func (t *usersTab) load() tea.Cmd { return func() tea.Msg { items, err := t.client.ListUsers() if err != nil { return userDoneMsg{err} } return userLoadedMsg(items) } } func (t *usersTab) openForm() { t.fields = []field{ newTextField("Username", "e.g. alice"), newPasswordField("Password"), newTextField("Airport Name", "e.g. Alice's Airport"), newTextField("Country ID", "1"), } t.fields[ufUsername].input.Focus() t.focusIdx = ufUsername t.isEdit = false t.inForm = true t.status = "" } func (t *usersTab) openEditForm(u db.AdminUser) { f := newPasswordField("New Password") f.input.Focus() t.fields = []field{f} t.focusIdx = 0 t.isEdit = true t.editUsername = u.Username t.inForm = true t.status = "" } func (t *usersTab) submit() tea.Cmd { if t.isEdit { password := t.fields[0].value() if len(password) < 4 { t.status = "password must be at least 4 characters" t.isErr = true return nil } username := t.editUsername return func() tea.Msg { return userDoneMsg{t.client.UpdateUserPassword(username, password)} } } username := strings.TrimSpace(t.fields[ufUsername].value()) password := t.fields[ufPassword].value() airportName := strings.TrimSpace(t.fields[ufAirportName].value()) countryIDStr := strings.TrimSpace(t.fields[ufCountryID].value()) if username == "" { t.status = "username is required" t.isErr = true return nil } if len(password) < 4 { t.status = "password must be at least 4 characters" t.isErr = true return nil } if airportName == "" { t.status = "airport name is required" t.isErr = true return nil } countryID, err := strconv.Atoi(countryIDStr) if err != nil || countryID <= 0 { t.status = "country ID must be a positive number" t.isErr = true return nil } return func() tea.Msg { return userDoneMsg{t.client.CreateUserWithAirport(username, password, airportName, countryID)} } } func (t *usersTab) 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 userLoadedMsg: t.items = []db.AdminUser(m) if t.cursor >= len(t.items) && t.cursor > 0 { t.cursor = len(t.items) - 1 } return t, nil case userDoneMsg: 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 { username := t.items[t.cursor].Username return t, func() tea.Msg { return userDoneMsg{t.client.DeleteUser(username)} } } case "r": return t, t.load() } } return t, nil } func (t *usersTab) View() string { var sb strings.Builder sb.WriteString(hdrSt.Render(" Users") + "\n\n") if t.inForm { formTitle := "Add User + Airport" if t.isEdit { formTitle = fmt.Sprintf("Change Password — %s", t.editUsername) } 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 %-20s %s", "ID", "Username", "Created", "Status") sb.WriteString(dimSt.Render(header) + "\n") sb.WriteString(dimSt.Render(" "+strings.Repeat("─", 60)) + "\n") if len(t.items) == 0 { sb.WriteString(dimSt.Render(" (no users — press a to add)") + "\n") } for i, u := range t.items { status := okSt.Render("active") if u.Deleted { status = errSt.Render("deleted") } created := u.CreatedAt.Format("2006-01-02") line := fmt.Sprintf(" %-5d %-20s %-20s ", u.ID, u.Username, created) if i == t.cursor { sb.WriteString(cursorSt.Render("▸") + activeTS.Render(line[1:]) + status + "\n") } else { sb.WriteString(line + status + "\n") } } sb.WriteString("\n " + renderStatus(t.status, t.isErr)) sb.WriteString("\n\n " + dimSt.Render("a:add e:edit(pw) d:soft-delete j/k:nav r:reload q:quit")) return sb.String() }