Skip to content

Commit

Permalink
add basic punch record view
Browse files Browse the repository at this point in the history
  • Loading branch information
tillkuhn committed Nov 16, 2024
1 parent 60707e0 commit d13d555
Show file tree
Hide file tree
Showing 7 changed files with 92 additions and 39 deletions.
29 changes: 24 additions & 5 deletions cmd/punch.go
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
package cmd

import (
"context"
"fmt"
"log"
"time"

"github.com/spf13/cobra"
"github.com/tillkuhn/billy-idle/pkg/tracker"
)

var punchOpts tracker.Options

// punchCmd represents the busy command
var punchCmd = &cobra.Command{
Use: "punch",
Short: "Punch the clock - enter actual busy time",
Example: "punch 10h5m 2024-11-07",
Args: cobra.MatchAll(cobra.MinimumNArgs(1), cobra.MaximumNArgs(2)),
Long: ``,
RunE: func(_ *cobra.Command, args []string) error {
RunE: func(cmd *cobra.Command, args []string) error {
var err error
var day time.Time
day = time.Now()
Expand All @@ -33,14 +33,33 @@ var punchCmd = &cobra.Command{
return err
}
t := tracker.New(&trackOpts)
if err := t.UpsertPunchRecord(context.Background(), dur, day); err != nil {
log.Println(err)
if err := t.UpsertPunchRecord(cmd.Context(), dur, day); err != nil {
return err
}

// show status
recs, err := t.PunchRecords(cmd.Context())
if err != nil {
return err
}
var spentBusy time.Duration
for _, r := range recs {
spentDay := time.Duration(r.BusySecs) * time.Second
fmt.Printf("🕰️ %s: actual busy time %v\n", r.Day.Format("2006-01-02"), spentDay)
spentBusy += spentDay
}
spentBusy = spentBusy.Round(time.Minute)
pDays := len(recs)
expected := time.Duration(pDays) * punchOpts.RegBusy
overtime := spentBusy - expected
fmt.Printf("TotalBusy(%dd): %v AvgPerDay: %v Expected(%dd*%v): %v Overtime: %v\n",
pDays, tracker.FDur(spentBusy), tracker.FDur(spentBusy/time.Duration(pDays)),
pDays, tracker.FDur(punchOpts.RegBusy), tracker.FDur(expected), tracker.FDur(overtime))
return nil
},
}

func init() {
rootCmd.AddCommand(punchCmd)
punchCmd.PersistentFlags().DurationVar(&punchOpts.RegBusy, "reg-busy", 7*time.Hour+48*time.Minute, "Regular busy period per day (w/o breaks), report only")
}
17 changes: 15 additions & 2 deletions pkg/tracker/punch.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ func (t *Tracker) UpsertPunchRecord(ctx context.Context, busyDuration time.Durat
uQuery := `UPDATE ` + tablePunch + `
SET busy_secs=$2,client=$3
WHERE day=$1`
day = trucateDay(day) // https://stackoverflow.com/a/38516536/4292075
day = truncateDay(day) // https://stackoverflow.com/a/38516536/4292075
uRes, err := t.db.ExecContext(ctx, uQuery, day, busyDuration.Seconds(), t.opts.ClientID)
if err != nil {
return errors.Wrap(err, "unable to update busy table")
Expand All @@ -37,6 +37,19 @@ func (t *Tracker) UpsertPunchRecord(ctx context.Context, busyDuration time.Durat
return nil
}

func trucateDay(t time.Time) time.Time {
// PunchRecords retried existing punch records for a specific time period
func (t *Tracker) PunchRecords(ctx context.Context) ([]PunchRecord, error) {
// select sum(ROUND((JULIANDAY(busy_end) - JULIANDAY(busy_start)) * 86400)) || ' secs' AS total from track
query := `SELECT day,busy_secs FROM ` + tablePunch + ` ORDER BY DAY` // WHERE busy_start >= DATE('now', '-7 days') ORDER BY busy_start LIMIT 500`
// We could use get since we expect a single result, but this would return an error if nothing is found
// which is a likely use case
var records []PunchRecord
if err := t.db.SelectContext(ctx, &records, query /*, args*/); err != nil {
return nil, err
}
return records, nil
}

func truncateDay(t time.Time) time.Time {
return t.Truncate(hoursPerDay * time.Hour).UTC()
}
20 changes: 18 additions & 2 deletions pkg/tracker/punch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (

func Test_UpsertPunchUpdate(t *testing.T) {
tracker, mock := DBMock(t)
day := trucateDay(time.Now())
day := truncateDay(time.Now())
sql1 := wildcardStatement("UPDATE " + tablePunch + " SET")
// mock.ExpectPrepare(sql1)
mock.ExpectExec(sql1).WithArgs(day, float64(3600), "test").
Expand All @@ -22,7 +22,7 @@ func Test_UpsertPunchUpdate(t *testing.T) {

func Test_UpsertPunchInsert(t *testing.T) {
tracker, mock := DBMock(t)
day := trucateDay(time.Now())
day := truncateDay(time.Now())
sql1 := wildcardStatement("UPDATE " + tablePunch + " SET")
// mock.ExpectPrepare(sql1)
mock.ExpectExec(sql1).WithArgs(day, float64(3600), "test").
Expand All @@ -34,3 +34,19 @@ func Test_UpsertPunchInsert(t *testing.T) {
err := tracker.UpsertPunchRecord(context.Background(), time.Second*3600, day)
assert.NoError(t, err)
}

func Test_SelectPunch(t *testing.T) {
tr, mock := DBMock(t)

today := truncateDay(time.Now())
mock.ExpectQuery("SELECT (.*)").
WillReturnRows(
mock.NewRows([]string{"day", "busy_secs"}).
AddRow(today, 3600).
AddRow(today, 7200),
)
mock.ExpectClose()
recs, err := tr.PunchRecords(context.Background())
assert.NoError(t, err)
assert.Equal(t, 2, len(recs))
}
34 changes: 26 additions & 8 deletions pkg/tracker/report.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,24 @@ import (
const minPerHour = 60
const sepLineLen = 100

// trackRecords retried existing track records for a specific time period
func (t *Tracker) trackRecords(ctx context.Context) (map[string][]TrackRecord, error) {
// select sum(ROUND((JULIANDAY(busy_end) - JULIANDAY(busy_start)) * 86400)) || ' secs' AS total from track
query := `SELECT * FROM track WHERE busy_start >= DATE('now', '-7 days') ORDER BY busy_start LIMIT 500`
// We could use get since we expect a single result, but this would return an error if nothing is found
// which is a likely use case
var records []TrackRecord
if err := t.db.SelectContext(ctx, &records, query /*, args*/); err != nil {
return nil, err
}
recMap := map[string][]TrackRecord{}
for _, r := range records {
k := r.BusyStart.Format("2006-01-02") // go ref Mon Jan 2 15:04:05 -0700 MST 2006
recMap[k] = append(recMap[k], r)
}
return recMap, nil
}

// Report experimental report for time tracking apps
func (t *Tracker) Report(ctx context.Context, w io.Writer) error {
recMap, err := t.trackRecords(ctx)
Expand Down Expand Up @@ -78,20 +96,20 @@ func (t *Tracker) Report(ctx context.Context, w io.Writer) error {
// todo: raise warning if totalBusy is > 10h (or busyPlus > 10:45), since more than 10h are not allowed
_, _ = fmt.Fprintf(w, "Busy: %s WithBreak(%vm): %s Skipped(<%v): %d >Max(%s): %v Range: %s\n",
// first.BusyStart.Format("2006-01-02 Mon"),
fDur(spentBusy),
FDur(spentBusy),
kitKat.Round(time.Minute).Minutes(),
fDur((spentBusy + kitKat).Round(time.Minute)),
fDur(t.opts.MinBusy), skippedTooShort,
fDur(t.opts.MaxBusy), spentBusy > t.opts.MaxBusy,
fDur(spentTotal),
FDur((spentBusy + kitKat).Round(time.Minute)),
FDur(t.opts.MinBusy), skippedTooShort,
FDur(t.opts.MaxBusy), spentBusy > t.opts.MaxBusy,
FDur(spentTotal),
)
sugStart, _ := time.Parse("15:04", "09:00")

_, _ = fmt.Fprintf(w, "Simplified Entry: %v → %v (inc. break) Overtime(>%v): %v\n",
// first.BusyStart.Format("Monday"),
sugStart.Format("15:04"),
sugStart.Add((spentBusy + kitKat).Round(time.Minute)).Format("15:04"),
fDur(t.opts.RegBusy), fDur(spentBusy-t.opts.RegBusy),
FDur(t.opts.RegBusy), FDur(spentBusy-t.opts.RegBusy),
)
color.Unset()
_, _ = fmt.Fprintln(w, strings.Repeat("=", sepLineLen))
Expand All @@ -100,8 +118,8 @@ func (t *Tracker) Report(ctx context.Context, w io.Writer) error {
return nil
}

// fDur formats a duration to a human readable string with hours (if > 0) and minutes
func fDur(d time.Duration) string {
// FDur formats a duration to a human-readable string with hours (if > 0) and minutes
func FDur(d time.Duration) string {
switch {
case d.Hours() > 0:
return fmt.Sprintf("%dh%dm", int(d.Hours()), int(d.Minutes())%minPerHour)
Expand Down
6 changes: 3 additions & 3 deletions pkg/tracker/report_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ func Test_Report(t *testing.T) {
}

func Test_FormatDuration(t *testing.T) {
assert.Equal(t, "2h5m", fDur(2*time.Hour+5*time.Minute))
assert.Equal(t, "0h5m", fDur(5*time.Minute))
assert.Equal(t, "-3h7m", fDur(-3*time.Hour+7*time.Minute*-1))
assert.Equal(t, "2h5m", FDur(2*time.Hour+5*time.Minute))
assert.Equal(t, "0h5m", FDur(5*time.Minute))
assert.Equal(t, "-3h7m", FDur(-3*time.Hour+7*time.Minute*-1))
}
18 changes: 0 additions & 18 deletions pkg/tracker/tracker.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,24 +146,6 @@ func (t *Tracker) completeTrackRecordWithTime(ctx context.Context, id int, msg s
return err
}

// trackRecords retried existing track records for a specific time period
func (t *Tracker) trackRecords(ctx context.Context) (map[string][]TrackRecord, error) {
// select sum(ROUND((JULIANDAY(busy_end) - JULIANDAY(busy_start)) * 86400)) || ' secs' AS total from track
query := `SELECT * FROM track WHERE busy_start >= DATE('now', '-7 days') ORDER BY busy_start LIMIT 500`
// We could use get since we expect a single result, but this would return an error if nothing is found
// which is a likely use case
var records []TrackRecord
if err := t.db.SelectContext(ctx, &records, query /*, args*/); err != nil {
return nil, err
}
recMap := map[string][]TrackRecord{}
for _, r := range records {
k := r.BusyStart.Format("2006-01-02") // go ref Mon Jan 2 15:04:05 -0700 MST 2006
recMap[k] = append(recMap[k], r)
}
return recMap, nil
}

func randomTask() string {
// r := rand.IntN(3)
switch rand.IntN(4) {
Expand Down
7 changes: 6 additions & 1 deletion pkg/tracker/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,16 @@ type TrackRecord struct {
Client string `db:"client"`
}

type PunchRecord struct {
Day time.Time `db:"day"`
BusySecs float64 `db:"busy_secs"`
}

// String returns a string representation of the TrackRecord
func (t TrackRecord) String() string {
var verb, to string
if t.BusyEnd.Valid {
verb = "Spent " + fDur(t.Duration())
verb = "Spent " + FDur(t.Duration())
to = t.BusyEnd.Time.Format("15:04:05")
} else {
verb = "Still busy with"
Expand Down

0 comments on commit d13d555

Please sign in to comment.