Skip to content

Commit

Permalink
commands/report: start
Browse files Browse the repository at this point in the history
Signed-off-by: Sumner Evans <me@sumnerevans.com>
  • Loading branch information
sumnerevans committed Mar 4, 2023
1 parent ee889ab commit 76e3afb
Show file tree
Hide file tree
Showing 20 changed files with 258 additions and 52 deletions.
16 changes: 8 additions & 8 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,24 +16,22 @@ repos:
- id: poetry-check

# black
- repo: local
- repo: https://github.com/psf/black
rev: 23.1.0
hooks:
- id: black
name: black
files: ^(tracktime|tests|examples)/.*\.py$
entry: black --check
language: system

# isort
- repo: https://github.com/PyCQA/isort
rev: 5.10.1
rev: 5.12.0
hooks:
- id: isort
files: ^(tracktime|tests|examples)/.*\.py$

# flake8
- repo: https://gitlab.com/pycqa/flake8
rev: 4.0.1
- repo: https://github.com/pycqa/flake8
rev: 6.0.0
hooks:
- id: flake8
files: ^(tracktime|tests|examples)/.*\.py$
Expand All @@ -55,7 +53,9 @@ repos:
- id: custom-style-check
name: custom style check
entry: ./cicd/custom_style_check.py
language: system
language: python
additional_dependencies:
- termcolor==2.1.1

- repo: https://github.com/tekwizely/pre-commit-golang
rev: v1.0.0-beta.5
Expand Down
1 change: 1 addition & 0 deletions commands/edit.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"runtime"

"github.com/rs/zerolog/log"

"github.com/sumnerevans/tracktime/lib"
)

Expand Down
4 changes: 2 additions & 2 deletions commands/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import (
)

type List struct {
Date lib.Date `arg:"-d,--date" help:"the date to list time entries for" default:"today"`
Customer string `arg:"-c,--customer" help:"list only time entries for the given customer"`
Date lib.Date `arg:"-d,--date" help:"the date to list time entries for" default:"today"`
Customer lib.Customer `arg:"-c,--customer" help:"list only time entries for the given customer"`
}

func (l *List) Run(config *lib.Config) error {
Expand Down
95 changes: 92 additions & 3 deletions commands/report.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ package commands

import (
"fmt"
"time"

"github.com/rs/zerolog/log"

"github.com/sumnerevans/tracktime/lib"
)
Expand Down Expand Up @@ -49,8 +52,8 @@ type Report struct {
NoDescriptionGrain bool `arg:"--no-descriptiongrain" help:"do not report on the task grain"`

// Narrow the set of time entries to report on
Customer string `arg:"-c,--customer" help:"customer ID to generate a report for"`
Project string `arg:"-p,--project" help:"project name to generate a report for"`
Customer lib.Customer `arg:"-c,--customer" help:"customer ID to generate a report for"`
Project lib.Project `arg:"-p,--project" help:"project name to generate a report for"`

// How to sort the report
Sort Sort `arg:"-s,--sort" help:"the grain to sort the report by (alphabetical,alpha,a or time-spent,time,t)" default:"alphabetical"`
Expand All @@ -61,6 +64,92 @@ type Report struct {
OutputFile lib.Filename `arg:"-o,--outfile" help:"specify the filename to export the report to (supports PDF, HTML, and RST files, if set to '-' then the report is printed to stdout)" default:"-"`
}

func (s *Report) Run(config *lib.Config) error {
func (r *Report) Run(config *lib.Config) error {
var start, end lib.Date
switch {
case r.Today:
start = lib.Today()
end = lib.Today()
case r.Yesterday:
start = lib.Today().AddDays(-1)
end = lib.Today().AddDays(-1)
case r.ThisWeek:
start = lib.Today().AddDays(-int(lib.Today().Weekday()))
end = lib.Today().AddDays(6 - int(lib.Today().Weekday()))
case r.LastWeek:
start = lib.Today().AddDays(-int(lib.Today().Weekday()) - 7)
end = lib.Today().AddDays(6 - int(lib.Today().Weekday()) - 7)
case r.ThisMonth:
start = lib.Today().AddDays(1 - int(lib.Today().Day()))
end = lib.Today().AddMonths(1).AddDays(-int(lib.Today().AddMonths(1).Day()))
case r.ThisYear:
start = lib.NewDate(lib.Today().Year(), 1, 1)
end = lib.NewDate(lib.Today().Year(), 12, 31)
case r.LastYear:
start = lib.NewDate(lib.Today().Year()-1, 1, 1)
end = lib.NewDate(lib.Today().Year()-1, 12, 31)
default: // Last month
start = lib.Today().AddMonths(-1).AddDays(1 - int(lib.Today().AddMonths(-1).Day()))
end = lib.Today().AddDays(-int(lib.Today().Day()))
}
if r.Start != nil {
start = *r.Start
}
if r.End != nil {
end = *r.End
}

if start.Equal(time.Time{}) {
return fmt.Errorf("start date is required")
}
if end.Equal(time.Time{}) {
return fmt.Errorf("end date is required")
}
if start.After(end.Time) {
return fmt.Errorf("start date must be before end date")
}

aggregatedTime := map[lib.Customer]map[lib.Project]map[lib.TaskID]map[string][]*lib.TimeEntry{}
dayStats := map[lib.Date]time.Duration{}

for day := start; day.Before(end.Time) || day.Equal(end.Time); day = day.AddDays(1) {
entryList, err := lib.EntryListForDay(config, day)
if err != nil {
log.Fatal().Err(err).Msg("Failed to read entry list")
}

for _, entry := range entryList.EntriesForCustomer(r.Customer) {
if r.Project != "" && entry.Project != r.Project {
continue
}

if _, ok := dayStats[day]; !ok {
dayStats[day] = time.Duration(0)
}
duration, err := entry.Duration(false)
if err != nil {
return fmt.Errorf("unended time entry on %s", day.Time.Format("2006-01-02"))
}
dayStats[day] = dayStats[day] + duration

if _, ok := aggregatedTime[entry.Customer]; !ok {
aggregatedTime[entry.Customer] = map[lib.Project]map[lib.TaskID]map[string][]*lib.TimeEntry{}
}
if _, ok := aggregatedTime[entry.Customer][entry.Project]; !ok {
aggregatedTime[entry.Customer][entry.Project] = map[lib.TaskID]map[string][]*lib.TimeEntry{}
}
if _, ok := aggregatedTime[entry.Customer][entry.Project][entry.TaskID]; !ok {
aggregatedTime[entry.Customer][entry.Project][entry.TaskID] = map[string][]*lib.TimeEntry{}
}
if _, ok := aggregatedTime[entry.Customer][entry.Project][entry.TaskID][entry.Description]; !ok {
aggregatedTime[entry.Customer][entry.Project][entry.TaskID][entry.Description] = append(aggregatedTime[entry.Customer][entry.Project][entry.TaskID][entry.Description], entry)
}

fmt.Printf(" ENTRY %v\n", entry)
}
}

fmt.Printf("REPORT %v\n", aggregatedTime)

return nil
}
1 change: 1 addition & 0 deletions commands/resume.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,6 @@ func (s *Resume) Run(config *lib.Config) error {
if err != nil {
return err
}
// TODO sync
return entryList.Resume(s.Entry, s.Description, s.Start)
}
7 changes: 4 additions & 3 deletions commands/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,16 @@ type Start struct {
Description string `arg:"positional" placeholder:"DESC" help:"the descripiton of the time entry"`
Start *lib.Time `arg:"-s,--start" help:"the start time of the time entry" default:"now"`
Type lib.TimeEntryType `arg:"-t,--type" help:"the type of the time entry"`
Project string `arg:"-p,--project" help:"the project of the time entry"`
Customer string `arg:"-c,--customer" help:"the customer of the time entry"`
TaskID string `arg:"-i,--taskid" help:"the task ID of the time entry"`
Project lib.Project `arg:"-p,--project" help:"the project of the time entry"`
Customer lib.Customer `arg:"-c,--customer" help:"the customer of the time entry"`
TaskID lib.TaskID `arg:"-i,--taskid" help:"the task ID of the time entry"`
}

func (s *Start) Run(config *lib.Config) error {
entryList, err := lib.EntryListForDay(config, lib.Today())
if err != nil {
return err
}
// TODO sync
return entryList.Start(s.Start, s.Description, s.Type, s.Project, s.Customer, s.TaskID)
}
1 change: 1 addition & 0 deletions commands/stop.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@ func (s *Stop) Run(config *lib.Config) error {
if err != nil {
return err
}
// TODO sync
return entryList.Stop(s.Stop)
}
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,15 @@ require (
github.com/fatih/color v1.13.0
github.com/rodaine/table v1.0.1
github.com/rs/zerolog v1.28.0
github.com/stretchr/testify v1.7.0
gopkg.in/yaml.v3 v3.0.1
)

require (
github.com/alexflint/go-scalar v1.2.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6 // indirect
)
32 changes: 26 additions & 6 deletions lib/date.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,43 @@ type Date struct {
time.Time
}

func NewDate(year, month, day int) Date {
return Date{Time: time.Date(year, time.Month(month), day, 0, 0, 0, 0, time.Local)}
}

func NewDateFromTime(t time.Time) Date {
return Date{Time: t}
}

func Today() Date {
return Date{Time: time.Now()}
return NewDateFromTime(time.Now())
}

func (d *Date) UnmarshalText(text []byte) error {
switch string(text) {
case "today":
d.Time = time.Now()
d.Time = time.Now().Local()
case "yesterday":
d.Time = time.Now().AddDate(0, 0, -1)
d.Time = time.Now().Local().AddDate(0, 0, -1)
default:
// TODO
return fmt.Errorf("Invalid date '%s'.", string(text))
// formatsWithoutYear := []string{"01", "January", "Jan", "1"}
// for _, format := range formats {
// parsed, err := time.Parse(format, string(text))
// if err == nil {
// d.year = now.Year()
// d.month = parsed.Month()
// return nil
// }
// }
return fmt.Errorf("invalid date '%s'", string(text))
}
return nil
}

func (d *Date) AddDays(numDays int) Date {
func (d Date) AddDays(numDays int) Date {
return Date{Time: d.Time.AddDate(0, 0, numDays)}
}

func (d Date) AddMonths(numMonths int) Date {
return Date{Time: d.Time.AddDate(0, numMonths, 0)}
}
34 changes: 19 additions & 15 deletions lib/entrylist.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,18 @@ func ParseTimeEntryType(typeStr string) TimeEntryType {
}
}

type Customer string
type Project string
type TaskID string

type TimeEntry struct {
Index int
Start *Time
Stop *Time
Type TimeEntryType
Project string
Customer string
TaskID string
Project Project
Customer Customer
TaskID TaskID
Description string
}

Expand All @@ -45,9 +49,9 @@ func NewEntryFromRecord(idx int, record []string) (te *TimeEntry, err error) {
return
}
te.Type = ParseTimeEntryType(record[2])
te.Project = record[3]
te.TaskID = record[4]
te.Customer = record[5]
te.Project = Project(record[3])
te.TaskID = TaskID(record[4])
te.Customer = Customer(record[5])
te.Description = record[6]
return
}
Expand All @@ -58,7 +62,7 @@ func (te *TimeEntry) Duration(allowUnended bool) (time.Duration, error) {
if allowUnended {
stop = CurrentTime()
} else {
return time.Duration(0), fmt.Errorf("Unended time entries cannot have a duration.")
return time.Duration(0), fmt.Errorf("unended time entries cannot have a duration")
}
}
return stop.Sub(te.Start), nil
Expand Down Expand Up @@ -112,7 +116,7 @@ func EntryListForDay(config *Config, date Date) (*EntryList, error) {
return &el, nil
}

func (el *EntryList) EntriesForCustomer(customer string) []*TimeEntry {
func (el *EntryList) EntriesForCustomer(customer Customer) []*TimeEntry {
if len(customer) == 0 {
return el.entries
}
Expand All @@ -126,7 +130,7 @@ func (el *EntryList) EntriesForCustomer(customer string) []*TimeEntry {
return entries
}

func (el *EntryList) TotalTimeForCustomer(customer string) time.Duration {
func (el *EntryList) TotalTimeForCustomer(customer Customer) time.Duration {
minutes := time.Duration(0)
for _, e := range el.entries {
if len(customer) == 0 || e.Customer == customer {
Expand Down Expand Up @@ -193,9 +197,9 @@ func (el *EntryList) Save() error {
e.Start.String(),
e.Stop.String(),
string(e.Type),
e.Project,
e.TaskID,
e.Customer,
string(e.Project),
string(e.TaskID),
string(e.Customer),
e.Description,
})
if err != nil {
Expand All @@ -219,7 +223,7 @@ func (el *EntryList) SaveAndSync() error {
return el.Sync()
}

func (el *EntryList) Start(start *Time, description string, taskType TimeEntryType, project, customer, taskID string) error {
func (el *EntryList) Start(start *Time, description string, taskType TimeEntryType, project Project, customer Customer, taskID TaskID) error {
newEntry := &TimeEntry{
Start: start,
Type: taskType,
Expand All @@ -234,7 +238,7 @@ func (el *EntryList) Start(start *Time, description string, taskType TimeEntryTy

func (el *EntryList) Stop(stop *Time) error {
if len(el.entries) == 0 || el.entries[len(el.entries)-1].Stop != nil {
return fmt.Errorf("No time entry to stop.")
return fmt.Errorf("no time entry to stop")
}
el.entries[len(el.entries)-1].Stop = stop
return el.SaveAndSync()
Expand All @@ -251,7 +255,7 @@ func (el *EntryList) Resume(resumeIndex int, description *string, start *Time) e
return err
}
if len(yesterdayEntries.entries) == 0 {
return fmt.Errorf("No time entry to resume.")
return fmt.Errorf("no time entry to resume")
}
oldEntry = yesterdayEntries.entries[len(yesterdayEntries.entries)-1]
}
Expand Down
Loading

0 comments on commit 76e3afb

Please sign in to comment.