diff options
Diffstat (limited to 'internal')
-rw-r--r-- | internal/actions/actions.go | 2 | ||||
-rw-r--r-- | internal/actions/clients.go | 31 | ||||
-rw-r--r-- | internal/actions/projects.go | 23 | ||||
-rw-r--r-- | internal/tui/app.go | 18 | ||||
-rw-r--r-- | internal/tui/commands.go | 37 | ||||
-rw-r--r-- | internal/tui/form.go | 12 | ||||
-rw-r--r-- | internal/tui/keys.go | 14 | ||||
-rw-r--r-- | internal/tui/modal.go | 88 |
8 files changed, 179 insertions, 46 deletions
diff --git a/internal/actions/actions.go b/internal/actions/actions.go index 61d007b..7f707d3 100644 --- a/internal/actions/actions.go +++ b/internal/actions/actions.go @@ -16,10 +16,12 @@ type Actions interface { // Client operations CreateClient(ctx context.Context, name, email string, billableRate *float64) (*queries.Client, error) + EditClient(ctx context.Context, id int64, name, email string, billableRate *float64) (*queries.Client, error) FindClient(ctx context.Context, nameOrID string) (*queries.Client, error) // Project operations CreateProject(ctx context.Context, name, client string, billableRate *float64) (*queries.Project, error) + EditProject(ctx context.Context, id int64, name string, billableRate *float64) (*queries.Project, error) FindProject(ctx context.Context, nameOrID string) (*queries.Project, error) } diff --git a/internal/actions/clients.go b/internal/actions/clients.go index 1a99d59..10e8e7d 100644 --- a/internal/actions/clients.go +++ b/internal/actions/clients.go @@ -22,9 +22,8 @@ func (a *actions) CreateClient(ctx context.Context, name, email string, billable } var billableRateParam sql.NullInt64 - if billableRate != nil && *billableRate > 0 { - rate := int64(*billableRate * 100) // Convert dollars to cents - billableRateParam = sql.NullInt64{Int64: rate, Valid: true} + if billableRate != nil { + billableRateParam = sql.NullInt64{Int64: int64(*billableRate * 100), Valid: true} } client, err := a.queries.CreateClient(ctx, queries.CreateClientParams{ @@ -39,6 +38,32 @@ func (a *actions) CreateClient(ctx context.Context, name, email string, billable return &client, nil } +func (a *actions) EditClient(ctx context.Context, id int64, name, email string, billableRate *float64) (*queries.Client, error) { + finalName, finalEmail := parseNameAndEmail(name, email) + + var emailParam sql.NullString + if finalEmail != "" { + emailParam = sql.NullString{String: finalEmail, Valid: true} + } + + var rateParam sql.NullInt64 + if billableRate != nil { + rateParam = sql.NullInt64{Int64: int64(*billableRate * 100), Valid: true} + } + + client, err := a.queries.UpdateClient(ctx, queries.UpdateClientParams{ + Name: finalName, + Email: emailParam, + BillableRate: rateParam, + ID: id, + }) + if err != nil { + return nil, fmt.Errorf("failed to update client: %w", err) + } + + return &client, nil +} + // FindClient finds a client by name or ID func (a *actions) FindClient(ctx context.Context, nameOrID string) (*queries.Client, error) { // Parse as ID if possible, otherwise use 0 diff --git a/internal/actions/projects.go b/internal/actions/projects.go index 4cb4638..d36780b 100644 --- a/internal/actions/projects.go +++ b/internal/actions/projects.go @@ -18,9 +18,8 @@ func (a *actions) CreateProject(ctx context.Context, name, client string, billab } var billableRateParam sql.NullInt64 - if billableRate != nil && *billableRate > 0 { - rate := int64(*billableRate * 100) // Convert dollars to cents - billableRateParam = sql.NullInt64{Int64: rate, Valid: true} + if billableRate != nil { + billableRateParam = sql.NullInt64{Int64: int64(*billableRate * 100), Valid: true} } project, err := a.queries.CreateProject(ctx, queries.CreateProjectParams{ @@ -35,6 +34,24 @@ func (a *actions) CreateProject(ctx context.Context, name, client string, billab return &project, nil } +func (a *actions) EditProject(ctx context.Context, id int64, name string, billableRate *float64) (*queries.Project, error) { + var rateParam sql.NullInt64 + if billableRate != nil { + rateParam = sql.NullInt64{Int64: int64(*billableRate * 100), Valid: true} + } + + project, err := a.queries.UpdateProject(ctx, queries.UpdateProjectParams{ + Name: name, + BillableRate: rateParam, + ID: id, + }) + if err != nil { + return nil, fmt.Errorf("failed to update project: %w", err) + } + + return &project, nil +} + // FindProject finds a project by name or ID func (a *actions) FindProject(ctx context.Context, nameOrID string) (*queries.Project, error) { // Parse as ID if possible, otherwise use 0 diff --git a/internal/tui/app.go b/internal/tui/app.go index 0b67933..d21df5c 100644 --- a/internal/tui/app.go +++ b/internal/tui/app.go @@ -188,6 +188,11 @@ func (m AppModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case openContractorEditor: m.openContractorEditor() + case openClientOrProjectEditor: + if m.selectedBox == ProjectsBox { + m.openClientOrProjectEditor() + } + case openModalUnchanged: m.modalBox.Active = true @@ -263,6 +268,19 @@ func (m *AppModel) openContractorEditor() { m.modalBox.form.fields[0].Focus() } +func (m *AppModel) openClientOrProjectEditor() { + client := m.projectsBox.clients[m.projectsBox.selectedClient] + if m.projectsBox.selectedProject == nil { + m.modalBox.activate(ModalTypeClient, client.ID, *m) + m.modalBox.populateClientFields(client) + } else { + project := m.projectsBox.projects[client.ID][*m.projectsBox.selectedProject] + m.modalBox.activate(ModalTypeProjectEdit, project.ID, *m) + m.modalBox.populateProjectFields(project) + } + m.modalBox.form.fields[0].Focus() +} + func (m *AppModel) openHistoryFilterModal() { m.modalBox.activate(ModalTypeHistoryFilter, 0, *m) m.modalBox.form.fields[0].Focus() diff --git a/internal/tui/commands.go b/internal/tui/commands.go index 363b570..66a0c4d 100644 --- a/internal/tui/commands.go +++ b/internal/tui/commands.go @@ -14,22 +14,23 @@ import ( ) type ( - navigationMsg struct{ Forward bool } - selectionMsg struct{ Forward bool } - selectionToEnd struct{ Top bool } - drillDownMsg struct{} - drillUpMsg struct{} - modalClosed struct{} - openTimeEntryEditor struct{} - openContractorEditor struct{} - openModalUnchanged struct{} - openDeleteConfirmation struct{} - recheckBounds struct{} - openCreateClientModal struct{} - openCreateProjectModal struct{} - openHistoryFilterModal struct{} - openReportModal struct{} - updateHistoryFilter HistoryFilter + navigationMsg struct{ Forward bool } + selectionMsg struct{ Forward bool } + selectionToEnd struct{ Top bool } + drillDownMsg struct{} + drillUpMsg struct{} + modalClosed struct{} + openTimeEntryEditor struct{} + openContractorEditor struct{} + openClientOrProjectEditor struct{} + openModalUnchanged struct{} + openDeleteConfirmation struct{} + recheckBounds struct{} + openCreateClientModal struct{} + openCreateProjectModal struct{} + openHistoryFilterModal struct{} + openReportModal struct{} + updateHistoryFilter HistoryFilter ) func navigate(forward bool) tea.Cmd { @@ -105,6 +106,10 @@ func editContractor() tea.Cmd { return func() tea.Msg { return openContractorEditor{} } } +func editClientOrProject() tea.Cmd { + return func() tea.Msg { return openClientOrProjectEditor{} } +} + func reOpenModal() tea.Cmd { return func() tea.Msg { return openModalUnchanged{} } } diff --git a/internal/tui/form.go b/internal/tui/form.go index d0a2025..b0ac18d 100644 --- a/internal/tui/form.go +++ b/internal/tui/form.go @@ -165,7 +165,7 @@ func NewClientForm() Form { return form } -func NewProjectForm() Form { +func NewProjectCreateForm() Form { form := NewForm([]FormField{ {Model: textinput.New(), label: "Name"}, {Model: textinput.New(), label: "Client", suggestions: suggestClients}, @@ -176,6 +176,16 @@ func NewProjectForm() Form { return form } +func NewProjectEditForm() Form { + form := NewForm([]FormField{ + {Model: textinput.New(), label: "Name"}, + newOptionalFloatField("Hourly Rate"), + }) + form.SelectedStyle = &modalFocusedInputStyle + form.UnselectedStyle = &modalBlurredInputStyle + return form +} + func NewHistoryFilterForm() Form { form := NewForm([]FormField{ newDateRangeField("Date Range"), diff --git a/internal/tui/keys.go b/internal/tui/keys.go index 52e15f6..5cbefa4 100644 --- a/internal/tui/keys.go +++ b/internal/tui/keys.go @@ -58,9 +58,9 @@ var Bindings map[KeyBindingScope]map[string]KeyBinding = map[KeyBindingScope]map Result: func(am *AppModel) tea.Cmd { return am.refreshCmd }, }, "c": KeyBinding{ - Key: "c", + Key: "c", Description: func(am AppModel) string { return "Edit Contractor" }, - Result: func(am *AppModel) tea.Cmd { return editContractor() }, + Result: func(am *AppModel) tea.Cmd { return editContractor() }, }, "q": KeyBinding{ Key: "q", @@ -108,6 +108,16 @@ var Bindings map[KeyBindingScope]map[string]KeyBinding = map[KeyBindingScope]map Description: func(AppModel) string { return "Up" }, Result: func(*AppModel) tea.Cmd { return changeSelection(false) }, }, + "e": KeyBinding{ + Key: "e", + Description: func(m AppModel) string { + if m.projectsBox.selectedProject != nil { + return "Edit Project" + } + return "Edit Client" + }, + Result: func(*AppModel) tea.Cmd { return editClientOrProject() }, + }, "down": KeyBinding{ Key: "down", Description: func(AppModel) string { return "Down" }, diff --git a/internal/tui/modal.go b/internal/tui/modal.go index 51ac384..248654b 100644 --- a/internal/tui/modal.go +++ b/internal/tui/modal.go @@ -19,7 +19,8 @@ type ModalType int const ( ModalTypeClient ModalType = iota - ModalTypeProject + ModalTypeProjectCreate + ModalTypeProjectEdit ModalTypeDeleteConfirmation ModalTypeEntry ModalTypeHistoryFilter @@ -33,8 +34,10 @@ func (mt ModalType) newForm() Form { return NewEntryEditorForm() case ModalTypeClient: return NewClientForm() - case ModalTypeProject: - return NewProjectForm() + case ModalTypeProjectCreate: + return NewProjectCreateForm() + case ModalTypeProjectEdit: + return NewProjectEditForm() case ModalTypeHistoryFilter: return NewHistoryFilterForm() case ModalTypeGenerateReport: @@ -93,7 +96,7 @@ func (m ModalBoxModel) Render() string { return m.RenderDeleteConfirmation() case ModalTypeClient: return m.RenderFormModal("👤 Client") - case ModalTypeProject: + case ModalTypeProjectCreate, ModalTypeProjectEdit: return m.RenderFormModal("📂 Project") case ModalTypeHistoryFilter: return m.RenderFormModal("🔍 History Filter") @@ -126,7 +129,7 @@ func (m ModalBoxModel) RenderDeleteConfirmation() string { } func (m *ModalBoxModel) activateCreateProjectModal(am AppModel) { - m.activate(ModalTypeProject, 0, am) + m.activate(ModalTypeProjectCreate, 0, am) if am.selectedBox == ProjectsBox && len(am.projectsBox.clients) > 0 { client := am.projectsBox.clients[am.projectsBox.selectedClient] m.form.fields[1].SetValue(client.Name) @@ -152,6 +155,23 @@ func (m *ModalBoxModel) populateContractorFields(contractor ContractorInfo) { m.form.fields[2].SetValue(contractor.email) } +func (m *ModalBoxModel) populateClientFields(client queries.Client) { + m.form.fields[0].SetValue(client.Name) + if client.Email.Valid { + m.form.fields[1].SetValue(client.Email.String) + } + if client.BillableRate.Valid { + m.form.fields[2].SetValue(fmt.Sprintf("%.2f", float64(client.BillableRate.Int64)/100)) + } +} + +func (m *ModalBoxModel) populateProjectFields(project queries.Project) { + m.form.fields[0].SetValue(project.Name) + if project.BillableRate.Valid { + m.form.fields[2].SetValue(fmt.Sprintf("%.2f", float64(project.BillableRate.Int64)/100)) + } +} + var ( boldStyle = lipgloss.NewStyle().Bold(true) modalTitleStyle = boldStyle @@ -182,8 +202,8 @@ func (m *ModalBoxModel) SubmitForm(am AppModel) tea.Cmd { return reOpenModal() } - msg := am.refreshCmd() - return func() tea.Msg { return msg } + return am.refreshCmd + case ModalTypeClient: if err := m.form.Error(); err != nil { return reOpenModal() @@ -196,10 +216,42 @@ func (m *ModalBoxModel) SubmitForm(am AppModel) tea.Cmd { } if m.editedID != 0 { - panic("editing a client not yet implemented") + if _, err := actions.New(am.queries).EditClient( + context.Background(), + m.editedID, + m.form.fields[0].Value(), + m.form.fields[1].Value(), + rate, + ); err != nil { + m.form.err = err + return reOpenModal() + } + } else { + if _, err := actions.New(am.queries).CreateClient( + context.Background(), + m.form.fields[0].Value(), + m.form.fields[1].Value(), + rate, + ); err != nil { + m.form.err = err + return reOpenModal() + } } - if _, err := actions.New(am.queries).CreateClient( + return am.refreshCmd + + case ModalTypeProjectCreate: + if err := m.form.Error(); err != nil { + return reOpenModal() + } + + var rate *float64 + if value := m.form.fields[2].Value(); value != "" { + r, _ := strconv.ParseFloat(value, 64) + rate = &r + } + + if _, err := actions.New(am.queries).CreateProject( context.Background(), m.form.fields[0].Value(), m.form.fields[1].Value(), @@ -209,36 +261,30 @@ func (m *ModalBoxModel) SubmitForm(am AppModel) tea.Cmd { return reOpenModal() } - msg := am.refreshCmd() - return func() tea.Msg { return msg } + return am.refreshCmd - case ModalTypeProject: + case ModalTypeProjectEdit: if err := m.form.Error(); err != nil { return reOpenModal() } var rate *float64 - if value := m.form.fields[2].Value(); value != "" { + if value := m.form.fields[1].Value(); value != "" { r, _ := strconv.ParseFloat(value, 64) rate = &r } - if m.editedID != 0 { - panic("editing a project not yet implemented") - } - - if _, err := actions.New(am.queries).CreateProject( + if _, err := actions.New(am.queries).EditProject( context.Background(), + m.editedID, m.form.fields[0].Value(), - m.form.fields[1].Value(), rate, ); err != nil { m.form.err = err return reOpenModal() } - msg := am.refreshCmd() - return func() tea.Msg { return msg } + return am.refreshCmd case ModalTypeHistoryFilter: if err := m.form.Error(); err != nil { |