Skip to content

Commit e721a0f

Browse files
acidghostdhth
andauthored
feat(stats): filter (in)active tasks (#56)
Co-authored-by: Dhruv Thakur <[email protected]>
1 parent 9f1c3b9 commit e721a0f

File tree

17 files changed

+611
-238
lines changed

17 files changed

+611
-238
lines changed

.golangci.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ linters:
88
- gosimple
99
- govet
1010
- ineffassign
11+
- intrange
1112
- nilerr
1213
- prealloc
1314
- predeclared

cmd/root.go

Lines changed: 81 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,12 @@ import (
1212
"path"
1313
"path/filepath"
1414
"strings"
15+
"time"
1516

1617
"github.com/charmbracelet/lipgloss"
1718
c "github.com/dhth/hours/internal/common"
1819
pers "github.com/dhth/hours/internal/persistence"
20+
"github.com/dhth/hours/internal/types"
1921
"github.com/dhth/hours/internal/ui"
2022
"github.com/spf13/cobra"
2123
)
@@ -150,6 +152,7 @@ func NewRootCommand() (*cobra.Command, error) {
150152
reportAgg bool
151153
recordsInteractive bool
152154
recordsOutputPlain bool
155+
taskStatusStr string
153156
activeTemplate string
154157
genNumDays uint8
155158
genNumTasks uint8
@@ -271,13 +274,15 @@ create dummy entries in it. You can run it on a throwaway database by passing a
271274
path for it via --dbpath/-d (use it for all further invocations of 'hours' as
272275
well).
273276
`))
274-
fmt.Print(`
277+
fmt.Printf(`
275278
The 'gen' subcommand is intended for new users of 'hours' so they can get a
276279
sense of its capabilities without actually tracking any time.
277280
281+
Running with --dbpath set to: %q
282+
278283
---
279284
280-
`)
285+
`, dbPathFull)
281286
confirm, err := getConfirmation()
282287
if err != nil {
283288
return err
@@ -294,9 +299,6 @@ sense of its capabilities without actually tracking any time.
294299
fmt.Printf(`
295300
Successfully generated dummy data in the database file: %s
296301
297-
If this is not the default database file path, use --dbpath/-d with 'hours' when
298-
you want to access the dummy data.
299-
300302
Go ahead and try the following!
301303
302304
hours --dbpath=%s
@@ -309,7 +311,7 @@ hours --dbpath=%s stats today -i
309311
}
310312

311313
reportCmd := &cobra.Command{
312-
Use: "report",
314+
Use: "report [PERIOD]",
313315
Short: "Output a report based on task log entries",
314316
Long: `Output a report based on task log entries.
315317
@@ -319,90 +321,132 @@ cumulative time spent on each task per day.
319321
320322
Accepts an argument, which can be one of the following:
321323
322-
today: for today's report
323-
yest: for yesterday's report
324-
3d: for a report on the last 3 days (default)
325-
week: for a report on the current week
326-
date: for a report for a specific date (eg. "2024/06/08")
327-
range: for a report for a date range (eg. "2024/06/08...2024/06/12")
324+
today for today's report
325+
yest for yesterday's report
326+
3d for a report on the last 3 days (default)
327+
week for a report on the current week
328+
date for a report for a specific date (eg. "2024/06/08")
329+
range for a report for a date range (eg. "2024/06/08...2024/06/12")
328330
329331
Note: If a task log continues past midnight in your local timezone, it
330332
will be reported on the day it ends.
331333
`,
332334
Args: cobra.MaximumNArgs(1),
333335
PreRunE: preRun,
334336
RunE: func(_ *cobra.Command, args []string) error {
337+
taskStatus, err := types.ParseTaskStatus(taskStatusStr)
338+
if err != nil {
339+
return err
340+
}
341+
335342
var period string
336343
if len(args) == 0 {
337344
period = "3d"
338345
} else {
339346
period = args[0]
340347
}
341348

342-
return ui.RenderReport(db, style, os.Stdout, recordsOutputPlain, period, reportAgg, recordsInteractive)
349+
var fullWeek bool
350+
if recordsInteractive {
351+
fullWeek = true
352+
}
353+
dateRange, err := types.GetDateRangeFromPeriod(period, time.Now(), fullWeek)
354+
if err != nil {
355+
return err
356+
}
357+
358+
return ui.RenderReport(db, style, os.Stdout, recordsOutputPlain, dateRange, period, taskStatus, reportAgg, recordsInteractive)
343359
},
344360
}
345361

346362
logCmd := &cobra.Command{
347-
Use: "log",
363+
Use: "log [PERIOD]",
348364
Short: "Output task log entries",
349365
Long: `Output task log entries.
350366
351367
Accepts an argument, which can be one of the following:
352368
353-
today: for log entries from today (default)
354-
yest: for log entries from yesterday
355-
3d: for log entries from the last 3 days
356-
week: for log entries from the current week
357-
date: for log entries from a specific date (eg. "2024/06/08")
358-
range: for log entries from a specific date range (eg. "2024/06/08...2024/06/12")
369+
today for log entries from today (default)
370+
yest for log entries from yesterday
371+
3d for log entries from the last 3 days
372+
week for log entries from the current week
373+
date for log entries from a specific date (eg. "2024/06/08")
374+
range for log entries from a specific date range (eg. "2024/06/08...2024/06/12")
359375
360376
Note: If a task log continues past midnight in your local timezone, it'll
361377
appear in the log for the day it ends.
362378
`,
363379
Args: cobra.MaximumNArgs(1),
364380
PreRunE: preRun,
365381
RunE: func(_ *cobra.Command, args []string) error {
382+
taskStatus, err := types.ParseTaskStatus(taskStatusStr)
383+
if err != nil {
384+
return err
385+
}
386+
366387
var period string
367388
if len(args) == 0 {
368389
period = "today"
369390
} else {
370391
period = args[0]
371392
}
372393

373-
return ui.RenderTaskLog(db, style, os.Stdout, recordsOutputPlain, period, recordsInteractive)
394+
dateRange, err := types.GetDateRangeFromPeriod(period, time.Now(), false)
395+
if err != nil {
396+
return err
397+
}
398+
399+
return ui.RenderTaskLog(db, style, os.Stdout, recordsOutputPlain, dateRange, period, taskStatus, recordsInteractive)
374400
},
375401
}
376402

377403
statsCmd := &cobra.Command{
378-
Use: "stats",
404+
Use: "stats [PERIOD]",
379405
Short: "Output statistics for tracked time",
380406
Long: `Output statistics for tracked time.
381407
382408
Accepts an argument, which can be one of the following:
383409
384-
today: show stats for today
385-
yest: show stats for yesterday
386-
3d: show stats for the last 3 days (default)
387-
week: show stats for the current week
388-
date: show stats for a specific date (eg. "2024/06/08")
389-
range: show stats for a specific date range (eg. "2024/06/08...2024/06/12")
390-
all: show stats for all log entries
410+
today show stats for today
411+
yest show stats for yesterday
412+
3d show stats for the last 3 days (default)
413+
week show stats for the current week
414+
date show stats for a specific date (eg. "2024/06/08")
415+
range show stats for a specific date range (eg. "2024/06/08...2024/06/12")
416+
all show stats for all log entries
391417
392418
Note: If a task log continues past midnight in your local timezone, it'll
393419
be considered in the stats for the day it ends.
394420
`,
395421
Args: cobra.MaximumNArgs(1),
396422
PreRunE: preRun,
397423
RunE: func(_ *cobra.Command, args []string) error {
424+
taskStatus, err := types.ParseTaskStatus(taskStatusStr)
425+
if err != nil {
426+
return err
427+
}
428+
398429
var period string
399430
if len(args) == 0 {
400431
period = "3d"
401432
} else {
402433
period = args[0]
403434
}
404435

405-
return ui.RenderStats(db, style, os.Stdout, recordsOutputPlain, period, recordsInteractive)
436+
var fullWeek bool
437+
if recordsInteractive {
438+
fullWeek = true
439+
}
440+
var dateRange types.DateRange
441+
if period != "all" {
442+
dateRange, err = types.GetDateRangeFromPeriod(period, time.Now(), fullWeek)
443+
if err != nil {
444+
return err
445+
}
446+
447+
}
448+
449+
return ui.RenderStats(db, style, os.Stdout, recordsOutputPlain, &dateRange, period, taskStatus, recordsInteractive)
406450
},
407451
}
408452

@@ -534,20 +578,23 @@ eg. hours active -t ' {{task}} ({{time}}) '
534578
reportCmd.Flags().BoolVarP(&recordsInteractive, "interactive", "i", false, "whether to view report interactively")
535579
reportCmd.Flags().BoolVarP(&recordsOutputPlain, "plain", "p", false, "whether to output report without any formatting")
536580
reportCmd.Flags().StringVarP(&dbPath, "dbpath", "d", defaultDBPath, "location of hours' database file")
581+
reportCmd.Flags().StringVarP(&taskStatusStr, "task-status", "s", "any", fmt.Sprintf("only show data for tasks with this status [possible values: %q]", types.ValidTaskStatusValues))
537582
reportCmd.Flags().StringVarP(&themeName, "theme", "t", defaultThemeName,
538-
fmt.Sprintf("UI theme to use; themes live in %s", themesDir))
583+
fmt.Sprintf("UI theme to use (themes live in %q)", themesDir))
539584

540585
logCmd.Flags().BoolVarP(&recordsOutputPlain, "plain", "p", false, "whether to output logs without any formatting")
541586
logCmd.Flags().BoolVarP(&recordsInteractive, "interactive", "i", false, "whether to view logs interactively")
542587
logCmd.Flags().StringVarP(&dbPath, "dbpath", "d", defaultDBPath, "location of hours' database file")
588+
logCmd.Flags().StringVarP(&taskStatusStr, "task-status", "s", "any", fmt.Sprintf("only show data for tasks with this status [possible values: %q]", types.ValidTaskStatusValues))
543589
logCmd.Flags().StringVarP(&themeName, "theme", "t", defaultThemeName,
544-
fmt.Sprintf("UI theme to use; themes live in %s", themesDir))
590+
fmt.Sprintf("UI theme to use (themes live in %q)", themesDir))
545591

546592
statsCmd.Flags().BoolVarP(&recordsOutputPlain, "plain", "p", false, "whether to output stats without any formatting")
547593
statsCmd.Flags().BoolVarP(&recordsInteractive, "interactive", "i", false, "whether to view stats interactively")
548594
statsCmd.Flags().StringVarP(&dbPath, "dbpath", "d", defaultDBPath, "location of hours' database file")
595+
statsCmd.Flags().StringVarP(&taskStatusStr, "task-status", "s", "any", fmt.Sprintf("only show data for tasks with this status [possible values: %q]", types.ValidTaskStatusValues))
549596
statsCmd.Flags().StringVarP(&themeName, "theme", "t", defaultThemeName,
550-
fmt.Sprintf("UI theme to use; themes live in %s", themesDir))
597+
fmt.Sprintf("UI theme to use (themes live in %q)", themesDir))
551598

552599
activeCmd.Flags().StringVarP(&activeTemplate, "template", "t", ui.ActiveTaskPlaceholder, "string template to use for outputting active task")
553600
activeCmd.Flags().StringVarP(&dbPath, "dbpath", "d", defaultDBPath, "location of hours' database file")
@@ -572,7 +619,7 @@ func getRandomChars(length int) string {
572619
const charset = "abcdefghijklmnopqrstuvwxyz"
573620

574621
var code string
575-
for i := 0; i < length; i++ {
622+
for range length {
576623
code += string(charset[rand.Intn(len(charset))])
577624
}
578625
return code

internal/persistence/queries.go

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -515,7 +515,15 @@ LIMIT ?;
515515
return logEntries, nil
516516
}
517517

518-
func FetchTLEntriesBetweenTS(db *sql.DB, beginTs, endTs time.Time, limit int) ([]types.TaskLogEntry, error) {
518+
func FetchTLEntriesBetweenTS(db *sql.DB, beginTs, endTs time.Time, taskStatus types.TaskStatus, limit int) ([]types.TaskLogEntry, error) {
519+
var tsFilter string
520+
switch taskStatus {
521+
case types.TaskStatusActive:
522+
tsFilter = "AND t.active is true"
523+
case types.TaskStatusInactive:
524+
tsFilter = "AND t.active is false"
525+
}
526+
519527
var logEntries []types.TaskLogEntry
520528

521529
rows, err := db.Query(`
@@ -524,6 +532,7 @@ FROM task_log tl left join task t on tl.task_id=t.id
524532
WHERE tl.active=false
525533
AND tl.end_ts >= ?
526534
AND tl.end_ts < ?
535+
`+tsFilter+`
527536
ORDER by tl.begin_ts ASC LIMIT ?;
528537
`, beginTs.UTC(), endTs.UTC(), limit)
529538
if err != nil {
@@ -555,11 +564,20 @@ ORDER by tl.begin_ts ASC LIMIT ?;
555564
return logEntries, nil
556565
}
557566

558-
func FetchStats(db *sql.DB, limit int) ([]types.TaskReportEntry, error) {
567+
func FetchStats(db *sql.DB, taskStatus types.TaskStatus, limit int) ([]types.TaskReportEntry, error) {
568+
var tsFilter string
569+
switch taskStatus {
570+
case types.TaskStatusActive:
571+
tsFilter = "WHERE t.active is true"
572+
case types.TaskStatusInactive:
573+
tsFilter = "WHERE t.active is false"
574+
}
575+
559576
rows, err := db.Query(`
560577
SELECT tl.task_id, t.summary, COUNT(tl.id) as num_entries, t.secs_spent
561578
from task_log tl
562579
LEFT JOIN task t on tl.task_id = t.id
580+
`+tsFilter+`
563581
GROUP BY tl.task_id
564582
ORDER BY t.secs_spent DESC
565583
limit ?;
@@ -591,12 +609,20 @@ limit ?;
591609
return tLE, nil
592610
}
593611

594-
func FetchStatsBetweenTS(db *sql.DB, beginTs, endTs time.Time, limit int) ([]types.TaskReportEntry, error) {
612+
func FetchStatsBetweenTS(db *sql.DB, beginTs, endTs time.Time, taskStatus types.TaskStatus, limit int) ([]types.TaskReportEntry, error) {
613+
var activeFilter string
614+
switch taskStatus {
615+
case types.TaskStatusActive:
616+
activeFilter = " AND t.active is true"
617+
case types.TaskStatusInactive:
618+
activeFilter = " AND t.active is false"
619+
}
620+
595621
rows, err := db.Query(`
596622
SELECT tl.task_id, t.summary, COUNT(tl.id) as num_entries, SUM(tl.secs_spent) AS secs_spent
597623
FROM task_log tl
598624
LEFT JOIN task t ON tl.task_id = t.id
599-
WHERE tl.end_ts >= ? AND tl.end_ts < ?
625+
WHERE tl.end_ts >= ? AND tl.end_ts < ?`+activeFilter+`
600626
GROUP BY tl.task_id
601627
ORDER BY secs_spent DESC
602628
LIMIT ?;
@@ -628,12 +654,21 @@ LIMIT ?;
628654
return tLE, nil
629655
}
630656

631-
func FetchReportBetweenTS(db *sql.DB, beginTs, endTs time.Time, limit int) ([]types.TaskReportEntry, error) {
657+
func FetchReportBetweenTS(db *sql.DB, beginTs, endTs time.Time, taskStatus types.TaskStatus, limit int) ([]types.TaskReportEntry, error) {
658+
var tsFilter string
659+
switch taskStatus {
660+
case types.TaskStatusActive:
661+
tsFilter = "AND t.active is true"
662+
case types.TaskStatusInactive:
663+
tsFilter = "AND t.active is false"
664+
}
665+
632666
rows, err := db.Query(`
633667
SELECT tl.task_id, t.summary, COUNT(tl.id) as num_entries, SUM(tl.secs_spent) AS secs_spent
634668
FROM task_log tl
635669
LEFT JOIN task t ON tl.task_id = t.id
636670
WHERE tl.end_ts >= ? AND tl.end_ts < ?
671+
`+tsFilter+`
637672
GROUP BY tl.task_id
638673
ORDER BY t.updated_at ASC
639674
LIMIT ?;

0 commit comments

Comments
 (0)