diff options
Diffstat (limited to 'internal/commands/in.go')
-rw-r--r-- | internal/commands/in.go | 193 |
1 files changed, 29 insertions, 164 deletions
diff --git a/internal/commands/in.go b/internal/commands/in.go index abb57f1..e7847f6 100644 --- a/internal/commands/in.go +++ b/internal/commands/in.go @@ -1,15 +1,10 @@ package commands import ( - "context" - "database/sql" - "errors" "fmt" - "strconv" - "time" + "punchcard/internal/actions" punchctx "punchcard/internal/context" - "punchcard/internal/queries" "github.com/spf13/cobra" ) @@ -43,122 +38,54 @@ Examples: } billableRateFloat, _ := cmd.Flags().GetFloat64("hourly-rate") - billableRate := int64(billableRateFloat * 100) // Convert dollars to cents + var billableRate *float64 + if billableRateFloat > 0 { + billableRate = &billableRateFloat + } q := punchctx.GetDB(cmd.Context()) if q == nil { return fmt.Errorf("database not available in context") } - // Check if there's already an active timer - activeEntry, err := q.GetActiveTimeEntry(cmd.Context()) - var hasActiveTimer bool - if err != nil && !errors.Is(err, sql.ErrNoRows) { - return fmt.Errorf("failed to check for active timer: %w", err) - } - hasActiveTimer = (err == nil) - - // Validate and get project first (if provided) - var project queries.Project - var projectID sql.NullInt64 - if projectFlag != "" { - proj, err := findProject(cmd.Context(), q, projectFlag) - if err != nil { - return fmt.Errorf("invalid project: %w", err) - } - project = proj - projectID = sql.NullInt64{Int64: project.ID, Valid: true} - } + a := actions.New(q) + + // Use the actions package based on what flags were provided + var session *actions.TimerSession + var err error - // Validate and get client - var clientID int64 - if clientFlag != "" { - client, err := findClient(cmd.Context(), q, clientFlag) - if err != nil { - return fmt.Errorf("invalid client: %w", err) - } - clientID = client.ID - - // If project is specified, verify it belongs to this client - if projectID.Valid && project.ClientID != clientID { - return fmt.Errorf("project %q does not belong to client %q", projectFlag, clientFlag) - } - } else if projectID.Valid { - clientID = project.ClientID - } else if clientFlag == "" && projectFlag == "" { - mostRecentEntry, err := q.GetMostRecentTimeEntry(cmd.Context()) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - return fmt.Errorf("no previous time entries found - client is required for first entry: use -c/--client flag") - } - return fmt.Errorf("failed to get most recent time entry: %w", err) - } - - clientID = mostRecentEntry.ClientID - projectID = mostRecentEntry.ProjectID - if description == "" && mostRecentEntry.Description.Valid { - description = mostRecentEntry.Description.String - } + if clientFlag == "" && projectFlag == "" { + // Use most recent entry + session, err = a.PunchInMostRecent(cmd.Context(), description, billableRate) } else { - return fmt.Errorf("client is required: use -c/--client flag to specify client") + // Use specified client/project + session, err = a.PunchIn(cmd.Context(), clientFlag, projectFlag, description, billableRate) } - if hasActiveTimer { - // Check if the new timer would be identical to the active one - if timeEntriesMatch(clientID, projectID, description, activeEntry) { - // No-op: identical timer already active - cmd.Printf("Timer already active with same parameters (ID: %d)\n", activeEntry.ID) - return nil - } - - // Stop the active timer before starting new one - stoppedEntry, err := q.StopTimeEntry(cmd.Context()) - if err != nil { - return fmt.Errorf("failed to stop active timer: %w", err) - } - - duration := stoppedEntry.EndTime.Time.Sub(stoppedEntry.StartTime) - cmd.Printf("Stopped previous timer (ID: %d). Duration: %v\n", - stoppedEntry.ID, duration.Round(time.Second)) + if err != nil { + return err } - // Create time entry - var descParam sql.NullString - if description != "" { - descParam = sql.NullString{String: description, Valid: true} + // Handle different response types + if session.WasNoOp { + cmd.Printf("Timer already active with same parameters (ID: %d)\n", session.ID) + return nil } - var billableRateParam sql.NullInt64 - if billableRate > 0 { - billableRateParam = sql.NullInt64{Int64: billableRate, Valid: true} - } - - timeEntry, err := q.CreateTimeEntry(cmd.Context(), queries.CreateTimeEntryParams{ - Description: descParam, - ClientID: clientID, - ProjectID: projectID, - BillableRate: billableRateParam, - }) - if err != nil { - return fmt.Errorf("failed to create time entry: %w", err) + // Print stopped timer message if we stopped one + if session.StoppedEntryID != nil { + cmd.Printf("Stopped previous timer (ID: %d)\n", *session.StoppedEntryID) } // Build output message - output := fmt.Sprintf("Started timer (ID: %d)", timeEntry.ID) - - // Add client info - client, _ := findClient(cmd.Context(), q, strconv.FormatInt(clientID, 10)) - output += fmt.Sprintf(" for client: %s", client.Name) + output := fmt.Sprintf("Started timer (ID: %d) for client: %s", session.ID, session.ClientName) - // Add project info if provided - if projectID.Valid { - project, _ := findProject(cmd.Context(), q, strconv.FormatInt(projectID.Int64, 10)) - output += fmt.Sprintf(", project: %s", project.Name) + if session.ProjectName != "" { + output += fmt.Sprintf(", project: %s", session.ProjectName) } - // Add description if provided - if description != "" { - output += fmt.Sprintf(", description: %s", description) + if session.Description != "" { + output += fmt.Sprintf(", description: %s", session.Description) } cmd.Print(output + "\n") @@ -172,65 +99,3 @@ Examples: return cmd } - -func findProject(ctx context.Context, q *queries.Queries, projectRef string) (queries.Project, error) { - // Parse projectRef as ID if possible, otherwise use 0 - var idParam int64 - if id, err := strconv.ParseInt(projectRef, 10, 64); err == nil { - idParam = id - } - - // Search by both ID and name using UNION ALL - projects, err := q.FindProject(ctx, queries.FindProjectParams{ - ID: idParam, - Name: projectRef, - }) - if err != nil { - return queries.Project{}, fmt.Errorf("database error looking up project: %w", err) - } - - // Check results - switch len(projects) { - case 0: - return queries.Project{}, fmt.Errorf("project not found: %s", projectRef) - case 1: - return projects[0], nil - default: - return queries.Project{}, fmt.Errorf("ambiguous project: %s", projectRef) - } -} - -// timeEntriesMatch checks if a new time entry would be identical to an active one -// by comparing client ID, project ID, and description -func timeEntriesMatch(clientID int64, projectID sql.NullInt64, description string, activeEntry queries.TimeEntry) bool { - // Client must match - if activeEntry.ClientID != clientID { - return false - } - - // Check project ID matching - if projectID.Valid != activeEntry.ProjectID.Valid { - // One has a project, the other doesn't - return false - } - if projectID.Valid { - // Both have projects - compare IDs - if activeEntry.ProjectID.Int64 != projectID.Int64 { - return false - } - } - - // Check description matching - if (description != "") != activeEntry.Description.Valid { - // One has description, the other doesn't - return false - } - if activeEntry.Description.Valid { - // Both have descriptions - compare strings - if activeEntry.Description.String != description { - return false - } - } - - return true -} |