From e062eb34bc8e89adf58a7e960b2d53ef2d67faad Mon Sep 17 00:00:00 2001 From: Tim Wehrle Date: Tue, 30 Sep 2025 22:02:35 +0200 Subject: [PATCH 1/2] feat: update create command with flags and avoid date prompt for better ux --- pkg/cmd/time/create/create.go | 85 ++++++++++++++++++++++------------- 1 file changed, 55 insertions(+), 30 deletions(-) diff --git a/pkg/cmd/time/create/create.go b/pkg/cmd/time/create/create.go index b54c2de..519db4a 100644 --- a/pkg/cmd/time/create/create.go +++ b/pkg/cmd/time/create/create.go @@ -5,6 +5,7 @@ import ( "strconv" "time" + "github.com/MakeNowJust/heredoc" "github.com/spf13/cobra" "github.com/timwehrle/asana/internal/api/asana" "github.com/timwehrle/asana/pkg/cmdutils" @@ -15,6 +16,10 @@ import ( type CreateOptions struct { cmdutils.BaseOptions + + Minutes int + DateStr string + Date *asana.Date } func NewCmdCreate(f factory.Factory, runF func(*CreateOptions) error) *cobra.Command { @@ -29,8 +34,15 @@ func NewCmdCreate(f factory.Factory, runF func(*CreateOptions) error) *cobra.Com cmd := &cobra.Command{ Use: "create", - Short: "Create a new time entry for a task", - Long: "Create and log a new time entry on a selected Asana task.", + Short: "Log time to a task", + Long: "Record a new time entry on a selected Asana task.", + Example: heredoc.Doc(` + # Log time via flags + asana time create --minutes 30 --date 2025-01-06 + + # Log time interactively + asana time create --date 2025-01-06 + `), RunE: func(cmd *cobra.Command, args []string) error { if runF == nil { return runCreate(opts) @@ -40,68 +52,81 @@ func NewCmdCreate(f factory.Factory, runF func(*CreateOptions) error) *cobra.Com }, } + cmd.Flags().IntVarP(&opts.Minutes, "minutes", "m", 0, "Minutes to log (prompted if not set)") + cmd.Flags().StringVar(&opts.DateStr, "date", "", "Entry date (YYYY-MM-DD, defaults to today)") + return cmd } -func runCreate(opts *CreateOptions) error { - io := opts.IO - cs := io.ColorScheme() +func (o *CreateOptions) Validate() error { + if o.Minutes < 0 { + return fmt.Errorf("minutes must be zero or a positive integer") + } - client, err := opts.Client() - if err != nil { - return err + if o.DateStr != "" { + date, err := convert.ToDate(o.DateStr, time.DateOnly) + if err != nil { + return fmt.Errorf("invalid date: %w", err) + } + o.Date = date + } else { + today := asana.Date(time.Now()) + o.Date = &today } + return nil +} - task, err := cmdutils.SelectTask(&opts.BaseOptions, client) - if err != nil { +func runCreate(opts *CreateOptions) error { + if err := opts.Validate(); err != nil { return err } - minutes, err := promptDuration(opts) + client, err := opts.Client() if err != nil { return err } - date, err := promptDate(opts) + task, err := cmdutils.SelectTask(&opts.BaseOptions, client) if err != nil { return err } + var minutes int + if opts.Minutes > 0 { + minutes = opts.Minutes + } else { + minutes, err = promptDuration(opts) + if err != nil { + return err + } + } + result, err := task.CreateTimeTrackingEntry(client, &asana.CreateTimeTrackingEntryRequest{ DurationMinutes: minutes, - EnteredOn: date, + EnteredOn: opts.Date, }) if err != nil { return fmt.Errorf("failed to create time tracking entry: %w", err) } - io.Printf("%s Logged %s to %q (%s)\n", cs.SuccessIcon, format.Duration(result.DurationMinutes), task.Name, format.HumanDate(*result.CreatedAt)) + opts.IO.Printf("%s Logged %s to %q on %s\n", + opts.IO.ColorScheme().SuccessIcon, + format.Duration(result.DurationMinutes), + task.Name, + format.Date(result.EnteredOn), + ) return nil } func promptDuration(opts *CreateOptions) (int, error) { - minutesStr, err := opts.Prompter.Input("How many minutes do you want to log? (e.g., 30)", "") + minutesStr, err := opts.Prompter.Input("Enter minutes to log (e.g., 30):", "") if err != nil { return 0, fmt.Errorf("failed to read duration: %w", err) } minutes, err := strconv.Atoi(minutesStr) if err != nil || minutes <= 0 { - return 0, fmt.Errorf("invalid duration: must be a positive number") + return 0, fmt.Errorf("invalid input: please enter a positive number") } return minutes, nil } - -func promptDate(opts *CreateOptions) (*asana.Date, error) { - input, err := opts.Prompter.Input("Enter date [YYYY-MM-DD] or leave empty for today:", "") - if err != nil { - return nil, fmt.Errorf("failed to read date: %w", err) - } - - if input == "" { - today := asana.Date(time.Now()) - return &today, nil - } - - return convert.ToDate(input, time.DateOnly) -} From ce5480d6b000e83820cd9d546341db0fa2d983f7 Mon Sep 17 00:00:00 2001 From: Tim Wehrle Date: Tue, 30 Sep 2025 22:02:53 +0200 Subject: [PATCH 2/2] docs: update readme to show new time entry commands --- README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.md b/README.md index 5145087..0fcc62e 100644 --- a/README.md +++ b/README.md @@ -110,6 +110,19 @@ View tasks with filters: asana tasks search --assignee me,12345678 # Search tasks by assignee and more filters ``` +Log, check and delete time entries on your tasks: + +```shell +# Create a new time entry +asana time create -m 23 --date 2025-01-06 + +# Check time entries +asana time status + +# Delete a time entry +asana time delete +``` + View the projects in your workspace: ```shell