Skip to content

Commit

Permalink
Use table format for idle/busy report
Browse files Browse the repository at this point in the history
  • Loading branch information
tillkuhn committed Jan 10, 2025
1 parent 532b386 commit f4b14a7
Show file tree
Hide file tree
Showing 6 changed files with 96 additions and 46 deletions.
22 changes: 12 additions & 10 deletions README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,19 @@ This data can later be used as input for time tracking tools or statistics.
== Example Time Tracking Report

----
🕰 Wednesday January 08, 2025 (2025-01-08) Daily Report
Friday January 10, 2025 (2025-01-10) Daily Report
----------------------------------------------------------------------------------------------------
☕ 08:53:01 → 12:12:24: Spent 3h19m Eating a Mimis maine blueberry cobbler topped with Redcurrant #854
🌞 13:06:13 → 15:04:00: Spent 1h57m Driving a Passenger car heavy Gto to San Jose #856
🌞 15:40:11 → 19:03:32: Spent 3h23m Building App Boardwash 5.6.3 in A++ #857
🌙 21:31:41 → 22:17:46: Spent 46m Building App Shoppingsnore 3.4.5 in Jako #858
🌙 22:17:46 → now : Still busy with Drinking a English Pale Ale Stone Imperial Russian Stout with 6.5% #859
----------------------------------------------------------------------------------------------------
BusyTime: 9h29m +Break: 10h14m Busy+Idle: 22h5m Skipped(<5m): 5 >Max(10h): false
Suggest.: 09:00 → 19:14 (45m break) OverReg(>7h48m): 1h41m OverMax(>10h): -31m
====================================================================================================
🕰 | TIME RANGE | 🐝 BUSY RECORD
-----+---------------------+--------------------------------------------------------------------------------
☕ | 08:42:52 → 11:55:07 | Spent 3h12m Driving a Pickup truck Mark Lt to Minneapolis #874
🌞 | 12:24:19 → 12:35:00 | Spent 10m Drinking a Belgian And French Ale Orval Trappist Ale with 2.6% #875
🌞 | 13:24:14 → 15:08:20 | Spent 1h44m Building App Tealgrammar 5.8.4 in Orc #876
🌞 | 15:09:27 → 15:24:27 | Spent 15m Driving a Van A4 Cabriolet to Orlando #877
🌞 | 15:24:27 → now | Still busy with Eating a Frozen kahlua creme topped with Clementine #878
-----+---------------------+--------------------------------------------------------------------------------
🧮 | 5H23M +BREAK: 5H23M | BUSY+IDLE: 15H15M SKIP(<5M): 4 >REG(7H48M): -2H25M >MAX(10H): -4H37M
-----+---------------------+--------------------------------------------------------------------------------
Suggestestion: 09:00 → 14:23 (inc. 0m break), you're expected to be busy for another 2h25m
----

== Example Punch Clock Report
Expand Down
1 change: 1 addition & 0 deletions pkg/tracker/punch.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ func (t *Tracker) PunchReport(ctx context.Context) error {
return err
}
var spentBusyTotal, plannedBusyTotal time.Duration

table := tablewriter.NewWriter(t.opts.Out)
bold := tablewriter.Colors{tablewriter.Bold}
table.SetHeader([]string{"CW", "📅 Date", "Weekday", "🐝 Busy", "⏲️ Plan", "🕰 Overtime"})
Expand Down
91 changes: 67 additions & 24 deletions pkg/tracker/report.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import (
"strings"
"time"

"github.com/olekukonko/tablewriter"

"github.com/fatih/color"
)

Expand Down Expand Up @@ -46,25 +48,43 @@ func (t *Tracker) Report(ctx context.Context, w io.Writer) error {
}
sort.Strings(dailyRecs)

// Outer Loop: key days (2024-10-04)
// Outer Loop: key days (20xx-10-04)
for dayIdx, day := range dailyRecs {
lastDay := dayIdx == len(dailyRecs)-1

// inner loop: track records per day
// inner loop: show all track records per day
recs := recMap[day]
first := recs[0]
last := recs[len(recs)-1]
var spentBusy, spentTotal time.Duration
var skippedTooShort int

// headline per day
table := tablewriter.NewWriter(t.opts.Out)
bold := tablewriter.Colors{tablewriter.Bold}
table.SetHeader([]string{"🕰", "Time Range", "🐝 Busy Record"})
table.SetHeaderColor(bold, bold, bold)
table.SetBorder(false)
table.SetAlignment(tablewriter.ALIGN_LEFT)
table.SetHeaderAlignment(tablewriter.ALIGN_LEFT)
table.SetAutoWrapText(false) // or long lines will be split
// table.SetAutoMergeCells(true) // don't merge

// Headline 📅 Friday, January 10, 20xx (YYYY-MM-DD) Daily Report
color.Set(color.FgCyan, color.Bold)
_, _ = fmt.Fprintf(w, "🕰 %s (%s) Daily Report\n%s\n", first.BusyStart.Format("Monday January 02, 2006"), day, strings.Repeat("-", sepLineLen))
_, _ = fmt.Fprintf(w, "%s (%s) Daily Report\n%s\n", first.BusyStart.Format("Monday January 02, 2006"), day, strings.Repeat("-", sepLineLen))
color.Unset()

// busy / idle entries for each record per day
for _, rec := range recs {
if rec.Duration() >= t.opts.MinBusy {
_, _ = fmt.Fprintln(w, rec) // print details
to := rec.BusyEnd.Time.Format("15:04:05")
// _, _ = fmt.Fprintln(w, rec) // print details
table.Append([]string{
DayTimeIcon(rec.BusyStart),
fmt.Sprintf("%s → %s", rec.BusyStart.Format("15:04:05"), to),
fmt.Sprintf("Spent %s %s #%d", FDur(rec.Duration()), rec.Task, rec.ID), // rec.Id
})
spentBusy += rec.Duration()
} else {
skippedTooShort++
Expand All @@ -74,10 +94,14 @@ func (t *Tracker) Report(ctx context.Context, w io.Writer) error {
if last.BusyEnd.Valid {
spentTotal = last.BusyEnd.Time.Sub(first.BusyStart) // last record is complete
} else {
// last record not complete, show anyway and use either start instead of end time
// end date for last record is missing: show anyway, and use either start instead of end time
// or if this is the last record of the last day, calculate the relative time to now()
// since this is likely the record that is still active
_, _ = fmt.Fprintln(w, last)
// since this is likely the record that is currently still active
table.Append([]string{
DayTimeIcon(last.BusyStart),
fmt.Sprintf("%s → now", last.BusyStart.Format("15:04:05")),
fmt.Sprintf("Still busy with %s #%d", last.Task, last.ID), // rec.Id
})
if lastDay {
spentTotal = time.Since(first.BusyStart)
spentBusy += time.Since(last.BusyStart)
Expand All @@ -86,34 +110,53 @@ func (t *Tracker) Report(ctx context.Context, w io.Writer) error {
// ignore incomplete record for busy calc
}
}
_, _ = fmt.Fprintln(w, strings.Repeat("-", sepLineLen))

// footer with summaries and assessment
// _, _ = fmt.Fprintln(w, strings.Repeat("-", sepLineLen))
kitKat := mandatoryBreak(spentBusy)
spentBusy = spentBusy.Round(time.Minute)
spentTotal = spentTotal.Round(time.Minute)
table.SetFooter([]string{"🧮",
fmt.Sprintf("%s +break: %s", FDur(spentBusy), FDur((spentBusy + kitKat).Round(time.Minute))),
fmt.Sprintf("Busy+Idle: %s Skip(<%v): %d >Reg(%v): %v >Max(%s): %v",
// first.BusyStart.Format("2006-01-02 Mon"),
// FDur(spentBusy), // busy time (total - idle)
// busy time including break
FDur(spentTotal), // total time both busy + idle
FDur(t.opts.MinBusy), skippedTooShort, // number of skipped records
FDur(t.opts.RegBusy) /* reg busy time e.g. (7:48) */, FDur(spentBusy-t.opts.RegBusy), // overtime
FDur(t.opts.MaxBusy), FDur(spentBusy-t.opts.MaxBusy), // over max
),
// fmt.Sprintf("%v\n>%v", FDur(overtime), FDur(plannedBusyTotal)),
}) // Add Footer
table.SetFooterColor(tablewriter.Colors{}, bold, tablewriter.Colors{})
table.SetFooterAlignment(tablewriter.ALIGN_LEFT)
// tablewriter.Colors{tablewriter.FgHiGreenColor}, tablewriter.Colors{}, tablewriter.Colors{})

table.Render()

// todo: raise warning if totalBusy is > 10h (or busyPlus > 10:45), since more than 10h are not allowed
_, _ = fmt.Fprintf(w, "BusyTime: %s +Break: %s Busy+Idle: %s Skipped(<%v): %d >Max(%s): %v\n",
// first.BusyStart.Format("2006-01-02 Mon"),
FDur(spentBusy), // busy time (total - idle)
FDur((spentBusy + kitKat).Round(time.Minute)), // busy time including break
FDur(spentTotal), // total time both busy + idle
FDur(t.opts.MinBusy), skippedTooShort, // number of skipped records
FDur(t.opts.MaxBusy) /* max busy time e.g. 10h */, spentBusy > t.opts.MaxBusy, /* over max? */
)
sugStart, _ := time.Parse("15:04", "09:00")

color.Set(color.FgGreen)
_, _ = fmt.Fprintf(w, "Suggest.: %v → %v (%vm break) OverReg(>%v): %v OverMax(>%v): %v\n",
// Suggestion for time entry:
overDur := spentBusy - t.opts.RegBusy
var overInfo string
if overDur > 0 {
color.Set(color.FgGreen)
overInfo = fmt.Sprintf("you've been busy %s longer than expected", FDur(overDur))
} else {
color.Set(color.FgHiMagenta)
overInfo = fmt.Sprintf("you're expected to be busy for another %s", FDur(overDur*-1))
}
_, _ = fmt.Fprintf(w, "Suggestestion: %v → %v (inc. %vm break), %s!\n\n",
// first.BusyStart.Format("Monday"),
sugStart.Format("15:04"), // Simplified start
sugStart.Add((spentBusy + kitKat).Round(time.Minute)).Format("15:04"), // Simplified end
kitKat.Round(time.Minute).Minutes(), // break time depending on total busy time
FDur(t.opts.RegBusy) /* reg busy time e.g. (7:48) */, FDur(spentBusy-t.opts.RegBusy), // overtime
FDur(t.opts.MaxBusy), FDur(spentBusy-t.opts.MaxBusy), // over max
sugStart.Add((spentBusy + kitKat).Round(time.Minute)).Format("15:04"), // Simplified end
kitKat.Round(time.Minute).Minutes(), // break duration depending on total busy time
overInfo,
)
color.Unset()
_, _ = fmt.Fprintln(w, strings.Repeat("=", sepLineLen))
_, _ = fmt.Fprintln(w, "")
// _, _ = fmt.Fprintln(w, strings.Repeat("=", sepLineLen))
}
return nil
}
1 change: 1 addition & 0 deletions pkg/tracker/report_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ func Test_Report(t *testing.T) {
)
mock.ExpectClose()
var output bytes.Buffer
tr.opts.Out = &output
assert.NoError(t, tr.Report(context.Background(), &output))
assert.Contains(t, output.String(), "DejaVu")
}
Expand Down
14 changes: 2 additions & 12 deletions pkg/tracker/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,19 +46,9 @@ type PunchRecord struct {

// 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())
to = t.BusyEnd.Time.Format("15:04:05")
} else {
verb = "Still busy with"
to = "now"
// suffix = fmt.Sprintf("since %v", time.Since(t.BusyStart).Round(time.Second))
}
// 2025-10-09 Wed 09:01:30 #144 Spent 3h31m20s Eating a Frosted rhubarb cookies topped with Honeydew until 12:32PM
return fmt.Sprintf("%s %s → %-8s: %s %s #%d",
DayTimeIcon(t.BusyStart),
t.BusyStart.Format("15:04:05"), to, verb, t.Task, t.ID)
return fmt.Sprintf("%s → %v: %s#%d",
t.BusyStart.Format("15:04:05"), t.BusyEnd, t.Task, t.ID)
}

// Duration returns the duration of a punch record, calculated as the difference between the end and start times.
Expand Down
13 changes: 13 additions & 0 deletions pkg/tracker/types_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package tracker

import (
"database/sql"
"path/filepath"
"testing"
"time"
Expand Down Expand Up @@ -34,6 +35,18 @@ func Test_AppDir(t *testing.T) {
assert.Equal(t, filepath.Join("/tmp", "test"), o.AppDir())
}

func Test_String(t *testing.T) {
tr := TrackRecord{
ID: 22,
BusyStart: time.Time{},
BusyEnd: sql.NullTime{},
Message: "hello",
Task: "123",
Client: "test",
}
assert.Contains(t, tr.String(), "22")
}

// Test add-break-on-top calc
//
// 05:00 -> 0m
Expand Down

0 comments on commit f4b14a7

Please sign in to comment.