Make great

This commit is contained in:
Justin Hawkins 2025-06-18 11:06:12 +02:00
parent 77d11c4351
commit 682c04ca79
3 changed files with 206 additions and 85 deletions

View File

@ -1,6 +1,7 @@
package mite
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
@ -219,6 +220,64 @@ func (a APIClient) GetTimeEntries(from, to time.Time) (TimeEntries, error) {
return out, nil
}
type requestAddTimeEntry struct {
RequestTimeEntryHolder struct {
DateAt string `json:"date_at"`
Minutes int `json:"minutes"`
ProjectID int `json:"project_id,omit_empty"`
ServiceID int `json:"service_id,omit_empty"`
Note string `json:"note"`
} `json:"time_entry"`
}
func (a APIClient) AddTimeEntry(date string, minutes int, notes string, projectId, serviceId int) error {
// POST /time_entries.json
// {
// "time_entry": {
// "date_at": "2015-9-15",
// "minutes": 185,
// "service_id": 243
// }
// }
req := requestAddTimeEntry{}
req.RequestTimeEntryHolder.DateAt = date
req.RequestTimeEntryHolder.Minutes = minutes
req.RequestTimeEntryHolder.Note = notes
req.RequestTimeEntryHolder.ProjectID = projectId
req.RequestTimeEntryHolder.ServiceID = serviceId
err := post(a.domain, a.apiKey, "/time_entries.json", req)
return err
}
func post(domain, apiKey, path string, data any) error {
b, err := json.Marshal(data)
if err != nil {
return err
}
req, err := http.NewRequest("POST", baseurl(domain, path), bytes.NewBuffer(b))
if err != nil {
return err
}
req.Header.Add("X-MiteApiKey", apiKey)
req.Header.Add("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode >= 300 {
return fmt.Errorf("expected 2XX, got %d", resp.StatusCode)
}
return nil
}
func get(domain, apiKey, path string, data any) error {
req, err := http.NewRequest("GET", baseurl(domain, path), nil)
if err != nil {

175
model.go
View File

@ -24,15 +24,22 @@ import (
var styleLocked = lipgloss.NewStyle().Foreground(lipgloss.Color("#666666")) // red
var styleTime = lipgloss.NewStyle().Foreground(lipgloss.Color("#22ff4d")) // green
var styleStatusBar = lipgloss.NewStyle().Background(lipgloss.Color("#00dda1")).Foreground(lipgloss.Color("#555555"))
type tuiMode string
const MODE_CAL tuiMode = "MODE_CAL"
const MODE_TIMEENTRIES tuiMode = "MODE_TIMEENTRIES"
const MODE_FORMENTRY tuiMode = "MODE_FORMENTRY"
type model struct {
miteAPI mite.APIClient
start calendarTime
dest calendarTime
dataFetchStatus string
focus string
debug []string
formData struct {
miteAPI mite.APIClient
start calendarTime
dest calendarTime
fetchedData bool // done initial fetch to get customers/projects etc
tuiMode tuiMode //
debug []string
formData struct {
form *huh.Form
customers mite.Customers
services mite.Services
@ -45,7 +52,8 @@ type model struct {
entries mite.TimeEntries
table table.Model
}
windowWidth int
statusBarMessage string
windowWidth int
}
func initialModel(miteDomain, miteApiKey string) model {
@ -74,12 +82,12 @@ func initialModel(miteDomain, miteApiKey string) model {
m.start = calendarTime{time.Now()}
m.dest = calendarTime{time.Now()}
m.debug = make([]string, 5, 5)
m.focus = "cal"
m.tuiMode = MODE_CAL
// m.formData.form = form
m.timeData.table = tab
m.dataFetchStatus = "not fetched"
m.statusBarMessage = "Fetching data from API"
return m
}
@ -138,11 +146,11 @@ func (m model) buildForm() *huh.Form {
return nil
}),
huh.NewInput().
Key("hours").
Key("minutes").
CharLimit(5).
Validate(
func(s string) error {
h, err := strconv.ParseFloat(s, 10)
h, err := strconv.ParseInt(s, 10, 64)
if err != nil {
return err
}
@ -151,7 +159,7 @@ func (m model) buildForm() *huh.Form {
}
return err
}).
Title("Hours"),
Title("Minutes"),
),
)
return form
@ -170,19 +178,42 @@ func (m model) Init() tea.Cmd {
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
// things we process no matter the tuiMode
switch msg := msg.(type) {
case miteDataFetchedMsg:
if msg.Error != nil {
m.statusBarMessage = fmt.Sprintf("Error fetching: %s", msg.Error.Error())
m.fetchedData = false
} else {
m.statusBarMessage = fmt.Sprintf("Fetched %d time entries", len(msg.TimeEntries))
m.timeData.entries = msg.TimeEntries
m.formData.customers = msg.Customers
m.formData.services = msg.Services
m.formData.projects = msg.Projects
m.fetchedData = true
// just in case there is data for the currently focused day
m.timeData.table.SetRows(m.tableDataForDate(m.dest.Time))
}
return m, nil
}
if len(m.debug) > 5 {
m.debug = m.debug[len(m.debug)-5:]
}
m.debug = append(m.debug, fmt.Sprintf("Got a %#v", msg))
if m.focus == "entries" {
if m.tuiMode == MODE_TIMEENTRIES {
return m.updateEntries(msg)
} else if m.focus == "cal" {
} else if m.tuiMode == MODE_CAL {
return m.updateCal(msg)
} else if m.focus == "addform" {
} else if m.tuiMode == MODE_FORMENTRY {
return m.updateForm(msg)
} else {
panic(m.focus)
panic(m.tuiMode)
}
}
@ -211,29 +242,8 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
// return m, cmd
// }
func (m model) updateForm(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
}
}
newForm, formCmd := m.formData.form.Update(msg)
m.formData.form = newForm.(*huh.Form)
if m.formData.form.State == huh.StateCompleted {
fmt.Printf("%#v", m.timeData.entries)
println(fmt.Sprintf(m.formData.form.GetString("client")))
println(fmt.Sprintf(m.formData.form.GetString("service")))
println(fmt.Sprintf(m.formData.form.GetString("project")))
println(fmt.Sprintf(m.formData.form.GetString("description")))
println(fmt.Sprintf(m.formData.form.GetString("hours")))
panic("You did it!")
}
return m, formCmd
}
// updateEntries handles processing tea messages when the days time entries are being
// processed
func (m model) updateEntries(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
@ -241,13 +251,13 @@ func (m model) updateEntries(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg.String() {
case "tab":
m.focus = "cal"
m.tuiMode = MODE_CAL
m.timeData.table.Blur()
return m, nil
case "ctrl+c", "q":
return m, tea.Quit
case "a":
m.focus = "addform"
m.tuiMode = MODE_FORMENTRY
m.formData.form = m.buildForm()
m.formData.form.Init()
return m, nil
@ -266,34 +276,23 @@ func (m model) updateCal(msg tea.Msg) (tea.Model, tea.Cmd) {
case tea.WindowSizeMsg:
m.windowWidth = msg.Width
case miteDataFetchedMsg:
if msg.Error != nil {
m.dataFetchStatus = "error: " + msg.Error.Error()
} else {
m.dataFetchStatus = "fetched"
m.timeData.entries = msg.TimeEntries
m.formData.customers = msg.Customers
m.formData.services = msg.Services
m.formData.projects = msg.Projects
// just in case there is data for the currently focused day
m.timeData.table.SetRows(m.tableDataForDate(m.dest.Time))
}
return m, nil
// Is it a key press?
case tea.KeyMsg:
// Cool, what was the actual key pressed?
switch msg.String() {
case "a":
m.focus = "addform"
m.formData.form = m.buildForm()
m.formData.form.Init()
if m.fetchedData {
m.tuiMode = MODE_FORMENTRY
m.formData.form = m.buildForm()
m.formData.form.Init()
} else {
m.statusBarMessage = "Not yet fetched data"
}
return m, nil
case "tab":
m.focus = "entries"
m.tuiMode = MODE_TIMEENTRIES
m.timeData.table.Focus()
m.timeData.table.SetCursor(0)
return m, nil
@ -304,7 +303,7 @@ func (m model) updateCal(msg tea.Msg) (tea.Model, tea.Cmd) {
// time entries
case "f":
m.dataFetchStatus = "fetching"
m.statusBarMessage = "Fetching data from API"
return m, m.fetchMiteData()
// // The "up" and "k" keys move the cursor up
@ -392,17 +391,14 @@ type miteDataFetchedMsg struct {
func (m model) fetchMiteData() tea.Cmd {
return func() tea.Msg {
time.Sleep(time.Second * 1)
// time.Sleep(time.Second * 5)
loc := time.Local
te, err1 := m.miteAPI.GetTimeEntries(
time.Date(2025, time.January, 1, 0, 0, 0, 0, loc),
time.Date(2024, time.January, 1, 0, 0, 0, 0, loc),
time.Date(2025, time.December, 31, 0, 0, 0, 0, loc),
)
cst, err2 := m.miteAPI.GetCustomers()
svc, err3 := m.miteAPI.GetServices()
pjt, err4 := m.miteAPI.GetProjects()
return miteDataFetchedMsg{
@ -440,25 +436,34 @@ func (m model) View() string {
lhs.WriteString(calendar(m.dest, m.start, m.dest, styles))
lhs.WriteString("nav: ←↑→↓[] (t)oday\n")
lhs.WriteString("(f)etch time data\n")
lhs.WriteString("(q)uit\n")
if m.tuiMode == MODE_CAL {
lhs.WriteString("(f)etch time data\n")
} else {
lhs.WriteString("\n")
}
if m.tuiMode != MODE_FORMENTRY {
lhs.WriteString("(a)dd time entry\n")
} else {
lhs.WriteString("\n")
}
if m.tuiMode == MODE_FORMENTRY {
lhs.WriteString("(esc) abort form\n")
lhs.WriteString("\n")
} else {
lhs.WriteString("(tab) switch panes\n")
lhs.WriteString("(q)uit\n")
}
calendarWidth := 25
tableWidth := m.windowWidth - calendarWidth
if m.dataFetchStatus != "fetched" {
rhs.WriteString("fetching mite data...")
if m.tuiMode == MODE_FORMENTRY {
rhs.WriteString(m.formData.form.View())
} else {
if m.focus == "addform" {
rhs.WriteString(m.formData.form.View())
} else {
if m.dataFetchStatus == "fetched" {
rhs.WriteString(m.timeData.table.View())
rhs.WriteString("\n")
} else {
panic("bad fetchstatus " + m.dataFetchStatus)
}
if m.fetchedData {
rhs.WriteString(m.timeData.table.View())
rhs.WriteString("\n")
}
}
@ -468,10 +473,10 @@ func (m model) View() string {
lipgloss.NewStyle().Width(tableWidth).Render(rhs.String()),
)
// if len(m.debug) > 5 {
// m.debug = m.debug[len(m.debug)-5:]
// }
sofar := lipgloss.Height(out)
out +=
styleStatusBar.MarginTop(19 - sofar).Width(m.windowWidth).Render(m.statusBarMessage)
// out += "\n\n" + strings.Join(m.debug, "\n")
return out
}

57
model_form.go Normal file
View File

@ -0,0 +1,57 @@
package main
import (
"errors"
"strconv"
"time"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/huh"
)
// updateForm handles processing tea.Msg's related to form processing
func (m model) updateForm(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "esc":
m.tuiMode = MODE_CAL
return m, nil
}
}
newForm, formCmd := m.formData.form.Update(msg)
m.formData.form = newForm.(*huh.Form)
if m.formData.form.State == huh.StateCompleted {
// fmt.Printf("%#v", m.timeData.entries)
// clientID := m.formData.form.GetString("client")
serviceID := m.formData.form.GetString("service")
projectID := m.formData.form.GetString("project")
description := m.formData.form.GetString("description")
minutes := m.formData.form.GetString("minutes")
minutesInt, err1 := strconv.ParseInt(minutes, 10, 64)
serviceIDInt, err2 := strconv.ParseInt(serviceID, 10, 64)
projectIDInt, err3 := strconv.ParseInt(projectID, 10, 64)
if errors.Join(err1, err2, err3) != nil {
m.statusBarMessage = errors.Join(err1, err2, err3).Error()
m.tuiMode = MODE_CAL
} else {
err := m.miteAPI.AddTimeEntry(m.dest.Format(time.DateOnly), int(minutesInt), description, int(projectIDInt), int(serviceIDInt))
if err != nil {
m.statusBarMessage = errors.Join(err1, err2, err3).Error()
m.tuiMode = MODE_CAL
} else {
m.statusBarMessage = "Successfully logged time"
m.tuiMode = MODE_CAL
}
}
}
return m, formCmd
}