summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorT <t@tjp.lol>2025-08-13 21:37:59 -0600
committerT <t@tjp.lol>2025-08-13 21:38:26 -0600
commit5c076e605185a09b1e570f9aa3c5ddb784ace0f3 (patch)
tree67b8103ab7d95302bb403388cfb0a61b70d606eb
parent7d0d21ba8663ab7ff777a06f4b113337fa717ff3 (diff)
edit contractor via modal
-rw-r--r--TODO.md4
-rw-r--r--internal/database/queries.sql2
-rw-r--r--internal/queries/queries.sql.go2
-rw-r--r--internal/tui/app.go45
-rw-r--r--internal/tui/commands.go5
-rw-r--r--internal/tui/form.go11
-rw-r--r--internal/tui/keys.go5
-rw-r--r--internal/tui/modal.go26
-rw-r--r--internal/tui/shared.go19
9 files changed, 104 insertions, 15 deletions
diff --git a/TODO.md b/TODO.md
new file mode 100644
index 0000000..61a562a
--- /dev/null
+++ b/TODO.md
@@ -0,0 +1,4 @@
+- [ ] support `punch set` operations in the TUI
+ - [x] on the contractor - global `[c] Edit Contractor`
+ - [ ] on clients (`[e] Edit` on a client)
+ - [ ] on projects (`[e] Edit` on a project)
diff --git a/internal/database/queries.sql b/internal/database/queries.sql
index 1ab9f44..4ed5578 100644
--- a/internal/database/queries.sql
+++ b/internal/database/queries.sql
@@ -203,7 +203,7 @@ returning *;
-- name: UpdateContractor :one
update contractor
set name = @name, label = @label, email = @email
-where id = (select id from contractor order by id limit 1)
+where id = (select id from contractor limit 1)
returning *;
-- name: UpdateClient :one
diff --git a/internal/queries/queries.sql.go b/internal/queries/queries.sql.go
index 0408942..4ae940c 100644
--- a/internal/queries/queries.sql.go
+++ b/internal/queries/queries.sql.go
@@ -1172,7 +1172,7 @@ func (q *Queries) UpdateClient(ctx context.Context, arg UpdateClientParams) (Cli
const updateContractor = `-- name: UpdateContractor :one
update contractor
set name = ?1, label = ?2, email = ?3
-where id = (select id from contractor order by id limit 1)
+where id = (select id from contractor limit 1)
returning id, name, label, email, created_at
`
diff --git a/internal/tui/app.go b/internal/tui/app.go
index f434034..0b67933 100644
--- a/internal/tui/app.go
+++ b/internal/tui/app.go
@@ -67,6 +67,7 @@ type AppModel struct {
projectsBox ClientsProjectsModel
historyBox HistoryBoxModel
modalBox ModalBoxModel
+ contractor ContractorInfo
width int
height int
@@ -81,6 +82,12 @@ type TimeStats struct {
WeekTotal time.Duration
}
+type ContractorInfo struct {
+ name string
+ label string
+ email string
+}
+
// NewApp creates a new TUI application
func NewApp(ctx context.Context, q *queries.Queries) *AppModel {
return &AppModel{
@@ -129,6 +136,7 @@ func (m AppModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
cmds = append(cmds, doTick())
case dataUpdatedMsg:
+ m.contractor = msg.contractor
m.timerBox.timerInfo = msg.timerInfo
m.timerBox.timerInfo.setNames(msg.clients, msg.projects)
m.timeStats = msg.stats
@@ -177,6 +185,9 @@ func (m AppModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.openEntryEditor()
}
+ case openContractorEditor:
+ m.openContractorEditor()
+
case openModalUnchanged:
m.modalBox.Active = true
@@ -246,6 +257,12 @@ func (m *AppModel) openEntryEditor() {
}
}
+func (m *AppModel) openContractorEditor() {
+ m.modalBox.activate(ModalTypeContractor, 0, *m)
+ m.modalBox.populateContractorFields(m.contractor)
+ m.modalBox.form.fields[0].Focus()
+}
+
func (m *AppModel) openHistoryFilterModal() {
m.modalBox.activate(ModalTypeHistoryFilter, 0, *m)
m.modalBox.form.fields[0].Focus()
@@ -369,17 +386,18 @@ func (m AppModel) View() string {
// dataUpdatedMsg is sent when data is updated from the database
type dataUpdatedMsg struct {
- timerInfo TimerInfo
- stats TimeStats
- clients []queries.Client
- projects map[int64][]queries.Project
- entries []queries.TimeEntry
- err error
+ contractor ContractorInfo
+ timerInfo TimerInfo
+ stats TimeStats
+ clients []queries.Client
+ projects map[int64][]queries.Project
+ entries []queries.TimeEntry
+ err error
}
// refreshCmd is a command to update all app data
func (m AppModel) refreshCmd() tea.Msg {
- timerInfo, stats, clients, projects, entries, err := getAppData(m.ctx, m.queries, m.historyBox.filter)
+ contractor, timerInfo, stats, clients, projects, entries, err := getAppData(m.ctx, m.queries, m.historyBox.filter)
if err != nil {
msg := dataUpdatedMsg{}
msg.err = err
@@ -387,12 +405,13 @@ func (m AppModel) refreshCmd() tea.Msg {
}
return dataUpdatedMsg{
- timerInfo: timerInfo,
- stats: stats,
- clients: clients,
- projects: projects,
- entries: entries,
- err: nil,
+ contractor: contractor,
+ timerInfo: timerInfo,
+ stats: stats,
+ clients: clients,
+ projects: projects,
+ entries: entries,
+ err: nil,
}
}
diff --git a/internal/tui/commands.go b/internal/tui/commands.go
index 657b3f5..363b570 100644
--- a/internal/tui/commands.go
+++ b/internal/tui/commands.go
@@ -21,6 +21,7 @@ type (
drillUpMsg struct{}
modalClosed struct{}
openTimeEntryEditor struct{}
+ openContractorEditor struct{}
openModalUnchanged struct{}
openDeleteConfirmation struct{}
recheckBounds struct{}
@@ -100,6 +101,10 @@ func editCurrentEntry() tea.Cmd {
return func() tea.Msg { return openTimeEntryEditor{} }
}
+func editContractor() tea.Cmd {
+ return func() tea.Msg { return openContractorEditor{} }
+}
+
func reOpenModal() tea.Cmd {
return func() tea.Msg { return openModalUnchanged{} }
}
diff --git a/internal/tui/form.go b/internal/tui/form.go
index 09db989..d0a2025 100644
--- a/internal/tui/form.go
+++ b/internal/tui/form.go
@@ -201,6 +201,17 @@ func NewGenerateReportForm() Form {
return form
}
+func NewContractorForm() Form {
+ form := NewForm([]FormField{
+ {Model: textinput.New(), label: "Your Name"},
+ {Model: textinput.New(), label: "Label for your work"},
+ {Model: textinput.New(), label: "Your Email"},
+ })
+ form.SelectedStyle = &modalFocusedInputStyle
+ form.UnselectedStyle = &modalBlurredInputStyle
+ return form
+}
+
func (f Form) Update(msg tea.Msg) (Form, tea.Cmd) {
if msg, ok := msg.(tea.KeyMsg); ok {
switch msg.String() {
diff --git a/internal/tui/keys.go b/internal/tui/keys.go
index 05e1a05..52e15f6 100644
--- a/internal/tui/keys.go
+++ b/internal/tui/keys.go
@@ -57,6 +57,11 @@ var Bindings map[KeyBindingScope]map[string]KeyBinding = map[KeyBindingScope]map
Description: func(am AppModel) string { return "Refresh" },
Result: func(am *AppModel) tea.Cmd { return am.refreshCmd },
},
+ "c": KeyBinding{
+ Key: "c",
+ Description: func(am AppModel) string { return "Edit Contractor" },
+ Result: func(am *AppModel) tea.Cmd { return editContractor() },
+ },
"q": KeyBinding{
Key: "q",
Description: func(am AppModel) string { return "Quit" },
diff --git a/internal/tui/modal.go b/internal/tui/modal.go
index 77b3fa9..51ac384 100644
--- a/internal/tui/modal.go
+++ b/internal/tui/modal.go
@@ -24,6 +24,7 @@ const (
ModalTypeEntry
ModalTypeHistoryFilter
ModalTypeGenerateReport
+ ModalTypeContractor
)
func (mt ModalType) newForm() Form {
@@ -38,6 +39,8 @@ func (mt ModalType) newForm() Form {
return NewHistoryFilterForm()
case ModalTypeGenerateReport:
return NewGenerateReportForm()
+ case ModalTypeContractor:
+ return NewContractorForm()
}
return Form{}
@@ -96,6 +99,8 @@ func (m ModalBoxModel) Render() string {
return m.RenderFormModal("🔍 History Filter")
case ModalTypeGenerateReport:
return m.RenderFormModal("📄 Generate Report")
+ case ModalTypeContractor:
+ return m.RenderFormModal("👤 Contractor")
default: // REMOVE ME
return "DEFAULT CONTENT"
}
@@ -141,6 +146,12 @@ func (m *ModalBoxModel) deactivate() {
m.Active = false
}
+func (m *ModalBoxModel) populateContractorFields(contractor ContractorInfo) {
+ m.form.fields[0].SetValue(contractor.name)
+ m.form.fields[1].SetValue(contractor.label)
+ m.form.fields[2].SetValue(contractor.email)
+}
+
var (
boldStyle = lipgloss.NewStyle().Bold(true)
modalTitleStyle = boldStyle
@@ -287,6 +298,21 @@ func (m *ModalBoxModel) SubmitForm(am AppModel) tea.Cmd {
return generateReport(m, am)
+ case ModalTypeContractor:
+ if err := m.form.Error(); err != nil {
+ return reOpenModal()
+ }
+
+ if _, err := am.queries.UpdateContractor(context.Background(), queries.UpdateContractorParams{
+ Name: m.form.fields[0].Value(),
+ Label: m.form.fields[1].Value(),
+ Email: m.form.fields[2].Value(),
+ }); err != nil {
+ m.form.err = err
+ return reOpenModal()
+ }
+
+ return am.refreshCmd
}
return nil
diff --git a/internal/tui/shared.go b/internal/tui/shared.go
index c79560a..c81827a 100644
--- a/internal/tui/shared.go
+++ b/internal/tui/shared.go
@@ -59,6 +59,19 @@ func FormatDuration(d time.Duration) string {
return fmt.Sprintf("%ds", seconds)
}
+func getContractorInfo(ctx context.Context, q *queries.Queries) (ContractorInfo, error) {
+ c, err := q.GetContractor(ctx)
+ if err != nil {
+ return ContractorInfo{}, err
+ }
+
+ return ContractorInfo{
+ name: c.Name,
+ label: c.Label,
+ email: c.Email,
+ }, nil
+}
+
func getTimerInfo(ctx context.Context, q *queries.Queries) (TimerInfo, error) {
var info TimerInfo
@@ -193,6 +206,7 @@ func getAppData(
q *queries.Queries,
filter HistoryFilter,
) (
+ contractor ContractorInfo,
info TimerInfo,
stats TimeStats,
clients []queries.Client,
@@ -200,6 +214,11 @@ func getAppData(
entries []queries.TimeEntry,
err error,
) {
+ contractor, err = getContractorInfo(ctx, q)
+ if err != nil {
+ return
+ }
+
info, err = getTimerInfo(ctx, q)
if err != nil {
return