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
|
package mite
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -219,6 +220,64 @@ func (a APIClient) GetTimeEntries(from, to time.Time) (TimeEntries, error) {
|
|||||||
return out, nil
|
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 {
|
func get(domain, apiKey, path string, data any) error {
|
||||||
req, err := http.NewRequest("GET", baseurl(domain, path), nil)
|
req, err := http.NewRequest("GET", baseurl(domain, path), nil)
|
||||||
if err != 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 styleLocked = lipgloss.NewStyle().Foreground(lipgloss.Color("#666666")) // red
|
||||||
var styleTime = lipgloss.NewStyle().Foreground(lipgloss.Color("#22ff4d")) // green
|
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 {
|
type model struct {
|
||||||
miteAPI mite.APIClient
|
miteAPI mite.APIClient
|
||||||
start calendarTime
|
start calendarTime
|
||||||
dest calendarTime
|
dest calendarTime
|
||||||
dataFetchStatus string
|
fetchedData bool // done initial fetch to get customers/projects etc
|
||||||
focus string
|
tuiMode tuiMode //
|
||||||
debug []string
|
debug []string
|
||||||
formData struct {
|
formData struct {
|
||||||
form *huh.Form
|
form *huh.Form
|
||||||
customers mite.Customers
|
customers mite.Customers
|
||||||
services mite.Services
|
services mite.Services
|
||||||
@ -45,7 +52,8 @@ type model struct {
|
|||||||
entries mite.TimeEntries
|
entries mite.TimeEntries
|
||||||
table table.Model
|
table table.Model
|
||||||
}
|
}
|
||||||
windowWidth int
|
statusBarMessage string
|
||||||
|
windowWidth int
|
||||||
}
|
}
|
||||||
|
|
||||||
func initialModel(miteDomain, miteApiKey string) model {
|
func initialModel(miteDomain, miteApiKey string) model {
|
||||||
@ -74,12 +82,12 @@ func initialModel(miteDomain, miteApiKey string) model {
|
|||||||
m.start = calendarTime{time.Now()}
|
m.start = calendarTime{time.Now()}
|
||||||
m.dest = calendarTime{time.Now()}
|
m.dest = calendarTime{time.Now()}
|
||||||
m.debug = make([]string, 5, 5)
|
m.debug = make([]string, 5, 5)
|
||||||
m.focus = "cal"
|
m.tuiMode = MODE_CAL
|
||||||
|
|
||||||
// m.formData.form = form
|
// m.formData.form = form
|
||||||
m.timeData.table = tab
|
m.timeData.table = tab
|
||||||
|
|
||||||
m.dataFetchStatus = "not fetched"
|
m.statusBarMessage = "Fetching data from API"
|
||||||
|
|
||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
@ -138,11 +146,11 @@ func (m model) buildForm() *huh.Form {
|
|||||||
return nil
|
return nil
|
||||||
}),
|
}),
|
||||||
huh.NewInput().
|
huh.NewInput().
|
||||||
Key("hours").
|
Key("minutes").
|
||||||
CharLimit(5).
|
CharLimit(5).
|
||||||
Validate(
|
Validate(
|
||||||
func(s string) error {
|
func(s string) error {
|
||||||
h, err := strconv.ParseFloat(s, 10)
|
h, err := strconv.ParseInt(s, 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -151,7 +159,7 @@ func (m model) buildForm() *huh.Form {
|
|||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}).
|
}).
|
||||||
Title("Hours"),
|
Title("Minutes"),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
return form
|
return form
|
||||||
@ -170,19 +178,42 @@ func (m model) Init() tea.Cmd {
|
|||||||
|
|
||||||
func (m model) Update(msg tea.Msg) (tea.Model, 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 {
|
if len(m.debug) > 5 {
|
||||||
m.debug = m.debug[len(m.debug)-5:]
|
m.debug = m.debug[len(m.debug)-5:]
|
||||||
}
|
}
|
||||||
m.debug = append(m.debug, fmt.Sprintf("Got a %#v", msg))
|
m.debug = append(m.debug, fmt.Sprintf("Got a %#v", msg))
|
||||||
|
|
||||||
if m.focus == "entries" {
|
if m.tuiMode == MODE_TIMEENTRIES {
|
||||||
return m.updateEntries(msg)
|
return m.updateEntries(msg)
|
||||||
} else if m.focus == "cal" {
|
} else if m.tuiMode == MODE_CAL {
|
||||||
return m.updateCal(msg)
|
return m.updateCal(msg)
|
||||||
} else if m.focus == "addform" {
|
} else if m.tuiMode == MODE_FORMENTRY {
|
||||||
return m.updateForm(msg)
|
return m.updateForm(msg)
|
||||||
} else {
|
} 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
|
// return m, cmd
|
||||||
// }
|
// }
|
||||||
|
|
||||||
func (m model) updateForm(msg tea.Msg) (tea.Model, tea.Cmd) {
|
// updateEntries handles processing tea messages when the days time entries are being
|
||||||
|
// processed
|
||||||
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
|
|
||||||
}
|
|
||||||
func (m model) updateEntries(msg tea.Msg) (tea.Model, tea.Cmd) {
|
func (m model) updateEntries(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
|
||||||
switch msg := msg.(type) {
|
switch msg := msg.(type) {
|
||||||
@ -241,13 +251,13 @@ func (m model) updateEntries(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
switch msg.String() {
|
switch msg.String() {
|
||||||
|
|
||||||
case "tab":
|
case "tab":
|
||||||
m.focus = "cal"
|
m.tuiMode = MODE_CAL
|
||||||
m.timeData.table.Blur()
|
m.timeData.table.Blur()
|
||||||
return m, nil
|
return m, nil
|
||||||
case "ctrl+c", "q":
|
case "ctrl+c", "q":
|
||||||
return m, tea.Quit
|
return m, tea.Quit
|
||||||
case "a":
|
case "a":
|
||||||
m.focus = "addform"
|
m.tuiMode = MODE_FORMENTRY
|
||||||
m.formData.form = m.buildForm()
|
m.formData.form = m.buildForm()
|
||||||
m.formData.form.Init()
|
m.formData.form.Init()
|
||||||
return m, nil
|
return m, nil
|
||||||
@ -266,34 +276,23 @@ func (m model) updateCal(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
case tea.WindowSizeMsg:
|
case tea.WindowSizeMsg:
|
||||||
m.windowWidth = msg.Width
|
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?
|
// Is it a key press?
|
||||||
case tea.KeyMsg:
|
case tea.KeyMsg:
|
||||||
|
|
||||||
// Cool, what was the actual key pressed?
|
// Cool, what was the actual key pressed?
|
||||||
switch msg.String() {
|
switch msg.String() {
|
||||||
case "a":
|
case "a":
|
||||||
m.focus = "addform"
|
if m.fetchedData {
|
||||||
m.formData.form = m.buildForm()
|
m.tuiMode = MODE_FORMENTRY
|
||||||
m.formData.form.Init()
|
m.formData.form = m.buildForm()
|
||||||
|
m.formData.form.Init()
|
||||||
|
} else {
|
||||||
|
m.statusBarMessage = "Not yet fetched data"
|
||||||
|
}
|
||||||
return m, nil
|
return m, nil
|
||||||
|
|
||||||
case "tab":
|
case "tab":
|
||||||
m.focus = "entries"
|
m.tuiMode = MODE_TIMEENTRIES
|
||||||
m.timeData.table.Focus()
|
m.timeData.table.Focus()
|
||||||
m.timeData.table.SetCursor(0)
|
m.timeData.table.SetCursor(0)
|
||||||
return m, nil
|
return m, nil
|
||||||
@ -304,7 +303,7 @@ func (m model) updateCal(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
|
|
||||||
// time entries
|
// time entries
|
||||||
case "f":
|
case "f":
|
||||||
m.dataFetchStatus = "fetching"
|
m.statusBarMessage = "Fetching data from API"
|
||||||
return m, m.fetchMiteData()
|
return m, m.fetchMiteData()
|
||||||
|
|
||||||
// // The "up" and "k" keys move the cursor up
|
// // The "up" and "k" keys move the cursor up
|
||||||
@ -392,17 +391,14 @@ type miteDataFetchedMsg struct {
|
|||||||
|
|
||||||
func (m model) fetchMiteData() tea.Cmd {
|
func (m model) fetchMiteData() tea.Cmd {
|
||||||
return func() tea.Msg {
|
return func() tea.Msg {
|
||||||
time.Sleep(time.Second * 1)
|
// time.Sleep(time.Second * 5)
|
||||||
loc := time.Local
|
loc := time.Local
|
||||||
te, err1 := m.miteAPI.GetTimeEntries(
|
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),
|
time.Date(2025, time.December, 31, 0, 0, 0, 0, loc),
|
||||||
)
|
)
|
||||||
|
|
||||||
cst, err2 := m.miteAPI.GetCustomers()
|
cst, err2 := m.miteAPI.GetCustomers()
|
||||||
|
|
||||||
svc, err3 := m.miteAPI.GetServices()
|
svc, err3 := m.miteAPI.GetServices()
|
||||||
|
|
||||||
pjt, err4 := m.miteAPI.GetProjects()
|
pjt, err4 := m.miteAPI.GetProjects()
|
||||||
|
|
||||||
return miteDataFetchedMsg{
|
return miteDataFetchedMsg{
|
||||||
@ -440,25 +436,34 @@ func (m model) View() string {
|
|||||||
|
|
||||||
lhs.WriteString(calendar(m.dest, m.start, m.dest, styles))
|
lhs.WriteString(calendar(m.dest, m.start, m.dest, styles))
|
||||||
lhs.WriteString("nav: ←↑→↓[] (t)oday\n")
|
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
|
calendarWidth := 25
|
||||||
tableWidth := m.windowWidth - calendarWidth
|
tableWidth := m.windowWidth - calendarWidth
|
||||||
|
|
||||||
if m.dataFetchStatus != "fetched" {
|
if m.tuiMode == MODE_FORMENTRY {
|
||||||
rhs.WriteString("fetching mite data...")
|
rhs.WriteString(m.formData.form.View())
|
||||||
} else {
|
} else {
|
||||||
|
if m.fetchedData {
|
||||||
if m.focus == "addform" {
|
rhs.WriteString(m.timeData.table.View())
|
||||||
rhs.WriteString(m.formData.form.View())
|
rhs.WriteString("\n")
|
||||||
} else {
|
|
||||||
if m.dataFetchStatus == "fetched" {
|
|
||||||
rhs.WriteString(m.timeData.table.View())
|
|
||||||
rhs.WriteString("\n")
|
|
||||||
} else {
|
|
||||||
panic("bad fetchstatus " + m.dataFetchStatus)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -468,10 +473,10 @@ func (m model) View() string {
|
|||||||
lipgloss.NewStyle().Width(tableWidth).Render(rhs.String()),
|
lipgloss.NewStyle().Width(tableWidth).Render(rhs.String()),
|
||||||
)
|
)
|
||||||
|
|
||||||
// if len(m.debug) > 5 {
|
sofar := lipgloss.Height(out)
|
||||||
// m.debug = m.debug[len(m.debug)-5:]
|
|
||||||
// }
|
out +=
|
||||||
|
styleStatusBar.MarginTop(19 - sofar).Width(m.windowWidth).Render(m.statusBarMessage)
|
||||||
|
|
||||||
// out += "\n\n" + strings.Join(m.debug, "\n")
|
|
||||||
return out
|
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