~ghost08/wt

07993237c79460f6d81fad83ba2b8aed31788c70 — ghost08 2 years ago 4670482
Add report to xlsx
4 files changed, 320 insertions(+), 6 deletions(-)

M go.mod
M go.sum
M main.go
A report.go
M go.mod => go.mod +8 -1
@@ 2,4 2,11 @@ module git.sr.ht/~ghost08/wt

go 1.16

require github.com/alecthomas/kong v0.2.16
require (
	github.com/360EntSecGroup-Skylar/excelize v1.4.1
	github.com/360EntSecGroup-Skylar/excelize/v2 v2.4.0 // indirect
	github.com/alecthomas/kong v0.2.16
	github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
	github.com/pkg/errors v0.9.1 // indirect
	github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
)

M go.sum => go.sum +36 -0
@@ 1,7 1,43 @@
github.com/360EntSecGroup-Skylar/excelize v1.4.1 h1:l55mJb6rkkaUzOpSsgEeKYtS6/0gHwBYyfo5Jcjv/Ks=
github.com/360EntSecGroup-Skylar/excelize v1.4.1/go.mod h1:vnax29X2usfl7HHkBrX5EvSCJcmH3dT9luvxzu8iGAE=
github.com/360EntSecGroup-Skylar/excelize/v2 v2.4.0 h1:X+2CWGf5W1tm2+W7Y/LLrAPLFSNlHATnqDudGoIzaxY=
github.com/360EntSecGroup-Skylar/excelize/v2 v2.4.0/go.mod h1:p9lGPoVX3HYEbFRfjgrPWaaKsHe/2u4EM9DB/qoctgU=
github.com/alecthomas/kong v0.2.16 h1:F232CiYSn54Tnl1sJGTeHmx4vJDNLVP2b9yCVMOQwHQ=
github.com/alecthomas/kong v0.2.16/go.mod h1:kQOmtJgV+Lb4aj+I2LEn40cbtawdWJ9Y8QLq+lElKxE=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/richardlehane/mscfb v1.0.3 h1:rD8TBkYWkObWO0oLDFCbwMeZ4KoalxQy+QgniCj3nKI=
github.com/richardlehane/mscfb v1.0.3/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk=
github.com/richardlehane/msoleps v1.0.1 h1:RfrALnSNXzmXLbGct/P2b4xkFz4e8Gmj/0Vj9M9xC1o=
github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA=
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.2.3-0.20181224173747-660f15d67dbb/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/xuri/efp v0.0.0-20210322160811-ab561f5b45e3 h1:EpI0bqf/eX9SdZDwlMmahKM+CDBgNbsXMhsN28XrM8o=
github.com/xuri/efp v0.0.0-20210322160811-ab561f5b45e3/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
golang.org/x/crypto v0.0.0-20210415154028-4f45737414dc h1:+q90ECDSAQirdykUN6sPEiBXBsp8Csjcca8Oy7bgLTA=
golang.org/x/crypto v0.0.0-20210415154028-4f45737414dc/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210415231046-e915ea6b2b7d h1:BgJvlyh+UqCUaPlscHJ+PN8GcpfrFdr7NHjd1JL0+Gs=
golang.org/x/net v0.0.0-20210415231046-e915ea6b2b7d/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

M main.go => main.go +23 -5
@@ 162,8 162,30 @@ func status() error {
			time.Now().Sub(last.Start),
		)
	}
	y, m, d := time.Now().Date()
	var todayWorked time.Duration
	for _, e := range es {
		if ey, em, ed := e.Date.Date(); ey != y || em != m || ed != d {
			break
		}
		end := e.End
		if end == zeroTime {
			end = time.Now()
		}
		todayWorked += end.Sub(e.Start)
	}
	fmt.Printf("today worked:\t%s\n", todayWorked)
	for _, e := range es {
		if ey, em, ed := e.Date.Date(); ey != y || em != m || ed != d {
			break
		}
		end := e.End
		if end == zeroTime {
			end = time.Now()
		}
		fmt.Printf("%s\t%s\t%s\n", e.Project, e.Description, end.Sub(e.Start))
	}
	var thisMonthWorked time.Duration
	y, m, _ := time.Now().Date()
	for _, e := range es {
		if ey, em, _ := e.Date.Date(); ey != y || em != m {
			break


@@ 177,7 199,3 @@ func status() error {
	fmt.Printf("this month:\t%s\n", thisMonthWorked)
	return nil
}

func report() error {
	return nil
}

A report.go => report.go +253 -0
@@ 0,0 1,253 @@
package main

import (
	"fmt"
	"os"
	"sort"
	"time"

	"github.com/360EntSecGroup-Skylar/excelize"
	"github.com/skratchdot/open-golang/open"
)

func report() error {
	now := time.Now()
	y, m, _ := now.Date()
	es, err := loadEntries()
	if err != nil {
		return fmt.Errorf("loading entries: %w", err)
	}
	var ry, rm int
	if CLI.Report.Month == "" {
		if m == time.January {
			ry = y - 1
			rm = 12
		} else {
			ry = y
			rm = int(m) - 1
		}
	} else {
		t, err := time.Parse("200601", CLI.Report.Month)
		if err != nil {
			return fmt.Errorf("parsing report month: %w", err)
		}
		ty, tm, _ := t.Date()
		ry = ty
		rm = int(tm)
	}
	var entries Entries
	for _, e := range es {
		if e.Date.Year() > ry || int(e.Date.Month()) > rm {
			continue
		}
		if e.Date.Year() < ry || int(e.Date.Month()) < rm {
			break
		}
		if e.End == zeroTime {
			continue
		}
		entries = append(entries, e)
	}
	data := make(map[int]Entries)
	for _, e := range entries {
		data[e.Start.Day()-1] = append(data[e.Start.Day()-1], e)
	}

	xlsx := excelize.NewFile()
	//styles
	centerStyle, err := xlsx.NewStyle(`{"alignment":{"horizontal":"center"}}`)
	if err != nil {
		panic(err)
	}
	centerBoldStyle, err := xlsx.NewStyle(`{"alignment":{"horizontal":"center"},"font":{"bold":true}}`)
	if err != nil {
		panic(err)
	}
	dateStyle, err := xlsx.NewStyle(`{"custom_number_format": "dd.mm.", "alignment":{"horizontal":"center"}}`)
	if err != nil {
		panic(err)
	}
	timeStyle, err := xlsx.NewStyle(`{"custom_number_format": "hh:mm:ss", "alignment":{"horizontal":"center"}}`)
	if err != nil {
		panic(err)
	}
	durationStyle, err := xlsx.NewStyle(`{"custom_number_format": "[hh]:mm:ss", "alignment":{"horizontal":"center"}}`)
	if err != nil {
		panic(err)
	}

	//detail sheet
	detailSheet := "Detail"
	xlsx.SetSheetName("Sheet1", detailSheet)
	xlsx.SetCellValue(detailSheet, "A1", "Client")
	xlsx.SetCellValue(detailSheet, "B1", "Project")
	xlsx.SetCellValue(detailSheet, "C1", "Description")
	xlsx.SetCellValue(detailSheet, "D1", "Start")
	xlsx.SetCellValue(detailSheet, "E1", "End")
	xlsx.SetCellValue(detailSheet, "F1", "Duration")
	xlsx.SetCellValue(detailSheet, "G1", "Tags")

	for i, e := range entries {
		xlsx.SetCellValue(detailSheet, fmt.Sprintf("A%d", i+2), e.Project)
		xlsx.SetCellValue(detailSheet, fmt.Sprintf("B%d", i+2), e.Description)
		xlsx.SetCellValue(detailSheet, fmt.Sprintf("C%d", i+2), e.Start)
		xlsx.SetCellValue(detailSheet, fmt.Sprintf("D%d", i+2), e.End)
		xlsx.SetCellValue(detailSheet, fmt.Sprintf("E%d", i+2), e.End.Sub(e.Start))
	}

	// sumary sheet
	e := entries[0]
	sheetName := e.Start.Format("January 2006")
	index := xlsx.NewSheet(sheetName)
	xlsx.SetCellValue(sheetName, "A1", "Dochádzková karta")
	xlsx.SetCellValue(sheetName, "B1", "Mesačný prehľad")
	xlsx.SetCellValue(sheetName, "A3", "Za mesiac:")
	xlsx.SetCellValue(sheetName, "B3", sheetName)
	xlsx.SetCellValue(sheetName, "A4", "Meno:")
	xlsx.SetCellValue(sheetName, "B4", os.Getenv("USER"))

	xlsx.SetCellStyle(sheetName, "A6", "G6", centerBoldStyle)
	xlsx.SetCellValue(sheetName, "A6", "Dátum")
	xlsx.SetCellValue(sheetName, "B6", "Projekt")
	xlsx.SetCellValue(sheetName, "C6", "Začiatok")
	xlsx.SetCellValue(sheetName, "D6", "Koniec")
	xlsx.SetCellValue(sheetName, "E6", "Celkom")
	xlsx.SetCellValue(sheetName, "F6", "Popis")
	xlsx.SetColWidth(sheetName, "A", "E", 19)
	xlsx.SetColWidth(sheetName, "F", "F", 40)

	d1 := time.Date(e.Start.Year(), e.Start.Month(), 1, 0, 0, 0, 0, time.UTC)
	row := 7
	for d := d1; d.Month() == d1.Month(); d = d.AddDate(0, 0, 1) {
		groups := groupEntries(data[d.Day()-1])
		for _, e := range groups {
			xlsx.SetCellStyle(sheetName, fmt.Sprintf("A%d", row), fmt.Sprintf("A%d", row), dateStyle)
			xlsx.SetCellStyle(sheetName, fmt.Sprintf("B%d", row), fmt.Sprintf("B%d", row), centerStyle)
			xlsx.SetCellStyle(sheetName, fmt.Sprintf("C%d", row), fmt.Sprintf("C%d", row), timeStyle)
			xlsx.SetCellStyle(sheetName, fmt.Sprintf("D%d", row), fmt.Sprintf("D%d", row), timeStyle)
			xlsx.SetCellStyle(sheetName, fmt.Sprintf("E%d", row), fmt.Sprintf("E%d", row), durationStyle)
			xlsx.SetCellStyle(sheetName, fmt.Sprintf("F%d", row), fmt.Sprintf("F%d", row), centerStyle)
			xlsx.SetCellValue(sheetName, fmt.Sprintf("A%d", row), d)
			xlsx.SetCellValue(sheetName, fmt.Sprintf("B%d", row), e.Project)
			xlsx.SetCellValue(sheetName, fmt.Sprintf("C%d", row), e.Start)
			xlsx.SetCellValue(sheetName, fmt.Sprintf("D%d", row), e.End)
			xlsx.SetCellValue(sheetName, fmt.Sprintf("E%d", row), e.End.Sub(e.Start).Seconds()/86400)
			xlsx.SetCellValue(sheetName, fmt.Sprintf("F%d", row), e.Description)
			row++
		}
		if len(groups) == 0 {
			xlsx.SetCellStyle(sheetName, fmt.Sprintf("A%d", row), fmt.Sprintf("A%d", row), dateStyle)
			xlsx.SetCellValue(sheetName, fmt.Sprintf("A%d", row), d)
			row++
		}
	}
	xlsx.SetCellValue(sheetName, fmt.Sprintf("D%d", row), "Spolu:")
	xlsx.SetCellStyle(sheetName, fmt.Sprintf("E%d", row), fmt.Sprintf("E%d", row), durationStyle)
	xlsx.SetCellFormula(sheetName, fmt.Sprintf("E%d", row), fmt.Sprintf("=SUM(E7:E%d)", row-1))

	//projects pivot table
	row += 4
	xlsx.SetCellStyle(sheetName, fmt.Sprintf("A%d", row), fmt.Sprintf("F%d", row), centerBoldStyle)
	xlsx.SetCellValue(sheetName, fmt.Sprintf("A%d", row), "Projekt")
	xlsx.SetCellValue(sheetName, fmt.Sprintf("B%d", row), "Celkom")
	projectsPivot := make(PivotTable)
	for _, e := range entries {
		projectsPivot.Add(e.Project, e.End.Sub(e.Start))
	}
	row++
	pivotStart := row
	xlsx.SetCellStyle(sheetName, fmt.Sprintf("B%d", row), fmt.Sprintf("B%d", row+len(projectsPivot)), durationStyle)
	for _, p := range projectsPivot.Sorted() {
		if p.Key == "" {
			xlsx.SetCellValue(sheetName, fmt.Sprintf("A%d", row), "-")
		} else {
			xlsx.SetCellValue(sheetName, fmt.Sprintf("A%d", row), p.Key)
		}
		xlsx.SetCellValue(sheetName, fmt.Sprintf("B%d", row), p.Value.Seconds()/86400)
		row++
	}
	pivotEnd := row

	xlsx.AddChart(
		sheetName,
		fmt.Sprintf("A%d", pivotEnd+1),
		fmt.Sprintf(
			`{"type":"pie","title":{"name":"Podľa projektov"},"series":[{"categories":"='%s'!$A$%d:$A$%d", "values":"='%s'!$B$%d:$B$%d"}],"format":{"x_scale":0.6,"y_scale":1.0}}`,
			sheetName, pivotStart, pivotEnd-1,
			sheetName, pivotStart, pivotEnd-1,
		),
	)

	xlsx.SetActiveSheet(index)
	// Save xlsx file by the given path.
	if err := xlsx.SaveAs(CLI.Report.Output); err != nil {
		return fmt.Errorf("writing file %s: %s", CLI.Report.Output, err)
	}
	open.Run(CLI.Report.Output)
	return nil
}

func groupEntries(es Entries) (ees Entries) {
	groups := make(map[string]Entries)
	for _, e := range es {
		key := e.Project + e.Description
		groups[key] = append(groups[key], e)
	}
	for _, group := range groups {
		start := group[0].Start
		end := group[0].End
		var duration time.Duration
		for _, g := range group {
			if start.After(g.Start) {
				start = g.Start
			}
			if end.Before(g.End) {
				end = g.End
			}
			duration += end.Sub(start)
		}
		ees = append(
			ees,
			Entry{
				Project:     group[0].Project,
				Description: group[0].Description,
				Start:       start,
				End:         end,
			},
		)
	}
	return
}

//PivotTable used to sum up the duration by category and get the sorted data
type PivotTable map[string]time.Duration

//Add adds new duration to the PivotTable
func (pt PivotTable) Add(category string, duration time.Duration) {
	pt[category] += duration
}

//Sorted returns an sorted list of key-value pairs
func (pt PivotTable) Sorted() PairList {
	pl := make(PairList, len(pt))
	i := 0
	for k, v := range pt {
		pl[i] = Pair{k, v}
		i++
	}
	sort.Sort(sort.Reverse(pl))
	return pl
}

//Pair represents a kev-value pair
type Pair struct {
	Key   string
	Value time.Duration
}

//PairList is a sortable list of key-value pairs
type PairList []Pair

func (p PairList) Len() int           { return len(p) }
func (p PairList) Less(i, j int) bool { return p[i].Value < p[j].Value }
func (p PairList) Swap(i, j int)      { p[i], p[j] = p[j], p[i] }