summaryrefslogtreecommitdiff
path: root/internal/actions
diff options
context:
space:
mode:
Diffstat (limited to 'internal/actions')
-rw-r--r--internal/actions/actions.go8
-rw-r--r--internal/actions/archive_test.go618
-rw-r--r--internal/actions/clients.go16
-rw-r--r--internal/actions/projects.go16
-rw-r--r--internal/actions/timer.go74
-rw-r--r--internal/actions/types.go2
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