Skip to content
This repository was archived by the owner on May 20, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
85 changes: 55 additions & 30 deletions pkg/cmd/time/create/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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 {
Expand All @@ -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)
Expand All @@ -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)
}