diff options
author | T <t@tjp.lol> | 2025-08-07 13:11:24 -0600 |
---|---|---|
committer | T <t@tjp.lol> | 2025-08-07 23:14:00 -0600 |
commit | a7ee7f7280d593481501446008acc05e32abcd22 (patch) | |
tree | f056bd9c72934a9e04aa5af872e836bc43d3739f /internal/tui/modal.go | |
parent | 4843deb9cfa6d91282c5124ec025c636137e9e94 (diff) |
entry edit and delete
Diffstat (limited to 'internal/tui/modal.go')
-rw-r--r-- | internal/tui/modal.go | 173 |
1 files changed, 164 insertions, 9 deletions
diff --git a/internal/tui/modal.go b/internal/tui/modal.go index 3a57880..88b2861 100644 --- a/internal/tui/modal.go +++ b/internal/tui/modal.go @@ -1,6 +1,18 @@ package tui -import "github.com/charmbracelet/lipgloss/v2" +import ( + "context" + "database/sql" + "fmt" + "strconv" + "time" + + "punchcard/internal/actions" + "punchcard/internal/queries" + + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss/v2" +) type ModalType int @@ -12,10 +24,23 @@ const ( ModalTypeEntry ) +func (mt ModalType) newForm() Form { + return NewEntryEditorForm() +} + type ModalBoxModel struct { Active bool Type ModalType + + form Form + editedID int64 +} + +func (m *ModalBoxModel) HandleKeyPress(msg tea.KeyMsg) tea.Cmd { + form, cmd := m.form.Update(msg) + m.form = form + return cmd } func (m ModalBoxModel) RenderCenteredOver(mainContent string, app AppModel) string { @@ -24,18 +49,16 @@ func (m ModalBoxModel) RenderCenteredOver(mainContent string, app AppModel) stri } modalContent := m.Render() - base := lipgloss.NewLayer(mainContent) - overlayWidth := 60 - overlayHeight := 5 + overlayStyle := lipgloss.NewStyle().Border(lipgloss.RoundedBorder()).BorderForeground(lipgloss.Color("62")).Padding(2, 4) + + overlay := lipgloss.NewLayer(overlayStyle.Render(modalContent)) + overlayWidth := overlay.GetWidth() + overlayHeight := overlay.GetHeight() overlayLeft := (app.width - overlayWidth) / 2 overlayTop := (app.height - overlayHeight) / 2 - overlayStyle := lipgloss.NewStyle().Height(overlayHeight).Width(overlayWidth).Border(lipgloss.RoundedBorder()).BorderForeground(lipgloss.Color("238")) - - overlay := lipgloss.NewLayer(overlayStyle.Render(modalContent)).Width(overlayWidth).Height(overlayHeight) - canvas := lipgloss.NewCanvas( base.Z(0), overlay.X(overlayLeft).Y(overlayTop).Z(1), @@ -47,13 +70,145 @@ func (m ModalBoxModel) RenderCenteredOver(mainContent string, app AppModel) stri func (m ModalBoxModel) Render() string { switch m.Type { case ModalTypeSearch: - return "SEARCH BOX" + return modalTitleStyle.Render("SEARCH BOX") + case ModalTypeEntry: + return m.RenderEntryEditor() + case ModalTypeDeleteConfirmation: + return m.RenderDeleteConfirmation() default: // REMOVE ME return "DEFAULT CONTENT" } } +func (m ModalBoxModel) RenderEntryEditor() string { + return fmt.Sprintf("%s\n\n%s", modalTitleStyle.Render("✏️ Edit Time Entry"), m.form.View()) +} + +func (m ModalBoxModel) RenderDeleteConfirmation() string { + title := modalTitleStyle.Render("🗑️ Delete Time Entry") + content := "Are you sure you want to delete this time entry?\nThis action cannot be undone.\n\n[Enter] Delete [Esc] Cancel" + return fmt.Sprintf("%s\n\n%s", title, content) +} + func (m *ModalBoxModel) activate(t ModalType) { m.Active = true m.Type = t + m.form = t.newForm() +} + +func (m *ModalBoxModel) deactivate() { + m.Active = false +} + +var modalTitleStyle = lipgloss.NewStyle(). + Bold(true) + +func (m ModalBoxModel) SubmitForm(am AppModel) tea.Cmd { + switch m.Type { + case ModalTypeDeleteConfirmation: + err := am.queries.RemoveTimeEntry(context.Background(), m.editedID) + if err != nil { + return reOpenModal() + } + return tea.Sequence(am.refreshCmd, func() tea.Msg { return recheckBounds{} }) + + case ModalTypeEntry: + if err := m.form.Error(); err != nil { + return reOpenModal() + } + + // Extract and validate form data + params, hasErrors := m.validateAndParseForm(am) + if hasErrors { + return reOpenModal() + } + + // Perform the edit + err := am.queries.EditTimeEntry(context.Background(), params) + if err != nil { + // TODO: Handle edit error (could set form error) + return reOpenModal() + } + + // Success - close modal and refresh data + return func() tea.Msg { return am.refreshCmd() } + } + + return nil +} + +func (m *ModalBoxModel) validateAndParseForm(am AppModel) (queries.EditTimeEntryParams, bool) { + var params queries.EditTimeEntryParams + var hasErrors bool + + // Set the entry ID + params.EntryID = m.editedID + + entry, _ := am.queries.GetTimeEntryById(context.Background(), params.EntryID) + + startTimeStr := m.form.fields[0].Value() + startTime, err := time.Parse(time.DateTime, startTimeStr) + if err != nil { + m.form.fields[0].Err = fmt.Errorf("invalid start time format") + hasErrors = true + } else { + params.StartTime = startTime + } + + endTimeStr := m.form.fields[1].Value() + if endTimeStr != "" { + endTime, err := time.Parse(time.DateTime, endTimeStr) + if err != nil { + m.form.fields[1].Err = fmt.Errorf("invalid end time format") + hasErrors = true + } else { + params.EndTime = sql.NullTime{Time: endTime, Valid: true} + } + } else if entry.EndTime.Valid { + m.form.fields[1].Err = fmt.Errorf("can not re-open an entry") + hasErrors = true + } + + clientStr := m.form.fields[2].Value() + if clientStr == "" { + m.form.fields[2].Err = fmt.Errorf("client is required") + hasErrors = true + } else { + client, err := actions.New(am.queries).FindClient(context.Background(), clientStr) + if err != nil { + m.form.fields[2].Err = fmt.Errorf("client not found: %s", clientStr) + hasErrors = true + } else { + params.ClientID = client.ID + } + } + + projectStr := m.form.fields[3].Value() + if projectStr != "" { + project, err := actions.New(am.queries).FindProject(context.Background(), projectStr) + if err != nil { + m.form.fields[3].Err = fmt.Errorf("project not found: %s", projectStr) + hasErrors = true + } else { + params.ProjectID = sql.NullInt64{Int64: project.ID, Valid: true} + } + } + + descriptionStr := m.form.fields[4].Value() + if descriptionStr != "" { + params.Description = sql.NullString{String: descriptionStr, Valid: true} + } + + rateStr := m.form.fields[5].Value() + if rateStr != "" { + rate, err := strconv.ParseFloat(rateStr, 64) + if err != nil { + m.form.fields[5].Err = fmt.Errorf("invalid hourly rate") + hasErrors = true + } else { + params.HourlyRate = sql.NullInt64{Int64: int64(rate * 100), Valid: true} + } + } + + return params, hasErrors } |