Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions cmd/tracking/resume.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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()

Expand All @@ -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)
Expand Down
37 changes: 37 additions & 0 deletions internal/storage/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 = ?",
Expand Down
80 changes: 80 additions & 0 deletions internal/storage/db_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down