238 lines
5.9 KiB
Go
238 lines
5.9 KiB
Go
|
|
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()
|
||
|
|
}
|