summaryrefslogtreecommitdiff
path: root/internal/tui/modal.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/tui/modal.go')
-rw-r--r--internal/tui/modal.go173
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
}