diff --git a/cmd/tracking/resume.go b/cmd/tracking/resume.go index 43f5a8f..201cb67 100644 --- a/cmd/tracking/resume.go +++ b/cmd/tracking/resume.go @@ -4,6 +4,7 @@ import ( "fmt" "os" + "github.com/DylanDevelops/tmpo/internal/project" "github.com/DylanDevelops/tmpo/internal/storage" "github.com/DylanDevelops/tmpo/internal/ui" "github.com/spf13/cobra" @@ -13,7 +14,7 @@ func ResumeCmd() *cobra.Command { cmd := &cobra.Command{ Use: "resume", Short: "Resume time tracking", - Long: `Resume time tracking by starting a new session with the same project and description as the last paused session.`, + Long: `Resume time tracking by starting a new session with the same project and description as the last stopped session for the current project.`, Run: func(cmd *cobra.Command, args []string) { ui.NewlineAbove() @@ -38,14 +39,20 @@ func ResumeCmd() *cobra.Command { os.Exit(1) } - lastStopped, err := db.GetLastStoppedEntry() + projectName, err := project.DetectConfiguredProject() + if err != nil { + ui.PrintError(ui.EmojiError, fmt.Sprintf("detecting project: %v", err)) + os.Exit(1) + } + + lastStopped, err := db.GetLastStoppedEntryByProject(projectName) if err != nil { ui.PrintError(ui.EmojiError, fmt.Sprintf("%v", err)) os.Exit(1) } if lastStopped == nil { - ui.PrintError(ui.EmojiError, "No previous session found to resume.") + ui.PrintError(ui.EmojiError, fmt.Sprintf("No previous session found for project '%s' to resume.", projectName)) ui.PrintMuted(0, "Use 'tmpo start' to begin a new session.") ui.NewlineBelow() os.Exit(1) diff --git a/internal/storage/db.go b/internal/storage/db.go index ca79bff..8f18b76 100644 --- a/internal/storage/db.go +++ b/internal/storage/db.go @@ -262,6 +262,43 @@ func (d *Database) GetLastStoppedEntry() (*TimeEntry, error) { return &entry, nil } +func (d *Database) GetLastStoppedEntryByProject(projectName string) (*TimeEntry, error) { + var entry TimeEntry + var endTime sql.NullTime + var hourlyRate sql.NullFloat64 + var milestoneName sql.NullString + + err := d.db.QueryRow(` + SELECT id, project_name, start_time, end_time, description, hourly_rate, milestone_name + FROM time_entries + WHERE end_time IS NOT NULL AND project_name = ? + ORDER BY start_time DESC + LIMIT 1 + `, projectName).Scan(&entry.ID, &entry.ProjectName, &entry.StartTime, &endTime, &entry.Description, &hourlyRate, &milestoneName) + + if err == sql.ErrNoRows { + return nil, nil + } + + if err != nil { + return nil, fmt.Errorf("failed to get last stopped entry for project: %w", err) + } + + if endTime.Valid { + entry.EndTime = &endTime.Time + } + + if hourlyRate.Valid { + entry.HourlyRate = &hourlyRate.Float64 + } + + if milestoneName.Valid { + entry.MilestoneName = &milestoneName.String + } + + return &entry, nil +} + func (d *Database) StopEntry(id int64) error { _, err := d.db.Exec( "UPDATE time_entries SET end_time = ? WHERE id = ?", diff --git a/internal/storage/db_test.go b/internal/storage/db_test.go index bdf08d2..a60c4b1 100644 --- a/internal/storage/db_test.go +++ b/internal/storage/db_test.go @@ -194,6 +194,86 @@ func TestGetLastStoppedEntry(t *testing.T) { assert.NotEqual(t, entry3.ID, stopped.ID) } +func TestGetLastStoppedEntryByProject(t *testing.T) { + db := setupTestDB(t) + defer db.Close() + + // No stopped entries initially + stopped, err := db.GetLastStoppedEntryByProject("project-1") + assert.NoError(t, err) + assert.Nil(t, stopped) + + // Create and stop first entry for project-1 + entry1, err := db.CreateEntry("project-1", "first task", nil, nil) + assert.NoError(t, err) + time.Sleep(10 * time.Millisecond) + err = db.StopEntry(entry1.ID) + assert.NoError(t, err) + + // Should return the stopped entry for project-1 + stopped, err = db.GetLastStoppedEntryByProject("project-1") + assert.NoError(t, err) + assert.NotNil(t, stopped) + assert.Equal(t, entry1.ID, stopped.ID) + assert.Equal(t, "project-1", stopped.ProjectName) + assert.NotNil(t, stopped.EndTime) + + // Create and stop entry for project-2 (more recent globally) + time.Sleep(10 * time.Millisecond) + entry2, err := db.CreateEntry("project-2", "second task", nil, nil) + assert.NoError(t, err) + time.Sleep(10 * time.Millisecond) + err = db.StopEntry(entry2.ID) + assert.NoError(t, err) + + // Should still return entry1 for project-1, not entry2 + stopped, err = db.GetLastStoppedEntryByProject("project-1") + assert.NoError(t, err) + assert.NotNil(t, stopped) + assert.Equal(t, entry1.ID, stopped.ID) + assert.Equal(t, "project-1", stopped.ProjectName) + assert.NotEqual(t, entry2.ID, stopped.ID) + + // Should return entry2 for project-2 + stopped, err = db.GetLastStoppedEntryByProject("project-2") + assert.NoError(t, err) + assert.NotNil(t, stopped) + assert.Equal(t, entry2.ID, stopped.ID) + assert.Equal(t, "project-2", stopped.ProjectName) + + // Create another stopped entry for project-1 (most recent for that project) + time.Sleep(10 * time.Millisecond) + entry3, err := db.CreateEntry("project-1", "third task", nil, nil) + assert.NoError(t, err) + time.Sleep(10 * time.Millisecond) + err = db.StopEntry(entry3.ID) + assert.NoError(t, err) + + // Should return entry3 for project-1 now (most recent) + stopped, err = db.GetLastStoppedEntryByProject("project-1") + assert.NoError(t, err) + assert.NotNil(t, stopped) + assert.Equal(t, entry3.ID, stopped.ID) + assert.Equal(t, "project-1", stopped.ProjectName) + assert.Equal(t, "third task", stopped.Description) + + // Create a running entry for project-1 + entry4, err := db.CreateEntry("project-1", "running task", nil, nil) + assert.NoError(t, err) + + // Should still return entry3 (last stopped), not the running entry4 + stopped, err = db.GetLastStoppedEntryByProject("project-1") + assert.NoError(t, err) + assert.NotNil(t, stopped) + assert.Equal(t, entry3.ID, stopped.ID) + assert.NotEqual(t, entry4.ID, stopped.ID) + + // Query for non-existent project + stopped, err = db.GetLastStoppedEntryByProject("non-existent") + assert.NoError(t, err) + assert.Nil(t, stopped) +} + func TestStopEntry(t *testing.T) { db := setupTestDB(t) defer db.Close()