Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ Accepts the same period options as `list`.
| `--to` | `-t` | no | End date (default: today) |
| `--period` | `-p` | no | Specific month in `YYYY/MM` format |
| `--work-item` | `-w` | no | Filter by Work Item ID |
| `--output` | `-o` | no | Output format: `json` or `csv`. Defaults to a table |
| `--today` | | no | Today |
| `--yesterday` | | no | Yesterday |
| `--week` | | no | Current week (Mon–Sun) |
Expand All @@ -222,6 +223,9 @@ Accepts the same period options as `list`.
# Daily totals for the current week
timetracker summary --week

# Export the monthly breakdown as CSV
timetracker summary --month --output csv > breakdown.csv

# Current month
timetracker summary --month

Expand Down
3 changes: 3 additions & 0 deletions Timetracker.Console/Options/SummaryOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,7 @@ public class SummaryOptions : IPeriodOptions

[Option('w', "work-item", Required = false, HelpText = "Filter entries by Work Item ID.")]
public int? WorkItemId { get; set; }

[Option('o', "output", Required = false, HelpText = "Output format: 'json' or 'csv'. Defaults to a table.")]
public string Output { get; set; }
}
50 changes: 41 additions & 9 deletions Timetracker.Console/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -382,21 +382,53 @@ static async Task<int> SummaryAction(SummaryOptions opts, CancellationToken canc

var totalHours = Math.Round(workLogs.Sum(x => x.Length) / 3600m, 2);

var days = workLogs
.GroupBy(x => x.TimeStamp.Date)
.OrderBy(g => g.Key)
.Select(g => new
{
Date = g.Key,
Hours = Math.Round(g.Sum(x => x.Length) / 3600m, 2),
Count = g.Count()
})
.ToList();

if (!string.IsNullOrEmpty(opts.Output))
{
switch (opts.Output.ToLowerInvariant())
{
case "json":
var payload = days.Select(d => new
{
date = d.Date.ToString("yyyy/MM/dd"),
weekday = d.Date.DayOfWeek.ToString(),
hours = d.Hours,
entries = d.Count
});
Console.WriteLine(JsonConvert.SerializeObject(payload, Formatting.Indented));
return 0;
case "csv":
Console.WriteLine("date,weekday,hours,entries");
foreach (var d in days)
Console.WriteLine($"{d.Date:yyyy/MM/dd},{d.Date.DayOfWeek},{d.Hours},{d.Count}");
return 0;
default:
ConsoleHelper.WriteError($"Unknown output format '{opts.Output}'. Use 'json' or 'csv'.");
return 1;
}
}

TableHelper.WriteMuted($"Summary from {from:yyyy/MM/dd} to {to:yyyy/MM/dd}:");
Console.WriteLine();

var table = TableHelper.NewTable("DATE", "WEEKDAY", "HOURS", "ENTRIES");

foreach (var day in workLogs.GroupBy(x => x.TimeStamp.Date).OrderBy(g => g.Key))
{
var dayHours = Math.Round(day.Sum(x => x.Length) / 3600m, 2);
var count = day.Count();
foreach (var d in days)
table.AddRow(
$"{day.Key:yyyy/MM/dd}",
Markup.Escape(day.Key.DayOfWeek.ToString()),
$"{dayHours}h",
count.ToString());
}
$"{d.Date:yyyy/MM/dd}",
Markup.Escape(d.Date.DayOfWeek.ToString()),
$"{d.Hours}h",
d.Count.ToString());

AnsiConsole.Write(table);

Expand Down
Loading