diff options
Diffstat (limited to 'internal/actions')
-rw-r--r-- | internal/actions/actions.go | 8 | ||||
-rw-r--r-- | internal/actions/archive_test.go | 618 | ||||
-rw-r--r-- | internal/actions/clients.go | 16 | ||||
-rw-r--r-- | internal/actions/projects.go | 16 | ||||
-rw-r--r-- | internal/actions/timer.go | 74 | ||||
-rw-r--r-- | internal/actions/types.go | 2 |
6 files changed, 715 insertions, 19 deletions
diff --git a/internal/actions/actions.go b/internal/actions/actions.go index 7f707d3..b421047 100644 --- a/internal/actions/actions.go +++ b/internal/actions/actions.go @@ -9,8 +9,8 @@ import ( // Actions provides high-level business operations for time tracking type Actions interface { // Timer operations - PunchIn(ctx context.Context, client, project, description string, billableRate *float64) (*TimerSession, error) - PunchInMostRecent(ctx context.Context, description string, billableRate *float64) (*TimerSession, error) + PunchIn(ctx context.Context, client, project, description string, billableRate *float64, autoUnarchive bool) (*TimerSession, error) + PunchInMostRecent(ctx context.Context, description string, billableRate *float64, autoUnarchive bool) (*TimerSession, error) PunchOut(ctx context.Context) (*TimerSession, error) EditEntry(ctx context.Context, entry queries.TimeEntry) error @@ -18,11 +18,15 @@ type Actions interface { CreateClient(ctx context.Context, name, email string, billableRate *float64) (*queries.Client, error) EditClient(ctx context.Context, id int64, name, email string, billableRate *float64) (*queries.Client, error) FindClient(ctx context.Context, nameOrID string) (*queries.Client, error) + ArchiveClient(ctx context.Context, id int64) error + UnarchiveClient(ctx context.Context, id int64) error // Project operations CreateProject(ctx context.Context, name, client string, billableRate *float64) (*queries.Project, error) EditProject(ctx context.Context, id int64, name string, billableRate *float64) (*queries.Project, error) FindProject(ctx context.Context, nameOrID string) (*queries.Project, error) + ArchiveProject(ctx context.Context, id int64) error + UnarchiveProject(ctx context.Context, id int64) error } // New creates a new Actions instance diff --git a/internal/actions/archive_test.go b/internal/actions/archive_test.go new file mode 100644 index 0000000..d205703 --- /dev/null +++ b/internal/actions/archive_test.go @@ -0,0 +1,618 @@ +package actions + +import ( + "context" + "database/sql" + "fmt" + "testing" + + "git.tjp.lol/punchcard/internal/database" + "git.tjp.lol/punchcard/internal/queries" +) + +func setupTestDB(t *testing.T) (*queries.Queries, func()) { + db, err := sql.Open("sqlite", ":memory:") + if err != nil { + t.Fatalf("Failed to open in-memory sqlite db: %v", err) + } + if err := database.InitializeDB(db); err != nil { + t.Fatalf("Failed to initialize in-memory sqlite db: %v", err) + } + q := queries.New(db) + + cleanup := func() { + if err := q.DBTX().(*sql.DB).Close(); err != nil { + t.Logf("error closing database: %v", err) + } + } + + return q, cleanup +} + +func TestArchiveClient(t *testing.T) { + tests := []struct { + name string + setupData func(*queries.Queries) int64 + expectError bool + }{ + { + name: "archive existing client", + setupData: func(q *queries.Queries) int64 { + client, _ := q.CreateClient(context.Background(), queries.CreateClientParams{ + Name: "TestClient", + }) + return client.ID + }, + expectError: false, + }, + { + name: "archive already archived client", + setupData: func(q *queries.Queries) int64 { + client, _ := q.CreateClient(context.Background(), queries.CreateClientParams{ + Name: "AlreadyArchived", + }) + _ = q.ArchiveClient(context.Background(), client.ID) + return client.ID + }, + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + q, cleanup := setupTestDB(t) + defer cleanup() + + a := New(q) + clientID := tt.setupData(q) + + err := a.ArchiveClient(context.Background(), clientID) + + if tt.expectError && err == nil { + t.Errorf("Expected error but got none") + } + if !tt.expectError && err != nil { + t.Errorf("Unexpected error: %v", err) + } + + if !tt.expectError { + client, err := a.FindClient(context.Background(), fmt.Sprintf("%d", clientID)) + if err != nil { + t.Fatalf("Failed to find client: %v", err) + } + if client.Archived == 0 { + t.Errorf("Expected client to be archived") + } + } + }) + } +} + +func TestUnarchiveClient(t *testing.T) { + tests := []struct { + name string + setupData func(*queries.Queries) int64 + expectError bool + }{ + { + name: "unarchive archived client", + setupData: func(q *queries.Queries) int64 { + client, _ := q.CreateClient(context.Background(), queries.CreateClientParams{ + Name: "ArchivedClient", + }) + _ = q.ArchiveClient(context.Background(), client.ID) + return client.ID + }, + expectError: false, + }, + { + name: "unarchive already active client", + setupData: func(q *queries.Queries) int64 { + client, _ := q.CreateClient(context.Background(), queries.CreateClientParams{ + Name: "ActiveClient", + }) + return client.ID + }, + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + q, cleanup := setupTestDB(t) + defer cleanup() + + a := New(q) + clientID := tt.setupData(q) + + err := a.UnarchiveClient(context.Background(), clientID) + + if tt.expectError && err == nil { + t.Errorf("Expected error but got none") + } + if !tt.expectError && err != nil { + t.Errorf("Unexpected error: %v", err) + } + + if !tt.expectError { + client, err := a.FindClient(context.Background(), fmt.Sprintf("%d", clientID)) + if err != nil { + t.Fatalf("Failed to find client: %v", err) + } + if client.Archived != 0 { + t.Errorf("Expected client to not be archived") + } + } + }) + } +} + +func TestArchiveProject(t *testing.T) { + tests := []struct { + name string + setupData func(*queries.Queries) int64 + expectError bool + }{ + { + name: "archive existing project", + setupData: func(q *queries.Queries) int64 { + client, _ := q.CreateClient(context.Background(), queries.CreateClientParams{ + Name: "TestClient", + }) + project, _ := q.CreateProject(context.Background(), queries.CreateProjectParams{ + Name: "TestProject", + ClientID: client.ID, + }) + return project.ID + }, + expectError: false, + }, + { + name: "archive already archived project", + setupData: func(q *queries.Queries) int64 { + client, _ := q.CreateClient(context.Background(), queries.CreateClientParams{ + Name: "TestClient", + }) + project, _ := q.CreateProject(context.Background(), queries.CreateProjectParams{ + Name: "AlreadyArchived", + ClientID: client.ID, + }) + _ = q.ArchiveProject(context.Background(), project.ID) + return project.ID + }, + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + q, cleanup := setupTestDB(t) + defer cleanup() + + a := New(q) + projectID := tt.setupData(q) + + err := a.ArchiveProject(context.Background(), projectID) + + if tt.expectError && err == nil { + t.Errorf("Expected error but got none") + } + if !tt.expectError && err != nil { + t.Errorf("Unexpected error: %v", err) + } + + if !tt.expectError { + project, err := a.FindProject(context.Background(), fmt.Sprintf("%d", projectID)) + if err != nil { + t.Fatalf("Failed to find project: %v", err) + } + if project.Archived == 0 { + t.Errorf("Expected project to be archived") + } + } + }) + } +} + +func TestUnarchiveProject(t *testing.T) { + tests := []struct { + name string + setupData func(*queries.Queries) int64 + expectError bool + }{ + { + name: "unarchive archived project", + setupData: func(q *queries.Queries) int64 { + client, _ := q.CreateClient(context.Background(), queries.CreateClientParams{ + Name: "TestClient", + }) + project, _ := q.CreateProject(context.Background(), queries.CreateProjectParams{ + Name: "ArchivedProject", + ClientID: client.ID, + }) + _ = q.ArchiveProject(context.Background(), project.ID) + return project.ID + }, + expectError: false, + }, + { + name: "unarchive already active project", + setupData: func(q *queries.Queries) int64 { + client, _ := q.CreateClient(context.Background(), queries.CreateClientParams{ + Name: "TestClient", + }) + project, _ := q.CreateProject(context.Background(), queries.CreateProjectParams{ + Name: "ActiveProject", + ClientID: client.ID, + }) + return project.ID + }, + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + q, cleanup := setupTestDB(t) + defer cleanup() + + a := New(q) + projectID := tt.setupData(q) + + err := a.UnarchiveProject(context.Background(), projectID) + + if tt.expectError && err == nil { + t.Errorf("Expected error but got none") + } + if !tt.expectError && err != nil { + t.Errorf("Unexpected error: %v", err) + } + + if !tt.expectError { + project, err := a.FindProject(context.Background(), fmt.Sprintf("%d", projectID)) + if err != nil { + t.Fatalf("Failed to find project: %v", err) + } + if project.Archived != 0 { + t.Errorf("Expected project to not be archived") + } + } + }) + } +} + +func TestPunchInWithArchivedClient(t *testing.T) { + tests := []struct { + name string + setupData func(*queries.Queries) (clientName string) + autoUnarchive bool + expectError bool + errorType error + }{ + { + name: "punch in on archived client without auto-unarchive returns error", + setupData: func(q *queries.Queries) string { + client, _ := q.CreateClient(context.Background(), queries.CreateClientParams{ + Name: "ArchivedClient", + }) + _ = q.ArchiveClient(context.Background(), client.ID) + return client.Name + }, + autoUnarchive: false, + expectError: true, + errorType: ErrArchivedClient, + }, + { + name: "punch in on archived client with auto-unarchive succeeds", + setupData: func(q *queries.Queries) string { + client, _ := q.CreateClient(context.Background(), queries.CreateClientParams{ + Name: "ArchivedClient", + }) + _ = q.ArchiveClient(context.Background(), client.ID) + return client.Name + }, + autoUnarchive: true, + expectError: false, + }, + { + name: "punch in on active client succeeds", + setupData: func(q *queries.Queries) string { + client, _ := q.CreateClient(context.Background(), queries.CreateClientParams{ + Name: "ActiveClient", + }) + return client.Name + }, + autoUnarchive: false, + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + q, cleanup := setupTestDB(t) + defer cleanup() + + a := New(q) + clientName := tt.setupData(q) + + session, err := a.PunchIn(context.Background(), clientName, "", "", nil, tt.autoUnarchive) + + if tt.expectError { + if err == nil { + t.Errorf("Expected error but got none") + } else if tt.errorType != nil && err != tt.errorType { + t.Errorf("Expected error %v, got %v", tt.errorType, err) + } + return + } + + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + if session == nil { + t.Fatalf("Expected session but got nil") + } + + if tt.autoUnarchive { + client, err := a.FindClient(context.Background(), clientName) + if err != nil { + t.Fatalf("Failed to find client: %v", err) + } + if client.Archived != 0 { + t.Errorf("Expected client to be unarchived after auto-unarchive") + } + } + }) + } +} + +func TestPunchInWithArchivedProject(t *testing.T) { + tests := []struct { + name string + setupData func(*queries.Queries) (projectName string) + autoUnarchive bool + expectError bool + errorType error + }{ + { + name: "punch in on archived project without auto-unarchive returns error", + setupData: func(q *queries.Queries) string { + client, _ := q.CreateClient(context.Background(), queries.CreateClientParams{ + Name: "TestClient", + }) + project, _ := q.CreateProject(context.Background(), queries.CreateProjectParams{ + Name: "ArchivedProject", + ClientID: client.ID, + }) + _ = q.ArchiveProject(context.Background(), project.ID) + return project.Name + }, + autoUnarchive: false, + expectError: true, + errorType: ErrArchivedProject, + }, + { + name: "punch in on archived project with auto-unarchive succeeds", + setupData: func(q *queries.Queries) string { + client, _ := q.CreateClient(context.Background(), queries.CreateClientParams{ + Name: "TestClient", + }) + project, _ := q.CreateProject(context.Background(), queries.CreateProjectParams{ + Name: "ArchivedProject", + ClientID: client.ID, + }) + _ = q.ArchiveProject(context.Background(), project.ID) + return project.Name + }, + autoUnarchive: true, + expectError: false, + }, + { + name: "punch in on active project succeeds", + setupData: func(q *queries.Queries) string { + client, _ := q.CreateClient(context.Background(), queries.CreateClientParams{ + Name: "TestClient", + }) + project, _ := q.CreateProject(context.Background(), queries.CreateProjectParams{ + Name: "ActiveProject", + ClientID: client.ID, + }) + return project.Name + }, + autoUnarchive: false, + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + q, cleanup := setupTestDB(t) + defer cleanup() + + a := New(q) + projectName := tt.setupData(q) + + session, err := a.PunchIn(context.Background(), "", projectName, "", nil, tt.autoUnarchive) + + if tt.expectError { + if err == nil { + t.Errorf("Expected error but got none") + } else if tt.errorType != nil && err != tt.errorType { + t.Errorf("Expected error %v, got %v", tt.errorType, err) + } + return + } + + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + if session == nil { + t.Fatalf("Expected session but got nil") + } + + if tt.autoUnarchive { + project, err := a.FindProject(context.Background(), projectName) + if err != nil { + t.Fatalf("Failed to find project: %v", err) + } + if project.Archived != 0 { + t.Errorf("Expected project to be unarchived after auto-unarchive") + } + } + }) + } +} + +func TestPunchInMostRecentWithArchivedClient(t *testing.T) { + tests := []struct { + name string + setupData func(*queries.Queries) + autoUnarchive bool + expectError bool + errorType error + }{ + { + name: "punch in most recent with archived client without auto-unarchive returns error", + setupData: func(q *queries.Queries) { + client, _ := q.CreateClient(context.Background(), queries.CreateClientParams{ + Name: "ArchivedClient", + }) + _, _ = q.CreateTimeEntry(context.Background(), queries.CreateTimeEntryParams{ + ClientID: client.ID, + }) + _, _ = q.StopTimeEntry(context.Background()) + _ = q.ArchiveClient(context.Background(), client.ID) + }, + autoUnarchive: false, + expectError: true, + errorType: ErrArchivedClient, + }, + { + name: "punch in most recent with archived client with auto-unarchive succeeds", + setupData: func(q *queries.Queries) { + client, _ := q.CreateClient(context.Background(), queries.CreateClientParams{ + Name: "ArchivedClient", + }) + _, _ = q.CreateTimeEntry(context.Background(), queries.CreateTimeEntryParams{ + ClientID: client.ID, + }) + _, _ = q.StopTimeEntry(context.Background()) + _ = q.ArchiveClient(context.Background(), client.ID) + }, + autoUnarchive: true, + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + q, cleanup := setupTestDB(t) + defer cleanup() + + a := New(q) + tt.setupData(q) + + session, err := a.PunchInMostRecent(context.Background(), "", nil, tt.autoUnarchive) + + if tt.expectError { + if err == nil { + t.Errorf("Expected error but got none") + } else if tt.errorType != nil && err != tt.errorType { + t.Errorf("Expected error %v, got %v", tt.errorType, err) + } + return + } + + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + if session == nil { + t.Fatalf("Expected session but got nil") + } + }) + } +} + +func TestPunchInMostRecentWithArchivedProject(t *testing.T) { + tests := []struct { + name string + setupData func(*queries.Queries) + autoUnarchive bool + expectError bool + errorType error + }{ + { + name: "punch in most recent with archived project without auto-unarchive returns error", + setupData: func(q *queries.Queries) { + client, _ := q.CreateClient(context.Background(), queries.CreateClientParams{ + Name: "TestClient", + }) + project, _ := q.CreateProject(context.Background(), queries.CreateProjectParams{ + Name: "ArchivedProject", + ClientID: client.ID, + }) + _, _ = q.CreateTimeEntry(context.Background(), queries.CreateTimeEntryParams{ + ClientID: client.ID, + ProjectID: sql.NullInt64{Int64: project.ID, Valid: true}, + }) + _, _ = q.StopTimeEntry(context.Background()) + _ = q.ArchiveProject(context.Background(), project.ID) + }, + autoUnarchive: false, + expectError: true, + errorType: ErrArchivedProject, + }, + { + name: "punch in most recent with archived project with auto-unarchive succeeds", + setupData: func(q *queries.Queries) { + client, _ := q.CreateClient(context.Background(), queries.CreateClientParams{ + Name: "TestClient", + }) + project, _ := q.CreateProject(context.Background(), queries.CreateProjectParams{ + Name: "ArchivedProject", + ClientID: client.ID, + }) + _, _ = q.CreateTimeEntry(context.Background(), queries.CreateTimeEntryParams{ + ClientID: client.ID, + ProjectID: sql.NullInt64{Int64: project.ID, Valid: true}, + }) + _, _ = q.StopTimeEntry(context.Background()) + _ = q.ArchiveProject(context.Background(), project.ID) + }, + autoUnarchive: true, + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + q, cleanup := setupTestDB(t) + defer cleanup() + + a := New(q) + tt.setupData(q) + + session, err := a.PunchInMostRecent(context.Background(), "", nil, tt.autoUnarchive) + + if tt.expectError { + if err == nil { + t.Errorf("Expected error but got none") + } else if tt.errorType != nil && err != tt.errorType { + t.Errorf("Expected error %v, got %v", tt.errorType, err) + } + return + } + + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + if session == nil { + t.Fatalf("Expected session but got nil") + } + }) + } +} diff --git a/internal/actions/clients.go b/internal/actions/clients.go index 10e8e7d..78b7934 100644 --- a/internal/actions/clients.go +++ b/internal/actions/clients.go @@ -115,3 +115,19 @@ func parseNameAndEmail(nameArg, emailArg string) (string, string) { } var emailAndNameRegex = regexp.MustCompile(`^(.+?)<([^>]+@[^>]+)>$`) + +func (a *actions) ArchiveClient(ctx context.Context, id int64) error { + err := a.queries.ArchiveClient(ctx, id) + if err != nil { + return fmt.Errorf("failed to archive client: %w", err) + } + return nil +} + +func (a *actions) UnarchiveClient(ctx context.Context, id int64) error { + err := a.queries.UnarchiveClient(ctx, id) + if err != nil { + return fmt.Errorf("failed to unarchive client: %w", err) + } + return nil +} diff --git a/internal/actions/projects.go b/internal/actions/projects.go index d36780b..d085faa 100644 --- a/internal/actions/projects.go +++ b/internal/actions/projects.go @@ -79,3 +79,19 @@ func (a *actions) FindProject(ctx context.Context, nameOrID string) (*queries.Pr return nil, fmt.Errorf("%w: %s matches multiple projects", ErrAmbiguousProject, nameOrID) } } + +func (a *actions) ArchiveProject(ctx context.Context, id int64) error { + err := a.queries.ArchiveProject(ctx, id) + if err != nil { + return fmt.Errorf("failed to archive project: %w", err) + } + return nil +} + +func (a *actions) UnarchiveProject(ctx context.Context, id int64) error { + err := a.queries.UnarchiveProject(ctx, id) + if err != nil { + return fmt.Errorf("failed to unarchive project: %w", err) + } + return nil +} diff --git a/internal/actions/timer.go b/internal/actions/timer.go index a7e7bbb..d5a85e1 100644 --- a/internal/actions/timer.go +++ b/internal/actions/timer.go @@ -11,10 +11,10 @@ import ( // PunchIn starts a timer for the specified client/project // Use empty strings for client/project to use most recent entry -func (a *actions) PunchIn(ctx context.Context, client, project, description string, billableRate *float64) (*TimerSession, error) { +func (a *actions) PunchIn(ctx context.Context, client, project, description string, billableRate *float64, autoUnarchive bool) (*TimerSession, error) { // If no client specified, delegate to PunchInMostRecent if client == "" && project == "" { - session, err := a.PunchInMostRecent(ctx, description, billableRate) + session, err := a.PunchInMostRecent(ctx, description, billableRate, autoUnarchive) if err != nil { // Convert "no recent entries" error to "client required" for better UX if errors.Is(err, ErrNoRecentEntries) { @@ -73,6 +73,28 @@ func (a *actions) PunchIn(ctx context.Context, client, project, description stri return nil, ErrClientRequired } + // Check if client is archived + if resolvedClient.Archived != 0 { + if !autoUnarchive { + return nil, ErrArchivedClient + } + // Auto-unarchive the client + if err := a.UnarchiveClient(ctx, resolvedClient.ID); err != nil { + return nil, fmt.Errorf("failed to unarchive client: %w", err) + } + } + + // Check if project is archived + if resolvedProject != nil && resolvedProject.Archived != 0 { + if !autoUnarchive { + return nil, ErrArchivedProject + } + // Auto-unarchive the project + if err := a.UnarchiveProject(ctx, resolvedProject.ID); err != nil { + return nil, fmt.Errorf("failed to unarchive project: %w", err) + } + } + var stoppedEntryID *int64 // Check for identical timer if one is active @@ -119,7 +141,7 @@ func (a *actions) PunchIn(ctx context.Context, client, project, description stri } // PunchInMostRecent starts a timer copying the most recent time entry -func (a *actions) PunchInMostRecent(ctx context.Context, description string, billableRate *float64) (*TimerSession, error) { +func (a *actions) PunchInMostRecent(ctx context.Context, description string, billableRate *float64, autoUnarchive bool) (*TimerSession, error) { // Get most recent entry mostRecent, err := a.queries.GetMostRecentTimeEntry(ctx) if err != nil { @@ -135,6 +157,37 @@ func (a *actions) PunchInMostRecent(ctx context.Context, description string, bil finalDescription = mostRecent.Description.String } + // Get client to check if archived + client, err := a.FindClient(ctx, fmt.Sprintf("%d", mostRecent.ClientID)) + if err != nil { + return nil, fmt.Errorf("failed to get client: %w", err) + } + + // Check if client is archived + if client.Archived != 0 { + if !autoUnarchive { + return nil, ErrArchivedClient + } + // Auto-unarchive the client + if err := a.UnarchiveClient(ctx, client.ID); err != nil { + return nil, fmt.Errorf("failed to unarchive client: %w", err) + } + } + + // Check if project is archived (if exists) + if mostRecent.ProjectID.Valid { + project, err := a.FindProject(ctx, fmt.Sprintf("%d", mostRecent.ProjectID.Int64)) + if err == nil && project.Archived != 0 { + if !autoUnarchive { + return nil, ErrArchivedProject + } + // Auto-unarchive the project + if err := a.UnarchiveProject(ctx, project.ID); err != nil { + return nil, fmt.Errorf("failed to unarchive project: %w", err) + } + } + } + // Check if there's already an active timer activeEntry, err := a.queries.GetActiveTimeEntry(ctx) var hasActiveTimer bool @@ -148,13 +201,6 @@ func (a *actions) PunchInMostRecent(ctx context.Context, description string, bil // Check for identical timer if one is active if hasActiveTimer { if timeEntriesMatch(mostRecent.ClientID, mostRecent.ProjectID, finalDescription, billableRate, activeEntry) { - // Get client/project names for the result - client, _ := a.FindClient(ctx, fmt.Sprintf("%d", mostRecent.ClientID)) - clientName := "" - if client != nil { - clientName = client.Name - } - var projectName string if mostRecent.ProjectID.Valid { project, _ := a.FindProject(ctx, fmt.Sprintf("%d", mostRecent.ProjectID.Int64)) @@ -166,7 +212,7 @@ func (a *actions) PunchInMostRecent(ctx context.Context, description string, bil // No-op: identical timer already active return &TimerSession{ ID: activeEntry.ID, - ClientName: clientName, + ClientName: client.Name, ProjectName: projectName, Description: finalDescription, StartTime: activeEntry.StartTime, @@ -190,12 +236,6 @@ func (a *actions) PunchInMostRecent(ctx context.Context, description string, bil return nil, err } - // Get client name - client, err := a.FindClient(ctx, fmt.Sprintf("%d", mostRecent.ClientID)) - if err != nil { - return nil, fmt.Errorf("failed to get client name: %w", err) - } - // Get project name if exists var projectName string if mostRecent.ProjectID.Valid { diff --git a/internal/actions/types.go b/internal/actions/types.go index 899583b..caf3dc5 100644 --- a/internal/actions/types.go +++ b/internal/actions/types.go @@ -15,6 +15,8 @@ var ( ErrAmbiguousProject = errors.New("ambiguous project reference") ErrProjectClientMismatch = errors.New("project does not belong to specified client") ErrNoRecentEntries = errors.New("no previous time entries found") + ErrArchivedClient = errors.New("client is archived") + ErrArchivedProject = errors.New("project is archived") ) // TimerSession represents an active or completed time tracking session |