Skip to content

Commit

Permalink
Add vacation command
Browse files Browse the repository at this point in the history
  • Loading branch information
roughy committed Apr 25, 2019
1 parent 4af6bd1 commit fcd6391
Show file tree
Hide file tree
Showing 8 changed files with 269 additions and 3 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ $PATH (or %PATH% on windows).
2. run `mite config activity."your activity name here".projectId="the project id"`
3. run `mite config activity."your activity name here".serviceId=<the service id"`
4. the activity names can be used in the `entries create` and `entries edit` sub-commands
6. Optional: set a project & service for your vacation tracking by:
1. retrieving the desired project & service id by executing `mite projects` and `mite services` respectively
2. configuring those id's as default by executing `mite config vacation.projectId="the project id"` and `mite config vacation.serviceId="the service id"`

# Usage

Expand Down
158 changes: 158 additions & 0 deletions cmd/vacation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
package cmd

import (
"errors"
"fmt"
"github.com/leanovate/mite-go/domain"
"github.com/spf13/cobra"
"strconv"
)

const (
fullVacationDayDuration = 8
halfVacationDayDuration = fullVacationDayDuration / 2
entryListFilterAtThisYear = "this_year"
textProjectOrServiceNotConfigured = "please set both the vacation project AND service id (either via arguments or config)"
)

var (
vacationDetailsverbose bool
vacationNote string
vacationHalfDay bool
vacationFrom string
vacationTo string
)

func init() {
vacationDetailCommand.Flags().BoolVarP(&vacationDetailsverbose, "verbose", "v", false, "verbose output")
vacationCommand.AddCommand(vacationDetailCommand)

vacationCreateCommand.Flags().StringVarP(&vacationNote, "note", "n", "", "a note describing your vacation")
vacationCreateCommand.Flags().BoolVarP(&vacationHalfDay, "halfday", "d", false, "If set vacation is entered as half a day")
vacationCreateCommand.Flags().StringVarP(&vacationFrom, "from", "f", "", "create vacation starting at date (in YYYY-MM-DD format)")
vacationCreateCommand.Flags().StringVarP(&vacationTo, "to", "t", "", "create vacation ending at date (in YYYY-MM-DD format)")
vacationCommand.AddCommand(vacationCreateCommand)

rootCmd.AddCommand(vacationCommand)
}

var vacationCommand = &cobra.Command{
Use: "vacation",
Short: "manage your vacation",
RunE: vacationDetailCommand.RunE,
}

var vacationDetailCommand = &cobra.Command{
Use: "details",
Short: "show vacation statistics",
RunE: func(cmd *cobra.Command, args []string) error {
vacationActivity := application.Conf.GetVacation()

if vacationActivity.ServiceId == "" {
return errors.New(textProjectOrServiceNotConfigured)
}

entries, err := application.MiteApi.TimeEntries(&domain.TimeEntryQuery{
At: entryListFilterAtThisYear,
ServiceId: 285835, // => user config, if not set explain how
})
if err != nil {
return err
}

today := domain.Today()
var minutesInYear int
var minutesInPast int
var minutesInFuture int
for _, entry := range entries {
minutesInYear += entry.Minutes.Value()

if entry.Date.Before(today) {
minutesInPast += entry.Minutes.Value()
} else {
minutesInFuture += entry.Minutes.Value()
}
}

var daysInYear = domain.MinutesAsDays(minutesInYear, fullVacationDayDuration)
var daysInPast = domain.MinutesAsDays(minutesInPast, fullVacationDayDuration)
var daysInFuture = domain.MinutesAsDays(minutesInFuture, fullVacationDayDuration)
var daysUnplanned = 28 - daysInYear // => user config, if not set explain how

if vacationDetailsverbose {
fmt.Printf("Vacation statistics of %d:\n"+
" - total: %d days\n"+
"---------------------\n"+
" - booked: %.1f days\n"+
" - taken: %.1f days\n"+
" - planned: %.1f days\n"+
" - unplanned: %.1f days\n",
domain.ThisYear(),
28,
daysInYear,
daysInPast,
daysInFuture,
daysUnplanned)
} else {
fmt.Printf("Vacation statistics of %d:\n"+
" - booked: %.1f days\n"+
" - unplanned: %.1f days\n",
domain.ThisYear(),
daysInYear,
daysUnplanned)
}

return nil
},
}

var vacationCreateCommand = &cobra.Command{
Use: "create",
Short: "creates a vacation entry (WIP: currently this command creates a vacation day only for today)",
RunE: func(cmd *cobra.Command, args []string) error {
vacationActivity := application.Conf.GetVacation()

if vacationActivity.ProjectId == "" || vacationActivity.ServiceId == "" {
return errors.New(textProjectOrServiceNotConfigured)
}

projectId, err := strconv.Atoi(vacationActivity.ProjectId)
if err != nil {
return errors.New(textProjectOrServiceNotConfigured)
}

serviceId, err := strconv.Atoi(vacationActivity.ServiceId)
if err != nil {
return errors.New(textProjectOrServiceNotConfigured)
}

projectIdForVacation := domain.NewProjectId(projectId)
serviceIdForVacation := domain.NewServiceId(serviceId)
today := domain.Today()

minutes := domain.NewMinutesFromHours(fullVacationDayDuration)
if vacationHalfDay {
minutes = domain.NewMinutesFromHours(halfVacationDayDuration)
}

var dates []domain.LocalDate
dates = append(dates, today)

for _, date := range dates {
timeEntry := domain.TimeEntryCommand{
Date: &date,
Minutes: &minutes,
Note: vacationNote,
ProjectId: projectIdForVacation,
ServiceId: serviceIdForVacation,
}

_, err := application.MiteApi.CreateTimeEntry(&timeEntry)
if err != nil {
return err
}
}

return nil
},
}
10 changes: 10 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ type Config interface {
GetApiKey() string
GetActivity(activity string) Activity
GetDisplayLocation() *time.Location
GetVacation() Activity
Get(key string) string
Set(key string, value string)
PrintAll()
Expand Down Expand Up @@ -60,6 +61,15 @@ func (c *config) GetActivity(activity string) Activity {
}
}

func (c *config) GetVacation() Activity {
projectId := c.Get("vacation.projectId")
serviceId := c.Get("vacation.serviceId")
return Activity{
ProjectId: projectId,
ServiceId: serviceId,
}
}

func (c *config) GetDisplayLocation() *time.Location {
s := c.Get("display.location")
if s == "" {
Expand Down
20 changes: 18 additions & 2 deletions domain/datetime.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ type LocalDate struct {
time time.Time
}

func (d *LocalDate) Before(b LocalDate) bool {
return d.time.Before(b.time)
}

func NewLocalDate(t time.Time) LocalDate {
return LocalDate{time: t}
}
Expand All @@ -20,6 +24,10 @@ func Today() LocalDate {
return NewLocalDate(time.Now().Local())
}

func ThisYear() int {
return time.Now().Year()
}

func ParseLocalDate(s string) (LocalDate, error) {
t, err := time.ParseInLocation(ISO8601, s, time.Local)
if err != nil {
Expand All @@ -41,8 +49,12 @@ type Minutes struct {
duration time.Duration
}

func NewMinutes(i int) Minutes {
return Minutes{duration: time.Duration(i) * time.Minute}
func NewMinutes(minutes int) Minutes {
return Minutes{duration: time.Duration(minutes) * time.Minute}
}

func NewMinutesFromHours(hours int) Minutes {
return Minutes{duration: time.Duration(hours*60) * time.Minute}
}

func ParseMinutes(s string) (Minutes, error) {
Expand All @@ -61,3 +73,7 @@ func (m Minutes) Value() int {
func (m Minutes) String() string {
return strings.TrimSuffix(m.duration.String(), "0s")
}

func MinutesAsDays(minutes int, workingDayInHours float64) float64 {
return float64(minutes) / 60 / workingDayInHours
}
66 changes: 66 additions & 0 deletions domain/datetime_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,25 @@ func TestToday(t *testing.T) {
assert.Equal(t, expected, actual)
}

func TestBefore(t *testing.T) {
timeOlder := time.Date(1979, 10, 07, 14, 23, 17, 12, time.Local)
timeNewer := time.Date(2015, 10, 22, 12, 17, 19, 33, time.Local)

older := domain.NewLocalDate(timeOlder)
newer := domain.NewLocalDate(timeNewer)

assert.True(t, older.Before(newer))
assert.False(t, newer.Before(older))
assert.False(t, newer.Before(newer))
}

func TestThisYear(t *testing.T) {
expected := time.Now().Year()
actual := domain.ThisYear()

assert.Equal(t, expected, actual)
}

func TestParseLocalDate(t *testing.T) {
expected := domain.NewLocalDate(time.Date(1970, time.January, 1, 0, 0, 0, 0, time.Local))
actual, err := domain.ParseLocalDate("1970-01-01")
Expand Down Expand Up @@ -71,3 +90,50 @@ func TestMinutes_String(t *testing.T) {

assert.Equal(t, expected, actual)
}

func TestMinutesFromHours_Value(t *testing.T) {
expected := 60
actual := domain.NewMinutesFromHours(1).Value()

assert.Equal(t, expected, actual)

expected = 480
actual = domain.NewMinutesFromHours(8).Value()

assert.Equal(t, expected, actual)
}

func TestMinutesFromHours_String(t *testing.T) {
expected := "1h0m"
actual := domain.NewMinutesFromHours(1).String()

assert.Equal(t, expected, actual)

expected = "8h0m"
actual = domain.NewMinutesFromHours(8).String()

assert.Equal(t, expected, actual)
}

func TestMinutesAsDays(t *testing.T) {
workingDayInHours := 8.0
minutes := 480
expected := 1.0
actual := domain.MinutesAsDays(minutes, workingDayInHours)

assert.Equal(t, expected, actual)

workingDayInHours = 8.0
minutes = 240
expected = 0.5
actual = domain.MinutesAsDays(minutes, workingDayInHours)

assert.Equal(t, expected, actual)

workingDayInHours = 8.0
minutes = 160
expected = 0.3333333333333333
actual = domain.MinutesAsDays(minutes, workingDayInHours)

assert.Equal(t, expected, actual)
}
2 changes: 2 additions & 0 deletions domain/time_entry.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,11 @@ type TimeEntryCommand struct {
}

type TimeEntryQuery struct {
At string
From *LocalDate
To *LocalDate
Direction string
ServiceId ServiceId
}

type TimeEntryApi interface {
Expand Down
6 changes: 6 additions & 0 deletions mite/time_entry.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ func fromCommand(c *domain.TimeEntryCommand) *timeEntryRequest {
func fromQuery(q *domain.TimeEntryQuery) url.Values {
v := url.Values{}
if q != nil {
if q.At != "" {
v.Add("at", q.At)
}
if q.From != nil {
v.Add("from", q.From.String())
}
Expand All @@ -36,6 +39,9 @@ func fromQuery(q *domain.TimeEntryQuery) url.Values {
if q.Direction != "" {
v.Add("direction", q.Direction)
}
if q.ServiceId != 0 {
v.Add("service_id", q.ServiceId.String())
}
}

return v
Expand Down
7 changes: 6 additions & 1 deletion mite/time_entry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,9 +109,11 @@ func TestApi_TimeEntries_WithQuery(t *testing.T) {
// when
today := domain.Today()
query := &domain.TimeEntryQuery{
At: "this_year",
From: &today,
To: &today,
Direction: "asc",
ServiceId: timeEntryObject.ServiceId,
}
timeEntries, err := api.TimeEntries(query)

Expand All @@ -120,7 +122,10 @@ func TestApi_TimeEntries_WithQuery(t *testing.T) {
assert.Equal(t, []*domain.TimeEntry{&timeEntryObject}, timeEntries)

assert.Equal(t, http.MethodGet, rec.RequestMethod())
assert.Equal(t, fmt.Sprintf("/time_entries.json?direction=%s&from=%s&to=%s", query.Direction, query.From, query.To), rec.RequestURI())
assert.Equal(t, fmt.Sprintf(
"/time_entries.json?at=%s&direction=%s&from=%s&service_id=%s&to=%s",
query.At, query.Direction, query.From, query.ServiceId, query.To),
rec.RequestURI())
assert.Empty(t, rec.RequestContentType())
assert.Equal(t, testUserAgent, rec.RequestUserAgent())
assert.Equal(t, testApiKey, rec.RequestMiteKey())
Expand Down

0 comments on commit fcd6391

Please sign in to comment.