232 lines
6.2 KiB
Go
232 lines
6.2 KiB
Go
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()
|
|
}
|