package tui import ( "context" "fmt" "strings" "time" "git.tjp.lol/punchcard/internal/actions" "git.tjp.lol/punchcard/internal/queries" "git.tjp.lol/punchcard/internal/reports" tea "github.com/charmbracelet/bubbletea" ) type ( navigationMsg struct{ Forward bool } selectionMsg struct{ Forward bool } selectionToEnd struct{ Top bool } drillDownMsg struct{} drillUpMsg struct{} modalClosed struct{} openTimeEntryEditor struct{} openModalUnchanged struct{} openDeleteConfirmation struct{} recheckBounds struct{} openCreateClientModal struct{} openCreateProjectModal struct{} openHistoryFilterModal struct{} openReportModal struct{} updateHistoryFilter HistoryFilter ) func navigate(forward bool) tea.Cmd { return func() tea.Msg { return navigationMsg{forward} } } func punchIn(m AppModel) tea.Cmd { return func() tea.Msg { _, _ = actions.New(m.queries).PunchInMostRecent(context.Background(), "", nil) // TODO: use the returned TimerSession instead of re-querying everything return m.refreshCmd() } } func punchOut(m AppModel) tea.Cmd { return func() tea.Msg { _, _ = actions.New(m.queries).PunchOut(context.Background()) // TODO: use the returned TimerSession instead of re-querying everything return m.refreshCmd() } } func punchInOnSelection(m AppModel) tea.Cmd { return func() tea.Msg { var clientID, projectID, description string var entryRate *float64 switch m.selectedBox { case ProjectsBox: clientID, projectID, description, entryRate = m.projectsBox.selection() case HistoryBox: clientID, projectID, description, entryRate = m.historyBox.selection() } if clientID == "" { return nil } _, _ = actions.New(m.queries).PunchIn(context.Background(), clientID, projectID, description, entryRate) // TODO: use the returned TimerSession instead of re-querying everything return m.refreshCmd() } } func selectHistorySummary() tea.Cmd { return func() tea.Msg { return drillDownMsg{} } } func backToHistorySummary() tea.Cmd { return func() tea.Msg { return drillUpMsg{} } } func changeSelection(forward bool) tea.Cmd { return func() tea.Msg { return selectionMsg{forward} } } func changeSelectionToTop() tea.Cmd { return func() tea.Msg { return selectionToEnd{true} } } func changeSelectionToBottom() tea.Cmd { return func() tea.Msg { return selectionToEnd{false} } } func closeModal() tea.Cmd { return func() tea.Msg { return modalClosed{} } } func editCurrentEntry() tea.Cmd { return func() tea.Msg { return openTimeEntryEditor{} } } func reOpenModal() tea.Cmd { return func() tea.Msg { return openModalUnchanged{} } } func confirmDeleteEntry() tea.Cmd { return func() tea.Msg { return openDeleteConfirmation{} } } func createClientModal() tea.Cmd { return func() tea.Msg { return openCreateClientModal{} } } func createProjectModal() tea.Cmd { return func() tea.Msg { return openCreateProjectModal{} } } func createHistoryFilterModal() tea.Cmd { return func() tea.Msg { return openHistoryFilterModal{} } } func createReportModal() tea.Cmd { return func() tea.Msg { return openReportModal{} } } func generateReport(m *ModalBoxModel, am AppModel) tea.Cmd { return func() tea.Msg { form := &m.form dateRange, err := reports.ParseDateRange(form.fields[1].Value()) if err != nil { form.fields[1].Err = fmt.Errorf("invalid date range: %v", err) return reOpenModal() } var tz *time.Location tzstr := form.fields[5].Value() if tzstr == "" { tz = time.Local } else { zone, err := time.LoadLocation(tzstr) if err != nil { form.fields[5].Err = err return reOpenModal() } tz = zone } var genFunc func(context.Context, *queries.Queries, reports.ReportParams) (*reports.ReportResult, error) switch strings.ToLower(form.fields[0].Value()) { case "invoice": genFunc = reports.GenerateInvoice case "timesheet": genFunc = reports.GenerateTimesheet case "unified": genFunc = reports.GenerateUnifiedReport } params := reports.ReportParams{ ClientName: form.fields[2].Value(), ProjectName: form.fields[3].Value(), DateRange: dateRange, OutputPath: form.fields[4].Value(), Timezone: tz, } if _, err := genFunc(context.Background(), am.queries, params); err != nil { form.err = err return reOpenModal() } return nil } }