Make great
This commit is contained in:
parent
77d11c4351
commit
682c04ca79
59
mite/mite.go
59
mite/mite.go
@ -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
175
model.go
@ -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
57
model_form.go
Normal 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
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user