334 lines
9 KiB
Go
334 lines
9 KiB
Go
package adminui
|
|
|
|
import (
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
|
|
tea "github.com/charmbracelet/bubbletea"
|
|
"skyrama-tui/internal/db"
|
|
)
|
|
|
|
// ── Messages ──────────────────────────────────────────────────────────────────
|
|
|
|
type planeLoadedMsg []db.AdminPlane
|
|
type planeDoneMsg struct{ err error }
|
|
|
|
// ── Field indices ─────────────────────────────────────────────────────────────
|
|
|
|
const (
|
|
pfName = 0
|
|
pfSize = 1
|
|
pfType = 2
|
|
pfOpType = 3
|
|
pfMinLevel = 4
|
|
pfTravelSec = 5
|
|
pfFuelCost = 6
|
|
pfCashCost = 7
|
|
pfMaxPax = 8
|
|
pfBuyCost = 9
|
|
pfXP = 10
|
|
pfCashRew = 11
|
|
pfCargoRew = 12
|
|
pfBoardingTime = 13
|
|
pfFuelingTime = 14
|
|
)
|
|
|
|
// ── Model ─────────────────────────────────────────────────────────────────────
|
|
|
|
type planesTab struct {
|
|
client *db.AdminClient
|
|
items []db.AdminPlane
|
|
cursor int
|
|
inForm bool
|
|
isEdit bool
|
|
editID uint32
|
|
fields []field
|
|
focusIdx int
|
|
status string
|
|
isErr bool
|
|
w, h int
|
|
}
|
|
|
|
func newPlanesTab(client *db.AdminClient) *planesTab {
|
|
return &planesTab{client: client}
|
|
}
|
|
|
|
func (t *planesTab) InForm() bool { return t.inForm }
|
|
func (t *planesTab) Init() tea.Cmd { return t.load() }
|
|
|
|
func (t *planesTab) load() tea.Cmd {
|
|
return func() tea.Msg {
|
|
items, err := t.client.ListPlanes()
|
|
if err != nil {
|
|
return planeDoneMsg{err}
|
|
}
|
|
return planeLoadedMsg(items)
|
|
}
|
|
}
|
|
|
|
func (t *planesTab) initFields() []field {
|
|
return []field{
|
|
newTextField("Name", "e.g. Cessna 172"),
|
|
newEnumField("Aircraft Size", []string{"small", "medium", "large", "huge"}),
|
|
newEnumField("Aircraft Type", []string{"plane", "helicopter", "seaplane", "spaceship"}),
|
|
newEnumField("Operation Type", []string{"passenger", "cargo"}),
|
|
newTextField("Min Level", "1"),
|
|
newTextField("Travel Time (sec)", "300"),
|
|
newTextField("Fuel Cost", "50"),
|
|
newTextField("Cash Cost", "10"),
|
|
newTextField("Max Passengers (0=null)", "0"),
|
|
newTextField("Buy Cost (cash)", "1000"),
|
|
newTextField("XP Reward", "20"),
|
|
newTextField("Cash Reward (0=null)", "50"),
|
|
newTextField("Cargo Reward (0=null)", "0"),
|
|
newTextField("Boarding Time (sec, 0=skip)", "0"),
|
|
newTextField("Fueling Time (sec, 0=skip)", "0"),
|
|
}
|
|
}
|
|
|
|
func (t *planesTab) openForm() {
|
|
t.fields = t.initFields()
|
|
t.fields[pfName].input.Focus()
|
|
t.focusIdx = pfName
|
|
t.isEdit = false
|
|
t.inForm = true
|
|
t.status = ""
|
|
}
|
|
|
|
func (t *planesTab) openEditForm(p db.AdminPlane) {
|
|
t.fields = t.initFields()
|
|
t.fields[pfName].input.SetValue(p.Name)
|
|
setEnumSel(&t.fields[pfSize], p.Size)
|
|
setEnumSel(&t.fields[pfType], p.AircraftType)
|
|
setEnumSel(&t.fields[pfOpType], p.OperationType)
|
|
t.fields[pfMinLevel].input.SetValue(strconv.Itoa(int(p.MinLevel)))
|
|
t.fields[pfTravelSec].input.SetValue(strconv.Itoa(int(p.TravelSec)))
|
|
t.fields[pfFuelCost].input.SetValue(strconv.Itoa(int(p.FuelCost)))
|
|
t.fields[pfCashCost].input.SetValue(strconv.Itoa(int(p.CashCost)))
|
|
t.fields[pfMaxPax].input.SetValue(strconv.Itoa(p.MaxPassengers))
|
|
t.fields[pfBuyCost].input.SetValue(strconv.Itoa(int(p.BuyCost)))
|
|
t.fields[pfXP].input.SetValue(strconv.Itoa(int(p.XP)))
|
|
t.fields[pfCashRew].input.SetValue(strconv.Itoa(p.CashReward))
|
|
t.fields[pfCargoRew].input.SetValue(strconv.Itoa(p.CargoReward))
|
|
t.fields[pfBoardingTime].input.SetValue(strconv.Itoa(int(p.BoardingTimeSec)))
|
|
t.fields[pfFuelingTime].input.SetValue(strconv.Itoa(int(p.FuelingTimeSec)))
|
|
t.fields[pfName].input.Focus()
|
|
t.focusIdx = pfName
|
|
t.isEdit = true
|
|
t.editID = p.ID
|
|
t.inForm = true
|
|
t.status = ""
|
|
}
|
|
|
|
func mustInt(s string) (int, error) {
|
|
return strconv.Atoi(strings.TrimSpace(s))
|
|
}
|
|
|
|
func (t *planesTab) submit() tea.Cmd {
|
|
name := strings.TrimSpace(t.fields[pfName].value())
|
|
if name == "" {
|
|
t.status = "name is required"
|
|
t.isErr = true
|
|
return nil
|
|
}
|
|
|
|
lvl, err := mustInt(t.fields[pfMinLevel].value())
|
|
if err != nil {
|
|
t.status = "min level must be a number"
|
|
t.isErr = true
|
|
return nil
|
|
}
|
|
travelSec, err := mustInt(t.fields[pfTravelSec].value())
|
|
if err != nil || travelSec <= 0 {
|
|
t.status = "travel time must be a positive number"
|
|
t.isErr = true
|
|
return nil
|
|
}
|
|
fuelCost, err := mustInt(t.fields[pfFuelCost].value())
|
|
if err != nil {
|
|
t.status = "fuel cost must be a number"
|
|
t.isErr = true
|
|
return nil
|
|
}
|
|
cashCost, err := mustInt(t.fields[pfCashCost].value())
|
|
if err != nil {
|
|
t.status = "cash cost must be a number"
|
|
t.isErr = true
|
|
return nil
|
|
}
|
|
maxPax, err := mustInt(t.fields[pfMaxPax].value())
|
|
if err != nil {
|
|
t.status = "max passengers must be a number (0 for none)"
|
|
t.isErr = true
|
|
return nil
|
|
}
|
|
buyCost, err := mustInt(t.fields[pfBuyCost].value())
|
|
if err != nil {
|
|
t.status = "buy cost must be a number"
|
|
t.isErr = true
|
|
return nil
|
|
}
|
|
xp, err := mustInt(t.fields[pfXP].value())
|
|
if err != nil {
|
|
t.status = "XP reward must be a number"
|
|
t.isErr = true
|
|
return nil
|
|
}
|
|
cashRew, err := mustInt(t.fields[pfCashRew].value())
|
|
if err != nil {
|
|
t.status = "cash reward must be a number (0 for none)"
|
|
t.isErr = true
|
|
return nil
|
|
}
|
|
cargoRew, err := mustInt(t.fields[pfCargoRew].value())
|
|
if err != nil {
|
|
t.status = "cargo reward must be a number (0 for none)"
|
|
t.isErr = true
|
|
return nil
|
|
}
|
|
boardingTime, err := mustInt(t.fields[pfBoardingTime].value())
|
|
if err != nil {
|
|
t.status = "boarding time must be a number"
|
|
t.isErr = true
|
|
return nil
|
|
}
|
|
fuelingTime, err := mustInt(t.fields[pfFuelingTime].value())
|
|
if err != nil {
|
|
t.status = "fueling time must be a number"
|
|
t.isErr = true
|
|
return nil
|
|
}
|
|
|
|
p := db.AddPlaneParams{
|
|
Name: name,
|
|
Size: t.fields[pfSize].value(),
|
|
AircraftType: t.fields[pfType].value(),
|
|
OperationType: t.fields[pfOpType].value(),
|
|
MinLevel: uint32(lvl),
|
|
FuelCost: uint32(fuelCost),
|
|
CashCost: uint32(cashCost),
|
|
MaxPassengers: maxPax,
|
|
TravelSec: uint32(travelSec),
|
|
BuyCost: uint32(buyCost),
|
|
XP: uint32(xp),
|
|
CashReward: cashRew,
|
|
CargoReward: cargoRew,
|
|
BoardingTimeSec: boardingTime,
|
|
FuelingTimeSec: fuelingTime,
|
|
}
|
|
if t.isEdit {
|
|
id := t.editID
|
|
return func() tea.Msg { return planeDoneMsg{t.client.UpdatePlane(id, p)} }
|
|
}
|
|
return func() tea.Msg { return planeDoneMsg{t.client.AddPlane(p)} }
|
|
}
|
|
|
|
func (t *planesTab) 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 planeLoadedMsg:
|
|
t.items = []db.AdminPlane(m)
|
|
if t.cursor >= len(t.items) && t.cursor > 0 {
|
|
t.cursor = len(t.items) - 1
|
|
}
|
|
return t, nil
|
|
|
|
case planeDoneMsg:
|
|
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 planeDoneMsg{t.client.DeletePlane(id)}
|
|
}
|
|
}
|
|
case "r":
|
|
return t, t.load()
|
|
}
|
|
}
|
|
return t, nil
|
|
}
|
|
|
|
func (t *planesTab) View() string {
|
|
var sb strings.Builder
|
|
sb.WriteString(hdrSt.Render(" Planes") + "\n\n")
|
|
|
|
if t.inForm {
|
|
formTitle := "Add Plane"
|
|
if t.isEdit {
|
|
formTitle = fmt.Sprintf("Edit Plane #%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 %-8s %-12s %-10s %-5s %-8s %s",
|
|
"ID", "Name", "Size", "Type", "Op", "Lvl", "Travel", "Buy$")
|
|
sb.WriteString(dimSt.Render(header) + "\n")
|
|
sb.WriteString(dimSt.Render(" "+strings.Repeat("─", 80)) + "\n")
|
|
|
|
if len(t.items) == 0 {
|
|
sb.WriteString(dimSt.Render(" (no planes — press a to add)") + "\n")
|
|
}
|
|
for i, p := range t.items {
|
|
travelFmt := fmt.Sprintf("%ds", p.TravelSec)
|
|
line := fmt.Sprintf(" %-5d %-20s %-8s %-12s %-10s %-5d %-8s %d",
|
|
p.ID, p.Name, p.Size, p.AircraftType, p.OperationType, p.MinLevel, travelFmt, p.BuyCost)
|
|
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(helpList))
|
|
return sb.String()
|
|
}
|