summaryrefslogtreecommitdiff
path: root/internal/commands/in.go
diff options
context:
space:
mode:
authorT <t@tjp.lol>2025-08-05 11:37:02 -0600
committerT <t@tjp.lol>2025-08-05 11:37:08 -0600
commit665bd389a0a1c8adadcaa1122e846cc81f5ead31 (patch)
treef34f9ec77891308c600c680683f60951599429c3 /internal/commands/in.go
parentdc895cec9d8a84af89ce2501db234dff33c757e2 (diff)
WIP TUI
Diffstat (limited to 'internal/commands/in.go')
-rw-r--r--internal/commands/in.go193
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
-}